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