Driver for Fusion-io ioControl Hybrid array
Standard Cinder driver for iSCSI target array. Uses REST API of the ioControl array. Implements Quality of Service through the 5 Policies avaliable on the array. Add Apache License block Test results: https://bugs.launchpad.net/cinder/+bug/1317248 Change-Id: I27fdaedb9f75629a6af625f4e1c7d3f89a8cbb48 Implements: blueprint fusion-io-iocontrol-driver
This commit is contained in:
parent
78e935fc37
commit
950feedd5a
|
@ -0,0 +1,839 @@
|
|||
# Copyright (c) 2014 Fusion-io, Inc.
|
||||
# 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
|
||||
import json
|
||||
|
||||
import mock
|
||||
import requests
|
||||
|
||||
from cinder import context
|
||||
from cinder.db.sqlalchemy.models import VolumeMetadata
|
||||
from cinder import exception
|
||||
from cinder.openstack.common import log as logging
|
||||
from cinder.openstack.common import timeutils
|
||||
from cinder.openstack.common import units
|
||||
from cinder import test
|
||||
from cinder.volume import configuration as conf
|
||||
from cinder.volume.drivers.fusionio.ioControl import FIOconnection
|
||||
from cinder.volume.drivers.fusionio.ioControl import FIOioControlDriver
|
||||
from cinder.volume import qos_specs
|
||||
from cinder.volume import volume_types
|
||||
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
basic_net_response = [{"IsManagementPort": True,
|
||||
"NetworkAddress": "10.10.1.82",
|
||||
"IsReplicationPort": True, "OperationalState": "up",
|
||||
"ControllerUID": "FakeControl1_UID",
|
||||
"IfIndex": 2},
|
||||
{"IsManagementPort": True,
|
||||
"NetworkAddress": "10.10.1.83",
|
||||
"IsReplicationPort": True, "OperationalState": "up",
|
||||
"ControllerUID": "FakeControl1_UID",
|
||||
"IfIndex": 3},
|
||||
{"IsManagementPort": False,
|
||||
"NetworkAddress": "",
|
||||
"IsReplicationPort": False, "OperationalState": "down",
|
||||
"ControllerUID": "FakeControl1_UID",
|
||||
"IfIndex": 4},
|
||||
{"IsManagementPort": True,
|
||||
"NetworkAddress": "10.10.2.88",
|
||||
"IsReplicationPort": True, "OperationalState": "up",
|
||||
"ControllerUID": "FakeControl2_UID",
|
||||
"IfIndex": 2},
|
||||
{"IsManagementPort": False,
|
||||
"NetworkAddress": "10.10.2.84",
|
||||
"IsReplicationPort": False, "OperationalState": "up",
|
||||
"ControllerUID": "FakeControl2_UID",
|
||||
"IfIndex": 3},
|
||||
{"IsManagementPort": False,
|
||||
"NetworkAddress": "",
|
||||
"IsReplicationPort": False, "OperationalState": "down",
|
||||
"ControllerUID": "FakeControl2_UID",
|
||||
"IfIndex": 4}]
|
||||
|
||||
basic_pools_response = [{"TotalMB": 5079, "Name": "PoolOwnerA",
|
||||
"ExportedVolumeMB": 2049,
|
||||
"basetype": "StoragePool", "UsedVolumeMB": 3,
|
||||
"ObjectPath": "", "UsedMetaMB": 4, "UsedMB": 4,
|
||||
"SizeMB": 68677278, "UsedSnapMB": 0,
|
||||
"PagingUsedMB": 4,
|
||||
"CurrentOwnerUUID": "FakeControl1_UID",
|
||||
"TaskId": "", "PagingTotalMB": 5079, "Ready": True,
|
||||
"id": "FakePoolA_id",
|
||||
"Size": 72013345456128},
|
||||
{"TotalMB": 5079, "Name": "PoolOwnerB",
|
||||
"ExportedVolumeMB": 2049,
|
||||
"basetype": "StoragePool", "UsedVolumeMB": 193,
|
||||
"ObjectPath": "", "UsedMetaMB": 3, "UsedMB": 211,
|
||||
"SizeMB": 68677278, "UsedSnapMB": 0,
|
||||
"PagingUsedMB": 211,
|
||||
"CurrentOwnerUUID": "FakeControl2_UID",
|
||||
"TaskId": "", "PagingTotalMB": 5079, "Ready": True,
|
||||
"id": "FakePoolB_id",
|
||||
"Size": 72013345456128}
|
||||
]
|
||||
|
||||
basic_vol_response = [{"basetype": "Volume", "ObjectPath": "", "TaskId": "",
|
||||
"id": "FakeBasicVolID",
|
||||
"Name": "cinderVolumeID",
|
||||
"IQN": "iqn.2010-11.com.ngs:Volume:FakeBasicVolID",
|
||||
"Size": 1074266112, "SizeMB": 1024, "HighWaterMark": 0,
|
||||
"HighWaterMarkMB": 0, "MetadataSize": 262144,
|
||||
"MetadataSizeMB": 0, "DupedSize": 1074266112,
|
||||
"DupedSizeMB": 1024, "FaultTolerance": 0,
|
||||
"PathTolerance": 0,
|
||||
"AllowedTierMask": 18446744073709551615,
|
||||
"RequiredTierMask": 0, "NumberOfPagesPerChapter": 0,
|
||||
"CreateDateTime": 1390837136,
|
||||
"LayerId": "407115424bb9539c",
|
||||
"ParentLayerId": "0", "Protocol": "iscsi",
|
||||
"PoolUUID": "FakePoolB_id",
|
||||
"PolicyUUID": "00000000-00000000-0000-000000000000",
|
||||
"CurrentOwnerUUID": "FakeControl2_UID",
|
||||
"AclGroupList": ["1"], "ReplicaPeerList": [],
|
||||
"SnapshotRetention": 0}
|
||||
]
|
||||
|
||||
basic_policy_response = [{"id": "00000000-00000000-0000-000000000000",
|
||||
"Name": "Policy 5", },
|
||||
{"id": "00000000-00000000-0000-000000000002",
|
||||
"Name": "Policy 4", },
|
||||
{"id": "00000000-00000000-0000-000000000004",
|
||||
"Name": "Policy 3", },
|
||||
{"id": "00000000-00000000-0000-000000000008",
|
||||
"Name": "Policy 2", },
|
||||
{"id": "00000000-00000000-0000-000000000010",
|
||||
"Name": "Policy 1", },
|
||||
]
|
||||
|
||||
basic_snapshot_response = [{"basetype": "Snapshot", "ObjectPath": "",
|
||||
"TaskId": "", "id": "407115424bb9539c",
|
||||
"Name": "cinderSnapshotID",
|
||||
"VolumeUUID": "FakeBasicVolID",
|
||||
"PoolUUID": "FakePoolB_id",
|
||||
"ParentUUID": "0", "Size": 1074266112,
|
||||
"SizeMB": 1024, "SizeUsed": 0, "SizeUsedMB": 0,
|
||||
"SizeReclaimable": 0, "SizeReclaimableMB": 0,
|
||||
"CreateDateTime": 1390952554, "ChildCount": 1,
|
||||
"IsMounted": False, "IsHostConsistent": False,
|
||||
"ReplicaInfoList": []}
|
||||
]
|
||||
|
||||
basic_acl_group_response = [{"id": 1,
|
||||
"GroupName": "Deny Access",
|
||||
"InitiatorList": [], },
|
||||
{"id": 2,
|
||||
"GroupName": "Allow Access",
|
||||
"InitiatorList": ["iqn*"], },
|
||||
{"id": 3,
|
||||
"GroupName": "fake:01", "Description": "",
|
||||
"InitiatorList": ["fake:01"], },
|
||||
{"id": 4,
|
||||
"GroupName": "iqn.1994-05.com.redhat:fake1",
|
||||
"InitiatorList": ["iqn.1994-05.com.rhel:fake1"],
|
||||
},
|
||||
{"id": 5,
|
||||
"GroupName": "MyGroup", "Description": "",
|
||||
"InitiatorList": "iqn.1994-05.com.rhel:fake2", }
|
||||
]
|
||||
|
||||
|
||||
def create_configuration():
|
||||
configuration = conf.Configuration(None)
|
||||
configuration.san_ip = "10.123.10.123"
|
||||
configuration.san_login = "fioTestUser"
|
||||
configuration.san_password = "fioTestUserPassword"
|
||||
# we can set targetdelay to 0 for testing
|
||||
configuration.fusionio_iocontrol_targetdelay = 0
|
||||
configuration.fusionio_iocontrol_retry = 3
|
||||
configuration.fusionio_iocontrol_verify_cert = True
|
||||
return configuration
|
||||
|
||||
|
||||
class FIOFakeResponse(object):
|
||||
"""Fake response to requests."""
|
||||
|
||||
def __init__(self, code=None, text=None):
|
||||
self.status_code = code
|
||||
self.text = text
|
||||
|
||||
def json(self):
|
||||
return json.loads(self.text)
|
||||
|
||||
def raise_for_status(self):
|
||||
if self.status_code > 300:
|
||||
raise requests.exceptions.HTTPError
|
||||
|
||||
|
||||
class FIOioControlConnectionTests(test.TestCase):
|
||||
|
||||
VERSION = '1.0.0'
|
||||
fakeSessionID = '12345678'
|
||||
|
||||
def setUp(self):
|
||||
super(FIOioControlConnectionTests, self).setUp()
|
||||
self.configuration = create_configuration()
|
||||
self.ctxt = context.get_admin_context()
|
||||
return_text = json.dumps({"Version": FIOconnection.APIVERSION})
|
||||
get_return = FIOFakeResponse(code=200,
|
||||
text=return_text)
|
||||
requests.get = mock.Mock(return_value=get_return)
|
||||
self.conn = FIOconnection(self.configuration.san_ip,
|
||||
self.configuration.san_login,
|
||||
self.configuration.san_password,
|
||||
self.configuration.fusionio_iocontrol_retry,
|
||||
(self.configuration.
|
||||
fusionio_iocontrol_verify_cert),)
|
||||
|
||||
def test_conn_init_sucess(self):
|
||||
expected = [mock.call(url=("https://" +
|
||||
self.configuration.san_ip +
|
||||
"/AUTH/Version"),
|
||||
headers=self.conn.defhdrs,
|
||||
verify=True)]
|
||||
requests.get.assert_has_calls(expected)
|
||||
|
||||
def test_wrong_version(self):
|
||||
expected = json.dumps({"Version": (FIOconnection.APIVERSION + ".1")})
|
||||
get_return = FIOFakeResponse(code=200,
|
||||
text=expected)
|
||||
requests.get = mock.Mock(return_value=get_return)
|
||||
self.assertRaises(exception.VolumeDriverException,
|
||||
FIOconnection,
|
||||
self.configuration.san_ip,
|
||||
self.configuration.san_login,
|
||||
self.configuration.san_password,
|
||||
self.configuration.fusionio_iocontrol_retry,
|
||||
self.configuration.fusionio_iocontrol_verify_cert,)
|
||||
|
||||
def test_create_session_sucess(self):
|
||||
expected_text = json.dumps({"id": self.fakeSessionID})
|
||||
post_return = FIOFakeResponse(code=201,
|
||||
text=expected_text)
|
||||
put_return = FIOFakeResponse(code=201,
|
||||
text=json.dumps({"Status": 1}))
|
||||
requests.post = mock.Mock(return_value=post_return)
|
||||
requests.put = mock.Mock(return_value=put_return)
|
||||
result = self.conn._create_session()
|
||||
expectedhdr = copy.deepcopy(self.conn.defhdrs)
|
||||
expectedhdr["Cookie"] = 'session=' + self.fakeSessionID
|
||||
assert result == expectedhdr
|
||||
|
||||
def test_create_session_auth_fail(self):
|
||||
expected_text = json.dumps({"id": self.fakeSessionID})
|
||||
post_return = FIOFakeResponse(code=201,
|
||||
text=expected_text)
|
||||
put_return = FIOFakeResponse(code=201,
|
||||
text=json.dumps({"Status": (-1)}))
|
||||
requests.post = mock.Mock(return_value=post_return)
|
||||
requests.put = mock.Mock(return_value=put_return)
|
||||
requests.delete = mock.Mock()
|
||||
self.assertRaises(exception.VolumeDriverException,
|
||||
self.conn._create_session,)
|
||||
|
||||
def test_delete_session_sucess(self):
|
||||
requests.delete = mock.Mock(return_value=True)
|
||||
hdrs = copy.deepcopy(self.conn.defhdrs)
|
||||
hdrs["Cookie"] = 'session=' + self.fakeSessionID
|
||||
self.conn._delete_session(hdrs)
|
||||
expected = [mock.call(url=("https://" +
|
||||
self.configuration.san_ip +
|
||||
"/AUTH/SESSION/" + self.fakeSessionID),
|
||||
headers=self.conn.defhdrs,
|
||||
verify=True), ]
|
||||
requests.delete.assert_has_calls(expected)
|
||||
|
||||
def test_put_sucess(self):
|
||||
put_return = FIOFakeResponse(code=201,
|
||||
text=json.dumps({"Status": 1}))
|
||||
requests.put = mock.Mock(return_value=put_return)
|
||||
expectedhdr = copy.deepcopy(self.conn.defhdrs)
|
||||
expectedhdr["Cookie"] = 'session=' + self.fakeSessionID
|
||||
self.conn._create_session = mock.Mock(return_value=expectedhdr)
|
||||
self.conn._delete_session = mock.Mock()
|
||||
testurl = '/test/url/'
|
||||
testcontent = {'testdict': 'testvalue'}
|
||||
self.conn.put(testurl, testcontent)
|
||||
self.conn.post(testurl, testcontent)
|
||||
expected = [mock.call(), ]
|
||||
self.conn._create_session.assert_has_calls(expected)
|
||||
expected = [mock.call(expectedhdr), ]
|
||||
self.conn._delete_session.assert_has_calls(expected)
|
||||
expected = [mock.call(url=self.conn._complete_uri(testurl),
|
||||
data=json.dumps(testcontent, sort_keys=True),
|
||||
headers=expectedhdr, verify=True), ]
|
||||
requests.put.assert_has_calls(expected)
|
||||
|
||||
def test_post_sucess(self):
|
||||
expected_text = json.dumps({"id": self.fakeSessionID})
|
||||
post_return = FIOFakeResponse(code=201,
|
||||
text=expected_text)
|
||||
requests.post = mock.Mock(return_value=post_return)
|
||||
expectedhdr = copy.deepcopy(self.conn.defhdrs)
|
||||
expectedhdr["Cookie"] = 'session=' + self.fakeSessionID
|
||||
self.conn._create_session = mock.Mock(return_value=expectedhdr)
|
||||
self.conn._delete_session = mock.Mock()
|
||||
testurl = '/test/url/'
|
||||
testcontent = {'testdict': 'testvalue'}
|
||||
self.conn.post(testurl, testcontent)
|
||||
expected = [mock.call(), ]
|
||||
self.conn._create_session.assert_has_calls(expected)
|
||||
expected = [mock.call(expectedhdr), ]
|
||||
self.conn._delete_session.assert_has_calls(expected)
|
||||
expected = [mock.call(url=self.conn._complete_uri(testurl),
|
||||
data=json.dumps(testcontent, sort_keys=True),
|
||||
headers=expectedhdr, verify=True), ]
|
||||
requests.post.assert_has_calls(expected)
|
||||
|
||||
def test_delete_sucess(self):
|
||||
del_return = FIOFakeResponse(code=201, text=json.dumps({}))
|
||||
requests.delete = mock.Mock(return_value=del_return)
|
||||
expectedhdr = copy.deepcopy(self.conn.defhdrs)
|
||||
expectedhdr["Cookie"] = 'session=' + self.fakeSessionID
|
||||
self.conn._create_session = mock.Mock(return_value=expectedhdr)
|
||||
self.conn._delete_session = mock.Mock()
|
||||
testurl = '/test/url/'
|
||||
self.conn.delete(testurl,)
|
||||
expected = [mock.call(), ]
|
||||
self.conn._create_session.assert_has_calls(expected)
|
||||
expected = [mock.call(expectedhdr), ]
|
||||
self.conn._delete_session.assert_has_calls(expected)
|
||||
expected = [mock.call(url=self.conn._complete_uri(testurl),
|
||||
headers=expectedhdr, verify=True), ]
|
||||
requests.delete.assert_has_calls(expected)
|
||||
|
||||
def test_get_sucess(self):
|
||||
get_return = FIOFakeResponse(code=200,
|
||||
text=json.dumps(basic_acl_group_response))
|
||||
expectedhdr = copy.deepcopy(self.conn.defhdrs)
|
||||
expectedhdr["Cookie"] = 'session=' + self.fakeSessionID
|
||||
self.conn._create_session = mock.Mock(return_value=expectedhdr)
|
||||
self.conn._delete_session = mock.Mock()
|
||||
requests.get = mock.Mock(return_value=get_return)
|
||||
testurl = '/test/url/'
|
||||
result = self.conn.get(testurl,)
|
||||
expected = [mock.call(), ]
|
||||
self.conn._create_session.assert_has_calls(expected)
|
||||
expected = [mock.call(expectedhdr), ]
|
||||
self.conn._delete_session.assert_has_calls(expected)
|
||||
expected = [mock.call(url=self.conn._complete_uri(testurl),
|
||||
headers=expectedhdr, verify=True), ]
|
||||
requests.get.assert_has_calls(expected)
|
||||
assert result == basic_acl_group_response
|
||||
|
||||
def test_get_bad_json_once(self):
|
||||
expectedhdr = copy.deepcopy(self.conn.defhdrs)
|
||||
expectedhdr["Cookie"] = 'session=' + self.fakeSessionID
|
||||
self.conn._create_session = mock.Mock(return_value=expectedhdr)
|
||||
self.conn._delete_session = mock.Mock()
|
||||
expected_text = json.dumps(basic_acl_group_response)
|
||||
jsonErrEffect = [FIOFakeResponse(code=200,
|
||||
text='{"badjson":"bad",,}'),
|
||||
FIOFakeResponse(code=200,
|
||||
text=expected_text)]
|
||||
requests.get = mock.Mock(side_effect=jsonErrEffect)
|
||||
testurl = '/test/url/'
|
||||
result = self.conn.get(testurl,)
|
||||
expected = [mock.call(), ]
|
||||
self.conn._create_session.assert_has_calls(expected)
|
||||
expected = [mock.call(expectedhdr), ]
|
||||
self.conn._delete_session.assert_has_calls(expected)
|
||||
expected = [mock.call(url=self.conn._complete_uri(testurl),
|
||||
headers=expectedhdr, verify=True), ]
|
||||
requests.get.assert_has_calls(expected)
|
||||
assert result == basic_acl_group_response
|
||||
|
||||
def test_get_bad_json_retry_expire(self):
|
||||
get_return = FIOFakeResponse(code=200, text='{"badjson":"bad",,}')
|
||||
expectedhdr = copy.deepcopy(self.conn.defhdrs)
|
||||
expectedhdr["Cookie"] = 'session=' + self.fakeSessionID
|
||||
self.conn._create_session = mock.Mock(return_value=expectedhdr)
|
||||
self.conn._delete_session = mock.Mock()
|
||||
requests.get = mock.Mock(return_value=get_return)
|
||||
testurl = '/test/url/'
|
||||
self.assertRaises(exception.VolumeDriverException,
|
||||
self.conn.get, testurl)
|
||||
expected = [mock.call(), ]
|
||||
self.conn._create_session.assert_has_calls(expected)
|
||||
expected = [mock.call(expectedhdr), ]
|
||||
self.conn._delete_session.assert_has_calls(expected)
|
||||
expected = [mock.call(url=self.conn._complete_uri(testurl),
|
||||
headers=expectedhdr, verify=True),
|
||||
mock.call(url=self.conn._complete_uri(testurl),
|
||||
headers=expectedhdr, verify=True),
|
||||
mock.call(url=self.conn._complete_uri(testurl),
|
||||
headers=expectedhdr, verify=True), ]
|
||||
requests.get.assert_has_calls(expected)
|
||||
|
||||
def test_get_failed_http_response(self):
|
||||
get_return = FIOFakeResponse(code=404,
|
||||
text=json.dumps(basic_acl_group_response))
|
||||
expectedhdr = copy.deepcopy(self.conn.defhdrs)
|
||||
expectedhdr["Cookie"] = 'session=' + self.fakeSessionID
|
||||
self.conn._create_session = mock.Mock(return_value=expectedhdr)
|
||||
self.conn._delete_session = mock.Mock()
|
||||
requests.get = mock.Mock(return_value=get_return)
|
||||
testurl = '/test/url/'
|
||||
self.assertRaises(requests.exceptions.HTTPError,
|
||||
self.conn.get, testurl)
|
||||
expected = [mock.call(), ]
|
||||
self.conn._create_session.assert_has_calls(expected)
|
||||
expected = [mock.call(expectedhdr), ]
|
||||
self.conn._delete_session.assert_has_calls(expected)
|
||||
expected = [mock.call(url=self.conn._complete_uri(testurl),
|
||||
headers=expectedhdr, verify=True), ]
|
||||
requests.get.assert_has_calls(expected)
|
||||
|
||||
|
||||
@mock.patch('cinder.volume.drivers.fusionio.ioControl.FIOconnection',
|
||||
autospec=True)
|
||||
class FIOioControlTestCases(test.TestCase):
|
||||
|
||||
VERSION = '1.0.0'
|
||||
policyTable = {'Policy 4': '00000000-00000000-0000-000000000002',
|
||||
'Policy 5': '00000000-00000000-0000-000000000000',
|
||||
'Policy 2': '00000000-00000000-0000-000000000008',
|
||||
'Policy 3': '00000000-00000000-0000-000000000004',
|
||||
'Policy 1': '00000000-00000000-0000-000000000010'}
|
||||
|
||||
def setUp(self):
|
||||
super(FIOioControlTestCases, self).setUp()
|
||||
self.configuration = create_configuration()
|
||||
self.ctxt = context.get_admin_context()
|
||||
self.drv = FIOioControlDriver(configuration=self.configuration)
|
||||
self.drv.fio_qos_dict = self.policyTable
|
||||
|
||||
def test_do_setup_sucess(self, connmock):
|
||||
# erase policy table, then make sure drv.do_setup builds it
|
||||
self.drv.fio_qos_dict = {}
|
||||
instance = connmock.return_value
|
||||
instance.get.return_value = basic_policy_response
|
||||
self.drv.do_setup(context="")
|
||||
self.assertEqual(self.policyTable, self.drv.fio_qos_dict,
|
||||
"wrong policy table built")
|
||||
|
||||
def test_create_volume_simple_success_poolA(self, connmock):
|
||||
self.drv.conn = connmock.return_value
|
||||
bPoolResponse = copy.deepcopy(basic_pools_response)
|
||||
bPoolResponse[1]['ExportedVolumeMB'] = 5009
|
||||
self.drv.conn.get.return_value = bPoolResponse
|
||||
testvol = {'project_id': 'testproject',
|
||||
'name': 'cinderVolumeName',
|
||||
'size': 1,
|
||||
'id': 'cinderVolumeID',
|
||||
'volume_type_id': None,
|
||||
'created_at': timeutils.utcnow()}
|
||||
self.drv.create_volume(testvol)
|
||||
cmd = {"Size": int(testvol['size']) * units.Gi,
|
||||
"PolicyUUID": '00000000-00000000-0000-000000000000',
|
||||
"PoolUUID": "FakePoolA_id",
|
||||
"Name": testvol['id'], }
|
||||
expected = [mock.call.get('TierStore/Pools/by-id/'),
|
||||
mock.call.post('TierStore/Volumes/by-id/', cmd)]
|
||||
self.drv.conn.assert_has_calls(expected)
|
||||
|
||||
def test_create_volume_simple_success_poolB(self, connmock):
|
||||
self.drv.conn = connmock.return_value
|
||||
bPoolResponse = copy.deepcopy(basic_pools_response)
|
||||
bPoolResponse[0]['ExportedVolumeMB'] = 5009
|
||||
self.drv.conn.get.return_value = bPoolResponse
|
||||
testvol = {'project_id': 'testproject',
|
||||
'name': 'cinderVolumeName',
|
||||
'size': 1,
|
||||
'id': 'cinderVolumeID',
|
||||
'volume_type_id': None,
|
||||
'created_at': timeutils.utcnow()}
|
||||
self.drv.create_volume(testvol)
|
||||
cmd = {"Size": int(testvol['size']) * units.Gi,
|
||||
"PolicyUUID": '00000000-00000000-0000-000000000000',
|
||||
"PoolUUID": "FakePoolB_id",
|
||||
"Name": testvol['id'], }
|
||||
expected = [mock.call.get('TierStore/Pools/by-id/'),
|
||||
mock.call.post('TierStore/Volumes/by-id/', cmd)]
|
||||
self.drv.conn.assert_has_calls(expected)
|
||||
|
||||
def test_delete_volume_sucess(self, connmock):
|
||||
self.drv.conn = connmock.return_value
|
||||
testvol = {'project_id': 'testproject',
|
||||
'name': 'cinderVolumeName',
|
||||
'size': 1,
|
||||
'id': 'cinderVolumeID',
|
||||
'volume_type_id': None,
|
||||
'created_at': timeutils.utcnow()}
|
||||
self.drv.conn.get.return_value = basic_vol_response
|
||||
self.drv.delete_volume(testvol)
|
||||
expected = [mock.call.get('TierStore/Volumes/by-id/'),
|
||||
mock.call.delete('TierStore/Volumes/by-id/FakeBasicVolID')]
|
||||
self.drv.conn.assert_has_calls(expected)
|
||||
|
||||
def test_create_snapshot_sucess(self, connmock):
|
||||
self.drv.conn = connmock.return_value
|
||||
snapshot = {'volume_id': 'cinderVolumeID',
|
||||
'id': 'a720b3c0-d1f0-11e1-9b23-1234500cab39', }
|
||||
self.drv.conn.get.return_value = basic_vol_response
|
||||
cmd = {"VolumeUUID": "FakeBasicVolID",
|
||||
"Name": snapshot['id'], }
|
||||
self.drv.create_snapshot(snapshot)
|
||||
expected = [mock.call.get('TierStore/Volumes/by-id/'),
|
||||
mock.call.post('TierStore/Snapshots/by-id/', cmd), ]
|
||||
self.drv.conn.assert_has_calls(expected)
|
||||
|
||||
def test_delete_snapshot_sucess(self, connmock):
|
||||
self.drv.conn = connmock.return_value
|
||||
snapshot = {'volume_id': '1dead3c0-d1f0-beef-9b23-1274500cab58',
|
||||
'id': 'cinderSnapshotID'}
|
||||
self.drv.conn.get.return_value = basic_snapshot_response
|
||||
self.drv.delete_snapshot(snapshot)
|
||||
expected = [mock.call.get('TierStore/Snapshots/by-id/'),
|
||||
mock.call.delete(
|
||||
('TierStore/Snapshots/by-id/' +
|
||||
'407115424bb9539c')), ]
|
||||
self.drv.conn.assert_has_calls(expected)
|
||||
|
||||
def test_create_volume_from_snapshot_simple_sucess(self, connmock):
|
||||
self.drv.conn = connmock.return_value
|
||||
testvol = {'project_id': 'testproject',
|
||||
'name': 'cinderVolumeName',
|
||||
'size': 1,
|
||||
'id': 'cinderVolumeID',
|
||||
'volume_type_id': None,
|
||||
'created_at': timeutils.utcnow()}
|
||||
snapshot = {'volume_id': testvol['id'],
|
||||
'id': 'cinderSnapshotID'}
|
||||
self.drv.conn.get.return_value = basic_snapshot_response
|
||||
cmd = {"ParentLayerId": "407115424bb9539c",
|
||||
"Name": testvol['id'],
|
||||
"PolicyUUID": '00000000-00000000-0000-000000000000'}
|
||||
self.drv.create_volume_from_snapshot(testvol, snapshot)
|
||||
expected = [mock.call.get('TierStore/Snapshots/by-id/'),
|
||||
mock.call.put(
|
||||
'TierStore/Snapshots/functions/CloneSnapshot', cmd), ]
|
||||
self.drv.conn.assert_has_calls(expected)
|
||||
|
||||
def test_initialize_connection_no_usable_Networks_fail(self, connmock):
|
||||
self.drv.conn = connmock.return_value
|
||||
connector = {'initiator': 'fake:01'}
|
||||
testvol = {'project_id': 'testproject',
|
||||
'name': 'cinderVolumeName',
|
||||
'size': 1,
|
||||
'id': 'cinderVolumeID',
|
||||
'volume_type_id': None,
|
||||
'created_at': timeutils.utcnow(),
|
||||
'provider_auth': {}}
|
||||
cmd = {"GroupName": "fake:01",
|
||||
"InitiatorList": ["fake:01"]}
|
||||
cmd2 = {"AclGroupList": ["3"], }
|
||||
netResponse = copy.deepcopy(basic_net_response)
|
||||
netResponse[4]['OperationalState'] = "down"
|
||||
get_effect = [basic_vol_response,
|
||||
basic_acl_group_response,
|
||||
basic_vol_response,
|
||||
netResponse, ]
|
||||
self.drv.conn.get.side_effect = get_effect
|
||||
self.assertRaises(exception.VolumeDriverException,
|
||||
self.drv.initialize_connection, testvol,
|
||||
connector)
|
||||
expected = [mock.call.get('TierStore/Volumes/by-id/'),
|
||||
mock.call.post('TierStore/ACLGroup/by-id/', cmd),
|
||||
mock.call.get('TierStore/ACLGroup/by-id/'),
|
||||
mock.call.put('TierStore/Volumes/by-id/FakeBasicVolID',
|
||||
cmd2),
|
||||
mock.call.get('TierStore/Volumes/by-id/'),
|
||||
mock.call.get('System/Network/by-id/'), ]
|
||||
self.drv.conn.assert_has_calls(expected)
|
||||
|
||||
def test_initialize_connection_simple_sucess(self, connmock):
|
||||
self.drv.conn = connmock.return_value
|
||||
connector = {'initiator': 'fake:01'}
|
||||
testvol = {'project_id': 'testproject',
|
||||
'name': 'cinderVolumeName',
|
||||
'size': 1,
|
||||
'id': 'cinderVolumeID',
|
||||
'volume_type_id': None,
|
||||
'created_at': timeutils.utcnow(),
|
||||
'provider_auth': {}}
|
||||
cmd = {"GroupName": "fake:01",
|
||||
"InitiatorList": ["fake:01"]}
|
||||
cmd2 = {"AclGroupList": ["3"], }
|
||||
netResponse = copy.deepcopy(basic_net_response)
|
||||
netResponse[2]['OperationalState'] = "up"
|
||||
get_effect = [basic_vol_response,
|
||||
basic_acl_group_response,
|
||||
basic_vol_response,
|
||||
netResponse, ]
|
||||
self.drv.conn.get.side_effect = get_effect
|
||||
result = self.drv.initialize_connection(testvol, connector)
|
||||
expected = {'driver_volume_type': 'iscsi',
|
||||
'data': {'target_lun': 0,
|
||||
'target_portal': u'10.10.2.84:3260',
|
||||
'target_iqn': (
|
||||
'iqn.2010-11.com.ngs:Volume:FakeBasicVolID'),
|
||||
'target_discovered': False,
|
||||
'volume_id': 'cinderVolumeID'}}
|
||||
self.assertEqual(result, expected, "wrong result from init connection")
|
||||
expected = [mock.call.get('TierStore/Volumes/by-id/'),
|
||||
mock.call.post('TierStore/ACLGroup/by-id/', cmd),
|
||||
mock.call.get('TierStore/ACLGroup/by-id/'),
|
||||
mock.call.put('TierStore/Volumes/by-id/FakeBasicVolID',
|
||||
cmd2),
|
||||
mock.call.get('TierStore/Volumes/by-id/'),
|
||||
mock.call.get('System/Network/by-id/'), ]
|
||||
self.drv.conn.assert_has_calls(expected)
|
||||
|
||||
def test_terminate_connection_single_delete_sucess(self, connmock):
|
||||
self.drv.conn = connmock.return_value
|
||||
connector = {'initiator': 'fake:01'}
|
||||
testvol = {'project_id': 'testproject',
|
||||
'name': 'cinderVolumeName',
|
||||
'size': 1,
|
||||
'id': 'cinderVolumeID',
|
||||
'volume_type_id': None,
|
||||
'created_at': timeutils.utcnow(),
|
||||
'provider_auth': {}}
|
||||
cmd = {"AclGroupList": ["1"], }
|
||||
get_effect = [basic_vol_response,
|
||||
basic_acl_group_response,
|
||||
basic_acl_group_response,
|
||||
basic_vol_response, ]
|
||||
self.drv.conn.get.side_effect = get_effect
|
||||
self.drv.terminate_connection(testvol, connector)
|
||||
expected = [mock.call.get('TierStore/Volumes/by-id/'),
|
||||
mock.call.get('TierStore/ACLGroup/by-id/'),
|
||||
mock.call.put('TierStore/Volumes/by-id/FakeBasicVolID',
|
||||
cmd),
|
||||
mock.call.get('TierStore/ACLGroup/by-id/'),
|
||||
mock.call.get('TierStore/Volumes/by-id/'),
|
||||
mock.call.delete('TierStore/ACLGroup/by-id/3')]
|
||||
self.drv.conn.assert_has_calls(expected)
|
||||
|
||||
def test_terminate_connection_multiple_no_delete(self, connmock):
|
||||
self.drv.conn = connmock.return_value
|
||||
connector = {'initiator': 'fake:01'}
|
||||
testvol = {'project_id': 'testproject',
|
||||
'name': 'cinderVolumeName',
|
||||
'size': 1,
|
||||
'id': 'cinderVolumeID',
|
||||
'volume_type_id': None,
|
||||
'created_at': timeutils.utcnow(),
|
||||
'provider_auth': {}}
|
||||
cmd = {"AclGroupList": ["1"], }
|
||||
return2vol = copy.deepcopy(basic_vol_response)
|
||||
return2vol.append(copy.deepcopy(basic_vol_response[0]))
|
||||
return2vol[1]['AclGroupList'] = ["3"]
|
||||
get_effect = [basic_vol_response,
|
||||
basic_acl_group_response,
|
||||
basic_acl_group_response,
|
||||
return2vol, ]
|
||||
self.drv.conn.get.side_effect = get_effect
|
||||
self.drv.terminate_connection(testvol, connector)
|
||||
expected = [mock.call.get('TierStore/Volumes/by-id/'),
|
||||
mock.call.get('TierStore/ACLGroup/by-id/'),
|
||||
mock.call.put('TierStore/Volumes/by-id/FakeBasicVolID',
|
||||
cmd),
|
||||
mock.call.get('TierStore/ACLGroup/by-id/'),
|
||||
mock.call.get('TierStore/Volumes/by-id/')]
|
||||
self.drv.conn.assert_has_calls(expected)
|
||||
|
||||
def test_terminate_connection_multiple_delete(self, connmock):
|
||||
self.drv.conn = connmock.return_value
|
||||
connector = {'initiator': 'fake:01'}
|
||||
testvol = {'project_id': 'testproject',
|
||||
'name': 'cinderVolumeName',
|
||||
'size': 1,
|
||||
'id': 'cinderVolumeID',
|
||||
'volume_type_id': None,
|
||||
'created_at': timeutils.utcnow(),
|
||||
'provider_auth': {}}
|
||||
cmd = {"AclGroupList": ["1"], }
|
||||
return2vol = copy.deepcopy(basic_vol_response)
|
||||
return2vol.append(copy.deepcopy(basic_vol_response[0]))
|
||||
get_effect = [basic_vol_response,
|
||||
basic_acl_group_response,
|
||||
basic_acl_group_response,
|
||||
return2vol, ]
|
||||
self.drv.conn.get.side_effect = get_effect
|
||||
self.drv.terminate_connection(testvol, connector)
|
||||
expected = [mock.call.get('TierStore/Volumes/by-id/'),
|
||||
mock.call.get('TierStore/ACLGroup/by-id/'),
|
||||
mock.call.put('TierStore/Volumes/by-id/FakeBasicVolID',
|
||||
cmd),
|
||||
mock.call.get('TierStore/ACLGroup/by-id/'),
|
||||
mock.call.get('TierStore/Volumes/by-id/'),
|
||||
mock.call.delete('TierStore/ACLGroup/by-id/3')]
|
||||
self.drv.conn.assert_has_calls(expected)
|
||||
|
||||
def test_create_cloned_volume_simple_sucess(self, connmock):
|
||||
self.drv.conn = connmock.return_value
|
||||
srcvol = {'id': 'cinderVolumeID'}
|
||||
dstvol = {'project_id': 'testproject',
|
||||
'name': 'cinderVolumeName',
|
||||
'size': 1,
|
||||
'id': 'cinderVolumeID-dst',
|
||||
'volume_type_id': None,
|
||||
'created_at': timeutils.utcnow()}
|
||||
cmd = {'VolumeUUID': 'FakeBasicVolID',
|
||||
'Name': 'mockedFakeUUID'}
|
||||
# also mock _getSnapshotByName because of the random snapshotname.
|
||||
self.drv._get_snapshot_by_name = mock.MagicMock()
|
||||
self.drv._get_snapshot_by_name.return_value = \
|
||||
basic_snapshot_response[0]
|
||||
cmd2 = {"ParentLayerId": "407115424bb9539c",
|
||||
"Name": "cinderVolumeID-dst",
|
||||
"PolicyUUID": "00000000-00000000-0000-000000000000"}
|
||||
get_effect = [basic_vol_response, ]
|
||||
self.drv.conn.get.side_effect = get_effect
|
||||
|
||||
with mock.patch('cinder.volume.drivers.fusionio.ioControl.uuid',
|
||||
autospec=True) as uuidmock:
|
||||
uuidmock.uuid4.return_value = cmd['Name']
|
||||
self.drv.create_cloned_volume(dstvol, srcvol)
|
||||
|
||||
expected = [mock.call.get('TierStore/Volumes/by-id/'),
|
||||
mock.call.post('TierStore/Snapshots/by-id/', cmd),
|
||||
mock.call.put(('TierStore/Snapshots/functions/' +
|
||||
'CloneSnapshot'), cmd2), ]
|
||||
self.drv.conn.assert_has_calls(expected)
|
||||
|
||||
def test_create_cloned_volume_snapfails(self, connmock):
|
||||
self.drv.conn = connmock.return_value
|
||||
# this operation is a 2 part process, snap, then clone.
|
||||
# This tests for the snap failing
|
||||
srcvol = {'id': 'cinderVolumeID'}
|
||||
dstvol = {'project_id': 'testproject',
|
||||
'name': 'cinderVolumeName',
|
||||
'size': 1,
|
||||
'id': 'cinderVolumeID-dst',
|
||||
'volume_type_id': None,
|
||||
'created_at': timeutils.utcnow()}
|
||||
cmd = {'VolumeUUID': 'FakeBasicVolID',
|
||||
'Name': 'mockedFakeUUID'}
|
||||
get_effect = [basic_vol_response, ]
|
||||
self.drv.conn.get.side_effect = get_effect
|
||||
self.drv.conn.post.side_effect = requests.exceptions.HTTPError
|
||||
with mock.patch('cinder.volume.drivers.fusionio.ioControl.uuid',
|
||||
autospec=True) as uuidmock:
|
||||
uuidmock.uuid4.return_value = cmd['Name']
|
||||
self.assertRaises(requests.exceptions.HTTPError,
|
||||
self.drv.create_cloned_volume,
|
||||
dstvol, srcvol)
|
||||
expected = [mock.call.get('TierStore/Volumes/by-id/'),
|
||||
mock.call.post('TierStore/Snapshots/by-id/', cmd), ]
|
||||
self.drv.conn.assert_has_calls(expected)
|
||||
|
||||
def test_create_cloned_volume_clonefails(self, connmock):
|
||||
self.drv.conn = connmock.return_value
|
||||
srcvol = {'id': 'cinderVolumeID'}
|
||||
dstvol = {'project_id': 'testproject',
|
||||
'name': 'cinderVolumeName',
|
||||
'size': 1,
|
||||
'id': 'cinderVolumeID-dst',
|
||||
'volume_type_id': None,
|
||||
'created_at': timeutils.utcnow()}
|
||||
get_effect = [basic_vol_response,
|
||||
basic_snapshot_response[0], ]
|
||||
self.drv.conn.get.side_effect = get_effect
|
||||
# also mock _getSnapshotByName because of the random snapshotname.
|
||||
self.drv._get_snapshot_by_name = mock.MagicMock()
|
||||
self.drv._get_snapshot_by_name.return_value = \
|
||||
basic_snapshot_response[0]
|
||||
cmd = {'VolumeUUID': 'FakeBasicVolID',
|
||||
'Name': 'mockedFakeUUID'}
|
||||
cmd2 = {"ParentLayerId": "407115424bb9539c",
|
||||
"Name": "cinderVolumeID-dst",
|
||||
"PolicyUUID": "00000000-00000000-0000-000000000000"}
|
||||
self.drv.conn.put.side_effect = requests.exceptions.HTTPError
|
||||
with mock.patch('cinder.volume.drivers.fusionio.ioControl.uuid',
|
||||
autospec=True) as uuidmock:
|
||||
uuidmock.uuid4.return_value = cmd['Name']
|
||||
self.assertRaises(requests.exceptions.HTTPError,
|
||||
self.drv.create_cloned_volume,
|
||||
dstvol, srcvol)
|
||||
expected = [mock.call.get('TierStore/Volumes/by-id/'),
|
||||
mock.call.post('TierStore/Snapshots/by-id/', cmd),
|
||||
mock.call.put(('TierStore/Snapshots/functions/' +
|
||||
'CloneSnapshot'), cmd2),
|
||||
mock.call.delete(('TierStore/Snapshots/by-id/' +
|
||||
cmd2['ParentLayerId'])), ]
|
||||
self.drv.conn.assert_has_calls(expected)
|
||||
|
||||
def test_get_volume_stats_simple_sucess(self, connmock):
|
||||
self.drv.conn = connmock.return_value
|
||||
self.drv.conn.get.side_effect = [basic_pools_response, ]
|
||||
result = self.drv.get_volume_stats(refresh=True)
|
||||
self.assertEqual(basic_pools_response[0]['PagingTotalMB'] +
|
||||
basic_pools_response[1]['PagingTotalMB'],
|
||||
result['total_capacity_gb'],
|
||||
"capacity calc wrong")
|
||||
self.assertEqual(self.VERSION, result['driver_version'],
|
||||
"Driver/Test version Mismatch")
|
||||
|
||||
def test_create_volume_QoS_by_presets(self, connmock):
|
||||
preset_qos = VolumeMetadata(key='fio-qos', value='Policy 2')
|
||||
testvol = {'project_id': 'testprjid',
|
||||
'name': 'testvol',
|
||||
'size': 1,
|
||||
'id': 'a720b3c0-d1f0-11e1-9b23-0800200c9a66',
|
||||
'volume_metadata': [preset_qos],
|
||||
'volume_type_id': None,
|
||||
'created_at': timeutils.utcnow()}
|
||||
|
||||
expected_qos_result = '00000000-00000000-0000-000000000008' # Policy 2
|
||||
qos = self.drv._set_qos_presets(testvol)
|
||||
self.assertEqual(qos, expected_qos_result)
|
||||
|
||||
def test_create_volume_Qos_by_volumeType_QoSspec(self, connmock):
|
||||
qos_ref = qos_specs.create(self.ctxt,
|
||||
'qos-specs-1', {'fio-qos': 'Policy 2'})
|
||||
type_ref = volume_types.create(self.ctxt,
|
||||
"type1",
|
||||
{"volume_backend_name": "fio-ioControl",
|
||||
"qos:fio-qos": "Policy 4"}
|
||||
)
|
||||
qos_specs.associate_qos_with_type(self.ctxt,
|
||||
qos_ref['id'],
|
||||
type_ref['id'])
|
||||
expected_qos_result = '00000000-00000000-0000-000000000008' # Policy 2
|
||||
qos = self.drv._set_qos_by_volume_type(type_ref['id'])
|
||||
self.assertEqual(qos, expected_qos_result)
|
||||
|
||||
def test_create_volume_Qos_by_volumeType_extraSpec(self, connmock):
|
||||
type_ref = volume_types.create(self.ctxt,
|
||||
"type1",
|
||||
{"volume_backend_name": "fio-ioControl",
|
||||
"qos:fio-qos": "Policy 4"}
|
||||
)
|
||||
expected_qos_result = '00000000-00000000-0000-000000000002' # Policy 4
|
||||
qos = self.drv._set_qos_by_volume_type(type_ref['id'])
|
||||
self.assertEqual(qos, expected_qos_result)
|
||||
|
||||
def test_extend_volume_simple_success(self, connmock):
|
||||
self.drv.conn = connmock.return_value
|
||||
testvol = {'project_id': 'testproject',
|
||||
'name': 'cinderVolumeName',
|
||||
'size': 1,
|
||||
'id': 'cinderVolumeID',
|
||||
'volume_type_id': None,
|
||||
'created_at': timeutils.utcnow()}
|
||||
new_size = 10
|
||||
cmd = {"Size": int(new_size) * units.Gi}
|
||||
self.drv.conn.get.side_effect = [basic_vol_response, ]
|
||||
self.drv.extend_volume(testvol, new_size)
|
||||
expected = [mock.call.get('TierStore/Volumes/by-id/'),
|
||||
mock.call.put('TierStore/Volumes/by-id/FakeBasicVolID',
|
||||
cmd)]
|
||||
self.drv.conn.assert_has_calls(expected)
|
|
@ -0,0 +1,557 @@
|
|||
# Copyright (c) 2014 Fusion-io, Inc.
|
||||
# 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.
|
||||
|
||||
"""
|
||||
Fusion-io Driver for the ioControl Hybrid storage subsystem
|
||||
"""
|
||||
|
||||
import copy
|
||||
import hashlib
|
||||
import json
|
||||
import random
|
||||
import uuid
|
||||
|
||||
from oslo.config import cfg
|
||||
import requests
|
||||
|
||||
from cinder import exception
|
||||
from cinder.i18n import _
|
||||
from cinder.openstack.common import log as logging
|
||||
from cinder.openstack.common import loopingcall
|
||||
from cinder.openstack.common import units
|
||||
from cinder.volume.drivers.san.san import SanISCSIDriver
|
||||
from cinder.volume import qos_specs
|
||||
from cinder.volume import volume_types
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
fusionio_iocontrol_opts = [
|
||||
cfg.IntOpt('fusionio_iocontrol_targetdelay',
|
||||
default=5,
|
||||
help='amount of time wait for iSCSI target to come online'),
|
||||
cfg.IntOpt('fusionio_iocontrol_retry',
|
||||
default=3,
|
||||
help='number of retries for GET operations'),
|
||||
cfg.BoolOpt('fusionio_iocontrol_verify_cert',
|
||||
default=True,
|
||||
help='verify the array certificate on each transaction'), ]
|
||||
|
||||
CONF = cfg.CONF
|
||||
CONF.register_opts(fusionio_iocontrol_opts)
|
||||
|
||||
|
||||
class FIOconnection(object):
|
||||
"""Connection class for connection to ioControl array."""
|
||||
|
||||
APIVERSION = '1.1'
|
||||
|
||||
def _complete_uri(self, suburi=None, ver='1', loc='en'):
|
||||
uri = "https://" + self.array_addr + "/API/"
|
||||
if ver is not None:
|
||||
uri = uri + ver + "/"
|
||||
if loc is not None:
|
||||
uri = uri + loc + "/"
|
||||
if suburi is not None:
|
||||
uri = uri + suburi
|
||||
return uri
|
||||
|
||||
def __init__(self, array_addr, array_login, array_passwd, retry, verify):
|
||||
self.client = "client=openstack"
|
||||
self.defhdrs = {"User-Agent": "OpenStack-agent",
|
||||
"Content-Type": "application/json"}
|
||||
self.array_addr = array_addr
|
||||
self.array_login = array_login
|
||||
self.hashpass = hashlib.md5()
|
||||
self.hashpass.update(array_passwd)
|
||||
self.login_content = ("username=" + array_login + "&hash=" +
|
||||
self.hashpass.hexdigest())
|
||||
self.retry = retry
|
||||
self.verify = verify
|
||||
# check the version of the API on the array. We only support 1.1
|
||||
# for now.
|
||||
resp = requests.get(url=("https://" + array_addr + "/AUTH/Version"),
|
||||
headers=self.defhdrs, verify=self.verify)
|
||||
resp.raise_for_status()
|
||||
dictresp = resp.json()
|
||||
if dictresp["Version"] != self.APIVERSION:
|
||||
msg = _("FIO ioControl API version not supported")
|
||||
raise exception.VolumeDriverException(message=msg)
|
||||
LOG.debug('FIO Connection initialized to %s' % array_addr)
|
||||
|
||||
def _create_session(self):
|
||||
# get the session id
|
||||
res = requests.post(url=("https://" + self.array_addr +
|
||||
"/AUTH/SESSION"),
|
||||
data=self.client,
|
||||
headers=self.defhdrs,
|
||||
verify=self.verify)
|
||||
res.raise_for_status()
|
||||
result = res.json()
|
||||
session_key = result["id"]
|
||||
hdrs = copy.deepcopy(self.defhdrs)
|
||||
hdrs["Cookie"] = "session=" + session_key
|
||||
# Authenticate the session
|
||||
res = requests.put(url=("https://" + self.array_addr +
|
||||
"/AUTH/SESSION/" + session_key),
|
||||
data=self.login_content,
|
||||
headers=self.defhdrs,
|
||||
verify=self.verify)
|
||||
try:
|
||||
res.raise_for_status()
|
||||
except requests.exceptions:
|
||||
self._delete_session(hdrs)
|
||||
raise
|
||||
result = res.json()
|
||||
if result["Status"] != 1:
|
||||
# Authentication error delete the session ID
|
||||
self._delete_session(hdrs)
|
||||
msg = (_('FIO ioControl Authentication Error: %s') % (result))
|
||||
raise exception.VolumeDriverException(message=msg)
|
||||
return hdrs
|
||||
|
||||
def _delete_session(self, hdrs):
|
||||
session = hdrs["Cookie"].split('=')[1]
|
||||
requests.delete(url=("https://" + self.array_addr +
|
||||
"/AUTH/SESSION/" + session),
|
||||
headers=self.defhdrs,
|
||||
verify=self.verify)
|
||||
|
||||
def get(self, suburl):
|
||||
session_hdrs = self._create_session()
|
||||
trynum = 0
|
||||
try:
|
||||
while (trynum < self.retry):
|
||||
trynum += 1
|
||||
res = requests.get(url=self._complete_uri(suburl),
|
||||
headers=session_hdrs,
|
||||
verify=self.verify)
|
||||
res.raise_for_status()
|
||||
# work around a bug whereby bad json is returned by the array
|
||||
try:
|
||||
jres = res.json()
|
||||
break
|
||||
except Exception:
|
||||
if (trynum == self.retry):
|
||||
# this shouldn't happen, but check for it
|
||||
msg = (_('FIO ioControl persistent json Error.'))
|
||||
raise exception.VolumeDriverException(message=msg)
|
||||
pass
|
||||
finally:
|
||||
# deal with the bad result here
|
||||
self._delete_session(session_hdrs)
|
||||
return jres
|
||||
|
||||
def put(self, suburl, content=None):
|
||||
session_hdrs = self._create_session()
|
||||
try:
|
||||
result = requests.put(url=self._complete_uri(suburl),
|
||||
data=json.dumps(content,
|
||||
sort_keys=True),
|
||||
headers=session_hdrs,
|
||||
verify=self.verify)
|
||||
result.raise_for_status()
|
||||
finally:
|
||||
self._delete_session(session_hdrs)
|
||||
return
|
||||
|
||||
def post(self, suburl, content=None):
|
||||
session_hdrs = self._create_session()
|
||||
try:
|
||||
result = requests.post(url=self._complete_uri(suburl),
|
||||
data=json.dumps(content,
|
||||
sort_keys=True),
|
||||
headers=session_hdrs,
|
||||
verify=self.verify)
|
||||
result.raise_for_status()
|
||||
finally:
|
||||
self._delete_session(session_hdrs)
|
||||
return
|
||||
|
||||
def delete(self, suburl,):
|
||||
session_hdrs = self._create_session()
|
||||
try:
|
||||
result = requests.delete(url=self._complete_uri(suburl),
|
||||
headers=session_hdrs,
|
||||
verify=self.verify)
|
||||
result.raise_for_status()
|
||||
finally:
|
||||
self._delete_session(session_hdrs)
|
||||
return
|
||||
|
||||
|
||||
class FIOioControlDriver(SanISCSIDriver):
|
||||
"""Fusion-io ioControl iSCSI volume driver."""
|
||||
|
||||
VERSION = '1.0.0'
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(FIOioControlDriver, self).__init__(*args, **kwargs)
|
||||
LOG.debug('FIO __init__ w/ %s' % kwargs)
|
||||
self.configuration.append_config_values(fusionio_iocontrol_opts)
|
||||
self.fio_qos_dict = {}
|
||||
|
||||
def _get_volume_by_name(self, name):
|
||||
result = self.conn.get("TierStore/Volumes/by-id/")
|
||||
vol = [x for x in result
|
||||
if x['Name'] == name]
|
||||
if len(vol) == 1:
|
||||
return vol[0]
|
||||
elif len(vol) == 0:
|
||||
raise exception.VolumeNotFound(name)
|
||||
else:
|
||||
msg = (_("FIO _get_volume_by_name Error: %(name)s, %(len)s") %
|
||||
{'name': name,
|
||||
'len': len(vol)})
|
||||
raise exception.VolumeDriverException(msg)
|
||||
|
||||
def _get_acl_by_name(self, name):
|
||||
result = self.conn.get("TierStore/ACLGroup/by-id/")
|
||||
acl = [x for x in result
|
||||
if x['GroupName'] == name]
|
||||
if len(acl) == 1:
|
||||
return acl[0]
|
||||
elif len(acl) == 0:
|
||||
return []
|
||||
else:
|
||||
msg = (_("FIO _get_acl_by_name Error: %(name)s, %(len)s") %
|
||||
{'name': name,
|
||||
'len': len(acl), })
|
||||
raise exception.VolumeDriverException(message=msg)
|
||||
|
||||
def _get_snapshot_by_name(self, name):
|
||||
result = self.conn.get("TierStore/Snapshots/by-id/")
|
||||
snap = [x for x in result
|
||||
if x['Name'] == name]
|
||||
if len(snap) == 1:
|
||||
return snap[0]
|
||||
elif len(snap) == 0:
|
||||
raise exception.SnapshotNotFound(name)
|
||||
else:
|
||||
msg = (_("FIO _get_snapshot_by_name Error: %(name)s, %(len)s") %
|
||||
{'name': name,
|
||||
'len': len(snap), })
|
||||
raise exception.VolumeDriverException(message=msg)
|
||||
|
||||
def _set_qos_presets(self, volume):
|
||||
valid_presets = self.fio_qos_dict.keys()
|
||||
|
||||
presets = [i.value for i in volume.get('volume_metadata')
|
||||
if i.key == 'fio-qos' and i.value in valid_presets]
|
||||
if len(presets) > 0:
|
||||
if len(presets) > 1:
|
||||
LOG.warning(_('More than one valid preset was '
|
||||
'detected, using %s') % presets[0])
|
||||
return self.fio_qos_dict[presets[0]]
|
||||
|
||||
def _set_qos_by_volume_type(self, type_id):
|
||||
valid_presets = self.fio_qos_dict.keys()
|
||||
volume_type = volume_types.get_volume_type(ctxt=None,
|
||||
id=type_id)
|
||||
qos_specs_id = volume_type.get('qos_specs_id')
|
||||
specs = volume_type.get('extra_specs')
|
||||
if qos_specs_id is not None:
|
||||
kvs = qos_specs.get_qos_specs(ctxt=None,
|
||||
id=qos_specs_id)['specs']
|
||||
else:
|
||||
kvs = specs
|
||||
for key, value in kvs.iteritems():
|
||||
if ':' in key:
|
||||
fields = key.split(':')
|
||||
key = fields[1]
|
||||
if 'fio-qos' in key:
|
||||
if value in valid_presets:
|
||||
return self.fio_qos_dict[value]
|
||||
|
||||
def do_setup(self, context):
|
||||
LOG.debug('FIO do_setup() called')
|
||||
required_flags = ['san_ip',
|
||||
'san_login',
|
||||
'san_password', ]
|
||||
for flag in required_flags:
|
||||
if not getattr(self.configuration, flag, None):
|
||||
raise exception.InvalidInput(reason=_('%s is not set') % flag)
|
||||
if not (self.configuration.san_ip and
|
||||
self.configuration.san_login and
|
||||
self.configuration.san_password):
|
||||
raise exception.InvalidInput(
|
||||
reason=_('All of '
|
||||
'san_ip '
|
||||
'san_login '
|
||||
'san_password '
|
||||
'must be set'))
|
||||
self.conn = FIOconnection(self.configuration.san_ip,
|
||||
self.configuration.san_login,
|
||||
self.configuration.san_password,
|
||||
self.configuration.fusionio_iocontrol_retry,
|
||||
(self.configuration.
|
||||
fusionio_iocontrol_verify_cert))
|
||||
result = self.conn.get("TierStore/Policies/by-id/")
|
||||
for x in result:
|
||||
self.fio_qos_dict[x['Name']] = x['id']
|
||||
|
||||
def check_for_setup_error(self):
|
||||
pass
|
||||
|
||||
def create_volume(self, volume):
|
||||
LOG.debug('FIO create_volume() called: %s' % (volume['id']))
|
||||
# Roughly we pick the less full pool.
|
||||
# Someday change the default the policy to be configurable
|
||||
qos = self.fio_qos_dict['Policy 5']
|
||||
result = self.conn.get("TierStore/Pools/by-id/")
|
||||
poola = result[0]['PagingTotalMB'] - result[0]['ExportedVolumeMB']
|
||||
poolb = result[1]['PagingTotalMB'] - result[1]['ExportedVolumeMB']
|
||||
if poola >= poolb:
|
||||
pool = result[0]['id']
|
||||
else:
|
||||
pool = result[1]['id']
|
||||
if volume.get('volume_metadata')is not None:
|
||||
qos = self._set_qos_presets(volume)
|
||||
|
||||
type_id = volume['volume_type_id']
|
||||
if type_id is not None:
|
||||
qos = self._set_qos_by_volume_type(type_id)
|
||||
|
||||
cmd = {"Size": int(volume['size']) * units.Gi,
|
||||
"PolicyUUID": qos,
|
||||
"PoolUUID": pool,
|
||||
"Name": volume['id'], }
|
||||
self.conn.post("TierStore/Volumes/by-id/", cmd)
|
||||
LOG.debug(('FIO create_vol(%(id)s) on %(pool)s vals %(poola)s '
|
||||
'%(poolb)s') %
|
||||
{'id': volume['id'],
|
||||
'pool': pool,
|
||||
'poola': poola,
|
||||
'poolb': poolb})
|
||||
|
||||
def delete_volume(self, volume):
|
||||
LOG.debug('FIO delete_volume() volID %s' % (volume['id']))
|
||||
vol = self._get_volume_by_name(volume['id'])
|
||||
self.conn.delete("TierStore/Volumes/by-id/" + vol['id'])
|
||||
|
||||
def ensure_export(self, context, volume):
|
||||
pass
|
||||
|
||||
def create_export(self, context, volume):
|
||||
pass
|
||||
|
||||
def remove_export(self, context, volume):
|
||||
pass
|
||||
|
||||
def initialize_connection(self, volume, connector):
|
||||
LOG.debug('FIO init_connection() w/ %(id)s and %(conn)s' %
|
||||
{'id': volume['id'],
|
||||
'conn': connector['initiator']})
|
||||
# setup the access group each initiator will go in a unique access
|
||||
# group.
|
||||
# TODO(ebalduf) implement w/ CHAP
|
||||
volumedata = self._get_volume_by_name(volume['id'])
|
||||
cmd = {"GroupName": connector['initiator'],
|
||||
"InitiatorList": [connector['initiator']]}
|
||||
self.conn.post("TierStore/ACLGroup/by-id/", cmd)
|
||||
|
||||
acl = self._get_acl_by_name(connector['initiator'])
|
||||
if acl is not []:
|
||||
cmd = {"AclGroupList": [str(acl['id'])], }
|
||||
self.conn.put("TierStore/Volumes/by-id/" + volumedata['id'], cmd)
|
||||
else:
|
||||
# this should never happen, but check for it in case
|
||||
msg = _('FIO: ACL does not exist!')
|
||||
raise exception.VolumeDriverException(message=msg)
|
||||
# handle the fact that the Application of the ACL to the volume
|
||||
# is asynchronous. In the future we'll add a call back to the API
|
||||
|
||||
def _wait_routine():
|
||||
# unfortunately, the array API at this time doesn't have any
|
||||
# way to poll. In the future we will add that ability and
|
||||
# this routine is where were will poll for ready.
|
||||
if self._looping_count == 0:
|
||||
self._looping_count += 1
|
||||
else:
|
||||
raise loopingcall.LoopingCallDone()
|
||||
|
||||
# time.sleep(self.configuration.fusionio_iocontrol_targetdelay)
|
||||
self._looping_count = 0
|
||||
timer = loopingcall.FixedIntervalLoopingCall(_wait_routine)
|
||||
timer.start(
|
||||
interval=self.configuration.fusionio_iocontrol_targetdelay).wait()
|
||||
volumedata = self._get_volume_by_name(volume['id'])
|
||||
|
||||
properties = {}
|
||||
properties['target_discovered'] = False
|
||||
properties['target_iqn'] = volumedata['IQN']
|
||||
properties['target_lun'] = 0
|
||||
properties['volume_id'] = volume['id']
|
||||
|
||||
result = self.conn.get("System/Network/by-id/")
|
||||
|
||||
# probably way too complicated, but pick a random network interface
|
||||
# on the controller this LUN is owned by
|
||||
networksinfo = [x for x in result
|
||||
if x['OperationalState'] == 'up'
|
||||
if x['IsManagementPort'] is not True
|
||||
if x['IsReplicationPort'] is not True
|
||||
if x['ControllerUID'] ==
|
||||
volumedata['CurrentOwnerUUID']]
|
||||
LOG.debug('NetworkInfo %s' % (networksinfo))
|
||||
if len(networksinfo):
|
||||
ipaddr = (networksinfo[random.randint(0, len(networksinfo) - 1)]
|
||||
['NetworkAddress'])
|
||||
else:
|
||||
msg = _('No usable Networks found: %s') % (result)
|
||||
raise exception.VolumeDriverException(message=msg)
|
||||
properties['target_portal'] = unicode('%s:%s' % (ipaddr, '3260'))
|
||||
|
||||
auth = volume['provider_auth']
|
||||
if auth:
|
||||
(auth_method, auth_username, auth_secret) = auth.split()
|
||||
|
||||
properties['auth_method'] = auth_method
|
||||
properties['auth_username'] = auth_username
|
||||
properties['auth_password'] = auth_secret
|
||||
|
||||
LOG.debug('Result from initialize connection: %s' % properties)
|
||||
return {
|
||||
'driver_volume_type': 'iscsi',
|
||||
'data': properties,
|
||||
}
|
||||
|
||||
def create_snapshot(self, snapshot):
|
||||
LOG.debug(('FIO create_snapshot() vol ID: %(volID)s snapID '
|
||||
'%(snapID)s') %
|
||||
{'volID': snapshot['volume_id'],
|
||||
'snapID': snapshot['id']})
|
||||
vol = self._get_volume_by_name(snapshot['volume_id'])
|
||||
cmd = {"VolumeUUID": vol['id'],
|
||||
"Name": snapshot['id'], }
|
||||
self.conn.post("TierStore/Snapshots/by-id/", cmd)
|
||||
|
||||
def delete_snapshot(self, snapshot):
|
||||
LOG.debug('FIO delete_snapshot() SnapID: %s' % (snapshot['id']))
|
||||
snap = self._get_snapshot_by_name(snapshot['id'])
|
||||
self.conn.delete("TierStore/Snapshots/by-id/" + snap['id'])
|
||||
|
||||
def create_volume_from_snapshot(self, volume, snapshot):
|
||||
LOG.debug('FIO create_volume_from_snapshot() w/ %s' %
|
||||
volume['id'])
|
||||
|
||||
qos = self.fio_qos_dict['Policy 5']
|
||||
if volume.get('volume_metadata')is not None:
|
||||
qos = self._set_qos_presets(volume)
|
||||
|
||||
type_id = volume['volume_type_id']
|
||||
if type_id is not None:
|
||||
qos = self._set_qos_by_volume_type(type_id)
|
||||
snap = self._get_snapshot_by_name(snapshot['id'])
|
||||
cmd = {"ParentLayerId": snap['id'],
|
||||
"Name": volume['id'],
|
||||
"PolicyUUID": qos}
|
||||
self.conn.put("TierStore/Snapshots/functions/CloneSnapshot", cmd)
|
||||
|
||||
def _delete_acl_by_name(self, name):
|
||||
aclname = self._get_acl_by_name(name)
|
||||
if aclname is []:
|
||||
return
|
||||
result = self.conn.get("TierStore/Volumes/by-id/")
|
||||
inuse = False
|
||||
for vol in result:
|
||||
for acl in vol['AclGroupList']:
|
||||
if int(acl) == aclname['id']:
|
||||
inuse = True
|
||||
break
|
||||
if inuse:
|
||||
break
|
||||
if not inuse:
|
||||
result = self.conn.delete("TierStore/ACLGroup/by-id/" +
|
||||
str(aclname['id']))
|
||||
|
||||
def terminate_connection(self, volume, connector, **kwargs):
|
||||
LOG.debug('FIO terminate_connection() w/ %(id)s %(conn)s ' %
|
||||
{'id': volume['id'],
|
||||
'conn': connector['initiator']})
|
||||
vol = self._get_volume_by_name(volume['id'])
|
||||
acl = self._get_acl_by_name("Deny Access")
|
||||
if acl is []:
|
||||
msg = _('FIO: ACL does not exist!')
|
||||
raise exception.VolumeDriverException(message=msg)
|
||||
cmd = {"AclGroupList": [str(acl['id'])], }
|
||||
self.conn.put("TierStore/Volumes/by-id/" + vol['id'], cmd)
|
||||
self._delete_acl_by_name(connector['initiator'])
|
||||
|
||||
def create_cloned_volume(self, volume, src_vref):
|
||||
LOG.debug('FIO create_cloned_volume() w/ %(id)s %(src)s' %
|
||||
{'id': volume['id'],
|
||||
'src': src_vref})
|
||||
qos = self.fio_qos_dict['Policy 5']
|
||||
# take a snapshot of the volume (use random UUID for name)
|
||||
snapshotname = str(uuid.uuid4())
|
||||
vol = self._get_volume_by_name(src_vref['id'])
|
||||
cmd = {"VolumeUUID": vol['id'],
|
||||
"Name": snapshotname, }
|
||||
self.conn.post("TierStore/Snapshots/by-id/", cmd)
|
||||
|
||||
# create a volume from the snapshot with the new name.
|
||||
# Rollback = Delete the snapshot if needed.
|
||||
if volume.get('volume_metadata')is not None:
|
||||
qos = self._set_qos_presets(volume)
|
||||
|
||||
type_id = volume['volume_type_id']
|
||||
if type_id is not None:
|
||||
qos = self._set_qos_by_volume_type(type_id)
|
||||
|
||||
snap = self._get_snapshot_by_name(snapshotname)
|
||||
cmd = {"ParentLayerId": snap['id'],
|
||||
"Name": volume['id'],
|
||||
"PolicyUUID": qos, }
|
||||
try:
|
||||
# watch for any issues here, and if there are, clean up the
|
||||
# snapshot and re-raise
|
||||
self.conn.put("TierStore/Snapshots/functions/CloneSnapshot", cmd)
|
||||
except Exception:
|
||||
snap = self._get_snapshot_by_name(snapshotname)
|
||||
self.conn.delete("TierStore/Snapshots/by-id/" + snap['id'])
|
||||
raise
|
||||
|
||||
def get_volume_stats(self, refresh=False):
|
||||
"""Retrieve status info from volume group."""
|
||||
LOG.debug("FIO Updating volume status")
|
||||
if refresh:
|
||||
result = self.conn.get("TierStore/Pools/by-id/")
|
||||
data = {}
|
||||
backend_name = self.configuration.safe_get('volume_backend_name')
|
||||
data["volume_backend_name"] = (backend_name
|
||||
or self.__class__.__name__)
|
||||
data["vendor_name"] = 'Fusion-io Inc'
|
||||
data["driver_version"] = self.VERSION
|
||||
data["storage_protocol"] = 'iSCSI'
|
||||
data['total_capacity_gb'] = (result[0]['PagingTotalMB'] +
|
||||
result[1]['PagingTotalMB'])
|
||||
data['free_capacity_gb'] = (max((result[0]['PagingTotalMB'] -
|
||||
result[0]['ExportedVolumeMB']),
|
||||
(result[1]['PagingTotalMB'] -
|
||||
result[1]['ExportedVolumeMB'])))
|
||||
data['reserved_percentage'] = 10
|
||||
data['QoS_support'] = True
|
||||
self._stats = data
|
||||
|
||||
LOG.debug('Result from status: %s' % data)
|
||||
return self._stats
|
||||
|
||||
def extend_volume(self, volume, new_size):
|
||||
LOG.debug("FIO extend_volume %(id)s to %(size)s" %
|
||||
{'id': volume['id'],
|
||||
'size': new_size})
|
||||
cmd = {"Size": int(new_size) * units.Gi}
|
||||
vol = self._get_volume_by_name(volume['id'])
|
||||
self.conn.put("TierStore/Volumes/by-id/" + vol['id'], cmd)
|
|
@ -1187,6 +1187,22 @@
|
|||
#eqlx_pool=default
|
||||
|
||||
|
||||
#
|
||||
# Options defined in cinder.volume.drivers.fusionio.ioControl
|
||||
#
|
||||
|
||||
# amount of time wait for iSCSI target to come online (integer
|
||||
# value)
|
||||
#fusionio_iocontrol_targetdelay=5
|
||||
|
||||
# number of retries for GET operations (integer value)
|
||||
#fusionio_iocontrol_retry=3
|
||||
|
||||
# verify the array certificate on each transaction (boolean
|
||||
# value)
|
||||
#fusionio_iocontrol_verify_cert=true
|
||||
|
||||
|
||||
#
|
||||
# Options defined in cinder.volume.drivers.glusterfs
|
||||
#
|
||||
|
|
Loading…
Reference in New Issue