diff options
| author | Ben Swartzlander <bswartz@netapp.com> | 2012-09-01 23:39:39 -0400 |
|---|---|---|
| committer | Ben Swartzlander <bswartz@netapp.com> | 2012-09-12 12:43:44 -0400 |
| commit | 772c5d47d5bdffcd4ff8e09f4116d22568bf6eb9 (patch) | |
| tree | c2a11bc6bb2dbe9e78e1f6f603e86042a5bb5087 /nova/tests | |
| parent | 76d094eeba1bcbba16d24e40aea24bb7729b4a30 (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.py | 389 | ||||
| -rw-r--r-- | nova/tests/test_netapp_nfs.py | 261 | ||||
| -rw-r--r-- | nova/tests/test_nfs.py | 629 |
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() |
