~felipe-alfaro-gmail/charms/xenial/neutron-api/trunk

« back to all changes in this revision

Viewing changes to hooks/charmhelpers/contrib/openstack/amulet/deployment.py

  • Committer: Felipe Alfaro Solana
  • Date: 2017-04-05 19:45:40 UTC
  • Revision ID: felipe.alfaro@gmail.com-20170405194540-85i0nhnp98ipob0y
Neutron API charm.

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# Copyright 2014-2015 Canonical Limited.
 
2
#
 
3
# Licensed under the Apache License, Version 2.0 (the "License");
 
4
# you may not use this file except in compliance with the License.
 
5
# You may obtain a copy of the License at
 
6
#
 
7
#  http://www.apache.org/licenses/LICENSE-2.0
 
8
#
 
9
# Unless required by applicable law or agreed to in writing, software
 
10
# distributed under the License is distributed on an "AS IS" BASIS,
 
11
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 
12
# See the License for the specific language governing permissions and
 
13
# limitations under the License.
 
14
 
 
15
import logging
 
16
import re
 
17
import sys
 
18
import six
 
19
from collections import OrderedDict
 
20
from charmhelpers.contrib.amulet.deployment import (
 
21
    AmuletDeployment
 
22
)
 
23
 
 
24
DEBUG = logging.DEBUG
 
25
ERROR = logging.ERROR
 
26
 
 
27
 
 
28
class OpenStackAmuletDeployment(AmuletDeployment):
 
29
    """OpenStack amulet deployment.
 
30
 
 
31
       This class inherits from AmuletDeployment and has additional support
 
32
       that is specifically for use by OpenStack charms.
 
33
       """
 
34
 
 
35
    def __init__(self, series=None, openstack=None, source=None,
 
36
                 stable=True, log_level=DEBUG):
 
37
        """Initialize the deployment environment."""
 
38
        super(OpenStackAmuletDeployment, self).__init__(series)
 
39
        self.log = self.get_logger(level=log_level)
 
40
        self.log.info('OpenStackAmuletDeployment:  init')
 
41
        self.openstack = openstack
 
42
        self.source = source
 
43
        self.stable = stable
 
44
 
 
45
    def get_logger(self, name="deployment-logger", level=logging.DEBUG):
 
46
        """Get a logger object that will log to stdout."""
 
47
        log = logging
 
48
        logger = log.getLogger(name)
 
49
        fmt = log.Formatter("%(asctime)s %(funcName)s "
 
50
                            "%(levelname)s: %(message)s")
 
51
 
 
52
        handler = log.StreamHandler(stream=sys.stdout)
 
53
        handler.setLevel(level)
 
54
        handler.setFormatter(fmt)
 
55
 
 
56
        logger.addHandler(handler)
 
57
        logger.setLevel(level)
 
58
 
 
59
        return logger
 
60
 
 
61
    def _determine_branch_locations(self, other_services):
 
62
        """Determine the branch locations for the other services.
 
63
 
 
64
           Determine if the local branch being tested is derived from its
 
65
           stable or next (dev) branch, and based on this, use the corresonding
 
66
           stable or next branches for the other_services."""
 
67
 
 
68
        self.log.info('OpenStackAmuletDeployment:  determine branch locations')
 
69
 
 
70
        # Charms outside the ~openstack-charmers
 
71
        base_charms = {
 
72
            'mysql': ['trusty'],
 
73
            'mongodb': ['trusty'],
 
74
            'nrpe': ['trusty', 'xenial'],
 
75
        }
 
76
 
 
77
        for svc in other_services:
 
78
            # If a location has been explicitly set, use it
 
79
            if svc.get('location'):
 
80
                continue
 
81
            if svc['name'] in base_charms:
 
82
                # NOTE: not all charms have support for all series we
 
83
                #       want/need to test against, so fix to most recent
 
84
                #       that each base charm supports
 
85
                target_series = self.series
 
86
                if self.series not in base_charms[svc['name']]:
 
87
                    target_series = base_charms[svc['name']][-1]
 
88
                svc['location'] = 'cs:{}/{}'.format(target_series,
 
89
                                                    svc['name'])
 
90
            elif self.stable:
 
91
                svc['location'] = 'cs:{}/{}'.format(self.series,
 
92
                                                    svc['name'])
 
93
            else:
 
94
                svc['location'] = 'cs:~openstack-charmers-next/{}/{}'.format(
 
95
                    self.series,
 
96
                    svc['name']
 
97
                )
 
98
 
 
99
        return other_services
 
100
 
 
101
    def _add_services(self, this_service, other_services, use_source=None,
 
102
                      no_origin=None):
 
103
        """Add services to the deployment and optionally set
 
104
        openstack-origin/source.
 
105
 
 
106
        :param this_service dict: Service dictionary describing the service
 
107
                                  whose amulet tests are being run
 
108
        :param other_services dict: List of service dictionaries describing
 
109
                                    the services needed to support the target
 
110
                                    service
 
111
        :param use_source list: List of services which use the 'source' config
 
112
                                option rather than 'openstack-origin'
 
113
        :param no_origin list: List of services which do not support setting
 
114
                               the Cloud Archive.
 
115
        Service Dict:
 
116
            {
 
117
                'name': str charm-name,
 
118
                'units': int number of units,
 
119
                'constraints': dict of juju constraints,
 
120
                'location': str location of charm,
 
121
            }
 
122
        eg
 
123
        this_service = {
 
124
            'name': 'openvswitch-odl',
 
125
            'constraints': {'mem': '8G'},
 
126
        }
 
127
        other_services = [
 
128
            {
 
129
                'name': 'nova-compute',
 
130
                'units': 2,
 
131
                'constraints': {'mem': '4G'},
 
132
                'location': cs:~bob/xenial/nova-compute
 
133
            },
 
134
            {
 
135
                'name': 'mysql',
 
136
                'constraints': {'mem': '2G'},
 
137
            },
 
138
            {'neutron-api-odl'}]
 
139
        use_source = ['mysql']
 
140
        no_origin = ['neutron-api-odl']
 
141
        """
 
142
        self.log.info('OpenStackAmuletDeployment:  adding services')
 
143
 
 
144
        other_services = self._determine_branch_locations(other_services)
 
145
 
 
146
        super(OpenStackAmuletDeployment, self)._add_services(this_service,
 
147
                                                             other_services)
 
148
 
 
149
        services = other_services
 
150
        services.append(this_service)
 
151
 
 
152
        use_source = use_source or []
 
153
        no_origin = no_origin or []
 
154
 
 
155
        # Charms which should use the source config option
 
156
        use_source = list(set(
 
157
            use_source + ['mysql', 'mongodb', 'rabbitmq-server', 'ceph',
 
158
                          'ceph-osd', 'ceph-radosgw', 'ceph-mon',
 
159
                          'ceph-proxy', 'percona-cluster', 'lxd']))
 
160
 
 
161
        # Charms which can not use openstack-origin, ie. many subordinates
 
162
        no_origin = list(set(
 
163
            no_origin + ['cinder-ceph', 'hacluster', 'neutron-openvswitch',
 
164
                         'nrpe', 'openvswitch-odl', 'neutron-api-odl',
 
165
                         'odl-controller', 'cinder-backup', 'nexentaedge-data',
 
166
                         'nexentaedge-iscsi-gw', 'nexentaedge-swift-gw',
 
167
                         'cinder-nexentaedge', 'nexentaedge-mgmt']))
 
168
 
 
169
        if self.openstack:
 
170
            for svc in services:
 
171
                if svc['name'] not in use_source + no_origin:
 
172
                    config = {'openstack-origin': self.openstack}
 
173
                    self.d.configure(svc['name'], config)
 
174
 
 
175
        if self.source:
 
176
            for svc in services:
 
177
                if svc['name'] in use_source and svc['name'] not in no_origin:
 
178
                    config = {'source': self.source}
 
179
                    self.d.configure(svc['name'], config)
 
180
 
 
181
    def _configure_services(self, configs):
 
182
        """Configure all of the services."""
 
183
        self.log.info('OpenStackAmuletDeployment:  configure services')
 
184
        for service, config in six.iteritems(configs):
 
185
            self.d.configure(service, config)
 
186
 
 
187
    def _auto_wait_for_status(self, message=None, exclude_services=None,
 
188
                              include_only=None, timeout=1800):
 
189
        """Wait for all units to have a specific extended status, except
 
190
        for any defined as excluded.  Unless specified via message, any
 
191
        status containing any case of 'ready' will be considered a match.
 
192
 
 
193
        Examples of message usage:
 
194
 
 
195
          Wait for all unit status to CONTAIN any case of 'ready' or 'ok':
 
196
              message = re.compile('.*ready.*|.*ok.*', re.IGNORECASE)
 
197
 
 
198
          Wait for all units to reach this status (exact match):
 
199
              message = re.compile('^Unit is ready and clustered$')
 
200
 
 
201
          Wait for all units to reach any one of these (exact match):
 
202
              message = re.compile('Unit is ready|OK|Ready')
 
203
 
 
204
          Wait for at least one unit to reach this status (exact match):
 
205
              message = {'ready'}
 
206
 
 
207
        See Amulet's sentry.wait_for_messages() for message usage detail.
 
208
        https://github.com/juju/amulet/blob/master/amulet/sentry.py
 
209
 
 
210
        :param message: Expected status match
 
211
        :param exclude_services: List of juju service names to ignore,
 
212
            not to be used in conjuction with include_only.
 
213
        :param include_only: List of juju service names to exclusively check,
 
214
            not to be used in conjuction with exclude_services.
 
215
        :param timeout: Maximum time in seconds to wait for status match
 
216
        :returns: None.  Raises if timeout is hit.
 
217
        """
 
218
        self.log.info('Waiting for extended status on units...')
 
219
 
 
220
        all_services = self.d.services.keys()
 
221
 
 
222
        if exclude_services and include_only:
 
223
            raise ValueError('exclude_services can not be used '
 
224
                             'with include_only')
 
225
 
 
226
        if message:
 
227
            if isinstance(message, re._pattern_type):
 
228
                match = message.pattern
 
229
            else:
 
230
                match = message
 
231
 
 
232
            self.log.debug('Custom extended status wait match: '
 
233
                           '{}'.format(match))
 
234
        else:
 
235
            self.log.debug('Default extended status wait match:  contains '
 
236
                           'READY (case-insensitive)')
 
237
            message = re.compile('.*ready.*', re.IGNORECASE)
 
238
 
 
239
        if exclude_services:
 
240
            self.log.debug('Excluding services from extended status match: '
 
241
                           '{}'.format(exclude_services))
 
242
        else:
 
243
            exclude_services = []
 
244
 
 
245
        if include_only:
 
246
            services = include_only
 
247
        else:
 
248
            services = list(set(all_services) - set(exclude_services))
 
249
 
 
250
        self.log.debug('Waiting up to {}s for extended status on services: '
 
251
                       '{}'.format(timeout, services))
 
252
        service_messages = {service: message for service in services}
 
253
        self.d.sentry.wait_for_messages(service_messages, timeout=timeout)
 
254
        self.log.info('OK')
 
255
 
 
256
    def _get_openstack_release(self):
 
257
        """Get openstack release.
 
258
 
 
259
           Return an integer representing the enum value of the openstack
 
260
           release.
 
261
           """
 
262
        # Must be ordered by OpenStack release (not by Ubuntu release):
 
263
        (self.trusty_icehouse, self.trusty_kilo, self.trusty_liberty,
 
264
         self.trusty_mitaka, self.xenial_mitaka, self.xenial_newton,
 
265
         self.yakkety_newton, self.xenial_ocata, self.zesty_ocata) = range(9)
 
266
 
 
267
        releases = {
 
268
            ('trusty', None): self.trusty_icehouse,
 
269
            ('trusty', 'cloud:trusty-kilo'): self.trusty_kilo,
 
270
            ('trusty', 'cloud:trusty-liberty'): self.trusty_liberty,
 
271
            ('trusty', 'cloud:trusty-mitaka'): self.trusty_mitaka,
 
272
            ('xenial', None): self.xenial_mitaka,
 
273
            ('xenial', 'cloud:xenial-newton'): self.xenial_newton,
 
274
            ('xenial', 'cloud:xenial-ocata'): self.xenial_ocata,
 
275
            ('yakkety', None): self.yakkety_newton,
 
276
            ('zesty', None): self.zesty_ocata,
 
277
        }
 
278
        return releases[(self.series, self.openstack)]
 
279
 
 
280
    def _get_openstack_release_string(self):
 
281
        """Get openstack release string.
 
282
 
 
283
           Return a string representing the openstack release.
 
284
           """
 
285
        releases = OrderedDict([
 
286
            ('trusty', 'icehouse'),
 
287
            ('xenial', 'mitaka'),
 
288
            ('yakkety', 'newton'),
 
289
            ('zesty', 'ocata'),
 
290
        ])
 
291
        if self.openstack:
 
292
            os_origin = self.openstack.split(':')[1]
 
293
            return os_origin.split('%s-' % self.series)[1].split('/')[0]
 
294
        else:
 
295
            return releases[self.series]
 
296
 
 
297
    def get_ceph_expected_pools(self, radosgw=False):
 
298
        """Return a list of expected ceph pools in a ceph + cinder + glance
 
299
        test scenario, based on OpenStack release and whether ceph radosgw
 
300
        is flagged as present or not."""
 
301
 
 
302
        if self._get_openstack_release() >= self.trusty_kilo:
 
303
            # Kilo or later
 
304
            pools = [
 
305
                'rbd',
 
306
                'cinder',
 
307
                'glance'
 
308
            ]
 
309
        else:
 
310
            # Juno or earlier
 
311
            pools = [
 
312
                'data',
 
313
                'metadata',
 
314
                'rbd',
 
315
                'cinder',
 
316
                'glance'
 
317
            ]
 
318
 
 
319
        if radosgw:
 
320
            pools.extend([
 
321
                '.rgw.root',
 
322
                '.rgw.control',
 
323
                '.rgw',
 
324
                '.rgw.gc',
 
325
                '.users.uid'
 
326
            ])
 
327
 
 
328
        return pools