Merge "Disable hacking rule H305"
diff --git a/etc/schemas/compute/flavors/flavor_details.json b/etc/schemas/compute/flavors/flavor_details.json
deleted file mode 100644
index c16075c..0000000
--- a/etc/schemas/compute/flavors/flavor_details.json
+++ /dev/null
@@ -1,8 +0,0 @@
-{
- "name": "get-flavor-details",
- "http-method": "GET",
- "url": "flavors/%s",
- "resources": [
- {"name": "flavor", "expected_result": 404}
- ]
-}
diff --git a/etc/schemas/compute/flavors/flavor_details_v3.json b/etc/schemas/compute/flavors/flavor_details_v3.json
deleted file mode 100644
index d1c1077..0000000
--- a/etc/schemas/compute/flavors/flavor_details_v3.json
+++ /dev/null
@@ -1,6 +0,0 @@
-{
- "name": "get-flavor-details",
- "http-method": "GET",
- "url": "flavors/%s",
- "resources": ["flavor"]
-}
diff --git a/etc/schemas/compute/flavors/flavors_list.json b/etc/schemas/compute/flavors/flavors_list.json
deleted file mode 100644
index eb8383b..0000000
--- a/etc/schemas/compute/flavors/flavors_list.json
+++ /dev/null
@@ -1,24 +0,0 @@
-{
- "name": "list-flavors-with-detail",
- "http-method": "GET",
- "url": "flavors/detail",
- "json-schema": {
- "type": "object",
- "properties": {
- "minRam": {
- "type": "integer",
- "results": {
- "gen_none": 400,
- "gen_string": 400
- }
- },
- "minDisk": {
- "type": "integer",
- "results": {
- "gen_none": 400,
- "gen_string": 400
- }
- }
- }
- }
-}
diff --git a/etc/schemas/compute/flavors/flavors_list_v3.json b/etc/schemas/compute/flavors/flavors_list_v3.json
deleted file mode 100644
index d5388b3..0000000
--- a/etc/schemas/compute/flavors/flavors_list_v3.json
+++ /dev/null
@@ -1,24 +0,0 @@
-{
- "name": "list-flavors-with-detail",
- "http-method": "GET",
- "url": "flavors/detail",
- "json-schema": {
- "type": "object",
- "properties": {
- "min_ram": {
- "type": "integer",
- "results": {
- "gen_none": 400,
- "gen_string": 400
- }
- },
- "min_disk": {
- "type": "integer",
- "results": {
- "gen_none": 400,
- "gen_string": 400
- }
- }
- }
- }
-}
diff --git a/tempest/api/compute/admin/test_networks.py b/tempest/api/compute/admin/test_networks.py
new file mode 100644
index 0000000..032e2f5
--- /dev/null
+++ b/tempest/api/compute/admin/test_networks.py
@@ -0,0 +1,52 @@
+# Copyright 2014 Hewlett-Packard Development Company, L.P.
+# 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 tempest.api.compute import base
+from tempest import config
+
+CONF = config.CONF
+
+
+class NetworksTest(base.BaseComputeAdminTest):
+ _api_version = 2
+
+ """
+ Tests Nova Networks API that usually requires admin privileges.
+ API docs:
+ http://developer.openstack.org/api-ref-compute-v2-ext.html#ext-os-networks
+ """
+
+ @classmethod
+ def setUpClass(cls):
+ super(NetworksTest, cls).setUpClass()
+ cls.client = cls.os_adm.networks_client
+
+ def test_get_network(self):
+ resp, networks = self.client.list_networks()
+ configured_network = [x for x in networks if x['label'] ==
+ CONF.compute.fixed_network_name]
+ self.assertEqual(1, len(configured_network),
+ "{0} networks with label {1}".format(
+ len(configured_network),
+ CONF.compute.fixed_network_name))
+ configured_network = configured_network[0]
+ _, network = self.client.get_network(configured_network['id'])
+ self.assertEqual(configured_network['label'], network['label'])
+
+ def test_list_all_networks(self):
+ _, networks = self.client.list_networks()
+ # Check the configured network is in the list
+ configured_network = CONF.compute.fixed_network_name
+ self.assertIn(configured_network, [x['label'] for x in networks])
diff --git a/tempest/api/compute/flavors/test_flavors_negative.py b/tempest/api/compute/flavors/test_flavors_negative.py
index 1638f2d..7672fc6 100644
--- a/tempest/api/compute/flavors/test_flavors_negative.py
+++ b/tempest/api/compute/flavors/test_flavors_negative.py
@@ -13,8 +13,8 @@
# License for the specific language governing permissions and limitations
# under the License.
-
from tempest.api.compute import base
+from tempest.api_schema.request.compute.v2 import flavors
from tempest import test
@@ -25,14 +25,14 @@
class FlavorsListWithDetailsNegativeTestJSON(base.BaseV2ComputeTest,
test.NegativeAutoTest):
_service = 'compute'
- _schema_file = 'compute/flavors/flavors_list.json'
+ _schema = flavors.flavor_list
@test.SimpleNegativeAutoTest
class FlavorDetailsNegativeTestJSON(base.BaseV2ComputeTest,
test.NegativeAutoTest):
_service = 'compute'
- _schema_file = 'compute/flavors/flavor_details.json'
+ _schema = flavors.flavors_details
@classmethod
def setUpClass(cls):
diff --git a/tempest/api/compute/v3/flavors/test_flavors_negative.py b/tempest/api/compute/v3/flavors/test_flavors_negative.py
index 657e2cd..cdf018f 100644
--- a/tempest/api/compute/v3/flavors/test_flavors_negative.py
+++ b/tempest/api/compute/v3/flavors/test_flavors_negative.py
@@ -14,6 +14,7 @@
# under the License.
from tempest.api.compute import base
+from tempest.api_schema.request.compute.v3 import flavors
from tempest import test
@@ -24,14 +25,14 @@
class FlavorsListNegativeV3Test(base.BaseV3ComputeTest,
test.NegativeAutoTest):
_service = 'computev3'
- _schema_file = 'compute/flavors/flavors_list_v3.json'
+ _schema = flavors.flavor_list
@test.SimpleNegativeAutoTest
class FlavorDetailsNegativeV3Test(base.BaseV3ComputeTest,
test.NegativeAutoTest):
_service = 'computev3'
- _schema_file = 'compute/flavors/flavor_details_v3.json'
+ _schema = flavors.flavors_details
@classmethod
def setUpClass(cls):
diff --git a/tempest/api/volume/test_volumes_actions.py b/tempest/api/volume/test_volumes_actions.py
index 6fef564..0f92f01 100644
--- a/tempest/api/volume/test_volumes_actions.py
+++ b/tempest/api/volume/test_volumes_actions.py
@@ -40,6 +40,10 @@
# Create a test shared volume for attach/detach tests
cls.volume = cls.create_volume()
+ def _delete_image_with_wait(self, image_id):
+ self.image_client.delete_image(image_id)
+ self.image_client.wait_for_resource_deletion(image_id)
+
@classmethod
def tearDownClass(cls):
# Delete the test instance
@@ -101,23 +105,12 @@
image_name,
CONF.volume.disk_format)
image_id = body["image_id"]
- self.addCleanup(self.image_client.delete_image, image_id)
+ self.addCleanup(self._delete_image_with_wait, image_id)
self.assertEqual(202, resp.status)
self.image_client.wait_for_image_status(image_id, 'active')
self.client.wait_for_volume_status(self.volume['id'], 'available')
@test.attr(type='gate')
- def test_volume_extend(self):
- # Extend Volume Test.
- extend_size = int(self.volume['size']) + 1
- resp, body = self.client.extend_volume(self.volume['id'], extend_size)
- self.assertEqual(202, resp.status)
- self.client.wait_for_volume_status(self.volume['id'], 'available')
- resp, volume = self.client.get_volume(self.volume['id'])
- self.assertEqual(200, resp.status)
- self.assertEqual(int(volume['size']), extend_size)
-
- @test.attr(type='gate')
def test_reserve_unreserve_volume(self):
# Mark volume as reserved.
resp, body = self.client.reserve_volume(self.volume['id'])
diff --git a/tempest/api/volume/test_volumes_extend.py b/tempest/api/volume/test_volumes_extend.py
new file mode 100644
index 0000000..58bbe41
--- /dev/null
+++ b/tempest/api/volume/test_volumes_extend.py
@@ -0,0 +1,53 @@
+# Copyright 2012 OpenStack Foundation
+# 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 tempest.api.volume import base
+from tempest import config
+from tempest import test
+
+CONF = config.CONF
+
+
+class VolumesV2ExtendTest(base.BaseVolumeTest):
+
+ @classmethod
+ @test.safe_setup
+ def setUpClass(cls):
+ super(VolumesV2ExtendTest, cls).setUpClass()
+ cls.client = cls.volumes_client
+
+ @test.attr(type='gate')
+ def test_volume_extend(self):
+ # Extend Volume Test.
+ self.volume = self.create_volume()
+ extend_size = int(self.volume['size']) + 1
+ resp, body = self.client.extend_volume(self.volume['id'], extend_size)
+ self.assertEqual(202, resp.status)
+ self.client.wait_for_volume_status(self.volume['id'], 'available')
+ resp, volume = self.client.get_volume(self.volume['id'])
+ self.assertEqual(200, resp.status)
+ self.assertEqual(int(volume['size']), extend_size)
+
+
+class VolumesV2ExtendTestXML(VolumesV2ExtendTest):
+ _interface = "xml"
+
+
+class VolumesV1ExtendTest(VolumesV2ExtendTest):
+ _api_version = 1
+
+
+class VolumesV1ExtendTestXML(VolumesV1ExtendTest):
+ _interface = "xml"
diff --git a/tempest/api_schema/request/__init__.py b/tempest/api_schema/request/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tempest/api_schema/request/__init__.py
diff --git a/tempest/api_schema/request/compute/__init__.py b/tempest/api_schema/request/compute/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tempest/api_schema/request/compute/__init__.py
diff --git a/tempest/api_schema/request/compute/flavors.py b/tempest/api_schema/request/compute/flavors.py
new file mode 100644
index 0000000..36e5a19
--- /dev/null
+++ b/tempest/api_schema/request/compute/flavors.py
@@ -0,0 +1,32 @@
+# (c) 2014 Deutsche Telekom AG
+# 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.
+
+common_flavor_details = {
+ "name": "get-flavor-details",
+ "http-method": "GET",
+ "url": "flavors/%s",
+ "resources": [
+ {"name": "flavor", "expected_result": 404}
+ ]
+}
+
+common_flavor_list = {
+ "name": "list-flavors-with-detail",
+ "http-method": "GET",
+ "url": "flavors/detail",
+ "json-schema": {
+ "type": "object",
+ "properties": {
+ }
+ }
+}
diff --git a/tempest/api_schema/request/compute/v2/__init__.py b/tempest/api_schema/request/compute/v2/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tempest/api_schema/request/compute/v2/__init__.py
diff --git a/tempest/api_schema/request/compute/v2/flavors.py b/tempest/api_schema/request/compute/v2/flavors.py
new file mode 100644
index 0000000..08f6c28
--- /dev/null
+++ b/tempest/api_schema/request/compute/v2/flavors.py
@@ -0,0 +1,37 @@
+# (c) 2014 Deutsche Telekom AG
+# 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 copy
+
+from tempest.api_schema.request.compute import flavors
+
+flavors_details = copy.deepcopy(flavors.common_flavor_details)
+
+flavor_list = copy.deepcopy(flavors.common_flavor_list)
+
+flavor_list["json-schema"]["properties"] = {
+ "minRam": {
+ "type": "integer",
+ "results": {
+ "gen_none": 400,
+ "gen_string": 400
+ }
+ },
+ "minDisk": {
+ "type": "integer",
+ "results": {
+ "gen_none": 400,
+ "gen_string": 400
+ }
+ }
+}
diff --git a/tempest/api_schema/request/compute/v3/__init__.py b/tempest/api_schema/request/compute/v3/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tempest/api_schema/request/compute/v3/__init__.py
diff --git a/tempest/api_schema/request/compute/v3/flavors.py b/tempest/api_schema/request/compute/v3/flavors.py
new file mode 100644
index 0000000..b913aca
--- /dev/null
+++ b/tempest/api_schema/request/compute/v3/flavors.py
@@ -0,0 +1,37 @@
+# (c) 2014 Deutsche Telekom AG
+# 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 copy
+
+from tempest.api_schema.request.compute import flavors
+
+flavors_details = copy.deepcopy(flavors.common_flavor_details)
+
+flavor_list = copy.deepcopy(flavors.common_flavor_list)
+
+flavor_list["json-schema"]["properties"] = {
+ "min_ram": {
+ "type": "integer",
+ "results": {
+ "gen_none": 400,
+ "gen_string": 400
+ }
+ },
+ "min_disk": {
+ "type": "integer",
+ "results": {
+ "gen_none": 400,
+ "gen_string": 400
+ }
+ }
+}
diff --git a/tempest/clients.py b/tempest/clients.py
index 0edcdf4..2b8b6fb 100644
--- a/tempest/clients.py
+++ b/tempest/clients.py
@@ -50,6 +50,7 @@
from tempest.services.compute.json.limits_client import LimitsClientJSON
from tempest.services.compute.json.migrations_client import \
MigrationsClientJSON
+from tempest.services.compute.json.networks_client import NetworksClientJSON
from tempest.services.compute.json.quotas_client import QuotaClassesClientJSON
from tempest.services.compute.json.quotas_client import QuotasClientJSON
from tempest.services.compute.json.security_group_default_rules_client import \
@@ -426,6 +427,7 @@
self.migrations_client = MigrationsClientJSON(self.auth_provider)
self.security_group_default_rules_client = (
SecurityGroupDefaultRulesClientJSON(self.auth_provider))
+ self.networks_client = NetworksClientJSON(self.auth_provider)
class AltManager(Manager):
diff --git a/tempest/common/custom_matchers.py b/tempest/common/custom_matchers.py
index 996c365..7348a7d 100644
--- a/tempest/common/custom_matchers.py
+++ b/tempest/common/custom_matchers.py
@@ -13,6 +13,9 @@
# under the License.
import re
+from unittest import util
+
+from testtools import helpers
class ExistsAllResponseHeaders(object):
@@ -172,3 +175,54 @@
def get_details(self):
return {}
+
+
+class MatchesDictExceptForKeys(object):
+ """Matches two dictionaries. Verifies all items are equals except for those
+ identified by a list of keys.
+ """
+
+ def __init__(self, expected, excluded_keys=None):
+ self.expected = expected
+ self.excluded_keys = excluded_keys if excluded_keys is not None else []
+
+ def match(self, actual):
+ filtered_expected = helpers.dict_subtract(self.expected,
+ self.excluded_keys)
+ filtered_actual = helpers.dict_subtract(actual,
+ self.excluded_keys)
+ if filtered_actual != filtered_expected:
+ return DictMismatch(filtered_expected, filtered_actual)
+
+
+class DictMismatch(object):
+ """Mismatch between two dicts describes deltas"""
+
+ def __init__(self, expected, actual):
+ self.expected = expected
+ self.actual = actual
+ self.intersect = set(self.expected) & set(self.actual)
+ self.symmetric_diff = set(self.expected) ^ set(self.actual)
+
+ def describe(self):
+ msg = ""
+ if self.symmetric_diff:
+ only_expected = helpers.dict_subtract(self.expected, self.actual)
+ only_actual = helpers.dict_subtract(self.actual, self.expected)
+ if only_expected:
+ msg += "Only in expected:\n %s\n" % \
+ util.safe_repr(only_expected)
+ if only_actual:
+ msg += "Only in actual:\n %s\n" % \
+ util.safe_repr(only_actual)
+ diff_set = set(o for o in self.intersect if
+ self.expected[o] != self.actual[o])
+ if diff_set:
+ msg += "Differences:\n"
+ for o in diff_set:
+ msg += " %s: expected %s, actual %s\n" % (
+ o, self.expected[o], self.actual[o])
+ return msg
+
+ def get_details(self):
+ return {}
diff --git a/tempest/scenario/manager.py b/tempest/scenario/manager.py
index 203b653..76d82aa 100644
--- a/tempest/scenario/manager.py
+++ b/tempest/scenario/manager.py
@@ -78,6 +78,11 @@
return cls._get_credentials(cls.isolated_creds.get_primary_creds,
'user')
+ @classmethod
+ def admin_credentials(cls):
+ return cls._get_credentials(cls.isolated_creds.get_admin_creds,
+ 'identity_admin')
+
class OfficialClientTest(tempest.test.BaseTestCase):
"""
@@ -355,6 +360,22 @@
return secgroup
+ def rebuild_server(self, server, client=None, image=None,
+ preserve_ephemeral=False, wait=True,
+ rebuild_kwargs=None):
+ if client is None:
+ client = self.compute_client
+ if image is None:
+ image = CONF.compute.image_ref
+ rebuild_kwargs = rebuild_kwargs or {}
+
+ LOG.debug("Rebuilding server (name: %s, image: %s, preserve eph: %s)",
+ server.name, image, preserve_ephemeral)
+ server.rebuild(image, preserve_ephemeral=preserve_ephemeral,
+ **rebuild_kwargs)
+ if wait:
+ self.status_timeout(client.servers, server.id, 'ACTIVE')
+
def create_server(self, client=None, name=None, image=None, flavor=None,
wait_on_boot=True, wait_on_delete=True,
create_kwargs={}):
diff --git a/tempest/scenario/test_aggregates_basic_ops.py b/tempest/scenario/test_aggregates_basic_ops.py
index 0059619..3ad5c69 100644
--- a/tempest/scenario/test_aggregates_basic_ops.py
+++ b/tempest/scenario/test_aggregates_basic_ops.py
@@ -23,7 +23,7 @@
LOG = logging.getLogger(__name__)
-class TestAggregatesBasicOps(manager.OfficialClientTest):
+class TestAggregatesBasicOps(manager.ScenarioTest):
"""
Creates an aggregate within an availability zone
Adds a host to the aggregate
@@ -33,74 +33,67 @@
Deletes aggregate
"""
@classmethod
+ def setUpClass(cls):
+ super(TestAggregatesBasicOps, cls).setUpClass()
+ cls.aggregates_client = cls.manager.aggregates_client
+ cls.hosts_client = cls.manager.hosts_client
+
+ @classmethod
def credentials(cls):
return cls.admin_credentials()
def _create_aggregate(self, **kwargs):
- aggregate = self.compute_client.aggregates.create(**kwargs)
+ _, aggregate = self.aggregates_client.create_aggregate(**kwargs)
+ self.addCleanup(self._delete_aggregate, aggregate)
aggregate_name = kwargs['name']
availability_zone = kwargs['availability_zone']
- self.assertEqual(aggregate.name, aggregate_name)
- self.assertEqual(aggregate.availability_zone, availability_zone)
- self.addCleanup(self._delete_aggregate, aggregate)
- LOG.debug("Aggregate %s created." % (aggregate.name))
+ self.assertEqual(aggregate['name'], aggregate_name)
+ self.assertEqual(aggregate['availability_zone'], availability_zone)
return aggregate
def _delete_aggregate(self, aggregate):
- self.compute_client.aggregates.delete(aggregate.id)
- LOG.debug("Aggregate %s deleted. " % (aggregate.name))
+ self.aggregates_client.delete_aggregate(aggregate['id'])
def _get_host_name(self):
- hosts = self.compute_client.hosts.list()
+ _, hosts = self.hosts_client.list_hosts()
self.assertTrue(len(hosts) >= 1)
- computes = [x for x in hosts if x.service == 'compute']
- return computes[0].host_name
+ computes = [x for x in hosts if x['service'] == 'compute']
+ return computes[0]['host_name']
- def _add_host(self, aggregate_name, host):
- aggregate = self.compute_client.aggregates.add_host(aggregate_name,
- host)
- self.addCleanup(self._remove_host, aggregate, host)
- self.assertIn(host, aggregate.hosts)
- LOG.debug("Host %s added to Aggregate %s." % (host, aggregate.name))
+ def _add_host(self, aggregate_id, host):
+ _, aggregate = self.aggregates_client.add_host(aggregate_id, host)
+ self.addCleanup(self._remove_host, aggregate['id'], host)
+ self.assertIn(host, aggregate['hosts'])
- def _remove_host(self, aggregate_name, host):
- aggregate = self.compute_client.aggregates.remove_host(aggregate_name,
- host)
- self.assertNotIn(host, aggregate.hosts)
- LOG.debug("Host %s removed to Aggregate %s." % (host, aggregate.name))
+ def _remove_host(self, aggregate_id, host):
+ _, aggregate = self.aggregates_client.remove_host(aggregate_id, host)
+ self.assertNotIn(host, aggregate['hosts'])
def _check_aggregate_details(self, aggregate, aggregate_name, azone,
hosts, metadata):
- aggregate = self.compute_client.aggregates.get(aggregate.id)
- self.assertEqual(aggregate_name, aggregate.name)
- self.assertEqual(azone, aggregate.availability_zone)
- self.assertEqual(aggregate.hosts, hosts)
+ _, aggregate = self.aggregates_client.get_aggregate(aggregate['id'])
+ self.assertEqual(aggregate_name, aggregate['name'])
+ self.assertEqual(azone, aggregate['availability_zone'])
+ self.assertEqual(hosts, aggregate['hosts'])
for meta_key in metadata.keys():
- self.assertIn(meta_key, aggregate.metadata)
- self.assertEqual(metadata[meta_key], aggregate.metadata[meta_key])
- LOG.debug("Aggregate %s details match." % aggregate.name)
+ self.assertIn(meta_key, aggregate['metadata'])
+ self.assertEqual(metadata[meta_key],
+ aggregate['metadata'][meta_key])
def _set_aggregate_metadata(self, aggregate, meta):
- aggregate = self.compute_client.aggregates.set_metadata(aggregate.id,
- meta)
+ _, aggregate = self.aggregates_client.set_metadata(aggregate['id'],
+ meta)
for key, value in meta.items():
- self.assertEqual(meta[key], aggregate.metadata[key])
- LOG.debug("Aggregate %s metadata updated successfully." %
- aggregate.name)
+ self.assertEqual(meta[key], aggregate['metadata'][key])
def _update_aggregate(self, aggregate, aggregate_name,
availability_zone):
- values = {}
- if aggregate_name:
- values.update({'name': aggregate_name})
- if availability_zone:
- values.update({'availability_zone': availability_zone})
- if values.keys():
- aggregate = self.compute_client.aggregates.update(aggregate.id,
- values)
- for key, values in values.items():
- self.assertEqual(getattr(aggregate, key), values)
+ _, aggregate = self.aggregates_client.update_aggregate(
+ aggregate['id'], name=aggregate_name,
+ availability_zone=availability_zone)
+ self.assertEqual(aggregate['name'], aggregate_name)
+ self.assertEqual(aggregate['availability_zone'], availability_zone)
return aggregate
@test.services('compute')
@@ -115,16 +108,17 @@
self._set_aggregate_metadata(aggregate, metadata)
host = self._get_host_name()
- self._add_host(aggregate, host)
+ self._add_host(aggregate['id'], host)
self._check_aggregate_details(aggregate, aggregate_name, az, [host],
metadata)
aggregate_name = data_utils.rand_name('renamed-aggregate-scenario')
- aggregate = self._update_aggregate(aggregate, aggregate_name, None)
+ # Updating the name alone. The az must be specified again otherwise
+ # the tempest client would send None in the put body
+ aggregate = self._update_aggregate(aggregate, aggregate_name, az)
- additional_metadata = {'foo': 'bar'}
- self._set_aggregate_metadata(aggregate, additional_metadata)
+ new_metadata = {'foo': 'bar'}
+ self._set_aggregate_metadata(aggregate, new_metadata)
- metadata.update(additional_metadata)
- self._check_aggregate_details(aggregate, aggregate.name, az, [host],
- metadata)
+ self._check_aggregate_details(aggregate, aggregate['name'], az,
+ [host], new_metadata)
diff --git a/tempest/scenario/test_baremetal_basic_ops.py b/tempest/scenario/test_baremetal_basic_ops.py
index f197c15..9ad6bc4 100644
--- a/tempest/scenario/test_baremetal_basic_ops.py
+++ b/tempest/scenario/test_baremetal_basic_ops.py
@@ -23,7 +23,7 @@
LOG = logging.getLogger(__name__)
-class BaremetalBasicOpsPXESSH(manager.BaremetalScenarioTest):
+class BaremetalBasicOps(manager.BaremetalScenarioTest):
"""
This smoke test tests the pxe_ssh Ironic driver. It follows this basic
set of operations:
@@ -35,10 +35,76 @@
* Verifies SSH connectivity using created keypair via fixed IP
* Associates a floating ip
* Verifies SSH connectivity using created keypair via floating IP
+ * Verifies instance rebuild with ephemeral partition preservation
* Deletes instance
* Monitors the associated Ironic node for power and
expected state transitions
"""
+ def rebuild_instance(self, preserve_ephemeral=False):
+ self.rebuild_server(self.instance,
+ preserve_ephemeral=preserve_ephemeral,
+ wait=False)
+
+ node = self.get_node(instance_id=self.instance.id)
+ self.instance = self.compute_client.servers.get(self.instance.id)
+
+ self.addCleanup_with_wait(self.compute_client.servers,
+ self.instance.id,
+ cleanup_callable=self.delete_wrapper,
+ cleanup_args=[self.instance])
+
+ # We should remain on the same node
+ self.assertEqual(self.node.uuid, node.uuid)
+ self.node = node
+
+ self.status_timeout(self.compute_client.servers, self.instance.id,
+ 'REBUILD')
+ self.status_timeout(self.compute_client.servers, self.instance.id,
+ 'ACTIVE')
+
+ def create_remote_file(self, client, filename):
+ """Create a file on the remote client connection.
+
+ After creating the file, force a filesystem sync. Otherwise,
+ if we issue a rebuild too quickly, the file may not exist.
+ """
+ client.exec_command('sudo touch ' + filename)
+ client.exec_command('sync')
+
+ def verify_partition(self, client, label, mount, gib_size):
+ """Verify a labeled partition's mount point and size."""
+ LOG.info("Looking for partition %s mounted on %s" % (label, mount))
+
+ # Validate we have a device with the given partition label
+ cmd = "/sbin/blkid | grep '%s' | cut -d':' -f1" % label
+ device = client.exec_command(cmd).rstrip('\n')
+ LOG.debug("Partition device is %s" % device)
+ self.assertNotEqual('', device)
+
+ # Validate the mount point for the device
+ cmd = "mount | grep '%s' | cut -d' ' -f3" % device
+ actual_mount = client.exec_command(cmd).rstrip('\n')
+ LOG.debug("Partition mount point is %s" % actual_mount)
+ self.assertEqual(actual_mount, mount)
+
+ # Validate the partition size matches what we expect
+ numbers = '0123456789'
+ devnum = device.replace('/dev/', '')
+ cmd = "cat /sys/block/%s/%s/size" % (devnum.rstrip(numbers), devnum)
+ num_bytes = client.exec_command(cmd).rstrip('\n')
+ num_bytes = int(num_bytes) * 512
+ actual_gib_size = num_bytes / (1024 * 1024 * 1024)
+ LOG.debug("Partition size is %d GiB" % actual_gib_size)
+ self.assertEqual(actual_gib_size, gib_size)
+
+ def get_flavor_ephemeral_size(self):
+ """Returns size of the ephemeral partition in GiB."""
+ f_id = self.instance.flavor['id']
+ ephemeral = self.compute_client.flavors.get(f_id).ephemeral
+ if ephemeral != 'N/A':
+ return int(ephemeral)
+ return None
+
def add_floating_ip(self):
floating_ip = self.compute_client.floating_ips.create()
self.instance.add_floating_ip(floating_ip)
@@ -53,10 +119,32 @@
@test.services('baremetal', 'compute', 'image', 'network')
def test_baremetal_server_ops(self):
+ test_filename = '/mnt/rebuild_test.txt'
self.add_keypair()
self.boot_instance()
self.validate_ports()
self.verify_connectivity()
floating_ip = self.add_floating_ip()
self.verify_connectivity(ip=floating_ip)
+
+ vm_client = self.get_remote_client(self.instance)
+
+ # We expect the ephemeral partition to be mounted on /mnt and to have
+ # the same size as our flavor definition.
+ eph_size = self.get_flavor_ephemeral_size()
+ self.assertIsNotNone(eph_size)
+ self.verify_partition(vm_client, 'ephemeral0', '/mnt', eph_size)
+
+ # Create the test file
+ self.create_remote_file(vm_client, test_filename)
+
+ # Rebuild and preserve the ephemeral partition
+ self.rebuild_instance(True)
+ self.verify_connectivity()
+
+ # Check that we maintained our data
+ vm_client = self.get_remote_client(self.instance)
+ self.verify_partition(vm_client, 'ephemeral0', '/mnt', eph_size)
+ vm_client.exec_command('ls ' + test_filename)
+
self.terminate_instance()
diff --git a/tempest/scenario/test_security_groups_basic_ops.py b/tempest/scenario/test_security_groups_basic_ops.py
index 8058b3d..ecb802f 100644
--- a/tempest/scenario/test_security_groups_basic_ops.py
+++ b/tempest/scenario/test_security_groups_basic_ops.py
@@ -213,10 +213,16 @@
myport = (tenant.router.id, tenant.subnet.id)
router_ports = [(i['device_id'], i['fixed_ips'][0]['subnet_id']) for i
in self.network_client.list_ports()['ports']
- if i['device_owner'] == 'network:router_interface']
+ if self._is_router_port(i)]
self.assertIn(myport, router_ports)
+ def _is_router_port(self, port):
+ """Return True if port is a router interface."""
+ # NOTE(armando-migliaccio): match device owner for both centralized
+ # and distributed routers; 'device_owner' is "" by default.
+ return port['device_owner'].startswith('network:router_interface')
+
def _create_server(self, name, tenant, security_groups=None):
"""
creates a server and assigns to security group
diff --git a/tempest/services/compute/json/networks_client.py b/tempest/services/compute/json/networks_client.py
new file mode 100644
index 0000000..40eb1a6
--- /dev/null
+++ b/tempest/services/compute/json/networks_client.py
@@ -0,0 +1,40 @@
+# Copyright 2012 OpenStack Foundation
+# 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
+
+from tempest.common import rest_client
+from tempest import config
+
+CONF = config.CONF
+
+
+class NetworksClientJSON(rest_client.RestClient):
+
+ def __init__(self, auth_provider):
+ super(NetworksClientJSON, self).__init__(auth_provider)
+ self.service = CONF.compute.catalog_type
+
+ def list_networks(self):
+ resp, body = self.get("os-networks")
+ body = json.loads(body)
+ self.expected_success(200, resp.status)
+ return resp, body['networks']
+
+ def get_network(self, network_id):
+ resp, body = self.get("os-networks/%s" % str(network_id))
+ body = json.loads(body)
+ self.expected_success(200, resp.status)
+ return resp, body['network']
diff --git a/tempest/services/image/v1/json/image_client.py b/tempest/services/image/v1/json/image_client.py
index 4a7c163..bc5e04a 100644
--- a/tempest/services/image/v1/json/image_client.py
+++ b/tempest/services/image/v1/json/image_client.py
@@ -235,7 +235,7 @@
def is_resource_deleted(self, id):
try:
- self.get_image(id)
+ self.get_image_meta(id)
except exceptions.NotFound:
return True
return False
diff --git a/tempest/tests/common/test_custom_matchers.py b/tempest/tests/common/test_custom_matchers.py
new file mode 100644
index 0000000..57217e3
--- /dev/null
+++ b/tempest/tests/common/test_custom_matchers.py
@@ -0,0 +1,66 @@
+# Copyright 2014 Hewlett-Packard Development Company, L.P.
+# 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 tempest.common import custom_matchers
+from tempest.tests import base
+
+from testtools.tests.matchers import helpers
+
+
+class TestMatchesDictExceptForKeys(base.TestCase,
+ helpers.TestMatchersInterface):
+
+ matches_matcher = custom_matchers.MatchesDictExceptForKeys(
+ {'a': 1, 'b': 2, 'c': 3, 'd': 4}, ['c', 'd'])
+ matches_matches = [
+ {'a': 1, 'b': 2, 'c': 3, 'd': 4},
+ {'a': 1, 'b': 2, 'c': 5},
+ {'a': 1, 'b': 2},
+ ]
+ matches_mismatches = [
+ {},
+ {'foo': 1},
+ {'a': 1, 'b': 3},
+ {'a': 1, 'b': 2, 'foo': 1},
+ {'a': 1, 'b': None, 'foo': 1},
+ ]
+
+ str_examples = []
+ describe_examples = [
+ ("Only in expected:\n"
+ " {'a': 1, 'b': 2}\n",
+ {},
+ matches_matcher),
+ ("Only in expected:\n"
+ " {'a': 1, 'b': 2}\n"
+ "Only in actual:\n"
+ " {'foo': 1}\n",
+ {'foo': 1},
+ matches_matcher),
+ ("Differences:\n"
+ " b: expected 2, actual 3\n",
+ {'a': 1, 'b': 3},
+ matches_matcher),
+ ("Only in actual:\n"
+ " {'foo': 1}\n",
+ {'a': 1, 'b': 2, 'foo': 1},
+ matches_matcher),
+ ("Only in actual:\n"
+ " {'foo': 1}\n"
+ "Differences:\n"
+ " b: expected 2, actual None\n",
+ {'a': 1, 'b': None, 'foo': 1},
+ matches_matcher)
+ ]
\ No newline at end of file