1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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 compat
40 from ganeti import utils
41
42
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
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)
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
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
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" % uid_range, errors.ECODE_INVAL)
115 uid_pool.remove(uid_range)
116
117
126
127
143
144
169
170
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
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
204 """Class representing a locked user-id in the uid-pool.
205
206 This binds together a userid and a lock.
207
208 """
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
220
221 self._lock.Close()
222
225
227 return "%s" % self._uid
228
229
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
271 try:
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)
275
276
277 try:
278 taken_uids = set()
279 for taken_uid in os.listdir(constants.UIDPOOL_LOCKDIR):
280 try:
281 taken_uid = int(taken_uid)
282 except ValueError, err:
283
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
290 taken_uids = all_uids.intersection(taken_uids)
291
292
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
298 random.shuffle(unused_uids)
299
300
301 taken_uids = list(taken_uids)
302 random.shuffle(taken_uids)
303
304 for uid in (unused_uids + taken_uids):
305 try:
306
307
308
309 uid_path = utils.PathJoin(constants.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
316 lock.Exclusive()
317
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
327 logging.debug("Lockfile for user-id is already locked %s: %s", uid, err)
328 continue
329 except errors.LockError, err:
330
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
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
346 uid.Unlock()
347 uid_filename = uid.AsStr()
348 else:
349 uid_filename = str(uid)
350
351 try:
352 uid_path = utils.PathJoin(constants.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
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
381
382 ReleaseUid(uid)
383 raise
384 uid.Unlock()
385 return return_value
386