Statistics
| Branch: | Tag: | Revision:

root / lib / uidpool.py @ c7e4b037

History | View | Annotate | Download (11.6 kB)

1
#
2
#
3

    
4
# Copyright (C) 2010 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 utils
40

    
41

    
42
def ParseUidPool(value, separator=None):
43
  """Parse a user-id pool definition.
44

45
  @param value: string representation of the user-id pool.
46
                The accepted input format is a list of integer ranges.
47
                The boundaries are inclusive.
48
                Example: '1000-5000,8000,9000-9010'.
49
  @param separator: the separator character between the uids/uid-ranges.
50
                    Defaults to a comma.
51
  @return: a list of integer pairs (lower, higher range boundaries)
52

53
  """
54
  if separator is None:
55
    separator = ","
56

    
57
  ranges = []
58
  for range_def in value.split(separator):
59
    if not range_def:
60
      # Skip empty strings
61
      continue
62
    boundaries = range_def.split("-")
63
    n_elements = len(boundaries)
64
    if n_elements > 2:
65
      raise errors.OpPrereqError(
66
          "Invalid user-id range definition. Only one hyphen allowed: %s"
67
          % boundaries)
68
    try:
69
      lower = int(boundaries[0])
70
    except (ValueError, TypeError), err:
71
      raise errors.OpPrereqError("Invalid user-id value for lower boundary of"
72
                                 " user-id range: %s"
73
                                 % str(err), errors.ECODE_INVAL)
74
    try:
75
      higher = int(boundaries[n_elements - 1])
76
    except (ValueError, TypeError), err:
77
      raise errors.OpPrereqError("Invalid user-id value for higher boundary of"
78
                                 " user-id range: %s"
79
                                 % str(err), errors.ECODE_INVAL)
80

    
81
    ranges.append((lower, higher))
82

    
83
  ranges.sort()
84
  return ranges
85

    
86

    
87
def AddToUidPool(uid_pool, add_uids):
88
  """Add a list of user-ids/user-id ranges to a user-id pool.
89

90
  @param uid_pool: a user-id pool (list of integer tuples)
91
  @param add_uids: user-id ranges to be added to the pool
92
                   (list of integer tuples)
93

94
  """
95
  for uid_range in add_uids:
96
    if uid_range not in uid_pool:
97
      uid_pool.append(uid_range)
98
  uid_pool.sort()
99

    
100

    
101
def RemoveFromUidPool(uid_pool, remove_uids):
102
  """Remove a list of user-ids/user-id ranges from a user-id pool.
103

104
  @param uid_pool: a user-id pool (list of integer tuples)
105
  @param remove_uids: user-id ranges to be removed from the pool
106
                      (list of integer tuples)
107

108
  """
109
  for uid_range in remove_uids:
110
    if uid_range not in uid_pool:
111
      raise errors.OpPrereqError(
112
          "User-id range to be removed is not found in the current"
113
          " user-id pool: %s" % uid_range, errors.ECODE_INVAL)
114
    uid_pool.remove(uid_range)
115

    
116

    
117
def _FormatUidRange(lower, higher):
118
  """Convert a user-id range definition into a string.
119

120
  """
121
  if lower == higher:
122
    return str(lower)
123
  return "%s-%s" % (lower, higher)
124

    
125

    
126
def FormatUidPool(uid_pool, separator=None):
127
  """Convert the internal representation of the user-id pool into a string.
128

129
  The output format is also accepted by ParseUidPool()
130

131
  @param uid_pool: a list of integer pairs representing UID ranges
132
  @param separator: the separator character between the uids/uid-ranges.
133
                    Defaults to ", ".
134
  @return: a string with the formatted results
135

136
  """
137
  if separator is None:
138
    separator = ", "
139
  return separator.join([_FormatUidRange(lower, higher)
140
                         for lower, higher in uid_pool])
141

    
142

    
143
def CheckUidPool(uid_pool):
144
  """Sanity check user-id pool range definition values.
145

146
  @param uid_pool: a list of integer pairs (lower, higher range boundaries)
147

148
  """
149
  for lower, higher in uid_pool:
150
    if lower > higher:
151
      raise errors.OpPrereqError(
152
          "Lower user-id range boundary value (%s)"
153
          " is larger than higher boundary value (%s)" %
154
          (lower, higher), errors.ECODE_INVAL)
155
    if lower < constants.UIDPOOL_UID_MIN:
156
      raise errors.OpPrereqError(
157
          "Lower user-id range boundary value (%s)"
158
          " is smaller than UIDPOOL_UID_MIN (%s)." %
159
          (lower, constants.UIDPOOL_UID_MIN),
160
          errors.ECODE_INVAL)
161
    if higher > constants.UIDPOOL_UID_MAX:
162
      raise errors.OpPrereqError(
163
          "Higher user-id boundary value (%s)"
164
          " is larger than UIDPOOL_UID_MAX (%s)." %
165
          (higher, constants.UIDPOOL_UID_MAX),
166
          errors.ECODE_INVAL)
167

    
168

    
169
def ExpandUidPool(uid_pool):
170
  """Expands a uid-pool definition to a list of uids.
171

172
  @param uid_pool: a list of integer pairs (lower, higher range boundaries)
173
  @return: a list of integers
174

175
  """
176
  uids = set()
177
  for lower, higher in uid_pool:
178
    uids.update(range(lower, higher + 1))
179
  return list(uids)
180

    
181

    
182
def _IsUidUsed(uid):
183
  """Check if there is any process in the system running with the given user-id
184

185
  @type uid: integer
186
  @param uid: the user-id to be checked.
187

188
  """
189
  pgrep_command = [constants.PGREP, "-u", uid]
190
  result = utils.RunCmd(pgrep_command)
191

    
192
  if result.exit_code == 0:
193
    return True
194
  elif result.exit_code == 1:
195
    return False
196
  else:
197
    raise errors.CommandError("Running pgrep failed. exit code: %s"
198
                              % result.exit_code)
199

    
200

    
201
class LockedUid(object):
202
  """Class representing a locked user-id in the uid-pool.
203

204
  This binds together a userid and a lock.
205

206
  """
207
  def __init__(self, uid, lock):
208
    """Constructor
209

210
    @param uid: a user-id
211
    @param lock: a utils.FileLock object
212

213
    """
214
    self._uid = uid
215
    self._lock = lock
216

    
217
  def Unlock(self):
218
    # Release the exclusive lock and close the filedescriptor
219
    self._lock.Close()
220

    
221
  def GetUid(self):
222
    return self._uid
223

    
224
  def AsStr(self):
225
    return "%s" % self._uid
226

    
227

    
228
def RequestUnusedUid(all_uids):
229
  """Tries to find an unused uid from the uid-pool, locks it and returns it.
230

231
  Usage pattern
232
  =============
233

234
  1. When starting a process::
235

236
      from ganeti import ssconf
237
      from ganeti import uidpool
238

239
      # Get list of all user-ids in the uid-pool from ssconf
240
      ss = ssconf.SimpleStore()
241
      uid_pool = uidpool.ParseUidPool(ss.GetUidPool(), separator="\\n")
242
      all_uids = set(uidpool.ExpandUidPool(uid_pool))
243

244
      uid = uidpool.RequestUnusedUid(all_uids)
245
      try:
246
        <start a process with the UID>
247
        # Once the process is started, we can release the file lock
248
        uid.Unlock()
249
      except ..., err:
250
        # Return the UID to the pool
251
        uidpool.ReleaseUid(uid)
252

253
  2. Stopping a process::
254

255
      from ganeti import uidpool
256

257
      uid = <get the UID the process is running under>
258
      <stop the process>
259
      uidpool.ReleaseUid(uid)
260

261
  @type all_uids: set of integers
262
  @param all_uids: a set containing all the user-ids in the user-id pool
263
  @return: a LockedUid object representing the unused uid. It's the caller's
264
           responsibility to unlock the uid once an instance is started with
265
           this uid.
266

267
  """
268
  # Create the lock dir if it's not yet present
269
  try:
270
    utils.EnsureDirs([(constants.UIDPOOL_LOCKDIR, 0755)])
271
  except errors.GenericError, err:
272
    raise errors.LockError("Failed to create user-id pool lock dir: %s" % err)
273

    
274
  # Get list of currently used uids from the filesystem
275
  try:
276
    taken_uids = set()
277
    for taken_uid in os.listdir(constants.UIDPOOL_LOCKDIR):
278
      try:
279
        taken_uid = int(taken_uid)
280
      except ValueError, err:
281
        # Skip directory entries that can't be converted into an integer
282
        continue
283
      taken_uids.add(taken_uid)
284
  except OSError, err:
285
    raise errors.LockError("Failed to get list of used user-ids: %s" % err)
286

    
287
  # Filter out spurious entries from the directory listing
288
  taken_uids = all_uids.intersection(taken_uids)
289

    
290
  # Remove the list of used uids from the list of all uids
291
  unused_uids = list(all_uids - taken_uids)
292
  if not unused_uids:
293
    logging.info("All user-ids in the uid-pool are marked 'taken'")
294

    
295
  # Randomize the order of the unused user-id list
296
  random.shuffle(unused_uids)
297

    
298
  # Randomize the order of the unused user-id list
299
  taken_uids = list(taken_uids)
300
  random.shuffle(taken_uids)
301

    
302
  for uid in (unused_uids + taken_uids):
303
    try:
304
      # Create the lock file
305
      # Note: we don't care if it exists. Only the fact that we can
306
      # (or can't) lock it later is what matters.
307
      uid_path = utils.PathJoin(constants.UIDPOOL_LOCKDIR, str(uid))
308
      lock = utils.FileLock.Open(uid_path)
309
    except OSError, err:
310
      raise errors.LockError("Failed to create lockfile for user-id %s: %s"
311
                             % (uid, err))
312
    try:
313
      # Try acquiring an exclusive lock on the lock file
314
      lock.Exclusive()
315
      # Check if there is any process running with this user-id
316
      if _IsUidUsed(uid):
317
        logging.debug("There is already a process running under"
318
                      " user-id %s", uid)
319
        lock.Unlock()
320
        continue
321
      return LockedUid(uid, lock)
322
    except IOError, err:
323
      if err.errno == errno.EAGAIN:
324
        # The file is already locked, let's skip it and try another unused uid
325
        logging.debug("Lockfile for user-id is already locked %s: %s", uid, err)
326
        continue
327
    except errors.LockError, err:
328
      # There was an unexpected error while trying to lock the file
329
      logging.error("Failed to lock the lockfile for user-id %s: %s", uid, err)
330
      raise
331

    
332
  raise errors.LockError("Failed to find an unused user-id")
333

    
334

    
335
def ReleaseUid(uid):
336
  """This should be called when the given user-id is no longer in use.
337

338
  @type uid: LockedUid or integer
339
  @param uid: the uid to release back to the pool
340

341
  """
342
  if isinstance(uid, LockedUid):
343
    # Make sure we release the exclusive lock, if there is any
344
    uid.Unlock()
345
    uid_filename = uid.AsStr()
346
  else:
347
    uid_filename = str(uid)
348

    
349
  try:
350
    uid_path = utils.PathJoin(constants.UIDPOOL_LOCKDIR, uid_filename)
351
    os.remove(uid_path)
352
  except OSError, err:
353
    raise errors.LockError("Failed to remove user-id lockfile"
354
                           " for user-id %s: %s" % (uid_filename, err))
355

    
356

    
357
def ExecWithUnusedUid(fn, all_uids, *args, **kwargs):
358
  """Execute a callable and provide an unused user-id in its kwargs.
359

360
  This wrapper function provides a simple way to handle the requesting,
361
  unlocking and releasing a user-id.
362
  "fn" is called by passing a "uid" keyword argument that
363
  contains an unused user-id (as an integer) selected from the set of user-ids
364
  passed in all_uids.
365
  If there is an error while executing "fn", the user-id is returned
366
  to the pool.
367

368
  @param fn: a callable that accepts a keyword argument called "uid"
369
  @type all_uids: a set of integers
370
  @param all_uids: a set containing all user-ids in the user-id pool
371

372
  """
373
  uid = RequestUnusedUid(all_uids)
374
  kwargs["uid"] = uid.GetUid()
375
  try:
376
    return_value = fn(*args, **kwargs)
377
  except:
378
    # The failure of "callabe" means that starting a process with the uid
379
    # failed, so let's put the uid back into the pool.
380
    ReleaseUid(uid)
381
    raise
382
  uid.Unlock()
383
  return return_value