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

Source Code for Module ganeti.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 time 
  26  import errno 
  27  import shlex 
  28  import stat 
  29  import pyparsing as pyp 
  30  import os 
  31  import logging 
  32  import math 
  33   
  34  from ganeti import utils 
  35  from ganeti import errors 
  36  from ganeti import constants 
  37  from ganeti import objects 
  38  from ganeti import compat 
  39  from ganeti import netutils 
  40  from ganeti import pathutils 
  41  from ganeti import serializer 
  42   
  43   
  44  # Size of reads in _CanReadDevice 
  45  _DEVICE_READ_SIZE = 128 * 1024 
46 47 48 -class RbdShowmappedJsonError(Exception):
49 """`rbd showmmapped' JSON formatting error Exception class. 50 51 """ 52 pass
53
54 55 -def _IgnoreError(fn, *args, **kwargs):
56 """Executes the given function, ignoring BlockDeviceErrors. 57 58 This is used in order to simplify the execution of cleanup or 59 rollback functions. 60 61 @rtype: boolean 62 @return: True when fn didn't raise an exception, False otherwise 63 64 """ 65 try: 66 fn(*args, **kwargs) 67 return True 68 except errors.BlockDeviceError, err: 69 logging.warning("Caught BlockDeviceError but ignoring: %s", str(err)) 70 return False
71
72 73 -def _ThrowError(msg, *args):
74 """Log an error to the node daemon and the raise an exception. 75 76 @type msg: string 77 @param msg: the text of the exception 78 @raise errors.BlockDeviceError 79 80 """ 81 if args: 82 msg = msg % args 83 logging.error(msg) 84 raise errors.BlockDeviceError(msg)
85
86 87 -def _CheckResult(result):
88 """Throws an error if the given result is a failed one. 89 90 @param result: result from RunCmd 91 92 """ 93 if result.failed: 94 _ThrowError("Command: %s error: %s - %s", result.cmd, result.fail_reason, 95 result.output)
96
97 98 -def _CanReadDevice(path):
99 """Check if we can read from the given device. 100 101 This tries to read the first 128k of the device. 102 103 """ 104 try: 105 utils.ReadFile(path, size=_DEVICE_READ_SIZE) 106 return True 107 except EnvironmentError: 108 logging.warning("Can't read from device %s", path, exc_info=True) 109 return False
110
111 112 -def _GetForbiddenFileStoragePaths():
113 """Builds a list of path prefixes which shouldn't be used for file storage. 114 115 @rtype: frozenset 116 117 """ 118 paths = set([ 119 "/boot", 120 "/dev", 121 "/etc", 122 "/home", 123 "/proc", 124 "/root", 125 "/sys", 126 ]) 127 128 for prefix in ["", "/usr", "/usr/local"]: 129 paths.update(map(lambda s: "%s/%s" % (prefix, s), 130 ["bin", "lib", "lib32", "lib64", "sbin"])) 131 132 return compat.UniqueFrozenset(map(os.path.normpath, paths))
133
134 135 -def _ComputeWrongFileStoragePaths(paths, 136 _forbidden=_GetForbiddenFileStoragePaths()):
137 """Cross-checks a list of paths for prefixes considered bad. 138 139 Some paths, e.g. "/bin", should not be used for file storage. 140 141 @type paths: list 142 @param paths: List of paths to be checked 143 @rtype: list 144 @return: Sorted list of paths for which the user should be warned 145 146 """ 147 def _Check(path): 148 return (not os.path.isabs(path) or 149 path in _forbidden or 150 filter(lambda p: utils.IsBelowDir(p, path), _forbidden))
151 152 return utils.NiceSort(filter(_Check, map(os.path.normpath, paths))) 153
154 155 -def ComputeWrongFileStoragePaths(_filename=pathutils.FILE_STORAGE_PATHS_FILE):
156 """Returns a list of file storage paths whose prefix is considered bad. 157 158 See L{_ComputeWrongFileStoragePaths}. 159 160 """ 161 return _ComputeWrongFileStoragePaths(_LoadAllowedFileStoragePaths(_filename))
162
163 164 -def _CheckFileStoragePath(path, allowed):
165 """Checks if a path is in a list of allowed paths for file storage. 166 167 @type path: string 168 @param path: Path to check 169 @type allowed: list 170 @param allowed: List of allowed paths 171 @raise errors.FileStoragePathError: If the path is not allowed 172 173 """ 174 if not os.path.isabs(path): 175 raise errors.FileStoragePathError("File storage path must be absolute," 176 " got '%s'" % path) 177 178 for i in allowed: 179 if not os.path.isabs(i): 180 logging.info("Ignoring relative path '%s' for file storage", i) 181 continue 182 183 if utils.IsBelowDir(i, path): 184 break 185 else: 186 raise errors.FileStoragePathError("Path '%s' is not acceptable for file" 187 " storage. A possible fix might be to add" 188 " it to /etc/ganeti/file-storage-paths" 189 " on all nodes." % path)
190
191 192 -def _LoadAllowedFileStoragePaths(filename):
193 """Loads file containing allowed file storage paths. 194 195 @rtype: list 196 @return: List of allowed paths (can be an empty list) 197 198 """ 199 try: 200 contents = utils.ReadFile(filename) 201 except EnvironmentError: 202 return [] 203 else: 204 return utils.FilterEmptyLinesAndComments(contents)
205
206 207 -def CheckFileStoragePath(path, _filename=pathutils.FILE_STORAGE_PATHS_FILE):
208 """Checks if a path is allowed for file storage. 209 210 @type path: string 211 @param path: Path to check 212 @raise errors.FileStoragePathError: If the path is not allowed 213 214 """ 215 allowed = _LoadAllowedFileStoragePaths(_filename) 216 217 if _ComputeWrongFileStoragePaths([path]): 218 raise errors.FileStoragePathError("Path '%s' uses a forbidden prefix" % 219 path) 220 221 _CheckFileStoragePath(path, allowed)
222
223 224 -class BlockDev(object):
225 """Block device abstract class. 226 227 A block device can be in the following states: 228 - not existing on the system, and by `Create()` it goes into: 229 - existing but not setup/not active, and by `Assemble()` goes into: 230 - active read-write and by `Open()` it goes into 231 - online (=used, or ready for use) 232 233 A device can also be online but read-only, however we are not using 234 the readonly state (LV has it, if needed in the future) and we are 235 usually looking at this like at a stack, so it's easier to 236 conceptualise the transition from not-existing to online and back 237 like a linear one. 238 239 The many different states of the device are due to the fact that we 240 need to cover many device types: 241 - logical volumes are created, lvchange -a y $lv, and used 242 - drbd devices are attached to a local disk/remote peer and made primary 243 244 A block device is identified by three items: 245 - the /dev path of the device (dynamic) 246 - a unique ID of the device (static) 247 - it's major/minor pair (dynamic) 248 249 Not all devices implement both the first two as distinct items. LVM 250 logical volumes have their unique ID (the pair volume group, logical 251 volume name) in a 1-to-1 relation to the dev path. For DRBD devices, 252 the /dev path is again dynamic and the unique id is the pair (host1, 253 dev1), (host2, dev2). 254 255 You can get to a device in two ways: 256 - creating the (real) device, which returns you 257 an attached instance (lvcreate) 258 - attaching of a python instance to an existing (real) device 259 260 The second point, the attachment to a device, is different 261 depending on whether the device is assembled or not. At init() time, 262 we search for a device with the same unique_id as us. If found, 263 good. It also means that the device is already assembled. If not, 264 after assembly we'll have our correct major/minor. 265 266 """
267 - def __init__(self, unique_id, children, size, params):
268 self._children = children 269 self.dev_path = None 270 self.unique_id = unique_id 271 self.major = None 272 self.minor = None 273 self.attached = False 274 self.size = size 275 self.params = params
276
277 - def Assemble(self):
278 """Assemble the device from its components. 279 280 Implementations of this method by child classes must ensure that: 281 - after the device has been assembled, it knows its major/minor 282 numbers; this allows other devices (usually parents) to probe 283 correctly for their children 284 - calling this method on an existing, in-use device is safe 285 - if the device is already configured (and in an OK state), 286 this method is idempotent 287 288 """ 289 pass
290
291 - def Attach(self):
292 """Find a device which matches our config and attach to it. 293 294 """ 295 raise NotImplementedError
296
297 - def Close(self):
298 """Notifies that the device will no longer be used for I/O. 299 300 """ 301 raise NotImplementedError
302 303 @classmethod
304 - def Create(cls, unique_id, children, size, params, excl_stor):
305 """Create the device. 306 307 If the device cannot be created, it will return None 308 instead. Error messages go to the logging system. 309 310 Note that for some devices, the unique_id is used, and for other, 311 the children. The idea is that these two, taken together, are 312 enough for both creation and assembly (later). 313 314 """ 315 raise NotImplementedError
316
317 - def Remove(self):
318 """Remove this device. 319 320 This makes sense only for some of the device types: LV and file 321 storage. Also note that if the device can't attach, the removal 322 can't be completed. 323 324 """ 325 raise NotImplementedError
326
327 - def Rename(self, new_id):
328 """Rename this device. 329 330 This may or may not make sense for a given device type. 331 332 """ 333 raise NotImplementedError
334
335 - def Open(self, force=False):
336 """Make the device ready for use. 337 338 This makes the device ready for I/O. For now, just the DRBD 339 devices need this. 340 341 The force parameter signifies that if the device has any kind of 342 --force thing, it should be used, we know what we are doing. 343 344 """ 345 raise NotImplementedError
346
347 - def Shutdown(self):
348 """Shut down the device, freeing its children. 349 350 This undoes the `Assemble()` work, except for the child 351 assembling; as such, the children on the device are still 352 assembled after this call. 353 354 """ 355 raise NotImplementedError
356
357 - def SetSyncParams(self, params):
358 """Adjust the synchronization parameters of the mirror. 359 360 In case this is not a mirroring device, this is no-op. 361 362 @param params: dictionary of LD level disk parameters related to the 363 synchronization. 364 @rtype: list 365 @return: a list of error messages, emitted both by the current node and by 366 children. An empty list means no errors. 367 368 """ 369 result = [] 370 if self._children: 371 for child in self._children: 372 result.extend(child.SetSyncParams(params)) 373 return result
374
375 - def PauseResumeSync(self, pause):
376 """Pause/Resume the sync of the mirror. 377 378 In case this is not a mirroring device, this is no-op. 379 380 @param pause: Whether to pause or resume 381 382 """ 383 result = True 384 if self._children: 385 for child in self._children: 386 result = result and child.PauseResumeSync(pause) 387 return result
388
389 - def GetSyncStatus(self):
390 """Returns the sync status of the device. 391 392 If this device is a mirroring device, this function returns the 393 status of the mirror. 394 395 If sync_percent is None, it means the device is not syncing. 396 397 If estimated_time is None, it means we can't estimate 398 the time needed, otherwise it's the time left in seconds. 399 400 If is_degraded is True, it means the device is missing 401 redundancy. This is usually a sign that something went wrong in 402 the device setup, if sync_percent is None. 403 404 The ldisk parameter represents the degradation of the local 405 data. This is only valid for some devices, the rest will always 406 return False (not degraded). 407 408 @rtype: objects.BlockDevStatus 409 410 """ 411 return objects.BlockDevStatus(dev_path=self.dev_path, 412 major=self.major, 413 minor=self.minor, 414 sync_percent=None, 415 estimated_time=None, 416 is_degraded=False, 417 ldisk_status=constants.LDS_OKAY)
418
419 - def CombinedSyncStatus(self):
420 """Calculate the mirror status recursively for our children. 421 422 The return value is the same as for `GetSyncStatus()` except the 423 minimum percent and maximum time are calculated across our 424 children. 425 426 @rtype: objects.BlockDevStatus 427 428 """ 429 status = self.GetSyncStatus() 430 431 min_percent = status.sync_percent 432 max_time = status.estimated_time 433 is_degraded = status.is_degraded 434 ldisk_status = status.ldisk_status 435 436 if self._children: 437 for child in self._children: 438 child_status = child.GetSyncStatus() 439 440 if min_percent is None: 441 min_percent = child_status.sync_percent 442 elif child_status.sync_percent is not None: 443 min_percent = min(min_percent, child_status.sync_percent) 444 445 if max_time is None: 446 max_time = child_status.estimated_time 447 elif child_status.estimated_time is not None: 448 max_time = max(max_time, child_status.estimated_time) 449 450 is_degraded = is_degraded or child_status.is_degraded 451 452 if ldisk_status is None: 453 ldisk_status = child_status.ldisk_status 454 elif child_status.ldisk_status is not None: 455 ldisk_status = max(ldisk_status, child_status.ldisk_status) 456 457 return objects.BlockDevStatus(dev_path=self.dev_path, 458 major=self.major, 459 minor=self.minor, 460 sync_percent=min_percent, 461 estimated_time=max_time, 462 is_degraded=is_degraded, 463 ldisk_status=ldisk_status)
464
465 - def SetInfo(self, text):
466 """Update metadata with info text. 467 468 Only supported for some device types. 469 470 """ 471 for child in self._children: 472 child.SetInfo(text)
473
474 - def Grow(self, amount, dryrun, backingstore):
475 """Grow the block device. 476 477 @type amount: integer 478 @param amount: the amount (in mebibytes) to grow with 479 @type dryrun: boolean 480 @param dryrun: whether to execute the operation in simulation mode 481 only, without actually increasing the size 482 @param backingstore: whether to execute the operation on backing storage 483 only, or on "logical" storage only; e.g. DRBD is logical storage, 484 whereas LVM, file, RBD are backing storage 485 486 """ 487 raise NotImplementedError
488
489 - def GetActualSize(self):
490 """Return the actual disk size. 491 492 @note: the device needs to be active when this is called 493 494 """ 495 assert self.attached, "BlockDevice not attached in GetActualSize()" 496 result = utils.RunCmd(["blockdev", "--getsize64", self.dev_path]) 497 if result.failed: 498 _ThrowError("blockdev failed (%s): %s", 499 result.fail_reason, result.output) 500 try: 501 sz = int(result.output.strip()) 502 except (ValueError, TypeError), err: 503 _ThrowError("Failed to parse blockdev output: %s", str(err)) 504 return sz
505
506 - def __repr__(self):
507 return ("<%s: unique_id: %s, children: %s, %s:%s, %s>" % 508 (self.__class__, self.unique_id, self._children, 509 self.major, self.minor, self.dev_path))
510
511 512 -class LogicalVolume(BlockDev):
513 """Logical Volume block device. 514 515 """ 516 _VALID_NAME_RE = re.compile("^[a-zA-Z0-9+_.-]*$") 517 _INVALID_NAMES = compat.UniqueFrozenset([".", "..", "snapshot", "pvmove"]) 518 _INVALID_SUBSTRINGS = compat.UniqueFrozenset(["_mlog", "_mimage"]) 519
520 - def __init__(self, unique_id, children, size, params):
521 """Attaches to a LV device. 522 523 The unique_id is a tuple (vg_name, lv_name) 524 525 """ 526 super(LogicalVolume, self).__init__(unique_id, children, size, params) 527 if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2: 528 raise ValueError("Invalid configuration data %s" % str(unique_id)) 529 self._vg_name, self._lv_name = unique_id 530 self._ValidateName(self._vg_name) 531 self._ValidateName(self._lv_name) 532 self.dev_path = utils.PathJoin("/dev", self._vg_name, self._lv_name) 533 self._degraded = True 534 self.major = self.minor = self.pe_size = self.stripe_count = None 535 self.Attach()
536 537 @staticmethod
538 - def _GetStdPvSize(pvs_info):
539 """Return the the standard PV size (used with exclusive storage). 540 541 @param pvs_info: list of objects.LvmPvInfo, cannot be empty 542 @rtype: float 543 @return: size in MiB 544 545 """ 546 assert len(pvs_info) > 0 547 smallest = min([pv.size for pv in pvs_info]) 548 return smallest / (1 + constants.PART_MARGIN + constants.PART_RESERVED)
549 550 @staticmethod
551 - def _ComputeNumPvs(size, pvs_info):
552 """Compute the number of PVs needed for an LV (with exclusive storage). 553 554 @type size: float 555 @param size: LV size in MiB 556 @param pvs_info: list of objects.LvmPvInfo, cannot be empty 557 @rtype: integer 558 @return: number of PVs needed 559 """ 560 assert len(pvs_info) > 0 561 pv_size = float(LogicalVolume._GetStdPvSize(pvs_info)) 562 return int(math.ceil(float(size) / pv_size))
563 564 @staticmethod
565 - def _GetEmptyPvNames(pvs_info, max_pvs=None):
566 """Return a list of empty PVs, by name. 567 568 """ 569 empty_pvs = filter(objects.LvmPvInfo.IsEmpty, pvs_info) 570 if max_pvs is not None: 571 empty_pvs = empty_pvs[:max_pvs] 572 return map((lambda pv: pv.name), empty_pvs)
573 574 @classmethod
575 - def Create(cls, unique_id, children, size, params, excl_stor):
576 """Create a new logical volume. 577 578 """ 579 if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2: 580 raise errors.ProgrammerError("Invalid configuration data %s" % 581 str(unique_id)) 582 vg_name, lv_name = unique_id 583 cls._ValidateName(vg_name) 584 cls._ValidateName(lv_name) 585 pvs_info = cls.GetPVInfo([vg_name]) 586 if not pvs_info: 587 if excl_stor: 588 msg = "No (empty) PVs found" 589 else: 590 msg = "Can't compute PV info for vg %s" % vg_name 591 _ThrowError(msg) 592 pvs_info.sort(key=(lambda pv: pv.free), reverse=True) 593 594 pvlist = [pv.name for pv in pvs_info] 595 if compat.any(":" in v for v in pvlist): 596 _ThrowError("Some of your PVs have the invalid character ':' in their" 597 " name, this is not supported - please filter them out" 598 " in lvm.conf using either 'filter' or 'preferred_names'") 599 600 current_pvs = len(pvlist) 601 desired_stripes = params[constants.LDP_STRIPES] 602 stripes = min(current_pvs, desired_stripes) 603 604 if excl_stor: 605 (err_msgs, _) = utils.LvmExclusiveCheckNodePvs(pvs_info) 606 if err_msgs: 607 for m in err_msgs: 608 logging.warning(m) 609 req_pvs = cls._ComputeNumPvs(size, pvs_info) 610 pvlist = cls._GetEmptyPvNames(pvs_info, req_pvs) 611 current_pvs = len(pvlist) 612 if current_pvs < req_pvs: 613 _ThrowError("Not enough empty PVs to create a disk of %d MB:" 614 " %d available, %d needed", size, current_pvs, req_pvs) 615 assert current_pvs == len(pvlist) 616 if stripes > current_pvs: 617 # No warning issued for this, as it's no surprise 618 stripes = current_pvs 619 620 else: 621 if stripes < desired_stripes: 622 logging.warning("Could not use %d stripes for VG %s, as only %d PVs are" 623 " available.", desired_stripes, vg_name, current_pvs) 624 free_size = sum([pv.free for pv in pvs_info]) 625 # The size constraint should have been checked from the master before 626 # calling the create function. 627 if free_size < size: 628 _ThrowError("Not enough free space: required %s," 629 " available %s", size, free_size) 630 631 # If the free space is not well distributed, we won't be able to 632 # create an optimally-striped volume; in that case, we want to try 633 # with N, N-1, ..., 2, and finally 1 (non-stripped) number of 634 # stripes 635 cmd = ["lvcreate", "-L%dm" % size, "-n%s" % lv_name] 636 for stripes_arg in range(stripes, 0, -1): 637 result = utils.RunCmd(cmd + ["-i%d" % stripes_arg] + [vg_name] + pvlist) 638 if not result.failed: 639 break 640 if result.failed: 641 _ThrowError("LV create failed (%s): %s", 642 result.fail_reason, result.output) 643 return LogicalVolume(unique_id, children, size, params)
644 645 @staticmethod
646 - def _GetVolumeInfo(lvm_cmd, fields):
647 """Returns LVM Volumen infos using lvm_cmd 648 649 @param lvm_cmd: Should be one of "pvs", "vgs" or "lvs" 650 @param fields: Fields to return 651 @return: A list of dicts each with the parsed fields 652 653 """ 654 if not fields: 655 raise errors.ProgrammerError("No fields specified") 656 657 sep = "|" 658 cmd = [lvm_cmd, "--noheadings", "--nosuffix", "--units=m", "--unbuffered", 659 "--separator=%s" % sep, "-o%s" % ",".join(fields)] 660 661 result = utils.RunCmd(cmd) 662 if result.failed: 663 raise errors.CommandError("Can't get the volume information: %s - %s" % 664 (result.fail_reason, result.output)) 665 666 data = [] 667 for line in result.stdout.splitlines(): 668 splitted_fields = line.strip().split(sep) 669 670 if len(fields) != len(splitted_fields): 671 raise errors.CommandError("Can't parse %s output: line '%s'" % 672 (lvm_cmd, line)) 673 674 data.append(splitted_fields) 675 676 return data
677 678 @classmethod
679 - def GetPVInfo(cls, vg_names, filter_allocatable=True, include_lvs=False):
680 """Get the free space info for PVs in a volume group. 681 682 @param vg_names: list of volume group names, if empty all will be returned 683 @param filter_allocatable: whether to skip over unallocatable PVs 684 @param include_lvs: whether to include a list of LVs hosted on each PV 685 686 @rtype: list 687 @return: list of objects.LvmPvInfo objects 688 689 """ 690 # We request "lv_name" field only if we care about LVs, so we don't get 691 # a long list of entries with many duplicates unless we really have to. 692 # The duplicate "pv_name" field will be ignored. 693 if include_lvs: 694 lvfield = "lv_name" 695 else: 696 lvfield = "pv_name" 697 try: 698 info = cls._GetVolumeInfo("pvs", ["pv_name", "vg_name", "pv_free", 699 "pv_attr", "pv_size", lvfield]) 700 except errors.GenericError, err: 701 logging.error("Can't get PV information: %s", err) 702 return None 703 704 # When asked for LVs, "pvs" may return multiple entries for the same PV-LV 705 # pair. We sort entries by PV name and then LV name, so it's easy to weed 706 # out duplicates. 707 if include_lvs: 708 info.sort(key=(lambda i: (i[0], i[5]))) 709 data = [] 710 lastpvi = None 711 for (pv_name, vg_name, pv_free, pv_attr, pv_size, lv_name) in info: 712 # (possibly) skip over pvs which are not allocatable 713 if filter_allocatable and pv_attr[0] != "a": 714 continue 715 # (possibly) skip over pvs which are not in the right volume group(s) 716 if vg_names and vg_name not in vg_names: 717 continue 718 # Beware of duplicates (check before inserting) 719 if lastpvi and lastpvi.name == pv_name: 720 if include_lvs and lv_name: 721 if not lastpvi.lv_list or lastpvi.lv_list[-1] != lv_name: 722 lastpvi.lv_list.append(lv_name) 723 else: 724 if include_lvs and lv_name: 725 lvl = [lv_name] 726 else: 727 lvl = [] 728 lastpvi = objects.LvmPvInfo(name=pv_name, vg_name=vg_name, 729 size=float(pv_size), free=float(pv_free), 730 attributes=pv_attr, lv_list=lvl) 731 data.append(lastpvi) 732 733 return data
734 735 @classmethod
736 - def _GetExclusiveStorageVgFree(cls, vg_name):
737 """Return the free disk space in the given VG, in exclusive storage mode. 738 739 @type vg_name: string 740 @param vg_name: VG name 741 @rtype: float 742 @return: free space in MiB 743 """ 744 pvs_info = cls.GetPVInfo([vg_name]) 745 if not pvs_info: 746 return 0.0 747 pv_size = cls._GetStdPvSize(pvs_info) 748 num_pvs = len(cls._GetEmptyPvNames(pvs_info)) 749 return pv_size * num_pvs
750 751 @classmethod
752 - def GetVGInfo(cls, vg_names, excl_stor, filter_readonly=True):
753 """Get the free space info for specific VGs. 754 755 @param vg_names: list of volume group names, if empty all will be returned 756 @param excl_stor: whether exclusive_storage is enabled 757 @param filter_readonly: whether to skip over readonly VGs 758 759 @rtype: list 760 @return: list of tuples (free_space, total_size, name) with free_space in 761 MiB 762 763 """ 764 try: 765 info = cls._GetVolumeInfo("vgs", ["vg_name", "vg_free", "vg_attr", 766 "vg_size"]) 767 except errors.GenericError, err: 768 logging.error("Can't get VG information: %s", err) 769 return None 770 771 data = [] 772 for vg_name, vg_free, vg_attr, vg_size in info: 773 # (possibly) skip over vgs which are not writable 774 if filter_readonly and vg_attr[0] == "r": 775 continue 776 # (possibly) skip over vgs which are not in the right volume group(s) 777 if vg_names and vg_name not in vg_names: 778 continue 779 # Exclusive storage needs a different concept of free space 780 if excl_stor: 781 es_free = cls._GetExclusiveStorageVgFree(vg_name) 782 assert es_free <= vg_free 783 vg_free = es_free 784 data.append((float(vg_free), float(vg_size), vg_name)) 785 786 return data
787 788 @classmethod
789 - def _ValidateName(cls, name):
790 """Validates that a given name is valid as VG or LV name. 791 792 The list of valid characters and restricted names is taken out of 793 the lvm(8) manpage, with the simplification that we enforce both 794 VG and LV restrictions on the names. 795 796 """ 797 if (not cls._VALID_NAME_RE.match(name) or 798 name in cls._INVALID_NAMES or 799 compat.any(substring in name for substring in cls._INVALID_SUBSTRINGS)): 800 _ThrowError("Invalid LVM name '%s'", name)
801
802 - def Remove(self):
803 """Remove this logical volume. 804 805 """ 806 if not self.minor and not self.Attach(): 807 # the LV does not exist 808 return 809 result = utils.RunCmd(["lvremove", "-f", "%s/%s" % 810 (self._vg_name, self._lv_name)]) 811 if result.failed: 812 _ThrowError("Can't lvremove: %s - %s", result.fail_reason, result.output)
813
814 - def Rename(self, new_id):
815 """Rename this logical volume. 816 817 """ 818 if not isinstance(new_id, (tuple, list)) or len(new_id) != 2: 819 raise errors.ProgrammerError("Invalid new logical id '%s'" % new_id) 820 new_vg, new_name = new_id 821 if new_vg != self._vg_name: 822 raise errors.ProgrammerError("Can't move a logical volume across" 823 " volume groups (from %s to to %s)" % 824 (self._vg_name, new_vg)) 825 result = utils.RunCmd(["lvrename", new_vg, self._lv_name, new_name]) 826 if result.failed: 827 _ThrowError("Failed to rename the logical volume: %s", result.output) 828 self._lv_name = new_name 829 self.dev_path = utils.PathJoin("/dev", self._vg_name, self._lv_name)
830
831 - def Attach(self):
832 """Attach to an existing LV. 833 834 This method will try to see if an existing and active LV exists 835 which matches our name. If so, its major/minor will be 836 recorded. 837 838 """ 839 self.attached = False 840 result = utils.RunCmd(["lvs", "--noheadings", "--separator=,", 841 "--units=k", "--nosuffix", 842 "-olv_attr,lv_kernel_major,lv_kernel_minor," 843 "vg_extent_size,stripes", self.dev_path]) 844 if result.failed: 845 logging.error("Can't find LV %s: %s, %s", 846 self.dev_path, result.fail_reason, result.output) 847 return False 848 # the output can (and will) have multiple lines for multi-segment 849 # LVs, as the 'stripes' parameter is a segment one, so we take 850 # only the last entry, which is the one we're interested in; note 851 # that with LVM2 anyway the 'stripes' value must be constant 852 # across segments, so this is a no-op actually 853 out = result.stdout.splitlines() 854 if not out: # totally empty result? splitlines() returns at least 855 # one line for any non-empty string 856 logging.error("Can't parse LVS output, no lines? Got '%s'", str(out)) 857 return False 858 out = out[-1].strip().rstrip(",") 859 out = out.split(",") 860 if len(out) != 5: 861 logging.error("Can't parse LVS output, len(%s) != 5", str(out)) 862 return False 863 864 status, major, minor, pe_size, stripes = out 865 if len(status) < 6: 866 logging.error("lvs lv_attr is not at least 6 characters (%s)", status) 867 return False 868 869 try: 870 major = int(major) 871 minor = int(minor) 872 except (TypeError, ValueError), err: 873 logging.error("lvs major/minor cannot be parsed: %s", str(err)) 874 875 try: 876 pe_size = int(float(pe_size)) 877 except (TypeError, ValueError), err: 878 logging.error("Can't parse vg extent size: %s", err) 879 return False 880 881 try: 882 stripes = int(stripes) 883 except (TypeError, ValueError), err: 884 logging.error("Can't parse the number of stripes: %s", err) 885 return False 886 887 self.major = major 888 self.minor = minor 889 self.pe_size = pe_size 890 self.stripe_count = stripes 891 self._degraded = status[0] == "v" # virtual volume, i.e. doesn't backing 892 # storage 893 self.attached = True 894 return True
895
896 - def Assemble(self):
897 """Assemble the device. 898 899 We always run `lvchange -ay` on the LV to ensure it's active before 900 use, as there were cases when xenvg was not active after boot 901 (also possibly after disk issues). 902 903 """ 904 result = utils.RunCmd(["lvchange", "-ay", self.dev_path]) 905 if result.failed: 906 _ThrowError("Can't activate lv %s: %s", self.dev_path, result.output)
907
908 - def Shutdown(self):
909 """Shutdown the device. 910 911 This is a no-op for the LV device type, as we don't deactivate the 912 volumes on shutdown. 913 914 """ 915 pass
916
917 - def GetSyncStatus(self):
918 """Returns the sync status of the device. 919 920 If this device is a mirroring device, this function returns the 921 status of the mirror. 922 923 For logical volumes, sync_percent and estimated_time are always 924 None (no recovery in progress, as we don't handle the mirrored LV 925 case). The is_degraded parameter is the inverse of the ldisk 926 parameter. 927 928 For the ldisk parameter, we check if the logical volume has the 929 'virtual' type, which means it's not backed by existing storage 930 anymore (read from it return I/O error). This happens after a 931 physical disk failure and subsequent 'vgreduce --removemissing' on 932 the volume group. 933 934 The status was already read in Attach, so we just return it. 935 936 @rtype: objects.BlockDevStatus 937 938 """ 939 if self._degraded: 940 ldisk_status = constants.LDS_FAULTY 941 else: 942 ldisk_status = constants.LDS_OKAY 943 944 return objects.BlockDevStatus(dev_path=self.dev_path, 945 major=self.major, 946 minor=self.minor, 947 sync_percent=None, 948 estimated_time=None, 949 is_degraded=self._degraded, 950 ldisk_status=ldisk_status)
951
952 - def Open(self, force=False):
953 """Make the device ready for I/O. 954 955 This is a no-op for the LV device type. 956 957 """ 958 pass
959
960 - def Close(self):
961 """Notifies that the device will no longer be used for I/O. 962 963 This is a no-op for the LV device type. 964 965 """ 966 pass
967
968 - def Snapshot(self, size):
969 """Create a snapshot copy of an lvm block device. 970 971 @returns: tuple (vg, lv) 972 973 """ 974 snap_name = self._lv_name + ".snap" 975 976 # remove existing snapshot if found 977 snap = LogicalVolume((self._vg_name, snap_name), None, size, self.params) 978 _IgnoreError(snap.Remove) 979 980 vg_info = self.GetVGInfo([self._vg_name], False) 981 if not vg_info: 982 _ThrowError("Can't compute VG info for vg %s", self._vg_name) 983 free_size, _, _ = vg_info[0] 984 if free_size < size: 985 _ThrowError("Not enough free space: required %s," 986 " available %s", size, free_size) 987 988 _CheckResult(utils.RunCmd(["lvcreate", "-L%dm" % size, "-s", 989 "-n%s" % snap_name, self.dev_path])) 990 991 return (self._vg_name, snap_name)
992
993 - def _RemoveOldInfo(self):
994 """Try to remove old tags from the lv. 995 996 """ 997 result = utils.RunCmd(["lvs", "-o", "tags", "--noheadings", "--nosuffix", 998 self.dev_path]) 999 _CheckResult(result) 1000 1001 raw_tags = result.stdout.strip() 1002 if raw_tags: 1003 for tag in raw_tags.split(","): 1004 _CheckResult(utils.RunCmd(["lvchange", "--deltag", 1005 tag.strip(), self.dev_path]))
1006
1007 - def SetInfo(self, text):
1008 """Update metadata with info text. 1009 1010 """ 1011 BlockDev.SetInfo(self, text) 1012 1013 self._RemoveOldInfo() 1014 1015 # Replace invalid characters 1016 text = re.sub("^[^A-Za-z0-9_+.]", "_", text) 1017 text = re.sub("[^-A-Za-z0-9_+.]", "_", text) 1018 1019 # Only up to 128 characters are allowed 1020 text = text[:128] 1021 1022 _CheckResult(utils.RunCmd(["lvchange", "--addtag", text, self.dev_path]))
1023
1024 - def Grow(self, amount, dryrun, backingstore):
1025 """Grow the logical volume. 1026 1027 """ 1028 if not backingstore: 1029 return 1030 if self.pe_size is None or self.stripe_count is None: 1031 if not self.Attach(): 1032 _ThrowError("Can't attach to LV during Grow()") 1033 full_stripe_size = self.pe_size * self.stripe_count 1034 # pe_size is in KB 1035 amount *= 1024 1036 rest = amount % full_stripe_size 1037 if rest != 0: 1038 amount += full_stripe_size - rest 1039 cmd = ["lvextend", "-L", "+%dk" % amount] 1040 if dryrun: 1041 cmd.append("--test") 1042 # we try multiple algorithms since the 'best' ones might not have 1043 # space available in the right place, but later ones might (since 1044 # they have less constraints); also note that only recent LVM 1045 # supports 'cling' 1046 for alloc_policy in "contiguous", "cling", "normal": 1047 result = utils.RunCmd(cmd + ["--alloc", alloc_policy, self.dev_path]) 1048 if not result.failed: 1049 return 1050 _ThrowError("Can't grow LV %s: %s", self.dev_path, result.output)
1051
1052 1053 -class DRBD8Status(object):
1054 """A DRBD status representation class. 1055 1056 Note that this doesn't support unconfigured devices (cs:Unconfigured). 1057 1058 """ 1059 UNCONF_RE = re.compile(r"\s*[0-9]+:\s*cs:Unconfigured$") 1060 LINE_RE = re.compile(r"\s*[0-9]+:\s*cs:(\S+)\s+(?:st|ro):([^/]+)/(\S+)" 1061 "\s+ds:([^/]+)/(\S+)\s+.*$") 1062 SYNC_RE = re.compile(r"^.*\ssync'ed:\s*([0-9.]+)%.*" 1063 # Due to a bug in drbd in the kernel, introduced in 1064 # commit 4b0715f096 (still unfixed as of 2011-08-22) 1065 "(?:\s|M)" 1066 "finish: ([0-9]+):([0-9]+):([0-9]+)\s.*$") 1067 1068 CS_UNCONFIGURED = "Unconfigured" 1069 CS_STANDALONE = "StandAlone" 1070 CS_WFCONNECTION = "WFConnection" 1071 CS_WFREPORTPARAMS = "WFReportParams" 1072 CS_CONNECTED = "Connected" 1073 CS_STARTINGSYNCS = "StartingSyncS" 1074 CS_STARTINGSYNCT = "StartingSyncT" 1075 CS_WFBITMAPS = "WFBitMapS" 1076 CS_WFBITMAPT = "WFBitMapT" 1077 CS_WFSYNCUUID = "WFSyncUUID" 1078 CS_SYNCSOURCE = "SyncSource" 1079 CS_SYNCTARGET = "SyncTarget" 1080 CS_PAUSEDSYNCS = "PausedSyncS" 1081 CS_PAUSEDSYNCT = "PausedSyncT" 1082 CSET_SYNC = compat.UniqueFrozenset([ 1083 CS_WFREPORTPARAMS, 1084 CS_STARTINGSYNCS, 1085 CS_STARTINGSYNCT, 1086 CS_WFBITMAPS, 1087 CS_WFBITMAPT, 1088 CS_WFSYNCUUID, 1089 CS_SYNCSOURCE, 1090 CS_SYNCTARGET, 1091 CS_PAUSEDSYNCS, 1092 CS_PAUSEDSYNCT, 1093 ]) 1094 1095 DS_DISKLESS = "Diskless" 1096 DS_ATTACHING = "Attaching" # transient state 1097 DS_FAILED = "Failed" # transient state, next: diskless 1098 DS_NEGOTIATING = "Negotiating" # transient state 1099 DS_INCONSISTENT = "Inconsistent" # while syncing or after creation 1100 DS_OUTDATED = "Outdated" 1101 DS_DUNKNOWN = "DUnknown" # shown for peer disk when not connected 1102 DS_CONSISTENT = "Consistent" 1103 DS_UPTODATE = "UpToDate" # normal state 1104 1105 RO_PRIMARY = "Primary" 1106 RO_SECONDARY = "Secondary" 1107 RO_UNKNOWN = "Unknown" 1108
1109 - def __init__(self, procline):
1110 u = self.UNCONF_RE.match(procline) 1111 if u: 1112 self.cstatus = self.CS_UNCONFIGURED 1113 self.lrole = self.rrole = self.ldisk = self.rdisk = None 1114 else: 1115 m = self.LINE_RE.match(procline) 1116 if not m: 1117 raise errors.BlockDeviceError("Can't parse input data '%s'" % procline) 1118 self.cstatus = m.group(1) 1119 self.lrole = m.group(2) 1120 self.rrole = m.group(3) 1121 self.ldisk = m.group(4) 1122 self.rdisk = m.group(5) 1123 1124 # end reading of data from the LINE_RE or UNCONF_RE 1125 1126 self.is_standalone = self.cstatus == self.CS_STANDALONE 1127 self.is_wfconn = self.cstatus == self.CS_WFCONNECTION 1128 self.is_connected = self.cstatus == self.CS_CONNECTED 1129 self.is_primary = self.lrole == self.RO_PRIMARY 1130 self.is_secondary = self.lrole == self.RO_SECONDARY 1131 self.peer_primary = self.rrole == self.RO_PRIMARY 1132 self.peer_secondary = self.rrole == self.RO_SECONDARY 1133 self.both_primary = self.is_primary and self.peer_primary 1134 self.both_secondary = self.is_secondary and self.peer_secondary 1135 1136 self.is_diskless = self.ldisk == self.DS_DISKLESS 1137 self.is_disk_uptodate = self.ldisk == self.DS_UPTODATE 1138 1139 self.is_in_resync = self.cstatus in self.CSET_SYNC 1140 self.is_in_use = self.cstatus != self.CS_UNCONFIGURED 1141 1142 m = self.SYNC_RE.match(procline) 1143 if m: 1144 self.sync_percent = float(m.group(1)) 1145 hours = int(m.group(2)) 1146 minutes = int(m.group(3)) 1147 seconds = int(m.group(4)) 1148 self.est_time = hours * 3600 + minutes * 60 + seconds 1149 else: 1150 # we have (in this if branch) no percent information, but if 1151 # we're resyncing we need to 'fake' a sync percent information, 1152 # as this is how cmdlib determines if it makes sense to wait for 1153 # resyncing or not 1154 if self.is_in_resync: 1155 self.sync_percent = 0 1156 else: 1157 self.sync_percent = None 1158 self.est_time = None
1159
1160 1161 -class BaseDRBD(BlockDev): # pylint: disable=W0223
1162 """Base DRBD class. 1163 1164 This class contains a few bits of common functionality between the 1165 0.7 and 8.x versions of DRBD. 1166 1167 """ 1168 _VERSION_RE = re.compile(r"^version: (\d+)\.(\d+)\.(\d+)(?:\.\d+)?" 1169 r" \(api:(\d+)/proto:(\d+)(?:-(\d+))?\)") 1170 _VALID_LINE_RE = re.compile("^ *([0-9]+): cs:([^ ]+).*$") 1171 _UNUSED_LINE_RE = re.compile("^ *([0-9]+): cs:Unconfigured$") 1172 1173 _DRBD_MAJOR = 147 1174 _ST_UNCONFIGURED = "Unconfigured" 1175 _ST_WFCONNECTION = "WFConnection" 1176 _ST_CONNECTED = "Connected" 1177 1178 _STATUS_FILE = constants.DRBD_STATUS_FILE 1179 _USERMODE_HELPER_FILE = "/sys/module/drbd/parameters/usermode_helper" 1180 1181 @staticmethod
1182 - def _GetProcData(filename=_STATUS_FILE):
1183 """Return data from /proc/drbd. 1184 1185 """ 1186 try: 1187 data = utils.ReadFile(filename).splitlines() 1188 except EnvironmentError, err: 1189 if err.errno == errno.ENOENT: 1190 _ThrowError("The file %s cannot be opened, check if the module" 1191 " is loaded (%s)", filename, str(err)) 1192 else: 1193 _ThrowError("Can't read the DRBD proc file %s: %s", filename, str(err)) 1194 if not data: 1195 _ThrowError("Can't read any data from %s", filename) 1196 return data
1197 1198 @classmethod
1199 - def _MassageProcData(cls, data):
1200 """Transform the output of _GetProdData into a nicer form. 1201 1202 @return: a dictionary of minor: joined lines from /proc/drbd 1203 for that minor 1204 1205 """ 1206 results = {} 1207 old_minor = old_line = None 1208 for line in data: 1209 if not line: # completely empty lines, as can be returned by drbd8.0+ 1210 continue 1211 lresult = cls._VALID_LINE_RE.match(line) 1212 if lresult is not None: 1213 if old_minor is not None: 1214 results[old_minor] = old_line 1215 old_minor = int(lresult.group(1)) 1216 old_line = line 1217 else: 1218 if old_minor is not None: 1219 old_line += " " + line.strip() 1220 # add last line 1221 if old_minor is not None: 1222 results[old_minor] = old_line 1223 return results
1224 1225 @classmethod
1226 - def _GetVersion(cls, proc_data):
1227 """Return the DRBD version. 1228 1229 This will return a dict with keys: 1230 - k_major 1231 - k_minor 1232 - k_point 1233 - api 1234 - proto 1235 - proto2 (only on drbd > 8.2.X) 1236 1237 """ 1238 first_line = proc_data[0].strip() 1239 version = cls._VERSION_RE.match(first_line) 1240 if not version: 1241 raise errors.BlockDeviceError("Can't parse DRBD version from '%s'" % 1242 first_line) 1243 1244 values = version.groups() 1245 retval = { 1246 "k_major": int(values[0]), 1247 "k_minor": int(values[1]), 1248 "k_point": int(values[2]), 1249 "api": int(values[3]), 1250 "proto": int(values[4]), 1251 } 1252 if values[5] is not None: 1253 retval["proto2"] = values[5] 1254 1255 return retval
1256 1257 @staticmethod
1258 - def GetUsermodeHelper(filename=_USERMODE_HELPER_FILE):
1259 """Returns DRBD usermode_helper currently set. 1260 1261 """ 1262 try: 1263 helper = utils.ReadFile(filename).splitlines()[0] 1264 except EnvironmentError, err: 1265 if err.errno == errno.ENOENT: 1266 _ThrowError("The file %s cannot be opened, check if the module" 1267 " is loaded (%s)", filename, str(err)) 1268 else: 1269 _ThrowError("Can't read DRBD helper file %s: %s", filename, str(err)) 1270 if not helper: 1271 _ThrowError("Can't read any data from %s", filename) 1272 return helper
1273 1274 @staticmethod
1275 - def _DevPath(minor):
1276 """Return the path to a drbd device for a given minor. 1277 1278 """ 1279 return "/dev/drbd%d" % minor
1280 1281 @classmethod
1282 - def GetUsedDevs(cls):
1283 """Compute the list of used DRBD devices. 1284 1285 """ 1286 data = cls._GetProcData() 1287 1288 used_devs = {} 1289 for line in data: 1290 match = cls._VALID_LINE_RE.match(line) 1291 if not match: 1292 continue 1293 minor = int(match.group(1)) 1294 state = match.group(2) 1295 if state == cls._ST_UNCONFIGURED: 1296 continue 1297 used_devs[minor] = state, line 1298 1299 return used_devs
1300
1301 - def _SetFromMinor(self, minor):
1302 """Set our parameters based on the given minor. 1303 1304 This sets our minor variable and our dev_path. 1305 1306 """ 1307 if minor is None: 1308 self.minor = self.dev_path = None 1309 self.attached = False 1310 else: 1311 self.minor = minor 1312 self.dev_path = self._DevPath(minor) 1313 self.attached = True
1314 1315 @staticmethod
1316 - def _CheckMetaSize(meta_device):
1317 """Check if the given meta device looks like a valid one. 1318 1319 This currently only checks the size, which must be around 1320 128MiB. 1321 1322 """ 1323 result = utils.RunCmd(["blockdev", "--getsize", meta_device]) 1324 if result.failed: 1325 _ThrowError("Failed to get device size: %s - %s", 1326 result.fail_reason, result.output) 1327 try: 1328 sectors = int(result.stdout) 1329 except (TypeError, ValueError): 1330 _ThrowError("Invalid output from blockdev: '%s'", result.stdout) 1331 num_bytes = sectors * 512 1332 if num_bytes < 128 * 1024 * 1024: # less than 128MiB 1333 _ThrowError("Meta device too small (%.2fMib)", (num_bytes / 1024 / 1024)) 1334 # the maximum *valid* size of the meta device when living on top 1335 # of LVM is hard to compute: it depends on the number of stripes 1336 # and the PE size; e.g. a 2-stripe, 64MB PE will result in a 128MB 1337 # (normal size), but an eight-stripe 128MB PE will result in a 1GB 1338 # size meta device; as such, we restrict it to 1GB (a little bit 1339 # too generous, but making assumptions about PE size is hard) 1340 if num_bytes > 1024 * 1024 * 1024: 1341 _ThrowError("Meta device too big (%.2fMiB)", (num_bytes / 1024 / 1024))
1342
1343 - def Rename(self, new_id):
1344 """Rename a device. 1345 1346 This is not supported for drbd devices. 1347 1348 """ 1349 raise errors.ProgrammerError("Can't rename a drbd device")
1350
1351 1352 -class DRBD8(BaseDRBD):
1353 """DRBD v8.x block device. 1354 1355 This implements the local host part of the DRBD device, i.e. it 1356 doesn't do anything to the supposed peer. If you need a fully 1357 connected DRBD pair, you need to use this class on both hosts. 1358 1359 The unique_id for the drbd device is a (local_ip, local_port, 1360 remote_ip, remote_port, local_minor, secret) tuple, and it must have 1361 two children: the data device and the meta_device. The meta device 1362 is checked for valid size and is zeroed on create. 1363 1364 """ 1365 _MAX_MINORS = 255 1366 _PARSE_SHOW = None 1367 1368 # timeout constants 1369 _NET_RECONFIG_TIMEOUT = 60 1370 1371 # command line options for barriers 1372 _DISABLE_DISK_OPTION = "--no-disk-barrier" # -a 1373 _DISABLE_DRAIN_OPTION = "--no-disk-drain" # -D 1374 _DISABLE_FLUSH_OPTION = "--no-disk-flushes" # -i 1375 _DISABLE_META_FLUSH_OPTION = "--no-md-flushes" # -m 1376
1377 - def __init__(self, unique_id, children, size, params):
1378 if children and children.count(None) > 0: 1379 children = [] 1380 if len(children) not in (0, 2): 1381 raise ValueError("Invalid configuration data %s" % str(children)) 1382 if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 6: 1383 raise ValueError("Invalid configuration data %s" % str(unique_id)) 1384 (self._lhost, self._lport, 1385 self._rhost, self._rport, 1386 self._aminor, self._secret) = unique_id 1387 if children: 1388 if not _CanReadDevice(children[1].dev_path): 1389 logging.info("drbd%s: Ignoring unreadable meta device", self._aminor) 1390 children = [] 1391 super(DRBD8, self).__init__(unique_id, children, size, params) 1392 self.major = self._DRBD_MAJOR 1393 version = self._GetVersion(self._GetProcData()) 1394 if version["k_major"] != 8: 1395 _ThrowError("Mismatch in DRBD kernel version and requested ganeti" 1396 " usage: kernel is %s.%s, ganeti wants 8.x", 1397 version["k_major"], version["k_minor"]) 1398 1399 if (self._lhost is not None and self._lhost == self._rhost and 1400 self._lport == self._rport): 1401 raise ValueError("Invalid configuration data, same local/remote %s" % 1402 (unique_id,)) 1403 self.Attach()
1404 1405 @classmethod
1406 - def _InitMeta(cls, minor, dev_path):
1407 """Initialize a meta device. 1408 1409 This will not work if the given minor is in use. 1410 1411 """ 1412 # Zero the metadata first, in order to make sure drbdmeta doesn't 1413 # try to auto-detect existing filesystems or similar (see 1414 # http://code.google.com/p/ganeti/issues/detail?id=182); we only 1415 # care about the first 128MB of data in the device, even though it 1416 # can be bigger 1417 result = utils.RunCmd([constants.DD_CMD, 1418 "if=/dev/zero", "of=%s" % dev_path, 1419 "bs=1048576", "count=128", "oflag=direct"]) 1420 if result.failed: 1421 _ThrowError("Can't wipe the meta device: %s", result.output) 1422 1423 result = utils.RunCmd(["drbdmeta", "--force", cls._DevPath(minor), 1424 "v08", dev_path, "0", "create-md"]) 1425 if result.failed: 1426 _ThrowError("Can't initialize meta device: %s", result.output)
1427 1428 @classmethod
1429 - def _FindUnusedMinor(cls):
1430 """Find an unused DRBD device. 1431 1432 This is specific to 8.x as the minors are allocated dynamically, 1433 so non-existing numbers up to a max minor count are actually free. 1434 1435 """ 1436 data = cls._GetProcData() 1437 1438 highest = None 1439 for line in data: 1440 match = cls._UNUSED_LINE_RE.match(line) 1441 if match: 1442 return int(match.group(1)) 1443 match = cls._VALID_LINE_RE.match(line) 1444 if match: 1445 minor = int(match.group(1)) 1446 highest = max(highest, minor) 1447 if highest is None: # there are no minors in use at all 1448 return 0 1449 if highest >= cls._MAX_MINORS: 1450 logging.error("Error: no free drbd minors!") 1451 raise errors.BlockDeviceError("Can't find a free DRBD minor") 1452 return highest + 1
1453 1454 @classmethod
1455 - def _GetShowParser(cls):
1456 """Return a parser for `drbd show` output. 1457 1458 This will either create or return an already-created parser for the 1459 output of the command `drbd show`. 1460 1461 """ 1462 if cls._PARSE_SHOW is not None: 1463 return cls._PARSE_SHOW 1464 1465 # pyparsing setup 1466 lbrace = pyp.Literal("{").suppress() 1467 rbrace = pyp.Literal("}").suppress() 1468 lbracket = pyp.Literal("[").suppress() 1469 rbracket = pyp.Literal("]").suppress() 1470 semi = pyp.Literal(";").suppress() 1471 colon = pyp.Literal(":").suppress() 1472 # this also converts the value to an int 1473 number = pyp.Word(pyp.nums).setParseAction(lambda s, l, t: int(t[0])) 1474 1475 comment = pyp.Literal("#") + pyp.Optional(pyp.restOfLine) 1476 defa = pyp.Literal("_is_default").suppress() 1477 dbl_quote = pyp.Literal('"').suppress() 1478 1479 keyword = pyp.Word(pyp.alphanums + "-") 1480 1481 # value types 1482 value = pyp.Word(pyp.alphanums + "_-/.:") 1483 quoted = dbl_quote + pyp.CharsNotIn('"') + dbl_quote 1484 ipv4_addr = (pyp.Optional(pyp.Literal("ipv4")).suppress() + 1485 pyp.Word(pyp.nums + ".") + colon + number) 1486 ipv6_addr = (pyp.Optional(pyp.Literal("ipv6")).suppress() + 1487 pyp.Optional(lbracket) + pyp.Word(pyp.hexnums + ":") + 1488 pyp.Optional(rbracket) + colon + number) 1489 # meta device, extended syntax 1490 meta_value = ((value ^ quoted) + lbracket + number + rbracket) 1491 # device name, extended syntax 1492 device_value = pyp.Literal("minor").suppress() + number 1493 1494 # a statement 1495 stmt = (~rbrace + keyword + ~lbrace + 1496 pyp.Optional(ipv4_addr ^ ipv6_addr ^ value ^ quoted ^ meta_value ^ 1497 device_value) + 1498 pyp.Optional(defa) + semi + 1499 pyp.Optional(pyp.restOfLine).suppress()) 1500 1501 # an entire section 1502 section_name = pyp.Word(pyp.alphas + "_") 1503 section = section_name + lbrace + pyp.ZeroOrMore(pyp.Group(stmt)) + rbrace 1504 1505 bnf = pyp.ZeroOrMore(pyp.Group(section ^ stmt)) 1506 bnf.ignore(comment) 1507 1508 cls._PARSE_SHOW = bnf 1509 1510 return bnf
1511 1512 @classmethod
1513 - def _GetShowData(cls, minor):
1514 """Return the `drbdsetup show` data for a minor. 1515 1516 """ 1517 result = utils.RunCmd(["drbdsetup", cls._DevPath(minor), "show"]) 1518 if result.failed: 1519 logging.error("Can't display the drbd config: %s - %s", 1520 result.fail_reason, result.output) 1521 return None 1522 return result.stdout
1523 1524 @classmethod
1525 - def _GetDevInfo(cls, out):
1526 """Parse details about a given DRBD minor. 1527 1528 This return, if available, the local backing device (as a path) 1529 and the local and remote (ip, port) information from a string 1530 containing the output of the `drbdsetup show` command as returned 1531 by _GetShowData. 1532 1533 """ 1534 data = {} 1535 if not out: 1536 return data 1537 1538 bnf = cls._GetShowParser() 1539 # run pyparse 1540 1541 try: 1542 results = bnf.parseString(out) 1543 except pyp.ParseException, err: 1544 _ThrowError("Can't parse drbdsetup show output: %s", str(err)) 1545 1546 # and massage the results into our desired format 1547 for section in results: 1548 sname = section[0] 1549 if sname == "_this_host": 1550 for lst in section[1:]: 1551 if lst[0] == "disk": 1552 data["local_dev"] = lst[1] 1553 elif lst[0] == "meta-disk": 1554 data["meta_dev"] = lst[1] 1555 data["meta_index"] = lst[2] 1556 elif lst[0] == "address": 1557 data["local_addr"] = tuple(lst[1:]) 1558 elif sname == "_remote_host": 1559 for lst in section[1:]: 1560 if lst[0] == "address": 1561 data["remote_addr"] = tuple(lst[1:]) 1562 return data
1563
1564 - def _MatchesLocal(self, info):
1565 """Test if our local config matches with an existing device. 1566 1567 The parameter should be as returned from `_GetDevInfo()`. This 1568 method tests if our local backing device is the same as the one in 1569 the info parameter, in effect testing if we look like the given 1570 device. 1571 1572 """ 1573 if self._children: 1574 backend, meta = self._children 1575 else: 1576 backend = meta = None 1577 1578 if backend is not None: 1579 retval = ("local_dev" in info and info["local_dev"] == backend.dev_path) 1580 else: 1581 retval = ("local_dev" not in info) 1582 1583 if meta is not None: 1584 retval = retval and ("meta_dev" in info and 1585 info["meta_dev"] == meta.dev_path) 1586 retval = retval and ("meta_index" in info and 1587 info["meta_index"] == 0) 1588 else: 1589 retval = retval and ("meta_dev" not in info and 1590 "meta_index" not in info) 1591 return retval
1592
1593 - def _MatchesNet(self, info):
1594 """Test if our network config matches with an existing device. 1595 1596 The parameter should be as returned from `_GetDevInfo()`. This 1597 method tests if our network configuration is the same as the one 1598 in the info parameter, in effect testing if we look like the given 1599 device. 1600 1601 """ 1602 if (((self._lhost is None and not ("local_addr" in info)) and 1603 (self._rhost is None and not ("remote_addr" in info)))): 1604 return True 1605 1606 if self._lhost is None: 1607 return False 1608 1609 if not ("local_addr" in info and 1610 "remote_addr" in info): 1611 return False 1612 1613 retval = (info["local_addr"] == (self._lhost, self._lport)) 1614 retval = (retval and 1615 info["remote_addr"] == (self._rhost, self._rport)) 1616 return retval
1617
1618 - def _AssembleLocal(self, minor, backend, meta, size):
1619 """Configure the local part of a DRBD device. 1620 1621 """ 1622 args = ["drbdsetup", self._DevPath(minor), "disk", 1623 backend, meta, "0", 1624 "-e", "detach", 1625 "--create-device"] 1626 if size: 1627 args.extend(["-d", "%sm" % size]) 1628 1629 version = self._GetVersion(self._GetProcData()) 1630 vmaj = version["k_major"] 1631 vmin = version["k_minor"] 1632 vrel = version["k_point"] 1633 1634 barrier_args = \ 1635 self._ComputeDiskBarrierArgs(vmaj, vmin, vrel, 1636 self.params[constants.LDP_BARRIERS], 1637 self.params[constants.LDP_NO_META_FLUSH]) 1638 args.extend(barrier_args) 1639 1640 if self.params[constants.LDP_DISK_CUSTOM]: 1641 args.extend(shlex.split(self.params[constants.LDP_DISK_CUSTOM])) 1642 1643 result = utils.RunCmd(args) 1644 if result.failed: 1645 _ThrowError("drbd%d: can't attach local disk: %s", minor, result.output)
1646 1647 @classmethod
1648 - def _ComputeDiskBarrierArgs(cls, vmaj, vmin, vrel, disabled_barriers, 1649 disable_meta_flush):
1650 """Compute the DRBD command line parameters for disk barriers 1651 1652 Returns a list of the disk barrier parameters as requested via the 1653 disabled_barriers and disable_meta_flush arguments, and according to the 1654 supported ones in the DRBD version vmaj.vmin.vrel 1655 1656 If the desired option is unsupported, raises errors.BlockDeviceError. 1657 1658 """ 1659 disabled_barriers_set = frozenset(disabled_barriers) 1660 if not disabled_barriers_set in constants.DRBD_VALID_BARRIER_OPT: 1661 raise errors.BlockDeviceError("%s is not a valid option set for DRBD" 1662 " barriers" % disabled_barriers) 1663 1664 args = [] 1665 1666 # The following code assumes DRBD 8.x, with x < 4 and x != 1 (DRBD 8.1.x 1667 # does not exist) 1668 if not vmaj == 8 and vmin in (0, 2, 3): 1669 raise errors.BlockDeviceError("Unsupported DRBD version: %d.%d.%d" % 1670 (vmaj, vmin, vrel)) 1671 1672 def _AppendOrRaise(option, min_version): 1673 """Helper for DRBD options""" 1674 if min_version is not None and vrel >= min_version: 1675 args.append(option) 1676 else: 1677 raise errors.BlockDeviceError("Could not use the option %s as the" 1678 " DRBD version %d.%d.%d does not support" 1679 " it." % (option, vmaj, vmin, vrel))
1680 1681 # the minimum version for each feature is encoded via pairs of (minor 1682 # version -> x) where x is version in which support for the option was 1683 # introduced. 1684 meta_flush_supported = disk_flush_supported = { 1685 0: 12, 1686 2: 7, 1687 3: 0, 1688 } 1689 1690 disk_drain_supported = { 1691 2: 7, 1692 3: 0, 1693 } 1694 1695 disk_barriers_supported = { 1696 3: 0, 1697 } 1698 1699 # meta flushes 1700 if disable_meta_flush: 1701 _AppendOrRaise(cls._DISABLE_META_FLUSH_OPTION, 1702 meta_flush_supported.get(vmin, None)) 1703 1704 # disk flushes 1705 if constants.DRBD_B_DISK_FLUSH in disabled_barriers_set: 1706 _AppendOrRaise(cls._DISABLE_FLUSH_OPTION, 1707 disk_flush_supported.get(vmin, None)) 1708 1709 # disk drain 1710 if constants.DRBD_B_DISK_DRAIN in disabled_barriers_set: 1711 _AppendOrRaise(cls._DISABLE_DRAIN_OPTION, 1712 disk_drain_supported.get(vmin, None)) 1713 1714 # disk barriers 1715 if constants.DRBD_B_DISK_BARRIERS in disabled_barriers_set: 1716 _AppendOrRaise(cls._DISABLE_DISK_OPTION, 1717 disk_barriers_supported.get(vmin, None)) 1718 1719 return args
1720
1721 - def _AssembleNet(self, minor, net_info, protocol, 1722 dual_pri=False, hmac=None, secret=None):
1723 """Configure the network part of the device. 1724 1725 """ 1726 lhost, lport, rhost, rport = net_info 1727 if None in net_info: 1728 # we don't want network connection and actually want to make 1729 # sure its shutdown 1730 self._ShutdownNet(minor) 1731 return 1732 1733 # Workaround for a race condition. When DRBD is doing its dance to 1734 # establish a connection with its peer, it also sends the 1735 # synchronization speed over the wire. In some cases setting the 1736 # sync speed only after setting up both sides can race with DRBD 1737 # connecting, hence we set it here before telling DRBD anything 1738 # about its peer. 1739 sync_errors = self._SetMinorSyncParams(minor, self.params) 1740 if sync_errors: 1741 _ThrowError("drbd%d: can't set the synchronization parameters: %s" % 1742 (minor, utils.CommaJoin(sync_errors))) 1743 1744 if netutils.IP6Address.IsValid(lhost): 1745 if not netutils.IP6Address.IsValid(rhost): 1746 _ThrowError("drbd%d: can't connect ip %s to ip %s" % 1747 (minor, lhost, rhost)) 1748 family = "ipv6" 1749 elif netutils.IP4Address.IsValid(lhost): 1750 if not netutils.IP4Address.IsValid(rhost): 1751 _ThrowError("drbd%d: can't connect ip %s to ip %s" % 1752 (minor, lhost, rhost)) 1753 family = "ipv4" 1754 else: 1755 _ThrowError("drbd%d: Invalid ip %s" % (minor, lhost)) 1756 1757 args = ["drbdsetup", self._DevPath(minor), "net", 1758 "%s:%s:%s" % (family, lhost, lport), 1759 "%s:%s:%s" % (family, rhost, rport), protocol, 1760 "-A", "discard-zero-changes", 1761 "-B", "consensus", 1762 "--create-device", 1763 ] 1764 if dual_pri: 1765 args.append("-m") 1766 if hmac and secret: 1767 args.extend(["-a", hmac, "-x", secret]) 1768 1769 if self.params[constants.LDP_NET_CUSTOM]: 1770 args.extend(shlex.split(self.params[constants.LDP_NET_CUSTOM])) 1771 1772 result = utils.RunCmd(args) 1773 if result.failed: 1774 _ThrowError("drbd%d: can't setup network: %s - %s", 1775 minor, result.fail_reason, result.output) 1776 1777 def _CheckNetworkConfig(): 1778 info = self._GetDevInfo(self._GetShowData(minor)) 1779 if not "local_addr" in info or not "remote_addr" in info: 1780 raise utils.RetryAgain() 1781 1782 if (info["local_addr"] != (lhost, lport) or 1783 info["remote_addr"] != (rhost, rport)): 1784 raise utils.RetryAgain()
1785 1786 try: 1787 utils.Retry(_CheckNetworkConfig, 1.0, 10.0) 1788 except utils.RetryTimeout: 1789 _ThrowError("drbd%d: timeout while configuring network", minor) 1790
1791 - def AddChildren(self, devices):
1792 """Add a disk to the DRBD device. 1793 1794 """ 1795 if self.minor is None: 1796 _ThrowError("drbd%d: can't attach to dbrd8 during AddChildren", 1797 self._aminor) 1798 if len(devices) != 2: 1799 _ThrowError("drbd%d: need two devices for AddChildren", self.minor) 1800 info = self._GetDevInfo(self._GetShowData(self.minor)) 1801 if "local_dev" in info: 1802 _ThrowError("drbd%d: already attached to a local disk", self.minor) 1803 backend, meta = devices 1804 if backend.dev_path is None or meta.dev_path is None: 1805 _ThrowError("drbd%d: children not ready during AddChildren", self.minor) 1806 backend.Open() 1807 meta.Open() 1808 self._CheckMetaSize(meta.dev_path) 1809 self._InitMeta(self._FindUnusedMinor(), meta.dev_path) 1810 1811 self._AssembleLocal(self.minor, backend.dev_path, meta.dev_path, self.size) 1812 self._children = devices
1813
1814 - def RemoveChildren(self, devices):
1815 """Detach the drbd device from local storage. 1816 1817 """ 1818 if self.minor is None: 1819 _ThrowError("drbd%d: can't attach to drbd8 during RemoveChildren", 1820 self._aminor) 1821 # early return if we don't actually have backing storage 1822 info = self._GetDevInfo(self._GetShowData(self.minor)) 1823 if "local_dev" not in info: 1824 return 1825 if len(self._children) != 2: 1826 _ThrowError("drbd%d: we don't have two children: %s", self.minor, 1827 self._children) 1828 if self._children.count(None) == 2: # we don't actually have children :) 1829 logging.warning("drbd%d: requested detach while detached", self.minor) 1830 return 1831 if len(devices) != 2: 1832 _ThrowError("drbd%d: we need two children in RemoveChildren", self.minor) 1833 for child, dev in zip(self._children, devices): 1834 if dev != child.dev_path: 1835 _ThrowError("drbd%d: mismatch in local storage (%s != %s) in" 1836 " RemoveChildren", self.minor, dev, child.dev_path) 1837 1838 self._ShutdownLocal(self.minor) 1839 self._children = []
1840 1841 @classmethod
1842 - def _SetMinorSyncParams(cls, minor, params):
1843 """Set the parameters of the DRBD syncer. 1844 1845 This is the low-level implementation. 1846 1847 @type minor: int 1848 @param minor: the drbd minor whose settings we change 1849 @type params: dict 1850 @param params: LD level disk parameters related to the synchronization 1851 @rtype: list 1852 @return: a list of error messages 1853 1854 """ 1855 1856 args = ["drbdsetup", cls._DevPath(minor), "syncer"] 1857 if params[constants.LDP_DYNAMIC_RESYNC]: 1858 version = cls._GetVersion(cls._GetProcData()) 1859 vmin = version["k_minor"] 1860 vrel = version["k_point"] 1861 1862 # By definition we are using 8.x, so just check the rest of the version 1863 # number 1864 if vmin != 3 or vrel < 9: 1865 msg = ("The current DRBD version (8.%d.%d) does not support the " 1866 "dynamic resync speed controller" % (vmin, vrel)) 1867 logging.error(msg) 1868 return [msg] 1869 1870 if params[constants.LDP_PLAN_AHEAD] == 0: 1871 msg = ("A value of 0 for c-plan-ahead disables the dynamic sync speed" 1872 " controller at DRBD level. If you want to disable it, please" 1873 " set the dynamic-resync disk parameter to False.") 1874 logging.error(msg) 1875 return [msg] 1876 1877 # add the c-* parameters to args 1878 args.extend(["--c-plan-ahead", params[constants.LDP_PLAN_AHEAD], 1879 "--c-fill-target", params[constants.LDP_FILL_TARGET], 1880 "--c-delay-target", params[constants.LDP_DELAY_TARGET], 1881 "--c-max-rate", params[constants.LDP_MAX_RATE], 1882 "--c-min-rate", params[constants.LDP_MIN_RATE], 1883 ]) 1884 1885 else: 1886 args.extend(["-r", "%d" % params[constants.LDP_RESYNC_RATE]]) 1887 1888 args.append("--create-device") 1889 result = utils.RunCmd(args) 1890 if result.failed: 1891 msg = ("Can't change syncer rate: %s - %s" % 1892 (result.fail_reason, result.output)) 1893 logging.error(msg) 1894 return [msg] 1895 1896 return []
1897
1898 - def SetSyncParams(self, params):
1899 """Set the synchronization parameters of the DRBD syncer. 1900 1901 @type params: dict 1902 @param params: LD level disk parameters related to the synchronization 1903 @rtype: list 1904 @return: a list of error messages, emitted both by the current node and by 1905 children. An empty list means no errors 1906 1907 """ 1908 if self.minor is None: 1909 err = "Not attached during SetSyncParams" 1910 logging.info(err) 1911 return [err] 1912 1913 children_result = super(DRBD8, self).SetSyncParams(params) 1914 children_result.extend(self._SetMinorSyncParams(self.minor, params)) 1915 return children_result
1916
1917 - def PauseResumeSync(self, pause):
1918 """Pauses or resumes the sync of a DRBD device. 1919 1920 @param pause: Wether to pause or resume 1921 @return: the success of the operation 1922 1923 """ 1924 if self.minor is None: 1925 logging.info("Not attached during PauseSync") 1926 return False 1927 1928 children_result = super(DRBD8, self).PauseResumeSync(pause) 1929 1930 if pause: 1931 cmd = "pause-sync" 1932 else: 1933 cmd = "resume-sync" 1934 1935 result = utils.RunCmd(["drbdsetup", self.dev_path, cmd]) 1936 if result.failed: 1937 logging.error("Can't %s: %s - %s", cmd, 1938 result.fail_reason, result.output) 1939 return not result.failed and children_result
1940
1941 - def GetProcStatus(self):
1942 """Return device data from /proc. 1943 1944 """ 1945 if self.minor is None: 1946 _ThrowError("drbd%d: GetStats() called while not attached", self._aminor) 1947 proc_info = self._MassageProcData(self._GetProcData()) 1948 if self.minor not in proc_info: 1949 _ThrowError("drbd%d: can't find myself in /proc", self.minor) 1950 return DRBD8Status(proc_info[self.minor])
1951
1952 - def GetSyncStatus(self):
1953 """Returns the sync status of the device. 1954 1955 1956 If sync_percent is None, it means all is ok 1957 If estimated_time is None, it means we can't estimate 1958 the time needed, otherwise it's the time left in seconds. 1959 1960 1961 We set the is_degraded parameter to True on two conditions: 1962 network not connected or local disk missing. 1963 1964 We compute the ldisk parameter based on whether we have a local 1965 disk or not. 1966 1967 @rtype: objects.BlockDevStatus 1968 1969 """ 1970 if self.minor is None and not self.Attach(): 1971 _ThrowError("drbd%d: can't Attach() in GetSyncStatus", self._aminor) 1972 1973 stats = self.GetProcStatus() 1974 is_degraded = not stats.is_connected or not stats.is_disk_uptodate 1975 1976 if stats.is_disk_uptodate: 1977 ldisk_status = constants.LDS_OKAY 1978 elif stats.is_diskless: 1979 ldisk_status = constants.LDS_FAULTY 1980 else: 1981 ldisk_status = constants.LDS_UNKNOWN 1982 1983 return objects.BlockDevStatus(dev_path=self.dev_path, 1984 major=self.major, 1985 minor=self.minor, 1986 sync_percent=stats.sync_percent, 1987 estimated_time=stats.est_time, 1988 is_degraded=is_degraded, 1989 ldisk_status=ldisk_status)
1990
1991 - def Open(self, force=False):
1992 """Make the local state primary. 1993 1994 If the 'force' parameter is given, the '-o' option is passed to 1995 drbdsetup. Since this is a potentially dangerous operation, the 1996 force flag should be only given after creation, when it actually 1997 is mandatory. 1998 1999 """ 2000 if self.minor is None and not self.Attach(): 2001 logging.error("DRBD cannot attach to a device during open") 2002 return False 2003 cmd = ["drbdsetup", self.dev_path, "primary"] 2004 if force: 2005 cmd.append("-o") 2006 result = utils.RunCmd(cmd) 2007 if result.failed: 2008 _ThrowError("drbd%d: can't make drbd device primary: %s", self.minor, 2009 result.output)
2010
2011 - def Close(self):
2012 """Make the local state secondary. 2013 2014 This will, of course, fail if the device is in use. 2015 2016 """ 2017 if self.minor is None and not self.Attach(): 2018 _ThrowError("drbd%d: can't Attach() in Close()", self._aminor) 2019 result = utils.RunCmd(["drbdsetup", self.dev_path, "secondary"]) 2020 if result.failed: 2021 _ThrowError("drbd%d: can't switch drbd device to secondary: %s", 2022 self.minor, result.output)
2023
2024 - def DisconnectNet(self):
2025 """Removes network configuration. 2026 2027 This method shutdowns the network side of the device. 2028 2029 The method will wait up to a hardcoded timeout for the device to 2030 go into standalone after the 'disconnect' command before 2031 re-configuring it, as sometimes it takes a while for the 2032 disconnect to actually propagate and thus we might issue a 'net' 2033 command while the device is still connected. If the device will 2034 still be attached to the network and we time out, we raise an 2035 exception. 2036 2037 """ 2038 if self.minor is None: 2039 _ThrowError("drbd%d: disk not attached in re-attach net", self._aminor) 2040 2041 if None in (self._lhost, self._lport, self._rhost, self._rport): 2042 _ThrowError("drbd%d: DRBD disk missing network info in" 2043 " DisconnectNet()", self.minor) 2044 2045 class _DisconnectStatus: 2046 def __init__(self, ever_disconnected): 2047 self.ever_disconnected = ever_disconnected
2048 2049 dstatus = _DisconnectStatus(_IgnoreError(self._ShutdownNet, self.minor)) 2050 2051 def _WaitForDisconnect(): 2052 if self.GetProcStatus().is_standalone: 2053 return 2054 2055 # retry the disconnect, it seems possible that due to a well-time 2056 # disconnect on the peer, my disconnect command might be ignored and 2057 # forgotten 2058 dstatus.ever_disconnected = \ 2059 _IgnoreError(self._ShutdownNet, self.minor) or dstatus.ever_disconnected 2060 2061 raise utils.RetryAgain() 2062 2063 # Keep start time 2064 start_time = time.time() 2065 2066 try: 2067 # Start delay at 100 milliseconds and grow up to 2 seconds 2068 utils.Retry(_WaitForDisconnect, (0.1, 1.5, 2.0), 2069 self._NET_RECONFIG_TIMEOUT) 2070 except utils.RetryTimeout: 2071 if dstatus.ever_disconnected: 2072 msg = ("drbd%d: device did not react to the" 2073 " 'disconnect' command in a timely manner") 2074 else: 2075 msg = "drbd%d: can't shutdown network, even after multiple retries" 2076 2077 _ThrowError(msg, self.minor) 2078 2079 reconfig_time = time.time() - start_time 2080 if reconfig_time > (self._NET_RECONFIG_TIMEOUT * 0.25): 2081 logging.info("drbd%d: DisconnectNet: detach took %.3f seconds", 2082 self.minor, reconfig_time) 2083
2084 - def AttachNet(self, multimaster):
2085 """Reconnects the network. 2086 2087 This method connects the network side of the device with a 2088 specified multi-master flag. The device needs to be 'Standalone' 2089 but have valid network configuration data. 2090 2091 Args: 2092 - multimaster: init the network in dual-primary mode 2093 2094 """ 2095 if self.minor is None: 2096 _ThrowError("drbd%d: device not attached in AttachNet", self._aminor) 2097 2098 if None in (self._lhost, self._lport, self._rhost, self._rport): 2099 _ThrowError("drbd%d: missing network info in AttachNet()", self.minor) 2100 2101 status = self.GetProcStatus() 2102 2103 if not status.is_standalone: 2104 _ThrowError("drbd%d: device is not standalone in AttachNet", self.minor) 2105 2106 self._AssembleNet(self.minor, 2107 (self._lhost, self._lport, self._rhost, self._rport), 2108 constants.DRBD_NET_PROTOCOL, dual_pri=multimaster, 2109 hmac=constants.DRBD_HMAC_ALG, secret=self._secret)
2110
2111 - def Attach(self):
2112 """Check if our minor is configured. 2113 2114 This doesn't do any device configurations - it only checks if the 2115 minor is in a state different from Unconfigured. 2116 2117 Note that this function will not change the state of the system in 2118 any way (except in case of side-effects caused by reading from 2119 /proc). 2120 2121 """ 2122 used_devs = self.GetUsedDevs() 2123 if self._aminor in used_devs: 2124 minor = self._aminor 2125 else: 2126 minor = None 2127 2128 self._SetFromMinor(minor) 2129 return minor is not None
2130
2131 - def Assemble(self):
2132 """Assemble the drbd. 2133 2134 Method: 2135 - if we have a configured device, we try to ensure that it matches 2136 our config 2137 - if not, we create it from zero 2138 - anyway, set the device parameters 2139 2140 """ 2141 super(DRBD8, self).Assemble() 2142 2143 self.Attach() 2144 if self.minor is None: 2145 # local device completely unconfigured 2146 self._FastAssemble() 2147 else: 2148 # we have to recheck the local and network status and try to fix 2149 # the device 2150 self._SlowAssemble() 2151 2152 sync_errors = self.SetSyncParams(self.params) 2153 if sync_errors: 2154 _ThrowError("drbd%d: can't set the synchronization parameters: %s" % 2155 (self.minor, utils.CommaJoin(sync_errors)))
2156
2157 - def _SlowAssemble(self):
2158 """Assembles the DRBD device from a (partially) configured device. 2159 2160 In case of partially attached (local device matches but no network 2161 setup), we perform the network attach. If successful, we re-test 2162 the attach if can return success. 2163 2164 """ 2165 # TODO: Rewrite to not use a for loop just because there is 'break' 2166 # pylint: disable=W0631 2167 net_data = (self._lhost, self._lport, self._rhost, self._rport) 2168 for minor in (self._aminor,): 2169 info = self._GetDevInfo(self._GetShowData(minor)) 2170 match_l = self._MatchesLocal(info) 2171 match_r = self._MatchesNet(info) 2172 2173 if match_l and match_r: 2174 # everything matches 2175 break 2176 2177 if match_l and not match_r and "local_addr" not in info: 2178 # disk matches, but not attached to network, attach and recheck 2179 self._AssembleNet(minor, net_data, constants.DRBD_NET_PROTOCOL, 2180 hmac=constants.DRBD_HMAC_ALG, secret=self._secret) 2181 if self._MatchesNet(self._GetDevInfo(self._GetShowData(minor))): 2182 break 2183 else: 2184 _ThrowError("drbd%d: network attach successful, but 'drbdsetup" 2185 " show' disagrees", minor) 2186 2187 if match_r and "local_dev" not in info: 2188 # no local disk, but network attached and it matches 2189 self._AssembleLocal(minor, self._children[0].dev_path, 2190 self._children[1].dev_path, self.size) 2191 if self._MatchesNet(self._GetDevInfo(self._GetShowData(minor))): 2192 break 2193 else: 2194 _ThrowError("drbd%d: disk attach successful, but 'drbdsetup" 2195 " show' disagrees", minor) 2196 2197 # this case must be considered only if we actually have local 2198 # storage, i.e. not in diskless mode, because all diskless 2199 # devices are equal from the point of view of local 2200 # configuration 2201 if (match_l and "local_dev" in info and 2202 not match_r and "local_addr" in info): 2203 # strange case - the device network part points to somewhere 2204 # else, even though its local storage is ours; as we own the 2205 # drbd space, we try to disconnect from the remote peer and 2206 # reconnect to our correct one 2207 try: 2208 self._ShutdownNet(minor) 2209 except errors.BlockDeviceError, err: 2210 _ThrowError("drbd%d: device has correct local storage, wrong" 2211 " remote peer and is unable to disconnect in order" 2212 " to attach to the correct peer: %s", minor, str(err)) 2213 # note: _AssembleNet also handles the case when we don't want 2214 # local storage (i.e. one or more of the _[lr](host|port) is 2215 # None) 2216 self._AssembleNet(minor, net_data, constants.DRBD_NET_PROTOCOL, 2217 hmac=constants.DRBD_HMAC_ALG, secret=self._secret) 2218 if self._MatchesNet(self._GetDevInfo(self._GetShowData(minor))): 2219 break 2220 else: 2221 _ThrowError("drbd%d: network attach successful, but 'drbdsetup" 2222 " show' disagrees", minor) 2223 2224 else: 2225 minor = None 2226 2227 self._SetFromMinor(minor) 2228 if minor is None: 2229 _ThrowError("drbd%d: cannot activate, unknown or unhandled reason", 2230 self._aminor)
2231
2232 - def _FastAssemble(self):
2233 """Assemble the drbd device from zero. 2234 2235 This is run when in Assemble we detect our minor is unused. 2236 2237 """ 2238 minor = self._aminor 2239 if self._children and self._children[0] and self._children[1]: 2240 self._AssembleLocal(minor, self._children[0].dev_path, 2241 self._children[1].dev_path, self.size) 2242 if self._lhost and self._lport and self._rhost and self._rport: 2243 self._AssembleNet(minor, 2244 (self._lhost, self._lport, self._rhost, self._rport), 2245 constants.DRBD_NET_PROTOCOL, 2246 hmac=constants.DRBD_HMAC_ALG, secret=self._secret) 2247 self._SetFromMinor(minor)
2248 2249 @classmethod
2250 - def _ShutdownLocal(cls, minor):
2251 """Detach from the local device. 2252 2253 I/Os will continue to be served from the remote device. If we 2254 don't have a remote device, this operation will fail. 2255 2256 """ 2257 result = utils.RunCmd(["drbdsetup", cls._DevPath(minor), "detach"]) 2258 if result.failed: 2259 _ThrowError("drbd%d: can't detach local disk: %s", minor, result.output)
2260 2261 @classmethod
2262 - def _ShutdownNet(cls, minor):
2263 """Disconnect from the remote peer. 2264 2265 This fails if we don't have a local device. 2266 2267 """ 2268 result = utils.RunCmd(["drbdsetup", cls._DevPath(minor), "disconnect"]) 2269 if result.failed: 2270 _ThrowError("drbd%d: can't shutdown network: %s", minor, result.output)
2271 2272 @classmethod
2273 - def _ShutdownAll(cls, minor):
2274 """Deactivate the device. 2275 2276 This will, of course, fail if the device is in use. 2277 2278 """ 2279 result = utils.RunCmd(["drbdsetup", cls._DevPath(minor), "down"]) 2280 if result.failed: 2281 _ThrowError("drbd%d: can't shutdown drbd device: %s", 2282 minor, result.output)
2283
2284 - def Shutdown(self):
2285 """Shutdown the DRBD device. 2286 2287 """ 2288 if self.minor is None and not self.Attach(): 2289 logging.info("drbd%d: not attached during Shutdown()", self._aminor) 2290 return 2291 minor = self.minor 2292 self.minor = None 2293 self.dev_path = None 2294 self._ShutdownAll(minor)
2295
2296 - def Remove(self):
2297 """Stub remove for DRBD devices. 2298 2299 """ 2300 self.Shutdown()
2301 2302 @classmethod
2303 - def Create(cls, unique_id, children, size, params, excl_stor):
2304 """Create a new DRBD8 device. 2305 2306 Since DRBD devices are not created per se, just assembled, this 2307 function only initializes the metadata. 2308 2309 """ 2310 if len(children) != 2: 2311 raise errors.ProgrammerError("Invalid setup for the drbd device") 2312 if excl_stor: 2313 raise errors.ProgrammerError("DRBD device requested with" 2314 " exclusive_storage") 2315 # check that the minor is unused 2316 aminor = unique_id[4] 2317 proc_info = cls._MassageProcData(cls._GetProcData()) 2318 if aminor in proc_info: 2319 status = DRBD8Status(proc_info[aminor]) 2320 in_use = status.is_in_use 2321 else: 2322 in_use = False 2323 if in_use: 2324 _ThrowError("drbd%d: minor is already in use at Create() time", aminor) 2325 meta = children[1] 2326 meta.Assemble() 2327 if not meta.Attach(): 2328 _ThrowError("drbd%d: can't attach to meta device '%s'", 2329 aminor, meta) 2330 cls._CheckMetaSize(meta.dev_path) 2331 cls._InitMeta(aminor, meta.dev_path) 2332 return cls(unique_id, children, size, params)
2333
2334 - def Grow(self, amount, dryrun, backingstore):
2335 """Resize the DRBD device and its backing storage. 2336 2337 """ 2338 if self.minor is None: 2339 _ThrowError("drbd%d: Grow called while not attached", self._aminor) 2340 if len(self._children) != 2 or None in self._children: 2341 _ThrowError("drbd%d: cannot grow diskless device", self.minor) 2342 self._children[0].Grow(amount, dryrun, backingstore) 2343 if dryrun or backingstore: 2344 # DRBD does not support dry-run mode and is not backing storage, 2345 # so we'll return here 2346 return 2347 result = utils.RunCmd(["drbdsetup", self.dev_path, "resize", "-s", 2348 "%dm" % (self.size + amount)]) 2349 if result.failed: 2350 _ThrowError("drbd%d: resize failed: %s", self.minor, result.output)
2351
2352 2353 -class FileStorage(BlockDev):
2354 """File device. 2355 2356 This class represents the a file storage backend device. 2357 2358 The unique_id for the file device is a (file_driver, file_path) tuple. 2359 2360 """
2361 - def __init__(self, unique_id, children, size, params):
2362 """Initalizes a file device backend. 2363 2364 """ 2365 if children: 2366 raise errors.BlockDeviceError("Invalid setup for file device") 2367 super(FileStorage, self).__init__(unique_id, children, size, params) 2368 if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2: 2369 raise ValueError("Invalid configuration data %s" % str(unique_id)) 2370 self.driver = unique_id[0] 2371 self.dev_path = unique_id[1] 2372 2373 CheckFileStoragePath(self.dev_path) 2374 2375 self.Attach()
2376
2377 - def Assemble(self):
2378 """Assemble the device. 2379 2380 Checks whether the file device exists, raises BlockDeviceError otherwise. 2381 2382 """ 2383 if not os.path.exists(self.dev_path): 2384 _ThrowError("File device '%s' does not exist" % self.dev_path)
2385
2386 - def Shutdown(self):
2387 """Shutdown the device. 2388 2389 This is a no-op for the file type, as we don't deactivate 2390 the file on shutdown. 2391 2392 """ 2393 pass
2394
2395 - def Open(self, force=False):
2396 """Make the device ready for I/O. 2397 2398 This is a no-op for the file type. 2399 2400 """ 2401 pass
2402
2403 - def Close(self):
2404 """Notifies that the device will no longer be used for I/O. 2405 2406 This is a no-op for the file type. 2407 2408 """ 2409 pass
2410
2411 - def Remove(self):
2412 """Remove the file backing the block device. 2413 2414 @rtype: boolean 2415 @return: True if the removal was successful 2416 2417 """ 2418 try: 2419 os.remove(self.dev_path) 2420 except OSError, err: 2421 if err.errno != errno.ENOENT: 2422 _ThrowError("Can't remove file '%s': %s", self.dev_path, err)
2423
2424 - def Rename(self, new_id):
2425 """Renames the file. 2426 2427 """ 2428 # TODO: implement rename for file-based storage 2429 _ThrowError("Rename is not supported for file-based storage")
2430
2431 - def Grow(self, amount, dryrun, backingstore):
2432 """Grow the file 2433 2434 @param amount: the amount (in mebibytes) to grow with 2435 2436 """ 2437 if not backingstore: 2438 return 2439 # Check that the file exists 2440 self.Assemble() 2441 current_size = self.GetActualSize() 2442 new_size = current_size + amount * 1024 * 1024 2443 assert new_size > current_size, "Cannot Grow with a negative amount" 2444 # We can't really simulate the growth 2445 if dryrun: 2446 return 2447 try: 2448 f = open(self.dev_path, "a+") 2449 f.truncate(new_size) 2450 f.close() 2451 except EnvironmentError, err: 2452 _ThrowError("Error in file growth: %", str(err))
2453
2454 - def Attach(self):
2455 """Attach to an existing file. 2456 2457 Check if this file already exists. 2458 2459 @rtype: boolean 2460 @return: True if file exists 2461 2462 """ 2463 self.attached = os.path.exists(self.dev_path) 2464 return self.attached
2465
2466 - def GetActualSize(self):
2467 """Return the actual disk size. 2468 2469 @note: the device needs to be active when this is called 2470 2471 """ 2472 assert self.attached, "BlockDevice not attached in GetActualSize()" 2473 try: 2474 st = os.stat(self.dev_path) 2475 return st.st_size 2476 except OSError, err: 2477 _ThrowError("Can't stat %s: %s", self.dev_path, err)
2478 2479 @classmethod
2480 - def Create(cls, unique_id, children, size, params, excl_stor):
2481 """Create a new file. 2482 2483 @param size: the size of file in MiB 2484 2485 @rtype: L{bdev.FileStorage} 2486 @return: an instance of FileStorage 2487 2488 """ 2489 if excl_stor: 2490 raise errors.ProgrammerError("FileStorage device requested with" 2491 " exclusive_storage") 2492 if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2: 2493 raise ValueError("Invalid configuration data %s" % str(unique_id)) 2494 2495 dev_path = unique_id[1] 2496 2497 CheckFileStoragePath(dev_path) 2498 2499 try: 2500 fd = os.open(dev_path, os.O_RDWR | os.O_CREAT | os.O_EXCL) 2501 f = os.fdopen(fd, "w") 2502 f.truncate(size * 1024 * 1024) 2503 f.close() 2504 except EnvironmentError, err: 2505 if err.errno == errno.EEXIST: 2506 _ThrowError("File already existing: %s", dev_path) 2507 _ThrowError("Error in file creation: %", str(err)) 2508 2509 return FileStorage(unique_id, children, size, params)
2510
2511 2512 -class PersistentBlockDevice(BlockDev):
2513 """A block device with persistent node 2514 2515 May be either directly attached, or exposed through DM (e.g. dm-multipath). 2516 udev helpers are probably required to give persistent, human-friendly 2517 names. 2518 2519 For the time being, pathnames are required to lie under /dev. 2520 2521 """
2522 - def __init__(self, unique_id, children, size, params):
2523 """Attaches to a static block device. 2524 2525 The unique_id is a path under /dev. 2526 2527 """ 2528 super(PersistentBlockDevice, self).__init__(unique_id, children, size, 2529 params) 2530 if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2: 2531 raise ValueError("Invalid configuration data %s" % str(unique_id)) 2532 self.dev_path = unique_id[1] 2533 if not os.path.realpath(self.dev_path).startswith("/dev/"): 2534 raise ValueError("Full path '%s' lies outside /dev" % 2535 os.path.realpath(self.dev_path)) 2536 # TODO: this is just a safety guard checking that we only deal with devices 2537 # we know how to handle. In the future this will be integrated with 2538 # external storage backends and possible values will probably be collected 2539 # from the cluster configuration. 2540 if unique_id[0] != constants.BLOCKDEV_DRIVER_MANUAL: 2541 raise ValueError("Got persistent block device of invalid type: %s" % 2542 unique_id[0]) 2543 2544 self.major = self.minor = None 2545 self.Attach()
2546 2547 @classmethod
2548 - def Create(cls, unique_id, children, size, params, excl_stor):
2549 """Create a new device 2550 2551 This is a noop, we only return a PersistentBlockDevice instance 2552 2553 """ 2554 if excl_stor: 2555 raise errors.ProgrammerError("Persistent block device requested with" 2556 " exclusive_storage") 2557 return PersistentBlockDevice(unique_id, children, 0, params)
2558
2559 - def Remove(self):
2560 """Remove a device 2561 2562 This is a noop 2563 2564 """ 2565 pass
2566
2567 - def Rename(self, new_id):
2568 """Rename this device. 2569 2570 """ 2571 _ThrowError("Rename is not supported for PersistentBlockDev storage")
2572
2573 - def Attach(self):
2574 """Attach to an existing block device. 2575 2576 2577 """ 2578 self.attached = False 2579 try: 2580 st = os.stat(self.dev_path) 2581 except OSError, err: 2582 logging.error("Error stat()'ing %s: %s", self.dev_path, str(err)) 2583 return False 2584 2585 if not stat.S_ISBLK(st.st_mode): 2586 logging.error("%s is not a block device", self.dev_path) 2587 return False 2588 2589 self.major = os.major(st.st_rdev) 2590 self.minor = os.minor(st.st_rdev) 2591 self.attached = True 2592 2593 return True
2594
2595 - def Assemble(self):
2596 """Assemble the device. 2597 2598 """ 2599 pass
2600
2601 - def Shutdown(self):
2602 """Shutdown the device. 2603 2604 """ 2605 pass
2606
2607 - def Open(self, force=False):
2608 """Make the device ready for I/O. 2609 2610 """ 2611 pass
2612
2613 - def Close(self):
2614 """Notifies that the device will no longer be used for I/O. 2615 2616 """ 2617 pass
2618
2619 - def Grow(self, amount, dryrun, backingstore):
2620 """Grow the logical volume. 2621 2622 """ 2623 _ThrowError("Grow is not supported for PersistentBlockDev storage")
2624
2625 2626 -class RADOSBlockDevice(BlockDev):
2627 """A RADOS Block Device (rbd). 2628 2629 This class implements the RADOS Block Device for the backend. You need 2630 the rbd kernel driver, the RADOS Tools and a working RADOS cluster for 2631 this to be functional. 2632 2633 """
2634 - def __init__(self, unique_id, children, size, params):
2635 """Attaches to an rbd device. 2636 2637 """ 2638 super(RADOSBlockDevice, self).__init__(unique_id, children, size, params) 2639 if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2: 2640 raise ValueError("Invalid configuration data %s" % str(unique_id)) 2641 2642 self.driver, self.rbd_name = unique_id 2643 2644 self.major = self.minor = None 2645 self.Attach()
2646 2647 @classmethod
2648 - def Create(cls, unique_id, children, size, params, excl_stor):
2649 """Create a new rbd device. 2650 2651 Provision a new rbd volume inside a RADOS pool. 2652 2653 """ 2654 if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2: 2655 raise errors.ProgrammerError("Invalid configuration data %s" % 2656 str(unique_id)) 2657 if excl_stor: 2658 raise errors.ProgrammerError("RBD device requested with" 2659 " exclusive_storage") 2660 rbd_pool = params[constants.LDP_POOL] 2661 rbd_name = unique_id[1] 2662 2663 # Provision a new rbd volume (Image) inside the RADOS cluster. 2664 cmd = [constants.RBD_CMD, "create", "-p", rbd_pool, 2665 rbd_name, "--size", "%s" % size] 2666 result = utils.RunCmd(cmd) 2667 if result.failed: 2668 _ThrowError("rbd creation failed (%s): %s", 2669 result.fail_reason, result.output) 2670 2671 return RADOSBlockDevice(unique_id, children, size, params)
2672
2673 - def Remove(self):
2674 """Remove the rbd device. 2675 2676 """ 2677 rbd_pool = self.params[constants.LDP_POOL] 2678 rbd_name = self.unique_id[1] 2679 2680 if not self.minor and not self.Attach(): 2681 # The rbd device doesn't exist. 2682 return 2683 2684 # First shutdown the device (remove mappings). 2685 self.Shutdown() 2686 2687 # Remove the actual Volume (Image) from the RADOS cluster. 2688 cmd = [constants.RBD_CMD, "rm", "-p", rbd_pool, rbd_name] 2689 result = utils.RunCmd(cmd) 2690 if result.failed: 2691 _ThrowError("Can't remove Volume from cluster with rbd rm: %s - %s", 2692 result.fail_reason, result.output)
2693
2694 - def Rename(self, new_id):
2695 """Rename this device. 2696 2697 """ 2698 pass
2699
2700 - def Attach(self):
2701 """Attach to an existing rbd device. 2702 2703 This method maps the rbd volume that matches our name with 2704 an rbd device and then attaches to this device. 2705 2706 """ 2707 self.attached = False 2708 2709 # Map the rbd volume to a block device under /dev 2710 self.dev_path = self._MapVolumeToBlockdev(self.unique_id) 2711 2712 try: 2713 st = os.stat(self.dev_path) 2714 except OSError, err: 2715 logging.error("Error stat()'ing %s: %s", self.dev_path, str(err)) 2716 return False 2717 2718 if not stat.S_ISBLK(st.st_mode): 2719 logging.error("%s is not a block device", self.dev_path) 2720 return False 2721 2722 self.major = os.major(st.st_rdev) 2723 self.minor = os.minor(st.st_rdev) 2724 self.attached = True 2725 2726 return True
2727
2728 - def _MapVolumeToBlockdev(self, unique_id):
2729 """Maps existing rbd volumes to block devices. 2730 2731 This method should be idempotent if the mapping already exists. 2732 2733 @rtype: string 2734 @return: the block device path that corresponds to the volume 2735 2736 """ 2737 pool = self.params[constants.LDP_POOL] 2738 name = unique_id[1] 2739 2740 # Check if the mapping already exists. 2741 rbd_dev = self._VolumeToBlockdev(pool, name) 2742 if rbd_dev: 2743 # The mapping exists. Return it. 2744 return rbd_dev 2745 2746 # The mapping doesn't exist. Create it. 2747 map_cmd = [constants.RBD_CMD, "map", "-p", pool, name] 2748 result = utils.RunCmd(map_cmd) 2749 if result.failed: 2750 _ThrowError("rbd map failed (%s): %s", 2751 result.fail_reason, result.output) 2752 2753 # Find the corresponding rbd device. 2754 rbd_dev = self._VolumeToBlockdev(pool, name) 2755 if not rbd_dev: 2756 _ThrowError("rbd map succeeded, but could not find the rbd block" 2757 " device in output of showmapped, for volume: %s", name) 2758 2759 # The device was successfully mapped. Return it. 2760 return rbd_dev
2761 2762 @classmethod
2763 - def _VolumeToBlockdev(cls, pool, volume_name):
2764 """Do the 'volume name'-to-'rbd block device' resolving. 2765 2766 @type pool: string 2767 @param pool: RADOS pool to use 2768 @type volume_name: string 2769 @param volume_name: the name of the volume whose device we search for 2770 @rtype: string or None 2771 @return: block device path if the volume is mapped, else None 2772 2773 """ 2774 try: 2775 # Newer versions of the rbd tool support json output formatting. Use it 2776 # if available. 2777 showmap_cmd = [ 2778 constants.RBD_CMD, 2779 "showmapped", 2780 "-p", 2781 pool, 2782 "--format", 2783 "json" 2784 ] 2785 result = utils.RunCmd(showmap_cmd) 2786 if result.failed: 2787 logging.error("rbd JSON output formatting returned error (%s): %s," 2788 "falling back to plain output parsing", 2789 result.fail_reason, result.output) 2790 raise RbdShowmappedJsonError 2791 2792 return cls._ParseRbdShowmappedJson(result.output, volume_name) 2793 except RbdShowmappedJsonError: 2794 # For older versions of rbd, we have to parse the plain / text output 2795 # manually. 2796 showmap_cmd = [constants.RBD_CMD, "showmapped", "-p", pool] 2797 result = utils.RunCmd(showmap_cmd) 2798 if result.failed: 2799 _ThrowError("rbd showmapped failed (%s): %s", 2800 result.fail_reason, result.output) 2801 2802 return cls._ParseRbdShowmappedPlain(result.output, volume_name)
2803 2804 @staticmethod
2805 - def _ParseRbdShowmappedJson(output, volume_name):
2806 """Parse the json output of `rbd showmapped'. 2807 2808 This method parses the json output of `rbd showmapped' and returns the rbd 2809 block device path (e.g. /dev/rbd0) that matches the given rbd volume. 2810 2811 @type output: string 2812 @param output: the json output of `rbd showmapped' 2813 @type volume_name: string 2814 @param volume_name: the name of the volume whose device we search for 2815 @rtype: string or None 2816 @return: block device path if the volume is mapped, else None 2817 2818 """ 2819 try: 2820 devices = serializer.LoadJson(output) 2821 except ValueError, err: 2822 _ThrowError("Unable to parse JSON data: %s" % err) 2823 2824 rbd_dev = None 2825 for d in devices.values(): # pylint: disable=E1103 2826 try: 2827 name = d["name"] 2828 except KeyError: 2829 _ThrowError("'name' key missing from json object %s", devices) 2830 2831 if name == volume_name: 2832 if rbd_dev is not None: 2833 _ThrowError("rbd volume %s is mapped more than once", volume_name) 2834 2835 rbd_dev = d["device"] 2836 2837 return rbd_dev
2838 2839 @staticmethod
2840 - def _ParseRbdShowmappedPlain(output, volume_name):
2841 """Parse the (plain / text) output of `rbd showmapped'. 2842 2843 This method parses the output of `rbd showmapped' and returns 2844 the rbd block device path (e.g. /dev/rbd0) that matches the 2845 given rbd volume. 2846 2847 @type output: string 2848 @param output: the plain text output of `rbd showmapped' 2849 @type volume_name: string 2850 @param volume_name: the name of the volume whose device we search for 2851 @rtype: string or None 2852 @return: block device path if the volume is mapped, else None 2853 2854 """ 2855 allfields = 5 2856 volumefield = 2 2857 devicefield = 4 2858 2859 lines = output.splitlines() 2860 2861 # Try parsing the new output format (ceph >= 0.55). 2862 splitted_lines = map(lambda l: l.split(), lines) 2863 2864 # Check for empty output. 2865 if not splitted_lines: 2866 return None 2867 2868 # Check showmapped output, to determine number of fields. 2869 field_cnt = len(splitted_lines[0]) 2870 if field_cnt != allfields: 2871 # Parsing the new format failed. Fallback to parsing the old output 2872 # format (< 0.55). 2873 splitted_lines = map(lambda l: l.split("\t"), lines) 2874 if field_cnt != allfields: 2875 _ThrowError("Cannot parse rbd showmapped output expected %s fields," 2876 " found %s", allfields, field_cnt) 2877 2878 matched_lines = \ 2879 filter(lambda l: len(l) == allfields and l[volumefield] == volume_name, 2880 splitted_lines) 2881 2882 if len(matched_lines) > 1: 2883 _ThrowError("rbd volume %s mapped more than once", volume_name) 2884 2885 if matched_lines: 2886 # rbd block device found. Return it. 2887 rbd_dev = matched_lines[0][devicefield] 2888 return rbd_dev 2889 2890 # The given volume is not mapped. 2891 return None
2892
2893 - def Assemble(self):
2894 """Assemble the device. 2895 2896 """ 2897 pass
2898
2899 - def Shutdown(self):
2900 """Shutdown the device. 2901 2902 """ 2903 if not self.minor and not self.Attach(): 2904 # The rbd device doesn't exist. 2905 return 2906 2907 # Unmap the block device from the Volume. 2908 self._UnmapVolumeFromBlockdev(self.unique_id) 2909 2910 self.minor = None 2911 self.dev_path = None
2912
2913 - def _UnmapVolumeFromBlockdev(self, unique_id):
2914 """Unmaps the rbd device from the Volume it is mapped. 2915 2916 Unmaps the rbd device from the Volume it was previously mapped to. 2917 This method should be idempotent if the Volume isn't mapped. 2918 2919 """ 2920 pool = self.params[constants.LDP_POOL] 2921 name = unique_id[1] 2922 2923 # Check if the mapping already exists. 2924 rbd_dev = self._VolumeToBlockdev(pool, name) 2925 2926 if rbd_dev: 2927 # The mapping exists. Unmap the rbd device. 2928 unmap_cmd = [constants.RBD_CMD, "unmap", "%s" % rbd_dev] 2929 result = utils.RunCmd(unmap_cmd) 2930 if result.failed: 2931 _ThrowError("rbd unmap failed (%s): %s", 2932 result.fail_reason, result.output)
2933
2934 - def Open(self, force=False):
2935 """Make the device ready for I/O. 2936 2937 """ 2938 pass
2939
2940 - def Close(self):
2941 """Notifies that the device will no longer be used for I/O. 2942 2943 """ 2944 pass
2945
2946 - def Grow(self, amount, dryrun, backingstore):
2947 """Grow the Volume. 2948 2949 @type amount: integer 2950 @param amount: the amount (in mebibytes) to grow with 2951 @type dryrun: boolean 2952 @param dryrun: whether to execute the operation in simulation mode 2953 only, without actually increasing the size 2954 2955 """ 2956 if not backingstore: 2957 return 2958 if not self.Attach(): 2959 _ThrowError("Can't attach to rbd device during Grow()") 2960 2961 if dryrun: 2962 # the rbd tool does not support dry runs of resize operations. 2963 # Since rbd volumes are thinly provisioned, we assume 2964 # there is always enough free space for the operation. 2965 return 2966 2967 rbd_pool = self.params[constants.LDP_POOL] 2968 rbd_name = self.unique_id[1] 2969 new_size = self.size + amount 2970 2971 # Resize the rbd volume (Image) inside the RADOS cluster. 2972 cmd = [constants.RBD_CMD, "resize", "-p", rbd_pool, 2973 rbd_name, "--size", "%s" % new_size] 2974 result = utils.RunCmd(cmd) 2975 if result.failed: 2976 _ThrowError("rbd resize failed (%s): %s", 2977 result.fail_reason, result.output)
2978
2979 2980 -class ExtStorageDevice(BlockDev):
2981 """A block device provided by an ExtStorage Provider. 2982 2983 This class implements the External Storage Interface, which means 2984 handling of the externally provided block devices. 2985 2986 """
2987 - def __init__(self, unique_id, children, size, params):
2988 """Attaches to an extstorage block device. 2989 2990 """ 2991 super(ExtStorageDevice, self).__init__(unique_id, children, size, params) 2992 if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2: 2993 raise ValueError("Invalid configuration data %s" % str(unique_id)) 2994 2995 self.driver, self.vol_name = unique_id 2996 self.ext_params = params 2997 2998 self.major = self.minor = None 2999 self.Attach()
3000 3001 @classmethod
3002 - def Create(cls, unique_id, children, size, params, excl_stor):
3003 """Create a new extstorage device. 3004 3005 Provision a new volume using an extstorage provider, which will 3006 then be mapped to a block device. 3007 3008 """ 3009 if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2: 3010 raise errors.ProgrammerError("Invalid configuration data %s" % 3011 str(unique_id)) 3012 if excl_stor: 3013 raise errors.ProgrammerError("extstorage device requested with" 3014 " exclusive_storage") 3015 3016 # Call the External Storage's create script, 3017 # to provision a new Volume inside the External Storage 3018 _ExtStorageAction(constants.ES_ACTION_CREATE, unique_id, 3019 params, str(size)) 3020 3021 return ExtStorageDevice(unique_id, children, size, params)
3022
3023 - def Remove(self):
3024 """Remove the extstorage device. 3025 3026 """ 3027 if not self.minor and not self.Attach(): 3028 # The extstorage device doesn't exist. 3029 return 3030 3031 # First shutdown the device (remove mappings). 3032 self.Shutdown() 3033 3034 # Call the External Storage's remove script, 3035 # to remove the Volume from the External Storage 3036 _ExtStorageAction(constants.ES_ACTION_REMOVE, self.unique_id, 3037 self.ext_params)
3038
3039 - def Rename(self, new_id):
3040 """Rename this device. 3041 3042 """ 3043 pass
3044
3045 - def Attach(self):
3046 """Attach to an existing extstorage device. 3047 3048 This method maps the extstorage volume that matches our name with 3049 a corresponding block device and then attaches to this device. 3050 3051 """ 3052 self.attached = False 3053 3054 # Call the External Storage's attach script, 3055 # to attach an existing Volume to a block device under /dev 3056 self.dev_path = _ExtStorageAction(constants.ES_ACTION_ATTACH, 3057 self.unique_id, self.ext_params) 3058 3059 try: 3060 st = os.stat(self.dev_path) 3061 except OSError, err: 3062 logging.error("Error stat()'ing %s: %s", self.dev_path, str(err)) 3063 return False 3064 3065 if not stat.S_ISBLK(st.st_mode): 3066 logging.error("%s is not a block device", self.dev_path) 3067 return False 3068 3069 self.major = os.major(st.st_rdev) 3070 self.minor = os.minor(st.st_rdev) 3071 self.attached = True 3072 3073 return True
3074
3075 - def Assemble(self):
3076 """Assemble the device. 3077 3078 """ 3079 pass
3080
3081 - def Shutdown(self):
3082 """Shutdown the device. 3083 3084 """ 3085 if not self.minor and not self.Attach(): 3086 # The extstorage device doesn't exist. 3087 return 3088 3089 # Call the External Storage's detach script, 3090 # to detach an existing Volume from it's block device under /dev 3091 _ExtStorageAction(constants.ES_ACTION_DETACH, self.unique_id, 3092 self.ext_params) 3093 3094 self.minor = None 3095 self.dev_path = None
3096
3097 - def Open(self, force=False):
3098 """Make the device ready for I/O. 3099 3100 """ 3101 pass
3102
3103 - def Close(self):
3104 """Notifies that the device will no longer be used for I/O. 3105 3106 """ 3107 pass
3108
3109 - def Grow(self, amount, dryrun, backingstore):
3110 """Grow the Volume. 3111 3112 @type amount: integer 3113 @param amount: the amount (in mebibytes) to grow with 3114 @type dryrun: boolean 3115 @param dryrun: whether to execute the operation in simulation mode 3116 only, without actually increasing the size 3117 3118 """ 3119 if not backingstore: 3120 return 3121 if not self.Attach(): 3122 _ThrowError("Can't attach to extstorage device during Grow()") 3123 3124 if dryrun: 3125 # we do not support dry runs of resize operations for now. 3126 return 3127 3128 new_size = self.size + amount 3129 3130 # Call the External Storage's grow script, 3131 # to grow an existing Volume inside the External Storage 3132 _ExtStorageAction(constants.ES_ACTION_GROW, self.unique_id, 3133 self.ext_params, str(self.size), grow=str(new_size))
3134
3135 - def SetInfo(self, text):
3136 """Update metadata with info text. 3137 3138 """ 3139 # Replace invalid characters 3140 text = re.sub("^[^A-Za-z0-9_+.]", "_", text) 3141 text = re.sub("[^-A-Za-z0-9_+.]", "_", text) 3142 3143 # Only up to 128 characters are allowed 3144 text = text[:128] 3145 3146 # Call the External Storage's setinfo script, 3147 # to set metadata for an existing Volume inside the External Storage 3148 _ExtStorageAction(constants.ES_ACTION_SETINFO, self.unique_id, 3149 self.ext_params, metadata=text)
3150
3151 3152 -def _ExtStorageAction(action, unique_id, ext_params, 3153 size=None, grow=None, metadata=None):
3154 """Take an External Storage action. 3155 3156 Take an External Storage action concerning or affecting 3157 a specific Volume inside the External Storage. 3158 3159 @type action: string 3160 @param action: which action to perform. One of: 3161 create / remove / grow / attach / detach 3162 @type unique_id: tuple (driver, vol_name) 3163 @param unique_id: a tuple containing the type of ExtStorage (driver) 3164 and the Volume name 3165 @type ext_params: dict 3166 @param ext_params: ExtStorage parameters 3167 @type size: integer 3168 @param size: the size of the Volume in mebibytes 3169 @type grow: integer 3170 @param grow: the new size in mebibytes (after grow) 3171 @type metadata: string 3172 @param metadata: metadata info of the Volume, for use by the provider 3173 @rtype: None or a block device path (during attach) 3174 3175 """ 3176 driver, vol_name = unique_id 3177 3178 # Create an External Storage instance of type `driver' 3179 status, inst_es = ExtStorageFromDisk(driver) 3180 if not status: 3181 _ThrowError("%s" % inst_es) 3182 3183 # Create the basic environment for the driver's scripts 3184 create_env = _ExtStorageEnvironment(unique_id, ext_params, size, 3185 grow, metadata) 3186 3187 # Do not use log file for action `attach' as we need 3188 # to get the output from RunResult 3189 # TODO: find a way to have a log file for attach too 3190 logfile = None 3191 if action is not constants.ES_ACTION_ATTACH: 3192 logfile = _VolumeLogName(action, driver, vol_name) 3193 3194 # Make sure the given action results in a valid script 3195 if action not in constants.ES_SCRIPTS: 3196 _ThrowError("Action '%s' doesn't result in a valid ExtStorage script" % 3197 action) 3198 3199 # Find out which external script to run according the given action 3200 script_name = action + "_script" 3201 script = getattr(inst_es, script_name) 3202 3203 # Run the external script 3204 result = utils.RunCmd([script], env=create_env, 3205 cwd=inst_es.path, output=logfile,) 3206 if result.failed: 3207 logging.error("External storage's %s command '%s' returned" 3208 " error: %s, logfile: %s, output: %s", 3209 action, result.cmd, result.fail_reason, 3210 logfile, result.output) 3211 3212 # If logfile is 'None' (during attach), it breaks TailFile 3213 # TODO: have a log file for attach too 3214 if action is not constants.ES_ACTION_ATTACH: 3215 lines = [utils.SafeEncode(val) 3216 for val in utils.TailFile(logfile, lines=20)] 3217 else: 3218 lines = result.output[-20:] 3219 3220 _ThrowError("External storage's %s script failed (%s), last" 3221 " lines of output:\n%s", 3222 action, result.fail_reason, "\n".join(lines)) 3223 3224 if action == constants.ES_ACTION_ATTACH: 3225 return result.stdout
3226
3227 3228 -def ExtStorageFromDisk(name, base_dir=None):
3229 """Create an ExtStorage instance from disk. 3230 3231 This function will return an ExtStorage instance 3232 if the given name is a valid ExtStorage name. 3233 3234 @type base_dir: string 3235 @keyword base_dir: Base directory containing ExtStorage installations. 3236 Defaults to a search in all the ES_SEARCH_PATH dirs. 3237 @rtype: tuple 3238 @return: True and the ExtStorage instance if we find a valid one, or 3239 False and the diagnose message on error 3240 3241 """ 3242 if base_dir is None: 3243 es_base_dir = pathutils.ES_SEARCH_PATH 3244 else: 3245 es_base_dir = [base_dir] 3246 3247 es_dir = utils.FindFile(name, es_base_dir, os.path.isdir) 3248 3249 if es_dir is None: 3250 return False, ("Directory for External Storage Provider %s not" 3251 " found in search path" % name) 3252 3253 # ES Files dictionary, we will populate it with the absolute path 3254 # names; if the value is True, then it is a required file, otherwise 3255 # an optional one 3256 es_files = dict.fromkeys(constants.ES_SCRIPTS, True) 3257 3258 es_files[constants.ES_PARAMETERS_FILE] = True 3259 3260 for (filename, _) in es_files.items(): 3261 es_files[filename] = utils.PathJoin(es_dir, filename) 3262 3263 try: 3264 st = os.stat(es_files[filename]) 3265 except EnvironmentError, err: 3266 return False, ("File '%s' under path '%s' is missing (%s)" % 3267 (filename, es_dir, utils.ErrnoOrStr(err))) 3268 3269 if not stat.S_ISREG(stat.S_IFMT(st.st_mode)): 3270 return False, ("File '%s' under path '%s' is not a regular file" % 3271 (filename, es_dir)) 3272 3273 if filename in constants.ES_SCRIPTS: 3274 if stat.S_IMODE(st.st_mode) & stat.S_IXUSR != stat.S_IXUSR: 3275 return False, ("File '%s' under path '%s' is not executable" % 3276 (filename, es_dir)) 3277 3278 parameters = [] 3279 if constants.ES_PARAMETERS_FILE in es_files: 3280 parameters_file = es_files[constants.ES_PARAMETERS_FILE] 3281 try: 3282 parameters = utils.ReadFile(parameters_file).splitlines() 3283 except EnvironmentError, err: 3284 return False, ("Error while reading the EXT parameters file at %s: %s" % 3285 (parameters_file, utils.ErrnoOrStr(err))) 3286 parameters = [v.split(None, 1) for v in parameters] 3287 3288 es_obj = \ 3289 objects.ExtStorage(name=name, path=es_dir, 3290 create_script=es_files[constants.ES_SCRIPT_CREATE], 3291 remove_script=es_files[constants.ES_SCRIPT_REMOVE], 3292 grow_script=es_files[constants.ES_SCRIPT_GROW], 3293 attach_script=es_files[constants.ES_SCRIPT_ATTACH], 3294 detach_script=es_files[constants.ES_SCRIPT_DETACH], 3295 setinfo_script=es_files[constants.ES_SCRIPT_SETINFO], 3296 verify_script=es_files[constants.ES_SCRIPT_VERIFY], 3297 supported_parameters=parameters) 3298 return True, es_obj
3299
3300 3301 -def _ExtStorageEnvironment(unique_id, ext_params, 3302 size=None, grow=None, metadata=None):
3303 """Calculate the environment for an External Storage script. 3304 3305 @type unique_id: tuple (driver, vol_name) 3306 @param unique_id: ExtStorage pool and name of the Volume 3307 @type ext_params: dict 3308 @param ext_params: the EXT parameters 3309 @type size: string 3310 @param size: size of the Volume (in mebibytes) 3311 @type grow: string 3312 @param grow: new size of Volume after grow (in mebibytes) 3313 @type metadata: string 3314 @param metadata: metadata info of the Volume 3315 @rtype: dict 3316 @return: dict of environment variables 3317 3318 """ 3319 vol_name = unique_id[1] 3320 3321 result = {} 3322 result["VOL_NAME"] = vol_name 3323 3324 # EXT params 3325 for pname, pvalue in ext_params.items(): 3326 result["EXTP_%s" % pname.upper()] = str(pvalue) 3327 3328 if size is not None: 3329 result["VOL_SIZE"] = size 3330 3331 if grow is not None: 3332 result["VOL_NEW_SIZE"] = grow 3333 3334 if metadata is not None: 3335 result["VOL_METADATA"] = metadata 3336 3337 return result
3338
3339 3340 -def _VolumeLogName(kind, es_name, volume):
3341 """Compute the ExtStorage log filename for a given Volume and operation. 3342 3343 @type kind: string 3344 @param kind: the operation type (e.g. create, remove etc.) 3345 @type es_name: string 3346 @param es_name: the ExtStorage name 3347 @type volume: string 3348 @param volume: the name of the Volume inside the External Storage 3349 3350 """ 3351 # Check if the extstorage log dir is a valid dir 3352 if not os.path.isdir(pathutils.LOG_ES_DIR): 3353 _ThrowError("Cannot find log directory: %s", pathutils.LOG_ES_DIR) 3354 3355 # TODO: Use tempfile.mkstemp to create unique filename 3356 base = ("%s-%s-%s-%s.log" % 3357 (kind, es_name, volume, utils.TimestampForFilename())) 3358 return utils.PathJoin(pathutils.LOG_ES_DIR, base)
3359 3360 3361 DEV_MAP = { 3362 constants.LD_LV: LogicalVolume, 3363 constants.LD_DRBD8: DRBD8, 3364 constants.LD_BLOCKDEV: PersistentBlockDevice, 3365 constants.LD_RBD: RADOSBlockDevice, 3366 constants.LD_EXT: ExtStorageDevice, 3367 } 3368 3369 if constants.ENABLE_FILE_STORAGE or constants.ENABLE_SHARED_FILE_STORAGE: 3370 DEV_MAP[constants.LD_FILE] = FileStorage
3371 3372 3373 -def _VerifyDiskType(dev_type):
3374 if dev_type not in DEV_MAP: 3375 raise errors.ProgrammerError("Invalid block device type '%s'" % dev_type)
3376
3377 3378 -def _VerifyDiskParams(disk):
3379 """Verifies if all disk parameters are set. 3380 3381 """ 3382 missing = set(constants.DISK_LD_DEFAULTS[disk.dev_type]) - set(disk.params) 3383 if missing: 3384 raise errors.ProgrammerError("Block device is missing disk parameters: %s" % 3385 missing)
3386
3387 3388 -def FindDevice(disk, children):
3389 """Search for an existing, assembled device. 3390 3391 This will succeed only if the device exists and is assembled, but it 3392 does not do any actions in order to activate the device. 3393 3394 @type disk: L{objects.Disk} 3395 @param disk: the disk object to find 3396 @type children: list of L{bdev.BlockDev} 3397 @param children: the list of block devices that are children of the device 3398 represented by the disk parameter 3399 3400 """ 3401 _VerifyDiskType(disk.dev_type) 3402 device = DEV_MAP[disk.dev_type](disk.physical_id, children, disk.size, 3403 disk.params) 3404 if not device.attached: 3405 return None 3406 return device
3407
3408 3409 -def Assemble(disk, children):
3410 """Try to attach or assemble an existing device. 3411 3412 This will attach to assemble the device, as needed, to bring it 3413 fully up. It must be safe to run on already-assembled devices. 3414 3415 @type disk: L{objects.Disk} 3416 @param disk: the disk object to assemble 3417 @type children: list of L{bdev.BlockDev} 3418 @param children: the list of block devices that are children of the device 3419 represented by the disk parameter 3420 3421 """ 3422 _VerifyDiskType(disk.dev_type) 3423 _VerifyDiskParams(disk) 3424 device = DEV_MAP[disk.dev_type](disk.physical_id, children, disk.size, 3425 disk.params) 3426 device.Assemble() 3427 return device
3428
3429 3430 -def Create(disk, children, excl_stor):
3431 """Create a device. 3432 3433 @type disk: L{objects.Disk} 3434 @param disk: the disk object to create 3435 @type children: list of L{bdev.BlockDev} 3436 @param children: the list of block devices that are children of the device 3437 represented by the disk parameter 3438 @type excl_stor: boolean 3439 @param excl_stor: Whether exclusive_storage is active 3440 3441 """ 3442 _VerifyDiskType(disk.dev_type) 3443 _VerifyDiskParams(disk) 3444 device = DEV_MAP[disk.dev_type].Create(disk.physical_id, children, disk.size, 3445 disk.params, excl_stor) 3446 return device
3447