blob: 6e1a83e10f0cf0e836224f32424b737e6888e569 [file] [log] [blame]
Peter Pentchev7d659682022-06-23 14:20:39 +03001From 78a748a4abd7aa1aa56256222754a1206001af12 Mon Sep 17 00:00:00 2001
2From: Peter Penchev <openstack-dev@storpool.com>
3Date: Wed, 20 Apr 2022 15:47:39 +0300
4Subject: [PATCH 6/8] StorPool: create_cloned_volume() improvements
Peter Pentchev0be59a32022-06-06 09:09:57 +03005
Peter Pentchev7d659682022-06-23 14:20:39 +03006If the source and destination volumes are in the same StorPool template
7(as defined by either the volume type or the global config setting),
8forego the need to create the transient snapshot at all and use
9StorPool's "base this volume on that one" API call (which does the same
10thing internally, but much more efficiently and atomically).
11
12If the destination volume should be in a different StorPool template,
13then make sure that the transient snapshot is also in that template so
14that, if other volumes are cloned from the same source volume later,
15they can all use the same data underneath (the internal workings of
16StorPool will detect that all those snapshots are exactly the same and
17not duplicate any data in the destination template). This will avoid
18data duplication, sometimes with drastic results.
19
20Bump the minimum required version of the "storpool" third-party library
21for snapshotUpdate(template=...) support.
22
23Change-Id: Ib9bb76cf2e2f2b035b92e596b1ef185558b190d6
24---
25 .../unit/volume/drivers/test_storpool.py | 152 +++++++++++++++---
26 cinder/volume/drivers/storpool.py | 87 ++++++++--
27 driver-requirements.txt | 2 +-
28 ...torpool-clone-better-dca90f40c9273de9.yaml | 6 +
29 4 files changed, 211 insertions(+), 36 deletions(-)
30 create mode 100644 releasenotes/notes/storpool-clone-better-dca90f40c9273de9.yaml
Peter Pentchev0be59a32022-06-06 09:09:57 +030031
32diff --git a/cinder/tests/unit/volume/drivers/test_storpool.py b/cinder/tests/unit/volume/drivers/test_storpool.py
33index 843283db4..51db7f292 100644
34--- a/cinder/tests/unit/volume/drivers/test_storpool.py
35+++ b/cinder/tests/unit/volume/drivers/test_storpool.py
36@@ -1,4 +1,4 @@
37-# Copyright 2014 - 2017, 2019 - 2021 StorPool
38+# Copyright 2014 - 2017, 2019 - 2022 StorPool
39 # All Rights Reserved.
40 #
41 # Licensed under the Apache License, Version 2.0 (the "License"); you may
42@@ -14,6 +14,7 @@
43 # under the License.
44
45
46+import itertools
47 import re
48 import sys
49 from unittest import mock
50@@ -110,13 +111,33 @@ class MockAPI(object):
51 def snapshotCreate(self, vname, snap):
52 snapshots[snap['name']] = dict(volumes[vname])
53
54+ def snapshotUpdate(self, snap, data):
55+ sdata = snapshots[snap]
56+ sdata.update(data)
57+
58 def snapshotDelete(self, name):
59 del snapshots[name]
60
61 def volumeCreate(self, v):
62- if v['name'] in volumes:
63+ name = v['name']
64+ if name in volumes:
65 raise MockApiError('volume already exists')
66- volumes[v['name']] = v
67+ data = dict(v)
68+
69+ if 'parent' in v and 'template' not in v:
70+ sdata = snapshots[v['parent']]
71+ if 'template' in sdata:
72+ data['template'] = sdata['template']
73+
74+ if 'baseOn' in v and 'template' not in v:
75+ vdata = volumes[v['baseOn']]
76+ if 'template' in vdata:
77+ data['template'] = vdata['template']
78+
79+ if 'template' not in data:
80+ data['template'] = None
81+
82+ volumes[name] = data
83
84 def volumeDelete(self, name):
85 del volumes[name]
86@@ -171,6 +192,23 @@ fakeStorPool.spopenstack.AttachDB = MockAttachDB
87 fakeStorPool.sptypes.VolumeUpdateDesc = MockVolumeUpdateDesc
88
89
90+class MockVolumeDB(object):
91+ """Simulate a Cinder database with a volume_get() method."""
92+
93+ def __init__(self, vol_types=None):
94+ """Store the specified volume types mapping if necessary."""
95+ self.vol_types = vol_types if vol_types is not None else {}
96+
97+ def volume_get(self, _context, vid):
98+ """Get a volume-like structure, only the fields we care about."""
99+ # Still, try to at least make sure we know about that volume
100+ return {
101+ 'id': vid,
102+ 'size': volumes[volumeName(vid)]['size'],
103+ 'volume_type': self.vol_types.get(vid),
104+ }
105+
106+
107 @ddt.ddt
108 class StorPoolTestCase(test.TestCase):
109
110@@ -260,6 +298,11 @@ class StorPoolTestCase(test.TestCase):
111 self.assertListEqual(sorted([volumeName(n) for n in names]),
112 sorted(volumes.keys()))
113
114+ def assertSnapshotNames(self, specs):
115+ self.assertListEqual(
116+ sorted(snapshotName(spec[0], spec[1]) for spec in specs),
117+ sorted(snapshots.keys()))
118+
119 @mock_volume_types
120 def test_create_delete_volume(self):
121 self.assertVolumeNames([])
122@@ -272,7 +315,7 @@ class StorPoolTestCase(test.TestCase):
123 self.assertVolumeNames(('1',))
124 v = volumes[volumeName('1')]
125 self.assertEqual(1 * units.Gi, v['size'])
126- self.assertNotIn('template', v.keys())
127+ self.assertIsNone(v['template'])
128 self.assertEqual(3, v['replication'])
129
130 caught = False
131@@ -292,7 +335,7 @@ class StorPoolTestCase(test.TestCase):
132 self.assertVolumeNames(('1',))
133 v = volumes[volumeName('1')]
134 self.assertEqual(2 * units.Gi, v['size'])
135- self.assertNotIn('template', v.keys())
136+ self.assertIsNone(v['template'])
137 self.assertEqual(3, v['replication'])
138
139 self.driver.create_volume({'id': '2', 'name': 'v2', 'size': 3,
140@@ -300,7 +343,7 @@ class StorPoolTestCase(test.TestCase):
141 self.assertVolumeNames(('1', '2'))
142 v = volumes[volumeName('2')]
143 self.assertEqual(3 * units.Gi, v['size'])
144- self.assertNotIn('template', v.keys())
145+ self.assertIsNone(v['template'])
146 self.assertEqual(3, v['replication'])
147
148 self.driver.create_volume({'id': '3', 'name': 'v2', 'size': 4,
149@@ -322,7 +365,7 @@ class StorPoolTestCase(test.TestCase):
150 # Make sure the dictionary is not corrupted somehow...
151 v = volumes[volumeName('1')]
152 self.assertEqual(2 * units.Gi, v['size'])
153- self.assertNotIn('template', v.keys())
154+ self.assertIsNone(v['template'])
155 self.assertEqual(3, v['replication'])
156
157 for vid in ('1', '2', '3', '4'):
158@@ -386,16 +429,17 @@ class StorPoolTestCase(test.TestCase):
159 self.driver.extend_volume({'id': '1'}, 2)
160 self.assertEqual(2 * units.Gi, volumes[volumeName('1')]['size'])
161
162- self.driver.create_cloned_volume({'id': '2', 'name': 'clo', 'size': 3},
163- {'id': 1})
164+ with mock.patch.object(self.driver, 'db', new=MockVolumeDB()):
165+ self.driver.create_cloned_volume(
166+ {'id': '2', 'name': 'clo', 'size': 3, 'volume_type': None},
167+ {'id': 1})
168 self.assertVolumeNames(('1', '2'))
169 self.assertDictEqual({}, snapshots)
170- # Note: this would not be true in a real environment (the snapshot will
171- # have been deleted, the volume would have no parent), but with this
172- # fake implementation it helps us make sure that the second volume was
173- # created with the proper options.
174- self.assertEqual(volumes[volumeName('2')]['parent'],
175- snapshotName('clone', '2'))
176+ # We do not provide a StorPool template name in either of the volumes'
177+ # types, so create_cloned_volume() should take the baseOn shortcut.
178+ vol2 = volumes[volumeName('2')]
179+ self.assertEqual(vol2['baseOn'], volumeName('1'))
180+ self.assertNotIn('parent', vol2)
181
182 self.driver.delete_volume({'id': 1})
183 self.driver.delete_volume({'id': 2})
184@@ -403,6 +447,78 @@ class StorPoolTestCase(test.TestCase):
185 self.assertDictEqual({}, volumes)
186 self.assertDictEqual({}, snapshots)
187
188+ @ddt.data(*itertools.product(
189+ [None] + [{'id': key} for key in sorted(volume_types.keys())],
190+ [None] + [{'id': key} for key in sorted(volume_types.keys())]))
191+ @ddt.unpack
192+ @mock_volume_types
193+ def test_create_cloned_volume(self, src_type, dst_type):
194+ self.assertDictEqual({}, volumes)
195+ self.assertDictEqual({}, snapshots)
196+
197+ src_template = (
198+ None
199+ if src_type is None
200+ else volume_types[src_type['id']].get('storpool_template')
201+ )
202+ dst_template = (
203+ None
204+ if dst_type is None
205+ else volume_types[dst_type['id']].get('storpool_template')
206+ )
207+ src_name = 's-none' if src_template is None else 's-' + src_template
208+ dst_name = 'd-none' if dst_template is None else 'd-' + dst_template
209+
210+ snap_name = snapshotName('clone', '2')
211+
212+ vdata1 = {
213+ 'id': '1',
214+ 'name': src_name,
215+ 'size': 1,
216+ 'volume_type': src_type,
217+ }
218+ self.assertEqual(
219+ self.driver._template_from_volume(vdata1),
220+ src_template)
221+ self.driver.create_volume(vdata1)
222+ self.assertVolumeNames(('1',))
223+
224+ vdata2 = {
225+ 'id': 2,
226+ 'name': dst_name,
227+ 'size': 1,
228+ 'volume_type': dst_type,
229+ }
230+ self.assertEqual(
231+ self.driver._template_from_volume(vdata2),
232+ dst_template)
233+ with mock.patch.object(self.driver, 'db',
234+ new=MockVolumeDB(vol_types={'1': src_type})):
235+ self.driver.create_cloned_volume(vdata2, {'id': '1'})
236+ self.assertVolumeNames(('1', '2'))
237+ vol2 = volumes[volumeName('2')]
238+ self.assertEqual(vol2['template'], dst_template)
239+
240+ if src_template == dst_template:
241+ self.assertEqual(vol2['baseOn'], volumeName('1'))
242+ self.assertNotIn('parent', vol2)
243+
244+ self.assertDictEqual({}, snapshots)
245+ else:
246+ self.assertNotIn('baseOn', vol2)
247+ self.assertEqual(vol2['parent'], snap_name)
248+
249+ self.assertSnapshotNames((('clone', '2'),))
250+ self.assertEqual(snapshots[snap_name]['template'], dst_template)
251+
252+ self.driver.delete_volume({'id': '1'})
253+ self.driver.delete_volume({'id': '2'})
254+ if src_template != dst_template:
255+ del snapshots[snap_name]
256+
257+ self.assertDictEqual({}, volumes)
258+ self.assertDictEqual({}, snapshots)
259+
260 @mock_volume_types
261 def test_config_replication(self):
262 self.assertVolumeNames([])
263@@ -422,7 +538,7 @@ class StorPoolTestCase(test.TestCase):
264 self.assertVolumeNames(('cfgrepl1',))
265 v = volumes[volumeName('cfgrepl1')]
266 self.assertEqual(3, v['replication'])
267- self.assertNotIn('template', v)
268+ self.assertIsNone(v['template'])
269 self.driver.delete_volume({'id': 'cfgrepl1'})
270
271 self.driver.configuration.storpool_replication = 2
272@@ -436,7 +552,7 @@ class StorPoolTestCase(test.TestCase):
273 self.assertVolumeNames(('cfgrepl2',))
274 v = volumes[volumeName('cfgrepl2')]
275 self.assertEqual(2, v['replication'])
276- self.assertNotIn('template', v)
277+ self.assertIsNone(v['template'])
278 self.driver.delete_volume({'id': 'cfgrepl2'})
279
280 self.driver.create_volume({'id': 'cfgrepl3', 'name': 'v1', 'size': 1,
281@@ -468,7 +584,7 @@ class StorPoolTestCase(test.TestCase):
282 self.assertVolumeNames(('cfgtempl1',))
283 v = volumes[volumeName('cfgtempl1')]
284 self.assertEqual(3, v['replication'])
285- self.assertNotIn('template', v)
286+ self.assertIsNone(v['template'])
287 self.driver.delete_volume({'id': 'cfgtempl1'})
288
289 self.driver.create_volume({'id': 'cfgtempl2', 'name': 'v1', 'size': 1,
290diff --git a/cinder/volume/drivers/storpool.py b/cinder/volume/drivers/storpool.py
Peter Pentchev7d659682022-06-23 14:20:39 +0300291index d76957c88..696c86550 100644
Peter Pentchev0be59a32022-06-06 09:09:57 +0300292--- a/cinder/volume/drivers/storpool.py
293+++ b/cinder/volume/drivers/storpool.py
294@@ -1,4 +1,4 @@
295-# Copyright (c) 2014 - 2021 StorPool
296+# Copyright (c) 2014 - 2022 StorPool
297 # All Rights Reserved.
298 #
299 # Licensed under the Apache License, Version 2.0 (the "License"); you may
300@@ -19,11 +19,13 @@ import platform
301
302 from oslo_config import cfg
303 from oslo_log import log as logging
304+from oslo_utils import excutils
305 from oslo_utils import importutils
306 from oslo_utils import units
307 import six
308
309 from cinder.common import constants
310+from cinder import context
311 from cinder import exception
312 from cinder.i18n import _
313 from cinder import interface
Peter Pentchev7d659682022-06-23 14:20:39 +0300314@@ -96,6 +98,7 @@ class StorPoolDriver(driver.VolumeDriver):
315 - Detach temporary snapshots and volumes after copying data
316 to or from from Glance images.
Peter Pentchev0be59a32022-06-06 09:09:57 +0300317 - Drop backup_volume()
318+ - Avoid data duplication in create_cloned_volume()
319 """
320
321 VERSION = '2.0.0'
Peter Pentchev7d659682022-06-23 14:20:39 +0300322@@ -202,30 +205,80 @@ class StorPoolDriver(driver.VolumeDriver):
Peter Pentchev0be59a32022-06-06 09:09:57 +0300323
324 def create_cloned_volume(self, volume, src_vref):
325 refname = self._attach.volumeName(src_vref['id'])
326+ size = int(volume['size']) * units.Gi
327+ volname = self._attach.volumeName(volume['id'])
328+
329+ src_volume = self.db.volume_get(
330+ context.get_admin_context(),
331+ src_vref['id'],
332+ )
333+ src_template = self._template_from_volume(src_volume)
334+
335+ template = self._template_from_volume(volume)
336+ LOG.debug('clone volume id %(vol_id)s template %(template)s', {
337+ 'vol_id': repr(volume['id']),
338+ 'template': repr(template),
339+ })
340+ if template == src_template:
341+ LOG.info('Using baseOn to clone a volume into the same template')
342+ try:
343+ self._attach.api().volumeCreate({
344+ 'name': volname,
345+ 'size': size,
346+ 'baseOn': refname,
347+ })
348+ except spapi.ApiError as e:
349+ raise self._backendException(e)
350+
351+ return None
352+
353 snapname = self._attach.snapshotName('clone', volume['id'])
354+ LOG.info(
355+ 'A transient snapshot for a %(src)s -> %(dst)s template change',
356+ {'src': src_template, 'dst': template})
357 try:
358 self._attach.api().snapshotCreate(refname, {'name': snapname})
359 except spapi.ApiError as e:
360- raise self._backendException(e)
361+ if e.name != 'objectExists':
362+ raise self._backendException(e)
363
364- size = int(volume['size']) * units.Gi
365- volname = self._attach.volumeName(volume['id'])
366 try:
367- self._attach.api().volumeCreate({
368- 'name': volname,
369- 'size': size,
370- 'parent': snapname
371- })
372- except spapi.ApiError as e:
373- raise self._backendException(e)
374- finally:
375 try:
376- self._attach.api().snapshotDelete(snapname)
377+ self._attach.api().snapshotUpdate(
378+ snapname,
379+ {'template': template},
380+ )
381+ except spapi.ApiError as e:
382+ raise self._backendException(e)
383+
384+ try:
385+ self._attach.api().volumeCreate({
386+ 'name': volname,
387+ 'size': size,
388+ 'parent': snapname
389+ })
390 except spapi.ApiError as e:
391- # ARGH!
392- LOG.error("Could not delete the temp snapshot %(name)s: "
393- "%(msg)s",
394- {'name': snapname, 'msg': e})
395+ raise self._backendException(e)
396+
397+ try:
398+ self._attach.api().snapshotUpdate(
399+ snapname,
400+ {'tags': {'transient': '1.0'}},
401+ )
402+ except spapi.ApiError as e:
403+ raise self._backendException(e)
404+ except Exception:
405+ with excutils.save_and_reraise_exception():
406+ try:
407+ LOG.warning(
408+ 'Something went wrong, removing the transient snapshot'
409+ )
410+ self._attach.api().snapshotDelete(snapname)
411+ except spapi.ApiError as e:
412+ LOG.error(
413+ 'Could not delete the %(name)s snapshot: %(err)s',
414+ {'name': snapname, 'err': str(e)}
415+ )
416
417 def create_export(self, context, volume, connector):
418 pass
419diff --git a/driver-requirements.txt b/driver-requirements.txt
420index cc8da8eb5..ec05b01c6 100644
421--- a/driver-requirements.txt
422+++ b/driver-requirements.txt
423@@ -40,7 +40,7 @@ infi.dtypes.wwn # PSF
424 infi.dtypes.iqn # PSF
425
426 # Storpool
427-storpool>=4.0.0 # Apache-2.0
428+storpool>=7.1.0 # Apache-2.0
429 storpool.spopenstack>=2.2.1 # Apache-2.0
430
431 # Datera
Peter Pentchev7d659682022-06-23 14:20:39 +0300432diff --git a/releasenotes/notes/storpool-clone-better-dca90f40c9273de9.yaml b/releasenotes/notes/storpool-clone-better-dca90f40c9273de9.yaml
433new file mode 100644
434index 000000000..180427d9e
435--- /dev/null
436+++ b/releasenotes/notes/storpool-clone-better-dca90f40c9273de9.yaml
437@@ -0,0 +1,6 @@
438+---
439+features:
440+ - |
441+ StorPool driver: improved the way volumes are clonsed into different
442+ StorPool templates (exposed as Cinder storage pools) if requested,
443+ eliminating some data duplication in the underlying StorPool cluster.
444--
4452.35.1
446