summaryrefslogtreecommitdiffstats
path: root/nova/tests
diff options
context:
space:
mode:
authorBen Swartzlander <bswartz@netapp.com>2012-09-01 23:39:39 -0400
committerBen Swartzlander <bswartz@netapp.com>2012-09-12 12:43:44 -0400
commit772c5d47d5bdffcd4ff8e09f4116d22568bf6eb9 (patch)
treec2a11bc6bb2dbe9e78e1f6f603e86042a5bb5087 /nova/tests
parent76d094eeba1bcbba16d24e40aea24bb7729b4a30 (diff)
Backport changes from Cinder to Nova-Volume
NetApp C-mode driver. Generic NFS-based block device driver. NetApp NFS-based block device driver. blueprint netapp-volume-driver-cmode blueprint nfs-files-as-virtual-block-devices blueprint netapp-nfs-cinder-driver bug 1037619 bug 1037622 Change-Id: I513c3f88bcb03f3b71a453f92f5912d7730a8bbc
Diffstat (limited to 'nova/tests')
-rw-r--r--nova/tests/test_netapp.py389
-rw-r--r--nova/tests/test_netapp_nfs.py261
-rw-r--r--nova/tests/test_nfs.py629
3 files changed, 1279 insertions, 0 deletions
diff --git a/nova/tests/test_netapp.py b/nova/tests/test_netapp.py
index 1fd95308d..79a8526ee 100644
--- a/nova/tests/test_netapp.py
+++ b/nova/tests/test_netapp.py
@@ -989,3 +989,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)
diff --git a/nova/tests/test_netapp_nfs.py b/nova/tests/test_netapp_nfs.py
new file mode 100644
index 000000000..2a0b4ffde
--- /dev/null
+++ b/nova/tests/test_netapp_nfs.py
@@ -0,0 +1,261 @@
+# 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.
+"""Unit tests for the NetApp-specific NFS driver module (netapp_nfs)"""
+
+from nova import context
+from nova import exception
+from nova import test
+
+from nova.volume import netapp
+from nova.volume import netapp_nfs
+from nova.volume import nfs
+
+from mox import IgnoreArg
+from mox import IsA
+from mox import MockObject
+
+import mox
+import suds
+import types
+
+
+class FakeVolume(object):
+ def __init__(self, size=0):
+ self.size = size
+ self.id = hash(self)
+ self.name = None
+
+ def __getitem__(self, key):
+ return self.__dict__[key]
+
+
+class FakeSnapshot(object):
+ def __init__(self, volume_size=0):
+ self.volume_name = None
+ self.name = None
+ self.volume_id = None
+ self.volume_size = volume_size
+ self.user_id = None
+ self.status = None
+
+ def __getitem__(self, key):
+ return self.__dict__[key]
+
+
+class FakeResponce(object):
+ def __init__(self, status):
+ """
+ :param status: Either 'failed' or 'passed'
+ """
+ self.Status = status
+
+ if status == 'failed':
+ self.Reason = 'Sample error'
+
+
+class NetappNfsDriverTestCase(test.TestCase):
+ """Test case for NetApp specific NFS clone driver"""
+
+ def setUp(self):
+ self._driver = netapp_nfs.NetAppNFSDriver()
+ self._mox = mox.Mox()
+
+ 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.NovaException,
+ drv.check_for_setup_error)
+
+ # set required flags
+ for flag in required_flags:
+ setattr(netapp.FLAGS, flag, 'val')
+
+ mox.StubOutWithMock(nfs.NfsDriver, 'check_for_setup_error')
+ nfs.NfsDriver.check_for_setup_error()
+ mox.ReplayAll()
+
+ drv.check_for_setup_error()
+
+ mox.VerifyAll()
+
+ # restore initial FLAGS
+ for flag in required_flags:
+ delattr(netapp.FLAGS, flag)
+
+ def test_do_setup(self):
+ mox = self._mox
+ drv = self._driver
+
+ mox.StubOutWithMock(drv, 'check_for_setup_error')
+ mox.StubOutWithMock(netapp_nfs.NetAppNFSDriver, '_get_client')
+
+ drv.check_for_setup_error()
+ netapp_nfs.NetAppNFSDriver._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.NovaException,
+ 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)
+
+ # ApiProxy() method is generated by ServiceSelector at runtime from the
+ # XML, so mocking is impossible.
+ setattr(drv._client.service,
+ 'ApiProxy',
+ types.MethodType(lambda *args, **kwargs: FakeResponce(status),
+ suds.client.ServiceSelector))
+ mox.StubOutWithMock(drv, '_get_host_id')
+ mox.StubOutWithMock(drv, '_get_full_export_path')
+
+ drv._get_host_id(IgnoreArg()).AndReturn('10')
+ drv._get_full_export_path(IgnoreArg(), IgnoreArg()).AndReturn('/nfs')
+
+ return mox
+
+ def test_successfull_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_failed_clone_volume(self):
+ drv = self._driver
+ mox = self._prepare_clone_mock('failed')
+
+ mox.ReplayAll()
+
+ volume_name = 'volume_name'
+ clone_name = 'clone_name'
+ volume_id = volume_name + str(hash(volume_name))
+
+ self.assertRaises(exception.NovaException,
+ drv._clone_volume,
+ volume_name, clone_name, volume_id)
+
+ mox.VerifyAll()
diff --git a/nova/tests/test_nfs.py b/nova/tests/test_nfs.py
new file mode 100644
index 000000000..8a931b081
--- /dev/null
+++ b/nova/tests/test_nfs.py
@@ -0,0 +1,629 @@
+# 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.
+"""Unit tests for the NFS driver module"""
+
+import __builtin__
+import errno
+import os
+
+import mox as mox_lib
+from mox import IgnoreArg
+from mox import IsA
+from mox import stubout
+
+from nova import context
+from nova import exception
+from nova.exception import ProcessExecutionError
+from nova import test
+
+from nova.volume import nfs
+
+
+class DumbVolume(object):
+ fields = {}
+
+ def __setitem__(self, key, value):
+ self.fields[key] = value
+
+ def __getitem__(self, item):
+ return self.fields[item]
+
+
+class NfsDriverTestCase(test.TestCase):
+ """Test case for NFS driver"""
+
+ TEST_NFS_EXPORT1 = 'nfs-host1:/export'
+ TEST_NFS_EXPORT2 = 'nfs-host2:/export'
+ TEST_SIZE_IN_GB = 1
+ TEST_MNT_POINT = '/mnt/nfs'
+ TEST_MNT_POINT_BASE = '/mnt/test'
+ TEST_LOCAL_PATH = '/mnt/nfs/volume-123'
+ TEST_FILE_NAME = 'test.txt'
+ TEST_SHARES_CONFIG_FILE = '/etc/cinder/test-shares.conf'
+ ONE_GB_IN_BYTES = 1024 * 1024 * 1024
+
+ def setUp(self):
+ self._driver = nfs.NfsDriver()
+ self._mox = mox_lib.Mox()
+ self.stubs = stubout.StubOutForTesting()
+
+ def tearDown(self):
+ self._mox.UnsetStubs()
+ self.stubs.UnsetAll()
+
+ def stub_out_not_replaying(self, obj, attr_name):
+ attr_to_replace = getattr(obj, attr_name)
+ stub = mox_lib.MockObject(attr_to_replace)
+ self.stubs.Set(obj, attr_name, stub)
+
+ def test_path_exists_should_return_true(self):
+ """_path_exists should return True if stat returns 0"""
+ mox = self._mox
+ drv = self._driver
+
+ mox.StubOutWithMock(drv, '_execute')
+ drv._execute('stat', self.TEST_FILE_NAME, run_as_root=True)
+
+ mox.ReplayAll()
+
+ self.assertTrue(drv._path_exists(self.TEST_FILE_NAME))
+
+ mox.VerifyAll()
+
+ def test_path_exists_should_return_false(self):
+ """_path_exists should return True if stat doesn't return 0"""
+ mox = self._mox
+ drv = self._driver
+
+ mox.StubOutWithMock(drv, '_execute')
+ drv._execute('stat', self.TEST_FILE_NAME, run_as_root=True).\
+ AndRaise(ProcessExecutionError(
+ stderr="stat: cannot stat `test.txt': No such file or directory"))
+
+ mox.ReplayAll()
+
+ self.assertFalse(drv._path_exists(self.TEST_FILE_NAME))
+
+ mox.VerifyAll()
+
+ def test_local_path(self):
+ """local_path common use case"""
+ nfs.FLAGS.nfs_mount_point_base = self.TEST_MNT_POINT_BASE
+ drv = self._driver
+
+ volume = DumbVolume()
+ volume['provider_location'] = self.TEST_NFS_EXPORT1
+ volume['name'] = 'volume-123'
+
+ self.assertEqual('/mnt/test/12118957640568004265/volume-123',
+ drv.local_path(volume))
+
+ def test_mount_nfs_should_mount_correctly(self):
+ """_mount_nfs common case usage"""
+ mox = self._mox
+ drv = self._driver
+
+ mox.StubOutWithMock(drv, '_path_exists')
+ drv._path_exists(self.TEST_MNT_POINT).AndReturn(True)
+
+ mox.StubOutWithMock(drv, '_execute')
+ drv._execute('mount', '-t', 'nfs', self.TEST_NFS_EXPORT1,
+ self.TEST_MNT_POINT, run_as_root=True)
+
+ mox.ReplayAll()
+
+ drv._mount_nfs(self.TEST_NFS_EXPORT1, self.TEST_MNT_POINT)
+
+ mox.VerifyAll()
+
+ def test_mount_nfs_should_suppress_already_mounted_error(self):
+ """_mount_nfs should suppress already mounted error if ensure=True
+ """
+ mox = self._mox
+ drv = self._driver
+
+ mox.StubOutWithMock(drv, '_path_exists')
+ drv._path_exists(self.TEST_MNT_POINT).AndReturn(True)
+
+ mox.StubOutWithMock(drv, '_execute')
+ drv._execute('mount', '-t', 'nfs', self.TEST_NFS_EXPORT1,
+ self.TEST_MNT_POINT, run_as_root=True).\
+ AndRaise(ProcessExecutionError(
+ stderr='is busy or already mounted'))
+
+ mox.ReplayAll()
+
+ drv._mount_nfs(self.TEST_NFS_EXPORT1, self.TEST_MNT_POINT, ensure=True)
+
+ mox.VerifyAll()
+
+ def test_mount_nfs_should_reraise_already_mounted_error(self):
+ """_mount_nfs should not suppress already mounted error if ensure=False
+ """
+ mox = self._mox
+ drv = self._driver
+
+ mox.StubOutWithMock(drv, '_path_exists')
+ drv._path_exists(self.TEST_MNT_POINT).AndReturn(True)
+
+ mox.StubOutWithMock(drv, '_execute')
+ drv._execute('mount', '-t', 'nfs', self.TEST_NFS_EXPORT1,
+ self.TEST_MNT_POINT, run_as_root=True).\
+ AndRaise(ProcessExecutionError(stderr='is busy or already mounted'))
+
+ mox.ReplayAll()
+
+ self.assertRaises(ProcessExecutionError, drv._mount_nfs,
+ self.TEST_NFS_EXPORT1, self.TEST_MNT_POINT,
+ ensure=False)
+
+ mox.VerifyAll()
+
+ def test_mount_nfs_should_create_mountpoint_if_not_yet(self):
+ """_mount_nfs should create mountpoint if it doesn't exist"""
+ mox = self._mox
+ drv = self._driver
+
+ mox.StubOutWithMock(drv, '_path_exists')
+ drv._path_exists(self.TEST_MNT_POINT).AndReturn(False)
+
+ mox.StubOutWithMock(drv, '_execute')
+ drv._execute('mkdir', '-p', self.TEST_MNT_POINT)
+ drv._execute(*([IgnoreArg()] * 5), run_as_root=IgnoreArg())
+
+ mox.ReplayAll()
+
+ drv._mount_nfs(self.TEST_NFS_EXPORT1, self.TEST_MNT_POINT)
+
+ mox.VerifyAll()
+
+ def test_mount_nfs_should_not_create_mountpoint_if_already(self):
+ """_mount_nfs should not create mountpoint if it already exists"""
+ mox = self._mox
+ drv = self._driver
+
+ mox.StubOutWithMock(drv, '_path_exists')
+ drv._path_exists(self.TEST_MNT_POINT).AndReturn(True)
+
+ mox.StubOutWithMock(drv, '_execute')
+ drv._execute(*([IgnoreArg()] * 5), run_as_root=IgnoreArg())
+
+ mox.ReplayAll()
+
+ drv._mount_nfs(self.TEST_NFS_EXPORT1, self.TEST_MNT_POINT)
+
+ mox.VerifyAll()
+
+ def test_get_hash_str(self):
+ """_get_hash_str should calculation correct value"""
+ drv = self._driver
+
+ self.assertEqual('12118957640568004265',
+ drv._get_hash_str(self.TEST_NFS_EXPORT1))
+
+ def test_get_mount_point_for_share(self):
+ """_get_mount_point_for_share should calculate correct value"""
+ drv = self._driver
+
+ nfs.FLAGS.nfs_mount_point_base = self.TEST_MNT_POINT_BASE
+
+ self.assertEqual('/mnt/test/12118957640568004265',
+ drv._get_mount_point_for_share(self.TEST_NFS_EXPORT1))
+
+ def test_get_available_capacity_with_df(self):
+ """_get_available_capacity should calculate correct value"""
+ mox = self._mox
+ drv = self._driver
+
+ df_avail = 1490560
+ df_head = 'Filesystem 1K-blocks Used Available Use% Mounted on\n'
+ df_data = 'nfs-host:/export 2620544 996864 %d 41%% /mnt' % df_avail
+ df_output = df_head + df_data
+
+ setattr(nfs.FLAGS, 'nfs_disk_util', 'df')
+
+ mox.StubOutWithMock(drv, '_get_mount_point_for_share')
+ drv._get_mount_point_for_share(self.TEST_NFS_EXPORT1).\
+ AndReturn(self.TEST_MNT_POINT)
+
+ mox.StubOutWithMock(drv, '_execute')
+ drv._execute('df', '-P', '-B', '1', self.TEST_MNT_POINT,
+ run_as_root=True).AndReturn((df_output, None))
+
+ mox.ReplayAll()
+
+ self.assertEquals(df_avail,
+ drv._get_available_capacity(self.TEST_NFS_EXPORT1))
+
+ mox.VerifyAll()
+
+ delattr(nfs.FLAGS, 'nfs_disk_util')
+
+ def test_get_available_capacity_with_du(self):
+ """_get_available_capacity should calculate correct value"""
+ mox = self._mox
+ drv = self._driver
+
+ setattr(nfs.FLAGS, 'nfs_disk_util', 'du')
+
+ df_total_size = 2620544
+ df_used_size = 996864
+ df_avail_size = 1490560
+ df_title = 'Filesystem 1-blocks Used Available Use% Mounted on\n'
+ df_mnt_data = 'nfs-host:/export %d %d %d 41%% /mnt' % (df_total_size,
+ df_used_size,
+ df_avail_size)
+ df_output = df_title + df_mnt_data
+
+ du_used = 490560
+ du_output = '%d /mnt' % du_used
+
+ mox.StubOutWithMock(drv, '_get_mount_point_for_share')
+ drv._get_mount_point_for_share(self.TEST_NFS_EXPORT1).\
+ AndReturn(self.TEST_MNT_POINT)
+
+ mox.StubOutWithMock(drv, '_execute')
+ drv._execute('df', '-P', '-B', '1', self.TEST_MNT_POINT,
+ run_as_root=True).\
+ AndReturn((df_output, None))
+ drv._execute('du', '-sb', '--apparent-size',
+ '--exclude', '*snapshot*',
+ self.TEST_MNT_POINT,
+ run_as_root=True).AndReturn((du_output, None))
+
+ mox.ReplayAll()
+
+ self.assertEquals(df_total_size - du_used,
+ drv._get_available_capacity(self.TEST_NFS_EXPORT1))
+
+ mox.VerifyAll()
+
+ delattr(nfs.FLAGS, 'nfs_disk_util')
+
+ def test_load_shares_config(self):
+ mox = self._mox
+ drv = self._driver
+
+ nfs.FLAGS.nfs_shares_config = self.TEST_SHARES_CONFIG_FILE
+
+ mox.StubOutWithMock(__builtin__, 'open')
+ config_data = []
+ config_data.append(self.TEST_NFS_EXPORT1)
+ config_data.append('#' + self.TEST_NFS_EXPORT2)
+ config_data.append('')
+ __builtin__.open(self.TEST_SHARES_CONFIG_FILE).AndReturn(config_data)
+ mox.ReplayAll()
+
+ shares = drv._load_shares_config()
+
+ self.assertEqual([self.TEST_NFS_EXPORT1], shares)
+
+ mox.VerifyAll()
+
+ def test_ensure_share_mounted(self):
+ """_ensure_share_mounted simple use case"""
+ mox = self._mox
+ drv = self._driver
+
+ mox.StubOutWithMock(drv, '_get_mount_point_for_share')
+ drv._get_mount_point_for_share(self.TEST_NFS_EXPORT1).\
+ AndReturn(self.TEST_MNT_POINT)
+
+ mox.StubOutWithMock(drv, '_mount_nfs')
+ drv._mount_nfs(self.TEST_NFS_EXPORT1, self.TEST_MNT_POINT, ensure=True)
+
+ mox.ReplayAll()
+
+ drv._ensure_share_mounted(self.TEST_NFS_EXPORT1)
+
+ mox.VerifyAll()
+
+ def test_ensure_shares_mounted_should_save_mounting_successfully(self):
+ """_ensure_shares_mounted should save share if mounted with success"""
+ mox = self._mox
+ drv = self._driver
+
+ mox.StubOutWithMock(drv, '_load_shares_config')
+ drv._load_shares_config().AndReturn([self.TEST_NFS_EXPORT1])
+ mox.StubOutWithMock(drv, '_ensure_share_mounted')
+ drv._ensure_share_mounted(self.TEST_NFS_EXPORT1)
+
+ mox.ReplayAll()
+
+ drv._ensure_shares_mounted()
+
+ self.assertEqual(1, len(drv._mounted_shares))
+ self.assertEqual(self.TEST_NFS_EXPORT1, drv._mounted_shares[0])
+
+ mox.VerifyAll()
+
+ def test_ensure_shares_mounted_should_not_save_mounting_with_error(self):
+ """_ensure_shares_mounted should not save share if failed to mount"""
+ mox = self._mox
+ drv = self._driver
+
+ mox.StubOutWithMock(drv, '_load_shares_config')
+ drv._load_shares_config().AndReturn([self.TEST_NFS_EXPORT1])
+ mox.StubOutWithMock(drv, '_ensure_share_mounted')
+ drv._ensure_share_mounted(self.TEST_NFS_EXPORT1).AndRaise(Exception())
+
+ mox.ReplayAll()
+
+ drv._ensure_shares_mounted()
+
+ self.assertEqual(0, len(drv._mounted_shares))
+
+ mox.VerifyAll()
+
+ def test_setup_should_throw_error_if_shares_config_not_configured(self):
+ """do_setup should throw error if shares config is not configured """
+ drv = self._driver
+
+ nfs.FLAGS.nfs_shares_config = self.TEST_SHARES_CONFIG_FILE
+
+ self.assertRaises(exception.NfsException,
+ drv.do_setup, IsA(context.RequestContext))
+
+ def test_setup_should_throw_exception_if_nfs_client_is_not_installed(self):
+ """do_setup should throw error if nfs client is not installed """
+ mox = self._mox
+ drv = self._driver
+
+ nfs.FLAGS.nfs_shares_config = self.TEST_SHARES_CONFIG_FILE
+
+ mox.StubOutWithMock(os.path, 'exists')
+ os.path.exists(self.TEST_SHARES_CONFIG_FILE).AndReturn(True)
+ mox.StubOutWithMock(drv, '_execute')
+ drv._execute('mount.nfs', check_exit_code=False).\
+ AndRaise(OSError(errno.ENOENT, 'No such file or directory'))
+
+ mox.ReplayAll()
+
+ self.assertRaises(exception.NfsException,
+ drv.do_setup, IsA(context.RequestContext))
+
+ mox.VerifyAll()
+
+ def test_find_share_should_throw_error_if_there_is_no_mounted_shares(self):
+ """_find_share should throw error if there is no mounted shares"""
+ drv = self._driver
+
+ drv._mounted_shares = []
+
+ self.assertRaises(exception.NotFound, drv._find_share,
+ self.TEST_SIZE_IN_GB)
+
+ def test_find_share(self):
+ """_find_share simple use case"""
+ mox = self._mox
+ drv = self._driver
+
+ drv._mounted_shares = [self.TEST_NFS_EXPORT1, self.TEST_NFS_EXPORT2]
+
+ mox.StubOutWithMock(drv, '_get_available_capacity')
+ drv._get_available_capacity(self.TEST_NFS_EXPORT1).\
+ AndReturn(2 * self.ONE_GB_IN_BYTES)
+ drv._get_available_capacity(self.TEST_NFS_EXPORT2).\
+ AndReturn(3 * self.ONE_GB_IN_BYTES)
+
+ mox.ReplayAll()
+
+ self.assertEqual(self.TEST_NFS_EXPORT2,
+ drv._find_share(self.TEST_SIZE_IN_GB))
+
+ mox.VerifyAll()
+
+ def test_find_share_should_throw_error_if_there_is_no_enough_place(self):
+ """_find_share should throw error if there is no share to host vol"""
+ mox = self._mox
+ drv = self._driver
+
+ drv._mounted_shares = [self.TEST_NFS_EXPORT1, self.TEST_NFS_EXPORT2]
+
+ mox.StubOutWithMock(drv, '_get_available_capacity')
+ drv._get_available_capacity(self.TEST_NFS_EXPORT1).\
+ AndReturn(0)
+ drv._get_available_capacity(self.TEST_NFS_EXPORT2).\
+ AndReturn(0)
+
+ mox.ReplayAll()
+
+ self.assertRaises(exception.NfsNoSuitableShareFound, drv._find_share,
+ self.TEST_SIZE_IN_GB)
+
+ mox.VerifyAll()
+
+ def _simple_volume(self):
+ volume = DumbVolume()
+ volume['provider_location'] = '127.0.0.1:/mnt'
+ volume['name'] = 'volume_name'
+ volume['size'] = 10
+
+ return volume
+
+ def test_create_sparsed_volume(self):
+ mox = self._mox
+ drv = self._driver
+ volume = self._simple_volume()
+
+ setattr(nfs.FLAGS, 'nfs_sparsed_volumes', True)
+
+ mox.StubOutWithMock(drv, '_create_sparsed_file')
+ mox.StubOutWithMock(drv, '_set_rw_permissions_for_all')
+
+ drv._create_sparsed_file(IgnoreArg(), IgnoreArg())
+ drv._set_rw_permissions_for_all(IgnoreArg())
+
+ mox.ReplayAll()
+
+ drv._do_create_volume(volume)
+
+ mox.VerifyAll()
+
+ delattr(nfs.FLAGS, 'nfs_sparsed_volumes')
+
+ def test_create_nonsparsed_volume(self):
+ mox = self._mox
+ drv = self._driver
+ volume = self._simple_volume()
+
+ setattr(nfs.FLAGS, 'nfs_sparsed_volumes', False)
+
+ mox.StubOutWithMock(drv, '_create_regular_file')
+ mox.StubOutWithMock(drv, '_set_rw_permissions_for_all')
+
+ drv._create_regular_file(IgnoreArg(), IgnoreArg())
+ drv._set_rw_permissions_for_all(IgnoreArg())
+
+ mox.ReplayAll()
+
+ drv._do_create_volume(volume)
+
+ mox.VerifyAll()
+
+ delattr(nfs.FLAGS, 'nfs_sparsed_volumes')
+
+ def test_create_volume_should_ensure_nfs_mounted(self):
+ """create_volume should ensure shares provided in config are mounted"""
+ mox = self._mox
+ drv = self._driver
+
+ self.stub_out_not_replaying(nfs, 'LOG')
+ self.stub_out_not_replaying(drv, '_find_share')
+ self.stub_out_not_replaying(drv, '_do_create_volume')
+
+ mox.StubOutWithMock(drv, '_ensure_shares_mounted')
+ drv._ensure_shares_mounted()
+
+ mox.ReplayAll()
+
+ volume = DumbVolume()
+ volume['size'] = self.TEST_SIZE_IN_GB
+ drv.create_volume(volume)
+
+ mox.VerifyAll()
+
+ def test_create_volume_should_return_provider_location(self):
+ """create_volume should return provider_location with found share """
+ mox = self._mox
+ drv = self._driver
+
+ self.stub_out_not_replaying(nfs, 'LOG')
+ self.stub_out_not_replaying(drv, '_ensure_shares_mounted')
+ self.stub_out_not_replaying(drv, '_do_create_volume')
+
+ mox.StubOutWithMock(drv, '_find_share')
+ drv._find_share(self.TEST_SIZE_IN_GB).AndReturn(self.TEST_NFS_EXPORT1)
+
+ mox.ReplayAll()
+
+ volume = DumbVolume()
+ volume['size'] = self.TEST_SIZE_IN_GB
+ result = drv.create_volume(volume)
+ self.assertEqual(self.TEST_NFS_EXPORT1, result['provider_location'])
+
+ mox.VerifyAll()
+
+ def test_delete_volume(self):
+ """delete_volume simple test case"""
+ mox = self._mox
+ drv = self._driver
+
+ self.stub_out_not_replaying(drv, '_ensure_share_mounted')
+
+ volume = DumbVolume()
+ volume['name'] = 'volume-123'
+ volume['provider_location'] = self.TEST_NFS_EXPORT1
+
+ mox.StubOutWithMock(drv, 'local_path')
+ drv.local_path(volume).AndReturn(self.TEST_LOCAL_PATH)
+
+ mox.StubOutWithMock(drv, '_path_exists')
+ drv._path_exists(self.TEST_LOCAL_PATH).AndReturn(True)
+
+ mox.StubOutWithMock(drv, '_execute')
+ drv._execute('rm', '-f', self.TEST_LOCAL_PATH, run_as_root=True)
+
+ mox.ReplayAll()
+
+ drv.delete_volume(volume)
+
+ mox.VerifyAll()
+
+ def test_delete_should_ensure_share_mounted(self):
+ """delete_volume should ensure that corresponding share is mounted"""
+ mox = self._mox
+ drv = self._driver
+
+ self.stub_out_not_replaying(drv, '_execute')
+
+ volume = DumbVolume()
+ volume['name'] = 'volume-123'
+ volume['provider_location'] = self.TEST_NFS_EXPORT1
+
+ mox.StubOutWithMock(drv, '_ensure_share_mounted')
+ drv._ensure_share_mounted(self.TEST_NFS_EXPORT1)
+
+ mox.ReplayAll()
+
+ drv.delete_volume(volume)
+
+ mox.VerifyAll()
+
+ def test_delete_should_not_delete_if_provider_location_not_provided(self):
+ """delete_volume shouldn't try to delete if provider_location missed"""
+ mox = self._mox
+ drv = self._driver
+
+ self.stub_out_not_replaying(drv, '_ensure_share_mounted')
+
+ volume = DumbVolume()
+ volume['name'] = 'volume-123'
+ volume['provider_location'] = None
+
+ mox.StubOutWithMock(drv, '_execute')
+
+ mox.ReplayAll()
+
+ drv.delete_volume(volume)
+
+ mox.VerifyAll()
+
+ def test_delete_should_not_delete_if_there_is_no_file(self):
+ """delete_volume should not try to delete if file missed"""
+ mox = self._mox
+ drv = self._driver
+
+ self.stub_out_not_replaying(drv, '_ensure_share_mounted')
+
+ volume = DumbVolume()
+ volume['name'] = 'volume-123'
+ volume['provider_location'] = self.TEST_NFS_EXPORT1
+
+ mox.StubOutWithMock(drv, 'local_path')
+ drv.local_path(volume).AndReturn(self.TEST_LOCAL_PATH)
+
+ mox.StubOutWithMock(drv, '_path_exists')
+ drv._path_exists(self.TEST_LOCAL_PATH).AndReturn(False)
+
+ mox.StubOutWithMock(drv, '_execute')
+
+ mox.ReplayAll()
+
+ drv.delete_volume(volume)
+
+ mox.VerifyAll()