Microversion v2.20 tests: nova volume operations when shelved
Compute microversion v2.20 allows the volume attachment operations
while server in shelved and shelved_offloaded state.
Ref- http://docs.openstack.org/developer/nova/api_microversion_history.html#id18
This commit adds tests for that behavior and verify the attaching and detaching
volumes from a shelved or shelved_offloaded server.
NOTE- In addition to tests, this also does some schema versioning.
Tempest strictly validate the response and does not allow additional elements
in response.
As v2.20 microversion request will respond as latest behavior of Nova till V2.20,
response schema needs to be version for older microversion if response is changed in between.
Adding schema versioning for v2.9 and 2.19 where server response has been changed.
Co-Authored-By: ghanshyam <ghanshyam.mann@nectechnologies.in>
Change-Id: I5ececf58f7ccda5521ace456f017795f14f11def
diff --git a/tempest/api/compute/volumes/test_attach_volume.py b/tempest/api/compute/volumes/test_attach_volume.py
index e9c8e30..37423a3 100644
--- a/tempest/api/compute/volumes/test_attach_volume.py
+++ b/tempest/api/compute/volumes/test_attach_volume.py
@@ -16,6 +16,7 @@
import testtools
from tempest.api.compute import base
+from tempest.common import compute
from tempest.common.utils.linux import remote_client
from tempest.common import waiters
from tempest import config
@@ -25,6 +26,7 @@
class AttachVolumeTestJSON(base.BaseV2ComputeTest):
+ max_microversion = '2.19'
def __init__(self, *args, **kwargs):
super(AttachVolumeTestJSON, self).__init__(*args, **kwargs)
@@ -62,7 +64,7 @@
self.volumes_client.wait_for_resource_deletion(self.volume['id'])
self.volume = None
- def _create_and_attach(self):
+ def _create_and_attach(self, shelve_server=False):
# Start a server and wait for it to become ready
self.admin_pass = self.image_ssh_password
self.server = self.create_test_server(
@@ -81,6 +83,9 @@
waiters.wait_for_volume_status(self.volumes_client,
self.volume['id'], 'available')
+ if shelve_server:
+ compute.shelve_server(self.servers_client, self.server['id'])
+
# Attach the volume to the server
self.attachment = self.servers_client.attach_volume(
self.server['id'],
@@ -152,3 +157,66 @@
self.assertEqual(self.server['id'], body['serverId'])
self.assertEqual(self.volume['id'], body['volumeId'])
self.assertEqual(self.attachment['id'], body['id'])
+
+
+class AttachVolumeShelveTestJSON(AttachVolumeTestJSON):
+ """Testing volume with shelved instance.
+
+ This test checks the attaching and detaching volumes from
+ a shelved or shelved ofload instance.
+ """
+
+ min_microversion = '2.20'
+ max_microversion = 'latest'
+
+ def _unshelve_server_and_check_volumes(self, number_of_partition):
+ # Unshelve the instance and check that there are expected volumes
+ self.servers_client.unshelve_server(self.server['id'])
+ waiters.wait_for_server_status(self.servers_client,
+ self.server['id'],
+ 'ACTIVE')
+ linux_client = remote_client.RemoteClient(
+ self.get_server_ip(self.server['id']),
+ self.image_ssh_user,
+ self.admin_pass,
+ self.validation_resources['keypair']['private_key'])
+
+ command = 'grep vd /proc/partitions | wc -l'
+ nb_partitions = linux_client.exec_command(command).strip()
+ self.assertEqual(number_of_partition, nb_partitions)
+
+ @test.idempotent_id('13a940b6-3474-4c3c-b03f-29b89112bfee')
+ @testtools.skipUnless(CONF.compute_feature_enabled.shelve,
+ 'Shelve is not available.')
+ @testtools.skipUnless(CONF.validation.run_validation,
+ 'SSH required for this test')
+ def test_attach_volume_shelved_or_offload_server(self):
+ self._create_and_attach(shelve_server=True)
+
+ # Unshelve the instance and check that there are two volumes
+ self._unshelve_server_and_check_volumes('2')
+
+ # Get Volume attachment of the server
+ volume_attachment = self.servers_client.show_volume_attachment(
+ self.server['id'],
+ self.attachment['id'])['volumeAttachment']
+ self.assertEqual(self.server['id'], volume_attachment['serverId'])
+ self.assertEqual(self.attachment['id'], volume_attachment['id'])
+ # Check the mountpoint is not None after unshelve server even in
+ # case of shelved_offloaded.
+ self.assertIsNotNone(volume_attachment['device'])
+
+ @test.idempotent_id('b54e86dd-a070-49c4-9c07-59ae6dae15aa')
+ @testtools.skipUnless(CONF.compute_feature_enabled.shelve,
+ 'Shelve is not available.')
+ @testtools.skipUnless(CONF.validation.run_validation,
+ 'SSH required for this test')
+ def test_detach_volume_shelved_or_offload_server(self):
+ self._create_and_attach(shelve_server=True)
+
+ # Detach the volume
+ self._detach(self.server['id'], self.volume['id'])
+ self.attachment = None
+
+ # Unshelve the instance and check that there is only one volume
+ self._unshelve_server_and_check_volumes('1')
diff --git a/tempest/lib/api_schema/response/compute/v2_19/__init__.py b/tempest/lib/api_schema/response/compute/v2_19/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tempest/lib/api_schema/response/compute/v2_19/__init__.py
diff --git a/tempest/lib/api_schema/response/compute/v2_19/servers.py b/tempest/lib/api_schema/response/compute/v2_19/servers.py
new file mode 100644
index 0000000..883839e
--- /dev/null
+++ b/tempest/lib/api_schema/response/compute/v2_19/servers.py
@@ -0,0 +1,49 @@
+# Copyright 2016 NEC Corporation. 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 copy
+
+from tempest.lib.api_schema.response.compute.v2_1 import servers as serversv21
+from tempest.lib.api_schema.response.compute.v2_9 import servers as serversv29
+
+get_server = copy.deepcopy(serversv29.get_server)
+get_server['response_body']['properties']['server'][
+ 'properties'].update({'description': {'type': ['string', 'null']}})
+get_server['response_body']['properties']['server'][
+ 'required'].append('description')
+
+list_servers_detail = copy.deepcopy(serversv29.list_servers_detail)
+list_servers_detail['response_body']['properties']['servers']['items'][
+ 'properties'].update({'description': {'type': ['string', 'null']}})
+list_servers_detail['response_body']['properties']['servers']['items'][
+ 'required'].append('description')
+
+update_server = copy.deepcopy(serversv21.update_server)
+update_server['response_body']['properties']['server'][
+ 'properties'].update({'description': {'type': ['string', 'null']}})
+update_server['response_body']['properties']['server'][
+ 'required'].append('description')
+
+rebuild_server = copy.deepcopy(serversv21.rebuild_server)
+rebuild_server['response_body']['properties']['server'][
+ 'properties'].update({'description': {'type': ['string', 'null']}})
+rebuild_server['response_body']['properties']['server'][
+ 'required'].append('description')
+
+rebuild_server_with_admin_pass = copy.deepcopy(
+ serversv21.rebuild_server_with_admin_pass)
+rebuild_server_with_admin_pass['response_body']['properties']['server'][
+ 'properties'].update({'description': {'type': ['string', 'null']}})
+rebuild_server_with_admin_pass['response_body']['properties']['server'][
+ 'required'].append('description')
diff --git a/tempest/lib/api_schema/response/compute/v2_9/__init__.py b/tempest/lib/api_schema/response/compute/v2_9/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tempest/lib/api_schema/response/compute/v2_9/__init__.py
diff --git a/tempest/lib/api_schema/response/compute/v2_9/servers.py b/tempest/lib/api_schema/response/compute/v2_9/servers.py
new file mode 100644
index 0000000..e9b7249
--- /dev/null
+++ b/tempest/lib/api_schema/response/compute/v2_9/servers.py
@@ -0,0 +1,29 @@
+# Copyright 2016 NEC Corporation. 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 copy
+
+from tempest.lib.api_schema.response.compute.v2_1 import servers
+
+get_server = copy.deepcopy(servers.get_server)
+get_server['response_body']['properties']['server'][
+ 'properties'].update({'locked': {'type': 'boolean'}})
+get_server['response_body']['properties']['server'][
+ 'required'].append('locked')
+
+list_servers_detail = copy.deepcopy(servers.list_servers_detail)
+list_servers_detail['response_body']['properties']['servers']['items'][
+ 'properties'].update({'locked': {'type': 'boolean'}})
+list_servers_detail['response_body']['properties']['servers']['items'][
+ 'required'].append('locked')
diff --git a/tempest/lib/services/compute/servers_client.py b/tempest/lib/services/compute/servers_client.py
index a37f167..0472eda 100644
--- a/tempest/lib/services/compute/servers_client.py
+++ b/tempest/lib/services/compute/servers_client.py
@@ -20,11 +20,17 @@
from six.moves.urllib import parse as urllib
from tempest.lib.api_schema.response.compute.v2_1 import servers as schema
+from tempest.lib.api_schema.response.compute.v2_19 import servers as schemav219
+from tempest.lib.api_schema.response.compute.v2_9 import servers as schemav29
from tempest.lib.common import rest_client
from tempest.lib.services.compute import base_compute_client
class ServersClient(base_compute_client.BaseComputeClient):
+ schema_versions_info = [
+ {'min': None, 'max': '2.8', 'schema': schema},
+ {'min': '2.9', 'max': '2.18', 'schema': schemav29},
+ {'min': '2.19', 'max': None, 'schema': schemav219}]
def __init__(self, auth_provider, service, region,
enable_instance_password=True, **kwargs):
@@ -88,6 +94,7 @@
post_body = json.dumps({'server': kwargs})
resp, body = self.put("servers/%s" % server_id, post_body)
body = json.loads(body)
+ schema = self.get_schema(self.schema_versions_info)
self.validate_response(schema.update_server, resp, body)
return rest_client.ResponseBody(resp, body)
@@ -95,6 +102,7 @@
"""Get server details."""
resp, body = self.get("servers/%s" % server_id)
body = json.loads(body)
+ schema = self.get_schema(self.schema_versions_info)
self.validate_response(schema.get_server, resp, body)
return rest_client.ResponseBody(resp, body)
@@ -114,6 +122,7 @@
"""
url = 'servers'
+ schema = self.get_schema(self.schema_versions_info)
_schema = schema.list_servers
if detail:
@@ -209,6 +218,7 @@
kwargs['imageRef'] = image_ref
if 'disk_config' in kwargs:
kwargs['OS-DCF:diskConfig'] = kwargs.pop('disk_config')
+ schema = self.get_schema(self.schema_versions_info)
if self.enable_instance_password:
rebuild_schema = schema.rebuild_server_with_admin_pass
else: