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

Source Code for Module ganeti.storage.bdev

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