diff --git a/cinder/tests/test_drivers_compatibility.py b/cinder/tests/test_drivers_compatibility.py
index c762002d4b3..07cdf7ff51d 100644
--- a/cinder/tests/test_drivers_compatibility.py
+++ b/cinder/tests/test_drivers_compatibility.py
@@ -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"
diff --git a/cinder/tests/test_netapp.py b/cinder/tests/test_netapp.py
index 5375924b4d6..c3ff075d391 100644
--- a/cinder/tests/test_netapp.py
+++ b/cinder/tests/test_netapp.py
@@ -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 = """
"""
elif 'CloneLun' == api:
body = """
- lun22
+ snapshot12
98ea1791d228453899d422b4611642c3
OsType
linux
@@ -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 = """
+"""
+
+RESPONSE_PREFIX_DIRECT_7MODE = """
+"""
+
+RESPONSE_PREFIX_DIRECT = """
+"""
+
+RESPONSE_SUFFIX_DIRECT = """"""
+
+
+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(''
+ '')
+
+ 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 = """
+
+ indeterminate
+ 512
+ 1354536362
+
+ false
+ true
+
+ falselinux
+
+ true/vol/navneet/lun2
+ 0
+ false2FfGI$APyN68
+ none20971520
+ 0false
+ 0
+ cec1f3d7-3d41-11e2-9cf4-123478563412
+ navneetben_vserver
+
+ <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>
+ 1"""
+ else:
+ body = """
+
+ indeterminate
+ 512
+ 1354536362
+
+ false
+ true
+
+ falselinux
+
+ true/vol/navneet/lun3
+ 0
+ false2FfGI$APyN68
+
+ none20971520
+ 0false
+ 0
+ cec1f3d7-3d41-11e2-9cf4-123478563412
+ navneetben_vserver
+
+ 1"""
+ elif 'volume-get-iter' == api:
+ tag = \
+ FakeDirectCMODEServerHandler._get_child_by_name(request, 'tag')
+ if tag is None:
+ body = """
+
+ iscsi
+ Openstack
+
+
+ 214748364
+
+ true
+
+ falseonline
+
+
+ nfsvol
+ openstack
+
+
+ 247483648
+
+ true
+
+ falseonline
+
+
+ <volume-get-iter-key-td>
+ <key-0>openstack</key-0>
+ <key-1>nfsvol</key-1>
+ </volume-get-iter-key-td>
+ 2"""
+ else:
+ body = """
+
+ iscsi
+ Openstack
+
+
+ 4147483648
+
+ true
+
+ falseonline
+
+
+ nfsvol
+ openstack
+
+
+ 8147483648
+
+ true
+
+ falseonline
+
+
+ 2"""
+ elif 'lun-create-by-size' == api:
+ body = """
+ 22020096"""
+ elif 'lun-destroy' == api:
+ body = """"""
+ 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 = """
+
+ openstack-01f5297b-00f7-4170-bf30-69b1314b2118
+
+ windows
+ iscsi
+
+
+ iqn.1993-08.org.debian:01:10
+
+ openstack
+
+ <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>
+ 1"""
+ else:
+ body = """
+
+ openstack-01f5297b-00f7-4170-bf30-69b1314b2118
+
+ linux
+ iscsi
+
+
+ iqn.1993-08.org.debian:01:10
+
+ openstack
+ 1"""
+ else:
+ body = """
+ 0
+ """
+ elif 'lun-map-get-iter' == api:
+ tag = \
+ FakeDirectCMODEServerHandler._get_child_by_name(request, 'tag')
+ if tag is None:
+ body = """
+
+ openstack-44c5e7e1-3306-4800-9623-259e57d56a83
+
+ 948ae304-06e9-11e2
+ 0
+ 5587e563-06e9-11e2-9cf4-123478563412
+ /vol/openvol/lun1
+ openstack
+
+
+ <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>
+
+ 1
+ """
+ else:
+ body = """
+
+ openstack-44c5e7e1-3306-4800-9623-259e57d56a83
+
+ 948ae304-06e9-11e2
+ 0
+ 5587e563-06e9-11e2-9cf4-123478563412
+ /vol/openvol/lun1
+ openstack
+ 1
+ """
+ elif 'lun-map' == api:
+ body = """1
+
+ """
+ elif 'iscsi-service-get-iter' == api:
+ body = """
+
+ openstack
+ true
+ iqn.1992-08.com.netapp:sn.fa9:vs.105
+ openstack
+ 1"""
+ elif 'iscsi-interface-get-iter' == api:
+ body = """
+
+ fas3170rre-cmode-01
+ e1b-1165
+
+ iscsi_data_if
+ 10.63.165.216
+ 3260true
+
+ 5
+ iscsi_data_if
+ 1038
+ openstack
+
+ 1"""
+ elif 'igroup-create' == api:
+ body = """"""
+ elif 'igroup-add' == api:
+ body = """"""
+ elif 'clone-create' == api:
+ body = """"""
+ elif 'lun-unmap' == api:
+ body = """"""
+ elif 'system-get-ontapi-version' == api:
+ body = """
+ 1
+ 19
+ """
+ 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(''
+ '')
+
+ 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 = """
+ false
+ false
+
+
+ /vol/vol1/clone1
+ 20971520
+ true
+ false
+ false
+ false
+ none
+ linux
+ e867d844-c2c0-11e0-9282-00a09825b3b5
+ P3lgP4eTyaNl
+ 512
+ true
+ 0
+ indeterminate
+
+
+ /vol/vol1/lun1
+ 20971520
+ true
+ false
+ false
+ false
+ none
+ linux
+ 8e1e9284-c288-11e0-9282-00a09825b3b5
+ P3lgP4eTc3lp
+ 512
+ true
+ 0
+ indeterminate
+
+
+ """
+ elif 'volume-list-info' == api:
+ body = """
+
+
+ vol0
+ 019c8f7a-9243-11e0-9281-00a09825b3b5
+ flex
+ 32_bit
+ online
+ 576914493440
+ 13820354560
+ 563094110208
+ 2
+ 20
+ 140848264
+ 0
+ 0
+ 0
+ 0
+ 20907162
+ 7010
+ 518
+ 31142
+ 31142
+ 0
+ false
+ aggr0
+
+
+ disabled
+ idle
+
+ regular
+ sun-sat@0
+ Mon Aug 8 09:34:15 EST 2011
+
+ Mon Aug 8 09:34:15 EST 2011
+
+ 0
+ 0
+ 0
+ 0
+ 0
+ 0
+ 0
+ 0
+ 0
+ 0
+
+
+
+ false
+
+ volume
+ true
+ 14
+ raid_dp,sis
+ block
+ true
+ false
+ false
+ false
+ false
+ unmirrored
+ 3
+ 1
+
+
+ /aggr0/plex0
+ true
+ false
+
+
+
+
+ vol1
+ 2d50ecf4-c288-11e0-9282-00a09825b3b5
+ flex
+ 32_bit
+ online
+ 42949672960
+ 44089344
+ 42905583616
+ 0
+ 20
+ 10485760
+ 8192
+ 8192
+ 0
+ 0
+ 1556480
+ 110
+ 504
+ 31142
+ 31142
+ 0
+ false
+ aggr1
+
+
+ disabled
+ idle
+
+ regular
+ sun-sat@0
+ Sun Aug 7 14:51:00 EST 2011
+
+ Sun Aug 7 14:51:00 EST 2011
+
+ 0
+ 0
+ 0
+ 0
+ 0
+ 0
+ 0
+ 0
+ 0
+ 0
+
+
+
+ false
+
+ volume
+ true
+ 7
+ raid4,sis
+ block
+ true
+ false
+ false
+ false
+ false
+ unmirrored
+ 2
+ 1
+
+
+ /aggr1/plex0
+ true
+ false
+
+
+
+
+ """
+ elif 'volume-options-list-info' == api:
+ body = """
+
+
+ snapmirrored
+ off
+
+
+ root
+ false
+
+
+ ha_policy
+ cfo
+
+
+ striping
+ not_striped
+
+
+ compression
+ off
+
+
+ """
+ elif 'lun-create-by-size' == api:
+ body = """
+ 22020096"""
+ elif 'lun-destroy' == api:
+ body = """"""
+ elif 'igroup-list-info' == api:
+ body = """
+
+
+ openstack-8bc96490
+ iscsi
+ b8e1d274-c378-11e0
+ linux
+ 0
+ false
+
+ false
+ false
+ true
+
+
+
+ iqn.1993-08.org.debian:01:10
+
+
+
+
+ iscsi_group
+ iscsi
+ ccb8cbe4-c36f
+ linux
+ 0
+ false
+
+ false
+ false
+ true
+
+
+
+ iqn.1993-08.org.debian:01:10ca
+
+
+
+
+ """
+ elif 'lun-map-list-info' == api:
+ body = """
+
+ """
+ elif 'lun-map' == api:
+ body = """1
+
+ """
+ elif 'iscsi-node-get-name' == api:
+ body = """
+ iqn.1992-08.com.netapp:sn.135093938
+ """
+ elif 'iscsi-portal-list-info' == api:
+ body = """
+
+
+ 10.61.176.156
+ 3260
+ 1000
+ e0a
+
+
+ """
+ elif 'igroup-create' == api:
+ body = """"""
+ elif 'igroup-add' == api:
+ body = """"""
+ elif 'clone-start' == api:
+ body = """
+
+
+ 2d50ecf4-c288-11e0-9282-00a09825b3b5
+ 11
+
+
+ """
+ elif 'clone-list-status' == api:
+ body = """
+
+
+ completed
+
+
+ """
+ elif 'lun-unmap' == api:
+ body = """"""
+ elif 'system-get-ontapi-version' == api:
+ body = """
+ 1
+ 8
+ """
+ elif 'lun-set-space-reservation-info' == api:
+ body = """"""
+ 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
diff --git a/cinder/tests/test_netapp_nfs.py b/cinder/tests/test_netapp_nfs.py
index 47c8c80c9a9..152a709c0a0 100644
--- a/cinder/tests/test_netapp_nfs.py
+++ b/cinder/tests/test_netapp_nfs.py
@@ -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 = """
+
+ 127.0.0.1
+ up
+ fas3170rre-cmode-01
+ e1b-1165
+
+ nfs
+
+ none
+
+ disabled
+ data
+ fas3170rre-cmode-01
+ e1b-1165
+ nfs_data1
+ false
+ true
+ 255.255.255.0
+ 24
+ up
+ data
+ c10.63.165.0/24
+ disabled
+ openstack
+ """
+ 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()
diff --git a/cinder/volume/drivers/netapp/__init__.py b/cinder/volume/drivers/netapp/__init__.py
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/cinder/volume/drivers/netapp/api.py b/cinder/volume/drivers/netapp/api.py
new file mode 100644
index 00000000000..aac07020fec
--- /dev/null
+++ b/cinder/volume/drivers/netapp/api.py
@@ -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)
diff --git a/cinder/volume/drivers/netapp.py b/cinder/volume/drivers/netapp/iscsi.py
similarity index 53%
rename from cinder/volume/drivers/netapp.py
rename to cinder/volume/drivers/netapp/iscsi.py
index 56d0b74c32a..2c0764a140e 100644
--- a/cinder/volume/drivers/netapp.py
+++ b/cinder/volume/drivers/netapp/iscsi.py
@@ -29,11 +29,16 @@ import suds
from suds import client
from suds.sax import text
+import uuid
+
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 import driver
+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 import volume_types
LOG = logging.getLogger(__name__)
@@ -41,20 +46,19 @@ LOG = logging.getLogger(__name__)
netapp_opts = [
cfg.StrOpt('netapp_wsdl_url',
default=None,
- help='URL of the WSDL file for the DFM server'),
+ help='URL of the WSDL file for the DFM/Webservice server'),
cfg.StrOpt('netapp_login',
default=None,
- help='User name for the DFM server'),
+ help='User name for the DFM/Controller server'),
cfg.StrOpt('netapp_password',
default=None,
- help='Password for the DFM server',
- secret=True),
+ help='Password for the DFM/Controller server'),
cfg.StrOpt('netapp_server_hostname',
default=None,
- help='Hostname for the DFM server'),
+ help='Hostname for the DFM/Controller server'),
cfg.IntOpt('netapp_server_port',
default=8088,
- help='Port number for the DFM server'),
+ help='Port number for the DFM/Controller server'),
cfg.StrOpt('netapp_storage_service',
default=None,
help=('Storage service to use for provisioning '
@@ -65,7 +69,16 @@ netapp_opts = [
'provisioning (volume_type name will be appended)')),
cfg.StrOpt('netapp_vfiler',
default=None,
- help='Vfiler to use for provisioning'), ]
+ help='Vfiler to use for provisioning'),
+ cfg.StrOpt('netapp_transport_type',
+ default='http',
+ help='Transport type protocol'),
+ cfg.StrOpt('netapp_vserver',
+ default='openstack',
+ help='Cluster vserver to use for provisioning'),
+ cfg.FloatOpt('netapp_size_multiplier',
+ default=1.2,
+ help='Volume size multiplier to ensure while creation'), ]
FLAGS = flags.FLAGS
FLAGS.register_opts(netapp_opts)
@@ -1023,7 +1036,60 @@ class NetAppISCSIDriver(driver.ISCSIDriver):
def create_cloned_volume(self, volume, src_vref):
"""Creates a clone of the specified volume."""
- raise NotImplementedError()
+ 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.VolumeBackendAPIException(data=msg % locals())
+ src_vol_name = src_vref['name']
+ project = src_vref['project_id']
+ lun = self._lookup_lun_for_volume(src_vol_name, project)
+ lun_id = lun.id
+ dataset = lun.dataset
+ old_type = dataset.type
+ new_type = self._get_ss_type(volume)
+ if new_type != old_type:
+ msg = _('Cannot create clone of type %(new_type)s from '
+ 'volume of type %(old_type)s')
+ raise exception.VolumeBackendAPIException(data=msg % locals())
+ lun = self._get_lun_details(lun_id)
+ extra_gb = vol_size
+ new_size = '+%dg' % extra_gb
+ self._resize_volume(lun.HostId, lun.VolumeName, new_size)
+ clone_name = volume['name']
+ self._create_qtree(lun.HostId, lun.VolumeName, clone_name)
+ src_path = '/vol/%s/%s/%s' % (lun.VolumeName, lun.QtreeName,
+ src_vol_name)
+ dest_path = '/vol/%s/%s/%s' % (lun.VolumeName, clone_name, clone_name)
+ self._clone_lun(lun.HostId, src_path, dest_path, False)
+ self._refresh_dfm_luns(lun.HostId)
+ self._discover_dataset_luns(dataset, clone_name)
+
+ 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_iSCSI_7mode'
+ data["vendor_name"] = 'NetApp'
+ data["driver_version"] = '1.0'
+ data["storage_protocol"] = 'iSCSI'
+
+ data['total_capacity_gb'] = 'infinite'
+ data['free_capacity_gb'] = 'infinite'
+ data['reserved_percentage'] = 100
+ data['QoS_support'] = False
+ self._stats = data
class NetAppLun(object):
@@ -1033,7 +1099,7 @@ class NetAppLun(object):
self.handle = handle
self.name = name
self.size = size
- self.metadata = metadata_dict
+ self.metadata = metadata_dict or {}
def get_metadata_property(self, prop):
"""Get the metadata property of a LUN."""
@@ -1043,6 +1109,10 @@ class NetAppLun(object):
msg = _("No metadata property %(prop)s defined for the LUN %(name)s")
LOG.debug(msg % locals())
+ def __str__(self, *args, **kwargs):
+ return 'NetApp Lun[handle:%s, name:%s, size:%s, metadata:%s]'\
+ % (self.handle, self.name, self.size, self.metadata)
+
class NetAppCmodeISCSIDriver(driver.ISCSIDriver):
"""NetApp C-mode iSCSI volume driver."""
@@ -1252,6 +1322,7 @@ class NetAppCmodeISCSIDriver(driver.ISCSIDriver):
handle = self._get_lun_handle(snapshot['name'])
self.client.service.DestroyLun(Handle=handle)
LOG.debug(_("Destroyed LUN %s") % handle)
+ self.lun_table.pop(snapshot['name'])
def create_volume_from_snapshot(self, volume, snapshot):
"""Driver entry point for creating a new volume from a snapshot.
@@ -1259,6 +1330,12 @@ class NetAppCmodeISCSIDriver(driver.ISCSIDriver):
Many would call this "cloning" and in fact we use cloning to implement
this feature.
"""
+ 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.VolumeBackendAPIException(data=msg % locals())
snapshot_name = snapshot['name']
lun = self.lun_table[snapshot_name]
new_name = volume['name']
@@ -1327,14 +1404,1057 @@ class NetAppCmodeISCSIDriver(driver.ISCSIDriver):
meta_dict[meta.Key] = meta.Value
return meta_dict
- def copy_image_to_volume(self, context, volume, image_service, image_id):
- """Fetch the image from image_service and write it to the volume."""
+ def create_cloned_volume(self, volume, src_vref):
+ """Creates a clone of the specified volume."""
+ vol_size = volume['size']
+ src_vol = self.lun_table[src_vref['name']]
+ src_vol_size = src_vref['size']
+ if vol_size != src_vol_size:
+ msg = _('Cannot clone volume of size %(vol_size)s from '
+ 'src volume of size %(src_vol_size)s')
+ raise exception.VolumeBackendAPIException(data=msg % locals())
+ new_name = volume['name']
+ extra_args = {}
+ extra_args['OsType'] = 'linux'
+ extra_args['QosType'] = self._get_qos_type(volume)
+ extra_args['Container'] = volume['project_id']
+ extra_args['Display'] = volume['display_name']
+ extra_args['Description'] = volume['display_description']
+ extra_args['SpaceReserved'] = True
+ self._clone_lun(src_vol.handle, new_name, extra_args)
+
+ 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_iSCSI_Cluster'
+ data["vendor_name"] = 'NetApp'
+ data["driver_version"] = '1.0'
+ data["storage_protocol"] = 'iSCSI'
+
+ data['total_capacity_gb'] = 'infinite'
+ data['free_capacity_gb'] = 'infinite'
+ data['reserved_percentage'] = 100
+ data['QoS_support'] = False
+ self._stats = data
+
+
+class NetAppDirectISCSIDriver(driver.ISCSIDriver):
+ """NetApp Direct iSCSI volume driver."""
+
+ IGROUP_PREFIX = 'openstack-'
+ required_flags = ['netapp_transport_type', 'netapp_login',
+ 'netapp_password', 'netapp_server_hostname',
+ 'netapp_server_port']
+
+ def __init__(self, *args, **kwargs):
+ super(NetAppDirectISCSIDriver, self).__init__(*args, **kwargs)
+ self.lun_table = {}
+
+ def _create_client(self, **kwargs):
+ """Instantiate a client for NetApp server.
+
+ This method creates NetApp server client for api communication.
+ """
+ host_filer = kwargs['hostname']
+ LOG.debug(_('Using NetApp filer: %s') % host_filer)
+ # Do not use client directly
+ # Use _invoke_successfully instead to make sure
+ # we use the right api i.e. cluster or vserver api
+ # and not the connection from previous call
+ self.client = NaServer(host=host_filer,
+ server_type=NaServer.SERVER_TYPE_FILER,
+ transport_type=kwargs['transport_type'],
+ style=NaServer.STYLE_LOGIN_PASSWORD,
+ username=kwargs['login'],
+ password=kwargs['password'])
+
+ def _do_custom_setup(self):
+ """Does custom setup depending on the type of filer."""
raise NotImplementedError()
- def copy_volume_to_image(self, context, volume, image_service, image_meta):
- """Copy the volume to the specified image."""
+ def _check_flags(self):
+ """Ensure that the flags we care about are set."""
+ required_flags = self.required_flags
+ for flag in required_flags:
+ if not getattr(FLAGS, flag, None):
+ msg = _('%s is not set') % flag
+ raise exception.InvalidInput(data=msg)
+
+ def do_setup(self, context):
+ """Setup the NetApp Volume driver.
+
+ Called one time by the manager after the driver is loaded.
+ Validate the flags we care about and setup NetApp
+ client.
+ """
+ self._check_flags()
+ self._create_client(
+ transport_type=FLAGS.netapp_transport_type,
+ login=FLAGS.netapp_login, password=FLAGS.netapp_password,
+ hostname=FLAGS.netapp_server_hostname,
+ port=FLAGS.netapp_server_port)
+ self._do_custom_setup()
+
+ def check_for_setup_error(self):
+ """Check that the driver is working and can communicate.
+
+ Discovers the LUNs on the NetApp server.
+ """
+ self.lun_table = {}
+ self._get_lun_list()
+ LOG.debug(_("Success getting LUN list from server"))
+
+ def create_volume(self, volume):
+ """Driver entry point for creating a new volume."""
+ default_size = '104857600' # 100 MB
+ gigabytes = 1073741824L # 2^30
+ name = volume['name']
+ if int(volume['size']) == 0:
+ size = default_size
+ else:
+ size = str(int(volume['size']) * gigabytes)
+ metadata = {}
+ metadata['OsType'] = 'linux'
+ metadata['SpaceReserved'] = 'true'
+ self._create_lun_on_eligible_vol(name, size, metadata)
+ LOG.debug(_("Created LUN with name %s") % name)
+ handle = self._create_lun_handle(metadata)
+ self._add_lun_to_table(NetAppLun(handle, name, size, metadata))
+
+ def delete_volume(self, volume):
+ """Driver entry point for destroying existing volumes."""
+ name = volume['name']
+ metadata = self._get_lun_attr(name, 'metadata')
+ lun_destroy = NaElement.create_node_with_children(
+ 'lun-destroy',
+ **{'path': metadata['Path'],
+ 'force': 'true'})
+ self._invoke_successfully(lun_destroy, True)
+ LOG.debug(_("Destroyed LUN %s") % name)
+ self.lun_table.pop(name)
+
+ def ensure_export(self, context, volume):
+ """Driver entry point to get the export info for an existing volume."""
+ handle = self._get_lun_attr(volume['name'], 'handle')
+ return {'provider_location': handle}
+
+ def create_export(self, context, volume):
+ """Driver entry point to get the export info for a new volume."""
+ handle = self._get_lun_attr(volume['name'], 'handle')
+ return {'provider_location': handle}
+
+ def remove_export(self, context, volume):
+ """Driver exntry point to remove an export for a volume.
+
+ Since exporting is idempotent in this driver, we have nothing
+ to do for unexporting.
+ """
+ pass
+
+ def initialize_connection(self, volume, connector):
+ """Driver entry point to attach a volume to an instance.
+
+ Do the LUN masking on the storage system so the initiator can access
+ the LUN on the target. Also return the iSCSI properties so the
+ initiator can find the LUN. This implementation does not call
+ _get_iscsi_properties() to get the properties because cannot store the
+ LUN number in the database. We only find out what the LUN number will
+ be during this method call so we construct the properties dictionary
+ ourselves.
+ """
+ initiator_name = connector['initiator']
+ name = volume['name']
+ lun_id = self._map_lun(name, initiator_name, 'iscsi', None)
+ msg = _("Mapped LUN %(name)s to the initiator %(initiator_name)s")
+ LOG.debug(msg % locals())
+ iqn = self._get_iscsi_service_details()
+ target_details_list = self._get_target_details()
+ msg = _("Succesfully fetched target details for LUN %(name)s and "
+ "initiator %(initiator_name)s")
+ LOG.debug(msg % locals())
+
+ if not target_details_list:
+ msg = _('Failed to get LUN target details for the LUN %s')
+ raise exception.VolumeBackendAPIException(data=msg % name)
+ target_details = None
+ for tgt_detail in target_details_list:
+ if tgt_detail.get('interface-enabled', 'true') == 'true':
+ target_details = tgt_detail
+ break
+ if not target_details:
+ target_details = target_details_list[0]
+
+ if not target_details['address'] and target_details['port']:
+ msg = _('Failed to get target portal for the LUN %s')
+ raise exception.VolumeBackendAPIException(data=msg % name)
+ if not iqn:
+ msg = _('Failed to get target IQN for the LUN %s')
+ raise exception.VolumeBackendAPIException(data=msg % name)
+
+ properties = {}
+ properties['target_discovered'] = False
+ (address, port) = (target_details['address'], target_details['port'])
+ properties['target_portal'] = '%s:%s' % (address, port)
+ properties['target_iqn'] = iqn
+ properties['target_lun'] = lun_id
+ properties['volume_id'] = volume['id']
+
+ auth = volume['provider_auth']
+ if auth:
+ (auth_method, auth_username, auth_secret) = auth.split()
+ properties['auth_method'] = auth_method
+ properties['auth_username'] = auth_username
+ properties['auth_password'] = auth_secret
+
+ return {
+ 'driver_volume_type': 'iscsi',
+ 'data': properties,
+ }
+
+ def create_snapshot(self, snapshot):
+ """Driver entry point for creating a snapshot.
+
+ This driver implements snapshots by using efficient single-file
+ (LUN) cloning.
+ """
+ vol_name = snapshot['volume_name']
+ snapshot_name = snapshot['name']
+ lun = self.lun_table[vol_name]
+ self._clone_lun(lun.name, snapshot_name, 'false')
+
+ def delete_snapshot(self, snapshot):
+ """Driver entry point for deleting a snapshot."""
+ self.delete_volume(snapshot)
+ LOG.debug(_("Snapshot %s deletion successful") % snapshot['name'])
+
+ def create_volume_from_snapshot(self, volume, snapshot):
+ """Driver entry point for creating a new volume from a snapshot.
+
+ Many would call this "cloning" and in fact we use cloning to implement
+ this feature.
+ """
+ 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.VolumeBackendAPIException(data=msg % locals())
+ snapshot_name = snapshot['name']
+ new_name = volume['name']
+ self._clone_lun(snapshot_name, new_name, 'true')
+
+ def terminate_connection(self, volume, connector, **kwargs):
+ """Driver entry point to unattach a volume from an instance.
+
+ Unmask the LUN on the storage system so the given intiator can no
+ longer access it.
+ """
+ initiator_name = connector['initiator']
+ name = volume['name']
+ metadata = self._get_lun_attr(name, 'metadata')
+ path = metadata['Path']
+ self._unmap_lun(path, initiator_name)
+ msg = _("Unmapped LUN %(name)s from the initiator "
+ "%(initiator_name)s")
+ LOG.debug(msg % locals())
+
+ 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)
+
+ def _create_lun_on_eligible_vol(self, name, size, metadata):
+ """Creates an actual lun on filer."""
+ req_size = float(size) * float(FLAGS.netapp_size_multiplier)
+ volume = self._get_avl_volume_by_size(req_size)
+ if not volume:
+ msg = _('Failed to get vol with required size for volume: %s')
+ raise exception.VolumeBackendAPIException(data=msg % name)
+ path = '/vol/%s/%s' % (volume['name'], name)
+ lun_create = NaElement.create_node_with_children(
+ 'lun-create-by-size',
+ **{'path': path, 'size': size,
+ 'ostype': metadata['OsType'],
+ 'space-reservation-enabled':
+ metadata['SpaceReserved']})
+ self._invoke_successfully(lun_create, True)
+ metadata['Path'] = '/vol/%s/%s' % (volume['name'], name)
+ metadata['Volume'] = volume['name']
+ metadata['Qtree'] = None
+
+ def _get_avl_volume_by_size(self, size):
+ """Get the available volume by size."""
+ raise NotImplementedError()
+
+ def _get_iscsi_service_details(self):
+ """Returns iscsi iqn."""
+ raise NotImplementedError()
+
+ def _get_target_details(self):
+ """Gets the target portal details."""
+ raise NotImplementedError()
+
+ def _create_lun_handle(self, metadata):
+ """Returns lun handle based on filer type."""
+ raise NotImplementedError()
+
+ def _get_lun_list(self):
+ """Gets the list of luns on filer."""
+ raise NotImplementedError()
+
+ def _extract_and_populate_luns(self, api_luns):
+ """Extracts the luns from api.
+ Populates in the lun table.
+ """
+ for lun in api_luns:
+ meta_dict = self._create_lun_meta(lun)
+ path = lun.get_child_content('path')
+ (rest, splitter, name) = path.rpartition('/')
+ handle = self._create_lun_handle(meta_dict)
+ size = lun.get_child_content('size')
+ discovered_lun = NetAppLun(handle, name,
+ size, meta_dict)
+ self._add_lun_to_table(discovered_lun)
+
+ def _invoke_successfully(self, na_element, do_tunneling=False):
+ """Invoke the api for successful result.
+ do_tunneling sets flag for tunneling.
+ """
+ self._is_naelement(na_element)
+ self._configure_tunneling(do_tunneling)
+ result = self.client.invoke_successfully(na_element)
+ return result
+
+ def _configure_tunneling(self, do_tunneling=False):
+ """Configures tunneling based on system type."""
+ raise NotImplementedError()
+
+ def _is_naelement(self, elem):
+ """Checks if element is NetApp element."""
+ if not isinstance(elem, NaElement):
+ raise ValueError('Expects NaElement')
+
+ def _map_lun(self, name, initiator, initiator_type='iscsi', lun_id=None):
+ """Maps lun to the initiator.
+ Returns lun id assigned.
+ """
+ metadata = self._get_lun_attr(name, 'metadata')
+ os = metadata['OsType']
+ path = metadata['Path']
+ if self._check_allowed_os(os):
+ os = os
+ else:
+ os = 'default'
+ igroup_name = self._get_or_create_igroup(initiator,
+ initiator_type, os)
+ lun_map = NaElement.create_node_with_children(
+ 'lun-map', **{'path': path,
+ 'initiator-group': igroup_name})
+ if lun_id:
+ lun_map.add_new_child('lun-id', lun_id)
+ try:
+ result = self._invoke_successfully(lun_map, True)
+ return result.get_child_content('lun-id-assigned')
+ except NaApiError as e:
+ code = e.code
+ message = e.message
+ msg = _('Error mapping lun. Code :%(code)s, Message:%(message)s')
+ LOG.warn(msg % locals())
+ (igroup, lun_id) = self._find_mapped_lun_igroup(path, initiator)
+ if lun_id is not None:
+ return lun_id
+ else:
+ raise e
+
+ def _unmap_lun(self, path, initiator):
+ """Unmaps a lun from given initiator."""
+ (igroup_name, lun_id) = self._find_mapped_lun_igroup(path, initiator)
+ lun_unmap = NaElement.create_node_with_children(
+ 'lun-unmap',
+ **{'path': path,
+ 'initiator-group': igroup_name})
+ try:
+ self._invoke_successfully(lun_unmap, True)
+ except NaApiError as e:
+ msg = _("Error unmapping lun. Code :%(code)s, Message:%(message)s")
+ code = e.code
+ message = e.message
+ LOG.warn(msg % locals())
+ # if the lun is already unmapped
+ if e.code == '13115' or e.code == '9016':
+ pass
+ else:
+ raise e
+
+ def _find_mapped_lun_igroup(self, path, initiator, os=None):
+ """Find the igroup for mapped lun with initiator."""
+ raise NotImplementedError()
+
+ def _get_or_create_igroup(self, initiator, initiator_type='iscsi',
+ os='default'):
+ """Checks for an igroup for an initiator.
+ Creates igroup if not found.
+ """
+ igroups = self._get_igroup_by_initiator(initiator=initiator)
+ igroup_name = None
+ for igroup in igroups:
+ if igroup['initiator-group-os-type'] == os:
+ if igroup['initiator-group-type'] == initiator_type or \
+ igroup['initiator-group-type'] == 'mixed':
+ if igroup['initiator-group-name'].startswith(
+ self.IGROUP_PREFIX):
+ igroup_name = igroup['initiator-group-name']
+ break
+ if not igroup_name:
+ igroup_name = self.IGROUP_PREFIX + str(uuid.uuid4())
+ self._create_igroup(igroup_name, initiator_type, os)
+ self._add_igroup_initiator(igroup_name, initiator)
+ return igroup_name
+
+ def _get_igroup_by_initiator(self, initiator):
+ """Get igroups by initiator."""
+ raise NotImplementedError()
+
+ def _check_allowed_os(self, os):
+ """Checks if the os type supplied is NetApp supported."""
+ if os in ['linux', 'aix', 'hpux', 'windows', 'solaris',
+ 'netware', 'vmware', 'openvms', 'xen', 'hyper_v']:
+ return True
+ else:
+ return False
+
+ def _create_igroup(self, igroup, igroup_type='iscsi', os_type='default'):
+ """Creates igoup with specified args."""
+ igroup_create = NaElement.create_node_with_children(
+ 'igroup-create',
+ **{'initiator-group-name': igroup,
+ 'initiator-group-type': igroup_type,
+ 'os-type': os_type})
+ self._invoke_successfully(igroup_create, True)
+
+ def _add_igroup_initiator(self, igroup, initiator):
+ """Adds initiators to the specified igroup."""
+ igroup_add = NaElement.create_node_with_children(
+ 'igroup-add',
+ **{'initiator-group-name': igroup,
+ 'initiator': initiator})
+ self._invoke_successfully(igroup_add, True)
+
+ def _get_qos_type(self, volume):
+ """Get the storage service type for a volume."""
+ type_id = volume['volume_type_id']
+ if not type_id:
+ return None
+ volume_type = volume_types.get_volume_type(None, type_id)
+ if not volume_type:
+ return None
+ return volume_type['name']
+
+ def _add_lun_to_table(self, lun):
+ """Adds LUN to cache table."""
+ if not isinstance(lun, NetAppLun):
+ msg = _("Object is not a NetApp LUN.")
+ raise exception.VolumeBackendAPIException(data=msg)
+ self.lun_table[lun.name] = lun
+
+ def _clone_lun(self, name, new_name, space_reserved):
+ """Clone LUN with the given name to the new name."""
+ raise NotImplementedError()
+
+ def _get_lun_by_args(self, **args):
+ """Retrives lun with specified args."""
+ raise NotImplementedError()
+
+ def _get_lun_attr(self, name, attr):
+ """Get the attributes for a LUN from our cache table."""
+ if not name in self.lun_table or not hasattr(
+ self.lun_table[name], attr):
+ LOG.warn(_("Could not find attribute for LUN named %s") % name)
+ return None
+ return getattr(self.lun_table[name], attr)
+
+ def _create_lun_meta(self, lun):
raise NotImplementedError()
def create_cloned_volume(self, volume, src_vref):
"""Creates a clone of the specified volume."""
+ vol_size = volume['size']
+ src_vol = self.lun_table[src_vref['name']]
+ src_vol_size = src_vref['size']
+ if vol_size != src_vol_size:
+ msg = _('Cannot clone volume of size %(vol_size)s from '
+ 'src volume of size %(src_vol_size)s')
+ raise exception.VolumeBackendAPIException(data=msg % locals())
+ new_name = volume['name']
+ self._clone_lun(src_vol.name, new_name, 'true')
+
+ 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."""
raise NotImplementedError()
+
+
+class NetAppDirectCmodeISCSIDriver(NetAppDirectISCSIDriver):
+ """NetApp C-mode iSCSI volume driver."""
+
+ def __init__(self, *args, **kwargs):
+ super(NetAppDirectCmodeISCSIDriver, self).__init__(*args, **kwargs)
+
+ def _do_custom_setup(self):
+ """Does custom setup for ontap cluster."""
+ self.vserver = FLAGS.netapp_vserver
+ # Default values to run first api
+ self.client.set_api_version(1, 15)
+ (major, minor) = self._get_ontapi_version()
+ self.client.set_api_version(major, minor)
+
+ def _get_avl_volume_by_size(self, size):
+ """Get the available volume by size."""
+ tag = None
+ while True:
+ vol_request = self._create_avl_vol_request(self.vserver, tag)
+ res = self._invoke_successfully(vol_request)
+ tag = res.get_child_content('next-tag')
+ attr_list = res.get_child_by_name('attributes-list')
+ vols = attr_list.get_children()
+ for vol in vols:
+ vol_space = vol.get_child_by_name('volume-space-attributes')
+ avl_size = vol_space.get_child_content('size-available')
+ if float(avl_size) >= float(size):
+ avl_vol = dict()
+ vol_id = vol.get_child_by_name('volume-id-attributes')
+ avl_vol['name'] = vol_id.get_child_content('name')
+ avl_vol['vserver'] = vol_id.get_child_content(
+ 'owning-vserver-name')
+ avl_vol['size-available'] = avl_size
+ return avl_vol
+ if tag is None:
+ break
+ return None
+
+ def _create_avl_vol_request(self, vserver, tag=None):
+ vol_get_iter = NaElement('volume-get-iter')
+ vol_get_iter.add_new_child('max-records', '100')
+ if tag:
+ vol_get_iter.add_new_child('tag', tag, True)
+ query = NaElement('query')
+ vol_get_iter.add_child_elem(query)
+ vol_attrs = NaElement('volume-attributes')
+ query.add_child_elem(vol_attrs)
+ if vserver:
+ vol_attrs.add_node_with_children(
+ 'volume-id-attributes',
+ **{"owning-vserver-name": vserver})
+ vol_attrs.add_node_with_children(
+ 'volume-state-attributes',
+ **{"is-vserver-root": "false", "state": "online"})
+ desired_attrs = NaElement('desired-attributes')
+ vol_get_iter.add_child_elem(desired_attrs)
+ des_vol_attrs = NaElement('volume-attributes')
+ desired_attrs.add_child_elem(des_vol_attrs)
+ des_vol_attrs.add_node_with_children(
+ 'volume-id-attributes',
+ **{"name": None, "owning-vserver-name": None})
+ des_vol_attrs.add_node_with_children(
+ 'volume-space-attributes',
+ **{"size-available": None})
+ des_vol_attrs.add_node_with_children('volume-state-attributes',
+ **{"is-cluster-volume": None,
+ "is-vserver-root": None,
+ "state": None})
+ return vol_get_iter
+
+ def _get_target_details(self):
+ """Gets the target portal details."""
+ iscsi_if_iter = NaElement('iscsi-interface-get-iter')
+ result = self._invoke_successfully(iscsi_if_iter, True)
+ tgt_list = []
+ 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')
+ iscsi_if_list = attr_list.get_children()
+ for iscsi_if in iscsi_if_list:
+ d = dict()
+ d['address'] = iscsi_if.get_child_content('ip-address')
+ d['port'] = iscsi_if.get_child_content('ip-port')
+ d['tpgroup-tag'] = iscsi_if.get_child_content('tpgroup-tag')
+ d['interface-enabled'] = iscsi_if.get_child_content(
+ 'is-interface-enabled')
+ tgt_list.append(d)
+ return tgt_list
+
+ def _get_iscsi_service_details(self):
+ """Returns iscsi iqn."""
+ iscsi_service_iter = NaElement('iscsi-service-get-iter')
+ result = self._invoke_successfully(iscsi_service_iter, True)
+ 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')
+ iscsi_service = attr_list.get_child_by_name('iscsi-service-info')
+ return iscsi_service.get_child_content('node-name')
+ LOG.debug(_('No iscsi service found for vserver %s') % (self.vserver))
+ return None
+
+ def _create_lun_handle(self, metadata):
+ """Returns lun handle based on filer type."""
+ return '%s:%s' % (self.vserver, metadata['Path'])
+
+ def _get_lun_list(self):
+ """Gets the list of luns on filer."""
+ """Gets the luns from cluster with vserver."""
+ tag = None
+ while True:
+ api = NaElement('lun-get-iter')
+ api.add_new_child('max-records', '100')
+ if tag:
+ api.add_new_child('tag', tag, True)
+ lun_info = NaElement('lun-info')
+ lun_info.add_new_child('vserver', self.vserver)
+ query = NaElement('query')
+ query.add_child_elem(lun_info)
+ api.add_child_elem(query)
+ result = self._invoke_successfully(api)
+ if result.get_child_by_name('num-records') and\
+ int(result.get_child_content('num-records')) >= 1:
+ attr_list = result.get_child_by_name('attributes-list')
+ self._extract_and_populate_luns(attr_list.get_children())
+ tag = result.get_child_content('next-tag')
+ if tag is None:
+ break
+
+ def _find_mapped_lun_igroup(self, path, initiator, os=None):
+ """Find the igroup for mapped lun with initiator."""
+ initiator_igroups = self._get_igroup_by_initiator(initiator=initiator)
+ lun_maps = self._get_lun_map(path)
+ if initiator_igroups and lun_maps:
+ for igroup in initiator_igroups:
+ igroup_name = igroup['initiator-group-name']
+ if igroup_name.startswith(self.IGROUP_PREFIX):
+ for lun_map in lun_maps:
+ if lun_map['initiator-group'] == igroup_name:
+ return (igroup_name, lun_map['lun-id'])
+ return (None, None)
+
+ def _get_lun_map(self, path):
+ """Gets the lun map by lun path."""
+ tag = None
+ map_list = []
+ while True:
+ lun_map_iter = NaElement('lun-map-get-iter')
+ lun_map_iter.add_new_child('max-records', '100')
+ if tag:
+ lun_map_iter.add_new_child('tag', tag, True)
+ query = NaElement('query')
+ lun_map_iter.add_child_elem(query)
+ query.add_node_with_children('lun-map-info', **{'path': path})
+ result = self._invoke_successfully(lun_map_iter, True)
+ tag = result.get_child_content('next-tag')
+ 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')
+ lun_maps = attr_list.get_children()
+ for lun_map in lun_maps:
+ lun_m = dict()
+ lun_m['initiator-group'] = lun_map.get_child_content(
+ 'initiator-group')
+ lun_m['lun-id'] = lun_map.get_child_content('lun-id')
+ lun_m['vserver'] = lun_map.get_child_content('vserver')
+ map_list.append(lun_m)
+ if tag is None:
+ break
+ return map_list
+
+ def _get_igroup_by_initiator(self, initiator):
+ """Get igroups by initiator."""
+ tag = None
+ igroup_list = []
+ while True:
+ igroup_iter = NaElement('igroup-get-iter')
+ igroup_iter.add_new_child('max-records', '100')
+ if tag:
+ igroup_iter.add_new_child('tag', tag, True)
+ query = NaElement('query')
+ igroup_iter.add_child_elem(query)
+ igroup_info = NaElement('initiator-group-info')
+ query.add_child_elem(igroup_info)
+ igroup_info.add_new_child('vserver', self.vserver)
+ initiators = NaElement('initiators')
+ igroup_info.add_child_elem(initiators)
+ initiators.add_node_with_children('initiator-info',
+ **{'initiator-name': initiator})
+ des_attrs = NaElement('desired-attributes')
+ des_ig_info = NaElement('initiator-group-info')
+ des_attrs.add_child_elem(des_ig_info)
+ des_ig_info.add_node_with_children('initiators',
+ **{'initiator-info': None})
+ des_ig_info.add_new_child('vserver', None)
+ des_ig_info.add_new_child('initiator-group-name', None)
+ des_ig_info.add_new_child('initiator-group-type', None)
+ des_ig_info.add_new_child('initiator-group-os-type', None)
+ igroup_iter.add_child_elem(des_attrs)
+ result = self._invoke_successfully(igroup_iter, None)
+ tag = result.get_child_content('next-tag')
+ if result.get_child_content('num-records') and\
+ int(result.get_child_content('num-records')) > 0:
+ attr_list = result.get_child_by_name('attributes-list')
+ igroups = attr_list.get_children()
+ for igroup in igroups:
+ ig = dict()
+ ig['initiator-group-os-type'] = igroup.get_child_content(
+ 'initiator-group-os-type')
+ ig['initiator-group-type'] = igroup.get_child_content(
+ 'initiator-group-type')
+ ig['initiator-group-name'] = igroup.get_child_content(
+ 'initiator-group-name')
+ igroup_list.append(ig)
+ if tag is None:
+ break
+ return igroup_list
+
+ def _clone_lun(self, name, new_name, space_reserved):
+ """Clone LUN with the given handle to the new name."""
+ metadata = self._get_lun_attr(name, 'metadata')
+ volume = metadata['Volume']
+ clone_create = NaElement.create_node_with_children(
+ 'clone-create',
+ **{'volume': volume, 'source-path': name,
+ 'destination-path': new_name,
+ 'space-reserve': space_reserved})
+ self._invoke_successfully(clone_create, True)
+ LOG.debug(_("Cloned LUN with new name %s") % new_name)
+ lun = self._get_lun_by_args(vserver=self.vserver, path='/vol/%s/%s'
+ % (volume, new_name))
+ if len(lun) == 0:
+ msg = _("No clonned lun named %s found on the filer")
+ raise exception.VolumeBackendAPIException(data=msg % (new_name))
+ clone_meta = self._create_lun_meta(lun[0])
+ self._add_lun_to_table(NetAppLun('%s:%s' % (clone_meta['Vserver'],
+ clone_meta['Path']),
+ new_name,
+ lun[0].get_child_content('size'),
+ clone_meta))
+
+ def _get_lun_by_args(self, **args):
+ """Retrives lun with specified args."""
+ lun_iter = NaElement('lun-get-iter')
+ lun_iter.add_new_child('max-records', '100')
+ query = NaElement('query')
+ lun_iter.add_child_elem(query)
+ query.add_node_with_children('lun-info', **args)
+ luns = self._invoke_successfully(lun_iter)
+ attr_list = luns.get_child_by_name('attributes-list')
+ return attr_list.get_children()
+
+ def _create_lun_meta(self, lun):
+ """Creates lun metadata dictionary."""
+ self._is_naelement(lun)
+ meta_dict = {}
+ self._is_naelement(lun)
+ meta_dict['Vserver'] = lun.get_child_content('vserver')
+ meta_dict['Volume'] = lun.get_child_content('volume')
+ meta_dict['Qtree'] = lun.get_child_content('qtree')
+ meta_dict['Path'] = lun.get_child_content('path')
+ meta_dict['OsType'] = lun.get_child_content('multiprotocol-type')
+ meta_dict['SpaceReserved'] = \
+ lun.get_child_content('is-space-reservation-enabled')
+ return meta_dict
+
+ def _configure_tunneling(self, do_tunneling=False):
+ """Configures tunneling for ontap cluster."""
+ if do_tunneling:
+ self.client.set_vserver(self.vserver)
+ else:
+ self.client.set_vserver(None)
+
+ def _update_volume_status(self):
+ """Retrieve status info from volume group."""
+
+ LOG.debug(_("Updating volume status"))
+ data = {}
+ data["volume_backend_name"] = 'NetApp_iSCSI_Cluster_direct'
+ data["vendor_name"] = 'NetApp'
+ data["driver_version"] = '1.0'
+ data["storage_protocol"] = 'iSCSI'
+
+ data['total_capacity_gb'] = 'infinite'
+ data['free_capacity_gb'] = 'infinite'
+ data['reserved_percentage'] = 100
+ data['QoS_support'] = False
+ self._stats = data
+
+
+class NetAppDirect7modeISCSIDriver(NetAppDirectISCSIDriver):
+ """NetApp 7-mode iSCSI volume driver."""
+
+ def __init__(self, *args, **kwargs):
+ super(NetAppDirect7modeISCSIDriver, self).__init__(*args, **kwargs)
+
+ def _do_custom_setup(self):
+ """Does custom setup depending on the type of filer."""
+ self.vfiler = FLAGS.netapp_vfiler
+ if self.vfiler:
+ (major, minor) = self._get_ontapi_version()
+ self.client.set_api_version(major, minor)
+
+ def _get_avl_volume_by_size(self, size):
+ """Get the available volume by size."""
+ vol_request = NaElement('volume-list-info')
+ res = self._invoke_successfully(vol_request, True)
+ volumes = res.get_child_by_name('volumes')
+ vols = volumes.get_children()
+ for vol in vols:
+ avl_size = vol.get_child_content('size-available')
+ state = vol.get_child_content('state')
+ if float(avl_size) >= float(size) and state == 'online':
+ avl_vol = dict()
+ avl_vol['name'] = vol.get_child_content('name')
+ avl_vol['block-type'] = vol.get_child_content('block-type')
+ avl_vol['type'] = vol.get_child_content('type')
+ avl_vol['size-available'] = avl_size
+ if self._check_vol_not_root(avl_vol):
+ return avl_vol
+ return None
+
+ def _check_vol_not_root(self, vol):
+ """Checks if a volume is not root."""
+ vol_options = NaElement.create_node_with_children(
+ 'volume-options-list-info', **{'volume': vol['name']})
+ result = self._invoke_successfully(vol_options, True)
+ options = result.get_child_by_name('options')
+ ops = options.get_children()
+ for op in ops:
+ if op.get_child_content('name') == 'root' and\
+ op.get_child_content('value') == 'true':
+ return False
+ return True
+
+ def _get_igroup_by_initiator(self, initiator):
+ """Get igroups by initiator."""
+ igroup_list = NaElement('igroup-list-info')
+ result = self._invoke_successfully(igroup_list, True)
+ igroups = []
+ igs = result.get_child_by_name('initiator-groups')
+ if igs:
+ ig_infos = igs.get_children()
+ if ig_infos:
+ for info in ig_infos:
+ initiators = info.get_child_by_name('initiators')
+ init_infos = initiators.get_children()
+ if init_infos:
+ for init in init_infos:
+ if init.get_child_content('initiator-name')\
+ == initiator:
+ d = dict()
+ d['initiator-group-os-type'] = \
+ info.get_child_content(
+ 'initiator-group-os-type')
+ d['initiator-group-type'] = \
+ info.get_child_content(
+ 'initiator-group-type')
+ d['initiator-group-name'] = \
+ info.get_child_content(
+ 'initiator-group-name')
+ igroups.append(d)
+ return igroups
+
+ def _get_target_details(self):
+ """Gets the target portal details."""
+ iscsi_if_iter = NaElement('iscsi-portal-list-info')
+ result = self._invoke_successfully(iscsi_if_iter, True)
+ tgt_list = []
+ portal_list_entries = result.get_child_by_name(
+ 'iscsi-portal-list-entries')
+ if portal_list_entries:
+ portal_list = portal_list_entries.get_children()
+ for iscsi_if in portal_list:
+ d = dict()
+ d['address'] = iscsi_if.get_child_content('ip-address')
+ d['port'] = iscsi_if.get_child_content('ip-port')
+ d['tpgroup-tag'] = iscsi_if.get_child_content('tpgroup-tag')
+ tgt_list.append(d)
+ return tgt_list
+
+ def _get_iscsi_service_details(self):
+ """Returns iscsi iqn."""
+ iscsi_service_iter = NaElement('iscsi-node-get-name')
+ result = self._invoke_successfully(iscsi_service_iter, True)
+ return result.get_child_content('node-name')
+
+ def _create_lun_handle(self, metadata):
+ """Returns lun handle based on filer type."""
+ if self.vfiler:
+ owner = '%s:%s' % (FLAGS.netapp_server_hostname, self.vfiler)
+ else:
+ owner = FLAGS.netapp_server_hostname
+ return '%s:%s' % (owner, metadata['Path'])
+
+ def _get_lun_list(self):
+ """Gets the list of luns on filer."""
+ api = NaElement('lun-list-info')
+ result = self._invoke_successfully(api, True)
+ luns = result.get_child_by_name('luns')
+ self._extract_and_populate_luns(luns.get_children())
+
+ def _find_mapped_lun_igroup(self, path, initiator, os=None):
+ """Find the igroup for mapped lun with initiator."""
+ lun_map_list = NaElement.create_node_with_children(
+ 'lun-map-list-info',
+ **{'path': path})
+ result = self._invoke_successfully(lun_map_list, True)
+ igroups = result.get_child_by_name('initiator-groups')
+ if igroups:
+ igroup = None
+ lun_id = None
+ found = False
+ igroup_infs = igroups.get_children()
+ for ig in igroup_infs:
+ initiators = ig.get_child_by_name('initiators')
+ init_infs = initiators.get_children()
+ for info in init_infs:
+ if info.get_child_content('initiator-name') == initiator:
+ found = True
+ igroup = ig.get_child_content('initiator-group-name')
+ lun_id = ig.get_child_content('lun-id')
+ break
+ if found:
+ break
+ return (igroup, lun_id)
+
+ def _clone_lun(self, name, new_name, space_reserved):
+ """Clone LUN with the given handle to the new name."""
+ metadata = self._get_lun_attr(name, 'metadata')
+ path = metadata['Path']
+ (parent, splitter, name) = path.rpartition('/')
+ clone_path = '%s/%s' % (parent, new_name)
+ clone_start = NaElement.create_node_with_children(
+ 'clone-start',
+ **{'source-path': path, 'destination-path': clone_path,
+ 'no-snap': 'true'})
+ result = self._invoke_successfully(clone_start, True)
+ 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')
+ if vol_uuid:
+ self._check_clone_status(clone_id, vol_uuid, name, new_name)
+ cloned_lun = self._get_lun_by_args(path=clone_path)
+ if cloned_lun:
+ self._set_space_reserve(clone_path, space_reserved)
+ clone_meta = self._create_lun_meta(cloned_lun)
+ handle = self._create_lun_handle(clone_meta)
+ self._add_lun_to_table(
+ NetAppLun(handle, new_name,
+ cloned_lun.get_child_content('size'),
+ clone_meta))
+ else:
+ raise NaApiError('ENOLUNENTRY', 'No Lun entry found on the filer')
+
+ def _set_space_reserve(self, path, enable):
+ """Sets the space reserve info."""
+ space_res = NaElement.create_node_with_children(
+ 'lun-set-space-reservation-info',
+ **{'path': path, 'enable': enable})
+ self._invoke_successfully(space_res, True)
+
+ def _check_clone_status(self, clone_id, vol_uuid, name, new_name):
+ """Checks for the job till completed."""
+ clone_status = NaElement('clone-list-status')
+ cl_id = NaElement('clone-id')
+ clone_status.add_child_elem(cl_id)
+ cl_id.add_node_with_children(
+ 'clone-id-info',
+ **{'clone-op-id': clone_id, 'volume-uuid': vol_uuid})
+ running = True
+ clone_ops_info = None
+ while running:
+ result = self._invoke_successfully(clone_status, True)
+ status = result.get_child_by_name('status')
+ ops_info = status.get_children()
+ if ops_info:
+ for info in ops_info:
+ if info.get_child_content('clone-state') == 'running':
+ time.sleep(1)
+ break
+ else:
+ running = False
+ clone_ops_info = info
+ break
+ else:
+ if clone_ops_info:
+ if clone_ops_info.get_child_content('clone-state')\
+ == 'completed':
+ LOG.debug(_("Clone operation with src %(name)s"
+ " and dest %(new_name)s completed") % locals())
+ else:
+ LOG.debug(_("Clone operation with src %(name)s"
+ " and dest %(new_name)s failed") % locals())
+ raise NaApiError(
+ clone_ops_info.get_child_content('error'),
+ clone_ops_info.get_child_content('reason'))
+
+ def _get_lun_by_args(self, **args):
+ """Retrives lun with specified args."""
+ lun_info = NaElement.create_node_with_children('lun-list-info', **args)
+ result = self._invoke_successfully(lun_info, True)
+ luns = result.get_child_by_name('luns')
+ if luns:
+ infos = luns.get_children()
+ if infos:
+ return infos[0]
+ return None
+
+ def _create_lun_meta(self, lun):
+ """Creates lun metadata dictionary."""
+ self._is_naelement(lun)
+ meta_dict = {}
+ self._is_naelement(lun)
+ meta_dict['Path'] = lun.get_child_content('path')
+ meta_dict['OsType'] = lun.get_child_content('multiprotocol-type')
+ meta_dict['SpaceReserved'] = lun.get_child_content(
+ 'is-space-reservation-enabled')
+ return meta_dict
+
+ def _configure_tunneling(self, do_tunneling=False):
+ """Configures tunneling for 7 mode."""
+ if do_tunneling:
+ self.client.set_vfiler(self.vfiler)
+ else:
+ self.client.set_vfiler(None)
+
+ def _update_volume_status(self):
+ """Retrieve status info from volume group."""
+
+ LOG.debug(_("Updating volume status"))
+ data = {}
+ data["volume_backend_name"] = 'NetApp_iSCSI_7mode_direct'
+ data["vendor_name"] = 'NetApp'
+ data["driver_version"] = '1.0'
+ data["storage_protocol"] = 'iSCSI'
+
+ data['total_capacity_gb'] = 'infinite'
+ data['free_capacity_gb'] = 'infinite'
+ data['reserved_percentage'] = 100
+ data['QoS_support'] = False
+ self._stats = data
diff --git a/cinder/volume/drivers/netapp/nfs.py b/cinder/volume/drivers/netapp/nfs.py
new file mode 100644
index 00000000000..0afa5af2643
--- /dev/null
+++ b/cinder/volume/drivers/netapp/nfs.py
@@ -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_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 = ('%s/%s'
+ '%s/%s')
+
+ 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 = (''
+ ''
+ '%d'
+ ''
+ ''
+ '')
+
+ 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 = '%s'
+
+ 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
diff --git a/cinder/volume/drivers/netapp_nfs.py b/cinder/volume/drivers/netapp_nfs.py
deleted file mode 100644
index 37880d25aba..00000000000
--- a/cinder/volume/drivers/netapp_nfs.py
+++ /dev/null
@@ -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_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 = ('%s/%s'
- '%s/%s')
-
- 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 = (''
- ''
- '%d'
- ''
- ''
- '')
-
- 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 = '%s'
-
- 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)
diff --git a/cinder/volume/manager.py b/cinder/volume/manager.py
index 41cfb66aa47..2b88d9a6168 100644
--- a/cinder/volume/manager.py
+++ b/cinder/volume/manager.py
@@ -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':