Merge "NetApp direct to filer drivers for iscsi and nfs."
This commit is contained in:
@@ -25,9 +25,10 @@ NEXENTA_MODULE = "cinder.volume.drivers.nexenta.volume.NexentaDriver"
|
||||
SAN_MODULE = "cinder.volume.drivers.san.san.SanISCSIDriver"
|
||||
SOLARIS_MODULE = "cinder.volume.drivers.san.solaris.SolarisISCSIDriver"
|
||||
LEFTHAND_MODULE = "cinder.volume.drivers.san.hp_lefthand.HpSanISCSIDriver"
|
||||
NETAPP_MODULE = "cinder.volume.drivers.netapp.NetAppISCSIDriver"
|
||||
NETAPP_CMODE_MODULE = "cinder.volume.drivers.netapp.NetAppCmodeISCSIDriver"
|
||||
NETAPP_NFS_MODULE = "cinder.volume.drivers.netapp_nfs.NetAppNFSDriver"
|
||||
NETAPP_MODULE = "cinder.volume.drivers.netapp.iscsi.NetAppISCSIDriver"
|
||||
NETAPP_CMODE_MODULE =\
|
||||
"cinder.volume.drivers.netapp.iscsi.NetAppCmodeISCSIDriver"
|
||||
NETAPP_NFS_MODULE = "cinder.volume.drivers.netapp.nfs.NetAppNFSDriver"
|
||||
NFS_MODULE = "cinder.volume.drivers.nfs.NfsDriver"
|
||||
SOLIDFIRE_MODULE = "cinder.volume.drivers.solidfire.SolidFire"
|
||||
STORWIZE_SVC_MODULE = "cinder.volume.drivers.storwize_svc.StorwizeSVCDriver"
|
||||
|
||||
@@ -25,9 +25,11 @@ import StringIO
|
||||
|
||||
from lxml import etree
|
||||
|
||||
from cinder.exception import VolumeBackendAPIException
|
||||
from cinder.openstack.common import log as logging
|
||||
from cinder import test
|
||||
from cinder.volume.drivers import netapp
|
||||
from cinder.volume.drivers.netapp import iscsi
|
||||
|
||||
|
||||
LOG = logging.getLogger("cinder.volume.driver")
|
||||
|
||||
@@ -973,7 +975,7 @@ class NetAppDriverTestCase(test.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
super(NetAppDriverTestCase, self).setUp()
|
||||
driver = netapp.NetAppISCSIDriver()
|
||||
driver = iscsi.NetAppISCSIDriver()
|
||||
self.stubs.Set(httplib, 'HTTPConnection', FakeHTTPConnection)
|
||||
driver._create_client(wsdl_url='http://localhost:8088/dfm.wsdl',
|
||||
login='root', password='password',
|
||||
@@ -1020,6 +1022,16 @@ class NetAppDriverTestCase(test.TestCase):
|
||||
self.driver._discover_luns()
|
||||
self.driver._is_clone_done(0, '0', 'xxx')
|
||||
|
||||
def test_cloned_volume_size_fail(self):
|
||||
volume_clone_fail = {'name': 'fail', 'size': '2'}
|
||||
volume_src = {'name': 'source_vol', 'size': '1'}
|
||||
try:
|
||||
self.driver.create_cloned_volume(volume_clone_fail,
|
||||
volume_src)
|
||||
raise AssertionError()
|
||||
except VolumeBackendAPIException:
|
||||
pass
|
||||
|
||||
|
||||
WSDL_HEADER_CMODE = """<?xml version="1.0" encoding="UTF-8"?>
|
||||
<definitions xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/"
|
||||
@@ -1269,7 +1281,7 @@ class FakeCMODEServerHandler(BaseHTTPServer.BaseHTTPRequestHandler):
|
||||
/>"""
|
||||
elif 'CloneLun' == api:
|
||||
body = """<ns:CloneLunResult xmlns:ns="http://cloud.netapp.com/">
|
||||
<Lun><Name>lun2</Name><Size>2</Size>
|
||||
<Lun><Name>snapshot1</Name><Size>2</Size>
|
||||
<Handle>98ea1791d228453899d422b4611642c3</Handle>
|
||||
<Metadata><Key>OsType</Key>
|
||||
<Value>linux</Value></Metadata>
|
||||
@@ -1354,22 +1366,36 @@ class FakeCmodeHTTPConnection(object):
|
||||
|
||||
class NetAppCmodeISCSIDriverTestCase(test.TestCase):
|
||||
"""Test case for NetAppISCSIDriver"""
|
||||
volume = {'name': 'lun1', 'size': 1, 'volume_name': 'lun1',
|
||||
volume = {'name': 'lun1', 'size': 2, 'volume_name': 'lun1',
|
||||
'os_type': 'linux', 'provider_location': 'lun1',
|
||||
'id': 'lun1', 'provider_auth': None, 'project_id': 'project',
|
||||
'display_name': None, 'display_description': 'lun1',
|
||||
'volume_type_id': None}
|
||||
snapshot = {'name': 'lun2', 'size': 1, 'volume_name': 'lun1',
|
||||
'volume_size': 1, 'project_id': 'project'}
|
||||
volume_sec = {'name': 'vol_snapshot', 'size': 1, 'volume_name': 'lun1',
|
||||
snapshot = {'name': 'snapshot1', 'size': 2, 'volume_name': 'lun1',
|
||||
'volume_size': 2, 'project_id': 'project',
|
||||
'display_name': None, 'display_description': 'lun1',
|
||||
'volume_type_id': None}
|
||||
snapshot_fail = {'name': 'snapshot2', 'size': 2, 'volume_name': 'lun1',
|
||||
'volume_size': 1, 'project_id': 'project'}
|
||||
volume_sec = {'name': 'vol_snapshot', 'size': 2, 'volume_name': 'lun1',
|
||||
'os_type': 'linux', 'provider_location': 'lun1',
|
||||
'id': 'lun1', 'provider_auth': None, 'project_id': 'project',
|
||||
'display_name': None, 'display_description': 'lun1',
|
||||
'volume_type_id': None}
|
||||
volume_clone_fail = {'name': 'cl_fail', 'size': 1, 'volume_name': 'fail',
|
||||
'os_type': 'linux', 'provider_location': 'cl_fail',
|
||||
'id': 'lun1', 'provider_auth': None,
|
||||
'project_id': 'project', 'display_name': None,
|
||||
'display_description': 'lun1',
|
||||
'volume_type_id': None}
|
||||
connector = {'initiator': 'iqn.1993-08.org.debian:01:10'}
|
||||
|
||||
def setUp(self):
|
||||
super(NetAppCmodeISCSIDriverTestCase, self).setUp()
|
||||
driver = netapp.NetAppCmodeISCSIDriver()
|
||||
self._custom_setup()
|
||||
|
||||
def _custom_setup(self):
|
||||
driver = iscsi.NetAppCmodeISCSIDriver()
|
||||
self.stubs.Set(httplib, 'HTTPConnection', FakeCmodeHTTPConnection)
|
||||
driver._create_client(wsdl_url='http://localhost:8080/ntap_cloud.wsdl',
|
||||
login='root', password='password',
|
||||
@@ -1395,10 +1421,884 @@ class NetAppCmodeISCSIDriverTestCase(test.TestCase):
|
||||
updates = self.driver.create_export(None, self.volume)
|
||||
self.assertTrue(updates['provider_location'])
|
||||
self.volume['provider_location'] = updates['provider_location']
|
||||
connector = {'initiator': 'init1'}
|
||||
|
||||
connection_info = self.driver.initialize_connection(self.volume,
|
||||
connector)
|
||||
self.connector)
|
||||
self.assertEqual(connection_info['driver_volume_type'], 'iscsi')
|
||||
properties = connection_info['data']
|
||||
self.driver.terminate_connection(self.volume, connector)
|
||||
if not properties:
|
||||
raise AssertionError('Target portal is none')
|
||||
self.driver.terminate_connection(self.volume, self.connector)
|
||||
self.driver.delete_volume(self.volume)
|
||||
|
||||
def test_fail_vol_from_snapshot_creation(self):
|
||||
self.driver.create_volume(self.volume)
|
||||
try:
|
||||
self.driver.create_volume_from_snapshot(self.volume,
|
||||
self.snapshot_fail)
|
||||
raise AssertionError()
|
||||
except VolumeBackendAPIException:
|
||||
pass
|
||||
finally:
|
||||
self.driver.delete_volume(self.volume)
|
||||
|
||||
def test_cloned_volume_destroy(self):
|
||||
self.driver.create_volume(self.volume)
|
||||
self.driver.create_cloned_volume(self.snapshot, self.volume)
|
||||
self.driver.delete_volume(self.snapshot)
|
||||
self.driver.delete_volume(self.volume)
|
||||
|
||||
def test_fail_cloned_volume_creation(self):
|
||||
self.driver.create_volume(self.volume)
|
||||
try:
|
||||
self.driver.create_cloned_volume(self.volume_clone_fail,
|
||||
self.volume)
|
||||
raise AssertionError()
|
||||
except VolumeBackendAPIException:
|
||||
pass
|
||||
finally:
|
||||
self.driver.delete_volume(self.volume)
|
||||
|
||||
|
||||
RESPONSE_PREFIX_DIRECT_CMODE = """<?xml version='1.0' encoding='UTF-8' ?>
|
||||
<!DOCTYPE netapp SYSTEM 'file:/etc/netapp_gx.dtd'>"""
|
||||
|
||||
RESPONSE_PREFIX_DIRECT_7MODE = """<?xml version='1.0' encoding='UTF-8' ?>
|
||||
<!DOCTYPE netapp SYSTEM "/na_admin/netapp_filer.dtd">"""
|
||||
|
||||
RESPONSE_PREFIX_DIRECT = """
|
||||
<netapp version='1.15' xmlns='http://www.netapp.com/filer/admin'>"""
|
||||
|
||||
RESPONSE_SUFFIX_DIRECT = """</netapp>"""
|
||||
|
||||
|
||||
class FakeDirectCMODEServerHandler(BaseHTTPServer.BaseHTTPRequestHandler):
|
||||
"""HTTP handler that fakes enough stuff to allow the driver to run"""
|
||||
|
||||
def do_GET(s):
|
||||
"""Respond to a GET request."""
|
||||
if '/servlets/netapp.servlets.admin.XMLrequest_filer' != s.path:
|
||||
s.send_response(404)
|
||||
s.end_headers
|
||||
return
|
||||
s.send_response(200)
|
||||
s.send_header("Content-Type", "text/xml; charset=utf-8")
|
||||
s.end_headers()
|
||||
out = s.wfile
|
||||
out.write('<netapp version="1.15">'
|
||||
'<results reason="Not supported method type"'
|
||||
' status="failed" errno="Not_Allowed"/></netapp>')
|
||||
|
||||
def do_POST(s):
|
||||
"""Respond to a POST request."""
|
||||
if '/servlets/netapp.servlets.admin.XMLrequest_filer' != s.path:
|
||||
s.send_response(404)
|
||||
s.end_headers
|
||||
return
|
||||
request_xml = s.rfile.read(int(s.headers['Content-Length']))
|
||||
root = etree.fromstring(request_xml)
|
||||
body = [x for x in root.iterchildren()]
|
||||
request = body[0]
|
||||
tag = request.tag
|
||||
api = etree.QName(tag).localname or tag
|
||||
if 'lun-get-iter' == api:
|
||||
tag = \
|
||||
FakeDirectCMODEServerHandler._get_child_by_name(request, 'tag')
|
||||
if tag is None:
|
||||
body = """<results status="passed"><attributes-list>
|
||||
<lun-info>
|
||||
<alignment>indeterminate</alignment>
|
||||
<block-size>512</block-size>
|
||||
<comment></comment><creation-timestamp>1354536362
|
||||
</creation-timestamp>
|
||||
<is-space-alloc-enabled>false</is-space-alloc-enabled>
|
||||
<is-space-reservation-enabled>true
|
||||
</is-space-reservation-enabled>
|
||||
<mapped>false</mapped><multiprotocol-type>linux
|
||||
</multiprotocol-type>
|
||||
<online>true</online><path>/vol/navneet/lun2</path>
|
||||
<prefix-size>0</prefix-size><qtree></qtree><read-only>
|
||||
false</read-only><serial-number>2FfGI$APyN68</serial-number>
|
||||
<share-state>none</share-state><size>20971520</size>
|
||||
<size-used>0</size-used><staging>false</staging>
|
||||
<suffix-size>0</suffix-size>
|
||||
<uuid>cec1f3d7-3d41-11e2-9cf4-123478563412</uuid>
|
||||
<volume>navneet</volume><vserver>ben_vserver</vserver>
|
||||
</lun-info></attributes-list>
|
||||
<next-tag><lun-get-iter-key-td>
|
||||
<key-0>ben_vserver</key-0>
|
||||
<key-1>/vol/navneet/lun2</key-1>
|
||||
<key-2>navneet</key-2>
|
||||
<key-3></key-3>
|
||||
<key-4>lun2</key-4>
|
||||
</lun-get-iter-key-td>
|
||||
</next-tag><num-records>1</num-records></results>"""
|
||||
else:
|
||||
body = """<results status="passed"><attributes-list>
|
||||
<lun-info>
|
||||
<alignment>indeterminate</alignment>
|
||||
<block-size>512</block-size>
|
||||
<comment></comment><creation-timestamp>1354536362
|
||||
</creation-timestamp>
|
||||
<is-space-alloc-enabled>false</is-space-alloc-enabled>
|
||||
<is-space-reservation-enabled>true
|
||||
</is-space-reservation-enabled>
|
||||
<mapped>false</mapped><multiprotocol-type>linux
|
||||
</multiprotocol-type>
|
||||
<online>true</online><path>/vol/navneet/lun3</path>
|
||||
<prefix-size>0</prefix-size><qtree></qtree><read-only>
|
||||
false</read-only><serial-number>2FfGI$APyN68
|
||||
</serial-number>
|
||||
<share-state>none</share-state><size>20971520</size>
|
||||
<size-used>0</size-used><staging>false</staging>
|
||||
<suffix-size>0</suffix-size>
|
||||
<uuid>cec1f3d7-3d41-11e2-9cf4-123478563412</uuid>
|
||||
<volume>navneet</volume><vserver>ben_vserver</vserver>
|
||||
</lun-info></attributes-list>
|
||||
<num-records>1</num-records></results>"""
|
||||
elif 'volume-get-iter' == api:
|
||||
tag = \
|
||||
FakeDirectCMODEServerHandler._get_child_by_name(request, 'tag')
|
||||
if tag is None:
|
||||
body = """<results status="passed"><attributes-list>
|
||||
<volume-attributes>
|
||||
<volume-id-attributes><name>iscsi</name>
|
||||
<owning-vserver-name>Openstack</owning-vserver-name>
|
||||
</volume-id-attributes>
|
||||
<volume-space-attributes>
|
||||
<size-available>214748364</size-available>
|
||||
</volume-space-attributes>
|
||||
<volume-state-attributes><is-cluster-volume>true
|
||||
</is-cluster-volume>
|
||||
<is-vserver-root>false</is-vserver-root><state>online</state>
|
||||
</volume-state-attributes></volume-attributes>
|
||||
<volume-attributes>
|
||||
<volume-id-attributes><name>nfsvol</name>
|
||||
<owning-vserver-name>openstack</owning-vserver-name>
|
||||
</volume-id-attributes>
|
||||
<volume-space-attributes>
|
||||
<size-available>247483648</size-available>
|
||||
</volume-space-attributes>
|
||||
<volume-state-attributes><is-cluster-volume>true
|
||||
</is-cluster-volume>
|
||||
<is-vserver-root>false</is-vserver-root><state>online</state>
|
||||
</volume-state-attributes></volume-attributes>
|
||||
</attributes-list>
|
||||
<next-tag><volume-get-iter-key-td>
|
||||
<key-0>openstack</key-0>
|
||||
<key-1>nfsvol</key-1>
|
||||
</volume-get-iter-key-td>
|
||||
</next-tag><num-records>2</num-records></results>"""
|
||||
else:
|
||||
body = """<results status="passed"><attributes-list>
|
||||
<volume-attributes>
|
||||
<volume-id-attributes><name>iscsi</name>
|
||||
<owning-vserver-name>Openstack</owning-vserver-name>
|
||||
</volume-id-attributes>
|
||||
<volume-space-attributes>
|
||||
<size-available>4147483648</size-available>
|
||||
</volume-space-attributes>
|
||||
<volume-state-attributes><is-cluster-volume>true
|
||||
</is-cluster-volume>
|
||||
<is-vserver-root>false</is-vserver-root><state>online</state>
|
||||
</volume-state-attributes></volume-attributes>
|
||||
<volume-attributes>
|
||||
<volume-id-attributes><name>nfsvol</name>
|
||||
<owning-vserver-name>openstack</owning-vserver-name>
|
||||
</volume-id-attributes>
|
||||
<volume-space-attributes>
|
||||
<size-available>8147483648</size-available>
|
||||
</volume-space-attributes>
|
||||
<volume-state-attributes><is-cluster-volume>true
|
||||
</is-cluster-volume>
|
||||
<is-vserver-root>false</is-vserver-root><state>online</state>
|
||||
</volume-state-attributes></volume-attributes>
|
||||
</attributes-list>
|
||||
<num-records>2</num-records></results>"""
|
||||
elif 'lun-create-by-size' == api:
|
||||
body = """<results status="passed">
|
||||
<actual-size>22020096</actual-size></results>"""
|
||||
elif 'lun-destroy' == api:
|
||||
body = """<results status="passed"/>"""
|
||||
elif 'igroup-get-iter' == api:
|
||||
init_found = True
|
||||
query = FakeDirectCMODEServerHandler._get_child_by_name(request,
|
||||
'query')
|
||||
if query:
|
||||
igroup_info = FakeDirectCMODEServerHandler._get_child_by_name(
|
||||
query, 'initiator-group-info')
|
||||
if igroup_info:
|
||||
inits = FakeDirectCMODEServerHandler._get_child_by_name(
|
||||
igroup_info, 'initiators')
|
||||
if inits:
|
||||
init_info = \
|
||||
FakeDirectCMODEServerHandler._get_child_by_name(
|
||||
inits, 'initiator-info')
|
||||
init_name = \
|
||||
FakeDirectCMODEServerHandler._get_child_content(
|
||||
init_info,
|
||||
'initiator-name')
|
||||
if init_name == 'iqn.1993-08.org.debian:01:10':
|
||||
init_found = True
|
||||
else:
|
||||
init_found = False
|
||||
if init_found:
|
||||
tag = \
|
||||
FakeDirectCMODEServerHandler._get_child_by_name(
|
||||
request, 'tag')
|
||||
if tag is None:
|
||||
body = """<results status="passed"><attributes-list>
|
||||
<initiator-group-info><initiator-group-name>
|
||||
openstack-01f5297b-00f7-4170-bf30-69b1314b2118
|
||||
</initiator-group-name>
|
||||
<initiator-group-os-type>windows</initiator-group-os-type>
|
||||
<initiator-group-type>iscsi</initiator-group-type>
|
||||
<initiators>
|
||||
<initiator-info>
|
||||
<initiator-name>iqn.1993-08.org.debian:01:10</initiator-name>
|
||||
</initiator-info></initiators>
|
||||
<vserver>openstack</vserver></initiator-group-info>
|
||||
</attributes-list><next-tag>
|
||||
<igroup-get-iter-key-td>
|
||||
<key-0>openstack</key-0>
|
||||
<key-1>
|
||||
openstack-01f5297b-00f7-4170-bf30-69b1314b2118<
|
||||
/key-1>
|
||||
</igroup-get-iter-key-td>
|
||||
</next-tag><num-records>1</num-records></results>"""
|
||||
else:
|
||||
body = """<results status="passed"><attributes-list>
|
||||
<initiator-group-info><initiator-group-name>
|
||||
openstack-01f5297b-00f7-4170-bf30-69b1314b2118
|
||||
</initiator-group-name>
|
||||
<initiator-group-os-type>linux</initiator-group-os-type>
|
||||
<initiator-group-type>iscsi</initiator-group-type>
|
||||
<initiators>
|
||||
<initiator-info>
|
||||
<initiator-name>iqn.1993-08.org.debian:01:10</initiator-name>
|
||||
</initiator-info></initiators>
|
||||
<vserver>openstack</vserver></initiator-group-info>
|
||||
</attributes-list><num-records>1</num-records></results>"""
|
||||
else:
|
||||
body = """<results status="passed">
|
||||
<num-records>0</num-records>
|
||||
</results>"""
|
||||
elif 'lun-map-get-iter' == api:
|
||||
tag = \
|
||||
FakeDirectCMODEServerHandler._get_child_by_name(request, 'tag')
|
||||
if tag is None:
|
||||
body = """<results status="passed"><attributes-list>
|
||||
<lun-map-info>
|
||||
<initiator-group>openstack-44c5e7e1-3306-4800-9623-259e57d56a83
|
||||
</initiator-group>
|
||||
<initiator-group-uuid>948ae304-06e9-11e2</initiator-group-uuid>
|
||||
<lun-id>0</lun-id>
|
||||
<lun-uuid>5587e563-06e9-11e2-9cf4-123478563412</lun-uuid>
|
||||
<path>/vol/openvol/lun1</path>
|
||||
<vserver>openstack</vserver>
|
||||
</lun-map-info></attributes-list>
|
||||
<next-tag>
|
||||
<lun-map-get-iter-key-td>
|
||||
<key-0>openstack</key-0>
|
||||
<key-1>openstack-01f5297b-00f7-4170-bf30-69b1314b2118<
|
||||
/key-1>
|
||||
</lun-map-get-iter-key-td>
|
||||
</next-tag>
|
||||
<num-records>1</num-records>
|
||||
</results>"""
|
||||
else:
|
||||
body = """<results status="passed"><attributes-list>
|
||||
<lun-map-info>
|
||||
<initiator-group>openstack-44c5e7e1-3306-4800-9623-259e57d56a83
|
||||
</initiator-group>
|
||||
<initiator-group-uuid>948ae304-06e9-11e2</initiator-group-uuid>
|
||||
<lun-id>0</lun-id>
|
||||
<lun-uuid>5587e563-06e9-11e2-9cf4-123478563412</lun-uuid>
|
||||
<path>/vol/openvol/lun1</path>
|
||||
<vserver>openstack</vserver>
|
||||
</lun-map-info></attributes-list><num-records>1</num-records>
|
||||
</results>"""
|
||||
elif 'lun-map' == api:
|
||||
body = """<results status="passed"><lun-id-assigned>1
|
||||
</lun-id-assigned>
|
||||
</results>"""
|
||||
elif 'iscsi-service-get-iter' == api:
|
||||
body = """<results status="passed"><attributes-list>
|
||||
<iscsi-service-info>
|
||||
<alias-name>openstack</alias-name>
|
||||
<is-available>true</is-available>
|
||||
<node-name>iqn.1992-08.com.netapp:sn.fa9:vs.105</node-name>
|
||||
<vserver>openstack</vserver></iscsi-service-info>
|
||||
</attributes-list><num-records>1</num-records></results>"""
|
||||
elif 'iscsi-interface-get-iter' == api:
|
||||
body = """<results status="passed"><attributes-list>
|
||||
<iscsi-interface-list-entry-info><current-node>
|
||||
fas3170rre-cmode-01
|
||||
</current-node><current-port>e1b-1165</current-port>
|
||||
<interface-name>
|
||||
iscsi_data_if</interface-name>
|
||||
<ip-address>10.63.165.216</ip-address>
|
||||
<ip-port>3260</ip-port><is-interface-enabled>true
|
||||
</is-interface-enabled>
|
||||
<relative-port-id>5</relative-port-id>
|
||||
<tpgroup-name>iscsi_data_if</tpgroup-name>
|
||||
<tpgroup-tag>1038</tpgroup-tag><vserver>
|
||||
openstack</vserver>
|
||||
</iscsi-interface-list-entry-info></attributes-list>
|
||||
<num-records>1</num-records></results>"""
|
||||
elif 'igroup-create' == api:
|
||||
body = """<results status="passed"/>"""
|
||||
elif 'igroup-add' == api:
|
||||
body = """<results status="passed"/>"""
|
||||
elif 'clone-create' == api:
|
||||
body = """<results status="passed"/>"""
|
||||
elif 'lun-unmap' == api:
|
||||
body = """<results status="passed"/>"""
|
||||
elif 'system-get-ontapi-version' == api:
|
||||
body = """<results status="passed">
|
||||
<major-version>1</major-version>
|
||||
<minor-version>19</minor-version>
|
||||
</results>"""
|
||||
else:
|
||||
# Unknown API
|
||||
s.send_response(500)
|
||||
s.end_headers
|
||||
return
|
||||
s.send_response(200)
|
||||
s.send_header("Content-Type", "text/xml; charset=utf-8")
|
||||
s.end_headers()
|
||||
s.wfile.write(RESPONSE_PREFIX_DIRECT_CMODE)
|
||||
s.wfile.write(RESPONSE_PREFIX_DIRECT)
|
||||
s.wfile.write(body)
|
||||
s.wfile.write(RESPONSE_SUFFIX_DIRECT)
|
||||
|
||||
@staticmethod
|
||||
def _get_child_by_name(self, name):
|
||||
for child in self.iterchildren():
|
||||
if child.tag == name or etree.QName(child.tag).localname == name:
|
||||
return child
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def _get_child_content(self, name):
|
||||
"""Get the content of the child"""
|
||||
for child in self.iterchildren():
|
||||
if child.tag == name or etree.QName(child.tag).localname == name:
|
||||
return child.text
|
||||
return None
|
||||
|
||||
|
||||
class FakeDirectCmodeHTTPConnection(object):
|
||||
"""A fake httplib.HTTPConnection for netapp tests
|
||||
|
||||
Requests made via this connection actually get translated and routed into
|
||||
the fake direct handler above, we then turn the response into
|
||||
the httplib.HTTPResponse that the caller expects.
|
||||
"""
|
||||
def __init__(self, host, timeout=None):
|
||||
self.host = host
|
||||
|
||||
def request(self, method, path, data=None, headers=None):
|
||||
if not headers:
|
||||
headers = {}
|
||||
req_str = '%s %s HTTP/1.1\r\n' % (method, path)
|
||||
for key, value in headers.iteritems():
|
||||
req_str += "%s: %s\r\n" % (key, value)
|
||||
if data:
|
||||
req_str += '\r\n%s' % data
|
||||
|
||||
# NOTE(vish): normally the http transport normailizes from unicode
|
||||
sock = FakeHttplibSocket(req_str.decode("latin-1").encode("utf-8"))
|
||||
# NOTE(vish): stop the server from trying to look up address from
|
||||
# the fake socket
|
||||
FakeDirectCMODEServerHandler.address_string = lambda x: '127.0.0.1'
|
||||
self.app = FakeDirectCMODEServerHandler(sock, '127.0.0.1:80', None)
|
||||
|
||||
self.sock = FakeHttplibSocket(sock.result)
|
||||
self.http_response = httplib.HTTPResponse(self.sock)
|
||||
|
||||
def set_debuglevel(self, level):
|
||||
pass
|
||||
|
||||
def getresponse(self):
|
||||
self.http_response.begin()
|
||||
return self.http_response
|
||||
|
||||
def getresponsebody(self):
|
||||
return self.sock.result
|
||||
|
||||
|
||||
class NetAppDirectCmodeISCSIDriverTestCase(NetAppCmodeISCSIDriverTestCase):
|
||||
"""Test case for NetAppISCSIDriver"""
|
||||
|
||||
vol_fail = {'name': 'lun_fail', 'size': 10000, 'volume_name': 'lun1',
|
||||
'os_type': 'linux', 'provider_location': 'lun1',
|
||||
'id': 'lun1', 'provider_auth': None, 'project_id': 'project',
|
||||
'display_name': None, 'display_description': 'lun1',
|
||||
'volume_type_id': None}
|
||||
|
||||
def setUp(self):
|
||||
super(NetAppDirectCmodeISCSIDriverTestCase, self).setUp()
|
||||
|
||||
def _custom_setup(self):
|
||||
driver = iscsi.NetAppDirectCmodeISCSIDriver()
|
||||
self.stubs.Set(httplib, 'HTTPConnection',
|
||||
FakeDirectCmodeHTTPConnection)
|
||||
driver._create_client(transport_type='http',
|
||||
login='admin', password='pass',
|
||||
hostname='127.0.0.1',
|
||||
port='80')
|
||||
driver.vserver = 'openstack'
|
||||
driver.client.set_api_version(1, 15)
|
||||
self.driver = driver
|
||||
|
||||
def test_map_by_creating_igroup(self):
|
||||
self.driver.create_volume(self.volume)
|
||||
updates = self.driver.create_export(None, self.volume)
|
||||
self.assertTrue(updates['provider_location'])
|
||||
self.volume['provider_location'] = updates['provider_location']
|
||||
connector_new = {'initiator': 'iqn.1993-08.org.debian:01:1001'}
|
||||
connection_info = self.driver.initialize_connection(self.volume,
|
||||
connector_new)
|
||||
self.assertEqual(connection_info['driver_volume_type'], 'iscsi')
|
||||
properties = connection_info['data']
|
||||
if not properties:
|
||||
raise AssertionError('Target portal is none')
|
||||
|
||||
def test_fail_create_vol(self):
|
||||
self.assertRaises(VolumeBackendAPIException,
|
||||
self.driver.create_volume, self.vol_fail)
|
||||
|
||||
|
||||
class FakeDirect7MODEServerHandler(BaseHTTPServer.BaseHTTPRequestHandler):
|
||||
"""HTTP handler that fakes enough stuff to allow the driver to run"""
|
||||
|
||||
def do_GET(s):
|
||||
"""Respond to a GET request."""
|
||||
if '/servlets/netapp.servlets.admin.XMLrequest_filer' != s.path:
|
||||
s.send_response(404)
|
||||
s.end_headers
|
||||
return
|
||||
s.send_response(200)
|
||||
s.send_header("Content-Type", "text/xml; charset=utf-8")
|
||||
s.end_headers()
|
||||
out = s.wfile
|
||||
out.write('<netapp version="1.15">'
|
||||
'<results reason="Not supported method type"'
|
||||
' status="failed" errno="Not_Allowed"/></netapp>')
|
||||
|
||||
def do_POST(s):
|
||||
"""Respond to a POST request."""
|
||||
if '/servlets/netapp.servlets.admin.XMLrequest_filer' != s.path:
|
||||
s.send_response(404)
|
||||
s.end_headers
|
||||
return
|
||||
request_xml = s.rfile.read(int(s.headers['Content-Length']))
|
||||
root = etree.fromstring(request_xml)
|
||||
body = [x for x in root.iterchildren()]
|
||||
request = body[0]
|
||||
tag = request.tag
|
||||
api = etree.QName(tag).localname or tag
|
||||
if 'lun-list-info' == api:
|
||||
body = """<results status="passed">
|
||||
<are-vols-onlining>false</are-vols-onlining>
|
||||
<are-vols-busy>false</are-vols-busy>
|
||||
<luns>
|
||||
<lun-info>
|
||||
<path>/vol/vol1/clone1</path>
|
||||
<size>20971520</size>
|
||||
<online>true</online>
|
||||
<mapped>false</mapped>
|
||||
<read-only>false</read-only>
|
||||
<staging>false</staging>
|
||||
<share-state>none</share-state>
|
||||
<multiprotocol-type>linux</multiprotocol-type>
|
||||
<uuid>e867d844-c2c0-11e0-9282-00a09825b3b5</uuid>
|
||||
<serial-number>P3lgP4eTyaNl</serial-number>
|
||||
<block-size>512</block-size>
|
||||
<is-space-reservation-enabled>true</is-space-reservation-enabled>
|
||||
<size-used>0</size-used>
|
||||
<alignment>indeterminate</alignment>
|
||||
</lun-info>
|
||||
<lun-info>
|
||||
<path>/vol/vol1/lun1</path>
|
||||
<size>20971520</size>
|
||||
<online>true</online>
|
||||
<mapped>false</mapped>
|
||||
<read-only>false</read-only>
|
||||
<staging>false</staging>
|
||||
<share-state>none</share-state>
|
||||
<multiprotocol-type>linux</multiprotocol-type>
|
||||
<uuid>8e1e9284-c288-11e0-9282-00a09825b3b5</uuid>
|
||||
<serial-number>P3lgP4eTc3lp</serial-number>
|
||||
<block-size>512</block-size>
|
||||
<is-space-reservation-enabled>true</is-space-reservation-enabled>
|
||||
<size-used>0</size-used>
|
||||
<alignment>indeterminate</alignment>
|
||||
</lun-info>
|
||||
</luns>
|
||||
</results>"""
|
||||
elif 'volume-list-info' == api:
|
||||
body = """<results status="passed">
|
||||
<volumes>
|
||||
<volume-info>
|
||||
<name>vol0</name>
|
||||
<uuid>019c8f7a-9243-11e0-9281-00a09825b3b5</uuid>
|
||||
<type>flex</type>
|
||||
<block-type>32_bit</block-type>
|
||||
<state>online</state>
|
||||
<size-total>576914493440</size-total>
|
||||
<size-used>13820354560</size-used>
|
||||
<size-available>563094110208</size-available>
|
||||
<percentage-used>2</percentage-used>
|
||||
<snapshot-percent-reserved>20</snapshot-percent-reserved>
|
||||
<snapshot-blocks-reserved>140848264</snapshot-blocks-reserved>
|
||||
<reserve-required>0</reserve-required>
|
||||
<reserve>0</reserve>
|
||||
<reserve-used>0</reserve-used>
|
||||
<reserve-used-actual>0</reserve-used-actual>
|
||||
<files-total>20907162</files-total>
|
||||
<files-used>7010</files-used>
|
||||
<files-private-used>518</files-private-used>
|
||||
<inodefile-public-capacity>31142</inodefile-public-capacity>
|
||||
<inodefile-private-capacity>31142</inodefile-private-capacity>
|
||||
<quota-init>0</quota-init>
|
||||
<is-snaplock>false</is-snaplock>
|
||||
<containing-aggregate>aggr0</containing-aggregate>
|
||||
<sis>
|
||||
<sis-info>
|
||||
<state>disabled</state>
|
||||
<status>idle</status>
|
||||
<progress>idle for 70:36:44</progress>
|
||||
<type>regular</type>
|
||||
<schedule>sun-sat@0</schedule>
|
||||
<last-operation-begin>Mon Aug 8 09:34:15 EST 2011
|
||||
</last-operation-begin>
|
||||
<last-operation-end>Mon Aug 8 09:34:15 EST 2011
|
||||
</last-operation-end>
|
||||
<last-operation-size>0</last-operation-size>
|
||||
<size-shared>0</size-shared>
|
||||
<size-saved>0</size-saved>
|
||||
<percentage-saved>0</percentage-saved>
|
||||
<compress-saved>0</compress-saved>
|
||||
<percent-compress-saved>0</percent-compress-saved>
|
||||
<dedup-saved>0</dedup-saved>
|
||||
<percent-dedup-saved>0</percent-dedup-saved>
|
||||
<total-saved>0</total-saved>
|
||||
<percent-total-saved>0</percent-total-saved>
|
||||
</sis-info>
|
||||
</sis>
|
||||
<compression-info>
|
||||
<is-compression-enabled>false</is-compression-enabled>
|
||||
</compression-info>
|
||||
<space-reserve>volume</space-reserve>
|
||||
<space-reserve-enabled>true</space-reserve-enabled>
|
||||
<raid-size>14</raid-size>
|
||||
<raid-status>raid_dp,sis</raid-status>
|
||||
<checksum-style>block</checksum-style>
|
||||
<is-checksum-enabled>true</is-checksum-enabled>
|
||||
<is-inconsistent>false</is-inconsistent>
|
||||
<is-unrecoverable>false</is-unrecoverable>
|
||||
<is-invalid>false</is-invalid>
|
||||
<is-in-snapmirror-jumpahead>false</is-in-snapmirror-jumpahead>
|
||||
<mirror-status>unmirrored</mirror-status>
|
||||
<disk-count>3</disk-count>
|
||||
<plex-count>1</plex-count>
|
||||
<plexes>
|
||||
<plex-info>
|
||||
<name>/aggr0/plex0</name>
|
||||
<is-online>true</is-online>
|
||||
<is-resyncing>false</is-resyncing>
|
||||
</plex-info>
|
||||
</plexes>
|
||||
</volume-info>
|
||||
<volume-info>
|
||||
<name>vol1</name>
|
||||
<uuid>2d50ecf4-c288-11e0-9282-00a09825b3b5</uuid>
|
||||
<type>flex</type>
|
||||
<block-type>32_bit</block-type>
|
||||
<state>online</state>
|
||||
<size-total>42949672960</size-total>
|
||||
<size-used>44089344</size-used>
|
||||
<size-available>42905583616</size-available>
|
||||
<percentage-used>0</percentage-used>
|
||||
<snapshot-percent-reserved>20</snapshot-percent-reserved>
|
||||
<snapshot-blocks-reserved>10485760</snapshot-blocks-reserved>
|
||||
<reserve-required>8192</reserve-required>
|
||||
<reserve>8192</reserve>
|
||||
<reserve-used>0</reserve-used>
|
||||
<reserve-used-actual>0</reserve-used-actual>
|
||||
<files-total>1556480</files-total>
|
||||
<files-used>110</files-used>
|
||||
<files-private-used>504</files-private-used>
|
||||
<inodefile-public-capacity>31142</inodefile-public-capacity>
|
||||
<inodefile-private-capacity>31142</inodefile-private-capacity>
|
||||
<quota-init>0</quota-init>
|
||||
<is-snaplock>false</is-snaplock>
|
||||
<containing-aggregate>aggr1</containing-aggregate>
|
||||
<sis>
|
||||
<sis-info>
|
||||
<state>disabled</state>
|
||||
<status>idle</status>
|
||||
<progress>idle for 89:19:59</progress>
|
||||
<type>regular</type>
|
||||
<schedule>sun-sat@0</schedule>
|
||||
<last-operation-begin>Sun Aug 7 14:51:00 EST 2011
|
||||
</last-operation-begin>
|
||||
<last-operation-end>Sun Aug 7 14:51:00 EST 2011
|
||||
</last-operation-end>
|
||||
<last-operation-size>0</last-operation-size>
|
||||
<size-shared>0</size-shared>
|
||||
<size-saved>0</size-saved>
|
||||
<percentage-saved>0</percentage-saved>
|
||||
<compress-saved>0</compress-saved>
|
||||
<percent-compress-saved>0</percent-compress-saved>
|
||||
<dedup-saved>0</dedup-saved>
|
||||
<percent-dedup-saved>0</percent-dedup-saved>
|
||||
<total-saved>0</total-saved>
|
||||
<percent-total-saved>0</percent-total-saved>
|
||||
</sis-info>
|
||||
</sis>
|
||||
<compression-info>
|
||||
<is-compression-enabled>false</is-compression-enabled>
|
||||
</compression-info>
|
||||
<space-reserve>volume</space-reserve>
|
||||
<space-reserve-enabled>true</space-reserve-enabled>
|
||||
<raid-size>7</raid-size>
|
||||
<raid-status>raid4,sis</raid-status>
|
||||
<checksum-style>block</checksum-style>
|
||||
<is-checksum-enabled>true</is-checksum-enabled>
|
||||
<is-inconsistent>false</is-inconsistent>
|
||||
<is-unrecoverable>false</is-unrecoverable>
|
||||
<is-invalid>false</is-invalid>
|
||||
<is-in-snapmirror-jumpahead>false</is-in-snapmirror-jumpahead>
|
||||
<mirror-status>unmirrored</mirror-status>
|
||||
<disk-count>2</disk-count>
|
||||
<plex-count>1</plex-count>
|
||||
<plexes>
|
||||
<plex-info>
|
||||
<name>/aggr1/plex0</name>
|
||||
<is-online>true</is-online>
|
||||
<is-resyncing>false</is-resyncing>
|
||||
</plex-info>
|
||||
</plexes>
|
||||
</volume-info>
|
||||
</volumes>
|
||||
</results>"""
|
||||
elif 'volume-options-list-info' == api:
|
||||
body = """<results status="passed">
|
||||
<options>
|
||||
<volume-option-info>
|
||||
<name>snapmirrored</name>
|
||||
<value>off</value>
|
||||
</volume-option-info>
|
||||
<volume-option-info>
|
||||
<name>root</name>
|
||||
<value>false</value>
|
||||
</volume-option-info>
|
||||
<volume-option-info>
|
||||
<name>ha_policy</name>
|
||||
<value>cfo</value>
|
||||
</volume-option-info>
|
||||
<volume-option-info>
|
||||
<name>striping</name>
|
||||
<value>not_striped</value>
|
||||
</volume-option-info>
|
||||
<volume-option-info>
|
||||
<name>compression</name>
|
||||
<value>off</value>
|
||||
</volume-option-info>
|
||||
</options>
|
||||
</results>"""
|
||||
elif 'lun-create-by-size' == api:
|
||||
body = """<results status="passed">
|
||||
<actual-size>22020096</actual-size></results>"""
|
||||
elif 'lun-destroy' == api:
|
||||
body = """<results status="passed"/>"""
|
||||
elif 'igroup-list-info' == api:
|
||||
body = """<results status="passed">
|
||||
<initiator-groups>
|
||||
<initiator-group-info>
|
||||
<initiator-group-name>openstack-8bc96490</initiator-group-name>
|
||||
<initiator-group-type>iscsi</initiator-group-type>
|
||||
<initiator-group-uuid>b8e1d274-c378-11e0</initiator-group-uuid>
|
||||
<initiator-group-os-type>linux</initiator-group-os-type>
|
||||
<initiator-group-throttle-reserve>0</initiator-group-throttle-reserve>
|
||||
<initiator-group-throttle-borrow>false
|
||||
</initiator-group-throttle-borrow>
|
||||
<initiator-group-vsa-enabled>false</initiator-group-vsa-enabled>
|
||||
<initiator-group-alua-enabled>false</initiator-group-alua-enabled>
|
||||
<initiator-group-report-scsi-name-enabled>true
|
||||
</initiator-group-report-scsi-name-enabled>
|
||||
<initiators>
|
||||
<initiator-info>
|
||||
<initiator-name>iqn.1993-08.org.debian:01:10</initiator-name>
|
||||
</initiator-info>
|
||||
</initiators>
|
||||
</initiator-group-info>
|
||||
<initiator-group-info>
|
||||
<initiator-group-name>iscsi_group</initiator-group-name>
|
||||
<initiator-group-type>iscsi</initiator-group-type>
|
||||
<initiator-group-uuid>ccb8cbe4-c36f</initiator-group-uuid>
|
||||
<initiator-group-os-type>linux</initiator-group-os-type>
|
||||
<initiator-group-throttle-reserve>0</initiator-group-throttle-reserve>
|
||||
<initiator-group-throttle-borrow>false
|
||||
</initiator-group-throttle-borrow>
|
||||
<initiator-group-vsa-enabled>false</initiator-group-vsa-enabled>
|
||||
<initiator-group-alua-enabled>false</initiator-group-alua-enabled>
|
||||
<initiator-group-report-scsi-name-enabled>true
|
||||
</initiator-group-report-scsi-name-enabled>
|
||||
<initiators>
|
||||
<initiator-info>
|
||||
<initiator-name>iqn.1993-08.org.debian:01:10ca</initiator-name>
|
||||
</initiator-info>
|
||||
</initiators>
|
||||
</initiator-group-info>
|
||||
</initiator-groups>
|
||||
</results>"""
|
||||
elif 'lun-map-list-info' == api:
|
||||
body = """<results status="passed">
|
||||
<initiator-groups/>
|
||||
</results>"""
|
||||
elif 'lun-map' == api:
|
||||
body = """<results status="passed"><lun-id-assigned>1
|
||||
</lun-id-assigned>
|
||||
</results>"""
|
||||
elif 'iscsi-node-get-name' == api:
|
||||
body = """<results status="passed">
|
||||
<node-name>iqn.1992-08.com.netapp:sn.135093938</node-name>
|
||||
</results>"""
|
||||
elif 'iscsi-portal-list-info' == api:
|
||||
body = """<results status="passed">
|
||||
<iscsi-portal-list-entries>
|
||||
<iscsi-portal-list-entry-info>
|
||||
<ip-address>10.61.176.156</ip-address>
|
||||
<ip-port>3260</ip-port>
|
||||
<tpgroup-tag>1000</tpgroup-tag>
|
||||
<interface-name>e0a</interface-name>
|
||||
</iscsi-portal-list-entry-info>
|
||||
</iscsi-portal-list-entries>
|
||||
</results>"""
|
||||
elif 'igroup-create' == api:
|
||||
body = """<results status="passed"/>"""
|
||||
elif 'igroup-add' == api:
|
||||
body = """<results status="passed"/>"""
|
||||
elif 'clone-start' == api:
|
||||
body = """<results status="passed">
|
||||
<clone-id>
|
||||
<clone-id-info>
|
||||
<volume-uuid>2d50ecf4-c288-11e0-9282-00a09825b3b5</volume-uuid>
|
||||
<clone-op-id>11</clone-op-id>
|
||||
</clone-id-info>
|
||||
</clone-id>
|
||||
</results>"""
|
||||
elif 'clone-list-status' == api:
|
||||
body = """<results status="passed">
|
||||
<status>
|
||||
<ops-info>
|
||||
<clone-state>completed</clone-state>
|
||||
</ops-info>
|
||||
</status>
|
||||
</results>"""
|
||||
elif 'lun-unmap' == api:
|
||||
body = """<results status="passed"/>"""
|
||||
elif 'system-get-ontapi-version' == api:
|
||||
body = """<results status="passed">
|
||||
<major-version>1</major-version>
|
||||
<minor-version>8</minor-version>
|
||||
</results>"""
|
||||
elif 'lun-set-space-reservation-info' == api:
|
||||
body = """<results status="passed"/>"""
|
||||
else:
|
||||
# Unknown API
|
||||
s.send_response(500)
|
||||
s.end_headers
|
||||
return
|
||||
s.send_response(200)
|
||||
s.send_header("Content-Type", "text/xml; charset=utf-8")
|
||||
s.end_headers()
|
||||
s.wfile.write(RESPONSE_PREFIX_DIRECT_7MODE)
|
||||
s.wfile.write(RESPONSE_PREFIX_DIRECT)
|
||||
s.wfile.write(body)
|
||||
s.wfile.write(RESPONSE_SUFFIX_DIRECT)
|
||||
|
||||
|
||||
class FakeDirect7modeHTTPConnection(object):
|
||||
"""A fake httplib.HTTPConnection for netapp tests
|
||||
|
||||
Requests made via this connection actually get translated and routed into
|
||||
the fake direct handler above, we then turn the response into
|
||||
the httplib.HTTPResponse that the caller expects.
|
||||
"""
|
||||
def __init__(self, host, timeout=None):
|
||||
self.host = host
|
||||
|
||||
def request(self, method, path, data=None, headers=None):
|
||||
if not headers:
|
||||
headers = {}
|
||||
req_str = '%s %s HTTP/1.1\r\n' % (method, path)
|
||||
for key, value in headers.iteritems():
|
||||
req_str += "%s: %s\r\n" % (key, value)
|
||||
if data:
|
||||
req_str += '\r\n%s' % data
|
||||
|
||||
# NOTE(vish): normally the http transport normailizes from unicode
|
||||
sock = FakeHttplibSocket(req_str.decode("latin-1").encode("utf-8"))
|
||||
# NOTE(vish): stop the server from trying to look up address from
|
||||
# the fake socket
|
||||
FakeDirect7MODEServerHandler.address_string = lambda x: '127.0.0.1'
|
||||
self.app = FakeDirect7MODEServerHandler(sock, '127.0.0.1:80', None)
|
||||
|
||||
self.sock = FakeHttplibSocket(sock.result)
|
||||
self.http_response = httplib.HTTPResponse(self.sock)
|
||||
|
||||
def set_debuglevel(self, level):
|
||||
pass
|
||||
|
||||
def getresponse(self):
|
||||
self.http_response.begin()
|
||||
return self.http_response
|
||||
|
||||
def getresponsebody(self):
|
||||
return self.sock.result
|
||||
|
||||
|
||||
class NetAppDirect7modeISCSIDriverTestCase_NV(
|
||||
NetAppDirectCmodeISCSIDriverTestCase):
|
||||
"""Test case for NetAppISCSIDriver
|
||||
No vfiler
|
||||
"""
|
||||
def setUp(self):
|
||||
super(NetAppDirect7modeISCSIDriverTestCase_NV, self).setUp()
|
||||
|
||||
def _custom_setup(self):
|
||||
driver = iscsi.NetAppDirect7modeISCSIDriver()
|
||||
self.stubs.Set(httplib,
|
||||
'HTTPConnection', FakeDirect7modeHTTPConnection)
|
||||
driver._create_client(transport_type='http',
|
||||
login='admin', password='pass',
|
||||
hostname='127.0.0.1',
|
||||
port='80')
|
||||
driver.vfiler = None
|
||||
self.driver = driver
|
||||
|
||||
|
||||
class NetAppDirect7modeISCSIDriverTestCase_WV(
|
||||
NetAppDirectCmodeISCSIDriverTestCase):
|
||||
"""Test case for NetAppISCSIDriver
|
||||
With vfiler
|
||||
"""
|
||||
def setUp(self):
|
||||
super(NetAppDirect7modeISCSIDriverTestCase_WV, self).setUp()
|
||||
|
||||
def _custom_setup(self):
|
||||
driver = iscsi.NetAppDirect7modeISCSIDriver()
|
||||
self.stubs.Set(httplib, 'HTTPConnection',
|
||||
FakeDirect7modeHTTPConnection)
|
||||
driver._create_client(transport_type='http',
|
||||
login='admin', password='pass',
|
||||
hostname='127.0.0.1',
|
||||
port='80')
|
||||
driver.vfiler = 'vfiler'
|
||||
driver.client.set_api_version(1, 7)
|
||||
self.driver = driver
|
||||
|
||||
@@ -20,9 +20,11 @@ from cinder import context
|
||||
from cinder import exception
|
||||
from cinder import test
|
||||
|
||||
from cinder.volume.drivers import netapp
|
||||
from cinder.volume.drivers import netapp_nfs
|
||||
from cinder.volume.drivers.netapp import api
|
||||
from cinder.volume.drivers.netapp import iscsi
|
||||
from cinder.volume.drivers.netapp import nfs as netapp_nfs
|
||||
from cinder.volume.drivers import nfs
|
||||
from lxml import etree
|
||||
from mox import IgnoreArg
|
||||
from mox import IsA
|
||||
from mox import MockObject
|
||||
@@ -91,7 +93,7 @@ class NetappNfsDriverTestCase(test.TestCase):
|
||||
|
||||
# set required flags
|
||||
for flag in required_flags:
|
||||
setattr(netapp.FLAGS, flag, 'val')
|
||||
setattr(iscsi.FLAGS, flag, 'val')
|
||||
|
||||
mox.StubOutWithMock(nfs.NfsDriver, 'check_for_setup_error')
|
||||
nfs.NfsDriver.check_for_setup_error()
|
||||
@@ -103,7 +105,7 @@ class NetappNfsDriverTestCase(test.TestCase):
|
||||
|
||||
# restore initial FLAGS
|
||||
for flag in required_flags:
|
||||
delattr(netapp.FLAGS, flag)
|
||||
delattr(iscsi.FLAGS, flag)
|
||||
|
||||
def test_do_setup(self):
|
||||
mox = self._mox
|
||||
@@ -256,3 +258,416 @@ class NetappNfsDriverTestCase(test.TestCase):
|
||||
volume_name, clone_name, volume_id)
|
||||
|
||||
mox.VerifyAll()
|
||||
|
||||
def test_cloned_volume_size_fail(self):
|
||||
volume_clone_fail = FakeVolume(1)
|
||||
volume_src = FakeVolume(2)
|
||||
try:
|
||||
self._driver.create_cloned_volume(volume_clone_fail,
|
||||
volume_src)
|
||||
raise AssertionError()
|
||||
except exception.CinderException:
|
||||
pass
|
||||
|
||||
|
||||
class NetappCmodeNfsDriverTestCase(test.TestCase):
|
||||
"""Test case for NetApp C Mode specific NFS clone driver"""
|
||||
|
||||
def setUp(self):
|
||||
self._mox = mox.Mox()
|
||||
self._custom_setup()
|
||||
|
||||
def _custom_setup(self):
|
||||
self._driver = netapp_nfs.NetAppCmodeNfsDriver()
|
||||
|
||||
def tearDown(self):
|
||||
self._mox.UnsetStubs()
|
||||
|
||||
def test_check_for_setup_error(self):
|
||||
mox = self._mox
|
||||
drv = self._driver
|
||||
required_flags = [
|
||||
'netapp_wsdl_url',
|
||||
'netapp_login',
|
||||
'netapp_password',
|
||||
'netapp_server_hostname',
|
||||
'netapp_server_port']
|
||||
|
||||
# check exception raises when flags are not set
|
||||
self.assertRaises(exception.CinderException,
|
||||
drv.check_for_setup_error)
|
||||
|
||||
# set required flags
|
||||
for flag in required_flags:
|
||||
setattr(iscsi.FLAGS, flag, 'val')
|
||||
|
||||
mox.ReplayAll()
|
||||
|
||||
drv.check_for_setup_error()
|
||||
|
||||
mox.VerifyAll()
|
||||
|
||||
# restore initial FLAGS
|
||||
for flag in required_flags:
|
||||
delattr(iscsi.FLAGS, flag)
|
||||
|
||||
def test_do_setup(self):
|
||||
mox = self._mox
|
||||
drv = self._driver
|
||||
|
||||
mox.StubOutWithMock(drv, 'check_for_setup_error')
|
||||
mox.StubOutWithMock(netapp_nfs.NetAppCmodeNfsDriver, '_get_client')
|
||||
|
||||
drv.check_for_setup_error()
|
||||
netapp_nfs.NetAppCmodeNfsDriver._get_client()
|
||||
|
||||
mox.ReplayAll()
|
||||
|
||||
drv.do_setup(IsA(context.RequestContext))
|
||||
|
||||
mox.VerifyAll()
|
||||
|
||||
def test_create_snapshot(self):
|
||||
"""Test snapshot can be created and deleted"""
|
||||
mox = self._mox
|
||||
drv = self._driver
|
||||
|
||||
mox.StubOutWithMock(drv, '_clone_volume')
|
||||
drv._clone_volume(IgnoreArg(), IgnoreArg(), IgnoreArg())
|
||||
mox.ReplayAll()
|
||||
|
||||
drv.create_snapshot(FakeSnapshot())
|
||||
|
||||
mox.VerifyAll()
|
||||
|
||||
def test_create_volume_from_snapshot(self):
|
||||
"""Tests volume creation from snapshot"""
|
||||
drv = self._driver
|
||||
mox = self._mox
|
||||
volume = FakeVolume(1)
|
||||
snapshot = FakeSnapshot(2)
|
||||
|
||||
self.assertRaises(exception.CinderException,
|
||||
drv.create_volume_from_snapshot,
|
||||
volume,
|
||||
snapshot)
|
||||
|
||||
snapshot = FakeSnapshot(1)
|
||||
|
||||
location = '127.0.0.1:/nfs'
|
||||
expected_result = {'provider_location': location}
|
||||
mox.StubOutWithMock(drv, '_clone_volume')
|
||||
mox.StubOutWithMock(drv, '_get_volume_location')
|
||||
drv._clone_volume(IgnoreArg(), IgnoreArg(), IgnoreArg())
|
||||
drv._get_volume_location(IgnoreArg()).AndReturn(location)
|
||||
|
||||
mox.ReplayAll()
|
||||
|
||||
loc = drv.create_volume_from_snapshot(volume, snapshot)
|
||||
|
||||
self.assertEquals(loc, expected_result)
|
||||
|
||||
mox.VerifyAll()
|
||||
|
||||
def _prepare_delete_snapshot_mock(self, snapshot_exists):
|
||||
drv = self._driver
|
||||
mox = self._mox
|
||||
|
||||
mox.StubOutWithMock(drv, '_get_provider_location')
|
||||
mox.StubOutWithMock(drv, '_volume_not_present')
|
||||
|
||||
if snapshot_exists:
|
||||
mox.StubOutWithMock(drv, '_execute')
|
||||
mox.StubOutWithMock(drv, '_get_volume_path')
|
||||
|
||||
drv._get_provider_location(IgnoreArg())
|
||||
drv._volume_not_present(IgnoreArg(), IgnoreArg())\
|
||||
.AndReturn(not snapshot_exists)
|
||||
|
||||
if snapshot_exists:
|
||||
drv._get_volume_path(IgnoreArg(), IgnoreArg())
|
||||
drv._execute('rm', None, run_as_root=True)
|
||||
|
||||
mox.ReplayAll()
|
||||
|
||||
return mox
|
||||
|
||||
def test_delete_existing_snapshot(self):
|
||||
drv = self._driver
|
||||
mox = self._prepare_delete_snapshot_mock(True)
|
||||
|
||||
drv.delete_snapshot(FakeSnapshot())
|
||||
|
||||
mox.VerifyAll()
|
||||
|
||||
def test_delete_missing_snapshot(self):
|
||||
drv = self._driver
|
||||
mox = self._prepare_delete_snapshot_mock(False)
|
||||
|
||||
drv.delete_snapshot(FakeSnapshot())
|
||||
|
||||
mox.VerifyAll()
|
||||
|
||||
def _prepare_clone_mock(self, status):
|
||||
drv = self._driver
|
||||
mox = self._mox
|
||||
|
||||
volume = FakeVolume()
|
||||
setattr(volume, 'provider_location', '127.0.0.1:/nfs')
|
||||
|
||||
drv._client = MockObject(suds.client.Client)
|
||||
drv._client.factory = MockObject(suds.client.Factory)
|
||||
drv._client.service = MockObject(suds.client.ServiceSelector)
|
||||
# CloneNasFile method is generated by ServiceSelector at runtime from
|
||||
# the
|
||||
# XML, so mocking is impossible.
|
||||
setattr(drv._client.service,
|
||||
'CloneNasFile',
|
||||
types.MethodType(lambda *args, **kwargs: FakeResponce(status),
|
||||
suds.client.ServiceSelector))
|
||||
mox.StubOutWithMock(drv, '_get_host_ip')
|
||||
mox.StubOutWithMock(drv, '_get_export_path')
|
||||
|
||||
drv._get_host_ip(IgnoreArg()).AndReturn('127.0.0.1')
|
||||
drv._get_export_path(IgnoreArg()).AndReturn('/nfs')
|
||||
return mox
|
||||
|
||||
def test_clone_volume(self):
|
||||
drv = self._driver
|
||||
mox = self._prepare_clone_mock('passed')
|
||||
|
||||
mox.ReplayAll()
|
||||
|
||||
volume_name = 'volume_name'
|
||||
clone_name = 'clone_name'
|
||||
volume_id = volume_name + str(hash(volume_name))
|
||||
|
||||
drv._clone_volume(volume_name, clone_name, volume_id)
|
||||
|
||||
mox.VerifyAll()
|
||||
|
||||
def test_cloned_volume_size_fail(self):
|
||||
volume_clone_fail = FakeVolume(1)
|
||||
volume_src = FakeVolume(2)
|
||||
try:
|
||||
self._driver.create_cloned_volume(volume_clone_fail,
|
||||
volume_src)
|
||||
raise AssertionError()
|
||||
except exception.CinderException:
|
||||
pass
|
||||
|
||||
|
||||
class NetappDirectCmodeNfsDriverTestCase(NetappCmodeNfsDriverTestCase):
|
||||
"""Test direct NetApp C Mode driver"""
|
||||
def _custom_setup(self):
|
||||
self._driver = netapp_nfs.NetAppDirectCmodeNfsDriver()
|
||||
|
||||
def test_check_for_setup_error(self):
|
||||
mox = self._mox
|
||||
drv = self._driver
|
||||
required_flags = [
|
||||
'netapp_transport_type',
|
||||
'netapp_login',
|
||||
'netapp_password',
|
||||
'netapp_server_hostname',
|
||||
'netapp_server_port']
|
||||
|
||||
# check exception raises when flags are not set
|
||||
self.assertRaises(exception.CinderException,
|
||||
drv.check_for_setup_error)
|
||||
|
||||
# set required flags
|
||||
for flag in required_flags:
|
||||
setattr(iscsi.FLAGS, flag, 'val')
|
||||
|
||||
mox.ReplayAll()
|
||||
|
||||
drv.check_for_setup_error()
|
||||
|
||||
mox.VerifyAll()
|
||||
|
||||
# restore initial FLAGS
|
||||
for flag in required_flags:
|
||||
delattr(iscsi.FLAGS, flag)
|
||||
|
||||
def test_do_setup(self):
|
||||
mox = self._mox
|
||||
drv = self._driver
|
||||
|
||||
mox.StubOutWithMock(drv, 'check_for_setup_error')
|
||||
mox.StubOutWithMock(netapp_nfs.NetAppDirectCmodeNfsDriver,
|
||||
'_get_client')
|
||||
mox.StubOutWithMock(drv, '_do_custom_setup')
|
||||
|
||||
drv.check_for_setup_error()
|
||||
netapp_nfs.NetAppDirectNfsDriver._get_client()
|
||||
drv._do_custom_setup(IgnoreArg())
|
||||
|
||||
mox.ReplayAll()
|
||||
|
||||
drv.do_setup(IsA(context.RequestContext))
|
||||
|
||||
mox.VerifyAll()
|
||||
|
||||
def _prepare_clone_mock(self, status):
|
||||
drv = self._driver
|
||||
mox = self._mox
|
||||
|
||||
volume = FakeVolume()
|
||||
setattr(volume, 'provider_location', '127.0.0.1:/nfs')
|
||||
|
||||
mox.StubOutWithMock(drv, '_get_host_ip')
|
||||
mox.StubOutWithMock(drv, '_get_export_path')
|
||||
mox.StubOutWithMock(drv, '_get_if_info_by_ip')
|
||||
mox.StubOutWithMock(drv, '_get_vol_by_junc_vserver')
|
||||
mox.StubOutWithMock(drv, '_clone_file')
|
||||
|
||||
drv._get_host_ip(IgnoreArg()).AndReturn('127.0.0.1')
|
||||
drv._get_export_path(IgnoreArg()).AndReturn('/nfs')
|
||||
drv._get_if_info_by_ip('127.0.0.1').AndReturn(
|
||||
self._prepare_info_by_ip_response())
|
||||
drv._get_vol_by_junc_vserver('openstack', '/nfs').AndReturn('nfsvol')
|
||||
drv._clone_file('nfsvol', 'volume_name', 'clone_name',
|
||||
'openstack')
|
||||
return mox
|
||||
|
||||
def _prepare_info_by_ip_response(self):
|
||||
res = """<attributes-list>
|
||||
<net-interface-info>
|
||||
<address>127.0.0.1</address>
|
||||
<administrative-status>up</administrative-status>
|
||||
<current-node>fas3170rre-cmode-01</current-node>
|
||||
<current-port>e1b-1165</current-port>
|
||||
<data-protocols>
|
||||
<data-protocol>nfs</data-protocol>
|
||||
</data-protocols>
|
||||
<dns-domain-name>none</dns-domain-name>
|
||||
<failover-group/>
|
||||
<failover-policy>disabled</failover-policy>
|
||||
<firewall-policy>data</firewall-policy>
|
||||
<home-node>fas3170rre-cmode-01</home-node>
|
||||
<home-port>e1b-1165</home-port>
|
||||
<interface-name>nfs_data1</interface-name>
|
||||
<is-auto-revert>false</is-auto-revert>
|
||||
<is-home>true</is-home>
|
||||
<netmask>255.255.255.0</netmask>
|
||||
<netmask-length>24</netmask-length>
|
||||
<operational-status>up</operational-status>
|
||||
<role>data</role>
|
||||
<routing-group-name>c10.63.165.0/24</routing-group-name>
|
||||
<use-failover-group>disabled</use-failover-group>
|
||||
<vserver>openstack</vserver>
|
||||
</net-interface-info></attributes-list>"""
|
||||
response_el = etree.XML(res)
|
||||
return api.NaElement(response_el).get_children()
|
||||
|
||||
def test_clone_volume(self):
|
||||
drv = self._driver
|
||||
mox = self._prepare_clone_mock('pass')
|
||||
|
||||
mox.ReplayAll()
|
||||
|
||||
volume_name = 'volume_name'
|
||||
clone_name = 'clone_name'
|
||||
volume_id = volume_name + str(hash(volume_name))
|
||||
|
||||
drv._clone_volume(volume_name, clone_name, volume_id)
|
||||
|
||||
mox.VerifyAll()
|
||||
|
||||
|
||||
class NetappDirect7modeNfsDriverTestCase(NetappDirectCmodeNfsDriverTestCase):
|
||||
"""Test direct NetApp C Mode driver"""
|
||||
def _custom_setup(self):
|
||||
self._driver = netapp_nfs.NetAppDirect7modeNfsDriver()
|
||||
|
||||
def test_check_for_setup_error(self):
|
||||
mox = self._mox
|
||||
drv = self._driver
|
||||
required_flags = [
|
||||
'netapp_transport_type',
|
||||
'netapp_login',
|
||||
'netapp_password',
|
||||
'netapp_server_hostname',
|
||||
'netapp_server_port']
|
||||
|
||||
# check exception raises when flags are not set
|
||||
self.assertRaises(exception.CinderException,
|
||||
drv.check_for_setup_error)
|
||||
|
||||
# set required flags
|
||||
for flag in required_flags:
|
||||
setattr(iscsi.FLAGS, flag, 'val')
|
||||
|
||||
mox.ReplayAll()
|
||||
|
||||
drv.check_for_setup_error()
|
||||
|
||||
mox.VerifyAll()
|
||||
|
||||
# restore initial FLAGS
|
||||
for flag in required_flags:
|
||||
delattr(iscsi.FLAGS, flag)
|
||||
|
||||
def test_do_setup(self):
|
||||
mox = self._mox
|
||||
drv = self._driver
|
||||
|
||||
mox.StubOutWithMock(drv, 'check_for_setup_error')
|
||||
mox.StubOutWithMock(netapp_nfs.NetAppDirect7modeNfsDriver,
|
||||
'_get_client')
|
||||
mox.StubOutWithMock(drv, '_do_custom_setup')
|
||||
|
||||
drv.check_for_setup_error()
|
||||
netapp_nfs.NetAppDirectNfsDriver._get_client()
|
||||
drv._do_custom_setup(IgnoreArg())
|
||||
|
||||
mox.ReplayAll()
|
||||
|
||||
drv.do_setup(IsA(context.RequestContext))
|
||||
|
||||
mox.VerifyAll()
|
||||
|
||||
def _prepare_clone_mock(self, status):
|
||||
drv = self._driver
|
||||
mox = self._mox
|
||||
|
||||
volume = FakeVolume()
|
||||
setattr(volume, 'provider_location', '127.0.0.1:/nfs')
|
||||
|
||||
mox.StubOutWithMock(drv, '_get_export_path')
|
||||
mox.StubOutWithMock(drv, '_get_actual_path_for_export')
|
||||
mox.StubOutWithMock(drv, '_start_clone')
|
||||
mox.StubOutWithMock(drv, '_wait_for_clone_finish')
|
||||
if status == 'fail':
|
||||
mox.StubOutWithMock(drv, '_clear_clone')
|
||||
|
||||
drv._get_export_path(IgnoreArg()).AndReturn('/nfs')
|
||||
drv._get_actual_path_for_export(IgnoreArg()).AndReturn('/vol/vol1/nfs')
|
||||
drv._start_clone(IgnoreArg(), IgnoreArg()).AndReturn(('1', '2'))
|
||||
if status == 'fail':
|
||||
drv._wait_for_clone_finish('1', '2').AndRaise(
|
||||
api.NaApiError('error', 'error'))
|
||||
drv._clear_clone('1')
|
||||
else:
|
||||
drv._wait_for_clone_finish('1', '2')
|
||||
return mox
|
||||
|
||||
def test_clone_volume_clear(self):
|
||||
drv = self._driver
|
||||
mox = self._prepare_clone_mock('fail')
|
||||
|
||||
mox.ReplayAll()
|
||||
|
||||
volume_name = 'volume_name'
|
||||
clone_name = 'clone_name'
|
||||
volume_id = volume_name + str(hash(volume_name))
|
||||
try:
|
||||
drv._clone_volume(volume_name, clone_name, volume_id)
|
||||
except Exception as e:
|
||||
if isinstance(e, api.NaApiError):
|
||||
pass
|
||||
else:
|
||||
raise e
|
||||
|
||||
mox.VerifyAll()
|
||||
|
||||
0
cinder/volume/drivers/netapp/__init__.py
Normal file
0
cinder/volume/drivers/netapp/__init__.py
Normal file
398
cinder/volume/drivers/netapp/api.py
Normal file
398
cinder/volume/drivers/netapp/api.py
Normal file
@@ -0,0 +1,398 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright (c) 2012 NetApp, Inc.
|
||||
# Copyright (c) 2012 OpenStack LLC.
|
||||
# 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.
|
||||
"""
|
||||
NetApp api for ONTAP and OnCommand DFM.
|
||||
|
||||
Contains classes required to issue api calls to ONTAP and OnCommand DFM.
|
||||
"""
|
||||
|
||||
from lxml import etree
|
||||
import urllib2
|
||||
|
||||
from cinder.openstack.common import log as logging
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class NaServer(object):
|
||||
""" Encapsulates server connection logic"""
|
||||
|
||||
TRANSPORT_TYPE_HTTP = 'http'
|
||||
TRANSPORT_TYPE_HTTPS = 'https'
|
||||
SERVER_TYPE_FILER = 'filer'
|
||||
SERVER_TYPE_DFM = 'dfm'
|
||||
URL_FILER = 'servlets/netapp.servlets.admin.XMLrequest_filer'
|
||||
URL_DFM = 'apis/XMLrequest'
|
||||
NETAPP_NS = 'http://www.netapp.com/filer/admin'
|
||||
STYLE_LOGIN_PASSWORD = 'basic_auth'
|
||||
STYLE_CERTIFICATE = 'certificate_auth'
|
||||
|
||||
def __init__(self, host, server_type=SERVER_TYPE_FILER,
|
||||
transport_type=TRANSPORT_TYPE_HTTP,
|
||||
style=STYLE_LOGIN_PASSWORD, username=None,
|
||||
password=None):
|
||||
self._host = host
|
||||
self.set_server_type(server_type)
|
||||
self.set_transport_type(transport_type)
|
||||
self.set_style(style)
|
||||
self._username = username
|
||||
self._password = password
|
||||
self._refresh_conn = True
|
||||
|
||||
def get_transport_type(self):
|
||||
"""Get the transport type protocol."""
|
||||
return self._protocol
|
||||
|
||||
def set_transport_type(self, transport_type):
|
||||
"""Set the transport type protocol for api.
|
||||
Supports http and https transport types.
|
||||
"""
|
||||
if transport_type.lower() not in (
|
||||
NaServer.TRANSPORT_TYPE_HTTP,
|
||||
NaServer.TRANSPORT_TYPE_HTTPS):
|
||||
raise ValueError('Unsupported transport type')
|
||||
self._protocol = transport_type.lower()
|
||||
if self._protocol == NaServer.TRANSPORT_TYPE_HTTP:
|
||||
if self._server_type == NaServer.SERVER_TYPE_FILER:
|
||||
self.set_port(80)
|
||||
else:
|
||||
self.set_port(8088)
|
||||
else:
|
||||
if self._server_type == NaServer.SERVER_TYPE_FILER:
|
||||
self.set_port(443)
|
||||
else:
|
||||
self.set_port(8488)
|
||||
self._refresh_conn = True
|
||||
|
||||
def get_style(self):
|
||||
"""Get the authorization style for communicating with the server."""
|
||||
return self._auth_style
|
||||
|
||||
def set_style(self, style):
|
||||
"""Set the authorization style for communicating with the server.
|
||||
Supports basic_auth for now.
|
||||
Certificate_auth mode to be done.
|
||||
"""
|
||||
if style.lower() not in (NaServer.STYLE_LOGIN_PASSWORD,
|
||||
NaServer.STYLE_CERTIFICATE):
|
||||
raise ValueError('Unsupported authentication style')
|
||||
self._auth_style = style.lower()
|
||||
|
||||
def get_server_type(self):
|
||||
"""Get the target server type."""
|
||||
return self._server_type
|
||||
|
||||
def set_server_type(self, server_type):
|
||||
"""Set the target server type.
|
||||
Supports filer and dfm server types.
|
||||
"""
|
||||
if server_type.lower() not in (NaServer.SERVER_TYPE_FILER,
|
||||
NaServer.SERVER_TYPE_DFM):
|
||||
raise ValueError('Unsupported server type')
|
||||
self._server_type = server_type.lower()
|
||||
if self._server_type == NaServer.SERVER_TYPE_FILER:
|
||||
self._url = NaServer.URL_FILER
|
||||
else:
|
||||
self._url = NaServer.URL_DFM
|
||||
self._ns = NaServer.NETAPP_NS
|
||||
self._refresh_conn = True
|
||||
|
||||
def set_api_version(self, major, minor):
|
||||
"""Set the api version."""
|
||||
try:
|
||||
self._api_major_version = int(major)
|
||||
self._api_minor_version = int(minor)
|
||||
self._api_version = str(major) + "." + str(minor)
|
||||
except ValueError:
|
||||
raise ValueError('Major and minor versions must be integers')
|
||||
self._refresh_conn = True
|
||||
|
||||
def get_api_version(self):
|
||||
"""Gets the api version."""
|
||||
if hasattr(self, '_api_version'):
|
||||
return self._api_version
|
||||
return self._api_version
|
||||
|
||||
def set_port(self, port):
|
||||
"""Set the server communication port."""
|
||||
try:
|
||||
int(port)
|
||||
except ValueError:
|
||||
raise ValueError('Port must be integer')
|
||||
self._port = str(port)
|
||||
self._refresh_conn = True
|
||||
|
||||
def get_port(self):
|
||||
"""Get the server communication port."""
|
||||
return self._port
|
||||
|
||||
def set_timeout(self, seconds):
|
||||
"""Sets the timeout in seconds"""
|
||||
try:
|
||||
self._timeout = int(seconds)
|
||||
except ValueError:
|
||||
raise ValueError('timeout in seconds must be integer')
|
||||
|
||||
def get_timeout(self):
|
||||
"""Gets the timeout in seconds if set."""
|
||||
if hasattr(self, '_timeout'):
|
||||
return self._timeout
|
||||
return None
|
||||
|
||||
def get_vfiler(self):
|
||||
"""Get the vfiler tunneling."""
|
||||
return self._vfiler
|
||||
|
||||
def set_vfiler(self, vfiler):
|
||||
"""Set the vfiler tunneling."""
|
||||
self._vfiler = vfiler
|
||||
|
||||
def get_vserver(self):
|
||||
"""Get the vserver for tunneling."""
|
||||
return self._vserver
|
||||
|
||||
def set_vserver(self, vserver):
|
||||
"""Set the vserver for tunneling."""
|
||||
self._vserver = vserver
|
||||
|
||||
def set_username(self, username):
|
||||
"""Set the username for authentication."""
|
||||
self._username = username
|
||||
self._refresh_conn = True
|
||||
|
||||
def set_password(self, password):
|
||||
"""Set the password for authentication."""
|
||||
self._password = password
|
||||
self._refresh_conn = True
|
||||
|
||||
def invoke_elem(self, na_element):
|
||||
"""Invoke the api on the server."""
|
||||
if na_element and not isinstance(na_element, NaElement):
|
||||
ValueError('NaElement must be supplied to invoke api')
|
||||
request = self._create_request(na_element)
|
||||
if not hasattr(self, '_opener') or not self._opener \
|
||||
or self._refresh_conn:
|
||||
self._build_opener()
|
||||
try:
|
||||
if hasattr(self, '_timeout'):
|
||||
response = self._opener.open(request, timeout=self._timeout)
|
||||
else:
|
||||
response = self._opener.open(request)
|
||||
except urllib2.HTTPError as e:
|
||||
raise NaApiError(e.code, e.msg)
|
||||
except Exception as e:
|
||||
raise NaApiError('Unexpected error', e)
|
||||
xml = response.read()
|
||||
return self._get_result(xml)
|
||||
|
||||
def invoke_successfully(self, na_element):
|
||||
"""Invokes api and checks execution status as success."""
|
||||
result = self.invoke_elem(na_element)
|
||||
if result.has_attr('status') and result.get_attr('status') == 'passed':
|
||||
return result
|
||||
code = result.get_attr('errno')\
|
||||
or result.get_child_content('errorno')\
|
||||
or 'ESTATUSFAILED'
|
||||
msg = result.get_attr('reason')\
|
||||
or result.get_child_content('reason')\
|
||||
or 'Execution status is failed due to unknown reason'
|
||||
raise NaApiError(code, msg)
|
||||
|
||||
def _create_request(self, na_element):
|
||||
"""Creates request in the desired format."""
|
||||
netapp_elem = NaElement('netapp')
|
||||
netapp_elem.add_attr('xmlns', self._ns)
|
||||
if hasattr(self, '_api_version'):
|
||||
netapp_elem.add_attr('version', self._api_version)
|
||||
if hasattr(self, '_vfiler') and self._vfiler:
|
||||
if hasattr(self, '_api_major_version') and \
|
||||
hasattr(self, '_api_minor_version') and \
|
||||
self._api_major_version >= 1 and \
|
||||
self._api_minor_version >= 7:
|
||||
netapp_elem.add_attr('vfiler', self._vfiler)
|
||||
else:
|
||||
raise ValueError('ontapi version has to be atleast 1.7'
|
||||
' to send request to vfiler')
|
||||
if hasattr(self, '_vserver') and self._vserver:
|
||||
if hasattr(self, '_api_major_version') and \
|
||||
hasattr(self, '_api_minor_version') and \
|
||||
self._api_major_version >= 1 and \
|
||||
self._api_minor_version >= 15:
|
||||
netapp_elem.add_attr('vfiler', self._vserver)
|
||||
else:
|
||||
raise ValueError('ontapi version has to be atleast 1.15'
|
||||
' to send request to vserver')
|
||||
netapp_elem.add_child_elem(na_element)
|
||||
request_d = netapp_elem.to_string()
|
||||
request = urllib2.Request(
|
||||
self._get_url(), data=request_d,
|
||||
headers={'Content-Type': 'text/xml', 'charset': 'utf-8'})
|
||||
return request
|
||||
|
||||
def _parse_response(self, response):
|
||||
"""Get the NaElement for the response."""
|
||||
if not response:
|
||||
raise NaApiError('No response received')
|
||||
xml = etree.XML(response)
|
||||
return NaElement(xml)
|
||||
|
||||
def _get_result(self, response):
|
||||
"""Gets the call result."""
|
||||
processed_response = self._parse_response(response)
|
||||
return processed_response.get_child_by_name('results')
|
||||
|
||||
def _get_url(self):
|
||||
return '%s://%s:%s/%s' % (self._protocol, self._host, self._port,
|
||||
self._url)
|
||||
|
||||
def _build_opener(self):
|
||||
if self._auth_style == NaServer.STYLE_LOGIN_PASSWORD:
|
||||
auth_handler = self._create_basic_auth_handler()
|
||||
else:
|
||||
auth_handler = self._create_certificate_auth_handler()
|
||||
opener = urllib2.build_opener(auth_handler)
|
||||
self._opener = opener
|
||||
|
||||
def _create_basic_auth_handler(self):
|
||||
password_man = urllib2.HTTPPasswordMgrWithDefaultRealm()
|
||||
password_man.add_password(None, self._get_url(), self._username,
|
||||
self._password)
|
||||
auth_handler = urllib2.HTTPBasicAuthHandler(password_man)
|
||||
return auth_handler
|
||||
|
||||
def _create_certificate_auth_handler(self):
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
class NaElement(object):
|
||||
"""Class wraps basic building block for NetApp api request."""
|
||||
|
||||
def __init__(self, name):
|
||||
"""Name of the element or etree.Element."""
|
||||
if isinstance(name, etree._Element):
|
||||
self._element = name
|
||||
else:
|
||||
self._element = etree.Element(name)
|
||||
|
||||
def get_name(self):
|
||||
"""Returns the tag name of the element."""
|
||||
return self._element.tag
|
||||
|
||||
def set_content(self, text):
|
||||
"""Set the text for the element."""
|
||||
self._element.text = text
|
||||
|
||||
def get_content(self):
|
||||
"""Get the text for the element."""
|
||||
return self._element.text
|
||||
|
||||
def add_attr(self, name, value):
|
||||
"""Add the attribute to the element."""
|
||||
self._element.set(name, value)
|
||||
|
||||
def add_attrs(self, **attrs):
|
||||
"""Add multiple attributes to the element."""
|
||||
for attr in attrs.keys():
|
||||
self._element.set(attr, attrs.get(attr))
|
||||
|
||||
def add_child_elem(self, na_element):
|
||||
"""Add the child element to the element."""
|
||||
if isinstance(na_element, NaElement):
|
||||
self._element.append(na_element._element)
|
||||
return
|
||||
raise
|
||||
|
||||
def get_child_by_name(self, name):
|
||||
"""Get the child element by the tag name."""
|
||||
for child in self._element.iterchildren():
|
||||
if child.tag == name or etree.QName(child.tag).localname == name:
|
||||
return NaElement(child)
|
||||
return None
|
||||
|
||||
def get_child_content(self, name):
|
||||
"""Get the content of the child."""
|
||||
for child in self._element.iterchildren():
|
||||
if child.tag == name or etree.QName(child.tag).localname == name:
|
||||
return child.text
|
||||
return None
|
||||
|
||||
def get_children(self):
|
||||
"""Get the children for the element."""
|
||||
return [NaElement(el) for el in self._element.iterchildren()]
|
||||
|
||||
def has_attr(self, name):
|
||||
"""Checks whether element has attribute."""
|
||||
attributes = self._element.attrib or {}
|
||||
return name in attributes.keys()
|
||||
|
||||
def get_attr(self, name):
|
||||
"""Get the attribute with the given name."""
|
||||
attributes = self._element.attrib or {}
|
||||
return attributes.get(name)
|
||||
|
||||
def get_attr_names(self):
|
||||
"""Returns the list of attribute names."""
|
||||
attributes = self._element.attrib or {}
|
||||
return attributes.keys()
|
||||
|
||||
def add_new_child(self, name, content, convert=False):
|
||||
"""Add child with tag name and context.
|
||||
Convert replaces entity refs to chars.
|
||||
"""
|
||||
child = NaElement(name)
|
||||
if convert:
|
||||
content = NaElement._convert_entity_refs(content)
|
||||
child.set_content(content)
|
||||
self.add_child_elem(child)
|
||||
|
||||
@staticmethod
|
||||
def _convert_entity_refs(text):
|
||||
"""Converts entity refs to chars
|
||||
neccessary to handle etree auto conversions.
|
||||
"""
|
||||
text = text.replace("<", "<")
|
||||
text = text.replace(">", ">")
|
||||
return text
|
||||
|
||||
@staticmethod
|
||||
def create_node_with_children(node, **children):
|
||||
"""Creates and returns named node with children."""
|
||||
parent = NaElement(node)
|
||||
for child in children.keys():
|
||||
parent.add_new_child(child, children.get(child, None))
|
||||
return parent
|
||||
|
||||
def add_node_with_children(self, node, **children):
|
||||
"""Creates named node with children."""
|
||||
parent = NaElement.create_node_with_children(node, **children)
|
||||
self.add_child_elem(parent)
|
||||
|
||||
def to_string(self, pretty=False, method='xml', encoding='UTF-8'):
|
||||
"""Prints the element to string"""
|
||||
return etree.tostring(self._element, method=method, encoding=encoding,
|
||||
pretty_print=pretty)
|
||||
|
||||
|
||||
class NaApiError(Exception):
|
||||
"""Base exception class for NetApp api errors."""
|
||||
def __init__(self, code='unknown', message='unknown'):
|
||||
self.code = code
|
||||
self.message = message
|
||||
|
||||
def __str__(self, *args, **kwargs):
|
||||
return 'NetApp api failed. Reason - %s:%s' % (self.code, self.message)
|
||||
File diff suppressed because it is too large
Load Diff
680
cinder/volume/drivers/netapp/nfs.py
Normal file
680
cinder/volume/drivers/netapp/nfs.py
Normal file
@@ -0,0 +1,680 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright (c) 2012 NetApp, 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.
|
||||
"""
|
||||
Volume driver for NetApp NFS storage.
|
||||
"""
|
||||
|
||||
import os
|
||||
import suds
|
||||
from suds.sax import text
|
||||
import time
|
||||
|
||||
from cinder import exception
|
||||
from cinder import flags
|
||||
from cinder.openstack.common import cfg
|
||||
from cinder.openstack.common import log as logging
|
||||
from cinder.volume.drivers.netapp.api import NaApiError
|
||||
from cinder.volume.drivers.netapp.api import NaElement
|
||||
from cinder.volume.drivers.netapp.api import NaServer
|
||||
from cinder.volume.drivers.netapp.iscsi import netapp_opts
|
||||
from cinder.volume.drivers import nfs
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
netapp_nfs_opts = [
|
||||
cfg.IntOpt('synchronous_snapshot_create',
|
||||
default=0,
|
||||
help='Does snapshot creation call returns immediately')]
|
||||
|
||||
FLAGS = flags.FLAGS
|
||||
FLAGS.register_opts(netapp_opts)
|
||||
FLAGS.register_opts(netapp_nfs_opts)
|
||||
|
||||
|
||||
class NetAppNFSDriver(nfs.NfsDriver):
|
||||
"""Executes commands relating to Volumes."""
|
||||
def __init__(self, *args, **kwargs):
|
||||
# NOTE(vish): db is set by Manager
|
||||
self._execute = None
|
||||
self._context = None
|
||||
super(NetAppNFSDriver, self).__init__(*args, **kwargs)
|
||||
|
||||
def set_execute(self, execute):
|
||||
self._execute = execute
|
||||
|
||||
def do_setup(self, context):
|
||||
self._context = context
|
||||
self.check_for_setup_error()
|
||||
self._client = NetAppNFSDriver._get_client()
|
||||
|
||||
def check_for_setup_error(self):
|
||||
"""Returns an error if prerequisites aren't met"""
|
||||
NetAppNFSDriver._check_dfm_flags()
|
||||
super(NetAppNFSDriver, self).check_for_setup_error()
|
||||
|
||||
def create_volume_from_snapshot(self, volume, snapshot):
|
||||
"""Creates a volume from a snapshot."""
|
||||
vol_size = volume.size
|
||||
snap_size = snapshot.volume_size
|
||||
|
||||
if vol_size != snap_size:
|
||||
msg = _('Cannot create volume of size %(vol_size)s from '
|
||||
'snapshot of size %(snap_size)s')
|
||||
raise exception.CinderException(msg % locals())
|
||||
|
||||
self._clone_volume(snapshot.name, volume.name, snapshot.volume_id)
|
||||
share = self._get_volume_location(snapshot.volume_id)
|
||||
|
||||
return {'provider_location': share}
|
||||
|
||||
def create_snapshot(self, snapshot):
|
||||
"""Creates a snapshot."""
|
||||
self._clone_volume(snapshot['volume_name'],
|
||||
snapshot['name'],
|
||||
snapshot['volume_id'])
|
||||
|
||||
def delete_snapshot(self, snapshot):
|
||||
"""Deletes a snapshot."""
|
||||
nfs_mount = self._get_provider_location(snapshot.volume_id)
|
||||
|
||||
if self._volume_not_present(nfs_mount, snapshot.name):
|
||||
return True
|
||||
|
||||
self._execute('rm', self._get_volume_path(nfs_mount, snapshot.name),
|
||||
run_as_root=True)
|
||||
|
||||
@staticmethod
|
||||
def _check_dfm_flags():
|
||||
"""Raises error if any required configuration flag for OnCommand proxy
|
||||
is missing."""
|
||||
required_flags = ['netapp_wsdl_url',
|
||||
'netapp_login',
|
||||
'netapp_password',
|
||||
'netapp_server_hostname',
|
||||
'netapp_server_port']
|
||||
for flag in required_flags:
|
||||
if not getattr(FLAGS, flag, None):
|
||||
raise exception.CinderException(_('%s is not set') % flag)
|
||||
|
||||
@staticmethod
|
||||
def _get_client():
|
||||
"""Creates SOAP _client for ONTAP-7 DataFabric Service."""
|
||||
client = suds.client.Client(FLAGS.netapp_wsdl_url,
|
||||
username=FLAGS.netapp_login,
|
||||
password=FLAGS.netapp_password)
|
||||
soap_url = 'http://%s:%s/apis/soap/v1' % (FLAGS.netapp_server_hostname,
|
||||
FLAGS.netapp_server_port)
|
||||
client.set_options(location=soap_url)
|
||||
|
||||
return client
|
||||
|
||||
def _get_volume_location(self, volume_id):
|
||||
"""Returns NFS mount address as <nfs_ip_address>:<nfs_mount_dir>"""
|
||||
nfs_server_ip = self._get_host_ip(volume_id)
|
||||
export_path = self._get_export_path(volume_id)
|
||||
return (nfs_server_ip + ':' + export_path)
|
||||
|
||||
def _clone_volume(self, volume_name, clone_name, volume_id):
|
||||
"""Clones mounted volume with OnCommand proxy API"""
|
||||
host_id = self._get_host_id(volume_id)
|
||||
export_path = self._get_full_export_path(volume_id, host_id)
|
||||
|
||||
request = self._client.factory.create('Request')
|
||||
request.Name = 'clone-start'
|
||||
|
||||
clone_start_args = ('<source-path>%s/%s</source-path>'
|
||||
'<destination-path>%s/%s</destination-path>')
|
||||
|
||||
request.Args = text.Raw(clone_start_args % (export_path,
|
||||
volume_name,
|
||||
export_path,
|
||||
clone_name))
|
||||
|
||||
resp = self._client.service.ApiProxy(Target=host_id,
|
||||
Request=request)
|
||||
|
||||
if resp.Status == 'passed' and FLAGS.synchronous_snapshot_create:
|
||||
clone_id = resp.Results['clone-id'][0]
|
||||
clone_id_info = clone_id['clone-id-info'][0]
|
||||
clone_operation_id = int(clone_id_info['clone-op-id'][0])
|
||||
|
||||
self._wait_for_clone_finished(clone_operation_id, host_id)
|
||||
elif resp.Status == 'failed':
|
||||
raise exception.CinderException(resp.Reason)
|
||||
|
||||
def _wait_for_clone_finished(self, clone_operation_id, host_id):
|
||||
"""
|
||||
Polls ONTAP7 for clone status. Returns once clone is finished.
|
||||
:param clone_operation_id: Identifier of ONTAP clone operation
|
||||
"""
|
||||
clone_list_options = ('<clone-id>'
|
||||
'<clone-id-info>'
|
||||
'<clone-op-id>%d</clone-op-id>'
|
||||
'<volume-uuid></volume-uuid>'
|
||||
'</clone-id>'
|
||||
'</clone-id-info>')
|
||||
|
||||
request = self._client.factory.create('Request')
|
||||
request.Name = 'clone-list-status'
|
||||
request.Args = text.Raw(clone_list_options % clone_operation_id)
|
||||
|
||||
resp = self._client.service.ApiProxy(Target=host_id, Request=request)
|
||||
|
||||
while resp.Status != 'passed':
|
||||
time.sleep(1)
|
||||
resp = self._client.service.ApiProxy(Target=host_id,
|
||||
Request=request)
|
||||
|
||||
def _get_provider_location(self, volume_id):
|
||||
"""
|
||||
Returns provider location for given volume
|
||||
:param volume_id:
|
||||
"""
|
||||
volume = self.db.volume_get(self._context, volume_id)
|
||||
return volume.provider_location
|
||||
|
||||
def _get_host_ip(self, volume_id):
|
||||
"""Returns IP address for the given volume"""
|
||||
return self._get_provider_location(volume_id).split(':')[0]
|
||||
|
||||
def _get_export_path(self, volume_id):
|
||||
"""Returns NFS export path for the given volume"""
|
||||
return self._get_provider_location(volume_id).split(':')[1]
|
||||
|
||||
def _get_host_id(self, volume_id):
|
||||
"""Returns ID of the ONTAP-7 host"""
|
||||
host_ip = self._get_host_ip(volume_id)
|
||||
server = self._client.service
|
||||
|
||||
resp = server.HostListInfoIterStart(ObjectNameOrId=host_ip)
|
||||
tag = resp.Tag
|
||||
|
||||
try:
|
||||
res = server.HostListInfoIterNext(Tag=tag, Maximum=1)
|
||||
if hasattr(res, 'Hosts') and res.Hosts.HostInfo:
|
||||
return res.Hosts.HostInfo[0].HostId
|
||||
finally:
|
||||
server.HostListInfoIterEnd(Tag=tag)
|
||||
|
||||
def _get_full_export_path(self, volume_id, host_id):
|
||||
"""Returns full path to the NFS share, e.g. /vol/vol0/home"""
|
||||
export_path = self._get_export_path(volume_id)
|
||||
command_args = '<pathname>%s</pathname>'
|
||||
|
||||
request = self._client.factory.create('Request')
|
||||
request.Name = 'nfs-exportfs-storage-path'
|
||||
request.Args = text.Raw(command_args % export_path)
|
||||
|
||||
resp = self._client.service.ApiProxy(Target=host_id,
|
||||
Request=request)
|
||||
|
||||
if resp.Status == 'passed':
|
||||
return resp.Results['actual-pathname'][0]
|
||||
elif resp.Status == 'failed':
|
||||
raise exception.CinderException(resp.Reason)
|
||||
|
||||
def _volume_not_present(self, nfs_mount, volume_name):
|
||||
"""
|
||||
Check if volume exists
|
||||
"""
|
||||
try:
|
||||
self._try_execute('ls', self._get_volume_path(nfs_mount,
|
||||
volume_name))
|
||||
except exception.ProcessExecutionError:
|
||||
# If the volume isn't present
|
||||
return True
|
||||
return False
|
||||
|
||||
def _try_execute(self, *command, **kwargs):
|
||||
# NOTE(vish): Volume commands can partially fail due to timing, but
|
||||
# running them a second time on failure will usually
|
||||
# recover nicely.
|
||||
tries = 0
|
||||
while True:
|
||||
try:
|
||||
self._execute(*command, **kwargs)
|
||||
return True
|
||||
except exception.ProcessExecutionError:
|
||||
tries = tries + 1
|
||||
if tries >= FLAGS.num_shell_tries:
|
||||
raise
|
||||
LOG.exception(_("Recovering from a failed execute. "
|
||||
"Try number %s"), tries)
|
||||
time.sleep(tries ** 2)
|
||||
|
||||
def _get_volume_path(self, nfs_share, volume_name):
|
||||
"""Get volume path (local fs path) for given volume name on given nfs
|
||||
share
|
||||
@param nfs_share string, example 172.18.194.100:/var/nfs
|
||||
@param volume_name string,
|
||||
example volume-91ee65ec-c473-4391-8c09-162b00c68a8c
|
||||
"""
|
||||
return os.path.join(self._get_mount_point_for_share(nfs_share),
|
||||
volume_name)
|
||||
|
||||
def create_cloned_volume(self, volume, src_vref):
|
||||
"""Creates a clone of the specified volume."""
|
||||
vol_size = volume.size
|
||||
src_vol_size = src_vref.size
|
||||
|
||||
if vol_size != src_vol_size:
|
||||
msg = _('Cannot create clone of size %(vol_size)s from '
|
||||
'volume of size %(src_vol_size)s')
|
||||
raise exception.CinderException(msg % locals())
|
||||
|
||||
self._clone_volume(src_vref.name, volume.name, src_vref.id)
|
||||
share = self._get_volume_location(src_vref.id)
|
||||
|
||||
return {'provider_location': share}
|
||||
|
||||
def get_volume_stats(self, refresh=False):
|
||||
"""Get volume status.
|
||||
|
||||
If 'refresh' is True, run update the stats first."""
|
||||
if refresh:
|
||||
self._update_volume_status()
|
||||
|
||||
return self._stats
|
||||
|
||||
def _update_volume_status(self):
|
||||
"""Retrieve status info from volume group."""
|
||||
|
||||
LOG.debug(_("Updating volume status"))
|
||||
data = {}
|
||||
data["volume_backend_name"] = 'NetApp_NFS_7mode'
|
||||
data["vendor_name"] = 'NetApp'
|
||||
data["driver_version"] = '1.0'
|
||||
data["storage_protocol"] = 'NFS'
|
||||
|
||||
data['total_capacity_gb'] = 'infinite'
|
||||
data['free_capacity_gb'] = 'infinite'
|
||||
data['reserved_percentage'] = 100
|
||||
data['QoS_support'] = False
|
||||
self._stats = data
|
||||
|
||||
|
||||
class NetAppCmodeNfsDriver (NetAppNFSDriver):
|
||||
"""Executes commands related to volumes on c mode"""
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(NetAppCmodeNfsDriver, self).__init__(*args, **kwargs)
|
||||
|
||||
def do_setup(self, context):
|
||||
self._context = context
|
||||
self.check_for_setup_error()
|
||||
self._client = NetAppCmodeNfsDriver._get_client()
|
||||
|
||||
def check_for_setup_error(self):
|
||||
"""Returns an error if prerequisites aren't met"""
|
||||
NetAppCmodeNfsDriver._check_flags()
|
||||
|
||||
def _clone_volume(self, volume_name, clone_name, volume_id):
|
||||
"""Clones mounted volume with NetApp Cloud Services"""
|
||||
host_ip = self._get_host_ip(volume_id)
|
||||
export_path = self._get_export_path(volume_id)
|
||||
LOG.debug(_("""Cloning with params ip %(host_ip)s, exp_path
|
||||
%(export_path)s, vol %(volume_name)s,
|
||||
clone_name %(clone_name)s""") % locals())
|
||||
self._client.service.CloneNasFile(host_ip, export_path,
|
||||
volume_name, clone_name)
|
||||
|
||||
@staticmethod
|
||||
def _check_flags():
|
||||
"""Raises error if any required configuration flag for NetApp Cloud
|
||||
Webservices is missing."""
|
||||
required_flags = ['netapp_wsdl_url',
|
||||
'netapp_login',
|
||||
'netapp_password',
|
||||
'netapp_server_hostname',
|
||||
'netapp_server_port']
|
||||
for flag in required_flags:
|
||||
if not getattr(FLAGS, flag, None):
|
||||
raise exception.CinderException(_('%s is not set') % flag)
|
||||
|
||||
@staticmethod
|
||||
def _get_client():
|
||||
"""Creates SOAP _client for NetApp Cloud service."""
|
||||
client = suds.client.Client(FLAGS.netapp_wsdl_url,
|
||||
username=FLAGS.netapp_login,
|
||||
password=FLAGS.netapp_password)
|
||||
return client
|
||||
|
||||
def get_volume_stats(self, refresh=False):
|
||||
"""Get volume status.
|
||||
|
||||
If 'refresh' is True, run update the stats first."""
|
||||
if refresh:
|
||||
self._update_volume_status()
|
||||
|
||||
return self._stats
|
||||
|
||||
def _update_volume_status(self):
|
||||
"""Retrieve status info from volume group."""
|
||||
|
||||
LOG.debug(_("Updating volume status"))
|
||||
data = {}
|
||||
data["volume_backend_name"] = 'NetApp_NFS_Cluster'
|
||||
data["vendor_name"] = 'NetApp'
|
||||
data["driver_version"] = '1.0'
|
||||
data["storage_protocol"] = 'NFS'
|
||||
|
||||
data['total_capacity_gb'] = 'infinite'
|
||||
data['free_capacity_gb'] = 'infinite'
|
||||
data['reserved_percentage'] = 100
|
||||
data['QoS_support'] = False
|
||||
self._stats = data
|
||||
|
||||
|
||||
class NetAppDirectNfsDriver (NetAppNFSDriver):
|
||||
"""Executes commands related to volumes on NetApp filer"""
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(NetAppDirectNfsDriver, self).__init__(*args, **kwargs)
|
||||
|
||||
def do_setup(self, context):
|
||||
self._context = context
|
||||
self.check_for_setup_error()
|
||||
self._client = NetAppDirectNfsDriver._get_client()
|
||||
self._do_custom_setup(self._client)
|
||||
|
||||
def check_for_setup_error(self):
|
||||
"""Returns an error if prerequisites aren't met"""
|
||||
NetAppDirectNfsDriver._check_flags()
|
||||
|
||||
def _clone_volume(self, volume_name, clone_name, volume_id):
|
||||
"""Clones mounted volume on NetApp filer"""
|
||||
raise NotImplementedError()
|
||||
|
||||
@staticmethod
|
||||
def _check_flags():
|
||||
"""Raises error if any required configuration flag for NetApp
|
||||
filer is missing."""
|
||||
required_flags = ['netapp_login',
|
||||
'netapp_password',
|
||||
'netapp_server_hostname',
|
||||
'netapp_server_port',
|
||||
'netapp_transport_type']
|
||||
for flag in required_flags:
|
||||
if not getattr(FLAGS, flag, None):
|
||||
raise exception.CinderException(_('%s is not set') % flag)
|
||||
|
||||
@staticmethod
|
||||
def _get_client():
|
||||
"""Creates NetApp api client."""
|
||||
client = NaServer(host=FLAGS.netapp_server_hostname,
|
||||
server_type=NaServer.SERVER_TYPE_FILER,
|
||||
transport_type=FLAGS.netapp_transport_type,
|
||||
style=NaServer.STYLE_LOGIN_PASSWORD,
|
||||
username=FLAGS.netapp_login,
|
||||
password=FLAGS.netapp_password)
|
||||
return client
|
||||
|
||||
def _do_custom_setup(self, client):
|
||||
"""Do the customized set up on client if any for different types"""
|
||||
raise NotImplementedError()
|
||||
|
||||
def _is_naelement(self, elem):
|
||||
"""Checks if element is NetApp element"""
|
||||
if not isinstance(elem, NaElement):
|
||||
raise ValueError('Expects NaElement')
|
||||
|
||||
def _invoke_successfully(self, na_element, vserver=None):
|
||||
"""Invoke the api for successful result.
|
||||
Vserver implies vserver api else filer/Cluster api.
|
||||
"""
|
||||
self._is_naelement(na_element)
|
||||
if vserver:
|
||||
self._client.set_vserver(vserver)
|
||||
else:
|
||||
self._client.set_vserver(None)
|
||||
result = self._client.invoke_successfully(na_element)
|
||||
return result
|
||||
|
||||
def _get_ontapi_version(self):
|
||||
"""Gets the supported ontapi version."""
|
||||
ontapi_version = NaElement('system-get-ontapi-version')
|
||||
res = self._invoke_successfully(ontapi_version, False)
|
||||
major = res.get_child_content('major-version')
|
||||
minor = res.get_child_content('minor-version')
|
||||
return (major, minor)
|
||||
|
||||
|
||||
class NetAppDirectCmodeNfsDriver (NetAppDirectNfsDriver):
|
||||
"""Executes commands related to volumes on c mode"""
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(NetAppDirectCmodeNfsDriver, self).__init__(*args, **kwargs)
|
||||
|
||||
def _do_custom_setup(self, client):
|
||||
"""Do the customized set up on client for cluster mode"""
|
||||
# Default values to run first api
|
||||
client.set_api_version(1, 15)
|
||||
(major, minor) = self._get_ontapi_version()
|
||||
client.set_api_version(major, minor)
|
||||
|
||||
def _clone_volume(self, volume_name, clone_name, volume_id):
|
||||
"""Clones mounted volume on NetApp Cluster"""
|
||||
host_ip = self._get_host_ip(volume_id)
|
||||
export_path = self._get_export_path(volume_id)
|
||||
ifs = self._get_if_info_by_ip(host_ip)
|
||||
vserver = ifs[0].get_child_content('vserver')
|
||||
exp_volume = self._get_vol_by_junc_vserver(vserver, export_path)
|
||||
self._clone_file(exp_volume, volume_name, clone_name, vserver)
|
||||
|
||||
def _get_if_info_by_ip(self, ip):
|
||||
"""Gets the network interface info by ip."""
|
||||
net_if_iter = NaElement('net-interface-get-iter')
|
||||
net_if_iter.add_new_child('max-records', '10')
|
||||
query = NaElement('query')
|
||||
net_if_iter.add_child_elem(query)
|
||||
query.add_node_with_children('net-interface-info', **{'address': ip})
|
||||
result = self._invoke_successfully(net_if_iter)
|
||||
if result.get_child_content('num-records') and\
|
||||
int(result.get_child_content('num-records')) >= 1:
|
||||
attr_list = result.get_child_by_name('attributes-list')
|
||||
return attr_list.get_children()
|
||||
raise exception.NotFound(
|
||||
_('No interface found on cluster for ip %s')
|
||||
% (ip))
|
||||
|
||||
def _get_vol_by_junc_vserver(self, vserver, junction):
|
||||
"""Gets the volume by junction path and vserver"""
|
||||
vol_iter = NaElement('volume-get-iter')
|
||||
vol_iter.add_new_child('max-records', '10')
|
||||
query = NaElement('query')
|
||||
vol_iter.add_child_elem(query)
|
||||
vol_attrs = NaElement('volume-attributes')
|
||||
query.add_child_elem(vol_attrs)
|
||||
vol_attrs.add_node_with_children(
|
||||
'volume-id-attributes',
|
||||
**{'junction-path': junction,
|
||||
'owning-vserver-name': vserver})
|
||||
des_attrs = NaElement('desired-attributes')
|
||||
des_attrs.add_node_with_children('volume-attributes',
|
||||
**{'volume-id-attributes': None})
|
||||
vol_iter.add_child_elem(des_attrs)
|
||||
result = self._invoke_successfully(vol_iter, vserver)
|
||||
if result.get_child_content('num-records') and\
|
||||
int(result.get_child_content('num-records')) >= 1:
|
||||
attr_list = result.get_child_by_name('attributes-list')
|
||||
vols = attr_list.get_children()
|
||||
vol_id = vols[0].get_child_by_name('volume-id-attributes')
|
||||
return vol_id.get_child_content('name')
|
||||
raise exception.NotFound(_("""No volume on cluster with vserver
|
||||
%(vserver)s and junction path %(junction)s
|
||||
""") % locals())
|
||||
|
||||
def _clone_file(self, volume, src_path, dest_path, vserver=None):
|
||||
"""Clones file on vserver"""
|
||||
LOG.debug(_("""Cloning with params volume %(volume)s,src %(src_path)s,
|
||||
dest %(dest_path)s, vserver %(vserver)s""")
|
||||
% locals())
|
||||
clone_create = NaElement.create_node_with_children(
|
||||
'clone-create',
|
||||
**{'volume': volume, 'source-path': src_path,
|
||||
'destination-path': dest_path})
|
||||
self._invoke_successfully(clone_create, vserver)
|
||||
|
||||
def get_volume_stats(self, refresh=False):
|
||||
"""Get volume status.
|
||||
|
||||
If 'refresh' is True, run update the stats first."""
|
||||
if refresh:
|
||||
self._update_volume_status()
|
||||
|
||||
return self._stats
|
||||
|
||||
def _update_volume_status(self):
|
||||
"""Retrieve status info from volume group."""
|
||||
|
||||
LOG.debug(_("Updating volume status"))
|
||||
data = {}
|
||||
data["volume_backend_name"] = 'NetApp_NFS_cluster_direct'
|
||||
data["vendor_name"] = 'NetApp'
|
||||
data["driver_version"] = '1.0'
|
||||
data["storage_protocol"] = 'NFS'
|
||||
|
||||
data['total_capacity_gb'] = 'infinite'
|
||||
data['free_capacity_gb'] = 'infinite'
|
||||
data['reserved_percentage'] = 100
|
||||
data['QoS_support'] = False
|
||||
self._stats = data
|
||||
|
||||
|
||||
class NetAppDirect7modeNfsDriver (NetAppDirectNfsDriver):
|
||||
"""Executes commands related to volumes on 7 mode"""
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(NetAppDirect7modeNfsDriver, self).__init__(*args, **kwargs)
|
||||
|
||||
def _do_custom_setup(self, client):
|
||||
"""Do the customized set up on client if any for 7 mode"""
|
||||
(major, minor) = self._get_ontapi_version()
|
||||
client.set_api_version(major, minor)
|
||||
|
||||
def _clone_volume(self, volume_name, clone_name, volume_id):
|
||||
"""Clones mounted volume with NetApp filer"""
|
||||
export_path = self._get_export_path(volume_id)
|
||||
storage_path = self._get_actual_path_for_export(export_path)
|
||||
target_path = '%s/%s' % (storage_path, clone_name)
|
||||
(clone_id, vol_uuid) = self._start_clone('%s/%s' % (storage_path,
|
||||
volume_name),
|
||||
target_path)
|
||||
if vol_uuid:
|
||||
try:
|
||||
self._wait_for_clone_finish(clone_id, vol_uuid)
|
||||
except NaApiError as e:
|
||||
if e.code != 'UnknownCloneId':
|
||||
self._clear_clone(clone_id)
|
||||
raise e
|
||||
|
||||
def _get_actual_path_for_export(self, export_path):
|
||||
"""Gets the actual path on the filer for export path"""
|
||||
storage_path = NaElement.create_node_with_children(
|
||||
'nfs-exportfs-storage-path', **{'pathname': export_path})
|
||||
result = self._invoke_successfully(storage_path, None)
|
||||
if result.get_child_content('actual-pathname'):
|
||||
return result.get_child_content('actual-pathname')
|
||||
raise exception.NotFound(_('No storage path found for export path %s')
|
||||
% (export_path))
|
||||
|
||||
def _start_clone(self, src_path, dest_path):
|
||||
"""Starts the clone operation.
|
||||
Returns the clone-id
|
||||
"""
|
||||
LOG.debug(_("""Cloning with src %(src_path)s, dest %(dest_path)s""")
|
||||
% locals())
|
||||
clone_start = NaElement.create_node_with_children(
|
||||
'clone-start',
|
||||
**{'source-path': src_path,
|
||||
'destination-path': dest_path,
|
||||
'no-snap': 'true'})
|
||||
result = self._invoke_successfully(clone_start, None)
|
||||
clone_id_el = result.get_child_by_name('clone-id')
|
||||
cl_id_info = clone_id_el.get_child_by_name('clone-id-info')
|
||||
vol_uuid = cl_id_info.get_child_content('volume-uuid')
|
||||
clone_id = cl_id_info.get_child_content('clone-op-id')
|
||||
return (clone_id, vol_uuid)
|
||||
|
||||
def _wait_for_clone_finish(self, clone_op_id, vol_uuid):
|
||||
"""
|
||||
Waits till a clone operation is complete or errored out.
|
||||
"""
|
||||
clone_ls_st = NaElement('clone-list-status')
|
||||
clone_id = NaElement('clone-id')
|
||||
clone_ls_st.add_child_elem(clone_id)
|
||||
clone_id.add_node_with_children('clone-id-info',
|
||||
**{'clone-op-id': clone_op_id,
|
||||
'volume-uuid': vol_uuid})
|
||||
task_running = True
|
||||
while task_running:
|
||||
result = self._invoke_successfully(clone_ls_st, None)
|
||||
status = result.get_child_by_name('status')
|
||||
ops_info = status.get_children()
|
||||
if ops_info:
|
||||
state = ops_info[0].get_child_content('clone-state')
|
||||
if state == 'completed':
|
||||
task_running = False
|
||||
elif state == 'failed':
|
||||
code = ops_info[0].get_child_content('error')
|
||||
reason = ops_info[0].get_child_content('reason')
|
||||
raise NaApiError(code, reason)
|
||||
else:
|
||||
time.sleep(1)
|
||||
else:
|
||||
raise NaApiError(
|
||||
'UnknownCloneId',
|
||||
'No clone operation for clone id %s found on the filer'
|
||||
% (clone_id))
|
||||
|
||||
def _clear_clone(self, clone_id):
|
||||
"""Clear the clone information.
|
||||
Invoke this in case of failed clone.
|
||||
"""
|
||||
clone_clear = NaElement.create_node_with_children(
|
||||
'clone-clear',
|
||||
**{'clone-id': clone_id})
|
||||
retry = 3
|
||||
while retry:
|
||||
try:
|
||||
self._invoke_successfully(clone_clear, None)
|
||||
break
|
||||
except Exception as e:
|
||||
# Filer might be rebooting
|
||||
time.sleep(5)
|
||||
retry = retry - 1
|
||||
|
||||
def get_volume_stats(self, refresh=False):
|
||||
"""Get volume status.
|
||||
|
||||
If 'refresh' is True, run update the stats first."""
|
||||
if refresh:
|
||||
self._update_volume_status()
|
||||
|
||||
return self._stats
|
||||
|
||||
def _update_volume_status(self):
|
||||
"""Retrieve status info from volume group."""
|
||||
|
||||
LOG.debug(_("Updating volume status"))
|
||||
data = {}
|
||||
data["volume_backend_name"] = 'NetApp_NFS_7mode_direct'
|
||||
data["vendor_name"] = 'NetApp'
|
||||
data["driver_version"] = '1.0'
|
||||
data["storage_protocol"] = 'NFS'
|
||||
|
||||
data['total_capacity_gb'] = 'infinite'
|
||||
data['free_capacity_gb'] = 'infinite'
|
||||
data['reserved_percentage'] = 100
|
||||
data['QoS_support'] = False
|
||||
self._stats = data
|
||||
@@ -1,264 +0,0 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright (c) 2012 NetApp, 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.
|
||||
"""
|
||||
Volume driver for NetApp NFS storage.
|
||||
"""
|
||||
|
||||
import os
|
||||
import suds
|
||||
from suds.sax import text
|
||||
import time
|
||||
|
||||
from cinder import exception
|
||||
from cinder import flags
|
||||
from cinder.openstack.common import cfg
|
||||
from cinder.openstack.common import log as logging
|
||||
from cinder.volume.drivers.netapp import netapp_opts
|
||||
from cinder.volume.drivers import nfs
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
netapp_nfs_opts = [
|
||||
cfg.IntOpt('synchronous_snapshot_create',
|
||||
default=0,
|
||||
help='Does snapshot creation call returns immediately')]
|
||||
|
||||
FLAGS = flags.FLAGS
|
||||
FLAGS.register_opts(netapp_opts)
|
||||
FLAGS.register_opts(netapp_nfs_opts)
|
||||
|
||||
|
||||
class NetAppNFSDriver(nfs.NfsDriver):
|
||||
"""Executes commands relating to Volumes."""
|
||||
def __init__(self, *args, **kwargs):
|
||||
# NOTE(vish): db is set by Manager
|
||||
self._execute = None
|
||||
self._context = None
|
||||
super(NetAppNFSDriver, self).__init__(*args, **kwargs)
|
||||
|
||||
def set_execute(self, execute):
|
||||
self._execute = execute
|
||||
|
||||
def do_setup(self, context):
|
||||
self._context = context
|
||||
self.check_for_setup_error()
|
||||
self._client = NetAppNFSDriver._get_client()
|
||||
|
||||
def check_for_setup_error(self):
|
||||
"""Returns an error if prerequisites aren't met"""
|
||||
NetAppNFSDriver._check_dfm_flags()
|
||||
super(NetAppNFSDriver, self).check_for_setup_error()
|
||||
|
||||
def create_volume_from_snapshot(self, volume, snapshot):
|
||||
"""Creates a volume from a snapshot."""
|
||||
vol_size = volume.size
|
||||
snap_size = snapshot.volume_size
|
||||
|
||||
if vol_size != snap_size:
|
||||
msg = _('Cannot create volume of size %(vol_size)s from '
|
||||
'snapshot of size %(snap_size)s')
|
||||
raise exception.CinderException(msg % locals())
|
||||
|
||||
self._clone_volume(snapshot.name, volume.name, snapshot.volume_id)
|
||||
share = self._get_volume_location(snapshot.volume_id)
|
||||
|
||||
return {'provider_location': share}
|
||||
|
||||
def create_snapshot(self, snapshot):
|
||||
"""Creates a snapshot."""
|
||||
self._clone_volume(snapshot['volume_name'],
|
||||
snapshot['name'],
|
||||
snapshot['volume_id'])
|
||||
|
||||
def delete_snapshot(self, snapshot):
|
||||
"""Deletes a snapshot."""
|
||||
nfs_mount = self._get_provider_location(snapshot.volume_id)
|
||||
|
||||
if self._volume_not_present(nfs_mount, snapshot.name):
|
||||
return True
|
||||
|
||||
self._execute('rm', self._get_volume_path(nfs_mount, snapshot.name),
|
||||
run_as_root=True)
|
||||
|
||||
@staticmethod
|
||||
def _check_dfm_flags():
|
||||
"""Raises error if any required configuration flag for OnCommand proxy
|
||||
is missing."""
|
||||
required_flags = ['netapp_wsdl_url',
|
||||
'netapp_login',
|
||||
'netapp_password',
|
||||
'netapp_server_hostname',
|
||||
'netapp_server_port']
|
||||
for flag in required_flags:
|
||||
if not getattr(FLAGS, flag, None):
|
||||
raise exception.CinderException(_('%s is not set') % flag)
|
||||
|
||||
@staticmethod
|
||||
def _get_client():
|
||||
"""Creates SOAP _client for ONTAP-7 DataFabric Service."""
|
||||
client = suds.client.Client(FLAGS.netapp_wsdl_url,
|
||||
username=FLAGS.netapp_login,
|
||||
password=FLAGS.netapp_password)
|
||||
soap_url = 'http://%s:%s/apis/soap/v1' % (FLAGS.netapp_server_hostname,
|
||||
FLAGS.netapp_server_port)
|
||||
client.set_options(location=soap_url)
|
||||
|
||||
return client
|
||||
|
||||
def _get_volume_location(self, volume_id):
|
||||
"""Returns NFS mount address as <nfs_ip_address>:<nfs_mount_dir>"""
|
||||
nfs_server_ip = self._get_host_ip(volume_id)
|
||||
export_path = self._get_export_path(volume_id)
|
||||
return (nfs_server_ip + ':' + export_path)
|
||||
|
||||
def _clone_volume(self, volume_name, clone_name, volume_id):
|
||||
"""Clones mounted volume with OnCommand proxy API"""
|
||||
host_id = self._get_host_id(volume_id)
|
||||
export_path = self._get_full_export_path(volume_id, host_id)
|
||||
|
||||
request = self._client.factory.create('Request')
|
||||
request.Name = 'clone-start'
|
||||
|
||||
clone_start_args = ('<source-path>%s/%s</source-path>'
|
||||
'<destination-path>%s/%s</destination-path>')
|
||||
|
||||
request.Args = text.Raw(clone_start_args % (export_path,
|
||||
volume_name,
|
||||
export_path,
|
||||
clone_name))
|
||||
|
||||
resp = self._client.service.ApiProxy(Target=host_id,
|
||||
Request=request)
|
||||
|
||||
if resp.Status == 'passed' and FLAGS.synchronous_snapshot_create:
|
||||
clone_id = resp.Results['clone-id'][0]
|
||||
clone_id_info = clone_id['clone-id-info'][0]
|
||||
clone_operation_id = int(clone_id_info['clone-op-id'][0])
|
||||
|
||||
self._wait_for_clone_finished(clone_operation_id, host_id)
|
||||
elif resp.Status == 'failed':
|
||||
raise exception.CinderException(resp.Reason)
|
||||
|
||||
def _wait_for_clone_finished(self, clone_operation_id, host_id):
|
||||
"""
|
||||
Polls ONTAP7 for clone status. Returns once clone is finished.
|
||||
:param clone_operation_id: Identifier of ONTAP clone operation
|
||||
"""
|
||||
clone_list_options = ('<clone-id>'
|
||||
'<clone-id-info>'
|
||||
'<clone-op-id>%d</clone-op-id>'
|
||||
'<volume-uuid></volume-uuid>'
|
||||
'</clone-id>'
|
||||
'</clone-id-info>')
|
||||
|
||||
request = self._client.factory.create('Request')
|
||||
request.Name = 'clone-list-status'
|
||||
request.Args = text.Raw(clone_list_options % clone_operation_id)
|
||||
|
||||
resp = self._client.service.ApiProxy(Target=host_id, Request=request)
|
||||
|
||||
while resp.Status != 'passed':
|
||||
time.sleep(1)
|
||||
resp = self._client.service.ApiProxy(Target=host_id,
|
||||
Request=request)
|
||||
|
||||
def _get_provider_location(self, volume_id):
|
||||
"""
|
||||
Returns provider location for given volume
|
||||
:param volume_id:
|
||||
"""
|
||||
volume = self.db.volume_get(self._context, volume_id)
|
||||
return volume.provider_location
|
||||
|
||||
def _get_host_ip(self, volume_id):
|
||||
"""Returns IP address for the given volume"""
|
||||
return self._get_provider_location(volume_id).split(':')[0]
|
||||
|
||||
def _get_export_path(self, volume_id):
|
||||
"""Returns NFS export path for the given volume"""
|
||||
return self._get_provider_location(volume_id).split(':')[1]
|
||||
|
||||
def _get_host_id(self, volume_id):
|
||||
"""Returns ID of the ONTAP-7 host"""
|
||||
host_ip = self._get_host_ip(volume_id)
|
||||
server = self._client.service
|
||||
|
||||
resp = server.HostListInfoIterStart(ObjectNameOrId=host_ip)
|
||||
tag = resp.Tag
|
||||
|
||||
try:
|
||||
res = server.HostListInfoIterNext(Tag=tag, Maximum=1)
|
||||
if hasattr(res, 'Hosts') and res.Hosts.HostInfo:
|
||||
return res.Hosts.HostInfo[0].HostId
|
||||
finally:
|
||||
server.HostListInfoIterEnd(Tag=tag)
|
||||
|
||||
def _get_full_export_path(self, volume_id, host_id):
|
||||
"""Returns full path to the NFS share, e.g. /vol/vol0/home"""
|
||||
export_path = self._get_export_path(volume_id)
|
||||
command_args = '<pathname>%s</pathname>'
|
||||
|
||||
request = self._client.factory.create('Request')
|
||||
request.Name = 'nfs-exportfs-storage-path'
|
||||
request.Args = text.Raw(command_args % export_path)
|
||||
|
||||
resp = self._client.service.ApiProxy(Target=host_id,
|
||||
Request=request)
|
||||
|
||||
if resp.Status == 'passed':
|
||||
return resp.Results['actual-pathname'][0]
|
||||
elif resp.Status == 'failed':
|
||||
raise exception.CinderException(resp.Reason)
|
||||
|
||||
def _volume_not_present(self, nfs_mount, volume_name):
|
||||
"""
|
||||
Check if volume exists
|
||||
"""
|
||||
try:
|
||||
self._try_execute('ls', self._get_volume_path(nfs_mount,
|
||||
volume_name))
|
||||
except exception.ProcessExecutionError:
|
||||
# If the volume isn't present
|
||||
return True
|
||||
return False
|
||||
|
||||
def _try_execute(self, *command, **kwargs):
|
||||
# NOTE(vish): Volume commands can partially fail due to timing, but
|
||||
# running them a second time on failure will usually
|
||||
# recover nicely.
|
||||
tries = 0
|
||||
while True:
|
||||
try:
|
||||
self._execute(*command, **kwargs)
|
||||
return True
|
||||
except exception.ProcessExecutionError:
|
||||
tries = tries + 1
|
||||
if tries >= FLAGS.num_shell_tries:
|
||||
raise
|
||||
LOG.exception(_("Recovering from a failed execute. "
|
||||
"Try number %s"), tries)
|
||||
time.sleep(tries ** 2)
|
||||
|
||||
def _get_volume_path(self, nfs_share, volume_name):
|
||||
"""Get volume path (local fs path) for given volume name on given nfs
|
||||
share
|
||||
@param nfs_share string, example 172.18.194.100:/var/nfs
|
||||
@param volume_name string,
|
||||
example volume-91ee65ec-c473-4391-8c09-162b00c68a8c
|
||||
"""
|
||||
return os.path.join(self._get_mount_point_for_share(nfs_share),
|
||||
volume_name)
|
||||
@@ -79,11 +79,11 @@ MAPPING = {
|
||||
'cinder.volume.san.HpSanISCSIDriver':
|
||||
'cinder.volume.drivers.san.hp_lefthand.HpSanISCSIDriver',
|
||||
'cinder.volume.netapp.NetAppISCSIDriver':
|
||||
'cinder.volume.drivers.netapp.NetAppISCSIDriver',
|
||||
'cinder.volume.drivers.netapp.iscsi.NetAppISCSIDriver',
|
||||
'cinder.volume.netapp.NetAppCmodeISCSIDriver':
|
||||
'cinder.volume.drivers.netapp.NetAppCmodeISCSIDriver',
|
||||
'cinder.volume.drivers.netapp.iscsi.NetAppCmodeISCSIDriver',
|
||||
'cinder.volume.netapp_nfs.NetAppNFSDriver':
|
||||
'cinder.volume.drivers.netapp_nfs.NetAppNFSDriver',
|
||||
'cinder.volume.drivers.netapp.nfs.NetAppNFSDriver',
|
||||
'cinder.volume.nfs.NfsDriver':
|
||||
'cinder.volume.drivers.nfs.NfsDriver',
|
||||
'cinder.volume.solidfire.SolidFire':
|
||||
|
||||
Reference in New Issue
Block a user