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
43 def ParseUidPool(value, separator=None):
44 """Parse a user-id pool definition.
46 @param value: string representation of the user-id pool.
47 The accepted input format is a list of integer ranges.
48 The boundaries are inclusive.
49 Example: '1000-5000,8000,9000-9010'.
50 @param separator: the separator character between the uids/uid-ranges.
52 @return: a list of integer pairs (lower, higher range boundaries)
59 for range_def in value.split(separator):
63 boundaries = range_def.split("-")
64 n_elements = len(boundaries)
66 raise errors.OpPrereqError(
67 "Invalid user-id range definition. Only one hyphen allowed: %s"
68 % boundaries, errors.ECODE_INVAL)
70 lower = int(boundaries[0])
71 except (ValueError, TypeError), err:
72 raise errors.OpPrereqError("Invalid user-id value for lower boundary of"
74 % str(err), errors.ECODE_INVAL)
76 higher = int(boundaries[n_elements - 1])
77 except (ValueError, TypeError), err:
78 raise errors.OpPrereqError("Invalid user-id value for higher boundary of"
80 % str(err), errors.ECODE_INVAL)
82 ranges.append((lower, higher))
88 def AddToUidPool(uid_pool, add_uids):
89 """Add a list of user-ids/user-id ranges to a user-id pool.
91 @param uid_pool: a user-id pool (list of integer tuples)
92 @param add_uids: user-id ranges to be added to the pool
93 (list of integer tuples)
96 for uid_range in add_uids:
97 if uid_range not in uid_pool:
98 uid_pool.append(uid_range)
102 def RemoveFromUidPool(uid_pool, remove_uids):
103 """Remove a list of user-ids/user-id ranges from a user-id pool.
105 @param uid_pool: a user-id pool (list of integer tuples)
106 @param remove_uids: user-id ranges to be removed from the pool
107 (list of integer tuples)
110 for uid_range in remove_uids:
111 if uid_range not in uid_pool:
112 raise errors.OpPrereqError(
113 "User-id range to be removed is not found in the current"
114 " user-id pool: %s" % uid_range, errors.ECODE_INVAL)
115 uid_pool.remove(uid_range)
118 def _FormatUidRange(lower, higher, roman=False):
119 """Convert a user-id range definition into a string.
123 return str(compat.TryToRoman(lower, convert=roman))
124 return "%s-%s" % (compat.TryToRoman(lower, convert=roman),
125 compat.TryToRoman(higher, convert=roman))
128 def FormatUidPool(uid_pool, separator=None, roman=False):
129 """Convert the internal representation of the user-id pool into a string.
131 The output format is also accepted by ParseUidPool()
133 @param uid_pool: a list of integer pairs representing UID ranges
134 @param separator: the separator character between the uids/uid-ranges.
136 @return: a string with the formatted results
139 if separator is None:
141 return separator.join([_FormatUidRange(lower, higher, roman=roman)
142 for lower, higher in uid_pool])
145 def CheckUidPool(uid_pool):
146 """Sanity check user-id pool range definition values.
148 @param uid_pool: a list of integer pairs (lower, higher range boundaries)
151 for lower, higher in uid_pool:
153 raise errors.OpPrereqError(
154 "Lower user-id range boundary value (%s)"
155 " is larger than higher boundary value (%s)" %
156 (lower, higher), errors.ECODE_INVAL)
157 if lower < constants.UIDPOOL_UID_MIN:
158 raise errors.OpPrereqError(
159 "Lower user-id range boundary value (%s)"
160 " is smaller than UIDPOOL_UID_MIN (%s)." %
161 (lower, constants.UIDPOOL_UID_MIN),
163 if higher > constants.UIDPOOL_UID_MAX:
164 raise errors.OpPrereqError(
165 "Higher user-id boundary value (%s)"
166 " is larger than UIDPOOL_UID_MAX (%s)." %
167 (higher, constants.UIDPOOL_UID_MAX),
171 def ExpandUidPool(uid_pool):
172 """Expands a uid-pool definition to a list of uids.
174 @param uid_pool: a list of integer pairs (lower, higher range boundaries)
175 @return: a list of integers
179 for lower, higher in uid_pool:
180 uids.update(range(lower, higher + 1))
185 """Check if there is any process in the system running with the given user-id
188 @param uid: the user-id to be checked.
191 pgrep_command = [constants.PGREP, "-u", uid]
192 result = utils.RunCmd(pgrep_command)
194 if result.exit_code == 0:
196 elif result.exit_code == 1:
199 raise errors.CommandError("Running pgrep failed. exit code: %s"
203 class LockedUid(object):
204 """Class representing a locked user-id in the uid-pool.
206 This binds together a userid and a lock.
209 def __init__(self, uid, lock):
212 @param uid: a user-id
213 @param lock: a utils.FileLock object
220 # Release the exclusive lock and close the filedescriptor
227 return "%s" % self._uid
230 def RequestUnusedUid(all_uids):
231 """Tries to find an unused uid from the uid-pool, locks it and returns it.
236 1. When starting a process::
238 from ganeti import ssconf
239 from ganeti import uidpool
241 # Get list of all user-ids in the uid-pool from ssconf
242 ss = ssconf.SimpleStore()
243 uid_pool = uidpool.ParseUidPool(ss.GetUidPool(), separator="\\n")
244 all_uids = set(uidpool.ExpandUidPool(uid_pool))
246 uid = uidpool.RequestUnusedUid(all_uids)
248 <start a process with the UID>
249 # Once the process is started, we can release the file lock
252 # Return the UID to the pool
253 uidpool.ReleaseUid(uid)
255 2. Stopping a process::
257 from ganeti import uidpool
259 uid = <get the UID the process is running under>
261 uidpool.ReleaseUid(uid)
263 @type all_uids: set of integers
264 @param all_uids: a set containing all the user-ids in the user-id pool
265 @return: a LockedUid object representing the unused uid. It's the caller's
266 responsibility to unlock the uid once an instance is started with
270 # Create the lock dir if it's not yet present
272 utils.EnsureDirs([(constants.UIDPOOL_LOCKDIR, 0755)])
273 except errors.GenericError, err:
274 raise errors.LockError("Failed to create user-id pool lock dir: %s" % err)
276 # Get list of currently used uids from the filesystem
279 for taken_uid in os.listdir(constants.UIDPOOL_LOCKDIR):
281 taken_uid = int(taken_uid)
282 except ValueError, err:
283 # Skip directory entries that can't be converted into an integer
285 taken_uids.add(taken_uid)
287 raise errors.LockError("Failed to get list of used user-ids: %s" % err)
289 # Filter out spurious entries from the directory listing
290 taken_uids = all_uids.intersection(taken_uids)
292 # Remove the list of used uids from the list of all uids
293 unused_uids = list(all_uids - taken_uids)
295 logging.info("All user-ids in the uid-pool are marked 'taken'")
297 # Randomize the order of the unused user-id list
298 random.shuffle(unused_uids)
300 # Randomize the order of the unused user-id list
301 taken_uids = list(taken_uids)
302 random.shuffle(taken_uids)
304 for uid in (unused_uids + taken_uids):
306 # Create the lock file
307 # Note: we don't care if it exists. Only the fact that we can
308 # (or can't) lock it later is what matters.
309 uid_path = utils.PathJoin(constants.UIDPOOL_LOCKDIR, str(uid))
310 lock = utils.FileLock.Open(uid_path)
312 raise errors.LockError("Failed to create lockfile for user-id %s: %s"
315 # Try acquiring an exclusive lock on the lock file
317 # Check if there is any process running with this user-id
319 logging.debug("There is already a process running under"
323 return LockedUid(uid, lock)
325 if err.errno == errno.EAGAIN:
326 # The file is already locked, let's skip it and try another unused uid
327 logging.debug("Lockfile for user-id is already locked %s: %s", uid, err)
329 except errors.LockError, err:
330 # There was an unexpected error while trying to lock the file
331 logging.error("Failed to lock the lockfile for user-id %s: %s", uid, err)
334 raise errors.LockError("Failed to find an unused user-id")
338 """This should be called when the given user-id is no longer in use.
340 @type uid: LockedUid or integer
341 @param uid: the uid to release back to the pool
344 if isinstance(uid, LockedUid):
345 # Make sure we release the exclusive lock, if there is any
347 uid_filename = uid.AsStr()
349 uid_filename = str(uid)
352 uid_path = utils.PathJoin(constants.UIDPOOL_LOCKDIR, uid_filename)
355 raise errors.LockError("Failed to remove user-id lockfile"
356 " for user-id %s: %s" % (uid_filename, err))
359 def ExecWithUnusedUid(fn, all_uids, *args, **kwargs):
360 """Execute a callable and provide an unused user-id in its kwargs.
362 This wrapper function provides a simple way to handle the requesting,
363 unlocking and releasing a user-id.
364 "fn" is called by passing a "uid" keyword argument that
365 contains an unused user-id (as an integer) selected from the set of user-ids
367 If there is an error while executing "fn", the user-id is returned
370 @param fn: a callable that accepts a keyword argument called "uid"
371 @type all_uids: a set of integers
372 @param all_uids: a set containing all user-ids in the user-id pool
375 uid = RequestUnusedUid(all_uids)
376 kwargs["uid"] = uid.GetUid()
378 return_value = fn(*args, **kwargs)
380 # The failure of "callabe" means that starting a process with the uid
381 # failed, so let's put the uid back into the pool.