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