Refactor configuration setup and document config
* Adds documentation to the sample config file/template
* Adds some log output for making diagnosing config issues easier
* Rework the authorization test config option names
* Remove obselete release_name config options
* Remove ssh_timeout unused option
Change-Id: Ia5d539771920728424bd73db3532f7670077e44d
diff --git a/etc/tempest.conf.sample b/etc/tempest.conf.sample
index b63b077..cc84994 100644
--- a/etc/tempest.conf.sample
+++ b/etc/tempest.conf.sample
@@ -1,36 +1,90 @@
[identity]
-use_ssl=False
-host=127.0.0.1
-port=5000
-api_version=v2.0
-path=tokens
-nonadmin_user1=user1
-nonadmin_user1_password=password
-nonadmin_user1_tenant_name=user1-project
-nonadmin_user2=user2
-nonadmin_user2_password=password
-nonadmin_user2_tenant_name=user2-project
-strategy=keystone
+# This section contains configuration options that a variety of Tempest
+# test clients use when authenticating with different user/tenant
+# combinations
+
+# Set to True if your test environment's Keystone authentication service should
+# be accessed over HTTPS
+use_ssl = False
+# This is the main host address of the authentication service API
+host = 127.0.0.1
+# Port that the authentication service API is running on
+port = 5000
+# Version of the authentication service API (a string)
+api_version = v2.0
+# Path to the authentication service tokens resource (do not modify unless you
+# have a custom authentication API and are not using Keystone)
+path = tokens
+# Should typically be left as keystone unless you have a non-Keystone
+# authentication API service
+strategy = keystone
[compute]
+# This section contains configuration options used when executing tests
+# against the OpenStack Compute API.
+
+# This should be the username of a user WITHOUT administrative privileges
+username = {$USERNAME}
+# The above non-administrative user's password
+password = {$PASSWORD}
+# The above non-administrative user's tenant name
+tenant_name = {$TENANT_NAME}
+
+# This should be the username of an alternate user WITHOUT
+# administrative privileges
+alt_username = {$ALT_USERNAME}
+# The above non-administrative user's password
+alt_password = {$ALT_PASSWORD}
+# The above non-administrative user's tenant name
+alt_tenant_name = {$ALT_TENANT_NAME}
+
# Reference data for tests. The ref and ref_alt should be
# distinct images/flavors.
-image_ref=e7ddc02e-92fa-4f82-b36f-59b39bf66a67
-image_ref_alt=346f4039-a81e-44e0-9223-4a3d13c92a07
-flavor_ref=1
-flavor_ref_alt=2
-ssh_timeout=300
-build_interval=10
-build_timeout=600
-catalog_type=compute
-create_image_enabled=true
+image_ref = {$IMAGE_ID}
+image_ref_alt = {$IMAGE_ID_ALT}
+flavor_ref = 1
+flavor_ref_alt = 2
+
+# Number of seconds to wait while looping to check the status of an
+# instance or volume that is building.
+build_interval = 10
+
+# Number of seconds to time out on waiting for an instance or volume
+# to build or reach an expected status
+build_timeout = 600
+
+# The type of endpoint for a Compute API service. Unless you have a
+# custom Keystone service catalog implementation, you probably want to leave
+# this value as "compute"
+catalog_type = compute
+
+# Does the Compute API support creation of images?
+create_image_enabled = true
+
# For resize to work with libvirt/kvm, one of the following must be true:
# Single node: allow_resize_to_same_host=True must be set in nova.conf
# Cluster: the 'nova' user must have scp access between cluster nodes
-resize_available=true
+resize_available = true
[image]
-username=admin
-password=********
-tenant=admin
-auth_url=http://localhost:5000/v2.0
+# This section contains configuration options used when executing tests
+# against the OpenStack Images API
+
+# This should be the username of a user WITHOUT administrative privileges
+username = {$USERNAME}
+# The above non-administrative user's password
+password = {$PASSWORD}
+# The above non-administrative user's tenant name
+tenant_name = {$TENANT_NAME}
+
+[compute-admin]
+# This section contains configuration options for an administrative
+# user of the Compute API. These options are used in tests that stress
+# the admin-only parts of the Compute API
+
+# This should be the username of a user WITH administrative privileges
+username = {$ADMIN_USERNAME}
+# The above administrative user's password
+password = {$ADMIN_PASSWORD}
+# The above administrative user's tenant name
+tenant_name = {$ADMIN_TENANT_NAME}
diff --git a/tempest/common/rest_client.py b/tempest/common/rest_client.py
index ff27384..7b5d87a 100644
--- a/tempest/common/rest_client.py
+++ b/tempest/common/rest_client.py
@@ -1,8 +1,26 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2012 OpenStack, LLC
+# All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
import json
import httplib2
import logging
import sys
import time
+
from tempest import exceptions
diff --git a/tempest/config.py b/tempest/config.py
index f805d93..f675ea6 100644
--- a/tempest/config.py
+++ b/tempest/config.py
@@ -1,24 +1,49 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2012 OpenStack, LLC
+# All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
import ConfigParser
import logging
import os
+
from tempest.common.utils import data_utils
LOG = logging.getLogger(__name__)
-class IdentityConfig(object):
- """Provides configuration information for authenticating with Keystone."""
+class BaseConfig(object):
+
+ SECTION_NAME = None
def __init__(self, conf):
- """Initialize an Identity-specific configuration object"""
self.conf = conf
def get(self, item_name, default_value=None):
try:
- return self.conf.get("identity", item_name)
+ return self.conf.get(self.SECTION_NAME, item_name)
except (ConfigParser.NoSectionError, ConfigParser.NoOptionError):
return default_value
+
+class IdentityConfig(BaseConfig):
+
+ """Provides configuration information for authenticating with Keystone."""
+
+ SECTION_NAME = "identity"
+
@property
def host(self):
"""Host IP for making Identity API requests."""
@@ -55,61 +80,54 @@
return self.get("use_ssl", 'false').lower() != 'false'
@property
- def nonadmin_user1(self):
- """Username to use for Nova API requests."""
- return self.get("nonadmin_user1")
-
- @property
- def nonadmin_user1_tenant_name(self):
- """Tenant name to use for Nova API requests."""
- return self.get("nonadmin_user1_tenant_name")
-
- @property
- def nonadmin_user1_password(self):
- """API key to use when authenticating."""
- return self.get("nonadmin_user1_password")
-
- @property
- def nonadmin_user2(self):
- """Alternate username to use for Nova API requests."""
- return self.get("nonadmin_user2")
-
- @property
- def nonadmin_user2_tenant_name(self):
- """Alternate tenant name for Nova API requests."""
- return self.get("nonadmin_user2_tenant_name")
-
- @property
- def nonadmin_user2_password(self):
- """Alternate API key to use when authenticating."""
- return self.get("nonadmin_user2_password")
-
- @property
def strategy(self):
"""Which auth method does the environment use? (basic|keystone)"""
return self.get("strategy", 'keystone')
-class ComputeConfig(object):
- def __init__(self, conf):
- """Initialize a Compute-specific configuration object."""
- self.conf = conf
+class ComputeConfig(BaseConfig):
- def get(self, item_name, default_value):
- try:
- return self.conf.get("compute", item_name)
- except (ConfigParser.NoSectionError, ConfigParser.NoOptionError):
- return default_value
+ SECTION_NAME = "compute"
+
+ @property
+ def username(self):
+ """Username to use for Nova API requests."""
+ return self.get("username", "demo")
+
+ @property
+ def tenant_name(self):
+ """Tenant name to use for Nova API requests."""
+ return self.get("tenant_name", "demo")
+
+ @property
+ def password(self):
+ """API key to use when authenticating."""
+ return self.get("password", "pass")
+
+ @property
+ def alt_username(self):
+ """Username of alternate user to use for Nova API requests."""
+ return self.get("alt_username", "demo")
+
+ @property
+ def alt_tenant_name(self):
+ """Alternate user's Tenant name to use for Nova API requests."""
+ return self.get("alt_tenant_name", "demo")
+
+ @property
+ def alt_password(self):
+ """API key to use when authenticating as alternate user."""
+ return self.get("alt_password", "pass")
@property
def image_ref(self):
"""Valid primary image to use in tests."""
- return self.get("image_ref", 'e7ddc02e-92fa-4f82-b36f-59b39bf66a67')
+ return self.get("image_ref", "{$IMAGE_ID}")
@property
def image_ref_alt(self):
"""Valid secondary image reference to be used in tests."""
- return self.get("image_ref_alt", '346f4039-a81e-44e0-9223-4a3d13c907')
+ return self.get("image_ref_alt", "{$IMAGE_ID_ALT}")
@property
def flavor_ref(self):
@@ -132,21 +150,11 @@
return self.get("create_image_enabled", 'false').lower() != 'false'
@property
- def release_name(self):
- """Which release is this?"""
- return self.get("release_name", 'essex')
-
- @property
def build_interval(self):
"""Time in seconds between build status checks."""
return float(self.get("build_interval", 10))
@property
- def ssh_timeout(self):
- """Timeout in seconds to use when connecting via ssh."""
- return float(self.get("ssh_timeout", 300))
-
- @property
def build_timeout(self):
"""Timeout in seconds to wait for an entity to build."""
return float(self.get("build_timeout", 300))
@@ -157,20 +165,14 @@
return self.get("catalog_type", 'compute')
-class ImagesConfig(object):
+class ImagesConfig(BaseConfig):
+
"""
Provides configuration information for connecting to an
OpenStack Images service.
"""
- def __init__(self, conf):
- self.conf = conf
-
- def get(self, item_name, default_value=None):
- try:
- return self.conf.get("image", item_name)
- except (ConfigParser.NoSectionError, ConfigParser.NoOptionError):
- return default_value
+ SECTION_NAME = "image"
@property
def host(self):
@@ -189,31 +191,34 @@
@property
def username(self):
- """Username to use for Images API requests. Defaults to 'admin'."""
- return self.get("user", "admin")
+ """Username to use for Images API requests. Defaults to 'demo'."""
+ return self.get("user", "demo")
@property
def password(self):
"""Password for user"""
- return self.get("password", "")
+ return self.get("password", "pass")
@property
- def tenant(self):
- """Tenant to use for Images API requests. Defaults to 'admin'."""
- return self.get("tenant", "admin")
-
- @property
- def service_token(self):
- """Token to use in querying the API. Default: None"""
- return self.get("service_token")
-
- @property
- def auth_url(self):
- """Optional URL to auth service. Will be discovered if None"""
- return self.get("auth_url")
+ def tenant_name(self):
+ """Tenant to use for Images API requests. Defaults to 'demo'."""
+ return self.get("tenant_name", "demo")
-class TempestConfig(object):
+# TODO(jaypipes): Move this to a common utils (not data_utils...)
+def singleton(cls):
+ """Simple wrapper for classes that should only have a single instance"""
+ instances = {}
+
+ def getinstance():
+ if cls not in instances:
+ instances[cls] = cls()
+ return instances[cls]
+ return getinstance
+
+
+@singleton
+class TempestConfig:
"""Provides OpenStack configuration information."""
DEFAULT_CONFIG_DIR = os.path.join(
@@ -235,6 +240,8 @@
path = os.path.join(conf_dir, conf_file)
+ LOG.info("Using tempest config file %s" % path)
+
if not os.path.exists(path):
msg = "Config file %(path)s not found" % locals()
raise RuntimeError(msg)
diff --git a/tempest/openstack.py b/tempest/openstack.py
index d52647b..ffe59c4 100644
--- a/tempest/openstack.py
+++ b/tempest/openstack.py
@@ -1,3 +1,22 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2012 OpenStack, LLC
+# All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+import logging
+
import tempest.config
from tempest import exceptions
from tempest.services.image import service as image_service
@@ -12,25 +31,28 @@
from tempest.services.nova.json.keypairs_client import KeyPairsClient
from tempest.services.nova.json.volumes_client import VolumesClient
+LOG = logging.getLogger(__name__)
+
class Manager(object):
- def __init__(self, username=None, password=None, tenant_name=None):
- """
- Top level manager for all Openstack APIs
- """
+ """
+ Top level manager for OpenStack Compute clients
+ """
+
+ def __init__(self):
self.config = tempest.config.TempestConfig()
- if None in [username, password, tenant_name]:
- # Pull from the default, the first non-admin user
- username = self.config.identity.nonadmin_user1
- password = self.config.identity.nonadmin_user1_password
- tenant_name = self.config.identity.nonadmin_user1_tenant_name
+ username = self.config.compute.username
+ password = self.config.compute.password
+ tenant_name = self.config.compute.tenant_name
- if None in [username, password, tenant_name]:
- # We can't find any usable credentials, fail early
- raise exceptions.InvalidConfiguration(message="Missing complete \
- user credentials.")
+ if None in (username, password, tenant_name):
+ msg = ("Missing required credentials. "
+ "username: %(username)s, password: %(password)s, "
+ "tenant_name: %(tenant_name)s") % locals()
+ raise exceptions.InvalidConfiguration(msg)
+
auth_url = self.config.identity.auth_url
if self.config.identity.strategy == 'keystone':
diff --git a/tempest/services/image/service.py b/tempest/services/image/service.py
index 2cf053f..901e497 100644
--- a/tempest/services/image/service.py
+++ b/tempest/services/image/service.py
@@ -42,14 +42,14 @@
creds = {
'username': config.images.username,
'password': config.images.password,
- 'tenant': config.images.tenant,
- 'auth_url': config.images.auth_url,
- 'strategy': 'keystone'
+ 'tenant': config.images.tenant_name,
+ # rstrip() is necessary here because Glance client
+ # automatically adds the tokens/ part...
+ 'auth_url': config.identity.auth_url.rstrip('/tokens'),
+ 'strategy': config.identity.strategy
}
- service_token = config.images.service_token
self._client = client.Client(config.images.host,
config.images.port,
- auth_tok=service_token,
creds=creds)
else:
raise NotImplementedError
diff --git a/tempest/tests/image/test_images.py b/tempest/tests/image/test_images.py
index 8f75f08..0e515dc 100644
--- a/tempest/tests/image/test_images.py
+++ b/tempest/tests/image/test_images.py
@@ -109,7 +109,6 @@
self.assertEqual(1024, results['size'])
@attr(type='image')
- @unittest.skip('Skipping until Glance Bug 912897 is fixed')
def test_register_remote_image(self):
"""Register a new remote image"""
meta = {
@@ -141,7 +140,6 @@
def setUpClass(cls):
if not GLANCE_INSTALLED:
raise SkipTest('Glance not installed')
- raise SkipTest('Skipping until Glance Bug 912897 is fixed')
cls.os = openstack.ServiceManager()
cls.client = cls.os.images.get_client()
cls.created_images = []
@@ -149,7 +147,7 @@
# We add a few images here to test the listing functionality of
# the images API
- for x in xrange(1, 10):
+ for x in xrange(0, 10):
# We make even images remote and odd images standard
if x % 2 == 0:
cls.created_images.append(cls._create_remote_image(x))
diff --git a/tempest/tests/test_authorization.py b/tempest/tests/test_authorization.py
index f71073d..fa7a003 100644
--- a/tempest/tests/test_authorization.py
+++ b/tempest/tests/test_authorization.py
@@ -4,6 +4,8 @@
from nose.tools import raises
from tempest import openstack
+from tempest.services.nova.json.images_client import ImagesClient
+from tempest.services.nova.json.servers_client import ServersClient
from tempest.common.utils.data_utils import rand_name
from tempest.exceptions import NotFound, ComputeFault, BadRequest, Unauthorized
from tempest.tests import utils
@@ -23,10 +25,10 @@
cls.flavor_ref_alt = cls.config.compute.flavor_ref_alt
# Verify the second user is not the same as the first and is configured
- cls.user1 = cls.config.identity.nonadmin_user1
- cls.user2 = cls.config.identity.nonadmin_user2
- cls.user2_password = cls.config.identity.nonadmin_user2_password
- cls.user2_tenant_name = cls.config.identity.nonadmin_user2_tenant_name
+ cls.user1 = cls.config.compute.username
+ cls.user2 = cls.config.compute.alt_username
+ cls.user2_password = cls.config.compute.alt_password
+ cls.user2_tenant_name = cls.config.compute.alt_tenant_name
cls.multi_user = False
if (cls.user2 != None and cls.user1 != cls.user2
@@ -35,10 +37,18 @@
# Setup a client instance for the second user
cls.multi_user = True
- cls.os_other = openstack.Manager(cls.user2, cls.user2_password,
- cls.user2_tenant_name)
- cls.other_client = cls.os_other.servers_client
- cls.other_images_client = cls.os_other.images_client
+
+ auth_url = self.config.identity.auth_url
+
+ if self.config.identity.strategy == 'keystone':
+ client_args = (self.config, cls.user2, cls.user_2password,
+ auth_url, cls.user2_tenant_name)
+ else:
+ client_args = (self.config, cls.user2, cls.user2_password,
+ auth_url)
+
+ cls.other_client = ServersClient(*client_args)
+ cls.other_images_client = ImagesClient(*client_args)
name = rand_name('server')
resp, server = cls.client.create_server(name, cls.image_ref,
diff --git a/tempest/tests/test_flavors.py b/tempest/tests/test_flavors.py
index ac58dba..7d8a94b 100644
--- a/tempest/tests/test_flavors.py
+++ b/tempest/tests/test_flavors.py
@@ -7,8 +7,6 @@
class FlavorsTest(unittest.TestCase):
- release = tempest.config.TempestConfig().compute.release_name
-
@classmethod
def setUpClass(cls):
cls.os = openstack.Manager()
@@ -44,7 +42,6 @@
self.assertRaises(exceptions.NotFound, self.client.get_flavor_details,
999)
- @unittest.skipIf(release == 'diablo', 'bug in diablo')
@attr(type='positive', bug='lp912922')
def test_list_flavors_limit_results(self):
"""Only the expected number of flavors should be returned"""
@@ -52,7 +49,6 @@
resp, flavors = self.client.list_flavors(params)
self.assertEqual(1, len(flavors))
- @unittest.skipIf(release == 'diablo', 'bug in diablo')
@attr(type='positive', bug='lp912922')
def test_list_flavors_detailed_limit_results(self):
"""Only the expected number of flavors (detailed) should be returned"""
@@ -84,7 +80,6 @@
self.assertFalse(any([i for i in flavors if i['id'] == flavor_id]),
'The list of flavors did not start after the marker.')
- @unittest.skipIf(release == 'diablo', 'bug in diablo')
@attr(type='positive')
def test_list_flavors_detailed_filter_by_min_disk(self):
"""The detailed list of flavors should be filtered by disk space"""
@@ -96,7 +91,6 @@
resp, flavors = self.client.list_flavors_with_detail(params)
self.assertFalse(any([i for i in flavors if i['id'] == flavor_id]))
- @unittest.skipIf(release == 'diablo', 'bug in diablo')
@attr(type='positive')
def test_list_flavors_detailed_filter_by_min_ram(self):
"""The detailed list of flavors should be filtered by RAM"""
@@ -108,7 +102,6 @@
resp, flavors = self.client.list_flavors_with_detail(params)
self.assertFalse(any([i for i in flavors if i['id'] == flavor_id]))
- @unittest.skipIf(release == 'diablo', 'bug in diablo')
@attr(type='positive')
def test_list_flavors_filter_by_min_disk(self):
"""The list of flavors should be filtered by disk space"""
@@ -120,7 +113,6 @@
resp, flavors = self.client.list_flavors(params)
self.assertFalse(any([i for i in flavors if i['id'] == flavor_id]))
- @unittest.skipIf(release == 'diablo', 'bug in diablo')
@attr(type='positive')
def test_list_flavors_filter_by_min_ram(self):
"""The list of flavors should be filtered by RAM"""
diff --git a/tempest/tests/test_keypairs.py b/tempest/tests/test_keypairs.py
index 9f17922..baedbb6 100644
--- a/tempest/tests/test_keypairs.py
+++ b/tempest/tests/test_keypairs.py
@@ -8,8 +8,6 @@
class KeyPairsTest(unittest.TestCase):
- release = tempest.config.TempestConfig().compute.release_name
-
@classmethod
def setUpClass(cls):
cls.os = openstack.Manager()
@@ -138,7 +136,6 @@
else:
self.fail('empty string')
- @unittest.skipIf(release == 'diablo', 'bug in diablo')
@attr(type='negative')
def test_create_keypair_with_long_keynames(self):
"""Keypairs with name longer than 255 chars should not be created"""
diff --git a/tempest/tests/test_servers_negative.py b/tempest/tests/test_servers_negative.py
index f8de727..6c738ea 100644
--- a/tempest/tests/test_servers_negative.py
+++ b/tempest/tests/test_servers_negative.py
@@ -6,8 +6,6 @@
class ServersNegativeTest(unittest.TestCase):
- release = tempest.config.TempestConfig().\
- compute.release_name
@classmethod
def setUpClass(cls):
@@ -16,7 +14,6 @@
cls.config = cls.os.config
cls.image_ref = cls.config.compute.image_ref
cls.flavor_ref = cls.config.compute.flavor_ref
- cls.ssh_timeout = cls.config.compute.ssh_timeout
def test_server_name_blank(self):
"""Create a server with name parameter empty"""
@@ -63,7 +60,6 @@
else:
self.fail('Cannot create a server with an invalid flavor')
- @unittest.skipIf(release == 'diablo', 'Bug in Diablo, lp#891264')
def test_invalid_access_ip_v4_address(self):
"""An access IPv4 address must match a valid address pattern"""
accessIPv4 = '1.1.1.1.1.1'
@@ -78,7 +74,6 @@
else:
self.fail('Access IPv4 address must match the correct format')
- @unittest.skipIf(release == 'diablo', 'Bug in Diablo, lp#891264')
def test_invalid_ip_v6_address(self):
"""An access IPv6 address must match a valid address pattern"""
accessIPv6 = 'notvalid'