Statistics
| Branch: | Tag: | Revision:

root / lib / utils.py @ 682f7601

History | View | Annotate | Download (81.2 kB)

1
#
2
#
3

    
4
# Copyright (C) 2006, 2007 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 time
32
import subprocess
33
import re
34
import socket
35
import tempfile
36
import shutil
37
import errno
38
import pwd
39
import itertools
40
import select
41
import fcntl
42
import resource
43
import logging
44
import logging.handlers
45
import signal
46
import datetime
47
import calendar
48
import collections
49
import struct
50
import IN
51

    
52
from cStringIO import StringIO
53

    
54
try:
55
  from hashlib import sha1
56
except ImportError:
57
  import sha
58
  sha1 = sha.new
59

    
60
from ganeti import errors
61
from ganeti import constants
62

    
63

    
64
_locksheld = []
65
_re_shell_unquoted = re.compile('^[-.,=:/_+@A-Za-z0-9]+$')
66

    
67
debug_locks = False
68

    
69
#: when set to True, L{RunCmd} is disabled
70
no_fork = False
71

    
72
_RANDOM_UUID_FILE = "/proc/sys/kernel/random/uuid"
73

    
74
# Structure definition for getsockopt(SOL_SOCKET, SO_PEERCRED, ...):
75
# struct ucred { pid_t pid; uid_t uid; gid_t gid; };
76
#
77
# The GNU C Library defines gid_t and uid_t to be "unsigned int" and
78
# pid_t to "int".
79
#
80
# IEEE Std 1003.1-2008:
81
# "nlink_t, uid_t, gid_t, and id_t shall be integer types"
82
# "blksize_t, pid_t, and ssize_t shall be signed integer types"
83
_STRUCT_UCRED = "iII"
84
_STRUCT_UCRED_SIZE = struct.calcsize(_STRUCT_UCRED)
85

    
86

    
87
class RunResult(object):
88
  """Holds the result of running external programs.
89

90
  @type exit_code: int
91
  @ivar exit_code: the exit code of the program, or None (if the program
92
      didn't exit())
93
  @type signal: int or None
94
  @ivar signal: the signal that caused the program to finish, or None
95
      (if the program wasn't terminated by a signal)
96
  @type stdout: str
97
  @ivar stdout: the standard output of the program
98
  @type stderr: str
99
  @ivar stderr: the standard error of the program
100
  @type failed: boolean
101
  @ivar failed: True in case the program was
102
      terminated by a signal or exited with a non-zero exit code
103
  @ivar fail_reason: a string detailing the termination reason
104

105
  """
106
  __slots__ = ["exit_code", "signal", "stdout", "stderr",
107
               "failed", "fail_reason", "cmd"]
108

    
109

    
110
  def __init__(self, exit_code, signal_, stdout, stderr, cmd):
111
    self.cmd = cmd
112
    self.exit_code = exit_code
113
    self.signal = signal_
114
    self.stdout = stdout
115
    self.stderr = stderr
116
    self.failed = (signal_ is not None or exit_code != 0)
117

    
118
    if self.signal is not None:
119
      self.fail_reason = "terminated by signal %s" % self.signal
120
    elif self.exit_code is not None:
121
      self.fail_reason = "exited with exit code %s" % self.exit_code
122
    else:
123
      self.fail_reason = "unable to determine termination reason"
124

    
125
    if self.failed:
126
      logging.debug("Command '%s' failed (%s); output: %s",
127
                    self.cmd, self.fail_reason, self.output)
128

    
129
  def _GetOutput(self):
130
    """Returns the combined stdout and stderr for easier usage.
131

132
    """
133
    return self.stdout + self.stderr
134

    
135
  output = property(_GetOutput, None, None, "Return full output")
136

    
137

    
138
def RunCmd(cmd, env=None, output=None, cwd='/', reset_env=False):
139
  """Execute a (shell) command.
140

141
  The command should not read from its standard input, as it will be
142
  closed.
143

144
  @type cmd: string or list
145
  @param cmd: Command to run
146
  @type env: dict
147
  @param env: Additional environment
148
  @type output: str
149
  @param output: if desired, the output of the command can be
150
      saved in a file instead of the RunResult instance; this
151
      parameter denotes the file name (if not None)
152
  @type cwd: string
153
  @param cwd: if specified, will be used as the working
154
      directory for the command; the default will be /
155
  @type reset_env: boolean
156
  @param reset_env: whether to reset or keep the default os environment
157
  @rtype: L{RunResult}
158
  @return: RunResult instance
159
  @raise errors.ProgrammerError: if we call this when forks are disabled
160

161
  """
162
  if no_fork:
163
    raise errors.ProgrammerError("utils.RunCmd() called with fork() disabled")
164

    
165
  if isinstance(cmd, list):
166
    cmd = [str(val) for val in cmd]
167
    strcmd = " ".join(cmd)
168
    shell = False
169
  else:
170
    strcmd = cmd
171
    shell = True
172
  logging.debug("RunCmd '%s'", strcmd)
173

    
174
  if not reset_env:
175
    cmd_env = os.environ.copy()
176
    cmd_env["LC_ALL"] = "C"
177
  else:
178
    cmd_env = {}
179

    
180
  if env is not None:
181
    cmd_env.update(env)
182

    
183
  try:
184
    if output is None:
185
      out, err, status = _RunCmdPipe(cmd, cmd_env, shell, cwd)
186
    else:
187
      status = _RunCmdFile(cmd, cmd_env, shell, output, cwd)
188
      out = err = ""
189
  except OSError, err:
190
    if err.errno == errno.ENOENT:
191
      raise errors.OpExecError("Can't execute '%s': not found (%s)" %
192
                               (strcmd, err))
193
    else:
194
      raise
195

    
196
  if status >= 0:
197
    exitcode = status
198
    signal_ = None
199
  else:
200
    exitcode = None
201
    signal_ = -status
202

    
203
  return RunResult(exitcode, signal_, out, err, strcmd)
204

    
205

    
206
def _RunCmdPipe(cmd, env, via_shell, cwd):
207
  """Run a command and return its output.
208

209
  @type  cmd: string or list
210
  @param cmd: Command to run
211
  @type env: dict
212
  @param env: The environment to use
213
  @type via_shell: bool
214
  @param via_shell: if we should run via the shell
215
  @type cwd: string
216
  @param cwd: the working directory for the program
217
  @rtype: tuple
218
  @return: (out, err, status)
219

220
  """
221
  poller = select.poll()
222
  child = subprocess.Popen(cmd, shell=via_shell,
223
                           stderr=subprocess.PIPE,
224
                           stdout=subprocess.PIPE,
225
                           stdin=subprocess.PIPE,
226
                           close_fds=True, env=env,
227
                           cwd=cwd)
228

    
229
  child.stdin.close()
230
  poller.register(child.stdout, select.POLLIN)
231
  poller.register(child.stderr, select.POLLIN)
232
  out = StringIO()
233
  err = StringIO()
234
  fdmap = {
235
    child.stdout.fileno(): (out, child.stdout),
236
    child.stderr.fileno(): (err, child.stderr),
237
    }
238
  for fd in fdmap:
239
    status = fcntl.fcntl(fd, fcntl.F_GETFL)
240
    fcntl.fcntl(fd, fcntl.F_SETFL, status | os.O_NONBLOCK)
241

    
242
  while fdmap:
243
    try:
244
      pollresult = poller.poll()
245
    except EnvironmentError, eerr:
246
      if eerr.errno == errno.EINTR:
247
        continue
248
      raise
249
    except select.error, serr:
250
      if serr[0] == errno.EINTR:
251
        continue
252
      raise
253

    
254
    for fd, event in pollresult:
255
      if event & select.POLLIN or event & select.POLLPRI:
256
        data = fdmap[fd][1].read()
257
        # no data from read signifies EOF (the same as POLLHUP)
258
        if not data:
259
          poller.unregister(fd)
260
          del fdmap[fd]
261
          continue
262
        fdmap[fd][0].write(data)
263
      if (event & select.POLLNVAL or event & select.POLLHUP or
264
          event & select.POLLERR):
265
        poller.unregister(fd)
266
        del fdmap[fd]
267

    
268
  out = out.getvalue()
269
  err = err.getvalue()
270

    
271
  status = child.wait()
272
  return out, err, status
273

    
274

    
275
def _RunCmdFile(cmd, env, via_shell, output, cwd):
276
  """Run a command and save its output to a file.
277

278
  @type  cmd: string or list
279
  @param cmd: Command to run
280
  @type env: dict
281
  @param env: The environment to use
282
  @type via_shell: bool
283
  @param via_shell: if we should run via the shell
284
  @type output: str
285
  @param output: the filename in which to save the output
286
  @type cwd: string
287
  @param cwd: the working directory for the program
288
  @rtype: int
289
  @return: the exit status
290

291
  """
292
  fh = open(output, "a")
293
  try:
294
    child = subprocess.Popen(cmd, shell=via_shell,
295
                             stderr=subprocess.STDOUT,
296
                             stdout=fh,
297
                             stdin=subprocess.PIPE,
298
                             close_fds=True, env=env,
299
                             cwd=cwd)
300

    
301
    child.stdin.close()
302
    status = child.wait()
303
  finally:
304
    fh.close()
305
  return status
306

    
307

    
308
def RunParts(dir_name, env=None, reset_env=False):
309
  """Run Scripts or programs in a directory
310

311
  @type dir_name: string
312
  @param dir_name: absolute path to a directory
313
  @type env: dict
314
  @param env: The environment to use
315
  @type reset_env: boolean
316
  @param reset_env: whether to reset or keep the default os environment
317
  @rtype: list of tuples
318
  @return: list of (name, (one of RUNDIR_STATUS), RunResult)
319

320
  """
321
  rr = []
322

    
323
  try:
324
    dir_contents = ListVisibleFiles(dir_name)
325
  except OSError, err:
326
    logging.warning("RunParts: skipping %s (cannot list: %s)", dir_name, err)
327
    return rr
328

    
329
  for relname in sorted(dir_contents):
330
    fname = PathJoin(dir_name, relname)
331
    if not (os.path.isfile(fname) and os.access(fname, os.X_OK) and
332
            constants.EXT_PLUGIN_MASK.match(relname) is not None):
333
      rr.append((relname, constants.RUNPARTS_SKIP, None))
334
    else:
335
      try:
336
        result = RunCmd([fname], env=env, reset_env=reset_env)
337
      except Exception, err: # pylint: disable-msg=W0703
338
        rr.append((relname, constants.RUNPARTS_ERR, str(err)))
339
      else:
340
        rr.append((relname, constants.RUNPARTS_RUN, result))
341

    
342
  return rr
343

    
344

    
345
def GetSocketCredentials(sock):
346
  """Returns the credentials of the foreign process connected to a socket.
347

348
  @param sock: Unix socket
349
  @rtype: tuple; (number, number, number)
350
  @return: The PID, UID and GID of the connected foreign process.
351

352
  """
353
  peercred = sock.getsockopt(socket.SOL_SOCKET, IN.SO_PEERCRED,
354
                             _STRUCT_UCRED_SIZE)
355
  return struct.unpack(_STRUCT_UCRED, peercred)
356

    
357

    
358
def RemoveFile(filename):
359
  """Remove a file ignoring some errors.
360

361
  Remove a file, ignoring non-existing ones or directories. Other
362
  errors are passed.
363

364
  @type filename: str
365
  @param filename: the file to be removed
366

367
  """
368
  try:
369
    os.unlink(filename)
370
  except OSError, err:
371
    if err.errno not in (errno.ENOENT, errno.EISDIR):
372
      raise
373

    
374

    
375
def RenameFile(old, new, mkdir=False, mkdir_mode=0750):
376
  """Renames a file.
377

378
  @type old: string
379
  @param old: Original path
380
  @type new: string
381
  @param new: New path
382
  @type mkdir: bool
383
  @param mkdir: Whether to create target directory if it doesn't exist
384
  @type mkdir_mode: int
385
  @param mkdir_mode: Mode for newly created directories
386

387
  """
388
  try:
389
    return os.rename(old, new)
390
  except OSError, err:
391
    # In at least one use case of this function, the job queue, directory
392
    # creation is very rare. Checking for the directory before renaming is not
393
    # as efficient.
394
    if mkdir and err.errno == errno.ENOENT:
395
      # Create directory and try again
396
      Makedirs(os.path.dirname(new), mode=mkdir_mode)
397

    
398
      return os.rename(old, new)
399

    
400
    raise
401

    
402

    
403
def Makedirs(path, mode=0750):
404
  """Super-mkdir; create a leaf directory and all intermediate ones.
405

406
  This is a wrapper around C{os.makedirs} adding error handling not implemented
407
  before Python 2.5.
408

409
  """
410
  try:
411
    os.makedirs(path, mode)
412
  except OSError, err:
413
    # Ignore EEXIST. This is only handled in os.makedirs as included in
414
    # Python 2.5 and above.
415
    if err.errno != errno.EEXIST or not os.path.exists(path):
416
      raise
417

    
418

    
419
def ResetTempfileModule():
420
  """Resets the random name generator of the tempfile module.
421

422
  This function should be called after C{os.fork} in the child process to
423
  ensure it creates a newly seeded random generator. Otherwise it would
424
  generate the same random parts as the parent process. If several processes
425
  race for the creation of a temporary file, this could lead to one not getting
426
  a temporary name.
427

428
  """
429
  # pylint: disable-msg=W0212
430
  if hasattr(tempfile, "_once_lock") and hasattr(tempfile, "_name_sequence"):
431
    tempfile._once_lock.acquire()
432
    try:
433
      # Reset random name generator
434
      tempfile._name_sequence = None
435
    finally:
436
      tempfile._once_lock.release()
437
  else:
438
    logging.critical("The tempfile module misses at least one of the"
439
                     " '_once_lock' and '_name_sequence' attributes")
440

    
441

    
442
def _FingerprintFile(filename):
443
  """Compute the fingerprint of a file.
444

445
  If the file does not exist, a None will be returned
446
  instead.
447

448
  @type filename: str
449
  @param filename: the filename to checksum
450
  @rtype: str
451
  @return: the hex digest of the sha checksum of the contents
452
      of the file
453

454
  """
455
  if not (os.path.exists(filename) and os.path.isfile(filename)):
456
    return None
457

    
458
  f = open(filename)
459

    
460
  fp = sha1()
461
  while True:
462
    data = f.read(4096)
463
    if not data:
464
      break
465

    
466
    fp.update(data)
467

    
468
  return fp.hexdigest()
469

    
470

    
471
def FingerprintFiles(files):
472
  """Compute fingerprints for a list of files.
473

474
  @type files: list
475
  @param files: the list of filename to fingerprint
476
  @rtype: dict
477
  @return: a dictionary filename: fingerprint, holding only
478
      existing files
479

480
  """
481
  ret = {}
482

    
483
  for filename in files:
484
    cksum = _FingerprintFile(filename)
485
    if cksum:
486
      ret[filename] = cksum
487

    
488
  return ret
489

    
490

    
491
def ForceDictType(target, key_types, allowed_values=None):
492
  """Force the values of a dict to have certain types.
493

494
  @type target: dict
495
  @param target: the dict to update
496
  @type key_types: dict
497
  @param key_types: dict mapping target dict keys to types
498
                    in constants.ENFORCEABLE_TYPES
499
  @type allowed_values: list
500
  @keyword allowed_values: list of specially allowed values
501

502
  """
503
  if allowed_values is None:
504
    allowed_values = []
505

    
506
  if not isinstance(target, dict):
507
    msg = "Expected dictionary, got '%s'" % target
508
    raise errors.TypeEnforcementError(msg)
509

    
510
  for key in target:
511
    if key not in key_types:
512
      msg = "Unknown key '%s'" % key
513
      raise errors.TypeEnforcementError(msg)
514

    
515
    if target[key] in allowed_values:
516
      continue
517

    
518
    ktype = key_types[key]
519
    if ktype not in constants.ENFORCEABLE_TYPES:
520
      msg = "'%s' has non-enforceable type %s" % (key, ktype)
521
      raise errors.ProgrammerError(msg)
522

    
523
    if ktype == constants.VTYPE_STRING:
524
      if not isinstance(target[key], basestring):
525
        if isinstance(target[key], bool) and not target[key]:
526
          target[key] = ''
527
        else:
528
          msg = "'%s' (value %s) is not a valid string" % (key, target[key])
529
          raise errors.TypeEnforcementError(msg)
530
    elif ktype == constants.VTYPE_BOOL:
531
      if isinstance(target[key], basestring) and target[key]:
532
        if target[key].lower() == constants.VALUE_FALSE:
533
          target[key] = False
534
        elif target[key].lower() == constants.VALUE_TRUE:
535
          target[key] = True
536
        else:
537
          msg = "'%s' (value %s) is not a valid boolean" % (key, target[key])
538
          raise errors.TypeEnforcementError(msg)
539
      elif target[key]:
540
        target[key] = True
541
      else:
542
        target[key] = False
543
    elif ktype == constants.VTYPE_SIZE:
544
      try:
545
        target[key] = ParseUnit(target[key])
546
      except errors.UnitParseError, err:
547
        msg = "'%s' (value %s) is not a valid size. error: %s" % \
548
              (key, target[key], err)
549
        raise errors.TypeEnforcementError(msg)
550
    elif ktype == constants.VTYPE_INT:
551
      try:
552
        target[key] = int(target[key])
553
      except (ValueError, TypeError):
554
        msg = "'%s' (value %s) is not a valid integer" % (key, target[key])
555
        raise errors.TypeEnforcementError(msg)
556

    
557

    
558
def IsProcessAlive(pid):
559
  """Check if a given pid exists on the system.
560

561
  @note: zombie status is not handled, so zombie processes
562
      will be returned as alive
563
  @type pid: int
564
  @param pid: the process ID to check
565
  @rtype: boolean
566
  @return: True if the process exists
567

568
  """
569
  def _TryStat(name):
570
    try:
571
      os.stat(name)
572
      return True
573
    except EnvironmentError, err:
574
      if err.errno in (errno.ENOENT, errno.ENOTDIR):
575
        return False
576
      elif err.errno == errno.EINVAL:
577
        raise RetryAgain(err)
578
      raise
579

    
580
  assert isinstance(pid, int), "pid must be an integer"
581
  if pid <= 0:
582
    return False
583

    
584
  proc_entry = "/proc/%d/status" % pid
585
  # /proc in a multiprocessor environment can have strange behaviors.
586
  # Retry the os.stat a few times until we get a good result.
587
  try:
588
    return Retry(_TryStat, (0.01, 1.5, 0.1), 0.5, args=[proc_entry])
589
  except RetryTimeout, err:
590
    err.RaiseInner()
591

    
592

    
593
def ReadPidFile(pidfile):
594
  """Read a pid from a file.
595

596
  @type  pidfile: string
597
  @param pidfile: path to the file containing the pid
598
  @rtype: int
599
  @return: The process id, if the file exists and contains a valid PID,
600
           otherwise 0
601

602
  """
603
  try:
604
    raw_data = ReadOneLineFile(pidfile)
605
  except EnvironmentError, err:
606
    if err.errno != errno.ENOENT:
607
      logging.exception("Can't read pid file")
608
    return 0
609

    
610
  try:
611
    pid = int(raw_data)
612
  except (TypeError, ValueError), err:
613
    logging.info("Can't parse pid file contents", exc_info=True)
614
    return 0
615

    
616
  return pid
617

    
618

    
619
def MatchNameComponent(key, name_list, case_sensitive=True):
620
  """Try to match a name against a list.
621

622
  This function will try to match a name like test1 against a list
623
  like C{['test1.example.com', 'test2.example.com', ...]}. Against
624
  this list, I{'test1'} as well as I{'test1.example'} will match, but
625
  not I{'test1.ex'}. A multiple match will be considered as no match
626
  at all (e.g. I{'test1'} against C{['test1.example.com',
627
  'test1.example.org']}), except when the key fully matches an entry
628
  (e.g. I{'test1'} against C{['test1', 'test1.example.com']}).
629

630
  @type key: str
631
  @param key: the name to be searched
632
  @type name_list: list
633
  @param name_list: the list of strings against which to search the key
634
  @type case_sensitive: boolean
635
  @param case_sensitive: whether to provide a case-sensitive match
636

637
  @rtype: None or str
638
  @return: None if there is no match I{or} if there are multiple matches,
639
      otherwise the element from the list which matches
640

641
  """
642
  if key in name_list:
643
    return key
644

    
645
  re_flags = 0
646
  if not case_sensitive:
647
    re_flags |= re.IGNORECASE
648
    key = key.upper()
649
  mo = re.compile("^%s(\..*)?$" % re.escape(key), re_flags)
650
  names_filtered = []
651
  string_matches = []
652
  for name in name_list:
653
    if mo.match(name) is not None:
654
      names_filtered.append(name)
655
      if not case_sensitive and key == name.upper():
656
        string_matches.append(name)
657

    
658
  if len(string_matches) == 1:
659
    return string_matches[0]
660
  if len(names_filtered) == 1:
661
    return names_filtered[0]
662
  return None
663

    
664

    
665
class HostInfo:
666
  """Class implementing resolver and hostname functionality
667

668
  """
669
  _VALID_NAME_RE = re.compile("^[a-z0-9._-]{1,255}$")
670

    
671
  def __init__(self, name=None):
672
    """Initialize the host name object.
673

674
    If the name argument is not passed, it will use this system's
675
    name.
676

677
    """
678
    if name is None:
679
      name = self.SysName()
680

    
681
    self.query = name
682
    self.name, self.aliases, self.ipaddrs = self.LookupHostname(name)
683
    self.ip = self.ipaddrs[0]
684

    
685
  def ShortName(self):
686
    """Returns the hostname without domain.
687

688
    """
689
    return self.name.split('.')[0]
690

    
691
  @staticmethod
692
  def SysName():
693
    """Return the current system's name.
694

695
    This is simply a wrapper over C{socket.gethostname()}.
696

697
    """
698
    return socket.gethostname()
699

    
700
  @staticmethod
701
  def LookupHostname(hostname):
702
    """Look up hostname
703

704
    @type hostname: str
705
    @param hostname: hostname to look up
706

707
    @rtype: tuple
708
    @return: a tuple (name, aliases, ipaddrs) as returned by
709
        C{socket.gethostbyname_ex}
710
    @raise errors.ResolverError: in case of errors in resolving
711

712
    """
713
    try:
714
      result = socket.gethostbyname_ex(hostname)
715
    except socket.gaierror, err:
716
      # hostname not found in DNS
717
      raise errors.ResolverError(hostname, err.args[0], err.args[1])
718

    
719
    return result
720

    
721
  @classmethod
722
  def NormalizeName(cls, hostname):
723
    """Validate and normalize the given hostname.
724

725
    @attention: the validation is a bit more relaxed than the standards
726
        require; most importantly, we allow underscores in names
727
    @raise errors.OpPrereqError: when the name is not valid
728

729
    """
730
    hostname = hostname.lower()
731
    if (not cls._VALID_NAME_RE.match(hostname) or
732
        # double-dots, meaning empty label
733
        ".." in hostname or
734
        # empty initial label
735
        hostname.startswith(".")):
736
      raise errors.OpPrereqError("Invalid hostname '%s'" % hostname,
737
                                 errors.ECODE_INVAL)
738
    if hostname.endswith("."):
739
      hostname = hostname.rstrip(".")
740
    return hostname
741

    
742

    
743
def GetHostInfo(name=None):
744
  """Lookup host name and raise an OpPrereqError for failures"""
745

    
746
  try:
747
    return HostInfo(name)
748
  except errors.ResolverError, err:
749
    raise errors.OpPrereqError("The given name (%s) does not resolve: %s" %
750
                               (err[0], err[2]), errors.ECODE_RESOLVER)
751

    
752

    
753
def ListVolumeGroups():
754
  """List volume groups and their size
755

756
  @rtype: dict
757
  @return:
758
       Dictionary with keys volume name and values
759
       the size of the volume
760

761
  """
762
  command = "vgs --noheadings --units m --nosuffix -o name,size"
763
  result = RunCmd(command)
764
  retval = {}
765
  if result.failed:
766
    return retval
767

    
768
  for line in result.stdout.splitlines():
769
    try:
770
      name, size = line.split()
771
      size = int(float(size))
772
    except (IndexError, ValueError), err:
773
      logging.error("Invalid output from vgs (%s): %s", err, line)
774
      continue
775

    
776
    retval[name] = size
777

    
778
  return retval
779

    
780

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

784
  @type bridge: str
785
  @param bridge: the bridge name to check
786
  @rtype: boolean
787
  @return: True if it does
788

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

    
792

    
793
def NiceSort(name_list):
794
  """Sort a list of strings based on digit and non-digit groupings.
795

796
  Given a list of names C{['a1', 'a10', 'a11', 'a2']} this function
797
  will sort the list in the logical order C{['a1', 'a2', 'a10',
798
  'a11']}.
799

800
  The sort algorithm breaks each name in groups of either only-digits
801
  or no-digits. Only the first eight such groups are considered, and
802
  after that we just use what's left of the string.
803

804
  @type name_list: list
805
  @param name_list: the names to be sorted
806
  @rtype: list
807
  @return: a copy of the name list sorted with our algorithm
808

809
  """
810
  _SORTER_BASE = "(\D+|\d+)"
811
  _SORTER_FULL = "^%s%s?%s?%s?%s?%s?%s?%s?.*$" % (_SORTER_BASE, _SORTER_BASE,
812
                                                  _SORTER_BASE, _SORTER_BASE,
813
                                                  _SORTER_BASE, _SORTER_BASE,
814
                                                  _SORTER_BASE, _SORTER_BASE)
815
  _SORTER_RE = re.compile(_SORTER_FULL)
816
  _SORTER_NODIGIT = re.compile("^\D*$")
817
  def _TryInt(val):
818
    """Attempts to convert a variable to integer."""
819
    if val is None or _SORTER_NODIGIT.match(val):
820
      return val
821
    rval = int(val)
822
    return rval
823

    
824
  to_sort = [([_TryInt(grp) for grp in _SORTER_RE.match(name).groups()], name)
825
             for name in name_list]
826
  to_sort.sort()
827
  return [tup[1] for tup in to_sort]
828

    
829

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

833
  This function tries to apply function I{fn} to I{val}. If no
834
  C{ValueError} or C{TypeError} exceptions are raised, it will return
835
  the result, else it will return the original value. Any other
836
  exceptions are propagated to the caller.
837

838
  @type fn: callable
839
  @param fn: function to apply to the value
840
  @param val: the value to be converted
841
  @return: The converted value if the conversion was successful,
842
      otherwise the original value.
843

844
  """
845
  try:
846
    nv = fn(val)
847
  except (ValueError, TypeError):
848
    nv = val
849
  return nv
850

    
851

    
852
def IsValidIP(ip):
853
  """Verifies the syntax of an IPv4 address.
854

855
  This function checks if the IPv4 address passes is valid or not based
856
  on syntax (not IP range, class calculations, etc.).
857

858
  @type ip: str
859
  @param ip: the address to be checked
860
  @rtype: a regular expression match object
861
  @return: a regular expression match object, or None if the
862
      address is not valid
863

864
  """
865
  unit = "(0|[1-9]\d{0,2})"
866
  #TODO: convert and return only boolean
867
  return re.match("^%s\.%s\.%s\.%s$" % (unit, unit, unit, unit), ip)
868

    
869

    
870
def IsValidShellParam(word):
871
  """Verifies is the given word is safe from the shell's p.o.v.
872

873
  This means that we can pass this to a command via the shell and be
874
  sure that it doesn't alter the command line and is passed as such to
875
  the actual command.
876

877
  Note that we are overly restrictive here, in order to be on the safe
878
  side.
879

880
  @type word: str
881
  @param word: the word to check
882
  @rtype: boolean
883
  @return: True if the word is 'safe'
884

885
  """
886
  return bool(re.match("^[-a-zA-Z0-9._+/:%@]+$", word))
887

    
888

    
889
def BuildShellCmd(template, *args):
890
  """Build a safe shell command line from the given arguments.
891

892
  This function will check all arguments in the args list so that they
893
  are valid shell parameters (i.e. they don't contain shell
894
  metacharacters). If everything is ok, it will return the result of
895
  template % args.
896

897
  @type template: str
898
  @param template: the string holding the template for the
899
      string formatting
900
  @rtype: str
901
  @return: the expanded command line
902

903
  """
904
  for word in args:
905
    if not IsValidShellParam(word):
906
      raise errors.ProgrammerError("Shell argument '%s' contains"
907
                                   " invalid characters" % word)
908
  return template % args
909

    
910

    
911
def FormatUnit(value, units):
912
  """Formats an incoming number of MiB with the appropriate unit.
913

914
  @type value: int
915
  @param value: integer representing the value in MiB (1048576)
916
  @type units: char
917
  @param units: the type of formatting we should do:
918
      - 'h' for automatic scaling
919
      - 'm' for MiBs
920
      - 'g' for GiBs
921
      - 't' for TiBs
922
  @rtype: str
923
  @return: the formatted value (with suffix)
924

925
  """
926
  if units not in ('m', 'g', 't', 'h'):
927
    raise errors.ProgrammerError("Invalid unit specified '%s'" % str(units))
928

    
929
  suffix = ''
930

    
931
  if units == 'm' or (units == 'h' and value < 1024):
932
    if units == 'h':
933
      suffix = 'M'
934
    return "%d%s" % (round(value, 0), suffix)
935

    
936
  elif units == 'g' or (units == 'h' and value < (1024 * 1024)):
937
    if units == 'h':
938
      suffix = 'G'
939
    return "%0.1f%s" % (round(float(value) / 1024, 1), suffix)
940

    
941
  else:
942
    if units == 'h':
943
      suffix = 'T'
944
    return "%0.1f%s" % (round(float(value) / 1024 / 1024, 1), suffix)
945

    
946

    
947
def ParseUnit(input_string):
948
  """Tries to extract number and scale from the given string.
949

950
  Input must be in the format C{NUMBER+ [DOT NUMBER+] SPACE*
951
  [UNIT]}. If no unit is specified, it defaults to MiB. Return value
952
  is always an int in MiB.
953

954
  """
955
  m = re.match('^([.\d]+)\s*([a-zA-Z]+)?$', str(input_string))
956
  if not m:
957
    raise errors.UnitParseError("Invalid format")
958

    
959
  value = float(m.groups()[0])
960

    
961
  unit = m.groups()[1]
962
  if unit:
963
    lcunit = unit.lower()
964
  else:
965
    lcunit = 'm'
966

    
967
  if lcunit in ('m', 'mb', 'mib'):
968
    # Value already in MiB
969
    pass
970

    
971
  elif lcunit in ('g', 'gb', 'gib'):
972
    value *= 1024
973

    
974
  elif lcunit in ('t', 'tb', 'tib'):
975
    value *= 1024 * 1024
976

    
977
  else:
978
    raise errors.UnitParseError("Unknown unit: %s" % unit)
979

    
980
  # Make sure we round up
981
  if int(value) < value:
982
    value += 1
983

    
984
  # Round up to the next multiple of 4
985
  value = int(value)
986
  if value % 4:
987
    value += 4 - value % 4
988

    
989
  return value
990

    
991

    
992
def AddAuthorizedKey(file_name, key):
993
  """Adds an SSH public key to an authorized_keys file.
994

995
  @type file_name: str
996
  @param file_name: path to authorized_keys file
997
  @type key: str
998
  @param key: string containing key
999

1000
  """
1001
  key_fields = key.split()
1002

    
1003
  f = open(file_name, 'a+')
1004
  try:
1005
    nl = True
1006
    for line in f:
1007
      # Ignore whitespace changes
1008
      if line.split() == key_fields:
1009
        break
1010
      nl = line.endswith('\n')
1011
    else:
1012
      if not nl:
1013
        f.write("\n")
1014
      f.write(key.rstrip('\r\n'))
1015
      f.write("\n")
1016
      f.flush()
1017
  finally:
1018
    f.close()
1019

    
1020

    
1021
def RemoveAuthorizedKey(file_name, key):
1022
  """Removes an SSH public key from an authorized_keys file.
1023

1024
  @type file_name: str
1025
  @param file_name: path to authorized_keys file
1026
  @type key: str
1027
  @param key: string containing key
1028

1029
  """
1030
  key_fields = key.split()
1031

    
1032
  fd, tmpname = tempfile.mkstemp(dir=os.path.dirname(file_name))
1033
  try:
1034
    out = os.fdopen(fd, 'w')
1035
    try:
1036
      f = open(file_name, 'r')
1037
      try:
1038
        for line in f:
1039
          # Ignore whitespace changes while comparing lines
1040
          if line.split() != key_fields:
1041
            out.write(line)
1042

    
1043
        out.flush()
1044
        os.rename(tmpname, file_name)
1045
      finally:
1046
        f.close()
1047
    finally:
1048
      out.close()
1049
  except:
1050
    RemoveFile(tmpname)
1051
    raise
1052

    
1053

    
1054
def SetEtcHostsEntry(file_name, ip, hostname, aliases):
1055
  """Sets the name of an IP address and hostname in /etc/hosts.
1056

1057
  @type file_name: str
1058
  @param file_name: path to the file to modify (usually C{/etc/hosts})
1059
  @type ip: str
1060
  @param ip: the IP address
1061
  @type hostname: str
1062
  @param hostname: the hostname to be added
1063
  @type aliases: list
1064
  @param aliases: the list of aliases to add for the hostname
1065

1066
  """
1067
  # FIXME: use WriteFile + fn rather than duplicating its efforts
1068
  # Ensure aliases are unique
1069
  aliases = UniqueSequence([hostname] + aliases)[1:]
1070

    
1071
  fd, tmpname = tempfile.mkstemp(dir=os.path.dirname(file_name))
1072
  try:
1073
    out = os.fdopen(fd, 'w')
1074
    try:
1075
      f = open(file_name, 'r')
1076
      try:
1077
        for line in f:
1078
          fields = line.split()
1079
          if fields and not fields[0].startswith('#') and ip == fields[0]:
1080
            continue
1081
          out.write(line)
1082

    
1083
        out.write("%s\t%s" % (ip, hostname))
1084
        if aliases:
1085
          out.write(" %s" % ' '.join(aliases))
1086
        out.write('\n')
1087

    
1088
        out.flush()
1089
        os.fsync(out)
1090
        os.chmod(tmpname, 0644)
1091
        os.rename(tmpname, file_name)
1092
      finally:
1093
        f.close()
1094
    finally:
1095
      out.close()
1096
  except:
1097
    RemoveFile(tmpname)
1098
    raise
1099

    
1100

    
1101
def AddHostToEtcHosts(hostname):
1102
  """Wrapper around SetEtcHostsEntry.
1103

1104
  @type hostname: str
1105
  @param hostname: a hostname that will be resolved and added to
1106
      L{constants.ETC_HOSTS}
1107

1108
  """
1109
  hi = HostInfo(name=hostname)
1110
  SetEtcHostsEntry(constants.ETC_HOSTS, hi.ip, hi.name, [hi.ShortName()])
1111

    
1112

    
1113
def RemoveEtcHostsEntry(file_name, hostname):
1114
  """Removes a hostname from /etc/hosts.
1115

1116
  IP addresses without names are removed from the file.
1117

1118
  @type file_name: str
1119
  @param file_name: path to the file to modify (usually C{/etc/hosts})
1120
  @type hostname: str
1121
  @param hostname: the hostname to be removed
1122

1123
  """
1124
  # FIXME: use WriteFile + fn rather than duplicating its efforts
1125
  fd, tmpname = tempfile.mkstemp(dir=os.path.dirname(file_name))
1126
  try:
1127
    out = os.fdopen(fd, 'w')
1128
    try:
1129
      f = open(file_name, 'r')
1130
      try:
1131
        for line in f:
1132
          fields = line.split()
1133
          if len(fields) > 1 and not fields[0].startswith('#'):
1134
            names = fields[1:]
1135
            if hostname in names:
1136
              while hostname in names:
1137
                names.remove(hostname)
1138
              if names:
1139
                out.write("%s %s\n" % (fields[0], ' '.join(names)))
1140
              continue
1141

    
1142
          out.write(line)
1143

    
1144
        out.flush()
1145
        os.fsync(out)
1146
        os.chmod(tmpname, 0644)
1147
        os.rename(tmpname, file_name)
1148
      finally:
1149
        f.close()
1150
    finally:
1151
      out.close()
1152
  except:
1153
    RemoveFile(tmpname)
1154
    raise
1155

    
1156

    
1157
def RemoveHostFromEtcHosts(hostname):
1158
  """Wrapper around RemoveEtcHostsEntry.
1159

1160
  @type hostname: str
1161
  @param hostname: hostname that will be resolved and its
1162
      full and shot name will be removed from
1163
      L{constants.ETC_HOSTS}
1164

1165
  """
1166
  hi = HostInfo(name=hostname)
1167
  RemoveEtcHostsEntry(constants.ETC_HOSTS, hi.name)
1168
  RemoveEtcHostsEntry(constants.ETC_HOSTS, hi.ShortName())
1169

    
1170

    
1171
def TimestampForFilename():
1172
  """Returns the current time formatted for filenames.
1173

1174
  The format doesn't contain colons as some shells and applications them as
1175
  separators.
1176

1177
  """
1178
  return time.strftime("%Y-%m-%d_%H_%M_%S")
1179

    
1180

    
1181
def CreateBackup(file_name):
1182
  """Creates a backup of a file.
1183

1184
  @type file_name: str
1185
  @param file_name: file to be backed up
1186
  @rtype: str
1187
  @return: the path to the newly created backup
1188
  @raise errors.ProgrammerError: for invalid file names
1189

1190
  """
1191
  if not os.path.isfile(file_name):
1192
    raise errors.ProgrammerError("Can't make a backup of a non-file '%s'" %
1193
                                file_name)
1194

    
1195
  prefix = ("%s.backup-%s." %
1196
            (os.path.basename(file_name), TimestampForFilename()))
1197
  dir_name = os.path.dirname(file_name)
1198

    
1199
  fsrc = open(file_name, 'rb')
1200
  try:
1201
    (fd, backup_name) = tempfile.mkstemp(prefix=prefix, dir=dir_name)
1202
    fdst = os.fdopen(fd, 'wb')
1203
    try:
1204
      logging.debug("Backing up %s at %s", file_name, backup_name)
1205
      shutil.copyfileobj(fsrc, fdst)
1206
    finally:
1207
      fdst.close()
1208
  finally:
1209
    fsrc.close()
1210

    
1211
  return backup_name
1212

    
1213

    
1214
def ShellQuote(value):
1215
  """Quotes shell argument according to POSIX.
1216

1217
  @type value: str
1218
  @param value: the argument to be quoted
1219
  @rtype: str
1220
  @return: the quoted value
1221

1222
  """
1223
  if _re_shell_unquoted.match(value):
1224
    return value
1225
  else:
1226
    return "'%s'" % value.replace("'", "'\\''")
1227

    
1228

    
1229
def ShellQuoteArgs(args):
1230
  """Quotes a list of shell arguments.
1231

1232
  @type args: list
1233
  @param args: list of arguments to be quoted
1234
  @rtype: str
1235
  @return: the quoted arguments concatenated with spaces
1236

1237
  """
1238
  return ' '.join([ShellQuote(i) for i in args])
1239

    
1240

    
1241
def TcpPing(target, port, timeout=10, live_port_needed=False, source=None):
1242
  """Simple ping implementation using TCP connect(2).
1243

1244
  Check if the given IP is reachable by doing attempting a TCP connect
1245
  to it.
1246

1247
  @type target: str
1248
  @param target: the IP or hostname to ping
1249
  @type port: int
1250
  @param port: the port to connect to
1251
  @type timeout: int
1252
  @param timeout: the timeout on the connection attempt
1253
  @type live_port_needed: boolean
1254
  @param live_port_needed: whether a closed port will cause the
1255
      function to return failure, as if there was a timeout
1256
  @type source: str or None
1257
  @param source: if specified, will cause the connect to be made
1258
      from this specific source address; failures to bind other
1259
      than C{EADDRNOTAVAIL} will be ignored
1260

1261
  """
1262
  sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
1263

    
1264
  success = False
1265

    
1266
  if source is not None:
1267
    try:
1268
      sock.bind((source, 0))
1269
    except socket.error, (errcode, _):
1270
      if errcode == errno.EADDRNOTAVAIL:
1271
        success = False
1272

    
1273
  sock.settimeout(timeout)
1274

    
1275
  try:
1276
    sock.connect((target, port))
1277
    sock.close()
1278
    success = True
1279
  except socket.timeout:
1280
    success = False
1281
  except socket.error, (errcode, _):
1282
    success = (not live_port_needed) and (errcode == errno.ECONNREFUSED)
1283

    
1284
  return success
1285

    
1286

    
1287
def OwnIpAddress(address):
1288
  """Check if the current host has the the given IP address.
1289

1290
  Currently this is done by TCP-pinging the address from the loopback
1291
  address.
1292

1293
  @type address: string
1294
  @param address: the address to check
1295
  @rtype: bool
1296
  @return: True if we own the address
1297

1298
  """
1299
  return TcpPing(address, constants.DEFAULT_NODED_PORT,
1300
                 source=constants.LOCALHOST_IP_ADDRESS)
1301

    
1302

    
1303
def ListVisibleFiles(path):
1304
  """Returns a list of visible files in a directory.
1305

1306
  @type path: str
1307
  @param path: the directory to enumerate
1308
  @rtype: list
1309
  @return: the list of all files not starting with a dot
1310
  @raise ProgrammerError: if L{path} is not an absolue and normalized path
1311

1312
  """
1313
  if not IsNormAbsPath(path):
1314
    raise errors.ProgrammerError("Path passed to ListVisibleFiles is not"
1315
                                 " absolute/normalized: '%s'" % path)
1316
  files = [i for i in os.listdir(path) if not i.startswith(".")]
1317
  files.sort()
1318
  return files
1319

    
1320

    
1321
def GetHomeDir(user, default=None):
1322
  """Try to get the homedir of the given user.
1323

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

1328
  """
1329
  try:
1330
    if isinstance(user, basestring):
1331
      result = pwd.getpwnam(user)
1332
    elif isinstance(user, (int, long)):
1333
      result = pwd.getpwuid(user)
1334
    else:
1335
      raise errors.ProgrammerError("Invalid type passed to GetHomeDir (%s)" %
1336
                                   type(user))
1337
  except KeyError:
1338
    return default
1339
  return result.pw_dir
1340

    
1341

    
1342
def NewUUID():
1343
  """Returns a random UUID.
1344

1345
  @note: This is a Linux-specific method as it uses the /proc
1346
      filesystem.
1347
  @rtype: str
1348

1349
  """
1350
  return ReadFile(_RANDOM_UUID_FILE, size=128).rstrip("\n")
1351

    
1352

    
1353
def GenerateSecret(numbytes=20):
1354
  """Generates a random secret.
1355

1356
  This will generate a pseudo-random secret returning an hex string
1357
  (so that it can be used where an ASCII string is needed).
1358

1359
  @param numbytes: the number of bytes which will be represented by the returned
1360
      string (defaulting to 20, the length of a SHA1 hash)
1361
  @rtype: str
1362
  @return: an hex representation of the pseudo-random sequence
1363

1364
  """
1365
  return os.urandom(numbytes).encode('hex')
1366

    
1367

    
1368
def EnsureDirs(dirs):
1369
  """Make required directories, if they don't exist.
1370

1371
  @param dirs: list of tuples (dir_name, dir_mode)
1372
  @type dirs: list of (string, integer)
1373

1374
  """
1375
  for dir_name, dir_mode in dirs:
1376
    try:
1377
      os.mkdir(dir_name, dir_mode)
1378
    except EnvironmentError, err:
1379
      if err.errno != errno.EEXIST:
1380
        raise errors.GenericError("Cannot create needed directory"
1381
                                  " '%s': %s" % (dir_name, err))
1382
    if not os.path.isdir(dir_name):
1383
      raise errors.GenericError("%s is not a directory" % dir_name)
1384

    
1385

    
1386
def ReadFile(file_name, size=-1):
1387
  """Reads a file.
1388

1389
  @type size: int
1390
  @param size: Read at most size bytes (if negative, entire file)
1391
  @rtype: str
1392
  @return: the (possibly partial) content of the file
1393

1394
  """
1395
  f = open(file_name, "r")
1396
  try:
1397
    return f.read(size)
1398
  finally:
1399
    f.close()
1400

    
1401

    
1402
def WriteFile(file_name, fn=None, data=None,
1403
              mode=None, uid=-1, gid=-1,
1404
              atime=None, mtime=None, close=True,
1405
              dry_run=False, backup=False,
1406
              prewrite=None, postwrite=None):
1407
  """(Over)write a file atomically.
1408

1409
  The file_name and either fn (a function taking one argument, the
1410
  file descriptor, and which should write the data to it) or data (the
1411
  contents of the file) must be passed. The other arguments are
1412
  optional and allow setting the file mode, owner and group, and the
1413
  mtime/atime of the file.
1414

1415
  If the function doesn't raise an exception, it has succeeded and the
1416
  target file has the new contents. If the function has raised an
1417
  exception, an existing target file should be unmodified and the
1418
  temporary file should be removed.
1419

1420
  @type file_name: str
1421
  @param file_name: the target filename
1422
  @type fn: callable
1423
  @param fn: content writing function, called with
1424
      file descriptor as parameter
1425
  @type data: str
1426
  @param data: contents of the file
1427
  @type mode: int
1428
  @param mode: file mode
1429
  @type uid: int
1430
  @param uid: the owner of the file
1431
  @type gid: int
1432
  @param gid: the group of the file
1433
  @type atime: int
1434
  @param atime: a custom access time to be set on the file
1435
  @type mtime: int
1436
  @param mtime: a custom modification time to be set on the file
1437
  @type close: boolean
1438
  @param close: whether to close file after writing it
1439
  @type prewrite: callable
1440
  @param prewrite: function to be called before writing content
1441
  @type postwrite: callable
1442
  @param postwrite: function to be called after writing content
1443

1444
  @rtype: None or int
1445
  @return: None if the 'close' parameter evaluates to True,
1446
      otherwise the file descriptor
1447

1448
  @raise errors.ProgrammerError: if any of the arguments are not valid
1449

1450
  """
1451
  if not os.path.isabs(file_name):
1452
    raise errors.ProgrammerError("Path passed to WriteFile is not"
1453
                                 " absolute: '%s'" % file_name)
1454

    
1455
  if [fn, data].count(None) != 1:
1456
    raise errors.ProgrammerError("fn or data required")
1457

    
1458
  if [atime, mtime].count(None) == 1:
1459
    raise errors.ProgrammerError("Both atime and mtime must be either"
1460
                                 " set or None")
1461

    
1462
  if backup and not dry_run and os.path.isfile(file_name):
1463
    CreateBackup(file_name)
1464

    
1465
  dir_name, base_name = os.path.split(file_name)
1466
  fd, new_name = tempfile.mkstemp('.new', base_name, dir_name)
1467
  do_remove = True
1468
  # here we need to make sure we remove the temp file, if any error
1469
  # leaves it in place
1470
  try:
1471
    if uid != -1 or gid != -1:
1472
      os.chown(new_name, uid, gid)
1473
    if mode:
1474
      os.chmod(new_name, mode)
1475
    if callable(prewrite):
1476
      prewrite(fd)
1477
    if data is not None:
1478
      os.write(fd, data)
1479
    else:
1480
      fn(fd)
1481
    if callable(postwrite):
1482
      postwrite(fd)
1483
    os.fsync(fd)
1484
    if atime is not None and mtime is not None:
1485
      os.utime(new_name, (atime, mtime))
1486
    if not dry_run:
1487
      os.rename(new_name, file_name)
1488
      do_remove = False
1489
  finally:
1490
    if close:
1491
      os.close(fd)
1492
      result = None
1493
    else:
1494
      result = fd
1495
    if do_remove:
1496
      RemoveFile(new_name)
1497

    
1498
  return result
1499

    
1500

    
1501
def ReadOneLineFile(file_name, strict=False):
1502
  """Return the first non-empty line from a file.
1503

1504
  @type strict: boolean
1505
  @param strict: if True, abort if the file has more than one
1506
      non-empty line
1507

1508
  """
1509
  file_lines = ReadFile(file_name).splitlines()
1510
  full_lines = filter(bool, file_lines)
1511
  if not file_lines or not full_lines:
1512
    raise errors.GenericError("No data in one-liner file %s" % file_name)
1513
  elif strict and len(full_lines) > 1:
1514
    raise errors.GenericError("Too many lines in one-liner file %s" %
1515
                              file_name)
1516
  return full_lines[0]
1517

    
1518

    
1519
def FirstFree(seq, base=0):
1520
  """Returns the first non-existing integer from seq.
1521

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

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

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

1531
  @type seq: sequence
1532
  @param seq: the sequence to be analyzed.
1533
  @type base: int
1534
  @param base: use this value as the base index of the sequence
1535
  @rtype: int
1536
  @return: the first non-used index in the sequence
1537

1538
  """
1539
  for idx, elem in enumerate(seq):
1540
    assert elem >= base, "Passed element is higher than base offset"
1541
    if elem > idx + base:
1542
      # idx is not used
1543
      return idx + base
1544
  return None
1545

    
1546

    
1547
def SingleWaitForFdCondition(fdobj, event, timeout):
1548
  """Waits for a condition to occur on the socket.
1549

1550
  Immediately returns at the first interruption.
1551

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

1561
  """
1562
  check = (event | select.POLLPRI |
1563
           select.POLLNVAL | select.POLLHUP | select.POLLERR)
1564

    
1565
  if timeout is not None:
1566
    # Poller object expects milliseconds
1567
    timeout *= 1000
1568

    
1569
  poller = select.poll()
1570
  poller.register(fdobj, event)
1571
  try:
1572
    # TODO: If the main thread receives a signal and we have no timeout, we
1573
    # could wait forever. This should check a global "quit" flag or something
1574
    # every so often.
1575
    io_events = poller.poll(timeout)
1576
  except select.error, err:
1577
    if err[0] != errno.EINTR:
1578
      raise
1579
    io_events = []
1580
  if io_events and io_events[0][1] & check:
1581
    return io_events[0][1]
1582
  else:
1583
    return None
1584

    
1585

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

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

1593
  """
1594

    
1595
  def __init__(self, timeout):
1596
    self.timeout = timeout
1597

    
1598
  def Poll(self, fdobj, event):
1599
    result = SingleWaitForFdCondition(fdobj, event, self.timeout)
1600
    if result is None:
1601
      raise RetryAgain()
1602
    else:
1603
      return result
1604

    
1605
  def UpdateTimeout(self, timeout):
1606
    self.timeout = timeout
1607

    
1608

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

1612
  Retries until the timeout is expired, even if interrupted.
1613

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

1623
  """
1624
  if timeout is not None:
1625
    retrywaiter = FdConditionWaiterHelper(timeout)
1626
    try:
1627
      result = Retry(retrywaiter.Poll, RETRY_REMAINING_TIME, timeout,
1628
                     args=(fdobj, event), wait_fn=retrywaiter.UpdateTimeout)
1629
    except RetryTimeout:
1630
      result = None
1631
  else:
1632
    result = None
1633
    while result is None:
1634
      result = SingleWaitForFdCondition(fdobj, event, timeout)
1635
  return result
1636

    
1637

    
1638
def UniqueSequence(seq):
1639
  """Returns a list with unique elements.
1640

1641
  Element order is preserved.
1642

1643
  @type seq: sequence
1644
  @param seq: the sequence with the source elements
1645
  @rtype: list
1646
  @return: list of unique elements from seq
1647

1648
  """
1649
  seen = set()
1650
  return [i for i in seq if i not in seen and not seen.add(i)]
1651

    
1652

    
1653
def NormalizeAndValidateMac(mac):
1654
  """Normalizes and check if a MAC address is valid.
1655

1656
  Checks whether the supplied MAC address is formally correct, only
1657
  accepts colon separated format. Normalize it to all lower.
1658

1659
  @type mac: str
1660
  @param mac: the MAC to be validated
1661
  @rtype: str
1662
  @return: returns the normalized and validated MAC.
1663

1664
  @raise errors.OpPrereqError: If the MAC isn't valid
1665

1666
  """
1667
  mac_check = re.compile("^([0-9a-f]{2}(:|$)){6}$", re.I)
1668
  if not mac_check.match(mac):
1669
    raise errors.OpPrereqError("Invalid MAC address specified: %s" %
1670
                               mac, errors.ECODE_INVAL)
1671

    
1672
  return mac.lower()
1673

    
1674

    
1675
def TestDelay(duration):
1676
  """Sleep for a fixed amount of time.
1677

1678
  @type duration: float
1679
  @param duration: the sleep duration
1680
  @rtype: boolean
1681
  @return: False for negative value, True otherwise
1682

1683
  """
1684
  if duration < 0:
1685
    return False, "Invalid sleep duration"
1686
  time.sleep(duration)
1687
  return True, None
1688

    
1689

    
1690
def _CloseFDNoErr(fd, retries=5):
1691
  """Close a file descriptor ignoring errors.
1692

1693
  @type fd: int
1694
  @param fd: the file descriptor
1695
  @type retries: int
1696
  @param retries: how many retries to make, in case we get any
1697
      other error than EBADF
1698

1699
  """
1700
  try:
1701
    os.close(fd)
1702
  except OSError, err:
1703
    if err.errno != errno.EBADF:
1704
      if retries > 0:
1705
        _CloseFDNoErr(fd, retries - 1)
1706
    # else either it's closed already or we're out of retries, so we
1707
    # ignore this and go on
1708

    
1709

    
1710
def CloseFDs(noclose_fds=None):
1711
  """Close file descriptors.
1712

1713
  This closes all file descriptors above 2 (i.e. except
1714
  stdin/out/err).
1715

1716
  @type noclose_fds: list or None
1717
  @param noclose_fds: if given, it denotes a list of file descriptor
1718
      that should not be closed
1719

1720
  """
1721
  # Default maximum for the number of available file descriptors.
1722
  if 'SC_OPEN_MAX' in os.sysconf_names:
1723
    try:
1724
      MAXFD = os.sysconf('SC_OPEN_MAX')
1725
      if MAXFD < 0:
1726
        MAXFD = 1024
1727
    except OSError:
1728
      MAXFD = 1024
1729
  else:
1730
    MAXFD = 1024
1731
  maxfd = resource.getrlimit(resource.RLIMIT_NOFILE)[1]
1732
  if (maxfd == resource.RLIM_INFINITY):
1733
    maxfd = MAXFD
1734

    
1735
  # Iterate through and close all file descriptors (except the standard ones)
1736
  for fd in range(3, maxfd):
1737
    if noclose_fds and fd in noclose_fds:
1738
      continue
1739
    _CloseFDNoErr(fd)
1740

    
1741

    
1742
def Daemonize(logfile):
1743
  """Daemonize the current process.
1744

1745
  This detaches the current process from the controlling terminal and
1746
  runs it in the background as a daemon.
1747

1748
  @type logfile: str
1749
  @param logfile: the logfile to which we should redirect stdout/stderr
1750
  @rtype: int
1751
  @return: the value zero
1752

1753
  """
1754
  # pylint: disable-msg=W0212
1755
  # yes, we really want os._exit
1756
  UMASK = 077
1757
  WORKDIR = "/"
1758

    
1759
  # this might fail
1760
  pid = os.fork()
1761
  if (pid == 0):  # The first child.
1762
    os.setsid()
1763
    # this might fail
1764
    pid = os.fork() # Fork a second child.
1765
    if (pid == 0):  # The second child.
1766
      os.chdir(WORKDIR)
1767
      os.umask(UMASK)
1768
    else:
1769
      # exit() or _exit()?  See below.
1770
      os._exit(0) # Exit parent (the first child) of the second child.
1771
  else:
1772
    os._exit(0) # Exit parent of the first child.
1773

    
1774
  for fd in range(3):
1775
    _CloseFDNoErr(fd)
1776
  i = os.open("/dev/null", os.O_RDONLY) # stdin
1777
  assert i == 0, "Can't close/reopen stdin"
1778
  i = os.open(logfile, os.O_WRONLY|os.O_CREAT|os.O_APPEND, 0600) # stdout
1779
  assert i == 1, "Can't close/reopen stdout"
1780
  # Duplicate standard output to standard error.
1781
  os.dup2(1, 2)
1782
  return 0
1783

    
1784

    
1785
def DaemonPidFileName(name):
1786
  """Compute a ganeti pid file absolute path
1787

1788
  @type name: str
1789
  @param name: the daemon name
1790
  @rtype: str
1791
  @return: the full path to the pidfile corresponding to the given
1792
      daemon name
1793

1794
  """
1795
  return PathJoin(constants.RUN_GANETI_DIR, "%s.pid" % name)
1796

    
1797

    
1798
def EnsureDaemon(name):
1799
  """Check for and start daemon if not alive.
1800

1801
  """
1802
  result = RunCmd([constants.DAEMON_UTIL, "check-and-start", name])
1803
  if result.failed:
1804
    logging.error("Can't start daemon '%s', failure %s, output: %s",
1805
                  name, result.fail_reason, result.output)
1806
    return False
1807

    
1808
  return True
1809

    
1810

    
1811
def WritePidFile(name):
1812
  """Write the current process pidfile.
1813

1814
  The file will be written to L{constants.RUN_GANETI_DIR}I{/name.pid}
1815

1816
  @type name: str
1817
  @param name: the daemon name to use
1818
  @raise errors.GenericError: if the pid file already exists and
1819
      points to a live process
1820

1821
  """
1822
  pid = os.getpid()
1823
  pidfilename = DaemonPidFileName(name)
1824
  if IsProcessAlive(ReadPidFile(pidfilename)):
1825
    raise errors.GenericError("%s contains a live process" % pidfilename)
1826

    
1827
  WriteFile(pidfilename, data="%d\n" % pid)
1828

    
1829

    
1830
def RemovePidFile(name):
1831
  """Remove the current process pidfile.
1832

1833
  Any errors are ignored.
1834

1835
  @type name: str
1836
  @param name: the daemon name used to derive the pidfile name
1837

1838
  """
1839
  pidfilename = DaemonPidFileName(name)
1840
  # TODO: we could check here that the file contains our pid
1841
  try:
1842
    RemoveFile(pidfilename)
1843
  except: # pylint: disable-msg=W0702
1844
    pass
1845

    
1846

    
1847
def KillProcess(pid, signal_=signal.SIGTERM, timeout=30,
1848
                waitpid=False):
1849
  """Kill a process given by its pid.
1850

1851
  @type pid: int
1852
  @param pid: The PID to terminate.
1853
  @type signal_: int
1854
  @param signal_: The signal to send, by default SIGTERM
1855
  @type timeout: int
1856
  @param timeout: The timeout after which, if the process is still alive,
1857
                  a SIGKILL will be sent. If not positive, no such checking
1858
                  will be done
1859
  @type waitpid: boolean
1860
  @param waitpid: If true, we should waitpid on this process after
1861
      sending signals, since it's our own child and otherwise it
1862
      would remain as zombie
1863

1864
  """
1865
  def _helper(pid, signal_, wait):
1866
    """Simple helper to encapsulate the kill/waitpid sequence"""
1867
    os.kill(pid, signal_)
1868
    if wait:
1869
      try:
1870
        os.waitpid(pid, os.WNOHANG)
1871
      except OSError:
1872
        pass
1873

    
1874
  if pid <= 0:
1875
    # kill with pid=0 == suicide
1876
    raise errors.ProgrammerError("Invalid pid given '%s'" % pid)
1877

    
1878
  if not IsProcessAlive(pid):
1879
    return
1880

    
1881
  _helper(pid, signal_, waitpid)
1882

    
1883
  if timeout <= 0:
1884
    return
1885

    
1886
  def _CheckProcess():
1887
    if not IsProcessAlive(pid):
1888
      return
1889

    
1890
    try:
1891
      (result_pid, _) = os.waitpid(pid, os.WNOHANG)
1892
    except OSError:
1893
      raise RetryAgain()
1894

    
1895
    if result_pid > 0:
1896
      return
1897

    
1898
    raise RetryAgain()
1899

    
1900
  try:
1901
    # Wait up to $timeout seconds
1902
    Retry(_CheckProcess, (0.01, 1.5, 0.1), timeout)
1903
  except RetryTimeout:
1904
    pass
1905

    
1906
  if IsProcessAlive(pid):
1907
    # Kill process if it's still alive
1908
    _helper(pid, signal.SIGKILL, waitpid)
1909

    
1910

    
1911
def FindFile(name, search_path, test=os.path.exists):
1912
  """Look for a filesystem object in a given path.
1913

1914
  This is an abstract method to search for filesystem object (files,
1915
  dirs) under a given search path.
1916

1917
  @type name: str
1918
  @param name: the name to look for
1919
  @type search_path: str
1920
  @param search_path: location to start at
1921
  @type test: callable
1922
  @param test: a function taking one argument that should return True
1923
      if the a given object is valid; the default value is
1924
      os.path.exists, causing only existing files to be returned
1925
  @rtype: str or None
1926
  @return: full path to the object if found, None otherwise
1927

1928
  """
1929
  # validate the filename mask
1930
  if constants.EXT_PLUGIN_MASK.match(name) is None:
1931
    logging.critical("Invalid value passed for external script name: '%s'",
1932
                     name)
1933
    return None
1934

    
1935
  for dir_name in search_path:
1936
    # FIXME: investigate switch to PathJoin
1937
    item_name = os.path.sep.join([dir_name, name])
1938
    # check the user test and that we're indeed resolving to the given
1939
    # basename
1940
    if test(item_name) and os.path.basename(item_name) == name:
1941
      return item_name
1942
  return None
1943

    
1944

    
1945
def CheckVolumeGroupSize(vglist, vgname, minsize):
1946
  """Checks if the volume group list is valid.
1947

1948
  The function will check if a given volume group is in the list of
1949
  volume groups and has a minimum size.
1950

1951
  @type vglist: dict
1952
  @param vglist: dictionary of volume group names and their size
1953
  @type vgname: str
1954
  @param vgname: the volume group we should check
1955
  @type minsize: int
1956
  @param minsize: the minimum size we accept
1957
  @rtype: None or str
1958
  @return: None for success, otherwise the error message
1959

1960
  """
1961
  vgsize = vglist.get(vgname, None)
1962
  if vgsize is None:
1963
    return "volume group '%s' missing" % vgname
1964
  elif vgsize < minsize:
1965
    return ("volume group '%s' too small (%s MiB required, %d MiB found)" %
1966
            (vgname, minsize, vgsize))
1967
  return None
1968

    
1969

    
1970
def SplitTime(value):
1971
  """Splits time as floating point number into a tuple.
1972

1973
  @param value: Time in seconds
1974
  @type value: int or float
1975
  @return: Tuple containing (seconds, microseconds)
1976

1977
  """
1978
  (seconds, microseconds) = divmod(int(value * 1000000), 1000000)
1979

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

    
1985
  return (int(seconds), int(microseconds))
1986

    
1987

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

1991
  @param timetuple: Time as tuple, (seconds, microseconds)
1992
  @type timetuple: tuple
1993
  @return: Time as a floating point number expressed in seconds
1994

1995
  """
1996
  (seconds, microseconds) = timetuple
1997

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

    
2003
  return float(seconds) + (float(microseconds) * 0.000001)
2004

    
2005

    
2006
def GetDaemonPort(daemon_name):
2007
  """Get the daemon port for this cluster.
2008

2009
  Note that this routine does not read a ganeti-specific file, but
2010
  instead uses C{socket.getservbyname} to allow pre-customization of
2011
  this parameter outside of Ganeti.
2012

2013
  @type daemon_name: string
2014
  @param daemon_name: daemon name (in constants.DAEMONS_PORTS)
2015
  @rtype: int
2016

2017
  """
2018
  if daemon_name not in constants.DAEMONS_PORTS:
2019
    raise errors.ProgrammerError("Unknown daemon: %s" % daemon_name)
2020

    
2021
  (proto, default_port) = constants.DAEMONS_PORTS[daemon_name]
2022
  try:
2023
    port = socket.getservbyname(daemon_name, proto)
2024
  except socket.error:
2025
    port = default_port
2026

    
2027
  return port
2028

    
2029

    
2030
def SetupLogging(logfile, debug=0, stderr_logging=False, program="",
2031
                 multithreaded=False, syslog=constants.SYSLOG_USAGE):
2032
  """Configures the logging module.
2033

2034
  @type logfile: str
2035
  @param logfile: the filename to which we should log
2036
  @type debug: integer
2037
  @param debug: if greater than zero, enable debug messages, otherwise
2038
      only those at C{INFO} and above level
2039
  @type stderr_logging: boolean
2040
  @param stderr_logging: whether we should also log to the standard error
2041
  @type program: str
2042
  @param program: the name under which we should log messages
2043
  @type multithreaded: boolean
2044
  @param multithreaded: if True, will add the thread name to the log file
2045
  @type syslog: string
2046
  @param syslog: one of 'no', 'yes', 'only':
2047
      - if no, syslog is not used
2048
      - if yes, syslog is used (in addition to file-logging)
2049
      - if only, only syslog is used
2050
  @raise EnvironmentError: if we can't open the log file and
2051
      syslog/stderr logging is disabled
2052

2053
  """
2054
  fmt = "%(asctime)s: " + program + " pid=%(process)d"
2055
  sft = program + "[%(process)d]:"
2056
  if multithreaded:
2057
    fmt += "/%(threadName)s"
2058
    sft += " (%(threadName)s)"
2059
  if debug:
2060
    fmt += " %(module)s:%(lineno)s"
2061
    # no debug info for syslog loggers
2062
  fmt += " %(levelname)s %(message)s"
2063
  # yes, we do want the textual level, as remote syslog will probably
2064
  # lose the error level, and it's easier to grep for it
2065
  sft += " %(levelname)s %(message)s"
2066
  formatter = logging.Formatter(fmt)
2067
  sys_fmt = logging.Formatter(sft)
2068

    
2069
  root_logger = logging.getLogger("")
2070
  root_logger.setLevel(logging.NOTSET)
2071

    
2072
  # Remove all previously setup handlers
2073
  for handler in root_logger.handlers:
2074
    handler.close()
2075
    root_logger.removeHandler(handler)
2076

    
2077
  if stderr_logging:
2078
    stderr_handler = logging.StreamHandler()
2079
    stderr_handler.setFormatter(formatter)
2080
    if debug:
2081
      stderr_handler.setLevel(logging.NOTSET)
2082
    else:
2083
      stderr_handler.setLevel(logging.CRITICAL)
2084
    root_logger.addHandler(stderr_handler)
2085

    
2086
  if syslog in (constants.SYSLOG_YES, constants.SYSLOG_ONLY):
2087
    facility = logging.handlers.SysLogHandler.LOG_DAEMON
2088
    syslog_handler = logging.handlers.SysLogHandler(constants.SYSLOG_SOCKET,
2089
                                                    facility)
2090
    syslog_handler.setFormatter(sys_fmt)
2091
    # Never enable debug over syslog
2092
    syslog_handler.setLevel(logging.INFO)
2093
    root_logger.addHandler(syslog_handler)
2094

    
2095
  if syslog != constants.SYSLOG_ONLY:
2096
    # this can fail, if the logging directories are not setup or we have
2097
    # a permisssion problem; in this case, it's best to log but ignore
2098
    # the error if stderr_logging is True, and if false we re-raise the
2099
    # exception since otherwise we could run but without any logs at all
2100
    try:
2101
      logfile_handler = logging.FileHandler(logfile)
2102
      logfile_handler.setFormatter(formatter)
2103
      if debug:
2104
        logfile_handler.setLevel(logging.DEBUG)
2105
      else:
2106
        logfile_handler.setLevel(logging.INFO)
2107
      root_logger.addHandler(logfile_handler)
2108
    except EnvironmentError:
2109
      if stderr_logging or syslog == constants.SYSLOG_YES:
2110
        logging.exception("Failed to enable logging to file '%s'", logfile)
2111
      else:
2112
        # we need to re-raise the exception
2113
        raise
2114

    
2115

    
2116
def IsNormAbsPath(path):
2117
  """Check whether a path is absolute and also normalized
2118

2119
  This avoids things like /dir/../../other/path to be valid.
2120

2121
  """
2122
  return os.path.normpath(path) == path and os.path.isabs(path)
2123

    
2124

    
2125
def PathJoin(*args):
2126
  """Safe-join a list of path components.
2127

2128
  Requirements:
2129
      - the first argument must be an absolute path
2130
      - no component in the path must have backtracking (e.g. /../),
2131
        since we check for normalization at the end
2132

2133
  @param args: the path components to be joined
2134
  @raise ValueError: for invalid paths
2135

2136
  """
2137
  # ensure we're having at least one path passed in
2138
  assert args
2139
  # ensure the first component is an absolute and normalized path name
2140
  root = args[0]
2141
  if not IsNormAbsPath(root):
2142
    raise ValueError("Invalid parameter to PathJoin: '%s'" % str(args[0]))
2143
  result = os.path.join(*args)
2144
  # ensure that the whole path is normalized
2145
  if not IsNormAbsPath(result):
2146
    raise ValueError("Invalid parameters to PathJoin: '%s'" % str(args))
2147
  # check that we're still under the original prefix
2148
  prefix = os.path.commonprefix([root, result])
2149
  if prefix != root:
2150
    raise ValueError("Error: path joining resulted in different prefix"
2151
                     " (%s != %s)" % (prefix, root))
2152
  return result
2153

    
2154

    
2155
def TailFile(fname, lines=20):
2156
  """Return the last lines from a file.
2157

2158
  @note: this function will only read and parse the last 4KB of
2159
      the file; if the lines are very long, it could be that less
2160
      than the requested number of lines are returned
2161

2162
  @param fname: the file name
2163
  @type lines: int
2164
  @param lines: the (maximum) number of lines to return
2165

2166
  """
2167
  fd = open(fname, "r")
2168
  try:
2169
    fd.seek(0, 2)
2170
    pos = fd.tell()
2171
    pos = max(0, pos-4096)
2172
    fd.seek(pos, 0)
2173
    raw_data = fd.read()
2174
  finally:
2175
    fd.close()
2176

    
2177
  rows = raw_data.splitlines()
2178
  return rows[-lines:]
2179

    
2180

    
2181
def _ParseAsn1Generalizedtime(value):
2182
  """Parses an ASN1 GENERALIZEDTIME timestamp as used by pyOpenSSL.
2183

2184
  @type value: string
2185
  @param value: ASN1 GENERALIZEDTIME timestamp
2186

2187
  """
2188
  m = re.match(r"^(\d+)([-+]\d\d)(\d\d)$", value)
2189
  if m:
2190
    # We have an offset
2191
    asn1time = m.group(1)
2192
    hours = int(m.group(2))
2193
    minutes = int(m.group(3))
2194
    utcoffset = (60 * hours) + minutes
2195
  else:
2196
    if not value.endswith("Z"):
2197
      raise ValueError("Missing timezone")
2198
    asn1time = value[:-1]
2199
    utcoffset = 0
2200

    
2201
  parsed = time.strptime(asn1time, "%Y%m%d%H%M%S")
2202

    
2203
  tt = datetime.datetime(*(parsed[:7])) - datetime.timedelta(minutes=utcoffset)
2204

    
2205
  return calendar.timegm(tt.utctimetuple())
2206

    
2207

    
2208
def GetX509CertValidity(cert):
2209
  """Returns the validity period of the certificate.
2210

2211
  @type cert: OpenSSL.crypto.X509
2212
  @param cert: X509 certificate object
2213

2214
  """
2215
  # The get_notBefore and get_notAfter functions are only supported in
2216
  # pyOpenSSL 0.7 and above.
2217
  try:
2218
    get_notbefore_fn = cert.get_notBefore
2219
  except AttributeError:
2220
    not_before = None
2221
  else:
2222
    not_before_asn1 = get_notbefore_fn()
2223

    
2224
    if not_before_asn1 is None:
2225
      not_before = None
2226
    else:
2227
      not_before = _ParseAsn1Generalizedtime(not_before_asn1)
2228

    
2229
  try:
2230
    get_notafter_fn = cert.get_notAfter
2231
  except AttributeError:
2232
    not_after = None
2233
  else:
2234
    not_after_asn1 = get_notafter_fn()
2235

    
2236
    if not_after_asn1 is None:
2237
      not_after = None
2238
    else:
2239
      not_after = _ParseAsn1Generalizedtime(not_after_asn1)
2240

    
2241
  return (not_before, not_after)
2242

    
2243

    
2244
def SafeEncode(text):
2245
  """Return a 'safe' version of a source string.
2246

2247
  This function mangles the input string and returns a version that
2248
  should be safe to display/encode as ASCII. To this end, we first
2249
  convert it to ASCII using the 'backslashreplace' encoding which
2250
  should get rid of any non-ASCII chars, and then we process it
2251
  through a loop copied from the string repr sources in the python; we
2252
  don't use string_escape anymore since that escape single quotes and
2253
  backslashes too, and that is too much; and that escaping is not
2254
  stable, i.e. string_escape(string_escape(x)) != string_escape(x).
2255

2256
  @type text: str or unicode
2257
  @param text: input data
2258
  @rtype: str
2259
  @return: a safe version of text
2260

2261
  """
2262
  if isinstance(text, unicode):
2263
    # only if unicode; if str already, we handle it below
2264
    text = text.encode('ascii', 'backslashreplace')
2265
  resu = ""
2266
  for char in text:
2267
    c = ord(char)
2268
    if char  == '\t':
2269
      resu += r'\t'
2270
    elif char == '\n':
2271
      resu += r'\n'
2272
    elif char == '\r':
2273
      resu += r'\'r'
2274
    elif c < 32 or c >= 127: # non-printable
2275
      resu += "\\x%02x" % (c & 0xff)
2276
    else:
2277
      resu += char
2278
  return resu
2279

    
2280

    
2281
def UnescapeAndSplit(text, sep=","):
2282
  """Split and unescape a string based on a given separator.
2283

2284
  This function splits a string based on a separator where the
2285
  separator itself can be escape in order to be an element of the
2286
  elements. The escaping rules are (assuming coma being the
2287
  separator):
2288
    - a plain , separates the elements
2289
    - a sequence \\\\, (double backslash plus comma) is handled as a
2290
      backslash plus a separator comma
2291
    - a sequence \, (backslash plus comma) is handled as a
2292
      non-separator comma
2293

2294
  @type text: string
2295
  @param text: the string to split
2296
  @type sep: string
2297
  @param text: the separator
2298
  @rtype: string
2299
  @return: a list of strings
2300

2301
  """
2302
  # we split the list by sep (with no escaping at this stage)
2303
  slist = text.split(sep)
2304
  # next, we revisit the elements and if any of them ended with an odd
2305
  # number of backslashes, then we join it with the next
2306
  rlist = []
2307
  while slist:
2308
    e1 = slist.pop(0)
2309
    if e1.endswith("\\"):
2310
      num_b = len(e1) - len(e1.rstrip("\\"))
2311
      if num_b % 2 == 1:
2312
        e2 = slist.pop(0)
2313
        # here the backslashes remain (all), and will be reduced in
2314
        # the next step
2315
        rlist.append(e1 + sep + e2)
2316
        continue
2317
    rlist.append(e1)
2318
  # finally, replace backslash-something with something
2319
  rlist = [re.sub(r"\\(.)", r"\1", v) for v in rlist]
2320
  return rlist
2321

    
2322

    
2323
def CommaJoin(names):
2324
  """Nicely join a set of identifiers.
2325

2326
  @param names: set, list or tuple
2327
  @return: a string with the formatted results
2328

2329
  """
2330
  return ", ".join([str(val) for val in names])
2331

    
2332

    
2333
def BytesToMebibyte(value):
2334
  """Converts bytes to mebibytes.
2335

2336
  @type value: int
2337
  @param value: Value in bytes
2338
  @rtype: int
2339
  @return: Value in mebibytes
2340

2341
  """
2342
  return int(round(value / (1024.0 * 1024.0), 0))
2343

    
2344

    
2345
def CalculateDirectorySize(path):
2346
  """Calculates the size of a directory recursively.
2347

2348
  @type path: string
2349
  @param path: Path to directory
2350
  @rtype: int
2351
  @return: Size in mebibytes
2352

2353
  """
2354
  size = 0
2355

    
2356
  for (curpath, _, files) in os.walk(path):
2357
    for filename in files:
2358
      st = os.lstat(PathJoin(curpath, filename))
2359
      size += st.st_size
2360

    
2361
  return BytesToMebibyte(size)
2362

    
2363

    
2364
def GetFilesystemStats(path):
2365
  """Returns the total and free space on a filesystem.
2366

2367
  @type path: string
2368
  @param path: Path on filesystem to be examined
2369
  @rtype: int
2370
  @return: tuple of (Total space, Free space) in mebibytes
2371

2372
  """
2373
  st = os.statvfs(path)
2374

    
2375
  fsize = BytesToMebibyte(st.f_bavail * st.f_frsize)
2376
  tsize = BytesToMebibyte(st.f_blocks * st.f_frsize)
2377
  return (tsize, fsize)
2378

    
2379

    
2380
def RunInSeparateProcess(fn, *args):
2381
  """Runs a function in a separate process.
2382

2383
  Note: Only boolean return values are supported.
2384

2385
  @type fn: callable
2386
  @param fn: Function to be called
2387
  @rtype: bool
2388
  @return: Function's result
2389

2390
  """
2391
  pid = os.fork()
2392
  if pid == 0:
2393
    # Child process
2394
    try:
2395
      # In case the function uses temporary files
2396
      ResetTempfileModule()
2397

    
2398
      # Call function
2399
      result = int(bool(fn(*args)))
2400
      assert result in (0, 1)
2401
    except: # pylint: disable-msg=W0702
2402
      logging.exception("Error while calling function in separate process")
2403
      # 0 and 1 are reserved for the return value
2404
      result = 33
2405

    
2406
    os._exit(result) # pylint: disable-msg=W0212
2407

    
2408
  # Parent process
2409

    
2410
  # Avoid zombies and check exit code
2411
  (_, status) = os.waitpid(pid, 0)
2412

    
2413
  if os.WIFSIGNALED(status):
2414
    exitcode = None
2415
    signum = os.WTERMSIG(status)
2416
  else:
2417
    exitcode = os.WEXITSTATUS(status)
2418
    signum = None
2419

    
2420
  if not (exitcode in (0, 1) and signum is None):
2421
    raise errors.GenericError("Child program failed (code=%s, signal=%s)" %
2422
                              (exitcode, signum))
2423

    
2424
  return bool(exitcode)
2425

    
2426

    
2427
def LockedMethod(fn):
2428
  """Synchronized object access decorator.
2429

2430
  This decorator is intended to protect access to an object using the
2431
  object's own lock which is hardcoded to '_lock'.
2432

2433
  """
2434
  def _LockDebug(*args, **kwargs):
2435
    if debug_locks:
2436
      logging.debug(*args, **kwargs)
2437

    
2438
  def wrapper(self, *args, **kwargs):
2439
    # pylint: disable-msg=W0212
2440
    assert hasattr(self, '_lock')
2441
    lock = self._lock
2442
    _LockDebug("Waiting for %s", lock)
2443
    lock.acquire()
2444
    try:
2445
      _LockDebug("Acquired %s", lock)
2446
      result = fn(self, *args, **kwargs)
2447
    finally:
2448
      _LockDebug("Releasing %s", lock)
2449
      lock.release()
2450
      _LockDebug("Released %s", lock)
2451
    return result
2452
  return wrapper
2453

    
2454

    
2455
def LockFile(fd):
2456
  """Locks a file using POSIX locks.
2457

2458
  @type fd: int
2459
  @param fd: the file descriptor we need to lock
2460

2461
  """
2462
  try:
2463
    fcntl.flock(fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
2464
  except IOError, err:
2465
    if err.errno == errno.EAGAIN:
2466
      raise errors.LockError("File already locked")
2467
    raise
2468

    
2469

    
2470
def FormatTime(val):
2471
  """Formats a time value.
2472

2473
  @type val: float or None
2474
  @param val: the timestamp as returned by time.time()
2475
  @return: a string value or N/A if we don't have a valid timestamp
2476

2477
  """
2478
  if val is None or not isinstance(val, (int, float)):
2479
    return "N/A"
2480
  # these two codes works on Linux, but they are not guaranteed on all
2481
  # platforms
2482
  return time.strftime("%F %T", time.localtime(val))
2483

    
2484

    
2485
def ReadWatcherPauseFile(filename, now=None, remove_after=3600):
2486
  """Reads the watcher pause file.
2487

2488
  @type filename: string
2489
  @param filename: Path to watcher pause file
2490
  @type now: None, float or int
2491
  @param now: Current time as Unix timestamp
2492
  @type remove_after: int
2493
  @param remove_after: Remove watcher pause file after specified amount of
2494
    seconds past the pause end time
2495

2496
  """
2497
  if now is None:
2498
    now = time.time()
2499

    
2500
  try:
2501
    value = ReadFile(filename)
2502
  except IOError, err:
2503
    if err.errno != errno.ENOENT:
2504
      raise
2505
    value = None
2506

    
2507
  if value is not None:
2508
    try:
2509
      value = int(value)
2510
    except ValueError:
2511
      logging.warning(("Watcher pause file (%s) contains invalid value,"
2512
                       " removing it"), filename)
2513
      RemoveFile(filename)
2514
      value = None
2515

    
2516
    if value is not None:
2517
      # Remove file if it's outdated
2518
      if now > (value + remove_after):
2519
        RemoveFile(filename)
2520
        value = None
2521

    
2522
      elif now > value:
2523
        value = None
2524

    
2525
  return value
2526

    
2527

    
2528
class RetryTimeout(Exception):
2529
  """Retry loop timed out.
2530

2531
  Any arguments which was passed by the retried function to RetryAgain will be
2532
  preserved in RetryTimeout, if it is raised. If such argument was an exception
2533
  the RaiseInner helper method will reraise it.
2534

2535
  """
2536
  def RaiseInner(self):
2537
    if self.args and isinstance(self.args[0], Exception):
2538
      raise self.args[0]
2539
    else:
2540
      raise RetryTimeout(*self.args)
2541

    
2542

    
2543
class RetryAgain(Exception):
2544
  """Retry again.
2545

2546
  Any arguments passed to RetryAgain will be preserved, if a timeout occurs, as
2547
  arguments to RetryTimeout. If an exception is passed, the RaiseInner() method
2548
  of the RetryTimeout() method can be used to reraise it.
2549

2550
  """
2551

    
2552

    
2553
class _RetryDelayCalculator(object):
2554
  """Calculator for increasing delays.
2555

2556
  """
2557
  __slots__ = [
2558
    "_factor",
2559
    "_limit",
2560
    "_next",
2561
    "_start",
2562
    ]
2563

    
2564
  def __init__(self, start, factor, limit):
2565
    """Initializes this class.
2566

2567
    @type start: float
2568
    @param start: Initial delay
2569
    @type factor: float
2570
    @param factor: Factor for delay increase
2571
    @type limit: float or None
2572
    @param limit: Upper limit for delay or None for no limit
2573

2574
    """
2575
    assert start > 0.0
2576
    assert factor >= 1.0
2577
    assert limit is None or limit >= 0.0
2578

    
2579
    self._start = start
2580
    self._factor = factor
2581
    self._limit = limit
2582

    
2583
    self._next = start
2584

    
2585
  def __call__(self):
2586
    """Returns current delay and calculates the next one.
2587

2588
    """
2589
    current = self._next
2590

    
2591
    # Update for next run
2592
    if self._limit is None or self._next < self._limit:
2593
      self._next = min(self._limit, self._next * self._factor)
2594

    
2595
    return current
2596

    
2597

    
2598
#: Special delay to specify whole remaining timeout
2599
RETRY_REMAINING_TIME = object()
2600

    
2601

    
2602
def Retry(fn, delay, timeout, args=None, wait_fn=time.sleep,
2603
          _time_fn=time.time):
2604
  """Call a function repeatedly until it succeeds.
2605

2606
  The function C{fn} is called repeatedly until it doesn't throw L{RetryAgain}
2607
  anymore. Between calls a delay, specified by C{delay}, is inserted. After a
2608
  total of C{timeout} seconds, this function throws L{RetryTimeout}.
2609

2610
  C{delay} can be one of the following:
2611
    - callable returning the delay length as a float
2612
    - Tuple of (start, factor, limit)
2613
    - L{RETRY_REMAINING_TIME} to sleep until the timeout expires (this is
2614
      useful when overriding L{wait_fn} to wait for an external event)
2615
    - A static delay as a number (int or float)
2616

2617
  @type fn: callable
2618
  @param fn: Function to be called
2619
  @param delay: Either a callable (returning the delay), a tuple of (start,
2620
                factor, limit) (see L{_RetryDelayCalculator}),
2621
                L{RETRY_REMAINING_TIME} or a number (int or float)
2622
  @type timeout: float
2623
  @param timeout: Total timeout
2624
  @type wait_fn: callable
2625
  @param wait_fn: Waiting function
2626
  @return: Return value of function
2627

2628
  """
2629
  assert callable(fn)
2630
  assert callable(wait_fn)
2631
  assert callable(_time_fn)
2632

    
2633
  if args is None:
2634
    args = []
2635

    
2636
  end_time = _time_fn() + timeout
2637

    
2638
  if callable(delay):
2639
    # External function to calculate delay
2640
    calc_delay = delay
2641

    
2642
  elif isinstance(delay, (tuple, list)):
2643
    # Increasing delay with optional upper boundary
2644
    (start, factor, limit) = delay
2645
    calc_delay = _RetryDelayCalculator(start, factor, limit)
2646

    
2647
  elif delay is RETRY_REMAINING_TIME:
2648
    # Always use the remaining time
2649
    calc_delay = None
2650

    
2651
  else:
2652
    # Static delay
2653
    calc_delay = lambda: delay
2654

    
2655
  assert calc_delay is None or callable(calc_delay)
2656

    
2657
  while True:
2658
    retry_args = []
2659
    try:
2660
      # pylint: disable-msg=W0142
2661
      return fn(*args)
2662
    except RetryAgain, err:
2663
      retry_args = err.args
2664
    except RetryTimeout:
2665
      raise errors.ProgrammerError("Nested retry loop detected that didn't"
2666
                                   " handle RetryTimeout")
2667

    
2668
    remaining_time = end_time - _time_fn()
2669

    
2670
    if remaining_time < 0.0:
2671
      # pylint: disable-msg=W0142
2672
      raise RetryTimeout(*retry_args)
2673

    
2674
    assert remaining_time >= 0.0
2675

    
2676
    if calc_delay is None:
2677
      wait_fn(remaining_time)
2678
    else:
2679
      current_delay = calc_delay()
2680
      if current_delay > 0.0:
2681
        wait_fn(current_delay)
2682

    
2683

    
2684
class FileLock(object):
2685
  """Utility class for file locks.
2686

2687
  """
2688
  def __init__(self, fd, filename):
2689
    """Constructor for FileLock.
2690

2691
    @type fd: file
2692
    @param fd: File object
2693
    @type filename: str
2694
    @param filename: Path of the file opened at I{fd}
2695

2696
    """
2697
    self.fd = fd
2698
    self.filename = filename
2699

    
2700
  @classmethod
2701
  def Open(cls, filename):
2702
    """Creates and opens a file to be used as a file-based lock.
2703

2704
    @type filename: string
2705
    @param filename: path to the file to be locked
2706

2707
    """
2708
    # Using "os.open" is necessary to allow both opening existing file
2709
    # read/write and creating if not existing. Vanilla "open" will truncate an
2710
    # existing file -or- allow creating if not existing.
2711
    return cls(os.fdopen(os.open(filename, os.O_RDWR | os.O_CREAT), "w+"),
2712
               filename)
2713

    
2714
  def __del__(self):
2715
    self.Close()
2716

    
2717
  def Close(self):
2718
    """Close the file and release the lock.
2719

2720
    """
2721
    if hasattr(self, "fd") and self.fd:
2722
      self.fd.close()
2723
      self.fd = None
2724

    
2725
  def _flock(self, flag, blocking, timeout, errmsg):
2726
    """Wrapper for fcntl.flock.
2727

2728
    @type flag: int
2729
    @param flag: operation flag
2730
    @type blocking: bool
2731
    @param blocking: whether the operation should be done in blocking mode.
2732
    @type timeout: None or float
2733
    @param timeout: for how long the operation should be retried (implies
2734
                    non-blocking mode).
2735
    @type errmsg: string
2736
    @param errmsg: error message in case operation fails.
2737

2738
    """
2739
    assert self.fd, "Lock was closed"
2740
    assert timeout is None or timeout >= 0, \
2741
      "If specified, timeout must be positive"
2742
    assert not (flag & fcntl.LOCK_NB), "LOCK_NB must not be set"
2743

    
2744
    # When a timeout is used, LOCK_NB must always be set
2745
    if not (timeout is None and blocking):
2746
      flag |= fcntl.LOCK_NB
2747

    
2748
    if timeout is None:
2749
      self._Lock(self.fd, flag, timeout)
2750
    else:
2751
      try:
2752
        Retry(self._Lock, (0.1, 1.2, 1.0), timeout,
2753
              args=(self.fd, flag, timeout))
2754
      except RetryTimeout:
2755
        raise errors.LockError(errmsg)
2756

    
2757
  @staticmethod
2758
  def _Lock(fd, flag, timeout):
2759
    try:
2760
      fcntl.flock(fd, flag)
2761
    except IOError, err:
2762
      if timeout is not None and err.errno == errno.EAGAIN:
2763
        raise RetryAgain()
2764

    
2765
      logging.exception("fcntl.flock failed")
2766
      raise
2767

    
2768
  def Exclusive(self, blocking=False, timeout=None):
2769
    """Locks the file in exclusive mode.
2770

2771
    @type blocking: boolean
2772
    @param blocking: whether to block and wait until we
2773
        can lock the file or return immediately
2774
    @type timeout: int or None
2775
    @param timeout: if not None, the duration to wait for the lock
2776
        (in blocking mode)
2777

2778
    """
2779
    self._flock(fcntl.LOCK_EX, blocking, timeout,
2780
                "Failed to lock %s in exclusive mode" % self.filename)
2781

    
2782
  def Shared(self, blocking=False, timeout=None):
2783
    """Locks the file in shared mode.
2784

2785
    @type blocking: boolean
2786
    @param blocking: whether to block and wait until we
2787
        can lock the file or return immediately
2788
    @type timeout: int or None
2789
    @param timeout: if not None, the duration to wait for the lock
2790
        (in blocking mode)
2791

2792
    """
2793
    self._flock(fcntl.LOCK_SH, blocking, timeout,
2794
                "Failed to lock %s in shared mode" % self.filename)
2795

    
2796
  def Unlock(self, blocking=True, timeout=None):
2797
    """Unlocks the file.
2798

2799
    According to C{flock(2)}, unlocking can also be a nonblocking
2800
    operation::
2801

2802
      To make a non-blocking request, include LOCK_NB with any of the above
2803
      operations.
2804

2805
    @type blocking: boolean
2806
    @param blocking: whether to block and wait until we
2807
        can lock the file or return immediately
2808
    @type timeout: int or None
2809
    @param timeout: if not None, the duration to wait for the lock
2810
        (in blocking mode)
2811

2812
    """
2813
    self._flock(fcntl.LOCK_UN, blocking, timeout,
2814
                "Failed to unlock %s" % self.filename)
2815

    
2816

    
2817
class LineSplitter:
2818
  """Splits data chunks into lines separated by newline.
2819

2820
  Instances provide a file-like interface.
2821

2822
  """
2823
  def __init__(self, line_fn, *args):
2824
    """Initializes this class.
2825

2826
    @type line_fn: callable
2827
    @param line_fn: Function called for each line, first parameter is line
2828
    @param args: Extra arguments for L{line_fn}
2829

2830
    """
2831
    assert callable(line_fn)
2832

    
2833
    if args:
2834
      # Python 2.4 doesn't have functools.partial yet
2835
      self._line_fn = \
2836
        lambda line: line_fn(line, *args) # pylint: disable-msg=W0142
2837
    else:
2838
      self._line_fn = line_fn
2839

    
2840
    self._lines = collections.deque()
2841
    self._buffer = ""
2842

    
2843
  def write(self, data):
2844
    parts = (self._buffer + data).split("\n")
2845
    self._buffer = parts.pop()
2846
    self._lines.extend(parts)
2847

    
2848
  def flush(self):
2849
    while self._lines:
2850
      self._line_fn(self._lines.popleft().rstrip("\r\n"))
2851

    
2852
  def close(self):
2853
    self.flush()
2854
    if self._buffer:
2855
      self._line_fn(self._buffer)
2856

    
2857

    
2858
def SignalHandled(signums):
2859
  """Signal Handled decoration.
2860

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

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

2869
  @type signums: list
2870
  @param signums: signals to intercept
2871

2872
  """
2873
  def wrap(fn):
2874
    def sig_function(*args, **kwargs):
2875
      assert 'signal_handlers' not in kwargs or \
2876
             kwargs['signal_handlers'] is None or \
2877
             isinstance(kwargs['signal_handlers'], dict), \
2878
             "Wrong signal_handlers parameter in original function call"
2879
      if 'signal_handlers' in kwargs and kwargs['signal_handlers'] is not None:
2880
        signal_handlers = kwargs['signal_handlers']
2881
      else:
2882
        signal_handlers = {}
2883
        kwargs['signal_handlers'] = signal_handlers
2884
      sighandler = SignalHandler(signums)
2885
      try:
2886
        for sig in signums:
2887
          signal_handlers[sig] = sighandler
2888
        return fn(*args, **kwargs)
2889
      finally:
2890
        sighandler.Reset()
2891
    return sig_function
2892
  return wrap
2893

    
2894

    
2895
class SignalHandler(object):
2896
  """Generic signal handler class.
2897

2898
  It automatically restores the original handler when deconstructed or
2899
  when L{Reset} is called. You can either pass your own handler
2900
  function in or query the L{called} attribute to detect whether the
2901
  signal was sent.
2902

2903
  @type signum: list
2904
  @ivar signum: the signals we handle
2905
  @type called: boolean
2906
  @ivar called: tracks whether any of the signals have been raised
2907

2908
  """
2909
  def __init__(self, signum):
2910
    """Constructs a new SignalHandler instance.
2911

2912
    @type signum: int or list of ints
2913
    @param signum: Single signal number or set of signal numbers
2914

2915
    """
2916
    self.signum = set(signum)
2917
    self.called = False
2918

    
2919
    self._previous = {}
2920
    try:
2921
      for signum in self.signum:
2922
        # Setup handler
2923
        prev_handler = signal.signal(signum, self._HandleSignal)
2924
        try:
2925
          self._previous[signum] = prev_handler
2926
        except:
2927
          # Restore previous handler
2928
          signal.signal(signum, prev_handler)
2929
          raise
2930
    except:
2931
      # Reset all handlers
2932
      self.Reset()
2933
      # Here we have a race condition: a handler may have already been called,
2934
      # but there's not much we can do about it at this point.
2935
      raise
2936

    
2937
  def __del__(self):
2938
    self.Reset()
2939

    
2940
  def Reset(self):
2941
    """Restore previous handler.
2942

2943
    This will reset all the signals to their previous handlers.
2944

2945
    """
2946
    for signum, prev_handler in self._previous.items():
2947
      signal.signal(signum, prev_handler)
2948
      # If successful, remove from dict
2949
      del self._previous[signum]
2950

    
2951
  def Clear(self):
2952
    """Unsets the L{called} flag.
2953

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

2956
    """
2957
    self.called = False
2958

    
2959
  # we don't care about arguments, but we leave them named for the future
2960
  def _HandleSignal(self, signum, frame): # pylint: disable-msg=W0613
2961
    """Actual signal handling function.
2962

2963
    """
2964
    # This is not nice and not absolutely atomic, but it appears to be the only
2965
    # solution in Python -- there are no atomic types.
2966
    self.called = True
2967

    
2968

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

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

2977
  Internally, all fields are held as regular expression objects.
2978

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

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

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

2990
    @type field: str
2991
    @param field: the string to match
2992
    @return: either None or a regular expression match object
2993

2994
    """
2995
    for m in itertools.ifilter(None, (val.match(field) for val in self.items)):
2996
      return m
2997
    return None
2998

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

3002
    @type items: list
3003
    @param items: the list of fields to check
3004
    @rtype: list
3005
    @return: list of non-matching fields
3006

3007
    """
3008
    return [val for val in items if not self.Matches(val)]