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

Source Code for Module ganeti.storage.extstorage

  1  # 
  2  # 
  3   
  4  # Copyright (C) 2014 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  """ExtStorage Interface related functionality 
 32   
 33  """ 
 34   
 35  import re 
 36  import stat 
 37  import os 
 38  import logging 
 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 pathutils 
 45  from ganeti.storage import base 
46 47 48 -class ExtStorageDevice(base.BlockDev):
49 """A block device provided by an ExtStorage Provider. 50 51 This class implements the External Storage Interface, which means 52 handling of the externally provided block devices. 53 54 """
55 - def __init__(self, unique_id, children, size, params, dyn_params, *args):
56 """Attaches to an extstorage block device. 57 58 """ 59 super(ExtStorageDevice, self).__init__(unique_id, children, size, params, 60 dyn_params, *args) 61 (self.name, self.uuid) = args 62 63 if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2: 64 raise ValueError("Invalid configuration data %s" % str(unique_id)) 65 66 self.driver, self.vol_name = unique_id 67 self.ext_params = params 68 69 self.major = self.minor = None 70 self.uris = [] 71 self.Attach()
72 73 @classmethod
74 - def Create(cls, unique_id, children, size, spindles, params, excl_stor, 75 dyn_params, *args):
76 """Create a new extstorage device. 77 78 Provision a new volume using an extstorage provider, which will 79 then be mapped to a block device. 80 81 """ 82 (name, uuid) = args 83 84 if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2: 85 raise errors.ProgrammerError("Invalid configuration data %s" % 86 str(unique_id)) 87 if excl_stor: 88 raise errors.ProgrammerError("extstorage device requested with" 89 " exclusive_storage") 90 91 # Call the External Storage's create script, 92 # to provision a new Volume inside the External Storage 93 _ExtStorageAction(constants.ES_ACTION_CREATE, unique_id, 94 params, size=size, name=name, uuid=uuid) 95 96 return ExtStorageDevice(unique_id, children, size, params, dyn_params, 97 *args)
98
99 - def Remove(self):
100 """Remove the extstorage device. 101 102 """ 103 if not self.minor and not self.Attach(): 104 # The extstorage device doesn't exist. 105 return 106 107 # First shutdown the device (remove mappings). 108 self.Shutdown() 109 110 # Call the External Storage's remove script, 111 # to remove the Volume from the External Storage 112 _ExtStorageAction(constants.ES_ACTION_REMOVE, self.unique_id, 113 self.ext_params, name=self.name, uuid=self.uuid)
114
115 - def Rename(self, new_id):
116 """Rename this device. 117 118 """ 119 pass
120
121 - def Attach(self):
122 """Attach to an existing extstorage device. 123 124 This method maps the extstorage volume that matches our name with 125 a corresponding block device and then attaches to this device. 126 127 """ 128 self.attached = False 129 130 # Call the External Storage's attach script, 131 # to attach an existing Volume to a block device under /dev 132 result = _ExtStorageAction(constants.ES_ACTION_ATTACH, 133 self.unique_id, self.ext_params, 134 name=self.name, uuid=self.uuid) 135 136 # Attach script returns the block device path and optionally 137 # the URIs to be used for userspace access (one URI for 138 # each hypervisor supported). 139 # If the provider doesn't support userspace access, then 140 # the 'uris' variable will be an empty list. 141 result = result.split("\n") 142 self.dev_path = result[0] 143 self.uris = result[1:] 144 145 if not self.dev_path: 146 logging.info("A local block device is not available") 147 self.dev_path = None 148 if not self.uris: 149 logging.error("Neither a block device nor a userspace URI is available") 150 return False 151 152 self.attached = True 153 return True 154 155 # Verify that dev_path exists and is a block device 156 try: 157 st = os.stat(self.dev_path) 158 except OSError, err: 159 logging.error("Error stat()'ing %s: %s", self.dev_path, str(err)) 160 return False 161 162 if not stat.S_ISBLK(st.st_mode): 163 logging.error("%s is not a block device", self.dev_path) 164 return False 165 166 self.major = os.major(st.st_rdev) 167 self.minor = utils.osminor(st.st_rdev) 168 self.attached = True 169 170 return True
171
172 - def Assemble(self):
173 """Assemble the device. 174 175 """ 176 pass
177
178 - def Shutdown(self):
179 """Shutdown the device. 180 181 """ 182 if not self.minor and not self.Attach(): 183 # The extstorage device doesn't exist. 184 return 185 186 # Call the External Storage's detach script, 187 # to detach an existing Volume from it's block device under /dev 188 _ExtStorageAction(constants.ES_ACTION_DETACH, self.unique_id, 189 self.ext_params, name=self.name, uuid=self.uuid) 190 191 self.minor = None 192 self.dev_path = None
193
194 - def Open(self, force=False, exclusive=True):
195 """Make the device ready for I/O. 196 197 """ 198 _ExtStorageAction(constants.ES_ACTION_OPEN, self.unique_id, 199 self.ext_params, 200 name=self.name, uuid=self.uuid, 201 exclusive=exclusive)
202
203 - def Close(self):
204 """Notifies that the device will no longer be used for I/O. 205 206 """ 207 _ExtStorageAction(constants.ES_ACTION_CLOSE, self.unique_id, 208 self.ext_params, 209 name=self.name, uuid=self.uuid)
210
211 - def Grow(self, amount, dryrun, backingstore, excl_stor):
212 """Grow the Volume. 213 214 @type amount: integer 215 @param amount: the amount (in mebibytes) to grow with 216 @type dryrun: boolean 217 @param dryrun: whether to execute the operation in simulation mode 218 only, without actually increasing the size 219 220 """ 221 if not backingstore: 222 return 223 if not self.Attach(): 224 base.ThrowError("Can't attach to extstorage device during Grow()") 225 226 if dryrun: 227 # we do not support dry runs of resize operations for now. 228 return 229 230 new_size = self.size + amount 231 232 # Call the External Storage's grow script, 233 # to grow an existing Volume inside the External Storage 234 _ExtStorageAction(constants.ES_ACTION_GROW, self.unique_id, 235 self.ext_params, size=self.size, grow=new_size, 236 name=self.name, uuid=self.uuid)
237
238 - def SetInfo(self, text):
239 """Update metadata with info text. 240 241 """ 242 # Replace invalid characters 243 text = re.sub("^[^A-Za-z0-9_+.]", "_", text) 244 text = re.sub("[^-A-Za-z0-9_+.]", "_", text) 245 246 # Only up to 128 characters are allowed 247 text = text[:128] 248 249 # Call the External Storage's setinfo script, 250 # to set metadata for an existing Volume inside the External Storage 251 _ExtStorageAction(constants.ES_ACTION_SETINFO, self.unique_id, 252 self.ext_params, metadata=text, 253 name=self.name, uuid=self.uuid)
254
255 - def GetUserspaceAccessUri(self, hypervisor):
256 """Generate KVM userspace URIs to be used as `-drive file` settings. 257 258 @see: L{base.BlockDev.GetUserspaceAccessUri} 259 260 """ 261 if not self.Attach(): 262 base.ThrowError("Can't attach to ExtStorage device") 263 264 # If the provider supports userspace access, the attach script has 265 # returned a list of URIs prefixed with the corresponding hypervisor. 266 prefix = hypervisor.lower() + ":" 267 for uri in self.uris: 268 if uri[:len(prefix)].lower() == prefix: 269 return uri[len(prefix):] 270 271 base.ThrowError("Userspace access is not supported by the '%s'" 272 " ExtStorage provider for the '%s' hypervisor" 273 % (self.driver, hypervisor))
274
275 - def Snapshot(self, snap_name=None, snap_size=None):
276 """Take a snapshot of the block device. 277 278 """ 279 provider, vol_name = self.unique_id 280 if not snap_name: 281 snap_name = vol_name + ".snap" 282 if not snap_size: 283 snap_size = self.size 284 285 _ExtStorageAction(constants.ES_ACTION_SNAPSHOT, self.unique_id, 286 self.ext_params, snap_name=snap_name, snap_size=snap_size) 287 288 return (provider, snap_name)
289
290 291 -def _ExtStorageAction(action, unique_id, ext_params, 292 size=None, grow=None, metadata=None, 293 name=None, uuid=None, 294 snap_name=None, snap_size=None, 295 exclusive=None):
296 """Take an External Storage action. 297 298 Take an External Storage action concerning or affecting 299 a specific Volume inside the External Storage. 300 301 @type action: string 302 @param action: which action to perform. One of: 303 create / remove / grow / attach / detach / snapshot 304 @type unique_id: tuple (driver, vol_name) 305 @param unique_id: a tuple containing the type of ExtStorage (driver) 306 and the Volume name 307 @type ext_params: dict 308 @param ext_params: ExtStorage parameters 309 @type size: integer 310 @param size: the size of the Volume in mebibytes 311 @type grow: integer 312 @param grow: the new size in mebibytes (after grow) 313 @type metadata: string 314 @param metadata: metadata info of the Volume, for use by the provider 315 @type name: string 316 @param name: name of the Volume (objects.Disk.name) 317 @type uuid: string 318 @type snap_size: integer 319 @param snap_size: the size of the snapshot 320 @type snap_name: string 321 @param snap_name: the name of the snapshot 322 @type exclusive: boolean 323 @param exclusive: Whether the Volume will be opened exclusively or not 324 @param uuid: uuid of the Volume (objects.Disk.uuid) 325 @rtype: None or a block device path (during attach) 326 327 """ 328 driver, vol_name = unique_id 329 330 # Create an External Storage instance of type `driver' 331 status, inst_es = ExtStorageFromDisk(driver) 332 if not status: 333 base.ThrowError("%s" % inst_es) 334 335 # Create the basic environment for the driver's scripts 336 create_env = _ExtStorageEnvironment(unique_id, ext_params, size, 337 grow, metadata, name, uuid, 338 snap_name, snap_size, 339 exclusive) 340 341 # Do not use log file for action `attach' as we need 342 # to get the output from RunResult 343 # TODO: find a way to have a log file for attach too 344 logfile = None 345 if action is not constants.ES_ACTION_ATTACH: 346 logfile = _VolumeLogName(action, driver, vol_name) 347 348 # Make sure the given action results in a valid script 349 if action not in constants.ES_SCRIPTS: 350 base.ThrowError("Action '%s' doesn't result in a valid ExtStorage script" % 351 action) 352 353 # Find out which external script to run according the given action 354 script_name = action + "_script" 355 script = getattr(inst_es, script_name) 356 357 # Here script is either a valid file path or None if the script is optional 358 if not script: 359 logging.info("Optional action '%s' is not supported by provider '%s'," 360 " skipping", action, driver) 361 return 362 363 # Run the external script 364 # pylint: disable=E1103 365 result = utils.RunCmd([script], env=create_env, 366 cwd=inst_es.path, output=logfile,) 367 if result.failed: 368 logging.error("External storage's %s command '%s' returned" 369 " error: %s, logfile: %s, output: %s", 370 action, result.cmd, result.fail_reason, 371 logfile, result.output) 372 373 # If logfile is 'None' (during attach), it breaks TailFile 374 # TODO: have a log file for attach too 375 if action is not constants.ES_ACTION_ATTACH: 376 lines = [utils.SafeEncode(val) 377 for val in utils.TailFile(logfile, lines=20)] 378 else: 379 lines = result.output.splitlines()[-20:] 380 381 base.ThrowError("External storage's %s script failed (%s), last" 382 " lines of output:\n%s", 383 action, result.fail_reason, "\n".join(lines)) 384 385 if action == constants.ES_ACTION_ATTACH: 386 return result.stdout
387
388 389 -def _CheckExtStorageFile(base_dir, filename, required):
390 """Check prereqs for an ExtStorage file. 391 392 Check if file exists, if it is a regular file and in case it is 393 one of extstorage scripts if it is executable. 394 395 @type base_dir: string 396 @param base_dir: Base directory containing ExtStorage installations. 397 @type filename: string 398 @param filename: The basename of the ExtStorage file. 399 @type required: bool 400 @param required: Whether the file is required or not. 401 402 @rtype: String 403 @return: The file path if the file is found and is valid, 404 None if the file is not found and not required. 405 406 @raises BlockDeviceError: In case prereqs are not met 407 (found and not valid/executable, not found and required) 408 409 """ 410 411 file_path = utils.PathJoin(base_dir, filename) 412 try: 413 st = os.stat(file_path) 414 except EnvironmentError, err: 415 if not required: 416 logging.info("Optional file '%s' under path '%s' is missing", 417 filename, base_dir) 418 return None 419 420 base.ThrowError("File '%s' under path '%s' is missing (%s)" % 421 (filename, base_dir, utils.ErrnoOrStr(err))) 422 423 if not stat.S_ISREG(stat.S_IFMT(st.st_mode)): 424 base.ThrowError("File '%s' under path '%s' is not a regular file" % 425 (filename, base_dir)) 426 427 if filename in constants.ES_SCRIPTS: 428 if stat.S_IMODE(st.st_mode) & stat.S_IXUSR != stat.S_IXUSR: 429 base.ThrowError("File '%s' under path '%s' is not executable" % 430 (filename, base_dir)) 431 432 return file_path
433
434 435 -def ExtStorageFromDisk(name, base_dir=None):
436 """Create an ExtStorage instance from disk. 437 438 This function will return an ExtStorage instance 439 if the given name is a valid ExtStorage name. 440 441 @type base_dir: string 442 @keyword base_dir: Base directory containing ExtStorage installations. 443 Defaults to a search in all the ES_SEARCH_PATH dirs. 444 @rtype: tuple 445 @return: True and the ExtStorage instance if we find a valid one, or 446 False and the diagnose message on error 447 448 """ 449 if base_dir is None: 450 es_base_dir = pathutils.ES_SEARCH_PATH 451 else: 452 es_base_dir = [base_dir] 453 454 es_dir = utils.FindFile(name, es_base_dir, os.path.isdir) 455 456 if es_dir is None: 457 return False, ("Directory for External Storage Provider %s not" 458 " found in search path" % name) 459 460 # ES Files dictionary: this will be populated later with the absolute path 461 # names for each script; currently we denote for each script if it is 462 # required (True) or optional (False) 463 es_files = dict.fromkeys(constants.ES_SCRIPTS, True) 464 465 # Let the snapshot, open, and close scripts be optional 466 # for backwards compatibility 467 es_files[constants.ES_SCRIPT_SNAPSHOT] = False 468 es_files[constants.ES_SCRIPT_OPEN] = False 469 es_files[constants.ES_SCRIPT_CLOSE] = False 470 471 es_files[constants.ES_PARAMETERS_FILE] = True 472 473 for (filename, required) in es_files.items(): 474 try: 475 # Here we actually fill the dict with the ablsolute path name for each 476 # script or None, depending on the corresponding checks. See the 477 # function's docstrings for more on these checks. 478 es_files[filename] = _CheckExtStorageFile(es_dir, filename, required) 479 except errors.BlockDeviceError, err: 480 return False, str(err) 481 482 parameters = [] 483 if constants.ES_PARAMETERS_FILE in es_files: 484 parameters_file = es_files[constants.ES_PARAMETERS_FILE] 485 try: 486 parameters = utils.ReadFile(parameters_file).splitlines() 487 except EnvironmentError, err: 488 return False, ("Error while reading the EXT parameters file at %s: %s" % 489 (parameters_file, utils.ErrnoOrStr(err))) 490 parameters = [v.split(None, 1) for v in parameters] 491 492 es_obj = \ 493 objects.ExtStorage(name=name, path=es_dir, 494 create_script=es_files[constants.ES_SCRIPT_CREATE], 495 remove_script=es_files[constants.ES_SCRIPT_REMOVE], 496 grow_script=es_files[constants.ES_SCRIPT_GROW], 497 attach_script=es_files[constants.ES_SCRIPT_ATTACH], 498 detach_script=es_files[constants.ES_SCRIPT_DETACH], 499 setinfo_script=es_files[constants.ES_SCRIPT_SETINFO], 500 verify_script=es_files[constants.ES_SCRIPT_VERIFY], 501 snapshot_script=es_files[constants.ES_SCRIPT_SNAPSHOT], 502 open_script=es_files[constants.ES_SCRIPT_OPEN], 503 close_script=es_files[constants.ES_SCRIPT_CLOSE], 504 supported_parameters=parameters) 505 return True, es_obj
506
507 508 -def _ExtStorageEnvironment(unique_id, ext_params, 509 size=None, grow=None, metadata=None, 510 name=None, uuid=None, 511 snap_name=None, snap_size=None, 512 exclusive=None):
513 """Calculate the environment for an External Storage script. 514 515 @type unique_id: tuple (driver, vol_name) 516 @param unique_id: ExtStorage pool and name of the Volume 517 @type ext_params: dict 518 @param ext_params: the EXT parameters 519 @type size: integer 520 @param size: size of the Volume (in mebibytes) 521 @type grow: integer 522 @param grow: new size of Volume after grow (in mebibytes) 523 @type metadata: string 524 @param metadata: metadata info of the Volume 525 @type name: string 526 @param name: name of the Volume (objects.Disk.name) 527 @type uuid: string 528 @param uuid: uuid of the Volume (objects.Disk.uuid) 529 @type snap_size: integer 530 @param snap_size: the size of the snapshot 531 @type snap_name: string 532 @param snap_name: the name of the snapshot 533 @type exclusive: boolean 534 @param exclusive: Whether the Volume will be opened exclusively or not 535 @rtype: dict 536 @return: dict of environment variables 537 538 """ 539 vol_name = unique_id[1] 540 541 result = {} 542 result["VOL_NAME"] = vol_name 543 544 # EXT params 545 for pname, pvalue in ext_params.items(): 546 result["EXTP_%s" % pname.upper()] = str(pvalue) 547 548 if size is not None: 549 result["VOL_SIZE"] = str(size) 550 551 if grow is not None: 552 result["VOL_NEW_SIZE"] = str(grow) 553 554 if metadata is not None: 555 result["VOL_METADATA"] = metadata 556 557 if name is not None: 558 result["VOL_CNAME"] = name 559 560 if uuid is not None: 561 result["VOL_UUID"] = uuid 562 563 if snap_name is not None: 564 result["VOL_SNAPSHOT_NAME"] = snap_name 565 566 if snap_size is not None: 567 result["VOL_SNAPSHOT_SIZE"] = str(snap_size) 568 569 if exclusive is not None: 570 result["VOL_OPEN_EXCLUSIVE"] = str(exclusive) 571 572 return result
573
574 575 -def _VolumeLogName(kind, es_name, volume):
576 """Compute the ExtStorage log filename for a given Volume and operation. 577 578 @type kind: string 579 @param kind: the operation type (e.g. create, remove etc.) 580 @type es_name: string 581 @param es_name: the ExtStorage name 582 @type volume: string 583 @param volume: the name of the Volume inside the External Storage 584 585 """ 586 # Check if the extstorage log dir is a valid dir 587 if not os.path.isdir(pathutils.LOG_ES_DIR): 588 base.ThrowError("Cannot find log directory: %s", pathutils.LOG_ES_DIR) 589 590 # TODO: Use tempfile.mkstemp to create unique filename 591 basename = ("%s-%s-%s-%s.log" % 592 (kind, es_name, volume, utils.TimestampForFilename())) 593 return utils.PathJoin(pathutils.LOG_ES_DIR, basename)
594