Merge lp:~corey.bryant/charms/trusty/glance/sync-ch into lp:~openstack-charmers-archive/charms/trusty/glance/next

Proposed by Corey Bryant
Status: Merged
Merged at revision: 128
Proposed branch: lp:~corey.bryant/charms/trusty/glance/sync-ch
Merge into: lp:~openstack-charmers-archive/charms/trusty/glance/next
Diff against target: 1095 lines (+437/-170)
15 files modified
hooks/charmhelpers/contrib/openstack/amulet/deployment.py (+36/-3)
hooks/charmhelpers/contrib/openstack/amulet/utils.py (+240/-49)
hooks/charmhelpers/contrib/openstack/context.py (+8/-7)
hooks/charmhelpers/contrib/openstack/templates/ceph.conf (+6/-6)
hooks/charmhelpers/contrib/openstack/utils.py (+9/-5)
hooks/charmhelpers/contrib/storage/linux/ceph.py (+6/-6)
hooks/charmhelpers/core/hookenv.py (+1/-0)
hooks/charmhelpers/core/host.py (+31/-5)
hooks/charmhelpers/core/services/helpers.py (+2/-2)
hooks/charmhelpers/fetch/__init__.py (+23/-14)
hooks/charmhelpers/fetch/archiveurl.py (+7/-1)
hooks/charmhelpers/fetch/giturl.py (+1/-1)
tests/charmhelpers/contrib/amulet/utils.py (+38/-46)
tests/charmhelpers/contrib/openstack/amulet/deployment.py (+3/-2)
tests/charmhelpers/contrib/openstack/amulet/utils.py (+26/-23)
To merge this branch: bzr merge lp:~corey.bryant/charms/trusty/glance/sync-ch
Reviewer Review Type Date Requested Status
OpenStack Charmers Pending
Review via email: mp+265047@code.launchpad.net
To post a comment you must log in.
Revision history for this message
uosci-testing-bot (uosci-testing-bot) wrote :

charm_lint_check #6285 glance-next for corey.bryant mp265047
    LINT OK: passed

Build: http://10.245.162.77:8080/job/charm_lint_check/6285/

Revision history for this message
uosci-testing-bot (uosci-testing-bot) wrote :

charm_unit_test #5917 glance-next for corey.bryant mp265047
    UNIT OK: passed

Build: http://10.245.162.77:8080/job/charm_unit_test/5917/

Revision history for this message
uosci-testing-bot (uosci-testing-bot) wrote :

charm_amulet_test #5140 glance-next for corey.bryant mp265047
    AMULET OK: passed

Build: http://10.245.162.77:8080/job/charm_amulet_test/5140/

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'hooks/charmhelpers/contrib/openstack/amulet/deployment.py'
--- hooks/charmhelpers/contrib/openstack/amulet/deployment.py 2015-06-19 15:08:48 +0000
+++ hooks/charmhelpers/contrib/openstack/amulet/deployment.py 2015-07-16 20:31:40 +0000
@@ -79,9 +79,9 @@
79 services.append(this_service)79 services.append(this_service)
80 use_source = ['mysql', 'mongodb', 'rabbitmq-server', 'ceph',80 use_source = ['mysql', 'mongodb', 'rabbitmq-server', 'ceph',
81 'ceph-osd', 'ceph-radosgw']81 'ceph-osd', 'ceph-radosgw']
82 # Openstack subordinate charms do not expose an origin option as that82 # Most OpenStack subordinate charms do not expose an origin option
83 # is controlled by the principle83 # as that is controlled by the principle.
84 ignore = ['neutron-openvswitch']84 ignore = ['cinder-ceph', 'hacluster', 'neutron-openvswitch']
8585
86 if self.openstack:86 if self.openstack:
87 for svc in services:87 for svc in services:
@@ -148,3 +148,36 @@
148 return os_origin.split('%s-' % self.series)[1].split('/')[0]148 return os_origin.split('%s-' % self.series)[1].split('/')[0]
149 else:149 else:
150 return releases[self.series]150 return releases[self.series]
151
152 def get_ceph_expected_pools(self, radosgw=False):
153 """Return a list of expected ceph pools in a ceph + cinder + glance
154 test scenario, based on OpenStack release and whether ceph radosgw
155 is flagged as present or not."""
156
157 if self._get_openstack_release() >= self.trusty_kilo:
158 # Kilo or later
159 pools = [
160 'rbd',
161 'cinder',
162 'glance'
163 ]
164 else:
165 # Juno or earlier
166 pools = [
167 'data',
168 'metadata',
169 'rbd',
170 'cinder',
171 'glance'
172 ]
173
174 if radosgw:
175 pools.extend([
176 '.rgw.root',
177 '.rgw.control',
178 '.rgw',
179 '.rgw.gc',
180 '.users.uid'
181 ])
182
183 return pools
151184
=== modified file 'hooks/charmhelpers/contrib/openstack/amulet/utils.py'
--- hooks/charmhelpers/contrib/openstack/amulet/utils.py 2015-06-19 15:08:48 +0000
+++ hooks/charmhelpers/contrib/openstack/amulet/utils.py 2015-07-16 20:31:40 +0000
@@ -14,16 +14,20 @@
14# You should have received a copy of the GNU Lesser General Public License14# You should have received a copy of the GNU Lesser General Public License
15# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.15# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
1616
17import amulet
18import json
17import logging19import logging
18import os20import os
19import six21import six
20import time22import time
21import urllib23import urllib
2224
25import cinderclient.v1.client as cinder_client
23import glanceclient.v1.client as glance_client26import glanceclient.v1.client as glance_client
24import heatclient.v1.client as heat_client27import heatclient.v1.client as heat_client
25import keystoneclient.v2_0 as keystone_client28import keystoneclient.v2_0 as keystone_client
26import novaclient.v1_1.client as nova_client29import novaclient.v1_1.client as nova_client
30import swiftclient
2731
28from charmhelpers.contrib.amulet.utils import (32from charmhelpers.contrib.amulet.utils import (
29 AmuletUtils33 AmuletUtils
@@ -171,6 +175,16 @@
171 self.log.debug('Checking if tenant exists ({})...'.format(tenant))175 self.log.debug('Checking if tenant exists ({})...'.format(tenant))
172 return tenant in [t.name for t in keystone.tenants.list()]176 return tenant in [t.name for t in keystone.tenants.list()]
173177
178 def authenticate_cinder_admin(self, keystone_sentry, username,
179 password, tenant):
180 """Authenticates admin user with cinder."""
181 # NOTE(beisner): cinder python client doesn't accept tokens.
182 service_ip = \
183 keystone_sentry.relation('shared-db',
184 'mysql:shared-db')['private-address']
185 ept = "http://{}:5000/v2.0".format(service_ip.strip().decode('utf-8'))
186 return cinder_client.Client(username, password, tenant, ept)
187
174 def authenticate_keystone_admin(self, keystone_sentry, user, password,188 def authenticate_keystone_admin(self, keystone_sentry, user, password,
175 tenant):189 tenant):
176 """Authenticates admin user with the keystone admin endpoint."""190 """Authenticates admin user with the keystone admin endpoint."""
@@ -212,9 +226,29 @@
212 return nova_client.Client(username=user, api_key=password,226 return nova_client.Client(username=user, api_key=password,
213 project_id=tenant, auth_url=ep)227 project_id=tenant, auth_url=ep)
214228
229 def authenticate_swift_user(self, keystone, user, password, tenant):
230 """Authenticates a regular user with swift api."""
231 self.log.debug('Authenticating swift user ({})...'.format(user))
232 ep = keystone.service_catalog.url_for(service_type='identity',
233 endpoint_type='publicURL')
234 return swiftclient.Connection(authurl=ep,
235 user=user,
236 key=password,
237 tenant_name=tenant,
238 auth_version='2.0')
239
215 def create_cirros_image(self, glance, image_name):240 def create_cirros_image(self, glance, image_name):
216 """Download the latest cirros image and upload it to glance."""241 """Download the latest cirros image and upload it to glance,
217 self.log.debug('Creating glance image ({})...'.format(image_name))242 validate and return a resource pointer.
243
244 :param glance: pointer to authenticated glance connection
245 :param image_name: display name for new image
246 :returns: glance image pointer
247 """
248 self.log.debug('Creating glance cirros image '
249 '({})...'.format(image_name))
250
251 # Download cirros image
218 http_proxy = os.getenv('AMULET_HTTP_PROXY')252 http_proxy = os.getenv('AMULET_HTTP_PROXY')
219 self.log.debug('AMULET_HTTP_PROXY: {}'.format(http_proxy))253 self.log.debug('AMULET_HTTP_PROXY: {}'.format(http_proxy))
220 if http_proxy:254 if http_proxy:
@@ -223,33 +257,51 @@
223 else:257 else:
224 opener = urllib.FancyURLopener()258 opener = urllib.FancyURLopener()
225259
226 f = opener.open("http://download.cirros-cloud.net/version/released")260 f = opener.open('http://download.cirros-cloud.net/version/released')
227 version = f.read().strip()261 version = f.read().strip()
228 cirros_img = "cirros-{}-x86_64-disk.img".format(version)262 cirros_img = 'cirros-{}-x86_64-disk.img'.format(version)
229 local_path = os.path.join('tests', cirros_img)263 local_path = os.path.join('tests', cirros_img)
230264
231 if not os.path.exists(local_path):265 if not os.path.exists(local_path):
232 cirros_url = "http://{}/{}/{}".format("download.cirros-cloud.net",266 cirros_url = 'http://{}/{}/{}'.format('download.cirros-cloud.net',
233 version, cirros_img)267 version, cirros_img)
234 opener.retrieve(cirros_url, local_path)268 opener.retrieve(cirros_url, local_path)
235 f.close()269 f.close()
236270
271 # Create glance image
237 with open(local_path) as f:272 with open(local_path) as f:
238 image = glance.images.create(name=image_name, is_public=True,273 image = glance.images.create(name=image_name, is_public=True,
239 disk_format='qcow2',274 disk_format='qcow2',
240 container_format='bare', data=f)275 container_format='bare', data=f)
241 count = 1276
242 status = image.status277 # Wait for image to reach active status
243 while status != 'active' and count < 10:278 img_id = image.id
244 time.sleep(3)279 ret = self.resource_reaches_status(glance.images, img_id,
245 image = glance.images.get(image.id)280 expected_stat='active',
246 status = image.status281 msg='Image status wait')
247 self.log.debug('image status: {}'.format(status))282 if not ret:
248 count += 1283 msg = 'Glance image failed to reach expected state.'
249284 amulet.raise_status(amulet.FAIL, msg=msg)
250 if status != 'active':285
251 self.log.error('image creation timed out')286 # Re-validate new image
252 return None287 self.log.debug('Validating image attributes...')
288 val_img_name = glance.images.get(img_id).name
289 val_img_stat = glance.images.get(img_id).status
290 val_img_pub = glance.images.get(img_id).is_public
291 val_img_cfmt = glance.images.get(img_id).container_format
292 val_img_dfmt = glance.images.get(img_id).disk_format
293 msg_attr = ('Image attributes - name:{} public:{} id:{} stat:{} '
294 'container fmt:{} disk fmt:{}'.format(
295 val_img_name, val_img_pub, img_id,
296 val_img_stat, val_img_cfmt, val_img_dfmt))
297
298 if val_img_name == image_name and val_img_stat == 'active' \
299 and val_img_pub is True and val_img_cfmt == 'bare' \
300 and val_img_dfmt == 'qcow2':
301 self.log.debug(msg_attr)
302 else:
303 msg = ('Volume validation failed, {}'.format(msg_attr))
304 amulet.raise_status(amulet.FAIL, msg=msg)
253305
254 return image306 return image
255307
@@ -260,22 +312,7 @@
260 self.log.warn('/!\\ DEPRECATION WARNING: use '312 self.log.warn('/!\\ DEPRECATION WARNING: use '
261 'delete_resource instead of delete_image.')313 'delete_resource instead of delete_image.')
262 self.log.debug('Deleting glance image ({})...'.format(image))314 self.log.debug('Deleting glance image ({})...'.format(image))
263 num_before = len(list(glance.images.list()))315 return self.delete_resource(glance.images, image, msg='glance image')
264 glance.images.delete(image)
265
266 count = 1
267 num_after = len(list(glance.images.list()))
268 while num_after != (num_before - 1) and count < 10:
269 time.sleep(3)
270 num_after = len(list(glance.images.list()))
271 self.log.debug('number of images: {}'.format(num_after))
272 count += 1
273
274 if num_after != (num_before - 1):
275 self.log.error('image deletion timed out')
276 return False
277
278 return True
279316
280 def create_instance(self, nova, image_name, instance_name, flavor):317 def create_instance(self, nova, image_name, instance_name, flavor):
281 """Create the specified instance."""318 """Create the specified instance."""
@@ -308,22 +345,8 @@
308 self.log.warn('/!\\ DEPRECATION WARNING: use '345 self.log.warn('/!\\ DEPRECATION WARNING: use '
309 'delete_resource instead of delete_instance.')346 'delete_resource instead of delete_instance.')
310 self.log.debug('Deleting instance ({})...'.format(instance))347 self.log.debug('Deleting instance ({})...'.format(instance))
311 num_before = len(list(nova.servers.list()))348 return self.delete_resource(nova.servers, instance,
312 nova.servers.delete(instance)349 msg='nova instance')
313
314 count = 1
315 num_after = len(list(nova.servers.list()))
316 while num_after != (num_before - 1) and count < 10:
317 time.sleep(3)
318 num_after = len(list(nova.servers.list()))
319 self.log.debug('number of instances: {}'.format(num_after))
320 count += 1
321
322 if num_after != (num_before - 1):
323 self.log.error('instance deletion timed out')
324 return False
325
326 return True
327350
328 def create_or_get_keypair(self, nova, keypair_name="testkey"):351 def create_or_get_keypair(self, nova, keypair_name="testkey"):
329 """Create a new keypair, or return pointer if it already exists."""352 """Create a new keypair, or return pointer if it already exists."""
@@ -339,6 +362,88 @@
339 _keypair = nova.keypairs.create(name=keypair_name)362 _keypair = nova.keypairs.create(name=keypair_name)
340 return _keypair363 return _keypair
341364
365 def create_cinder_volume(self, cinder, vol_name="demo-vol", vol_size=1,
366 img_id=None, src_vol_id=None, snap_id=None):
367 """Create cinder volume, optionally from a glance image, OR
368 optionally as a clone of an existing volume, OR optionally
369 from a snapshot. Wait for the new volume status to reach
370 the expected status, validate and return a resource pointer.
371
372 :param vol_name: cinder volume display name
373 :param vol_size: size in gigabytes
374 :param img_id: optional glance image id
375 :param src_vol_id: optional source volume id to clone
376 :param snap_id: optional snapshot id to use
377 :returns: cinder volume pointer
378 """
379 # Handle parameter input and avoid impossible combinations
380 if img_id and not src_vol_id and not snap_id:
381 # Create volume from image
382 self.log.debug('Creating cinder volume from glance image...')
383 bootable = 'true'
384 elif src_vol_id and not img_id and not snap_id:
385 # Clone an existing volume
386 self.log.debug('Cloning cinder volume...')
387 bootable = cinder.volumes.get(src_vol_id).bootable
388 elif snap_id and not src_vol_id and not img_id:
389 # Create volume from snapshot
390 self.log.debug('Creating cinder volume from snapshot...')
391 snap = cinder.volume_snapshots.find(id=snap_id)
392 vol_size = snap.size
393 snap_vol_id = cinder.volume_snapshots.get(snap_id).volume_id
394 bootable = cinder.volumes.get(snap_vol_id).bootable
395 elif not img_id and not src_vol_id and not snap_id:
396 # Create volume
397 self.log.debug('Creating cinder volume...')
398 bootable = 'false'
399 else:
400 # Impossible combination of parameters
401 msg = ('Invalid method use - name:{} size:{} img_id:{} '
402 'src_vol_id:{} snap_id:{}'.format(vol_name, vol_size,
403 img_id, src_vol_id,
404 snap_id))
405 amulet.raise_status(amulet.FAIL, msg=msg)
406
407 # Create new volume
408 try:
409 vol_new = cinder.volumes.create(display_name=vol_name,
410 imageRef=img_id,
411 size=vol_size,
412 source_volid=src_vol_id,
413 snapshot_id=snap_id)
414 vol_id = vol_new.id
415 except Exception as e:
416 msg = 'Failed to create volume: {}'.format(e)
417 amulet.raise_status(amulet.FAIL, msg=msg)
418
419 # Wait for volume to reach available status
420 ret = self.resource_reaches_status(cinder.volumes, vol_id,
421 expected_stat="available",
422 msg="Volume status wait")
423 if not ret:
424 msg = 'Cinder volume failed to reach expected state.'
425 amulet.raise_status(amulet.FAIL, msg=msg)
426
427 # Re-validate new volume
428 self.log.debug('Validating volume attributes...')
429 val_vol_name = cinder.volumes.get(vol_id).display_name
430 val_vol_boot = cinder.volumes.get(vol_id).bootable
431 val_vol_stat = cinder.volumes.get(vol_id).status
432 val_vol_size = cinder.volumes.get(vol_id).size
433 msg_attr = ('Volume attributes - name:{} id:{} stat:{} boot:'
434 '{} size:{}'.format(val_vol_name, vol_id,
435 val_vol_stat, val_vol_boot,
436 val_vol_size))
437
438 if val_vol_boot == bootable and val_vol_stat == 'available' \
439 and val_vol_name == vol_name and val_vol_size == vol_size:
440 self.log.debug(msg_attr)
441 else:
442 msg = ('Volume validation failed, {}'.format(msg_attr))
443 amulet.raise_status(amulet.FAIL, msg=msg)
444
445 return vol_new
446
342 def delete_resource(self, resource, resource_id,447 def delete_resource(self, resource, resource_id,
343 msg="resource", max_wait=120):448 msg="resource", max_wait=120):
344 """Delete one openstack resource, such as one instance, keypair,449 """Delete one openstack resource, such as one instance, keypair,
@@ -350,6 +455,8 @@
350 :param max_wait: maximum wait time in seconds455 :param max_wait: maximum wait time in seconds
351 :returns: True if successful, otherwise False456 :returns: True if successful, otherwise False
352 """457 """
458 self.log.debug('Deleting OpenStack resource '
459 '{} ({})'.format(resource_id, msg))
353 num_before = len(list(resource.list()))460 num_before = len(list(resource.list()))
354 resource.delete(resource_id)461 resource.delete(resource_id)
355462
@@ -411,3 +518,87 @@
411 self.log.debug('{} never reached expected status: '518 self.log.debug('{} never reached expected status: '
412 '{}'.format(resource_id, expected_stat))519 '{}'.format(resource_id, expected_stat))
413 return False520 return False
521
522 def get_ceph_osd_id_cmd(self, index):
523 """Produce a shell command that will return a ceph-osd id."""
524 return ("`initctl list | grep 'ceph-osd ' | "
525 "awk 'NR=={} {{ print $2 }}' | "
526 "grep -o '[0-9]*'`".format(index + 1))
527
528 def get_ceph_pools(self, sentry_unit):
529 """Return a dict of ceph pools from a single ceph unit, with
530 pool name as keys, pool id as vals."""
531 pools = {}
532 cmd = 'sudo ceph osd lspools'
533 output, code = sentry_unit.run(cmd)
534 if code != 0:
535 msg = ('{} `{}` returned {} '
536 '{}'.format(sentry_unit.info['unit_name'],
537 cmd, code, output))
538 amulet.raise_status(amulet.FAIL, msg=msg)
539
540 # Example output: 0 data,1 metadata,2 rbd,3 cinder,4 glance,
541 for pool in str(output).split(','):
542 pool_id_name = pool.split(' ')
543 if len(pool_id_name) == 2:
544 pool_id = pool_id_name[0]
545 pool_name = pool_id_name[1]
546 pools[pool_name] = int(pool_id)
547
548 self.log.debug('Pools on {}: {}'.format(sentry_unit.info['unit_name'],
549 pools))
550 return pools
551
552 def get_ceph_df(self, sentry_unit):
553 """Return dict of ceph df json output, including ceph pool state.
554
555 :param sentry_unit: Pointer to amulet sentry instance (juju unit)
556 :returns: Dict of ceph df output
557 """
558 cmd = 'sudo ceph df --format=json'
559 output, code = sentry_unit.run(cmd)
560 if code != 0:
561 msg = ('{} `{}` returned {} '
562 '{}'.format(sentry_unit.info['unit_name'],
563 cmd, code, output))
564 amulet.raise_status(amulet.FAIL, msg=msg)
565 return json.loads(output)
566
567 def get_ceph_pool_sample(self, sentry_unit, pool_id=0):
568 """Take a sample of attributes of a ceph pool, returning ceph
569 pool name, object count and disk space used for the specified
570 pool ID number.
571
572 :param sentry_unit: Pointer to amulet sentry instance (juju unit)
573 :param pool_id: Ceph pool ID
574 :returns: List of pool name, object count, kb disk space used
575 """
576 df = self.get_ceph_df(sentry_unit)
577 pool_name = df['pools'][pool_id]['name']
578 obj_count = df['pools'][pool_id]['stats']['objects']
579 kb_used = df['pools'][pool_id]['stats']['kb_used']
580 self.log.debug('Ceph {} pool (ID {}): {} objects, '
581 '{} kb used'.format(pool_name, pool_id,
582 obj_count, kb_used))
583 return pool_name, obj_count, kb_used
584
585 def validate_ceph_pool_samples(self, samples, sample_type="resource pool"):
586 """Validate ceph pool samples taken over time, such as pool
587 object counts or pool kb used, before adding, after adding, and
588 after deleting items which affect those pool attributes. The
589 2nd element is expected to be greater than the 1st; 3rd is expected
590 to be less than the 2nd.
591
592 :param samples: List containing 3 data samples
593 :param sample_type: String for logging and usage context
594 :returns: None if successful, Failure message otherwise
595 """
596 original, created, deleted = range(3)
597 if samples[created] <= samples[original] or \
598 samples[deleted] >= samples[created]:
599 return ('Ceph {} samples ({}) '
600 'unexpected.'.format(sample_type, samples))
601 else:
602 self.log.debug('Ceph {} samples (OK): '
603 '{}'.format(sample_type, samples))
604 return None
414605
=== modified file 'hooks/charmhelpers/contrib/openstack/context.py'
--- hooks/charmhelpers/contrib/openstack/context.py 2015-06-19 15:08:48 +0000
+++ hooks/charmhelpers/contrib/openstack/context.py 2015-07-16 20:31:40 +0000
@@ -122,21 +122,24 @@
122 of specifying multiple key value pairs within the same string. For122 of specifying multiple key value pairs within the same string. For
123 example, a string in the format of 'key1=value1, key2=value2' will123 example, a string in the format of 'key1=value1, key2=value2' will
124 return a dict of:124 return a dict of:
125 {'key1': 'value1',125
126 'key2': 'value2'}.126 {'key1': 'value1',
127 'key2': 'value2'}.
127128
128 2. A string in the above format, but supporting a comma-delimited list129 2. A string in the above format, but supporting a comma-delimited list
129 of values for the same key. For example, a string in the format of130 of values for the same key. For example, a string in the format of
130 'key1=value1, key2=value3,value4,value5' will return a dict of:131 'key1=value1, key2=value3,value4,value5' will return a dict of:
131 {'key1', 'value1',132
132 'key2', 'value2,value3,value4'}133 {'key1', 'value1',
134 'key2', 'value2,value3,value4'}
133135
134 3. A string containing a colon character (:) prior to an equal136 3. A string containing a colon character (:) prior to an equal
135 character (=) will be treated as yaml and parsed as such. This can be137 character (=) will be treated as yaml and parsed as such. This can be
136 used to specify more complex key value pairs. For example,138 used to specify more complex key value pairs. For example,
137 a string in the format of 'key1: subkey1=value1, subkey2=value2' will139 a string in the format of 'key1: subkey1=value1, subkey2=value2' will
138 return a dict of:140 return a dict of:
139 {'key1', 'subkey1=value1, subkey2=value2'}141
142 {'key1', 'subkey1=value1, subkey2=value2'}
140143
141 The provided config_flags string may be a list of comma-separated values144 The provided config_flags string may be a list of comma-separated values
142 which themselves may be comma-separated list of values.145 which themselves may be comma-separated list of values.
@@ -891,8 +894,6 @@
891 return ctxt894 return ctxt
892895
893 def __call__(self):896 def __call__(self):
894 self._ensure_packages()
895
896 if self.network_manager not in ['quantum', 'neutron']:897 if self.network_manager not in ['quantum', 'neutron']:
897 return {}898 return {}
898899
899900
=== modified file 'hooks/charmhelpers/contrib/openstack/templates/ceph.conf'
--- hooks/charmhelpers/contrib/openstack/templates/ceph.conf 2014-06-25 11:34:08 +0000
+++ hooks/charmhelpers/contrib/openstack/templates/ceph.conf 2015-07-16 20:31:40 +0000
@@ -5,11 +5,11 @@
5###############################################################################5###############################################################################
6[global]6[global]
7{% if auth -%}7{% if auth -%}
8 auth_supported = {{ auth }}8auth_supported = {{ auth }}
9 keyring = /etc/ceph/$cluster.$name.keyring9keyring = /etc/ceph/$cluster.$name.keyring
10 mon host = {{ mon_hosts }}10mon host = {{ mon_hosts }}
11{% endif -%}11{% endif -%}
12 log to syslog = {{ use_syslog }}12log to syslog = {{ use_syslog }}
13 err to syslog = {{ use_syslog }}13err to syslog = {{ use_syslog }}
14 clog to syslog = {{ use_syslog }}14clog to syslog = {{ use_syslog }}
1515
1616
=== modified file 'hooks/charmhelpers/contrib/openstack/utils.py'
--- hooks/charmhelpers/contrib/openstack/utils.py 2015-06-19 15:08:48 +0000
+++ hooks/charmhelpers/contrib/openstack/utils.py 2015-07-16 20:31:40 +0000
@@ -522,6 +522,7 @@
522 Clone/install all specified OpenStack repositories.522 Clone/install all specified OpenStack repositories.
523523
524 The expected format of projects_yaml is:524 The expected format of projects_yaml is:
525
525 repositories:526 repositories:
526 - {name: keystone,527 - {name: keystone,
527 repository: 'git://git.openstack.org/openstack/keystone.git',528 repository: 'git://git.openstack.org/openstack/keystone.git',
@@ -529,11 +530,13 @@
529 - {name: requirements,530 - {name: requirements,
530 repository: 'git://git.openstack.org/openstack/requirements.git',531 repository: 'git://git.openstack.org/openstack/requirements.git',
531 branch: 'stable/icehouse'}532 branch: 'stable/icehouse'}
533
532 directory: /mnt/openstack-git534 directory: /mnt/openstack-git
533 http_proxy: squid-proxy-url535 http_proxy: squid-proxy-url
534 https_proxy: squid-proxy-url536 https_proxy: squid-proxy-url
535537
536 The directory, http_proxy, and https_proxy keys are optional.538 The directory, http_proxy, and https_proxy keys are optional.
539
537 """540 """
538 global requirements_dir541 global requirements_dir
539 parent_dir = '/mnt/openstack-git'542 parent_dir = '/mnt/openstack-git'
@@ -555,10 +558,11 @@
555558
556 pip_create_virtualenv(os.path.join(parent_dir, 'venv'))559 pip_create_virtualenv(os.path.join(parent_dir, 'venv'))
557560
558 # Upgrade setuptools from default virtualenv version. The default version561 # Upgrade setuptools and pip from default virtualenv versions. The default
559 # in trusty breaks update.py in global requirements master branch.562 # versions in trusty break master OpenStack branch deployments.
560 pip_install('setuptools', upgrade=True, proxy=http_proxy,563 for p in ['pip', 'setuptools']:
561 venv=os.path.join(parent_dir, 'venv'))564 pip_install(p, upgrade=True, proxy=http_proxy,
565 venv=os.path.join(parent_dir, 'venv'))
562566
563 for p in projects['repositories']:567 for p in projects['repositories']:
564 repo = p['repository']568 repo = p['repository']
565569
=== modified file 'hooks/charmhelpers/contrib/storage/linux/ceph.py'
--- hooks/charmhelpers/contrib/storage/linux/ceph.py 2015-03-20 17:15:02 +0000
+++ hooks/charmhelpers/contrib/storage/linux/ceph.py 2015-07-16 20:31:40 +0000
@@ -60,12 +60,12 @@
60KEYFILE = '/etc/ceph/ceph.client.{}.key'60KEYFILE = '/etc/ceph/ceph.client.{}.key'
6161
62CEPH_CONF = """[global]62CEPH_CONF = """[global]
63 auth supported = {auth}63auth supported = {auth}
64 keyring = {keyring}64keyring = {keyring}
65 mon host = {mon_hosts}65mon host = {mon_hosts}
66 log to syslog = {use_syslog}66log to syslog = {use_syslog}
67 err to syslog = {use_syslog}67err to syslog = {use_syslog}
68 clog to syslog = {use_syslog}68clog to syslog = {use_syslog}
69"""69"""
7070
7171
7272
=== modified file 'hooks/charmhelpers/core/hookenv.py'
--- hooks/charmhelpers/core/hookenv.py 2015-06-26 18:56:53 +0000
+++ hooks/charmhelpers/core/hookenv.py 2015-07-16 20:31:40 +0000
@@ -761,6 +761,7 @@
761761
762 This is useful for modules and classes to perform initialization762 This is useful for modules and classes to perform initialization
763 and inject behavior. In particular:763 and inject behavior. In particular:
764
764 - Run common code before all of your hooks, such as logging765 - Run common code before all of your hooks, such as logging
765 the hook name or interesting relation data.766 the hook name or interesting relation data.
766 - Defer object or module initialization that requires a hook767 - Defer object or module initialization that requires a hook
767768
=== modified file 'hooks/charmhelpers/core/host.py'
--- hooks/charmhelpers/core/host.py 2015-06-19 15:08:48 +0000
+++ hooks/charmhelpers/core/host.py 2015-07-16 20:31:40 +0000
@@ -63,6 +63,36 @@
63 return service_result63 return service_result
6464
6565
66def service_pause(service_name, init_dir=None):
67 """Pause a system service.
68
69 Stop it, and prevent it from starting again at boot."""
70 if init_dir is None:
71 init_dir = "/etc/init"
72 stopped = service_stop(service_name)
73 # XXX: Support systemd too
74 override_path = os.path.join(
75 init_dir, '{}.conf.override'.format(service_name))
76 with open(override_path, 'w') as fh:
77 fh.write("manual\n")
78 return stopped
79
80
81def service_resume(service_name, init_dir=None):
82 """Resume a system service.
83
84 Reenable starting again at boot. Start the service"""
85 # XXX: Support systemd too
86 if init_dir is None:
87 init_dir = "/etc/init"
88 override_path = os.path.join(
89 init_dir, '{}.conf.override'.format(service_name))
90 if os.path.exists(override_path):
91 os.unlink(override_path)
92 started = service_start(service_name)
93 return started
94
95
66def service(action, service_name):96def service(action, service_name):
67 """Control a system service"""97 """Control a system service"""
68 cmd = ['service', service_name, action]98 cmd = ['service', service_name, action]
@@ -140,11 +170,7 @@
140170
141def add_user_to_group(username, group):171def add_user_to_group(username, group):
142 """Add a user to a group"""172 """Add a user to a group"""
143 cmd = [173 cmd = ['gpasswd', '-a', username, group]
144 'gpasswd', '-a',
145 username,
146 group
147 ]
148 log("Adding user {} to group {}".format(username, group))174 log("Adding user {} to group {}".format(username, group))
149 subprocess.check_call(cmd)175 subprocess.check_call(cmd)
150176
151177
=== modified file 'hooks/charmhelpers/core/services/helpers.py'
--- hooks/charmhelpers/core/services/helpers.py 2015-05-11 07:28:22 +0000
+++ hooks/charmhelpers/core/services/helpers.py 2015-07-16 20:31:40 +0000
@@ -239,12 +239,12 @@
239 action.239 action.
240240
241 :param str source: The template source file, relative to241 :param str source: The template source file, relative to
242 `$CHARM_DIR/templates`242 `$CHARM_DIR/templates`
243
244 :param str target: The target to write the rendered template to243 :param str target: The target to write the rendered template to
245 :param str owner: The owner of the rendered file244 :param str owner: The owner of the rendered file
246 :param str group: The group of the rendered file245 :param str group: The group of the rendered file
247 :param int perms: The permissions of the rendered file246 :param int perms: The permissions of the rendered file
247
248 """248 """
249 def __init__(self, source, target,249 def __init__(self, source, target,
250 owner='root', group='root', perms=0o444):250 owner='root', group='root', perms=0o444):
251251
=== modified file 'hooks/charmhelpers/fetch/__init__.py'
--- hooks/charmhelpers/fetch/__init__.py 2015-06-10 20:31:46 +0000
+++ hooks/charmhelpers/fetch/__init__.py 2015-07-16 20:31:40 +0000
@@ -215,19 +215,27 @@
215 _run_apt_command(cmd, fatal)215 _run_apt_command(cmd, fatal)
216216
217217
218def apt_mark(packages, mark, fatal=False):
219 """Flag one or more packages using apt-mark"""
220 cmd = ['apt-mark', mark]
221 if isinstance(packages, six.string_types):
222 cmd.append(packages)
223 else:
224 cmd.extend(packages)
225 log("Holding {}".format(packages))
226
227 if fatal:
228 subprocess.check_call(cmd, universal_newlines=True)
229 else:
230 subprocess.call(cmd, universal_newlines=True)
231
232
218def apt_hold(packages, fatal=False):233def apt_hold(packages, fatal=False):
219 """Hold one or more packages"""234 return apt_mark(packages, 'hold', fatal=fatal)
220 cmd = ['apt-mark', 'hold']235
221 if isinstance(packages, six.string_types):236
222 cmd.append(packages)237def apt_unhold(packages, fatal=False):
223 else:238 return apt_mark(packages, 'unhold', fatal=fatal)
224 cmd.extend(packages)
225 log("Holding {}".format(packages))
226
227 if fatal:
228 subprocess.check_call(cmd)
229 else:
230 subprocess.call(cmd)
231239
232240
233def add_source(source, key=None):241def add_source(source, key=None):
@@ -370,8 +378,9 @@
370 for handler in handlers:378 for handler in handlers:
371 try:379 try:
372 installed_to = handler.install(source, *args, **kwargs)380 installed_to = handler.install(source, *args, **kwargs)
373 except UnhandledSource:381 except UnhandledSource as e:
374 pass382 log('Install source attempt unsuccessful: {}'.format(e),
383 level='WARNING')
375 if not installed_to:384 if not installed_to:
376 raise UnhandledSource("No handler found for source {}".format(source))385 raise UnhandledSource("No handler found for source {}".format(source))
377 return installed_to386 return installed_to
378387
=== modified file 'hooks/charmhelpers/fetch/archiveurl.py'
--- hooks/charmhelpers/fetch/archiveurl.py 2015-03-20 17:15:02 +0000
+++ hooks/charmhelpers/fetch/archiveurl.py 2015-07-16 20:31:40 +0000
@@ -77,6 +77,8 @@
77 def can_handle(self, source):77 def can_handle(self, source):
78 url_parts = self.parse_url(source)78 url_parts = self.parse_url(source)
79 if url_parts.scheme not in ('http', 'https', 'ftp', 'file'):79 if url_parts.scheme not in ('http', 'https', 'ftp', 'file'):
80 # XXX: Why is this returning a boolean and a string? It's
81 # doomed to fail since "bool(can_handle('foo://'))" will be True.
80 return "Wrong source type"82 return "Wrong source type"
81 if get_archive_handler(self.base_url(source)):83 if get_archive_handler(self.base_url(source)):
82 return True84 return True
@@ -155,7 +157,11 @@
155 else:157 else:
156 algorithms = hashlib.algorithms_available158 algorithms = hashlib.algorithms_available
157 if key in algorithms:159 if key in algorithms:
158 check_hash(dld_file, value, key)160 if len(value) != 1:
161 raise TypeError(
162 "Expected 1 hash value, not %d" % len(value))
163 expected = value[0]
164 check_hash(dld_file, expected, key)
159 if checksum:165 if checksum:
160 check_hash(dld_file, checksum, hash_type)166 check_hash(dld_file, checksum, hash_type)
161 return extract(dld_file, dest)167 return extract(dld_file, dest)
162168
=== modified file 'hooks/charmhelpers/fetch/giturl.py'
--- hooks/charmhelpers/fetch/giturl.py 2015-05-27 13:01:32 +0000
+++ hooks/charmhelpers/fetch/giturl.py 2015-07-16 20:31:40 +0000
@@ -67,7 +67,7 @@
67 try:67 try:
68 self.clone(source, dest_dir, branch, depth)68 self.clone(source, dest_dir, branch, depth)
69 except GitCommandError as e:69 except GitCommandError as e:
70 raise UnhandledSource(e.message)70 raise UnhandledSource(e)
71 except OSError as e:71 except OSError as e:
72 raise UnhandledSource(e.strerror)72 raise UnhandledSource(e.strerror)
73 return dest_dir73 return dest_dir
7474
=== modified file 'tests/charmhelpers/contrib/amulet/utils.py'
--- tests/charmhelpers/contrib/amulet/utils.py 2015-06-26 19:03:04 +0000
+++ tests/charmhelpers/contrib/amulet/utils.py 2015-07-16 20:31:40 +0000
@@ -14,6 +14,7 @@
14# You should have received a copy of the GNU Lesser General Public License14# You should have received a copy of the GNU Lesser General Public License
15# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.15# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
1616
17import amulet
17import ConfigParser18import ConfigParser
18import distro_info19import distro_info
19import io20import io
@@ -173,6 +174,11 @@
173174
174 Verify that the specified section of the config file contains175 Verify that the specified section of the config file contains
175 the expected option key:value pairs.176 the expected option key:value pairs.
177
178 Compare expected dictionary data vs actual dictionary data.
179 The values in the 'expected' dictionary can be strings, bools, ints,
180 longs, or can be a function that evaluates a variable and returns a
181 bool.
176 """182 """
177 self.log.debug('Validating config file data ({} in {} on {})'183 self.log.debug('Validating config file data ({} in {} on {})'
178 '...'.format(section, config_file,184 '...'.format(section, config_file,
@@ -195,20 +201,18 @@
195 if actual != v:201 if actual != v:
196 return "section [{}] {}:{} != expected {}:{}".format(202 return "section [{}] {}:{} != expected {}:{}".format(
197 section, k, actual, k, expected[k])203 section, k, actual, k, expected[k])
198 else:204 # handle function pointers, such as not_null or valid_ip
199 # handle not_null, valid_ip boolean comparison methods, etc.205 elif not v(actual):
200 if v(actual):206 return "section [{}] {}:{} != expected {}:{}".format(
201 return None207 section, k, actual, k, expected[k])
202 else:208 return None
203 return "section [{}] {}:{} != expected {}:{}".format(
204 section, k, actual, k, expected[k])
205209
206 def _validate_dict_data(self, expected, actual):210 def _validate_dict_data(self, expected, actual):
207 """Validate dictionary data.211 """Validate dictionary data.
208212
209 Compare expected dictionary data vs actual dictionary data.213 Compare expected dictionary data vs actual dictionary data.
210 The values in the 'expected' dictionary can be strings, bools, ints,214 The values in the 'expected' dictionary can be strings, bools, ints,
211 longs, or can be a function that evaluate a variable and returns a215 longs, or can be a function that evaluates a variable and returns a
212 bool.216 bool.
213 """217 """
214 self.log.debug('actual: {}'.format(repr(actual)))218 self.log.debug('actual: {}'.format(repr(actual)))
@@ -219,8 +223,10 @@
219 if (isinstance(v, six.string_types) or223 if (isinstance(v, six.string_types) or
220 isinstance(v, bool) or224 isinstance(v, bool) or
221 isinstance(v, six.integer_types)):225 isinstance(v, six.integer_types)):
226 # handle explicit values
222 if v != actual[k]:227 if v != actual[k]:
223 return "{}:{}".format(k, actual[k])228 return "{}:{}".format(k, actual[k])
229 # handle function pointers, such as not_null or valid_ip
224 elif not v(actual[k]):230 elif not v(actual[k]):
225 return "{}:{}".format(k, actual[k])231 return "{}:{}".format(k, actual[k])
226 else:232 else:
@@ -435,15 +441,13 @@
435 for cmd in commands:441 for cmd in commands:
436 output, code = sentry_unit.run(cmd)442 output, code = sentry_unit.run(cmd)
437 if code == 0:443 if code == 0:
438 msg = ('{} `{}` returned {} '444 self.log.debug('{} `{}` returned {} '
439 '(OK)'.format(sentry_unit.info['unit_name'],445 '(OK)'.format(sentry_unit.info['unit_name'],
440 cmd, code))446 cmd, code))
441 self.log.debug(msg)
442 else:447 else:
443 msg = ('{} `{}` returned {} '448 return ('{} `{}` returned {} '
444 '{}'.format(sentry_unit.info['unit_name'],449 '{}'.format(sentry_unit.info['unit_name'],
445 cmd, code, output))450 cmd, code, output))
446 return msg
447 return None451 return None
448452
449 def get_process_id_list(self, sentry_unit, process_name):453 def get_process_id_list(self, sentry_unit, process_name):
@@ -460,7 +464,7 @@
460 msg = ('{} `{}` returned {} '464 msg = ('{} `{}` returned {} '
461 '{}'.format(sentry_unit.info['unit_name'],465 '{}'.format(sentry_unit.info['unit_name'],
462 cmd, code, output))466 cmd, code, output))
463 raise RuntimeError(msg)467 amulet.raise_status(amulet.FAIL, msg=msg)
464 return str(output).split()468 return str(output).split()
465469
466 def get_unit_process_ids(self, unit_processes):470 def get_unit_process_ids(self, unit_processes):
@@ -481,47 +485,37 @@
481 self.log.debug('Actual PIDs: {}'.format(actual))485 self.log.debug('Actual PIDs: {}'.format(actual))
482486
483 if len(actual) != len(expected):487 if len(actual) != len(expected):
484 msg = ('Unit count mismatch. expected, actual: {}, '488 return ('Unit count mismatch. expected, actual: {}, '
485 '{} '.format(len(expected), len(actual)))489 '{} '.format(len(expected), len(actual)))
486 return msg
487490
488 for (e_sentry, e_proc_names) in expected.iteritems():491 for (e_sentry, e_proc_names) in expected.iteritems():
489 e_sentry_name = e_sentry.info['unit_name']492 e_sentry_name = e_sentry.info['unit_name']
490 if e_sentry in actual.keys():493 if e_sentry in actual.keys():
491 a_proc_names = actual[e_sentry]494 a_proc_names = actual[e_sentry]
492 else:495 else:
493 msg = ('Expected sentry ({}) not found in actual dict data.'496 return ('Expected sentry ({}) not found in actual dict data.'
494 '{}'.format(e_sentry_name, e_sentry))497 '{}'.format(e_sentry_name, e_sentry))
495 return msg
496498
497 if len(e_proc_names.keys()) != len(a_proc_names.keys()):499 if len(e_proc_names.keys()) != len(a_proc_names.keys()):
498 msg = ('Process name count mismatch. expected, actual: {}, '500 return ('Process name count mismatch. expected, actual: {}, '
499 '{}'.format(len(expected), len(actual)))501 '{}'.format(len(expected), len(actual)))
500 return msg
501502
502 for (e_proc_name, e_pids_length), (a_proc_name, a_pids) in \503 for (e_proc_name, e_pids_length), (a_proc_name, a_pids) in \
503 zip(e_proc_names.items(), a_proc_names.items()):504 zip(e_proc_names.items(), a_proc_names.items()):
504 if e_proc_name != a_proc_name:505 if e_proc_name != a_proc_name:
505 msg = ('Process name mismatch. expected, actual: {}, '506 return ('Process name mismatch. expected, actual: {}, '
506 '{}'.format(e_proc_name, a_proc_name))507 '{}'.format(e_proc_name, a_proc_name))
507 return msg
508508
509 a_pids_length = len(a_pids)509 a_pids_length = len(a_pids)
510 if e_pids_length != a_pids_length:510 if e_pids_length != a_pids_length:
511 msg = ('PID count mismatch. {} ({}) expected, actual: {}, '511 return ('PID count mismatch. {} ({}) expected, actual: '
512 '{} ({})'.format(e_sentry_name,512 '{}, {} ({})'.format(e_sentry_name, e_proc_name,
513 e_proc_name,513 e_pids_length, a_pids_length,
514 e_pids_length,514 a_pids))
515 a_pids_length,
516 a_pids))
517 return msg
518 else:515 else:
519 msg = ('PID check OK: {} {} {}: '516 self.log.debug('PID check OK: {} {} {}: '
520 '{}'.format(e_sentry_name,517 '{}'.format(e_sentry_name, e_proc_name,
521 e_proc_name,518 e_pids_length, a_pids))
522 e_pids_length,
523 a_pids))
524 self.log.debug(msg)
525 return None519 return None
526520
527 def validate_list_of_identical_dicts(self, list_of_dicts):521 def validate_list_of_identical_dicts(self, list_of_dicts):
@@ -532,10 +526,8 @@
532526
533 self.log.debug('Hashes: {}'.format(hashes))527 self.log.debug('Hashes: {}'.format(hashes))
534 if len(set(hashes)) == 1:528 if len(set(hashes)) == 1:
535 msg = 'Dicts within list are identical'529 self.log.debug('Dicts within list are identical')
536 self.log.debug(msg)
537 else:530 else:
538 msg = 'Dicts within list are not identical'531 return 'Dicts within list are not identical'
539 return msg
540532
541 return None533 return None
542534
=== modified file 'tests/charmhelpers/contrib/openstack/amulet/deployment.py'
--- tests/charmhelpers/contrib/openstack/amulet/deployment.py 2015-06-26 19:03:04 +0000
+++ tests/charmhelpers/contrib/openstack/amulet/deployment.py 2015-07-16 20:31:40 +0000
@@ -150,8 +150,9 @@
150 return releases[self.series]150 return releases[self.series]
151151
152 def get_ceph_expected_pools(self, radosgw=False):152 def get_ceph_expected_pools(self, radosgw=False):
153 """Return a list of expected ceph pools based on Ubuntu-OpenStack153 """Return a list of expected ceph pools in a ceph + cinder + glance
154 release and whether ceph radosgw is flagged as present or not."""154 test scenario, based on OpenStack release and whether ceph radosgw
155 is flagged as present or not."""
155156
156 if self._get_openstack_release() >= self.trusty_kilo:157 if self._get_openstack_release() >= self.trusty_kilo:
157 # Kilo or later158 # Kilo or later
158159
=== modified file 'tests/charmhelpers/contrib/openstack/amulet/utils.py'
--- tests/charmhelpers/contrib/openstack/amulet/utils.py 2015-06-26 19:03:04 +0000
+++ tests/charmhelpers/contrib/openstack/amulet/utils.py 2015-07-16 20:31:40 +0000
@@ -14,6 +14,7 @@
14# You should have received a copy of the GNU Lesser General Public License14# You should have received a copy of the GNU Lesser General Public License
15# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.15# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
1616
17import amulet
17import json18import json
18import logging19import logging
19import os20import os
@@ -177,6 +178,7 @@
177 def authenticate_cinder_admin(self, keystone_sentry, username,178 def authenticate_cinder_admin(self, keystone_sentry, username,
178 password, tenant):179 password, tenant):
179 """Authenticates admin user with cinder."""180 """Authenticates admin user with cinder."""
181 # NOTE(beisner): cinder python client doesn't accept tokens.
180 service_ip = \182 service_ip = \
181 keystone_sentry.relation('shared-db',183 keystone_sentry.relation('shared-db',
182 'mysql:shared-db')['private-address']184 'mysql:shared-db')['private-address']
@@ -279,7 +281,7 @@
279 msg='Image status wait')281 msg='Image status wait')
280 if not ret:282 if not ret:
281 msg = 'Glance image failed to reach expected state.'283 msg = 'Glance image failed to reach expected state.'
282 raise RuntimeError(msg)284 amulet.raise_status(amulet.FAIL, msg=msg)
283285
284 # Re-validate new image286 # Re-validate new image
285 self.log.debug('Validating image attributes...')287 self.log.debug('Validating image attributes...')
@@ -299,7 +301,7 @@
299 self.log.debug(msg_attr)301 self.log.debug(msg_attr)
300 else:302 else:
301 msg = ('Volume validation failed, {}'.format(msg_attr))303 msg = ('Volume validation failed, {}'.format(msg_attr))
302 raise RuntimeError(msg)304 amulet.raise_status(amulet.FAIL, msg=msg)
303305
304 return image306 return image
305307
@@ -362,8 +364,8 @@
362364
363 def create_cinder_volume(self, cinder, vol_name="demo-vol", vol_size=1,365 def create_cinder_volume(self, cinder, vol_name="demo-vol", vol_size=1,
364 img_id=None, src_vol_id=None, snap_id=None):366 img_id=None, src_vol_id=None, snap_id=None):
365 """Create cinder volume, optionally from a glance image, or367 """Create cinder volume, optionally from a glance image, OR
366 optionally as a clone of an existing volume, or optionally368 optionally as a clone of an existing volume, OR optionally
367 from a snapshot. Wait for the new volume status to reach369 from a snapshot. Wait for the new volume status to reach
368 the expected status, validate and return a resource pointer.370 the expected status, validate and return a resource pointer.
369371
@@ -374,29 +376,33 @@
374 :param snap_id: optional snapshot id to use376 :param snap_id: optional snapshot id to use
375 :returns: cinder volume pointer377 :returns: cinder volume pointer
376 """378 """
377 # Handle parameter input379 # Handle parameter input and avoid impossible combinations
378 if img_id and not src_vol_id and not snap_id:380 if img_id and not src_vol_id and not snap_id:
379 self.log.debug('Creating cinder volume from glance image '381 # Create volume from image
380 '({})...'.format(img_id))382 self.log.debug('Creating cinder volume from glance image...')
381 bootable = 'true'383 bootable = 'true'
382 elif src_vol_id and not img_id and not snap_id:384 elif src_vol_id and not img_id and not snap_id:
385 # Clone an existing volume
383 self.log.debug('Cloning cinder volume...')386 self.log.debug('Cloning cinder volume...')
384 bootable = cinder.volumes.get(src_vol_id).bootable387 bootable = cinder.volumes.get(src_vol_id).bootable
385 elif snap_id and not src_vol_id and not img_id:388 elif snap_id and not src_vol_id and not img_id:
389 # Create volume from snapshot
386 self.log.debug('Creating cinder volume from snapshot...')390 self.log.debug('Creating cinder volume from snapshot...')
387 snap = cinder.volume_snapshots.find(id=snap_id)391 snap = cinder.volume_snapshots.find(id=snap_id)
388 vol_size = snap.size392 vol_size = snap.size
389 snap_vol_id = cinder.volume_snapshots.get(snap_id).volume_id393 snap_vol_id = cinder.volume_snapshots.get(snap_id).volume_id
390 bootable = cinder.volumes.get(snap_vol_id).bootable394 bootable = cinder.volumes.get(snap_vol_id).bootable
391 elif not img_id and not src_vol_id and not snap_id:395 elif not img_id and not src_vol_id and not snap_id:
396 # Create volume
392 self.log.debug('Creating cinder volume...')397 self.log.debug('Creating cinder volume...')
393 bootable = 'false'398 bootable = 'false'
394 else:399 else:
400 # Impossible combination of parameters
395 msg = ('Invalid method use - name:{} size:{} img_id:{} '401 msg = ('Invalid method use - name:{} size:{} img_id:{} '
396 'src_vol_id:{} snap_id:{}'.format(vol_name, vol_size,402 'src_vol_id:{} snap_id:{}'.format(vol_name, vol_size,
397 img_id, src_vol_id,403 img_id, src_vol_id,
398 snap_id))404 snap_id))
399 raise RuntimeError(msg)405 amulet.raise_status(amulet.FAIL, msg=msg)
400406
401 # Create new volume407 # Create new volume
402 try:408 try:
@@ -408,7 +414,7 @@
408 vol_id = vol_new.id414 vol_id = vol_new.id
409 except Exception as e:415 except Exception as e:
410 msg = 'Failed to create volume: {}'.format(e)416 msg = 'Failed to create volume: {}'.format(e)
411 raise RuntimeError(msg)417 amulet.raise_status(amulet.FAIL, msg=msg)
412418
413 # Wait for volume to reach available status419 # Wait for volume to reach available status
414 ret = self.resource_reaches_status(cinder.volumes, vol_id,420 ret = self.resource_reaches_status(cinder.volumes, vol_id,
@@ -416,7 +422,7 @@
416 msg="Volume status wait")422 msg="Volume status wait")
417 if not ret:423 if not ret:
418 msg = 'Cinder volume failed to reach expected state.'424 msg = 'Cinder volume failed to reach expected state.'
419 raise RuntimeError(msg)425 amulet.raise_status(amulet.FAIL, msg=msg)
420426
421 # Re-validate new volume427 # Re-validate new volume
422 self.log.debug('Validating volume attributes...')428 self.log.debug('Validating volume attributes...')
@@ -434,7 +440,7 @@
434 self.log.debug(msg_attr)440 self.log.debug(msg_attr)
435 else:441 else:
436 msg = ('Volume validation failed, {}'.format(msg_attr))442 msg = ('Volume validation failed, {}'.format(msg_attr))
437 raise RuntimeError(msg)443 amulet.raise_status(amulet.FAIL, msg=msg)
438444
439 return vol_new445 return vol_new
440446
@@ -515,9 +521,9 @@
515521
516 def get_ceph_osd_id_cmd(self, index):522 def get_ceph_osd_id_cmd(self, index):
517 """Produce a shell command that will return a ceph-osd id."""523 """Produce a shell command that will return a ceph-osd id."""
518 cmd = ("`initctl list | grep 'ceph-osd ' | awk 'NR=={} {{ print $2 }}'"524 return ("`initctl list | grep 'ceph-osd ' | "
519 " | grep -o '[0-9]*'`".format(index + 1))525 "awk 'NR=={} {{ print $2 }}' | "
520 return cmd526 "grep -o '[0-9]*'`".format(index + 1))
521527
522 def get_ceph_pools(self, sentry_unit):528 def get_ceph_pools(self, sentry_unit):
523 """Return a dict of ceph pools from a single ceph unit, with529 """Return a dict of ceph pools from a single ceph unit, with
@@ -529,7 +535,7 @@
529 msg = ('{} `{}` returned {} '535 msg = ('{} `{}` returned {} '
530 '{}'.format(sentry_unit.info['unit_name'],536 '{}'.format(sentry_unit.info['unit_name'],
531 cmd, code, output))537 cmd, code, output))
532 raise RuntimeError(msg)538 amulet.raise_status(amulet.FAIL, msg=msg)
533539
534 # Example output: 0 data,1 metadata,2 rbd,3 cinder,4 glance,540 # Example output: 0 data,1 metadata,2 rbd,3 cinder,4 glance,
535 for pool in str(output).split(','):541 for pool in str(output).split(','):
@@ -555,7 +561,7 @@
555 msg = ('{} `{}` returned {} '561 msg = ('{} `{}` returned {} '
556 '{}'.format(sentry_unit.info['unit_name'],562 '{}'.format(sentry_unit.info['unit_name'],
557 cmd, code, output))563 cmd, code, output))
558 raise RuntimeError(msg)564 amulet.raise_status(amulet.FAIL, msg=msg)
559 return json.loads(output)565 return json.loads(output)
560566
561 def get_ceph_pool_sample(self, sentry_unit, pool_id=0):567 def get_ceph_pool_sample(self, sentry_unit, pool_id=0):
@@ -572,10 +578,8 @@
572 obj_count = df['pools'][pool_id]['stats']['objects']578 obj_count = df['pools'][pool_id]['stats']['objects']
573 kb_used = df['pools'][pool_id]['stats']['kb_used']579 kb_used = df['pools'][pool_id]['stats']['kb_used']
574 self.log.debug('Ceph {} pool (ID {}): {} objects, '580 self.log.debug('Ceph {} pool (ID {}): {} objects, '
575 '{} kb used'.format(pool_name,581 '{} kb used'.format(pool_name, pool_id,
576 pool_id,582 obj_count, kb_used))
577 obj_count,
578 kb_used))
579 return pool_name, obj_count, kb_used583 return pool_name, obj_count, kb_used
580584
581 def validate_ceph_pool_samples(self, samples, sample_type="resource pool"):585 def validate_ceph_pool_samples(self, samples, sample_type="resource pool"):
@@ -592,9 +596,8 @@
592 original, created, deleted = range(3)596 original, created, deleted = range(3)
593 if samples[created] <= samples[original] or \597 if samples[created] <= samples[original] or \
594 samples[deleted] >= samples[created]:598 samples[deleted] >= samples[created]:
595 msg = ('Ceph {} samples ({}) '599 return ('Ceph {} samples ({}) '
596 'unexpected.'.format(sample_type, samples))600 'unexpected.'.format(sample_type, samples))
597 return msg
598 else:601 else:
599 self.log.debug('Ceph {} samples (OK): '602 self.log.debug('Ceph {} samples (OK): '
600 '{}'.format(sample_type, samples))603 '{}'.format(sample_type, samples))

Subscribers

People subscribed via source and target branches