blob: 2c255511738f0e1495062f3b91679c5a00e5bdf6 [file] [log] [blame]
Peter Pentchevfad59c62023-01-17 08:44:27 +02001From 110d748871ed078eff42bd370bcdfb643614e6f5 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 Pentchevfad59c62023-01-17 08:44:27 +02004Subject: [PATCH 3/3] 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
33index 24715fa29..95a1ffffd 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 Pentchevfad59c62023-01-17 08:44:27 +020044@@ -110,13 +111,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 Pentchevfad59c62023-01-17 08:44:27 +020080@@ -174,6 +195,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 Pentchevfad59c62023-01-17 08:44:27 +0200104@@ -265,6 +303,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 Pentchevfad59c62023-01-17 08:44:27 +0200116@@ -277,7 +320,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 Pentchevfad59c62023-01-17 08:44:27 +0200125@@ -297,7 +340,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 Pentchevfad59c62023-01-17 08:44:27 +0200134@@ -305,7 +348,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 Pentchevfad59c62023-01-17 08:44:27 +0200143@@ -327,7 +370,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 Pentchevfad59c62023-01-17 08:44:27 +0200152@@ -395,16 +438,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 Pentchevfad59c62023-01-17 08:44:27 +0200178@@ -412,6 +456,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 Pentchevfad59c62023-01-17 08:44:27 +0200257@@ -431,7 +547,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 Pentchevfad59c62023-01-17 08:44:27 +0200266@@ -445,7 +561,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 Pentchevfad59c62023-01-17 08:44:27 +0200275@@ -477,7 +593,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
285index cb5680d29..5aa30398e 100644
Peter Pentchev6ef0f072022-09-26 16:20:30 +0300286--- a/cinder/volume/drivers/storpool.py
287+++ b/cinder/volume/drivers/storpool.py
Peter Pentchevfad59c62023-01-17 08:44:27 +0200288@@ -19,11 +19,13 @@ 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
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
Peter Pentchevfad59c62023-01-17 08:44:27 +0200302@@ -197,30 +199,80 @@ class StorPoolDriver(driver.VolumeDriver):
Peter Pentchev6ef0f072022-09-26 16:20:30 +0300303
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+ )
Peter Pentchevfad59c62023-01-17 08:44:27 +0200361 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})
Peter Pentchev6ef0f072022-09-26 16:20:30 +0300366+ 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+ )
Peter Pentchevfad59c62023-01-17 08:44:27 +0200382+ except spapi.ApiError as e:
Peter Pentchev6ef0f072022-09-26 16:20:30 +0300383+ 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
Peter Pentchevfad59c62023-01-17 08:44:27 +0200399diff --git a/driver-requirements.txt b/driver-requirements.txt
400index f501e9fcd..d55fb86c7 100644
Peter Pentchev6ef0f072022-09-26 16:20:30 +0300401--- a/driver-requirements.txt
402+++ b/driver-requirements.txt
Peter Pentchevfad59c62023-01-17 08:44:27 +0200403@@ -41,7 +41,7 @@ infi.dtypes.wwn # PSF
Peter Pentchev6ef0f072022-09-26 16:20:30 +0300404 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
Peter Pentchevfad59c62023-01-17 08:44:27 +0200412diff --git a/releasenotes/notes/storpool-clone-better-dca90f40c9273de9.yaml b/releasenotes/notes/storpool-clone-better-dca90f40c9273de9.yaml
413new file mode 100644
414index 000000000..180427d9e
Peter Pentchev6ef0f072022-09-26 16:20:30 +0300415--- /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.
Peter Pentchevfad59c62023-01-17 08:44:27 +0200424--
4252.39.0
426