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

Source Code for Module ganeti.bdev

   1  # 
   2  # 
   3   
   4  # Copyright (C) 2006, 2007, 2010 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 pyparsing as pyp 
  28  import os 
  29  import logging 
  30   
  31  from ganeti import utils 
  32  from ganeti import errors 
  33  from ganeti import constants 
  34  from ganeti import objects 
  35  from ganeti import compat 
  36  from ganeti import netutils 
  37   
  38   
  39  # Size of reads in _CanReadDevice 
  40  _DEVICE_READ_SIZE = 128 * 1024 
41 42 43 -def _IgnoreError(fn, *args, **kwargs):
44 """Executes the given function, ignoring BlockDeviceErrors. 45 46 This is used in order to simplify the execution of cleanup or 47 rollback functions. 48 49 @rtype: boolean 50 @return: True when fn didn't raise an exception, False otherwise 51 52 """ 53 try: 54 fn(*args, **kwargs) 55 return True 56 except errors.BlockDeviceError, err: 57 logging.warning("Caught BlockDeviceError but ignoring: %s", str(err)) 58 return False
59
60 61 -def _ThrowError(msg, *args):
62 """Log an error to the node daemon and the raise an exception. 63 64 @type msg: string 65 @param msg: the text of the exception 66 @raise errors.BlockDeviceError 67 68 """ 69 if args: 70 msg = msg % args 71 logging.error(msg) 72 raise errors.BlockDeviceError(msg)
73
74 75 -def _CanReadDevice(path):
76 """Check if we can read from the given device. 77 78 This tries to read the first 128k of the device. 79 80 """ 81 try: 82 utils.ReadFile(path, size=_DEVICE_READ_SIZE) 83 return True 84 except EnvironmentError: 85 logging.warning("Can't read from device %s", path, exc_info=True) 86 return False
87
88 89 -class BlockDev(object):
90 """Block device abstract class. 91 92 A block device can be in the following states: 93 - not existing on the system, and by `Create()` it goes into: 94 - existing but not setup/not active, and by `Assemble()` goes into: 95 - active read-write and by `Open()` it goes into 96 - online (=used, or ready for use) 97 98 A device can also be online but read-only, however we are not using 99 the readonly state (LV has it, if needed in the future) and we are 100 usually looking at this like at a stack, so it's easier to 101 conceptualise the transition from not-existing to online and back 102 like a linear one. 103 104 The many different states of the device are due to the fact that we 105 need to cover many device types: 106 - logical volumes are created, lvchange -a y $lv, and used 107 - drbd devices are attached to a local disk/remote peer and made primary 108 109 A block device is identified by three items: 110 - the /dev path of the device (dynamic) 111 - a unique ID of the device (static) 112 - it's major/minor pair (dynamic) 113 114 Not all devices implement both the first two as distinct items. LVM 115 logical volumes have their unique ID (the pair volume group, logical 116 volume name) in a 1-to-1 relation to the dev path. For DRBD devices, 117 the /dev path is again dynamic and the unique id is the pair (host1, 118 dev1), (host2, dev2). 119 120 You can get to a device in two ways: 121 - creating the (real) device, which returns you 122 an attached instance (lvcreate) 123 - attaching of a python instance to an existing (real) device 124 125 The second point, the attachement to a device, is different 126 depending on whether the device is assembled or not. At init() time, 127 we search for a device with the same unique_id as us. If found, 128 good. It also means that the device is already assembled. If not, 129 after assembly we'll have our correct major/minor. 130 131 """
132 - def __init__(self, unique_id, children, size):
133 self._children = children 134 self.dev_path = None 135 self.unique_id = unique_id 136 self.major = None 137 self.minor = None 138 self.attached = False 139 self.size = size
140
141 - def Assemble(self):
142 """Assemble the device from its components. 143 144 Implementations of this method by child classes must ensure that: 145 - after the device has been assembled, it knows its major/minor 146 numbers; this allows other devices (usually parents) to probe 147 correctly for their children 148 - calling this method on an existing, in-use device is safe 149 - if the device is already configured (and in an OK state), 150 this method is idempotent 151 152 """ 153 pass
154
155 - def Attach(self):
156 """Find a device which matches our config and attach to it. 157 158 """ 159 raise NotImplementedError
160
161 - def Close(self):
162 """Notifies that the device will no longer be used for I/O. 163 164 """ 165 raise NotImplementedError
166 167 @classmethod
168 - def Create(cls, unique_id, children, size):
169 """Create the device. 170 171 If the device cannot be created, it will return None 172 instead. Error messages go to the logging system. 173 174 Note that for some devices, the unique_id is used, and for other, 175 the children. The idea is that these two, taken together, are 176 enough for both creation and assembly (later). 177 178 """ 179 raise NotImplementedError
180
181 - def Remove(self):
182 """Remove this device. 183 184 This makes sense only for some of the device types: LV and file 185 storage. Also note that if the device can't attach, the removal 186 can't be completed. 187 188 """ 189 raise NotImplementedError
190
191 - def Rename(self, new_id):
192 """Rename this device. 193 194 This may or may not make sense for a given device type. 195 196 """ 197 raise NotImplementedError
198
199 - def Open(self, force=False):
200 """Make the device ready for use. 201 202 This makes the device ready for I/O. For now, just the DRBD 203 devices need this. 204 205 The force parameter signifies that if the device has any kind of 206 --force thing, it should be used, we know what we are doing. 207 208 """ 209 raise NotImplementedError
210
211 - def Shutdown(self):
212 """Shut down the device, freeing its children. 213 214 This undoes the `Assemble()` work, except for the child 215 assembling; as such, the children on the device are still 216 assembled after this call. 217 218 """ 219 raise NotImplementedError
220
221 - def SetSyncSpeed(self, speed):
222 """Adjust the sync speed of the mirror. 223 224 In case this is not a mirroring device, this is no-op. 225 226 """ 227 result = True 228 if self._children: 229 for child in self._children: 230 result = result and child.SetSyncSpeed(speed) 231 return result
232
233 - def GetSyncStatus(self):
234 """Returns the sync status of the device. 235 236 If this device is a mirroring device, this function returns the 237 status of the mirror. 238 239 If sync_percent is None, it means the device is not syncing. 240 241 If estimated_time is None, it means we can't estimate 242 the time needed, otherwise it's the time left in seconds. 243 244 If is_degraded is True, it means the device is missing 245 redundancy. This is usually a sign that something went wrong in 246 the device setup, if sync_percent is None. 247 248 The ldisk parameter represents the degradation of the local 249 data. This is only valid for some devices, the rest will always 250 return False (not degraded). 251 252 @rtype: objects.BlockDevStatus 253 254 """ 255 return objects.BlockDevStatus(dev_path=self.dev_path, 256 major=self.major, 257 minor=self.minor, 258 sync_percent=None, 259 estimated_time=None, 260 is_degraded=False, 261 ldisk_status=constants.LDS_OKAY)
262
263 - def CombinedSyncStatus(self):
264 """Calculate the mirror status recursively for our children. 265 266 The return value is the same as for `GetSyncStatus()` except the 267 minimum percent and maximum time are calculated across our 268 children. 269 270 @rtype: objects.BlockDevStatus 271 272 """ 273 status = self.GetSyncStatus() 274 275 min_percent = status.sync_percent 276 max_time = status.estimated_time 277 is_degraded = status.is_degraded 278 ldisk_status = status.ldisk_status 279 280 if self._children: 281 for child in self._children: 282 child_status = child.GetSyncStatus() 283 284 if min_percent is None: 285 min_percent = child_status.sync_percent 286 elif child_status.sync_percent is not None: 287 min_percent = min(min_percent, child_status.sync_percent) 288 289 if max_time is None: 290 max_time = child_status.estimated_time 291 elif child_status.estimated_time is not None: 292 max_time = max(max_time, child_status.estimated_time) 293 294 is_degraded = is_degraded or child_status.is_degraded 295 296 if ldisk_status is None: 297 ldisk_status = child_status.ldisk_status 298 elif child_status.ldisk_status is not None: 299 ldisk_status = max(ldisk_status, child_status.ldisk_status) 300 301 return objects.BlockDevStatus(dev_path=self.dev_path, 302 major=self.major, 303 minor=self.minor, 304 sync_percent=min_percent, 305 estimated_time=max_time, 306 is_degraded=is_degraded, 307 ldisk_status=ldisk_status)
308 309
310 - def SetInfo(self, text):
311 """Update metadata with info text. 312 313 Only supported for some device types. 314 315 """ 316 for child in self._children: 317 child.SetInfo(text)
318
319 - def Grow(self, amount):
320 """Grow the block device. 321 322 @param amount: the amount (in mebibytes) to grow with 323 324 """ 325 raise NotImplementedError
326
327 - def GetActualSize(self):
328 """Return the actual disk size. 329 330 @note: the device needs to be active when this is called 331 332 """ 333 assert self.attached, "BlockDevice not attached in GetActualSize()" 334 result = utils.RunCmd(["blockdev", "--getsize64", self.dev_path]) 335 if result.failed: 336 _ThrowError("blockdev failed (%s): %s", 337 result.fail_reason, result.output) 338 try: 339 sz = int(result.output.strip()) 340 except (ValueError, TypeError), err: 341 _ThrowError("Failed to parse blockdev output: %s", str(err)) 342 return sz
343
344 - def __repr__(self):
345 return ("<%s: unique_id: %s, children: %s, %s:%s, %s>" % 346 (self.__class__, self.unique_id, self._children, 347 self.major, self.minor, self.dev_path))
348
349 350 -class LogicalVolume(BlockDev):
351 """Logical Volume block device. 352 353 """ 354 _VALID_NAME_RE = re.compile("^[a-zA-Z0-9+_.-]*$") 355 _INVALID_NAMES = frozenset([".", "..", "snapshot", "pvmove"]) 356 _INVALID_SUBSTRINGS = frozenset(["_mlog", "_mimage"]) 357
358 - def __init__(self, unique_id, children, size):
359 """Attaches to a LV device. 360 361 The unique_id is a tuple (vg_name, lv_name) 362 363 """ 364 super(LogicalVolume, self).__init__(unique_id, children, size) 365 if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2: 366 raise ValueError("Invalid configuration data %s" % str(unique_id)) 367 self._vg_name, self._lv_name = unique_id 368 self._ValidateName(self._vg_name) 369 self._ValidateName(self._lv_name) 370 self.dev_path = utils.PathJoin("/dev", self._vg_name, self._lv_name) 371 self._degraded = True 372 self.major = self.minor = self.pe_size = self.stripe_count = None 373 self.Attach()
374 375 @classmethod
376 - def Create(cls, unique_id, children, size):
377 """Create a new logical volume. 378 379 """ 380 if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2: 381 raise errors.ProgrammerError("Invalid configuration data %s" % 382 str(unique_id)) 383 vg_name, lv_name = unique_id 384 cls._ValidateName(vg_name) 385 cls._ValidateName(lv_name) 386 pvs_info = cls.GetPVInfo([vg_name]) 387 if not pvs_info: 388 _ThrowError("Can't compute PV info for vg %s", vg_name) 389 pvs_info.sort() 390 pvs_info.reverse() 391 392 pvlist = [ pv[1] for pv in pvs_info ] 393 if compat.any(":" in v for v in pvlist): 394 _ThrowError("Some of your PVs have the invalid character ':' in their" 395 " name, this is not supported - please filter them out" 396 " in lvm.conf using either 'filter' or 'preferred_names'") 397 free_size = sum([ pv[0] for pv in pvs_info ]) 398 current_pvs = len(pvlist) 399 stripes = min(current_pvs, constants.LVM_STRIPECOUNT) 400 401 # The size constraint should have been checked from the master before 402 # calling the create function. 403 if free_size < size: 404 _ThrowError("Not enough free space: required %s," 405 " available %s", size, free_size) 406 cmd = ["lvcreate", "-L%dm" % size, "-n%s" % lv_name] 407 # If the free space is not well distributed, we won't be able to 408 # create an optimally-striped volume; in that case, we want to try 409 # with N, N-1, ..., 2, and finally 1 (non-stripped) number of 410 # stripes 411 for stripes_arg in range(stripes, 0, -1): 412 result = utils.RunCmd(cmd + ["-i%d" % stripes_arg] + [vg_name] + pvlist) 413 if not result.failed: 414 break 415 if result.failed: 416 _ThrowError("LV create failed (%s): %s", 417 result.fail_reason, result.output) 418 return LogicalVolume(unique_id, children, size)
419 420 @staticmethod
421 - def GetPVInfo(vg_names, filter_allocatable=True):
422 """Get the free space info for PVs in a volume group. 423 424 @param vg_names: list of volume group names, if empty all will be returned 425 @param filter_allocatable: whether to skip over unallocatable PVs 426 427 @rtype: list 428 @return: list of tuples (free_space, name) with free_space in mebibytes 429 430 """ 431 sep = "|" 432 command = ["pvs", "--noheadings", "--nosuffix", "--units=m", 433 "-opv_name,vg_name,pv_free,pv_attr", "--unbuffered", 434 "--separator=%s" % sep ] 435 result = utils.RunCmd(command) 436 if result.failed: 437 logging.error("Can't get the PV information: %s - %s", 438 result.fail_reason, result.output) 439 return None 440 data = [] 441 for line in result.stdout.splitlines(): 442 fields = line.strip().split(sep) 443 if len(fields) != 4: 444 logging.error("Can't parse pvs output: line '%s'", line) 445 return None 446 # (possibly) skip over pvs which are not allocatable 447 if filter_allocatable and fields[3][0] != 'a': 448 continue 449 # (possibly) skip over pvs which are not in the right volume group(s) 450 if vg_names and fields[1] not in vg_names: 451 continue 452 data.append((float(fields[2]), fields[0], fields[1])) 453 454 return data
455 456 @classmethod
457 - def _ValidateName(cls, name):
458 """Validates that a given name is valid as VG or LV name. 459 460 The list of valid characters and restricted names is taken out of 461 the lvm(8) manpage, with the simplification that we enforce both 462 VG and LV restrictions on the names. 463 464 """ 465 if (not cls._VALID_NAME_RE.match(name) or 466 name in cls._INVALID_NAMES or 467 compat.any(substring in name for substring in cls._INVALID_SUBSTRINGS)): 468 _ThrowError("Invalid LVM name '%s'", name)
469
470 - def Remove(self):
471 """Remove this logical volume. 472 473 """ 474 if not self.minor and not self.Attach(): 475 # the LV does not exist 476 return 477 result = utils.RunCmd(["lvremove", "-f", "%s/%s" % 478 (self._vg_name, self._lv_name)]) 479 if result.failed: 480 _ThrowError("Can't lvremove: %s - %s", result.fail_reason, result.output)
481
482 - def Rename(self, new_id):
483 """Rename this logical volume. 484 485 """ 486 if not isinstance(new_id, (tuple, list)) or len(new_id) != 2: 487 raise errors.ProgrammerError("Invalid new logical id '%s'" % new_id) 488 new_vg, new_name = new_id 489 if new_vg != self._vg_name: 490 raise errors.ProgrammerError("Can't move a logical volume across" 491 " volume groups (from %s to to %s)" % 492 (self._vg_name, new_vg)) 493 result = utils.RunCmd(["lvrename", new_vg, self._lv_name, new_name]) 494 if result.failed: 495 _ThrowError("Failed to rename the logical volume: %s", result.output) 496 self._lv_name = new_name 497 self.dev_path = utils.PathJoin("/dev", self._vg_name, self._lv_name)
498
499 - def Attach(self):
500 """Attach to an existing LV. 501 502 This method will try to see if an existing and active LV exists 503 which matches our name. If so, its major/minor will be 504 recorded. 505 506 """ 507 self.attached = False 508 result = utils.RunCmd(["lvs", "--noheadings", "--separator=,", 509 "--units=m", "--nosuffix", 510 "-olv_attr,lv_kernel_major,lv_kernel_minor," 511 "vg_extent_size,stripes", self.dev_path]) 512 if result.failed: 513 logging.error("Can't find LV %s: %s, %s", 514 self.dev_path, result.fail_reason, result.output) 515 return False 516 # the output can (and will) have multiple lines for multi-segment 517 # LVs, as the 'stripes' parameter is a segment one, so we take 518 # only the last entry, which is the one we're interested in; note 519 # that with LVM2 anyway the 'stripes' value must be constant 520 # across segments, so this is a no-op actually 521 out = result.stdout.splitlines() 522 if not out: # totally empty result? splitlines() returns at least 523 # one line for any non-empty string 524 logging.error("Can't parse LVS output, no lines? Got '%s'", str(out)) 525 return False 526 out = out[-1].strip().rstrip(',') 527 out = out.split(",") 528 if len(out) != 5: 529 logging.error("Can't parse LVS output, len(%s) != 5", str(out)) 530 return False 531 532 status, major, minor, pe_size, stripes = out 533 if len(status) != 6: 534 logging.error("lvs lv_attr is not 6 characters (%s)", status) 535 return False 536 537 try: 538 major = int(major) 539 minor = int(minor) 540 except (TypeError, ValueError), err: 541 logging.error("lvs major/minor cannot be parsed: %s", str(err)) 542 543 try: 544 pe_size = int(float(pe_size)) 545 except (TypeError, ValueError), err: 546 logging.error("Can't parse vg extent size: %s", err) 547 return False 548 549 try: 550 stripes = int(stripes) 551 except (TypeError, ValueError), err: 552 logging.error("Can't parse the number of stripes: %s", err) 553 return False 554 555 self.major = major 556 self.minor = minor 557 self.pe_size = pe_size 558 self.stripe_count = stripes 559 self._degraded = status[0] == 'v' # virtual volume, i.e. doesn't backing 560 # storage 561 self.attached = True 562 return True
563
564 - def Assemble(self):
565 """Assemble the device. 566 567 We always run `lvchange -ay` on the LV to ensure it's active before 568 use, as there were cases when xenvg was not active after boot 569 (also possibly after disk issues). 570 571 """ 572 result = utils.RunCmd(["lvchange", "-ay", self.dev_path]) 573 if result.failed: 574 _ThrowError("Can't activate lv %s: %s", self.dev_path, result.output)
575
576 - def Shutdown(self):
577 """Shutdown the device. 578 579 This is a no-op for the LV device type, as we don't deactivate the 580 volumes on shutdown. 581 582 """ 583 pass
584
585 - def GetSyncStatus(self):
586 """Returns the sync status of the device. 587 588 If this device is a mirroring device, this function returns the 589 status of the mirror. 590 591 For logical volumes, sync_percent and estimated_time are always 592 None (no recovery in progress, as we don't handle the mirrored LV 593 case). The is_degraded parameter is the inverse of the ldisk 594 parameter. 595 596 For the ldisk parameter, we check if the logical volume has the 597 'virtual' type, which means it's not backed by existing storage 598 anymore (read from it return I/O error). This happens after a 599 physical disk failure and subsequent 'vgreduce --removemissing' on 600 the volume group. 601 602 The status was already read in Attach, so we just return it. 603 604 @rtype: objects.BlockDevStatus 605 606 """ 607 if self._degraded: 608 ldisk_status = constants.LDS_FAULTY 609 else: 610 ldisk_status = constants.LDS_OKAY 611 612 return objects.BlockDevStatus(dev_path=self.dev_path, 613 major=self.major, 614 minor=self.minor, 615 sync_percent=None, 616 estimated_time=None, 617 is_degraded=self._degraded, 618 ldisk_status=ldisk_status)
619
620 - def Open(self, force=False):
621 """Make the device ready for I/O. 622 623 This is a no-op for the LV device type. 624 625 """ 626 pass
627
628 - def Close(self):
629 """Notifies that the device will no longer be used for I/O. 630 631 This is a no-op for the LV device type. 632 633 """ 634 pass
635
636 - def Snapshot(self, size):
637 """Create a snapshot copy of an lvm block device. 638 639 """ 640 snap_name = self._lv_name + ".snap" 641 642 # remove existing snapshot if found 643 snap = LogicalVolume((self._vg_name, snap_name), None, size) 644 _IgnoreError(snap.Remove) 645 646 pvs_info = self.GetPVInfo([self._vg_name]) 647 if not pvs_info: 648 _ThrowError("Can't compute PV info for vg %s", self._vg_name) 649 pvs_info.sort() 650 pvs_info.reverse() 651 free_size, _, _ = pvs_info[0] 652 if free_size < size: 653 _ThrowError("Not enough free space: required %s," 654 " available %s", size, free_size) 655 656 result = utils.RunCmd(["lvcreate", "-L%dm" % size, "-s", 657 "-n%s" % snap_name, self.dev_path]) 658 if result.failed: 659 _ThrowError("command: %s error: %s - %s", 660 result.cmd, result.fail_reason, result.output) 661 662 return snap_name
663
664 - def SetInfo(self, text):
665 """Update metadata with info text. 666 667 """ 668 BlockDev.SetInfo(self, text) 669 670 # Replace invalid characters 671 text = re.sub('^[^A-Za-z0-9_+.]', '_', text) 672 text = re.sub('[^-A-Za-z0-9_+.]', '_', text) 673 674 # Only up to 128 characters are allowed 675 text = text[:128] 676 677 result = utils.RunCmd(["lvchange", "--addtag", text, 678 self.dev_path]) 679 if result.failed: 680 _ThrowError("Command: %s error: %s - %s", result.cmd, result.fail_reason, 681 result.output)
682
683 - def Grow(self, amount):
684 """Grow the logical volume. 685 686 """ 687 if self.pe_size is None or self.stripe_count is None: 688 if not self.Attach(): 689 _ThrowError("Can't attach to LV during Grow()") 690 full_stripe_size = self.pe_size * self.stripe_count 691 rest = amount % full_stripe_size 692 if rest != 0: 693 amount += full_stripe_size - rest 694 # we try multiple algorithms since the 'best' ones might not have 695 # space available in the right place, but later ones might (since 696 # they have less constraints); also note that only recent LVM 697 # supports 'cling' 698 for alloc_policy in "contiguous", "cling", "normal": 699 result = utils.RunCmd(["lvextend", "--alloc", alloc_policy, 700 "-L", "+%dm" % amount, self.dev_path]) 701 if not result.failed: 702 return 703 _ThrowError("Can't grow LV %s: %s", self.dev_path, result.output)
704
705 706 -class DRBD8Status(object):
707 """A DRBD status representation class. 708 709 Note that this doesn't support unconfigured devices (cs:Unconfigured). 710 711 """ 712 UNCONF_RE = re.compile(r"\s*[0-9]+:\s*cs:Unconfigured$") 713 LINE_RE = re.compile(r"\s*[0-9]+:\s*cs:(\S+)\s+(?:st|ro):([^/]+)/(\S+)" 714 "\s+ds:([^/]+)/(\S+)\s+.*$") 715 SYNC_RE = re.compile(r"^.*\ssync'ed:\s*([0-9.]+)%.*" 716 "\sfinish: ([0-9]+):([0-9]+):([0-9]+)\s.*$") 717 718 CS_UNCONFIGURED = "Unconfigured" 719 CS_STANDALONE = "StandAlone" 720 CS_WFCONNECTION = "WFConnection" 721 CS_WFREPORTPARAMS = "WFReportParams" 722 CS_CONNECTED = "Connected" 723 CS_STARTINGSYNCS = "StartingSyncS" 724 CS_STARTINGSYNCT = "StartingSyncT" 725 CS_WFBITMAPS = "WFBitMapS" 726 CS_WFBITMAPT = "WFBitMapT" 727 CS_WFSYNCUUID = "WFSyncUUID" 728 CS_SYNCSOURCE = "SyncSource" 729 CS_SYNCTARGET = "SyncTarget" 730 CS_PAUSEDSYNCS = "PausedSyncS" 731 CS_PAUSEDSYNCT = "PausedSyncT" 732 CSET_SYNC = frozenset([ 733 CS_WFREPORTPARAMS, 734 CS_STARTINGSYNCS, 735 CS_STARTINGSYNCT, 736 CS_WFBITMAPS, 737 CS_WFBITMAPT, 738 CS_WFSYNCUUID, 739 CS_SYNCSOURCE, 740 CS_SYNCTARGET, 741 CS_PAUSEDSYNCS, 742 CS_PAUSEDSYNCT, 743 ]) 744 745 DS_DISKLESS = "Diskless" 746 DS_ATTACHING = "Attaching" # transient state 747 DS_FAILED = "Failed" # transient state, next: diskless 748 DS_NEGOTIATING = "Negotiating" # transient state 749 DS_INCONSISTENT = "Inconsistent" # while syncing or after creation 750 DS_OUTDATED = "Outdated" 751 DS_DUNKNOWN = "DUnknown" # shown for peer disk when not connected 752 DS_CONSISTENT = "Consistent" 753 DS_UPTODATE = "UpToDate" # normal state 754 755 RO_PRIMARY = "Primary" 756 RO_SECONDARY = "Secondary" 757 RO_UNKNOWN = "Unknown" 758
759 - def __init__(self, procline):
760 u = self.UNCONF_RE.match(procline) 761 if u: 762 self.cstatus = self.CS_UNCONFIGURED 763 self.lrole = self.rrole = self.ldisk = self.rdisk = None 764 else: 765 m = self.LINE_RE.match(procline) 766 if not m: 767 raise errors.BlockDeviceError("Can't parse input data '%s'" % procline) 768 self.cstatus = m.group(1) 769 self.lrole = m.group(2) 770 self.rrole = m.group(3) 771 self.ldisk = m.group(4) 772 self.rdisk = m.group(5) 773 774 # end reading of data from the LINE_RE or UNCONF_RE 775 776 self.is_standalone = self.cstatus == self.CS_STANDALONE 777 self.is_wfconn = self.cstatus == self.CS_WFCONNECTION 778 self.is_connected = self.cstatus == self.CS_CONNECTED 779 self.is_primary = self.lrole == self.RO_PRIMARY 780 self.is_secondary = self.lrole == self.RO_SECONDARY 781 self.peer_primary = self.rrole == self.RO_PRIMARY 782 self.peer_secondary = self.rrole == self.RO_SECONDARY 783 self.both_primary = self.is_primary and self.peer_primary 784 self.both_secondary = self.is_secondary and self.peer_secondary 785 786 self.is_diskless = self.ldisk == self.DS_DISKLESS 787 self.is_disk_uptodate = self.ldisk == self.DS_UPTODATE 788 789 self.is_in_resync = self.cstatus in self.CSET_SYNC 790 self.is_in_use = self.cstatus != self.CS_UNCONFIGURED 791 792 m = self.SYNC_RE.match(procline) 793 if m: 794 self.sync_percent = float(m.group(1)) 795 hours = int(m.group(2)) 796 minutes = int(m.group(3)) 797 seconds = int(m.group(4)) 798 self.est_time = hours * 3600 + minutes * 60 + seconds 799 else: 800 # we have (in this if branch) no percent information, but if 801 # we're resyncing we need to 'fake' a sync percent information, 802 # as this is how cmdlib determines if it makes sense to wait for 803 # resyncing or not 804 if self.is_in_resync: 805 self.sync_percent = 0 806 else: 807 self.sync_percent = None 808 self.est_time = None
809
810 811 -class BaseDRBD(BlockDev): # pylint: disable-msg=W0223
812 """Base DRBD class. 813 814 This class contains a few bits of common functionality between the 815 0.7 and 8.x versions of DRBD. 816 817 """ 818 _VERSION_RE = re.compile(r"^version: (\d+)\.(\d+)\.(\d+)(?:\.\d+)?" 819 r" \(api:(\d+)/proto:(\d+)(?:-(\d+))?\)") 820 _VALID_LINE_RE = re.compile("^ *([0-9]+): cs:([^ ]+).*$") 821 _UNUSED_LINE_RE = re.compile("^ *([0-9]+): cs:Unconfigured$") 822 823 _DRBD_MAJOR = 147 824 _ST_UNCONFIGURED = "Unconfigured" 825 _ST_WFCONNECTION = "WFConnection" 826 _ST_CONNECTED = "Connected" 827 828 _STATUS_FILE = "/proc/drbd" 829 _USERMODE_HELPER_FILE = "/sys/module/drbd/parameters/usermode_helper" 830 831 @staticmethod
832 - def _GetProcData(filename=_STATUS_FILE):
833 """Return data from /proc/drbd. 834 835 """ 836 try: 837 data = utils.ReadFile(filename).splitlines() 838 except EnvironmentError, err: 839 if err.errno == errno.ENOENT: 840 _ThrowError("The file %s cannot be opened, check if the module" 841 " is loaded (%s)", filename, str(err)) 842 else: 843 _ThrowError("Can't read the DRBD proc file %s: %s", filename, str(err)) 844 if not data: 845 _ThrowError("Can't read any data from %s", filename) 846 return data
847 848 @classmethod
849 - def _MassageProcData(cls, data):
850 """Transform the output of _GetProdData into a nicer form. 851 852 @return: a dictionary of minor: joined lines from /proc/drbd 853 for that minor 854 855 """ 856 results = {} 857 old_minor = old_line = None 858 for line in data: 859 if not line: # completely empty lines, as can be returned by drbd8.0+ 860 continue 861 lresult = cls._VALID_LINE_RE.match(line) 862 if lresult is not None: 863 if old_minor is not None: 864 results[old_minor] = old_line 865 old_minor = int(lresult.group(1)) 866 old_line = line 867 else: 868 if old_minor is not None: 869 old_line += " " + line.strip() 870 # add last line 871 if old_minor is not None: 872 results[old_minor] = old_line 873 return results
874 875 @classmethod
876 - def _GetVersion(cls, proc_data):
877 """Return the DRBD version. 878 879 This will return a dict with keys: 880 - k_major 881 - k_minor 882 - k_point 883 - api 884 - proto 885 - proto2 (only on drbd > 8.2.X) 886 887 """ 888 first_line = proc_data[0].strip() 889 version = cls._VERSION_RE.match(first_line) 890 if not version: 891 raise errors.BlockDeviceError("Can't parse DRBD version from '%s'" % 892 first_line) 893 894 values = version.groups() 895 retval = {'k_major': int(values[0]), 896 'k_minor': int(values[1]), 897 'k_point': int(values[2]), 898 'api': int(values[3]), 899 'proto': int(values[4]), 900 } 901 if values[5] is not None: 902 retval['proto2'] = values[5] 903 904 return retval
905 906 @staticmethod
907 - def GetUsermodeHelper(filename=_USERMODE_HELPER_FILE):
908 """Returns DRBD usermode_helper currently set. 909 910 """ 911 try: 912 helper = utils.ReadFile(filename).splitlines()[0] 913 except EnvironmentError, err: 914 if err.errno == errno.ENOENT: 915 _ThrowError("The file %s cannot be opened, check if the module" 916 " is loaded (%s)", filename, str(err)) 917 else: 918 _ThrowError("Can't read DRBD helper file %s: %s", filename, str(err)) 919 if not helper: 920 _ThrowError("Can't read any data from %s", filename) 921 return helper
922 923 @staticmethod
924 - def _DevPath(minor):
925 """Return the path to a drbd device for a given minor. 926 927 """ 928 return "/dev/drbd%d" % minor
929 930 @classmethod
931 - def GetUsedDevs(cls):
932 """Compute the list of used DRBD devices. 933 934 """ 935 data = cls._GetProcData() 936 937 used_devs = {} 938 for line in data: 939 match = cls._VALID_LINE_RE.match(line) 940 if not match: 941 continue 942 minor = int(match.group(1)) 943 state = match.group(2) 944 if state == cls._ST_UNCONFIGURED: 945 continue 946 used_devs[minor] = state, line 947 948 return used_devs
949
950 - def _SetFromMinor(self, minor):
951 """Set our parameters based on the given minor. 952 953 This sets our minor variable and our dev_path. 954 955 """ 956 if minor is None: 957 self.minor = self.dev_path = None 958 self.attached = False 959 else: 960 self.minor = minor 961 self.dev_path = self._DevPath(minor) 962 self.attached = True
963 964 @staticmethod
965 - def _CheckMetaSize(meta_device):
966 """Check if the given meta device looks like a valid one. 967 968 This currently only check the size, which must be around 969 128MiB. 970 971 """ 972 result = utils.RunCmd(["blockdev", "--getsize", meta_device]) 973 if result.failed: 974 _ThrowError("Failed to get device size: %s - %s", 975 result.fail_reason, result.output) 976 try: 977 sectors = int(result.stdout) 978 except (TypeError, ValueError): 979 _ThrowError("Invalid output from blockdev: '%s'", result.stdout) 980 num_bytes = sectors * 512 981 if num_bytes < 128 * 1024 * 1024: # less than 128MiB 982 _ThrowError("Meta device too small (%.2fMib)", (num_bytes / 1024 / 1024)) 983 # the maximum *valid* size of the meta device when living on top 984 # of LVM is hard to compute: it depends on the number of stripes 985 # and the PE size; e.g. a 2-stripe, 64MB PE will result in a 128MB 986 # (normal size), but an eight-stripe 128MB PE will result in a 1GB 987 # size meta device; as such, we restrict it to 1GB (a little bit 988 # too generous, but making assumptions about PE size is hard) 989 if num_bytes > 1024 * 1024 * 1024: 990 _ThrowError("Meta device too big (%.2fMiB)", (num_bytes / 1024 / 1024))
991
992 - def Rename(self, new_id):
993 """Rename a device. 994 995 This is not supported for drbd devices. 996 997 """ 998 raise errors.ProgrammerError("Can't rename a drbd device")
999
1000 1001 -class DRBD8(BaseDRBD):
1002 """DRBD v8.x block device. 1003 1004 This implements the local host part of the DRBD device, i.e. it 1005 doesn't do anything to the supposed peer. If you need a fully 1006 connected DRBD pair, you need to use this class on both hosts. 1007 1008 The unique_id for the drbd device is the (local_ip, local_port, 1009 remote_ip, remote_port) tuple, and it must have two children: the 1010 data device and the meta_device. The meta device is checked for 1011 valid size and is zeroed on create. 1012 1013 """ 1014 _MAX_MINORS = 255 1015 _PARSE_SHOW = None 1016 1017 # timeout constants 1018 _NET_RECONFIG_TIMEOUT = 60 1019
1020 - def __init__(self, unique_id, children, size):
1021 if children and children.count(None) > 0: 1022 children = [] 1023 if len(children) not in (0, 2): 1024 raise ValueError("Invalid configuration data %s" % str(children)) 1025 if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 6: 1026 raise ValueError("Invalid configuration data %s" % str(unique_id)) 1027 (self._lhost, self._lport, 1028 self._rhost, self._rport, 1029 self._aminor, self._secret) = unique_id 1030 if children: 1031 if not _CanReadDevice(children[1].dev_path): 1032 logging.info("drbd%s: Ignoring unreadable meta device", self._aminor) 1033 children = [] 1034 super(DRBD8, self).__init__(unique_id, children, size) 1035 self.major = self._DRBD_MAJOR 1036 version = self._GetVersion(self._GetProcData()) 1037 if version['k_major'] != 8 : 1038 _ThrowError("Mismatch in DRBD kernel version and requested ganeti" 1039 " usage: kernel is %s.%s, ganeti wants 8.x", 1040 version['k_major'], version['k_minor']) 1041 1042 if (self._lhost is not None and self._lhost == self._rhost and 1043 self._lport == self._rport): 1044 raise ValueError("Invalid configuration data, same local/remote %s" % 1045 (unique_id,)) 1046 self.Attach()
1047 1048 @classmethod
1049 - def _InitMeta(cls, minor, dev_path):
1050 """Initialize a meta device. 1051 1052 This will not work if the given minor is in use. 1053 1054 """ 1055 result = utils.RunCmd(["drbdmeta", "--force", cls._DevPath(minor), 1056 "v08", dev_path, "0", "create-md"]) 1057 if result.failed: 1058 _ThrowError("Can't initialize meta device: %s", result.output)
1059 1060 @classmethod
1061 - def _FindUnusedMinor(cls):
1062 """Find an unused DRBD device. 1063 1064 This is specific to 8.x as the minors are allocated dynamically, 1065 so non-existing numbers up to a max minor count are actually free. 1066 1067 """ 1068 data = cls._GetProcData() 1069 1070 highest = None 1071 for line in data: 1072 match = cls._UNUSED_LINE_RE.match(line) 1073 if match: 1074 return int(match.group(1)) 1075 match = cls._VALID_LINE_RE.match(line) 1076 if match: 1077 minor = int(match.group(1)) 1078 highest = max(highest, minor) 1079 if highest is None: # there are no minors in use at all 1080 return 0 1081 if highest >= cls._MAX_MINORS: 1082 logging.error("Error: no free drbd minors!") 1083 raise errors.BlockDeviceError("Can't find a free DRBD minor") 1084 return highest + 1
1085 1086 @classmethod
1087 - def _GetShowParser(cls):
1088 """Return a parser for `drbd show` output. 1089 1090 This will either create or return an already-create parser for the 1091 output of the command `drbd show`. 1092 1093 """ 1094 if cls._PARSE_SHOW is not None: 1095 return cls._PARSE_SHOW 1096 1097 # pyparsing setup 1098 lbrace = pyp.Literal("{").suppress() 1099 rbrace = pyp.Literal("}").suppress() 1100 lbracket = pyp.Literal("[").suppress() 1101 rbracket = pyp.Literal("]").suppress() 1102 semi = pyp.Literal(";").suppress() 1103 colon = pyp.Literal(":").suppress() 1104 # this also converts the value to an int 1105 number = pyp.Word(pyp.nums).setParseAction(lambda s, l, t: int(t[0])) 1106 1107 comment = pyp.Literal ("#") + pyp.Optional(pyp.restOfLine) 1108 defa = pyp.Literal("_is_default").suppress() 1109 dbl_quote = pyp.Literal('"').suppress() 1110 1111 keyword = pyp.Word(pyp.alphanums + '-') 1112 1113 # value types 1114 value = pyp.Word(pyp.alphanums + '_-/.:') 1115 quoted = dbl_quote + pyp.CharsNotIn('"') + dbl_quote 1116 ipv4_addr = (pyp.Optional(pyp.Literal("ipv4")).suppress() + 1117 pyp.Word(pyp.nums + ".") + colon + number) 1118 ipv6_addr = (pyp.Optional(pyp.Literal("ipv6")).suppress() + 1119 pyp.Optional(lbracket) + pyp.Word(pyp.hexnums + ":") + 1120 pyp.Optional(rbracket) + colon + number) 1121 # meta device, extended syntax 1122 meta_value = ((value ^ quoted) + lbracket + number + rbracket) 1123 # device name, extended syntax 1124 device_value = pyp.Literal("minor").suppress() + number 1125 1126 # a statement 1127 stmt = (~rbrace + keyword + ~lbrace + 1128 pyp.Optional(ipv4_addr ^ ipv6_addr ^ value ^ quoted ^ meta_value ^ 1129 device_value) + 1130 pyp.Optional(defa) + semi + 1131 pyp.Optional(pyp.restOfLine).suppress()) 1132 1133 # an entire section 1134 section_name = pyp.Word(pyp.alphas + '_') 1135 section = section_name + lbrace + pyp.ZeroOrMore(pyp.Group(stmt)) + rbrace 1136 1137 bnf = pyp.ZeroOrMore(pyp.Group(section ^ stmt)) 1138 bnf.ignore(comment) 1139 1140 cls._PARSE_SHOW = bnf 1141 1142 return bnf
1143 1144 @classmethod
1145 - def _GetShowData(cls, minor):
1146 """Return the `drbdsetup show` data for a minor. 1147 1148 """ 1149 result = utils.RunCmd(["drbdsetup", cls._DevPath(minor), "show"]) 1150 if result.failed: 1151 logging.error("Can't display the drbd config: %s - %s", 1152 result.fail_reason, result.output) 1153 return None 1154 return result.stdout
1155 1156 @classmethod
1157 - def _GetDevInfo(cls, out):
1158 """Parse details about a given DRBD minor. 1159 1160 This return, if available, the local backing device (as a path) 1161 and the local and remote (ip, port) information from a string 1162 containing the output of the `drbdsetup show` command as returned 1163 by _GetShowData. 1164 1165 """ 1166 data = {} 1167 if not out: 1168 return data 1169 1170 bnf = cls._GetShowParser() 1171 # run pyparse 1172 1173 try: 1174 results = bnf.parseString(out) 1175 except pyp.ParseException, err: 1176 _ThrowError("Can't parse drbdsetup show output: %s", str(err)) 1177 1178 # and massage the results into our desired format 1179 for section in results: 1180 sname = section[0] 1181 if sname == "_this_host": 1182 for lst in section[1:]: 1183 if lst[0] == "disk": 1184 data["local_dev"] = lst[1] 1185 elif lst[0] == "meta-disk": 1186 data["meta_dev"] = lst[1] 1187 data["meta_index"] = lst[2] 1188 elif lst[0] == "address": 1189 data["local_addr"] = tuple(lst[1:]) 1190 elif sname == "_remote_host": 1191 for lst in section[1:]: 1192 if lst[0] == "address": 1193 data["remote_addr"] = tuple(lst[1:]) 1194 return data
1195
1196 - def _MatchesLocal(self, info):
1197 """Test if our local config matches with an existing device. 1198 1199 The parameter should be as returned from `_GetDevInfo()`. This 1200 method tests if our local backing device is the same as the one in 1201 the info parameter, in effect testing if we look like the given 1202 device. 1203 1204 """ 1205 if self._children: 1206 backend, meta = self._children 1207 else: 1208 backend = meta = None 1209 1210 if backend is not None: 1211 retval = ("local_dev" in info and info["local_dev"] == backend.dev_path) 1212 else: 1213 retval = ("local_dev" not in info) 1214 1215 if meta is not None: 1216 retval = retval and ("meta_dev" in info and 1217 info["meta_dev"] == meta.dev_path) 1218 retval = retval and ("meta_index" in info and 1219 info["meta_index"] == 0) 1220 else: 1221 retval = retval and ("meta_dev" not in info and 1222 "meta_index" not in info) 1223 return retval
1224
1225 - def _MatchesNet(self, info):
1226 """Test if our network config matches with an existing device. 1227 1228 The parameter should be as returned from `_GetDevInfo()`. This 1229 method tests if our network configuration is the same as the one 1230 in the info parameter, in effect testing if we look like the given 1231 device. 1232 1233 """ 1234 if (((self._lhost is None and not ("local_addr" in info)) and 1235 (self._rhost is None and not ("remote_addr" in info)))): 1236 return True 1237 1238 if self._lhost is None: 1239 return False 1240 1241 if not ("local_addr" in info and 1242 "remote_addr" in info): 1243 return False 1244 1245 retval = (info["local_addr"] == (self._lhost, self._lport)) 1246 retval = (retval and 1247 info["remote_addr"] == (self._rhost, self._rport)) 1248 return retval
1249 1250 @classmethod
1251 - def _AssembleLocal(cls, minor, backend, meta, size):
1252 """Configure the local part of a DRBD device. 1253 1254 """ 1255 args = ["drbdsetup", cls._DevPath(minor), "disk", 1256 backend, meta, "0", 1257 "-e", "detach", 1258 "--create-device"] 1259 if size: 1260 args.extend(["-d", "%sm" % size]) 1261 if not constants.DRBD_BARRIERS: # disable barriers, if configured so 1262 version = cls._GetVersion(cls._GetProcData()) 1263 # various DRBD versions support different disk barrier options; 1264 # what we aim here is to revert back to the 'drain' method of 1265 # disk flushes and to disable metadata barriers, in effect going 1266 # back to pre-8.0.7 behaviour 1267 vmaj = version['k_major'] 1268 vmin = version['k_minor'] 1269 vrel = version['k_point'] 1270 assert vmaj == 8 1271 if vmin == 0: # 8.0.x 1272 if vrel >= 12: 1273 args.extend(['-i', '-m']) 1274 elif vmin == 2: # 8.2.x 1275 if vrel >= 7: 1276 args.extend(['-i', '-m']) 1277 elif vmaj >= 3: # 8.3.x or newer 1278 args.extend(['-i', '-a', 'm']) 1279 result = utils.RunCmd(args) 1280 if result.failed: 1281 _ThrowError("drbd%d: can't attach local disk: %s", minor, result.output)
1282 1283 @classmethod
1284 - def _AssembleNet(cls, minor, net_info, protocol, 1285 dual_pri=False, hmac=None, secret=None):
1286 """Configure the network part of the device. 1287 1288 """ 1289 lhost, lport, rhost, rport = net_info 1290 if None in net_info: 1291 # we don't want network connection and actually want to make 1292 # sure its shutdown 1293 cls._ShutdownNet(minor) 1294 return 1295 1296 # Workaround for a race condition. When DRBD is doing its dance to 1297 # establish a connection with its peer, it also sends the 1298 # synchronization speed over the wire. In some cases setting the 1299 # sync speed only after setting up both sides can race with DRBD 1300 # connecting, hence we set it here before telling DRBD anything 1301 # about its peer. 1302 cls._SetMinorSyncSpeed(minor, constants.SYNC_SPEED) 1303 1304 if netutils.IsValidIP6(lhost): 1305 if not netutils.IsValidIP6(rhost): 1306 _ThrowError("drbd%d: can't connect ip %s to ip %s" % 1307 (minor, lhost, rhost)) 1308 family = "ipv6" 1309 elif netutils.IsValidIP4(lhost): 1310 if not netutils.IsValidIP4(rhost): 1311 _ThrowError("drbd%d: can't connect ip %s to ip %s" % 1312 (minor, lhost, rhost)) 1313 family = "ipv4" 1314 else: 1315 _ThrowError("drbd%d: Invalid ip %s" % (minor, lhost)) 1316 1317 args = ["drbdsetup", cls._DevPath(minor), "net", 1318 "%s:%s:%s" % (family, lhost, lport), 1319 "%s:%s:%s" % (family, rhost, rport), protocol, 1320 "-A", "discard-zero-changes", 1321 "-B", "consensus", 1322 "--create-device", 1323 ] 1324 if dual_pri: 1325 args.append("-m") 1326 if hmac and secret: 1327 args.extend(["-a", hmac, "-x", secret]) 1328 result = utils.RunCmd(args) 1329 if result.failed: 1330 _ThrowError("drbd%d: can't setup network: %s - %s", 1331 minor, result.fail_reason, result.output) 1332 1333 def _CheckNetworkConfig(): 1334 info = cls._GetDevInfo(cls._GetShowData(minor)) 1335 if not "local_addr" in info or not "remote_addr" in info: 1336 raise utils.RetryAgain() 1337 1338 if (info["local_addr"] != (lhost, lport) or 1339 info["remote_addr"] != (rhost, rport)): 1340 raise utils.RetryAgain()
1341 1342 try: 1343 utils.Retry(_CheckNetworkConfig, 1.0, 10.0) 1344 except utils.RetryTimeout: 1345 _ThrowError("drbd%d: timeout while configuring network", minor)
1346
1347 - def AddChildren(self, devices):
1348 """Add a disk to the DRBD device. 1349 1350 """ 1351 if self.minor is None: 1352 _ThrowError("drbd%d: can't attach to dbrd8 during AddChildren", 1353 self._aminor) 1354 if len(devices) != 2: 1355 _ThrowError("drbd%d: need two devices for AddChildren", self.minor) 1356 info = self._GetDevInfo(self._GetShowData(self.minor)) 1357 if "local_dev" in info: 1358 _ThrowError("drbd%d: already attached to a local disk", self.minor) 1359 backend, meta = devices 1360 if backend.dev_path is None or meta.dev_path is None: 1361 _ThrowError("drbd%d: children not ready during AddChildren", self.minor) 1362 backend.Open() 1363 meta.Open() 1364 self._CheckMetaSize(meta.dev_path) 1365 self._InitMeta(self._FindUnusedMinor(), meta.dev_path) 1366 1367 self._AssembleLocal(self.minor, backend.dev_path, meta.dev_path, self.size) 1368 self._children = devices
1369
1370 - def RemoveChildren(self, devices):
1371 """Detach the drbd device from local storage. 1372 1373 """ 1374 if self.minor is None: 1375 _ThrowError("drbd%d: can't attach to drbd8 during RemoveChildren", 1376 self._aminor) 1377 # early return if we don't actually have backing storage 1378 info = self._GetDevInfo(self._GetShowData(self.minor)) 1379 if "local_dev" not in info: 1380 return 1381 if len(self._children) != 2: 1382 _ThrowError("drbd%d: we don't have two children: %s", self.minor, 1383 self._children) 1384 if self._children.count(None) == 2: # we don't actually have children :) 1385 logging.warning("drbd%d: requested detach while detached", self.minor) 1386 return 1387 if len(devices) != 2: 1388 _ThrowError("drbd%d: we need two children in RemoveChildren", self.minor) 1389 for child, dev in zip(self._children, devices): 1390 if dev != child.dev_path: 1391 _ThrowError("drbd%d: mismatch in local storage (%s != %s) in" 1392 " RemoveChildren", self.minor, dev, child.dev_path) 1393 1394 self._ShutdownLocal(self.minor) 1395 self._children = []
1396 1397 @classmethod
1398 - def _SetMinorSyncSpeed(cls, minor, kbytes):
1399 """Set the speed of the DRBD syncer. 1400 1401 This is the low-level implementation. 1402 1403 @type minor: int 1404 @param minor: the drbd minor whose settings we change 1405 @type kbytes: int 1406 @param kbytes: the speed in kbytes/second 1407 @rtype: boolean 1408 @return: the success of the operation 1409 1410 """ 1411 result = utils.RunCmd(["drbdsetup", cls._DevPath(minor), "syncer", 1412 "-r", "%d" % kbytes, "--create-device"]) 1413 if result.failed: 1414 logging.error("Can't change syncer rate: %s - %s", 1415 result.fail_reason, result.output) 1416 return not result.failed
1417
1418 - def SetSyncSpeed(self, kbytes):
1419 """Set the speed of the DRBD syncer. 1420 1421 @type kbytes: int 1422 @param kbytes: the speed in kbytes/second 1423 @rtype: boolean 1424 @return: the success of the operation 1425 1426 """ 1427 if self.minor is None: 1428 logging.info("Not attached during SetSyncSpeed") 1429 return False 1430 children_result = super(DRBD8, self).SetSyncSpeed(kbytes) 1431 return self._SetMinorSyncSpeed(self.minor, kbytes) and children_result
1432
1433 - def GetProcStatus(self):
1434 """Return device data from /proc. 1435 1436 """ 1437 if self.minor is None: 1438 _ThrowError("drbd%d: GetStats() called while not attached", self._aminor) 1439 proc_info = self._MassageProcData(self._GetProcData()) 1440 if self.minor not in proc_info: 1441 _ThrowError("drbd%d: can't find myself in /proc", self.minor) 1442 return DRBD8Status(proc_info[self.minor])
1443
1444 - def GetSyncStatus(self):
1445 """Returns the sync status of the device. 1446 1447 1448 If sync_percent is None, it means all is ok 1449 If estimated_time is None, it means we can't estimate 1450 the time needed, otherwise it's the time left in seconds. 1451 1452 1453 We set the is_degraded parameter to True on two conditions: 1454 network not connected or local disk missing. 1455 1456 We compute the ldisk parameter based on whether we have a local 1457 disk or not. 1458 1459 @rtype: objects.BlockDevStatus 1460 1461 """ 1462 if self.minor is None and not self.Attach(): 1463 _ThrowError("drbd%d: can't Attach() in GetSyncStatus", self._aminor) 1464 1465 stats = self.GetProcStatus() 1466 is_degraded = not stats.is_connected or not stats.is_disk_uptodate 1467 1468 if stats.is_disk_uptodate: 1469 ldisk_status = constants.LDS_OKAY 1470 elif stats.is_diskless: 1471 ldisk_status = constants.LDS_FAULTY 1472 else: 1473 ldisk_status = constants.LDS_UNKNOWN 1474 1475 return objects.BlockDevStatus(dev_path=self.dev_path, 1476 major=self.major, 1477 minor=self.minor, 1478 sync_percent=stats.sync_percent, 1479 estimated_time=stats.est_time, 1480 is_degraded=is_degraded, 1481 ldisk_status=ldisk_status)
1482
1483 - def Open(self, force=False):
1484 """Make the local state primary. 1485 1486 If the 'force' parameter is given, the '-o' option is passed to 1487 drbdsetup. Since this is a potentially dangerous operation, the 1488 force flag should be only given after creation, when it actually 1489 is mandatory. 1490 1491 """ 1492 if self.minor is None and not self.Attach(): 1493 logging.error("DRBD cannot attach to a device during open") 1494 return False 1495 cmd = ["drbdsetup", self.dev_path, "primary"] 1496 if force: 1497 cmd.append("-o") 1498 result = utils.RunCmd(cmd) 1499 if result.failed: 1500 _ThrowError("drbd%d: can't make drbd device primary: %s", self.minor, 1501 result.output)
1502
1503 - def Close(self):
1504 """Make the local state secondary. 1505 1506 This will, of course, fail if the device is in use. 1507 1508 """ 1509 if self.minor is None and not self.Attach(): 1510 _ThrowError("drbd%d: can't Attach() in Close()", self._aminor) 1511 result = utils.RunCmd(["drbdsetup", self.dev_path, "secondary"]) 1512 if result.failed: 1513 _ThrowError("drbd%d: can't switch drbd device to secondary: %s", 1514 self.minor, result.output)
1515
1516 - def DisconnectNet(self):
1517 """Removes network configuration. 1518 1519 This method shutdowns the network side of the device. 1520 1521 The method will wait up to a hardcoded timeout for the device to 1522 go into standalone after the 'disconnect' command before 1523 re-configuring it, as sometimes it takes a while for the 1524 disconnect to actually propagate and thus we might issue a 'net' 1525 command while the device is still connected. If the device will 1526 still be attached to the network and we time out, we raise an 1527 exception. 1528 1529 """ 1530 if self.minor is None: 1531 _ThrowError("drbd%d: disk not attached in re-attach net", self._aminor) 1532 1533 if None in (self._lhost, self._lport, self._rhost, self._rport): 1534 _ThrowError("drbd%d: DRBD disk missing network info in" 1535 " DisconnectNet()", self.minor) 1536 1537 class _DisconnectStatus: 1538 def __init__(self, ever_disconnected): 1539 self.ever_disconnected = ever_disconnected
1540 1541 dstatus = _DisconnectStatus(_IgnoreError(self._ShutdownNet, self.minor)) 1542 1543 def _WaitForDisconnect(): 1544 if self.GetProcStatus().is_standalone: 1545 return 1546 1547 # retry the disconnect, it seems possible that due to a well-time 1548 # disconnect on the peer, my disconnect command might be ignored and 1549 # forgotten 1550 dstatus.ever_disconnected = \ 1551 _IgnoreError(self._ShutdownNet, self.minor) or dstatus.ever_disconnected 1552 1553 raise utils.RetryAgain() 1554 1555 # Keep start time 1556 start_time = time.time() 1557 1558 try: 1559 # Start delay at 100 milliseconds and grow up to 2 seconds 1560 utils.Retry(_WaitForDisconnect, (0.1, 1.5, 2.0), 1561 self._NET_RECONFIG_TIMEOUT) 1562 except utils.RetryTimeout: 1563 if dstatus.ever_disconnected: 1564 msg = ("drbd%d: device did not react to the" 1565 " 'disconnect' command in a timely manner") 1566 else: 1567 msg = "drbd%d: can't shutdown network, even after multiple retries" 1568 1569 _ThrowError(msg, self.minor) 1570 1571 reconfig_time = time.time() - start_time 1572 if reconfig_time > (self._NET_RECONFIG_TIMEOUT * 0.25): 1573 logging.info("drbd%d: DisconnectNet: detach took %.3f seconds", 1574 self.minor, reconfig_time) 1575
1576 - def AttachNet(self, multimaster):
1577 """Reconnects the network. 1578 1579 This method connects the network side of the device with a 1580 specified multi-master flag. The device needs to be 'Standalone' 1581 but have valid network configuration data. 1582 1583 Args: 1584 - multimaster: init the network in dual-primary mode 1585 1586 """ 1587 if self.minor is None: 1588 _ThrowError("drbd%d: device not attached in AttachNet", self._aminor) 1589 1590 if None in (self._lhost, self._lport, self._rhost, self._rport): 1591 _ThrowError("drbd%d: missing network info in AttachNet()", self.minor) 1592 1593 status = self.GetProcStatus() 1594 1595 if not status.is_standalone: 1596 _ThrowError("drbd%d: device is not standalone in AttachNet", self.minor) 1597 1598 self._AssembleNet(self.minor, 1599 (self._lhost, self._lport, self._rhost, self._rport), 1600 constants.DRBD_NET_PROTOCOL, dual_pri=multimaster, 1601 hmac=constants.DRBD_HMAC_ALG, secret=self._secret)
1602
1603 - def Attach(self):
1604 """Check if our minor is configured. 1605 1606 This doesn't do any device configurations - it only checks if the 1607 minor is in a state different from Unconfigured. 1608 1609 Note that this function will not change the state of the system in 1610 any way (except in case of side-effects caused by reading from 1611 /proc). 1612 1613 """ 1614 used_devs = self.GetUsedDevs() 1615 if self._aminor in used_devs: 1616 minor = self._aminor 1617 else: 1618 minor = None 1619 1620 self._SetFromMinor(minor) 1621 return minor is not None
1622
1623 - def Assemble(self):
1624 """Assemble the drbd. 1625 1626 Method: 1627 - if we have a configured device, we try to ensure that it matches 1628 our config 1629 - if not, we create it from zero 1630 1631 """ 1632 super(DRBD8, self).Assemble() 1633 1634 self.Attach() 1635 if self.minor is None: 1636 # local device completely unconfigured 1637 self._FastAssemble() 1638 else: 1639 # we have to recheck the local and network status and try to fix 1640 # the device 1641 self._SlowAssemble()
1642
1643 - def _SlowAssemble(self):
1644 """Assembles the DRBD device from a (partially) configured device. 1645 1646 In case of partially attached (local device matches but no network 1647 setup), we perform the network attach. If successful, we re-test 1648 the attach if can return success. 1649 1650 """ 1651 # TODO: Rewrite to not use a for loop just because there is 'break' 1652 # pylint: disable-msg=W0631 1653 net_data = (self._lhost, self._lport, self._rhost, self._rport) 1654 for minor in (self._aminor,): 1655 info = self._GetDevInfo(self._GetShowData(minor)) 1656 match_l = self._MatchesLocal(info) 1657 match_r = self._MatchesNet(info) 1658 1659 if match_l and match_r: 1660 # everything matches 1661 break 1662 1663 if match_l and not match_r and "local_addr" not in info: 1664 # disk matches, but not attached to network, attach and recheck 1665 self._AssembleNet(minor, net_data, constants.DRBD_NET_PROTOCOL, 1666 hmac=constants.DRBD_HMAC_ALG, secret=self._secret) 1667 if self._MatchesNet(self._GetDevInfo(self._GetShowData(minor))): 1668 break 1669 else: 1670 _ThrowError("drbd%d: network attach successful, but 'drbdsetup" 1671 " show' disagrees", minor) 1672 1673 if match_r and "local_dev" not in info: 1674 # no local disk, but network attached and it matches 1675 self._AssembleLocal(minor, self._children[0].dev_path, 1676 self._children[1].dev_path, self.size) 1677 if self._MatchesNet(self._GetDevInfo(self._GetShowData(minor))): 1678 break 1679 else: 1680 _ThrowError("drbd%d: disk attach successful, but 'drbdsetup" 1681 " show' disagrees", minor) 1682 1683 # this case must be considered only if we actually have local 1684 # storage, i.e. not in diskless mode, because all diskless 1685 # devices are equal from the point of view of local 1686 # configuration 1687 if (match_l and "local_dev" in info and 1688 not match_r and "local_addr" in info): 1689 # strange case - the device network part points to somewhere 1690 # else, even though its local storage is ours; as we own the 1691 # drbd space, we try to disconnect from the remote peer and 1692 # reconnect to our correct one 1693 try: 1694 self._ShutdownNet(minor) 1695 except errors.BlockDeviceError, err: 1696 _ThrowError("drbd%d: device has correct local storage, wrong" 1697 " remote peer and is unable to disconnect in order" 1698 " to attach to the correct peer: %s", minor, str(err)) 1699 # note: _AssembleNet also handles the case when we don't want 1700 # local storage (i.e. one or more of the _[lr](host|port) is 1701 # None) 1702 self._AssembleNet(minor, net_data, constants.DRBD_NET_PROTOCOL, 1703 hmac=constants.DRBD_HMAC_ALG, secret=self._secret) 1704 if self._MatchesNet(self._GetDevInfo(self._GetShowData(minor))): 1705 break 1706 else: 1707 _ThrowError("drbd%d: network attach successful, but 'drbdsetup" 1708 " show' disagrees", minor) 1709 1710 else: 1711 minor = None 1712 1713 self._SetFromMinor(minor) 1714 if minor is None: 1715 _ThrowError("drbd%d: cannot activate, unknown or unhandled reason", 1716 self._aminor)
1717
1718 - def _FastAssemble(self):
1719 """Assemble the drbd device from zero. 1720 1721 This is run when in Assemble we detect our minor is unused. 1722 1723 """ 1724 minor = self._aminor 1725 if self._children and self._children[0] and self._children[1]: 1726 self._AssembleLocal(minor, self._children[0].dev_path, 1727 self._children[1].dev_path, self.size) 1728 if self._lhost and self._lport and self._rhost and self._rport: 1729 self._AssembleNet(minor, 1730 (self._lhost, self._lport, self._rhost, self._rport), 1731 constants.DRBD_NET_PROTOCOL, 1732 hmac=constants.DRBD_HMAC_ALG, secret=self._secret) 1733 self._SetFromMinor(minor)
1734 1735 @classmethod
1736 - def _ShutdownLocal(cls, minor):
1737 """Detach from the local device. 1738 1739 I/Os will continue to be served from the remote device. If we 1740 don't have a remote device, this operation will fail. 1741 1742 """ 1743 result = utils.RunCmd(["drbdsetup", cls._DevPath(minor), "detach"]) 1744 if result.failed: 1745 _ThrowError("drbd%d: can't detach local disk: %s", minor, result.output)
1746 1747 @classmethod
1748 - def _ShutdownNet(cls, minor):
1749 """Disconnect from the remote peer. 1750 1751 This fails if we don't have a local device. 1752 1753 """ 1754 result = utils.RunCmd(["drbdsetup", cls._DevPath(minor), "disconnect"]) 1755 if result.failed: 1756 _ThrowError("drbd%d: can't shutdown network: %s", minor, result.output)
1757 1758 @classmethod
1759 - def _ShutdownAll(cls, minor):
1760 """Deactivate the device. 1761 1762 This will, of course, fail if the device is in use. 1763 1764 """ 1765 result = utils.RunCmd(["drbdsetup", cls._DevPath(minor), "down"]) 1766 if result.failed: 1767 _ThrowError("drbd%d: can't shutdown drbd device: %s", 1768 minor, result.output)
1769
1770 - def Shutdown(self):
1771 """Shutdown the DRBD device. 1772 1773 """ 1774 if self.minor is None and not self.Attach(): 1775 logging.info("drbd%d: not attached during Shutdown()", self._aminor) 1776 return 1777 minor = self.minor 1778 self.minor = None 1779 self.dev_path = None 1780 self._ShutdownAll(minor)
1781
1782 - def Remove(self):
1783 """Stub remove for DRBD devices. 1784 1785 """ 1786 self.Shutdown()
1787 1788 @classmethod
1789 - def Create(cls, unique_id, children, size):
1790 """Create a new DRBD8 device. 1791 1792 Since DRBD devices are not created per se, just assembled, this 1793 function only initializes the metadata. 1794 1795 """ 1796 if len(children) != 2: 1797 raise errors.ProgrammerError("Invalid setup for the drbd device") 1798 # check that the minor is unused 1799 aminor = unique_id[4] 1800 proc_info = cls._MassageProcData(cls._GetProcData()) 1801 if aminor in proc_info: 1802 status = DRBD8Status(proc_info[aminor]) 1803 in_use = status.is_in_use 1804 else: 1805 in_use = False 1806 if in_use: 1807 _ThrowError("drbd%d: minor is already in use at Create() time", aminor) 1808 meta = children[1] 1809 meta.Assemble() 1810 if not meta.Attach(): 1811 _ThrowError("drbd%d: can't attach to meta device '%s'", 1812 aminor, meta) 1813 cls._CheckMetaSize(meta.dev_path) 1814 cls._InitMeta(aminor, meta.dev_path) 1815 return cls(unique_id, children, size)
1816
1817 - def Grow(self, amount):
1818 """Resize the DRBD device and its backing storage. 1819 1820 """ 1821 if self.minor is None: 1822 _ThrowError("drbd%d: Grow called while not attached", self._aminor) 1823 if len(self._children) != 2 or None in self._children: 1824 _ThrowError("drbd%d: cannot grow diskless device", self.minor) 1825 self._children[0].Grow(amount) 1826 result = utils.RunCmd(["drbdsetup", self.dev_path, "resize", "-s", 1827 "%dm" % (self.size + amount)]) 1828 if result.failed: 1829 _ThrowError("drbd%d: resize failed: %s", self.minor, result.output)
1830
1831 1832 -class FileStorage(BlockDev):
1833 """File device. 1834 1835 This class represents the a file storage backend device. 1836 1837 The unique_id for the file device is a (file_driver, file_path) tuple. 1838 1839 """
1840 - def __init__(self, unique_id, children, size):
1841 """Initalizes a file device backend. 1842 1843 """ 1844 if children: 1845 raise errors.BlockDeviceError("Invalid setup for file device") 1846 super(FileStorage, self).__init__(unique_id, children, size) 1847 if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2: 1848 raise ValueError("Invalid configuration data %s" % str(unique_id)) 1849 self.driver = unique_id[0] 1850 self.dev_path = unique_id[1] 1851 self.Attach()
1852
1853 - def Assemble(self):
1854 """Assemble the device. 1855 1856 Checks whether the file device exists, raises BlockDeviceError otherwise. 1857 1858 """ 1859 if not os.path.exists(self.dev_path): 1860 _ThrowError("File device '%s' does not exist" % self.dev_path)
1861
1862 - def Shutdown(self):
1863 """Shutdown the device. 1864 1865 This is a no-op for the file type, as we don't deactivate 1866 the file on shutdown. 1867 1868 """ 1869 pass
1870
1871 - def Open(self, force=False):
1872 """Make the device ready for I/O. 1873 1874 This is a no-op for the file type. 1875 1876 """ 1877 pass
1878
1879 - def Close(self):
1880 """Notifies that the device will no longer be used for I/O. 1881 1882 This is a no-op for the file type. 1883 1884 """ 1885 pass
1886
1887 - def Remove(self):
1888 """Remove the file backing the block device. 1889 1890 @rtype: boolean 1891 @return: True if the removal was successful 1892 1893 """ 1894 try: 1895 os.remove(self.dev_path) 1896 except OSError, err: 1897 if err.errno != errno.ENOENT: 1898 _ThrowError("Can't remove file '%s': %s", self.dev_path, err)
1899
1900 - def Rename(self, new_id):
1901 """Renames the file. 1902 1903 """ 1904 # TODO: implement rename for file-based storage 1905 _ThrowError("Rename is not supported for file-based storage")
1906
1907 - def Grow(self, amount):
1908 """Grow the file 1909 1910 @param amount: the amount (in mebibytes) to grow with 1911 1912 """ 1913 # Check that the file exists 1914 self.Assemble() 1915 current_size = self.GetActualSize() 1916 new_size = current_size + amount * 1024 * 1024 1917 assert new_size > current_size, "Cannot Grow with a negative amount" 1918 try: 1919 f = open(self.dev_path, "a+") 1920 f.truncate(new_size) 1921 f.close() 1922 except EnvironmentError, err: 1923 _ThrowError("Error in file growth: %", str(err))
1924
1925 - def Attach(self):
1926 """Attach to an existing file. 1927 1928 Check if this file already exists. 1929 1930 @rtype: boolean 1931 @return: True if file exists 1932 1933 """ 1934 self.attached = os.path.exists(self.dev_path) 1935 return self.attached
1936
1937 - def GetActualSize(self):
1938 """Return the actual disk size. 1939 1940 @note: the device needs to be active when this is called 1941 1942 """ 1943 assert self.attached, "BlockDevice not attached in GetActualSize()" 1944 try: 1945 st = os.stat(self.dev_path) 1946 return st.st_size 1947 except OSError, err: 1948 _ThrowError("Can't stat %s: %s", self.dev_path, err)
1949 1950 @classmethod
1951 - def Create(cls, unique_id, children, size):
1952 """Create a new file. 1953 1954 @param size: the size of file in MiB 1955 1956 @rtype: L{bdev.FileStorage} 1957 @return: an instance of FileStorage 1958 1959 """ 1960 if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2: 1961 raise ValueError("Invalid configuration data %s" % str(unique_id)) 1962 dev_path = unique_id[1] 1963 try: 1964 fd = os.open(dev_path, os.O_RDWR | os.O_CREAT | os.O_EXCL) 1965 f = os.fdopen(fd, "w") 1966 f.truncate(size * 1024 * 1024) 1967 f.close() 1968 except EnvironmentError, err: 1969 if err.errno == errno.EEXIST: 1970 _ThrowError("File already existing: %s", dev_path) 1971 _ThrowError("Error in file creation: %", str(err)) 1972 1973 return FileStorage(unique_id, children, size)
1974 1975 1976 DEV_MAP = { 1977 constants.LD_LV: LogicalVolume, 1978 constants.LD_DRBD8: DRBD8, 1979 } 1980 1981 if constants.ENABLE_FILE_STORAGE: 1982 DEV_MAP[constants.LD_FILE] = FileStorage
1983 1984 1985 -def FindDevice(dev_type, unique_id, children, size):
1986 """Search for an existing, assembled device. 1987 1988 This will succeed only if the device exists and is assembled, but it 1989 does not do any actions in order to activate the device. 1990 1991 """ 1992 if dev_type not in DEV_MAP: 1993 raise errors.ProgrammerError("Invalid block device type '%s'" % dev_type) 1994 device = DEV_MAP[dev_type](unique_id, children, size) 1995 if not device.attached: 1996 return None 1997 return device
1998
1999 2000 -def Assemble(dev_type, unique_id, children, size):
2001 """Try to attach or assemble an existing device. 2002 2003 This will attach to assemble the device, as needed, to bring it 2004 fully up. It must be safe to run on already-assembled devices. 2005 2006 """ 2007 if dev_type not in DEV_MAP: 2008 raise errors.ProgrammerError("Invalid block device type '%s'" % dev_type) 2009 device = DEV_MAP[dev_type](unique_id, children, size) 2010 device.Assemble() 2011 return device
2012
2013 2014 -def Create(dev_type, unique_id, children, size):
2015 """Create a device. 2016 2017 """ 2018 if dev_type not in DEV_MAP: 2019 raise errors.ProgrammerError("Invalid block device type '%s'" % dev_type) 2020 device = DEV_MAP[dev_type].Create(unique_id, children, size) 2021 return device
2022