"Fossies" - the Fresh Open Source Software Archive 
Member "manila-8.1.4/manila/share/drivers/lvm.py" (19 Nov 2020, 24324 Bytes) of package /linux/misc/openstack/manila-8.1.4.tar.gz:
As a special service "Fossies" has tried to format the requested source page into HTML format using (guessed) Python source code syntax highlighting (style:
standard) with prefixed line numbers.
Alternatively you can here
view or
download the uninterpreted source code file.
For more information about "lvm.py" see the
Fossies "Dox" file reference documentation and the latest
Fossies "Diffs" side-by-side code changes report:
8.1.3_vs_8.1.4.
1 # Copyright 2012 NetApp
2 # Copyright 2016 Mirantis Inc.
3 # All Rights Reserved.
4 #
5 # Licensed under the Apache License, Version 2.0 (the "License"); you may
6 # not use this file except in compliance with the License. You may obtain
7 # a copy of the License at
8 #
9 # http://www.apache.org/licenses/LICENSE-2.0
10 #
11 # Unless required by applicable law or agreed to in writing, software
12 # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
13 # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
14 # License for the specific language governing permissions and limitations
15 # under the License.
16 """
17 LVM Driver for shares.
18
19 """
20
21 import ipaddress
22 import math
23 import os
24 import re
25
26 from oslo_config import cfg
27 from oslo_log import log
28 from oslo_utils import importutils
29 from oslo_utils import timeutils
30 import six
31
32 from manila import exception
33 from manila.i18n import _
34 from manila.share import driver
35 from manila.share.drivers import generic
36 from manila.share import utils
37
38 LOG = log.getLogger(__name__)
39
40 share_opts = [
41 cfg.StrOpt('lvm_share_export_root',
42 default='$state_path/mnt',
43 help='Base folder where exported shares are located.'),
44 cfg.StrOpt('lvm_share_export_ip',
45 deprecated_for_removal=True,
46 deprecated_reason='Use lvm_share_export_ips instead.',
47 help='IP to be added to export string.'),
48 cfg.ListOpt('lvm_share_export_ips',
49 help='List of IPs to export shares.'),
50 cfg.IntOpt('lvm_share_mirrors',
51 default=0,
52 help='If set, create LVMs with multiple mirrors. Note that '
53 'this requires lvm_mirrors + 2 PVs with available space.'),
54 cfg.StrOpt('lvm_share_volume_group',
55 default='lvm-shares',
56 help='Name for the VG that will contain exported shares.'),
57 cfg.ListOpt('lvm_share_helpers',
58 default=[
59 'CIFS=manila.share.drivers.helpers.CIFSHelperUserAccess',
60 'NFS=manila.share.drivers.helpers.NFSHelper',
61 ],
62 help='Specify list of share export helpers.'),
63 ]
64
65 CONF = cfg.CONF
66 CONF.register_opts(share_opts)
67 CONF.register_opts(generic.share_opts)
68
69
70 class LVMMixin(driver.ExecuteMixin):
71 def check_for_setup_error(self):
72 """Returns an error if prerequisites aren't met."""
73 out, err = self._execute('vgs', '--noheadings', '-o', 'name',
74 run_as_root=True)
75 volume_groups = out.split()
76 if self.configuration.lvm_share_volume_group not in volume_groups:
77 msg = (_("Share volume group %s doesn't exist.")
78 % self.configuration.lvm_share_volume_group)
79 raise exception.InvalidParameterValue(err=msg)
80
81 if (self.configuration.lvm_share_export_ip and
82 self.configuration.lvm_share_export_ips):
83 msg = (_("Only one of lvm_share_export_ip or lvm_share_export_ips"
84 " may be specified."))
85 raise exception.InvalidParameterValue(err=msg)
86 if not (self.configuration.lvm_share_export_ip or
87 self.configuration.lvm_share_export_ips):
88 msg = (_("Neither lvm_share_export_ip nor lvm_share_export_ips is"
89 " specified."))
90 raise exception.InvalidParameterValue(err=msg)
91
92 def _allocate_container(self, share):
93 sizestr = '%sG' % share['size']
94 cmd = ['lvcreate', '-L', sizestr, '-n', share['name'],
95 self.configuration.lvm_share_volume_group]
96 if self.configuration.lvm_share_mirrors:
97 cmd += ['-m', self.configuration.lvm_share_mirrors, '--nosync']
98 terras = int(sizestr[:-1]) / 1024.0
99 if terras >= 1.5:
100 rsize = int(2 ** math.ceil(math.log(terras) / math.log(2)))
101 # NOTE(vish): Next power of two for region size. See:
102 # http://red.ht/U2BPOD
103 cmd += ['-R', six.text_type(rsize)]
104
105 self._try_execute(*cmd, run_as_root=True)
106 device_name = self._get_local_path(share)
107 self._execute('mkfs.%s' % self.configuration.share_volume_fstype,
108 device_name, run_as_root=True)
109
110 def _extend_container(self, share, device_name, size):
111 cmd = ['lvextend', '-L', '%sG' % size, '-r', device_name]
112 self._try_execute(*cmd, run_as_root=True)
113
114 def _deallocate_container(self, share_name):
115 """Deletes a logical volume for share."""
116 try:
117 self._try_execute('lvremove', '-f', "%s/%s" %
118 (self.configuration.lvm_share_volume_group,
119 share_name), run_as_root=True)
120 except exception.ProcessExecutionError as exc:
121 err_pattern = re.compile(".*failed to find.*|.*not found.*",
122 re.IGNORECASE)
123 if not err_pattern.match(exc.stderr):
124 LOG.exception("Error deleting volume")
125 raise
126 LOG.warning("Volume not found: %s", exc.stderr)
127
128 def _create_snapshot(self, context, snapshot):
129 """Creates a snapshot."""
130 orig_lv_name = "%s/%s" % (self.configuration.lvm_share_volume_group,
131 snapshot['share_name'])
132 self._try_execute(
133 'lvcreate', '-L', '%sG' % snapshot['share']['size'],
134 '--name', snapshot['name'],
135 '--snapshot', orig_lv_name, run_as_root=True)
136
137 self._set_random_uuid_to_device(snapshot)
138
139 def _set_random_uuid_to_device(self, share_or_snapshot):
140 # NOTE(vponomaryov): 'tune2fs' is required to make
141 # filesystem of share created from snapshot have
142 # unique ID, in case of LVM volumes, by default,
143 # it will have the same UUID as source volume. Closes #1645751
144 # NOTE(gouthamr): Executing tune2fs -U only works on
145 # a recently checked filesystem.
146 # See: https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=857336
147 device_path = self._get_local_path(share_or_snapshot)
148 self._execute('e2fsck', '-y', '-f', device_path, run_as_root=True)
149 self._execute(
150 'tune2fs', '-U', 'random', device_path, run_as_root=True,
151 )
152
153 def create_snapshot(self, context, snapshot, share_server=None):
154 self._create_snapshot(context, snapshot)
155
156 def delete_snapshot(self, context, snapshot, share_server=None):
157 """Deletes a snapshot."""
158 self._deallocate_container(snapshot['name'])
159
160
161 class LVMShareDriver(LVMMixin, driver.ShareDriver):
162 """Executes commands relating to Shares."""
163
164 def __init__(self, *args, **kwargs):
165 """Do initialization."""
166 super(LVMShareDriver, self).__init__([False], *args, **kwargs)
167 self.configuration.append_config_values(share_opts)
168 self.configuration.append_config_values(generic.share_opts)
169 self.configuration.share_mount_path = (
170 self.configuration.lvm_share_export_root)
171 self._helpers = None
172 self.configured_ip_version = None
173 self.backend_name = self.configuration.safe_get(
174 'share_backend_name') or 'LVM'
175 # Set of parameters used for compatibility with
176 # Generic driver's helpers.
177 self.share_server = {
178 'instance_id': self.backend_name,
179 'lock_name': 'manila_lvm',
180 }
181 if self.configuration.lvm_share_export_ip:
182 self.share_server['public_addresses'] = [
183 self.configuration.lvm_share_export_ip]
184 else:
185 self.share_server['public_addresses'] = (
186 self.configuration.lvm_share_export_ips)
187 self.ipv6_implemented = True
188
189 def _ssh_exec_as_root(self, server, command, check_exit_code=True):
190 kwargs = {}
191 if 'sudo' in command:
192 kwargs['run_as_root'] = True
193 command.remove('sudo')
194 kwargs['check_exit_code'] = check_exit_code
195 return self._execute(*command, **kwargs)
196
197 def do_setup(self, context):
198 """Any initialization the volume driver does while starting."""
199 super(LVMShareDriver, self).do_setup(context)
200 self._setup_helpers()
201
202 def _setup_helpers(self):
203 """Initializes protocol-specific NAS drivers."""
204 self._helpers = {}
205 for helper_str in self.configuration.lvm_share_helpers:
206 share_proto, _, import_str = helper_str.partition('=')
207 helper = importutils.import_class(import_str)
208 # TODO(rushiagr): better way to handle configuration
209 # instead of just passing to the helper
210 self._helpers[share_proto.upper()] = helper(
211 self._execute, self._ssh_exec_as_root, self.configuration)
212
213 def _get_local_path(self, share):
214 # The escape characters are expected by the device mapper.
215 escaped_group = (
216 self.configuration.lvm_share_volume_group.replace('-', '--'))
217 escaped_name = share['name'].replace('-', '--')
218 return "/dev/mapper/%s-%s" % (escaped_group, escaped_name)
219
220 def _update_share_stats(self):
221 """Retrieve stats info from share volume group."""
222 data = {
223 'share_backend_name': self.backend_name,
224 'storage_protocol': 'NFS_CIFS',
225 'reserved_percentage':
226 self.configuration.reserved_share_percentage,
227 'snapshot_support': True,
228 'create_share_from_snapshot_support': True,
229 'revert_to_snapshot_support': True,
230 'mount_snapshot_support': True,
231 'driver_name': 'LVMShareDriver',
232 'pools': self.get_share_server_pools(),
233 }
234 super(LVMShareDriver, self)._update_share_stats(data)
235
236 def get_share_server_pools(self, share_server=None):
237 out, err = self._execute('vgs',
238 self.configuration.lvm_share_volume_group,
239 '--rows', '--units', 'g',
240 run_as_root=True)
241 total_size = re.findall("VSize\s[0-9.]+g", out)[0][6:-1]
242 free_size = re.findall("VFree\s[0-9.]+g", out)[0][6:-1]
243 return [{
244 'pool_name': 'lvm-single-pool',
245 'total_capacity_gb': float(total_size),
246 'free_capacity_gb': float(free_size),
247 'reserved_percentage': 0,
248 }, ]
249
250 def create_share(self, context, share, share_server=None):
251 self._allocate_container(share)
252 # create file system
253 device_name = self._get_local_path(share)
254 location = self._get_helper(share).create_exports(
255 self.share_server, share['name'])
256 self._mount_device(share, device_name)
257 return location
258
259 def create_share_from_snapshot(self, context, share, snapshot,
260 share_server=None):
261 """Is called to create share from snapshot."""
262 self._allocate_container(share)
263 snapshot_device_name = self._get_local_path(snapshot)
264 share_device_name = self._get_local_path(share)
265 self._set_random_uuid_to_device(share)
266 self._copy_volume(
267 snapshot_device_name, share_device_name, share['size'])
268 location = self._get_helper(share).create_exports(
269 self.share_server, share['name'])
270 self._mount_device(share, share_device_name)
271 return location
272
273 def delete_share(self, context, share, share_server=None):
274 self._unmount_device(share, raise_if_missing=False)
275 self._delete_share(context, share)
276 self._deallocate_container(share['name'])
277
278 def _unmount_device(self, share_or_snapshot, raise_if_missing=True):
279 """Unmount the filesystem of a share or snapshot LV."""
280 mount_path = self._get_mount_path(share_or_snapshot)
281 if os.path.exists(mount_path):
282 # umount, may be busy
283 try:
284 self._execute('umount', '-f', mount_path, run_as_root=True)
285 except exception.ProcessExecutionError as exc:
286 if 'device is busy' in exc.stderr.lower():
287 raise exception.ShareBusyException(
288 reason=share_or_snapshot['name'])
289 elif 'not mounted' in exc.stderr.lower():
290 if raise_if_missing:
291 LOG.error('Unable to find device: %s', exc)
292 raise
293 else:
294 LOG.error('Unable to umount: %s', exc)
295 raise
296 # remove dir
297 self._execute('rmdir', mount_path, run_as_root=True)
298
299 def ensure_shares(self, context, shares):
300 updates = {}
301 for share in shares:
302 updates[share['id']] = {
303 'export_locations': self.ensure_share(context, share)}
304 return updates
305
306 def ensure_share(self, ctx, share, share_server=None):
307 """Ensure that storage are mounted and exported."""
308 device_name = self._get_local_path(share)
309 self._mount_device(share, device_name)
310 return self._get_helper(share).create_exports(
311 self.share_server, share['name'], recreate=True)
312
313 def _delete_share(self, ctx, share):
314 """Delete a share."""
315 try:
316 self._get_helper(share).remove_exports(
317 self.share_server, share['name'])
318 except exception.ProcessExecutionError:
319 LOG.warning("Can't remove share %r", share['id'])
320 except exception.InvalidShare as exc:
321 LOG.warning(exc)
322
323 def update_access(self, context, share, access_rules, add_rules,
324 delete_rules, share_server=None):
325 """Update access rules for given share.
326
327 This driver has two different behaviors according to parameters:
328 1. Recovery after error - 'access_rules' contains all access_rules,
329 'add_rules' and 'delete_rules' shall be empty. Previously existing
330 access rules are cleared and then added back according
331 to 'access_rules'.
332
333 2. Adding/Deleting of several access rules - 'access_rules' contains
334 all access_rules, 'add_rules' and 'delete_rules' contain rules which
335 should be added/deleted. Rules in 'access_rules' are ignored and
336 only rules from 'add_rules' and 'delete_rules' are applied.
337
338 :param context: Current context
339 :param share: Share model with share data.
340 :param access_rules: All access rules for given share
341 :param add_rules: Empty List or List of access rules which should be
342 added. access_rules already contains these rules.
343 :param delete_rules: Empty List or List of access rules which should be
344 removed. access_rules doesn't contain these rules.
345 :param share_server: None or Share server model
346 """
347 self._get_helper(share).update_access(self.share_server,
348 share['name'], access_rules,
349 add_rules=add_rules,
350 delete_rules=delete_rules)
351
352 def _get_helper(self, share):
353 if share['share_proto'].lower().startswith('nfs'):
354 return self._helpers['NFS']
355 elif share['share_proto'].lower().startswith('cifs'):
356 return self._helpers['CIFS']
357 else:
358 raise exception.InvalidShare(reason='Wrong share protocol')
359
360 def _mount_device(self, share_or_snapshot, device_name):
361 """Mount LV for share or snapshot and ignore if already mounted."""
362 mount_path = self._get_mount_path(share_or_snapshot)
363 self._execute('mkdir', '-p', mount_path)
364 try:
365 self._execute('mount', device_name, mount_path,
366 run_as_root=True, check_exit_code=True)
367 self._execute('chmod', '777', mount_path,
368 run_as_root=True, check_exit_code=True)
369 except exception.ProcessExecutionError:
370 out, err = self._execute('mount', '-l', run_as_root=True)
371 if device_name in out:
372 LOG.warning("%s is already mounted", device_name)
373 else:
374 raise
375 return mount_path
376
377 def _get_mount_path(self, share_or_snapshot):
378 """Returns path where share or snapshot is mounted."""
379 return os.path.join(self.configuration.share_mount_path,
380 share_or_snapshot['name'])
381
382 def _copy_volume(self, srcstr, deststr, size_in_g):
383 # Use O_DIRECT to avoid thrashing the system buffer cache
384 extra_flags = ['iflag=direct', 'oflag=direct']
385
386 # Check whether O_DIRECT is supported
387 try:
388 self._execute('dd', 'count=0', 'if=%s' % srcstr, 'of=%s' % deststr,
389 *extra_flags, run_as_root=True)
390 except exception.ProcessExecutionError:
391 extra_flags = []
392
393 # Perform the copy
394 self._execute('dd', 'if=%s' % srcstr, 'of=%s' % deststr,
395 'count=%d' % (size_in_g * 1024), 'bs=1M',
396 *extra_flags, run_as_root=True)
397
398 def extend_share(self, share, new_size, share_server=None):
399 device_name = self._get_local_path(share)
400 self._extend_container(share, device_name, new_size)
401
402 def revert_to_snapshot(self, context, snapshot, share_access_rules,
403 snapshot_access_rules, share_server=None):
404 share = snapshot['share']
405 # Temporarily remove all access rules
406 self._get_helper(share).update_access(self.share_server,
407 snapshot['name'], [], [], [])
408 self._get_helper(share).update_access(self.share_server,
409 share['name'], [], [], [])
410 # Unmount the snapshot filesystem
411 self._unmount_device(snapshot)
412 # Unmount the share filesystem
413 self._unmount_device(share)
414 # Merge the snapshot LV back into the share, reverting it
415 snap_lv_name = "%s/%s" % (self.configuration.lvm_share_volume_group,
416 snapshot['name'])
417 self._execute('lvconvert', '--merge', snap_lv_name, run_as_root=True)
418
419 # Now recreate the snapshot that was destroyed by the merge
420 self._create_snapshot(context, snapshot)
421 # At this point we can mount the share again
422 device_name = self._get_local_path(share)
423 self._mount_device(share, device_name)
424 # Also remount the snapshot
425 device_name = self._get_local_path(snapshot)
426 self._mount_device(snapshot, device_name)
427 # Lastly we add all the access rules back
428 self._get_helper(share).update_access(self.share_server,
429 share['name'],
430 share_access_rules,
431 [], [])
432 snapshot_access_rules, __, __ = utils.change_rules_to_readonly(
433 snapshot_access_rules, [], [])
434 self._get_helper(share).update_access(self.share_server,
435 snapshot['name'],
436 snapshot_access_rules,
437 [], [])
438
439 def create_snapshot(self, context, snapshot, share_server=None):
440 self._create_snapshot(context, snapshot)
441
442 device_name = self._get_local_path(snapshot)
443 self._mount_device(snapshot, device_name)
444
445 helper = self._get_helper(snapshot['share'])
446 exports = helper.create_exports(self.share_server, snapshot['name'])
447
448 return {'export_locations': exports}
449
450 def delete_snapshot(self, context, snapshot, share_server=None):
451 self._unmount_device(snapshot, raise_if_missing=False)
452
453 super(LVMShareDriver, self).delete_snapshot(context, snapshot,
454 share_server)
455
456 def get_configured_ip_versions(self):
457 if self.configured_ip_version is None:
458 try:
459 self.configured_ip_version = []
460 if self.configuration.lvm_share_export_ip:
461 self.configured_ip_version.append(ipaddress.ip_address(
462 six.text_type(
463 self.configuration.lvm_share_export_ip)).version)
464 else:
465 for ip in self.configuration.lvm_share_export_ips:
466 self.configured_ip_version.append(
467 ipaddress.ip_address(six.text_type(ip)).version)
468 except Exception:
469 if self.configuration.lvm_share_export_ip:
470 message = (_("Invalid 'lvm_share_export_ip' option "
471 "supplied %s.") %
472 self.configuration.lvm_share_export_ip)
473 else:
474 message = (_("Invalid 'lvm_share_export_ips' option "
475 "supplied %s.") %
476 self.configuration.lvm_share_export_ips)
477 raise exception.InvalidInput(reason=message)
478 return self.configured_ip_version
479
480 def snapshot_update_access(self, context, snapshot, access_rules,
481 add_rules, delete_rules, share_server=None):
482 """Update access rules for given snapshot.
483
484 This driver has two different behaviors according to parameters:
485 1. Recovery after error - 'access_rules' contains all access_rules,
486 'add_rules' and 'delete_rules' shall be empty. Previously existing
487 access rules are cleared and then added back according
488 to 'access_rules'.
489
490 2. Adding/Deleting of several access rules - 'access_rules' contains
491 all access_rules, 'add_rules' and 'delete_rules' contain rules which
492 should be added/deleted. Rules in 'access_rules' are ignored and
493 only rules from 'add_rules' and 'delete_rules' are applied.
494
495 :param context: Current context
496 :param snapshot: Snapshot model with snapshot data.
497 :param access_rules: All access rules for given snapshot
498 :param add_rules: Empty List or List of access rules which should be
499 added. access_rules already contains these rules.
500 :param delete_rules: Empty List or List of access rules which should be
501 removed. access_rules doesn't contain these rules.
502 :param share_server: None or Share server model
503 """
504 helper = self._get_helper(snapshot['share'])
505 access_rules, add_rules, delete_rules = utils.change_rules_to_readonly(
506 access_rules, add_rules, delete_rules)
507
508 helper.update_access(self.share_server,
509 snapshot['name'], access_rules,
510 add_rules=add_rules, delete_rules=delete_rules)
511
512 def update_share_usage_size(self, context, shares):
513 updated_shares = []
514 out, err = self._execute(
515 'df', '-l', '--output=target,used',
516 '--block-size=g')
517 gathered_at = timeutils.utcnow()
518
519 for share in shares:
520 try:
521 mount_path = self._get_mount_path(share)
522 if os.path.exists(mount_path):
523 used_size = (re.findall(
524 mount_path + "\s*[0-9.]+G", out)[0].
525 split(' ')[-1][:-1])
526 updated_shares.append({'id': share['id'],
527 'used_size': used_size,
528 'gathered_at': gathered_at})
529 else:
530 raise exception.NotFound(
531 _("Share mount path %s could not be "
532 "found.") % mount_path)
533 except Exception:
534 LOG.exception("Failed to gather 'used_size' for share %s.",
535 share['id'])
536
537 return updated_shares
538
539 def get_backend_info(self, context):
540 return {
541 'export_ips': ','.join(self.share_server['public_addresses']),
542 'db_version': utils.get_recent_db_migration_id(),
543 }