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  # All rights reserved. 
  6  # 
  7  # Redistribution and use in source and binary forms, with or without 
  8  # modification, are permitted provided that the following conditions are 
  9  # met: 
 10  # 
 11  # 1. Redistributions of source code must retain the above copyright notice, 
 12  # this list of conditions and the following disclaimer. 
 13  # 
 14  # 2. Redistributions in binary form must reproduce the above copyright 
 15  # notice, this list of conditions and the following disclaimer in the 
 16  # documentation and/or other materials provided with the distribution. 
 17  # 
 18  # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS 
 19  # IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED 
 20  # TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 
 21  # PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR 
 22  # CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, 
 23  # EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, 
 24  # PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 
 25  # PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 
 26  # LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 
 27  # NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 
 28  # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 
 29   
 30   
 31  """User-id pool related functions. 
 32   
 33  The user-id pool is cluster-wide configuration option. 
 34  It is stored as a list of user-id ranges. 
 35  This module contains functions used for manipulating the 
 36  user-id pool parameter and for requesting/returning user-ids 
 37  from the pool. 
 38   
 39  """ 
 40   
 41  import errno 
 42  import logging 
 43  import os 
 44  import random 
 45   
 46  from ganeti import errors 
 47  from ganeti import constants 
 48  from ganeti import utils 
 49  from ganeti import pathutils 
 50   
 51   
52 -def ParseUidPool(value, separator=None):
53 """Parse a user-id pool definition. 54 55 @param value: string representation of the user-id pool. 56 The accepted input format is a list of integer ranges. 57 The boundaries are inclusive. 58 Example: '1000-5000,8000,9000-9010'. 59 @param separator: the separator character between the uids/uid-ranges. 60 Defaults to a comma. 61 @return: a list of integer pairs (lower, higher range boundaries) 62 63 """ 64 if separator is None: 65 separator = "," 66 67 ranges = [] 68 for range_def in value.split(separator): 69 if not range_def: 70 # Skip empty strings 71 continue 72 boundaries = range_def.split("-") 73 n_elements = len(boundaries) 74 if n_elements > 2: 75 raise errors.OpPrereqError( 76 "Invalid user-id range definition. Only one hyphen allowed: %s" 77 % boundaries, errors.ECODE_INVAL) 78 try: 79 lower = int(boundaries[0]) 80 except (ValueError, TypeError), err: 81 raise errors.OpPrereqError("Invalid user-id value for lower boundary of" 82 " user-id range: %s" 83 % str(err), errors.ECODE_INVAL) 84 try: 85 higher = int(boundaries[n_elements - 1]) 86 except (ValueError, TypeError), err: 87 raise errors.OpPrereqError("Invalid user-id value for higher boundary of" 88 " user-id range: %s" 89 % str(err), errors.ECODE_INVAL) 90 91 ranges.append((lower, higher)) 92 93 ranges.sort() 94 return ranges
95 96
97 -def AddToUidPool(uid_pool, add_uids):
98 """Add a list of user-ids/user-id ranges to a user-id pool. 99 100 @param uid_pool: a user-id pool (list of integer tuples) 101 @param add_uids: user-id ranges to be added to the pool 102 (list of integer tuples) 103 104 """ 105 for uid_range in add_uids: 106 if uid_range not in uid_pool: 107 uid_pool.append(uid_range) 108 uid_pool.sort()
109 110
111 -def RemoveFromUidPool(uid_pool, remove_uids):
112 """Remove a list of user-ids/user-id ranges from a user-id pool. 113 114 @param uid_pool: a user-id pool (list of integer tuples) 115 @param remove_uids: user-id ranges to be removed from the pool 116 (list of integer tuples) 117 118 """ 119 for uid_range in remove_uids: 120 if uid_range not in uid_pool: 121 raise errors.OpPrereqError( 122 "User-id range to be removed is not found in the current" 123 " user-id pool: %s" % str(uid_range), errors.ECODE_INVAL) 124 uid_pool.remove(uid_range)
125 126
127 -def _FormatUidRange(lower, higher):
128 """Convert a user-id range definition into a string. 129 130 """ 131 if lower == higher: 132 return str(lower) 133 134 return "%s-%s" % (lower, higher)
135 136
137 -def FormatUidPool(uid_pool, separator=None):
138 """Convert the internal representation of the user-id pool into a string. 139 140 The output format is also accepted by ParseUidPool() 141 142 @param uid_pool: a list of integer pairs representing UID ranges 143 @param separator: the separator character between the uids/uid-ranges. 144 Defaults to ", ". 145 @return: a string with the formatted results 146 147 """ 148 if separator is None: 149 separator = ", " 150 return separator.join([_FormatUidRange(lower, higher) 151 for lower, higher in uid_pool])
152 153
154 -def CheckUidPool(uid_pool):
155 """Sanity check user-id pool range definition values. 156 157 @param uid_pool: a list of integer pairs (lower, higher range boundaries) 158 159 """ 160 for lower, higher in uid_pool: 161 if lower > higher: 162 raise errors.OpPrereqError( 163 "Lower user-id range boundary value (%s)" 164 " is larger than higher boundary value (%s)" % 165 (lower, higher), errors.ECODE_INVAL) 166 if lower < constants.UIDPOOL_UID_MIN: 167 raise errors.OpPrereqError( 168 "Lower user-id range boundary value (%s)" 169 " is smaller than UIDPOOL_UID_MIN (%s)." % 170 (lower, constants.UIDPOOL_UID_MIN), 171 errors.ECODE_INVAL) 172 if higher > constants.UIDPOOL_UID_MAX: 173 raise errors.OpPrereqError( 174 "Higher user-id boundary value (%s)" 175 " is larger than UIDPOOL_UID_MAX (%s)." % 176 (higher, constants.UIDPOOL_UID_MAX), 177 errors.ECODE_INVAL)
178 179
180 -def ExpandUidPool(uid_pool):
181 """Expands a uid-pool definition to a list of uids. 182 183 @param uid_pool: a list of integer pairs (lower, higher range boundaries) 184 @return: a list of integers 185 186 """ 187 uids = set() 188 for lower, higher in uid_pool: 189 uids.update(range(lower, higher + 1)) 190 return list(uids)
191 192
193 -def _IsUidUsed(uid):
194 """Check if there is any process in the system running with the given user-id 195 196 @type uid: integer 197 @param uid: the user-id to be checked. 198 199 """ 200 pgrep_command = [constants.PGREP, "-u", uid] 201 result = utils.RunCmd(pgrep_command) 202 203 if result.exit_code == 0: 204 return True 205 elif result.exit_code == 1: 206 return False 207 else: 208 raise errors.CommandError("Running pgrep failed. exit code: %s" 209 % result.exit_code)
210 211
212 -class LockedUid(object):
213 """Class representing a locked user-id in the uid-pool. 214 215 This binds together a userid and a lock. 216 217 """
218 - def __init__(self, uid, lock):
219 """Constructor 220 221 @param uid: a user-id 222 @param lock: a utils.FileLock object 223 224 """ 225 self._uid = uid 226 self._lock = lock
227
228 - def Unlock(self):
229 # Release the exclusive lock and close the filedescriptor 230 self._lock.Close()
231
232 - def GetUid(self):
233 return self._uid
234
235 - def AsStr(self):
236 return "%s" % self._uid
237 238
239 -def RequestUnusedUid(all_uids):
240 """Tries to find an unused uid from the uid-pool, locks it and returns it. 241 242 Usage pattern 243 ============= 244 245 1. When starting a process:: 246 247 from ganeti import ssconf 248 from ganeti import uidpool 249 250 # Get list of all user-ids in the uid-pool from ssconf 251 ss = ssconf.SimpleStore() 252 uid_pool = uidpool.ParseUidPool(ss.GetUidPool(), separator="\\n") 253 all_uids = set(uidpool.ExpandUidPool(uid_pool)) 254 255 uid = uidpool.RequestUnusedUid(all_uids) 256 try: 257 <start a process with the UID> 258 # Once the process is started, we can release the file lock 259 uid.Unlock() 260 except ..., err: 261 # Return the UID to the pool 262 uidpool.ReleaseUid(uid) 263 264 2. Stopping a process:: 265 266 from ganeti import uidpool 267 268 uid = <get the UID the process is running under> 269 <stop the process> 270 uidpool.ReleaseUid(uid) 271 272 @type all_uids: set of integers 273 @param all_uids: a set containing all the user-ids in the user-id pool 274 @return: a LockedUid object representing the unused uid. It's the caller's 275 responsibility to unlock the uid once an instance is started with 276 this uid. 277 278 """ 279 # Create the lock dir if it's not yet present 280 try: 281 utils.EnsureDirs([(pathutils.UIDPOOL_LOCKDIR, 0755)]) 282 except errors.GenericError, err: 283 raise errors.LockError("Failed to create user-id pool lock dir: %s" % err) 284 285 # Get list of currently used uids from the filesystem 286 try: 287 taken_uids = set() 288 for taken_uid in os.listdir(pathutils.UIDPOOL_LOCKDIR): 289 try: 290 taken_uid = int(taken_uid) 291 except ValueError, err: 292 # Skip directory entries that can't be converted into an integer 293 continue 294 taken_uids.add(taken_uid) 295 except OSError, err: 296 raise errors.LockError("Failed to get list of used user-ids: %s" % err) 297 298 # Filter out spurious entries from the directory listing 299 taken_uids = all_uids.intersection(taken_uids) 300 301 # Remove the list of used uids from the list of all uids 302 unused_uids = list(all_uids - taken_uids) 303 if not unused_uids: 304 logging.info("All user-ids in the uid-pool are marked 'taken'") 305 306 # Randomize the order of the unused user-id list 307 random.shuffle(unused_uids) 308 309 # Randomize the order of the unused user-id list 310 taken_uids = list(taken_uids) 311 random.shuffle(taken_uids) 312 313 for uid in (unused_uids + taken_uids): 314 try: 315 # Create the lock file 316 # Note: we don't care if it exists. Only the fact that we can 317 # (or can't) lock it later is what matters. 318 uid_path = utils.PathJoin(pathutils.UIDPOOL_LOCKDIR, str(uid)) 319 lock = utils.FileLock.Open(uid_path) 320 except OSError, err: 321 raise errors.LockError("Failed to create lockfile for user-id %s: %s" 322 % (uid, err)) 323 try: 324 # Try acquiring an exclusive lock on the lock file 325 lock.Exclusive() 326 # Check if there is any process running with this user-id 327 if _IsUidUsed(uid): 328 logging.debug("There is already a process running under" 329 " user-id %s", uid) 330 lock.Unlock() 331 continue 332 return LockedUid(uid, lock) 333 except IOError, err: 334 if err.errno == errno.EAGAIN: 335 # The file is already locked, let's skip it and try another unused uid 336 logging.debug("Lockfile for user-id is already locked %s: %s", uid, err) 337 continue 338 except errors.LockError, err: 339 # There was an unexpected error while trying to lock the file 340 logging.error("Failed to lock the lockfile for user-id %s: %s", uid, err) 341 raise 342 343 raise errors.LockError("Failed to find an unused user-id")
344 345
346 -def ReleaseUid(uid):
347 """This should be called when the given user-id is no longer in use. 348 349 @type uid: LockedUid or integer 350 @param uid: the uid to release back to the pool 351 352 """ 353 if isinstance(uid, LockedUid): 354 # Make sure we release the exclusive lock, if there is any 355 uid.Unlock() 356 uid_filename = uid.AsStr() 357 else: 358 uid_filename = str(uid) 359 360 try: 361 uid_path = utils.PathJoin(pathutils.UIDPOOL_LOCKDIR, uid_filename) 362 os.remove(uid_path) 363 except OSError, err: 364 raise errors.LockError("Failed to remove user-id lockfile" 365 " for user-id %s: %s" % (uid_filename, err))
366 367
368 -def ExecWithUnusedUid(fn, all_uids, *args, **kwargs):
369 """Execute a callable and provide an unused user-id in its kwargs. 370 371 This wrapper function provides a simple way to handle the requesting, 372 unlocking and releasing a user-id. 373 "fn" is called by passing a "uid" keyword argument that 374 contains an unused user-id (as an integer) selected from the set of user-ids 375 passed in all_uids. 376 If there is an error while executing "fn", the user-id is returned 377 to the pool. 378 379 @param fn: a callable that accepts a keyword argument called "uid" 380 @type all_uids: a set of integers 381 @param all_uids: a set containing all user-ids in the user-id pool 382 383 """ 384 uid = RequestUnusedUid(all_uids) 385 kwargs["uid"] = uid.GetUid() 386 try: 387 return_value = fn(*args, **kwargs) 388 except: 389 # The failure of "callabe" means that starting a process with the uid 390 # failed, so let's put the uid back into the pool. 391 ReleaseUid(uid) 392 raise 393 uid.Unlock() 394 return return_value
395