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 # Verify that dev_path exists and is a block device 146 try: 147 st = os.stat(self.dev_path) 148 except OSError, err: 149 logging.error("Error stat()'ing %s: %s", self.dev_path, str(err)) 150 return False 151 152 if not stat.S_ISBLK(st.st_mode): 153 logging.error("%s is not a block device", self.dev_path) 154 return False 155 156 self.major = os.major(st.st_rdev) 157 self.minor = utils.osminor(st.st_rdev) 158 self.attached = True 159 160 return True
161
162 - def Assemble(self):
163 """Assemble the device. 164 165 """ 166 pass
167
168 - def Shutdown(self):
169 """Shutdown the device. 170 171 """ 172 if not self.minor and not self.Attach(): 173 # The extstorage device doesn't exist. 174 return 175 176 # Call the External Storage's detach script, 177 # to detach an existing Volume from it's block device under /dev 178 _ExtStorageAction(constants.ES_ACTION_DETACH, self.unique_id, 179 self.ext_params, name=self.name, uuid=self.uuid) 180 181 self.minor = None 182 self.dev_path = None
183
184 - def Open(self, force=False):
185 """Make the device ready for I/O. 186 187 """ 188 pass
189
190 - def Close(self):
191 """Notifies that the device will no longer be used for I/O. 192 193 """ 194 pass
195
196 - def Grow(self, amount, dryrun, backingstore, excl_stor):
197 """Grow the Volume. 198 199 @type amount: integer 200 @param amount: the amount (in mebibytes) to grow with 201 @type dryrun: boolean 202 @param dryrun: whether to execute the operation in simulation mode 203 only, without actually increasing the size 204 205 """ 206 if not backingstore: 207 return 208 if not self.Attach(): 209 base.ThrowError("Can't attach to extstorage device during Grow()") 210 211 if dryrun: 212 # we do not support dry runs of resize operations for now. 213 return 214 215 new_size = self.size + amount 216 217 # Call the External Storage's grow script, 218 # to grow an existing Volume inside the External Storage 219 _ExtStorageAction(constants.ES_ACTION_GROW, self.unique_id, 220 self.ext_params, size=self.size, grow=new_size, 221 name=self.name, uuid=self.uuid)
222
223 - def SetInfo(self, text):
224 """Update metadata with info text. 225 226 """ 227 # Replace invalid characters 228 text = re.sub("^[^A-Za-z0-9_+.]", "_", text) 229 text = re.sub("[^-A-Za-z0-9_+.]", "_", text) 230 231 # Only up to 128 characters are allowed 232 text = text[:128] 233 234 # Call the External Storage's setinfo script, 235 # to set metadata for an existing Volume inside the External Storage 236 _ExtStorageAction(constants.ES_ACTION_SETINFO, self.unique_id, 237 self.ext_params, metadata=text, 238 name=self.name, uuid=self.uuid)
239
240 - def GetUserspaceAccessUri(self, hypervisor):
241 """Generate KVM userspace URIs to be used as `-drive file` settings. 242 243 @see: L{base.BlockDev.GetUserspaceAccessUri} 244 245 """ 246 if not self.Attach(): 247 base.ThrowError("Can't attach to ExtStorage device") 248 249 # If the provider supports userspace access, the attach script has 250 # returned a list of URIs prefixed with the corresponding hypervisor. 251 prefix = hypervisor.lower() + ":" 252 for uri in self.uris: 253 if uri[:len(prefix)].lower() == prefix: 254 return uri[len(prefix):] 255 256 base.ThrowError("Userspace access is not supported by the '%s'" 257 " ExtStorage provider for the '%s' hypervisor" 258 % (self.driver, hypervisor))
259
260 - def Snapshot(self, snap_name=None, snap_size=None):
261 """Take a snapshot of the block device. 262 263 """ 264 provider, vol_name = self.unique_id 265 if not snap_name: 266 snap_name = vol_name + ".snap" 267 if not snap_size: 268 snap_size = self.size 269 270 _ExtStorageAction(constants.ES_ACTION_SNAPSHOT, self.unique_id, 271 self.ext_params, snap_name=snap_name, snap_size=snap_size) 272 273 return (provider, snap_name)
274
275 276 -def _ExtStorageAction(action, unique_id, ext_params, 277 size=None, grow=None, metadata=None, 278 name=None, uuid=None, 279 snap_name=None, snap_size=None):
280 """Take an External Storage action. 281 282 Take an External Storage action concerning or affecting 283 a specific Volume inside the External Storage. 284 285 @type action: string 286 @param action: which action to perform. One of: 287 create / remove / grow / attach / detach / snapshot 288 @type unique_id: tuple (driver, vol_name) 289 @param unique_id: a tuple containing the type of ExtStorage (driver) 290 and the Volume name 291 @type ext_params: dict 292 @param ext_params: ExtStorage parameters 293 @type size: integer 294 @param size: the size of the Volume in mebibytes 295 @type grow: integer 296 @param grow: the new size in mebibytes (after grow) 297 @type metadata: string 298 @param metadata: metadata info of the Volume, for use by the provider 299 @type name: string 300 @param name: name of the Volume (objects.Disk.name) 301 @type uuid: string 302 @type snap_size: integer 303 @param snap_size: the size of the snapshot 304 @type snap_name: string 305 @param snap_name: the name of the snapshot 306 @param uuid: uuid of the Volume (objects.Disk.uuid) 307 @rtype: None or a block device path (during attach) 308 309 """ 310 driver, vol_name = unique_id 311 312 # Create an External Storage instance of type `driver' 313 status, inst_es = ExtStorageFromDisk(driver) 314 if not status: 315 base.ThrowError("%s" % inst_es) 316 317 # Create the basic environment for the driver's scripts 318 create_env = _ExtStorageEnvironment(unique_id, ext_params, size, 319 grow, metadata, name, uuid, 320 snap_name, snap_size) 321 322 # Do not use log file for action `attach' as we need 323 # to get the output from RunResult 324 # TODO: find a way to have a log file for attach too 325 logfile = None 326 if action is not constants.ES_ACTION_ATTACH: 327 logfile = _VolumeLogName(action, driver, vol_name) 328 329 # Make sure the given action results in a valid script 330 if action not in constants.ES_SCRIPTS: 331 base.ThrowError("Action '%s' doesn't result in a valid ExtStorage script" % 332 action) 333 334 # Explicitly check if the script is valid 335 try: 336 _CheckExtStorageFile(inst_es.path, action) # pylint: disable=E1103 337 except errors.BlockDeviceError: 338 base.ThrowError("Action '%s' is not supported by provider '%s'" % 339 (action, driver)) 340 341 # Find out which external script to run according the given action 342 script_name = action + "_script" 343 script = getattr(inst_es, script_name) 344 345 # Run the external script 346 # pylint: disable=E1103 347 result = utils.RunCmd([script], env=create_env, 348 cwd=inst_es.path, output=logfile,) 349 if result.failed: 350 logging.error("External storage's %s command '%s' returned" 351 " error: %s, logfile: %s, output: %s", 352 action, result.cmd, result.fail_reason, 353 logfile, result.output) 354 355 # If logfile is 'None' (during attach), it breaks TailFile 356 # TODO: have a log file for attach too 357 if action is not constants.ES_ACTION_ATTACH: 358 lines = [utils.SafeEncode(val) 359 for val in utils.TailFile(logfile, lines=20)] 360 else: 361 lines = result.output.splitlines()[-20:] 362 363 base.ThrowError("External storage's %s script failed (%s), last" 364 " lines of output:\n%s", 365 action, result.fail_reason, "\n".join(lines)) 366 367 if action == constants.ES_ACTION_ATTACH: 368 return result.stdout
369
370 371 -def _CheckExtStorageFile(base_dir, filename):
372 """Check prereqs for an ExtStorage file. 373 374 Check if file exists, if it is a regular file and in case it is 375 one of extstorage scripts if it is executable. 376 377 @type base_dir: string 378 @param base_dir: Base directory containing ExtStorage installations. 379 @type filename: string 380 @param filename: The basename of the ExtStorage file. 381 382 @raises BlockDeviceError: In case prereqs are not met. 383 384 """ 385 386 file_path = utils.PathJoin(base_dir, filename) 387 try: 388 st = os.stat(file_path) 389 except EnvironmentError, err: 390 base.ThrowError("File '%s' under path '%s' is missing (%s)" % 391 (filename, base_dir, utils.ErrnoOrStr(err))) 392 393 if not stat.S_ISREG(stat.S_IFMT(st.st_mode)): 394 base.ThrowError("File '%s' under path '%s' is not a regular file" % 395 (filename, base_dir)) 396 397 if filename in constants.ES_SCRIPTS: 398 if stat.S_IMODE(st.st_mode) & stat.S_IXUSR != stat.S_IXUSR: 399 base.ThrowError("File '%s' under path '%s' is not executable" % 400 (filename, base_dir))
401
402 403 -def ExtStorageFromDisk(name, base_dir=None):
404 """Create an ExtStorage instance from disk. 405 406 This function will return an ExtStorage instance 407 if the given name is a valid ExtStorage name. 408 409 @type base_dir: string 410 @keyword base_dir: Base directory containing ExtStorage installations. 411 Defaults to a search in all the ES_SEARCH_PATH dirs. 412 @rtype: tuple 413 @return: True and the ExtStorage instance if we find a valid one, or 414 False and the diagnose message on error 415 416 """ 417 if base_dir is None: 418 es_base_dir = pathutils.ES_SEARCH_PATH 419 else: 420 es_base_dir = [base_dir] 421 422 es_dir = utils.FindFile(name, es_base_dir, os.path.isdir) 423 424 if es_dir is None: 425 return False, ("Directory for External Storage Provider %s not" 426 " found in search path" % name) 427 428 # ES Files dictionary, we will populate it with the absolute path 429 # names; if the value is True, then it is a required file, otherwise 430 # an optional one 431 es_files = dict.fromkeys(constants.ES_SCRIPTS, True) 432 433 # Let the snapshot script be optional 434 es_files[constants.ES_SCRIPT_SNAPSHOT] = False 435 436 es_files[constants.ES_PARAMETERS_FILE] = True 437 438 for (filename, required) in es_files.items(): 439 es_files[filename] = utils.PathJoin(es_dir, filename) 440 try: 441 _CheckExtStorageFile(es_dir, filename) 442 except errors.BlockDeviceError, err: 443 if required: 444 return False, str(err) 445 446 parameters = [] 447 if constants.ES_PARAMETERS_FILE in es_files: 448 parameters_file = es_files[constants.ES_PARAMETERS_FILE] 449 try: 450 parameters = utils.ReadFile(parameters_file).splitlines() 451 except EnvironmentError, err: 452 return False, ("Error while reading the EXT parameters file at %s: %s" % 453 (parameters_file, utils.ErrnoOrStr(err))) 454 parameters = [v.split(None, 1) for v in parameters] 455 456 es_obj = \ 457 objects.ExtStorage(name=name, path=es_dir, 458 create_script=es_files[constants.ES_SCRIPT_CREATE], 459 remove_script=es_files[constants.ES_SCRIPT_REMOVE], 460 grow_script=es_files[constants.ES_SCRIPT_GROW], 461 attach_script=es_files[constants.ES_SCRIPT_ATTACH], 462 detach_script=es_files[constants.ES_SCRIPT_DETACH], 463 setinfo_script=es_files[constants.ES_SCRIPT_SETINFO], 464 verify_script=es_files[constants.ES_SCRIPT_VERIFY], 465 snapshot_script=es_files[constants.ES_SCRIPT_SNAPSHOT], 466 supported_parameters=parameters) 467 return True, es_obj
468
469 470 -def _ExtStorageEnvironment(unique_id, ext_params, 471 size=None, grow=None, metadata=None, 472 name=None, uuid=None, 473 snap_name=None, snap_size=None):
474 """Calculate the environment for an External Storage script. 475 476 @type unique_id: tuple (driver, vol_name) 477 @param unique_id: ExtStorage pool and name of the Volume 478 @type ext_params: dict 479 @param ext_params: the EXT parameters 480 @type size: integer 481 @param size: size of the Volume (in mebibytes) 482 @type grow: integer 483 @param grow: new size of Volume after grow (in mebibytes) 484 @type metadata: string 485 @param metadata: metadata info of the Volume 486 @type name: string 487 @param name: name of the Volume (objects.Disk.name) 488 @type uuid: string 489 @param uuid: uuid of the Volume (objects.Disk.uuid) 490 @type snap_size: integer 491 @param snap_size: the size of the snapshot 492 @type snap_name: string 493 @param snap_name: the name of the snapshot 494 @rtype: dict 495 @return: dict of environment variables 496 497 """ 498 vol_name = unique_id[1] 499 500 result = {} 501 result["VOL_NAME"] = vol_name 502 503 # EXT params 504 for pname, pvalue in ext_params.items(): 505 result["EXTP_%s" % pname.upper()] = str(pvalue) 506 507 if size is not None: 508 result["VOL_SIZE"] = str(size) 509 510 if grow is not None: 511 result["VOL_NEW_SIZE"] = str(grow) 512 513 if metadata is not None: 514 result["VOL_METADATA"] = metadata 515 516 if name is not None: 517 result["VOL_CNAME"] = name 518 519 if uuid is not None: 520 result["VOL_UUID"] = uuid 521 522 if snap_name is not None: 523 result["VOL_SNAPSHOT_NAME"] = snap_name 524 525 if snap_size is not None: 526 result["VOL_SNAPSHOT_SIZE"] = str(snap_size) 527 528 return result
529
530 531 -def _VolumeLogName(kind, es_name, volume):
532 """Compute the ExtStorage log filename for a given Volume and operation. 533 534 @type kind: string 535 @param kind: the operation type (e.g. create, remove etc.) 536 @type es_name: string 537 @param es_name: the ExtStorage name 538 @type volume: string 539 @param volume: the name of the Volume inside the External Storage 540 541 """ 542 # Check if the extstorage log dir is a valid dir 543 if not os.path.isdir(pathutils.LOG_ES_DIR): 544 base.ThrowError("Cannot find log directory: %s", pathutils.LOG_ES_DIR) 545 546 # TODO: Use tempfile.mkstemp to create unique filename 547 basename = ("%s-%s-%s-%s.log" % 548 (kind, es_name, volume, utils.TimestampForFilename())) 549 return utils.PathJoin(pathutils.LOG_ES_DIR, basename)
550