X-Git-Url: https://code.grnet.gr/git/ganeti-local/blobdiff_plain/fdad8c4dba0e730b3d34d0b2608892919ca94e12..090377807b5214b3ae4a6bfe294d94df3eb5d6df:/lib/uidpool.py diff --git a/lib/uidpool.py b/lib/uidpool.py index 15ab314..9be7381 100644 --- a/lib/uidpool.py +++ b/lib/uidpool.py @@ -29,8 +29,15 @@ from the pool. """ +import errno +import logging +import os +import random + from ganeti import errors from ganeti import constants +from ganeti import compat +from ganeti import utils def ParseUidPool(value, separator=None): @@ -108,6 +115,33 @@ def RemoveFromUidPool(uid_pool, remove_uids): uid_pool.remove(uid_range) +def _FormatUidRange(lower, higher, roman=False): + """Convert a user-id range definition into a string. + + """ + if lower == higher: + return str(compat.TryToRoman(lower, convert=roman)) + return "%s-%s" % (compat.TryToRoman(lower, convert=roman), + compat.TryToRoman(higher, convert=roman)) + + +def FormatUidPool(uid_pool, separator=None, roman=False): + """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, roman=roman) + for lower, higher in uid_pool]) + + def CheckUidPool(uid_pool): """Sanity check user-id pool range definition values. @@ -145,3 +179,207 @@ def ExpandUidPool(uid_pool): 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: + + # 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 = + + 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([(constants.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(constants.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(constants.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(constants.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