4 # Copyright (C) 2010, 2012 Google Inc.
6 # This program is free software; you can redistribute it and/or modify
7 # it under the terms of the GNU General Public License as published by
8 # the Free Software Foundation; either version 2 of the License, or
9 # (at your option) any later version.
11 # This program is distributed in the hope that it will be useful, but
12 # WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14 # General Public License for more details.
16 # You should have received a copy of the GNU General Public License
17 # along with this program; if not, write to the Free Software
18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
22 """User-id pool related functions.
24 The user-id pool is cluster-wide configuration option.
25 It is stored as a list of user-id ranges.
26 This module contains functions used for manipulating the
27 user-id pool parameter and for requesting/returning user-ids
37 from ganeti import errors
38 from ganeti import constants
39 from ganeti import compat
40 from ganeti import utils
41 from ganeti import pathutils
44 def ParseUidPool(value, separator=None):
45 """Parse a user-id pool definition.
47 @param value: string representation of the user-id pool.
48 The accepted input format is a list of integer ranges.
49 The boundaries are inclusive.
50 Example: '1000-5000,8000,9000-9010'.
51 @param separator: the separator character between the uids/uid-ranges.
53 @return: a list of integer pairs (lower, higher range boundaries)
60 for range_def in value.split(separator):
64 boundaries = range_def.split("-")
65 n_elements = len(boundaries)
67 raise errors.OpPrereqError(
68 "Invalid user-id range definition. Only one hyphen allowed: %s"
69 % boundaries, errors.ECODE_INVAL)
71 lower = int(boundaries[0])
72 except (ValueError, TypeError), err:
73 raise errors.OpPrereqError("Invalid user-id value for lower boundary of"
75 % str(err), errors.ECODE_INVAL)
77 higher = int(boundaries[n_elements - 1])
78 except (ValueError, TypeError), err:
79 raise errors.OpPrereqError("Invalid user-id value for higher boundary of"
81 % str(err), errors.ECODE_INVAL)
83 ranges.append((lower, higher))
89 def AddToUidPool(uid_pool, add_uids):
90 """Add a list of user-ids/user-id ranges to a user-id pool.
92 @param uid_pool: a user-id pool (list of integer tuples)
93 @param add_uids: user-id ranges to be added to the pool
94 (list of integer tuples)
97 for uid_range in add_uids:
98 if uid_range not in uid_pool:
99 uid_pool.append(uid_range)
103 def RemoveFromUidPool(uid_pool, remove_uids):
104 """Remove a list of user-ids/user-id ranges from a user-id pool.
106 @param uid_pool: a user-id pool (list of integer tuples)
107 @param remove_uids: user-id ranges to be removed from the pool
108 (list of integer tuples)
111 for uid_range in remove_uids:
112 if uid_range not in uid_pool:
113 raise errors.OpPrereqError(
114 "User-id range to be removed is not found in the current"
115 " user-id pool: %s" % uid_range, errors.ECODE_INVAL)
116 uid_pool.remove(uid_range)
119 def _FormatUidRange(lower, higher, roman=False):
120 """Convert a user-id range definition into a string.
124 return str(compat.TryToRoman(lower, convert=roman))
125 return "%s-%s" % (compat.TryToRoman(lower, convert=roman),
126 compat.TryToRoman(higher, convert=roman))
129 def FormatUidPool(uid_pool, separator=None, roman=False):
130 """Convert the internal representation of the user-id pool into a string.
132 The output format is also accepted by ParseUidPool()
134 @param uid_pool: a list of integer pairs representing UID ranges
135 @param separator: the separator character between the uids/uid-ranges.
137 @return: a string with the formatted results
140 if separator is None:
142 return separator.join([_FormatUidRange(lower, higher, roman=roman)
143 for lower, higher in uid_pool])
146 def CheckUidPool(uid_pool):
147 """Sanity check user-id pool range definition values.
149 @param uid_pool: a list of integer pairs (lower, higher range boundaries)
152 for lower, higher in uid_pool:
154 raise errors.OpPrereqError(
155 "Lower user-id range boundary value (%s)"
156 " is larger than higher boundary value (%s)" %
157 (lower, higher), errors.ECODE_INVAL)
158 if lower < constants.UIDPOOL_UID_MIN:
159 raise errors.OpPrereqError(
160 "Lower user-id range boundary value (%s)"
161 " is smaller than UIDPOOL_UID_MIN (%s)." %
162 (lower, constants.UIDPOOL_UID_MIN),
164 if higher > constants.UIDPOOL_UID_MAX:
165 raise errors.OpPrereqError(
166 "Higher user-id boundary value (%s)"
167 " is larger than UIDPOOL_UID_MAX (%s)." %
168 (higher, constants.UIDPOOL_UID_MAX),
172 def ExpandUidPool(uid_pool):
173 """Expands a uid-pool definition to a list of uids.
175 @param uid_pool: a list of integer pairs (lower, higher range boundaries)
176 @return: a list of integers
180 for lower, higher in uid_pool:
181 uids.update(range(lower, higher + 1))
186 """Check if there is any process in the system running with the given user-id
189 @param uid: the user-id to be checked.
192 pgrep_command = [constants.PGREP, "-u", uid]
193 result = utils.RunCmd(pgrep_command)
195 if result.exit_code == 0:
197 elif result.exit_code == 1:
200 raise errors.CommandError("Running pgrep failed. exit code: %s"
204 class LockedUid(object):
205 """Class representing a locked user-id in the uid-pool.
207 This binds together a userid and a lock.
210 def __init__(self, uid, lock):
213 @param uid: a user-id
214 @param lock: a utils.FileLock object
221 # Release the exclusive lock and close the filedescriptor
228 return "%s" % self._uid
231 def RequestUnusedUid(all_uids):
232 """Tries to find an unused uid from the uid-pool, locks it and returns it.
237 1. When starting a process::
239 from ganeti import ssconf
240 from ganeti import uidpool
242 # Get list of all user-ids in the uid-pool from ssconf
243 ss = ssconf.SimpleStore()
244 uid_pool = uidpool.ParseUidPool(ss.GetUidPool(), separator="\\n")
245 all_uids = set(uidpool.ExpandUidPool(uid_pool))
247 uid = uidpool.RequestUnusedUid(all_uids)
249 <start a process with the UID>
250 # Once the process is started, we can release the file lock
253 # Return the UID to the pool
254 uidpool.ReleaseUid(uid)
256 2. Stopping a process::
258 from ganeti import uidpool
260 uid = <get the UID the process is running under>
262 uidpool.ReleaseUid(uid)
264 @type all_uids: set of integers
265 @param all_uids: a set containing all the user-ids in the user-id pool
266 @return: a LockedUid object representing the unused uid. It's the caller's
267 responsibility to unlock the uid once an instance is started with
271 # Create the lock dir if it's not yet present
273 utils.EnsureDirs([(pathutils.UIDPOOL_LOCKDIR, 0755)])
274 except errors.GenericError, err:
275 raise errors.LockError("Failed to create user-id pool lock dir: %s" % err)
277 # Get list of currently used uids from the filesystem
280 for taken_uid in os.listdir(pathutils.UIDPOOL_LOCKDIR):
282 taken_uid = int(taken_uid)
283 except ValueError, err:
284 # Skip directory entries that can't be converted into an integer
286 taken_uids.add(taken_uid)
288 raise errors.LockError("Failed to get list of used user-ids: %s" % err)
290 # Filter out spurious entries from the directory listing
291 taken_uids = all_uids.intersection(taken_uids)
293 # Remove the list of used uids from the list of all uids
294 unused_uids = list(all_uids - taken_uids)
296 logging.info("All user-ids in the uid-pool are marked 'taken'")
298 # Randomize the order of the unused user-id list
299 random.shuffle(unused_uids)
301 # Randomize the order of the unused user-id list
302 taken_uids = list(taken_uids)
303 random.shuffle(taken_uids)
305 for uid in (unused_uids + taken_uids):
307 # Create the lock file
308 # Note: we don't care if it exists. Only the fact that we can
309 # (or can't) lock it later is what matters.
310 uid_path = utils.PathJoin(pathutils.UIDPOOL_LOCKDIR, str(uid))
311 lock = utils.FileLock.Open(uid_path)
313 raise errors.LockError("Failed to create lockfile for user-id %s: %s"
316 # Try acquiring an exclusive lock on the lock file
318 # Check if there is any process running with this user-id
320 logging.debug("There is already a process running under"
324 return LockedUid(uid, lock)
326 if err.errno == errno.EAGAIN:
327 # The file is already locked, let's skip it and try another unused uid
328 logging.debug("Lockfile for user-id is already locked %s: %s", uid, err)
330 except errors.LockError, err:
331 # There was an unexpected error while trying to lock the file
332 logging.error("Failed to lock the lockfile for user-id %s: %s", uid, err)
335 raise errors.LockError("Failed to find an unused user-id")
339 """This should be called when the given user-id is no longer in use.
341 @type uid: LockedUid or integer
342 @param uid: the uid to release back to the pool
345 if isinstance(uid, LockedUid):
346 # Make sure we release the exclusive lock, if there is any
348 uid_filename = uid.AsStr()
350 uid_filename = str(uid)
353 uid_path = utils.PathJoin(pathutils.UIDPOOL_LOCKDIR, uid_filename)
356 raise errors.LockError("Failed to remove user-id lockfile"
357 " for user-id %s: %s" % (uid_filename, err))
360 def ExecWithUnusedUid(fn, all_uids, *args, **kwargs):
361 """Execute a callable and provide an unused user-id in its kwargs.
363 This wrapper function provides a simple way to handle the requesting,
364 unlocking and releasing a user-id.
365 "fn" is called by passing a "uid" keyword argument that
366 contains an unused user-id (as an integer) selected from the set of user-ids
368 If there is an error while executing "fn", the user-id is returned
371 @param fn: a callable that accepts a keyword argument called "uid"
372 @type all_uids: a set of integers
373 @param all_uids: a set containing all user-ids in the user-id pool
376 uid = RequestUnusedUid(all_uids)
377 kwargs["uid"] = uid.GetUid()
379 return_value = fn(*args, **kwargs)
381 # The failure of "callabe" means that starting a process with the uid
382 # failed, so let's put the uid back into the pool.