Statistics
| Branch: | Tag: | Revision:

root / lib / uidpool.py @ d3b790bb

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 __str__(self):
219
    return "%s" % self._uid
220

    
221

    
222
def RequestUnusedUid(all_uids):
223
  """Tries to find an unused uid from the uid-pool, locks it and returns it.
224

225
  Usage pattern
226
  =============
227

228
  1. When starting a process::
229

230
      from ganeti import ssconf
231
      from ganeti import uidpool
232

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

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

247
  2. Stopping a process::
248

249
      from ganeti import uidpool
250

251
      uid = <get the UID the process is running under>
252
      <stop the process>
253
      uidpool.ReleaseUid(uid)
254

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

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

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

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

    
280
  # Randomize the order of the unused user-id list
281
  random.shuffle(unused_uids)
282

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

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

    
317
  raise errors.LockError("Failed to find an unused user-id")
318

    
319

    
320
def ReleaseUid(uid):
321
  """This should be called when the given user-id is no longer in use.
322

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

    
333

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

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

345
  @param fn: a callable
346
  @param all_uids: a set containing all user-ids in the user-id pool
347

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