Statistics
| Branch: | Tag: | Revision:

root / lib / uidpool.py @ 5833b7e6

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
    1) When starting a process
228

229
      from ganeti import ssconf
230
      from ganeti import uidpool
231

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

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

246
    2) Stopping a process
247

248
      from ganeti import uidpool
249

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

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

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

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

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

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

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

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

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

    
318

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

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

    
332

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

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

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

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