| # |
| # |
| |
| # Copyright (C) 2013 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. |
| |
| |
| """Filesystem-based access functions and disk templates. |
| |
| """ |
| |
| import logging |
| import errno |
| import os |
| |
| from ganeti import compat |
| from ganeti import constants |
| from ganeti import errors |
| from ganeti import pathutils |
| from ganeti import utils |
| from ganeti.utils import io |
| from ganeti.storage import base |
| |
| |
| class FileDeviceHelper(object): |
| |
| @classmethod |
| def CreateFile(cls, path, size, create_folders=False, |
| _file_path_acceptance_fn=None): |
| """Create a new file and its file device helper. |
| |
| @param size: the size in MiBs the file should be truncated to. |
| @param create_folders: create the directories for the path if necessary |
| (using L{ganeti.utils.io.Makedirs}) |
| |
| @rtype: FileDeviceHelper |
| @return: The FileDeviceHelper object representing the object. |
| @raise errors.FileStoragePathError: if the file path is disallowed by policy |
| |
| """ |
| |
| if not _file_path_acceptance_fn: |
| _file_path_acceptance_fn = CheckFileStoragePathAcceptance |
| _file_path_acceptance_fn(path) |
| |
| if create_folders: |
| folder = os.path.dirname(path) |
| io.Makedirs(folder) |
| |
| try: |
| fd = os.open(path, os.O_RDWR | os.O_CREAT | os.O_EXCL) |
| f = os.fdopen(fd, "w") |
| f.truncate(size * 1024 * 1024) |
| f.close() |
| except EnvironmentError as err: |
| base.ThrowError("%s: can't create: %s", path, str(err)) |
| |
| return FileDeviceHelper(path, |
| _file_path_acceptance_fn=_file_path_acceptance_fn) |
| |
| def __init__(self, path, _file_path_acceptance_fn=None): |
| """Create a new file device helper. |
| |
| @raise errors.FileStoragePathError: if the file path is disallowed by policy |
| |
| """ |
| if not _file_path_acceptance_fn: |
| _file_path_acceptance_fn = CheckFileStoragePathAcceptance |
| _file_path_acceptance_fn(path) |
| |
| self.file_path_acceptance_fn = _file_path_acceptance_fn |
| self.path = path |
| |
| def Exists(self, assert_exists=None): |
| """Check for the existence of the given file. |
| |
| @param assert_exists: creates an assertion on the result value: |
| - if true, raise errors.BlockDeviceError if the file doesn't exist |
| - if false, raise errors.BlockDeviceError if the file does exist |
| @rtype: boolean |
| @return: True if the file exists |
| |
| """ |
| |
| exists = os.path.isfile(self.path) |
| |
| if not exists and assert_exists is True: |
| raise base.ThrowError("%s: No such file", self.path) |
| if exists and assert_exists is False: |
| raise base.ThrowError("%s: File exists", self.path) |
| |
| return exists |
| |
| def Remove(self): |
| """Remove the file backing the block device. |
| |
| @rtype: boolean |
| @return: True if the removal was successful |
| |
| """ |
| try: |
| os.remove(self.path) |
| return True |
| except OSError as err: |
| if err.errno != errno.ENOENT: |
| base.ThrowError("%s: can't remove: %s", self.path, err) |
| return False |
| |
| def Size(self): |
| """Return the actual disk size in bytes. |
| |
| @rtype: int |
| @return: The file size in bytes. |
| |
| """ |
| self.Exists(assert_exists=True) |
| try: |
| return os.stat(self.path).st_size |
| except OSError as err: |
| base.ThrowError("%s: can't stat: %s", self.path, err) |
| |
| def Grow(self, amount, dryrun, backingstore, _excl_stor): |
| """Grow the file |
| |
| @param amount: the amount (in mebibytes) to grow by. |
| |
| """ |
| # Check that the file exists |
| self.Exists(assert_exists=True) |
| |
| if amount < 0: |
| base.ThrowError("%s: can't grow by negative amount", self.path) |
| |
| if dryrun: |
| return |
| if not backingstore: |
| return |
| |
| current_size = self.Size() |
| new_size = current_size + amount * 1024 * 1024 |
| try: |
| f = open(self.path, "a+") |
| f.truncate(new_size) |
| f.close() |
| except EnvironmentError, err: |
| base.ThrowError("%s: can't grow: ", self.path, str(err)) |
| |
| def Move(self, new_path): |
| """Move file to a location inside the file storage dir. |
| |
| """ |
| # Check that the file exists |
| self.Exists(assert_exists=True) |
| self.file_path_acceptance_fn(new_path) |
| try: |
| os.rename(self.path, new_path) |
| self.path = new_path |
| except OSError, err: |
| base.ThrowError("%s: can't rename to %s: ", str(err), new_path) |
| |
| |
| class FileStorage(base.BlockDev): |
| """File device. |
| |
| This class represents a file storage backend device. |
| |
| The unique_id for the file device is a (file_driver, file_path) tuple. |
| |
| """ |
| def __init__(self, unique_id, children, size, params, dyn_params, **kwargs): |
| """Initalizes a file device backend. |
| |
| """ |
| if children: |
| raise errors.BlockDeviceError("Invalid setup for file device") |
| super(FileStorage, self).__init__(unique_id, children, size, params, |
| dyn_params, **kwargs) |
| if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2: |
| raise ValueError("Invalid configuration data %s" % str(unique_id)) |
| self.driver = unique_id[0] |
| self.dev_path = unique_id[1] |
| self.file = FileDeviceHelper(self.dev_path) |
| self.Attach() |
| |
| def Assemble(self): |
| """Assemble the device. |
| |
| Checks whether the file device exists, raises BlockDeviceError otherwise. |
| |
| """ |
| self.file.Exists(assert_exists=True) |
| |
| def Shutdown(self): |
| """Shutdown the device. |
| |
| This is a no-op for the file type, as we don't deactivate |
| the file on shutdown. |
| |
| """ |
| pass |
| |
| def Open(self, force=False, exclusive=True): |
| """Make the device ready for I/O. |
| |
| This is a no-op for the file type. |
| |
| """ |
| pass |
| |
| def Close(self): |
| """Notifies that the device will no longer be used for I/O. |
| |
| This is a no-op for the file type. |
| |
| """ |
| pass |
| |
| def Remove(self): |
| """Remove the file backing the block device. |
| |
| @rtype: boolean |
| @return: True if the removal was successful |
| |
| """ |
| return self.file.Remove() |
| |
| def Rename(self, new_id): |
| """Renames the file. |
| |
| """ |
| return self.file.Move(new_id[1]) |
| |
| def Grow(self, amount, dryrun, backingstore, excl_stor): |
| """Grow the file |
| |
| @param amount: the amount (in mebibytes) to grow with |
| |
| """ |
| if not backingstore: |
| return |
| if dryrun: |
| return |
| self.file.Grow(amount, dryrun, backingstore, excl_stor) |
| |
| def Attach(self, **kwargs): |
| """Attach to an existing file. |
| |
| Check if this file already exists. |
| |
| @rtype: boolean |
| @return: True if file exists |
| |
| """ |
| self.attached = self.file.Exists() |
| return self.attached |
| |
| def GetActualSize(self): |
| """Return the actual disk size. |
| |
| @note: the device needs to be active when this is called |
| |
| """ |
| return self.file.Size() |
| |
| @classmethod |
| def Create(cls, unique_id, children, size, spindles, params, excl_stor, |
| dyn_params, **kwargs): |
| """Create a new file. |
| |
| @type size: int |
| @param size: the size of file in MiB |
| |
| @rtype: L{bdev.FileStorage} |
| @return: an instance of FileStorage |
| |
| """ |
| if excl_stor: |
| raise errors.ProgrammerError("FileStorage device requested with" |
| " exclusive_storage") |
| if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2: |
| raise ValueError("Invalid configuration data %s" % str(unique_id)) |
| |
| dev_path = unique_id[1] |
| |
| FileDeviceHelper.CreateFile(dev_path, size) |
| return FileStorage(unique_id, children, size, params, dyn_params, |
| **kwargs) |
| |
| |
| def GetFileStorageSpaceInfo(path): |
| """Retrieves the free and total space of the device where the file is |
| located. |
| |
| @type path: string |
| @param path: Path of the file whose embracing device's capacity is |
| reported. |
| @return: a dictionary containing 'vg_size' and 'vg_free' given in MebiBytes |
| |
| """ |
| try: |
| result = os.statvfs(path) |
| free = (result.f_frsize * result.f_bavail) / (1024 * 1024) |
| size = (result.f_frsize * result.f_blocks) / (1024 * 1024) |
| return {"type": constants.ST_FILE, |
| "name": path, |
| "storage_size": size, |
| "storage_free": free} |
| except OSError, e: |
| raise errors.CommandError("Failed to retrieve file system information about" |
| " path: %s - %s" % (path, e.strerror)) |
| |
| |
| def _GetForbiddenFileStoragePaths(): |
| """Builds a list of path prefixes which shouldn't be used for file storage. |
| |
| @rtype: frozenset |
| |
| """ |
| paths = set([ |
| "/boot", |
| "/dev", |
| "/etc", |
| "/home", |
| "/proc", |
| "/root", |
| "/sys", |
| ]) |
| |
| for prefix in ["", "/usr", "/usr/local"]: |
| paths.update(["%s/%s" % (prefix, s) for s in |
| ["bin", "lib", "lib32", "lib64", "sbin"]]) |
| |
| return compat.UniqueFrozenset(map(os.path.normpath, paths)) |
| |
| |
| def _ComputeWrongFileStoragePaths(paths, |
| _forbidden=_GetForbiddenFileStoragePaths()): |
| """Cross-checks a list of paths for prefixes considered bad. |
| |
| Some paths, e.g. "/bin", should not be used for file storage. |
| |
| @type paths: list |
| @param paths: List of paths to be checked |
| @rtype: list |
| @return: Sorted list of paths for which the user should be warned |
| |
| """ |
| def _Check(path): |
| return (not os.path.isabs(path) or |
| path in _forbidden or |
| filter(lambda p: utils.IsBelowDir(p, path), _forbidden)) |
| |
| return utils.NiceSort(filter(_Check, map(os.path.normpath, paths))) |
| |
| |
| def ComputeWrongFileStoragePaths(_filename=pathutils.FILE_STORAGE_PATHS_FILE): |
| """Returns a list of file storage paths whose prefix is considered bad. |
| |
| See L{_ComputeWrongFileStoragePaths}. |
| |
| """ |
| return _ComputeWrongFileStoragePaths(_LoadAllowedFileStoragePaths(_filename)) |
| |
| |
| def _CheckFileStoragePath(path, allowed, exact_match_ok=False): |
| """Checks if a path is in a list of allowed paths for file storage. |
| |
| @type path: string |
| @param path: Path to check |
| @type allowed: list |
| @param allowed: List of allowed paths |
| @type exact_match_ok: bool |
| @param exact_match_ok: whether or not it is okay when the path is exactly |
| equal to an allowed path and not a subdir of it |
| @raise errors.FileStoragePathError: If the path is not allowed |
| |
| """ |
| if not os.path.isabs(path): |
| raise errors.FileStoragePathError("File storage path must be absolute," |
| " got '%s'" % path) |
| |
| for i in allowed: |
| if not os.path.isabs(i): |
| logging.info("Ignoring relative path '%s' for file storage", i) |
| continue |
| |
| if exact_match_ok: |
| if os.path.normpath(i) == os.path.normpath(path): |
| break |
| |
| if utils.IsBelowDir(i, path): |
| break |
| else: |
| raise errors.FileStoragePathError("Path '%s' is not acceptable for file" |
| " storage" % path) |
| |
| |
| def _LoadAllowedFileStoragePaths(filename): |
| """Loads file containing allowed file storage paths. |
| |
| @rtype: list |
| @return: List of allowed paths (can be an empty list) |
| |
| """ |
| try: |
| contents = utils.ReadFile(filename) |
| except EnvironmentError: |
| return [] |
| else: |
| return utils.FilterEmptyLinesAndComments(contents) |
| |
| |
| def CheckFileStoragePathAcceptance( |
| path, _filename=pathutils.FILE_STORAGE_PATHS_FILE, |
| exact_match_ok=False): |
| """Checks if a path is allowed for file storage. |
| |
| @type path: string |
| @param path: Path to check |
| @raise errors.FileStoragePathError: If the path is not allowed |
| |
| """ |
| allowed = _LoadAllowedFileStoragePaths(_filename) |
| if not allowed: |
| raise errors.FileStoragePathError("No paths are valid or path file '%s'" |
| " was not accessible." % _filename) |
| |
| if _ComputeWrongFileStoragePaths([path]): |
| raise errors.FileStoragePathError("Path '%s' uses a forbidden prefix" % |
| path) |
| |
| _CheckFileStoragePath(path, allowed, exact_match_ok=exact_match_ok) |
| |
| |
| def _CheckFileStoragePathExistance(path): |
| """Checks whether the given path is usable on the file system. |
| |
| This checks wether the path is existing, a directory and writable. |
| |
| @type path: string |
| @param path: path to check |
| |
| """ |
| if not os.path.isdir(path): |
| raise errors.FileStoragePathError("Path '%s' does not exist or is not a" |
| " directory." % path) |
| if not os.access(path, os.W_OK): |
| raise errors.FileStoragePathError("Path '%s' is not writable" % path) |
| |
| |
| def CheckFileStoragePath( |
| path, _allowed_paths_file=pathutils.FILE_STORAGE_PATHS_FILE): |
| """Checks whether the path exists and is acceptable to use. |
| |
| Can be used for any file-based storage, for example shared-file storage. |
| |
| @type path: string |
| @param path: path to check |
| @rtype: string |
| @returns: error message if the path is not ready to use |
| |
| """ |
| try: |
| CheckFileStoragePathAcceptance(path, _filename=_allowed_paths_file, |
| exact_match_ok=True) |
| except errors.FileStoragePathError as e: |
| return str(e) |
| if not os.path.isdir(path): |
| return "Path '%s' is not existing or not a directory." % path |
| if not os.access(path, os.W_OK): |
| return "Path '%s' is not writable" % path |