1
# Copyright 2014-2015 Canonical Limited.
3
# This file is part of charm-helpers.
5
# charm-helpers is free software: you can redistribute it and/or modify
6
# it under the terms of the GNU Lesser General Public License version 3 as
7
# published by the Free Software Foundation.
9
# charm-helpers is distributed in the hope that it will be useful,
10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
# GNU Lesser General Public License for more details.
14
# You should have received a copy of the GNU Lesser General Public License
15
# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
21
from collections import OrderedDict
22
from charmhelpers.contrib.amulet.deployment import (
30
class OpenStackAmuletDeployment(AmuletDeployment):
31
"""OpenStack amulet deployment.
33
This class inherits from AmuletDeployment and has additional support
34
that is specifically for use by OpenStack charms.
37
def __init__(self, series=None, openstack=None, source=None,
38
stable=True, log_level=DEBUG):
39
"""Initialize the deployment environment."""
40
super(OpenStackAmuletDeployment, self).__init__(series)
41
self.log = self.get_logger(level=log_level)
42
self.log.info('OpenStackAmuletDeployment: init')
43
self.openstack = openstack
46
# Note(coreycb): this needs to be changed when new next branches come
48
self.current_next = "trusty"
50
def get_logger(self, name="deployment-logger", level=logging.DEBUG):
51
"""Get a logger object that will log to stdout."""
53
logger = log.getLogger(name)
54
fmt = log.Formatter("%(asctime)s %(funcName)s "
55
"%(levelname)s: %(message)s")
57
handler = log.StreamHandler(stream=sys.stdout)
58
handler.setLevel(level)
59
handler.setFormatter(fmt)
61
logger.addHandler(handler)
62
logger.setLevel(level)
66
def _determine_branch_locations(self, other_services):
67
"""Determine the branch locations for the other services.
69
Determine if the local branch being tested is derived from its
70
stable or next (dev) branch, and based on this, use the corresonding
71
stable or next branches for the other_services."""
73
self.log.info('OpenStackAmuletDeployment: determine branch locations')
75
# Charms outside the lp:~openstack-charmers namespace
76
base_charms = ['mysql', 'mongodb', 'nrpe']
78
# Force these charms to current series even when using an older series.
79
# ie. Use trusty/nrpe even when series is precise, as the P charm
80
# does not possess the necessary external master config and hooks.
81
force_series_current = ['nrpe']
83
if self.series in ['precise', 'trusty']:
84
base_series = self.series
86
base_series = self.current_next
88
for svc in other_services:
89
if svc['name'] in force_series_current:
90
base_series = self.current_next
91
# If a location has been explicitly set, use it
92
if svc.get('location'):
95
temp = 'lp:charms/{}/{}'
96
svc['location'] = temp.format(base_series,
99
if svc['name'] in base_charms:
100
temp = 'lp:charms/{}/{}'
101
svc['location'] = temp.format(base_series,
104
temp = 'lp:~openstack-charmers/charms/{}/{}/next'
105
svc['location'] = temp.format(self.current_next,
108
return other_services
110
def _add_services(self, this_service, other_services):
111
"""Add services to the deployment and set openstack-origin/source."""
112
self.log.info('OpenStackAmuletDeployment: adding services')
114
other_services = self._determine_branch_locations(other_services)
116
super(OpenStackAmuletDeployment, self)._add_services(this_service,
119
services = other_services
120
services.append(this_service)
122
# Charms which should use the source config option
123
use_source = ['mysql', 'mongodb', 'rabbitmq-server', 'ceph',
124
'ceph-osd', 'ceph-radosgw']
126
# Charms which can not use openstack-origin, ie. many subordinates
127
no_origin = ['cinder-ceph', 'hacluster', 'neutron-openvswitch', 'nrpe']
131
if svc['name'] not in use_source + no_origin:
132
config = {'openstack-origin': self.openstack}
133
self.d.configure(svc['name'], config)
137
if svc['name'] in use_source and svc['name'] not in no_origin:
138
config = {'source': self.source}
139
self.d.configure(svc['name'], config)
141
def _configure_services(self, configs):
142
"""Configure all of the services."""
143
self.log.info('OpenStackAmuletDeployment: configure services')
144
for service, config in six.iteritems(configs):
145
self.d.configure(service, config)
147
def _auto_wait_for_status(self, message=None, exclude_services=None,
148
include_only=None, timeout=1800):
149
"""Wait for all units to have a specific extended status, except
150
for any defined as excluded. Unless specified via message, any
151
status containing any case of 'ready' will be considered a match.
153
Examples of message usage:
155
Wait for all unit status to CONTAIN any case of 'ready' or 'ok':
156
message = re.compile('.*ready.*|.*ok.*', re.IGNORECASE)
158
Wait for all units to reach this status (exact match):
159
message = re.compile('^Unit is ready and clustered$')
161
Wait for all units to reach any one of these (exact match):
162
message = re.compile('Unit is ready|OK|Ready')
164
Wait for at least one unit to reach this status (exact match):
167
See Amulet's sentry.wait_for_messages() for message usage detail.
168
https://github.com/juju/amulet/blob/master/amulet/sentry.py
170
:param message: Expected status match
171
:param exclude_services: List of juju service names to ignore,
172
not to be used in conjuction with include_only.
173
:param include_only: List of juju service names to exclusively check,
174
not to be used in conjuction with exclude_services.
175
:param timeout: Maximum time in seconds to wait for status match
176
:returns: None. Raises if timeout is hit.
178
self.log.info('Waiting for extended status on units...')
180
all_services = self.d.services.keys()
182
if exclude_services and include_only:
183
raise ValueError('exclude_services can not be used '
187
if isinstance(message, re._pattern_type):
188
match = message.pattern
192
self.log.debug('Custom extended status wait match: '
195
self.log.debug('Default extended status wait match: contains '
196
'READY (case-insensitive)')
197
message = re.compile('.*ready.*', re.IGNORECASE)
200
self.log.debug('Excluding services from extended status match: '
201
'{}'.format(exclude_services))
203
exclude_services = []
206
services = include_only
208
services = list(set(all_services) - set(exclude_services))
210
self.log.debug('Waiting up to {}s for extended status on services: '
211
'{}'.format(timeout, services))
212
service_messages = {service: message for service in services}
213
self.d.sentry.wait_for_messages(service_messages, timeout=timeout)
216
def _get_openstack_release(self):
217
"""Get openstack release.
219
Return an integer representing the enum value of the openstack
222
# Must be ordered by OpenStack release (not by Ubuntu release):
223
(self.precise_essex, self.precise_folsom, self.precise_grizzly,
224
self.precise_havana, self.precise_icehouse,
225
self.trusty_icehouse, self.trusty_juno, self.utopic_juno,
226
self.trusty_kilo, self.vivid_kilo, self.trusty_liberty,
227
self.wily_liberty) = range(12)
230
('precise', None): self.precise_essex,
231
('precise', 'cloud:precise-folsom'): self.precise_folsom,
232
('precise', 'cloud:precise-grizzly'): self.precise_grizzly,
233
('precise', 'cloud:precise-havana'): self.precise_havana,
234
('precise', 'cloud:precise-icehouse'): self.precise_icehouse,
235
('trusty', None): self.trusty_icehouse,
236
('trusty', 'cloud:trusty-juno'): self.trusty_juno,
237
('trusty', 'cloud:trusty-kilo'): self.trusty_kilo,
238
('trusty', 'cloud:trusty-liberty'): self.trusty_liberty,
239
('utopic', None): self.utopic_juno,
240
('vivid', None): self.vivid_kilo,
241
('wily', None): self.wily_liberty}
242
return releases[(self.series, self.openstack)]
244
def _get_openstack_release_string(self):
245
"""Get openstack release string.
247
Return a string representing the openstack release.
249
releases = OrderedDict([
250
('precise', 'essex'),
251
('quantal', 'folsom'),
252
('raring', 'grizzly'),
254
('trusty', 'icehouse'),
260
os_origin = self.openstack.split(':')[1]
261
return os_origin.split('%s-' % self.series)[1].split('/')[0]
263
return releases[self.series]
265
def get_ceph_expected_pools(self, radosgw=False):
266
"""Return a list of expected ceph pools in a ceph + cinder + glance
267
test scenario, based on OpenStack release and whether ceph radosgw
268
is flagged as present or not."""
270
if self._get_openstack_release() >= self.trusty_kilo: