Refactoring ITRI DISCO cinder volume driver

This commit is the refactored version of DISCO volume driver of ITRI.

We change the SOAP API to REST API. It adds also two different classes:

* Class for the different calls using REST API
* Class for attach/detach a DISCO volume

It also includes the unit tests.

DocImpact
Implements: blueprint refactor-disco-volume-driver

Change-Id: Id0e2d2ef4a873e86e514d5784a957ea4f593a027
This commit is contained in:
Guy Kim 2016-07-26 11:04:15 +08:00
parent 0ed3a80d6c
commit 5c841f2d7b
12 changed files with 442 additions and 180 deletions

View File

@ -20,12 +20,15 @@ import mock
from suds import client
from os_brick.initiator import connector
from oslo_config import cfg
from cinder import context
from cinder import test
from cinder.tests.unit import fake_volume
from cinder.volume import configuration as conf
import cinder.volume.drivers.disco.disco as driver
import cinder.volume.drivers.disco.disco_api as disco_api
import cinder.volume.drivers.disco.disco_attach_detach as attach_detach
class TestDISCODriver(test.TestCase):
@ -55,9 +58,14 @@ class TestDISCODriver(test.TestCase):
self.cfg.restore_check_timeout = 3600
self.cfg.clone_check_timeout = 3600
self.cfg.snapshot_reserve_days = -1
self.cfg.retry_interval = 1
self.cfg.retry_interval = 2
self.cfg.num_volume_device_scan_tries = 3
self.FAKE_SOAP_RESPONSE = {
CONF = cfg.CONF
CONF.choice_client = 'SOAP'
CONF.rest_ip = '127.0.0.1'
self.FAKE_RESPONSE = {
'standard': {
'success': {'status': 0, 'result': 'a normal message'},
'fail': {'status': 1, 'result': 'an error message'}}
@ -67,26 +75,28 @@ class TestDISCODriver(test.TestCase):
'Client',
self.create_client).start()
mock.patch.object(disco_api,
'DiscoApi',
self.create_client).start()
mock.patch.object(connector.InitiatorConnector,
'factory',
self.get_mock_connector).start()
mock.patch.object(driver.DiscoDriver,
'_get_connector_identifier',
self.get_mock_attribute).start()
self.driver = driver.DiscoDriver(execute=mock_exec,
configuration=self.cfg)
self.driver.do_setup(None)
self.attach_detach = attach_detach.AttachDetachDiscoVolume()
self.ctx = context.RequestContext('fake', 'fake', auth_token=True)
self.volume = fake_volume.fake_volume_obj(self.ctx)
self.volume['volume_id'] = '1234567'
self.requester = self.driver.client.service
self.requester = self.driver.client
def create_client(self, *cmd, **kwargs):
"""Mock the suds client."""
"""Mock the client's methods."""
return FakeClient()
def get_mock_connector(self, *cmd, **kwargs):
@ -97,9 +107,13 @@ class TestDISCODriver(test.TestCase):
"""Mock the os_brick connector."""
return 'DISCO'
def get_fake_volume(self, *cmd, **kwards):
"""Return a volume object for the tests."""
return self.volume
class FakeClient(object):
"""Fake class to mock suds.Client."""
"""Fake class to mock client."""
def __init__(self, *args, **kwargs):
"""Create a fake service attribute."""
@ -107,10 +121,10 @@ class FakeClient(object):
class FakeMethod(object):
"""Fake class recensing some of the method of the suds client."""
"""Fake class recensing some of the method of the rest client."""
def __init__(self, *args, **kwargs):
"""Fake class to mock the suds client."""
"""Fake class to mock the client."""
def volumeCreate(self, *args, **kwargs):
""""Mock function to create a volume."""
@ -133,6 +147,9 @@ class FakeMethod(object):
def restoreDetail(self, *args, **kwargs):
""""Mock function to detail the restore operation."""
def volumeDetail(self, *args, **kwargs):
"""Mock function to get the volume detail from its id."""
def volumeDetailByName(self, *args, **kwargs):
""""Mock function to get the volume detail from its name."""

View File

@ -34,7 +34,7 @@ class CreateCloneVolumeTestCase(disco.TestDISCODriver):
super(CreateCloneVolumeTestCase, self).setUp()
self.dest_volume = fake_volume.fake_volume_obj(self.ctx)
# Create mock functions for all the suds call done by the driver."""
# Create mock functions for all the call done by the driver."""
mock.patch.object(self.requester,
'volumeClone',
self.clone_request).start()
@ -54,13 +54,13 @@ class CreateCloneVolumeTestCase(disco.TestDISCODriver):
}
clone_success = (
copy.deepcopy(self.FAKE_SOAP_RESPONSE['standard']['success']))
copy.deepcopy(self.FAKE_RESPONSE['standard']['success']))
clone_pending = (
copy.deepcopy(self.FAKE_SOAP_RESPONSE['standard']['success']))
copy.deepcopy(self.FAKE_RESPONSE['standard']['success']))
clone_fail = (
copy.deepcopy(self.FAKE_SOAP_RESPONSE['standard']['success']))
copy.deepcopy(self.FAKE_RESPONSE['standard']['success']))
clone_response_fail = (
copy.deepcopy(self.FAKE_SOAP_RESPONSE['standard']['success']))
copy.deepcopy(self.FAKE_RESPONSE['standard']['success']))
clone_success['result'] = (
six.text_type(self.DETAIL_OPTIONS['success']))
@ -70,18 +70,18 @@ class CreateCloneVolumeTestCase(disco.TestDISCODriver):
six.text_type(self.DETAIL_OPTIONS['failure']))
clone_response_fail['status'] = 1
self.FAKE_SOAP_RESPONSE['clone_detail'] = {
self.FAKE_RESPONSE['clone_detail'] = {
'success': clone_success,
'fail': clone_fail,
'pending': clone_pending,
'request_fail': clone_response_fail
}
self.response = self.FAKE_SOAP_RESPONSE['standard']['success']
self.response = self.FAKE_RESPONSE['standard']['success']
self.response['result'] = '1234'
self.response_detail = (
self.FAKE_SOAP_RESPONSE['clone_detail']['success'])
self.FAKE_RESPONSE['clone_detail']['success'])
self.test_pending = False
self.test_pending_count = 0
@ -94,9 +94,9 @@ class CreateCloneVolumeTestCase(disco.TestDISCODriver):
if self.test_pending:
if self.test_pending_count == 0:
self.test_pending_count += 1
return self.FAKE_SOAP_RESPONSE['clone_detail']['pending']
return self.FAKE_RESPONSE['clone_detail']['pending']
else:
return self.FAKE_SOAP_RESPONSE['clone_detail']['success']
return self.FAKE_RESPONSE['clone_detail']['success']
else:
return self.response_detail
@ -113,29 +113,29 @@ class CreateCloneVolumeTestCase(disco.TestDISCODriver):
def test_create_clone_volume_fail(self):
"""Clone volume request to DISCO fails."""
self.response = self.FAKE_SOAP_RESPONSE['standard']['fail']
self.response = self.FAKE_RESPONSE['standard']['fail']
self.assertRaises(exception.VolumeBackendAPIException,
self.test_create_cloned_volume)
def test_create_cloned_volume_fail_not_immediate(self):
"""Get clone detail returns that the clone fails."""
self.response = self.FAKE_SOAP_RESPONSE['standard']['success']
self.response = self.FAKE_RESPONSE['standard']['success']
self.response_detail = (
self.FAKE_SOAP_RESPONSE['clone_detail']['fail'])
self.FAKE_RESPONSE['clone_detail']['fail'])
self.assertRaises(exception.VolumeBackendAPIException,
self.test_create_cloned_volume)
def test_create_cloned_volume_fail_not_immediate_response_fail(self):
"""Get clone detail request to DISCO fails."""
self.response = self.FAKE_SOAP_RESPONSE['standard']['success']
self.response = self.FAKE_RESPONSE['standard']['success']
self.response_detail = (
self.FAKE_SOAP_RESPONSE['clone_detail']['request_fail'])
self.FAKE_RESPONSE['clone_detail']['request_fail'])
self.assertRaises(exception.VolumeBackendAPIException,
self.test_create_cloned_volume)
def test_create_cloned_volume_fail_not_immediate_request_fail(self):
"""Get clone detail returns the task is pending then complete."""
self.response = self.FAKE_SOAP_RESPONSE['standard']['success']
self.response = self.FAKE_RESPONSE['standard']['success']
self.test_pending = True
self.test_create_cloned_volume()
@ -145,9 +145,9 @@ class CreateCloneVolumeTestCase(disco.TestDISCODriver):
timeout = 3
mock_time.side_effect = utils.generate_timeout_series(timeout)
self.driver.configuration.clone_check_timeout = timeout
self.response = self.FAKE_SOAP_RESPONSE['standard']['success']
self.response = self.FAKE_RESPONSE['standard']['success']
self.response_detail = (
self.FAKE_SOAP_RESPONSE['clone_detail']['pending'])
self.FAKE_RESPONSE['clone_detail']['pending'])
self.assertRaises(exception.VolumeBackendAPIException,
self.test_create_cloned_volume)

View File

@ -75,18 +75,18 @@ class CreateSnapshotTestCase(disco.TestDISCODriver):
self.DETAIL_OPTIONS['failure'])
snap_response_fail['status'] = 1
self.FAKE_SOAP_RESPONSE['snapshot_detail'] = {
self.FAKE_RESPONSE['snapshot_detail'] = {
'success': snap_success,
'fail': snap_fail,
'pending': snap_pending,
'request_fail': snap_response_fail}
self.response = (
self.FAKE_SOAP_RESPONSE['standard']['success'])
self.FAKE_RESPONSE['standard']['success'])
self.response['result'] = 1234
self.response_detail = (
self.FAKE_SOAP_RESPONSE['snapshot_detail']['success'])
self.FAKE_RESPONSE['snapshot_detail']['success'])
self.test_pending = False
self.test_pending_count = 0
@ -100,9 +100,9 @@ class CreateSnapshotTestCase(disco.TestDISCODriver):
if self.test_pending:
if self.test_pending_count == 0:
self.test_pending_count += 1
return self.FAKE_SOAP_RESPONSE['snapshot_detail']['pending']
return self.FAKE_RESPONSE['snapshot_detail']['pending']
else:
return self.FAKE_SOAP_RESPONSE['snapshot_detail']['success']
return self.FAKE_RESPONSE['snapshot_detail']['success']
else:
return self.response_detail
@ -114,29 +114,29 @@ class CreateSnapshotTestCase(disco.TestDISCODriver):
def test_create_snapshot_fail(self):
"""Request to DISCO failed."""
self.response = self.FAKE_SOAP_RESPONSE['standard']['fail']
self.response = self.FAKE_RESPONSE['standard']['fail']
self.assertRaises(exception.VolumeBackendAPIException,
self.test_create_snapshot)
def test_create_snapshot_fail_not_immediate(self):
"""Request to DISCO failed when monitoring the snapshot details."""
self.response = self.FAKE_SOAP_RESPONSE['standard']['success']
self.response = self.FAKE_RESPONSE['standard']['success']
self.response_detail = (
self.FAKE_SOAP_RESPONSE['snapshot_detail']['fail'])
self.FAKE_RESPONSE['snapshot_detail']['fail'])
self.assertRaises(exception.VolumeBackendAPIException,
self.test_create_snapshot)
def test_create_snapshot_fail_not_immediate_response_fail(self):
"""Request to get the snapshot details returns a failure."""
self.response = self.FAKE_SOAP_RESPONSE['standard']['success']
self.response = self.FAKE_RESPONSE['standard']['success']
self.response_detail = (
self.FAKE_SOAP_RESPONSE['snapshot_detail']['request_fail'])
self.FAKE_RESPONSE['snapshot_detail']['request_fail'])
self.assertRaises(exception.VolumeBackendAPIException,
self.test_create_snapshot)
def test_create_snapshot_detail_pending(self):
"""Request to get the snapshot detail return pending then success."""
self.response = self.FAKE_SOAP_RESPONSE['standard']['success']
self.response = self.FAKE_RESPONSE['standard']['success']
self.test_pending = True
self.test_create_snapshot()
@ -146,8 +146,8 @@ class CreateSnapshotTestCase(disco.TestDISCODriver):
timeout = 3
mock_time.side_effect = utils.generate_timeout_series(timeout)
self.driver.configuration.snapshot_check_timeout = timeout
self.response = self.FAKE_SOAP_RESPONSE['standard']['success']
self.response = self.FAKE_RESPONSE['standard']['success']
self.response_detail = (
self.FAKE_SOAP_RESPONSE['snapshot_detail']['pending'])
self.FAKE_RESPONSE['snapshot_detail']['pending'])
self.assertRaises(exception.VolumeBackendAPIException,
self.test_create_snapshot)

View File

@ -27,12 +27,12 @@ class CreateVolumeTestCase(disco.TestDISCODriver):
"""Prepare variables and mock functions."""
super(CreateVolumeTestCase, self).setUp()
# Mock the suds cliebt.
# Mock the method volumeCreate.
mock.patch.object(self.requester,
'volumeCreate',
self.perform_disco_request).start()
self.response = self.FAKE_SOAP_RESPONSE['standard']['success']
self.response = self.FAKE_RESPONSE['standard']['success']
def perform_disco_request(self, *cmd, **kwargs):
"""Mock function for the suds client."""
@ -48,6 +48,6 @@ class CreateVolumeTestCase(disco.TestDISCODriver):
def test_create_volume_fail(self):
"""Request to DISCO failed."""
self.response = self.FAKE_SOAP_RESPONSE['standard']['fail']
self.response = self.FAKE_RESPONSE['standard']['fail']
self.assertRaises(exception.VolumeBackendAPIException,
self.test_create_volume)

View File

@ -77,18 +77,18 @@ class CreateVolumeFromSnapshotTestCase(disco.TestDISCODriver):
self.DETAIL_OPTIONS['failure'])
rest_response_fail['status'] = 1
self.FAKE_SOAP_RESPONSE['restore_detail'] = {
self.FAKE_RESPONSE['restore_detail'] = {
'success': rest_success,
'fail': rest_fail,
'pending': rest_pending,
'request_fail': rest_response_fail
}
self.response = self.FAKE_SOAP_RESPONSE['standard']['success']
self.response = self.FAKE_RESPONSE['standard']['success']
self.response['result'] = '1234'
self.response_detail = (
self.FAKE_SOAP_RESPONSE['restore_detail']['success'])
self.FAKE_RESPONSE['restore_detail']['success'])
self.test_pending = False
self.test_pending_count = 0
@ -102,9 +102,9 @@ class CreateVolumeFromSnapshotTestCase(disco.TestDISCODriver):
if self.test_pending:
if self.test_pending_count == 0:
self.test_pending_count += 1
return self.FAKE_SOAP_RESPONSE['restore_detail']['pending']
return self.FAKE_RESPONSE['restore_detail']['pending']
else:
return self.FAKE_SOAP_RESPONSE['restore_detail']['success']
return self.FAKE_RESPONSE['restore_detail']['success']
else:
return self.response_detail
@ -121,29 +121,29 @@ class CreateVolumeFromSnapshotTestCase(disco.TestDISCODriver):
def test_create_volume_from_snapshot_fail(self):
"""Create volume from snapshot request fails."""
self.response = self.FAKE_SOAP_RESPONSE['standard']['fail']
self.response = self.FAKE_RESPONSE['standard']['fail']
self.assertRaises(exception.VolumeBackendAPIException,
self.test_create_volume_from_snapshot)
def test_create_volume_from_snapshot_fail_not_immediate(self):
"""Get restore details request fails."""
self.response = self.FAKE_SOAP_RESPONSE['standard']['success']
self.response = self.FAKE_RESPONSE['standard']['success']
self.response_detail = (
self.FAKE_SOAP_RESPONSE['restore_detail']['fail'])
self.FAKE_RESPONSE['restore_detail']['fail'])
self.assertRaises(exception.VolumeBackendAPIException,
self.test_create_volume_from_snapshot)
def test_create_volume_from_snapshot_fail_detail_response_fail(self):
"""Get restore details reports that restore operation fails."""
self.response = self.FAKE_SOAP_RESPONSE['standard']['success']
self.response = self.FAKE_RESPONSE['standard']['success']
self.response_detail = (
self.FAKE_SOAP_RESPONSE['restore_detail']['request_fail'])
self.FAKE_RESPONSE['restore_detail']['request_fail'])
self.assertRaises(exception.VolumeBackendAPIException,
self.test_create_volume_from_snapshot)
def test_create_volume_from_snapshot_fail_not_immediate_resp_fail(self):
"""Get restore details reports that the task is pending, then done."""
self.response = self.FAKE_SOAP_RESPONSE['standard']['success']
self.response = self.FAKE_RESPONSE['standard']['success']
self.test_pending = True
self.test_create_volume_from_snapshot()
@ -153,9 +153,9 @@ class CreateVolumeFromSnapshotTestCase(disco.TestDISCODriver):
timeout = 3
mock_time.side_effect = utils.generate_timeout_series(timeout)
self.driver.configuration.restore_check_timeout = timeout
self.response = self.FAKE_SOAP_RESPONSE['standard']['success']
self.response = self.FAKE_RESPONSE['standard']['success']
self.response_detail = (
self.FAKE_SOAP_RESPONSE['restore_detail']['pending'])
self.FAKE_RESPONSE['restore_detail']['pending'])
self.assertRaises(exception.VolumeBackendAPIException,
self.test_create_volume_from_snapshot)

View File

@ -27,12 +27,12 @@ class DeleteSnapshotTestCase(disco.TestDISCODriver):
"""Initialise variables and mock functions."""
super(DeleteSnapshotTestCase, self).setUp()
# Mock snapshotDelete function from suds client.
# Mock snapshotDelete function.
mock.patch.object(self.requester,
'snapshotDelete',
self.perform_disco_request).start()
self.response = self.FAKE_SOAP_RESPONSE['standard']['success']
self.response = self.FAKE_RESPONSE['standard']['success']
self.snapshot = fake_snapshot.fake_snapshot_obj(
self.ctx, **{'volume': self.volume})
@ -46,6 +46,6 @@ class DeleteSnapshotTestCase(disco.TestDISCODriver):
def test_delete_snapshot_fail(self):
"""Make the API returns an error while deleting."""
self.response = self.FAKE_SOAP_RESPONSE['standard']['fail']
self.response = self.FAKE_RESPONSE['standard']['fail']
self.assertRaises(exception.VolumeBackendAPIException,
self.test_delete_snapshot)

View File

@ -27,12 +27,12 @@ class DeleteVolumeTestCase(disco.TestDISCODriver):
"""Initialise variables and mock functions."""
super(DeleteVolumeTestCase, self).setUp()
# Mock volumeDelete function from suds client.
# Mock volumeDelete function.
mock.patch.object(self.requester,
'volumeDelete',
self.perform_disco_request).start()
self.response = self.FAKE_SOAP_RESPONSE['standard']['success']
self.response = self.FAKE_RESPONSE['standard']['success']
def perform_disco_request(self, *cmd, **kwargs):
"""Mock function to delete a volume."""
@ -44,6 +44,6 @@ class DeleteVolumeTestCase(disco.TestDISCODriver):
def test_delete_volume_fail(self):
"""Make the API returns an error while deleting."""
self.response = self.FAKE_SOAP_RESPONSE['standard']['fail']
self.response = self.FAKE_RESPONSE['standard']['fail']
self.assertRaises(exception.VolumeBackendAPIException,
self.test_delete_volume)

View File

@ -32,7 +32,7 @@ class VolumeExtendTestCase(disco.TestDISCODriver):
'volumeExtend',
self.perform_disco_request).start()
self.response = self.FAKE_SOAP_RESPONSE['standard']['success']
self.response = self.FAKE_RESPONSE['standard']['success']
self.new_size = 5
def perform_disco_request(self, *cmd, **kwargs):
@ -45,6 +45,6 @@ class VolumeExtendTestCase(disco.TestDISCODriver):
def test_extend_volume_fail(self):
"""Request to DISCO failed."""
self.response = self.FAKE_SOAP_RESPONSE['standard']['fail']
self.response = self.FAKE_RESPONSE['standard']['fail']
self.assertRaises(exception.VolumeBackendAPIException,
self.test_extend_volume)

View File

@ -1,4 +1,4 @@
# Copyright (c) 2015 Industrial Technology Research Institute.
# copyright (c) 2016 Industrial Technology Research Institute.
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
@ -18,7 +18,6 @@
import os
import time
from os_brick.initiator import connector
from oslo_config import cfg
from oslo_log import log as logging
from oslo_service import loopingcall
@ -32,8 +31,9 @@ from cinder import exception
from cinder.i18n import _
from cinder.image import image_utils
from cinder import interface
from cinder import utils
from cinder.volume import driver
from cinder.volume.drivers.disco import disco_api
from cinder.volume.drivers.disco import disco_attach_detach
LOG = logging.getLogger(__name__)
@ -47,8 +47,18 @@ disco_opts = [
help='The port to connect DMS client socket server'),
cfg.StrOpt('disco_wsdl_path',
default='/etc/cinder/DISCOService.wsdl',
deprecated_for_removal=True,
help='Path to the wsdl file '
'to communicate with DISCO request manager'),
cfg.IPOpt('rest_ip',
help='The IP address of the REST server'),
cfg.StrOpt('choice_client',
help='Use soap client or rest client for communicating '
'with DISCO. Possible values are "soap" or '
'"rest".'),
cfg.PortOpt('disco_src_api_port',
default='8080',
help='The port of DISCO source API'),
cfg.StrOpt('volume_name_prefix',
default='openstack-',
help='Prefix before volume name to differentiate '
@ -85,9 +95,16 @@ CONF.register_opts(disco_opts)
# Driver to communicate with DISCO storage solution
@interface.volumedriver
class DiscoDriver(driver.VolumeDriver):
"""Execute commands related to DISCO Volumes."""
"""Execute commands related to DISCO Volumes.
VERSION = "1.0"
Version history:
1.0 - disco volume driver using SOAP
1.1 - disco volume driver using REST and only compatible
with version greater than disco-1.6.4
"""
VERSION = "1.1"
CI_WIKI_NAME = "ITRI_DISCO_CI"
def __init__(self, *args, **kwargs):
@ -95,40 +112,30 @@ class DiscoDriver(driver.VolumeDriver):
super(DiscoDriver, self).__init__(*args, **kwargs)
self.configuration.append_config_values(disco_opts)
self.ctxt = context.get_admin_context()
self.connector = connector.InitiatorConnector.factory(
self._get_connector_identifier(), utils.get_root_helper(),
device_scan_attempts=(
self.configuration.num_volume_device_scan_tries)
)
self.connection_conf = {}
self.connection_conf['server_ip'] = self.configuration.disco_client
self.connection_conf['server_port'] = (
self.configuration.disco_client_port)
self.connection_properties = {}
self.connection_properties['name'] = None
self.connection_properties['disco_id'] = None
self.connection_properties['conf'] = self.connection_conf
self.attach_detach_volume = (
disco_attach_detach.AttachDetachDiscoVolume())
def do_setup(self, context):
"""Create client for DISCO request manager."""
LOG.debug("Enter in DiscoDriver do_setup.")
path = ''.join(['file:', self.configuration.disco_wsdl_path])
self.client = client.Client(path, cache=None)
if CONF.choice_client.lower() == "rest":
self.client = disco_api.DiscoApi(
CONF.rest_ip, CONF.disco_src_api_port)
else:
path = ''.join(['file:', self.configuration.disco_wsdl_path])
init_client = client.Client(path, cache=None)
self.client = init_client.service
def check_for_setup_error(self):
"""Make sure we have the pre-requisites."""
LOG.debug("Enter in DiscoDriver check_for_setup_error.")
path = self.configuration.disco_wsdl_path
if not os.path.exists(path):
msg = _("Could not find DISCO wsdl file.")
if not CONF.rest_ip and CONF.choice_client.lower() == "rest":
msg = _("Could not find the IP address of the REST server.")
raise exception.VolumeBackendAPIException(data=msg)
def _get_connector_identifier(self):
"""Return connector identifier, put here to mock it in unit tests."""
return connector.DISCO
else:
path = self.configuration.disco_wsdl_path
if not os.path.exists(path):
msg = _("Could not find DISCO wsdl file.")
raise exception.VolumeBackendAPIException(data=msg)
def create_volume(self, volume):
"""Create a disco volume."""
@ -137,13 +144,13 @@ class DiscoDriver(driver.VolumeDriver):
vol_size = volume['size'] * units.Ki
LOG.debug("Create volume : [name] %(vname)s - [size] %(vsize)s.",
{'vname': vol_name, 'vsize': six.text_type(vol_size)})
reply = self.client.service.volumeCreate(vol_name, vol_size)
reply = self.client.volumeCreate(vol_name, vol_size)
status = reply['status']
result = reply['result']
LOG.debug("Create volume : [status] %(stat)s - [result] %(res)s.",
{'stat': six.text_type(status), 'res': result})
if status != 0:
if status:
msg = (_("Error while creating volume "
"[status] %(stat)s - [result] %(res)s.") %
{'stat': six.text_type(status), 'res': result})
@ -156,14 +163,14 @@ class DiscoDriver(driver.VolumeDriver):
"""Delete a logical volume."""
disco_vol_id = volume['provider_location']
LOG.debug("Delete disco volume : %s.", disco_vol_id)
reply = self.client.service.volumeDelete(disco_vol_id)
reply = self.client.volumeDelete(disco_vol_id)
status = reply['status']
result = reply['result']
LOG.debug("Delete volume [status] %(stat)s - [result] %(res)s.",
{'stat': six.text_type(status), 'res': result})
if status != 0:
if status:
msg = (_("Error while deleting volume "
"[status] %(stat)s - [result] %(res)s.") %
{'stat': six.text_type(status), 'res': result})
@ -182,15 +189,15 @@ class DiscoDriver(driver.VolumeDriver):
{'id': vol_id, 'desc': description})
# Trigger an asynchronous local snapshot
reply = self.client.service.snapshotCreate(vol_id,
-1, -1,
description)
reply = self.client.snapshotCreate(vol_id,
-1, -1,
description)
status = reply['status']
result = reply['result']
LOG.debug("Create snapshot : [status] %(stat)s - [result] %(res)s.",
{'stat': six.text_type(status), 'res': result})
if status != 0:
if status:
msg = (_("Error while creating snapshot "
"[status] %(stat)s - [result] %(res)s.") %
{'stat': six.text_type(status), 'res': result})
@ -200,14 +207,12 @@ class DiscoDriver(driver.VolumeDriver):
# Monitor the status until it becomes either success or fail
params = {'snapshot_id': int(result)}
start_time = int(time.time())
timer = loopingcall.FixedIntervalLoopingCall(
self._retry_get_detail,
start_time,
self.configuration.snapshot_check_timeout,
'snapshot_detail',
params)
reply = timer.start(interval=self.configuration.retry_interval).wait()
snapshot_request = DISCOCheck(self.client,
params,
start_time,
"snapshot_detail")
timeout = self.configuration.snapshot_check_timeout
snapshot_request._monitor_request(timeout)
snapshot['provider_location'] = result
LOG.debug("snapshot taken successfully on volume : %(volume)s.",
@ -220,14 +225,14 @@ class DiscoDriver(driver.VolumeDriver):
snap_id = snapshot['provider_location']
LOG.debug("[start] Delete snapshot : %s.", snap_id)
reply = self.client.service.snapshotDelete(snap_id)
reply = self.client.snapshotDelete(snap_id)
status = reply['status']
result = reply['result']
LOG.debug("[End] Delete snapshot : "
"[status] %(stat)s - [result] %(res)s.",
{'stat': six.text_type(status), 'res': result})
if status != 0:
if status:
msg = (_("Error while deleting snapshot "
"[status] %(stat)s - [result] %(res)s") %
{'stat': six.text_type(status), 'res': result})
@ -243,14 +248,15 @@ class DiscoDriver(driver.VolumeDriver):
LOG.debug("[start] Create volume from snapshot : "
"%(snap_id)s - name : %(vol_name)s.",
{'snap_id': snap_id, 'vol_name': vol_name})
reply = self.client.service.restoreFromSnapshot(snap_id, vol_name)
reply = self.client.restoreFromSnapshot(snap_id, vol_name, -1, None,
-1)
status = reply['status']
result = reply['result']
LOG.debug("Restore volume from snapshot "
"[status] %(stat)s - [result] %(res)s.",
{'stat': six.text_type(status), 'res': result})
if status != 0:
if status:
msg = (_("Error[%(stat)s - %(res)s] while restoring snapshot "
"[%(snap_id)s] into volume [%(vol)s].") %
{'stat': six.text_type(status), 'res': result,
@ -262,20 +268,17 @@ class DiscoDriver(driver.VolumeDriver):
# either success, fail or timeout
params = {'restore_id': int(result)}
start_time = int(time.time())
timer = loopingcall.FixedIntervalLoopingCall(
self._retry_get_detail,
start_time,
self.configuration.restore_check_timeout,
'restore_detail',
params)
reply = timer.start(interval=self.configuration.retry_interval).wait()
reply = self.client.service.volumeDetailByName(vol_name)
restore_request = DISCOCheck(self.client,
params,
start_time,
"restore_detail")
timeout = self.configuration.restore_check_timeout
restore_request._monitor_request(timeout)
reply = self.client.volumeDetailByName(vol_name)
status = reply['status']
new_vol_id = reply['volumeInfoResult']['volumeId']
if status != 0:
if status:
msg = (_("Error[status] %(stat)s - [result] %(res)s] "
"while getting volume id.") %
{'stat': six.text_type(status), 'res': result})
@ -298,13 +301,13 @@ class DiscoDriver(driver.VolumeDriver):
{'name': vol_name,
'source': src_vol_id,
'size': six.text_type(vol_size)})
reply = self.client.service.volumeClone(src_vol_id, vol_name)
reply = self.client.volumeClone(src_vol_id, vol_name)
status = reply['status']
result = reply['result']
LOG.debug("Clone volume : [status] %(stat)s - [result] %(res)s.",
{'stat': six.text_type(status), 'res': result})
if status != 0:
if status:
msg = (_("Error while creating volume "
"[status] %(stat)s - [result] %(res)s.") %
{'stat': six.text_type(status), 'res': result})
@ -316,20 +319,16 @@ class DiscoDriver(driver.VolumeDriver):
params = {'clone_id': int(result),
'vol_name': vol_name}
start_time = int(time.time())
timer = loopingcall.FixedIntervalLoopingCall(
self._retry_get_detail,
start_time,
self.configuration.clone_check_timeout,
'clone_detail',
params)
reply = timer.start(interval=self.configuration.retry_interval).wait()
reply = self.client.service.volumeDetailByName(vol_name)
clone_request = DISCOCheck(self.client,
params,
start_time,
"clone_detail")
clone_request._monitor_request(self.configuration.clone_check_timeout)
reply = self.client.volumeDetailByName(vol_name)
status = reply['status']
new_vol_id = reply['volumeInfoResult']['volumeId']
if status != 0:
if status:
msg = (_("Error[%(stat)s - %(res)s] "
"while getting volume id."),
{'stat': six.text_type(status), 'res': result})
@ -346,7 +345,9 @@ class DiscoDriver(driver.VolumeDriver):
LOG.debug("Enter in copy image to volume for disco.")
try:
device_info = self._attach_volume(volume)
attach_detach_volume = (
disco_attach_detach.AttachDetachDiscoVolume())
device_info = attach_detach_volume._attach_volume(volume)
image_utils.fetch_to_raw(context,
image_service,
image_id,
@ -354,30 +355,21 @@ class DiscoDriver(driver.VolumeDriver):
self.configuration.volume_dd_blocksize,
size=volume['size'])
finally:
self._detach_volume(volume)
def _attach_volume(self, volume):
"""Call the connector.connect_volume()."""
connection_properties = self._get_connection_properties(volume)
device_info = self.connector.connect_volume(connection_properties)
return device_info
def _detach_volume(self, volume):
"""Call the connector.disconnect_volume()."""
connection_properties = self._get_connection_properties(volume)
self.connector.disconnect_volume(connection_properties, volume)
attach_detach_volume._detach_volume(volume)
def copy_volume_to_image(self, context, volume, image_service, image_meta):
"""Copy a volume to a new image."""
LOG.debug("Enter in copy image to volume for disco.")
try:
device_info = self._attach_volume(volume)
attach_detach_volume = (
disco_attach_detach.AttachDetachDiscoVolume())
device_info = attach_detach_volume._attach_volume(volume)
image_utils.upload_volume(context,
image_service,
image_meta,
device_info['path'])
finally:
self._detach_volume(volume)
attach_detach_volume._detach_volume(volume)
def extend_volume(self, volume, new_size):
"""Extend an existing volume's size."""
@ -385,11 +377,10 @@ class DiscoDriver(driver.VolumeDriver):
LOG.debug("Extends volume : %(id)s, new size : %(size)s.",
{'id': vol_id, 'size': new_size})
new_size_mb = new_size * units.Ki
reply = self.client.service.volumeExtend(vol_id, new_size_mb)
reply = self.client.volumeExtend(vol_id, new_size_mb)
status = reply['status']
result = reply['result']
if status != 0:
if status:
msg = (_("Error while extending volume "
"[status] %(stat)s - [result] %(res)s."),
{'stat': six.text_type(status), 'res': result})
@ -405,20 +396,14 @@ class DiscoDriver(driver.VolumeDriver):
"""Function called before attaching a volume."""
LOG.debug("Enter in initialize connection with disco, "
"connector is %s.", connector)
cp = self.attach_detach_volume._get_connection_properties(volume)
data = {
'driver_volume_type': 'disco',
'data': self._get_connection_properties(volume)
'data': cp
}
LOG.debug("Initialize connection [data]: %s.", data)
return data
def _get_connection_properties(self, volume):
"""Return a dictionnary with the connection properties."""
connection_properties = dict(self.connection_properties)
connection_properties['name'] = volume['name']
connection_properties['disco_id'] = volume['provider_location']
return connection_properties
def terminate_connection(self, volume, connector, **kwargs):
"""Function called after attaching a volume."""
LOG.debug("Enter in terminate connection with disco.")
@ -435,10 +420,10 @@ class DiscoDriver(driver.VolumeDriver):
stats['QoS_support'] = False
try:
reply = self.client.service.systemInformationList()
reply = self.client.systemInformationList()
status = reply['status']
if status != 0:
if status:
msg = (_("Error while getting "
"disco information [%s].") %
six.text_type(status))
@ -479,24 +464,30 @@ class DiscoDriver(driver.VolumeDriver):
"""Remove an export for a logical volume."""
pass
class DISCOCheck(object):
"""Used to monitor DISCO operations."""
def __init__(self, client, param, start_time, function):
"""Init some variables for checking some requests done in DISCO."""
self.start_time = start_time
self.function = function
self.client = client
self.param = param
def is_timeout(self, start_time, timeout):
"""Check whether we reach the timeout."""
current_time = int(time.time())
if current_time - start_time > timeout:
return True
else:
return False
return current_time - start_time > timeout
def _retry_get_detail(self, start_time, timeout, operation, params):
"""Keep trying to query an item detail unless we reach the timeout."""
reply = self._call_api(operation, params)
status = reply['status']
msg = (_("Error while getting %(op)s details, "
"returned code: %(status)s.") %
{'op': operation, 'status': six.text_type(status)})
if status != 0:
if status:
LOG.error(msg)
raise exception.VolumeBackendAPIException(data=msg)
@ -515,12 +506,12 @@ class DiscoDriver(driver.VolumeDriver):
def _call_api(self, operation, params):
"""Make the call to the SOAP api."""
if operation == 'snapshot_detail':
return self.client.service.snapshotDetail(params['snapshot_id'])
return self.client.snapshotDetail(params['snapshot_id'])
if operation == 'restore_detail':
return self.client.service.restoreDetail(params['restore_id'])
return self.client.restoreDetail(params['restore_id'])
if operation == 'clone_detail':
return self.client.service.cloneDetail(params['clone_id'],
params['vol_name'])
return self.client.cloneDetail(params['clone_id'],
params['vol_name'])
else:
msg = (_("Unknown operation %s."), operation)
LOG.error(msg)
@ -543,3 +534,13 @@ class DiscoDriver(driver.VolumeDriver):
"%s."), operation)
LOG.error(msg)
raise exception.VolumeBackendAPIException(data=msg)
def _monitor_request(self, timeout):
"""Monitor the request."""
timer = loopingcall.FixedIntervalLoopingCall(
self._retry_get_detail,
self.start_time,
timeout,
self.function,
self.param)
timer.start(interval=CONF.retry_interval).wait()

View File

@ -0,0 +1,169 @@
# copyright (c) 2016 Industrial Technology Research Institute.
# 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.
"""DISCO Backup Service Implementation."""
import json
from oslo_log import log as logging
import requests
import six
LOG = logging.getLogger(__name__)
class DiscoApi(object):
"""Class for all the requests to Disco API."""
def __init__(self, ip, port):
"""Init client."""
# Rest related variables
self.req_headers = {'Content-type': 'application/json'}
prefix_vars = {'server_ip': ip,
'server_port': port,
'api_prefix': 'RM-REST-Server/disco'}
self.request_prefix = ("http://%(server_ip)s:%(server_port)s"
"/%(api_prefix)s") % prefix_vars
self.prefix_var = {'req_prefix': self.request_prefix}
def volumeCreate(self, volume_name, size):
"""Create a DISCO volume."""
params = {'volumeName': volume_name, 'volumeSize': size,
'backupPolicyId': -1}
data = json.dumps(params,
sort_keys=True,
indent=4,
separators=(',', ': '))
request = ("%(req_prefix)s/volume" % self.prefix_var)
r = requests.post(request, data, headers=self.req_headers)
return r.json()
def volumeDelete(self, volume_id):
"""Delete the temporary volume."""
request_vars = {'req_prefix': self.request_prefix,
'volume_id': six.text_type(volume_id)}
request = ("%(req_prefix)s/volume/%(volume_id)s") % request_vars
r = requests.delete(request)
return r.json()
def volumeExtend(self, vol_id, size):
"""Extend DISCO volume."""
params = {'volumeSize': six.text_type(size),
'volumeId': six.text_type(vol_id)}
data = json.dumps(params,
sort_keys=True,
indent=4,
separators=(',', ': '))
request = ("%(req_prefix)s/volume/extend" % self.prefix_var)
r = requests.put(request, data, headers=self.req_headers)
return r.json()
def volumeDetail(self, volume_id):
"""Get volume information of the destination DISCO volume."""
request_vars = {'req_prefix': self.request_prefix,
'vol_id': six.text_type(volume_id)}
request = ("%(req_prefix)s/volume/%(vol_id)s") % request_vars
r = requests.get(request)
volume_info = r.json()
return volume_info
def volumeDetailByName(self, volume_name):
"""Get volume information of the DISCO volume."""
request_vars = {'req_prefix': self.request_prefix,
'volume_name': six.text_type(volume_name)}
request = ("%(req_prefix)s/volume?name=%(volume_name)s") % request_vars
r = requests.get(request)
return r.json()
def volumeClone(self, volume_id, volume_name):
"""Clone a DISCO volume."""
params = {'volumeName': volume_name, 'volumeId': volume_id}
data = json.dumps(params,
sort_keys=True,
indent=4,
separators=(',', ': '))
request = ("%(req_prefix)s/clone" % self.prefix_var)
r = requests.post(request, data, headers=self.req_headers)
return r.json()
def cloneDetail(self, clone_id, clone_name):
"""Get detail of the clone."""
request_vars = {'req_prefix': self.request_prefix,
'clone_name': clone_name,
'clone_id': six.text_type(clone_id)}
request = ("%(req_prefix)s/clone?cloneId=%(clone_id)s&"
"name=%(clone_name)s") % request_vars
r = requests.get(request)
return r.json()
def snapshotCreate(self, disco_volume_id, reserve_days, zone_id=None,
description=None):
"""Take a snapshot of the volume."""
params = {'volumeId': disco_volume_id,
'reserveDays': reserve_days,
'description': description}
data = json.dumps(params, sort_keys=True, indent=4,
separators=(',', ': '))
request = ("%(req_prefix)s/snapshot" % self.prefix_var)
r = requests.post(request, data, headers=self.req_headers)
return r.json()
def snapshotDelete(self, snapshot_id):
"""Delete a snapshot."""
request_vars = {'req_prefix': self.request_prefix,
'snapshot_id': six.text_type(snapshot_id)}
request = ("%(req_prefix)s/snapshot/%(snapshot_id)s") % request_vars
r = requests.delete(request)
return r.json()
def snapshotDetail(self, snapshot_id):
"""Monitor end of the snapshot."""
request_vars = {'req_prefix': self.request_prefix,
'snapshot_id': snapshot_id}
request = ("%(req_prefix)s/snapshot/%(snapshot_id)s") % request_vars
r = requests.get(request)
return r.json()
def restoreFromSnapshot(self, snapshot_id, volume_name, zone_id,
description, volume_id):
"""restore a snapshot of into a volume."""
params = {'snapshotId': snapshot_id,
'volumeName': volume_name,
'zone_id': zone_id,
'description': "local restore snapshot",
'volumeId': volume_id}
data = json.dumps(params,
sort_keys=True,
indent=4,
separators=(',', ': '))
request = ("%(req_prefix)s/restore" % self.prefix_var)
r = requests.post(request, data, headers=self.req_headers)
return r.json()
def restoreDetail(self, restore_id):
"""Monitor end of the restore."""
request_vars = {'req_prefix': self.request_prefix,
'restore_id': restore_id}
request = ("%(req_prefix)s/restore/%(restore_id)s") % request_vars
r = requests.get(request)
return r.json()
def systemInformationList(self):
"""Get the list of the system information."""
request = ("%(req_prefix)s/systemInformationList") % self.prefix_var
r = requests.get(request)
return r.json()

View File

@ -0,0 +1,70 @@
# copyright (c) 2016 Industrial Technology Research Institute.
# 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.
"""Class for DISCO to attach and detach volume."""
from os_brick.initiator import connector
from oslo_config import cfg
from oslo_log import log as logging
from cinder import utils
LOG = logging.getLogger(__name__)
CONF = cfg.CONF
class AttachDetachDiscoVolume(object):
"""Class for attach and detach a DISCO volume."""
def __init__(self):
"""Init volume attachment class."""
self.connector = connector.InitiatorConnector.factory(
self._get_connector_identifier(), utils.get_root_helper(),
device_scan_attempts=(
CONF.num_volume_device_scan_tries)
)
self.connection_conf = {}
self.connection_conf['server_ip'] = CONF.disco_client
self.connection_conf['server_port'] = (
CONF.disco_client_port)
self.connection_properties = {}
self.connection_properties['name'] = None
self.connection_properties['disco_id'] = None
self.connection_properties['conf'] = self.connection_conf
def _get_connection_properties(self, volume):
"""Return a dictionnary with the connection properties."""
connection_properties = dict(self.connection_properties)
connection_properties['name'] = volume['name']
connection_properties['disco_id'] = volume['provider_location']
return connection_properties
def _get_connector_identifier(self):
"""Return connector identifier, put here to mock it in unit tests."""
return connector.DISCO
def _attach_volume(self, volume):
"""Call the connector.connect_volume()."""
connection_properties = self._get_connection_properties(volume)
device_info = self.connector.connect_volume(connection_properties)
return device_info
def _detach_volume(self, volume):
"""Call the connector.disconnect_volume()."""
connection_properties = self._get_connection_properties(volume)
self.connector.disconnect_volume(connection_properties, volume)

View File

@ -0,0 +1,5 @@
---
Deprecations:
- Marked the ITRI DISCO driver option ``disco_wsdl_path`` as deprecated.
The new preferred protocol for array communication is REST and SOAP
support will be removed.