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
1=== modified file 'Makefile'
2--- Makefile 2015-10-06 15:06:44 +0000
3+++ Makefile 2016-02-18 14:28:13 +0000
4@@ -13,6 +13,7 @@
5
6 functional_test:
7 @echo Starting Amulet tests...
8+ @tests/setup/00-setup
9 @juju test -v -p AMULET_HTTP_PROXY,AMULET_OS_VIP --timeout 2700
10
11 bin/charm_helpers_sync.py:
12
13=== added symlink 'actions/openstack-upgrade'
14=== target is u'openstack_upgrade.py'
15=== removed symlink 'actions/openstack-upgrade'
16=== target was u'openstack_upgrade.py'
17=== added file 'actions/openstack_upgrade.py'
18--- actions/openstack_upgrade.py 1970-01-01 00:00:00 +0000
19+++ actions/openstack_upgrade.py 2016-02-18 14:28:13 +0000
20@@ -0,0 +1,34 @@
21+#!/usr/bin/python
22+import sys
23+
24+sys.path.append('hooks/')
25+
26+from charmhelpers.contrib.openstack.utils import (
27+ do_action_openstack_upgrade,
28+)
29+
30+from horizon_utils import (
31+ do_openstack_upgrade,
32+)
33+
34+from horizon_hooks import (
35+ config_changed,
36+ CONFIGS,
37+)
38+
39+
40+def openstack_upgrade():
41+ """Upgrade packages to config-set Openstack version.
42+
43+ If the charm was installed from source we cannot upgrade it.
44+ For backwards compatibility a config flag must be set for this
45+ code to run, otherwise a full service level upgrade will fire
46+ on config-changed."""
47+
48+ if do_action_openstack_upgrade('openstack-dashboard',
49+ do_openstack_upgrade,
50+ CONFIGS):
51+ config_changed()
52+
53+if __name__ == '__main__':
54+ openstack_upgrade()
55
56=== removed file 'actions/openstack_upgrade.py'
57--- actions/openstack_upgrade.py 2015-09-23 14:37:57 +0000
58+++ actions/openstack_upgrade.py 1970-01-01 00:00:00 +0000
59@@ -1,34 +0,0 @@
60-#!/usr/bin/python
61-import sys
62-
63-sys.path.append('hooks/')
64-
65-from charmhelpers.contrib.openstack.utils import (
66- do_action_openstack_upgrade,
67-)
68-
69-from horizon_utils import (
70- do_openstack_upgrade,
71-)
72-
73-from horizon_hooks import (
74- config_changed,
75- CONFIGS,
76-)
77-
78-
79-def openstack_upgrade():
80- """Upgrade packages to config-set Openstack version.
81-
82- If the charm was installed from source we cannot upgrade it.
83- For backwards compatibility a config flag must be set for this
84- code to run, otherwise a full service level upgrade will fire
85- on config-changed."""
86-
87- if do_action_openstack_upgrade('openstack-dashboard',
88- do_openstack_upgrade,
89- CONFIGS):
90- config_changed()
91-
92-if __name__ == '__main__':
93- openstack_upgrade()
94
95=== modified file 'charm-helpers-hooks.yaml'
96--- charm-helpers-hooks.yaml 2015-07-31 13:11:17 +0000
97+++ charm-helpers-hooks.yaml 2016-02-18 14:28:13 +0000
98@@ -1,4 +1,4 @@
99-branch: lp:charm-helpers
100+branch: lp:~openstack-charmers/charm-helpers/stable
101 destination: hooks/charmhelpers
102 include:
103 - core
104
105=== modified file 'charm-helpers-tests.yaml'
106--- charm-helpers-tests.yaml 2015-02-10 18:50:39 +0000
107+++ charm-helpers-tests.yaml 2016-02-18 14:28:13 +0000
108@@ -1,4 +1,4 @@
109-branch: lp:charm-helpers
110+branch: lp:~openstack-charmers/charm-helpers/stable
111 destination: tests/charmhelpers
112 include:
113 - contrib.amulet
114
115=== modified file 'config.yaml'
116--- config.yaml 2015-10-08 20:29:11 +0000
117+++ config.yaml 2016-02-18 14:28:13 +0000
118@@ -193,6 +193,30 @@
119 wait for you to execute the openstack-upgrade action for this charm on
120 each unit. If False it will revert to existing behavior of upgrading
121 all units on config change.
122+ haproxy-server-timeout:
123+ type: int
124+ default:
125+ description: |
126+ Server timeout configuration in ms for haproxy, used in HA
127+ configurations. If not provided, default value of 30000ms is used.
128+ haproxy-client-timeout:
129+ type: int
130+ default:
131+ description: |
132+ Client timeout configuration in ms for haproxy, used in HA
133+ configurations. If not provided, default value of 30000ms is used.
134+ haproxy-queue-timeout:
135+ type: int
136+ default:
137+ description: |
138+ Queue timeout configuration in ms for haproxy, used in HA
139+ configurations. If not provided, default value of 5000ms is used.
140+ haproxy-connect-timeout:
141+ type: int
142+ default:
143+ description: |
144+ Connect timeout configuration in ms for haproxy, used in HA
145+ configurations. If not provided, default value of 5000ms is used.
146 apache_http_addendum:
147 type: string
148 default: ''
149
150=== added directory 'hooks/charmhelpers/cli'
151=== removed directory 'hooks/charmhelpers/cli'
152=== added file 'hooks/charmhelpers/cli/__init__.py'
153--- hooks/charmhelpers/cli/__init__.py 1970-01-01 00:00:00 +0000
154+++ hooks/charmhelpers/cli/__init__.py 2016-02-18 14:28:13 +0000
155@@ -0,0 +1,191 @@
156+# Copyright 2014-2015 Canonical Limited.
157+#
158+# This file is part of charm-helpers.
159+#
160+# charm-helpers is free software: you can redistribute it and/or modify
161+# it under the terms of the GNU Lesser General Public License version 3 as
162+# published by the Free Software Foundation.
163+#
164+# charm-helpers is distributed in the hope that it will be useful,
165+# but WITHOUT ANY WARRANTY; without even the implied warranty of
166+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
167+# GNU Lesser General Public License for more details.
168+#
169+# You should have received a copy of the GNU Lesser General Public License
170+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
171+
172+import inspect
173+import argparse
174+import sys
175+
176+from six.moves import zip
177+
178+import charmhelpers.core.unitdata
179+
180+
181+class OutputFormatter(object):
182+ def __init__(self, outfile=sys.stdout):
183+ self.formats = (
184+ "raw",
185+ "json",
186+ "py",
187+ "yaml",
188+ "csv",
189+ "tab",
190+ )
191+ self.outfile = outfile
192+
193+ def add_arguments(self, argument_parser):
194+ formatgroup = argument_parser.add_mutually_exclusive_group()
195+ choices = self.supported_formats
196+ formatgroup.add_argument("--format", metavar='FMT',
197+ help="Select output format for returned data, "
198+ "where FMT is one of: {}".format(choices),
199+ choices=choices, default='raw')
200+ for fmt in self.formats:
201+ fmtfunc = getattr(self, fmt)
202+ formatgroup.add_argument("-{}".format(fmt[0]),
203+ "--{}".format(fmt), action='store_const',
204+ const=fmt, dest='format',
205+ help=fmtfunc.__doc__)
206+
207+ @property
208+ def supported_formats(self):
209+ return self.formats
210+
211+ def raw(self, output):
212+ """Output data as raw string (default)"""
213+ if isinstance(output, (list, tuple)):
214+ output = '\n'.join(map(str, output))
215+ self.outfile.write(str(output))
216+
217+ def py(self, output):
218+ """Output data as a nicely-formatted python data structure"""
219+ import pprint
220+ pprint.pprint(output, stream=self.outfile)
221+
222+ def json(self, output):
223+ """Output data in JSON format"""
224+ import json
225+ json.dump(output, self.outfile)
226+
227+ def yaml(self, output):
228+ """Output data in YAML format"""
229+ import yaml
230+ yaml.safe_dump(output, self.outfile)
231+
232+ def csv(self, output):
233+ """Output data as excel-compatible CSV"""
234+ import csv
235+ csvwriter = csv.writer(self.outfile)
236+ csvwriter.writerows(output)
237+
238+ def tab(self, output):
239+ """Output data in excel-compatible tab-delimited format"""
240+ import csv
241+ csvwriter = csv.writer(self.outfile, dialect=csv.excel_tab)
242+ csvwriter.writerows(output)
243+
244+ def format_output(self, output, fmt='raw'):
245+ fmtfunc = getattr(self, fmt)
246+ fmtfunc(output)
247+
248+
249+class CommandLine(object):
250+ argument_parser = None
251+ subparsers = None
252+ formatter = None
253+ exit_code = 0
254+
255+ def __init__(self):
256+ if not self.argument_parser:
257+ self.argument_parser = argparse.ArgumentParser(description='Perform common charm tasks')
258+ if not self.formatter:
259+ self.formatter = OutputFormatter()
260+ self.formatter.add_arguments(self.argument_parser)
261+ if not self.subparsers:
262+ self.subparsers = self.argument_parser.add_subparsers(help='Commands')
263+
264+ def subcommand(self, command_name=None):
265+ """
266+ Decorate a function as a subcommand. Use its arguments as the
267+ command-line arguments"""
268+ def wrapper(decorated):
269+ cmd_name = command_name or decorated.__name__
270+ subparser = self.subparsers.add_parser(cmd_name,
271+ description=decorated.__doc__)
272+ for args, kwargs in describe_arguments(decorated):
273+ subparser.add_argument(*args, **kwargs)
274+ subparser.set_defaults(func=decorated)
275+ return decorated
276+ return wrapper
277+
278+ def test_command(self, decorated):
279+ """
280+ Subcommand is a boolean test function, so bool return values should be
281+ converted to a 0/1 exit code.
282+ """
283+ decorated._cli_test_command = True
284+ return decorated
285+
286+ def no_output(self, decorated):
287+ """
288+ Subcommand is not expected to return a value, so don't print a spurious None.
289+ """
290+ decorated._cli_no_output = True
291+ return decorated
292+
293+ def subcommand_builder(self, command_name, description=None):
294+ """
295+ Decorate a function that builds a subcommand. Builders should accept a
296+ single argument (the subparser instance) and return the function to be
297+ run as the command."""
298+ def wrapper(decorated):
299+ subparser = self.subparsers.add_parser(command_name)
300+ func = decorated(subparser)
301+ subparser.set_defaults(func=func)
302+ subparser.description = description or func.__doc__
303+ return wrapper
304+
305+ def run(self):
306+ "Run cli, processing arguments and executing subcommands."
307+ arguments = self.argument_parser.parse_args()
308+ argspec = inspect.getargspec(arguments.func)
309+ vargs = []
310+ for arg in argspec.args:
311+ vargs.append(getattr(arguments, arg))
312+ if argspec.varargs:
313+ vargs.extend(getattr(arguments, argspec.varargs))
314+ output = arguments.func(*vargs)
315+ if getattr(arguments.func, '_cli_test_command', False):
316+ self.exit_code = 0 if output else 1
317+ output = ''
318+ if getattr(arguments.func, '_cli_no_output', False):
319+ output = ''
320+ self.formatter.format_output(output, arguments.format)
321+ if charmhelpers.core.unitdata._KV:
322+ charmhelpers.core.unitdata._KV.flush()
323+
324+
325+cmdline = CommandLine()
326+
327+
328+def describe_arguments(func):
329+ """
330+ Analyze a function's signature and return a data structure suitable for
331+ passing in as arguments to an argparse parser's add_argument() method."""
332+
333+ argspec = inspect.getargspec(func)
334+ # we should probably raise an exception somewhere if func includes **kwargs
335+ if argspec.defaults:
336+ positional_args = argspec.args[:-len(argspec.defaults)]
337+ keyword_names = argspec.args[-len(argspec.defaults):]
338+ for arg, default in zip(keyword_names, argspec.defaults):
339+ yield ('--{}'.format(arg),), {'default': default}
340+ else:
341+ positional_args = argspec.args
342+
343+ for arg in positional_args:
344+ yield (arg,), {}
345+ if argspec.varargs:
346+ yield (argspec.varargs,), {'nargs': '*'}
347
348=== removed file 'hooks/charmhelpers/cli/__init__.py'
349--- hooks/charmhelpers/cli/__init__.py 2015-08-18 17:34:36 +0000
350+++ hooks/charmhelpers/cli/__init__.py 1970-01-01 00:00:00 +0000
351@@ -1,191 +0,0 @@
352-# Copyright 2014-2015 Canonical Limited.
353-#
354-# This file is part of charm-helpers.
355-#
356-# charm-helpers is free software: you can redistribute it and/or modify
357-# it under the terms of the GNU Lesser General Public License version 3 as
358-# published by the Free Software Foundation.
359-#
360-# charm-helpers is distributed in the hope that it will be useful,
361-# but WITHOUT ANY WARRANTY; without even the implied warranty of
362-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
363-# GNU Lesser General Public License for more details.
364-#
365-# You should have received a copy of the GNU Lesser General Public License
366-# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
367-
368-import inspect
369-import argparse
370-import sys
371-
372-from six.moves import zip
373-
374-from charmhelpers.core import unitdata
375-
376-
377-class OutputFormatter(object):
378- def __init__(self, outfile=sys.stdout):
379- self.formats = (
380- "raw",
381- "json",
382- "py",
383- "yaml",
384- "csv",
385- "tab",
386- )
387- self.outfile = outfile
388-
389- def add_arguments(self, argument_parser):
390- formatgroup = argument_parser.add_mutually_exclusive_group()
391- choices = self.supported_formats
392- formatgroup.add_argument("--format", metavar='FMT',
393- help="Select output format for returned data, "
394- "where FMT is one of: {}".format(choices),
395- choices=choices, default='raw')
396- for fmt in self.formats:
397- fmtfunc = getattr(self, fmt)
398- formatgroup.add_argument("-{}".format(fmt[0]),
399- "--{}".format(fmt), action='store_const',
400- const=fmt, dest='format',
401- help=fmtfunc.__doc__)
402-
403- @property
404- def supported_formats(self):
405- return self.formats
406-
407- def raw(self, output):
408- """Output data as raw string (default)"""
409- if isinstance(output, (list, tuple)):
410- output = '\n'.join(map(str, output))
411- self.outfile.write(str(output))
412-
413- def py(self, output):
414- """Output data as a nicely-formatted python data structure"""
415- import pprint
416- pprint.pprint(output, stream=self.outfile)
417-
418- def json(self, output):
419- """Output data in JSON format"""
420- import json
421- json.dump(output, self.outfile)
422-
423- def yaml(self, output):
424- """Output data in YAML format"""
425- import yaml
426- yaml.safe_dump(output, self.outfile)
427-
428- def csv(self, output):
429- """Output data as excel-compatible CSV"""
430- import csv
431- csvwriter = csv.writer(self.outfile)
432- csvwriter.writerows(output)
433-
434- def tab(self, output):
435- """Output data in excel-compatible tab-delimited format"""
436- import csv
437- csvwriter = csv.writer(self.outfile, dialect=csv.excel_tab)
438- csvwriter.writerows(output)
439-
440- def format_output(self, output, fmt='raw'):
441- fmtfunc = getattr(self, fmt)
442- fmtfunc(output)
443-
444-
445-class CommandLine(object):
446- argument_parser = None
447- subparsers = None
448- formatter = None
449- exit_code = 0
450-
451- def __init__(self):
452- if not self.argument_parser:
453- self.argument_parser = argparse.ArgumentParser(description='Perform common charm tasks')
454- if not self.formatter:
455- self.formatter = OutputFormatter()
456- self.formatter.add_arguments(self.argument_parser)
457- if not self.subparsers:
458- self.subparsers = self.argument_parser.add_subparsers(help='Commands')
459-
460- def subcommand(self, command_name=None):
461- """
462- Decorate a function as a subcommand. Use its arguments as the
463- command-line arguments"""
464- def wrapper(decorated):
465- cmd_name = command_name or decorated.__name__
466- subparser = self.subparsers.add_parser(cmd_name,
467- description=decorated.__doc__)
468- for args, kwargs in describe_arguments(decorated):
469- subparser.add_argument(*args, **kwargs)
470- subparser.set_defaults(func=decorated)
471- return decorated
472- return wrapper
473-
474- def test_command(self, decorated):
475- """
476- Subcommand is a boolean test function, so bool return values should be
477- converted to a 0/1 exit code.
478- """
479- decorated._cli_test_command = True
480- return decorated
481-
482- def no_output(self, decorated):
483- """
484- Subcommand is not expected to return a value, so don't print a spurious None.
485- """
486- decorated._cli_no_output = True
487- return decorated
488-
489- def subcommand_builder(self, command_name, description=None):
490- """
491- Decorate a function that builds a subcommand. Builders should accept a
492- single argument (the subparser instance) and return the function to be
493- run as the command."""
494- def wrapper(decorated):
495- subparser = self.subparsers.add_parser(command_name)
496- func = decorated(subparser)
497- subparser.set_defaults(func=func)
498- subparser.description = description or func.__doc__
499- return wrapper
500-
501- def run(self):
502- "Run cli, processing arguments and executing subcommands."
503- arguments = self.argument_parser.parse_args()
504- argspec = inspect.getargspec(arguments.func)
505- vargs = []
506- for arg in argspec.args:
507- vargs.append(getattr(arguments, arg))
508- if argspec.varargs:
509- vargs.extend(getattr(arguments, argspec.varargs))
510- output = arguments.func(*vargs)
511- if getattr(arguments.func, '_cli_test_command', False):
512- self.exit_code = 0 if output else 1
513- output = ''
514- if getattr(arguments.func, '_cli_no_output', False):
515- output = ''
516- self.formatter.format_output(output, arguments.format)
517- if unitdata._KV:
518- unitdata._KV.flush()
519-
520-
521-cmdline = CommandLine()
522-
523-
524-def describe_arguments(func):
525- """
526- Analyze a function's signature and return a data structure suitable for
527- passing in as arguments to an argparse parser's add_argument() method."""
528-
529- argspec = inspect.getargspec(func)
530- # we should probably raise an exception somewhere if func includes **kwargs
531- if argspec.defaults:
532- positional_args = argspec.args[:-len(argspec.defaults)]
533- keyword_names = argspec.args[-len(argspec.defaults):]
534- for arg, default in zip(keyword_names, argspec.defaults):
535- yield ('--{}'.format(arg),), {'default': default}
536- else:
537- positional_args = argspec.args
538-
539- for arg in positional_args:
540- yield (arg,), {}
541- if argspec.varargs:
542- yield (argspec.varargs,), {'nargs': '*'}
543
544=== added file 'hooks/charmhelpers/cli/benchmark.py'
545--- hooks/charmhelpers/cli/benchmark.py 1970-01-01 00:00:00 +0000
546+++ hooks/charmhelpers/cli/benchmark.py 2016-02-18 14:28:13 +0000
547@@ -0,0 +1,36 @@
548+# Copyright 2014-2015 Canonical Limited.
549+#
550+# This file is part of charm-helpers.
551+#
552+# charm-helpers is free software: you can redistribute it and/or modify
553+# it under the terms of the GNU Lesser General Public License version 3 as
554+# published by the Free Software Foundation.
555+#
556+# charm-helpers is distributed in the hope that it will be useful,
557+# but WITHOUT ANY WARRANTY; without even the implied warranty of
558+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
559+# GNU Lesser General Public License for more details.
560+#
561+# You should have received a copy of the GNU Lesser General Public License
562+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
563+
564+from . import cmdline
565+from charmhelpers.contrib.benchmark import Benchmark
566+
567+
568+@cmdline.subcommand(command_name='benchmark-start')
569+def start():
570+ Benchmark.start()
571+
572+
573+@cmdline.subcommand(command_name='benchmark-finish')
574+def finish():
575+ Benchmark.finish()
576+
577+
578+@cmdline.subcommand_builder('benchmark-composite', description="Set the benchmark composite score")
579+def service(subparser):
580+ subparser.add_argument("value", help="The composite score.")
581+ subparser.add_argument("units", help="The units the composite score represents, i.e., 'reads/sec'.")
582+ subparser.add_argument("direction", help="'asc' if a lower score is better, 'desc' if a higher score is better.")
583+ return Benchmark.set_composite_score
584
585=== removed file 'hooks/charmhelpers/cli/benchmark.py'
586--- hooks/charmhelpers/cli/benchmark.py 2015-07-31 13:11:17 +0000
587+++ hooks/charmhelpers/cli/benchmark.py 1970-01-01 00:00:00 +0000
588@@ -1,36 +0,0 @@
589-# Copyright 2014-2015 Canonical Limited.
590-#
591-# This file is part of charm-helpers.
592-#
593-# charm-helpers is free software: you can redistribute it and/or modify
594-# it under the terms of the GNU Lesser General Public License version 3 as
595-# published by the Free Software Foundation.
596-#
597-# charm-helpers is distributed in the hope that it will be useful,
598-# but WITHOUT ANY WARRANTY; without even the implied warranty of
599-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
600-# GNU Lesser General Public License for more details.
601-#
602-# You should have received a copy of the GNU Lesser General Public License
603-# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
604-
605-from . import cmdline
606-from charmhelpers.contrib.benchmark import Benchmark
607-
608-
609-@cmdline.subcommand(command_name='benchmark-start')
610-def start():
611- Benchmark.start()
612-
613-
614-@cmdline.subcommand(command_name='benchmark-finish')
615-def finish():
616- Benchmark.finish()
617-
618-
619-@cmdline.subcommand_builder('benchmark-composite', description="Set the benchmark composite score")
620-def service(subparser):
621- subparser.add_argument("value", help="The composite score.")
622- subparser.add_argument("units", help="The units the composite score represents, i.e., 'reads/sec'.")
623- subparser.add_argument("direction", help="'asc' if a lower score is better, 'desc' if a higher score is better.")
624- return Benchmark.set_composite_score
625
626=== added file 'hooks/charmhelpers/cli/commands.py'
627--- hooks/charmhelpers/cli/commands.py 1970-01-01 00:00:00 +0000
628+++ hooks/charmhelpers/cli/commands.py 2016-02-18 14:28:13 +0000
629@@ -0,0 +1,32 @@
630+# Copyright 2014-2015 Canonical Limited.
631+#
632+# This file is part of charm-helpers.
633+#
634+# charm-helpers is free software: you can redistribute it and/or modify
635+# it under the terms of the GNU Lesser General Public License version 3 as
636+# published by the Free Software Foundation.
637+#
638+# charm-helpers is distributed in the hope that it will be useful,
639+# but WITHOUT ANY WARRANTY; without even the implied warranty of
640+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
641+# GNU Lesser General Public License for more details.
642+#
643+# You should have received a copy of the GNU Lesser General Public License
644+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
645+
646+"""
647+This module loads sub-modules into the python runtime so they can be
648+discovered via the inspect module. In order to prevent flake8 from (rightfully)
649+telling us these are unused modules, throw a ' # noqa' at the end of each import
650+so that the warning is suppressed.
651+"""
652+
653+from . import CommandLine # noqa
654+
655+"""
656+Import the sub-modules which have decorated subcommands to register with chlp.
657+"""
658+from . import host # noqa
659+from . import benchmark # noqa
660+from . import unitdata # noqa
661+from . import hookenv # noqa
662
663=== removed file 'hooks/charmhelpers/cli/commands.py'
664--- hooks/charmhelpers/cli/commands.py 2015-08-18 17:34:36 +0000
665+++ hooks/charmhelpers/cli/commands.py 1970-01-01 00:00:00 +0000
666@@ -1,32 +0,0 @@
667-# Copyright 2014-2015 Canonical Limited.
668-#
669-# This file is part of charm-helpers.
670-#
671-# charm-helpers is free software: you can redistribute it and/or modify
672-# it under the terms of the GNU Lesser General Public License version 3 as
673-# published by the Free Software Foundation.
674-#
675-# charm-helpers is distributed in the hope that it will be useful,
676-# but WITHOUT ANY WARRANTY; without even the implied warranty of
677-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
678-# GNU Lesser General Public License for more details.
679-#
680-# You should have received a copy of the GNU Lesser General Public License
681-# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
682-
683-"""
684-This module loads sub-modules into the python runtime so they can be
685-discovered via the inspect module. In order to prevent flake8 from (rightfully)
686-telling us these are unused modules, throw a ' # noqa' at the end of each import
687-so that the warning is suppressed.
688-"""
689-
690-from . import CommandLine # noqa
691-
692-"""
693-Import the sub-modules which have decorated subcommands to register with chlp.
694-"""
695-from . import host # noqa
696-from . import benchmark # noqa
697-from . import unitdata # noqa
698-from . import hookenv # noqa
699
700=== added file 'hooks/charmhelpers/cli/hookenv.py'
701--- hooks/charmhelpers/cli/hookenv.py 1970-01-01 00:00:00 +0000
702+++ hooks/charmhelpers/cli/hookenv.py 2016-02-18 14:28:13 +0000
703@@ -0,0 +1,23 @@
704+# Copyright 2014-2015 Canonical Limited.
705+#
706+# This file is part of charm-helpers.
707+#
708+# charm-helpers is free software: you can redistribute it and/or modify
709+# it under the terms of the GNU Lesser General Public License version 3 as
710+# published by the Free Software Foundation.
711+#
712+# charm-helpers is distributed in the hope that it will be useful,
713+# but WITHOUT ANY WARRANTY; without even the implied warranty of
714+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
715+# GNU Lesser General Public License for more details.
716+#
717+# You should have received a copy of the GNU Lesser General Public License
718+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
719+
720+from . import cmdline
721+from charmhelpers.core import hookenv
722+
723+
724+cmdline.subcommand('relation-id')(hookenv.relation_id._wrapped)
725+cmdline.subcommand('service-name')(hookenv.service_name)
726+cmdline.subcommand('remote-service-name')(hookenv.remote_service_name._wrapped)
727
728=== removed file 'hooks/charmhelpers/cli/hookenv.py'
729--- hooks/charmhelpers/cli/hookenv.py 2015-08-18 17:34:36 +0000
730+++ hooks/charmhelpers/cli/hookenv.py 1970-01-01 00:00:00 +0000
731@@ -1,23 +0,0 @@
732-# Copyright 2014-2015 Canonical Limited.
733-#
734-# This file is part of charm-helpers.
735-#
736-# charm-helpers is free software: you can redistribute it and/or modify
737-# it under the terms of the GNU Lesser General Public License version 3 as
738-# published by the Free Software Foundation.
739-#
740-# charm-helpers is distributed in the hope that it will be useful,
741-# but WITHOUT ANY WARRANTY; without even the implied warranty of
742-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
743-# GNU Lesser General Public License for more details.
744-#
745-# You should have received a copy of the GNU Lesser General Public License
746-# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
747-
748-from . import cmdline
749-from charmhelpers.core import hookenv
750-
751-
752-cmdline.subcommand('relation-id')(hookenv.relation_id._wrapped)
753-cmdline.subcommand('service-name')(hookenv.service_name)
754-cmdline.subcommand('remote-service-name')(hookenv.remote_service_name._wrapped)
755
756=== added file 'hooks/charmhelpers/cli/host.py'
757--- hooks/charmhelpers/cli/host.py 1970-01-01 00:00:00 +0000
758+++ hooks/charmhelpers/cli/host.py 2016-02-18 14:28:13 +0000
759@@ -0,0 +1,31 @@
760+# Copyright 2014-2015 Canonical Limited.
761+#
762+# This file is part of charm-helpers.
763+#
764+# charm-helpers is free software: you can redistribute it and/or modify
765+# it under the terms of the GNU Lesser General Public License version 3 as
766+# published by the Free Software Foundation.
767+#
768+# charm-helpers is distributed in the hope that it will be useful,
769+# but WITHOUT ANY WARRANTY; without even the implied warranty of
770+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
771+# GNU Lesser General Public License for more details.
772+#
773+# You should have received a copy of the GNU Lesser General Public License
774+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
775+
776+from . import cmdline
777+from charmhelpers.core import host
778+
779+
780+@cmdline.subcommand()
781+def mounts():
782+ "List mounts"
783+ return host.mounts()
784+
785+
786+@cmdline.subcommand_builder('service', description="Control system services")
787+def service(subparser):
788+ subparser.add_argument("action", help="The action to perform (start, stop, etc...)")
789+ subparser.add_argument("service_name", help="Name of the service to control")
790+ return host.service
791
792=== removed file 'hooks/charmhelpers/cli/host.py'
793--- hooks/charmhelpers/cli/host.py 2015-07-31 13:11:17 +0000
794+++ hooks/charmhelpers/cli/host.py 1970-01-01 00:00:00 +0000
795@@ -1,31 +0,0 @@
796-# Copyright 2014-2015 Canonical Limited.
797-#
798-# This file is part of charm-helpers.
799-#
800-# charm-helpers is free software: you can redistribute it and/or modify
801-# it under the terms of the GNU Lesser General Public License version 3 as
802-# published by the Free Software Foundation.
803-#
804-# charm-helpers is distributed in the hope that it will be useful,
805-# but WITHOUT ANY WARRANTY; without even the implied warranty of
806-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
807-# GNU Lesser General Public License for more details.
808-#
809-# You should have received a copy of the GNU Lesser General Public License
810-# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
811-
812-from . import cmdline
813-from charmhelpers.core import host
814-
815-
816-@cmdline.subcommand()
817-def mounts():
818- "List mounts"
819- return host.mounts()
820-
821-
822-@cmdline.subcommand_builder('service', description="Control system services")
823-def service(subparser):
824- subparser.add_argument("action", help="The action to perform (start, stop, etc...)")
825- subparser.add_argument("service_name", help="Name of the service to control")
826- return host.service
827
828=== added file 'hooks/charmhelpers/cli/unitdata.py'
829--- hooks/charmhelpers/cli/unitdata.py 1970-01-01 00:00:00 +0000
830+++ hooks/charmhelpers/cli/unitdata.py 2016-02-18 14:28:13 +0000
831@@ -0,0 +1,39 @@
832+# Copyright 2014-2015 Canonical Limited.
833+#
834+# This file is part of charm-helpers.
835+#
836+# charm-helpers is free software: you can redistribute it and/or modify
837+# it under the terms of the GNU Lesser General Public License version 3 as
838+# published by the Free Software Foundation.
839+#
840+# charm-helpers is distributed in the hope that it will be useful,
841+# but WITHOUT ANY WARRANTY; without even the implied warranty of
842+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
843+# GNU Lesser General Public License for more details.
844+#
845+# You should have received a copy of the GNU Lesser General Public License
846+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
847+
848+from . import cmdline
849+from charmhelpers.core import unitdata
850+
851+
852+@cmdline.subcommand_builder('unitdata', description="Store and retrieve data")
853+def unitdata_cmd(subparser):
854+ nested = subparser.add_subparsers()
855+ get_cmd = nested.add_parser('get', help='Retrieve data')
856+ get_cmd.add_argument('key', help='Key to retrieve the value of')
857+ get_cmd.set_defaults(action='get', value=None)
858+ set_cmd = nested.add_parser('set', help='Store data')
859+ set_cmd.add_argument('key', help='Key to set')
860+ set_cmd.add_argument('value', help='Value to store')
861+ set_cmd.set_defaults(action='set')
862+
863+ def _unitdata_cmd(action, key, value):
864+ if action == 'get':
865+ return unitdata.kv().get(key)
866+ elif action == 'set':
867+ unitdata.kv().set(key, value)
868+ unitdata.kv().flush()
869+ return ''
870+ return _unitdata_cmd
871
872=== removed file 'hooks/charmhelpers/cli/unitdata.py'
873--- hooks/charmhelpers/cli/unitdata.py 2015-07-31 13:11:17 +0000
874+++ hooks/charmhelpers/cli/unitdata.py 1970-01-01 00:00:00 +0000
875@@ -1,39 +0,0 @@
876-# Copyright 2014-2015 Canonical Limited.
877-#
878-# This file is part of charm-helpers.
879-#
880-# charm-helpers is free software: you can redistribute it and/or modify
881-# it under the terms of the GNU Lesser General Public License version 3 as
882-# published by the Free Software Foundation.
883-#
884-# charm-helpers is distributed in the hope that it will be useful,
885-# but WITHOUT ANY WARRANTY; without even the implied warranty of
886-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
887-# GNU Lesser General Public License for more details.
888-#
889-# You should have received a copy of the GNU Lesser General Public License
890-# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
891-
892-from . import cmdline
893-from charmhelpers.core import unitdata
894-
895-
896-@cmdline.subcommand_builder('unitdata', description="Store and retrieve data")
897-def unitdata_cmd(subparser):
898- nested = subparser.add_subparsers()
899- get_cmd = nested.add_parser('get', help='Retrieve data')
900- get_cmd.add_argument('key', help='Key to retrieve the value of')
901- get_cmd.set_defaults(action='get', value=None)
902- set_cmd = nested.add_parser('set', help='Store data')
903- set_cmd.add_argument('key', help='Key to set')
904- set_cmd.add_argument('value', help='Value to store')
905- set_cmd.set_defaults(action='set')
906-
907- def _unitdata_cmd(action, key, value):
908- if action == 'get':
909- return unitdata.kv().get(key)
910- elif action == 'set':
911- unitdata.kv().set(key, value)
912- unitdata.kv().flush()
913- return ''
914- return _unitdata_cmd
915
916=== modified file 'hooks/charmhelpers/contrib/charmsupport/nrpe.py'
917--- hooks/charmhelpers/contrib/charmsupport/nrpe.py 2015-04-19 09:02:03 +0000
918+++ hooks/charmhelpers/contrib/charmsupport/nrpe.py 2016-02-18 14:28:13 +0000
919@@ -148,6 +148,13 @@
920 self.description = description
921 self.check_cmd = self._locate_cmd(check_cmd)
922
923+ def _get_check_filename(self):
924+ return os.path.join(NRPE.nrpe_confdir, '{}.cfg'.format(self.command))
925+
926+ def _get_service_filename(self, hostname):
927+ return os.path.join(NRPE.nagios_exportdir,
928+ 'service__{}_{}.cfg'.format(hostname, self.command))
929+
930 def _locate_cmd(self, check_cmd):
931 search_path = (
932 '/usr/lib/nagios/plugins',
933@@ -163,9 +170,21 @@
934 log('Check command not found: {}'.format(parts[0]))
935 return ''
936
937+ def _remove_service_files(self):
938+ if not os.path.exists(NRPE.nagios_exportdir):
939+ return
940+ for f in os.listdir(NRPE.nagios_exportdir):
941+ if f.endswith('_{}.cfg'.format(self.command)):
942+ os.remove(os.path.join(NRPE.nagios_exportdir, f))
943+
944+ def remove(self, hostname):
945+ nrpe_check_file = self._get_check_filename()
946+ if os.path.exists(nrpe_check_file):
947+ os.remove(nrpe_check_file)
948+ self._remove_service_files()
949+
950 def write(self, nagios_context, hostname, nagios_servicegroups):
951- nrpe_check_file = '/etc/nagios/nrpe.d/{}.cfg'.format(
952- self.command)
953+ nrpe_check_file = self._get_check_filename()
954 with open(nrpe_check_file, 'w') as nrpe_check_config:
955 nrpe_check_config.write("# check {}\n".format(self.shortname))
956 nrpe_check_config.write("command[{}]={}\n".format(
957@@ -180,9 +199,7 @@
958
959 def write_service_config(self, nagios_context, hostname,
960 nagios_servicegroups):
961- for f in os.listdir(NRPE.nagios_exportdir):
962- if re.search('.*{}.cfg'.format(self.command), f):
963- os.remove(os.path.join(NRPE.nagios_exportdir, f))
964+ self._remove_service_files()
965
966 templ_vars = {
967 'nagios_hostname': hostname,
968@@ -192,8 +209,7 @@
969 'command': self.command,
970 }
971 nrpe_service_text = Check.service_template.format(**templ_vars)
972- nrpe_service_file = '{}/service__{}_{}.cfg'.format(
973- NRPE.nagios_exportdir, hostname, self.command)
974+ nrpe_service_file = self._get_service_filename(hostname)
975 with open(nrpe_service_file, 'w') as nrpe_service_config:
976 nrpe_service_config.write(str(nrpe_service_text))
977
978@@ -218,12 +234,32 @@
979 if hostname:
980 self.hostname = hostname
981 else:
982- self.hostname = "{}-{}".format(self.nagios_context, self.unit_name)
983+ nagios_hostname = get_nagios_hostname()
984+ if nagios_hostname:
985+ self.hostname = nagios_hostname
986+ else:
987+ self.hostname = "{}-{}".format(self.nagios_context, self.unit_name)
988 self.checks = []
989
990 def add_check(self, *args, **kwargs):
991 self.checks.append(Check(*args, **kwargs))
992
993+ def remove_check(self, *args, **kwargs):
994+ if kwargs.get('shortname') is None:
995+ raise ValueError('shortname of check must be specified')
996+
997+ # Use sensible defaults if they're not specified - these are not
998+ # actually used during removal, but they're required for constructing
999+ # the Check object; check_disk is chosen because it's part of the
1000+ # nagios-plugins-basic package.
1001+ if kwargs.get('check_cmd') is None:
1002+ kwargs['check_cmd'] = 'check_disk'
1003+ if kwargs.get('description') is None:
1004+ kwargs['description'] = ''
1005+
1006+ check = Check(*args, **kwargs)
1007+ check.remove(self.hostname)
1008+
1009 def write(self):
1010 try:
1011 nagios_uid = pwd.getpwnam('nagios').pw_uid
1012@@ -260,7 +296,7 @@
1013 :param str relation_name: Name of relation nrpe sub joined to
1014 """
1015 for rel in relations_of_type(relation_name):
1016- if 'nagios_hostname' in rel:
1017+ if 'nagios_host_context' in rel:
1018 return rel['nagios_host_context']
1019
1020
1021@@ -301,11 +337,13 @@
1022 upstart_init = '/etc/init/%s.conf' % svc
1023 sysv_init = '/etc/init.d/%s' % svc
1024 if os.path.exists(upstart_init):
1025- nrpe.add_check(
1026- shortname=svc,
1027- description='process check {%s}' % unit_name,
1028- check_cmd='check_upstart_job %s' % svc
1029- )
1030+ # Don't add a check for these services from neutron-gateway
1031+ if svc not in ['ext-port', 'os-charm-phy-nic-mtu']:
1032+ nrpe.add_check(
1033+ shortname=svc,
1034+ description='process check {%s}' % unit_name,
1035+ check_cmd='check_upstart_job %s' % svc
1036+ )
1037 elif os.path.exists(sysv_init):
1038 cronpath = '/etc/cron.d/nagios-service-check-%s' % svc
1039 cron_file = ('*/5 * * * * root '
1040
1041=== modified file 'hooks/charmhelpers/contrib/network/ip.py'
1042--- hooks/charmhelpers/contrib/network/ip.py 2015-09-03 09:42:35 +0000
1043+++ hooks/charmhelpers/contrib/network/ip.py 2016-02-18 14:28:13 +0000
1044@@ -23,7 +23,7 @@
1045 from functools import partial
1046
1047 from charmhelpers.core.hookenv import unit_get
1048-from charmhelpers.fetch import apt_install
1049+from charmhelpers.fetch import apt_install, apt_update
1050 from charmhelpers.core.hookenv import (
1051 log,
1052 WARNING,
1053@@ -32,13 +32,15 @@
1054 try:
1055 import netifaces
1056 except ImportError:
1057- apt_install('python-netifaces')
1058+ apt_update(fatal=True)
1059+ apt_install('python-netifaces', fatal=True)
1060 import netifaces
1061
1062 try:
1063 import netaddr
1064 except ImportError:
1065- apt_install('python-netaddr')
1066+ apt_update(fatal=True)
1067+ apt_install('python-netaddr', fatal=True)
1068 import netaddr
1069
1070
1071@@ -51,7 +53,7 @@
1072
1073
1074 def no_ip_found_error_out(network):
1075- errmsg = ("No IP address found in network: %s" % network)
1076+ errmsg = ("No IP address found in network(s): %s" % network)
1077 raise ValueError(errmsg)
1078
1079
1080@@ -59,7 +61,7 @@
1081 """Get an IPv4 or IPv6 address within the network from the host.
1082
1083 :param network (str): CIDR presentation format. For example,
1084- '192.168.1.0/24'.
1085+ '192.168.1.0/24'. Supports multiple networks as a space-delimited list.
1086 :param fallback (str): If no address is found, return fallback.
1087 :param fatal (boolean): If no address is found, fallback is not
1088 set and fatal is True then exit(1).
1089@@ -73,24 +75,26 @@
1090 else:
1091 return None
1092
1093- _validate_cidr(network)
1094- network = netaddr.IPNetwork(network)
1095- for iface in netifaces.interfaces():
1096- addresses = netifaces.ifaddresses(iface)
1097- if network.version == 4 and netifaces.AF_INET in addresses:
1098- addr = addresses[netifaces.AF_INET][0]['addr']
1099- netmask = addresses[netifaces.AF_INET][0]['netmask']
1100- cidr = netaddr.IPNetwork("%s/%s" % (addr, netmask))
1101- if cidr in network:
1102- return str(cidr.ip)
1103+ networks = network.split() or [network]
1104+ for network in networks:
1105+ _validate_cidr(network)
1106+ network = netaddr.IPNetwork(network)
1107+ for iface in netifaces.interfaces():
1108+ addresses = netifaces.ifaddresses(iface)
1109+ if network.version == 4 and netifaces.AF_INET in addresses:
1110+ addr = addresses[netifaces.AF_INET][0]['addr']
1111+ netmask = addresses[netifaces.AF_INET][0]['netmask']
1112+ cidr = netaddr.IPNetwork("%s/%s" % (addr, netmask))
1113+ if cidr in network:
1114+ return str(cidr.ip)
1115
1116- if network.version == 6 and netifaces.AF_INET6 in addresses:
1117- for addr in addresses[netifaces.AF_INET6]:
1118- if not addr['addr'].startswith('fe80'):
1119- cidr = netaddr.IPNetwork("%s/%s" % (addr['addr'],
1120- addr['netmask']))
1121- if cidr in network:
1122- return str(cidr.ip)
1123+ if network.version == 6 and netifaces.AF_INET6 in addresses:
1124+ for addr in addresses[netifaces.AF_INET6]:
1125+ if not addr['addr'].startswith('fe80'):
1126+ cidr = netaddr.IPNetwork("%s/%s" % (addr['addr'],
1127+ addr['netmask']))
1128+ if cidr in network:
1129+ return str(cidr.ip)
1130
1131 if fallback is not None:
1132 return fallback
1133
1134=== modified file 'hooks/charmhelpers/contrib/openstack/amulet/deployment.py'
1135--- hooks/charmhelpers/contrib/openstack/amulet/deployment.py 2015-08-18 17:34:36 +0000
1136+++ hooks/charmhelpers/contrib/openstack/amulet/deployment.py 2016-02-18 14:28:13 +0000
1137@@ -14,12 +14,18 @@
1138 # You should have received a copy of the GNU Lesser General Public License
1139 # along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
1140
1141+import logging
1142+import re
1143+import sys
1144 import six
1145 from collections import OrderedDict
1146 from charmhelpers.contrib.amulet.deployment import (
1147 AmuletDeployment
1148 )
1149
1150+DEBUG = logging.DEBUG
1151+ERROR = logging.ERROR
1152+
1153
1154 class OpenStackAmuletDeployment(AmuletDeployment):
1155 """OpenStack amulet deployment.
1156@@ -28,9 +34,12 @@
1157 that is specifically for use by OpenStack charms.
1158 """
1159
1160- def __init__(self, series=None, openstack=None, source=None, stable=True):
1161+ def __init__(self, series=None, openstack=None, source=None,
1162+ stable=True, log_level=DEBUG):
1163 """Initialize the deployment environment."""
1164 super(OpenStackAmuletDeployment, self).__init__(series)
1165+ self.log = self.get_logger(level=log_level)
1166+ self.log.info('OpenStackAmuletDeployment: init')
1167 self.openstack = openstack
1168 self.source = source
1169 self.stable = stable
1170@@ -38,26 +47,55 @@
1171 # out.
1172 self.current_next = "trusty"
1173
1174+ def get_logger(self, name="deployment-logger", level=logging.DEBUG):
1175+ """Get a logger object that will log to stdout."""
1176+ log = logging
1177+ logger = log.getLogger(name)
1178+ fmt = log.Formatter("%(asctime)s %(funcName)s "
1179+ "%(levelname)s: %(message)s")
1180+
1181+ handler = log.StreamHandler(stream=sys.stdout)
1182+ handler.setLevel(level)
1183+ handler.setFormatter(fmt)
1184+
1185+ logger.addHandler(handler)
1186+ logger.setLevel(level)
1187+
1188+ return logger
1189+
1190 def _determine_branch_locations(self, other_services):
1191 """Determine the branch locations for the other services.
1192
1193 Determine if the local branch being tested is derived from its
1194 stable or next (dev) branch, and based on this, use the corresonding
1195 stable or next branches for the other_services."""
1196+
1197+ self.log.info('OpenStackAmuletDeployment: determine branch locations')
1198+
1199+ # Charms outside the lp:~openstack-charmers namespace
1200 base_charms = ['mysql', 'mongodb', 'nrpe']
1201
1202+ # Force these charms to current series even when using an older series.
1203+ # ie. Use trusty/nrpe even when series is precise, as the P charm
1204+ # does not possess the necessary external master config and hooks.
1205+ force_series_current = ['nrpe']
1206+
1207 if self.series in ['precise', 'trusty']:
1208 base_series = self.series
1209 else:
1210 base_series = self.current_next
1211
1212- if self.stable:
1213- for svc in other_services:
1214+ for svc in other_services:
1215+ if svc['name'] in force_series_current:
1216+ base_series = self.current_next
1217+ # If a location has been explicitly set, use it
1218+ if svc.get('location'):
1219+ continue
1220+ if self.stable:
1221 temp = 'lp:charms/{}/{}'
1222 svc['location'] = temp.format(base_series,
1223 svc['name'])
1224- else:
1225- for svc in other_services:
1226+ else:
1227 if svc['name'] in base_charms:
1228 temp = 'lp:charms/{}/{}'
1229 svc['location'] = temp.format(base_series,
1230@@ -66,10 +104,13 @@
1231 temp = 'lp:~openstack-charmers/charms/{}/{}/next'
1232 svc['location'] = temp.format(self.current_next,
1233 svc['name'])
1234+
1235 return other_services
1236
1237 def _add_services(self, this_service, other_services):
1238 """Add services to the deployment and set openstack-origin/source."""
1239+ self.log.info('OpenStackAmuletDeployment: adding services')
1240+
1241 other_services = self._determine_branch_locations(other_services)
1242
1243 super(OpenStackAmuletDeployment, self)._add_services(this_service,
1244@@ -77,29 +118,102 @@
1245
1246 services = other_services
1247 services.append(this_service)
1248+
1249+ # Charms which should use the source config option
1250 use_source = ['mysql', 'mongodb', 'rabbitmq-server', 'ceph',
1251 'ceph-osd', 'ceph-radosgw']
1252- # Most OpenStack subordinate charms do not expose an origin option
1253- # as that is controlled by the principle.
1254- ignore = ['cinder-ceph', 'hacluster', 'neutron-openvswitch', 'nrpe']
1255+
1256+ # Charms which can not use openstack-origin, ie. many subordinates
1257+ no_origin = ['cinder-ceph', 'hacluster', 'neutron-openvswitch', 'nrpe',
1258+ 'openvswitch-odl', 'neutron-api-odl', 'odl-controller']
1259
1260 if self.openstack:
1261 for svc in services:
1262- if svc['name'] not in use_source + ignore:
1263+ if svc['name'] not in use_source + no_origin:
1264 config = {'openstack-origin': self.openstack}
1265 self.d.configure(svc['name'], config)
1266
1267 if self.source:
1268 for svc in services:
1269- if svc['name'] in use_source and svc['name'] not in ignore:
1270+ if svc['name'] in use_source and svc['name'] not in no_origin:
1271 config = {'source': self.source}
1272 self.d.configure(svc['name'], config)
1273
1274 def _configure_services(self, configs):
1275 """Configure all of the services."""
1276+ self.log.info('OpenStackAmuletDeployment: configure services')
1277 for service, config in six.iteritems(configs):
1278 self.d.configure(service, config)
1279
1280+ def _auto_wait_for_status(self, message=None, exclude_services=None,
1281+ include_only=None, timeout=1800):
1282+ """Wait for all units to have a specific extended status, except
1283+ for any defined as excluded. Unless specified via message, any
1284+ status containing any case of 'ready' will be considered a match.
1285+
1286+ Examples of message usage:
1287+
1288+ Wait for all unit status to CONTAIN any case of 'ready' or 'ok':
1289+ message = re.compile('.*ready.*|.*ok.*', re.IGNORECASE)
1290+
1291+ Wait for all units to reach this status (exact match):
1292+ message = re.compile('^Unit is ready and clustered$')
1293+
1294+ Wait for all units to reach any one of these (exact match):
1295+ message = re.compile('Unit is ready|OK|Ready')
1296+
1297+ Wait for at least one unit to reach this status (exact match):
1298+ message = {'ready'}
1299+
1300+ See Amulet's sentry.wait_for_messages() for message usage detail.
1301+ https://github.com/juju/amulet/blob/master/amulet/sentry.py
1302+
1303+ :param message: Expected status match
1304+ :param exclude_services: List of juju service names to ignore,
1305+ not to be used in conjuction with include_only.
1306+ :param include_only: List of juju service names to exclusively check,
1307+ not to be used in conjuction with exclude_services.
1308+ :param timeout: Maximum time in seconds to wait for status match
1309+ :returns: None. Raises if timeout is hit.
1310+ """
1311+ self.log.info('Waiting for extended status on units...')
1312+
1313+ all_services = self.d.services.keys()
1314+
1315+ if exclude_services and include_only:
1316+ raise ValueError('exclude_services can not be used '
1317+ 'with include_only')
1318+
1319+ if message:
1320+ if isinstance(message, re._pattern_type):
1321+ match = message.pattern
1322+ else:
1323+ match = message
1324+
1325+ self.log.debug('Custom extended status wait match: '
1326+ '{}'.format(match))
1327+ else:
1328+ self.log.debug('Default extended status wait match: contains '
1329+ 'READY (case-insensitive)')
1330+ message = re.compile('.*ready.*', re.IGNORECASE)
1331+
1332+ if exclude_services:
1333+ self.log.debug('Excluding services from extended status match: '
1334+ '{}'.format(exclude_services))
1335+ else:
1336+ exclude_services = []
1337+
1338+ if include_only:
1339+ services = include_only
1340+ else:
1341+ services = list(set(all_services) - set(exclude_services))
1342+
1343+ self.log.debug('Waiting up to {}s for extended status on services: '
1344+ '{}'.format(timeout, services))
1345+ service_messages = {service: message for service in services}
1346+ self.d.sentry.wait_for_messages(service_messages, timeout=timeout)
1347+ self.log.info('OK')
1348+
1349 def _get_openstack_release(self):
1350 """Get openstack release.
1351
1352@@ -111,7 +225,8 @@
1353 self.precise_havana, self.precise_icehouse,
1354 self.trusty_icehouse, self.trusty_juno, self.utopic_juno,
1355 self.trusty_kilo, self.vivid_kilo, self.trusty_liberty,
1356- self.wily_liberty) = range(12)
1357+ self.wily_liberty, self.trusty_mitaka,
1358+ self.xenial_mitaka) = range(14)
1359
1360 releases = {
1361 ('precise', None): self.precise_essex,
1362@@ -123,9 +238,11 @@
1363 ('trusty', 'cloud:trusty-juno'): self.trusty_juno,
1364 ('trusty', 'cloud:trusty-kilo'): self.trusty_kilo,
1365 ('trusty', 'cloud:trusty-liberty'): self.trusty_liberty,
1366+ ('trusty', 'cloud:trusty-mitaka'): self.trusty_mitaka,
1367 ('utopic', None): self.utopic_juno,
1368 ('vivid', None): self.vivid_kilo,
1369- ('wily', None): self.wily_liberty}
1370+ ('wily', None): self.wily_liberty,
1371+ ('xenial', None): self.xenial_mitaka}
1372 return releases[(self.series, self.openstack)]
1373
1374 def _get_openstack_release_string(self):
1375@@ -142,6 +259,7 @@
1376 ('utopic', 'juno'),
1377 ('vivid', 'kilo'),
1378 ('wily', 'liberty'),
1379+ ('xenial', 'mitaka'),
1380 ])
1381 if self.openstack:
1382 os_origin = self.openstack.split(':')[1]
1383
1384=== modified file 'hooks/charmhelpers/contrib/openstack/amulet/utils.py'
1385--- hooks/charmhelpers/contrib/openstack/amulet/utils.py 2015-07-17 13:24:05 +0000
1386+++ hooks/charmhelpers/contrib/openstack/amulet/utils.py 2016-02-18 14:28:13 +0000
1387@@ -18,6 +18,7 @@
1388 import json
1389 import logging
1390 import os
1391+import re
1392 import six
1393 import time
1394 import urllib
1395@@ -27,6 +28,7 @@
1396 import heatclient.v1.client as heat_client
1397 import keystoneclient.v2_0 as keystone_client
1398 import novaclient.v1_1.client as nova_client
1399+import pika
1400 import swiftclient
1401
1402 from charmhelpers.contrib.amulet.utils import (
1403@@ -602,3 +604,382 @@
1404 self.log.debug('Ceph {} samples (OK): '
1405 '{}'.format(sample_type, samples))
1406 return None
1407+
1408+ # rabbitmq/amqp specific helpers:
1409+
1410+ def rmq_wait_for_cluster(self, deployment, init_sleep=15, timeout=1200):
1411+ """Wait for rmq units extended status to show cluster readiness,
1412+ after an optional initial sleep period. Initial sleep is likely
1413+ necessary to be effective following a config change, as status
1414+ message may not instantly update to non-ready."""
1415+
1416+ if init_sleep:
1417+ time.sleep(init_sleep)
1418+
1419+ message = re.compile('^Unit is ready and clustered$')
1420+ deployment._auto_wait_for_status(message=message,
1421+ timeout=timeout,
1422+ include_only=['rabbitmq-server'])
1423+
1424+ def add_rmq_test_user(self, sentry_units,
1425+ username="testuser1", password="changeme"):
1426+ """Add a test user via the first rmq juju unit, check connection as
1427+ the new user against all sentry units.
1428+
1429+ :param sentry_units: list of sentry unit pointers
1430+ :param username: amqp user name, default to testuser1
1431+ :param password: amqp user password
1432+ :returns: None if successful. Raise on error.
1433+ """
1434+ self.log.debug('Adding rmq user ({})...'.format(username))
1435+
1436+ # Check that user does not already exist
1437+ cmd_user_list = 'rabbitmqctl list_users'
1438+ output, _ = self.run_cmd_unit(sentry_units[0], cmd_user_list)
1439+ if username in output:
1440+ self.log.warning('User ({}) already exists, returning '
1441+ 'gracefully.'.format(username))
1442+ return
1443+
1444+ perms = '".*" ".*" ".*"'
1445+ cmds = ['rabbitmqctl add_user {} {}'.format(username, password),
1446+ 'rabbitmqctl set_permissions {} {}'.format(username, perms)]
1447+
1448+ # Add user via first unit
1449+ for cmd in cmds:
1450+ output, _ = self.run_cmd_unit(sentry_units[0], cmd)
1451+
1452+ # Check connection against the other sentry_units
1453+ self.log.debug('Checking user connect against units...')
1454+ for sentry_unit in sentry_units:
1455+ connection = self.connect_amqp_by_unit(sentry_unit, ssl=False,
1456+ username=username,
1457+ password=password)
1458+ connection.close()
1459+
1460+ def delete_rmq_test_user(self, sentry_units, username="testuser1"):
1461+ """Delete a rabbitmq user via the first rmq juju unit.
1462+
1463+ :param sentry_units: list of sentry unit pointers
1464+ :param username: amqp user name, default to testuser1
1465+ :param password: amqp user password
1466+ :returns: None if successful or no such user.
1467+ """
1468+ self.log.debug('Deleting rmq user ({})...'.format(username))
1469+
1470+ # Check that the user exists
1471+ cmd_user_list = 'rabbitmqctl list_users'
1472+ output, _ = self.run_cmd_unit(sentry_units[0], cmd_user_list)
1473+
1474+ if username not in output:
1475+ self.log.warning('User ({}) does not exist, returning '
1476+ 'gracefully.'.format(username))
1477+ return
1478+
1479+ # Delete the user
1480+ cmd_user_del = 'rabbitmqctl delete_user {}'.format(username)
1481+ output, _ = self.run_cmd_unit(sentry_units[0], cmd_user_del)
1482+
1483+ def get_rmq_cluster_status(self, sentry_unit):
1484+ """Execute rabbitmq cluster status command on a unit and return
1485+ the full output.
1486+
1487+ :param unit: sentry unit
1488+ :returns: String containing console output of cluster status command
1489+ """
1490+ cmd = 'rabbitmqctl cluster_status'
1491+ output, _ = self.run_cmd_unit(sentry_unit, cmd)
1492+ self.log.debug('{} cluster_status:\n{}'.format(
1493+ sentry_unit.info['unit_name'], output))
1494+ return str(output)
1495+
1496+ def get_rmq_cluster_running_nodes(self, sentry_unit):
1497+ """Parse rabbitmqctl cluster_status output string, return list of
1498+ running rabbitmq cluster nodes.
1499+
1500+ :param unit: sentry unit
1501+ :returns: List containing node names of running nodes
1502+ """
1503+ # NOTE(beisner): rabbitmqctl cluster_status output is not
1504+ # json-parsable, do string chop foo, then json.loads that.
1505+ str_stat = self.get_rmq_cluster_status(sentry_unit)
1506+ if 'running_nodes' in str_stat:
1507+ pos_start = str_stat.find("{running_nodes,") + 15
1508+ pos_end = str_stat.find("]},", pos_start) + 1
1509+ str_run_nodes = str_stat[pos_start:pos_end].replace("'", '"')
1510+ run_nodes = json.loads(str_run_nodes)
1511+ return run_nodes
1512+ else:
1513+ return []
1514+
1515+ def validate_rmq_cluster_running_nodes(self, sentry_units):
1516+ """Check that all rmq unit hostnames are represented in the
1517+ cluster_status output of all units.
1518+
1519+ :param host_names: dict of juju unit names to host names
1520+ :param units: list of sentry unit pointers (all rmq units)
1521+ :returns: None if successful, otherwise return error message
1522+ """
1523+ host_names = self.get_unit_hostnames(sentry_units)
1524+ errors = []
1525+
1526+ # Query every unit for cluster_status running nodes
1527+ for query_unit in sentry_units:
1528+ query_unit_name = query_unit.info['unit_name']
1529+ running_nodes = self.get_rmq_cluster_running_nodes(query_unit)
1530+
1531+ # Confirm that every unit is represented in the queried unit's
1532+ # cluster_status running nodes output.
1533+ for validate_unit in sentry_units:
1534+ val_host_name = host_names[validate_unit.info['unit_name']]
1535+ val_node_name = 'rabbit@{}'.format(val_host_name)
1536+
1537+ if val_node_name not in running_nodes:
1538+ errors.append('Cluster member check failed on {}: {} not '
1539+ 'in {}\n'.format(query_unit_name,
1540+ val_node_name,
1541+ running_nodes))
1542+ if errors:
1543+ return ''.join(errors)
1544+
1545+ def rmq_ssl_is_enabled_on_unit(self, sentry_unit, port=None):
1546+ """Check a single juju rmq unit for ssl and port in the config file."""
1547+ host = sentry_unit.info['public-address']
1548+ unit_name = sentry_unit.info['unit_name']
1549+
1550+ conf_file = '/etc/rabbitmq/rabbitmq.config'
1551+ conf_contents = str(self.file_contents_safe(sentry_unit,
1552+ conf_file, max_wait=16))
1553+ # Checks
1554+ conf_ssl = 'ssl' in conf_contents
1555+ conf_port = str(port) in conf_contents
1556+
1557+ # Port explicitly checked in config
1558+ if port and conf_port and conf_ssl:
1559+ self.log.debug('SSL is enabled @{}:{} '
1560+ '({})'.format(host, port, unit_name))
1561+ return True
1562+ elif port and not conf_port and conf_ssl:
1563+ self.log.debug('SSL is enabled @{} but not on port {} '
1564+ '({})'.format(host, port, unit_name))
1565+ return False
1566+ # Port not checked (useful when checking that ssl is disabled)
1567+ elif not port and conf_ssl:
1568+ self.log.debug('SSL is enabled @{}:{} '
1569+ '({})'.format(host, port, unit_name))
1570+ return True
1571+ elif not conf_ssl:
1572+ self.log.debug('SSL not enabled @{}:{} '
1573+ '({})'.format(host, port, unit_name))
1574+ return False
1575+ else:
1576+ msg = ('Unknown condition when checking SSL status @{}:{} '
1577+ '({})'.format(host, port, unit_name))
1578+ amulet.raise_status(amulet.FAIL, msg)
1579+
1580+ def validate_rmq_ssl_enabled_units(self, sentry_units, port=None):
1581+ """Check that ssl is enabled on rmq juju sentry units.
1582+
1583+ :param sentry_units: list of all rmq sentry units
1584+ :param port: optional ssl port override to validate
1585+ :returns: None if successful, otherwise return error message
1586+ """
1587+ for sentry_unit in sentry_units:
1588+ if not self.rmq_ssl_is_enabled_on_unit(sentry_unit, port=port):
1589+ return ('Unexpected condition: ssl is disabled on unit '
1590+ '({})'.format(sentry_unit.info['unit_name']))
1591+ return None
1592+
1593+ def validate_rmq_ssl_disabled_units(self, sentry_units):
1594+ """Check that ssl is enabled on listed rmq juju sentry units.
1595+
1596+ :param sentry_units: list of all rmq sentry units
1597+ :returns: True if successful. Raise on error.
1598+ """
1599+ for sentry_unit in sentry_units:
1600+ if self.rmq_ssl_is_enabled_on_unit(sentry_unit):
1601+ return ('Unexpected condition: ssl is enabled on unit '
1602+ '({})'.format(sentry_unit.info['unit_name']))
1603+ return None
1604+
1605+ def configure_rmq_ssl_on(self, sentry_units, deployment,
1606+ port=None, max_wait=60):
1607+ """Turn ssl charm config option on, with optional non-default
1608+ ssl port specification. Confirm that it is enabled on every
1609+ unit.
1610+
1611+ :param sentry_units: list of sentry units
1612+ :param deployment: amulet deployment object pointer
1613+ :param port: amqp port, use defaults if None
1614+ :param max_wait: maximum time to wait in seconds to confirm
1615+ :returns: None if successful. Raise on error.
1616+ """
1617+ self.log.debug('Setting ssl charm config option: on')
1618+
1619+ # Enable RMQ SSL
1620+ config = {'ssl': 'on'}
1621+ if port:
1622+ config['ssl_port'] = port
1623+
1624+ deployment.d.configure('rabbitmq-server', config)
1625+
1626+ # Wait for unit status
1627+ self.rmq_wait_for_cluster(deployment)
1628+
1629+ # Confirm
1630+ tries = 0
1631+ ret = self.validate_rmq_ssl_enabled_units(sentry_units, port=port)
1632+ while ret and tries < (max_wait / 4):
1633+ time.sleep(4)
1634+ self.log.debug('Attempt {}: {}'.format(tries, ret))
1635+ ret = self.validate_rmq_ssl_enabled_units(sentry_units, port=port)
1636+ tries += 1
1637+
1638+ if ret:
1639+ amulet.raise_status(amulet.FAIL, ret)
1640+
1641+ def configure_rmq_ssl_off(self, sentry_units, deployment, max_wait=60):
1642+ """Turn ssl charm config option off, confirm that it is disabled
1643+ on every unit.
1644+
1645+ :param sentry_units: list of sentry units
1646+ :param deployment: amulet deployment object pointer
1647+ :param max_wait: maximum time to wait in seconds to confirm
1648+ :returns: None if successful. Raise on error.
1649+ """
1650+ self.log.debug('Setting ssl charm config option: off')
1651+
1652+ # Disable RMQ SSL
1653+ config = {'ssl': 'off'}
1654+ deployment.d.configure('rabbitmq-server', config)
1655+
1656+ # Wait for unit status
1657+ self.rmq_wait_for_cluster(deployment)
1658+
1659+ # Confirm
1660+ tries = 0
1661+ ret = self.validate_rmq_ssl_disabled_units(sentry_units)
1662+ while ret and tries < (max_wait / 4):
1663+ time.sleep(4)
1664+ self.log.debug('Attempt {}: {}'.format(tries, ret))
1665+ ret = self.validate_rmq_ssl_disabled_units(sentry_units)
1666+ tries += 1
1667+
1668+ if ret:
1669+ amulet.raise_status(amulet.FAIL, ret)
1670+
1671+ def connect_amqp_by_unit(self, sentry_unit, ssl=False,
1672+ port=None, fatal=True,
1673+ username="testuser1", password="changeme"):
1674+ """Establish and return a pika amqp connection to the rabbitmq service
1675+ running on a rmq juju unit.
1676+
1677+ :param sentry_unit: sentry unit pointer
1678+ :param ssl: boolean, default to False
1679+ :param port: amqp port, use defaults if None
1680+ :param fatal: boolean, default to True (raises on connect error)
1681+ :param username: amqp user name, default to testuser1
1682+ :param password: amqp user password
1683+ :returns: pika amqp connection pointer or None if failed and non-fatal
1684+ """
1685+ host = sentry_unit.info['public-address']
1686+ unit_name = sentry_unit.info['unit_name']
1687+
1688+ # Default port logic if port is not specified
1689+ if ssl and not port:
1690+ port = 5671
1691+ elif not ssl and not port:
1692+ port = 5672
1693+
1694+ self.log.debug('Connecting to amqp on {}:{} ({}) as '
1695+ '{}...'.format(host, port, unit_name, username))
1696+
1697+ try:
1698+ credentials = pika.PlainCredentials(username, password)
1699+ parameters = pika.ConnectionParameters(host=host, port=port,
1700+ credentials=credentials,
1701+ ssl=ssl,
1702+ connection_attempts=3,
1703+ retry_delay=5,
1704+ socket_timeout=1)
1705+ connection = pika.BlockingConnection(parameters)
1706+ assert connection.server_properties['product'] == 'RabbitMQ'
1707+ self.log.debug('Connect OK')
1708+ return connection
1709+ except Exception as e:
1710+ msg = ('amqp connection failed to {}:{} as '
1711+ '{} ({})'.format(host, port, username, str(e)))
1712+ if fatal:
1713+ amulet.raise_status(amulet.FAIL, msg)
1714+ else:
1715+ self.log.warn(msg)
1716+ return None
1717+
1718+ def publish_amqp_message_by_unit(self, sentry_unit, message,
1719+ queue="test", ssl=False,
1720+ username="testuser1",
1721+ password="changeme",
1722+ port=None):
1723+ """Publish an amqp message to a rmq juju unit.
1724+
1725+ :param sentry_unit: sentry unit pointer
1726+ :param message: amqp message string
1727+ :param queue: message queue, default to test
1728+ :param username: amqp user name, default to testuser1
1729+ :param password: amqp user password
1730+ :param ssl: boolean, default to False
1731+ :param port: amqp port, use defaults if None
1732+ :returns: None. Raises exception if publish failed.
1733+ """
1734+ self.log.debug('Publishing message to {} queue:\n{}'.format(queue,
1735+ message))
1736+ connection = self.connect_amqp_by_unit(sentry_unit, ssl=ssl,
1737+ port=port,
1738+ username=username,
1739+ password=password)
1740+
1741+ # NOTE(beisner): extra debug here re: pika hang potential:
1742+ # https://github.com/pika/pika/issues/297
1743+ # https://groups.google.com/forum/#!topic/rabbitmq-users/Ja0iyfF0Szw
1744+ self.log.debug('Defining channel...')
1745+ channel = connection.channel()
1746+ self.log.debug('Declaring queue...')
1747+ channel.queue_declare(queue=queue, auto_delete=False, durable=True)
1748+ self.log.debug('Publishing message...')
1749+ channel.basic_publish(exchange='', routing_key=queue, body=message)
1750+ self.log.debug('Closing channel...')
1751+ channel.close()
1752+ self.log.debug('Closing connection...')
1753+ connection.close()
1754+
1755+ def get_amqp_message_by_unit(self, sentry_unit, queue="test",
1756+ username="testuser1",
1757+ password="changeme",
1758+ ssl=False, port=None):
1759+ """Get an amqp message from a rmq juju unit.
1760+
1761+ :param sentry_unit: sentry unit pointer
1762+ :param queue: message queue, default to test
1763+ :param username: amqp user name, default to testuser1
1764+ :param password: amqp user password
1765+ :param ssl: boolean, default to False
1766+ :param port: amqp port, use defaults if None
1767+ :returns: amqp message body as string. Raise if get fails.
1768+ """
1769+ connection = self.connect_amqp_by_unit(sentry_unit, ssl=ssl,
1770+ port=port,
1771+ username=username,
1772+ password=password)
1773+ channel = connection.channel()
1774+ method_frame, _, body = channel.basic_get(queue)
1775+
1776+ if method_frame:
1777+ self.log.debug('Retreived message from {} queue:\n{}'.format(queue,
1778+ body))
1779+ channel.basic_ack(method_frame.delivery_tag)
1780+ channel.close()
1781+ connection.close()
1782+ return body
1783+ else:
1784+ msg = 'No message retrieved.'
1785+ amulet.raise_status(amulet.FAIL, msg)
1786
1787=== modified file 'hooks/charmhelpers/contrib/openstack/context.py'
1788--- hooks/charmhelpers/contrib/openstack/context.py 2015-09-12 10:58:20 +0000
1789+++ hooks/charmhelpers/contrib/openstack/context.py 2016-02-18 14:28:13 +0000
1790@@ -14,6 +14,7 @@
1791 # You should have received a copy of the GNU Lesser General Public License
1792 # along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
1793
1794+import glob
1795 import json
1796 import os
1797 import re
1798@@ -56,6 +57,7 @@
1799 get_nic_hwaddr,
1800 mkdir,
1801 write_file,
1802+ pwgen,
1803 )
1804 from charmhelpers.contrib.hahelpers.cluster import (
1805 determine_apache_port,
1806@@ -86,6 +88,8 @@
1807 is_bridge_member,
1808 )
1809 from charmhelpers.contrib.openstack.utils import get_host_ip
1810+from charmhelpers.core.unitdata import kv
1811+
1812 CA_CERT_PATH = '/usr/local/share/ca-certificates/keystone_juju_ca_cert.crt'
1813 ADDRESS_TYPES = ['admin', 'internal', 'public']
1814
1815@@ -194,10 +198,50 @@
1816 class OSContextGenerator(object):
1817 """Base class for all context generators."""
1818 interfaces = []
1819+ related = False
1820+ complete = False
1821+ missing_data = []
1822
1823 def __call__(self):
1824 raise NotImplementedError
1825
1826+ def context_complete(self, ctxt):
1827+ """Check for missing data for the required context data.
1828+ Set self.missing_data if it exists and return False.
1829+ Set self.complete if no missing data and return True.
1830+ """
1831+ # Fresh start
1832+ self.complete = False
1833+ self.missing_data = []
1834+ for k, v in six.iteritems(ctxt):
1835+ if v is None or v == '':
1836+ if k not in self.missing_data:
1837+ self.missing_data.append(k)
1838+
1839+ if self.missing_data:
1840+ self.complete = False
1841+ log('Missing required data: %s' % ' '.join(self.missing_data), level=INFO)
1842+ else:
1843+ self.complete = True
1844+ return self.complete
1845+
1846+ def get_related(self):
1847+ """Check if any of the context interfaces have relation ids.
1848+ Set self.related and return True if one of the interfaces
1849+ has relation ids.
1850+ """
1851+ # Fresh start
1852+ self.related = False
1853+ try:
1854+ for interface in self.interfaces:
1855+ if relation_ids(interface):
1856+ self.related = True
1857+ return self.related
1858+ except AttributeError as e:
1859+ log("{} {}"
1860+ "".format(self, e), 'INFO')
1861+ return self.related
1862+
1863
1864 class SharedDBContext(OSContextGenerator):
1865 interfaces = ['shared-db']
1866@@ -213,6 +257,7 @@
1867 self.database = database
1868 self.user = user
1869 self.ssl_dir = ssl_dir
1870+ self.rel_name = self.interfaces[0]
1871
1872 def __call__(self):
1873 self.database = self.database or config('database')
1874@@ -246,6 +291,7 @@
1875 password_setting = self.relation_prefix + '_password'
1876
1877 for rid in relation_ids(self.interfaces[0]):
1878+ self.related = True
1879 for unit in related_units(rid):
1880 rdata = relation_get(rid=rid, unit=unit)
1881 host = rdata.get('db_host')
1882@@ -257,7 +303,7 @@
1883 'database_password': rdata.get(password_setting),
1884 'database_type': 'mysql'
1885 }
1886- if context_complete(ctxt):
1887+ if self.context_complete(ctxt):
1888 db_ssl(rdata, ctxt, self.ssl_dir)
1889 return ctxt
1890 return {}
1891@@ -278,6 +324,7 @@
1892
1893 ctxt = {}
1894 for rid in relation_ids(self.interfaces[0]):
1895+ self.related = True
1896 for unit in related_units(rid):
1897 rel_host = relation_get('host', rid=rid, unit=unit)
1898 rel_user = relation_get('user', rid=rid, unit=unit)
1899@@ -287,7 +334,7 @@
1900 'database_user': rel_user,
1901 'database_password': rel_passwd,
1902 'database_type': 'postgresql'}
1903- if context_complete(ctxt):
1904+ if self.context_complete(ctxt):
1905 return ctxt
1906
1907 return {}
1908@@ -348,6 +395,7 @@
1909 ctxt['signing_dir'] = cachedir
1910
1911 for rid in relation_ids(self.rel_name):
1912+ self.related = True
1913 for unit in related_units(rid):
1914 rdata = relation_get(rid=rid, unit=unit)
1915 serv_host = rdata.get('service_host')
1916@@ -366,7 +414,7 @@
1917 'service_protocol': svc_protocol,
1918 'auth_protocol': auth_protocol})
1919
1920- if context_complete(ctxt):
1921+ if self.context_complete(ctxt):
1922 # NOTE(jamespage) this is required for >= icehouse
1923 # so a missing value just indicates keystone needs
1924 # upgrading
1925@@ -405,6 +453,7 @@
1926 ctxt = {}
1927 for rid in relation_ids(self.rel_name):
1928 ha_vip_only = False
1929+ self.related = True
1930 for unit in related_units(rid):
1931 if relation_get('clustered', rid=rid, unit=unit):
1932 ctxt['clustered'] = True
1933@@ -437,7 +486,7 @@
1934 ha_vip_only = relation_get('ha-vip-only',
1935 rid=rid, unit=unit) is not None
1936
1937- if context_complete(ctxt):
1938+ if self.context_complete(ctxt):
1939 if 'rabbit_ssl_ca' in ctxt:
1940 if not self.ssl_dir:
1941 log("Charm not setup for ssl support but ssl ca "
1942@@ -469,7 +518,7 @@
1943 ctxt['oslo_messaging_flags'] = config_flags_parser(
1944 oslo_messaging_flags)
1945
1946- if not context_complete(ctxt):
1947+ if not self.complete:
1948 return {}
1949
1950 return ctxt
1951@@ -485,13 +534,15 @@
1952
1953 log('Generating template context for ceph', level=DEBUG)
1954 mon_hosts = []
1955- auth = None
1956- key = None
1957- use_syslog = str(config('use-syslog')).lower()
1958+ ctxt = {
1959+ 'use_syslog': str(config('use-syslog')).lower()
1960+ }
1961 for rid in relation_ids('ceph'):
1962 for unit in related_units(rid):
1963- auth = relation_get('auth', rid=rid, unit=unit)
1964- key = relation_get('key', rid=rid, unit=unit)
1965+ if not ctxt.get('auth'):
1966+ ctxt['auth'] = relation_get('auth', rid=rid, unit=unit)
1967+ if not ctxt.get('key'):
1968+ ctxt['key'] = relation_get('key', rid=rid, unit=unit)
1969 ceph_pub_addr = relation_get('ceph-public-address', rid=rid,
1970 unit=unit)
1971 unit_priv_addr = relation_get('private-address', rid=rid,
1972@@ -500,15 +551,12 @@
1973 ceph_addr = format_ipv6_addr(ceph_addr) or ceph_addr
1974 mon_hosts.append(ceph_addr)
1975
1976- ctxt = {'mon_hosts': ' '.join(sorted(mon_hosts)),
1977- 'auth': auth,
1978- 'key': key,
1979- 'use_syslog': use_syslog}
1980+ ctxt['mon_hosts'] = ' '.join(sorted(mon_hosts))
1981
1982 if not os.path.isdir('/etc/ceph'):
1983 os.mkdir('/etc/ceph')
1984
1985- if not context_complete(ctxt):
1986+ if not self.context_complete(ctxt):
1987 return {}
1988
1989 ensure_packages(['ceph-common'])
1990@@ -581,15 +629,28 @@
1991 if config('haproxy-client-timeout'):
1992 ctxt['haproxy_client_timeout'] = config('haproxy-client-timeout')
1993
1994+ if config('haproxy-queue-timeout'):
1995+ ctxt['haproxy_queue_timeout'] = config('haproxy-queue-timeout')
1996+
1997+ if config('haproxy-connect-timeout'):
1998+ ctxt['haproxy_connect_timeout'] = config('haproxy-connect-timeout')
1999+
2000 if config('prefer-ipv6'):
2001 ctxt['ipv6'] = True
2002 ctxt['local_host'] = 'ip6-localhost'
2003 ctxt['haproxy_host'] = '::'
2004- ctxt['stat_port'] = ':::8888'
2005 else:
2006 ctxt['local_host'] = '127.0.0.1'
2007 ctxt['haproxy_host'] = '0.0.0.0'
2008- ctxt['stat_port'] = ':8888'
2009+
2010+ ctxt['stat_port'] = '8888'
2011+
2012+ db = kv()
2013+ ctxt['stat_password'] = db.get('stat-password')
2014+ if not ctxt['stat_password']:
2015+ ctxt['stat_password'] = db.set('stat-password',
2016+ pwgen(32))
2017+ db.flush()
2018
2019 for frontend in cluster_hosts:
2020 if (len(cluster_hosts[frontend]['backends']) > 1 or
2021@@ -907,6 +968,19 @@
2022 'config': config}
2023 return ovs_ctxt
2024
2025+ def midonet_ctxt(self):
2026+ driver = neutron_plugin_attribute(self.plugin, 'driver',
2027+ self.network_manager)
2028+ midonet_config = neutron_plugin_attribute(self.plugin, 'config',
2029+ self.network_manager)
2030+ mido_ctxt = {'core_plugin': driver,
2031+ 'neutron_plugin': 'midonet',
2032+ 'neutron_security_groups': self.neutron_security_groups,
2033+ 'local_ip': unit_private_ip(),
2034+ 'config': midonet_config}
2035+
2036+ return mido_ctxt
2037+
2038 def __call__(self):
2039 if self.network_manager not in ['quantum', 'neutron']:
2040 return {}
2041@@ -928,6 +1002,8 @@
2042 ctxt.update(self.nuage_ctxt())
2043 elif self.plugin == 'plumgrid':
2044 ctxt.update(self.pg_ctxt())
2045+ elif self.plugin == 'midonet':
2046+ ctxt.update(self.midonet_ctxt())
2047
2048 alchemy_flags = config('neutron-alchemy-flags')
2049 if alchemy_flags:
2050@@ -1028,6 +1104,20 @@
2051 config_flags_parser(config_flags)}
2052
2053
2054+class LibvirtConfigFlagsContext(OSContextGenerator):
2055+ """
2056+ This context provides support for extending
2057+ the libvirt section through user-defined flags.
2058+ """
2059+ def __call__(self):
2060+ ctxt = {}
2061+ libvirt_flags = config('libvirt-flags')
2062+ if libvirt_flags:
2063+ ctxt['libvirt_flags'] = config_flags_parser(
2064+ libvirt_flags)
2065+ return ctxt
2066+
2067+
2068 class SubordinateConfigContext(OSContextGenerator):
2069
2070 """
2071@@ -1060,7 +1150,7 @@
2072
2073 ctxt = {
2074 ... other context ...
2075- 'subordinate_config': {
2076+ 'subordinate_configuration': {
2077 'DEFAULT': {
2078 'key1': 'value1',
2079 },
2080@@ -1101,22 +1191,23 @@
2081 try:
2082 sub_config = json.loads(sub_config)
2083 except:
2084- log('Could not parse JSON from subordinate_config '
2085- 'setting from %s' % rid, level=ERROR)
2086+ log('Could not parse JSON from '
2087+ 'subordinate_configuration setting from %s'
2088+ % rid, level=ERROR)
2089 continue
2090
2091 for service in self.services:
2092 if service not in sub_config:
2093- log('Found subordinate_config on %s but it contained'
2094- 'nothing for %s service' % (rid, service),
2095- level=INFO)
2096+ log('Found subordinate_configuration on %s but it '
2097+ 'contained nothing for %s service'
2098+ % (rid, service), level=INFO)
2099 continue
2100
2101 sub_config = sub_config[service]
2102 if self.config_file not in sub_config:
2103- log('Found subordinate_config on %s but it contained'
2104- 'nothing for %s' % (rid, self.config_file),
2105- level=INFO)
2106+ log('Found subordinate_configuration on %s but it '
2107+ 'contained nothing for %s'
2108+ % (rid, self.config_file), level=INFO)
2109 continue
2110
2111 sub_config = sub_config[self.config_file]
2112@@ -1319,7 +1410,7 @@
2113 normalized.update({port: port for port in resolved
2114 if port in ports})
2115 if resolved:
2116- return {bridge: normalized[port] for port, bridge in
2117+ return {normalized[port]: bridge for port, bridge in
2118 six.iteritems(portmap) if port in normalized.keys()}
2119
2120 return None
2121@@ -1330,12 +1421,22 @@
2122 def __call__(self):
2123 ctxt = {}
2124 mappings = super(PhyNICMTUContext, self).__call__()
2125- if mappings and mappings.values():
2126- ports = mappings.values()
2127+ if mappings and mappings.keys():
2128+ ports = sorted(mappings.keys())
2129 napi_settings = NeutronAPIContext()()
2130 mtu = napi_settings.get('network_device_mtu')
2131+ all_ports = set()
2132+ # If any of ports is a vlan device, its underlying device must have
2133+ # mtu applied first.
2134+ for port in ports:
2135+ for lport in glob.glob("/sys/class/net/%s/lower_*" % port):
2136+ lport = os.path.basename(lport)
2137+ all_ports.add(lport.split('_')[1])
2138+
2139+ all_ports = list(all_ports)
2140+ all_ports.extend(ports)
2141 if mtu:
2142- ctxt["devs"] = '\\n'.join(ports)
2143+ ctxt["devs"] = '\\n'.join(all_ports)
2144 ctxt['mtu'] = mtu
2145
2146 return ctxt
2147@@ -1367,6 +1468,6 @@
2148 'auth_protocol':
2149 rdata.get('auth_protocol') or 'http',
2150 }
2151- if context_complete(ctxt):
2152+ if self.context_complete(ctxt):
2153 return ctxt
2154 return {}
2155
2156=== modified file 'hooks/charmhelpers/contrib/openstack/files/check_haproxy.sh'
2157--- hooks/charmhelpers/contrib/openstack/files/check_haproxy.sh 2015-02-24 05:48:43 +0000
2158+++ hooks/charmhelpers/contrib/openstack/files/check_haproxy.sh 2016-02-18 14:28:13 +0000
2159@@ -9,15 +9,17 @@
2160 CRITICAL=0
2161 NOTACTIVE=''
2162 LOGFILE=/var/log/nagios/check_haproxy.log
2163-AUTH=$(grep -r "stats auth" /etc/haproxy | head -1 | awk '{print $4}')
2164+AUTH=$(grep -r "stats auth" /etc/haproxy | awk 'NR=1{print $4}')
2165
2166-for appserver in $(grep ' server' /etc/haproxy/haproxy.cfg | awk '{print $2'});
2167+typeset -i N_INSTANCES=0
2168+for appserver in $(awk '/^\s+server/{print $2}' /etc/haproxy/haproxy.cfg)
2169 do
2170- 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')
2171+ N_INSTANCES=N_INSTANCES+1
2172+ output=$(/usr/lib/nagios/plugins/check_http -a ${AUTH} -I 127.0.0.1 -p 8888 -u '/;csv' --regex=",${appserver},.*,UP.*" -e ' 200 OK')
2173 if [ $? != 0 ]; then
2174 date >> $LOGFILE
2175 echo $output >> $LOGFILE
2176- /usr/lib/nagios/plugins/check_http -a ${AUTH} -I 127.0.0.1 -p 8888 -v | grep $appserver >> $LOGFILE 2>&1
2177+ /usr/lib/nagios/plugins/check_http -a ${AUTH} -I 127.0.0.1 -p 8888 -u '/;csv' -v | grep ",${appserver}," >> $LOGFILE 2>&1
2178 CRITICAL=1
2179 NOTACTIVE="${NOTACTIVE} $appserver"
2180 fi
2181@@ -28,5 +30,5 @@
2182 exit 2
2183 fi
2184
2185-echo "OK: All haproxy instances looking good"
2186+echo "OK: All haproxy instances ($N_INSTANCES) looking good"
2187 exit 0
2188
2189=== modified file 'hooks/charmhelpers/contrib/openstack/neutron.py'
2190--- hooks/charmhelpers/contrib/openstack/neutron.py 2015-09-03 09:42:35 +0000
2191+++ hooks/charmhelpers/contrib/openstack/neutron.py 2016-02-18 14:28:13 +0000
2192@@ -204,11 +204,25 @@
2193 database=config('database'),
2194 ssl_dir=NEUTRON_CONF_DIR)],
2195 'services': [],
2196- 'packages': [['plumgrid-lxc'],
2197- ['iovisor-dkms']],
2198+ 'packages': ['plumgrid-lxc',
2199+ 'iovisor-dkms'],
2200 'server_packages': ['neutron-server',
2201 'neutron-plugin-plumgrid'],
2202 'server_services': ['neutron-server']
2203+ },
2204+ 'midonet': {
2205+ 'config': '/etc/neutron/plugins/midonet/midonet.ini',
2206+ 'driver': 'midonet.neutron.plugin.MidonetPluginV2',
2207+ 'contexts': [
2208+ context.SharedDBContext(user=config('neutron-database-user'),
2209+ database=config('neutron-database'),
2210+ relation_prefix='neutron',
2211+ ssl_dir=NEUTRON_CONF_DIR)],
2212+ 'services': [],
2213+ 'packages': [[headers_package()] + determine_dkms_package()],
2214+ 'server_packages': ['neutron-server',
2215+ 'python-neutron-plugin-midonet'],
2216+ 'server_services': ['neutron-server']
2217 }
2218 }
2219 if release >= 'icehouse':
2220@@ -310,10 +324,10 @@
2221 def parse_data_port_mappings(mappings, default_bridge='br-data'):
2222 """Parse data port mappings.
2223
2224- Mappings must be a space-delimited list of port:bridge mappings.
2225+ Mappings must be a space-delimited list of bridge:port.
2226
2227- Returns dict of the form {port:bridge} where port may be an mac address or
2228- interface name.
2229+ Returns dict of the form {port:bridge} where ports may be mac addresses or
2230+ interface names.
2231 """
2232
2233 # NOTE(dosaboy): we use rvalue for key to allow multiple values to be
2234
2235=== modified file 'hooks/charmhelpers/contrib/openstack/templates/ceph.conf'
2236--- hooks/charmhelpers/contrib/openstack/templates/ceph.conf 2015-07-17 13:24:05 +0000
2237+++ hooks/charmhelpers/contrib/openstack/templates/ceph.conf 2016-02-18 14:28:13 +0000
2238@@ -13,3 +13,9 @@
2239 err to syslog = {{ use_syslog }}
2240 clog to syslog = {{ use_syslog }}
2241
2242+[client]
2243+{% if rbd_client_cache_settings -%}
2244+{% for key, value in rbd_client_cache_settings.iteritems() -%}
2245+{{ key }} = {{ value }}
2246+{% endfor -%}
2247+{%- endif %}
2248\ No newline at end of file
2249
2250=== modified file 'hooks/charmhelpers/contrib/openstack/templates/haproxy.cfg'
2251--- hooks/charmhelpers/contrib/openstack/templates/haproxy.cfg 2015-02-24 05:48:43 +0000
2252+++ hooks/charmhelpers/contrib/openstack/templates/haproxy.cfg 2016-02-18 14:28:13 +0000
2253@@ -12,27 +12,35 @@
2254 option tcplog
2255 option dontlognull
2256 retries 3
2257- timeout queue 1000
2258- timeout connect 1000
2259-{% if haproxy_client_timeout -%}
2260+{%- if haproxy_queue_timeout %}
2261+ timeout queue {{ haproxy_queue_timeout }}
2262+{%- else %}
2263+ timeout queue 5000
2264+{%- endif %}
2265+{%- if haproxy_connect_timeout %}
2266+ timeout connect {{ haproxy_connect_timeout }}
2267+{%- else %}
2268+ timeout connect 5000
2269+{%- endif %}
2270+{%- if haproxy_client_timeout %}
2271 timeout client {{ haproxy_client_timeout }}
2272-{% else -%}
2273+{%- else %}
2274 timeout client 30000
2275-{% endif -%}
2276-
2277-{% if haproxy_server_timeout -%}
2278+{%- endif %}
2279+{%- if haproxy_server_timeout %}
2280 timeout server {{ haproxy_server_timeout }}
2281-{% else -%}
2282+{%- else %}
2283 timeout server 30000
2284-{% endif -%}
2285+{%- endif %}
2286
2287-listen stats {{ stat_port }}
2288+listen stats
2289+ bind {{ local_host }}:{{ stat_port }}
2290 mode http
2291 stats enable
2292 stats hide-version
2293 stats realm Haproxy\ Statistics
2294 stats uri /
2295- stats auth admin:password
2296+ stats auth admin:{{ stat_password }}
2297
2298 {% if frontends -%}
2299 {% for service, ports in service_ports.items() -%}
2300
2301=== modified file 'hooks/charmhelpers/contrib/openstack/templating.py'
2302--- hooks/charmhelpers/contrib/openstack/templating.py 2015-08-27 15:02:34 +0000
2303+++ hooks/charmhelpers/contrib/openstack/templating.py 2016-02-18 14:28:13 +0000
2304@@ -18,7 +18,7 @@
2305
2306 import six
2307
2308-from charmhelpers.fetch import apt_install
2309+from charmhelpers.fetch import apt_install, apt_update
2310 from charmhelpers.core.hookenv import (
2311 log,
2312 ERROR,
2313@@ -29,6 +29,7 @@
2314 try:
2315 from jinja2 import FileSystemLoader, ChoiceLoader, Environment, exceptions
2316 except ImportError:
2317+ apt_update(fatal=True)
2318 apt_install('python-jinja2', fatal=True)
2319 from jinja2 import FileSystemLoader, ChoiceLoader, Environment, exceptions
2320
2321@@ -112,7 +113,7 @@
2322
2323 def complete_contexts(self):
2324 '''
2325- Return a list of interfaces that have atisfied contexts.
2326+ Return a list of interfaces that have satisfied contexts.
2327 '''
2328 if self._complete_contexts:
2329 return self._complete_contexts
2330@@ -293,3 +294,30 @@
2331 [interfaces.extend(i.complete_contexts())
2332 for i in six.itervalues(self.templates)]
2333 return interfaces
2334+
2335+ def get_incomplete_context_data(self, interfaces):
2336+ '''
2337+ Return dictionary of relation status of interfaces and any missing
2338+ required context data. Example:
2339+ {'amqp': {'missing_data': ['rabbitmq_password'], 'related': True},
2340+ 'zeromq-configuration': {'related': False}}
2341+ '''
2342+ incomplete_context_data = {}
2343+
2344+ for i in six.itervalues(self.templates):
2345+ for context in i.contexts:
2346+ for interface in interfaces:
2347+ related = False
2348+ if interface in context.interfaces:
2349+ related = context.get_related()
2350+ missing_data = context.missing_data
2351+ if missing_data:
2352+ incomplete_context_data[interface] = {'missing_data': missing_data}
2353+ if related:
2354+ if incomplete_context_data.get(interface):
2355+ incomplete_context_data[interface].update({'related': True})
2356+ else:
2357+ incomplete_context_data[interface] = {'related': True}
2358+ else:
2359+ incomplete_context_data[interface] = {'related': False}
2360+ return incomplete_context_data
2361
2362=== modified file 'hooks/charmhelpers/contrib/openstack/utils.py'
2363--- hooks/charmhelpers/contrib/openstack/utils.py 2015-09-14 20:23:58 +0000
2364+++ hooks/charmhelpers/contrib/openstack/utils.py 2016-02-18 14:28:13 +0000
2365@@ -26,6 +26,7 @@
2366
2367 import six
2368 import traceback
2369+import uuid
2370 import yaml
2371
2372 from charmhelpers.contrib.network import ip
2373@@ -41,8 +42,11 @@
2374 log as juju_log,
2375 charm_dir,
2376 INFO,
2377+ related_units,
2378 relation_ids,
2379- relation_set
2380+ relation_set,
2381+ status_set,
2382+ hook_name
2383 )
2384
2385 from charmhelpers.contrib.storage.linux.lvm import (
2386@@ -52,7 +56,8 @@
2387 )
2388
2389 from charmhelpers.contrib.network.ip import (
2390- get_ipv6_addr
2391+ get_ipv6_addr,
2392+ is_ipv6,
2393 )
2394
2395 from charmhelpers.contrib.python.packages import (
2396@@ -81,6 +86,7 @@
2397 ('utopic', 'juno'),
2398 ('vivid', 'kilo'),
2399 ('wily', 'liberty'),
2400+ ('xenial', 'mitaka'),
2401 ])
2402
2403
2404@@ -94,6 +100,7 @@
2405 ('2014.2', 'juno'),
2406 ('2015.1', 'kilo'),
2407 ('2015.2', 'liberty'),
2408+ ('2016.1', 'mitaka'),
2409 ])
2410
2411 # The ugly duckling
2412@@ -118,36 +125,46 @@
2413 ('2.2.2', 'kilo'),
2414 ('2.3.0', 'liberty'),
2415 ('2.4.0', 'liberty'),
2416+ ('2.5.0', 'liberty'),
2417 ])
2418
2419 # >= Liberty version->codename mapping
2420 PACKAGE_CODENAMES = {
2421 'nova-common': OrderedDict([
2422- ('12.0.0', 'liberty'),
2423+ ('12.0', 'liberty'),
2424+ ('13.0', 'mitaka'),
2425 ]),
2426 'neutron-common': OrderedDict([
2427- ('7.0.0', 'liberty'),
2428+ ('7.0', 'liberty'),
2429+ ('8.0', 'mitaka'),
2430 ]),
2431 'cinder-common': OrderedDict([
2432- ('7.0.0', 'liberty'),
2433+ ('7.0', 'liberty'),
2434+ ('8.0', 'mitaka'),
2435 ]),
2436 'keystone': OrderedDict([
2437- ('8.0.0', 'liberty'),
2438+ ('8.0', 'liberty'),
2439+ ('9.0', 'mitaka'),
2440 ]),
2441 'horizon-common': OrderedDict([
2442- ('8.0.0', 'liberty'),
2443+ ('8.0', 'liberty'),
2444+ ('9.0', 'mitaka'),
2445 ]),
2446 'ceilometer-common': OrderedDict([
2447- ('5.0.0', 'liberty'),
2448+ ('5.0', 'liberty'),
2449+ ('6.0', 'mitaka'),
2450 ]),
2451 'heat-common': OrderedDict([
2452- ('5.0.0', 'liberty'),
2453+ ('5.0', 'liberty'),
2454+ ('6.0', 'mitaka'),
2455 ]),
2456 'glance-common': OrderedDict([
2457- ('11.0.0', 'liberty'),
2458+ ('11.0', 'liberty'),
2459+ ('12.0', 'mitaka'),
2460 ]),
2461 'openstack-dashboard': OrderedDict([
2462- ('8.0.0', 'liberty'),
2463+ ('8.0', 'liberty'),
2464+ ('9.0', 'mitaka'),
2465 ]),
2466 }
2467
2468@@ -234,7 +251,14 @@
2469 error_out(e)
2470
2471 vers = apt.upstream_version(pkg.current_ver.ver_str)
2472- match = re.match('^(\d+)\.(\d+)\.(\d+)', vers)
2473+ if 'swift' in pkg.name:
2474+ # Fully x.y.z match for swift versions
2475+ match = re.match('^(\d+)\.(\d+)\.(\d+)', vers)
2476+ else:
2477+ # x.y match only for 20XX.X
2478+ # and ignore patch level for other packages
2479+ match = re.match('^(\d+)\.(\d+)', vers)
2480+
2481 if match:
2482 vers = match.group(0)
2483
2484@@ -246,13 +270,8 @@
2485 # < Liberty co-ordinated project versions
2486 try:
2487 if 'swift' in pkg.name:
2488- swift_vers = vers[:5]
2489- if swift_vers not in SWIFT_CODENAMES:
2490- # Deal with 1.10.0 upward
2491- swift_vers = vers[:6]
2492- return SWIFT_CODENAMES[swift_vers]
2493+ return SWIFT_CODENAMES[vers]
2494 else:
2495- vers = vers[:6]
2496 return OPENSTACK_CODENAMES[vers]
2497 except KeyError:
2498 if not fatal:
2499@@ -371,6 +390,9 @@
2500 'liberty': 'trusty-updates/liberty',
2501 'liberty/updates': 'trusty-updates/liberty',
2502 'liberty/proposed': 'trusty-proposed/liberty',
2503+ 'mitaka': 'trusty-updates/mitaka',
2504+ 'mitaka/updates': 'trusty-updates/mitaka',
2505+ 'mitaka/proposed': 'trusty-proposed/mitaka',
2506 }
2507
2508 try:
2509@@ -517,6 +539,12 @@
2510 relation_prefix=None):
2511 hosts = get_ipv6_addr(dynamic_only=False)
2512
2513+ if config('vip'):
2514+ vips = config('vip').split()
2515+ for vip in vips:
2516+ if vip and is_ipv6(vip):
2517+ hosts.append(vip)
2518+
2519 kwargs = {'database': database,
2520 'username': database_user,
2521 'hostname': json.dumps(hosts)}
2522@@ -565,7 +593,7 @@
2523 return yaml.load(projects_yaml)
2524
2525
2526-def git_clone_and_install(projects_yaml, core_project, depth=1):
2527+def git_clone_and_install(projects_yaml, core_project):
2528 """
2529 Clone/install all specified OpenStack repositories.
2530
2531@@ -615,6 +643,9 @@
2532 for p in projects['repositories']:
2533 repo = p['repository']
2534 branch = p['branch']
2535+ depth = '1'
2536+ if 'depth' in p.keys():
2537+ depth = p['depth']
2538 if p['name'] == 'requirements':
2539 repo_dir = _git_clone_and_install_single(repo, branch, depth,
2540 parent_dir, http_proxy,
2541@@ -659,19 +690,13 @@
2542 """
2543 Clone and install a single git repository.
2544 """
2545- dest_dir = os.path.join(parent_dir, os.path.basename(repo))
2546-
2547 if not os.path.exists(parent_dir):
2548 juju_log('Directory already exists at {}. '
2549 'No need to create directory.'.format(parent_dir))
2550 os.mkdir(parent_dir)
2551
2552- if not os.path.exists(dest_dir):
2553- juju_log('Cloning git repo: {}, branch: {}'.format(repo, branch))
2554- repo_dir = install_remote(repo, dest=parent_dir, branch=branch,
2555- depth=depth)
2556- else:
2557- repo_dir = dest_dir
2558+ juju_log('Cloning git repo: {}, branch: {}'.format(repo, branch))
2559+ repo_dir = install_remote(repo, dest=parent_dir, branch=branch, depth=depth)
2560
2561 venv = os.path.join(parent_dir, 'venv')
2562
2563@@ -754,6 +779,178 @@
2564 return None
2565
2566
2567+def os_workload_status(configs, required_interfaces, charm_func=None):
2568+ """
2569+ Decorator to set workload status based on complete contexts
2570+ """
2571+ def wrap(f):
2572+ @wraps(f)
2573+ def wrapped_f(*args, **kwargs):
2574+ # Run the original function first
2575+ f(*args, **kwargs)
2576+ # Set workload status now that contexts have been
2577+ # acted on
2578+ set_os_workload_status(configs, required_interfaces, charm_func)
2579+ return wrapped_f
2580+ return wrap
2581+
2582+
2583+def set_os_workload_status(configs, required_interfaces, charm_func=None):
2584+ """
2585+ Set workload status based on complete contexts.
2586+ status-set missing or incomplete contexts
2587+ and juju-log details of missing required data.
2588+ charm_func is a charm specific function to run checking
2589+ for charm specific requirements such as a VIP setting.
2590+ """
2591+ incomplete_rel_data = incomplete_relation_data(configs, required_interfaces)
2592+ state = 'active'
2593+ missing_relations = []
2594+ incomplete_relations = []
2595+ message = None
2596+ charm_state = None
2597+ charm_message = None
2598+
2599+ for generic_interface in incomplete_rel_data.keys():
2600+ related_interface = None
2601+ missing_data = {}
2602+ # Related or not?
2603+ for interface in incomplete_rel_data[generic_interface]:
2604+ if incomplete_rel_data[generic_interface][interface].get('related'):
2605+ related_interface = interface
2606+ missing_data = incomplete_rel_data[generic_interface][interface].get('missing_data')
2607+ # No relation ID for the generic_interface
2608+ if not related_interface:
2609+ juju_log("{} relation is missing and must be related for "
2610+ "functionality. ".format(generic_interface), 'WARN')
2611+ state = 'blocked'
2612+ if generic_interface not in missing_relations:
2613+ missing_relations.append(generic_interface)
2614+ else:
2615+ # Relation ID exists but no related unit
2616+ if not missing_data:
2617+ # Edge case relation ID exists but departing
2618+ if ('departed' in hook_name() or 'broken' in hook_name()) \
2619+ and related_interface in hook_name():
2620+ state = 'blocked'
2621+ if generic_interface not in missing_relations:
2622+ missing_relations.append(generic_interface)
2623+ juju_log("{} relation's interface, {}, "
2624+ "relationship is departed or broken "
2625+ "and is required for functionality."
2626+ "".format(generic_interface, related_interface), "WARN")
2627+ # Normal case relation ID exists but no related unit
2628+ # (joining)
2629+ else:
2630+ juju_log("{} relations's interface, {}, is related but has "
2631+ "no units in the relation."
2632+ "".format(generic_interface, related_interface), "INFO")
2633+ # Related unit exists and data missing on the relation
2634+ else:
2635+ juju_log("{} relation's interface, {}, is related awaiting "
2636+ "the following data from the relationship: {}. "
2637+ "".format(generic_interface, related_interface,
2638+ ", ".join(missing_data)), "INFO")
2639+ if state != 'blocked':
2640+ state = 'waiting'
2641+ if generic_interface not in incomplete_relations \
2642+ and generic_interface not in missing_relations:
2643+ incomplete_relations.append(generic_interface)
2644+
2645+ if missing_relations:
2646+ message = "Missing relations: {}".format(", ".join(missing_relations))
2647+ if incomplete_relations:
2648+ message += "; incomplete relations: {}" \
2649+ "".format(", ".join(incomplete_relations))
2650+ state = 'blocked'
2651+ elif incomplete_relations:
2652+ message = "Incomplete relations: {}" \
2653+ "".format(", ".join(incomplete_relations))
2654+ state = 'waiting'
2655+
2656+ # Run charm specific checks
2657+ if charm_func:
2658+ charm_state, charm_message = charm_func(configs)
2659+ if charm_state != 'active' and charm_state != 'unknown':
2660+ state = workload_state_compare(state, charm_state)
2661+ if message:
2662+ charm_message = charm_message.replace("Incomplete relations: ",
2663+ "")
2664+ message = "{}, {}".format(message, charm_message)
2665+ else:
2666+ message = charm_message
2667+
2668+ # Set to active if all requirements have been met
2669+ if state == 'active':
2670+ message = "Unit is ready"
2671+ juju_log(message, "INFO")
2672+
2673+ status_set(state, message)
2674+
2675+
2676+def workload_state_compare(current_workload_state, workload_state):
2677+ """ Return highest priority of two states"""
2678+ hierarchy = {'unknown': -1,
2679+ 'active': 0,
2680+ 'maintenance': 1,
2681+ 'waiting': 2,
2682+ 'blocked': 3,
2683+ }
2684+
2685+ if hierarchy.get(workload_state) is None:
2686+ workload_state = 'unknown'
2687+ if hierarchy.get(current_workload_state) is None:
2688+ current_workload_state = 'unknown'
2689+
2690+ # Set workload_state based on hierarchy of statuses
2691+ if hierarchy.get(current_workload_state) > hierarchy.get(workload_state):
2692+ return current_workload_state
2693+ else:
2694+ return workload_state
2695+
2696+
2697+def incomplete_relation_data(configs, required_interfaces):
2698+ """
2699+ Check complete contexts against required_interfaces
2700+ Return dictionary of incomplete relation data.
2701+
2702+ configs is an OSConfigRenderer object with configs registered
2703+
2704+ required_interfaces is a dictionary of required general interfaces
2705+ with dictionary values of possible specific interfaces.
2706+ Example:
2707+ required_interfaces = {'database': ['shared-db', 'pgsql-db']}
2708+
2709+ The interface is said to be satisfied if anyone of the interfaces in the
2710+ list has a complete context.
2711+
2712+ Return dictionary of incomplete or missing required contexts with relation
2713+ status of interfaces and any missing data points. Example:
2714+ {'message':
2715+ {'amqp': {'missing_data': ['rabbitmq_password'], 'related': True},
2716+ 'zeromq-configuration': {'related': False}},
2717+ 'identity':
2718+ {'identity-service': {'related': False}},
2719+ 'database':
2720+ {'pgsql-db': {'related': False},
2721+ 'shared-db': {'related': True}}}
2722+ """
2723+ complete_ctxts = configs.complete_contexts()
2724+ incomplete_relations = []
2725+ for svc_type in required_interfaces.keys():
2726+ # Avoid duplicates
2727+ found_ctxt = False
2728+ for interface in required_interfaces[svc_type]:
2729+ if interface in complete_ctxts:
2730+ found_ctxt = True
2731+ if not found_ctxt:
2732+ incomplete_relations.append(svc_type)
2733+ incomplete_context_data = {}
2734+ for i in incomplete_relations:
2735+ incomplete_context_data[i] = configs.get_incomplete_context_data(required_interfaces[i])
2736+ return incomplete_context_data
2737+
2738+
2739 def do_action_openstack_upgrade(package, upgrade_callback, configs):
2740 """Perform action-managed OpenStack upgrade.
2741
2742@@ -796,3 +993,19 @@
2743 action_set({'outcome': 'no upgrade available.'})
2744
2745 return ret
2746+
2747+
2748+def remote_restart(rel_name, remote_service=None):
2749+ trigger = {
2750+ 'restart-trigger': str(uuid.uuid4()),
2751+ }
2752+ if remote_service:
2753+ trigger['remote-service'] = remote_service
2754+ for rid in relation_ids(rel_name):
2755+ # This subordinate can be related to two seperate services using
2756+ # different subordinate relations so only issue the restart if
2757+ # the principle is conencted down the relation we think it is
2758+ if related_units(relid=rid):
2759+ relation_set(relation_id=rid,
2760+ relation_settings=trigger,
2761+ )
2762
2763=== modified file 'hooks/charmhelpers/contrib/python/packages.py'
2764--- hooks/charmhelpers/contrib/python/packages.py 2015-06-24 19:07:21 +0000
2765+++ hooks/charmhelpers/contrib/python/packages.py 2016-02-18 14:28:13 +0000
2766@@ -42,8 +42,12 @@
2767 yield "--{0}={1}".format(key, value)
2768
2769
2770-def pip_install_requirements(requirements, **options):
2771- """Install a requirements file """
2772+def pip_install_requirements(requirements, constraints=None, **options):
2773+ """Install a requirements file.
2774+
2775+ :param constraints: Path to pip constraints file.
2776+ http://pip.readthedocs.org/en/stable/user_guide/#constraints-files
2777+ """
2778 command = ["install"]
2779
2780 available_options = ('proxy', 'src', 'log', )
2781@@ -51,8 +55,13 @@
2782 command.append(option)
2783
2784 command.append("-r {0}".format(requirements))
2785- log("Installing from file: {} with options: {}".format(requirements,
2786- command))
2787+ if constraints:
2788+ command.append("-c {0}".format(constraints))
2789+ log("Installing from file: {} with constraints {} "
2790+ "and options: {}".format(requirements, constraints, command))
2791+ else:
2792+ log("Installing from file: {} with options: {}".format(requirements,
2793+ command))
2794 pip_execute(command)
2795
2796
2797
2798=== modified file 'hooks/charmhelpers/contrib/storage/linux/ceph.py'
2799--- hooks/charmhelpers/contrib/storage/linux/ceph.py 2015-07-17 13:24:05 +0000
2800+++ hooks/charmhelpers/contrib/storage/linux/ceph.py 2016-02-18 14:28:13 +0000
2801@@ -23,11 +23,14 @@
2802 # James Page <james.page@ubuntu.com>
2803 # Adam Gandelman <adamg@ubuntu.com>
2804 #
2805+import bisect
2806+import six
2807
2808 import os
2809 import shutil
2810 import json
2811 import time
2812+import uuid
2813
2814 from subprocess import (
2815 check_call,
2816@@ -35,8 +38,10 @@
2817 CalledProcessError,
2818 )
2819 from charmhelpers.core.hookenv import (
2820+ local_unit,
2821 relation_get,
2822 relation_ids,
2823+ relation_set,
2824 related_units,
2825 log,
2826 DEBUG,
2827@@ -56,6 +61,8 @@
2828 apt_install,
2829 )
2830
2831+from charmhelpers.core.kernel import modprobe
2832+
2833 KEYRING = '/etc/ceph/ceph.client.{}.keyring'
2834 KEYFILE = '/etc/ceph/ceph.client.{}.key'
2835
2836@@ -67,6 +74,394 @@
2837 err to syslog = {use_syslog}
2838 clog to syslog = {use_syslog}
2839 """
2840+# For 50 < osds < 240,000 OSDs (Roughly 1 Exabyte at 6T OSDs)
2841+powers_of_two = [8192, 16384, 32768, 65536, 131072, 262144, 524288, 1048576, 2097152, 4194304, 8388608]
2842+
2843+
2844+def validator(value, valid_type, valid_range=None):
2845+ """
2846+ Used to validate these: http://docs.ceph.com/docs/master/rados/operations/pools/#set-pool-values
2847+ Example input:
2848+ validator(value=1,
2849+ valid_type=int,
2850+ valid_range=[0, 2])
2851+ This says I'm testing value=1. It must be an int inclusive in [0,2]
2852+
2853+ :param value: The value to validate
2854+ :param valid_type: The type that value should be.
2855+ :param valid_range: A range of values that value can assume.
2856+ :return:
2857+ """
2858+ assert isinstance(value, valid_type), "{} is not a {}".format(
2859+ value,
2860+ valid_type)
2861+ if valid_range is not None:
2862+ assert isinstance(valid_range, list), \
2863+ "valid_range must be a list, was given {}".format(valid_range)
2864+ # If we're dealing with strings
2865+ if valid_type is six.string_types:
2866+ assert value in valid_range, \
2867+ "{} is not in the list {}".format(value, valid_range)
2868+ # Integer, float should have a min and max
2869+ else:
2870+ if len(valid_range) != 2:
2871+ raise ValueError(
2872+ "Invalid valid_range list of {} for {}. "
2873+ "List must be [min,max]".format(valid_range, value))
2874+ assert value >= valid_range[0], \
2875+ "{} is less than minimum allowed value of {}".format(
2876+ value, valid_range[0])
2877+ assert value <= valid_range[1], \
2878+ "{} is greater than maximum allowed value of {}".format(
2879+ value, valid_range[1])
2880+
2881+
2882+class PoolCreationError(Exception):
2883+ """
2884+ A custom error to inform the caller that a pool creation failed. Provides an error message
2885+ """
2886+ def __init__(self, message):
2887+ super(PoolCreationError, self).__init__(message)
2888+
2889+
2890+class Pool(object):
2891+ """
2892+ An object oriented approach to Ceph pool creation. This base class is inherited by ReplicatedPool and ErasurePool.
2893+ Do not call create() on this base class as it will not do anything. Instantiate a child class and call create().
2894+ """
2895+ def __init__(self, service, name):
2896+ self.service = service
2897+ self.name = name
2898+
2899+ # Create the pool if it doesn't exist already
2900+ # To be implemented by subclasses
2901+ def create(self):
2902+ pass
2903+
2904+ def add_cache_tier(self, cache_pool, mode):
2905+ """
2906+ Adds a new cache tier to an existing pool.
2907+ :param cache_pool: six.string_types. The cache tier pool name to add.
2908+ :param mode: six.string_types. The caching mode to use for this pool. valid range = ["readonly", "writeback"]
2909+ :return: None
2910+ """
2911+ # Check the input types and values
2912+ validator(value=cache_pool, valid_type=six.string_types)
2913+ validator(value=mode, valid_type=six.string_types, valid_range=["readonly", "writeback"])
2914+
2915+ check_call(['ceph', '--id', self.service, 'osd', 'tier', 'add', self.name, cache_pool])
2916+ check_call(['ceph', '--id', self.service, 'osd', 'tier', 'cache-mode', cache_pool, mode])
2917+ check_call(['ceph', '--id', self.service, 'osd', 'tier', 'set-overlay', self.name, cache_pool])
2918+ check_call(['ceph', '--id', self.service, 'osd', 'pool', 'set', cache_pool, 'hit_set_type', 'bloom'])
2919+
2920+ def remove_cache_tier(self, cache_pool):
2921+ """
2922+ Removes a cache tier from Ceph. Flushes all dirty objects from writeback pools and waits for that to complete.
2923+ :param cache_pool: six.string_types. The cache tier pool name to remove.
2924+ :return: None
2925+ """
2926+ # read-only is easy, writeback is much harder
2927+ mode = get_cache_mode(cache_pool)
2928+ if mode == 'readonly':
2929+ check_call(['ceph', '--id', self.service, 'osd', 'tier', 'cache-mode', cache_pool, 'none'])
2930+ check_call(['ceph', '--id', self.service, 'osd', 'tier', 'remove', self.name, cache_pool])
2931+
2932+ elif mode == 'writeback':
2933+ check_call(['ceph', '--id', self.service, 'osd', 'tier', 'cache-mode', cache_pool, 'forward'])
2934+ # Flush the cache and wait for it to return
2935+ check_call(['ceph', '--id', self.service, '-p', cache_pool, 'cache-flush-evict-all'])
2936+ check_call(['ceph', '--id', self.service, 'osd', 'tier', 'remove-overlay', self.name])
2937+ check_call(['ceph', '--id', self.service, 'osd', 'tier', 'remove', self.name, cache_pool])
2938+
2939+ def get_pgs(self, pool_size):
2940+ """
2941+ :param pool_size: int. pool_size is either the number of replicas for replicated pools or the K+M sum for
2942+ erasure coded pools
2943+ :return: int. The number of pgs to use.
2944+ """
2945+ validator(value=pool_size, valid_type=int)
2946+ osds = get_osds(self.service)
2947+ if not osds:
2948+ # NOTE(james-page): Default to 200 for older ceph versions
2949+ # which don't support OSD query from cli
2950+ return 200
2951+
2952+ # Calculate based on Ceph best practices
2953+ if osds < 5:
2954+ return 128
2955+ elif 5 < osds < 10:
2956+ return 512
2957+ elif 10 < osds < 50:
2958+ return 4096
2959+ else:
2960+ estimate = (osds * 100) / pool_size
2961+ # Return the next nearest power of 2
2962+ index = bisect.bisect_right(powers_of_two, estimate)
2963+ return powers_of_two[index]
2964+
2965+
2966+class ReplicatedPool(Pool):
2967+ def __init__(self, service, name, replicas=2):
2968+ super(ReplicatedPool, self).__init__(service=service, name=name)
2969+ self.replicas = replicas
2970+
2971+ def create(self):
2972+ if not pool_exists(self.service, self.name):
2973+ # Create it
2974+ pgs = self.get_pgs(self.replicas)
2975+ cmd = ['ceph', '--id', self.service, 'osd', 'pool', 'create', self.name, str(pgs)]
2976+ try:
2977+ check_call(cmd)
2978+ except CalledProcessError:
2979+ raise
2980+
2981+
2982+# Default jerasure erasure coded pool
2983+class ErasurePool(Pool):
2984+ def __init__(self, service, name, erasure_code_profile="default"):
2985+ super(ErasurePool, self).__init__(service=service, name=name)
2986+ self.erasure_code_profile = erasure_code_profile
2987+
2988+ def create(self):
2989+ if not pool_exists(self.service, self.name):
2990+ # Try to find the erasure profile information so we can properly size the pgs
2991+ erasure_profile = get_erasure_profile(service=self.service, name=self.erasure_code_profile)
2992+
2993+ # Check for errors
2994+ if erasure_profile is None:
2995+ log(message='Failed to discover erasure_profile named={}'.format(self.erasure_code_profile),
2996+ level=ERROR)
2997+ raise PoolCreationError(message='unable to find erasure profile {}'.format(self.erasure_code_profile))
2998+ if 'k' not in erasure_profile or 'm' not in erasure_profile:
2999+ # Error
3000+ log(message='Unable to find k (data chunks) or m (coding chunks) in {}'.format(erasure_profile),
3001+ level=ERROR)
3002+ raise PoolCreationError(
3003+ message='unable to find k (data chunks) or m (coding chunks) in {}'.format(erasure_profile))
3004+
3005+ pgs = self.get_pgs(int(erasure_profile['k']) + int(erasure_profile['m']))
3006+ # Create it
3007+ cmd = ['ceph', '--id', self.service, 'osd', 'pool', 'create', self.name, str(pgs),
3008+ 'erasure', self.erasure_code_profile]
3009+ try:
3010+ check_call(cmd)
3011+ except CalledProcessError:
3012+ raise
3013+
3014+ """Get an existing erasure code profile if it already exists.
3015+ Returns json formatted output"""
3016+
3017+
3018+def get_erasure_profile(service, name):
3019+ """
3020+ :param service: six.string_types. The Ceph user name to run the command under
3021+ :param name:
3022+ :return:
3023+ """
3024+ try:
3025+ out = check_output(['ceph', '--id', service,
3026+ 'osd', 'erasure-code-profile', 'get',
3027+ name, '--format=json'])
3028+ return json.loads(out)
3029+ except (CalledProcessError, OSError, ValueError):
3030+ return None
3031+
3032+
3033+def pool_set(service, pool_name, key, value):
3034+ """
3035+ Sets a value for a RADOS pool in ceph.
3036+ :param service: six.string_types. The Ceph user name to run the command under
3037+ :param pool_name: six.string_types
3038+ :param key: six.string_types
3039+ :param value:
3040+ :return: None. Can raise CalledProcessError
3041+ """
3042+ cmd = ['ceph', '--id', service, 'osd', 'pool', 'set', pool_name, key, value]
3043+ try:
3044+ check_call(cmd)
3045+ except CalledProcessError:
3046+ raise
3047+
3048+
3049+def snapshot_pool(service, pool_name, snapshot_name):
3050+ """
3051+ Snapshots a RADOS pool in ceph.
3052+ :param service: six.string_types. The Ceph user name to run the command under
3053+ :param pool_name: six.string_types
3054+ :param snapshot_name: six.string_types
3055+ :return: None. Can raise CalledProcessError
3056+ """
3057+ cmd = ['ceph', '--id', service, 'osd', 'pool', 'mksnap', pool_name, snapshot_name]
3058+ try:
3059+ check_call(cmd)
3060+ except CalledProcessError:
3061+ raise
3062+
3063+
3064+def remove_pool_snapshot(service, pool_name, snapshot_name):
3065+ """
3066+ Remove a snapshot from a RADOS pool in ceph.
3067+ :param service: six.string_types. The Ceph user name to run the command under
3068+ :param pool_name: six.string_types
3069+ :param snapshot_name: six.string_types
3070+ :return: None. Can raise CalledProcessError
3071+ """
3072+ cmd = ['ceph', '--id', service, 'osd', 'pool', 'rmsnap', pool_name, snapshot_name]
3073+ try:
3074+ check_call(cmd)
3075+ except CalledProcessError:
3076+ raise
3077+
3078+
3079+# max_bytes should be an int or long
3080+def set_pool_quota(service, pool_name, max_bytes):
3081+ """
3082+ :param service: six.string_types. The Ceph user name to run the command under
3083+ :param pool_name: six.string_types
3084+ :param max_bytes: int or long
3085+ :return: None. Can raise CalledProcessError
3086+ """
3087+ # Set a byte quota on a RADOS pool in ceph.
3088+ cmd = ['ceph', '--id', service, 'osd', 'pool', 'set-quota', pool_name, 'max_bytes', max_bytes]
3089+ try:
3090+ check_call(cmd)
3091+ except CalledProcessError:
3092+ raise
3093+
3094+
3095+def remove_pool_quota(service, pool_name):
3096+ """
3097+ Set a byte quota on a RADOS pool in ceph.
3098+ :param service: six.string_types. The Ceph user name to run the command under
3099+ :param pool_name: six.string_types
3100+ :return: None. Can raise CalledProcessError
3101+ """
3102+ cmd = ['ceph', '--id', service, 'osd', 'pool', 'set-quota', pool_name, 'max_bytes', '0']
3103+ try:
3104+ check_call(cmd)
3105+ except CalledProcessError:
3106+ raise
3107+
3108+
3109+def create_erasure_profile(service, profile_name, erasure_plugin_name='jerasure', failure_domain='host',
3110+ data_chunks=2, coding_chunks=1,
3111+ locality=None, durability_estimator=None):
3112+ """
3113+ Create a new erasure code profile if one does not already exist for it. Updates
3114+ the profile if it exists. Please see http://docs.ceph.com/docs/master/rados/operations/erasure-code-profile/
3115+ for more details
3116+ :param service: six.string_types. The Ceph user name to run the command under
3117+ :param profile_name: six.string_types
3118+ :param erasure_plugin_name: six.string_types
3119+ :param failure_domain: six.string_types. One of ['chassis', 'datacenter', 'host', 'osd', 'pdu', 'pod', 'rack', 'region',
3120+ 'room', 'root', 'row'])
3121+ :param data_chunks: int
3122+ :param coding_chunks: int
3123+ :param locality: int
3124+ :param durability_estimator: int
3125+ :return: None. Can raise CalledProcessError
3126+ """
3127+ # Ensure this failure_domain is allowed by Ceph
3128+ validator(failure_domain, six.string_types,
3129+ ['chassis', 'datacenter', 'host', 'osd', 'pdu', 'pod', 'rack', 'region', 'room', 'root', 'row'])
3130+
3131+ cmd = ['ceph', '--id', service, 'osd', 'erasure-code-profile', 'set', profile_name,
3132+ 'plugin=' + erasure_plugin_name, 'k=' + str(data_chunks), 'm=' + str(coding_chunks),
3133+ 'ruleset_failure_domain=' + failure_domain]
3134+ if locality is not None and durability_estimator is not None:
3135+ raise ValueError("create_erasure_profile should be called with k, m and one of l or c but not both.")
3136+
3137+ # Add plugin specific information
3138+ if locality is not None:
3139+ # For local erasure codes
3140+ cmd.append('l=' + str(locality))
3141+ if durability_estimator is not None:
3142+ # For Shec erasure codes
3143+ cmd.append('c=' + str(durability_estimator))
3144+
3145+ if erasure_profile_exists(service, profile_name):
3146+ cmd.append('--force')
3147+
3148+ try:
3149+ check_call(cmd)
3150+ except CalledProcessError:
3151+ raise
3152+
3153+
3154+def rename_pool(service, old_name, new_name):
3155+ """
3156+ Rename a Ceph pool from old_name to new_name
3157+ :param service: six.string_types. The Ceph user name to run the command under
3158+ :param old_name: six.string_types
3159+ :param new_name: six.string_types
3160+ :return: None
3161+ """
3162+ validator(value=old_name, valid_type=six.string_types)
3163+ validator(value=new_name, valid_type=six.string_types)
3164+
3165+ cmd = ['ceph', '--id', service, 'osd', 'pool', 'rename', old_name, new_name]
3166+ check_call(cmd)
3167+
3168+
3169+def erasure_profile_exists(service, name):
3170+ """
3171+ Check to see if an Erasure code profile already exists.
3172+ :param service: six.string_types. The Ceph user name to run the command under
3173+ :param name: six.string_types
3174+ :return: int or None
3175+ """
3176+ validator(value=name, valid_type=six.string_types)
3177+ try:
3178+ check_call(['ceph', '--id', service,
3179+ 'osd', 'erasure-code-profile', 'get',
3180+ name])
3181+ return True
3182+ except CalledProcessError:
3183+ return False
3184+
3185+
3186+def get_cache_mode(service, pool_name):
3187+ """
3188+ Find the current caching mode of the pool_name given.
3189+ :param service: six.string_types. The Ceph user name to run the command under
3190+ :param pool_name: six.string_types
3191+ :return: int or None
3192+ """
3193+ validator(value=service, valid_type=six.string_types)
3194+ validator(value=pool_name, valid_type=six.string_types)
3195+ out = check_output(['ceph', '--id', service, 'osd', 'dump', '--format=json'])
3196+ try:
3197+ osd_json = json.loads(out)
3198+ for pool in osd_json['pools']:
3199+ if pool['pool_name'] == pool_name:
3200+ return pool['cache_mode']
3201+ return None
3202+ except ValueError:
3203+ raise
3204+
3205+
3206+def pool_exists(service, name):
3207+ """Check to see if a RADOS pool already exists."""
3208+ try:
3209+ out = check_output(['rados', '--id', service,
3210+ 'lspools']).decode('UTF-8')
3211+ except CalledProcessError:
3212+ return False
3213+
3214+ return name in out
3215+
3216+
3217+def get_osds(service):
3218+ """Return a list of all Ceph Object Storage Daemons currently in the
3219+ cluster.
3220+ """
3221+ version = ceph_version()
3222+ if version and version >= '0.56':
3223+ return json.loads(check_output(['ceph', '--id', service,
3224+ 'osd', 'ls',
3225+ '--format=json']).decode('UTF-8'))
3226+
3227+ return None
3228
3229
3230 def install():
3231@@ -96,53 +491,37 @@
3232 check_call(cmd)
3233
3234
3235-def pool_exists(service, name):
3236- """Check to see if a RADOS pool already exists."""
3237- try:
3238- out = check_output(['rados', '--id', service,
3239- 'lspools']).decode('UTF-8')
3240- except CalledProcessError:
3241- return False
3242-
3243- return name in out
3244-
3245-
3246-def get_osds(service):
3247- """Return a list of all Ceph Object Storage Daemons currently in the
3248- cluster.
3249- """
3250- version = ceph_version()
3251- if version and version >= '0.56':
3252- return json.loads(check_output(['ceph', '--id', service,
3253- 'osd', 'ls',
3254- '--format=json']).decode('UTF-8'))
3255-
3256- return None
3257-
3258-
3259-def create_pool(service, name, replicas=3):
3260+def update_pool(client, pool, settings):
3261+ cmd = ['ceph', '--id', client, 'osd', 'pool', 'set', pool]
3262+ for k, v in six.iteritems(settings):
3263+ cmd.append(k)
3264+ cmd.append(v)
3265+
3266+ check_call(cmd)
3267+
3268+
3269+def create_pool(service, name, replicas=3, pg_num=None):
3270 """Create a new RADOS pool."""
3271 if pool_exists(service, name):
3272 log("Ceph pool {} already exists, skipping creation".format(name),
3273 level=WARNING)
3274 return
3275
3276- # Calculate the number of placement groups based
3277- # on upstream recommended best practices.
3278- osds = get_osds(service)
3279- if osds:
3280- pgnum = (len(osds) * 100 // replicas)
3281- else:
3282- # NOTE(james-page): Default to 200 for older ceph versions
3283- # which don't support OSD query from cli
3284- pgnum = 200
3285-
3286- cmd = ['ceph', '--id', service, 'osd', 'pool', 'create', name, str(pgnum)]
3287- check_call(cmd)
3288-
3289- cmd = ['ceph', '--id', service, 'osd', 'pool', 'set', name, 'size',
3290- str(replicas)]
3291- check_call(cmd)
3292+ if not pg_num:
3293+ # Calculate the number of placement groups based
3294+ # on upstream recommended best practices.
3295+ osds = get_osds(service)
3296+ if osds:
3297+ pg_num = (len(osds) * 100 // replicas)
3298+ else:
3299+ # NOTE(james-page): Default to 200 for older ceph versions
3300+ # which don't support OSD query from cli
3301+ pg_num = 200
3302+
3303+ cmd = ['ceph', '--id', service, 'osd', 'pool', 'create', name, str(pg_num)]
3304+ check_call(cmd)
3305+
3306+ update_pool(service, name, settings={'size': str(replicas)})
3307
3308
3309 def delete_pool(service, name):
3310@@ -197,10 +576,10 @@
3311 log('Created new keyfile at %s.' % keyfile, level=INFO)
3312
3313
3314-def get_ceph_nodes():
3315- """Query named relation 'ceph' to determine current nodes."""
3316+def get_ceph_nodes(relation='ceph'):
3317+ """Query named relation to determine current nodes."""
3318 hosts = []
3319- for r_id in relation_ids('ceph'):
3320+ for r_id in relation_ids(relation):
3321 for unit in related_units(r_id):
3322 hosts.append(relation_get('private-address', unit=unit, rid=r_id))
3323
3324@@ -288,17 +667,6 @@
3325 os.chown(data_src_dst, uid, gid)
3326
3327
3328-# TODO: re-use
3329-def modprobe(module):
3330- """Load a kernel module and configure for auto-load on reboot."""
3331- log('Loading kernel module', level=INFO)
3332- cmd = ['modprobe', module]
3333- check_call(cmd)
3334- with open('/etc/modules', 'r+') as modules:
3335- if module not in modules.read():
3336- modules.write(module)
3337-
3338-
3339 def copy_files(src, dst, symlinks=False, ignore=None):
3340 """Copy files from src to dst."""
3341 for item in os.listdir(src):
3342@@ -363,14 +731,14 @@
3343 service_start(svc)
3344
3345
3346-def ensure_ceph_keyring(service, user=None, group=None):
3347+def ensure_ceph_keyring(service, user=None, group=None, relation='ceph'):
3348 """Ensures a ceph keyring is created for a named service and optionally
3349 ensures user and group ownership.
3350
3351 Returns False if no ceph key is available in relation state.
3352 """
3353 key = None
3354- for rid in relation_ids('ceph'):
3355+ for rid in relation_ids(relation):
3356 for unit in related_units(rid):
3357 key = relation_get('key', rid=rid, unit=unit)
3358 if key:
3359@@ -411,17 +779,60 @@
3360
3361 The API is versioned and defaults to version 1.
3362 """
3363- def __init__(self, api_version=1):
3364+
3365+ def __init__(self, api_version=1, request_id=None):
3366 self.api_version = api_version
3367+ if request_id:
3368+ self.request_id = request_id
3369+ else:
3370+ self.request_id = str(uuid.uuid1())
3371 self.ops = []
3372
3373- def add_op_create_pool(self, name, replica_count=3):
3374+ def add_op_create_pool(self, name, replica_count=3, pg_num=None):
3375+ """Adds an operation to create a pool.
3376+
3377+ @param pg_num setting: optional setting. If not provided, this value
3378+ will be calculated by the broker based on how many OSDs are in the
3379+ cluster at the time of creation. Note that, if provided, this value
3380+ will be capped at the current available maximum.
3381+ """
3382 self.ops.append({'op': 'create-pool', 'name': name,
3383- 'replicas': replica_count})
3384+ 'replicas': replica_count, 'pg_num': pg_num})
3385+
3386+ def set_ops(self, ops):
3387+ """Set request ops to provided value.
3388+
3389+ Useful for injecting ops that come from a previous request
3390+ to allow comparisons to ensure validity.
3391+ """
3392+ self.ops = ops
3393
3394 @property
3395 def request(self):
3396- return json.dumps({'api-version': self.api_version, 'ops': self.ops})
3397+ return json.dumps({'api-version': self.api_version, 'ops': self.ops,
3398+ 'request-id': self.request_id})
3399+
3400+ def _ops_equal(self, other):
3401+ if len(self.ops) == len(other.ops):
3402+ for req_no in range(0, len(self.ops)):
3403+ for key in ['replicas', 'name', 'op', 'pg_num']:
3404+ if self.ops[req_no].get(key) != other.ops[req_no].get(key):
3405+ return False
3406+ else:
3407+ return False
3408+ return True
3409+
3410+ def __eq__(self, other):
3411+ if not isinstance(other, self.__class__):
3412+ return False
3413+ if self.api_version == other.api_version and \
3414+ self._ops_equal(other):
3415+ return True
3416+ else:
3417+ return False
3418+
3419+ def __ne__(self, other):
3420+ return not self.__eq__(other)
3421
3422
3423 class CephBrokerRsp(object):
3424@@ -431,14 +842,198 @@
3425
3426 The API is versioned and defaults to version 1.
3427 """
3428+
3429 def __init__(self, encoded_rsp):
3430 self.api_version = None
3431 self.rsp = json.loads(encoded_rsp)
3432
3433 @property
3434+ def request_id(self):
3435+ return self.rsp.get('request-id')
3436+
3437+ @property
3438 def exit_code(self):
3439 return self.rsp.get('exit-code')
3440
3441 @property
3442 def exit_msg(self):
3443 return self.rsp.get('stderr')
3444+
3445+
3446+# Ceph Broker Conversation:
3447+# If a charm needs an action to be taken by ceph it can create a CephBrokerRq
3448+# and send that request to ceph via the ceph relation. The CephBrokerRq has a
3449+# unique id so that the client can identity which CephBrokerRsp is associated
3450+# with the request. Ceph will also respond to each client unit individually
3451+# creating a response key per client unit eg glance/0 will get a CephBrokerRsp
3452+# via key broker-rsp-glance-0
3453+#
3454+# To use this the charm can just do something like:
3455+#
3456+# from charmhelpers.contrib.storage.linux.ceph import (
3457+# send_request_if_needed,
3458+# is_request_complete,
3459+# CephBrokerRq,
3460+# )
3461+#
3462+# @hooks.hook('ceph-relation-changed')
3463+# def ceph_changed():
3464+# rq = CephBrokerRq()
3465+# rq.add_op_create_pool(name='poolname', replica_count=3)
3466+#
3467+# if is_request_complete(rq):
3468+# <Request complete actions>
3469+# else:
3470+# send_request_if_needed(get_ceph_request())
3471+#
3472+# CephBrokerRq and CephBrokerRsp are serialized into JSON. Below is an example
3473+# of glance having sent a request to ceph which ceph has successfully processed
3474+# 'ceph:8': {
3475+# 'ceph/0': {
3476+# 'auth': 'cephx',
3477+# 'broker-rsp-glance-0': '{"request-id": "0bc7dc54", "exit-code": 0}',
3478+# 'broker_rsp': '{"request-id": "0da543b8", "exit-code": 0}',
3479+# 'ceph-public-address': '10.5.44.103',
3480+# 'key': 'AQCLDttVuHXINhAAvI144CB09dYchhHyTUY9BQ==',
3481+# 'private-address': '10.5.44.103',
3482+# },
3483+# 'glance/0': {
3484+# 'broker_req': ('{"api-version": 1, "request-id": "0bc7dc54", '
3485+# '"ops": [{"replicas": 3, "name": "glance", '
3486+# '"op": "create-pool"}]}'),
3487+# 'private-address': '10.5.44.109',
3488+# },
3489+# }
3490+
3491+def get_previous_request(rid):
3492+ """Return the last ceph broker request sent on a given relation
3493+
3494+ @param rid: Relation id to query for request
3495+ """
3496+ request = None
3497+ broker_req = relation_get(attribute='broker_req', rid=rid,
3498+ unit=local_unit())
3499+ if broker_req:
3500+ request_data = json.loads(broker_req)
3501+ request = CephBrokerRq(api_version=request_data['api-version'],
3502+ request_id=request_data['request-id'])
3503+ request.set_ops(request_data['ops'])
3504+
3505+ return request
3506+
3507+
3508+def get_request_states(request, relation='ceph'):
3509+ """Return a dict of requests per relation id with their corresponding
3510+ completion state.
3511+
3512+ This allows a charm, which has a request for ceph, to see whether there is
3513+ an equivalent request already being processed and if so what state that
3514+ request is in.
3515+
3516+ @param request: A CephBrokerRq object
3517+ """
3518+ complete = []
3519+ requests = {}
3520+ for rid in relation_ids(relation):
3521+ complete = False
3522+ previous_request = get_previous_request(rid)
3523+ if request == previous_request:
3524+ sent = True
3525+ complete = is_request_complete_for_rid(previous_request, rid)
3526+ else:
3527+ sent = False
3528+ complete = False
3529+
3530+ requests[rid] = {
3531+ 'sent': sent,
3532+ 'complete': complete,
3533+ }
3534+
3535+ return requests
3536+
3537+
3538+def is_request_sent(request, relation='ceph'):
3539+ """Check to see if a functionally equivalent request has already been sent
3540+
3541+ Returns True if a similair request has been sent
3542+
3543+ @param request: A CephBrokerRq object
3544+ """
3545+ states = get_request_states(request, relation=relation)
3546+ for rid in states.keys():
3547+ if not states[rid]['sent']:
3548+ return False
3549+
3550+ return True
3551+
3552+
3553+def is_request_complete(request, relation='ceph'):
3554+ """Check to see if a functionally equivalent request has already been
3555+ completed
3556+
3557+ Returns True if a similair request has been completed
3558+
3559+ @param request: A CephBrokerRq object
3560+ """
3561+ states = get_request_states(request, relation=relation)
3562+ for rid in states.keys():
3563+ if not states[rid]['complete']:
3564+ return False
3565+
3566+ return True
3567+
3568+
3569+def is_request_complete_for_rid(request, rid):
3570+ """Check if a given request has been completed on the given relation
3571+
3572+ @param request: A CephBrokerRq object
3573+ @param rid: Relation ID
3574+ """
3575+ broker_key = get_broker_rsp_key()
3576+ for unit in related_units(rid):
3577+ rdata = relation_get(rid=rid, unit=unit)
3578+ if rdata.get(broker_key):
3579+ rsp = CephBrokerRsp(rdata.get(broker_key))
3580+ if rsp.request_id == request.request_id:
3581+ if not rsp.exit_code:
3582+ return True
3583+ else:
3584+ # The remote unit sent no reply targeted at this unit so either the
3585+ # remote ceph cluster does not support unit targeted replies or it
3586+ # has not processed our request yet.
3587+ if rdata.get('broker_rsp'):
3588+ request_data = json.loads(rdata['broker_rsp'])
3589+ if request_data.get('request-id'):
3590+ log('Ignoring legacy broker_rsp without unit key as remote '
3591+ 'service supports unit specific replies', level=DEBUG)
3592+ else:
3593+ log('Using legacy broker_rsp as remote service does not '
3594+ 'supports unit specific replies', level=DEBUG)
3595+ rsp = CephBrokerRsp(rdata['broker_rsp'])
3596+ if not rsp.exit_code:
3597+ return True
3598+
3599+ return False
3600+
3601+
3602+def get_broker_rsp_key():
3603+ """Return broker response key for this unit
3604+
3605+ This is the key that ceph is going to use to pass request status
3606+ information back to this unit
3607+ """
3608+ return 'broker-rsp-' + local_unit().replace('/', '-')
3609+
3610+
3611+def send_request_if_needed(request, relation='ceph'):
3612+ """Send broker request if an equivalent request has not already been sent
3613+
3614+ @param request: A CephBrokerRq object
3615+ """
3616+ if is_request_sent(request, relation=relation):
3617+ log('Request already sent but not complete, not sending new request',
3618+ level=DEBUG)
3619+ else:
3620+ for rid in relation_ids(relation):
3621+ log('Sending request {}'.format(request.request_id), level=DEBUG)
3622+ relation_set(relation_id=rid, broker_req=request.request)
3623
3624=== modified file 'hooks/charmhelpers/contrib/storage/linux/loopback.py'
3625--- hooks/charmhelpers/contrib/storage/linux/loopback.py 2015-01-26 09:46:38 +0000
3626+++ hooks/charmhelpers/contrib/storage/linux/loopback.py 2016-02-18 14:28:13 +0000
3627@@ -76,3 +76,13 @@
3628 check_call(cmd)
3629
3630 return create_loopback(path)
3631+
3632+
3633+def is_mapped_loopback_device(device):
3634+ """
3635+ Checks if a given device name is an existing/mapped loopback device.
3636+ :param device: str: Full path to the device (eg, /dev/loop1).
3637+ :returns: str: Path to the backing file if is a loopback device
3638+ empty string otherwise
3639+ """
3640+ return loopback_devices().get(device, "")
3641
3642=== added file 'hooks/charmhelpers/core/files.py'
3643--- hooks/charmhelpers/core/files.py 1970-01-01 00:00:00 +0000
3644+++ hooks/charmhelpers/core/files.py 2016-02-18 14:28:13 +0000
3645@@ -0,0 +1,45 @@
3646+#!/usr/bin/env python
3647+# -*- coding: utf-8 -*-
3648+
3649+# Copyright 2014-2015 Canonical Limited.
3650+#
3651+# This file is part of charm-helpers.
3652+#
3653+# charm-helpers is free software: you can redistribute it and/or modify
3654+# it under the terms of the GNU Lesser General Public License version 3 as
3655+# published by the Free Software Foundation.
3656+#
3657+# charm-helpers is distributed in the hope that it will be useful,
3658+# but WITHOUT ANY WARRANTY; without even the implied warranty of
3659+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
3660+# GNU Lesser General Public License for more details.
3661+#
3662+# You should have received a copy of the GNU Lesser General Public License
3663+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
3664+
3665+__author__ = 'Jorge Niedbalski <niedbalski@ubuntu.com>'
3666+
3667+import os
3668+import subprocess
3669+
3670+
3671+def sed(filename, before, after, flags='g'):
3672+ """
3673+ Search and replaces the given pattern on filename.
3674+
3675+ :param filename: relative or absolute file path.
3676+ :param before: expression to be replaced (see 'man sed')
3677+ :param after: expression to replace with (see 'man sed')
3678+ :param flags: sed-compatible regex flags in example, to make
3679+ the search and replace case insensitive, specify ``flags="i"``.
3680+ The ``g`` flag is always specified regardless, so you do not
3681+ need to remember to include it when overriding this parameter.
3682+ :returns: If the sed command exit code was zero then return,
3683+ otherwise raise CalledProcessError.
3684+ """
3685+ expression = r's/{0}/{1}/{2}'.format(before,
3686+ after, flags)
3687+
3688+ return subprocess.check_call(["sed", "-i", "-r", "-e",
3689+ expression,
3690+ os.path.expanduser(filename)])
3691
3692=== removed file 'hooks/charmhelpers/core/files.py'
3693--- hooks/charmhelpers/core/files.py 2015-07-29 10:48:39 +0000
3694+++ hooks/charmhelpers/core/files.py 1970-01-01 00:00:00 +0000
3695@@ -1,45 +0,0 @@
3696-#!/usr/bin/env python
3697-# -*- coding: utf-8 -*-
3698-
3699-# Copyright 2014-2015 Canonical Limited.
3700-#
3701-# This file is part of charm-helpers.
3702-#
3703-# charm-helpers is free software: you can redistribute it and/or modify
3704-# it under the terms of the GNU Lesser General Public License version 3 as
3705-# published by the Free Software Foundation.
3706-#
3707-# charm-helpers is distributed in the hope that it will be useful,
3708-# but WITHOUT ANY WARRANTY; without even the implied warranty of
3709-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
3710-# GNU Lesser General Public License for more details.
3711-#
3712-# You should have received a copy of the GNU Lesser General Public License
3713-# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
3714-
3715-__author__ = 'Jorge Niedbalski <niedbalski@ubuntu.com>'
3716-
3717-import os
3718-import subprocess
3719-
3720-
3721-def sed(filename, before, after, flags='g'):
3722- """
3723- Search and replaces the given pattern on filename.
3724-
3725- :param filename: relative or absolute file path.
3726- :param before: expression to be replaced (see 'man sed')
3727- :param after: expression to replace with (see 'man sed')
3728- :param flags: sed-compatible regex flags in example, to make
3729- the search and replace case insensitive, specify ``flags="i"``.
3730- The ``g`` flag is always specified regardless, so you do not
3731- need to remember to include it when overriding this parameter.
3732- :returns: If the sed command exit code was zero then return,
3733- otherwise raise CalledProcessError.
3734- """
3735- expression = r's/{0}/{1}/{2}'.format(before,
3736- after, flags)
3737-
3738- return subprocess.check_call(["sed", "-i", "-r", "-e",
3739- expression,
3740- os.path.expanduser(filename)])
3741
3742=== modified file 'hooks/charmhelpers/core/hookenv.py'
3743--- hooks/charmhelpers/core/hookenv.py 2015-09-03 09:42:35 +0000
3744+++ hooks/charmhelpers/core/hookenv.py 2016-02-18 14:28:13 +0000
3745@@ -491,6 +491,19 @@
3746
3747
3748 @cached
3749+def peer_relation_id():
3750+ '''Get the peers relation id if a peers relation has been joined, else None.'''
3751+ md = metadata()
3752+ section = md.get('peers')
3753+ if section:
3754+ for key in section:
3755+ relids = relation_ids(key)
3756+ if relids:
3757+ return relids[0]
3758+ return None
3759+
3760+
3761+@cached
3762 def relation_to_interface(relation_name):
3763 """
3764 Given the name of a relation, return the interface that relation uses.
3765@@ -504,12 +517,12 @@
3766 def relation_to_role_and_interface(relation_name):
3767 """
3768 Given the name of a relation, return the role and the name of the interface
3769- that relation uses (where role is one of ``provides``, ``requires``, or ``peer``).
3770+ that relation uses (where role is one of ``provides``, ``requires``, or ``peers``).
3771
3772 :returns: A tuple containing ``(role, interface)``, or ``(None, None)``.
3773 """
3774 _metadata = metadata()
3775- for role in ('provides', 'requires', 'peer'):
3776+ for role in ('provides', 'requires', 'peers'):
3777 interface = _metadata.get(role, {}).get(relation_name, {}).get('interface')
3778 if interface:
3779 return role, interface
3780@@ -521,7 +534,7 @@
3781 """
3782 Given a role and interface name, return a list of relation names for the
3783 current charm that use that interface under that role (where role is one
3784- of ``provides``, ``requires``, or ``peer``).
3785+ of ``provides``, ``requires``, or ``peers``).
3786
3787 :returns: A list of relation names.
3788 """
3789@@ -542,7 +555,7 @@
3790 :returns: A list of relation names.
3791 """
3792 results = []
3793- for role in ('provides', 'requires', 'peer'):
3794+ for role in ('provides', 'requires', 'peers'):
3795 results.extend(role_and_interface_to_relations(role, interface_name))
3796 return results
3797
3798@@ -623,6 +636,38 @@
3799 return unit_get('private-address')
3800
3801
3802+@cached
3803+def storage_get(attribute=None, storage_id=None):
3804+ """Get storage attributes"""
3805+ _args = ['storage-get', '--format=json']
3806+ if storage_id:
3807+ _args.extend(('-s', storage_id))
3808+ if attribute:
3809+ _args.append(attribute)
3810+ try:
3811+ return json.loads(subprocess.check_output(_args).decode('UTF-8'))
3812+ except ValueError:
3813+ return None
3814+
3815+
3816+@cached
3817+def storage_list(storage_name=None):
3818+ """List the storage IDs for the unit"""
3819+ _args = ['storage-list', '--format=json']
3820+ if storage_name:
3821+ _args.append(storage_name)
3822+ try:
3823+ return json.loads(subprocess.check_output(_args).decode('UTF-8'))
3824+ except ValueError:
3825+ return None
3826+ except OSError as e:
3827+ import errno
3828+ if e.errno == errno.ENOENT:
3829+ # storage-list does not exist
3830+ return []
3831+ raise
3832+
3833+
3834 class UnregisteredHookError(Exception):
3835 """Raised when an undefined hook is called"""
3836 pass
3837@@ -788,6 +833,7 @@
3838
3839 def translate_exc(from_exc, to_exc):
3840 def inner_translate_exc1(f):
3841+ @wraps(f)
3842 def inner_translate_exc2(*args, **kwargs):
3843 try:
3844 return f(*args, **kwargs)
3845@@ -832,6 +878,40 @@
3846 subprocess.check_call(cmd)
3847
3848
3849+@translate_exc(from_exc=OSError, to_exc=NotImplementedError)
3850+def payload_register(ptype, klass, pid):
3851+ """ is used while a hook is running to let Juju know that a
3852+ payload has been started."""
3853+ cmd = ['payload-register']
3854+ for x in [ptype, klass, pid]:
3855+ cmd.append(x)
3856+ subprocess.check_call(cmd)
3857+
3858+
3859+@translate_exc(from_exc=OSError, to_exc=NotImplementedError)
3860+def payload_unregister(klass, pid):
3861+ """ is used while a hook is running to let Juju know
3862+ that a payload has been manually stopped. The <class> and <id> provided
3863+ must match a payload that has been previously registered with juju using
3864+ payload-register."""
3865+ cmd = ['payload-unregister']
3866+ for x in [klass, pid]:
3867+ cmd.append(x)
3868+ subprocess.check_call(cmd)
3869+
3870+
3871+@translate_exc(from_exc=OSError, to_exc=NotImplementedError)
3872+def payload_status_set(klass, pid, status):
3873+ """is used to update the current status of a registered payload.
3874+ The <class> and <id> provided must match a payload that has been previously
3875+ registered with juju using payload-register. The <status> must be one of the
3876+ follow: starting, started, stopping, stopped"""
3877+ cmd = ['payload-status-set']
3878+ for x in [klass, pid, status]:
3879+ cmd.append(x)
3880+ subprocess.check_call(cmd)
3881+
3882+
3883 @cached
3884 def juju_version():
3885 """Full version string (eg. '1.23.3.1-trusty-amd64')"""
3886
3887=== modified file 'hooks/charmhelpers/core/host.py'
3888--- hooks/charmhelpers/core/host.py 2015-08-27 15:02:34 +0000
3889+++ hooks/charmhelpers/core/host.py 2016-02-18 14:28:13 +0000
3890@@ -63,55 +63,85 @@
3891 return service_result
3892
3893
3894-def service_pause(service_name, init_dir=None):
3895+def service_pause(service_name, init_dir="/etc/init", initd_dir="/etc/init.d"):
3896 """Pause a system service.
3897
3898 Stop it, and prevent it from starting again at boot."""
3899- if init_dir is None:
3900- init_dir = "/etc/init"
3901- stopped = service_stop(service_name)
3902- # XXX: Support systemd too
3903- override_path = os.path.join(
3904- init_dir, '{}.override'.format(service_name))
3905- with open(override_path, 'w') as fh:
3906- fh.write("manual\n")
3907+ stopped = True
3908+ if service_running(service_name):
3909+ stopped = service_stop(service_name)
3910+ upstart_file = os.path.join(init_dir, "{}.conf".format(service_name))
3911+ sysv_file = os.path.join(initd_dir, service_name)
3912+ if init_is_systemd():
3913+ service('disable', service_name)
3914+ elif os.path.exists(upstart_file):
3915+ override_path = os.path.join(
3916+ init_dir, '{}.override'.format(service_name))
3917+ with open(override_path, 'w') as fh:
3918+ fh.write("manual\n")
3919+ elif os.path.exists(sysv_file):
3920+ subprocess.check_call(["update-rc.d", service_name, "disable"])
3921+ else:
3922+ raise ValueError(
3923+ "Unable to detect {0} as SystemD, Upstart {1} or"
3924+ " SysV {2}".format(
3925+ service_name, upstart_file, sysv_file))
3926 return stopped
3927
3928
3929-def service_resume(service_name, init_dir=None):
3930+def service_resume(service_name, init_dir="/etc/init",
3931+ initd_dir="/etc/init.d"):
3932 """Resume a system service.
3933
3934 Reenable starting again at boot. Start the service"""
3935- # XXX: Support systemd too
3936- if init_dir is None:
3937- init_dir = "/etc/init"
3938- override_path = os.path.join(
3939- init_dir, '{}.override'.format(service_name))
3940- if os.path.exists(override_path):
3941- os.unlink(override_path)
3942- started = service_start(service_name)
3943+ upstart_file = os.path.join(init_dir, "{}.conf".format(service_name))
3944+ sysv_file = os.path.join(initd_dir, service_name)
3945+ if init_is_systemd():
3946+ service('enable', service_name)
3947+ elif os.path.exists(upstart_file):
3948+ override_path = os.path.join(
3949+ init_dir, '{}.override'.format(service_name))
3950+ if os.path.exists(override_path):
3951+ os.unlink(override_path)
3952+ elif os.path.exists(sysv_file):
3953+ subprocess.check_call(["update-rc.d", service_name, "enable"])
3954+ else:
3955+ raise ValueError(
3956+ "Unable to detect {0} as SystemD, Upstart {1} or"
3957+ " SysV {2}".format(
3958+ service_name, upstart_file, sysv_file))
3959+
3960+ started = service_running(service_name)
3961+ if not started:
3962+ started = service_start(service_name)
3963 return started
3964
3965
3966 def service(action, service_name):
3967 """Control a system service"""
3968- cmd = ['service', service_name, action]
3969+ if init_is_systemd():
3970+ cmd = ['systemctl', action, service_name]
3971+ else:
3972+ cmd = ['service', service_name, action]
3973 return subprocess.call(cmd) == 0
3974
3975
3976-def service_running(service):
3977+def service_running(service_name):
3978 """Determine whether a system service is running"""
3979- try:
3980- output = subprocess.check_output(
3981- ['service', service, 'status'],
3982- stderr=subprocess.STDOUT).decode('UTF-8')
3983- except subprocess.CalledProcessError:
3984- return False
3985+ if init_is_systemd():
3986+ return service('is-active', service_name)
3987 else:
3988- if ("start/running" in output or "is running" in output):
3989- return True
3990- else:
3991+ try:
3992+ output = subprocess.check_output(
3993+ ['service', service_name, 'status'],
3994+ stderr=subprocess.STDOUT).decode('UTF-8')
3995+ except subprocess.CalledProcessError:
3996 return False
3997+ else:
3998+ if ("start/running" in output or "is running" in output):
3999+ return True
4000+ else:
4001+ return False
4002
4003
4004 def service_available(service_name):
4005@@ -126,8 +156,29 @@
4006 return True
4007
4008
4009-def adduser(username, password=None, shell='/bin/bash', system_user=False):
4010- """Add a user to the system"""
4011+SYSTEMD_SYSTEM = '/run/systemd/system'
4012+
4013+
4014+def init_is_systemd():
4015+ return os.path.isdir(SYSTEMD_SYSTEM)
4016+
4017+
4018+def adduser(username, password=None, shell='/bin/bash', system_user=False,
4019+ primary_group=None, secondary_groups=None):
4020+ """
4021+ Add a user to the system.
4022+
4023+ Will log but otherwise succeed if the user already exists.
4024+
4025+ :param str username: Username to create
4026+ :param str password: Password for user; if ``None``, create a system user
4027+ :param str shell: The default shell for the user
4028+ :param bool system_user: Whether to create a login or system user
4029+ :param str primary_group: Primary group for user; defaults to their username
4030+ :param list secondary_groups: Optional list of additional groups
4031+
4032+ :returns: The password database entry struct, as returned by `pwd.getpwnam`
4033+ """
4034 try:
4035 user_info = pwd.getpwnam(username)
4036 log('user {0} already exists!'.format(username))
4037@@ -142,6 +193,16 @@
4038 '--shell', shell,
4039 '--password', password,
4040 ])
4041+ if not primary_group:
4042+ try:
4043+ grp.getgrnam(username)
4044+ primary_group = username # avoid "group exists" error
4045+ except KeyError:
4046+ pass
4047+ if primary_group:
4048+ cmd.extend(['-g', primary_group])
4049+ if secondary_groups:
4050+ cmd.extend(['-G', ','.join(secondary_groups)])
4051 cmd.append(username)
4052 subprocess.check_call(cmd)
4053 user_info = pwd.getpwnam(username)
4054@@ -550,7 +611,14 @@
4055 os.chdir(cur)
4056
4057
4058-def chownr(path, owner, group, follow_links=True):
4059+def chownr(path, owner, group, follow_links=True, chowntopdir=False):
4060+ """
4061+ Recursively change user and group ownership of files and directories
4062+ in given path. Doesn't chown path itself by default, only its children.
4063+
4064+ :param bool follow_links: Also Chown links if True
4065+ :param bool chowntopdir: Also chown path itself if True
4066+ """
4067 uid = pwd.getpwnam(owner).pw_uid
4068 gid = grp.getgrnam(group).gr_gid
4069 if follow_links:
4070@@ -558,6 +626,10 @@
4071 else:
4072 chown = os.lchown
4073
4074+ if chowntopdir:
4075+ broken_symlink = os.path.lexists(path) and not os.path.exists(path)
4076+ if not broken_symlink:
4077+ chown(path, uid, gid)
4078 for root, dirs, files in os.walk(path):
4079 for name in dirs + files:
4080 full = os.path.join(root, name)
4081@@ -568,3 +640,19 @@
4082
4083 def lchownr(path, owner, group):
4084 chownr(path, owner, group, follow_links=False)
4085+
4086+
4087+def get_total_ram():
4088+ '''The total amount of system RAM in bytes.
4089+
4090+ This is what is reported by the OS, and may be overcommitted when
4091+ there are multiple containers hosted on the same machine.
4092+ '''
4093+ with open('/proc/meminfo', 'r') as f:
4094+ for line in f.readlines():
4095+ if line:
4096+ key, value, unit = line.split()
4097+ if key == 'MemTotal:':
4098+ assert unit == 'kB', 'Unknown unit'
4099+ return int(value) * 1024 # Classic, not KiB.
4100+ raise NotImplementedError()
4101
4102=== added file 'hooks/charmhelpers/core/hugepage.py'
4103--- hooks/charmhelpers/core/hugepage.py 1970-01-01 00:00:00 +0000
4104+++ hooks/charmhelpers/core/hugepage.py 2016-02-18 14:28:13 +0000
4105@@ -0,0 +1,71 @@
4106+# -*- coding: utf-8 -*-
4107+
4108+# Copyright 2014-2015 Canonical Limited.
4109+#
4110+# This file is part of charm-helpers.
4111+#
4112+# charm-helpers is free software: you can redistribute it and/or modify
4113+# it under the terms of the GNU Lesser General Public License version 3 as
4114+# published by the Free Software Foundation.
4115+#
4116+# charm-helpers is distributed in the hope that it will be useful,
4117+# but WITHOUT ANY WARRANTY; without even the implied warranty of
4118+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
4119+# GNU Lesser General Public License for more details.
4120+#
4121+# You should have received a copy of the GNU Lesser General Public License
4122+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
4123+
4124+import yaml
4125+from charmhelpers.core import fstab
4126+from charmhelpers.core import sysctl
4127+from charmhelpers.core.host import (
4128+ add_group,
4129+ add_user_to_group,
4130+ fstab_mount,
4131+ mkdir,
4132+)
4133+from charmhelpers.core.strutils import bytes_from_string
4134+from subprocess import check_output
4135+
4136+
4137+def hugepage_support(user, group='hugetlb', nr_hugepages=256,
4138+ max_map_count=65536, mnt_point='/run/hugepages/kvm',
4139+ pagesize='2MB', mount=True, set_shmmax=False):
4140+ """Enable hugepages on system.
4141+
4142+ Args:
4143+ user (str) -- Username to allow access to hugepages to
4144+ group (str) -- Group name to own hugepages
4145+ nr_hugepages (int) -- Number of pages to reserve
4146+ max_map_count (int) -- Number of Virtual Memory Areas a process can own
4147+ mnt_point (str) -- Directory to mount hugepages on
4148+ pagesize (str) -- Size of hugepages
4149+ mount (bool) -- Whether to Mount hugepages
4150+ """
4151+ group_info = add_group(group)
4152+ gid = group_info.gr_gid
4153+ add_user_to_group(user, group)
4154+ if max_map_count < 2 * nr_hugepages:
4155+ max_map_count = 2 * nr_hugepages
4156+ sysctl_settings = {
4157+ 'vm.nr_hugepages': nr_hugepages,
4158+ 'vm.max_map_count': max_map_count,
4159+ 'vm.hugetlb_shm_group': gid,
4160+ }
4161+ if set_shmmax:
4162+ shmmax_current = int(check_output(['sysctl', '-n', 'kernel.shmmax']))
4163+ shmmax_minsize = bytes_from_string(pagesize) * nr_hugepages
4164+ if shmmax_minsize > shmmax_current:
4165+ sysctl_settings['kernel.shmmax'] = shmmax_minsize
4166+ sysctl.create(yaml.dump(sysctl_settings), '/etc/sysctl.d/10-hugepage.conf')
4167+ mkdir(mnt_point, owner='root', group='root', perms=0o755, force=False)
4168+ lfstab = fstab.Fstab()
4169+ fstab_entry = lfstab.get_entry_by_attr('mountpoint', mnt_point)
4170+ if fstab_entry:
4171+ lfstab.remove_entry(fstab_entry)
4172+ entry = lfstab.Entry('nodev', mnt_point, 'hugetlbfs',
4173+ 'mode=1770,gid={},pagesize={}'.format(gid, pagesize), 0, 0)
4174+ lfstab.add_entry(entry)
4175+ if mount:
4176+ fstab_mount(mnt_point)
4177
4178=== removed file 'hooks/charmhelpers/core/hugepage.py'
4179--- hooks/charmhelpers/core/hugepage.py 2015-08-19 13:51:03 +0000
4180+++ hooks/charmhelpers/core/hugepage.py 1970-01-01 00:00:00 +0000
4181@@ -1,62 +0,0 @@
4182-# -*- coding: utf-8 -*-
4183-
4184-# Copyright 2014-2015 Canonical Limited.
4185-#
4186-# This file is part of charm-helpers.
4187-#
4188-# charm-helpers is free software: you can redistribute it and/or modify
4189-# it under the terms of the GNU Lesser General Public License version 3 as
4190-# published by the Free Software Foundation.
4191-#
4192-# charm-helpers is distributed in the hope that it will be useful,
4193-# but WITHOUT ANY WARRANTY; without even the implied warranty of
4194-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
4195-# GNU Lesser General Public License for more details.
4196-#
4197-# You should have received a copy of the GNU Lesser General Public License
4198-# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
4199-
4200-import yaml
4201-from charmhelpers.core import fstab
4202-from charmhelpers.core import sysctl
4203-from charmhelpers.core.host import (
4204- add_group,
4205- add_user_to_group,
4206- fstab_mount,
4207- mkdir,
4208-)
4209-
4210-
4211-def hugepage_support(user, group='hugetlb', nr_hugepages=256,
4212- max_map_count=65536, mnt_point='/run/hugepages/kvm',
4213- pagesize='2MB', mount=True):
4214- """Enable hugepages on system.
4215-
4216- Args:
4217- user (str) -- Username to allow access to hugepages to
4218- group (str) -- Group name to own hugepages
4219- nr_hugepages (int) -- Number of pages to reserve
4220- max_map_count (int) -- Number of Virtual Memory Areas a process can own
4221- mnt_point (str) -- Directory to mount hugepages on
4222- pagesize (str) -- Size of hugepages
4223- mount (bool) -- Whether to Mount hugepages
4224- """
4225- group_info = add_group(group)
4226- gid = group_info.gr_gid
4227- add_user_to_group(user, group)
4228- sysctl_settings = {
4229- 'vm.nr_hugepages': nr_hugepages,
4230- 'vm.max_map_count': max_map_count,
4231- 'vm.hugetlb_shm_group': gid,
4232- }
4233- sysctl.create(yaml.dump(sysctl_settings), '/etc/sysctl.d/10-hugepage.conf')
4234- mkdir(mnt_point, owner='root', group='root', perms=0o755, force=False)
4235- lfstab = fstab.Fstab()
4236- fstab_entry = lfstab.get_entry_by_attr('mountpoint', mnt_point)
4237- if fstab_entry:
4238- lfstab.remove_entry(fstab_entry)
4239- entry = lfstab.Entry('nodev', mnt_point, 'hugetlbfs',
4240- 'mode=1770,gid={},pagesize={}'.format(gid, pagesize), 0, 0)
4241- lfstab.add_entry(entry)
4242- if mount:
4243- fstab_mount(mnt_point)
4244
4245=== added file 'hooks/charmhelpers/core/kernel.py'
4246--- hooks/charmhelpers/core/kernel.py 1970-01-01 00:00:00 +0000
4247+++ hooks/charmhelpers/core/kernel.py 2016-02-18 14:28:13 +0000
4248@@ -0,0 +1,68 @@
4249+#!/usr/bin/env python
4250+# -*- coding: utf-8 -*-
4251+
4252+# Copyright 2014-2015 Canonical Limited.
4253+#
4254+# This file is part of charm-helpers.
4255+#
4256+# charm-helpers is free software: you can redistribute it and/or modify
4257+# it under the terms of the GNU Lesser General Public License version 3 as
4258+# published by the Free Software Foundation.
4259+#
4260+# charm-helpers is distributed in the hope that it will be useful,
4261+# but WITHOUT ANY WARRANTY; without even the implied warranty of
4262+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
4263+# GNU Lesser General Public License for more details.
4264+#
4265+# You should have received a copy of the GNU Lesser General Public License
4266+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
4267+
4268+__author__ = "Jorge Niedbalski <jorge.niedbalski@canonical.com>"
4269+
4270+from charmhelpers.core.hookenv import (
4271+ log,
4272+ INFO
4273+)
4274+
4275+from subprocess import check_call, check_output
4276+import re
4277+
4278+
4279+def modprobe(module, persist=True):
4280+ """Load a kernel module and configure for auto-load on reboot."""
4281+ cmd = ['modprobe', module]
4282+
4283+ log('Loading kernel module %s' % module, level=INFO)
4284+
4285+ check_call(cmd)
4286+ if persist:
4287+ with open('/etc/modules', 'r+') as modules:
4288+ if module not in modules.read():
4289+ modules.write(module)
4290+
4291+
4292+def rmmod(module, force=False):
4293+ """Remove a module from the linux kernel"""
4294+ cmd = ['rmmod']
4295+ if force:
4296+ cmd.append('-f')
4297+ cmd.append(module)
4298+ log('Removing kernel module %s' % module, level=INFO)
4299+ return check_call(cmd)
4300+
4301+
4302+def lsmod():
4303+ """Shows what kernel modules are currently loaded"""
4304+ return check_output(['lsmod'],
4305+ universal_newlines=True)
4306+
4307+
4308+def is_module_loaded(module):
4309+ """Checks if a kernel module is already loaded"""
4310+ matches = re.findall('^%s[ ]+' % module, lsmod(), re.M)
4311+ return len(matches) > 0
4312+
4313+
4314+def update_initramfs(version='all'):
4315+ """Updates an initramfs image"""
4316+ return check_call(["update-initramfs", "-k", version, "-u"])
4317
4318=== modified file 'hooks/charmhelpers/core/services/helpers.py'
4319--- hooks/charmhelpers/core/services/helpers.py 2015-08-18 17:34:36 +0000
4320+++ hooks/charmhelpers/core/services/helpers.py 2016-02-18 14:28:13 +0000
4321@@ -243,33 +243,40 @@
4322 :param str source: The template source file, relative to
4323 `$CHARM_DIR/templates`
4324
4325- :param str target: The target to write the rendered template to
4326+ :param str target: The target to write the rendered template to (or None)
4327 :param str owner: The owner of the rendered file
4328 :param str group: The group of the rendered file
4329 :param int perms: The permissions of the rendered file
4330 :param partial on_change_action: functools partial to be executed when
4331 rendered file changes
4332+ :param jinja2 loader template_loader: A jinja2 template loader
4333+
4334+ :return str: The rendered template
4335 """
4336 def __init__(self, source, target,
4337 owner='root', group='root', perms=0o444,
4338- on_change_action=None):
4339+ on_change_action=None, template_loader=None):
4340 self.source = source
4341 self.target = target
4342 self.owner = owner
4343 self.group = group
4344 self.perms = perms
4345 self.on_change_action = on_change_action
4346+ self.template_loader = template_loader
4347
4348 def __call__(self, manager, service_name, event_name):
4349 pre_checksum = ''
4350 if self.on_change_action and os.path.isfile(self.target):
4351 pre_checksum = host.file_hash(self.target)
4352 service = manager.get_service(service_name)
4353- context = {}
4354+ context = {'ctx': {}}
4355 for ctx in service.get('required_data', []):
4356 context.update(ctx)
4357- templating.render(self.source, self.target, context,
4358- self.owner, self.group, self.perms)
4359+ context['ctx'].update(ctx)
4360+
4361+ result = templating.render(self.source, self.target, context,
4362+ self.owner, self.group, self.perms,
4363+ template_loader=self.template_loader)
4364 if self.on_change_action:
4365 if pre_checksum == host.file_hash(self.target):
4366 hookenv.log(
4367@@ -278,6 +285,8 @@
4368 else:
4369 self.on_change_action()
4370
4371+ return result
4372+
4373
4374 # Convenience aliases for templates
4375 render_template = template = TemplateCallback
4376
4377=== modified file 'hooks/charmhelpers/core/strutils.py'
4378--- hooks/charmhelpers/core/strutils.py 2015-04-16 20:24:28 +0000
4379+++ hooks/charmhelpers/core/strutils.py 2016-02-18 14:28:13 +0000
4380@@ -18,6 +18,7 @@
4381 # along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
4382
4383 import six
4384+import re
4385
4386
4387 def bool_from_string(value):
4388@@ -40,3 +41,32 @@
4389
4390 msg = "Unable to interpret string value '%s' as boolean" % (value)
4391 raise ValueError(msg)
4392+
4393+
4394+def bytes_from_string(value):
4395+ """Interpret human readable string value as bytes.
4396+
4397+ Returns int
4398+ """
4399+ BYTE_POWER = {
4400+ 'K': 1,
4401+ 'KB': 1,
4402+ 'M': 2,
4403+ 'MB': 2,
4404+ 'G': 3,
4405+ 'GB': 3,
4406+ 'T': 4,
4407+ 'TB': 4,
4408+ 'P': 5,
4409+ 'PB': 5,
4410+ }
4411+ if isinstance(value, six.string_types):
4412+ value = six.text_type(value)
4413+ else:
4414+ msg = "Unable to interpret non-string value '%s' as boolean" % (value)
4415+ raise ValueError(msg)
4416+ matches = re.match("([0-9]+)([a-zA-Z]+)", value)
4417+ if not matches:
4418+ msg = "Unable to interpret string value '%s' as bytes" % (value)
4419+ raise ValueError(msg)
4420+ return int(matches.group(1)) * (1024 ** BYTE_POWER[matches.group(2)])
4421
4422=== modified file 'hooks/charmhelpers/core/templating.py'
4423--- hooks/charmhelpers/core/templating.py 2015-02-26 10:11:26 +0000
4424+++ hooks/charmhelpers/core/templating.py 2016-02-18 14:28:13 +0000
4425@@ -21,13 +21,14 @@
4426
4427
4428 def render(source, target, context, owner='root', group='root',
4429- perms=0o444, templates_dir=None, encoding='UTF-8'):
4430+ perms=0o444, templates_dir=None, encoding='UTF-8', template_loader=None):
4431 """
4432 Render a template.
4433
4434 The `source` path, if not absolute, is relative to the `templates_dir`.
4435
4436- The `target` path should be absolute.
4437+ The `target` path should be absolute. It can also be `None`, in which
4438+ case no file will be written.
4439
4440 The context should be a dict containing the values to be replaced in the
4441 template.
4442@@ -36,6 +37,9 @@
4443
4444 If omitted, `templates_dir` defaults to the `templates` folder in the charm.
4445
4446+ The rendered template will be written to the file as well as being returned
4447+ as a string.
4448+
4449 Note: Using this requires python-jinja2; if it is not installed, calling
4450 this will attempt to use charmhelpers.fetch.apt_install to install it.
4451 """
4452@@ -52,17 +56,26 @@
4453 apt_install('python-jinja2', fatal=True)
4454 from jinja2 import FileSystemLoader, Environment, exceptions
4455
4456- if templates_dir is None:
4457- templates_dir = os.path.join(hookenv.charm_dir(), 'templates')
4458- loader = Environment(loader=FileSystemLoader(templates_dir))
4459+ if template_loader:
4460+ template_env = Environment(loader=template_loader)
4461+ else:
4462+ if templates_dir is None:
4463+ templates_dir = os.path.join(hookenv.charm_dir(), 'templates')
4464+ template_env = Environment(loader=FileSystemLoader(templates_dir))
4465 try:
4466 source = source
4467- template = loader.get_template(source)
4468+ template = template_env.get_template(source)
4469 except exceptions.TemplateNotFound as e:
4470 hookenv.log('Could not load template %s from %s.' %
4471 (source, templates_dir),
4472 level=hookenv.ERROR)
4473 raise e
4474 content = template.render(context)
4475- host.mkdir(os.path.dirname(target), owner, group, perms=0o755)
4476- host.write_file(target, content.encode(encoding), owner, group, perms)
4477+ if target is not None:
4478+ target_dir = os.path.dirname(target)
4479+ if not os.path.exists(target_dir):
4480+ # This is a terrible default directory permission, as the file
4481+ # or its siblings will often contain secrets.
4482+ host.mkdir(os.path.dirname(target), owner, group, perms=0o755)
4483+ host.write_file(target, content.encode(encoding), owner, group, perms)
4484+ return content
4485
4486=== modified file 'hooks/charmhelpers/fetch/__init__.py'
4487--- hooks/charmhelpers/fetch/__init__.py 2015-08-18 17:34:36 +0000
4488+++ hooks/charmhelpers/fetch/__init__.py 2016-02-18 14:28:13 +0000
4489@@ -98,6 +98,14 @@
4490 'liberty/proposed': 'trusty-proposed/liberty',
4491 'trusty-liberty/proposed': 'trusty-proposed/liberty',
4492 'trusty-proposed/liberty': 'trusty-proposed/liberty',
4493+ # Mitaka
4494+ 'mitaka': 'trusty-updates/mitaka',
4495+ 'trusty-mitaka': 'trusty-updates/mitaka',
4496+ 'trusty-mitaka/updates': 'trusty-updates/mitaka',
4497+ 'trusty-updates/mitaka': 'trusty-updates/mitaka',
4498+ 'mitaka/proposed': 'trusty-proposed/mitaka',
4499+ 'trusty-mitaka/proposed': 'trusty-proposed/mitaka',
4500+ 'trusty-proposed/mitaka': 'trusty-proposed/mitaka',
4501 }
4502
4503 # The order of this list is very important. Handlers should be listed in from
4504@@ -225,12 +233,12 @@
4505
4506 def apt_mark(packages, mark, fatal=False):
4507 """Flag one or more packages using apt-mark"""
4508+ log("Marking {} as {}".format(packages, mark))
4509 cmd = ['apt-mark', mark]
4510 if isinstance(packages, six.string_types):
4511 cmd.append(packages)
4512 else:
4513 cmd.extend(packages)
4514- log("Holding {}".format(packages))
4515
4516 if fatal:
4517 subprocess.check_call(cmd, universal_newlines=True)
4518@@ -411,7 +419,7 @@
4519 importlib.import_module(package),
4520 classname)
4521 plugin_list.append(handler_class())
4522- except (ImportError, AttributeError):
4523+ except NotImplementedError:
4524 # Skip missing plugins so that they can be ommitted from
4525 # installation if desired
4526 log("FetchHandler {} not found, skipping plugin".format(
4527
4528=== modified file 'hooks/charmhelpers/fetch/archiveurl.py'
4529--- hooks/charmhelpers/fetch/archiveurl.py 2015-07-17 13:24:05 +0000
4530+++ hooks/charmhelpers/fetch/archiveurl.py 2016-02-18 14:28:13 +0000
4531@@ -108,7 +108,7 @@
4532 install_opener(opener)
4533 response = urlopen(source)
4534 try:
4535- with open(dest, 'w') as dest_file:
4536+ with open(dest, 'wb') as dest_file:
4537 dest_file.write(response.read())
4538 except Exception as e:
4539 if os.path.isfile(dest):
4540
4541=== modified file 'hooks/charmhelpers/fetch/bzrurl.py'
4542--- hooks/charmhelpers/fetch/bzrurl.py 2015-01-26 09:46:38 +0000
4543+++ hooks/charmhelpers/fetch/bzrurl.py 2016-02-18 14:28:13 +0000
4544@@ -15,60 +15,50 @@
4545 # along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
4546
4547 import os
4548+from subprocess import check_call
4549 from charmhelpers.fetch import (
4550 BaseFetchHandler,
4551- UnhandledSource
4552+ UnhandledSource,
4553+ filter_installed_packages,
4554+ apt_install,
4555 )
4556 from charmhelpers.core.host import mkdir
4557
4558-import six
4559-if six.PY3:
4560- raise ImportError('bzrlib does not support Python3')
4561
4562-try:
4563- from bzrlib.branch import Branch
4564- from bzrlib import bzrdir, workingtree, errors
4565-except ImportError:
4566- from charmhelpers.fetch import apt_install
4567- apt_install("python-bzrlib")
4568- from bzrlib.branch import Branch
4569- from bzrlib import bzrdir, workingtree, errors
4570+if filter_installed_packages(['bzr']) != []:
4571+ apt_install(['bzr'])
4572+ if filter_installed_packages(['bzr']) != []:
4573+ raise NotImplementedError('Unable to install bzr')
4574
4575
4576 class BzrUrlFetchHandler(BaseFetchHandler):
4577 """Handler for bazaar branches via generic and lp URLs"""
4578 def can_handle(self, source):
4579 url_parts = self.parse_url(source)
4580- if url_parts.scheme not in ('bzr+ssh', 'lp'):
4581+ if url_parts.scheme not in ('bzr+ssh', 'lp', ''):
4582 return False
4583+ elif not url_parts.scheme:
4584+ return os.path.exists(os.path.join(source, '.bzr'))
4585 else:
4586 return True
4587
4588 def branch(self, source, dest):
4589- url_parts = self.parse_url(source)
4590- # If we use lp:branchname scheme we need to load plugins
4591 if not self.can_handle(source):
4592 raise UnhandledSource("Cannot handle {}".format(source))
4593- if url_parts.scheme == "lp":
4594- from bzrlib.plugin import load_plugins
4595- load_plugins()
4596- try:
4597- local_branch = bzrdir.BzrDir.create_branch_convenience(dest)
4598- except errors.AlreadyControlDirError:
4599- local_branch = Branch.open(dest)
4600- try:
4601- remote_branch = Branch.open(source)
4602- remote_branch.push(local_branch)
4603- tree = workingtree.WorkingTree.open(dest)
4604- tree.update()
4605- except Exception as e:
4606- raise e
4607+ if os.path.exists(dest):
4608+ check_call(['bzr', 'pull', '--overwrite', '-d', dest, source])
4609+ else:
4610+ check_call(['bzr', 'branch', source, dest])
4611
4612- def install(self, source):
4613+ def install(self, source, dest=None):
4614 url_parts = self.parse_url(source)
4615 branch_name = url_parts.path.strip("/").split("/")[-1]
4616- dest_dir = os.path.join(os.environ.get('CHARM_DIR'), "fetched",
4617- branch_name)
4618+ if dest:
4619+ dest_dir = os.path.join(dest, branch_name)
4620+ else:
4621+ dest_dir = os.path.join(os.environ.get('CHARM_DIR'), "fetched",
4622+ branch_name)
4623+
4624 if not os.path.exists(dest_dir):
4625 mkdir(dest_dir, perms=0o755)
4626 try:
4627
4628=== modified file 'hooks/charmhelpers/fetch/giturl.py'
4629--- hooks/charmhelpers/fetch/giturl.py 2015-07-17 13:24:05 +0000
4630+++ hooks/charmhelpers/fetch/giturl.py 2016-02-18 14:28:13 +0000
4631@@ -15,24 +15,18 @@
4632 # along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
4633
4634 import os
4635+from subprocess import check_call
4636 from charmhelpers.fetch import (
4637 BaseFetchHandler,
4638- UnhandledSource
4639+ UnhandledSource,
4640+ filter_installed_packages,
4641+ apt_install,
4642 )
4643-from charmhelpers.core.host import mkdir
4644-
4645-import six
4646-if six.PY3:
4647- raise ImportError('GitPython does not support Python 3')
4648-
4649-try:
4650- from git import Repo
4651-except ImportError:
4652- from charmhelpers.fetch import apt_install
4653- apt_install("python-git")
4654- from git import Repo
4655-
4656-from git.exc import GitCommandError # noqa E402
4657+
4658+if filter_installed_packages(['git']) != []:
4659+ apt_install(['git'])
4660+ if filter_installed_packages(['git']) != []:
4661+ raise NotImplementedError('Unable to install git')
4662
4663
4664 class GitUrlFetchHandler(BaseFetchHandler):
4665@@ -40,19 +34,24 @@
4666 def can_handle(self, source):
4667 url_parts = self.parse_url(source)
4668 # TODO (mattyw) no support for ssh git@ yet
4669- if url_parts.scheme not in ('http', 'https', 'git'):
4670+ if url_parts.scheme not in ('http', 'https', 'git', ''):
4671 return False
4672+ elif not url_parts.scheme:
4673+ return os.path.exists(os.path.join(source, '.git'))
4674 else:
4675 return True
4676
4677- def clone(self, source, dest, branch, depth=None):
4678+ def clone(self, source, dest, branch="master", depth=None):
4679 if not self.can_handle(source):
4680 raise UnhandledSource("Cannot handle {}".format(source))
4681
4682+ if os.path.exists(dest):
4683+ cmd = ['git', '-C', dest, 'pull', source, branch]
4684+ else:
4685+ cmd = ['git', 'clone', source, dest, '--branch', branch]
4686 if depth:
4687- Repo.clone_from(source, dest, branch=branch, depth=depth)
4688- else:
4689- Repo.clone_from(source, dest, branch=branch)
4690+ cmd.extend(['--depth', depth])
4691+ check_call(cmd)
4692
4693 def install(self, source, branch="master", dest=None, depth=None):
4694 url_parts = self.parse_url(source)
4695@@ -62,12 +61,8 @@
4696 else:
4697 dest_dir = os.path.join(os.environ.get('CHARM_DIR'), "fetched",
4698 branch_name)
4699- if not os.path.exists(dest_dir):
4700- mkdir(dest_dir, perms=0o755)
4701 try:
4702 self.clone(source, dest_dir, branch, depth)
4703- except GitCommandError as e:
4704- raise UnhandledSource(e)
4705 except OSError as e:
4706 raise UnhandledSource(e.strerror)
4707 return dest_dir
4708
4709=== added file 'hooks/charmhelpers/payload/archive.py'
4710--- hooks/charmhelpers/payload/archive.py 1970-01-01 00:00:00 +0000
4711+++ hooks/charmhelpers/payload/archive.py 2016-02-18 14:28:13 +0000
4712@@ -0,0 +1,73 @@
4713+# Copyright 2014-2015 Canonical Limited.
4714+#
4715+# This file is part of charm-helpers.
4716+#
4717+# charm-helpers is free software: you can redistribute it and/or modify
4718+# it under the terms of the GNU Lesser General Public License version 3 as
4719+# published by the Free Software Foundation.
4720+#
4721+# charm-helpers is distributed in the hope that it will be useful,
4722+# but WITHOUT ANY WARRANTY; without even the implied warranty of
4723+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
4724+# GNU Lesser General Public License for more details.
4725+#
4726+# You should have received a copy of the GNU Lesser General Public License
4727+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
4728+
4729+import os
4730+import tarfile
4731+import zipfile
4732+from charmhelpers.core import (
4733+ host,
4734+ hookenv,
4735+)
4736+
4737+
4738+class ArchiveError(Exception):
4739+ pass
4740+
4741+
4742+def get_archive_handler(archive_name):
4743+ if os.path.isfile(archive_name):
4744+ if tarfile.is_tarfile(archive_name):
4745+ return extract_tarfile
4746+ elif zipfile.is_zipfile(archive_name):
4747+ return extract_zipfile
4748+ else:
4749+ # look at the file name
4750+ for ext in ('.tar', '.tar.gz', '.tgz', 'tar.bz2', '.tbz2', '.tbz'):
4751+ if archive_name.endswith(ext):
4752+ return extract_tarfile
4753+ for ext in ('.zip', '.jar'):
4754+ if archive_name.endswith(ext):
4755+ return extract_zipfile
4756+
4757+
4758+def archive_dest_default(archive_name):
4759+ archive_file = os.path.basename(archive_name)
4760+ return os.path.join(hookenv.charm_dir(), "archives", archive_file)
4761+
4762+
4763+def extract(archive_name, destpath=None):
4764+ handler = get_archive_handler(archive_name)
4765+ if handler:
4766+ if not destpath:
4767+ destpath = archive_dest_default(archive_name)
4768+ if not os.path.isdir(destpath):
4769+ host.mkdir(destpath)
4770+ handler(archive_name, destpath)
4771+ return destpath
4772+ else:
4773+ raise ArchiveError("No handler for archive")
4774+
4775+
4776+def extract_tarfile(archive_name, destpath):
4777+ "Unpack a tar archive, optionally compressed"
4778+ archive = tarfile.open(archive_name)
4779+ archive.extractall(destpath)
4780+
4781+
4782+def extract_zipfile(archive_name, destpath):
4783+ "Unpack a zip file"
4784+ archive = zipfile.ZipFile(archive_name)
4785+ archive.extractall(destpath)
4786
4787=== added symlink 'hooks/dashboard-plugin-relation-changed'
4788=== target is u'horizon_hooks.py'
4789=== removed symlink 'hooks/dashboard-plugin-relation-changed'
4790=== target was u'horizon_hooks.py'
4791=== added symlink 'hooks/dashboard-plugin-relation-joined'
4792=== target is u'horizon_hooks.py'
4793=== removed symlink 'hooks/dashboard-plugin-relation-joined'
4794=== target was u'horizon_hooks.py'
4795=== modified file 'hooks/horizon_hooks.py'
4796--- hooks/horizon_hooks.py 2015-09-28 19:15:37 +0000
4797+++ hooks/horizon_hooks.py 2016-02-18 14:28:13 +0000
4798@@ -10,7 +10,8 @@
4799 relation_set,
4800 relation_get,
4801 relation_ids,
4802- unit_get
4803+ unit_get,
4804+ status_set,
4805 )
4806 from charmhelpers.fetch import (
4807 apt_update, apt_install,
4808@@ -27,7 +28,8 @@
4809 git_pip_venv_dir,
4810 openstack_upgrade_available,
4811 os_release,
4812- save_script_rc
4813+ save_script_rc,
4814+ set_os_workload_status,
4815 )
4816 from horizon_utils import (
4817 determine_packages,
4818@@ -40,7 +42,8 @@
4819 git_install,
4820 git_post_install_late,
4821 setup_ipv6,
4822- INSTALL_DIR
4823+ INSTALL_DIR,
4824+ REQUIRED_INTERFACES,
4825 )
4826 from charmhelpers.contrib.network.ip import (
4827 get_iface_for_address,
4828@@ -70,7 +73,10 @@
4829 if lsb_release()['DISTRIB_CODENAME'] == 'precise':
4830 # Explicitly upgrade python-six Bug#1420708
4831 apt_install('python-six', fatal=True)
4832- apt_install(filter_installed_packages(packages), fatal=True)
4833+ packages = filter_installed_packages(packages)
4834+ if packages:
4835+ status_set('maintenance', 'Installing packages')
4836+ apt_install(packages, fatal=True)
4837
4838 git_install(config('openstack-origin-git'))
4839
4840@@ -108,6 +114,7 @@
4841 git_install(config('openstack-origin-git'))
4842 elif not config('action-managed-upgrade'):
4843 if openstack_upgrade_available('openstack-dashboard'):
4844+ status_set('maintenance', 'Upgrading to new OpenStack release')
4845 do_openstack_upgrade(configs=CONFIGS)
4846
4847 env_vars = {
4848@@ -265,10 +272,12 @@
4849
4850
4851 def main():
4852+ print sys.argv
4853 try:
4854 hooks.execute(sys.argv)
4855 except UnregisteredHookError as e:
4856 log('Unknown hook {} - skipping.'.format(e))
4857+ set_os_workload_status(CONFIGS, REQUIRED_INTERFACES)
4858
4859
4860 if __name__ == '__main__':
4861
4862=== modified file 'hooks/horizon_utils.py'
4863--- hooks/horizon_utils.py 2015-09-28 19:15:37 +0000
4864+++ hooks/horizon_utils.py 2016-02-18 14:28:13 +0000
4865@@ -72,6 +72,9 @@
4866 'zlib1g-dev',
4867 ]
4868
4869+REQUIRED_INTERFACES = {
4870+ 'identity': ['identity-service'],
4871+}
4872 # ubuntu packages that should not be installed when deploying from git
4873 GIT_PACKAGE_BLACKLIST = [
4874 'openstack-dashboard',
4875
4876=== added symlink 'hooks/identity-service-relation-departed'
4877=== target is u'horizon_hooks.py'
4878=== added symlink 'hooks/install.real'
4879=== target is u'horizon_hooks.py'
4880=== removed symlink 'hooks/install.real'
4881=== target was u'horizon_hooks.py'
4882=== modified file 'metadata.yaml'
4883--- metadata.yaml 2015-09-30 13:56:20 +0000
4884+++ metadata.yaml 2016-02-18 14:28:13 +0000
4885@@ -1,6 +1,6 @@
4886 name: openstack-dashboard
4887-summary: a Django web interface to OpenStack
4888-maintainer: Adam Gandelman <adamg@canonical.com>
4889+summary: Web dashboard for OpenStack
4890+maintainer: OpenStack Charmers <openstack-charmers@lists.ubuntu.com>
4891 description: |
4892 The OpenStack Dashboard provides a full feature web interface for interacting
4893 with instances, images, volumes and networks within an OpenStack deployment.
4894
4895=== modified file 'templates/icehouse/local_settings.py'
4896--- templates/icehouse/local_settings.py 2015-09-25 02:05:05 +0000
4897+++ templates/icehouse/local_settings.py 2016-02-18 14:28:13 +0000
4898@@ -213,7 +213,7 @@
4899 # external to the OpenStack environment. The default is 'publicURL'.
4900 #OPENSTACK_ENDPOINT_TYPE = "publicURL"
4901 {% if primary_endpoint -%}
4902-OPENSTACK_ENDPOINT_TYPE = {{ primary_endpoint }}
4903+OPENSTACK_ENDPOINT_TYPE = "{{ primary_endpoint }}"
4904 {% endif -%}
4905
4906 # SECONDARY_ENDPOINT_TYPE specifies the fallback endpoint type to use in the
4907@@ -223,7 +223,7 @@
4908 # value should differ from OPENSTACK_ENDPOINT_TYPE if used.
4909 #SECONDARY_ENDPOINT_TYPE = "publicURL"
4910 {% if secondary_endpoint -%}
4911-SECONDARY_ENDPOINT_TYPE = {{ secondary_endpoint }}
4912+SECONDARY_ENDPOINT_TYPE = "{{ secondary_endpoint }}"
4913 {% endif -%}
4914
4915 # The number of objects (Swift containers/objects or images) to display
4916@@ -521,4 +521,4 @@
4917 # see https://docs.djangoproject.com/en/dev/ref/settings/.
4918 ALLOWED_HOSTS = '*'
4919
4920-{{ settings|join('\n\n') }}
4921\ No newline at end of file
4922+{{ settings|join('\n\n') }}
4923
4924=== modified file 'templates/juno/local_settings.py'
4925--- templates/juno/local_settings.py 2015-09-25 02:05:05 +0000
4926+++ templates/juno/local_settings.py 2016-02-18 14:28:13 +0000
4927@@ -251,7 +251,7 @@
4928 # external to the OpenStack environment. The default is 'publicURL'.
4929 #OPENSTACK_ENDPOINT_TYPE = "publicURL"
4930 {% if primary_endpoint -%}
4931-OPENSTACK_ENDPOINT_TYPE = {{ primary_endpoint }}
4932+OPENSTACK_ENDPOINT_TYPE = "{{ primary_endpoint }}"
4933 {% endif -%}
4934
4935 # SECONDARY_ENDPOINT_TYPE specifies the fallback endpoint type to use in the
4936@@ -261,7 +261,7 @@
4937 # value should differ from OPENSTACK_ENDPOINT_TYPE if used.
4938 #SECONDARY_ENDPOINT_TYPE = "publicURL"
4939 {% if secondary_endpoint -%}
4940-SECONDARY_ENDPOINT_TYPE = {{ secondary_endpoint }}
4941+SECONDARY_ENDPOINT_TYPE = "{{ secondary_endpoint }}"
4942 {% endif -%}
4943
4944 # The number of objects (Swift containers/objects or images) to display
4945@@ -626,4 +626,4 @@
4946 # see https://docs.djangoproject.com/en/dev/ref/settings/.
4947 ALLOWED_HOSTS = '*'
4948
4949-{{ settings|join('\n\n') }}
4950\ No newline at end of file
4951+{{ settings|join('\n\n') }}
4952
4953=== added file 'tests/018-basic-trusty-liberty'
4954--- tests/018-basic-trusty-liberty 1970-01-01 00:00:00 +0000
4955+++ tests/018-basic-trusty-liberty 2016-02-18 14:28:13 +0000
4956@@ -0,0 +1,11 @@
4957+#!/usr/bin/python
4958+
4959+"""Amulet tests on a basic openstack-dashboard deployment on trusty-liberty."""
4960+
4961+from basic_deployment import OpenstackDashboardBasicDeployment
4962+
4963+if __name__ == '__main__':
4964+ deployment = OpenstackDashboardBasicDeployment(series='trusty',
4965+ openstack='cloud:trusty-liberty',
4966+ source='cloud:trusty-updates/liberty')
4967+ deployment.run_tests()
4968
4969=== added file 'tests/019-basic-trusty-mitaka'
4970--- tests/019-basic-trusty-mitaka 1970-01-01 00:00:00 +0000
4971+++ tests/019-basic-trusty-mitaka 2016-02-18 14:28:13 +0000
4972@@ -0,0 +1,11 @@
4973+#!/usr/bin/python
4974+
4975+"""Amulet tests on a basic openstack-dashboard deployment on trusty-mitaka."""
4976+
4977+from basic_deployment import OpenstackDashboardBasicDeployment
4978+
4979+if __name__ == '__main__':
4980+ deployment = OpenstackDashboardBasicDeployment(series='trusty',
4981+ openstack='cloud:trusty-mitaka',
4982+ source='cloud:trusty-updates/mitaka')
4983+ deployment.run_tests()
4984
4985=== added file 'tests/020-basic-wily-liberty'
4986--- tests/020-basic-wily-liberty 1970-01-01 00:00:00 +0000
4987+++ tests/020-basic-wily-liberty 2016-02-18 14:28:13 +0000
4988@@ -0,0 +1,9 @@
4989+#!/usr/bin/python
4990+
4991+"""Amulet tests on a basic openstack-dashboard deployment on wily-liberty."""
4992+
4993+from basic_deployment import OpenstackDashboardBasicDeployment
4994+
4995+if __name__ == '__main__':
4996+ deployment = OpenstackDashboardBasicDeployment(series='wily')
4997+ deployment.run_tests()
4998
4999=== added file 'tests/021-basic-xenial-mitaka'
5000--- 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: