Statistics
| Branch: | Tag: | Revision:

root / lib / utils / __init__.py @ 111a7d04

History | View | Annotate | Download (23.1 kB)

1
#
2
#
3

    
4
# Copyright (C) 2006, 2007, 2010, 2011 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
"""Ganeti utility module.
23

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

27
"""
28

    
29
# Allow wildcard import in pylint: disable=W0401
30

    
31
import os
32
import re
33
import errno
34
import pwd
35
import time
36
import itertools
37
import select
38
import logging
39
import signal
40

    
41
from ganeti import errors
42
from ganeti import constants
43
from ganeti import compat
44
from ganeti import pathutils
45

    
46
from ganeti.utils.algo import *
47
from ganeti.utils.filelock import *
48
from ganeti.utils.hash import *
49
from ganeti.utils.io import *
50
from ganeti.utils.log import *
51
from ganeti.utils.mlock import *
52
from ganeti.utils.nodesetup import *
53
from ganeti.utils.process import *
54
from ganeti.utils.retry import *
55
from ganeti.utils.text import *
56
from ganeti.utils.wrapper import *
57
from ganeti.utils.x509 import *
58

    
59

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

    
62
UUID_RE = re.compile("^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-"
63
                     "[a-f0-9]{4}-[a-f0-9]{12}$")
64

    
65

    
66
def ForceDictType(target, key_types, allowed_values=None):
67
  """Force the values of a dict to have certain types.
68

69
  @type target: dict
70
  @param target: the dict to update
71
  @type key_types: dict
72
  @param key_types: dict mapping target dict keys to types
73
                    in constants.ENFORCEABLE_TYPES
74
  @type allowed_values: list
75
  @keyword allowed_values: list of specially allowed values
76

77
  """
78
  if allowed_values is None:
79
    allowed_values = []
80

    
81
  if not isinstance(target, dict):
82
    msg = "Expected dictionary, got '%s'" % target
83
    raise errors.TypeEnforcementError(msg)
84

    
85
  for key in target:
86
    if key not in key_types:
87
      msg = "Unknown parameter '%s'" % key
88
      raise errors.TypeEnforcementError(msg)
89

    
90
    if target[key] in allowed_values:
91
      continue
92

    
93
    ktype = key_types[key]
94
    if ktype not in constants.ENFORCEABLE_TYPES:
95
      msg = "'%s' has non-enforceable type %s" % (key, ktype)
96
      raise errors.ProgrammerError(msg)
97

    
98
    if ktype in (constants.VTYPE_STRING, constants.VTYPE_MAYBE_STRING):
99
      if target[key] is None and ktype == constants.VTYPE_MAYBE_STRING:
100
        pass
101
      elif not isinstance(target[key], basestring):
102
        if isinstance(target[key], bool) and not target[key]:
103
          target[key] = ""
104
        else:
105
          msg = "'%s' (value %s) is not a valid string" % (key, target[key])
106
          raise errors.TypeEnforcementError(msg)
107
    elif ktype == constants.VTYPE_BOOL:
108
      if isinstance(target[key], basestring) and target[key]:
109
        if target[key].lower() == constants.VALUE_FALSE:
110
          target[key] = False
111
        elif target[key].lower() == constants.VALUE_TRUE:
112
          target[key] = True
113
        else:
114
          msg = "'%s' (value %s) is not a valid boolean" % (key, target[key])
115
          raise errors.TypeEnforcementError(msg)
116
      elif target[key]:
117
        target[key] = True
118
      else:
119
        target[key] = False
120
    elif ktype == constants.VTYPE_SIZE:
121
      try:
122
        target[key] = ParseUnit(target[key])
123
      except errors.UnitParseError, err:
124
        msg = "'%s' (value %s) is not a valid size. error: %s" % \
125
              (key, target[key], err)
126
        raise errors.TypeEnforcementError(msg)
127
    elif ktype == constants.VTYPE_INT:
128
      try:
129
        target[key] = int(target[key])
130
      except (ValueError, TypeError):
131
        msg = "'%s' (value %s) is not a valid integer" % (key, target[key])
132
        raise errors.TypeEnforcementError(msg)
133

    
134

    
135
def ValidateServiceName(name):
136
  """Validate the given service name.
137

138
  @type name: number or string
139
  @param name: Service name or port specification
140

141
  """
142
  try:
143
    numport = int(name)
144
  except (ValueError, TypeError):
145
    # Non-numeric service name
146
    valid = _VALID_SERVICE_NAME_RE.match(name)
147
  else:
148
    # Numeric port (protocols other than TCP or UDP might need adjustments
149
    # here)
150
    valid = (numport >= 0 and numport < (1 << 16))
151

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

    
156
  return name
157

    
158

    
159
def _ComputeMissingKeys(key_path, options, defaults):
160
  """Helper functions to compute which keys a invalid.
161

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

167
  """
168
  defaults_keys = frozenset(defaults.keys())
169
  invalid = []
170
  for key, value in options.items():
171
    if key_path:
172
      new_path = "%s/%s" % (key_path, key)
173
    else:
174
      new_path = key
175

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

    
181
  return invalid
182

    
183

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

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

191
  """
192
  invalid = _ComputeMissingKeys("", options, defaults)
193

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

    
198

    
199
def ListVolumeGroups():
200
  """List volume groups and their size
201

202
  @rtype: dict
203
  @return:
204
       Dictionary with keys volume name and values
205
       the size of the volume
206

207
  """
208
  command = "vgs --noheadings --units m --nosuffix -o name,size"
209
  result = RunCmd(command)
210
  retval = {}
211
  if result.failed:
212
    return retval
213

    
214
  for line in result.stdout.splitlines():
215
    try:
216
      name, size = line.split()
217
      size = int(float(size))
218
    except (IndexError, ValueError), err:
219
      logging.error("Invalid output from vgs (%s): %s", err, line)
220
      continue
221

    
222
    retval[name] = size
223

    
224
  return retval
225

    
226

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

230
  @type bridge: str
231
  @param bridge: the bridge name to check
232
  @rtype: boolean
233
  @return: True if it does
234

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

    
238

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

242
  This function tries to apply function I{fn} to I{val}. If no
243
  C{ValueError} or C{TypeError} exceptions are raised, it will return
244
  the result, else it will return the original value. Any other
245
  exceptions are propagated to the caller.
246

247
  @type fn: callable
248
  @param fn: function to apply to the value
249
  @param val: the value to be converted
250
  @return: The converted value if the conversion was successful,
251
      otherwise the original value.
252

253
  """
254
  try:
255
    nv = fn(val)
256
  except (ValueError, TypeError):
257
    nv = val
258
  return nv
259

    
260

    
261
def ParseCpuMask(cpu_mask):
262
  """Parse a CPU mask definition and return the list of CPU IDs.
263

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

268
  @type cpu_mask: str
269
  @param cpu_mask: CPU mask definition
270
  @rtype: list of int
271
  @return: list of CPU IDs
272

273
  """
274
  if not cpu_mask:
275
    return []
276
  cpu_list = []
277
  for range_def in cpu_mask.split(","):
278
    boundaries = range_def.split("-")
279
    n_elements = len(boundaries)
280
    if n_elements > 2:
281
      raise errors.ParseError("Invalid CPU ID range definition"
282
                              " (only one hyphen allowed): %s" % range_def)
283
    try:
284
      lower = int(boundaries[0])
285
    except (ValueError, TypeError), err:
286
      raise errors.ParseError("Invalid CPU ID value for lower boundary of"
287
                              " CPU ID range: %s" % str(err))
288
    try:
289
      higher = int(boundaries[-1])
290
    except (ValueError, TypeError), err:
291
      raise errors.ParseError("Invalid CPU ID value for higher boundary of"
292
                              " CPU ID range: %s" % str(err))
293
    if lower > higher:
294
      raise errors.ParseError("Invalid CPU ID range definition"
295
                              " (%d > %d): %s" % (lower, higher, range_def))
296
    cpu_list.extend(range(lower, higher + 1))
297
  return cpu_list
298

    
299

    
300
def ParseMultiCpuMask(cpu_mask):
301
  """Parse a multiple CPU mask definition and return the list of CPU IDs.
302

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

307
  @type cpu_mask: str
308
  @param cpu_mask: multiple CPU mask definition
309
  @rtype: list of lists of int
310
  @return: list of lists of CPU IDs
311

312
  """
313
  if not cpu_mask:
314
    return []
315
  cpu_list = []
316
  for range_def in cpu_mask.split(constants.CPU_PINNING_SEP):
317
    if range_def == constants.CPU_PINNING_ALL:
318
      cpu_list.append([constants.CPU_PINNING_ALL_VAL, ])
319
    else:
320
      # Uniquify and sort the list before adding
321
      cpu_list.append(sorted(set(ParseCpuMask(range_def))))
322

    
323
  return cpu_list
324

    
325

    
326
def GetHomeDir(user, default=None):
327
  """Try to get the homedir of the given user.
328

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

333
  """
334
  try:
335
    if isinstance(user, basestring):
336
      result = pwd.getpwnam(user)
337
    elif isinstance(user, (int, long)):
338
      result = pwd.getpwuid(user)
339
    else:
340
      raise errors.ProgrammerError("Invalid type passed to GetHomeDir (%s)" %
341
                                   type(user))
342
  except KeyError:
343
    return default
344
  return result.pw_dir
345

    
346

    
347
def FirstFree(seq, base=0):
348
  """Returns the first non-existing integer from seq.
349

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

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

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

359
  @type seq: sequence
360
  @param seq: the sequence to be analyzed.
361
  @type base: int
362
  @param base: use this value as the base index of the sequence
363
  @rtype: int
364
  @return: the first non-used index in the sequence
365

366
  """
367
  for idx, elem in enumerate(seq):
368
    assert elem >= base, "Passed element is higher than base offset"
369
    if elem > idx + base:
370
      # idx is not used
371
      return idx + base
372
  return None
373

    
374

    
375
def SingleWaitForFdCondition(fdobj, event, timeout):
376
  """Waits for a condition to occur on the socket.
377

378
  Immediately returns at the first interruption.
379

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

389
  """
390
  check = (event | select.POLLPRI |
391
           select.POLLNVAL | select.POLLHUP | select.POLLERR)
392

    
393
  if timeout is not None:
394
    # Poller object expects milliseconds
395
    timeout *= 1000
396

    
397
  poller = select.poll()
398
  poller.register(fdobj, event)
399
  try:
400
    # TODO: If the main thread receives a signal and we have no timeout, we
401
    # could wait forever. This should check a global "quit" flag or something
402
    # every so often.
403
    io_events = poller.poll(timeout)
404
  except select.error, err:
405
    if err[0] != errno.EINTR:
406
      raise
407
    io_events = []
408
  if io_events and io_events[0][1] & check:
409
    return io_events[0][1]
410
  else:
411
    return None
412

    
413

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

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

421
  """
422

    
423
  def __init__(self, timeout):
424
    self.timeout = timeout
425

    
426
  def Poll(self, fdobj, event):
427
    result = SingleWaitForFdCondition(fdobj, event, self.timeout)
428
    if result is None:
429
      raise RetryAgain()
430
    else:
431
      return result
432

    
433
  def UpdateTimeout(self, timeout):
434
    self.timeout = timeout
435

    
436

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

440
  Retries until the timeout is expired, even if interrupted.
441

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

451
  """
452
  if timeout is not None:
453
    retrywaiter = FdConditionWaiterHelper(timeout)
454
    try:
455
      result = Retry(retrywaiter.Poll, RETRY_REMAINING_TIME, timeout,
456
                     args=(fdobj, event), wait_fn=retrywaiter.UpdateTimeout)
457
    except RetryTimeout:
458
      result = None
459
  else:
460
    result = None
461
    while result is None:
462
      result = SingleWaitForFdCondition(fdobj, event, timeout)
463
  return result
464

    
465

    
466
def EnsureDaemon(name):
467
  """Check for and start daemon if not alive.
468

469
  """
470
  result = RunCmd([pathutils.DAEMON_UTIL, "check-and-start", name])
471
  if result.failed:
472
    logging.error("Can't start daemon '%s', failure %s, output: %s",
473
                  name, result.fail_reason, result.output)
474
    return False
475

    
476
  return True
477

    
478

    
479
def StopDaemon(name):
480
  """Stop daemon
481

482
  """
483
  result = RunCmd([pathutils.DAEMON_UTIL, "stop", name])
484
  if result.failed:
485
    logging.error("Can't stop daemon '%s', failure %s, output: %s",
486
                  name, result.fail_reason, result.output)
487
    return False
488

    
489
  return True
490

    
491

    
492
def CheckVolumeGroupSize(vglist, vgname, minsize):
493
  """Checks if the volume group list is valid.
494

495
  The function will check if a given volume group is in the list of
496
  volume groups and has a minimum size.
497

498
  @type vglist: dict
499
  @param vglist: dictionary of volume group names and their size
500
  @type vgname: str
501
  @param vgname: the volume group we should check
502
  @type minsize: int
503
  @param minsize: the minimum size we accept
504
  @rtype: None or str
505
  @return: None for success, otherwise the error message
506

507
  """
508
  vgsize = vglist.get(vgname, None)
509
  if vgsize is None:
510
    return "volume group '%s' missing" % vgname
511
  elif vgsize < minsize:
512
    return ("volume group '%s' too small (%s MiB required, %d MiB found)" %
513
            (vgname, minsize, vgsize))
514
  return None
515

    
516

    
517
def SplitTime(value):
518
  """Splits time as floating point number into a tuple.
519

520
  @param value: Time in seconds
521
  @type value: int or float
522
  @return: Tuple containing (seconds, microseconds)
523

524
  """
525
  (seconds, microseconds) = divmod(int(value * 1000000), 1000000)
526

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

    
532
  return (int(seconds), int(microseconds))
533

    
534

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

538
  @param timetuple: Time as tuple, (seconds, microseconds)
539
  @type timetuple: tuple
540
  @return: Time as a floating point number expressed in seconds
541

542
  """
543
  (seconds, microseconds) = timetuple
544

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

    
550
  return float(seconds) + (float(microseconds) * 0.000001)
551

    
552

    
553
def FindMatch(data, name):
554
  """Tries to find an item in a dictionary matching a name.
555

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

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

566
  """
567
  if name in data:
568
    return (data[name], [])
569

    
570
  for key, value in data.items():
571
    # Regex objects
572
    if hasattr(key, "match"):
573
      m = key.match(name)
574
      if m:
575
        return (value, list(m.groups()))
576

    
577
  return None
578

    
579

    
580
def GetMounts(filename=constants.PROC_MOUNTS):
581
  """Returns the list of mounted filesystems.
582

583
  This function is Linux-specific.
584

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

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

    
597
  return data
598

    
599

    
600
def SignalHandled(signums):
601
  """Signal Handled decoration.
602

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

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

611
  @type signums: list
612
  @param signums: signals to intercept
613

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

    
636

    
637
def TimeoutExpired(epoch, timeout, _time_fn=time.time):
638
  """Checks whether a timeout has expired.
639

640
  """
641
  return _time_fn() > (epoch + timeout)
642

    
643

    
644
class SignalWakeupFd(object):
645
  try:
646
    # This is only supported in Python 2.5 and above (some distributions
647
    # backported it to Python 2.4)
648
    _set_wakeup_fd_fn = signal.set_wakeup_fd
649
  except AttributeError:
650
    # Not supported
651
    def _SetWakeupFd(self, _): # pylint: disable=R0201
652
      return -1
653
  else:
654
    def _SetWakeupFd(self, fd):
655
      return self._set_wakeup_fd_fn(fd)
656

    
657
  def __init__(self):
658
    """Initializes this class.
659

660
    """
661
    (read_fd, write_fd) = os.pipe()
662

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

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

    
671
    # Utility functions
672
    self.fileno = self._read_fh.fileno
673
    self.read = self._read_fh.read
674

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

678
    """
679
    if hasattr(self, "_previous") and self._previous is not None:
680
      self._SetWakeupFd(self._previous)
681
      self._previous = None
682

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

686
    """
687
    self._write_fh.write("\0")
688

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

692
    """
693
    self.Reset()
694

    
695

    
696
class SignalHandler(object):
697
  """Generic signal handler class.
698

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

704
  @type signum: list
705
  @ivar signum: the signals we handle
706
  @type called: boolean
707
  @ivar called: tracks whether any of the signals have been raised
708

709
  """
710
  def __init__(self, signum, handler_fn=None, wakeup=None):
711
    """Constructs a new SignalHandler instance.
712

713
    @type signum: int or list of ints
714
    @param signum: Single signal number or set of signal numbers
715
    @type handler_fn: callable
716
    @param handler_fn: Signal handling function
717

718
    """
719
    assert handler_fn is None or callable(handler_fn)
720

    
721
    self.signum = set(signum)
722
    self.called = False
723

    
724
    self._handler_fn = handler_fn
725
    self._wakeup = wakeup
726

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

    
745
  def __del__(self):
746
    self.Reset()
747

    
748
  def Reset(self):
749
    """Restore previous handler.
750

751
    This will reset all the signals to their previous handlers.
752

753
    """
754
    for signum, prev_handler in self._previous.items():
755
      signal.signal(signum, prev_handler)
756
      # If successful, remove from dict
757
      del self._previous[signum]
758

    
759
  def Clear(self):
760
    """Unsets the L{called} flag.
761

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

764
    """
765
    self.called = False
766

    
767
  def _HandleSignal(self, signum, frame):
768
    """Actual signal handling function.
769

770
    """
771
    # This is not nice and not absolutely atomic, but it appears to be the only
772
    # solution in Python -- there are no atomic types.
773
    self.called = True
774

    
775
    if self._wakeup:
776
      # Notify whoever is interested in signals
777
      self._wakeup.Notify()
778

    
779
    if self._handler_fn:
780
      self._handler_fn(signum, frame)
781

    
782

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

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

791
  Internally, all fields are held as regular expression objects.
792

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

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

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

804
    @type field: str
805
    @param field: the string to match
806
    @return: either None or a regular expression match object
807

808
    """
809
    for m in itertools.ifilter(None, (val.match(field) for val in self.items)):
810
      return m
811
    return None
812

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

816
    @type items: list
817
    @param items: the list of fields to check
818
    @rtype: list
819
    @return: list of non-matching fields
820

821
    """
822
    return [val for val in items if not self.Matches(val)]