Statistics
| Branch: | Tag: | Revision:

root / lib / uidpool.py @ 1a2eb2dc

History | View | Annotate | Download (11.8 kB)

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