This commit is contained in:
James Page 2014-07-25 10:37:25 +01:00
commit 2a221767f8
21 changed files with 1127 additions and 4 deletions

View File

@ -2,10 +2,14 @@
PYTHON := /usr/bin/env python
lint:
@flake8 --exclude hooks/charmhelpers hooks unit_tests
@echo "Running flake8 tests: "
@flake8 --exclude hooks/charmhelpers hooks unit_tests tests
@echo "OK"
@echo "Running charm proof: "
@charm proof
@echo "OK"
test:
unit_test:
@$(PYTHON) /usr/bin/nosetests --nologcapture --with-coverage unit_tests
bin/charm_helpers_sync.py:
@ -14,8 +18,18 @@ bin/charm_helpers_sync.py:
> bin/charm_helpers_sync.py
sync: bin/charm_helpers_sync.py
@$(PYTHON) bin/charm_helpers_sync.py -c charm-helpers.yaml
@$(PYTHON) bin/charm_helpers_sync.py -c charm-helpers-hooks.yaml
@$(PYTHON) bin/charm_helpers_sync.py -c charm-helpers-tests.yaml
publish: lint test
test:
@echo Starting Amulet tests...
# /!\ Note: The -v should only be temporary until Amulet sends
# raise_status() messages to stderr:
# https://bugs.launchpad.net/amulet/+bug/1320357
@juju test -v -p AMULET_HTTP_PROXY
publish: lint unit_test
bzr push lp:charms/glance
bzr push lp:charms/trusty/glance
all: unit_test lint

5
charm-helpers-tests.yaml Normal file
View File

@ -0,0 +1,5 @@
branch: lp:charm-helpers
destination: tests/charmhelpers
include:
- contrib.amulet
- contrib.openstack.amulet

10
tests/00-setup Executable file
View File

@ -0,0 +1,10 @@
#!/bin/bash
set -ex
sudo add-apt-repository --yes ppa:juju/stable
sudo apt-get update --yes
sudo apt-get install --yes python-amulet
sudo apt-get install --yes python-keystoneclient
sudo apt-get install --yes python-glanceclient

9
tests/10-basic-precise-essex Executable file
View File

@ -0,0 +1,9 @@
#!/usr/bin/python
"""Amulet tests on a basic glance deployment on precise-essex."""
from basic_deployment import GlanceBasicDeployment
if __name__ == '__main__':
deployment = GlanceBasicDeployment(series='precise')
deployment.run_tests()

11
tests/11-basic-precise-folsom Executable file
View File

@ -0,0 +1,11 @@
#!/usr/bin/python
"""Amulet tests on a basic glance deployment on precise-folsom."""
from basic_deployment import GlanceBasicDeployment
if __name__ == '__main__':
deployment = GlanceBasicDeployment(series='precise',
openstack='cloud:precise-folsom',
source='cloud:precise-updates/folsom')
deployment.run_tests()

11
tests/12-basic-precise-grizzly Executable file
View File

@ -0,0 +1,11 @@
#!/usr/bin/python
"""Amulet tests on a basic glance deployment on precise-grizzly."""
from basic_deployment import GlanceBasicDeployment
if __name__ == '__main__':
deployment = GlanceBasicDeployment(series='precise',
openstack='cloud:precise-grizzly',
source='cloud:precise-updates/grizzly')
deployment.run_tests()

11
tests/13-basic-precise-havana Executable file
View File

@ -0,0 +1,11 @@
#!/usr/bin/python
"""Amulet tests on a basic glance deployment on precise-havana."""
from basic_deployment import GlanceBasicDeployment
if __name__ == '__main__':
deployment = GlanceBasicDeployment(series='precise',
openstack='cloud:precise-havana',
source='cloud:precise-updates/havana')
deployment.run_tests()

11
tests/14-basic-precise-icehouse Executable file
View File

@ -0,0 +1,11 @@
#!/usr/bin/python
"""Amulet tests on a basic glance deployment on precise-icehouse."""
from basic_deployment import GlanceBasicDeployment
if __name__ == '__main__':
deployment = GlanceBasicDeployment(series='precise',
openstack='cloud:precise-icehouse',
source='cloud:precise-updates/icehouse')
deployment.run_tests()

9
tests/15-basic-trusty-icehouse Executable file
View File

@ -0,0 +1,9 @@
#!/usr/bin/python
"""Amulet tests on a basic Glance deployment on trusty-icehouse."""
from basic_deployment import GlanceBasicDeployment
if __name__ == '__main__':
deployment = GlanceBasicDeployment(series='trusty')
deployment.run_tests()

58
tests/README Normal file
View File

@ -0,0 +1,58 @@
This directory provides Amulet tests that focus on verification of Glance
deployments.
If you use a web proxy server to access the web, you'll need to set the
AMULET_HTTP_PROXY environment variable to the http URL of the proxy server.
The following examples demonstrate different ways that tests can be executed.
All examples are run from the charm's root directory.
* To run all tests (starting with 00-setup):
make test
* To run a specific test module (or modules):
juju test -v -p AMULET_HTTP_PROXY 15-basic-trusty-icehouse
* To run a specific test module (or modules), and keep the environment
deployed after a failure:
juju test --set-e -v -p AMULET_HTTP_PROXY 15-basic-trusty-icehouse
* To re-run a test module against an already deployed environment (one
that was deployed by a previous call to 'juju test --set-e'):
./tests/15-basic-trusty-icehouse
For debugging and test development purposes, all code should be idempotent.
In other words, the code should have the ability to be re-run without changing
the results beyond the initial run. This enables editing and re-running of a
test module against an already deployed environment, as described above.
Notes for additional test writing:
* Use DEBUG to turn on debug logging, use ERROR otherwise.
u = OpenStackAmuletUtils(ERROR)
u = OpenStackAmuletUtils(DEBUG)
* To interact with the deployed environment:
export OS_USERNAME=admin
export OS_PASSWORD=openstack
export OS_TENANT_NAME=admin
export OS_REGION_NAME=RegionOne
export OS_AUTH_URL=${OS_AUTH_PROTOCOL:-http}://`juju-deployer -e trusty -f keystone`:5000/v2.0
keystone user-list
glance image-list
* Preserving the deployed environment:
Even with juju --set-e, amulet will tear down the juju environment
when all tests pass. This force_fail 'test' can be used in basic_deployment.py
to simulate a failed test and keep the environment.
def test_zzzz_fake_fail(self):
'''Force a fake fail to keep juju environment after a successful test run'''
# Useful in test writing, when used with: juju test --set-e
amulet.raise_status(amulet.FAIL, msg='using fake fail to keep juju environment')

495
tests/basic_deployment.py Executable file
View File

@ -0,0 +1,495 @@
#!/usr/bin/python
import amulet
import time
from charmhelpers.contrib.openstack.amulet.deployment import (
OpenStackAmuletDeployment
)
from charmhelpers.contrib.openstack.amulet.utils import (
OpenStackAmuletUtils,
DEBUG, # flake8: noqa
ERROR
)
# Use DEBUG to turn on debug logging
u = OpenStackAmuletUtils(ERROR)
class GlanceBasicDeployment(OpenStackAmuletDeployment):
'''Amulet tests on a basic file-backed glance deployment. Verify relations,
service status, endpoint service catalog, create and delete new image.'''
# TO-DO(beisner):
# * Add tests with different storage back ends
# * Resolve Essex->Havana juju set charm bug
def __init__(self, series=None, openstack=None, source=None):
'''Deploy the entire test environment.'''
super(GlanceBasicDeployment, self).__init__(series, openstack, source)
self._add_services()
self._add_relations()
self._configure_services()
self._deploy()
self._initialize_tests()
def _add_services(self):
'''Add the service that we're testing, including the number of units,
where this charm is local, and the other charms are from
the charm store.'''
this_service = ('glance', 1)
other_services = [('mysql', 1), ('rabbitmq-server', 1), ('keystone', 1)]
super(GlanceBasicDeployment, self)._add_services(this_service,
other_services)
def _add_relations(self):
'''Add relations for the services.'''
relations = {'glance:identity-service': 'keystone:identity-service',
'glance:shared-db': 'mysql:shared-db',
'keystone:shared-db': 'mysql:shared-db',
'glance:amqp': 'rabbitmq-server:amqp'}
super(GlanceBasicDeployment, self)._add_relations(relations)
def _configure_services(self):
'''Configure all of the services.'''
keystone_config = {'admin-password': 'openstack',
'admin-token': 'ubuntutesting'}
mysql_config = {'dataset-size': '50%'}
configs = {'keystone': keystone_config,
'mysql': mysql_config}
super(GlanceBasicDeployment, self)._configure_services(configs)
def _initialize_tests(self):
'''Perform final initialization before tests get run.'''
# Access the sentries for inspecting service units
self.mysql_sentry = self.d.sentry.unit['mysql/0']
self.glance_sentry = self.d.sentry.unit['glance/0']
self.keystone_sentry = self.d.sentry.unit['keystone/0']
self.rabbitmq_sentry = self.d.sentry.unit['rabbitmq-server/0']
# Authenticate admin with keystone
self.keystone = u.authenticate_keystone_admin(self.keystone_sentry,
user='admin',
password='openstack',
tenant='admin')
# Authenticate admin with glance endpoint
self.glance = u.authenticate_glance_admin(self.keystone)
u.log.debug('openstack release: {}'.format(self._get_openstack_release()))
def test_services(self):
'''Verify that the expected services are running on the
corresponding service units.'''
commands = {
self.mysql_sentry: ['status mysql'],
self.keystone_sentry: ['status keystone'],
self.glance_sentry: ['status glance-api', 'status glance-registry'],
self.rabbitmq_sentry: ['sudo service rabbitmq-server status']
}
u.log.debug('commands: {}'.format(commands))
ret = u.validate_services(commands)
if ret:
amulet.raise_status(amulet.FAIL, msg=ret)
def test_service_catalog(self):
'''Verify that the service catalog endpoint data'''
endpoint_vol = {'adminURL': u.valid_url,
'region': 'RegionOne',
'publicURL': u.valid_url,
'internalURL': u.valid_url}
endpoint_id = {'adminURL': u.valid_url,
'region': 'RegionOne',
'publicURL': u.valid_url,
'internalURL': u.valid_url}
if self._get_openstack_release() >= self.trusty_icehouse:
endpoint_vol['id'] = u.not_null
endpoint_id['id'] = u.not_null
expected = {'image': [endpoint_id],
'identity': [endpoint_id]}
actual = self.keystone.service_catalog.get_endpoints()
ret = u.validate_svc_catalog_endpoint_data(expected, actual)
if ret:
amulet.raise_status(amulet.FAIL, msg=ret)
def test_mysql_glance_db_relation(self):
'''Verify the mysql:glance shared-db relation data'''
unit = self.mysql_sentry
relation = ['shared-db', 'glance:shared-db']
expected = {
'private-address': u.valid_ip,
'db_host': u.valid_ip
}
ret = u.validate_relation_data(unit, relation, expected)
if ret:
message = u.relation_error('mysql shared-db', ret)
amulet.raise_status(amulet.FAIL, msg=message)
def test_glance_mysql_db_relation(self):
'''Verify the glance:mysql shared-db relation data'''
unit = self.glance_sentry
relation = ['shared-db', 'mysql:shared-db']
expected = {
'private-address': u.valid_ip,
'hostname': u.valid_ip,
'username': 'glance',
'database': 'glance'
}
ret = u.validate_relation_data(unit, relation, expected)
if ret:
message = u.relation_error('glance shared-db', ret)
amulet.raise_status(amulet.FAIL, msg=message)
def test_keystone_glance_id_relation(self):
'''Verify the keystone:glance identity-service relation data'''
unit = self.keystone_sentry
relation = ['identity-service',
'glance:identity-service']
expected = {
'service_protocol': 'http',
'service_tenant': 'services',
'admin_token': 'ubuntutesting',
'service_password': u.not_null,
'service_port': '5000',
'auth_port': '35357',
'auth_protocol': 'http',
'private-address': u.valid_ip,
'https_keystone': 'False',
'auth_host': u.valid_ip,
'service_username': 'glance',
'service_tenant_id': u.not_null,
'service_host': u.valid_ip
}
ret = u.validate_relation_data(unit, relation, expected)
if ret:
message = u.relation_error('keystone identity-service', ret)
amulet.raise_status(amulet.FAIL, msg=message)
def test_glance_keystone_id_relation(self):
'''Verify the glance:keystone identity-service relation data'''
unit = self.glance_sentry
relation = ['identity-service',
'keystone:identity-service']
expected = {
'service': 'glance',
'region': 'RegionOne',
'public_url': u.valid_url,
'internal_url': u.valid_url,
'admin_url': u.valid_url,
'private-address': u.valid_ip
}
ret = u.validate_relation_data(unit, relation, expected)
if ret:
message = u.relation_error('glance identity-service', ret)
amulet.raise_status(amulet.FAIL, msg=message)
def test_rabbitmq_glance_amqp_relation(self):
'''Verify the rabbitmq-server:glance amqp relation data'''
unit = self.rabbitmq_sentry
relation = ['amqp', 'glance:amqp']
expected = {
'private-address': u.valid_ip,
'password': u.not_null,
'hostname': u.valid_ip
}
ret = u.validate_relation_data(unit, relation, expected)
if ret:
message = u.relation_error('rabbitmq amqp', ret)
amulet.raise_status(amulet.FAIL, msg=message)
def test_glance_rabbitmq_amqp_relation(self):
'''Verify the glance:rabbitmq-server amqp relation data'''
unit = self.glance_sentry
relation = ['amqp', 'rabbitmq-server:amqp']
expected = {
'private-address': u.valid_ip,
'vhost': 'openstack',
'username': u.not_null
}
ret = u.validate_relation_data(unit, relation, expected)
if ret:
message = u.relation_error('glance amqp', ret)
amulet.raise_status(amulet.FAIL, msg=message)
def test_image_create_delete(self):
'''Create new cirros image in glance, verify, then delete it'''
# Create a new image
image_name = 'cirros-image-1'
image_new = u.create_cirros_image(self.glance, image_name)
# Confirm image is created and has status of 'active'
if not image_new:
message = 'glance image create failed'
amulet.raise_status(amulet.FAIL, msg=message)
# Verify new image name
images_list = list(self.glance.images.list())
if images_list[0].name != image_name:
message = 'glance image create failed or unexpected image name {}'.format(images_list[0].name)
amulet.raise_status(amulet.FAIL, msg=message)
# Delete the new image
u.log.debug('image count before delete: {}'.format(len(list(self.glance.images.list()))))
u.delete_image(self.glance, image_new)
u.log.debug('image count after delete: {}'.format(len(list(self.glance.images.list()))))
def test_glance_api_default_config(self):
'''Verify default section configs in glance-api.conf and
compare some of the parameters to relation data.'''
unit = self.glance_sentry
rel_gl_mq = unit.relation('amqp', 'rabbitmq-server:amqp')
conf = '/etc/glance/glance-api.conf'
expected = {'use_syslog': 'False',
'default_store': 'file',
'filesystem_store_datadir': '/var/lib/glance/images/',
'rabbit_userid': rel_gl_mq['username'],
'log_file': '/var/log/glance/api.log',
'debug': 'False',
'verbose': 'False'}
section = 'DEFAULT'
if self._get_openstack_release() <= self.precise_havana:
# Defaults were different before icehouse
expected['debug'] = 'True'
expected['verbose'] = 'True'
ret = u.validate_config_data(unit, conf, section, expected)
if ret:
message = "glance-api default config error: {}".format(ret)
amulet.raise_status(amulet.FAIL, msg=message)
def test_glance_api_auth_config(self):
'''Verify authtoken section config in glance-api.conf using
glance/keystone relation data.'''
unit_gl = self.glance_sentry
unit_ks = self.keystone_sentry
rel_gl_mq = unit_gl.relation('amqp', 'rabbitmq-server:amqp')
rel_ks_gl = unit_ks.relation('identity-service', 'glance:identity-service')
conf = '/etc/glance/glance-api.conf'
section = 'keystone_authtoken'
if self._get_openstack_release() > self.precise_havana:
# No auth config exists in this file before icehouse
expected = {'admin_user': 'glance',
'admin_password': rel_ks_gl['service_password']}
ret = u.validate_config_data(unit_gl, conf, section, expected)
if ret:
message = "glance-api auth config error: {}".format(ret)
amulet.raise_status(amulet.FAIL, msg=message)
def test_glance_api_paste_auth_config(self):
'''Verify authtoken section config in glance-api-paste.ini using
glance/keystone relation data.'''
unit_gl = self.glance_sentry
unit_ks = self.keystone_sentry
rel_gl_mq = unit_gl.relation('amqp', 'rabbitmq-server:amqp')
rel_ks_gl = unit_ks.relation('identity-service', 'glance:identity-service')
conf = '/etc/glance/glance-api-paste.ini'
section = 'filter:authtoken'
if self._get_openstack_release() <= self.precise_havana:
# No auth config exists in this file after havana
expected = {'admin_user': 'glance',
'admin_password': rel_ks_gl['service_password']}
ret = u.validate_config_data(unit_gl, conf, section, expected)
if ret:
message = "glance-api-paste auth config error: {}".format(ret)
amulet.raise_status(amulet.FAIL, msg=message)
def test_glance_registry_paste_auth_config(self):
'''Verify authtoken section config in glance-registry-paste.ini using
glance/keystone relation data.'''
unit_gl = self.glance_sentry
unit_ks = self.keystone_sentry
rel_gl_mq = unit_gl.relation('amqp', 'rabbitmq-server:amqp')
rel_ks_gl = unit_ks.relation('identity-service', 'glance:identity-service')
conf = '/etc/glance/glance-registry-paste.ini'
section = 'filter:authtoken'
if self._get_openstack_release() <= self.precise_havana:
# No auth config exists in this file after havana
expected = {'admin_user': 'glance',
'admin_password': rel_ks_gl['service_password']}
ret = u.validate_config_data(unit_gl, conf, section, expected)
if ret:
message = "glance-registry-paste auth config error: {}".format(ret)
amulet.raise_status(amulet.FAIL, msg=message)
def test_glance_registry_default_config(self):
'''Verify default section configs in glance-registry.conf'''
unit = self.glance_sentry
conf = '/etc/glance/glance-registry.conf'
expected = {'use_syslog': 'False',
'log_file': '/var/log/glance/registry.log',
'debug': 'False',
'verbose': 'False'}
section = 'DEFAULT'
if self._get_openstack_release() <= self.precise_havana:
# Defaults were different before icehouse
expected['debug'] = 'True'
expected['verbose'] = 'True'
ret = u.validate_config_data(unit, conf, section, expected)
if ret:
message = "glance-registry default config error: {}".format(ret)
amulet.raise_status(amulet.FAIL, msg=message)
def test_glance_registry_auth_config(self):
'''Verify authtoken section config in glance-registry.conf
using glance/keystone relation data.'''
unit_gl = self.glance_sentry
unit_ks = self.keystone_sentry
rel_gl_mq = unit_gl.relation('amqp', 'rabbitmq-server:amqp')
rel_ks_gl = unit_ks.relation('identity-service', 'glance:identity-service')
conf = '/etc/glance/glance-registry.conf'
section = 'keystone_authtoken'
if self._get_openstack_release() > self.precise_havana:
# No auth config exists in this file before icehouse
expected = {'admin_user': 'glance',
'admin_password': rel_ks_gl['service_password']}
ret = u.validate_config_data(unit_gl, conf, section, expected)
if ret:
message = "glance-registry keystone_authtoken config error: {}".format(ret)
amulet.raise_status(amulet.FAIL, msg=message)
def test_glance_api_database_config(self):
'''Verify database config in glance-api.conf and
compare with a db uri constructed from relation data.'''
unit = self.glance_sentry
conf = '/etc/glance/glance-api.conf'
relation = self.mysql_sentry.relation('shared-db', 'glance:shared-db')
db_uri = "mysql://{}:{}@{}/{}".format('glance', relation['password'],
relation['db_host'], 'glance')
expected = {'connection': db_uri, 'sql_idle_timeout': '3600'}
section = 'database'
if self._get_openstack_release() <= self.precise_havana:
# Section and directive for this config changed in icehouse
expected = {'sql_connection': db_uri, 'sql_idle_timeout': '3600'}
section = 'DEFAULT'
ret = u.validate_config_data(unit, conf, section, expected)
if ret:
message = "glance db config error: {}".format(ret)
amulet.raise_status(amulet.FAIL, msg=message)
def test_glance_registry_database_config(self):
'''Verify database config in glance-registry.conf and
compare with a db uri constructed from relation data.'''
unit = self.glance_sentry
conf = '/etc/glance/glance-registry.conf'
relation = self.mysql_sentry.relation('shared-db', 'glance:shared-db')
db_uri = "mysql://{}:{}@{}/{}".format('glance', relation['password'],
relation['db_host'], 'glance')
expected = {'connection': db_uri, 'sql_idle_timeout': '3600'}
section = 'database'
if self._get_openstack_release() <= self.precise_havana:
# Section and directive for this config changed in icehouse
expected = {'sql_connection': db_uri, 'sql_idle_timeout': '3600'}
section = 'DEFAULT'
ret = u.validate_config_data(unit, conf, section, expected)
if ret:
message = "glance db config error: {}".format(ret)
amulet.raise_status(amulet.FAIL, msg=message)
def test_glance_endpoint(self):
'''Verify the glance endpoint data.'''
endpoints = self.keystone.endpoints.list()
admin_port = internal_port = public_port = '9292'
expected = {'id': u.not_null,
'region': 'RegionOne',
'adminurl': u.valid_url,
'internalurl': u.valid_url,
'publicurl': u.valid_url,
'service_id': u.not_null}
ret = u.validate_endpoint_data(endpoints, admin_port, internal_port,
public_port, expected)
if ret:
amulet.raise_status(amulet.FAIL,
msg='glance endpoint: {}'.format(ret))
def test_keystone_endpoint(self):
'''Verify the keystone endpoint data.'''
endpoints = self.keystone.endpoints.list()
admin_port = '35357'
internal_port = public_port = '5000'
expected = {'id': u.not_null,
'region': 'RegionOne',
'adminurl': u.valid_url,
'internalurl': u.valid_url,
'publicurl': u.valid_url,
'service_id': u.not_null}
ret = u.validate_endpoint_data(endpoints, admin_port, internal_port,
public_port, expected)
if ret:
amulet.raise_status(amulet.FAIL,
msg='keystone endpoint: {}'.format(ret))
def test_glance_restart_on_config_change(self):
'''Verify that glance is restarted when the config is changed.'''
# Make config change to trigger a service restart
if self._get_openstack_release() > self.precise_havana:
self.d.configure('glance', {'verbose': 'True'})
self.d.configure('glance', {'debug': 'True'})
elif self._get_openstack_release() <= self.precise_havana:
# /!\ NOTE(beisner): Glance charm before Icehouse doesn't respond
# to attempted config changes via juju / juju set.
# https://bugs.launchpad.net/charms/+source/glance/+bug/1340307
u.log.error('NOTE(beisner): skipping glance restart on config ' +
'change check due to bug 1340307.')
return
self.d.configure('glance', {'verbose': 'False'})
self.d.configure('glance', {'debug': 'False'})
if not u.service_restarted(self.glance_sentry, 'glance-api',
'/etc/glance/glance-api.conf'):
message = "glance service didn't restart after config change"
amulet.raise_status(amulet.FAIL, msg=message)
if not u.service_restarted(self.glance_sentry, 'glance-registry',
'/etc/glance/glance-registry.conf',
sleep_time=0):
message = "glance service didn't restart after config change"
amulet.raise_status(amulet.FAIL, msg=message)
# Return to original config
if self._get_openstack_release() > self.precise_havana:
self.d.configure('glance', {'verbose': 'False'})
self.d.configure('glance', {'debug': 'False'})
else:
self.d.configure('glance', {'verbose': 'True'})
self.d.configure('glance', {'debug': 'True'})
def test_users(self):
'''Verify expected users.'''
user0 = {'name': 'glance',
'enabled': True,
'tenantId': u.not_null,
'id': u.not_null,
'email': 'juju@localhost'}
user1 = {'name': 'admin',
'enabled': True,
'tenantId': u.not_null,
'id': u.not_null,
'email': 'juju@localhost'}
expected = [user0, user1]
actual = self.keystone.users.list()
ret = u.validate_user_data(expected, actual)
if ret:
amulet.raise_status(amulet.FAIL, msg=ret)

View File

View File

View File

@ -0,0 +1,58 @@
import amulet
class AmuletDeployment(object):
"""This class provides generic Amulet deployment and test runner
methods."""
def __init__(self, series=None):
"""Initialize the deployment environment."""
self.series = None
if series:
self.series = series
self.d = amulet.Deployment(series=self.series)
else:
self.d = amulet.Deployment()
def _add_services(self, this_service, other_services):
"""Add services to the deployment where this_service is the local charm
that we're focused on testing and other_services are the other
charms that come from the charm store."""
name, units = range(2)
self.this_service = this_service[name]
self.d.add(this_service[name], units=this_service[units])
for svc in other_services:
if self.series:
self.d.add(svc[name],
charm='cs:{}/{}'.format(self.series, svc[name]),
units=svc[units])
else:
self.d.add(svc[name], units=svc[units])
def _add_relations(self, relations):
"""Add all of the relations for the services."""
for k, v in relations.iteritems():
self.d.relate(k, v)
def _configure_services(self, configs):
"""Configure all of the services."""
for service, config in configs.iteritems():
self.d.configure(service, config)
def _deploy(self):
"""Deploy environment and wait for all hooks to finish executing."""
try:
self.d.setup()
self.d.sentry.wait()
except amulet.helpers.TimeoutError:
amulet.raise_status(amulet.FAIL, msg="Deployment timed out")
except:
raise
def run_tests(self):
"""Run all of the methods that are prefixed with 'test_'."""
for test in dir(self):
if test.startswith('test_'):
getattr(self, test)()

View File

@ -0,0 +1,157 @@
import ConfigParser
import io
import logging
import re
import sys
from time import sleep
class AmuletUtils(object):
"""This class provides common utility functions that are used by Amulet
tests."""
def __init__(self, log_level=logging.ERROR):
self.log = self.get_logger(level=log_level)
def get_logger(self, name="amulet-logger", level=logging.DEBUG):
"""Get a logger object that will log to stdout."""
log = logging
logger = log.getLogger(name)
fmt = \
log.Formatter("%(asctime)s %(funcName)s %(levelname)s: %(message)s")
handler = log.StreamHandler(stream=sys.stdout)
handler.setLevel(level)
handler.setFormatter(fmt)
logger.addHandler(handler)
logger.setLevel(level)
return logger
def valid_ip(self, ip):
if re.match(r"^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$", ip):
return True
else:
return False
def valid_url(self, url):
p = re.compile(
r'^(?:http|ftp)s?://'
r'(?:(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+(?:[A-Z]{2,6}\.?|[A-Z0-9-]{2,}\.?)|' # flake8: noqa
r'localhost|'
r'\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})'
r'(?::\d+)?'
r'(?:/?|[/?]\S+)$',
re.IGNORECASE)
if p.match(url):
return True
else:
return False
def validate_services(self, commands):
"""Verify the specified services are running on the corresponding
service units."""
for k, v in commands.iteritems():
for cmd in v:
output, code = k.run(cmd)
if code != 0:
return "command `{}` returned {}".format(cmd, str(code))
return None
def _get_config(self, unit, filename):
"""Get a ConfigParser object for parsing a unit's config file."""
file_contents = unit.file_contents(filename)
config = ConfigParser.ConfigParser()
config.readfp(io.StringIO(file_contents))
return config
def validate_config_data(self, sentry_unit, config_file, section, expected):
"""Verify that the specified section of the config file contains
the expected option key:value pairs."""
config = self._get_config(sentry_unit, config_file)
if section != 'DEFAULT' and not config.has_section(section):
return "section [{}] does not exist".format(section)
for k in expected.keys():
if not config.has_option(section, k):
return "section [{}] is missing option {}".format(section, k)
if config.get(section, k) != expected[k]:
return "section [{}] {}:{} != expected {}:{}".format(section,
k, config.get(section, k), k, expected[k])
return None
def _validate_dict_data(self, expected, actual):
"""Compare expected dictionary data vs actual dictionary data.
The values in the 'expected' dictionary can be strings, bools, ints,
longs, or can be a function that evaluate a variable and returns a
bool."""
for k, v in expected.iteritems():
if k in actual:
if isinstance(v, basestring) or \
isinstance(v, bool) or \
isinstance(v, (int, long)):
if v != actual[k]:
return "{}:{}".format(k, actual[k])
elif not v(actual[k]):
return "{}:{}".format(k, actual[k])
else:
return "key '{}' does not exist".format(k)
return None
def validate_relation_data(self, sentry_unit, relation, expected):
"""Validate actual relation data based on expected relation data."""
actual = sentry_unit.relation(relation[0], relation[1])
self.log.debug('actual: {}'.format(repr(actual)))
return self._validate_dict_data(expected, actual)
def _validate_list_data(self, expected, actual):
"""Compare expected list vs actual list data."""
for e in expected:
if e not in actual:
return "expected item {} not found in actual list".format(e)
return None
def not_null(self, string):
if string != None:
return True
else:
return False
def _get_file_mtime(self, sentry_unit, filename):
"""Get last modification time of file."""
return sentry_unit.file_stat(filename)['mtime']
def _get_dir_mtime(self, sentry_unit, directory):
"""Get last modification time of directory."""
return sentry_unit.directory_stat(directory)['mtime']
def _get_proc_start_time(self, sentry_unit, service, pgrep_full=False):
"""Determine start time of the process based on the last modification
time of the /proc/pid directory. If pgrep_full is True, the process
name is matched against the full command line."""
if pgrep_full:
cmd = 'pgrep -o -f {}'.format(service)
else:
cmd = 'pgrep -o {}'.format(service)
proc_dir = '/proc/{}'.format(sentry_unit.run(cmd)[0].strip())
return self._get_dir_mtime(sentry_unit, proc_dir)
def service_restarted(self, sentry_unit, service, filename,
pgrep_full=False):
"""Compare a service's start time vs a file's last modification time
(such as a config file for that service) to determine if the service
has been restarted."""
sleep(10)
if self._get_proc_start_time(sentry_unit, service, pgrep_full) >= \
self._get_file_mtime(sentry_unit, filename):
return True
else:
return False
def relation_error(self, name, data):
return 'unexpected relation data in {} - {}'.format(name, data)
def endpoint_error(self, name, data):
return 'unexpected endpoint data in {} - {}'.format(name, data)

View File

@ -0,0 +1,55 @@
from charmhelpers.contrib.amulet.deployment import (
AmuletDeployment
)
class OpenStackAmuletDeployment(AmuletDeployment):
"""This class inherits from AmuletDeployment and has additional support
that is specifically for use by OpenStack charms."""
def __init__(self, series=None, openstack=None, source=None):
"""Initialize the deployment environment."""
super(OpenStackAmuletDeployment, self).__init__(series)
self.openstack = openstack
self.source = source
def _add_services(self, this_service, other_services):
"""Add services to the deployment and set openstack-origin."""
super(OpenStackAmuletDeployment, self)._add_services(this_service,
other_services)
name = 0
services = other_services
services.append(this_service)
use_source = ['mysql', 'mongodb', 'rabbitmq-server', 'ceph']
if self.openstack:
for svc in services:
if svc[name] not in use_source:
config = {'openstack-origin': self.openstack}
self.d.configure(svc[name], config)
if self.source:
for svc in services:
if svc[name] in use_source:
config = {'source': self.source}
self.d.configure(svc[name], config)
def _configure_services(self, configs):
"""Configure all of the services."""
for service, config in configs.iteritems():
self.d.configure(service, config)
def _get_openstack_release(self):
"""Return an integer representing the enum value of the openstack
release."""
self.precise_essex, self.precise_folsom, self.precise_grizzly, \
self.precise_havana, self.precise_icehouse, \
self.trusty_icehouse = range(6)
releases = {
('precise', None): self.precise_essex,
('precise', 'cloud:precise-folsom'): self.precise_folsom,
('precise', 'cloud:precise-grizzly'): self.precise_grizzly,
('precise', 'cloud:precise-havana'): self.precise_havana,
('precise', 'cloud:precise-icehouse'): self.precise_icehouse,
('trusty', None): self.trusty_icehouse}
return releases[(self.series, self.openstack)]

View File

@ -0,0 +1,209 @@
import logging
import os
import time
import urllib
import glanceclient.v1.client as glance_client
import keystoneclient.v2_0 as keystone_client
import novaclient.v1_1.client as nova_client
from charmhelpers.contrib.amulet.utils import (
AmuletUtils
)
DEBUG = logging.DEBUG
ERROR = logging.ERROR
class OpenStackAmuletUtils(AmuletUtils):
"""This class inherits from AmuletUtils and has additional support
that is specifically for use by OpenStack charms."""
def __init__(self, log_level=ERROR):
"""Initialize the deployment environment."""
super(OpenStackAmuletUtils, self).__init__(log_level)
def validate_endpoint_data(self, endpoints, admin_port, internal_port,
public_port, expected):
"""Validate actual endpoint data vs expected endpoint data. The ports
are used to find the matching endpoint."""
found = False
for ep in endpoints:
self.log.debug('endpoint: {}'.format(repr(ep)))
if admin_port in ep.adminurl and internal_port in ep.internalurl \
and public_port in ep.publicurl:
found = True
actual = {'id': ep.id,
'region': ep.region,
'adminurl': ep.adminurl,
'internalurl': ep.internalurl,
'publicurl': ep.publicurl,
'service_id': ep.service_id}
ret = self._validate_dict_data(expected, actual)
if ret:
return 'unexpected endpoint data - {}'.format(ret)
if not found:
return 'endpoint not found'
def validate_svc_catalog_endpoint_data(self, expected, actual):
"""Validate a list of actual service catalog endpoints vs a list of
expected service catalog endpoints."""
self.log.debug('actual: {}'.format(repr(actual)))
for k, v in expected.iteritems():
if k in actual:
ret = self._validate_dict_data(expected[k][0], actual[k][0])
if ret:
return self.endpoint_error(k, ret)
else:
return "endpoint {} does not exist".format(k)
return ret
def validate_tenant_data(self, expected, actual):
"""Validate a list of actual tenant data vs list of expected tenant
data."""
self.log.debug('actual: {}'.format(repr(actual)))
for e in expected:
found = False
for act in actual:
a = {'enabled': act.enabled, 'description': act.description,
'name': act.name, 'id': act.id}
if e['name'] == a['name']:
found = True
ret = self._validate_dict_data(e, a)
if ret:
return "unexpected tenant data - {}".format(ret)
if not found:
return "tenant {} does not exist".format(e['name'])
return ret
def validate_role_data(self, expected, actual):
"""Validate a list of actual role data vs a list of expected role
data."""
self.log.debug('actual: {}'.format(repr(actual)))
for e in expected:
found = False
for act in actual:
a = {'name': act.name, 'id': act.id}
if e['name'] == a['name']:
found = True
ret = self._validate_dict_data(e, a)
if ret:
return "unexpected role data - {}".format(ret)
if not found:
return "role {} does not exist".format(e['name'])
return ret
def validate_user_data(self, expected, actual):
"""Validate a list of actual user data vs a list of expected user
data."""
self.log.debug('actual: {}'.format(repr(actual)))
for e in expected:
found = False
for act in actual:
a = {'enabled': act.enabled, 'name': act.name,
'email': act.email, 'tenantId': act.tenantId,
'id': act.id}
if e['name'] == a['name']:
found = True
ret = self._validate_dict_data(e, a)
if ret:
return "unexpected user data - {}".format(ret)
if not found:
return "user {} does not exist".format(e['name'])
return ret
def validate_flavor_data(self, expected, actual):
"""Validate a list of actual flavors vs a list of expected flavors."""
self.log.debug('actual: {}'.format(repr(actual)))
act = [a.name for a in actual]
return self._validate_list_data(expected, act)
def tenant_exists(self, keystone, tenant):
"""Return True if tenant exists"""
return tenant in [t.name for t in keystone.tenants.list()]
def authenticate_keystone_admin(self, keystone_sentry, user, password,
tenant):
"""Authenticates admin user with the keystone admin endpoint."""
service_ip = \
keystone_sentry.relation('shared-db',
'mysql:shared-db')['private-address']
ep = "http://{}:35357/v2.0".format(service_ip.strip().decode('utf-8'))
return keystone_client.Client(username=user, password=password,
tenant_name=tenant, auth_url=ep)
def authenticate_keystone_user(self, keystone, user, password, tenant):
"""Authenticates a regular user with the keystone public endpoint."""
ep = keystone.service_catalog.url_for(service_type='identity',
endpoint_type='publicURL')
return keystone_client.Client(username=user, password=password,
tenant_name=tenant, auth_url=ep)
def authenticate_glance_admin(self, keystone):
"""Authenticates admin user with glance."""
ep = keystone.service_catalog.url_for(service_type='image',
endpoint_type='adminURL')
return glance_client.Client(ep, token=keystone.auth_token)
def authenticate_nova_user(self, keystone, user, password, tenant):
"""Authenticates a regular user with nova-api."""
ep = keystone.service_catalog.url_for(service_type='identity',
endpoint_type='publicURL')
return nova_client.Client(username=user, api_key=password,
project_id=tenant, auth_url=ep)
def create_cirros_image(self, glance, image_name):
"""Download the latest cirros image and upload it to glance."""
http_proxy = os.getenv('AMULET_HTTP_PROXY')
self.log.debug('AMULET_HTTP_PROXY: {}'.format(http_proxy))
if http_proxy:
proxies = {'http': http_proxy}
opener = urllib.FancyURLopener(proxies)
else:
opener = urllib.FancyURLopener()
f = opener.open("http://download.cirros-cloud.net/version/released")
version = f.read().strip()
cirros_img = "tests/cirros-{}-x86_64-disk.img".format(version)
if not os.path.exists(cirros_img):
cirros_url = "http://{}/{}/{}".format("download.cirros-cloud.net",
version, cirros_img)
opener.retrieve(cirros_url, cirros_img)
f.close()
with open(cirros_img) as f:
image = glance.images.create(name=image_name, is_public=True,
disk_format='qcow2',
container_format='bare', data=f)
return image
def delete_image(self, glance, image):
"""Delete the specified image."""
glance.images.delete(image)
def create_instance(self, nova, image_name, instance_name, flavor):
"""Create the specified instance."""
image = nova.images.find(name=image_name)
flavor = nova.flavors.find(name=flavor)
instance = nova.servers.create(name=instance_name, image=image,
flavor=flavor)
count = 1
status = instance.status
while status != 'ACTIVE' and count < 60:
time.sleep(3)
instance = nova.servers.get(instance.id)
status = instance.status
self.log.debug('instance status: {}'.format(status))
count += 1
if status == 'BUILD':
return None
return instance
def delete_instance(self, nova, instance):
"""Delete the specified instance."""
nova.servers.delete(instance)