4 # Copyright (C) 2010 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 utils
42 def ParseUidPool(value, separator=None):
43 """Parse a user-id pool definition.
45 @param value: string representation of the user-id pool.
46 The accepted input format is a list of integer ranges.
47 The boundaries are inclusive.
48 Example: '1000-5000,8000,9000-9010'.
49 @param separator: the separator character between the uids/uid-ranges.
51 @return: a list of integer pairs (lower, higher range boundaries)
58 for range_def in value.split(separator):
62 boundaries = range_def.split("-")
63 n_elements = len(boundaries)
65 raise errors.OpPrereqError(
66 "Invalid user-id range definition. Only one hyphen allowed: %s"
69 lower = int(boundaries[0])
70 except (ValueError, TypeError), err:
71 raise errors.OpPrereqError("Invalid user-id value for lower boundary of"
73 % str(err), errors.ECODE_INVAL)
75 higher = int(boundaries[n_elements - 1])
76 except (ValueError, TypeError), err:
77 raise errors.OpPrereqError("Invalid user-id value for higher boundary of"
79 % str(err), errors.ECODE_INVAL)
81 ranges.append((lower, higher))
87 def AddToUidPool(uid_pool, add_uids):
88 """Add a list of user-ids/user-id ranges to a user-id pool.
90 @param uid_pool: a user-id pool (list of integer tuples)
91 @param add_uids: user-id ranges to be added to the pool
92 (list of integer tuples)
95 for uid_range in add_uids:
96 if uid_range not in uid_pool:
97 uid_pool.append(uid_range)
101 def RemoveFromUidPool(uid_pool, remove_uids):
102 """Remove a list of user-ids/user-id ranges from a user-id pool.
104 @param uid_pool: a user-id pool (list of integer tuples)
105 @param remove_uids: user-id ranges to be removed from the pool
106 (list of integer tuples)
109 for uid_range in remove_uids:
110 if uid_range not in uid_pool:
111 raise errors.OpPrereqError(
112 "User-id range to be removed is not found in the current"
113 " user-id pool: %s" % uid_range, errors.ECODE_INVAL)
114 uid_pool.remove(uid_range)
117 def _FormatUidRange(lower, higher):
118 """Convert a user-id range definition into a string.
123 return "%s-%s" % (lower, higher)
126 def FormatUidPool(uid_pool, separator=None):
127 """Convert the internal representation of the user-id pool into a string.
129 The output format is also accepted by ParseUidPool()
131 @param uid_pool: a list of integer pairs representing UID ranges
132 @param separator: the separator character between the uids/uid-ranges.
134 @return: a string with the formatted results
137 if separator is None:
139 return separator.join([_FormatUidRange(lower, higher)
140 for lower, higher in uid_pool])
143 def CheckUidPool(uid_pool):
144 """Sanity check user-id pool range definition values.
146 @param uid_pool: a list of integer pairs (lower, higher range boundaries)
149 for lower, higher in uid_pool:
151 raise errors.OpPrereqError(
152 "Lower user-id range boundary value (%s)"
153 " is larger than higher boundary value (%s)" %
154 (lower, higher), errors.ECODE_INVAL)
155 if lower < constants.UIDPOOL_UID_MIN:
156 raise errors.OpPrereqError(
157 "Lower user-id range boundary value (%s)"
158 " is smaller than UIDPOOL_UID_MIN (%s)." %
159 (lower, constants.UIDPOOL_UID_MIN),
161 if higher > constants.UIDPOOL_UID_MAX:
162 raise errors.OpPrereqError(
163 "Higher user-id boundary value (%s)"
164 " is larger than UIDPOOL_UID_MAX (%s)." %
165 (higher, constants.UIDPOOL_UID_MAX),
169 def ExpandUidPool(uid_pool):
170 """Expands a uid-pool definition to a list of uids.
172 @param uid_pool: a list of integer pairs (lower, higher range boundaries)
173 @return: a list of integers
177 for lower, higher in uid_pool:
178 uids.update(range(lower, higher + 1))
183 """Check if there is any process in the system running with the given user-id
186 @param uid: the user-id to be checked.
189 pgrep_command = [constants.PGREP, "-u", uid]
190 result = utils.RunCmd(pgrep_command)
192 if result.exit_code == 0:
194 elif result.exit_code == 1:
197 raise errors.CommandError("Running pgrep failed. exit code: %s"
201 class LockedUid(object):
202 """Class representing a locked user-id in the uid-pool.
204 This binds together a userid and a lock.
207 def __init__(self, uid, lock):
210 @param uid: a user-id
211 @param lock: a utils.FileLock object
218 # Release the exclusive lock and close the filedescriptor
225 return "%s" % self._uid
228 def RequestUnusedUid(all_uids):
229 """Tries to find an unused uid from the uid-pool, locks it and returns it.
234 1. When starting a process::
236 from ganeti import ssconf
237 from ganeti import uidpool
239 # Get list of all user-ids in the uid-pool from ssconf
240 ss = ssconf.SimpleStore()
241 uid_pool = uidpool.ParseUidPool(ss.GetUidPool(), separator="\\n")
242 all_uids = set(uidpool.ExpandUidPool(uid_pool))
244 uid = uidpool.RequestUnusedUid(all_uids)
246 <start a process with the UID>
247 # Once the process is started, we can release the file lock
250 # Return the UID to the pool
251 uidpool.ReleaseUid(uid)
253 2. Stopping a process::
255 from ganeti import uidpool
257 uid = <get the UID the process is running under>
259 uidpool.ReleaseUid(uid)
261 @type all_uids: set of integers
262 @param all_uids: a set containing all the user-ids in the user-id pool
263 @return: a LockedUid object representing the unused uid. It's the caller's
264 responsibility to unlock the uid once an instance is started with
268 # Create the lock dir if it's not yet present
270 utils.EnsureDirs([(constants.UIDPOOL_LOCKDIR, 0755)])
271 except errors.GenericError, err:
272 raise errors.LockError("Failed to create user-id pool lock dir: %s" % err)
274 # Get list of currently used uids from the filesystem
277 for taken_uid in os.listdir(constants.UIDPOOL_LOCKDIR):
279 taken_uid = int(taken_uid)
280 except ValueError, err:
281 # Skip directory entries that can't be converted into an integer
283 taken_uids.add(taken_uid)
285 raise errors.LockError("Failed to get list of used user-ids: %s" % err)
287 # Filter out spurious entries from the directory listing
288 taken_uids = all_uids.intersection(taken_uids)
290 # Remove the list of used uids from the list of all uids
291 unused_uids = list(all_uids - taken_uids)
293 logging.info("All user-ids in the uid-pool are marked 'taken'")
295 # Randomize the order of the unused user-id list
296 random.shuffle(unused_uids)
298 # Randomize the order of the unused user-id list
299 taken_uids = list(taken_uids)
300 random.shuffle(taken_uids)
302 for uid in (unused_uids + taken_uids):
304 # Create the lock file
305 # Note: we don't care if it exists. Only the fact that we can
306 # (or can't) lock it later is what matters.
307 uid_path = utils.PathJoin(constants.UIDPOOL_LOCKDIR, str(uid))
308 lock = utils.FileLock.Open(uid_path)
310 raise errors.LockError("Failed to create lockfile for user-id %s: %s"
313 # Try acquiring an exclusive lock on the lock file
315 # Check if there is any process running with this user-id
317 logging.debug("There is already a process running under"
321 return LockedUid(uid, lock)
323 if err.errno == errno.EAGAIN:
324 # The file is already locked, let's skip it and try another unused uid
325 logging.debug("Lockfile for user-id is already locked %s: %s", uid, err)
327 except errors.LockError, err:
328 # There was an unexpected error while trying to lock the file
329 logging.error("Failed to lock the lockfile for user-id %s: %s", uid, err)
332 raise errors.LockError("Failed to find an unused user-id")
336 """This should be called when the given user-id is no longer in use.
338 @type uid: LockedUid or integer
339 @param uid: the uid to release back to the pool
342 if isinstance(uid, LockedUid):
343 # Make sure we release the exclusive lock, if there is any
345 uid_filename = uid.AsStr()
347 uid_filename = str(uid)
350 uid_path = utils.PathJoin(constants.UIDPOOL_LOCKDIR, uid_filename)
353 raise errors.LockError("Failed to remove user-id lockfile"
354 " for user-id %s: %s" % (uid_filename, err))
357 def ExecWithUnusedUid(fn, all_uids, *args, **kwargs):
358 """Execute a callable and provide an unused user-id in its kwargs.
360 This wrapper function provides a simple way to handle the requesting,
361 unlocking and releasing a user-id.
362 "fn" is called by passing a "uid" keyword argument that
363 contains an unused user-id (as an integer) selected from the set of user-ids
365 If there is an error while executing "fn", the user-id is returned
368 @param fn: a callable that accepts a keyword argument called "uid"
369 @type all_uids: a set of integers
370 @param all_uids: a set containing all user-ids in the user-id pool
373 uid = RequestUnusedUid(all_uids)
374 kwargs["uid"] = uid.GetUid()
376 return_value = fn(*args, **kwargs)
378 # The failure of "callabe" means that starting a process with the uid
379 # failed, so let's put the uid back into the pool.