Statistics
| Branch: | Tag: | Revision:

root / lib / utils / __init__.py @ a4ccecf6

History | View | Annotate | Download (23 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

    
30
import os
31
import sys
32
import time
33
import subprocess
34
import re
35
import socket
36
import tempfile
37
import shutil
38
import errno
39
import pwd
40
import itertools
41
import select
42
import fcntl
43
import resource
44
import logging
45
import signal
46
import datetime
47
import calendar
48

    
49
from cStringIO import StringIO
50

    
51
from ganeti import errors
52
from ganeti import constants
53
from ganeti import compat
54

    
55
from ganeti.utils.algo import * # pylint: disable-msg=W0401
56
from ganeti.utils.retry import * # pylint: disable-msg=W0401
57
from ganeti.utils.text import * # pylint: disable-msg=W0401
58
from ganeti.utils.mlock import * # pylint: disable-msg=W0401
59
from ganeti.utils.log import * # pylint: disable-msg=W0401
60
from ganeti.utils.hash import * # pylint: disable-msg=W0401
61
from ganeti.utils.wrapper import * # pylint: disable-msg=W0401
62
from ganeti.utils.filelock import * # pylint: disable-msg=W0401
63
from ganeti.utils.io import * # pylint: disable-msg=W0401
64
from ganeti.utils.x509 import * # pylint: disable-msg=W0401
65
from ganeti.utils.nodesetup import * # pylint: disable-msg=W0401
66
from ganeti.utils.process import * # pylint: disable-msg=W0401
67

    
68

    
69
_RANDOM_UUID_FILE = "/proc/sys/kernel/random/uuid"
70

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

    
73
UUID_RE = re.compile('^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-'
74
                     '[a-f0-9]{4}-[a-f0-9]{12}$')
75

    
76
#: Shell param checker regexp
77
_SHELLPARAM_REGEX = re.compile(r"^[-a-zA-Z0-9._+/:%@]+$")
78

    
79

    
80
def ForceDictType(target, key_types, allowed_values=None):
81
  """Force the values of a dict to have certain types.
82

83
  @type target: dict
84
  @param target: the dict to update
85
  @type key_types: dict
86
  @param key_types: dict mapping target dict keys to types
87
                    in constants.ENFORCEABLE_TYPES
88
  @type allowed_values: list
89
  @keyword allowed_values: list of specially allowed values
90

91
  """
92
  if allowed_values is None:
93
    allowed_values = []
94

    
95
  if not isinstance(target, dict):
96
    msg = "Expected dictionary, got '%s'" % target
97
    raise errors.TypeEnforcementError(msg)
98

    
99
  for key in target:
100
    if key not in key_types:
101
      msg = "Unknown key '%s'" % key
102
      raise errors.TypeEnforcementError(msg)
103

    
104
    if target[key] in allowed_values:
105
      continue
106

    
107
    ktype = key_types[key]
108
    if ktype not in constants.ENFORCEABLE_TYPES:
109
      msg = "'%s' has non-enforceable type %s" % (key, ktype)
110
      raise errors.ProgrammerError(msg)
111

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

    
148

    
149
def ValidateServiceName(name):
150
  """Validate the given service name.
151

152
  @type name: number or string
153
  @param name: Service name or port specification
154

155
  """
156
  try:
157
    numport = int(name)
158
  except (ValueError, TypeError):
159
    # Non-numeric service name
160
    valid = _VALID_SERVICE_NAME_RE.match(name)
161
  else:
162
    # Numeric port (protocols other than TCP or UDP might need adjustments
163
    # here)
164
    valid = (numport >= 0 and numport < (1 << 16))
165

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

    
170
  return name
171

    
172

    
173
def ListVolumeGroups():
174
  """List volume groups and their size
175

176
  @rtype: dict
177
  @return:
178
       Dictionary with keys volume name and values
179
       the size of the volume
180

181
  """
182
  command = "vgs --noheadings --units m --nosuffix -o name,size"
183
  result = RunCmd(command)
184
  retval = {}
185
  if result.failed:
186
    return retval
187

    
188
  for line in result.stdout.splitlines():
189
    try:
190
      name, size = line.split()
191
      size = int(float(size))
192
    except (IndexError, ValueError), err:
193
      logging.error("Invalid output from vgs (%s): %s", err, line)
194
      continue
195

    
196
    retval[name] = size
197

    
198
  return retval
199

    
200

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

204
  @type bridge: str
205
  @param bridge: the bridge name to check
206
  @rtype: boolean
207
  @return: True if it does
208

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

    
212

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

216
  This function tries to apply function I{fn} to I{val}. If no
217
  C{ValueError} or C{TypeError} exceptions are raised, it will return
218
  the result, else it will return the original value. Any other
219
  exceptions are propagated to the caller.
220

221
  @type fn: callable
222
  @param fn: function to apply to the value
223
  @param val: the value to be converted
224
  @return: The converted value if the conversion was successful,
225
      otherwise the original value.
226

227
  """
228
  try:
229
    nv = fn(val)
230
  except (ValueError, TypeError):
231
    nv = val
232
  return nv
233

    
234

    
235
def IsValidShellParam(word):
236
  """Verifies is the given word is safe from the shell's p.o.v.
237

238
  This means that we can pass this to a command via the shell and be
239
  sure that it doesn't alter the command line and is passed as such to
240
  the actual command.
241

242
  Note that we are overly restrictive here, in order to be on the safe
243
  side.
244

245
  @type word: str
246
  @param word: the word to check
247
  @rtype: boolean
248
  @return: True if the word is 'safe'
249

250
  """
251
  return bool(_SHELLPARAM_REGEX.match(word))
252

    
253

    
254
def BuildShellCmd(template, *args):
255
  """Build a safe shell command line from the given arguments.
256

257
  This function will check all arguments in the args list so that they
258
  are valid shell parameters (i.e. they don't contain shell
259
  metacharacters). If everything is ok, it will return the result of
260
  template % args.
261

262
  @type template: str
263
  @param template: the string holding the template for the
264
      string formatting
265
  @rtype: str
266
  @return: the expanded command line
267

268
  """
269
  for word in args:
270
    if not IsValidShellParam(word):
271
      raise errors.ProgrammerError("Shell argument '%s' contains"
272
                                   " invalid characters" % word)
273
  return template % args
274

    
275

    
276
def ParseCpuMask(cpu_mask):
277
  """Parse a CPU mask definition and return the list of CPU IDs.
278

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

283
  @type cpu_mask: str
284
  @param cpu_mask: CPU mask definition
285
  @rtype: list of int
286
  @return: list of CPU IDs
287

288
  """
289
  if not cpu_mask:
290
    return []
291
  cpu_list = []
292
  for range_def in cpu_mask.split(","):
293
    boundaries = range_def.split("-")
294
    n_elements = len(boundaries)
295
    if n_elements > 2:
296
      raise errors.ParseError("Invalid CPU ID range definition"
297
                              " (only one hyphen allowed): %s" % range_def)
298
    try:
299
      lower = int(boundaries[0])
300
    except (ValueError, TypeError), err:
301
      raise errors.ParseError("Invalid CPU ID value for lower boundary of"
302
                              " CPU ID range: %s" % str(err))
303
    try:
304
      higher = int(boundaries[-1])
305
    except (ValueError, TypeError), err:
306
      raise errors.ParseError("Invalid CPU ID value for higher boundary of"
307
                              " CPU ID range: %s" % str(err))
308
    if lower > higher:
309
      raise errors.ParseError("Invalid CPU ID range definition"
310
                              " (%d > %d): %s" % (lower, higher, range_def))
311
    cpu_list.extend(range(lower, higher + 1))
312
  return cpu_list
313

    
314

    
315
def GetHomeDir(user, default=None):
316
  """Try to get the homedir of the given user.
317

318
  The user can be passed either as a string (denoting the name) or as
319
  an integer (denoting the user id). If the user is not found, the
320
  'default' argument is returned, which defaults to None.
321

322
  """
323
  try:
324
    if isinstance(user, basestring):
325
      result = pwd.getpwnam(user)
326
    elif isinstance(user, (int, long)):
327
      result = pwd.getpwuid(user)
328
    else:
329
      raise errors.ProgrammerError("Invalid type passed to GetHomeDir (%s)" %
330
                                   type(user))
331
  except KeyError:
332
    return default
333
  return result.pw_dir
334

    
335

    
336
def NewUUID():
337
  """Returns a random UUID.
338

339
  @note: This is a Linux-specific method as it uses the /proc
340
      filesystem.
341
  @rtype: str
342

343
  """
344
  return ReadFile(_RANDOM_UUID_FILE, size=128).rstrip("\n")
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([constants.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([constants.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
class SignalWakeupFd(object):
638
  try:
639
    # This is only supported in Python 2.5 and above (some distributions
640
    # backported it to Python 2.4)
641
    _set_wakeup_fd_fn = signal.set_wakeup_fd
642
  except AttributeError:
643
    # Not supported
644
    def _SetWakeupFd(self, _): # pylint: disable-msg=R0201
645
      return -1
646
  else:
647
    def _SetWakeupFd(self, fd):
648
      return self._set_wakeup_fd_fn(fd)
649

    
650
  def __init__(self):
651
    """Initializes this class.
652

653
    """
654
    (read_fd, write_fd) = os.pipe()
655

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

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

    
664
    # Utility functions
665
    self.fileno = self._read_fh.fileno
666
    self.read = self._read_fh.read
667

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

671
    """
672
    if hasattr(self, "_previous") and self._previous is not None:
673
      self._SetWakeupFd(self._previous)
674
      self._previous = None
675

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

679
    """
680
    self._write_fh.write("\0")
681

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

685
    """
686
    self.Reset()
687

    
688

    
689
class SignalHandler(object):
690
  """Generic signal handler class.
691

692
  It automatically restores the original handler when deconstructed or
693
  when L{Reset} is called. You can either pass your own handler
694
  function in or query the L{called} attribute to detect whether the
695
  signal was sent.
696

697
  @type signum: list
698
  @ivar signum: the signals we handle
699
  @type called: boolean
700
  @ivar called: tracks whether any of the signals have been raised
701

702
  """
703
  def __init__(self, signum, handler_fn=None, wakeup=None):
704
    """Constructs a new SignalHandler instance.
705

706
    @type signum: int or list of ints
707
    @param signum: Single signal number or set of signal numbers
708
    @type handler_fn: callable
709
    @param handler_fn: Signal handling function
710

711
    """
712
    assert handler_fn is None or callable(handler_fn)
713

    
714
    self.signum = set(signum)
715
    self.called = False
716

    
717
    self._handler_fn = handler_fn
718
    self._wakeup = wakeup
719

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

    
738
  def __del__(self):
739
    self.Reset()
740

    
741
  def Reset(self):
742
    """Restore previous handler.
743

744
    This will reset all the signals to their previous handlers.
745

746
    """
747
    for signum, prev_handler in self._previous.items():
748
      signal.signal(signum, prev_handler)
749
      # If successful, remove from dict
750
      del self._previous[signum]
751

    
752
  def Clear(self):
753
    """Unsets the L{called} flag.
754

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

757
    """
758
    self.called = False
759

    
760
  def _HandleSignal(self, signum, frame):
761
    """Actual signal handling function.
762

763
    """
764
    # This is not nice and not absolutely atomic, but it appears to be the only
765
    # solution in Python -- there are no atomic types.
766
    self.called = True
767

    
768
    if self._wakeup:
769
      # Notify whoever is interested in signals
770
      self._wakeup.Notify()
771

    
772
    if self._handler_fn:
773
      self._handler_fn(signum, frame)
774

    
775

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

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

784
  Internally, all fields are held as regular expression objects.
785

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

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

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

797
    @type field: str
798
    @param field: the string to match
799
    @return: either None or a regular expression match object
800

801
    """
802
    for m in itertools.ifilter(None, (val.match(field) for val in self.items)):
803
      return m
804
    return None
805

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

809
    @type items: list
810
    @param items: the list of fields to check
811
    @rtype: list
812
    @return: list of non-matching fields
813

814
    """
815
    return [val for val in items if not self.Matches(val)]