Statistics
| Branch: | Tag: | Revision:

root / lib / uidpool.py @ 14850c5e

History | View | Annotate | Download (11 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
  """
186
  pgrep_command = [constants.PGREP, "-u", uid]
187
  result = utils.RunCmd(pgrep_command)
188

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

    
197

    
198
class LockedUid(object):
199
  """Class representing a locked user-id in the uid-pool.
200

201
  This binds together a userid and a lock.
202

203
  """
204
  def __init__(self, uid, lock):
205
    """Constructor
206

207
    @param uid: a user-id
208
    @param lock: a utils.FileLock object
209

210
    """
211
    self._uid = uid
212
    self._lock = lock
213

    
214
  def Unlock(self):
215
    # Release the exclusive lock and close the filedescriptor
216
    self._lock.Close()
217

    
218
  def GetUid(self):
219
    return self._uid
220

    
221
  def __str__(self):
222
    return "%s" % self._uid
223

    
224

    
225
def RequestUnusedUid(all_uids):
226
  """Tries to find an unused uid from the uid-pool, locks it and returns it.
227

228
  Usage pattern
229
  =============
230

231
  1. When starting a process::
232

233
      from ganeti import ssconf
234
      from ganeti import uidpool
235

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

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

250
  2. Stopping a process::
251

252
      from ganeti import uidpool
253

254
      uid = <get the UID the process is running under>
255
      <stop the process>
256
      uidpool.ReleaseUid(uid)
257

258
  @param all_uids: a set containing all the user-ids in the user-id pool
259
  @return: a LockedUid object representing the unused uid. It's the caller's
260
           responsibility to unlock the uid once an instance is started with
261
           this uid.
262

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

    
270
  # Get list of currently used uids from the filesystem
271
  try:
272
    taken_uids = set(os.listdir(constants.UIDPOOL_LOCKDIR))
273
    # Filter out spurious entries from the directory listing
274
    taken_uids = all_uids.intersection(taken_uids)
275
  except OSError, err:
276
    raise errors.LockError("Failed to get list of used user-ids: %s" % err)
277

    
278
  # Remove the list of used uids from the list of all uids
279
  unused_uids = list(all_uids - taken_uids)
280
  if not unused_uids:
281
    logging.info("All user-ids in the uid-pool are marked 'taken'")
282

    
283
  # Randomize the order of the unused user-id list
284
  random.shuffle(unused_uids)
285

    
286
  # Randomize the order of the unused user-id list
287
  taken_uids = list(taken_uids)
288
  random.shuffle(taken_uids)
289

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

    
320
  raise errors.LockError("Failed to find an unused user-id")
321

    
322

    
323
def ReleaseUid(uid):
324
  """This should be called when the given user-id is no longer in use.
325

326
  """
327
  # Make sure we release the exclusive lock, if there is any
328
  uid.Unlock()
329
  try:
330
    uid_path = utils.PathJoin(constants.UIDPOOL_LOCKDIR, str(uid))
331
    os.remove(uid_path)
332
  except OSError, err:
333
    raise errors.LockError("Failed to remove user-id lockfile"
334
                           " for user-id %s: %s" % (uid, err))
335

    
336

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

340
  This wrapper function provides a simple way to handle the requesting,
341
  unlocking and releasing a user-id.
342
  "fn" is called by passing a "uid" keyword argument that
343
  contains an unused user-id (as a string) selected from the set of user-ids
344
  passed in all_uids.
345
  If there is an error while executing "fn", the user-id is returned
346
  to the pool.
347

348
  @param fn: a callable
349
  @param all_uids: a set containing all user-ids in the user-id pool
350

351
  """
352
  uid = RequestUnusedUid(all_uids)
353
  kwargs["uid"] = str(uid)
354
  try:
355
    return_value = fn(*args, **kwargs)
356
  except:
357
    # The failure of "callabe" means that starting a process with the uid
358
    # failed, so let's put the uid back into the pool.
359
    ReleaseUid(uid)
360
    raise
361
  uid.Unlock()
362
  return return_value