Sync with charm-helpers

This commit is contained in:
Corey Bryant 2014-07-09 19:25:29 +00:00
parent 0798c73db9
commit d99fe56ea4
18 changed files with 725 additions and 61 deletions

View File

@ -170,6 +170,7 @@ def canonical_url(configs, vip_setting='vip'):
:configs : OSTemplateRenderer: A config tempating object to inspect for :configs : OSTemplateRenderer: A config tempating object to inspect for
a complete https context. a complete https context.
:vip_setting: str: Setting in charm config that specifies :vip_setting: str: Setting in charm config that specifies
VIP address. VIP address.
''' '''

View File

@ -7,19 +7,38 @@ class OpenStackAmuletDeployment(AmuletDeployment):
"""This class inherits from AmuletDeployment and has additional support """This class inherits from AmuletDeployment and has additional support
that is specifically for use by OpenStack charms.""" that is specifically for use by OpenStack charms."""
def __init__(self, series=None, openstack=None): def __init__(self, series, openstack=None, source=None):
"""Initialize the deployment environment.""" """Initialize the deployment environment."""
self.openstack = None
super(OpenStackAmuletDeployment, self).__init__(series) super(OpenStackAmuletDeployment, self).__init__(series)
if openstack:
self.openstack = openstack 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:
charm_name = self._get_charm_name(svc[name])
if charm_name not in use_source:
config = {'openstack-origin': self.openstack}
self.d.configure(svc[name], config)
if self.source:
for svc in services:
charm_name = self._get_charm_name(svc[name])
if charm_name in use_source:
config = {'source': self.source}
self.d.configure(svc[name], config)
def _configure_services(self, configs): def _configure_services(self, configs):
"""Configure all of the services.""" """Configure all of the services."""
for service, config in configs.iteritems(): for service, config in configs.iteritems():
if service == self.this_service:
config['openstack-origin'] = self.openstack
self.d.configure(service, config) self.d.configure(service, config)
def _get_openstack_release(self): def _get_openstack_release(self):

View File

@ -1,4 +1,7 @@
import logging import logging
import os
import time
import urllib
import glanceclient.v1.client as glance_client import glanceclient.v1.client as glance_client
import keystoneclient.v2_0 as keystone_client import keystoneclient.v2_0 as keystone_client
@ -71,7 +74,7 @@ class OpenStackAmuletUtils(AmuletUtils):
if ret: if ret:
return "unexpected tenant data - {}".format(ret) return "unexpected tenant data - {}".format(ret)
if not found: if not found:
return "tenant {} does not exist".format(e.name) return "tenant {} does not exist".format(e['name'])
return ret return ret
def validate_role_data(self, expected, actual): def validate_role_data(self, expected, actual):
@ -88,7 +91,7 @@ class OpenStackAmuletUtils(AmuletUtils):
if ret: if ret:
return "unexpected role data - {}".format(ret) return "unexpected role data - {}".format(ret)
if not found: if not found:
return "role {} does not exist".format(e.name) return "role {} does not exist".format(e['name'])
return ret return ret
def validate_user_data(self, expected, actual): def validate_user_data(self, expected, actual):
@ -107,7 +110,7 @@ class OpenStackAmuletUtils(AmuletUtils):
if ret: if ret:
return "unexpected user data - {}".format(ret) return "unexpected user data - {}".format(ret)
if not found: if not found:
return "user {} does not exist".format(e.name) return "user {} does not exist".format(e['name'])
return ret return ret
def validate_flavor_data(self, expected, actual): def validate_flavor_data(self, expected, actual):
@ -149,3 +152,102 @@ class OpenStackAmuletUtils(AmuletUtils):
endpoint_type='publicURL') endpoint_type='publicURL')
return nova_client.Client(username=user, api_key=password, return nova_client.Client(username=user, api_key=password,
project_id=tenant, auth_url=ep) 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)
count = 1
status = image.status
while status != 'active' and count < 10:
time.sleep(3)
image = glance.images.get(image.id)
status = image.status
self.log.debug('image status: {}'.format(status))
count += 1
if status != 'active':
self.log.error('image creation timed out')
return None
return image
def delete_image(self, glance, image):
"""Delete the specified image."""
num_before = len(list(glance.images.list()))
glance.images.delete(image)
count = 1
num_after = len(list(glance.images.list()))
while num_after != (num_before - 1) and count < 10:
time.sleep(3)
num_after = len(list(glance.images.list()))
self.log.debug('number of images: {}'.format(num_after))
count += 1
if num_after != (num_before - 1):
self.log.error('image deletion timed out')
return False
return True
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 != 'ACTIVE':
self.log.error('instance creation timed out')
return None
return instance
def delete_instance(self, nova, instance):
"""Delete the specified instance."""
num_before = len(list(nova.servers.list()))
nova.servers.delete(instance)
count = 1
num_after = len(list(nova.servers.list()))
while num_after != (num_before - 1) and count < 10:
time.sleep(3)
num_after = len(list(nova.servers.list()))
self.log.debug('number of instances: {}'.format(num_after))
count += 1
if num_after != (num_before - 1):
self.log.error('instance deletion timed out')
return False
return True

View File

@ -426,7 +426,8 @@ class ApacheSSLContext(OSContextGenerator):
""" """
Generates a context for an apache vhost configuration that configures Generates a context for an apache vhost configuration that configures
HTTPS reverse proxying for one or many endpoints. Generated context HTTPS reverse proxying for one or many endpoints. Generated context
looks something like: looks something like::
{ {
'namespace': 'cinder', 'namespace': 'cinder',
'private_address': 'iscsi.mycinderhost.com', 'private_address': 'iscsi.mycinderhost.com',
@ -641,7 +642,7 @@ class SubordinateConfigContext(OSContextGenerator):
The subordinate interface allows subordinates to export their The subordinate interface allows subordinates to export their
configuration requirements to the principle for multiple config configuration requirements to the principle for multiple config
files and multiple serivces. Ie, a subordinate that has interfaces files and multiple serivces. Ie, a subordinate that has interfaces
to both glance and nova may export to following yaml blob as json: to both glance and nova may export to following yaml blob as json::
glance: glance:
/etc/glance/glance-api.conf: /etc/glance/glance-api.conf:
@ -660,7 +661,8 @@ class SubordinateConfigContext(OSContextGenerator):
It is then up to the principle charms to subscribe this context to It is then up to the principle charms to subscribe this context to
the service+config file it is interestd in. Configuration data will the service+config file it is interestd in. Configuration data will
be available in the template context, in glance's case, as: be available in the template context, in glance's case, as::
ctxt = { ctxt = {
... other context ... ... other context ...
'subordinate_config': { 'subordinate_config': {

View File

@ -30,15 +30,15 @@ def get_loader(templates_dir, os_release):
loading dir. loading dir.
A charm may also ship a templates dir with this module A charm may also ship a templates dir with this module
and it will be appended to the bottom of the search list, eg: and it will be appended to the bottom of the search list, eg::
hooks/charmhelpers/contrib/openstack/templates.
:param templates_dir: str: Base template directory containing release hooks/charmhelpers/contrib/openstack/templates
:param templates_dir (str): Base template directory containing release
sub-directories. sub-directories.
:param os_release : str: OpenStack release codename to construct template :param os_release (str): OpenStack release codename to construct template
loader. loader.
:returns: jinja2.ChoiceLoader constructed with a list of
:returns : jinja2.ChoiceLoader constructed with a list of
jinja2.FilesystemLoaders, ordered in descending jinja2.FilesystemLoaders, ordered in descending
order by OpenStack release. order by OpenStack release.
""" """
@ -111,7 +111,8 @@ class OSConfigRenderer(object):
and ease the burden of managing config templates across multiple OpenStack and ease the burden of managing config templates across multiple OpenStack
releases. releases.
Basic usage: Basic usage::
# import some common context generates from charmhelpers # import some common context generates from charmhelpers
from charmhelpers.contrib.openstack import context from charmhelpers.contrib.openstack import context
@ -131,10 +132,8 @@ class OSConfigRenderer(object):
# write out all registered configs # write out all registered configs
configs.write_all() configs.write_all()
Details: **OpenStack Releases and template loading**
OpenStack Releases and template loading
---------------------------------------
When the object is instantiated, it is associated with a specific OS When the object is instantiated, it is associated with a specific OS
release. This dictates how the template loader will be constructed. release. This dictates how the template loader will be constructed.
@ -144,8 +143,8 @@ class OSConfigRenderer(object):
- the base templates_dir - the base templates_dir
- a template directory shipped in the charm with this helper file. - a template directory shipped in the charm with this helper file.
For the example above, '/tmp/templates' contains the following structure::
For the example above, '/tmp/templates' contains the following structure:
/tmp/templates/nova.conf /tmp/templates/nova.conf
/tmp/templates/api-paste.ini /tmp/templates/api-paste.ini
/tmp/templates/grizzly/api-paste.ini /tmp/templates/grizzly/api-paste.ini
@ -169,8 +168,8 @@ class OSConfigRenderer(object):
$CHARM/hooks/charmhelpers/contrib/openstack/templates. This allows $CHARM/hooks/charmhelpers/contrib/openstack/templates. This allows
us to ship common templates (haproxy, apache) with the helpers. us to ship common templates (haproxy, apache) with the helpers.
Context generators **Context generators**
---------------------------------------
Context generators are used to generate template contexts during hook Context generators are used to generate template contexts during hook
execution. Doing so may require inspecting service relations, charm execution. Doing so may require inspecting service relations, charm
config, etc. When registered, a config file is associated with a list config, etc. When registered, a config file is associated with a list

View File

@ -25,7 +25,7 @@ cache = {}
def cached(func): def cached(func):
"""Cache return values for multiple executions of func + args """Cache return values for multiple executions of func + args
For example: For example::
@cached @cached
def unit_get(attribute): def unit_get(attribute):
@ -445,18 +445,19 @@ class UnregisteredHookError(Exception):
class Hooks(object): class Hooks(object):
"""A convenient handler for hook functions. """A convenient handler for hook functions.
Example: Example::
hooks = Hooks() hooks = Hooks()
# register a hook, taking its name from the function name # register a hook, taking its name from the function name
@hooks.hook() @hooks.hook()
def install(): def install():
... pass # your code here
# register a hook, providing a custom hook name # register a hook, providing a custom hook name
@hooks.hook("config-changed") @hooks.hook("config-changed")
def config_changed(): def config_changed():
... pass # your code here
if __name__ == "__main__": if __name__ == "__main__":
# execute a hook based on the name the program is called by # execute a hook based on the name the program is called by

View File

@ -211,13 +211,13 @@ def file_hash(path):
def restart_on_change(restart_map, stopstart=False): def restart_on_change(restart_map, stopstart=False):
"""Restart services based on configuration files changing """Restart services based on configuration files changing
This function is used a decorator, for example This function is used a decorator, for example::
@restart_on_change({ @restart_on_change({
'/etc/ceph/ceph.conf': [ 'cinder-api', 'cinder-volume' ] '/etc/ceph/ceph.conf': [ 'cinder-api', 'cinder-volume' ]
}) })
def ceph_client_changed(): def ceph_client_changed():
... pass # your code here
In this example, the cinder-api and cinder-volume services In this example, the cinder-api and cinder-volume services
would be restarted if /etc/ceph/ceph.conf is changed by the would be restarted if /etc/ceph/ceph.conf is changed by the
@ -313,9 +313,11 @@ def get_nic_hwaddr(nic):
def cmp_pkgrevno(package, revno, pkgcache=None): def cmp_pkgrevno(package, revno, pkgcache=None):
'''Compare supplied revno with the revno of the installed package '''Compare supplied revno with the revno of the installed package
1 => Installed revno is greater than supplied arg
0 => Installed revno is the same as supplied arg * 1 => Installed revno is greater than supplied arg
-1 => Installed revno is less than supplied arg * 0 => Installed revno is the same as supplied arg
* -1 => Installed revno is less than supplied arg
''' '''
import apt_pkg import apt_pkg
if not pkgcache: if not pkgcache:

View File

@ -235,31 +235,39 @@ def configure_sources(update=False,
sources_var='install_sources', sources_var='install_sources',
keys_var='install_keys'): keys_var='install_keys'):
""" """
Configure multiple sources from charm configuration Configure multiple sources from charm configuration.
The lists are encoded as yaml fragments in the configuration.
The frament needs to be included as a string.
Example config: Example config:
install_sources: install_sources: |
- "ppa:foo" - "ppa:foo"
- "http://example.com/repo precise main" - "http://example.com/repo precise main"
install_keys: install_keys: |
- null - null
- "a1b2c3d4" - "a1b2c3d4"
Note that 'null' (a.k.a. None) should not be quoted. Note that 'null' (a.k.a. None) should not be quoted.
""" """
sources = safe_load(config(sources_var)) sources = safe_load((config(sources_var) or '').strip()) or []
keys = config(keys_var) keys = safe_load((config(keys_var) or '').strip()) or None
if keys is not None:
keys = safe_load(keys) if isinstance(sources, basestring):
if isinstance(sources, basestring) and ( sources = [sources]
keys is None or isinstance(keys, basestring)):
add_source(sources, keys) if keys is None:
for source in sources:
add_source(source, None)
else: else:
if not len(sources) == len(keys): if isinstance(keys, basestring):
msg = 'Install sources and keys lists are different lengths' keys = [keys]
raise SourceConfigError(msg)
for src_num in range(len(sources)): if len(sources) != len(keys):
add_source(sources[src_num], keys[src_num]) raise SourceConfigError(
'Install sources and keys lists are different lengths')
for source, key in zip(sources, keys):
add_source(source, key)
if update: if update:
apt_update(fatal=True) apt_update(fatal=True)

View File

View File

View File

@ -0,0 +1,63 @@
import amulet
import re
class AmuletDeployment(object):
"""This class provides generic Amulet deployment and test runner
methods."""
def __init__(self, series):
"""Initialize the deployment environment."""
self.series = series
self.d = amulet.Deployment(series=self.series)
def _get_charm_name(self, service_name):
"""Gets the charm name from the service name. Unique service names can
be specified with a '-service#' suffix (e.g. mysql-service1)."""
if re.match(r"^.*-service\d{1,3}$", service_name):
charm_name = re.sub('\-service\d{1,3}$', '', service_name)
else:
charm_name = service_name
return charm_name
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)
charm_name = self._get_charm_name(this_service[name])
self.d.add(this_service[name],
units=this_service[units])
for svc in other_services:
charm_name = self._get_charm_name(svc[name])
self.d.add(svc[name],
charm='cs:{}/{}'.format(self.series, charm_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, sleep_time=20):
"""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(sleep_time)
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,57 @@
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, 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:
charm_name = self._get_charm_name(svc[name])
if charm_name not in use_source:
config = {'openstack-origin': self.openstack}
self.d.configure(svc[name], config)
if self.source:
for svc in services:
charm_name = self._get_charm_name(svc[name])
if charm_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,253 @@
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)
count = 1
status = image.status
while status != 'active' and count < 10:
time.sleep(3)
image = glance.images.get(image.id)
status = image.status
self.log.debug('image status: {}'.format(status))
count += 1
if status != 'active':
self.log.error('image creation timed out')
return None
return image
def delete_image(self, glance, image):
"""Delete the specified image."""
num_before = len(list(glance.images.list()))
glance.images.delete(image)
count = 1
num_after = len(list(glance.images.list()))
while num_after != (num_before - 1) and count < 10:
time.sleep(3)
num_after = len(list(glance.images.list()))
self.log.debug('number of images: {}'.format(num_after))
count += 1
if num_after != (num_before - 1):
self.log.error('image deletion timed out')
return False
return True
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 != 'ACTIVE':
self.log.error('instance creation timed out')
return None
return instance
def delete_instance(self, nova, instance):
"""Delete the specified instance."""
num_before = len(list(nova.servers.list()))
nova.servers.delete(instance)
count = 1
num_after = len(list(nova.servers.list()))
while num_after != (num_before - 1) and count < 10:
time.sleep(3)
num_after = len(list(nova.servers.list()))
self.log.debug('number of instances: {}'.format(num_after))
count += 1
if num_after != (num_before - 1):
self.log.error('instance deletion timed out')
return False
return True