uidpool.py 11.8 KB
Newer Older
Balazs Lecz's avatar
Balazs Lecz committed
1
2
3
#
#

4
# Copyright (C) 2010, 2012 Google Inc.
Balazs Lecz's avatar
Balazs Lecz committed
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
# General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
# 02110-1301, USA.


"""User-id pool related functions.

The user-id pool is cluster-wide configuration option.
It is stored as a list of user-id ranges.
This module contains functions used for manipulating the
user-id pool parameter and for requesting/returning user-ids
from the pool.

"""

32
33
34
35
36
import errno
import logging
import os
import random

Balazs Lecz's avatar
Balazs Lecz committed
37
38
from ganeti import errors
from ganeti import constants
39
from ganeti import compat
40
from ganeti import utils
41
from ganeti import pathutils
Balazs Lecz's avatar
Balazs Lecz committed
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68


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

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

  """
  if separator is None:
    separator = ","

  ranges = []
  for range_def in value.split(separator):
    if not range_def:
      # Skip empty strings
      continue
    boundaries = range_def.split("-")
    n_elements = len(boundaries)
    if n_elements > 2:
      raise errors.OpPrereqError(
          "Invalid user-id range definition. Only one hyphen allowed: %s"
69
          % boundaries, errors.ECODE_INVAL)
Balazs Lecz's avatar
Balazs Lecz committed
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
    try:
      lower = int(boundaries[0])
    except (ValueError, TypeError), err:
      raise errors.OpPrereqError("Invalid user-id value for lower boundary of"
                                 " user-id range: %s"
                                 % str(err), errors.ECODE_INVAL)
    try:
      higher = int(boundaries[n_elements - 1])
    except (ValueError, TypeError), err:
      raise errors.OpPrereqError("Invalid user-id value for higher boundary of"
                                 " user-id range: %s"
                                 % str(err), errors.ECODE_INVAL)

    ranges.append((lower, higher))

  ranges.sort()
  return ranges


89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
def AddToUidPool(uid_pool, add_uids):
  """Add a list of user-ids/user-id ranges to a user-id pool.

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

  """
  for uid_range in add_uids:
    if uid_range not in uid_pool:
      uid_pool.append(uid_range)
  uid_pool.sort()


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

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

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


119
def _FormatUidRange(lower, higher, roman=False):
120
121
122
123
  """Convert a user-id range definition into a string.

  """
  if lower == higher:
124
125
126
    return str(compat.TryToRoman(lower, convert=roman))
  return "%s-%s" % (compat.TryToRoman(lower, convert=roman),
                    compat.TryToRoman(higher, convert=roman))
127
128


129
def FormatUidPool(uid_pool, separator=None, roman=False):
130
131
132
133
134
  """Convert the internal representation of the user-id pool into a string.

  The output format is also accepted by ParseUidPool()

  @param uid_pool: a list of integer pairs representing UID ranges
Balazs Lecz's avatar
Balazs Lecz committed
135
136
  @param separator: the separator character between the uids/uid-ranges.
                    Defaults to ", ".
137
138
139
  @return: a string with the formatted results

  """
Balazs Lecz's avatar
Balazs Lecz committed
140
141
  if separator is None:
    separator = ", "
142
  return separator.join([_FormatUidRange(lower, higher, roman=roman)
Balazs Lecz's avatar
Balazs Lecz committed
143
                         for lower, higher in uid_pool])
144
145


Balazs Lecz's avatar
Balazs Lecz committed
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
def CheckUidPool(uid_pool):
  """Sanity check user-id pool range definition values.

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

  """
  for lower, higher in uid_pool:
    if lower > higher:
      raise errors.OpPrereqError(
          "Lower user-id range boundary value (%s)"
          " is larger than higher boundary value (%s)" %
          (lower, higher), errors.ECODE_INVAL)
    if lower < constants.UIDPOOL_UID_MIN:
      raise errors.OpPrereqError(
          "Lower user-id range boundary value (%s)"
          " is smaller than UIDPOOL_UID_MIN (%s)." %
          (lower, constants.UIDPOOL_UID_MIN),
          errors.ECODE_INVAL)
    if higher > constants.UIDPOOL_UID_MAX:
      raise errors.OpPrereqError(
          "Higher user-id boundary value (%s)"
          " is larger than UIDPOOL_UID_MAX (%s)." %
          (higher, constants.UIDPOOL_UID_MAX),
          errors.ECODE_INVAL)


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

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

  """
  uids = set()
  for lower, higher in uid_pool:
    uids.update(range(lower, higher + 1))
  return list(uids)
183
184
185
186
187


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

188
189
190
  @type uid: integer
  @param uid: the user-id to be checked.

191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
  """
  pgrep_command = [constants.PGREP, "-u", uid]
  result = utils.RunCmd(pgrep_command)

  if result.exit_code == 0:
    return True
  elif result.exit_code == 1:
    return False
  else:
    raise errors.CommandError("Running pgrep failed. exit code: %s"
                              % result.exit_code)


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

  This binds together a userid and a lock.

  """
  def __init__(self, uid, lock):
    """Constructor

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

    """
    self._uid = uid
    self._lock = lock

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

Guido Trotter's avatar
Guido Trotter committed
224
225
226
  def GetUid(self):
    return self._uid

227
  def AsStr(self):
228
229
230
231
232
233
    return "%s" % self._uid


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

234
235
  Usage pattern
  =============
236

237
  1. When starting a process::
238
239
240
241
242
243

      from ganeti import ssconf
      from ganeti import uidpool

      # Get list of all user-ids in the uid-pool from ssconf
      ss = ssconf.SimpleStore()
244
      uid_pool = uidpool.ParseUidPool(ss.GetUidPool(), separator="\\n")
245
246
247
248
249
250
251
252
253
254
255
      all_uids = set(uidpool.ExpandUidPool(uid_pool))

      uid = uidpool.RequestUnusedUid(all_uids)
      try:
        <start a process with the UID>
        # Once the process is started, we can release the file lock
        uid.Unlock()
      except ..., err:
        # Return the UID to the pool
        uidpool.ReleaseUid(uid)

256
  2. Stopping a process::
257
258
259
260
261
262
263

      from ganeti import uidpool

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

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

  """
  # Create the lock dir if it's not yet present
  try:
273
    utils.EnsureDirs([(pathutils.UIDPOOL_LOCKDIR, 0755)])
274
275
276
277
278
  except errors.GenericError, err:
    raise errors.LockError("Failed to create user-id pool lock dir: %s" % err)

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

290
291
292
  # Filter out spurious entries from the directory listing
  taken_uids = all_uids.intersection(taken_uids)

293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
  # Remove the list of used uids from the list of all uids
  unused_uids = list(all_uids - taken_uids)
  if not unused_uids:
    logging.info("All user-ids in the uid-pool are marked 'taken'")

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

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

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

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


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

341
342
343
  @type uid: LockedUid or integer
  @param uid: the uid to release back to the pool

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

352
  try:
353
    uid_path = utils.PathJoin(pathutils.UIDPOOL_LOCKDIR, uid_filename)
354
355
356
    os.remove(uid_path)
  except OSError, err:
    raise errors.LockError("Failed to remove user-id lockfile"
357
                           " for user-id %s: %s" % (uid_filename, err))
358
359
360
361
362
363
364
365


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

  This wrapper function provides a simple way to handle the requesting,
  unlocking and releasing a user-id.
  "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
368
369
370
  passed in all_uids.
  If there is an error while executing "fn", the user-id is returned
  to the pool.

371
372
  @param fn: a callable that accepts a keyword argument called "uid"
  @type all_uids: a set of integers
373
374
375
376
  @param all_uids: a set containing all user-ids in the user-id pool

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