Merge "Test to GET public-readable container's object"
diff --git a/etc/tempest.conf.sample b/etc/tempest.conf.sample
index cc91716..534f3d9 100644
--- a/etc/tempest.conf.sample
+++ b/etc/tempest.conf.sample
@@ -185,6 +185,44 @@
# Catalog type of the Quantum Service
catalog_type = network
+# This should be the username of a user WITHOUT administrative privileges
+username = demo
+# The above non-administrative user's password
+password = pass
+# The above non-administrative user's tenant name
+tenant_name = demo
+
+# A large private cidr block from which to allocate smaller blocks for
+# tenant networks.
+tenant_network_cidr = 10.100.0.0/16
+
+# The mask bits used to partition the tenant block.
+tenant_network_mask_bits = 29
+
+# If tenant networks are reachable, connectivity checks will be
+# performed directly against addresses on those networks.
+tenant_networks_reachable = false
+
+# Id of the public network that provides external connectivity.
+public_network_id = {$PUBLIC_NETWORK_UUID}
+
+# Id of a shared public router that provides external connectivity.
+# A shared public router would commonly be used where IP namespaces
+# were disabled. If namespaces are enabled, it would be preferable
+# for each tenant to have their own router.
+public_router_id =
+
+[network-admin]
+# This section contains configuration options for an administrative
+# user of the Network API.
+
+# This should be the username of a user WITH administrative privileges
+username = admin
+# The above administrative user's password
+password = pass
+# The above administrative user's tenant name
+tenant_name = admin
+
[identity-admin]
# This section contains configuration options for an administrative
# user of the Compute API. These options are used in tests that stress
diff --git a/tempest/common/rest_client.py b/tempest/common/rest_client.py
index 8924fd3..cb18a9c 100644
--- a/tempest/common/rest_client.py
+++ b/tempest/common/rest_client.py
@@ -141,13 +141,7 @@
mgmt_url = None
for ep in auth_data['serviceCatalog']:
- if ep["type"] == service and service != 'volume':
- mgmt_url = ep['endpoints'][self.region][self.endpoint_url]
- tenant_id = auth_data['token']['tenant']['id']
- break
-
- elif (ep["type"] == service and ep['name'] == 'cinder' and
- service == 'volume'):
+ if ep["type"] == service:
mgmt_url = ep['endpoints'][self.region][self.endpoint_url]
tenant_id = auth_data['token']['tenant']['id']
break
diff --git a/tempest/config.py b/tempest/config.py
index 25fbbb9..1d1aa49 100644
--- a/tempest/config.py
+++ b/tempest/config.py
@@ -378,6 +378,68 @@
"""Version of Quantum API"""
return self.get("api_version", "v1.1")
+ @property
+ def username(self):
+ """Username to use for Quantum API requests."""
+ return self.get("username", "demo")
+
+ @property
+ def tenant_name(self):
+ """Tenant name to use for Quantum API requests."""
+ return self.get("tenant_name", "demo")
+
+ @property
+ def password(self):
+ """API key to use when authenticating as admin."""
+ return self.get("password", "pass")
+
+ @property
+ def tenant_network_cidr(self):
+ """The cidr block to allocate tenant networks from"""
+ return self.get("tenant_network_cidr", "10.100.0.0/16")
+
+ @property
+ def tenant_network_mask_bits(self):
+ """The mask bits for tenant networks"""
+ return int(self.get("tenant_network_mask_bits", "29"))
+
+ @property
+ def tenant_networks_reachable(self):
+ """Whether tenant network connectivity should be evaluated directly"""
+ return (
+ self.get("tenant_networks_reachable", 'false').lower() != 'false'
+ )
+
+ @property
+ def public_network_id(self):
+ """Id of the public network that provides external connectivity"""
+ return self.get("public_network_id", "")
+
+ @property
+ def public_router_id(self):
+ """Id of the public router that provides external connectivity"""
+ return self.get("public_router_id", "")
+
+
+class NetworkAdminConfig(BaseConfig):
+
+ SECTION_NAME = "network-admin"
+
+ @property
+ def username(self):
+ """Administrative Username to use for Quantum API requests."""
+ return self.get("username", "admin")
+
+ @property
+ def tenant_name(self):
+ """Administrative Tenant name to use for Quantum API requests."""
+ return self.get("tenant_name", "admin")
+
+ @property
+ def password(self):
+ """API key to use when authenticating as admin."""
+ return self.get("password", "pass")
+
class VolumeConfig(BaseConfig):
"""Provides configuration information for connecting to an OpenStack Block
@@ -544,6 +606,7 @@
self.identity_admin = IdentityAdminConfig(self._conf)
self.images = ImagesConfig(self._conf)
self.network = NetworkConfig(self._conf)
+ self.network_admin = NetworkAdminConfig(self._conf)
self.volume = VolumeConfig(self._conf)
self.object_storage = ObjectStorageConfig(self._conf)
self.boto = BotoConfig(self._conf)
diff --git a/tempest/manager.py b/tempest/manager.py
index a17aa21..92caf57 100644
--- a/tempest/manager.py
+++ b/tempest/manager.py
@@ -145,12 +145,16 @@
endpoint_type='publicURL')
return glanceclient.Client('1', endpoint=endpoint, token=token)
- def _get_identity_client(self):
+ def _get_identity_client(self, username=None, password=None,
+ tenant_name=None):
# This identity client is not intended to check the security
- # of the identity service, so use admin credentials.
- username = self.config.identity_admin.username
- password = self.config.identity_admin.password
- tenant_name = self.config.identity_admin.tenant_name
+ # of the identity service, so use admin credentials by default.
+ if not username:
+ username = self.config.identity_admin.username
+ if not password:
+ password = self.config.identity_admin.password
+ if not tenant_name:
+ tenant_name = self.config.identity_admin.tenant_name
if None in (username, password, tenant_name):
msg = ("Missing required credentials for identity client. "
@@ -166,10 +170,15 @@
auth_url=auth_url)
def _get_network_client(self):
- # TODO(mnewby) add network-specific auth configuration
- username = self.config.compute.username
- password = self.config.compute.password
- tenant_name = self.config.compute.tenant_name
+ # The intended configuration is for the network client to have
+ # admin privileges and indicate for whom resources are being
+ # created via a 'tenant_id' parameter. This will often be
+ # preferable to authenticating as a specific user because
+ # working with certain resources (public routers and networks)
+ # often requires admin privileges anyway.
+ username = self.config.network_admin.username
+ password = self.config.network_admin.password
+ tenant_name = self.config.network_admin.tenant_name
if None in (username, password, tenant_name):
msg = ("Missing required credentials for network client. "
diff --git a/tempest/smoke.py b/tempest/smoke.py
index c929273..68d0927 100644
--- a/tempest/smoke.py
+++ b/tempest/smoke.py
@@ -51,15 +51,29 @@
# order, and because test methods in smoke tests generally create
# resources in a particular order, we destroy resources in the reverse
# order in which resources are added to the smoke test class object
- if not cls.resources:
- return
- thing = cls.resources.pop()
- while True:
+ while cls.resources:
+ thing = cls.resources.pop()
LOG.debug("Deleting %r from shared resources of %s" %
(thing, cls.__name__))
- # Resources in novaclient all have a delete() method
- # which destroys the resource...
+
+ # OpenStack resources are assumed to have a delete()
+ # method which destroys the resource...
thing.delete()
- if not cls.resources:
- return
- thing = cls.resources.pop()
+
+ def is_deletion_complete():
+ # Deletion testing is only required for objects whose
+ # existence cannot be checked via retrieval.
+ if isinstance(thing, dict):
+ return True
+ try:
+ thing.get()
+ except Exception as e:
+ # Clients are expected to return an exception
+ # called 'NotFound' if retrieval fails.
+ if e.__class__.__name__ == 'NotFound':
+ return True
+ raise
+ return False
+
+ # Block until resource deletion has completed or timed-out
+ test.call_until_true(is_deletion_complete, 10, 1)
diff --git a/tempest/tests/compute/base.py b/tempest/tests/compute/base.py
index 5094b46..1195cca 100644
--- a/tempest/tests/compute/base.py
+++ b/tempest/tests/compute/base.py
@@ -193,6 +193,24 @@
cls.servers.append(server)
return server
+ @classmethod
+ def create_server_with_extras(cls, name, image_id=None,
+ flavor=None, **kwargs):
+ # TODO(sdague) transitional function because many
+ # server tests were using extra args and resp so can't
+ # easily be ported to create_server. Will be merged
+ # later
+ if not flavor:
+ flavor = cls.flavor_ref
+ if not image_id:
+ image_id = cls.image_ref
+
+ resp, server = cls.servers_client.create_server(name,
+ image_id, flavor,
+ **kwargs)
+ cls.servers.append(server)
+ return resp, server
+
def wait_for(self, condition):
"""Repeatedly calls condition() until a timeout"""
start_time = int(time.time())
diff --git a/tempest/tests/compute/servers/test_disk_config.py b/tempest/tests/compute/servers/test_disk_config.py
index 638e093..7ff666f 100644
--- a/tempest/tests/compute/servers/test_disk_config.py
+++ b/tempest/tests/compute/servers/test_disk_config.py
@@ -39,10 +39,10 @@
def test_rebuild_server_with_manual_disk_config(self):
"""A server should be rebuilt using the manual disk config option"""
name = rand_name('server')
- resp, server = self.client.create_server(name,
- self.image_ref,
- self.flavor_ref,
- disk_config='AUTO')
+ resp, server = self.create_server_with_extras(name,
+ self.image_ref,
+ self.flavor_ref,
+ disk_config='AUTO')
#Wait for the server to become active
self.client.wait_for_server_status(server['id'], 'ACTIVE')
@@ -69,10 +69,10 @@
def test_rebuild_server_with_auto_disk_config(self):
"""A server should be rebuilt using the auto disk config option"""
name = rand_name('server')
- resp, server = self.client.create_server(name,
- self.image_ref,
- self.flavor_ref,
- disk_config='MANUAL')
+ resp, server = self.create_server_with_extras(name,
+ self.image_ref,
+ self.flavor_ref,
+ disk_config='MANUAL')
#Wait for the server to become active
self.client.wait_for_server_status(server['id'], 'ACTIVE')
@@ -100,10 +100,10 @@
def test_resize_server_from_manual_to_auto(self):
"""A server should be resized from manual to auto disk config"""
name = rand_name('server')
- resp, server = self.client.create_server(name,
- self.image_ref,
- self.flavor_ref,
- disk_config='MANUAL')
+ resp, server = self.create_server_with_extras(name,
+ self.image_ref,
+ self.flavor_ref,
+ disk_config='MANUAL')
#Wait for the server to become active
self.client.wait_for_server_status(server['id'], 'ACTIVE')
@@ -126,10 +126,10 @@
def test_resize_server_from_auto_to_manual(self):
"""A server should be resized from auto to manual disk config"""
name = rand_name('server')
- resp, server = self.client.create_server(name,
- self.image_ref,
- self.flavor_ref,
- disk_config='AUTO')
+ resp, server = self.create_server_with_extras(name,
+ self.image_ref,
+ self.flavor_ref,
+ disk_config='AUTO')
#Wait for the server to become active
self.client.wait_for_server_status(server['id'], 'ACTIVE')
diff --git a/tempest/tests/compute/servers/test_server_actions.py b/tempest/tests/compute/servers/test_server_actions.py
index 835afb0..63308fc 100644
--- a/tempest/tests/compute/servers/test_server_actions.py
+++ b/tempest/tests/compute/servers/test_server_actions.py
@@ -36,9 +36,9 @@
def setUp(self):
self.name = rand_name('server')
- resp, server = self.client.create_server(self.name,
- self.image_ref,
- self.flavor_ref)
+ resp, server = self.create_server_with_extras(self.name,
+ self.image_ref,
+ self.flavor_ref)
self.server_id = server['id']
self.password = server['adminPass']
self.client.wait_for_server_status(self.server_id, 'ACTIVE')
diff --git a/tempest/tests/compute/servers/test_server_metadata.py b/tempest/tests/compute/servers/test_server_metadata.py
index 844e394..0198e4e 100644
--- a/tempest/tests/compute/servers/test_server_metadata.py
+++ b/tempest/tests/compute/servers/test_server_metadata.py
@@ -80,7 +80,8 @@
meta = {key: 'data1'}
name = rand_name('server')
self.assertRaises(exceptions.OverLimit,
- self.client.create_server, name, self.image_ref,
+ self.create_server_with_extras,
+ name, self.image_ref,
self.flavor_ref, meta=meta)
# no teardown - all creates should fail
diff --git a/tempest/tests/compute/servers/test_server_personality.py b/tempest/tests/compute/servers/test_server_personality.py
index 3003a52..320bac4 100644
--- a/tempest/tests/compute/servers/test_server_personality.py
+++ b/tempest/tests/compute/servers/test_server_personality.py
@@ -41,8 +41,9 @@
personality.append({'path': path,
'contents': base64.b64encode(file_contents)})
try:
- self.client.create_server(name, self.image_ref, self.flavor_ref,
- personality=personality)
+ self.create_server_with_extras(name, self.image_ref,
+ self.flavor_ref,
+ personality=personality)
except exceptions.OverLimit:
pass
else:
@@ -61,16 +62,16 @@
max_file_limit = \
self.user_client.get_specific_absolute_limit("maxPersonality")
- personality = []
+ person = []
for i in range(0, int(max_file_limit)):
path = 'etc/test' + str(i) + '.txt'
- personality.append({
+ person.append({
'path': path,
'contents': base64.b64encode(file_contents),
})
- resp, server = self.client.create_server(name, self.image_ref,
- self.flavor_ref,
- personality=personality)
+ resp, server = self.create_server_with_extras(name, self.image_ref,
+ self.flavor_ref,
+ personality=person)
self.assertEqual('202', resp['status'])
except Exception:
diff --git a/tempest/tests/compute/servers/test_servers.py b/tempest/tests/compute/servers/test_servers.py
index c534829..fcf9975 100644
--- a/tempest/tests/compute/servers/test_servers.py
+++ b/tempest/tests/compute/servers/test_servers.py
@@ -33,9 +33,10 @@
try:
server = None
name = rand_name('server')
- resp, server = self.client.create_server(name, self.image_ref,
- self.flavor_ref,
- adminPass='testpassword')
+ resp, server = self.create_server_with_extras(name, self.image_ref,
+ self.flavor_ref,
+ adminPass='test'
+ 'password')
#Verify the password is set correctly in the response
self.assertEqual('testpassword', server['adminPass'])
@@ -52,14 +53,14 @@
id1 = None
id2 = None
server_name = rand_name('server')
- resp, server = self.client.create_server(server_name,
- self.image_ref,
- self.flavor_ref)
+ resp, server = self.create_server_with_extras(server_name,
+ self.image_ref,
+ self.flavor_ref)
self.client.wait_for_server_status(server['id'], 'ACTIVE')
id1 = server['id']
- resp, server = self.client.create_server(server_name,
- self.image_ref,
- self.flavor_ref)
+ resp, server = self.create_server_with_extras(server_name,
+ self.image_ref,
+ self.flavor_ref)
self.client.wait_for_server_status(server['id'], 'ACTIVE')
id2 = server['id']
self.assertNotEqual(id1, id2, "Did not create a new server")
@@ -83,10 +84,10 @@
resp, keypair = self.keypairs_client.create_keypair(key_name)
resp, body = self.keypairs_client.list_keypairs()
server_name = rand_name('server')
- resp, server = self.client.create_server(server_name,
- self.image_ref,
- self.flavor_ref,
- key_name=key_name)
+ resp, server = self.create_server_with_extras(server_name,
+ self.image_ref,
+ self.flavor_ref,
+ key_name=key_name)
self.assertEqual('202', resp['status'])
self.client.wait_for_server_status(server['id'], 'ACTIVE')
resp, server = self.client.get_server(server['id'])
@@ -101,8 +102,8 @@
try:
server = None
name = rand_name('server')
- resp, server = self.client.create_server(name, self.image_ref,
- self.flavor_ref)
+ resp, server = self.create_server_with_extras(name, self.image_ref,
+ self.flavor_ref)
self.client.wait_for_server_status(server['id'], 'ACTIVE')
#Update the server with a new name
@@ -128,8 +129,8 @@
try:
server = None
name = rand_name('server')
- resp, server = self.client.create_server(name, self.image_ref,
- self.flavor_ref)
+ resp, server = self.create_server_with_extras(name, self.image_ref,
+ self.flavor_ref)
self.client.wait_for_server_status(server['id'], 'ACTIVE')
#Update the IPv4 and IPv6 access addresses
@@ -152,8 +153,8 @@
def test_delete_server_while_in_building_state(self):
"""Delete a server while it's VM state is Building"""
name = rand_name('server')
- resp, server = self.client.create_server(name, self.image_ref,
- self.flavor_ref)
+ resp, server = self.create_server_with_extras(name, self.image_ref,
+ self.flavor_ref)
self.client.wait_for_server_status(server['id'], 'BUILD')
resp, _ = self.client.delete_server(server['id'])
self.assertEqual('204', resp['status'])
@@ -165,9 +166,27 @@
super(ServersTestJSON, cls).setUpClass()
cls.client = cls.servers_client
+ def tearDown(self):
+ # clean up any remaining servers and wait for them to fully
+ # delete. This is done because delete calls are async, and if
+ # deletes are running slow we could very well overrun system
+ # memory
+ self.clear_servers()
+
+ super(ServersTestJSON, self).tearDown()
+
class ServersTestXML(base.BaseComputeTestXML, ServersTestBase):
@classmethod
def setUpClass(cls):
super(ServersTestXML, cls).setUpClass()
cls.client = cls.servers_client
+
+ def tearDown(self):
+ # clean up any remaining servers and wait for them to fully
+ # delete. This is done because delete calls are async, and if
+ # deletes are running slow we could very well overrun system
+ # memory
+ self.clear_servers()
+
+ super(ServersTestXML, self).tearDown()
diff --git a/tempest/tests/compute/servers/test_servers_negative.py b/tempest/tests/compute/servers/test_servers_negative.py
index 60f3daf..c9ed5ed 100644
--- a/tempest/tests/compute/servers/test_servers_negative.py
+++ b/tempest/tests/compute/servers/test_servers_negative.py
@@ -42,8 +42,9 @@
def test_server_name_blank(self):
"""Create a server with name parameter empty"""
try:
- resp, server = self.client.create_server('', self.image_ref,
- self.flavor_ref)
+ resp, server = self.create_server_with_extras('',
+ self.image_ref,
+ self.flavor_ref)
except exceptions.BadRequest:
pass
else:
@@ -53,14 +54,14 @@
def test_personality_file_contents_not_encoded(self):
"""Use an unencoded file when creating a server with personality"""
file_contents = 'This is a test file.'
- personality = [{'path': '/etc/testfile.txt',
- 'contents': file_contents}]
+ person = [{'path': '/etc/testfile.txt',
+ 'contents': file_contents}]
try:
- resp, server = self.client.create_server('test',
- self.image_ref,
- self.flavor_ref,
- personality=personality)
+ resp, server = self.create_server_with_extras('test',
+ self.image_ref,
+ self.flavor_ref,
+ personality=person)
except exceptions.BadRequest:
pass
else:
@@ -70,8 +71,8 @@
def test_create_with_invalid_image(self):
"""Create a server with an unknown image"""
try:
- resp, server = self.client.create_server('fail', -1,
- self.flavor_ref)
+ resp, server = self.create_server_with_extras('fail', -1,
+ self.flavor_ref)
except exceptions.BadRequest:
pass
else:
@@ -81,7 +82,7 @@
def test_create_with_invalid_flavor(self):
"""Create a server with an unknown flavor"""
try:
- self.client.create_server('fail', self.image_ref, -1)
+ self.create_server_with_extras('fail', self.image_ref, -1)
except exceptions.BadRequest:
pass
else:
@@ -90,13 +91,13 @@
@attr(type='negative')
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'
+ IPv4 = '1.1.1.1.1.1'
name = rand_name('server')
try:
- resp, server = self.client.create_server(name,
- self.image_ref,
- self.flavor_ref,
- accessIPv4=accessIPv4)
+ resp, server = self.create_server_with_extras(name,
+ self.image_ref,
+ self.flavor_ref,
+ accessIPv4=IPv4)
except exceptions.BadRequest:
pass
else:
@@ -105,13 +106,13 @@
@attr(type='negative')
def test_invalid_ip_v6_address(self):
"""An access IPv6 address must match a valid address pattern"""
- accessIPv6 = 'notvalid'
+ IPv6 = 'notvalid'
name = rand_name('server')
try:
- resp, server = self.client.create_server(name,
- self.image_ref,
- self.flavor_ref,
- accessIPv6=accessIPv6)
+ resp, server = self.create_server_with_extras(name,
+ self.image_ref,
+ self.flavor_ref,
+ accessIPv6=IPv6)
except exceptions.BadRequest:
pass
else:
@@ -121,9 +122,9 @@
def test_reboot_deleted_server(self):
"""Reboot a deleted server"""
self.name = rand_name('server')
- resp, create_server = self.client.create_server(self.name,
- self.image_ref,
- self.flavor_ref)
+ resp, create_server = self.create_server_with_extras(self.name,
+ self.image_ref,
+ self.flavor_ref)
self.server_id = create_server['id']
self.client.delete_server(self.server_id)
self.client.wait_for_server_termination(self.server_id)
@@ -138,9 +139,9 @@
def test_rebuild_deleted_server(self):
"""Rebuild a deleted server"""
self.name = rand_name('server')
- resp, create_server = self.client.create_server(self.name,
- self.image_ref,
- self.flavor_ref)
+ resp, create_server = self.create_server_with_extras(self.name,
+ self.image_ref,
+ self.flavor_ref)
self.server_id = create_server['id']
self.client.delete_server(self.server_id)
self.client.wait_for_server_termination(self.server_id)
@@ -157,7 +158,8 @@
"""Create a server with a numeric name"""
server_name = 12345
- self.assertRaises(exceptions.BadRequest, self.client.create_server,
+ self.assertRaises(exceptions.BadRequest,
+ self.create_server_with_extras,
server_name, self.image_ref, self.flavor_ref)
@attr(type='negative')
@@ -165,7 +167,8 @@
"""Create a server with name length exceeding 256 characters"""
server_name = 'a' * 256
- self.assertRaises(exceptions.BadRequest, self.client.create_server,
+ self.assertRaises(exceptions.BadRequest,
+ self.create_server_with_extras,
server_name, self.image_ref, self.flavor_ref)
@attr(type='negative')
@@ -175,7 +178,8 @@
server_name = rand_name('server')
networks = [{'fixed_ip': '10.0.1.1', 'uuid':'a-b-c-d-e-f-g-h-i-j'}]
- self.assertRaises(exceptions.BadRequest, self.client.create_server,
+ self.assertRaises(exceptions.BadRequest,
+ self.create_server_with_extras,
server_name, self.image_ref, self.flavor_ref,
networks=networks)
@@ -185,7 +189,8 @@
key_name = rand_name('key')
server_name = rand_name('server')
- self.assertRaises(exceptions.BadRequest, self.client.create_server,
+ self.assertRaises(exceptions.BadRequest,
+ self.create_server_with_extras,
server_name, self.image_ref, self.flavor_ref,
key_name=key_name)
@@ -196,7 +201,8 @@
server_name = rand_name('server')
metadata = {'a': 'b' * 260}
- self.assertRaises(exceptions.OverLimit, self.client.create_server,
+ self.assertRaises(exceptions.OverLimit,
+ self.create_server_with_extras,
server_name, self.image_ref, self.flavor_ref,
meta=metadata)
diff --git a/tempest/tests/network/test_network_basic_ops.py b/tempest/tests/network/test_network_basic_ops.py
new file mode 100644
index 0000000..1d88759
--- /dev/null
+++ b/tempest/tests/network/test_network_basic_ops.py
@@ -0,0 +1,454 @@
+# 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 subprocess
+
+import netaddr
+import nose
+
+from quantumclient.common import exceptions as exc
+
+from tempest.common.utils.data_utils import rand_name
+from tempest import smoke
+from tempest import test
+
+
+LOG = logging.getLogger(__name__)
+
+
+class AttributeDict(dict):
+
+ """
+ Provide attribute access (dict.key) to dictionary values.
+ """
+
+ def __getattr__(self, name):
+ """Allow attribute access for all keys in the dict."""
+ if name in self:
+ return self[name]
+ return super(AttributeDict, self).__getattribute__(name)
+
+
+class DeletableResource(AttributeDict):
+
+ """
+ Support deletion of quantum resources (networks, subnets) via a
+ delete() method, as is supported by keystone and nova resources.
+ """
+
+ def __init__(self, *args, **kwargs):
+ self.client = kwargs.pop('client', None)
+ super(DeletableResource, self).__init__(*args, **kwargs)
+
+ def __str__(self):
+ return '<%s id="%s" name="%s">' % (self.__class__.__name__,
+ self.id, self.name)
+
+ def delete(self):
+ raise NotImplemented()
+
+
+class DeletableNetwork(DeletableResource):
+
+ def delete(self):
+ self.client.delete_network(self.id)
+
+
+class DeletableSubnet(DeletableResource):
+
+ _router_ids = set()
+
+ def add_to_router(self, router_id):
+ self._router_ids.add(router_id)
+ body = dict(subnet_id=self.id)
+ self.client.add_interface_router(router_id, body=body)
+
+ def delete(self):
+ for router_id in self._router_ids.copy():
+ body = dict(subnet_id=self.id)
+ self.client.remove_interface_router(router_id, body=body)
+ self._router_ids.remove(router_id)
+ self.client.delete_subnet(self.id)
+
+
+class DeletableRouter(DeletableResource):
+
+ def add_gateway(self, network_id):
+ body = dict(network_id=network_id)
+ self.client.add_gateway_router(self.id, body=body)
+
+ def delete(self):
+ self.client.remove_gateway_router(self.id)
+ self.client.delete_router(self.id)
+
+
+class DeletableFloatingIp(DeletableResource):
+
+ def delete(self):
+ self.client.delete_floatingip(self.id)
+
+
+class TestNetworkBasicOps(smoke.DefaultClientSmokeTest):
+
+ """
+ This smoke test suite assumes that Nova has been configured to
+ boot VM's with Quantum-managed networking, and attempts to
+ verify network connectivity as follows:
+
+ * For a freshly-booted VM with an IP address ("port") on a given network:
+
+ - the Tempest host can ping the IP address. This implies that
+ the VM has been assigned the correct IP address and has
+ connectivity to the Tempest host.
+
+ #TODO(mnewby) - Need to implement the following:
+ - the Tempest host can ssh into the VM via the IP address and
+ successfully execute the following:
+
+ - ping an external IP address, implying external connectivity.
+
+ - ping an external hostname, implying that dns is correctly
+ configured.
+
+ - ping an internal IP address, implying connectivity to another
+ VM on the same network.
+
+ There are presumed to be two types of networks: tenant and
+ public. A tenant network may or may not be reachable from the
+ Tempest host. A public network is assumed to be reachable from
+ the Tempest host, and it should be possible to associate a public
+ ('floating') IP address with a tenant ('fixed') IP address to
+ faciliate external connectivity to a potentially unroutable
+ tenant IP address.
+
+ This test suite can be configured to test network connectivity to
+ a VM via a tenant network, a public network, or both. If both
+ networking types are to be evaluated, tests that need to be
+ executed remotely on the VM (via ssh) will only be run against
+ one of the networks (to minimize test execution time).
+
+ Determine which types of networks to test as follows:
+
+ * Configure tenant network checks (via the
+ 'tenant_networks_reachable' key) if the Tempest host should
+ have direct connectivity to tenant networks. This is likely to
+ be the case if Tempest is running on the same host as a
+ single-node devstack installation with IP namespaces disabled.
+
+ * Configure checks for a public network if a public network has
+ been configured prior to the test suite being run and if the
+ Tempest host should have connectivity to that public network.
+ Checking connectivity for a public network requires that a
+ value be provided for 'public_network_id'. A value can
+ optionally be provided for 'public_router_id' if tenants will
+ use a shared router to access a public network (as is likely to
+ be the case when IP namespaces are not enabled). If a value is
+ not provided for 'public_router_id', a router will be created
+ for each tenant and use the network identified by
+ 'public_network_id' as its gateway.
+
+ """
+
+ @classmethod
+ def check_preconditions(cls):
+ cfg = cls.config.network
+ msg = None
+ if not (cfg.tenant_networks_reachable or cfg.public_network_id):
+ msg = ('Either tenant_networks_reachable must be "true", or '
+ 'public_network_id must be defined.')
+ else:
+ try:
+ cls.network_client.list_networks()
+ except exc.QuantumClientException:
+ msg = 'Unable to connect to Quantum service.'
+
+ cls.enabled = not bool(msg)
+ if msg:
+ raise nose.SkipTest(msg)
+
+ @classmethod
+ def setUpClass(cls):
+ super(TestNetworkBasicOps, cls).setUpClass()
+ cls.check_preconditions()
+ cfg = cls.config.network
+ cls.tenant_id = cls.manager._get_identity_client(
+ cfg.username,
+ cfg.password,
+ cfg.tenant_name).tenant_id
+ # TODO(mnewby) Consider looking up entities as needed instead
+ # of storing them as collections on the class.
+ cls.keypairs = {}
+ cls.security_groups = {}
+ cls.networks = []
+ cls.servers = []
+ cls.floating_ips = {}
+
+ def _create_keypair(self, client):
+ kp_name = rand_name('keypair-smoke-')
+ keypair = client.keypairs.create(kp_name)
+ try:
+ self.assertEqual(keypair.id, kp_name)
+ self.set_resource(kp_name, keypair)
+ except AttributeError:
+ self.fail("Keypair object not successfully created.")
+ return keypair
+
+ def _create_security_group(self, client):
+ # Create security group
+ sg_name = rand_name('secgroup-smoke-')
+ sg_desc = sg_name + " description"
+ secgroup = client.security_groups.create(sg_name, sg_desc)
+ try:
+ self.assertEqual(secgroup.name, sg_name)
+ self.assertEqual(secgroup.description, sg_desc)
+ self.set_resource(sg_name, secgroup)
+ except AttributeError:
+ self.fail("SecurityGroup object not successfully created.")
+
+ # Add rules to the security group
+ rulesets = [
+ {
+ # ssh
+ 'ip_protocol': 'tcp',
+ 'from_port': 22,
+ 'to_port': 22,
+ 'cidr': '0.0.0.0/0',
+ 'group_id': secgroup.id
+ },
+ {
+ # ping
+ 'ip_protocol': 'icmp',
+ 'from_port': -1,
+ 'to_port': -1,
+ 'cidr': '0.0.0.0/0',
+ 'group_id': secgroup.id
+ }
+ ]
+ for ruleset in rulesets:
+ try:
+ client.security_group_rules.create(secgroup.id, **ruleset)
+ except Exception:
+ self.fail("Failed to create rule in security group.")
+
+ return secgroup
+
+ def _get_router(self, tenant_id):
+ """Retrieve a router for the given tenant id.
+
+ If a public router has been configured, it will be returned.
+
+ If a public router has not been configured, but a public
+ network has, a tenant router will be created and returned that
+ routes traffic to the public network.
+
+ """
+ router_id = self.config.network.public_router_id
+ network_id = self.config.network.public_network_id
+ if router_id:
+ result = self.network_client.show_router(router_id)
+ return AttributeDict(**result['router'])
+ elif network_id:
+ router = self._create_router(tenant_id)
+ router.add_gateway(network_id)
+ return router
+ else:
+ raise Exception("Neither of 'public_router_id' or "
+ "'public_network_id' has been defined.")
+
+ def _create_router(self, tenant_id):
+ name = rand_name('router-smoke-')
+ body = dict(
+ router=dict(
+ name=name,
+ admin_state_up=True,
+ tenant_id=tenant_id,
+ ),
+ )
+ result = self.network_client.create_router(body=body)
+ router = DeletableRouter(client=self.network_client,
+ **result['router'])
+ self.assertEqual(router.name, name)
+ self.set_resource(name, router)
+ return router
+
+ def _create_network(self, tenant_id):
+ name = rand_name('network-smoke-')
+ body = dict(
+ network=dict(
+ name=name,
+ tenant_id=tenant_id,
+ ),
+ )
+ result = self.network_client.create_network(body=body)
+ network = DeletableNetwork(client=self.network_client,
+ **result['network'])
+ self.assertEqual(network.name, name)
+ self.set_resource(name, network)
+ return network
+
+ def _create_subnet(self, network):
+ """
+ Create a subnet for the given network within the cidr block
+ configured for tenant networks.
+ """
+ cfg = self.config.network
+ tenant_cidr = netaddr.IPNetwork(cfg.tenant_network_cidr)
+ result = None
+ # Repeatedly attempt subnet creation with sequential cidr
+ # blocks until an unallocated block is found.
+ for subnet_cidr in tenant_cidr.subnet(cfg.tenant_network_mask_bits):
+ body = dict(
+ subnet=dict(
+ ip_version=4,
+ network_id=network.id,
+ tenant_id=network.tenant_id,
+ cidr=str(subnet_cidr),
+ ),
+ )
+ try:
+ result = self.network_client.create_subnet(body=body)
+ break
+ except exc.QuantumClientException as e:
+ is_overlapping_cidr = 'overlaps with another subnet' in str(e)
+ if not is_overlapping_cidr:
+ raise
+ self.assertIsNotNone(result, 'Unable to allocate tenant network')
+ subnet = DeletableSubnet(client=self.network_client,
+ **result['subnet'])
+ self.assertEqual(subnet.cidr, str(subnet_cidr))
+ self.set_resource(rand_name('subnet-smoke-'), subnet)
+ return subnet
+
+ def _create_server(self, client, network, name, key_name, security_groups):
+ flavor_id = self.config.compute.flavor_ref
+ base_image_id = self.config.compute.image_ref
+ create_kwargs = {
+ 'nics': [
+ {'net-id': network.id},
+ ],
+ 'key_name': key_name,
+ 'security_groups': security_groups,
+ }
+ server = client.servers.create(name, base_image_id, flavor_id,
+ **create_kwargs)
+ try:
+ self.assertEqual(server.name, name)
+ self.set_resource(name, server)
+ except AttributeError:
+ self.fail("Server not successfully created.")
+ self.status_timeout(client.servers, server.id, 'ACTIVE')
+ # The instance retrieved on creation is missing network
+ # details, necessitating retrieval after it becomes active to
+ # ensure correct details.
+ server = client.servers.get(server.id)
+ self.set_resource(name, server)
+ return server
+
+ def _create_floating_ip(self, server, external_network_id):
+ result = self.network_client.list_ports(device_id=server.id)
+ ports = result.get('ports', [])
+ self.assertEqual(len(ports), 1,
+ "Unable to determine which port to target.")
+ port_id = ports[0]['id']
+ body = dict(
+ floatingip=dict(
+ floating_network_id=external_network_id,
+ port_id=port_id,
+ tenant_id=server.tenant_id,
+ )
+ )
+ result = self.network_client.create_floatingip(body=body)
+ floating_ip = DeletableFloatingIp(client=self.network_client,
+ **result['floatingip'])
+ self.set_resource(rand_name('floatingip-'), floating_ip)
+ return floating_ip
+
+ def _ping_ip_address(self, ip_address):
+ cmd = ['ping', '-c1', '-w1', ip_address]
+
+ def ping():
+ proc = subprocess.Popen(cmd,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE)
+ proc.wait()
+ if proc.returncode == 0:
+ return True
+
+ # TODO(mnewby) Allow configuration of execution and sleep duration.
+ return test.call_until_true(ping, 20, 1)
+
+ def test_001_create_keypairs(self):
+ self.keypairs[self.tenant_id] = self._create_keypair(
+ self.compute_client)
+
+ def test_002_create_security_groups(self):
+ self.security_groups[self.tenant_id] = self._create_security_group(
+ self.compute_client)
+
+ def test_003_create_networks(self):
+ network = self._create_network(self.tenant_id)
+ router = self._get_router(self.tenant_id)
+ subnet = self._create_subnet(network)
+ subnet.add_to_router(router.id)
+ self.networks.append(network)
+
+ def test_004_create_servers(self):
+ if not (self.keypairs or self.security_groups or self.networks):
+ raise nose.SkipTest('Necessary resources have not been defined')
+ for i, network in enumerate(self.networks):
+ tenant_id = network.tenant_id
+ name = rand_name('server-smoke-%d-' % i)
+ keypair_name = self.keypairs[tenant_id].name
+ security_groups = [self.security_groups[tenant_id].name]
+ server = self._create_server(self.compute_client, network,
+ name, keypair_name, security_groups)
+ self.servers.append(server)
+
+ def test_005_check_tenant_network_connectivity(self):
+ if not self.config.network.tenant_networks_reachable:
+ msg = 'Tenant networks not configured to be reachable.'
+ raise nose.SkipTest(msg)
+ if not self.servers:
+ raise nose.SkipTest("No VM's have been created")
+ for server in self.servers:
+ for net_name, ip_addresses in server.networks.iteritems():
+ for ip_address in ip_addresses:
+ self.assertTrue(self._ping_ip_address(ip_address),
+ "Timed out waiting for %s's ip to become "
+ "reachable" % server.name)
+
+ def test_006_assign_floating_ips(self):
+ public_network_id = self.config.network.public_network_id
+ if not public_network_id:
+ raise nose.SkipTest('Public network not configured')
+ if not self.servers:
+ raise nose.SkipTest("No VM's have been created")
+ for server in self.servers:
+ floating_ip = self._create_floating_ip(server, public_network_id)
+ self.floating_ips.setdefault(server, [])
+ self.floating_ips[server].append(floating_ip)
+
+ def test_007_check_public_network_connectivity(self):
+ if not self.floating_ips:
+ raise nose.SkipTest('No floating ips have been allocated.')
+ for server, floating_ips in self.floating_ips.iteritems():
+ for floating_ip in floating_ips:
+ ip_address = floating_ip.floating_ip_address
+ self.assertTrue(self._ping_ip_address(ip_address),
+ "Timed out waiting for %s's ip to become "
+ "reachable" % server.name)
diff --git a/tempest/tests/object_storage/test_object_expiry.py b/tempest/tests/object_storage/test_object_expiry.py
new file mode 100644
index 0000000..8437d55
--- /dev/null
+++ b/tempest/tests/object_storage/test_object_expiry.py
@@ -0,0 +1,93 @@
+# 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.
+
+from nose.plugins.attrib import attr
+from tempest.common.utils.data_utils import arbitrary_string
+from tempest.common.utils.data_utils import rand_name
+from tempest import exceptions
+from tempest.tests.object_storage import base
+from time import sleep
+import unittest2 as unittest
+
+
+class ObjectExpiryTest(base.BaseObjectTest):
+
+ @classmethod
+ def setUpClass(cls):
+ super(ObjectExpiryTest, cls).setUpClass()
+
+ #Create a container
+ cls.container_name = rand_name(name='TestContainer')
+ cls.container_client.create_container(cls.container_name)
+
+ @classmethod
+ def tearDownClass(cls):
+ """The test script fails in tear down class
+ as the container contains expired objects (LP bug 1069849).
+ But delete action for the expired object is raising
+ NotFound exception and also non empty container cannot be deleted."""
+
+ #Get list of all object in the container
+ objlist = \
+ cls.container_client.list_all_container_objects(cls.container_name)
+
+ #Attempt to delete every object in the container
+ if objlist:
+ for obj in objlist:
+ resp, _ = cls.object_client.delete_object(cls.container_name,
+ obj['name'])
+
+ #Attempt to delete the container
+ resp, _ = cls.container_client.delete_container(cls.container_name)
+
+ @unittest.skip('Until bug 1069849 is resolved.')
+ @attr(type='regression')
+ def test_get_object_after_expiry_time(self):
+ """GET object after expiry time"""
+ #TODO(harika-vakadi): Similar test case has to be created for
+ # "X-Delete-At", after this test case works.
+
+ #Create Object
+ object_name = rand_name(name='TestObject')
+ data = arbitrary_string()
+ resp, _ = self.object_client.create_object(self.container_name,
+ object_name, data)
+
+ #Update object metadata with expiry time of 3 seconds
+ metadata = {'X-Delete-After': '3'}
+ resp, _ = \
+ self.object_client.update_object_metadata(self.container_name,
+ object_name, metadata,
+ metadata_prefix='')
+
+ resp, _ = \
+ self.object_client.list_object_metadata(self.container_name,
+ object_name)
+
+ self.assertEqual(resp['status'], '200')
+ self.assertIn('x-delete-at', resp)
+
+ resp, body = self.object_client.get_object(self.container_name,
+ object_name)
+ self.assertEqual(resp['status'], '200')
+ # Check data
+ self.assertEqual(body, data)
+ # Sleep for over 5 seconds, so that object is expired
+ sleep(5)
+ # Verification of raised exception after object gets expired
+ self.assertRaises(exceptions.NotFound, self.object_client.get_object,
+ self.container_name, object_name)
diff --git a/tools/pip-requires b/tools/pip-requires
index 3a2283f..7877906 100644
--- a/tools/pip-requires
+++ b/tools/pip-requires
@@ -5,3 +5,4 @@
lxml
boto>=2.2.1
paramiko
+netaddr