Merge branch 'stable-2.16' into stable-2.17
[ganeti-github.git] / lib / uidpool.py
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