Coverage for manila/share/drivers/glusterfs/layout_volume.py: 98%
294 statements
« prev ^ index » next coverage.py v7.11.0, created at 2026-02-18 22:19 +0000
« prev ^ index » next coverage.py v7.11.0, created at 2026-02-18 22:19 +0000
1# Copyright (c) 2015 Red Hat, Inc.
2# All Rights Reserved.
3#
4# Licensed under the Apache License, Version 2.0 (the "License"); you may
5# not use this file except in compliance with the License. You may obtain
6# a copy of the License at
7#
8# http://www.apache.org/licenses/LICENSE-2.0
9#
10# Unless required by applicable law or agreed to in writing, software
11# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13# License for the specific language governing permissions and limitations
14# under the License.
16"""GlusterFS volume mapped share layout."""
18import os
19import random
20import re
21import shutil
22import string
23import tempfile
25from defusedxml import ElementTree as etree
26from oslo_config import cfg
27from oslo_log import log
29from manila import exception
30from manila.i18n import _
31from manila.privsep import os as privsep_os
32from manila.share.drivers.glusterfs import common
33from manila.share.drivers.glusterfs import layout
34from manila import utils
36LOG = log.getLogger(__name__)
39glusterfs_volume_mapped_opts = [
40 cfg.ListOpt('glusterfs_servers',
41 default=[],
42 help='List of GlusterFS servers that can be used to create '
43 'shares. Each GlusterFS server should be of the form '
44 '[remoteuser@]<volserver>, and they are assumed to '
45 'belong to distinct Gluster clusters.'),
46 cfg.StrOpt('glusterfs_volume_pattern',
47 help='Regular expression template used to filter '
48 'GlusterFS volumes for share creation. '
49 'The regex template can optionally (ie. with support '
50 'of the GlusterFS backend) contain the #{size} '
51 'parameter which matches an integer (sequence of '
52 'digits) in which case the value shall be interpreted as '
53 'size of the volume in GB. Examples: '
54 r'"manila-share-volume-\d+$", '
55 r'"manila-share-volume-#{size}G-\d+$"; '
56 'with matching volume names, respectively: '
57 '"manila-share-volume-12", "manila-share-volume-3G-13". '
58 'In latter example, the number that matches "#{size}", '
59 'that is, 3, is an indication that the size of volume '
60 'is 3G.'),
61]
64CONF = cfg.CONF
65CONF.register_opts(glusterfs_volume_mapped_opts)
67# The dict specifying named parameters
68# that can be used with glusterfs_volume_pattern
69# in #{<param>} format.
70# For each of them we give regex pattern it matches
71# and a transformer function ('trans') for the matched
72# string value.
73# Currently we handle only #{size}.
74PATTERN_DICT = {'size': {'pattern': r'(?P<size>\d+)', 'trans': int}}
75USER_MANILA_SHARE = 'user.manila-share'
76USER_CLONED_FROM = 'user.manila-cloned-from'
77UUID_RE = re.compile(r'\A[\da-f]{8}-([\da-f]{4}-){3}[\da-f]{12}\Z', re.I)
80class GlusterfsVolumeMappedLayout(layout.GlusterfsShareLayoutBase):
82 _snapshots_are_supported = True
84 def __init__(self, driver, *args, **kwargs):
85 super(GlusterfsVolumeMappedLayout, self).__init__(
86 driver, *args, **kwargs)
87 self.gluster_used_vols = set()
88 self.configuration.append_config_values(
89 common.glusterfs_common_opts)
90 self.configuration.append_config_values(
91 glusterfs_volume_mapped_opts)
92 self.gluster_nosnap_vols_dict = {}
93 self.volume_pattern = self._compile_volume_pattern()
94 self.volume_pattern_keys = self.volume_pattern.groupindex.keys()
95 for srvaddr in self.configuration.glusterfs_servers:
96 # format check for srvaddr
97 self._glustermanager(srvaddr, False)
98 self.glusterfs_versions = {}
99 self.private_storage = kwargs.get('private_storage')
101 def _compile_volume_pattern(self):
102 """Compile a RegexObject from the config specified regex template.
104 (cfg.glusterfs_volume_pattern)
105 """
107 subdict = {}
108 for key, val in PATTERN_DICT.items():
109 subdict[key] = val['pattern']
111 # Using templates with placeholder syntax #{<var>}
112 class CustomTemplate(string.Template):
113 delimiter = '#'
115 volume_pattern = CustomTemplate(
116 self.configuration.glusterfs_volume_pattern).substitute(
117 subdict)
118 return re.compile(volume_pattern)
120 def do_setup(self, context):
121 """Setup the GlusterFS volumes."""
122 glusterfs_versions, exceptions = {}, {}
123 for srvaddr in self.configuration.glusterfs_servers:
124 try:
125 glusterfs_versions[srvaddr] = self._glustermanager(
126 srvaddr, False).get_gluster_version()
127 except exception.GlusterfsException as exc:
128 exceptions[srvaddr] = str(exc)
129 if exceptions:
130 for srvaddr, excmsg in exceptions.items():
131 LOG.error("'gluster version' failed on server "
132 "%(server)s with: %(message)s",
133 {'server': srvaddr, 'message': excmsg})
134 raise exception.GlusterfsException(_(
135 "'gluster version' failed on servers %s") % (
136 ','.join(exceptions.keys())))
137 notsupp_servers = []
138 for srvaddr, vers in glusterfs_versions.items():
139 if common.numreduct(vers) < self.driver.GLUSTERFS_VERSION_MIN:
140 notsupp_servers.append(srvaddr)
141 if notsupp_servers:
142 gluster_version_min_str = '.'.join(
143 str(c) for c in self.driver.GLUSTERFS_VERSION_MIN)
144 for srvaddr in notsupp_servers:
145 LOG.error("GlusterFS version %(version)s on server "
146 "%(server)s is not supported, "
147 "minimum requirement: %(minvers)s",
148 {'server': srvaddr,
149 'version': '.'.join(glusterfs_versions[srvaddr]),
150 'minvers': gluster_version_min_str})
151 raise exception.GlusterfsException(_(
152 "Unsupported GlusterFS version on servers %(servers)s, "
153 "minimum requirement: %(minvers)s") % {
154 'servers': ','.join(notsupp_servers),
155 'minvers': gluster_version_min_str})
156 self.glusterfs_versions = glusterfs_versions
158 gluster_volumes_initial = set(
159 self._fetch_gluster_volumes(filter_used=False))
160 if not gluster_volumes_initial:
161 # No suitable volumes are found on the Gluster end.
162 # Raise exception.
163 msg = (_("Gluster backend does not provide any volume "
164 "matching pattern %s"
165 ) % self.configuration.glusterfs_volume_pattern)
166 LOG.error(msg)
167 raise exception.GlusterfsException(msg)
169 LOG.info("Found %d Gluster volumes allocated for Manila.",
170 len(gluster_volumes_initial))
172 self._check_mount_glusterfs()
174 def _glustermanager(self, gluster_address, req_volume=True):
175 """Create GlusterManager object for gluster_address."""
177 return common.GlusterManager(
178 gluster_address, self.driver._execute,
179 self.configuration.glusterfs_path_to_private_key,
180 self.configuration.glusterfs_server_password,
181 requires={'volume': req_volume})
183 def _share_manager(self, share):
184 """Return GlusterManager object representing share's backend."""
185 gluster_address = self.private_storage.get(share['id'], 'volume')
186 if gluster_address is None:
187 return
188 return self._glustermanager(gluster_address)
190 def _fetch_gluster_volumes(self, filter_used=True):
191 """Do a 'gluster volume list | grep <volume pattern>'.
193 Aggregate the results from all servers.
194 Extract the named groups from the matching volume names
195 using the specs given in PATTERN_DICT.
196 Return a dict with keys of the form <server>:/<volname>
197 and values being dicts that map names of named groups
198 to their extracted value.
199 """
201 volumes_dict = {}
202 for srvaddr in self.configuration.glusterfs_servers:
203 gluster_mgr = self._glustermanager(srvaddr, False)
204 if gluster_mgr.user: 204 ↛ 208line 204 didn't jump to line 208 because the condition on line 204 was always true
205 logmsg = ("Retrieving volume list "
206 "on host %s") % gluster_mgr.host
207 else:
208 logmsg = ("Retrieving volume list")
209 out, err = gluster_mgr.gluster_call('volume', 'list', log=logmsg)
210 for volname in out.split("\n"):
211 patmatch = self.volume_pattern.match(volname)
212 if not patmatch:
213 continue
214 comp_vol = gluster_mgr.components.copy()
215 comp_vol.update({'volume': volname})
216 gluster_mgr_vol = self._glustermanager(comp_vol)
217 if filter_used:
218 vshr = gluster_mgr_vol.get_vol_option(
219 USER_MANILA_SHARE) or ''
220 if UUID_RE.search(vshr):
221 continue
222 pattern_dict = {}
223 for key in self.volume_pattern_keys:
224 keymatch = patmatch.group(key)
225 if keymatch is None:
226 pattern_dict[key] = None
227 else:
228 trans = PATTERN_DICT[key].get('trans', lambda x: x)
229 pattern_dict[key] = trans(keymatch)
230 volumes_dict[gluster_mgr_vol.qualified] = pattern_dict
231 return volumes_dict
233 @utils.synchronized("glusterfs_native", external=False)
234 def _pop_gluster_vol(self, size=None):
235 """Pick an unbound volume.
237 Do a _fetch_gluster_volumes() first to get the complete
238 list of usable volumes.
239 Keep only the unbound ones (ones that are not yet used to
240 back a share).
241 If size is given, try to pick one which has a size specification
242 (according to the 'size' named group of the volume pattern),
243 and its size is greater-than-or-equal to the given size.
244 Return the volume chosen (in <host>:/<volname> format).
245 """
247 voldict = self._fetch_gluster_volumes()
248 # calculate the set of unused volumes
249 unused_vols = set(voldict) - self.gluster_used_vols
251 if not unused_vols:
252 # No volumes available for use as share. Warn user.
253 LOG.warning("No unused gluster volumes available for use as "
254 "share! Create share won't be supported unless "
255 "existing shares are deleted or some gluster "
256 "volumes are created with names matching "
257 "'glusterfs_volume_pattern'.")
258 else:
259 LOG.info("Number of gluster volumes in use: "
260 "%(inuse-numvols)s. Number of gluster volumes "
261 "available for use as share: %(unused-numvols)s",
262 {'inuse-numvols': len(self.gluster_used_vols),
263 'unused-numvols': len(unused_vols)})
265 # volmap is the data structure used to categorize and sort
266 # the unused volumes. It's a nested dictionary of structure
267 # {<size>: <hostmap>}
268 # where <size> is either an integer or None,
269 # <hostmap> is a dictionary of structure {<host>: <vols>}
270 # where <host> is a host name (IP address), <vols> is a list
271 # of volumes (gluster addresses).
272 volmap = {None: {}}
273 # if both caller has specified size and 'size' occurs as
274 # a parameter in the volume pattern...
275 if size and 'size' in self.volume_pattern_keys:
276 # then this function is used to extract the
277 # size value for a given volume from the voldict...
278 get_volsize = lambda vol: voldict[vol]['size'] # noqa: E731
279 else:
280 # else just use a stub.
281 get_volsize = lambda vol: None # noqa: E731
282 for vol in unused_vols:
283 # For each unused volume, we extract the <size>
284 # and <host> values with which it can be inserted
285 # into the volmap, and conditionally perform
286 # the insertion (with the condition being: once
287 # caller specified size and a size indication was
288 # found in the volume name, we require that the
289 # indicated size adheres to caller's spec).
290 volsize = get_volsize(vol)
291 if not volsize or volsize >= size:
292 hostmap = volmap.get(volsize)
293 if not hostmap: 293 ↛ 296line 293 didn't jump to line 296 because the condition on line 293 was always true
294 hostmap = {}
295 volmap[volsize] = hostmap
296 host = self._glustermanager(vol).host
297 hostvols = hostmap.get(host)
298 if not hostvols: 298 ↛ 301line 298 didn't jump to line 301 because the condition on line 298 was always true
299 hostvols = []
300 hostmap[host] = hostvols
301 hostvols.append(vol)
302 if len(volmap) > 1:
303 # volmap has keys apart from the default None,
304 # ie. volumes with sensible and adherent size
305 # indication have been found. Then pick the smallest
306 # of the size values.
307 chosen_size = sorted(n for n in volmap.keys() if n)[0]
308 else:
309 chosen_size = None
310 chosen_hostmap = volmap[chosen_size]
311 if not chosen_hostmap:
312 msg = (_("Couldn't find a free gluster volume to use."))
313 LOG.error(msg)
314 raise exception.GlusterfsException(msg)
316 # From the hosts we choose randomly to tend towards
317 # even distribution of share backing volumes among
318 # Gluster clusters.
319 chosen_host = random.choice(list(chosen_hostmap.keys()))
320 # Within a host's volumes, choose alphabetically first,
321 # to make it predictable.
322 vol = sorted(chosen_hostmap[chosen_host])[0]
323 self.gluster_used_vols.add(vol)
324 return vol
326 @utils.synchronized("glusterfs_native", external=False)
327 def _push_gluster_vol(self, exp_locn):
328 try:
329 self.gluster_used_vols.remove(exp_locn)
330 except KeyError:
331 msg = (_("Couldn't find the share in used list."))
332 LOG.error(msg)
333 raise exception.GlusterfsException(msg)
335 def _wipe_gluster_vol(self, gluster_mgr):
337 # Create a temporary mount.
338 gluster_export = gluster_mgr.export
339 tmpdir = tempfile.mkdtemp()
340 try:
341 common._mount_gluster_vol(self.driver._execute, gluster_export,
342 tmpdir)
343 except exception.GlusterfsException:
344 shutil.rmtree(tmpdir, ignore_errors=True)
345 raise
347 # Delete the contents of a GlusterFS volume that is temporarily
348 # mounted.
349 # From GlusterFS version 3.7, two directories, '.trashcan' at the root
350 # of the GlusterFS volume and 'internal_op' within the '.trashcan'
351 # directory, are internally created when a GlusterFS volume is started.
352 # GlusterFS does not allow unlink(2) of the two directories. So do not
353 # delete the paths of the two directories, but delete their contents
354 # along with the rest of the contents of the volume.
355 srvaddr = gluster_mgr.host_access
356 ignored_dirs = []
357 if common.numreduct(self.glusterfs_versions[srvaddr]) > (3, 6):
358 ignored_dirs = map(lambda x: os.path.join(tmpdir, *x),
359 [('.trashcan', ), ('.trashcan', 'internal_op')])
360 ignored_dirs = list(ignored_dirs)
361 ignored_dirs = [ignored_dirs[0], ignored_dirs[1]]
363 try:
364 privsep_os.find(
365 tmpdir, dirs_to_ignore=ignored_dirs, delete=True)
366 except exception.ProcessExecutionError as exc:
367 msg = (_("Error trying to wipe gluster volume. "
368 "gluster_export: %(export)s, Error: %(error)s") %
369 {'export': gluster_export, 'error': exc.stderr})
370 LOG.error(msg)
371 raise exception.GlusterfsException(msg)
372 finally:
373 # Unmount.
374 common._umount_gluster_vol(tmpdir)
375 shutil.rmtree(tmpdir, ignore_errors=True)
377 def create_share(self, context, share, share_server=None):
378 """Create a share using GlusterFS volume.
380 1 Manila share = 1 GlusterFS volume. Pick an unused
381 GlusterFS volume for use as a share.
382 """
383 try:
384 vol = self._pop_gluster_vol(share['size'])
385 except exception.GlusterfsException:
386 msg = ("Error creating share %(share_id)s",
387 {'share_id': share['id']})
388 LOG.error(msg)
389 raise
391 gmgr = self._glustermanager(vol)
392 export = self.driver._setup_via_manager(
393 {'share': share, 'manager': gmgr})
395 gmgr.set_vol_option(USER_MANILA_SHARE, share['id'])
396 self.private_storage.update(share['id'], {'volume': vol})
398 # TODO(deepakcs): Enable quota and set it to the share size.
400 # For native protocol, the export_location should be of the form:
401 # server:/volname
402 LOG.info("export_location sent back from create_share: %s",
403 export)
404 return export
406 def delete_share(self, context, share, share_server=None):
407 """Delete a share on the GlusterFS volume.
409 1 Manila share = 1 GlusterFS volume. Put the gluster
410 volume back in the available list.
411 """
412 gmgr = self._share_manager(share)
413 if not gmgr:
414 # Share does not have a record in private storage.
415 # It means create_share{,_from_snapshot} did not
416 # succeed(*). In that case we should not obstruct
417 # share deletion, so we just return doing nothing.
418 #
419 # (*) or we have a database corruption but then
420 # basically does not matter what we do here
421 return
422 clone_of = gmgr.get_vol_option(USER_CLONED_FROM) or ''
423 try:
424 if UUID_RE.search(clone_of):
425 # We take responsibility for the lifecycle
426 # management of those volumes which were
427 # created by us (as snapshot clones) ...
428 gmgr.gluster_call('volume', 'delete', gmgr.volume)
429 else:
430 # ... for volumes that come from the pool, we return
431 # them to the pool (after some purification rituals)
432 self._wipe_gluster_vol(gmgr)
433 gmgr.set_vol_option(USER_MANILA_SHARE, 'NONE')
434 gmgr.set_vol_option('nfs.disable', 'on')
436 # When deleting the share instance, we may need to
437 # update'self.gluster_used_vols' again
438 self.gluster_used_vols.add(gmgr.qualified)
439 self._push_gluster_vol(gmgr.qualified)
440 except exception.GlusterfsException:
441 msg = ("Error during delete_share request for "
442 "share %(share_id)s", {'share_id': share['id']})
443 LOG.error(msg)
444 raise
446 self.private_storage.delete(share['id'])
447 # TODO(deepakcs): Disable quota.
449 @staticmethod
450 def _find_actual_backend_snapshot_name(gluster_mgr, snapshot):
451 args = ('snapshot', 'list', gluster_mgr.volume, '--mode=script')
452 out, err = gluster_mgr.gluster_call(
453 *args,
454 log=("Retrieving snapshot list"))
455 snapgrep = list(filter(lambda x: snapshot['id'] in x, out.split("\n")))
456 if len(snapgrep) != 1:
457 msg = (_("Failed to identify backing GlusterFS object "
458 "for snapshot %(snap_id)s of share %(share_id)s: "
459 "a single candidate was expected, %(found)d was found.") %
460 {'snap_id': snapshot['id'],
461 'share_id': snapshot['share_id'],
462 'found': len(snapgrep)})
463 raise exception.GlusterfsException(msg)
464 backend_snapshot_name = snapgrep[0]
465 return backend_snapshot_name
467 def create_share_from_snapshot(self, context, share, snapshot,
468 share_server=None, parent_share=None):
469 old_gmgr = self._share_manager(snapshot['share_instance'])
471 # Snapshot clone feature in GlusterFS server essential to support this
472 # API is available in GlusterFS server versions 3.7 and higher. So do
473 # a version check.
474 vers = self.glusterfs_versions[old_gmgr.host_access]
475 minvers = (3, 7)
476 if common.numreduct(vers) < minvers:
477 minvers_str = '.'.join(str(c) for c in minvers)
478 vers_str = '.'.join(vers)
479 msg = (_("GlusterFS version %(version)s on server %(server)s does "
480 "not support creation of shares from snapshot. "
481 "minimum requirement: %(minversion)s") %
482 {'version': vers_str, 'server': old_gmgr.host,
483 'minversion': minvers_str})
484 LOG.error(msg)
485 raise exception.GlusterfsException(msg)
487 # Clone the snapshot. The snapshot clone, a new GlusterFS volume
488 # would serve as a share.
489 backend_snapshot_name = self._find_actual_backend_snapshot_name(
490 old_gmgr, snapshot)
491 volume = ''.join(['manila-', share['id']])
493 # Query the status of the snapshot, if it is Started, the activate
494 # step will be skipped
495 args = ('snapshot', 'info', backend_snapshot_name)
496 out, err = old_gmgr.gluster_call(
497 *args,
498 log=("Query the status of the snapshot"))
500 gfs_snapshot_state = ""
501 for gfs_snapshot_info in out.split('\t'):
502 gfs_snapshot_states = re.search(r'Started', gfs_snapshot_info,
503 re.I)
504 if gfs_snapshot_states: 504 ↛ 505line 504 didn't jump to line 505 because the condition on line 504 was never true
505 gfs_snapshot_state = "Started"
507 if gfs_snapshot_state == "Started": 507 ↛ 508line 507 didn't jump to line 508 because the condition on line 507 was never true
508 args_tuple = (('snapshot', 'clone',
509 volume, backend_snapshot_name),
510 ('volume', 'start', volume))
511 else:
512 args_tuple = (('snapshot', 'activate', backend_snapshot_name,
513 'force', '--mode=script'),
514 ('snapshot', 'clone', volume,
515 backend_snapshot_name),
516 ('volume', 'start', volume))
518 for args in args_tuple:
519 out, err = old_gmgr.gluster_call(
520 *args,
521 log=("Creating share from snapshot"))
523 # Get a manager for the new volume/share.
524 comp_vol = old_gmgr.components.copy()
525 comp_vol.update({'volume': volume})
526 gmgr = self._glustermanager(comp_vol)
527 export = self.driver._setup_via_manager(
528 {'share': share, 'manager': gmgr},
529 {'share': snapshot['share_instance'], 'manager': old_gmgr})
531 export = [export, ]
532 argseq = (('set',
533 [USER_CLONED_FROM, snapshot['share_id']]),
534 ('set', [USER_MANILA_SHARE, share['id']]))
535 for op, opargs in argseq:
536 args = ['volume', op, gmgr.volume] + opargs
537 gmgr.gluster_call(*args, log=("Creating share from snapshot"))
539 self.gluster_used_vols.add(gmgr.qualified)
540 self.private_storage.update(share['id'], {'volume': gmgr.qualified})
542 return export
544 def create_snapshot(self, context, snapshot, share_server=None):
545 """Creates a snapshot."""
547 gluster_mgr = self._share_manager(snapshot['share'])
548 if gluster_mgr.qualified in self.gluster_nosnap_vols_dict:
549 opret, operrno = -1, 0
550 operrstr = self.gluster_nosnap_vols_dict[gluster_mgr.qualified]
551 else:
552 args = ('--xml', 'snapshot', 'create', 'manila-' + snapshot['id'],
553 gluster_mgr.volume)
554 out, err = gluster_mgr.gluster_call(
555 *args,
556 log=("Retrieving volume info"))
558 if not out:
559 raise exception.GlusterfsException(
560 'gluster volume info %s: no data received' %
561 gluster_mgr.volume
562 )
564 outxml = etree.fromstring(out)
565 opret = int(common.volxml_get(outxml, 'opRet'))
566 operrno = int(common.volxml_get(outxml, 'opErrno'))
567 operrstr = common.volxml_get(outxml, 'opErrstr', default=None)
569 if opret == -1:
570 vers = self.glusterfs_versions[gluster_mgr.host_access]
571 if common.numreduct(vers) > (3, 6):
572 # This logic has not yet been implemented in GlusterFS 3.6
573 if operrno == 0: 573 ↛ 581line 573 didn't jump to line 581 because the condition on line 573 was always true
574 self.gluster_nosnap_vols_dict[
575 gluster_mgr.qualified] = operrstr
576 msg = _("Share %(share_id)s does not support snapshots: "
577 "%(errstr)s.") % {'share_id': snapshot['share_id'],
578 'errstr': operrstr}
579 LOG.error(msg)
580 raise exception.ShareSnapshotNotSupported(msg)
581 raise exception.GlusterfsException(
582 _("Creating snapshot for share %(share_id)s failed "
583 "with %(errno)d: %(errstr)s") % {
584 'share_id': snapshot['share_id'],
585 'errno': operrno,
586 'errstr': operrstr})
588 def delete_snapshot(self, context, snapshot, share_server=None):
589 """Deletes a snapshot."""
591 gluster_mgr = self._share_manager(snapshot['share'])
592 backend_snapshot_name = self._find_actual_backend_snapshot_name(
593 gluster_mgr, snapshot)
594 args = ('--xml', 'snapshot', 'delete', backend_snapshot_name,
595 '--mode=script')
596 out, err = gluster_mgr.gluster_call(
597 *args,
598 log=("Error deleting snapshot"))
600 if not out:
601 raise exception.GlusterfsException(
602 _('gluster snapshot delete %s: no data received') %
603 gluster_mgr.volume
604 )
606 outxml = etree.fromstring(out)
607 gluster_mgr.xml_response_check(outxml, args[1:])
609 def ensure_share(self, context, share, share_server=None):
610 """Invoked to ensure that share is exported."""
611 gmgr = self._share_manager(share)
612 self.gluster_used_vols.add(gmgr.qualified)
614 gmgr.set_vol_option(USER_MANILA_SHARE, share['id'])
616 # Debt...
618 def manage_existing(self, share, driver_options):
619 raise NotImplementedError()
621 def unmanage(self, share):
622 raise NotImplementedError()
624 def extend_share(self, share, new_size, share_server=None):
625 raise NotImplementedError()
627 def shrink_share(self, share, new_size, share_server=None):
628 raise NotImplementedError()