blob: 6004ac7e06f49bcf9f6313e54a84f1c941f053e8 [file] [log] [blame]
#
#
# Copyright (C) 2014 Google Inc.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are
# met:
#
# 1. Redistributions of source code must retain the above copyright notice,
# this list of conditions and the following disclaimer.
#
# 2. Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
# IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
# TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
"""ExtStorage Interface related functionality
"""
import re
import stat
import os
import logging
from ganeti import utils
from ganeti import errors
from ganeti import constants
from ganeti import objects
from ganeti import pathutils
from ganeti.storage import base
class ExtStorageDevice(base.BlockDev):
"""A block device provided by an ExtStorage Provider.
This class implements the External Storage Interface, which means
handling of the externally provided block devices.
"""
def __init__(self, unique_id, children, size, params, dyn_params, **kwargs):
"""Attaches to an extstorage block device.
"""
super(ExtStorageDevice, self).__init__(unique_id, children, size, params,
dyn_params, **kwargs)
self.name = kwargs["name"]
self.uuid = kwargs["uuid"]
if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
raise ValueError("Invalid configuration data %s" % str(unique_id))
self.driver, self.vol_name = unique_id
self.ext_params = params
self.major = self.minor = None
self.uris = []
self.Attach()
@classmethod
def Create(cls, unique_id, children, size, spindles, params, excl_stor,
dyn_params, **kwargs):
"""Create a new extstorage device.
Provision a new volume using an extstorage provider, which will
then be mapped to a block device.
"""
if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
raise errors.ProgrammerError("Invalid configuration data %s" %
str(unique_id))
if excl_stor:
raise errors.ProgrammerError("extstorage device requested with"
" exclusive_storage")
# Call the External Storage's create script,
# to provision a new Volume inside the External Storage
_ExtStorageAction(constants.ES_ACTION_CREATE, unique_id,
params, size=size, name=kwargs["name"],
uuid=kwargs["uuid"])
return ExtStorageDevice(unique_id, children, size, params, dyn_params,
**kwargs)
def Remove(self):
"""Remove the extstorage device.
"""
if not self.minor and not self.Attach():
# The extstorage device doesn't exist.
return
# First shutdown the device (remove mappings).
self.Shutdown()
# Call the External Storage's remove script,
# to remove the Volume from the External Storage
_ExtStorageAction(constants.ES_ACTION_REMOVE, self.unique_id,
self.ext_params, name=self.name, uuid=self.uuid)
def Rename(self, new_id):
"""Rename this device.
"""
pass
def Attach(self, **kwargs):
"""Attach to an existing extstorage device.
This method maps the extstorage volume that matches our name with
a corresponding block device and then attaches to this device.
"""
self.attached = False
# Call the External Storage's attach script,
# to attach an existing Volume to a block device under /dev
result = _ExtStorageAction(constants.ES_ACTION_ATTACH,
self.unique_id, self.ext_params,
name=self.name, uuid=self.uuid)
# Attach script returns the block device path and optionally
# the URIs to be used for userspace access (one URI for
# each hypervisor supported).
# If the provider doesn't support userspace access, then
# the 'uris' variable will be an empty list.
result = result.split("\n")
self.dev_path = result[0]
self.uris = result[1:]
if not self.dev_path:
logging.info("A local block device is not available")
self.dev_path = None
if not self.uris:
logging.error("Neither a block device nor a userspace URI is available")
return False
self.attached = True
return True
# Verify that dev_path exists and is a block device
try:
st = os.stat(self.dev_path)
except OSError, err:
logging.error("Error stat()'ing %s: %s", self.dev_path, str(err))
return False
if not stat.S_ISBLK(st.st_mode):
logging.error("%s is not a block device", self.dev_path)
return False
self.major = os.major(st.st_rdev)
self.minor = utils.osminor(st.st_rdev)
self.attached = True
return True
def Assemble(self):
"""Assemble the device.
"""
pass
def Shutdown(self):
"""Shutdown the device.
"""
if not self.minor and not self.Attach():
# The extstorage device doesn't exist.
return
# Call the External Storage's detach script,
# to detach an existing Volume from it's block device under /dev
_ExtStorageAction(constants.ES_ACTION_DETACH, self.unique_id,
self.ext_params, name=self.name, uuid=self.uuid)
self.minor = None
self.dev_path = None
def Open(self, force=False, exclusive=True):
"""Make the device ready for I/O.
"""
_ExtStorageAction(constants.ES_ACTION_OPEN, self.unique_id,
self.ext_params,
name=self.name, uuid=self.uuid,
exclusive=exclusive)
def Close(self):
"""Notifies that the device will no longer be used for I/O.
"""
_ExtStorageAction(constants.ES_ACTION_CLOSE, self.unique_id,
self.ext_params,
name=self.name, uuid=self.uuid)
def Grow(self, amount, dryrun, backingstore, excl_stor):
"""Grow the Volume.
@type amount: integer
@param amount: the amount (in mebibytes) to grow with
@type dryrun: boolean
@param dryrun: whether to execute the operation in simulation mode
only, without actually increasing the size
"""
if not backingstore:
return
if not self.Attach():
base.ThrowError("Can't attach to extstorage device during Grow()")
if dryrun:
# we do not support dry runs of resize operations for now.
return
new_size = self.size + amount
# Call the External Storage's grow script,
# to grow an existing Volume inside the External Storage
_ExtStorageAction(constants.ES_ACTION_GROW, self.unique_id,
self.ext_params, size=self.size, grow=new_size,
name=self.name, uuid=self.uuid)
def SetInfo(self, text):
"""Update metadata with info text.
"""
# Replace invalid characters
text = re.sub("^[^A-Za-z0-9_+.]", "_", text)
text = re.sub("[^-A-Za-z0-9_+.]", "_", text)
# Only up to 128 characters are allowed
text = text[:128]
# Call the External Storage's setinfo script,
# to set metadata for an existing Volume inside the External Storage
_ExtStorageAction(constants.ES_ACTION_SETINFO, self.unique_id,
self.ext_params, metadata=text,
name=self.name, uuid=self.uuid)
def GetUserspaceAccessUri(self, hypervisor):
"""Generate KVM userspace URIs to be used as `-drive file` settings.
@see: L{base.BlockDev.GetUserspaceAccessUri}
"""
if not self.Attach():
base.ThrowError("Can't attach to ExtStorage device")
# If the provider supports userspace access, the attach script has
# returned a list of URIs prefixed with the corresponding hypervisor.
prefix = hypervisor.lower() + ":"
for uri in self.uris:
if uri[:len(prefix)].lower() == prefix:
return uri[len(prefix):]
base.ThrowError("Userspace access is not supported by the '%s'"
" ExtStorage provider for the '%s' hypervisor"
% (self.driver, hypervisor))
def Snapshot(self, snap_name=None, snap_size=None):
"""Take a snapshot of the block device.
"""
provider, vol_name = self.unique_id
if not snap_name:
snap_name = vol_name + ".snap"
if not snap_size:
snap_size = self.size
_ExtStorageAction(constants.ES_ACTION_SNAPSHOT, self.unique_id,
self.ext_params, snap_name=snap_name, snap_size=snap_size)
return (provider, snap_name)
def _ExtStorageAction(action, unique_id, ext_params,
size=None, grow=None, metadata=None,
name=None, uuid=None,
snap_name=None, snap_size=None,
exclusive=None):
"""Take an External Storage action.
Take an External Storage action concerning or affecting
a specific Volume inside the External Storage.
@type action: string
@param action: which action to perform. One of:
create / remove / grow / attach / detach / snapshot
@type unique_id: tuple (driver, vol_name)
@param unique_id: a tuple containing the type of ExtStorage (driver)
and the Volume name
@type ext_params: dict
@param ext_params: ExtStorage parameters
@type size: integer
@param size: the size of the Volume in mebibytes
@type grow: integer
@param grow: the new size in mebibytes (after grow)
@type metadata: string
@param metadata: metadata info of the Volume, for use by the provider
@type name: string
@param name: name of the Volume (objects.Disk.name)
@type uuid: string
@type snap_size: integer
@param snap_size: the size of the snapshot
@type snap_name: string
@param snap_name: the name of the snapshot
@type exclusive: boolean
@param exclusive: Whether the Volume will be opened exclusively or not
@param uuid: uuid of the Volume (objects.Disk.uuid)
@rtype: None or a block device path (during attach)
"""
driver, vol_name = unique_id
# Create an External Storage instance of type `driver'
status, inst_es = ExtStorageFromDisk(driver)
if not status:
base.ThrowError("%s" % inst_es)
# Create the basic environment for the driver's scripts
create_env = _ExtStorageEnvironment(unique_id, ext_params, size,
grow, metadata, name, uuid,
snap_name, snap_size,
exclusive)
# Do not use log file for action `attach' as we need
# to get the output from RunResult
# TODO: find a way to have a log file for attach too
logfile = None
if action is not constants.ES_ACTION_ATTACH:
logfile = _VolumeLogName(action, driver, vol_name)
# Make sure the given action results in a valid script
if action not in constants.ES_SCRIPTS:
base.ThrowError("Action '%s' doesn't result in a valid ExtStorage script" %
action)
# Find out which external script to run according the given action
script_name = action + "_script"
script = getattr(inst_es, script_name)
# Here script is either a valid file path or None if the script is optional
if not script:
logging.info("Optional action '%s' is not supported by provider '%s',"
" skipping", action, driver)
return
# Run the external script
# pylint: disable=E1103
result = utils.RunCmd([script], env=create_env,
cwd=inst_es.path, output=logfile,)
if result.failed:
logging.error("External storage's %s command '%s' returned"
" error: %s, logfile: %s, output: %s",
action, result.cmd, result.fail_reason,
logfile, result.output)
# If logfile is 'None' (during attach), it breaks TailFile
# TODO: have a log file for attach too
if action is not constants.ES_ACTION_ATTACH:
lines = [utils.SafeEncode(val)
for val in utils.TailFile(logfile, lines=20)]
else:
lines = result.output.splitlines()[-20:]
base.ThrowError("External storage's %s script failed (%s), last"
" lines of output:\n%s",
action, result.fail_reason, "\n".join(lines))
if action == constants.ES_ACTION_ATTACH:
return result.stdout
def _CheckExtStorageFile(base_dir, filename, required):
"""Check prereqs for an ExtStorage file.
Check if file exists, if it is a regular file and in case it is
one of extstorage scripts if it is executable.
@type base_dir: string
@param base_dir: Base directory containing ExtStorage installations.
@type filename: string
@param filename: The basename of the ExtStorage file.
@type required: bool
@param required: Whether the file is required or not.
@rtype: String
@return: The file path if the file is found and is valid,
None if the file is not found and not required.
@raises BlockDeviceError: In case prereqs are not met
(found and not valid/executable, not found and required)
"""
file_path = utils.PathJoin(base_dir, filename)
try:
st = os.stat(file_path)
except EnvironmentError, err:
if not required:
logging.info("Optional file '%s' under path '%s' is missing",
filename, base_dir)
return None
base.ThrowError("File '%s' under path '%s' is missing (%s)" %
(filename, base_dir, utils.ErrnoOrStr(err)))
if not stat.S_ISREG(stat.S_IFMT(st.st_mode)):
base.ThrowError("File '%s' under path '%s' is not a regular file" %
(filename, base_dir))
if filename in constants.ES_SCRIPTS:
if stat.S_IMODE(st.st_mode) & stat.S_IXUSR != stat.S_IXUSR:
base.ThrowError("File '%s' under path '%s' is not executable" %
(filename, base_dir))
return file_path
def ExtStorageFromDisk(name, base_dir=None):
"""Create an ExtStorage instance from disk.
This function will return an ExtStorage instance
if the given name is a valid ExtStorage name.
@type base_dir: string
@keyword base_dir: Base directory containing ExtStorage installations.
Defaults to a search in all the ES_SEARCH_PATH dirs.
@rtype: tuple
@return: True and the ExtStorage instance if we find a valid one, or
False and the diagnose message on error
"""
if base_dir is None:
es_base_dir = pathutils.ES_SEARCH_PATH
else:
es_base_dir = [base_dir]
es_dir = utils.FindFile(name, es_base_dir, os.path.isdir)
if es_dir is None:
return False, ("Directory for External Storage Provider %s not"
" found in search path" % name)
# ES Files dictionary: this will be populated later with the absolute path
# names for each script; currently we denote for each script if it is
# required (True) or optional (False)
es_files = dict.fromkeys(constants.ES_SCRIPTS, True)
# Let the snapshot, open, and close scripts be optional
# for backwards compatibility
es_files[constants.ES_SCRIPT_SNAPSHOT] = False
es_files[constants.ES_SCRIPT_OPEN] = False
es_files[constants.ES_SCRIPT_CLOSE] = False
es_files[constants.ES_PARAMETERS_FILE] = True
for (filename, required) in es_files.items():
try:
# Here we actually fill the dict with the ablsolute path name for each
# script or None, depending on the corresponding checks. See the
# function's docstrings for more on these checks.
es_files[filename] = _CheckExtStorageFile(es_dir, filename, required)
except errors.BlockDeviceError, err:
return False, str(err)
parameters = []
if constants.ES_PARAMETERS_FILE in es_files:
parameters_file = es_files[constants.ES_PARAMETERS_FILE]
try:
parameters = utils.ReadFile(parameters_file).splitlines()
except EnvironmentError, err:
return False, ("Error while reading the EXT parameters file at %s: %s" %
(parameters_file, utils.ErrnoOrStr(err)))
parameters = [v.split(None, 1) for v in parameters]
es_obj = \
objects.ExtStorage(name=name, path=es_dir,
create_script=es_files[constants.ES_SCRIPT_CREATE],
remove_script=es_files[constants.ES_SCRIPT_REMOVE],
grow_script=es_files[constants.ES_SCRIPT_GROW],
attach_script=es_files[constants.ES_SCRIPT_ATTACH],
detach_script=es_files[constants.ES_SCRIPT_DETACH],
setinfo_script=es_files[constants.ES_SCRIPT_SETINFO],
verify_script=es_files[constants.ES_SCRIPT_VERIFY],
snapshot_script=es_files[constants.ES_SCRIPT_SNAPSHOT],
open_script=es_files[constants.ES_SCRIPT_OPEN],
close_script=es_files[constants.ES_SCRIPT_CLOSE],
supported_parameters=parameters)
return True, es_obj
def _ExtStorageEnvironment(unique_id, ext_params,
size=None, grow=None, metadata=None,
name=None, uuid=None,
snap_name=None, snap_size=None,
exclusive=None):
"""Calculate the environment for an External Storage script.
@type unique_id: tuple (driver, vol_name)
@param unique_id: ExtStorage pool and name of the Volume
@type ext_params: dict
@param ext_params: the EXT parameters
@type size: integer
@param size: size of the Volume (in mebibytes)
@type grow: integer
@param grow: new size of Volume after grow (in mebibytes)
@type metadata: string
@param metadata: metadata info of the Volume
@type name: string
@param name: name of the Volume (objects.Disk.name)
@type uuid: string
@param uuid: uuid of the Volume (objects.Disk.uuid)
@type snap_size: integer
@param snap_size: the size of the snapshot
@type snap_name: string
@param snap_name: the name of the snapshot
@type exclusive: boolean
@param exclusive: Whether the Volume will be opened exclusively or not
@rtype: dict
@return: dict of environment variables
"""
vol_name = unique_id[1]
result = {}
result["VOL_NAME"] = vol_name
# EXT params
for pname, pvalue in ext_params.items():
result["EXTP_%s" % pname.upper()] = str(pvalue)
if size is not None:
result["VOL_SIZE"] = str(size)
if grow is not None:
result["VOL_NEW_SIZE"] = str(grow)
if metadata is not None:
result["VOL_METADATA"] = metadata
if name is not None:
result["VOL_CNAME"] = name
if uuid is not None:
result["VOL_UUID"] = uuid
if snap_name is not None:
result["VOL_SNAPSHOT_NAME"] = snap_name
if snap_size is not None:
result["VOL_SNAPSHOT_SIZE"] = str(snap_size)
if exclusive is not None:
result["VOL_OPEN_EXCLUSIVE"] = str(exclusive)
return result
def _VolumeLogName(kind, es_name, volume):
"""Compute the ExtStorage log filename for a given Volume and operation.
@type kind: string
@param kind: the operation type (e.g. create, remove etc.)
@type es_name: string
@param es_name: the ExtStorage name
@type volume: string
@param volume: the name of the Volume inside the External Storage
"""
# Check if the extstorage log dir is a valid dir
if not os.path.isdir(pathutils.LOG_ES_DIR):
base.ThrowError("Cannot find log directory: %s", pathutils.LOG_ES_DIR)
# TODO: Use tempfile.mkstemp to create unique filename
basename = ("%s-%s-%s-%s.log" %
(kind, es_name, volume, utils.TimestampForFilename()))
return utils.PathJoin(pathutils.LOG_ES_DIR, basename)