Add a volume from snapshot test case
Fixes bug #1034513 Change-Id: Ie37a0ae59c2dc2d805113c73a824951acef13663
This commit is contained in:
parent
5c5bda92cd
commit
36b1fcf417
|
@ -57,7 +57,9 @@ from tempest.services.network.json.network_client import NetworkClient
|
|||
from tempest.services.object_storage.account_client import AccountClient
|
||||
from tempest.services.object_storage.container_client import ContainerClient
|
||||
from tempest.services.object_storage.object_client import ObjectClient
|
||||
from tempest.services.volume.json.snapshots_client import SnapshotsClientJSON
|
||||
from tempest.services.volume.json.volumes_client import VolumesClientJSON
|
||||
from tempest.services.volume.xml.snapshots_client import SnapshotsClientXML
|
||||
from tempest.services.volume.xml.volumes_client import VolumesClientXML
|
||||
from tempest.services.object_storage.object_client import \
|
||||
ObjectClientCustomizedHeader
|
||||
|
@ -111,6 +113,11 @@ FLOAT_CLIENTS = {
|
|||
"xml": FloatingIPsClientXML,
|
||||
}
|
||||
|
||||
SNAPSHOTS_CLIENTS = {
|
||||
"json": SnapshotsClientJSON,
|
||||
"xml": SnapshotsClientXML,
|
||||
}
|
||||
|
||||
VOLUMES_CLIENTS = {
|
||||
"json": VolumesClientJSON,
|
||||
"xml": VolumesClientXML,
|
||||
|
@ -184,6 +191,7 @@ class Manager(object):
|
|||
vol_ext_cli = VOLUMES_EXTENSIONS_CLIENTS[interface](*client_args)
|
||||
self.volumes_extensions_client = vol_ext_cli
|
||||
self.floating_ips_client = FLOAT_CLIENTS[interface](*client_args)
|
||||
self.snapshots_client = SNAPSHOTS_CLIENTS[interface](*client_args)
|
||||
self.volumes_client = VOLUMES_CLIENTS[interface](*client_args)
|
||||
self.identity_client = IDENTITY_CLIENT[interface](*client_args)
|
||||
self.token_client = TOKEN_CLIENT[interface](self.config)
|
||||
|
|
|
@ -86,6 +86,10 @@ class VolumeBuildErrorException(TempestException):
|
|||
message = "Volume %(volume_id)s failed to build and is in ERROR status"
|
||||
|
||||
|
||||
class SnapshotBuildErrorException(TempestException):
|
||||
message = "Snapshot %(snapshot_id)s failed to build and is in ERROR status"
|
||||
|
||||
|
||||
class BadRequest(RestClientException):
|
||||
message = "Bad request"
|
||||
|
||||
|
|
|
@ -40,6 +40,7 @@ from tempest.services.compute.json import security_groups_client
|
|||
from tempest.services.compute.json import servers_client
|
||||
from tempest.services.compute.json import volumes_extensions_client
|
||||
from tempest.services.network.json import network_client
|
||||
from tempest.services.volume.json import snapshots_client
|
||||
from tempest.services.volume.json import volumes_client
|
||||
|
||||
NetworkClient = network_client.NetworkClient
|
||||
|
@ -53,6 +54,7 @@ SecurityGroupsClient = security_groups_client.SecurityGroupsClientJSON
|
|||
KeyPairsClient = keypairs_client.KeyPairsClientJSON
|
||||
VolumesExtensionsClient = volumes_extensions_client.VolumesExtensionsClientJSON
|
||||
VolumesClient = volumes_client.VolumesClientJSON
|
||||
SnapshotsClient = snapshots_client.SnapshotsClientJSON
|
||||
QuotasClient = quotas_client.QuotasClientJSON
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
@ -252,6 +254,7 @@ class ComputeFuzzClientManager(FuzzClientManager):
|
|||
self.floating_ips_client = FloatingIPsClient(*client_args)
|
||||
self.volumes_extensions_client = VolumesExtensionsClient(*client_args)
|
||||
self.volumes_client = VolumesClient(*client_args)
|
||||
self.snapshots_client = SnapshotsClient(*client_args)
|
||||
self.quotas_client = QuotasClient(*client_args)
|
||||
self.network_client = NetworkClient(*client_args)
|
||||
|
||||
|
|
|
@ -0,0 +1,125 @@
|
|||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# 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 logging
|
||||
import time
|
||||
import urllib
|
||||
|
||||
from tempest.common.rest_client import RestClient
|
||||
from tempest import exceptions
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class SnapshotsClientJSON(RestClient):
|
||||
"""Client class to send CRUD Volume API requests."""
|
||||
|
||||
def __init__(self, config, username, password, auth_url, tenant_name=None):
|
||||
super(SnapshotsClientJSON, self).__init__(config, username, password,
|
||||
auth_url, tenant_name)
|
||||
|
||||
self.service = self.config.volume.catalog_type
|
||||
self.build_interval = self.config.volume.build_interval
|
||||
self.build_timeout = self.config.volume.build_timeout
|
||||
|
||||
def list_snapshots(self, params=None):
|
||||
"""List all the snapshot."""
|
||||
url = 'snapshots'
|
||||
if params:
|
||||
url += '?%s' % urllib.urlencode(params)
|
||||
|
||||
resp, body = self.get(url)
|
||||
body = json.loads(body)
|
||||
return resp, body['snapshots']
|
||||
|
||||
def list_snapshot_with_detail(self, params=None):
|
||||
"""List the details of all snapshots."""
|
||||
url = 'snapshots/detail'
|
||||
if params:
|
||||
url += '?%s' % urllib.urlencode(params)
|
||||
|
||||
resp, body = self.get(url)
|
||||
body = json.loads(body)
|
||||
return resp, body['snapshots']
|
||||
|
||||
def get_snapshot(self, snapshot_id):
|
||||
"""Returns the details of a single snapshot."""
|
||||
url = "snapshots/%s" % str(snapshot_id)
|
||||
resp, body = self.get(url)
|
||||
body = json.loads(body)
|
||||
return resp, body['snapshot']
|
||||
|
||||
def create_snapshot(self, volume_id, **kwargs):
|
||||
"""
|
||||
Creates a new snapshot.
|
||||
volume_id(Required): id of the volume.
|
||||
force: Create a snapshot even if the volume attached (Default=False)
|
||||
display_name: Optional snapshot Name.
|
||||
display_description: User friendly snapshot description.
|
||||
"""
|
||||
post_body = {'volume_id': volume_id}
|
||||
post_body.update(kwargs)
|
||||
post_body = json.dumps({'snapshot': post_body})
|
||||
resp, body = self.post('snapshots', post_body, self.headers)
|
||||
body = json.loads(body)
|
||||
return resp, body['snapshot']
|
||||
|
||||
#NOTE(afazekas): just for the wait function
|
||||
def _get_snapshot_status(self, snapshot_id):
|
||||
resp, body = self.get_snapshot(snapshot_id)
|
||||
status = body['status']
|
||||
#NOTE(afazekas): snapshot can reach an "error"
|
||||
# state in a "normal" lifecycle
|
||||
if (status == 'error'):
|
||||
raise exceptions.SnapshotBuildErrorException(
|
||||
snapshot_id=snapshot_id)
|
||||
|
||||
return status
|
||||
|
||||
#NOTE(afazkas): Wait reinvented again. It is not in the correct layer
|
||||
def wait_for_snapshot_status(self, snapshot_id, status):
|
||||
"""Waits for a Snapshot to reach a given status."""
|
||||
start_time = time.time()
|
||||
old_value = value = self._get_snapshot_status(snapshot_id)
|
||||
while True:
|
||||
dtime = time.time() - start_time
|
||||
time.sleep(self.build_interval)
|
||||
if value != old_value:
|
||||
LOG.info('Value transition from "%s" to "%s"'
|
||||
'in %d second(s).', old_value,
|
||||
value, dtime)
|
||||
if (value == status):
|
||||
return value
|
||||
|
||||
if dtime > self.build_timeout:
|
||||
message = ('Time Limit Exceeded! (%ds)'
|
||||
'while waiting for %s, '
|
||||
'but we got %s.' %
|
||||
(self.build_timeout, status, value))
|
||||
raise exceptions.TimeoutException(message)
|
||||
time.sleep(self.build_interval)
|
||||
old_value = value
|
||||
value = self._get_snapshot_status(snapshot_id)
|
||||
|
||||
def delete_snapshot(self, snapshot_id):
|
||||
"""Delete Snapshot."""
|
||||
return self.delete("snapshots/%s" % str(snapshot_id))
|
||||
|
||||
def is_resource_deleted(self, id):
|
||||
try:
|
||||
self.get_snapshot(id)
|
||||
except exceptions.NotFound:
|
||||
return True
|
||||
return False
|
|
@ -71,14 +71,10 @@ class VolumesClientJSON(RestClient):
|
|||
display_name: Optional Volume Name.
|
||||
metadata: A dictionary of values to be used as metadata.
|
||||
volume_type: Optional Name of volume_type for the volume
|
||||
snapshot_id: When specified the volume is created from this snapshot
|
||||
"""
|
||||
post_body = {
|
||||
'size': size,
|
||||
'display_name': kwargs.get('display_name'),
|
||||
'metadata': kwargs.get('metadata'),
|
||||
'volume_type': kwargs.get('volume_type')
|
||||
}
|
||||
|
||||
post_body = {'size': size}
|
||||
post_body.update(kwargs)
|
||||
post_body = json.dumps({'volume': post_body})
|
||||
resp, body = self.post('volumes', post_body, self.headers)
|
||||
body = json.loads(body)
|
||||
|
|
|
@ -0,0 +1,138 @@
|
|||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# 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 time
|
||||
import urllib
|
||||
|
||||
from lxml import etree
|
||||
|
||||
from tempest.common.rest_client import RestClientXML
|
||||
from tempest import exceptions
|
||||
from tempest.services.compute.xml.common import Document
|
||||
from tempest.services.compute.xml.common import Element
|
||||
from tempest.services.compute.xml.common import xml_to_json
|
||||
from tempest.services.compute.xml.common import XMLNS_11
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class SnapshotsClientXML(RestClientXML):
|
||||
"""Client class to send CRUD Volume API requests."""
|
||||
|
||||
def __init__(self, config, username, password, auth_url, tenant_name=None):
|
||||
super(SnapshotsClientXML, self).__init__(config, username, password,
|
||||
auth_url, tenant_name)
|
||||
|
||||
self.service = self.config.volume.catalog_type
|
||||
self.build_interval = self.config.volume.build_interval
|
||||
self.build_timeout = self.config.volume.build_timeout
|
||||
|
||||
def list_snapshots(self, params=None):
|
||||
"""List all snapshot."""
|
||||
url = 'snapshots'
|
||||
|
||||
if params:
|
||||
url += '?%s' % urllib.urlencode(params)
|
||||
|
||||
resp, body = self.get(url, self.headers)
|
||||
body = etree.fromstring(body)
|
||||
return resp, xml_to_json(body)
|
||||
|
||||
def list_snapshots_with_detail(self, params=None):
|
||||
"""List all the details of snapshot."""
|
||||
url = 'snapshots/detail'
|
||||
|
||||
if params:
|
||||
url += '?%s' % urllib.urlencode(params)
|
||||
|
||||
resp, body = self.get(url, self.headers)
|
||||
body = etree.fromstring(body)
|
||||
snapshots = []
|
||||
return resp, snapshots(xml_to_json(body))
|
||||
|
||||
def get_snapshot(self, snapshot_id):
|
||||
"""Returns the details of a single snapshot."""
|
||||
url = "snapshots/%s" % str(snapshot_id)
|
||||
resp, body = self.get(url, self.headers)
|
||||
body = etree.fromstring(body)
|
||||
return resp, xml_to_json(body)
|
||||
|
||||
def create_snapshot(self, volume_id, **kwargs):
|
||||
""" Creates a new snapshot.
|
||||
volume_id(Required): id of the volume.
|
||||
force: Create a snapshot even if the volume attached (Default=False)
|
||||
display_name: Optional snapshot Name.
|
||||
display_description: User friendly snapshot description.
|
||||
"""
|
||||
#NOTE(afazekas): it should use the volume namaspace
|
||||
snapshot = Element("snapshot", xmlns=XMLNS_11, volume_id=volume_id)
|
||||
for key, value in kwargs.items():
|
||||
snapshot.add_attr(key, value)
|
||||
resp, body = self.post('snapshots', str(Document(snapshot)),
|
||||
self.headers)
|
||||
body = xml_to_json(etree.fromstring(body))
|
||||
return resp, body
|
||||
|
||||
def _get_snapshot_status(self, snapshot_id):
|
||||
resp, body = self.get_snapshot(snapshot_id)
|
||||
return body['status']
|
||||
|
||||
#NOTE(afazekas): just for the wait function
|
||||
def _get_snapshot_status(self, snapshot_id):
|
||||
resp, body = self.get_snapshot(snapshot_id)
|
||||
status = body['status']
|
||||
#NOTE(afazekas): snapshot can reach an "error"
|
||||
# state in a "normal" lifecycle
|
||||
if (status == 'error'):
|
||||
raise exceptions.SnapshotBuildErrorException(
|
||||
snapshot_id=snapshot_id)
|
||||
|
||||
return status
|
||||
|
||||
#NOTE(afazkas): Wait reinvented again. It is not in the correct layer
|
||||
def wait_for_snapshot_status(self, snapshot_id, status):
|
||||
"""Waits for a Snapshot to reach a given status."""
|
||||
start_time = time.time()
|
||||
old_value = value = self._get_snapshot_status(snapshot_id)
|
||||
while True:
|
||||
dtime = time.time() - start_time
|
||||
time.sleep(self.build_interval)
|
||||
if value != old_value:
|
||||
LOG.info('Value transition from "%s" to "%s"'
|
||||
'in %d second(s).', old_value,
|
||||
value, dtime)
|
||||
if (value == status):
|
||||
return value
|
||||
|
||||
if dtime > self.build_timeout:
|
||||
message = ('Time Limit Exceeded! (%ds)'
|
||||
'while waiting for %s, '
|
||||
'but we got %s.' %
|
||||
(self.build_timeout, status, value))
|
||||
raise exceptions.TimeoutException(message)
|
||||
time.sleep(self.build_interval)
|
||||
old_value = value
|
||||
value = self._get_snapshot_status(snapshot_id)
|
||||
|
||||
def delete_snapshot(self, snapshot_id):
|
||||
"""Delete Snapshot."""
|
||||
return self.delete("snapshots/%s" % str(snapshot_id))
|
||||
|
||||
def is_resource_deleted(self, id):
|
||||
try:
|
||||
self.get_snapshot(id)
|
||||
except exceptions.NotFound:
|
||||
return True
|
||||
return False
|
|
@ -101,6 +101,7 @@ class VolumesClientXML(RestClientXML):
|
|||
:param snapshot_id: When specified the volume is created from
|
||||
this snapshot
|
||||
"""
|
||||
#NOTE(afazekas): it should use a volume namespace
|
||||
volume = Element("volume", xmlns=XMLNS_11, size=size)
|
||||
|
||||
if 'metadata' in kwargs:
|
||||
|
|
|
@ -50,12 +50,14 @@ class BaseVolumeTest(testtools.TestCase):
|
|||
|
||||
cls.os = os
|
||||
cls.volumes_client = os.volumes_client
|
||||
cls.snapshots_client = os.snapshots_client
|
||||
cls.servers_client = os.servers_client
|
||||
cls.image_ref = cls.config.compute.image_ref
|
||||
cls.flavor_ref = cls.config.compute.flavor_ref
|
||||
cls.build_interval = cls.config.volume.build_interval
|
||||
cls.build_timeout = cls.config.volume.build_timeout
|
||||
cls.volumes = {}
|
||||
cls.snapshots = []
|
||||
cls.volumes = []
|
||||
|
||||
skip_msg = ("%s skipped as Cinder endpoint is not available" %
|
||||
cls.__name__)
|
||||
|
@ -120,19 +122,61 @@ class BaseVolumeTest(testtools.TestCase):
|
|||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
cls.clear_snapshots()
|
||||
cls.clear_volumes()
|
||||
cls.clear_isolated_creds()
|
||||
|
||||
def create_volume(self, size=1, metadata={}):
|
||||
@classmethod
|
||||
def create_snapshot(cls, volume_id=1, **kwargs):
|
||||
"""Wrapper utility that returns a test snapshot."""
|
||||
resp, snapshot = cls.snapshots_client.create_snapshot(volume_id,
|
||||
**kwargs)
|
||||
assert 200 == resp.status
|
||||
cls.snapshots_client.wait_for_snapshot_status(snapshot['id'],
|
||||
'available')
|
||||
cls.snapshots.append(snapshot)
|
||||
return snapshot
|
||||
|
||||
#NOTE(afazekas): these create_* and clean_* could be defined
|
||||
# only in a single location in the source, and could be more general.
|
||||
|
||||
@classmethod
|
||||
def create_volume(cls, size=1, **kwargs):
|
||||
"""Wrapper utility that returns a test volume."""
|
||||
display_name = rand_name(self.__class__.__name__ + "-volume")
|
||||
cli_resp = self.volumes_client.create_volume(size=size,
|
||||
display_name=display_name,
|
||||
metdata=metadata)
|
||||
resp, volume = cli_resp
|
||||
self.volumes_client.wait_for_volume_status(volume['id'], 'available')
|
||||
self.volumes.append(volume)
|
||||
resp, volume = cls.volumes_client.create_volume(size, **kwargs)
|
||||
assert 200 == resp.status
|
||||
cls.volumes_client.wait_for_volume_status(volume['id'], 'available')
|
||||
cls.volumes.append(volume)
|
||||
return volume
|
||||
|
||||
@classmethod
|
||||
def clear_volumes(cls):
|
||||
for volume in cls.volumes:
|
||||
try:
|
||||
cls.volume_client.delete_volume(volume['id'])
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
for volume in cls.volumes:
|
||||
try:
|
||||
cls.servers_client.wait_for_resource_deletion(volume['id'])
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def clear_snapshots(cls):
|
||||
for snapshot in cls.snapshots:
|
||||
try:
|
||||
cls.snapshots_client.delete_snapshot(snapshot['id'])
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
for snapshot in cls.snapshots:
|
||||
try:
|
||||
cls.snapshots_client.wait_for_resource_deletion(snapshot['id'])
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def wait_for(self, condition):
|
||||
"""Repeatedly calls condition() until a timeout."""
|
||||
start_time = int(time.time())
|
||||
|
|
|
@ -0,0 +1,55 @@
|
|||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# 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 rand_name
|
||||
from tempest.tests.volume import base
|
||||
|
||||
|
||||
class VolumesSnapshotTestBase(object):
|
||||
|
||||
def test_volume_from_snapshot(self):
|
||||
volume_origin = self.create_volume(size=1)
|
||||
snapshot = self.create_snapshot(volume_origin['id'])
|
||||
volume_snap = self.create_volume(size=1,
|
||||
snapshot_id=
|
||||
snapshot['id'])
|
||||
self.snapshots_client.delete_snapshot(snapshot['id'])
|
||||
self.client.delete_volume(volume_snap['id'])
|
||||
self.snapshots_client.wait_for_resource_deletion(snapshot['id'])
|
||||
self.snapshots.remove(snapshot)
|
||||
self.client.delete_volume(volume_origin['id'])
|
||||
self.client.wait_for_resource_deletion(volume_snap['id'])
|
||||
self.volumes.remove(volume_snap)
|
||||
self.client.wait_for_resource_deletion(volume_origin['id'])
|
||||
self.volumes.remove(volume_origin)
|
||||
|
||||
|
||||
class VolumesSnapshotTestXML(base.BaseVolumeTestXML,
|
||||
VolumesSnapshotTestBase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
cls._interface = "xml"
|
||||
super(VolumesSnapshotTestXML, cls).setUpClass()
|
||||
cls.client = cls.volumes_client
|
||||
|
||||
|
||||
class VolumesSnapshotTestJSON(base.BaseVolumeTestJSON,
|
||||
VolumesSnapshotTestBase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
cls._interface = "json"
|
||||
super(VolumesSnapshotTestJSON, cls).setUpClass()
|
||||
cls.client = cls.volumes_client
|
Loading…
Reference in New Issue