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