]> review.fuel-infra Code Review - openstack-build/cinder-build.git/commitdiff
Add C-mode driver for NetApp.
authorBen Swartzlander <bswartz@netapp.com>
Sun, 12 Aug 2012 04:43:05 +0000 (00:43 -0400)
committerBen Swartzlander <bswartz@netapp.com>
Sun, 12 Aug 2012 15:44:41 +0000 (11:44 -0400)
blueprint netapp-volume-driver-cmode

Change-Id: I1eb418d05f557068bc0d4f359e19721c9c61068b

cinder/tests/test_netapp.py
cinder/volume/netapp.py

index fc47b5988884282775a0b0b708ba27b8b99d7edf..cbf0e57f3ad12f482b388d16d75ec8d450054c04 100644 (file)
@@ -31,7 +31,6 @@ from cinder.volume import netapp
 
 LOG = logging.getLogger("cinder.volume.driver")
 
-
 WSDL_HEADER = """<?xml version="1.0" encoding="UTF-8" standalone="no"?>
 <definitions xmlns="http://schemas.xmlsoap.org/wsdl/"
     xmlns:na="http://www.netapp.com/management/v1"
@@ -988,3 +987,392 @@ class NetAppDriverTestCase(test.TestCase):
         properties = connection_info['data']
         self.driver.terminate_connection(volume, connector)
         self.driver._remove_destroy(self.VOLUME_NAME, self.PROJECT_ID)
+
+
+WSDL_HEADER_CMODE = """<?xml version="1.0" encoding="UTF-8"?>
+<definitions xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/"
+ xmlns:na="http://cloud.netapp.com/"
+xmlns:xsd="http://www.w3.org/2001/XMLSchema"
+xmlns="http://schemas.xmlsoap.org/wsdl/"
+targetNamespace="http://cloud.netapp.com/" name="CloudStorageService">
+"""
+
+WSDL_TYPES_CMODE = """<types>
+<xs:schema xmlns:na="http://cloud.netapp.com/"
+xmlns:xs="http://www.w3.org/2001/XMLSchema" version="1.0"
+targetNamespace="http://cloud.netapp.com/">
+
+      <xs:element name="ProvisionLun">
+        <xs:complexType>
+          <xs:all>
+            <xs:element name="Name" type="xs:string"/>
+            <xs:element name="Size" type="xsd:long"/>
+            <xs:element name="Metadata" type="na:Metadata" minOccurs="0"
+              maxOccurs="unbounded"/>
+          </xs:all>
+        </xs:complexType>
+      </xs:element>
+      <xs:element name="ProvisionLunResult">
+        <xs:complexType>
+          <xs:all>
+            <xs:element name="Lun" type="na:Lun"/>
+          </xs:all>
+        </xs:complexType>
+      </xs:element>
+
+      <xs:element name="DestroyLun">
+        <xs:complexType>
+          <xs:all>
+            <xs:element name="Handle" type="xsd:string"/>
+          </xs:all>
+        </xs:complexType>
+      </xs:element>
+      <xs:element name="DestroyLunResult">
+        <xs:complexType>
+          <xs:all/>
+        </xs:complexType>
+      </xs:element>
+
+      <xs:element name="CloneLun">
+        <xs:complexType>
+          <xs:all>
+            <xs:element name="Handle" type="xsd:string"/>
+            <xs:element name="NewName" type="xsd:string"/>
+            <xs:element name="Metadata" type="na:Metadata" minOccurs="0"
+              maxOccurs="unbounded"/>
+          </xs:all>
+        </xs:complexType>
+      </xs:element>
+      <xs:element name="CloneLunResult">
+        <xs:complexType>
+          <xs:all>
+            <xs:element name="Lun" type="na:Lun"/>
+          </xs:all>
+        </xs:complexType>
+      </xs:element>
+
+      <xs:element name="MapLun">
+        <xs:complexType>
+          <xs:all>
+            <xs:element name="Handle" type="xsd:string"/>
+            <xs:element name="InitiatorType" type="xsd:string"/>
+            <xs:element name="InitiatorName" type="xsd:string"/>
+          </xs:all>
+        </xs:complexType>
+      </xs:element>
+      <xs:element name="MapLunResult">
+        <xs:complexType>
+          <xs:all/>
+        </xs:complexType>
+      </xs:element>
+
+      <xs:element name="UnmapLun">
+        <xs:complexType>
+          <xs:all>
+            <xs:element name="Handle" type="xsd:string"/>
+            <xs:element name="InitiatorType" type="xsd:string"/>
+            <xs:element name="InitiatorName" type="xsd:string"/>
+          </xs:all>
+        </xs:complexType>
+      </xs:element>
+      <xs:element name="UnmapLunResult">
+        <xs:complexType>
+          <xs:all/>
+        </xs:complexType>
+      </xs:element>
+
+      <xs:element name="ListLuns">
+        <xs:complexType>
+          <xs:all>
+            <xs:element name="NameFilter" type="xsd:string" minOccurs="0"/>
+          </xs:all>
+        </xs:complexType>
+      </xs:element>
+      <xs:element name="ListLunsResult">
+        <xs:complexType>
+          <xs:all>
+            <xs:element name="Lun" type="na:Lun" minOccurs="0"
+              maxOccurs="unbounded"/>
+          </xs:all>
+        </xs:complexType>
+      </xs:element>
+
+      <xs:element name="GetLunTargetDetails">
+        <xs:complexType>
+          <xs:all>
+            <xs:element name="Handle" type="xsd:string"/>
+            <xs:element name="InitiatorType" type="xsd:string"/>
+            <xs:element name="InitiatorName" type="xsd:string"/>
+          </xs:all>
+        </xs:complexType>
+      </xs:element>
+      <xs:element name="GetLunTargetDetailsResult">
+        <xs:complexType>
+          <xs:all>
+            <xs:element name="TargetDetails" type="na:TargetDetails"
+              minOccurs="0" maxOccurs="unbounded"/>
+          </xs:all>
+        </xs:complexType>
+      </xs:element>
+
+      <xs:complexType name="Metadata">
+        <xs:sequence>
+          <xs:element name="Key" type="xs:string"/>
+          <xs:element name="Value" type="xs:string"/>
+        </xs:sequence>
+      </xs:complexType>
+
+      <xs:complexType name="Lun">
+        <xs:sequence>
+          <xs:element name="Name" type="xs:string"/>
+          <xs:element name="Size" type="xs:long"/>
+          <xs:element name="Handle" type="xs:string"/>
+          <xs:element name="Metadata" type="na:Metadata" minOccurs="0"
+            maxOccurs="unbounded"/>
+        </xs:sequence>
+      </xs:complexType>
+
+      <xs:complexType name="TargetDetails">
+        <xs:sequence>
+          <xs:element name="Address" type="xs:string"/>
+          <xs:element name="Port" type="xs:int"/>
+          <xs:element name="Portal" type="xs:int"/>
+          <xs:element name="Iqn" type="xs:string"/>
+          <xs:element name="LunNumber" type="xs:int"/>
+        </xs:sequence>
+      </xs:complexType>
+
+    </xs:schema></types>"""
+
+WSDL_TRAILER_CMODE = """<service name="CloudStorageService">
+    <port name="CloudStoragePort" binding="na:CloudStorageBinding">
+      <soap:address location="http://hostname:8080/ws/ntapcloud"/>
+    </port>
+  </service>
+</definitions>"""
+
+RESPONSE_PREFIX_CMODE = """<?xml version='1.0' encoding='UTF-8'?>
+<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/">
+<soapenv:Body>"""
+
+RESPONSE_SUFFIX_CMODE = """</soapenv:Body></soapenv:Envelope>"""
+
+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('<message name="%sRequest">' % api)
+            out.write('<part element="na:%s" name="req"/>' % api)
+            out.write('</message>')
+            out.write('<message name="%sResponse">' % api)
+            out.write('<part element="na:%sResult" name="res"/>' % api)
+            out.write('</message>')
+        out.write('<portType name="CloudStorage">')
+        for api in CMODE_APIS:
+            out.write('<operation name="%s">' % api)
+            out.write('<input message="na:%sRequest"/>' % api)
+            out.write('<output message="na:%sResponse"/>' % api)
+            out.write('</operation>')
+        out.write('</portType>')
+        out.write('<binding name="CloudStorageBinding" '
+                  'type="na:CloudStorage">')
+        out.write('<soap:binding style="document" ' +
+            'transport="http://schemas.xmlsoap.org/soap/http"/>')
+        for api in CMODE_APIS:
+            out.write('<operation name="%s">' % api)
+            out.write('<soap:operation soapAction=""/>')
+            out.write('<input><soap:body use="literal"/></input>')
+            out.write('<output><soap:body use="literal"/></output>')
+            out.write('</operation>')
+        out.write('</binding>')
+        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 = """<ns:ProvisionLunResult xmlns:ns=
+            "http://cloud.netapp.com/">
+            <Lun><Name>lun1</Name><Size>20</Size>
+             <Handle>1d9c006c-a406-42f6-a23f-5ed7a6dc33e3</Handle>
+            <Metadata><Key>OsType</Key>
+            <Value>linux</Value></Metadata></Lun>
+            </ns:ProvisionLunResult>"""
+        elif 'DestroyLun' == api:
+            body = """<ns:DestroyLunResult xmlns:ns="http://cloud.netapp.com/"
+             />"""
+        elif 'CloneLun' == api:
+            body = """<ns:CloneLunResult xmlns:ns="http://cloud.netapp.com/">
+                     <Lun><Name>lun2</Name><Size>2</Size>
+                     <Handle>98ea1791d228453899d422b4611642c3</Handle>
+                     <Metadata><Key>OsType</Key>
+                     <Value>linux</Value></Metadata>
+                     </Lun></ns:CloneLunResult>"""
+        elif 'MapLun' == api:
+            body = """<ns1:MapLunResult xmlns:ns="http://cloud.netapp.com/"
+             />"""
+        elif 'Unmap' == api:
+            body = """<ns1:UnmapLunResult xmlns:ns="http://cloud.netapp.com/"
+             />"""
+        elif 'ListLuns' == api:
+            body = """<ns:ListLunsResult xmlns:ns="http://cloud.netapp.com/">
+                 <Lun>
+                 <Name>lun1</Name>
+                 <Size>20</Size>
+                 <Handle>asdjdnsd</Handle>
+                 </Lun>
+                 </ns:ListLunsResult>"""
+        elif 'GetLunTargetDetails' == api:
+            body = """<ns:GetLunTargetDetailsResult
+            xmlns:ns="http://cloud.netapp.com/">
+                    <TargetDetail>
+                     <Address>1.2.3.4</Address>
+                     <Port>3260</Port>
+                     <Portal>1000</Portal>
+                     <Iqn>iqn.199208.com.netapp:sn.123456789</Iqn>
+                     <LunNumber>0</LunNumber>
+                    </TargetDetail>
+                    </ns:GetLunTargetDetailsResult>"""
+        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)
index 70f2536980d524a1bfc273c2759a5eeb03d024b5..5299ff26c092b13bf293614d80bafba6c6b851e5 100644 (file)
@@ -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