__init__.py 24.2 KB
Newer Older
Iustin Pop's avatar
Iustin Pop committed
1
#
Iustin Pop's avatar
Iustin Pop committed
2
3
#

4
# Copyright (C) 2006, 2007, 2010, 2011 Google Inc.
Iustin Pop's avatar
Iustin Pop committed
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#
# 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.


22
23
24
25
"""Ganeti utility module.

This module holds functions that can be used in both daemons (all) and
the command line scripts.
26

Iustin Pop's avatar
Iustin Pop committed
27
28
"""

29
# Allow wildcard import in pylint: disable=W0401
Iustin Pop's avatar
Iustin Pop committed
30
31
32

import os
import re
33
import errno
34
import pwd
35
import time
Guido Trotter's avatar
Guido Trotter committed
36
import itertools
37
import select
38
import logging
Michael Hanselmann's avatar
Michael Hanselmann committed
39
import signal
Iustin Pop's avatar
Iustin Pop committed
40
41

from ganeti import errors
Iustin Pop's avatar
Iustin Pop committed
42
from ganeti import constants
43
from ganeti import compat
44
from ganeti import pathutils
Iustin Pop's avatar
Iustin Pop committed
45

46
47
48
49
from ganeti.utils.algo import *
from ganeti.utils.filelock import *
from ganeti.utils.hash import *
from ganeti.utils.io import *
50
from ganeti.utils.livelock import *
51
from ganeti.utils.log import *
52
from ganeti.utils.lvm import *
53
from ganeti.utils.mlock import *
54
55
56
from ganeti.utils.nodesetup import *
from ganeti.utils.process import *
from ganeti.utils.retry import *
57
from ganeti.utils.security import *
Helga Velroyen's avatar
Helga Velroyen committed
58
from ganeti.utils.storage import *
59
60
from ganeti.utils.text import *
from ganeti.utils.wrapper import *
61
from ganeti.utils.version import *
62
from ganeti.utils.x509 import *
63

64

65
66
_VALID_SERVICE_NAME_RE = re.compile("^[-_.a-zA-Z0-9]{1,128}$")

67
UUID_RE = re.compile(constants.UUID_REGEX)
68

69

70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
def ForceDictType(target, key_types, allowed_values=None):
  """Force the values of a dict to have certain types.

  @type target: dict
  @param target: the dict to update
  @type key_types: dict
  @param key_types: dict mapping target dict keys to types
                    in constants.ENFORCEABLE_TYPES
  @type allowed_values: list
  @keyword allowed_values: list of specially allowed values

  """
  if allowed_values is None:
    allowed_values = []

85
86
87
88
  if not isinstance(target, dict):
    msg = "Expected dictionary, got '%s'" % target
    raise errors.TypeEnforcementError(msg)

89
90
  for key in target:
    if key not in key_types:
91
      msg = "Unknown parameter '%s'" % key
92
93
94
95
96
      raise errors.TypeEnforcementError(msg)

    if target[key] in allowed_values:
      continue

Iustin Pop's avatar
Iustin Pop committed
97
98
99
    ktype = key_types[key]
    if ktype not in constants.ENFORCEABLE_TYPES:
      msg = "'%s' has non-enforceable type %s" % (key, ktype)
100
101
      raise errors.ProgrammerError(msg)

102
103
104
105
    if ktype in (constants.VTYPE_STRING, constants.VTYPE_MAYBE_STRING):
      if target[key] is None and ktype == constants.VTYPE_MAYBE_STRING:
        pass
      elif not isinstance(target[key], basestring):
106
        if isinstance(target[key], bool) and not target[key]:
Iustin Pop's avatar
Iustin Pop committed
107
          target[key] = ""
108
109
110
        else:
          msg = "'%s' (value %s) is not a valid string" % (key, target[key])
          raise errors.TypeEnforcementError(msg)
Iustin Pop's avatar
Iustin Pop committed
111
    elif ktype == constants.VTYPE_BOOL:
112
113
114
115
116
117
118
119
120
121
122
123
      if isinstance(target[key], basestring) and target[key]:
        if target[key].lower() == constants.VALUE_FALSE:
          target[key] = False
        elif target[key].lower() == constants.VALUE_TRUE:
          target[key] = True
        else:
          msg = "'%s' (value %s) is not a valid boolean" % (key, target[key])
          raise errors.TypeEnforcementError(msg)
      elif target[key]:
        target[key] = True
      else:
        target[key] = False
Iustin Pop's avatar
Iustin Pop committed
124
    elif ktype == constants.VTYPE_SIZE:
125
126
127
128
129
130
      try:
        target[key] = ParseUnit(target[key])
      except errors.UnitParseError, err:
        msg = "'%s' (value %s) is not a valid size. error: %s" % \
              (key, target[key], err)
        raise errors.TypeEnforcementError(msg)
Iustin Pop's avatar
Iustin Pop committed
131
    elif ktype == constants.VTYPE_INT:
132
133
134
135
136
      try:
        target[key] = int(target[key])
      except (ValueError, TypeError):
        msg = "'%s' (value %s) is not a valid integer" % (key, target[key])
        raise errors.TypeEnforcementError(msg)
Klaus Aehlig's avatar
Klaus Aehlig committed
137
138
139
140
141
142
    elif ktype == constants.VTYPE_FLOAT:
      try:
        target[key] = float(target[key])
      except (ValueError, TypeError):
        msg = "'%s' (value %s) is not a valid float" % (key, target[key])
        raise errors.TypeEnforcementError(msg)
143
144


145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
def ValidateServiceName(name):
  """Validate the given service name.

  @type name: number or string
  @param name: Service name or port specification

  """
  try:
    numport = int(name)
  except (ValueError, TypeError):
    # Non-numeric service name
    valid = _VALID_SERVICE_NAME_RE.match(name)
  else:
    # Numeric port (protocols other than TCP or UDP might need adjustments
    # here)
    valid = (numport >= 0 and numport < (1 << 16))

  if not valid:
    raise errors.OpPrereqError("Invalid service name '%s'" % name,
                               errors.ECODE_INVAL)

  return name


169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
def _ComputeMissingKeys(key_path, options, defaults):
  """Helper functions to compute which keys a invalid.

  @param key_path: The current key path (if any)
  @param options: The user provided options
  @param defaults: The default dictionary
  @return: A list of invalid keys

  """
  defaults_keys = frozenset(defaults.keys())
  invalid = []
  for key, value in options.items():
    if key_path:
      new_path = "%s/%s" % (key_path, key)
    else:
      new_path = key

    if key not in defaults_keys:
      invalid.append(new_path)
    elif isinstance(value, dict):
      invalid.extend(_ComputeMissingKeys(new_path, value, defaults[key]))

  return invalid


def VerifyDictOptions(options, defaults):
  """Verify a dict has only keys set which also are in the defaults dict.

  @param options: The user provided options
  @param defaults: The default dictionary
  @raise error.OpPrereqError: If one of the keys is not supported

  """
  invalid = _ComputeMissingKeys("", options, defaults)

  if invalid:
    raise errors.OpPrereqError("Provided option keys not supported: %s" %
                               CommaJoin(invalid), errors.ECODE_INVAL)


Iustin Pop's avatar
Iustin Pop committed
209
210
211
def ListVolumeGroups():
  """List volume groups and their size

212
213
214
215
  @rtype: dict
  @return:
       Dictionary with keys volume name and values
       the size of the volume
Iustin Pop's avatar
Iustin Pop committed
216
217
218
219
220
221
222
223
224
225
226
227
228

  """
  command = "vgs --noheadings --units m --nosuffix -o name,size"
  result = RunCmd(command)
  retval = {}
  if result.failed:
    return retval

  for line in result.stdout.splitlines():
    try:
      name, size = line.split()
      size = int(float(size))
    except (IndexError, ValueError), err:
229
      logging.error("Invalid output from vgs (%s): %s", err, line)
Iustin Pop's avatar
Iustin Pop committed
230
231
232
233
234
235
236
237
238
239
      continue

    retval[name] = size

  return retval


def BridgeExists(bridge):
  """Check whether the given bridge exists in the system

240
241
242
243
  @type bridge: str
  @param bridge: the bridge name to check
  @rtype: boolean
  @return: True if it does
Iustin Pop's avatar
Iustin Pop committed
244
245
246
247
248
249
250
251

  """
  return os.path.isdir("/sys/class/net/%s/bridge" % bridge)


def TryConvert(fn, val):
  """Try to convert a value ignoring errors.

252
253
254
255
256
257
258
259
260
261
  This function tries to apply function I{fn} to I{val}. If no
  C{ValueError} or C{TypeError} exceptions are raised, it will return
  the result, else it will return the original value. Any other
  exceptions are propagated to the caller.

  @type fn: callable
  @param fn: function to apply to the value
  @param val: the value to be converted
  @return: The converted value if the conversion was successful,
      otherwise the original value.
Iustin Pop's avatar
Iustin Pop committed
262
263
264
265

  """
  try:
    nv = fn(val)
Michael Hanselmann's avatar
Michael Hanselmann committed
266
  except (ValueError, TypeError):
Iustin Pop's avatar
Iustin Pop committed
267
268
269
270
    nv = val
  return nv


271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
def ParseCpuMask(cpu_mask):
  """Parse a CPU mask definition and return the list of CPU IDs.

  CPU mask format: comma-separated list of CPU IDs
  or dash-separated ID ranges
  Example: "0-2,5" -> "0,1,2,5"

  @type cpu_mask: str
  @param cpu_mask: CPU mask definition
  @rtype: list of int
  @return: list of CPU IDs

  """
  if not cpu_mask:
    return []
  cpu_list = []
  for range_def in cpu_mask.split(","):
    boundaries = range_def.split("-")
    n_elements = len(boundaries)
    if n_elements > 2:
      raise errors.ParseError("Invalid CPU ID range definition"
                              " (only one hyphen allowed): %s" % range_def)
    try:
      lower = int(boundaries[0])
    except (ValueError, TypeError), err:
      raise errors.ParseError("Invalid CPU ID value for lower boundary of"
                              " CPU ID range: %s" % str(err))
    try:
      higher = int(boundaries[-1])
    except (ValueError, TypeError), err:
      raise errors.ParseError("Invalid CPU ID value for higher boundary of"
                              " CPU ID range: %s" % str(err))
    if lower > higher:
      raise errors.ParseError("Invalid CPU ID range definition"
                              " (%d > %d): %s" % (lower, higher, range_def))
    cpu_list.extend(range(lower, higher + 1))
  return cpu_list


310
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
def ParseMultiCpuMask(cpu_mask):
  """Parse a multiple CPU mask definition and return the list of CPU IDs.

  CPU mask format: colon-separated list of comma-separated list of CPU IDs
  or dash-separated ID ranges, with optional "all" as CPU value
  Example: "0-2,5:all:1,5,6:2" -> [ [ 0,1,2,5 ], [ -1 ], [ 1, 5, 6 ], [ 2 ] ]

  @type cpu_mask: str
  @param cpu_mask: multiple CPU mask definition
  @rtype: list of lists of int
  @return: list of lists of CPU IDs

  """
  if not cpu_mask:
    return []
  cpu_list = []
  for range_def in cpu_mask.split(constants.CPU_PINNING_SEP):
    if range_def == constants.CPU_PINNING_ALL:
      cpu_list.append([constants.CPU_PINNING_ALL_VAL, ])
    else:
      # Uniquify and sort the list before adding
      cpu_list.append(sorted(set(ParseCpuMask(range_def))))

  return cpu_list


336
337
338
339
340
def GetHomeDir(user, default=None):
  """Try to get the homedir of the given user.

  The user can be passed either as a string (denoting the name) or as
  an integer (denoting the user id). If the user is not found, the
341
  C{default} argument is returned, which defaults to C{None}.
342
343
344

  """
  try:
345
346
347
348
349
350
351
    if isinstance(user, basestring):
      result = pwd.getpwnam(user)
    elif isinstance(user, (int, long)):
      result = pwd.getpwuid(user)
    else:
      raise errors.ProgrammerError("Invalid type passed to GetHomeDir (%s)" %
                                   type(user))
352
353
354
  except KeyError:
    return default
  return result.pw_dir
355
356


357
358
359
360
361
362
363
364
def FirstFree(seq, base=0):
  """Returns the first non-existing integer from seq.

  The seq argument should be a sorted list of positive integers. The
  first time the index of an element is smaller than the element
  value, the index will be returned.

  The base argument is used to start at a different offset,
365
366
367
  i.e. C{[3, 4, 6]} with I{offset=3} will return 5.

  Example: C{[0, 1, 3]} will return I{2}.
368

369
370
371
372
373
374
  @type seq: sequence
  @param seq: the sequence to be analyzed.
  @type base: int
  @param base: use this value as the base index of the sequence
  @rtype: int
  @return: the first non-used index in the sequence
375
376
377
378
379
380
381
382
383
384

  """
  for idx, elem in enumerate(seq):
    assert elem >= base, "Passed element is higher than base offset"
    if elem > idx + base:
      # idx is not used
      return idx + base
  return None


385
def SingleWaitForFdCondition(fdobj, event, timeout):
386
387
  """Waits for a condition to occur on the socket.

388
389
390
391
392
  Immediately returns at the first interruption.

  @type fdobj: integer or object supporting a fileno() method
  @param fdobj: entity to wait for events on
  @type event: integer
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
  @param event: ORed condition (see select module)
  @type timeout: float or None
  @param timeout: Timeout in seconds
  @rtype: int or None
  @return: None for timeout, otherwise occured conditions

  """
  check = (event | select.POLLPRI |
           select.POLLNVAL | select.POLLHUP | select.POLLERR)

  if timeout is not None:
    # Poller object expects milliseconds
    timeout *= 1000

  poller = select.poll()
408
  poller.register(fdobj, event)
409
  try:
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
    # TODO: If the main thread receives a signal and we have no timeout, we
    # could wait forever. This should check a global "quit" flag or something
    # every so often.
    io_events = poller.poll(timeout)
  except select.error, err:
    if err[0] != errno.EINTR:
      raise
    io_events = []
  if io_events and io_events[0][1] & check:
    return io_events[0][1]
  else:
    return None


class FdConditionWaiterHelper(object):
  """Retry helper for WaitForFdCondition.

  This class contains the retried and wait functions that make sure
  WaitForFdCondition can continue waiting until the timeout is actually
  expired.

  """

  def __init__(self, timeout):
    self.timeout = timeout

  def Poll(self, fdobj, event):
    result = SingleWaitForFdCondition(fdobj, event, self.timeout)
    if result is None:
      raise RetryAgain()
    else:
      return result

  def UpdateTimeout(self, timeout):
    self.timeout = timeout


def WaitForFdCondition(fdobj, event, timeout):
  """Waits for a condition to occur on the socket.

  Retries until the timeout is expired, even if interrupted.

  @type fdobj: integer or object supporting a fileno() method
  @param fdobj: entity to wait for events on
  @type event: integer
  @param event: ORed condition (see select module)
  @type timeout: float or None
  @param timeout: Timeout in seconds
  @rtype: int or None
  @return: None for timeout, otherwise occured conditions

  """
  if timeout is not None:
    retrywaiter = FdConditionWaiterHelper(timeout)
464
465
466
467
468
    try:
      result = Retry(retrywaiter.Poll, RETRY_REMAINING_TIME, timeout,
                     args=(fdobj, event), wait_fn=retrywaiter.UpdateTimeout)
    except RetryTimeout:
      result = None
469
470
471
472
473
  else:
    result = None
    while result is None:
      result = SingleWaitForFdCondition(fdobj, event, timeout)
  return result
474
475


476
477
478
479
def EnsureDaemon(name):
  """Check for and start daemon if not alive.

  """
480
  result = RunCmd([pathutils.DAEMON_UTIL, "check-and-start", name])
481
482
483
484
485
486
  if result.failed:
    logging.error("Can't start daemon '%s', failure %s, output: %s",
                  name, result.fail_reason, result.output)
    return False

  return True
487
488


489
490
491
492
def StopDaemon(name):
  """Stop daemon

  """
493
  result = RunCmd([pathutils.DAEMON_UTIL, "stop", name])
494
495
496
497
498
499
500
501
  if result.failed:
    logging.error("Can't stop daemon '%s', failure %s, output: %s",
                  name, result.fail_reason, result.output)
    return False

  return True


502
def SplitTime(value):
503
504
  """Splits time as floating point number into a tuple.

505
506
507
  @param value: Time in seconds
  @type value: int or float
  @return: Tuple containing (seconds, microseconds)
508
509

  """
510
511
512
513
514
515
516
517
  (seconds, microseconds) = divmod(int(value * 1000000), 1000000)

  assert 0 <= seconds, \
    "Seconds must be larger than or equal to 0, but are %s" % seconds
  assert 0 <= microseconds <= 999999, \
    "Microseconds must be 0-999999, but are %s" % microseconds

  return (int(seconds), int(microseconds))
518
519
520
521
522


def MergeTime(timetuple):
  """Merges a tuple into time as a floating point number.

523
  @param timetuple: Time as tuple, (seconds, microseconds)
524
525
526
527
  @type timetuple: tuple
  @return: Time as a floating point number expressed in seconds

  """
528
  (seconds, microseconds) = timetuple
529

530
531
532
533
  assert 0 <= seconds, \
    "Seconds must be larger than or equal to 0, but are %s" % seconds
  assert 0 <= microseconds <= 999999, \
    "Microseconds must be 0-999999, but are %s" % microseconds
534

535
  return float(seconds) + (float(microseconds) * 0.000001)
536
537


538
539
540
541
542
543
544
545
546
547
def EpochNano():
  """Return the current timestamp expressed as number of nanoseconds since the
  unix epoch

  @return: nanoseconds since the Unix epoch

  """
  return int(time.time() * 1000000000)


548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
def FindMatch(data, name):
  """Tries to find an item in a dictionary matching a name.

  Callers have to ensure the data names aren't contradictory (e.g. a regexp
  that matches a string). If the name isn't a direct key, all regular
  expression objects in the dictionary are matched against it.

  @type data: dict
  @param data: Dictionary containing data
  @type name: string
  @param name: Name to look for
  @rtype: tuple; (value in dictionary, matched groups as list)

  """
  if name in data:
    return (data[name], [])

  for key, value in data.items():
    # Regex objects
    if hasattr(key, "match"):
      m = key.match(name)
      if m:
        return (value, list(m.groups()))

  return None


Balazs Lecz's avatar
Balazs Lecz committed
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
def GetMounts(filename=constants.PROC_MOUNTS):
  """Returns the list of mounted filesystems.

  This function is Linux-specific.

  @param filename: path of mounts file (/proc/mounts by default)
  @rtype: list of tuples
  @return: list of mount entries (device, mountpoint, fstype, options)

  """
  # TODO(iustin): investigate non-Linux options (e.g. via mount output)
  data = []
  mountlines = ReadFile(filename).splitlines()
  for line in mountlines:
    device, mountpoint, fstype, options, _ = line.split(None, 4)
    data.append((device, mountpoint, fstype, options))

  return data


595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
def SignalHandled(signums):
  """Signal Handled decoration.

  This special decorator installs a signal handler and then calls the target
  function. The function must accept a 'signal_handlers' keyword argument,
  which will contain a dict indexed by signal number, with SignalHandler
  objects as values.

  The decorator can be safely stacked with iself, to handle multiple signals
  with different handlers.

  @type signums: list
  @param signums: signals to intercept

  """
  def wrap(fn):
    def sig_function(*args, **kwargs):
Iustin Pop's avatar
Iustin Pop committed
612
613
614
      assert "signal_handlers" not in kwargs or \
             kwargs["signal_handlers"] is None or \
             isinstance(kwargs["signal_handlers"], dict), \
615
             "Wrong signal_handlers parameter in original function call"
Iustin Pop's avatar
Iustin Pop committed
616
617
      if "signal_handlers" in kwargs and kwargs["signal_handlers"] is not None:
        signal_handlers = kwargs["signal_handlers"]
618
619
      else:
        signal_handlers = {}
Iustin Pop's avatar
Iustin Pop committed
620
        kwargs["signal_handlers"] = signal_handlers
621
622
623
624
625
626
627
628
629
630
631
      sighandler = SignalHandler(signums)
      try:
        for sig in signums:
          signal_handlers[sig] = sighandler
        return fn(*args, **kwargs)
      finally:
        sighandler.Reset()
    return sig_function
  return wrap


632
633
634
635
636
637
638
def TimeoutExpired(epoch, timeout, _time_fn=time.time):
  """Checks whether a timeout has expired.

  """
  return _time_fn() > (epoch + timeout)


639
640
641
642
643
644
645
class SignalWakeupFd(object):
  try:
    # This is only supported in Python 2.5 and above (some distributions
    # backported it to Python 2.4)
    _set_wakeup_fd_fn = signal.set_wakeup_fd
  except AttributeError:
    # Not supported
646

647
    def _SetWakeupFd(self, _): # pylint: disable=R0201
648
649
      return -1
  else:
650

651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
    def _SetWakeupFd(self, fd):
      return self._set_wakeup_fd_fn(fd)

  def __init__(self):
    """Initializes this class.

    """
    (read_fd, write_fd) = os.pipe()

    # Once these succeeded, the file descriptors will be closed automatically.
    # Buffer size 0 is important, otherwise .read() with a specified length
    # might buffer data and the file descriptors won't be marked readable.
    self._read_fh = os.fdopen(read_fd, "r", 0)
    self._write_fh = os.fdopen(write_fd, "w", 0)

    self._previous = self._SetWakeupFd(self._write_fh.fileno())

    # Utility functions
    self.fileno = self._read_fh.fileno
    self.read = self._read_fh.read

  def Reset(self):
    """Restores the previous wakeup file descriptor.

    """
    if hasattr(self, "_previous") and self._previous is not None:
      self._SetWakeupFd(self._previous)
      self._previous = None

  def Notify(self):
    """Notifies the wakeup file descriptor.

    """
684
    self._write_fh.write(chr(0))
685
686
687
688
689
690
691
692

  def __del__(self):
    """Called before object deletion.

    """
    self.Reset()


Michael Hanselmann's avatar
Michael Hanselmann committed
693
694
695
class SignalHandler(object):
  """Generic signal handler class.

696
697
698
699
700
701
702
703
704
  It automatically restores the original handler when deconstructed or
  when L{Reset} is called. You can either pass your own handler
  function in or query the L{called} attribute to detect whether the
  signal was sent.

  @type signum: list
  @ivar signum: the signals we handle
  @type called: boolean
  @ivar called: tracks whether any of the signals have been raised
Michael Hanselmann's avatar
Michael Hanselmann committed
705
706

  """
707
  def __init__(self, signum, handler_fn=None, wakeup=None):
Michael Hanselmann's avatar
Michael Hanselmann committed
708
709
    """Constructs a new SignalHandler instance.

710
    @type signum: int or list of ints
Michael Hanselmann's avatar
Michael Hanselmann committed
711
    @param signum: Single signal number or set of signal numbers
712
713
    @type handler_fn: callable
    @param handler_fn: Signal handling function
Michael Hanselmann's avatar
Michael Hanselmann committed
714
715

    """
716
717
    assert handler_fn is None or callable(handler_fn)

718
    self.signum = set(signum)
Michael Hanselmann's avatar
Michael Hanselmann committed
719
720
    self.called = False

721
    self._handler_fn = handler_fn
722
    self._wakeup = wakeup
723

Michael Hanselmann's avatar
Michael Hanselmann committed
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
    self._previous = {}
    try:
      for signum in self.signum:
        # Setup handler
        prev_handler = signal.signal(signum, self._HandleSignal)
        try:
          self._previous[signum] = prev_handler
        except:
          # Restore previous handler
          signal.signal(signum, prev_handler)
          raise
    except:
      # Reset all handlers
      self.Reset()
      # Here we have a race condition: a handler may have already been called,
      # but there's not much we can do about it at this point.
      raise

  def __del__(self):
    self.Reset()

  def Reset(self):
    """Restore previous handler.

748
749
    This will reset all the signals to their previous handlers.

Michael Hanselmann's avatar
Michael Hanselmann committed
750
751
752
753
754
755
756
    """
    for signum, prev_handler in self._previous.items():
      signal.signal(signum, prev_handler)
      # If successful, remove from dict
      del self._previous[signum]

  def Clear(self):
757
    """Unsets the L{called} flag.
Michael Hanselmann's avatar
Michael Hanselmann committed
758
759
760
761
762
763

    This function can be used in case a signal may arrive several times.

    """
    self.called = False

764
  def _HandleSignal(self, signum, frame):
Michael Hanselmann's avatar
Michael Hanselmann committed
765
766
767
768
769
770
    """Actual signal handling function.

    """
    # This is not nice and not absolutely atomic, but it appears to be the only
    # solution in Python -- there are no atomic types.
    self.called = True
Iustin Pop's avatar
Iustin Pop committed
771

772
773
774
775
    if self._wakeup:
      # Notify whoever is interested in signals
      self._wakeup.Notify()

776
777
778
    if self._handler_fn:
      self._handler_fn(signum, frame)

Iustin Pop's avatar
Iustin Pop committed
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802

class FieldSet(object):
  """A simple field set.

  Among the features are:
    - checking if a string is among a list of static string or regex objects
    - checking if a whole list of string matches
    - returning the matching groups from a regex match

  Internally, all fields are held as regular expression objects.

  """
  def __init__(self, *items):
    self.items = [re.compile("^%s$" % value) for value in items]

  def Extend(self, other_set):
    """Extend the field set with the items from another one"""
    self.items.extend(other_set.items)

  def Matches(self, field):
    """Checks if a field matches the current set

    @type field: str
    @param field: the string to match
Iustin Pop's avatar
Iustin Pop committed
803
    @return: either None or a regular expression match object
Iustin Pop's avatar
Iustin Pop committed
804
805
806
807

    """
    for m in itertools.ifilter(None, (val.match(field) for val in self.items)):
      return m
Iustin Pop's avatar
Iustin Pop committed
808
    return None
Iustin Pop's avatar
Iustin Pop committed
809
810
811
812
813
814
815
816
817
818
819

  def NonMatching(self, items):
    """Returns the list of fields not matching the current set

    @type items: list
    @param items: the list of fields to check
    @rtype: list
    @return: list of non-matching fields

    """
    return [val for val in items if not self.Matches(val)]
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863


def ValidateDeviceNames(kind, container):
  """Validate instance device names.

  Check that a device container contains only unique and valid names.

  @type kind: string
  @param kind: One-word item description
  @type container: list
  @param container: Container containing the devices

  """

  valid = []
  for device in container:
    if isinstance(device, dict):
      if kind == "NIC":
        name = device.get(constants.INIC_NAME, None)
      elif kind == "disk":
        name = device.get(constants.IDISK_NAME, None)
      else:
        raise errors.OpPrereqError("Invalid container kind '%s'" % kind,
                                   errors.ECODE_INVAL)
    else:
      name = device.name
      # Check that a device name is not the UUID of another device
      valid.append(device.uuid)

    try:
      int(name)
    except (ValueError, TypeError):
      pass
    else:
      raise errors.OpPrereqError("Invalid name '%s'. Purely numeric %s names"
                                 " are not allowed" % (name, kind),
                                 errors.ECODE_INVAL)

    if name is not None and name.lower() != constants.VALUE_NONE:
      if name in valid:
        raise errors.OpPrereqError("%s name '%s' already used" % (kind, name),
                                   errors.ECODE_NOTUNIQUE)
      else:
        valid.append(name)