From: Ben Swartzlander Date: Sun, 12 Aug 2012 04:43:05 +0000 (-0400) Subject: Add C-mode driver for NetApp. X-Git-Url: https://review.fuel-infra.org/gitweb?a=commitdiff_plain;h=6454235a27f6c167290ef55a8262277f10d0febf;p=openstack-build%2Fcinder-build.git Add C-mode driver for NetApp. blueprint netapp-volume-driver-cmode Change-Id: I1eb418d05f557068bc0d4f359e19721c9c61068b --- diff --git a/cinder/tests/test_netapp.py b/cinder/tests/test_netapp.py index fc47b5988..cbf0e57f3 100644 --- a/cinder/tests/test_netapp.py +++ b/cinder/tests/test_netapp.py @@ -31,7 +31,6 @@ from cinder.volume import netapp LOG = logging.getLogger("cinder.volume.driver") - WSDL_HEADER = """ + +""" + +WSDL_TYPES_CMODE = """ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + """ + +WSDL_TRAILER_CMODE = """ + + + + +""" + +RESPONSE_PREFIX_CMODE = """ + +""" + +RESPONSE_SUFFIX_CMODE = """""" + +CMODE_APIS = ['ProvisionLun', 'DestroyLun', 'CloneLun', 'MapLun', 'UnmapLun', + 'ListLuns', 'GetLunTargetDetails'] + + +class FakeCMODEServerHandler(BaseHTTPServer.BaseHTTPRequestHandler): + """HTTP handler that fakes enough stuff to allow the driver to run""" + + def do_GET(s): + """Respond to a GET request.""" + if '/ntap_cloud.wsdl' != s.path: + s.send_response(404) + s.end_headers + return + s.send_response(200) + s.send_header("Content-Type", "application/wsdl+xml") + s.end_headers() + out = s.wfile + out.write(WSDL_HEADER_CMODE) + out.write(WSDL_TYPES_CMODE) + for api in CMODE_APIS: + out.write('' % api) + out.write('' % api) + out.write('') + out.write('' % api) + out.write('' % api) + out.write('') + out.write('') + for api in CMODE_APIS: + out.write('' % api) + out.write('' % api) + out.write('' % api) + out.write('') + out.write('') + out.write('') + out.write('') + for api in CMODE_APIS: + out.write('' % api) + out.write('') + out.write('') + out.write('') + out.write('') + out.write('') + out.write(WSDL_TRAILER_CMODE) + + def do_POST(s): + """Respond to a POST request.""" + if '/ws/ntapcloud' != s.path: + s.send_response(404) + s.end_headers + return + request_xml = s.rfile.read(int(s.headers['Content-Length'])) + ntap_ns = 'http://cloud.netapp.com/' + nsmap = {'soapenv': 'http://schemas.xmlsoap.org/soap/envelope/', + 'na': ntap_ns} + root = etree.fromstring(request_xml) + + body = root.xpath('/soapenv:Envelope/soapenv:Body', + namespaces=nsmap)[0] + request = body.getchildren()[0] + tag = request.tag + if not tag.startswith('{' + ntap_ns + '}'): + s.send_response(500) + s.end_headers + return + api = tag[(2 + len(ntap_ns)):] + if 'ProvisionLun' == api: + body = """ + lun120 + 1d9c006c-a406-42f6-a23f-5ed7a6dc33e3 + OsType + linux + """ + elif 'DestroyLun' == api: + body = """""" + elif 'CloneLun' == api: + body = """ + lun22 + 98ea1791d228453899d422b4611642c3 + OsType + linux + """ + elif 'MapLun' == api: + body = """""" + elif 'Unmap' == api: + body = """""" + elif 'ListLuns' == api: + body = """ + + lun1 + 20 + asdjdnsd + + """ + elif 'GetLunTargetDetails' == api: + body = """ + +
1.2.3.4
+ 3260 + 1000 + iqn.199208.com.netapp:sn.123456789 + 0 +
+
""" + 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_CMODE) + s.wfile.write(body) + s.wfile.write(RESPONSE_SUFFIX_CMODE) + + +class FakeCmodeHTTPConnection(object): + """A fake httplib.HTTPConnection for netapp tests + + Requests made via this connection actually get translated and routed into + the fake Dfm 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 + FakeCMODEServerHandler.address_string = lambda x: '127.0.0.1' + self.app = FakeCMODEServerHandler(sock, '127.0.0.1:8080', 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 NetAppCmodeISCSIDriverTestCase(test.TestCase): + """Test case for NetAppISCSIDriver""" + volume = { + 'name': 'lun1', 'size': 1, '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', + '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(NetAppCmodeISCSIDriverTestCase, self).setUp() + driver = netapp.NetAppCmodeISCSIDriver() + self.stubs.Set(httplib, 'HTTPConnection', FakeCmodeHTTPConnection) + driver._create_client(wsdl_url='http://localhost:8080/ntap_cloud.wsdl', + login='root', password='password', + hostname='localhost', port=8080, cache=False) + self.driver = driver + + def test_connect(self): + self.driver.check_for_setup_error() + + def test_create_destroy(self): + self.driver.create_volume(self.volume) + self.driver.delete_volume(self.volume) + + def test_create_vol_snapshot_destroy(self): + self.driver.create_volume(self.volume) + self.driver.create_snapshot(self.snapshot) + self.driver.create_volume_from_snapshot(self.volume_sec, self.snapshot) + self.driver.delete_snapshot(self.snapshot) + self.driver.delete_volume(self.volume) + + def test_map_unmap(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 = {'initiator': 'init1'} + connection_info = self.driver.initialize_connection(self.volume, + connector) + self.assertEqual(connection_info['driver_volume_type'], 'iscsi') + properties = connection_info['data'] + self.driver.terminate_connection(self.volume, connector) + self.driver.delete_volume(self.volume) diff --git a/cinder/volume/netapp.py b/cinder/volume/netapp.py index 70f253698..5299ff26c 100644 --- a/cinder/volume/netapp.py +++ b/cinder/volume/netapp.py @@ -996,3 +996,297 @@ class NetAppISCSIDriver(driver.ISCSIDriver): def check_for_export(self, context, volume_id): raise NotImplementedError() + + +class NetAppLun(object): + """Represents a LUN on NetApp storage.""" + + def __init__(self, handle, name, size, metadata_dict): + self.handle = handle + self.name = name + self.size = size + self.metadata = metadata_dict + + def get_metadata_property(self, prop): + """Get the metadata property of a LUN.""" + if prop in self.metadata: + return self.metadata[prop] + name = self.name + msg = _("No metadata property %(prop)s defined for the LUN %(name)s") + LOG.debug(msg % locals()) + + +class NetAppCmodeISCSIDriver(driver.ISCSIDriver): + """NetApp C-mode iSCSI volume driver.""" + + def __init__(self, *args, **kwargs): + super(NetAppCmodeISCSIDriver, self).__init__(*args, **kwargs) + self.lun_table = {} + + def _create_client(self, **kwargs): + """Instantiate a web services client. + + This method creates a "suds" client to make web services calls to the + DFM server. Note that the WSDL file is quite large and may take + a few seconds to parse. + """ + wsdl_url = kwargs['wsdl_url'] + LOG.debug(_('Using WSDL: %s') % wsdl_url) + if kwargs['cache']: + self.client = client.Client(wsdl_url, username=kwargs['login'], + password=kwargs['password']) + else: + self.client = client.Client(wsdl_url, username=kwargs['login'], + password=kwargs['password'], + cache=None) + + def _check_flags(self): + """Ensure that the flags we care about are set.""" + 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): + 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 the suds (web services) + client. + """ + self._check_flags() + self._create_client(wsdl_url=FLAGS.netapp_wsdl_url, + login=FLAGS.netapp_login, password=FLAGS.netapp_password, + hostname=FLAGS.netapp_server_hostname, + port=FLAGS.netapp_server_port, cache=True) + + 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 = {} + luns = self.client.service.ListLuns() + for lun in luns: + meta_dict = {} + if hasattr(lun, 'Metadata'): + meta_dict = self._create_dict_from_meta(lun.Metadata) + discovered_lun = NetAppLun(lun.Handle, lun.Name, lun.Size, + meta_dict) + self._add_lun_to_table(discovered_lun) + 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) + 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 + server = self.client.service + metadata = self._create_metadata_list(extra_args) + lun = server.ProvisionLun(Name=name, Size=size, + Metadata=metadata) + LOG.debug(_("Created LUN with name %s") % name) + self._add_lun_to_table(NetAppLun(lun.Handle, lun.Name, + lun.Size, self._create_dict_from_meta(lun.Metadata))) + + def delete_volume(self, volume): + """Driver entry point for destroying existing volumes.""" + name = volume['name'] + handle = self._get_lun_handle(name) + self.client.service.DestroyLun(Handle=handle) + LOG.debug(_("Destroyed LUN %s") % handle) + 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_handle(volume['name']) + 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_handle(volume['name']) + 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'] + handle = volume['provider_location'] + server = self.client.service + server.MapLun(Handle=handle, InitiatorType="iscsi", + InitiatorName=initiator_name) + msg = _("Mapped LUN %(handle)s to the initiator %(initiator_name)s") + LOG.debug(msg % locals()) + + target_details_list = server.GetLunTargetDetails(Handle=handle, + InitiatorType="iscsi", InitiatorName=initiator_name) + msg = _("Succesfully fetched target details for LUN %(handle)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 % handle) + 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 % handle) + iqn = target_details.Iqn + if not iqn: + msg = _('Failed to get target IQN for the LUN %s') + raise exception.VolumeBackendAPIException(data=msg % handle) + + 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'] = target_details.LunNumber + 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 terminate_connection(self, volume, connector): + """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'] + handle = volume['provider_location'] + self.client.service.UnmapLun(Handle=handle, InitiatorType="iscsi", + InitiatorName=initiator_name) + msg = _("Unmapped LUN %(handle)s from the initiator " + "%(initiator_name)s") + LOG.debug(msg % locals()) + + 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] + extra_args = {'SpaceReserved': False} + self._clone_lun(lun.handle, snapshot_name, extra_args) + + def delete_snapshot(self, snapshot): + """Driver entry point for deleting a snapshot.""" + handle = self._get_lun_handle(snapshot['name']) + self.client.service.DestroyLun(Handle=handle) + LOG.debug(_("Destroyed LUN %s") % handle) + + 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. + """ + snapshot_name = snapshot['name'] + lun = self.lun_table[snapshot_name] + 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(lun.handle, new_name, extra_args) + + def check_for_export(self, context, volume_id): + raise NotImplementedError() + + 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, handle, new_name, extra_args): + """Clone LUN with the given handle to the new name.""" + server = self.client.service + metadata = self._create_metadata_list(extra_args) + lun = server.CloneLun(Handle=handle, NewName=new_name, + Metadata=metadata) + LOG.debug(_("Cloned LUN with new name %s") % new_name) + self._add_lun_to_table(NetAppLun(lun.Handle, lun.Name, + lun.Size, self._create_dict_from_meta(lun.Metadata))) + + def _create_metadata_list(self, extra_args): + """Creates metadata from kwargs.""" + metadata = [] + for key in extra_args.keys(): + meta = self.client.factory.create("Metadata") + meta.Key = key + meta.Value = extra_args[key] + metadata.append(meta) + return metadata + + def _get_lun_handle(self, name): + """Get the details for a LUN from our cache table.""" + if not name in self.lun_table: + LOG.warn(_("Could not find handle for LUN named %s") % name) + return None + return self.lun_table[name] + + def _create_dict_from_meta(self, metadata): + """Creates dictionary from metadata array.""" + meta_dict = {} + if not metadata: + return meta_dict + for meta in metadata: + meta_dict[meta.Key] = meta.Value + return meta_dict