1
# vim: tabstop=4 shiftwidth=4 softtabstop=4
3
# Copyright (c) 2012 NetApp, Inc.
6
# Licensed under the Apache License, Version 2.0 (the "License"); you may
7
# not use this file except in compliance with the License. You may obtain
8
# a copy of the License at
10
# http://www.apache.org/licenses/LICENSE-2.0
12
# Unless required by applicable law or agreed to in writing, software
13
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
14
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
15
# License for the specific language governing permissions and limitations
17
"""Unit tests for the NetApp-specific ssc module."""
22
from lxml import etree
24
from mox import IgnoreArg
26
from mox import MockObject
29
from cinder import context
30
from cinder import exception
31
from cinder import test
32
from cinder.volume import configuration as conf
33
from cinder.volume.drivers.netapp import api
34
from cinder.volume.drivers.netapp import ssc_utils
37
class FakeHTTPRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler):
38
"""HTTP handler that doesn't spam the log."""
40
def log_message(self, format, *args):
44
class FakeHttplibSocket(object):
45
"""A fake socket implementation for httplib.HTTPResponse."""
46
def __init__(self, value):
47
self._rbuffer = StringIO.StringIO(value)
48
self._wbuffer = StringIO.StringIO('')
49
oldclose = self._wbuffer.close
52
self.result = self._wbuffer.getvalue()
54
self._wbuffer.close = newclose
56
def makefile(self, mode, _other):
57
"""Returns the socket's internal buffer"""
58
if mode == 'r' or mode == 'rb':
60
if mode == 'w' or mode == 'wb':
64
RESPONSE_PREFIX_DIRECT_CMODE = """<?xml version='1.0' encoding='UTF-8' ?>
65
<!DOCTYPE netapp SYSTEM 'file:/etc/netapp_gx.dtd'>"""
67
RESPONSE_PREFIX_DIRECT = """
68
<netapp version='1.15' xmlns='http://www.netapp.com/filer/admin'>"""
70
RESPONSE_SUFFIX_DIRECT = """</netapp>"""
73
class FakeDirectCMODEServerHandler(FakeHTTPRequestHandler):
74
"""HTTP handler that fakes enough stuff to allow the driver to run."""
77
"""Respond to a GET request."""
78
if '/servlets/netapp.servlets.admin.XMLrequest_filer' != s.path:
83
s.send_header("Content-Type", "text/xml; charset=utf-8")
86
out.write('<netapp version="1.15">'
87
'<results reason="Not supported method type"'
88
' status="failed" errno="Not_Allowed"/></netapp>')
91
"""Respond to a POST request."""
92
if '/servlets/netapp.servlets.admin.XMLrequest_filer' != s.path:
96
request_xml = s.rfile.read(int(s.headers['Content-Length']))
97
root = etree.fromstring(request_xml)
98
body = [x for x in root.iterchildren()]
101
localname = etree.QName(tag).localname or tag
102
if 'volume-get-iter' == localname:
103
body = """<results status="passed"><attributes-list>
105
<volume-id-attributes>
107
<owning-vserver-name>Openstack</owning-vserver-name>
108
<containing-aggregate-name>aggr0
109
</containing-aggregate-name>
110
<junction-path>/iscsi</junction-path>
112
</volume-id-attributes>
113
<volume-space-attributes>
114
<size-available>214748364</size-available>
115
<size-total>224748364</size-total>
116
<space-guarantee-enabled>enabled</space-guarantee-enabled>
117
<space-guarantee>file</space-guarantee>
118
</volume-space-attributes>
119
<volume-state-attributes>
120
<is-cluster-volume>true
122
<is-vserver-root>false</is-vserver-root>
123
<state>online</state>
124
<is-inconsistent>false</is-inconsistent>
125
<is-invalid>false</is-invalid>
126
<is-junction-active>true</is-junction-active>
127
</volume-state-attributes>
130
<volume-id-attributes>
132
<owning-vserver-name>Openstack
133
</owning-vserver-name>
134
<containing-aggregate-name>aggr0
135
</containing-aggregate-name>
136
<junction-path>/nfs</junction-path>
138
</volume-id-attributes>
139
<volume-space-attributes>
140
<size-available>14748364</size-available>
141
<size-total>24748364</size-total>
142
<space-guarantee-enabled>enabled
143
</space-guarantee-enabled>
144
<space-guarantee>volume</space-guarantee>
145
</volume-space-attributes>
146
<volume-state-attributes>
147
<is-cluster-volume>true
149
<is-vserver-root>false</is-vserver-root>
150
<state>online</state>
151
<is-inconsistent>false</is-inconsistent>
152
<is-invalid>false</is-invalid>
153
<is-junction-active>true</is-junction-active>
154
</volume-state-attributes>
157
<volume-id-attributes>
159
<owning-vserver-name>Openstack
160
</owning-vserver-name>
161
<containing-aggregate-name>aggr0
162
</containing-aggregate-name>
163
<junction-path>/nfs2</junction-path>
165
</volume-id-attributes>
166
<volume-space-attributes>
167
<size-available>14748364</size-available>
168
<size-total>24748364</size-total>
169
<space-guarantee-enabled>enabled
170
</space-guarantee-enabled>
171
<space-guarantee>volume</space-guarantee>
172
</volume-space-attributes>
173
<volume-state-attributes>
174
<is-cluster-volume>true
176
<is-vserver-root>false</is-vserver-root>
177
<state>online</state>
178
<is-inconsistent>true</is-inconsistent>
179
<is-invalid>true</is-invalid>
180
<is-junction-active>true</is-junction-active>
181
</volume-state-attributes>
184
<volume-id-attributes>
186
<owning-vserver-name>Openstack
187
</owning-vserver-name>
188
<containing-aggregate-name>aggr0
189
</containing-aggregate-name>
190
<junction-path>/nfs3</junction-path>
192
</volume-id-attributes>
193
<volume-space-attributes>
194
<space-guarantee-enabled>enabled
195
</space-guarantee-enabled>
196
<space-guarantee>volume
198
</volume-space-attributes>
199
<volume-state-attributes>
200
<is-cluster-volume>true
202
<is-vserver-root>false</is-vserver-root>
203
<state>online</state>
204
<is-inconsistent>false</is-inconsistent>
205
<is-invalid>false</is-invalid>
206
<is-junction-active>true</is-junction-active>
207
</volume-state-attributes>
210
<num-records>4</num-records></results>"""
211
elif 'aggr-options-list-info' == localname:
212
body = """<results status="passed">
215
<name>ha_policy</name>
219
<name>raidtype</name>
220
<value>raid_dp</value>
224
elif 'sis-get-iter' == localname:
225
body = """<results status="passed">
228
<path>/vol/iscsi</path>
229
<is-compression-enabled>
231
</is-compression-enabled>
232
<state>enabled</state>
236
elif 'storage-disk-get-iter' == localname:
237
body = """<results status="passed">
241
<effective-disk-type>SATA</effective-disk-type>
252
s.send_header("Content-Type", "text/xml; charset=utf-8")
254
s.wfile.write(RESPONSE_PREFIX_DIRECT_CMODE)
255
s.wfile.write(RESPONSE_PREFIX_DIRECT)
257
s.wfile.write(RESPONSE_SUFFIX_DIRECT)
260
class FakeDirectCmodeHTTPConnection(object):
261
"""A fake httplib.HTTPConnection for netapp tests.
263
Requests made via this connection actually get translated and routed into
264
the fake direct handler above, we then turn the response into
265
the httplib.HTTPResponse that the caller expects.
267
def __init__(self, host, timeout=None):
270
def request(self, method, path, data=None, headers=None):
273
req_str = '%s %s HTTP/1.1\r\n' % (method, path)
274
for key, value in headers.iteritems():
275
req_str += "%s: %s\r\n" % (key, value)
277
req_str += '\r\n%s' % data
279
# NOTE(vish): normally the http transport normailizes from unicode
280
sock = FakeHttplibSocket(req_str.decode("latin-1").encode("utf-8"))
281
# NOTE(vish): stop the server from trying to look up address from
283
FakeDirectCMODEServerHandler.address_string = lambda x: '127.0.0.1'
284
self.app = FakeDirectCMODEServerHandler(sock, '127.0.0.1:80', None)
286
self.sock = FakeHttplibSocket(sock.result)
287
self.http_response = httplib.HTTPResponse(self.sock)
289
def set_debuglevel(self, level):
292
def getresponse(self):
293
self.http_response.begin()
294
return self.http_response
296
def getresponsebody(self):
297
return self.sock.result
300
def createNetAppVolume(**kwargs):
301
vol = ssc_utils.NetAppVolume(kwargs['name'], kwargs['vs'])
302
vol.state['vserver_root'] = kwargs.get('vs_root')
303
vol.state['status'] = kwargs.get('status')
304
vol.state['junction_active'] = kwargs.get('junc_active')
305
vol.space['size_avl_bytes'] = kwargs.get('avl_byt')
306
vol.space['size_total_bytes'] = kwargs.get('total_byt')
307
vol.space['space-guarantee-enabled'] = kwargs.get('sg_enabled')
308
vol.space['space-guarantee'] = kwargs.get('sg')
309
vol.space['thin_provisioned'] = kwargs.get('thin')
310
vol.mirror['mirrored'] = kwargs.get('mirrored')
311
vol.qos['qos_policy_group'] = kwargs.get('qos')
312
vol.aggr['name'] = kwargs.get('aggr_name')
313
vol.aggr['junction'] = kwargs.get('junction')
314
vol.sis['dedup'] = kwargs.get('dedup')
315
vol.sis['compression'] = kwargs.get('compression')
316
vol.aggr['raid_type'] = kwargs.get('raid')
317
vol.aggr['ha_policy'] = kwargs.get('ha')
318
vol.aggr['disk_type'] = kwargs.get('disk')
322
class SscUtilsTestCase(test.TestCase):
324
vol1 = createNetAppVolume(name='vola', vs='openstack',
325
vs_root=False, status='online', junc_active=True,
326
avl_byt='1000', total_byt='1500',
328
sg='file', thin=False, mirrored=False,
329
qos=None, aggr_name='aggr1', junction='/vola',
330
dedup=False, compression=False,
331
raid='raiddp', ha='cfo', disk='SSD')
333
vol2 = createNetAppVolume(name='volb', vs='openstack',
334
vs_root=False, status='online', junc_active=True,
335
avl_byt='2000', total_byt='2500',
337
sg='file', thin=True, mirrored=False,
338
qos=None, aggr_name='aggr2', junction='/volb',
339
dedup=True, compression=False,
340
raid='raid4', ha='cfo', disk='SSD')
342
vol3 = createNetAppVolume(name='volc', vs='openstack',
343
vs_root=False, status='online', junc_active=True,
344
avl_byt='3000', total_byt='3500',
346
sg='volume', thin=True, mirrored=False,
347
qos=None, aggr_name='aggr1', junction='/volc',
348
dedup=True, compression=True,
349
raid='raiddp', ha='cfo', disk='SAS')
351
vol4 = createNetAppVolume(name='vold', vs='openstack',
352
vs_root=False, status='online', junc_active=True,
353
avl_byt='4000', total_byt='4500',
355
sg='none', thin=False, mirrored=False,
356
qos=None, aggr_name='aggr1', junction='/vold',
357
dedup=False, compression=False,
358
raid='raiddp', ha='cfo', disk='SSD')
360
vol5 = createNetAppVolume(name='vole', vs='openstack',
361
vs_root=False, status='online', junc_active=True,
362
avl_byt='5000', total_byt='5500',
364
sg='none', thin=False, mirrored=True,
365
qos=None, aggr_name='aggr2', junction='/vole',
366
dedup=True, compression=False,
367
raid='raid4', ha='cfo', disk='SAS')
370
super(SscUtilsTestCase, self).setUp()
371
self.stubs.Set(httplib, 'HTTPConnection',
372
FakeDirectCmodeHTTPConnection)
374
def test_cl_vols_ssc_all(self):
375
"""Test cluster ssc for all vols."""
376
na_server = api.NaServer('127.0.0.1')
377
vserver = 'openstack'
378
test_vols = set([copy.deepcopy(self.vol1),
379
copy.deepcopy(self.vol2), copy.deepcopy(self.vol3)])
380
sis = {'vola': {'dedup': False, 'compression': False},
381
'volb': {'dedup': True, 'compression': False}}
383
self.mox.StubOutWithMock(ssc_utils, 'query_cluster_vols_for_ssc')
384
self.mox.StubOutWithMock(ssc_utils, 'get_sis_vol_dict')
385
self.mox.StubOutWithMock(ssc_utils, 'query_aggr_options')
386
self.mox.StubOutWithMock(ssc_utils, 'query_aggr_storage_disk')
387
ssc_utils.query_cluster_vols_for_ssc(
388
na_server, vserver, None).AndReturn(test_vols)
389
ssc_utils.get_sis_vol_dict(na_server, vserver, None).AndReturn(sis)
390
raiddp = {'ha_policy': 'cfo', 'raid_type': 'raiddp'}
391
ssc_utils.query_aggr_options(
392
na_server, IgnoreArg()).AndReturn(raiddp)
393
ssc_utils.query_aggr_storage_disk(
394
na_server, IgnoreArg()).AndReturn('SSD')
395
raid4 = {'ha_policy': 'cfo', 'raid_type': 'raid4'}
396
ssc_utils.query_aggr_options(
397
na_server, IgnoreArg()).AndReturn(raid4)
398
ssc_utils.query_aggr_storage_disk(
399
na_server, IgnoreArg()).AndReturn('SAS')
402
res_vols = ssc_utils.get_cluster_vols_with_ssc(
403
na_server, vserver, volume=None)
407
if vol.id['name'] == 'volc':
408
self.assertEqual(vol.sis['compression'], False)
409
self.assertEqual(vol.sis['dedup'], False)
413
def test_cl_vols_ssc_single(self):
414
"""Test cluster ssc for single vol."""
415
na_server = api.NaServer('127.0.0.1')
416
vserver = 'openstack'
417
test_vols = set([copy.deepcopy(self.vol1)])
418
sis = {'vola': {'dedup': False, 'compression': False}}
420
self.mox.StubOutWithMock(ssc_utils, 'query_cluster_vols_for_ssc')
421
self.mox.StubOutWithMock(ssc_utils, 'get_sis_vol_dict')
422
self.mox.StubOutWithMock(ssc_utils, 'query_aggr_options')
423
self.mox.StubOutWithMock(ssc_utils, 'query_aggr_storage_disk')
424
ssc_utils.query_cluster_vols_for_ssc(
425
na_server, vserver, 'vola').AndReturn(test_vols)
426
ssc_utils.get_sis_vol_dict(
427
na_server, vserver, 'vola').AndReturn(sis)
428
raiddp = {'ha_policy': 'cfo', 'raid_type': 'raiddp'}
429
ssc_utils.query_aggr_options(
430
na_server, 'aggr1').AndReturn(raiddp)
431
ssc_utils.query_aggr_storage_disk(na_server, 'aggr1').AndReturn('SSD')
434
res_vols = ssc_utils.get_cluster_vols_with_ssc(
435
na_server, vserver, volume='vola')
438
self.assertEqual(len(res_vols), 1)
440
def test_get_cluster_ssc(self):
441
"""Test get cluster ssc map."""
442
na_server = api.NaServer('127.0.0.1')
443
vserver = 'openstack'
445
[self.vol1, self.vol2, self.vol3, self.vol4, self.vol5])
447
self.mox.StubOutWithMock(ssc_utils, 'get_cluster_vols_with_ssc')
448
ssc_utils.get_cluster_vols_with_ssc(
449
na_server, vserver).AndReturn(test_vols)
452
res_map = ssc_utils.get_cluster_ssc(na_server, vserver)
455
self.assertEqual(len(res_map['mirrored']), 1)
456
self.assertEqual(len(res_map['dedup']), 3)
457
self.assertEqual(len(res_map['compression']), 1)
458
self.assertEqual(len(res_map['thin']), 2)
459
self.assertEqual(len(res_map['all']), 5)
461
def test_vols_for_boolean_specs(self):
462
"""Test ssc for boolean specs."""
464
[self.vol1, self.vol2, self.vol3, self.vol4, self.vol5])
465
ssc_map = {'mirrored': set([self.vol1]),
466
'dedup': set([self.vol1, self.vol2, self.vol3]),
467
'compression': set([self.vol3, self.vol4]),
468
'thin': set([self.vol5, self.vol2]), 'all': test_vols}
469
test_map = {'mirrored': ('netapp_mirrored', 'netapp_unmirrored'),
470
'dedup': ('netapp_dedup', 'netapp_nodedup'),
471
'compression': ('netapp_compression',
472
'netapp_nocompression'),
473
'thin': ('netapp_thin_provisioned',
474
'netapp_thick_provisioned')}
475
for type in test_map.keys():
477
extra_specs = {test_map[type][0]: 'true'}
478
res = ssc_utils.get_volumes_for_specs(ssc_map, extra_specs)
479
self.assertEqual(len(res), len(ssc_map[type]))
481
extra_specs = {test_map[type][1]: 'true'}
482
res = ssc_utils.get_volumes_for_specs(ssc_map, extra_specs)
483
self.assertEqual(len(res), len(ssc_map['all'] - ssc_map[type]))
486
{test_map[type][0]: 'true', test_map[type][1]: 'true'}
487
res = ssc_utils.get_volumes_for_specs(ssc_map, extra_specs)
488
self.assertEqual(len(res), len(ssc_map['all']))
490
def test_vols_for_optional_specs(self):
491
"""Test ssc for optional specs."""
493
set([self.vol1, self.vol2, self.vol3, self.vol4, self.vol5])
494
ssc_map = {'mirrored': set([self.vol1]),
495
'dedup': set([self.vol1, self.vol2, self.vol3]),
496
'compression': set([self.vol3, self.vol4]),
497
'thin': set([self.vol5, self.vol2]), 'all': test_vols}
499
{'netapp_dedup': 'true',
500
'netapp:raid_type': 'raid4', 'netapp:disk_type': 'SSD'}
501
res = ssc_utils.get_volumes_for_specs(ssc_map, extra_specs)
502
self.assertEqual(len(res), 1)
504
def test_query_cl_vols_for_ssc(self):
505
na_server = api.NaServer('127.0.0.1')
506
na_server.set_api_version(1, 15)
507
vols = ssc_utils.query_cluster_vols_for_ssc(na_server, 'Openstack')
508
self.assertEqual(len(vols), 2)
510
if vol.id['name'] != 'iscsi' or vol.id['name'] != 'nfsvol':
513
raise exception.InvalidVolume('Invalid volume returned.')
515
def test_query_aggr_options(self):
516
na_server = api.NaServer('127.0.0.1')
517
aggr_attribs = ssc_utils.query_aggr_options(na_server, 'aggr0')
519
self.assertEqual(aggr_attribs['ha_policy'], 'cfo')
520
self.assertEqual(aggr_attribs['raid_type'], 'raid_dp')
522
raise exception.InvalidParameterValue("Incorrect aggr options")
524
def test_query_aggr_storage_disk(self):
525
na_server = api.NaServer('127.0.0.1')
526
eff_disk_type = ssc_utils.query_aggr_storage_disk(na_server, 'aggr0')
527
self.assertEqual(eff_disk_type, 'SATA')