1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
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
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
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
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
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
135
136
152
153
178
179
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
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
213 """Class representing a locked user-id in the uid-pool.
214
215 This binds together a userid and a lock.
216
217 """
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
229
230 self._lock.Close()
231
234
236 return "%s" % self._uid
237
238
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
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
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
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
299 taken_uids = all_uids.intersection(taken_uids)
300
301
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
307 random.shuffle(unused_uids)
308
309
310 taken_uids = list(taken_uids)
311 random.shuffle(taken_uids)
312
313 for uid in (unused_uids + taken_uids):
314 try:
315
316
317
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
325 lock.Exclusive()
326
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
336 logging.debug("Lockfile for user-id is already locked %s: %s", uid, err)
337 continue
338 except errors.LockError, err:
339
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
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
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
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
390
391 ReleaseUid(uid)
392 raise
393 uid.Unlock()
394 return return_value
395