+
+
+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