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 + idle for 70:36:44 + 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 + idle for 89:19:59 + 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':