blob: 8dbbe2e9571d88f573815635f6db9e219dc07de6 [file] [log] [blame]
Peter Pentchev5cf673c2024-02-20 10:04:41 +02001From 609436981cbd13488eb1345d41ebffcd2347d2de Mon Sep 17 00:00:00 2001
Peter Pentchev6ef0f072022-09-26 16:20:30 +03002From: Peter Penchev <openstack-dev@storpool.com>
3Date: Wed, 20 Apr 2022 15:47:39 +0300
Peter Pentchev5cf673c2024-02-20 10:04:41 +02004Subject: [PATCH 6/9] StorPool: create_cloned_volume() improvements
Peter Pentchev6ef0f072022-09-26 16:20:30 +03005
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 Pentchevfad59c62023-01-17 08:44:27 +020032diff --git a/cinder/tests/unit/volume/drivers/test_storpool.py b/cinder/tests/unit/volume/drivers/test_storpool.py
Peter Pentchev5cf673c2024-02-20 10:04:41 +020033index 8792678ed..94074d218 100644
Peter Pentchev6ef0f072022-09-26 16:20:30 +030034--- 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
Peter Pentchev5cf673c2024-02-20 10:04:41 +020044@@ -109,13 +110,33 @@ class MockAPI(object):
Peter Pentchev6ef0f072022-09-26 16:20:30 +030045 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]
Peter Pentchev5cf673c2024-02-20 10:04:41 +020080@@ -173,6 +194,23 @@ fakeStorPool.spopenstack.AttachDB = MockAttachDB
Peter Pentchev6ef0f072022-09-26 16:20:30 +030081 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
Peter Pentchev5cf673c2024-02-20 10:04:41 +0200104@@ -264,6 +302,11 @@ class StorPoolTestCase(test.TestCase):
Peter Pentchev6ef0f072022-09-26 16:20:30 +0300105 self.assertListEqual(sorted([volumeName(n) for n in names]),
Peter Pentchev844a5d92023-01-17 03:07:56 +0200106 sorted(data['name'] for data in volumes.values()))
Peter Pentchev6ef0f072022-09-26 16:20:30 +0300107
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([])
Peter Pentchev5cf673c2024-02-20 10:04:41 +0200116@@ -276,7 +319,7 @@ class StorPoolTestCase(test.TestCase):
Peter Pentchev6ef0f072022-09-26 16:20:30 +0300117 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
Peter Pentchev5cf673c2024-02-20 10:04:41 +0200125@@ -296,7 +339,7 @@ class StorPoolTestCase(test.TestCase):
Peter Pentchev6ef0f072022-09-26 16:20:30 +0300126 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,
Peter Pentchev5cf673c2024-02-20 10:04:41 +0200134@@ -304,7 +347,7 @@ class StorPoolTestCase(test.TestCase):
Peter Pentchev6ef0f072022-09-26 16:20:30 +0300135 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,
Peter Pentchev5cf673c2024-02-20 10:04:41 +0200143@@ -326,7 +369,7 @@ class StorPoolTestCase(test.TestCase):
Peter Pentchev6ef0f072022-09-26 16:20:30 +0300144 # 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'):
Peter Pentchev5cf673c2024-02-20 10:04:41 +0200152@@ -392,16 +435,17 @@ class StorPoolTestCase(test.TestCase):
Peter Pentchev6ef0f072022-09-26 16:20:30 +0300153 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})
Peter Pentchev5cf673c2024-02-20 10:04:41 +0200178@@ -409,6 +453,78 @@ class StorPoolTestCase(test.TestCase):
Peter Pentchev6ef0f072022-09-26 16:20:30 +0300179 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([])
Peter Pentchev5cf673c2024-02-20 10:04:41 +0200257@@ -428,7 +544,7 @@ class StorPoolTestCase(test.TestCase):
Peter Pentchev6ef0f072022-09-26 16:20:30 +0300258 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
Peter Pentchev5cf673c2024-02-20 10:04:41 +0200266@@ -442,7 +558,7 @@ class StorPoolTestCase(test.TestCase):
Peter Pentchev6ef0f072022-09-26 16:20:30 +0300267 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,
Peter Pentchev5cf673c2024-02-20 10:04:41 +0200275@@ -474,7 +590,7 @@ class StorPoolTestCase(test.TestCase):
Peter Pentchev6ef0f072022-09-26 16:20:30 +0300276 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,
Peter Pentchevfad59c62023-01-17 08:44:27 +0200284diff --git a/cinder/volume/drivers/storpool.py b/cinder/volume/drivers/storpool.py
Peter Pentchev5cf673c2024-02-20 10:04:41 +0200285index c2b566f4a..131ba68fb 100644
Peter Pentchev6ef0f072022-09-26 16:20:30 +0300286--- a/cinder/volume/drivers/storpool.py
287+++ b/cinder/volume/drivers/storpool.py
Peter Pentchev5cf673c2024-02-20 10:04:41 +0200288@@ -19,10 +19,12 @@ import platform
Peter Pentchev6ef0f072022-09-26 16:20:30 +0300289
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
Peter Pentchev6ef0f072022-09-26 16:20:30 +0300295
296 from cinder.common import constants
297+from cinder import context
298 from cinder import exception
299 from cinder.i18n import _
300 from cinder import interface
Peter Pentchev5cf673c2024-02-20 10:04:41 +0200301@@ -196,30 +198,80 @@ class StorPoolDriver(driver.VolumeDriver):
Peter Pentchev6ef0f072022-09-26 16:20:30 +0300302
303 def create_cloned_volume(self, volume, src_vref):
304 refname = self._attach.volumeName(src_vref['id'])
305+ size = int(volume['size']) * units.Gi
306+ volname = self._attach.volumeName(volume['id'])
307+
308+ src_volume = self.db.volume_get(
309+ context.get_admin_context(),
310+ src_vref['id'],
311+ )
312+ src_template = self._template_from_volume(src_volume)
313+
314+ template = self._template_from_volume(volume)
Peter Pentchevea354462023-07-18 11:15:56 +0300315+ LOG.debug('clone volume id %(vol_id)r template %(template)r', {
316+ 'vol_id': volume['id'],
317+ 'template': template,
Peter Pentchev6ef0f072022-09-26 16:20:30 +0300318+ })
319+ if template == src_template:
320+ LOG.info('Using baseOn to clone a volume into the same template')
321+ try:
322+ self._attach.api().volumeCreate({
323+ 'name': volname,
324+ 'size': size,
325+ 'baseOn': refname,
326+ })
327+ except spapi.ApiError as e:
328+ raise self._backendException(e)
329+
330+ return None
331+
332 snapname = self._attach.snapshotName('clone', volume['id'])
333+ LOG.info(
334+ 'A transient snapshot for a %(src)s -> %(dst)s template change',
335+ {'src': src_template, 'dst': template})
336 try:
337 self._attach.api().snapshotCreate(refname, {'name': snapname})
338 except spapi.ApiError as e:
339- raise self._backendException(e)
340+ if e.name != 'objectExists':
341+ raise self._backendException(e)
342
343- size = int(volume['size']) * units.Gi
344- volname = self._attach.volumeName(volume['id'])
345 try:
346- self._attach.api().volumeCreate({
347- 'name': volname,
348- 'size': size,
349- 'parent': snapname
350- })
351- except spapi.ApiError as e:
352- raise self._backendException(e)
353- finally:
354 try:
355- self._attach.api().snapshotDelete(snapname)
356+ self._attach.api().snapshotUpdate(
357+ snapname,
358+ {'template': template},
359+ )
Peter Pentchevfad59c62023-01-17 08:44:27 +0200360 except spapi.ApiError as e:
361- # ARGH!
362- LOG.error("Could not delete the temp snapshot %(name)s: "
363- "%(msg)s",
364- {'name': snapname, 'msg': e})
Peter Pentchev6ef0f072022-09-26 16:20:30 +0300365+ raise self._backendException(e)
366+
367+ try:
368+ self._attach.api().volumeCreate({
369+ 'name': volname,
370+ 'size': size,
371+ 'parent': snapname
372+ })
373+ except spapi.ApiError as e:
374+ raise self._backendException(e)
375+
376+ try:
377+ self._attach.api().snapshotUpdate(
378+ snapname,
379+ {'tags': {'transient': '1.0'}},
380+ )
Peter Pentchevfad59c62023-01-17 08:44:27 +0200381+ except spapi.ApiError as e:
Peter Pentchev6ef0f072022-09-26 16:20:30 +0300382+ raise self._backendException(e)
383+ except Exception:
384+ with excutils.save_and_reraise_exception():
385+ try:
386+ LOG.warning(
387+ 'Something went wrong, removing the transient snapshot'
388+ )
389+ self._attach.api().snapshotDelete(snapname)
390+ except spapi.ApiError as e:
391+ LOG.error(
392+ 'Could not delete the %(name)s snapshot: %(err)s',
393+ {'name': snapname, 'err': str(e)}
394+ )
395
396 def create_export(self, context, volume, connector):
397 pass
Peter Pentchevfad59c62023-01-17 08:44:27 +0200398diff --git a/driver-requirements.txt b/driver-requirements.txt
Peter Pentchev5cf673c2024-02-20 10:04:41 +0200399index 8cff370ed..aa1211c19 100644
Peter Pentchev6ef0f072022-09-26 16:20:30 +0300400--- a/driver-requirements.txt
401+++ b/driver-requirements.txt
Peter Pentchev5cf673c2024-02-20 10:04:41 +0200402@@ -42,7 +42,7 @@ infi.dtypes.wwn # PSF
Peter Pentchev6ef0f072022-09-26 16:20:30 +0300403 infi.dtypes.iqn # PSF
404
405 # Storpool
406-storpool>=4.0.0 # Apache-2.0
407+storpool>=7.1.0 # Apache-2.0
408 storpool.spopenstack>=2.2.1 # Apache-2.0
409
410 # Datera
Peter Pentchevfad59c62023-01-17 08:44:27 +0200411diff --git a/releasenotes/notes/storpool-clone-better-dca90f40c9273de9.yaml b/releasenotes/notes/storpool-clone-better-dca90f40c9273de9.yaml
412new file mode 100644
Peter Pentchevea354462023-07-18 11:15:56 +0300413index 000000000..9d512c831
Peter Pentchev6ef0f072022-09-26 16:20:30 +0300414--- /dev/null
415+++ b/releasenotes/notes/storpool-clone-better-dca90f40c9273de9.yaml
416@@ -0,0 +1,6 @@
417+---
418+features:
419+ - |
Peter Pentchevea354462023-07-18 11:15:56 +0300420+ StorPool driver: improved the way volumes are cloned into different
Peter Pentchev6ef0f072022-09-26 16:20:30 +0300421+ StorPool templates (exposed as Cinder storage pools) if requested,
422+ eliminating some data duplication in the underlying StorPool cluster.
Peter Pentchevfad59c62023-01-17 08:44:27 +0200423--
Peter Pentchev5cf673c2024-02-20 10:04:41 +02004242.43.0
Peter Pentchevfad59c62023-01-17 08:44:27 +0200425