Package ganeti :: Package storage :: Module bdev
[hide private]
[frames] | no frames]

Source Code for Module ganeti.storage.bdev

   1  # 
   2  # 
   3   
   4  # Copyright (C) 2006, 2007, 2010, 2011, 2012, 2013 Google Inc. 
   5  # All rights reserved. 
   6  # 
   7  # Redistribution and use in source and binary forms, with or without 
   8  # modification, are permitted provided that the following conditions are 
   9  # met: 
  10  # 
  11  # 1. Redistributions of source code must retain the above copyright notice, 
  12  # this list of conditions and the following disclaimer. 
  13  # 
  14  # 2. Redistributions in binary form must reproduce the above copyright 
  15  # notice, this list of conditions and the following disclaimer in the 
  16  # documentation and/or other materials provided with the distribution. 
  17  # 
  18  # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS 
  19  # IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED 
  20  # TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 
  21  # PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR 
  22  # CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, 
  23  # EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, 
  24  # PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 
  25  # PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 
  26  # LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 
  27  # NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 
  28  # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 
  29   
  30   
  31  """Block device abstraction. 
  32   
  33  """ 
  34   
  35  import re 
  36  import stat 
  37  import os 
  38  import logging 
  39  import math 
  40   
  41  from ganeti import utils 
  42  from ganeti import errors 
  43  from ganeti import constants 
  44  from ganeti import objects 
  45  from ganeti import compat 
  46  from ganeti import pathutils 
  47  from ganeti import serializer 
  48  from ganeti.storage import base 
  49  from ganeti.storage import drbd 
  50  from ganeti.storage.filestorage import FileStorage 
  51  from ganeti.storage.gluster import GlusterStorage 
52 53 54 -class RbdShowmappedJsonError(Exception):
55 """`rbd showmmapped' JSON formatting error Exception class. 56 57 """ 58 pass
59
60 61 -def _CheckResult(result):
62 """Throws an error if the given result is a failed one. 63 64 @param result: result from RunCmd 65 66 """ 67 if result.failed: 68 base.ThrowError("Command: %s error: %s - %s", 69 result.cmd, result.fail_reason, result.output)
70
71 72 -class LogicalVolume(base.BlockDev):
73 """Logical Volume block device. 74 75 """ 76 _VALID_NAME_RE = re.compile("^[a-zA-Z0-9+_.-]*$") 77 _PARSE_PV_DEV_RE = re.compile(r"^([^ ()]+)\([0-9]+\)$") 78 _INVALID_NAMES = compat.UniqueFrozenset([".", "..", "snapshot", "pvmove"]) 79 _INVALID_SUBSTRINGS = compat.UniqueFrozenset(["_mlog", "_mimage"]) 80
81 - def __init__(self, unique_id, children, size, params, dyn_params, *args):
82 """Attaches to a LV device. 83 84 The unique_id is a tuple (vg_name, lv_name) 85 86 """ 87 super(LogicalVolume, self).__init__(unique_id, children, size, params, 88 dyn_params, *args) 89 if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2: 90 raise ValueError("Invalid configuration data %s" % str(unique_id)) 91 self._vg_name, self._lv_name = unique_id 92 self._ValidateName(self._vg_name) 93 self._ValidateName(self._lv_name) 94 self.dev_path = utils.PathJoin("/dev", self._vg_name, self._lv_name) 95 self._degraded = True 96 self.major = self.minor = self.pe_size = self.stripe_count = None 97 self.pv_names = None 98 self.Attach()
99 100 @staticmethod
101 - def _GetStdPvSize(pvs_info):
102 """Return the the standard PV size (used with exclusive storage). 103 104 @param pvs_info: list of objects.LvmPvInfo, cannot be empty 105 @rtype: float 106 @return: size in MiB 107 108 """ 109 assert len(pvs_info) > 0 110 smallest = min([pv.size for pv in pvs_info]) 111 return smallest / (1 + constants.PART_MARGIN + constants.PART_RESERVED)
112 113 @staticmethod
114 - def _ComputeNumPvs(size, pvs_info):
115 """Compute the number of PVs needed for an LV (with exclusive storage). 116 117 @type size: float 118 @param size: LV size in MiB 119 @param pvs_info: list of objects.LvmPvInfo, cannot be empty 120 @rtype: integer 121 @return: number of PVs needed 122 """ 123 assert len(pvs_info) > 0 124 pv_size = float(LogicalVolume._GetStdPvSize(pvs_info)) 125 return int(math.ceil(float(size) / pv_size))
126 127 @staticmethod
128 - def _GetEmptyPvNames(pvs_info, max_pvs=None):
129 """Return a list of empty PVs, by name. 130 131 """ 132 empty_pvs = filter(objects.LvmPvInfo.IsEmpty, pvs_info) 133 if max_pvs is not None: 134 empty_pvs = empty_pvs[:max_pvs] 135 return map((lambda pv: pv.name), empty_pvs)
136 137 @classmethod
138 - def Create(cls, unique_id, children, size, spindles, params, excl_stor, 139 dyn_params, *args):
140 """Create a new logical volume. 141 142 """ 143 if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2: 144 raise errors.ProgrammerError("Invalid configuration data %s" % 145 str(unique_id)) 146 vg_name, lv_name = unique_id 147 cls._ValidateName(vg_name) 148 cls._ValidateName(lv_name) 149 pvs_info = cls.GetPVInfo([vg_name]) 150 if not pvs_info: 151 if excl_stor: 152 msg = "No (empty) PVs found" 153 else: 154 msg = "Can't compute PV info for vg %s" % vg_name 155 base.ThrowError(msg) 156 pvs_info.sort(key=(lambda pv: pv.free), reverse=True) 157 158 pvlist = [pv.name for pv in pvs_info] 159 if compat.any(":" in v for v in pvlist): 160 base.ThrowError("Some of your PVs have the invalid character ':' in their" 161 " name, this is not supported - please filter them out" 162 " in lvm.conf using either 'filter' or 'preferred_names'") 163 164 current_pvs = len(pvlist) 165 desired_stripes = params[constants.LDP_STRIPES] 166 stripes = min(current_pvs, desired_stripes) 167 168 if excl_stor: 169 if spindles is None: 170 base.ThrowError("Unspecified number of spindles: this is required" 171 "when exclusive storage is enabled, try running" 172 " gnt-cluster repair-disk-sizes") 173 (err_msgs, _) = utils.LvmExclusiveCheckNodePvs(pvs_info) 174 if err_msgs: 175 for m in err_msgs: 176 logging.warning(m) 177 req_pvs = cls._ComputeNumPvs(size, pvs_info) 178 if spindles < req_pvs: 179 base.ThrowError("Requested number of spindles (%s) is not enough for" 180 " a disk of %d MB (at least %d spindles needed)", 181 spindles, size, req_pvs) 182 else: 183 req_pvs = spindles 184 pvlist = cls._GetEmptyPvNames(pvs_info, req_pvs) 185 current_pvs = len(pvlist) 186 if current_pvs < req_pvs: 187 base.ThrowError("Not enough empty PVs (spindles) to create a disk of %d" 188 " MB: %d available, %d needed", 189 size, current_pvs, req_pvs) 190 assert current_pvs == len(pvlist) 191 # We must update stripes to be sure to use all the desired spindles 192 stripes = current_pvs 193 if stripes > desired_stripes: 194 # Don't warn when lowering stripes, as it's no surprise 195 logging.warning("Using %s stripes instead of %s, to be able to use" 196 " %s spindles", stripes, desired_stripes, current_pvs) 197 198 else: 199 if stripes < desired_stripes: 200 logging.warning("Could not use %d stripes for VG %s, as only %d PVs are" 201 " available.", desired_stripes, vg_name, current_pvs) 202 free_size = sum([pv.free for pv in pvs_info]) 203 # The size constraint should have been checked from the master before 204 # calling the create function. 205 if free_size < size: 206 base.ThrowError("Not enough free space: required %s," 207 " available %s", size, free_size) 208 209 # If the free space is not well distributed, we won't be able to 210 # create an optimally-striped volume; in that case, we want to try 211 # with N, N-1, ..., 2, and finally 1 (non-stripped) number of 212 # stripes 213 cmd = ["lvcreate", "-L%dm" % size, "-n%s" % lv_name] 214 for stripes_arg in range(stripes, 0, -1): 215 result = utils.RunCmd(cmd + ["-i%d" % stripes_arg] + [vg_name] + pvlist) 216 if not result.failed: 217 break 218 if result.failed: 219 base.ThrowError("LV create failed (%s): %s", 220 result.fail_reason, result.output) 221 return LogicalVolume(unique_id, children, size, params, dyn_params, *args)
222 223 @staticmethod
224 - def _GetVolumeInfo(lvm_cmd, fields):
225 """Returns LVM Volume infos using lvm_cmd 226 227 @param lvm_cmd: Should be one of "pvs", "vgs" or "lvs" 228 @param fields: Fields to return 229 @return: A list of dicts each with the parsed fields 230 231 """ 232 if not fields: 233 raise errors.ProgrammerError("No fields specified") 234 235 sep = "|" 236 cmd = [lvm_cmd, "--noheadings", "--nosuffix", "--units=m", "--unbuffered", 237 "--separator=%s" % sep, "-o%s" % ",".join(fields)] 238 239 result = utils.RunCmd(cmd) 240 if result.failed: 241 raise errors.CommandError("Can't get the volume information: %s - %s" % 242 (result.fail_reason, result.output)) 243 244 data = [] 245 for line in result.stdout.splitlines(): 246 splitted_fields = line.strip().split(sep) 247 248 if len(fields) != len(splitted_fields): 249 raise errors.CommandError("Can't parse %s output: line '%s'" % 250 (lvm_cmd, line)) 251 252 data.append(splitted_fields) 253 254 return data
255 256 @classmethod
257 - def GetPVInfo(cls, vg_names, filter_allocatable=True, include_lvs=False):
258 """Get the free space info for PVs in a volume group. 259 260 @param vg_names: list of volume group names, if empty all will be returned 261 @param filter_allocatable: whether to skip over unallocatable PVs 262 @param include_lvs: whether to include a list of LVs hosted on each PV 263 264 @rtype: list 265 @return: list of objects.LvmPvInfo objects 266 267 """ 268 # We request "lv_name" field only if we care about LVs, so we don't get 269 # a long list of entries with many duplicates unless we really have to. 270 # The duplicate "pv_name" field will be ignored. 271 if include_lvs: 272 lvfield = "lv_name" 273 else: 274 lvfield = "pv_name" 275 try: 276 info = cls._GetVolumeInfo("pvs", ["pv_name", "vg_name", "pv_free", 277 "pv_attr", "pv_size", lvfield]) 278 except errors.GenericError, err: 279 logging.error("Can't get PV information: %s", err) 280 return None 281 282 # When asked for LVs, "pvs" may return multiple entries for the same PV-LV 283 # pair. We sort entries by PV name and then LV name, so it's easy to weed 284 # out duplicates. 285 if include_lvs: 286 info.sort(key=(lambda i: (i[0], i[5]))) 287 data = [] 288 lastpvi = None 289 for (pv_name, vg_name, pv_free, pv_attr, pv_size, lv_name) in info: 290 # (possibly) skip over pvs which are not allocatable 291 if filter_allocatable and pv_attr[0] != "a": 292 continue 293 # (possibly) skip over pvs which are not in the right volume group(s) 294 if vg_names and vg_name not in vg_names: 295 continue 296 # Beware of duplicates (check before inserting) 297 if lastpvi and lastpvi.name == pv_name: 298 if include_lvs and lv_name: 299 if not lastpvi.lv_list or lastpvi.lv_list[-1] != lv_name: 300 lastpvi.lv_list.append(lv_name) 301 else: 302 if include_lvs and lv_name: 303 lvl = [lv_name] 304 else: 305 lvl = [] 306 lastpvi = objects.LvmPvInfo(name=pv_name, vg_name=vg_name, 307 size=float(pv_size), free=float(pv_free), 308 attributes=pv_attr, lv_list=lvl) 309 data.append(lastpvi) 310 311 return data
312 313 @classmethod
314 - def _GetRawFreePvInfo(cls, vg_name):
315 """Return info (size/free) about PVs. 316 317 @type vg_name: string 318 @param vg_name: VG name 319 @rtype: tuple 320 @return: (standard_pv_size_in_MiB, number_of_free_pvs, total_number_of_pvs) 321 322 """ 323 pvs_info = cls.GetPVInfo([vg_name]) 324 if not pvs_info: 325 pv_size = 0.0 326 free_pvs = 0 327 num_pvs = 0 328 else: 329 pv_size = cls._GetStdPvSize(pvs_info) 330 free_pvs = len(cls._GetEmptyPvNames(pvs_info)) 331 num_pvs = len(pvs_info) 332 return (pv_size, free_pvs, num_pvs)
333 334 @classmethod
335 - def _GetExclusiveStorageVgFree(cls, vg_name):
336 """Return the free disk space in the given VG, in exclusive storage mode. 337 338 @type vg_name: string 339 @param vg_name: VG name 340 @rtype: float 341 @return: free space in MiB 342 """ 343 (pv_size, free_pvs, _) = cls._GetRawFreePvInfo(vg_name) 344 return pv_size * free_pvs
345 346 @classmethod
347 - def GetVgSpindlesInfo(cls, vg_name):
348 """Get the free space info for specific VGs. 349 350 @param vg_name: volume group name 351 @rtype: tuple 352 @return: (free_spindles, total_spindles) 353 354 """ 355 (_, free_pvs, num_pvs) = cls._GetRawFreePvInfo(vg_name) 356 return (free_pvs, num_pvs)
357 358 @classmethod
359 - def GetVGInfo(cls, vg_names, excl_stor, filter_readonly=True):
360 """Get the free space info for specific VGs. 361 362 @param vg_names: list of volume group names, if empty all will be returned 363 @param excl_stor: whether exclusive_storage is enabled 364 @param filter_readonly: whether to skip over readonly VGs 365 366 @rtype: list 367 @return: list of tuples (free_space, total_size, name) with free_space in 368 MiB 369 370 """ 371 try: 372 info = cls._GetVolumeInfo("vgs", ["vg_name", "vg_free", "vg_attr", 373 "vg_size"]) 374 except errors.GenericError, err: 375 logging.error("Can't get VG information: %s", err) 376 return None 377 378 data = [] 379 for vg_name, vg_free, vg_attr, vg_size in info: 380 # (possibly) skip over vgs which are not writable 381 if filter_readonly and vg_attr[0] == "r": 382 continue 383 # (possibly) skip over vgs which are not in the right volume group(s) 384 if vg_names and vg_name not in vg_names: 385 continue 386 # Exclusive storage needs a different concept of free space 387 if excl_stor: 388 es_free = cls._GetExclusiveStorageVgFree(vg_name) 389 assert es_free <= vg_free 390 vg_free = es_free 391 data.append((float(vg_free), float(vg_size), vg_name)) 392 393 return data
394 395 @classmethod
396 - def _ValidateName(cls, name):
397 """Validates that a given name is valid as VG or LV name. 398 399 The list of valid characters and restricted names is taken out of 400 the lvm(8) manpage, with the simplification that we enforce both 401 VG and LV restrictions on the names. 402 403 """ 404 if (not cls._VALID_NAME_RE.match(name) or 405 name in cls._INVALID_NAMES or 406 compat.any(substring in name for substring in cls._INVALID_SUBSTRINGS)): 407 base.ThrowError("Invalid LVM name '%s'", name)
408
409 - def Remove(self):
410 """Remove this logical volume. 411 412 """ 413 if not self.minor and not self.Attach(): 414 # the LV does not exist 415 return 416 result = utils.RunCmd(["lvremove", "-f", "%s/%s" % 417 (self._vg_name, self._lv_name)]) 418 if result.failed: 419 base.ThrowError("Can't lvremove: %s - %s", 420 result.fail_reason, result.output)
421
422 - def Rename(self, new_id):
423 """Rename this logical volume. 424 425 """ 426 if not isinstance(new_id, (tuple, list)) or len(new_id) != 2: 427 raise errors.ProgrammerError("Invalid new logical id '%s'" % new_id) 428 new_vg, new_name = new_id 429 if new_vg != self._vg_name: 430 raise errors.ProgrammerError("Can't move a logical volume across" 431 " volume groups (from %s to to %s)" % 432 (self._vg_name, new_vg)) 433 result = utils.RunCmd(["lvrename", new_vg, self._lv_name, new_name]) 434 if result.failed: 435 base.ThrowError("Failed to rename the logical volume: %s", result.output) 436 self._lv_name = new_name 437 self.dev_path = utils.PathJoin("/dev", self._vg_name, self._lv_name)
438 439 @classmethod
440 - def _ParseLvInfoLine(cls, line, sep):
441 """Parse one line of the lvs output used in L{_GetLvInfo}. 442 443 """ 444 elems = line.strip().rstrip(sep).split(sep) 445 if len(elems) != 6: 446 base.ThrowError("Can't parse LVS output, len(%s) != 6", str(elems)) 447 448 (status, major, minor, pe_size, stripes, pvs) = elems 449 if len(status) < 6: 450 base.ThrowError("lvs lv_attr is not at least 6 characters (%s)", status) 451 452 try: 453 major = int(major) 454 minor = int(minor) 455 except (TypeError, ValueError), err: 456 base.ThrowError("lvs major/minor cannot be parsed: %s", str(err)) 457 458 try: 459 pe_size = int(float(pe_size)) 460 except (TypeError, ValueError), err: 461 base.ThrowError("Can't parse vg extent size: %s", err) 462 463 try: 464 stripes = int(stripes) 465 except (TypeError, ValueError), err: 466 base.ThrowError("Can't parse the number of stripes: %s", err) 467 468 pv_names = [] 469 for pv in pvs.split(","): 470 m = re.match(cls._PARSE_PV_DEV_RE, pv) 471 if not m: 472 base.ThrowError("Can't parse this device list: %s", pvs) 473 pv_names.append(m.group(1)) 474 assert len(pv_names) > 0 475 476 return (status, major, minor, pe_size, stripes, pv_names)
477 478 @classmethod
479 - def _GetLvInfo(cls, dev_path, _run_cmd=utils.RunCmd):
480 """Get info about the given existing LV to be used. 481 482 """ 483 sep = "|" 484 result = _run_cmd(["lvs", "--noheadings", "--separator=%s" % sep, 485 "--units=k", "--nosuffix", 486 "-olv_attr,lv_kernel_major,lv_kernel_minor," 487 "vg_extent_size,stripes,devices", dev_path]) 488 if result.failed: 489 base.ThrowError("Can't find LV %s: %s, %s", 490 dev_path, result.fail_reason, result.output) 491 # the output can (and will) have multiple lines for multi-segment 492 # LVs, as the 'stripes' parameter is a segment one, so we take 493 # only the last entry, which is the one we're interested in; note 494 # that with LVM2 anyway the 'stripes' value must be constant 495 # across segments, so this is a no-op actually 496 out = result.stdout.splitlines() 497 if not out: # totally empty result? splitlines() returns at least 498 # one line for any non-empty string 499 base.ThrowError("Can't parse LVS output, no lines? Got '%s'", str(out)) 500 pv_names = set() 501 for line in out: 502 (status, major, minor, pe_size, stripes, more_pvs) = \ 503 cls._ParseLvInfoLine(line, sep) 504 pv_names.update(more_pvs) 505 return (status, major, minor, pe_size, stripes, pv_names)
506
507 - def Attach(self):
508 """Attach to an existing LV. 509 510 This method will try to see if an existing and active LV exists 511 which matches our name. If so, its major/minor will be 512 recorded. 513 514 """ 515 self.attached = False 516 try: 517 (status, major, minor, pe_size, stripes, pv_names) = \ 518 self._GetLvInfo(self.dev_path) 519 except errors.BlockDeviceError: 520 return False 521 522 self.major = major 523 self.minor = minor 524 self.pe_size = pe_size 525 self.stripe_count = stripes 526 self._degraded = status[0] == "v" # virtual volume, i.e. doesn't backing 527 # storage 528 self.pv_names = pv_names 529 self.attached = True 530 return True
531
532 - def Assemble(self):
533 """Assemble the device. 534 535 We always run `lvchange -ay` on the LV to ensure it's active before 536 use, as there were cases when xenvg was not active after boot 537 (also possibly after disk issues). 538 539 """ 540 result = utils.RunCmd(["lvchange", "-ay", self.dev_path]) 541 if result.failed: 542 base.ThrowError("Can't activate lv %s: %s", self.dev_path, result.output)
543
544 - def Shutdown(self):
545 """Shutdown the device. 546 547 This is a no-op for the LV device type, as we don't deactivate the 548 volumes on shutdown. 549 550 """ 551 pass
552
553 - def GetSyncStatus(self):
554 """Returns the sync status of the device. 555 556 If this device is a mirroring device, this function returns the 557 status of the mirror. 558 559 For logical volumes, sync_percent and estimated_time are always 560 None (no recovery in progress, as we don't handle the mirrored LV 561 case). The is_degraded parameter is the inverse of the ldisk 562 parameter. 563 564 For the ldisk parameter, we check if the logical volume has the 565 'virtual' type, which means it's not backed by existing storage 566 anymore (read from it return I/O error). This happens after a 567 physical disk failure and subsequent 'vgreduce --removemissing' on 568 the volume group. 569 570 The status was already read in Attach, so we just return it. 571 572 @rtype: objects.BlockDevStatus 573 574 """ 575 if self._degraded: 576 ldisk_status = constants.LDS_FAULTY 577 else: 578 ldisk_status = constants.LDS_OKAY 579 580 return objects.BlockDevStatus(dev_path=self.dev_path, 581 major=self.major, 582 minor=self.minor, 583 sync_percent=None, 584 estimated_time=None, 585 is_degraded=self._degraded, 586 ldisk_status=ldisk_status)
587
588 - def Open(self, force=False):
589 """Make the device ready for I/O. 590 591 This is a no-op for the LV device type. 592 593 """ 594 pass
595
596 - def Close(self):
597 """Notifies that the device will no longer be used for I/O. 598 599 This is a no-op for the LV device type. 600 601 """ 602 pass
603
604 - def Snapshot(self, size):
605 """Create a snapshot copy of an lvm block device. 606 607 @returns: tuple (vg, lv) 608 609 """ 610 snap_name = self._lv_name + ".snap" 611 612 # remove existing snapshot if found 613 snap = LogicalVolume((self._vg_name, snap_name), None, size, self.params, 614 self.dyn_params) 615 base.IgnoreError(snap.Remove) 616 617 vg_info = self.GetVGInfo([self._vg_name], False) 618 if not vg_info: 619 base.ThrowError("Can't compute VG info for vg %s", self._vg_name) 620 free_size, _, _ = vg_info[0] 621 if free_size < size: 622 base.ThrowError("Not enough free space: required %s," 623 " available %s", size, free_size) 624 625 _CheckResult(utils.RunCmd(["lvcreate", "-L%dm" % size, "-s", 626 "-n%s" % snap_name, self.dev_path])) 627 628 return (self._vg_name, snap_name)
629
630 - def _RemoveOldInfo(self):
631 """Try to remove old tags from the lv. 632 633 """ 634 result = utils.RunCmd(["lvs", "-o", "tags", "--noheadings", "--nosuffix", 635 self.dev_path]) 636 _CheckResult(result) 637 638 raw_tags = result.stdout.strip() 639 if raw_tags: 640 for tag in raw_tags.split(","): 641 _CheckResult(utils.RunCmd(["lvchange", "--deltag", 642 tag.strip(), self.dev_path]))
643
644 - def SetInfo(self, text):
645 """Update metadata with info text. 646 647 """ 648 base.BlockDev.SetInfo(self, text) 649 650 self._RemoveOldInfo() 651 652 # Replace invalid characters 653 text = re.sub("^[^A-Za-z0-9_+.]", "_", text) 654 text = re.sub("[^-A-Za-z0-9_+.]", "_", text) 655 656 # Only up to 128 characters are allowed 657 text = text[:128] 658 659 _CheckResult(utils.RunCmd(["lvchange", "--addtag", text, self.dev_path]))
660
662 """Return how much the disk can grow with exclusive storage. 663 664 @rtype: float 665 @return: available space in Mib 666 667 """ 668 pvs_info = self.GetPVInfo([self._vg_name]) 669 if not pvs_info: 670 base.ThrowError("Cannot get information about PVs for %s", self.dev_path) 671 std_pv_size = self._GetStdPvSize(pvs_info) 672 free_space = sum(pvi.free - (pvi.size - std_pv_size) 673 for pvi in pvs_info 674 if pvi.name in self.pv_names) 675 return free_space
676
677 - def Grow(self, amount, dryrun, backingstore, excl_stor):
678 """Grow the logical volume. 679 680 """ 681 if not backingstore: 682 return 683 if self.pe_size is None or self.stripe_count is None: 684 if not self.Attach(): 685 base.ThrowError("Can't attach to LV during Grow()") 686 full_stripe_size = self.pe_size * self.stripe_count 687 # pe_size is in KB 688 amount *= 1024 689 rest = amount % full_stripe_size 690 if rest != 0: 691 amount += full_stripe_size - rest 692 cmd = ["lvextend", "-L", "+%dk" % amount] 693 if dryrun: 694 cmd.append("--test") 695 if excl_stor: 696 free_space = self._GetGrowthAvaliabilityExclStor() 697 # amount is in KiB, free_space in MiB 698 if amount > free_space * 1024: 699 base.ThrowError("Not enough free space to grow %s: %d MiB required," 700 " %d available", self.dev_path, amount / 1024, 701 free_space) 702 # Disk growth doesn't grow the number of spindles, so we must stay within 703 # our assigned volumes 704 pvlist = list(self.pv_names) 705 else: 706 pvlist = [] 707 # we try multiple algorithms since the 'best' ones might not have 708 # space available in the right place, but later ones might (since 709 # they have less constraints); also note that only recent LVM 710 # supports 'cling' 711 for alloc_policy in "contiguous", "cling", "normal": 712 result = utils.RunCmd(cmd + ["--alloc", alloc_policy, self.dev_path] + 713 pvlist) 714 if not result.failed: 715 return 716 base.ThrowError("Can't grow LV %s: %s", self.dev_path, result.output)
717
718 - def GetActualSpindles(self):
719 """Return the number of spindles used. 720 721 """ 722 assert self.attached, "BlockDevice not attached in GetActualSpindles()" 723 return len(self.pv_names)
724
725 726 -class PersistentBlockDevice(base.BlockDev):
727 """A block device with persistent node 728 729 May be either directly attached, or exposed through DM (e.g. dm-multipath). 730 udev helpers are probably required to give persistent, human-friendly 731 names. 732 733 For the time being, pathnames are required to lie under /dev. 734 735 """
736 - def __init__(self, unique_id, children, size, params, dyn_params, *args):
737 """Attaches to a static block device. 738 739 The unique_id is a path under /dev. 740 741 """ 742 super(PersistentBlockDevice, self).__init__(unique_id, children, size, 743 params, dyn_params, *args) 744 if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2: 745 raise ValueError("Invalid configuration data %s" % str(unique_id)) 746 self.dev_path = unique_id[1] 747 if not os.path.realpath(self.dev_path).startswith("/dev/"): 748 raise ValueError("Full path '%s' lies outside /dev" % 749 os.path.realpath(self.dev_path)) 750 # TODO: this is just a safety guard checking that we only deal with devices 751 # we know how to handle. In the future this will be integrated with 752 # external storage backends and possible values will probably be collected 753 # from the cluster configuration. 754 if unique_id[0] != constants.BLOCKDEV_DRIVER_MANUAL: 755 raise ValueError("Got persistent block device of invalid type: %s" % 756 unique_id[0]) 757 758 self.major = self.minor = None 759 self.Attach()
760 761 @classmethod
762 - def Create(cls, unique_id, children, size, spindles, params, excl_stor, 763 dyn_params, *args):
764 """Create a new device 765 766 This is a noop, we only return a PersistentBlockDevice instance 767 768 """ 769 if excl_stor: 770 raise errors.ProgrammerError("Persistent block device requested with" 771 " exclusive_storage") 772 return PersistentBlockDevice(unique_id, children, 0, params, dyn_params, 773 *args)
774
775 - def Remove(self):
776 """Remove a device 777 778 This is a noop 779 780 """ 781 pass
782
783 - def Rename(self, new_id):
784 """Rename this device. 785 786 """ 787 base.ThrowError("Rename is not supported for PersistentBlockDev storage")
788
789 - def Attach(self):
790 """Attach to an existing block device. 791 792 793 """ 794 self.attached = False 795 try: 796 st = os.stat(self.dev_path) 797 except OSError, err: 798 logging.error("Error stat()'ing %s: %s", self.dev_path, str(err)) 799 return False 800 801 if not stat.S_ISBLK(st.st_mode): 802 logging.error("%s is not a block device", self.dev_path) 803 return False 804 805 self.major = os.major(st.st_rdev) 806 self.minor = utils.osminor(st.st_rdev) 807 self.attached = True 808 809 return True
810
811 - def Assemble(self):
812 """Assemble the device. 813 814 """ 815 pass
816
817 - def Shutdown(self):
818 """Shutdown the device. 819 820 """ 821 pass
822
823 - def Open(self, force=False):
824 """Make the device ready for I/O. 825 826 """ 827 pass
828
829 - def Close(self):
830 """Notifies that the device will no longer be used for I/O. 831 832 """ 833 pass
834
835 - def Grow(self, amount, dryrun, backingstore, excl_stor):
836 """Grow the logical volume. 837 838 """ 839 base.ThrowError("Grow is not supported for PersistentBlockDev storage")
840
841 842 -class RADOSBlockDevice(base.BlockDev):
843 """A RADOS Block Device (rbd). 844 845 This class implements the RADOS Block Device for the backend. You need 846 the rbd kernel driver, the RADOS Tools and a working RADOS cluster for 847 this to be functional. 848 849 """
850 - def __init__(self, unique_id, children, size, params, dyn_params, *args):
851 """Attaches to an rbd device. 852 853 """ 854 super(RADOSBlockDevice, self).__init__(unique_id, children, size, params, 855 dyn_params, *args) 856 if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2: 857 raise ValueError("Invalid configuration data %s" % str(unique_id)) 858 859 self.driver, self.rbd_name = unique_id 860 self.rbd_pool = params[constants.LDP_POOL] 861 862 self.major = self.minor = None 863 self.Attach()
864 865 @classmethod
866 - def Create(cls, unique_id, children, size, spindles, params, excl_stor, 867 dyn_params, *args):
868 """Create a new rbd device. 869 870 Provision a new rbd volume inside a RADOS pool. 871 872 """ 873 if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2: 874 raise errors.ProgrammerError("Invalid configuration data %s" % 875 str(unique_id)) 876 if excl_stor: 877 raise errors.ProgrammerError("RBD device requested with" 878 " exclusive_storage") 879 rbd_pool = params[constants.LDP_POOL] 880 rbd_name = unique_id[1] 881 882 # Provision a new rbd volume (Image) inside the RADOS cluster. 883 cmd = [constants.RBD_CMD, "create", "-p", rbd_pool, 884 rbd_name, "--size", "%s" % size] 885 result = utils.RunCmd(cmd) 886 if result.failed: 887 base.ThrowError("rbd creation failed (%s): %s", 888 result.fail_reason, result.output) 889 890 return RADOSBlockDevice(unique_id, children, size, params, dyn_params, 891 *args)
892
893 - def Remove(self):
894 """Remove the rbd device. 895 896 """ 897 rbd_pool = self.params[constants.LDP_POOL] 898 rbd_name = self.unique_id[1] 899 900 if not self.minor and not self.Attach(): 901 # The rbd device doesn't exist. 902 return 903 904 # First shutdown the device (remove mappings). 905 self.Shutdown() 906 907 # Remove the actual Volume (Image) from the RADOS cluster. 908 cmd = [constants.RBD_CMD, "rm", "-p", rbd_pool, rbd_name] 909 result = utils.RunCmd(cmd) 910 if result.failed: 911 base.ThrowError("Can't remove Volume from cluster with rbd rm: %s - %s", 912 result.fail_reason, result.output)
913
914 - def Rename(self, new_id):
915 """Rename this device. 916 917 """ 918 pass
919
920 - def Attach(self):
921 """Attach to an existing rbd device. 922 923 This method maps the rbd volume that matches our name with 924 an rbd device and then attaches to this device. 925 926 """ 927 self.attached = False 928 929 # Map the rbd volume to a block device under /dev 930 self.dev_path = self._MapVolumeToBlockdev(self.unique_id) 931 932 try: 933 st = os.stat(self.dev_path) 934 except OSError, err: 935 logging.error("Error stat()'ing %s: %s", self.dev_path, str(err)) 936 return False 937 938 if not stat.S_ISBLK(st.st_mode): 939 logging.error("%s is not a block device", self.dev_path) 940 return False 941 942 self.major = os.major(st.st_rdev) 943 self.minor = utils.osminor(st.st_rdev) 944 self.attached = True 945 946 return True
947
948 - def _MapVolumeToBlockdev(self, unique_id):
949 """Maps existing rbd volumes to block devices. 950 951 This method should be idempotent if the mapping already exists. 952 953 @rtype: string 954 @return: the block device path that corresponds to the volume 955 956 """ 957 pool = self.params[constants.LDP_POOL] 958 name = unique_id[1] 959 960 # Check if the mapping already exists. 961 rbd_dev = self._VolumeToBlockdev(pool, name) 962 if rbd_dev: 963 # The mapping exists. Return it. 964 return rbd_dev 965 966 # The mapping doesn't exist. Create it. 967 map_cmd = [constants.RBD_CMD, "map", "-p", pool, name] 968 result = utils.RunCmd(map_cmd) 969 if result.failed: 970 base.ThrowError("rbd map failed (%s): %s", 971 result.fail_reason, result.output) 972 973 # Find the corresponding rbd device. 974 rbd_dev = self._VolumeToBlockdev(pool, name) 975 if not rbd_dev: 976 base.ThrowError("rbd map succeeded, but could not find the rbd block" 977 " device in output of showmapped, for volume: %s", name) 978 979 # The device was successfully mapped. Return it. 980 return rbd_dev
981 982 @classmethod
983 - def _VolumeToBlockdev(cls, pool, volume_name):
984 """Do the 'volume name'-to-'rbd block device' resolving. 985 986 @type pool: string 987 @param pool: RADOS pool to use 988 @type volume_name: string 989 @param volume_name: the name of the volume whose device we search for 990 @rtype: string or None 991 @return: block device path if the volume is mapped, else None 992 993 """ 994 try: 995 # Newer versions of the rbd tool support json output formatting. Use it 996 # if available. 997 showmap_cmd = [ 998 constants.RBD_CMD, 999 "showmapped", 1000 "-p", 1001 pool, 1002 "--format", 1003 "json" 1004 ] 1005 result = utils.RunCmd(showmap_cmd) 1006 if result.failed: 1007 logging.error("rbd JSON output formatting returned error (%s): %s," 1008 "falling back to plain output parsing", 1009 result.fail_reason, result.output) 1010 raise RbdShowmappedJsonError 1011 1012 return cls._ParseRbdShowmappedJson(result.output, volume_name) 1013 except RbdShowmappedJsonError: 1014 # For older versions of rbd, we have to parse the plain / text output 1015 # manually. 1016 showmap_cmd = [constants.RBD_CMD, "showmapped", "-p", pool] 1017 result = utils.RunCmd(showmap_cmd) 1018 if result.failed: 1019 base.ThrowError("rbd showmapped failed (%s): %s", 1020 result.fail_reason, result.output) 1021 1022 return cls._ParseRbdShowmappedPlain(result.output, volume_name)
1023 1024 @staticmethod
1025 - def _ParseRbdShowmappedJson(output, volume_name):
1026 """Parse the json output of `rbd showmapped'. 1027 1028 This method parses the json output of `rbd showmapped' and returns the rbd 1029 block device path (e.g. /dev/rbd0) that matches the given rbd volume. 1030 1031 @type output: string 1032 @param output: the json output of `rbd showmapped' 1033 @type volume_name: string 1034 @param volume_name: the name of the volume whose device we search for 1035 @rtype: string or None 1036 @return: block device path if the volume is mapped, else None 1037 1038 """ 1039 try: 1040 devices = serializer.LoadJson(output) 1041 except ValueError, err: 1042 base.ThrowError("Unable to parse JSON data: %s" % err) 1043 1044 rbd_dev = None 1045 for d in devices.values(): # pylint: disable=E1103 1046 try: 1047 name = d["name"] 1048 except KeyError: 1049 base.ThrowError("'name' key missing from json object %s", devices) 1050 1051 if name == volume_name: 1052 if rbd_dev is not None: 1053 base.ThrowError("rbd volume %s is mapped more than once", volume_name) 1054 1055 rbd_dev = d["device"] 1056 1057 return rbd_dev
1058 1059 @staticmethod
1060 - def _ParseRbdShowmappedPlain(output, volume_name):
1061 """Parse the (plain / text) output of `rbd showmapped'. 1062 1063 This method parses the output of `rbd showmapped' and returns 1064 the rbd block device path (e.g. /dev/rbd0) that matches the 1065 given rbd volume. 1066 1067 @type output: string 1068 @param output: the plain text output of `rbd showmapped' 1069 @type volume_name: string 1070 @param volume_name: the name of the volume whose device we search for 1071 @rtype: string or None 1072 @return: block device path if the volume is mapped, else None 1073 1074 """ 1075 allfields = 5 1076 volumefield = 2 1077 devicefield = 4 1078 1079 lines = output.splitlines() 1080 1081 # Try parsing the new output format (ceph >= 0.55). 1082 splitted_lines = map(lambda l: l.split(), lines) 1083 1084 # Check for empty output. 1085 if not splitted_lines: 1086 return None 1087 1088 # Check showmapped output, to determine number of fields. 1089 field_cnt = len(splitted_lines[0]) 1090 if field_cnt != allfields: 1091 # Parsing the new format failed. Fallback to parsing the old output 1092 # format (< 0.55). 1093 splitted_lines = map(lambda l: l.split("\t"), lines) 1094 if field_cnt != allfields: 1095 base.ThrowError("Cannot parse rbd showmapped output expected %s fields," 1096 " found %s", allfields, field_cnt) 1097 1098 matched_lines = \ 1099 filter(lambda l: len(l) == allfields and l[volumefield] == volume_name, 1100 splitted_lines) 1101 1102 if len(matched_lines) > 1: 1103 base.ThrowError("rbd volume %s mapped more than once", volume_name) 1104 1105 if matched_lines: 1106 # rbd block device found. Return it. 1107 rbd_dev = matched_lines[0][devicefield] 1108 return rbd_dev 1109 1110 # The given volume is not mapped. 1111 return None
1112
1113 - def Assemble(self):
1114 """Assemble the device. 1115 1116 """ 1117 pass
1118
1119 - def Shutdown(self):
1120 """Shutdown the device. 1121 1122 """ 1123 if not self.minor and not self.Attach(): 1124 # The rbd device doesn't exist. 1125 return 1126 1127 # Unmap the block device from the Volume. 1128 self._UnmapVolumeFromBlockdev(self.unique_id) 1129 1130 self.minor = None 1131 self.dev_path = None
1132
1133 - def _UnmapVolumeFromBlockdev(self, unique_id):
1134 """Unmaps the rbd device from the Volume it is mapped. 1135 1136 Unmaps the rbd device from the Volume it was previously mapped to. 1137 This method should be idempotent if the Volume isn't mapped. 1138 1139 """ 1140 pool = self.params[constants.LDP_POOL] 1141 name = unique_id[1] 1142 1143 # Check if the mapping already exists. 1144 rbd_dev = self._VolumeToBlockdev(pool, name) 1145 1146 if rbd_dev: 1147 # The mapping exists. Unmap the rbd device. 1148 unmap_cmd = [constants.RBD_CMD, "unmap", "%s" % rbd_dev] 1149 result = utils.RunCmd(unmap_cmd) 1150 if result.failed: 1151 base.ThrowError("rbd unmap failed (%s): %s", 1152 result.fail_reason, result.output)
1153
1154 - def Open(self, force=False):
1155 """Make the device ready for I/O. 1156 1157 """ 1158 pass
1159
1160 - def Close(self):
1161 """Notifies that the device will no longer be used for I/O. 1162 1163 """ 1164 pass
1165
1166 - def Grow(self, amount, dryrun, backingstore, excl_stor):
1167 """Grow the Volume. 1168 1169 @type amount: integer 1170 @param amount: the amount (in mebibytes) to grow with 1171 @type dryrun: boolean 1172 @param dryrun: whether to execute the operation in simulation mode 1173 only, without actually increasing the size 1174 1175 """ 1176 if not backingstore: 1177 return 1178 if not self.Attach(): 1179 base.ThrowError("Can't attach to rbd device during Grow()") 1180 1181 if dryrun: 1182 # the rbd tool does not support dry runs of resize operations. 1183 # Since rbd volumes are thinly provisioned, we assume 1184 # there is always enough free space for the operation. 1185 return 1186 1187 rbd_pool = self.params[constants.LDP_POOL] 1188 rbd_name = self.unique_id[1] 1189 new_size = self.size + amount 1190 1191 # Resize the rbd volume (Image) inside the RADOS cluster. 1192 cmd = [constants.RBD_CMD, "resize", "-p", rbd_pool, 1193 rbd_name, "--size", "%s" % new_size] 1194 result = utils.RunCmd(cmd) 1195 if result.failed: 1196 base.ThrowError("rbd resize failed (%s): %s", 1197 result.fail_reason, result.output)
1198
1199 - def GetUserspaceAccessUri(self, hypervisor):
1200 """Generate KVM userspace URIs to be used as `-drive file` settings. 1201 1202 @see: L{BlockDev.GetUserspaceAccessUri} 1203 1204 """ 1205 if hypervisor == constants.HT_KVM: 1206 return "rbd:" + self.rbd_pool + "/" + self.rbd_name 1207 else: 1208 base.ThrowError("Hypervisor %s doesn't support RBD userspace access" % 1209 hypervisor)
1210
1211 1212 -class ExtStorageDevice(base.BlockDev):
1213 """A block device provided by an ExtStorage Provider. 1214 1215 This class implements the External Storage Interface, which means 1216 handling of the externally provided block devices. 1217 1218 """
1219 - def __init__(self, unique_id, children, size, params, dyn_params, *args):
1220 """Attaches to an extstorage block device. 1221 1222 """ 1223 super(ExtStorageDevice, self).__init__(unique_id, children, size, params, 1224 dyn_params, *args) 1225 (self.name, self.uuid) = args 1226 1227 if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2: 1228 raise ValueError("Invalid configuration data %s" % str(unique_id)) 1229 1230 self.driver, self.vol_name = unique_id 1231 self.ext_params = params 1232 1233 self.major = self.minor = None 1234 self.Attach()
1235 1236 @classmethod
1237 - def Create(cls, unique_id, children, size, spindles, params, excl_stor, 1238 dyn_params, *args):
1239 """Create a new extstorage device. 1240 1241 Provision a new volume using an extstorage provider, which will 1242 then be mapped to a block device. 1243 1244 """ 1245 (name, uuid) = args 1246 1247 if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2: 1248 raise errors.ProgrammerError("Invalid configuration data %s" % 1249 str(unique_id)) 1250 if excl_stor: 1251 raise errors.ProgrammerError("extstorage device requested with" 1252 " exclusive_storage") 1253 1254 # Call the External Storage's create script, 1255 # to provision a new Volume inside the External Storage 1256 _ExtStorageAction(constants.ES_ACTION_CREATE, unique_id, 1257 params, size=str(size), name=name, uuid=uuid) 1258 1259 return ExtStorageDevice(unique_id, children, size, params, dyn_params, 1260 *args)
1261
1262 - def Remove(self):
1263 """Remove the extstorage device. 1264 1265 """ 1266 if not self.minor and not self.Attach(): 1267 # The extstorage device doesn't exist. 1268 return 1269 1270 # First shutdown the device (remove mappings). 1271 self.Shutdown() 1272 1273 # Call the External Storage's remove script, 1274 # to remove the Volume from the External Storage 1275 _ExtStorageAction(constants.ES_ACTION_REMOVE, self.unique_id, 1276 self.ext_params, name=self.name, uuid=self.uuid)
1277
1278 - def Rename(self, new_id):
1279 """Rename this device. 1280 1281 """ 1282 pass
1283
1284 - def Attach(self):
1285 """Attach to an existing extstorage device. 1286 1287 This method maps the extstorage volume that matches our name with 1288 a corresponding block device and then attaches to this device. 1289 1290 """ 1291 self.attached = False 1292 1293 # Call the External Storage's attach script, 1294 # to attach an existing Volume to a block device under /dev 1295 self.dev_path = _ExtStorageAction(constants.ES_ACTION_ATTACH, 1296 self.unique_id, self.ext_params, 1297 name=self.name, uuid=self.uuid) 1298 1299 try: 1300 st = os.stat(self.dev_path) 1301 except OSError, err: 1302 logging.error("Error stat()'ing %s: %s", self.dev_path, str(err)) 1303 return False 1304 1305 if not stat.S_ISBLK(st.st_mode): 1306 logging.error("%s is not a block device", self.dev_path) 1307 return False 1308 1309 self.major = os.major(st.st_rdev) 1310 self.minor = utils.osminor(st.st_rdev) 1311 self.attached = True 1312 1313 return True
1314
1315 - def Assemble(self):
1316 """Assemble the device. 1317 1318 """ 1319 pass
1320
1321 - def Shutdown(self):
1322 """Shutdown the device. 1323 1324 """ 1325 if not self.minor and not self.Attach(): 1326 # The extstorage device doesn't exist. 1327 return 1328 1329 # Call the External Storage's detach script, 1330 # to detach an existing Volume from it's block device under /dev 1331 _ExtStorageAction(constants.ES_ACTION_DETACH, self.unique_id, 1332 self.ext_params, name=self.name, uuid=self.uuid) 1333 1334 self.minor = None 1335 self.dev_path = None
1336
1337 - def Open(self, force=False):
1338 """Make the device ready for I/O. 1339 1340 """ 1341 pass
1342
1343 - def Close(self):
1344 """Notifies that the device will no longer be used for I/O. 1345 1346 """ 1347 pass
1348
1349 - def Grow(self, amount, dryrun, backingstore, excl_stor):
1350 """Grow the Volume. 1351 1352 @type amount: integer 1353 @param amount: the amount (in mebibytes) to grow with 1354 @type dryrun: boolean 1355 @param dryrun: whether to execute the operation in simulation mode 1356 only, without actually increasing the size 1357 1358 """ 1359 if not backingstore: 1360 return 1361 if not self.Attach(): 1362 base.ThrowError("Can't attach to extstorage device during Grow()") 1363 1364 if dryrun: 1365 # we do not support dry runs of resize operations for now. 1366 return 1367 1368 new_size = self.size + amount 1369 1370 # Call the External Storage's grow script, 1371 # to grow an existing Volume inside the External Storage 1372 _ExtStorageAction(constants.ES_ACTION_GROW, self.unique_id, 1373 self.ext_params, size=str(self.size), grow=str(new_size), 1374 name=self.name, uuid=self.uuid)
1375
1376 - def SetInfo(self, text):
1377 """Update metadata with info text. 1378 1379 """ 1380 # Replace invalid characters 1381 text = re.sub("^[^A-Za-z0-9_+.]", "_", text) 1382 text = re.sub("[^-A-Za-z0-9_+.]", "_", text) 1383 1384 # Only up to 128 characters are allowed 1385 text = text[:128] 1386 1387 # Call the External Storage's setinfo script, 1388 # to set metadata for an existing Volume inside the External Storage 1389 _ExtStorageAction(constants.ES_ACTION_SETINFO, self.unique_id, 1390 self.ext_params, metadata=text, 1391 name=self.name, uuid=self.uuid)
1392
1393 1394 -def _ExtStorageAction(action, unique_id, ext_params, 1395 size=None, grow=None, metadata=None, 1396 name=None, uuid=None):
1397 """Take an External Storage action. 1398 1399 Take an External Storage action concerning or affecting 1400 a specific Volume inside the External Storage. 1401 1402 @type action: string 1403 @param action: which action to perform. One of: 1404 create / remove / grow / attach / detach 1405 @type unique_id: tuple (driver, vol_name) 1406 @param unique_id: a tuple containing the type of ExtStorage (driver) 1407 and the Volume name 1408 @type ext_params: dict 1409 @param ext_params: ExtStorage parameters 1410 @type size: integer 1411 @param size: the size of the Volume in mebibytes 1412 @type grow: integer 1413 @param grow: the new size in mebibytes (after grow) 1414 @type metadata: string 1415 @param metadata: metadata info of the Volume, for use by the provider 1416 @type name: string 1417 @param name: name of the Volume (objects.Disk.name) 1418 @type uuid: string 1419 @param uuid: uuid of the Volume (objects.Disk.uuid) 1420 @rtype: None or a block device path (during attach) 1421 1422 """ 1423 driver, vol_name = unique_id 1424 1425 # Create an External Storage instance of type `driver' 1426 status, inst_es = ExtStorageFromDisk(driver) 1427 if not status: 1428 base.ThrowError("%s" % inst_es) 1429 1430 # Create the basic environment for the driver's scripts 1431 create_env = _ExtStorageEnvironment(unique_id, ext_params, size, 1432 grow, metadata, name, uuid) 1433 1434 # Do not use log file for action `attach' as we need 1435 # to get the output from RunResult 1436 # TODO: find a way to have a log file for attach too 1437 logfile = None 1438 if action is not constants.ES_ACTION_ATTACH: 1439 logfile = _VolumeLogName(action, driver, vol_name) 1440 1441 # Make sure the given action results in a valid script 1442 if action not in constants.ES_SCRIPTS: 1443 base.ThrowError("Action '%s' doesn't result in a valid ExtStorage script" % 1444 action) 1445 1446 # Find out which external script to run according the given action 1447 script_name = action + "_script" 1448 script = getattr(inst_es, script_name) 1449 1450 # Run the external script 1451 result = utils.RunCmd([script], env=create_env, 1452 cwd=inst_es.path, output=logfile,) 1453 if result.failed: 1454 logging.error("External storage's %s command '%s' returned" 1455 " error: %s, logfile: %s, output: %s", 1456 action, result.cmd, result.fail_reason, 1457 logfile, result.output) 1458 1459 # If logfile is 'None' (during attach), it breaks TailFile 1460 # TODO: have a log file for attach too 1461 if action is not constants.ES_ACTION_ATTACH: 1462 lines = [utils.SafeEncode(val) 1463 for val in utils.TailFile(logfile, lines=20)] 1464 else: 1465 lines = result.output[-20:] 1466 1467 base.ThrowError("External storage's %s script failed (%s), last" 1468 " lines of output:\n%s", 1469 action, result.fail_reason, "\n".join(lines)) 1470 1471 if action == constants.ES_ACTION_ATTACH: 1472 return result.stdout
1473
1474 1475 -def ExtStorageFromDisk(name, base_dir=None):
1476 """Create an ExtStorage instance from disk. 1477 1478 This function will return an ExtStorage instance 1479 if the given name is a valid ExtStorage name. 1480 1481 @type base_dir: string 1482 @keyword base_dir: Base directory containing ExtStorage installations. 1483 Defaults to a search in all the ES_SEARCH_PATH dirs. 1484 @rtype: tuple 1485 @return: True and the ExtStorage instance if we find a valid one, or 1486 False and the diagnose message on error 1487 1488 """ 1489 if base_dir is None: 1490 es_base_dir = pathutils.ES_SEARCH_PATH 1491 else: 1492 es_base_dir = [base_dir] 1493 1494 es_dir = utils.FindFile(name, es_base_dir, os.path.isdir) 1495 1496 if es_dir is None: 1497 return False, ("Directory for External Storage Provider %s not" 1498 " found in search path" % name) 1499 1500 # ES Files dictionary, we will populate it with the absolute path 1501 # names; if the value is True, then it is a required file, otherwise 1502 # an optional one 1503 es_files = dict.fromkeys(constants.ES_SCRIPTS, True) 1504 1505 es_files[constants.ES_PARAMETERS_FILE] = True 1506 1507 for (filename, _) in es_files.items(): 1508 es_files[filename] = utils.PathJoin(es_dir, filename) 1509 1510 try: 1511 st = os.stat(es_files[filename]) 1512 except EnvironmentError, err: 1513 return False, ("File '%s' under path '%s' is missing (%s)" % 1514 (filename, es_dir, utils.ErrnoOrStr(err))) 1515 1516 if not stat.S_ISREG(stat.S_IFMT(st.st_mode)): 1517 return False, ("File '%s' under path '%s' is not a regular file" % 1518 (filename, es_dir)) 1519 1520 if filename in constants.ES_SCRIPTS: 1521 if stat.S_IMODE(st.st_mode) & stat.S_IXUSR != stat.S_IXUSR: 1522 return False, ("File '%s' under path '%s' is not executable" % 1523 (filename, es_dir)) 1524 1525 parameters = [] 1526 if constants.ES_PARAMETERS_FILE in es_files: 1527 parameters_file = es_files[constants.ES_PARAMETERS_FILE] 1528 try: 1529 parameters = utils.ReadFile(parameters_file).splitlines() 1530 except EnvironmentError, err: 1531 return False, ("Error while reading the EXT parameters file at %s: %s" % 1532 (parameters_file, utils.ErrnoOrStr(err))) 1533 parameters = [v.split(None, 1) for v in parameters] 1534 1535 es_obj = \ 1536 objects.ExtStorage(name=name, path=es_dir, 1537 create_script=es_files[constants.ES_SCRIPT_CREATE], 1538 remove_script=es_files[constants.ES_SCRIPT_REMOVE], 1539 grow_script=es_files[constants.ES_SCRIPT_GROW], 1540 attach_script=es_files[constants.ES_SCRIPT_ATTACH], 1541 detach_script=es_files[constants.ES_SCRIPT_DETACH], 1542 setinfo_script=es_files[constants.ES_SCRIPT_SETINFO], 1543 verify_script=es_files[constants.ES_SCRIPT_VERIFY], 1544 supported_parameters=parameters) 1545 return True, es_obj
1546
1547 1548 -def _ExtStorageEnvironment(unique_id, ext_params, 1549 size=None, grow=None, metadata=None, 1550 name=None, uuid=None):
1551 """Calculate the environment for an External Storage script. 1552 1553 @type unique_id: tuple (driver, vol_name) 1554 @param unique_id: ExtStorage pool and name of the Volume 1555 @type ext_params: dict 1556 @param ext_params: the EXT parameters 1557 @type size: string 1558 @param size: size of the Volume (in mebibytes) 1559 @type grow: string 1560 @param grow: new size of Volume after grow (in mebibytes) 1561 @type metadata: string 1562 @param metadata: metadata info of the Volume 1563 @type name: string 1564 @param name: name of the Volume (objects.Disk.name) 1565 @type uuid: string 1566 @param uuid: uuid of the Volume (objects.Disk.uuid) 1567 @rtype: dict 1568 @return: dict of environment variables 1569 1570 """ 1571 vol_name = unique_id[1] 1572 1573 result = {} 1574 result["VOL_NAME"] = vol_name 1575 1576 # EXT params 1577 for pname, pvalue in ext_params.items(): 1578 result["EXTP_%s" % pname.upper()] = str(pvalue) 1579 1580 if size is not None: 1581 result["VOL_SIZE"] = size 1582 1583 if grow is not None: 1584 result["VOL_NEW_SIZE"] = grow 1585 1586 if metadata is not None: 1587 result["VOL_METADATA"] = metadata 1588 1589 if name is not None: 1590 result["VOL_CNAME"] = name 1591 1592 if uuid is not None: 1593 result["VOL_UUID"] = uuid 1594 1595 return result
1596
1597 1598 -def _VolumeLogName(kind, es_name, volume):
1599 """Compute the ExtStorage log filename for a given Volume and operation. 1600 1601 @type kind: string 1602 @param kind: the operation type (e.g. create, remove etc.) 1603 @type es_name: string 1604 @param es_name: the ExtStorage name 1605 @type volume: string 1606 @param volume: the name of the Volume inside the External Storage 1607 1608 """ 1609 # Check if the extstorage log dir is a valid dir 1610 if not os.path.isdir(pathutils.LOG_ES_DIR): 1611 base.ThrowError("Cannot find log directory: %s", pathutils.LOG_ES_DIR) 1612 1613 # TODO: Use tempfile.mkstemp to create unique filename 1614 basename = ("%s-%s-%s-%s.log" % 1615 (kind, es_name, volume, utils.TimestampForFilename())) 1616 return utils.PathJoin(pathutils.LOG_ES_DIR, basename)
1617
1618 1619 -def _VerifyDiskType(dev_type):
1620 if dev_type not in DEV_MAP: 1621 raise errors.ProgrammerError("Invalid block device type '%s'" % dev_type)
1622
1623 1624 -def _VerifyDiskParams(disk):
1625 """Verifies if all disk parameters are set. 1626 1627 """ 1628 missing = set(constants.DISK_LD_DEFAULTS[disk.dev_type]) - set(disk.params) 1629 if missing: 1630 raise errors.ProgrammerError("Block device is missing disk parameters: %s" % 1631 missing)
1632
1633 1634 -def FindDevice(disk, children):
1635 """Search for an existing, assembled device. 1636 1637 This will succeed only if the device exists and is assembled, but it 1638 does not do any actions in order to activate the device. 1639 1640 @type disk: L{objects.Disk} 1641 @param disk: the disk object to find 1642 @type children: list of L{bdev.BlockDev} 1643 @param children: the list of block devices that are children of the device 1644 represented by the disk parameter 1645 1646 """ 1647 _VerifyDiskType(disk.dev_type) 1648 device = DEV_MAP[disk.dev_type](disk.logical_id, children, disk.size, 1649 disk.params, disk.dynamic_params, 1650 disk.name, disk.uuid) 1651 if not device.attached: 1652 return None 1653 return device
1654
1655 1656 -def Assemble(disk, children):
1657 """Try to attach or assemble an existing device. 1658 1659 This will attach to assemble the device, as needed, to bring it 1660 fully up. It must be safe to run on already-assembled devices. 1661 1662 @type disk: L{objects.Disk} 1663 @param disk: the disk object to assemble 1664 @type children: list of L{bdev.BlockDev} 1665 @param children: the list of block devices that are children of the device 1666 represented by the disk parameter 1667 1668 """ 1669 _VerifyDiskType(disk.dev_type) 1670 _VerifyDiskParams(disk) 1671 device = DEV_MAP[disk.dev_type](disk.logical_id, children, disk.size, 1672 disk.params, disk.dynamic_params, 1673 disk.name, disk.uuid) 1674 device.Assemble() 1675 return device
1676
1677 1678 -def Create(disk, children, excl_stor):
1679 """Create a device. 1680 1681 @type disk: L{objects.Disk} 1682 @param disk: the disk object to create 1683 @type children: list of L{bdev.BlockDev} 1684 @param children: the list of block devices that are children of the device 1685 represented by the disk parameter 1686 @type excl_stor: boolean 1687 @param excl_stor: Whether exclusive_storage is active 1688 @rtype: L{bdev.BlockDev} 1689 @return: the created device, or C{None} in case of an error 1690 1691 """ 1692 _VerifyDiskType(disk.dev_type) 1693 _VerifyDiskParams(disk) 1694 device = DEV_MAP[disk.dev_type].Create(disk.logical_id, children, disk.size, 1695 disk.spindles, disk.params, excl_stor, 1696 disk.dynamic_params, 1697 disk.name, disk.uuid) 1698 return device
1699 1700 # Please keep this at the bottom of the file for visibility. 1701 DEV_MAP = { 1702 constants.DT_PLAIN: LogicalVolume, 1703 constants.DT_DRBD8: drbd.DRBD8Dev, 1704 constants.DT_BLOCK: PersistentBlockDevice, 1705 constants.DT_RBD: RADOSBlockDevice, 1706 constants.DT_EXT: ExtStorageDevice, 1707 constants.DT_FILE: FileStorage, 1708 constants.DT_SHARED_FILE: FileStorage, 1709 constants.DT_GLUSTER: GlusterStorage, 1710 } 1711 """Map disk types to disk type classes. 1712 1713 @see: L{Assemble}, L{FindDevice}, L{Create}.""" # pylint: disable=W0105 1714