| # |
| # |
| |
| # Copyright (C) 2010, 2012 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. |
| |
| |
| """User-id pool related functions. |
| |
| The user-id pool is cluster-wide configuration option. |
| It is stored as a list of user-id ranges. |
| This module contains functions used for manipulating the |
| user-id pool parameter and for requesting/returning user-ids |
| from the pool. |
| |
| """ |
| |
| import errno |
| import logging |
| import os |
| import random |
| |
| from ganeti import errors |
| from ganeti import constants |
| from ganeti import utils |
| from ganeti import pathutils |
| |
| |
| def ParseUidPool(value, separator=None): |
| """Parse a user-id pool definition. |
| |
| @param value: string representation of the user-id pool. |
| The accepted input format is a list of integer ranges. |
| The boundaries are inclusive. |
| Example: '1000-5000,8000,9000-9010'. |
| @param separator: the separator character between the uids/uid-ranges. |
| Defaults to a comma. |
| @return: a list of integer pairs (lower, higher range boundaries) |
| |
| """ |
| if separator is None: |
| separator = "," |
| |
| ranges = [] |
| for range_def in value.split(separator): |
| if not range_def: |
| # Skip empty strings |
| continue |
| boundaries = range_def.split("-") |
| n_elements = len(boundaries) |
| if n_elements > 2: |
| raise errors.OpPrereqError( |
| "Invalid user-id range definition. Only one hyphen allowed: %s" |
| % boundaries, errors.ECODE_INVAL) |
| try: |
| lower = int(boundaries[0]) |
| except (ValueError, TypeError), err: |
| raise errors.OpPrereqError("Invalid user-id value for lower boundary of" |
| " user-id range: %s" |
| % str(err), errors.ECODE_INVAL) |
| try: |
| higher = int(boundaries[n_elements - 1]) |
| except (ValueError, TypeError), err: |
| raise errors.OpPrereqError("Invalid user-id value for higher boundary of" |
| " user-id range: %s" |
| % str(err), errors.ECODE_INVAL) |
| |
| ranges.append((lower, higher)) |
| |
| ranges.sort() |
| return ranges |
| |
| |
| def AddToUidPool(uid_pool, add_uids): |
| """Add a list of user-ids/user-id ranges to a user-id pool. |
| |
| @param uid_pool: a user-id pool (list of integer tuples) |
| @param add_uids: user-id ranges to be added to the pool |
| (list of integer tuples) |
| |
| """ |
| for uid_range in add_uids: |
| if uid_range not in uid_pool: |
| uid_pool.append(uid_range) |
| uid_pool.sort() |
| |
| |
| def RemoveFromUidPool(uid_pool, remove_uids): |
| """Remove a list of user-ids/user-id ranges from a user-id pool. |
| |
| @param uid_pool: a user-id pool (list of integer tuples) |
| @param remove_uids: user-id ranges to be removed from the pool |
| (list of integer tuples) |
| |
| """ |
| for uid_range in remove_uids: |
| if uid_range not in uid_pool: |
| raise errors.OpPrereqError( |
| "User-id range to be removed is not found in the current" |
| " user-id pool: %s" % str(uid_range), errors.ECODE_INVAL) |
| uid_pool.remove(uid_range) |
| |
| |
| def _FormatUidRange(lower, higher): |
| """Convert a user-id range definition into a string. |
| |
| """ |
| if lower == higher: |
| return str(lower) |
| |
| return "%s-%s" % (lower, higher) |
| |
| |
| def FormatUidPool(uid_pool, separator=None): |
| """Convert the internal representation of the user-id pool into a string. |
| |
| The output format is also accepted by ParseUidPool() |
| |
| @param uid_pool: a list of integer pairs representing UID ranges |
| @param separator: the separator character between the uids/uid-ranges. |
| Defaults to ", ". |
| @return: a string with the formatted results |
| |
| """ |
| if separator is None: |
| separator = ", " |
| return separator.join([_FormatUidRange(lower, higher) |
| for lower, higher in uid_pool]) |
| |
| |
| def CheckUidPool(uid_pool): |
| """Sanity check user-id pool range definition values. |
| |
| @param uid_pool: a list of integer pairs (lower, higher range boundaries) |
| |
| """ |
| for lower, higher in uid_pool: |
| if lower > higher: |
| raise errors.OpPrereqError( |
| "Lower user-id range boundary value (%s)" |
| " is larger than higher boundary value (%s)" % |
| (lower, higher), errors.ECODE_INVAL) |
| if lower < constants.UIDPOOL_UID_MIN: |
| raise errors.OpPrereqError( |
| "Lower user-id range boundary value (%s)" |
| " is smaller than UIDPOOL_UID_MIN (%s)." % |
| (lower, constants.UIDPOOL_UID_MIN), |
| errors.ECODE_INVAL) |
| if higher > constants.UIDPOOL_UID_MAX: |
| raise errors.OpPrereqError( |
| "Higher user-id boundary value (%s)" |
| " is larger than UIDPOOL_UID_MAX (%s)." % |
| (higher, constants.UIDPOOL_UID_MAX), |
| errors.ECODE_INVAL) |
| |
| |
| def ExpandUidPool(uid_pool): |
| """Expands a uid-pool definition to a list of uids. |
| |
| @param uid_pool: a list of integer pairs (lower, higher range boundaries) |
| @return: a list of integers |
| |
| """ |
| uids = set() |
| for lower, higher in uid_pool: |
| uids.update(range(lower, higher + 1)) |
| return list(uids) |
| |
| |
| def _IsUidUsed(uid): |
| """Check if there is any process in the system running with the given user-id |
| |
| @type uid: integer |
| @param uid: the user-id to be checked. |
| |
| """ |
| pgrep_command = [constants.PGREP, "-u", uid] |
| result = utils.RunCmd(pgrep_command) |
| |
| if result.exit_code == 0: |
| return True |
| elif result.exit_code == 1: |
| return False |
| else: |
| raise errors.CommandError("Running pgrep failed. exit code: %s" |
| % result.exit_code) |
| |
| |
| class LockedUid(object): |
| """Class representing a locked user-id in the uid-pool. |
| |
| This binds together a userid and a lock. |
| |
| """ |
| def __init__(self, uid, lock): |
| """Constructor |
| |
| @param uid: a user-id |
| @param lock: a utils.FileLock object |
| |
| """ |
| self._uid = uid |
| self._lock = lock |
| |
| def Unlock(self): |
| # Release the exclusive lock and close the filedescriptor |
| self._lock.Close() |
| |
| def GetUid(self): |
| return self._uid |
| |
| def AsStr(self): |
| return "%s" % self._uid |
| |
| |
| def RequestUnusedUid(all_uids): |
| """Tries to find an unused uid from the uid-pool, locks it and returns it. |
| |
| Usage pattern |
| ============= |
| |
| 1. When starting a process:: |
| |
| from ganeti import ssconf |
| from ganeti import uidpool |
| |
| # Get list of all user-ids in the uid-pool from ssconf |
| ss = ssconf.SimpleStore() |
| uid_pool = uidpool.ParseUidPool(ss.GetUidPool(), separator="\\n") |
| all_uids = set(uidpool.ExpandUidPool(uid_pool)) |
| |
| uid = uidpool.RequestUnusedUid(all_uids) |
| try: |
| <start a process with the UID> |
| # Once the process is started, we can release the file lock |
| uid.Unlock() |
| except ..., err: |
| # Return the UID to the pool |
| uidpool.ReleaseUid(uid) |
| |
| 2. Stopping a process:: |
| |
| from ganeti import uidpool |
| |
| uid = <get the UID the process is running under> |
| <stop the process> |
| uidpool.ReleaseUid(uid) |
| |
| @type all_uids: set of integers |
| @param all_uids: a set containing all the user-ids in the user-id pool |
| @return: a LockedUid object representing the unused uid. It's the caller's |
| responsibility to unlock the uid once an instance is started with |
| this uid. |
| |
| """ |
| # Create the lock dir if it's not yet present |
| try: |
| utils.EnsureDirs([(pathutils.UIDPOOL_LOCKDIR, 0755)]) |
| except errors.GenericError, err: |
| raise errors.LockError("Failed to create user-id pool lock dir: %s" % err) |
| |
| # Get list of currently used uids from the filesystem |
| try: |
| taken_uids = set() |
| for taken_uid in os.listdir(pathutils.UIDPOOL_LOCKDIR): |
| try: |
| taken_uid = int(taken_uid) |
| except ValueError, err: |
| # Skip directory entries that can't be converted into an integer |
| continue |
| taken_uids.add(taken_uid) |
| except OSError, err: |
| raise errors.LockError("Failed to get list of used user-ids: %s" % err) |
| |
| # Filter out spurious entries from the directory listing |
| taken_uids = all_uids.intersection(taken_uids) |
| |
| # Remove the list of used uids from the list of all uids |
| unused_uids = list(all_uids - taken_uids) |
| if not unused_uids: |
| logging.info("All user-ids in the uid-pool are marked 'taken'") |
| |
| # Randomize the order of the unused user-id list |
| random.shuffle(unused_uids) |
| |
| # Randomize the order of the unused user-id list |
| taken_uids = list(taken_uids) |
| random.shuffle(taken_uids) |
| |
| for uid in (unused_uids + taken_uids): |
| try: |
| # Create the lock file |
| # Note: we don't care if it exists. Only the fact that we can |
| # (or can't) lock it later is what matters. |
| uid_path = utils.PathJoin(pathutils.UIDPOOL_LOCKDIR, str(uid)) |
| lock = utils.FileLock.Open(uid_path) |
| except OSError, err: |
| raise errors.LockError("Failed to create lockfile for user-id %s: %s" |
| % (uid, err)) |
| try: |
| # Try acquiring an exclusive lock on the lock file |
| lock.Exclusive() |
| # Check if there is any process running with this user-id |
| if _IsUidUsed(uid): |
| logging.debug("There is already a process running under" |
| " user-id %s", uid) |
| lock.Unlock() |
| continue |
| return LockedUid(uid, lock) |
| except IOError, err: |
| if err.errno == errno.EAGAIN: |
| # The file is already locked, let's skip it and try another unused uid |
| logging.debug("Lockfile for user-id is already locked %s: %s", uid, err) |
| continue |
| except errors.LockError, err: |
| # There was an unexpected error while trying to lock the file |
| logging.error("Failed to lock the lockfile for user-id %s: %s", uid, err) |
| raise |
| |
| raise errors.LockError("Failed to find an unused user-id") |
| |
| |
| def ReleaseUid(uid): |
| """This should be called when the given user-id is no longer in use. |
| |
| @type uid: LockedUid or integer |
| @param uid: the uid to release back to the pool |
| |
| """ |
| if isinstance(uid, LockedUid): |
| # Make sure we release the exclusive lock, if there is any |
| uid.Unlock() |
| uid_filename = uid.AsStr() |
| else: |
| uid_filename = str(uid) |
| |
| try: |
| uid_path = utils.PathJoin(pathutils.UIDPOOL_LOCKDIR, uid_filename) |
| os.remove(uid_path) |
| except OSError, err: |
| raise errors.LockError("Failed to remove user-id lockfile" |
| " for user-id %s: %s" % (uid_filename, err)) |
| |
| |
| def ExecWithUnusedUid(fn, all_uids, *args, **kwargs): |
| """Execute a callable and provide an unused user-id in its kwargs. |
| |
| This wrapper function provides a simple way to handle the requesting, |
| unlocking and releasing a user-id. |
| "fn" is called by passing a "uid" keyword argument that |
| contains an unused user-id (as an integer) selected from the set of user-ids |
| passed in all_uids. |
| If there is an error while executing "fn", the user-id is returned |
| to the pool. |
| |
| @param fn: a callable that accepts a keyword argument called "uid" |
| @type all_uids: a set of integers |
| @param all_uids: a set containing all user-ids in the user-id pool |
| |
| """ |
| uid = RequestUnusedUid(all_uids) |
| kwargs["uid"] = uid.GetUid() |
| try: |
| return_value = fn(*args, **kwargs) |
| except: |
| # The failure of "callabe" means that starting a process with the uid |
| # failed, so let's put the uid back into the pool. |
| ReleaseUid(uid) |
| raise |
| uid.Unlock() |
| return return_value |