Package ganeti :: Module uidpool
[hide private]
[frames] | no frames]

Source Code for Module ganeti.uidpool

  1  # 
  2  # 
  3   
  4  # Copyright (C) 2010, 2012 Google Inc. 
  5  # 
  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. 
 10  # 
 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. 
 15  # 
 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 
 19  # 02110-1301, USA. 
 20   
 21   
 22  """User-id pool related functions. 
 23   
 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 
 28  from the pool. 
 29   
 30  """ 
 31   
 32  import errno 
 33  import logging 
 34  import os 
 35  import random 
 36   
 37  from ganeti import errors 
 38  from ganeti import constants 
 39  from ganeti import utils 
 40  from ganeti import pathutils 
 41   
 42   
43 -def ParseUidPool(value, separator=None):
44 """Parse a user-id pool definition. 45 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. 51 Defaults to a comma. 52 @return: a list of integer pairs (lower, higher range boundaries) 53 54 """ 55 if separator is None: 56 separator = "," 57 58 ranges = [] 59 for range_def in value.split(separator): 60 if not range_def: 61 # Skip empty strings 62 continue 63 boundaries = range_def.split("-") 64 n_elements = len(boundaries) 65 if n_elements > 2: 66 raise errors.OpPrereqError( 67 "Invalid user-id range definition. Only one hyphen allowed: %s" 68 % boundaries, errors.ECODE_INVAL) 69 try: 70 lower = int(boundaries[0]) 71 except (ValueError, TypeError), err: 72 raise errors.OpPrereqError("Invalid user-id value for lower boundary of" 73 " user-id range: %s" 74 % str(err), errors.ECODE_INVAL) 75 try: 76 higher = int(boundaries[n_elements - 1]) 77 except (ValueError, TypeError), err: 78 raise errors.OpPrereqError("Invalid user-id value for higher boundary of" 79 " user-id range: %s" 80 % str(err), errors.ECODE_INVAL) 81 82 ranges.append((lower, higher)) 83 84 ranges.sort() 85 return ranges
86 87
88 -def AddToUidPool(uid_pool, add_uids):
89 """Add a list of user-ids/user-id ranges to a user-id pool. 90 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) 94 95 """ 96 for uid_range in add_uids: 97 if uid_range not in uid_pool: 98 uid_pool.append(uid_range) 99 uid_pool.sort()
100 101
102 -def RemoveFromUidPool(uid_pool, remove_uids):
103 """Remove a list of user-ids/user-id ranges from a user-id pool. 104 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) 108 109 """ 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" % str(uid_range), errors.ECODE_INVAL) 115 uid_pool.remove(uid_range)
116 117
118 -def _FormatUidRange(lower, higher):
119 """Convert a user-id range definition into a string. 120 121 """ 122 if lower == higher: 123 return str(lower) 124 125 return "%s-%s" % (lower, higher)
126 127
128 -def FormatUidPool(uid_pool, separator=None):
129 """Convert the internal representation of the user-id pool into a string. 130 131 The output format is also accepted by ParseUidPool() 132 133 @param uid_pool: a list of integer pairs representing UID ranges 134 @param separator: the separator character between the uids/uid-ranges. 135 Defaults to ", ". 136 @return: a string with the formatted results 137 138 """ 139 if separator is None: 140 separator = ", " 141 return separator.join([_FormatUidRange(lower, higher) 142 for lower, higher in uid_pool])
143 144
145 -def CheckUidPool(uid_pool):
146 """Sanity check user-id pool range definition values. 147 148 @param uid_pool: a list of integer pairs (lower, higher range boundaries) 149 150 """ 151 for lower, higher in uid_pool: 152 if lower > higher: 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), 162 errors.ECODE_INVAL) 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), 168 errors.ECODE_INVAL)
169 170
171 -def ExpandUidPool(uid_pool):
172 """Expands a uid-pool definition to a list of uids. 173 174 @param uid_pool: a list of integer pairs (lower, higher range boundaries) 175 @return: a list of integers 176 177 """ 178 uids = set() 179 for lower, higher in uid_pool: 180 uids.update(range(lower, higher + 1)) 181 return list(uids)
182 183
184 -def _IsUidUsed(uid):
185 """Check if there is any process in the system running with the given user-id 186 187 @type uid: integer 188 @param uid: the user-id to be checked. 189 190 """ 191 pgrep_command = [constants.PGREP, "-u", uid] 192 result = utils.RunCmd(pgrep_command) 193 194 if result.exit_code == 0: 195 return True 196 elif result.exit_code == 1: 197 return False 198 else: 199 raise errors.CommandError("Running pgrep failed. exit code: %s" 200 % result.exit_code)
201 202
203 -class LockedUid(object):
204 """Class representing a locked user-id in the uid-pool. 205 206 This binds together a userid and a lock. 207 208 """
209 - def __init__(self, uid, lock):
210 """Constructor 211 212 @param uid: a user-id 213 @param lock: a utils.FileLock object 214 215 """ 216 self._uid = uid 217 self._lock = lock
218
219 - def Unlock(self):
220 # Release the exclusive lock and close the filedescriptor 221 self._lock.Close()
222
223 - def GetUid(self):
224 return self._uid
225
226 - def AsStr(self):
227 return "%s" % self._uid
228 229
230 -def RequestUnusedUid(all_uids):
231 """Tries to find an unused uid from the uid-pool, locks it and returns it. 232 233 Usage pattern 234 ============= 235 236 1. When starting a process:: 237 238 from ganeti import ssconf 239 from ganeti import uidpool 240 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)) 245 246 uid = uidpool.RequestUnusedUid(all_uids) 247 try: 248 <start a process with the UID> 249 # Once the process is started, we can release the file lock 250 uid.Unlock() 251 except ..., err: 252 # Return the UID to the pool 253 uidpool.ReleaseUid(uid) 254 255 2. Stopping a process:: 256 257 from ganeti import uidpool 258 259 uid = <get the UID the process is running under> 260 <stop the process> 261 uidpool.ReleaseUid(uid) 262 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 267 this uid. 268 269 """ 270 # Create the lock dir if it's not yet present 271 try: 272 utils.EnsureDirs([(pathutils.UIDPOOL_LOCKDIR, 0755)]) 273 except errors.GenericError, err: 274 raise errors.LockError("Failed to create user-id pool lock dir: %s" % err) 275 276 # Get list of currently used uids from the filesystem 277 try: 278 taken_uids = set() 279 for taken_uid in os.listdir(pathutils.UIDPOOL_LOCKDIR): 280 try: 281 taken_uid = int(taken_uid) 282 except ValueError, err: 283 # Skip directory entries that can't be converted into an integer 284 continue 285 taken_uids.add(taken_uid) 286 except OSError, err: 287 raise errors.LockError("Failed to get list of used user-ids: %s" % err) 288 289 # Filter out spurious entries from the directory listing 290 taken_uids = all_uids.intersection(taken_uids) 291 292 # Remove the list of used uids from the list of all uids 293 unused_uids = list(all_uids - taken_uids) 294 if not unused_uids: 295 logging.info("All user-ids in the uid-pool are marked 'taken'") 296 297 # Randomize the order of the unused user-id list 298 random.shuffle(unused_uids) 299 300 # Randomize the order of the unused user-id list 301 taken_uids = list(taken_uids) 302 random.shuffle(taken_uids) 303 304 for uid in (unused_uids + taken_uids): 305 try: 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(pathutils.UIDPOOL_LOCKDIR, str(uid)) 310 lock = utils.FileLock.Open(uid_path) 311 except OSError, err: 312 raise errors.LockError("Failed to create lockfile for user-id %s: %s" 313 % (uid, err)) 314 try: 315 # Try acquiring an exclusive lock on the lock file 316 lock.Exclusive() 317 # Check if there is any process running with this user-id 318 if _IsUidUsed(uid): 319 logging.debug("There is already a process running under" 320 " user-id %s", uid) 321 lock.Unlock() 322 continue 323 return LockedUid(uid, lock) 324 except IOError, err: 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) 328 continue 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) 332 raise 333 334 raise errors.LockError("Failed to find an unused user-id")
335 336
337 -def ReleaseUid(uid):
338 """This should be called when the given user-id is no longer in use. 339 340 @type uid: LockedUid or integer 341 @param uid: the uid to release back to the pool 342 343 """ 344 if isinstance(uid, LockedUid): 345 # Make sure we release the exclusive lock, if there is any 346 uid.Unlock() 347 uid_filename = uid.AsStr() 348 else: 349 uid_filename = str(uid) 350 351 try: 352 uid_path = utils.PathJoin(pathutils.UIDPOOL_LOCKDIR, uid_filename) 353 os.remove(uid_path) 354 except OSError, err: 355 raise errors.LockError("Failed to remove user-id lockfile" 356 " for user-id %s: %s" % (uid_filename, err))
357 358
359 -def ExecWithUnusedUid(fn, all_uids, *args, **kwargs):
360 """Execute a callable and provide an unused user-id in its kwargs. 361 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 366 passed in all_uids. 367 If there is an error while executing "fn", the user-id is returned 368 to the pool. 369 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 373 374 """ 375 uid = RequestUnusedUid(all_uids) 376 kwargs["uid"] = uid.GetUid() 377 try: 378 return_value = fn(*args, **kwargs) 379 except: 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. 382 ReleaseUid(uid) 383 raise 384 uid.Unlock() 385 return return_value
386