# 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. """ Tests for NetApp volume driver """ import BaseHTTPServer import httplib import StringIO from lxml import etree from nova import log as logging from nova import test from nova.volume import netapp LOG = logging.getLogger(__name__) WSDL_HEADER = """ """ WSDL_TYPES = """ """ WSDL_TRAILER = """ """ RESPONSE_PREFIX = """ """ RESPONSE_SUFFIX = """""" APIS = ['ApiProxy', 'DatasetListInfoIterStart', 'DatasetListInfoIterNext', 'DatasetListInfoIterEnd', 'DatasetEditBegin', 'DatasetEditCommit', 'DatasetProvisionMember', 'DatasetRemoveMember', 'DfmAbout', 'DpJobProgressEventListIterStart', 'DpJobProgressEventListIterNext', 'DpJobProgressEventListIterEnd', 'DatasetMemberListInfoIterStart', 'DatasetMemberListInfoIterNext', 'DatasetMemberListInfoIterEnd', 'HostListInfoIterStart', 'HostListInfoIterNext', 'HostListInfoIterEnd', 'LunListInfoIterStart', 'LunListInfoIterNext', 'LunListInfoIterEnd', 'StorageServiceDatasetProvision'] iter_count = 0 iter_table = {} class FakeDfmServerHandler(BaseHTTPServer.BaseHTTPRequestHandler): """HTTP handler that fakes enough stuff to allow the driver to run""" def do_GET(s): """Respond to a GET request.""" if '/dfm.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) out.write(WSDL_TYPES) for api in APIS: out.write('' % api) out.write('' % api) out.write('') out.write('' % api) out.write('' % api) out.write('') out.write('') for api in APIS: out.write('' % api) out.write('' % api) out.write('' % api) out.write('') out.write('') out.write('') out.write('') for api in APIS: out.write('' % api) out.write('' % api) out.write('') out.write('') out.write('') out.write('') out.write(WSDL_TRAILER) def do_POST(s): """Respond to a POST request.""" if '/apis/soap/v1' != s.path: s.send_response(404) s.end_headers return request_xml = s.rfile.read(int(s.headers['Content-Length'])) ntap_ns = 'http://www.netapp.com/management/v1' nsmap = {'env': 'http://schemas.xmlsoap.org/soap/envelope/', 'na': ntap_ns} root = etree.fromstring(request_xml) body = root.xpath('/env:Envelope/env: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)):] global iter_count global iter_table if 'DatasetListInfoIterStart' == api: body = """ 1 dataset """ elif 'DatasetListInfoIterNext' == api: body = """ 0 1 """ elif 'DatasetListInfoIterEnd' == api: body = """""" elif 'DatasetEditBegin' == api: body = """ 0 """ elif 'DatasetEditCommit' == api: body = """ false 0 """ elif 'DatasetProvisionMember' == api: body = """""" elif 'DatasetRemoveMember' == api: body = """""" elif 'DfmAbout' == api: body = """""" elif 'DpJobProgressEventListIterStart' == api: iter_name = 'dpjobprogress_%s' % iter_count iter_count = iter_count + 1 iter_table[iter_name] = 0 body = """ 2 %s """ % iter_name elif 'DpJobProgressEventListIterNext' == api: tags = body.xpath('na:DpJobProgressEventListIterNext/na:Tag', namespaces=nsmap) iter_name = tags[0].text if iter_table[iter_name]: body = """""" else: iter_table[iter_name] = 1 body = """ normal lun-create 0 normal job-end 2 """ elif 'DpJobProgressEventListIterEnd' == api: body = """""" elif 'DatasetMemberListInfoIterStart' == api: body = """ 1 dataset-member """ elif 'DatasetMemberListInfoIterNext' == api: name = 'filer:/OpenStack_testproj/volume-00000001/volume-00000001' body = """ 0 %s 1 """ % name elif 'DatasetMemberListInfoIterEnd' == api: body = """""" elif 'HostListInfoIterStart' == api: body = """ 1 host """ elif 'HostListInfoIterNext' == api: body = """ 1.2.3.4 0 filer 1 """ elif 'HostListInfoIterEnd' == api: body = """""" elif 'LunListInfoIterStart' == api: body = """ 1 lun """ elif 'LunListInfoIterNext' == api: path = 'OpenStack_testproj/volume-00000001/volume-00000001' body = """ 0 %s 1 """ % path elif 'LunListInfoIterEnd' == api: body = """""" elif 'ApiProxy' == api: names = body.xpath('na:ApiProxy/na:Request/na:Name', namespaces=nsmap) proxy = names[0].text if 'igroup-list-info' == proxy: igroup = 'openstack-iqn.1993-08.org.debian:01:23456789' initiator = 'iqn.1993-08.org.debian:01:23456789' proxy_body = """ %s iscsi linux %s """ % (igroup, initiator) elif 'igroup-create' == proxy: proxy_body = '' elif 'igroup-add' == proxy: proxy_body = '' elif 'lun-map-list-info' == proxy: proxy_body = '' elif 'lun-map' == proxy: proxy_body = '0' elif 'lun-unmap' == proxy: proxy_body = '' elif 'iscsi-portal-list-info' == proxy: proxy_body = """ 1.2.3.4 3260 1000 """ elif 'iscsi-node-get-name' == proxy: target = 'iqn.1992-08.com.netapp:sn.111111111' proxy_body = '%s' % target else: # Unknown proxy API s.send_response(500) s.end_headers return api = api + ':' + proxy proxy_header = '' proxy_trailer = """passed """ body = proxy_header + proxy_body + proxy_trailer 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) s.wfile.write(body) s.wfile.write(RESPONSE_SUFFIX) class FakeHttplibSocket(object): """A fake socket implementation for httplib.HTTPResponse""" def __init__(self, value): self._rbuffer = StringIO.StringIO(value) self._wbuffer = StringIO.StringIO('') oldclose = self._wbuffer.close def newclose(): self.result = self._wbuffer.getvalue() oldclose() self._wbuffer.close = newclose def makefile(self, mode, _other): """Returns the socket's internal buffer""" if mode == 'r' or mode == 'rb': return self._rbuffer if mode == 'w' or mode == 'wb': return self._wbuffer class FakeHTTPConnection(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 FakeDfmServerHandler.address_string = lambda x: '127.0.0.1' self.app = FakeDfmServerHandler(sock, '127.0.0.1:8088', 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 NetAppDriverTestCase(test.TestCase): """Test case for NetAppISCSIDriver""" STORAGE_SERVICE = 'Thin Provisioned Space for VMFS Datastores' PROJECT_ID = 'testproj' VOLUME_NAME = 'volume-00000001' VOLUME_SIZE = 2147483648L # 2 GB INITIATOR = 'iqn.1993-08.org.debian:01:23456789' def setUp(self): super(NetAppDriverTestCase, self).setUp() driver = netapp.NetAppISCSIDriver() self.stubs.Set(httplib, 'HTTPConnection', FakeHTTPConnection) driver._create_client('http://localhost:8088/dfm.wsdl', 'root', 'password', 'localhost', 8088) driver._set_storage_service(self.STORAGE_SERVICE) self.driver = driver def test_connect(self): self.driver.check_for_setup_error() def test_create_destroy(self): self.driver._provision(self.VOLUME_NAME, None, self.PROJECT_ID, self.VOLUME_SIZE) self.driver._remove_destroy(self.VOLUME_NAME, self.PROJECT_ID) def test_map_unmap(self): self.driver._provision(self.VOLUME_NAME, None, self.PROJECT_ID, self.VOLUME_SIZE) volume = {'name': self.VOLUME_NAME, 'project_id': self.PROJECT_ID, 'id': 0, 'provider_auth': None} updates = self.driver._get_export(volume) self.assertTrue(updates['provider_location']) volume['provider_location'] = updates['provider_location'] connector = {'initiator': self.INITIATOR} connection_info = self.driver.initialize_connection(volume, connector) self.assertEqual(connection_info['driver_volume_type'], 'iscsi') properties = connection_info['data'] self.driver.terminate_connection(volume, connector) self.driver._remove_destroy(self.VOLUME_NAME, self.PROJECT_ID)