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