Revision a4ccecf6

b/Makefile.am
220 220
	lib/utils/log.py \
221 221
	lib/utils/mlock.py \
222 222
	lib/utils/nodesetup.py \
223
	lib/utils/process.py \
223 224
	lib/utils/retry.py \
224 225
	lib/utils/text.py \
225 226
	lib/utils/wrapper.py \
......
495 496
	test/ganeti.utils.io_unittest.py \
496 497
	test/ganeti.utils.mlock_unittest.py \
497 498
	test/ganeti.utils.nodesetup_unittest.py \
499
	test/ganeti.utils.process_unittest.py \
498 500
	test/ganeti.utils.retry_unittest.py \
499 501
	test/ganeti.utils.text_unittest.py \
500 502
	test/ganeti.utils.wrapper_unittest.py \
b/lib/ssh.py
106 106
    @param quiet: whether to enable -q to ssh
107 107

  
108 108
    @rtype: list
109
    @return: the list of options ready to use in L{utils.RunCmd}
109
    @return: the list of options ready to use in L{utils.process.RunCmd}
110 110

  
111 111
    """
112 112
    options = [
......
194 194

  
195 195
    Args: see SshRunner.BuildCmd.
196 196

  
197
    @rtype: L{utils.RunResult}
198
    @return: the result as from L{utils.RunCmd()}
197
    @rtype: L{utils.process.RunResult}
198
    @return: the result as from L{utils.process.RunCmd()}
199 199

  
200 200
    """
201 201
    return utils.RunCmd(self.BuildCmd(*args, **kwargs))
b/lib/utils/__init__.py
63 63
from ganeti.utils.io import * # pylint: disable-msg=W0401
64 64
from ganeti.utils.x509 import * # pylint: disable-msg=W0401
65 65
from ganeti.utils.nodesetup import * # pylint: disable-msg=W0401
66
from ganeti.utils.process import * # pylint: disable-msg=W0401
66 67

  
67 68

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

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

  
73 71
_VALID_SERVICE_NAME_RE = re.compile("^[-_.a-zA-Z0-9]{1,128}$")
......
75 73
UUID_RE = re.compile('^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-'
76 74
                     '[a-f0-9]{4}-[a-f0-9]{12}$')
77 75

  
78
(_TIMEOUT_NONE,
79
 _TIMEOUT_TERM,
80
 _TIMEOUT_KILL) = range(3)
81

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

  
85 79

  
86
def DisableFork():
87
  """Disables the use of fork(2).
88

  
89
  """
90
  global _no_fork # pylint: disable-msg=W0603
91

  
92
  _no_fork = True
93

  
94

  
95
class RunResult(object):
96
  """Holds the result of running external programs.
97

  
98
  @type exit_code: int
99
  @ivar exit_code: the exit code of the program, or None (if the program
100
      didn't exit())
101
  @type signal: int or None
102
  @ivar signal: the signal that caused the program to finish, or None
103
      (if the program wasn't terminated by a signal)
104
  @type stdout: str
105
  @ivar stdout: the standard output of the program
106
  @type stderr: str
107
  @ivar stderr: the standard error of the program
108
  @type failed: boolean
109
  @ivar failed: True in case the program was
110
      terminated by a signal or exited with a non-zero exit code
111
  @ivar fail_reason: a string detailing the termination reason
112

  
113
  """
114
  __slots__ = ["exit_code", "signal", "stdout", "stderr",
115
               "failed", "fail_reason", "cmd"]
116

  
117

  
118
  def __init__(self, exit_code, signal_, stdout, stderr, cmd, timeout_action,
119
               timeout):
120
    self.cmd = cmd
121
    self.exit_code = exit_code
122
    self.signal = signal_
123
    self.stdout = stdout
124
    self.stderr = stderr
125
    self.failed = (signal_ is not None or exit_code != 0)
126

  
127
    fail_msgs = []
128
    if self.signal is not None:
129
      fail_msgs.append("terminated by signal %s" % self.signal)
130
    elif self.exit_code is not None:
131
      fail_msgs.append("exited with exit code %s" % self.exit_code)
132
    else:
133
      fail_msgs.append("unable to determine termination reason")
134

  
135
    if timeout_action == _TIMEOUT_TERM:
136
      fail_msgs.append("terminated after timeout of %.2f seconds" % timeout)
137
    elif timeout_action == _TIMEOUT_KILL:
138
      fail_msgs.append(("force termination after timeout of %.2f seconds"
139
                        " and linger for another %.2f seconds") %
140
                       (timeout, constants.CHILD_LINGER_TIMEOUT))
141

  
142
    if fail_msgs and self.failed:
143
      self.fail_reason = CommaJoin(fail_msgs)
144

  
145
    if self.failed:
146
      logging.debug("Command '%s' failed (%s); output: %s",
147
                    self.cmd, self.fail_reason, self.output)
148

  
149
  def _GetOutput(self):
150
    """Returns the combined stdout and stderr for easier usage.
151

  
152
    """
153
    return self.stdout + self.stderr
154

  
155
  output = property(_GetOutput, None, None, "Return full output")
156

  
157

  
158
def _BuildCmdEnvironment(env, reset):
159
  """Builds the environment for an external program.
160

  
161
  """
162
  if reset:
163
    cmd_env = {}
164
  else:
165
    cmd_env = os.environ.copy()
166
    cmd_env["LC_ALL"] = "C"
167

  
168
  if env is not None:
169
    cmd_env.update(env)
170

  
171
  return cmd_env
172

  
173

  
174
def RunCmd(cmd, env=None, output=None, cwd="/", reset_env=False,
175
           interactive=False, timeout=None):
176
  """Execute a (shell) command.
177

  
178
  The command should not read from its standard input, as it will be
179
  closed.
180

  
181
  @type cmd: string or list
182
  @param cmd: Command to run
183
  @type env: dict
184
  @param env: Additional environment variables
185
  @type output: str
186
  @param output: if desired, the output of the command can be
187
      saved in a file instead of the RunResult instance; this
188
      parameter denotes the file name (if not None)
189
  @type cwd: string
190
  @param cwd: if specified, will be used as the working
191
      directory for the command; the default will be /
192
  @type reset_env: boolean
193
  @param reset_env: whether to reset or keep the default os environment
194
  @type interactive: boolean
195
  @param interactive: weather we pipe stdin, stdout and stderr
196
                      (default behaviour) or run the command interactive
197
  @type timeout: int
198
  @param timeout: If not None, timeout in seconds until child process gets
199
                  killed
200
  @rtype: L{RunResult}
201
  @return: RunResult instance
202
  @raise errors.ProgrammerError: if we call this when forks are disabled
203

  
204
  """
205
  if _no_fork:
206
    raise errors.ProgrammerError("utils.RunCmd() called with fork() disabled")
207

  
208
  if output and interactive:
209
    raise errors.ProgrammerError("Parameters 'output' and 'interactive' can"
210
                                 " not be provided at the same time")
211

  
212
  if isinstance(cmd, basestring):
213
    strcmd = cmd
214
    shell = True
215
  else:
216
    cmd = [str(val) for val in cmd]
217
    strcmd = ShellQuoteArgs(cmd)
218
    shell = False
219

  
220
  if output:
221
    logging.debug("RunCmd %s, output file '%s'", strcmd, output)
222
  else:
223
    logging.debug("RunCmd %s", strcmd)
224

  
225
  cmd_env = _BuildCmdEnvironment(env, reset_env)
226

  
227
  try:
228
    if output is None:
229
      out, err, status, timeout_action = _RunCmdPipe(cmd, cmd_env, shell, cwd,
230
                                                     interactive, timeout)
231
    else:
232
      timeout_action = _TIMEOUT_NONE
233
      status = _RunCmdFile(cmd, cmd_env, shell, output, cwd)
234
      out = err = ""
235
  except OSError, err:
236
    if err.errno == errno.ENOENT:
237
      raise errors.OpExecError("Can't execute '%s': not found (%s)" %
238
                               (strcmd, err))
239
    else:
240
      raise
241

  
242
  if status >= 0:
243
    exitcode = status
244
    signal_ = None
245
  else:
246
    exitcode = None
247
    signal_ = -status
248

  
249
  return RunResult(exitcode, signal_, out, err, strcmd, timeout_action, timeout)
250

  
251

  
252
def SetupDaemonEnv(cwd="/", umask=077):
253
  """Setup a daemon's environment.
254

  
255
  This should be called between the first and second fork, due to
256
  setsid usage.
257

  
258
  @param cwd: the directory to which to chdir
259
  @param umask: the umask to setup
260

  
261
  """
262
  os.chdir(cwd)
263
  os.umask(umask)
264
  os.setsid()
265

  
266

  
267
def SetupDaemonFDs(output_file, output_fd):
268
  """Setups up a daemon's file descriptors.
269

  
270
  @param output_file: if not None, the file to which to redirect
271
      stdout/stderr
272
  @param output_fd: if not None, the file descriptor for stdout/stderr
273

  
274
  """
275
  # check that at most one is defined
276
  assert [output_file, output_fd].count(None) >= 1
277

  
278
  # Open /dev/null (read-only, only for stdin)
279
  devnull_fd = os.open(os.devnull, os.O_RDONLY)
280

  
281
  if output_fd is not None:
282
    pass
283
  elif output_file is not None:
284
    # Open output file
285
    try:
286
      output_fd = os.open(output_file,
287
                          os.O_WRONLY | os.O_CREAT | os.O_APPEND, 0600)
288
    except EnvironmentError, err:
289
      raise Exception("Opening output file failed: %s" % err)
290
  else:
291
    output_fd = os.open(os.devnull, os.O_WRONLY)
292

  
293
  # Redirect standard I/O
294
  os.dup2(devnull_fd, 0)
295
  os.dup2(output_fd, 1)
296
  os.dup2(output_fd, 2)
297

  
298

  
299
def StartDaemon(cmd, env=None, cwd="/", output=None, output_fd=None,
300
                pidfile=None):
301
  """Start a daemon process after forking twice.
302

  
303
  @type cmd: string or list
304
  @param cmd: Command to run
305
  @type env: dict
306
  @param env: Additional environment variables
307
  @type cwd: string
308
  @param cwd: Working directory for the program
309
  @type output: string
310
  @param output: Path to file in which to save the output
311
  @type output_fd: int
312
  @param output_fd: File descriptor for output
313
  @type pidfile: string
314
  @param pidfile: Process ID file
315
  @rtype: int
316
  @return: Daemon process ID
317
  @raise errors.ProgrammerError: if we call this when forks are disabled
318

  
319
  """
320
  if _no_fork:
321
    raise errors.ProgrammerError("utils.StartDaemon() called with fork()"
322
                                 " disabled")
323

  
324
  if output and not (bool(output) ^ (output_fd is not None)):
325
    raise errors.ProgrammerError("Only one of 'output' and 'output_fd' can be"
326
                                 " specified")
327

  
328
  if isinstance(cmd, basestring):
329
    cmd = ["/bin/sh", "-c", cmd]
330

  
331
  strcmd = ShellQuoteArgs(cmd)
332

  
333
  if output:
334
    logging.debug("StartDaemon %s, output file '%s'", strcmd, output)
335
  else:
336
    logging.debug("StartDaemon %s", strcmd)
337

  
338
  cmd_env = _BuildCmdEnvironment(env, False)
339

  
340
  # Create pipe for sending PID back
341
  (pidpipe_read, pidpipe_write) = os.pipe()
342
  try:
343
    try:
344
      # Create pipe for sending error messages
345
      (errpipe_read, errpipe_write) = os.pipe()
346
      try:
347
        try:
348
          # First fork
349
          pid = os.fork()
350
          if pid == 0:
351
            try:
352
              # Child process, won't return
353
              _StartDaemonChild(errpipe_read, errpipe_write,
354
                                pidpipe_read, pidpipe_write,
355
                                cmd, cmd_env, cwd,
356
                                output, output_fd, pidfile)
357
            finally:
358
              # Well, maybe child process failed
359
              os._exit(1) # pylint: disable-msg=W0212
360
        finally:
361
          CloseFdNoError(errpipe_write)
362

  
363
        # Wait for daemon to be started (or an error message to
364
        # arrive) and read up to 100 KB as an error message
365
        errormsg = RetryOnSignal(os.read, errpipe_read, 100 * 1024)
366
      finally:
367
        CloseFdNoError(errpipe_read)
368
    finally:
369
      CloseFdNoError(pidpipe_write)
370

  
371
    # Read up to 128 bytes for PID
372
    pidtext = RetryOnSignal(os.read, pidpipe_read, 128)
373
  finally:
374
    CloseFdNoError(pidpipe_read)
375

  
376
  # Try to avoid zombies by waiting for child process
377
  try:
378
    os.waitpid(pid, 0)
379
  except OSError:
380
    pass
381

  
382
  if errormsg:
383
    raise errors.OpExecError("Error when starting daemon process: %r" %
384
                             errormsg)
385

  
386
  try:
387
    return int(pidtext)
388
  except (ValueError, TypeError), err:
389
    raise errors.OpExecError("Error while trying to parse PID %r: %s" %
390
                             (pidtext, err))
391

  
392

  
393
def _StartDaemonChild(errpipe_read, errpipe_write,
394
                      pidpipe_read, pidpipe_write,
395
                      args, env, cwd,
396
                      output, fd_output, pidfile):
397
  """Child process for starting daemon.
398

  
399
  """
400
  try:
401
    # Close parent's side
402
    CloseFdNoError(errpipe_read)
403
    CloseFdNoError(pidpipe_read)
404

  
405
    # First child process
406
    SetupDaemonEnv()
407

  
408
    # And fork for the second time
409
    pid = os.fork()
410
    if pid != 0:
411
      # Exit first child process
412
      os._exit(0) # pylint: disable-msg=W0212
413

  
414
    # Make sure pipe is closed on execv* (and thereby notifies
415
    # original process)
416
    SetCloseOnExecFlag(errpipe_write, True)
417

  
418
    # List of file descriptors to be left open
419
    noclose_fds = [errpipe_write]
420

  
421
    # Open PID file
422
    if pidfile:
423
      fd_pidfile = WritePidFile(pidfile)
424

  
425
      # Keeping the file open to hold the lock
426
      noclose_fds.append(fd_pidfile)
427

  
428
      SetCloseOnExecFlag(fd_pidfile, False)
429
    else:
430
      fd_pidfile = None
431

  
432
    SetupDaemonFDs(output, fd_output)
433

  
434
    # Send daemon PID to parent
435
    RetryOnSignal(os.write, pidpipe_write, str(os.getpid()))
436

  
437
    # Close all file descriptors except stdio and error message pipe
438
    CloseFDs(noclose_fds=noclose_fds)
439

  
440
    # Change working directory
441
    os.chdir(cwd)
442

  
443
    if env is None:
444
      os.execvp(args[0], args)
445
    else:
446
      os.execvpe(args[0], args, env)
447
  except: # pylint: disable-msg=W0702
448
    try:
449
      # Report errors to original process
450
      WriteErrorToFD(errpipe_write, str(sys.exc_info()[1]))
451
    except: # pylint: disable-msg=W0702
452
      # Ignore errors in error handling
453
      pass
454

  
455
  os._exit(1) # pylint: disable-msg=W0212
456

  
457

  
458
def WriteErrorToFD(fd, err):
459
  """Possibly write an error message to a fd.
460

  
461
  @type fd: None or int (file descriptor)
462
  @param fd: if not None, the error will be written to this fd
463
  @param err: string, the error message
464

  
465
  """
466
  if fd is None:
467
    return
468

  
469
  if not err:
470
    err = "<unknown error>"
471

  
472
  RetryOnSignal(os.write, fd, err)
473

  
474

  
475
def _CheckIfAlive(child):
476
  """Raises L{RetryAgain} if child is still alive.
477

  
478
  @raises RetryAgain: If child is still alive
479

  
480
  """
481
  if child.poll() is None:
482
    raise RetryAgain()
483

  
484

  
485
def _WaitForProcess(child, timeout):
486
  """Waits for the child to terminate or until we reach timeout.
487

  
488
  """
489
  try:
490
    Retry(_CheckIfAlive, (1.0, 1.2, 5.0), max(0, timeout), args=[child])
491
  except RetryTimeout:
492
    pass
493

  
494

  
495
def _RunCmdPipe(cmd, env, via_shell, cwd, interactive, timeout,
496
                _linger_timeout=constants.CHILD_LINGER_TIMEOUT):
497
  """Run a command and return its output.
498

  
499
  @type  cmd: string or list
500
  @param cmd: Command to run
501
  @type env: dict
502
  @param env: The environment to use
503
  @type via_shell: bool
504
  @param via_shell: if we should run via the shell
505
  @type cwd: string
506
  @param cwd: the working directory for the program
507
  @type interactive: boolean
508
  @param interactive: Run command interactive (without piping)
509
  @type timeout: int
510
  @param timeout: Timeout after the programm gets terminated
511
  @rtype: tuple
512
  @return: (out, err, status)
513

  
514
  """
515
  poller = select.poll()
516

  
517
  stderr = subprocess.PIPE
518
  stdout = subprocess.PIPE
519
  stdin = subprocess.PIPE
520

  
521
  if interactive:
522
    stderr = stdout = stdin = None
523

  
524
  child = subprocess.Popen(cmd, shell=via_shell,
525
                           stderr=stderr,
526
                           stdout=stdout,
527
                           stdin=stdin,
528
                           close_fds=True, env=env,
529
                           cwd=cwd)
530

  
531
  out = StringIO()
532
  err = StringIO()
533

  
534
  linger_timeout = None
535

  
536
  if timeout is None:
537
    poll_timeout = None
538
  else:
539
    poll_timeout = RunningTimeout(timeout, True).Remaining
540

  
541
  msg_timeout = ("Command %s (%d) run into execution timeout, terminating" %
542
                 (cmd, child.pid))
543
  msg_linger = ("Command %s (%d) run into linger timeout, killing" %
544
                (cmd, child.pid))
545

  
546
  timeout_action = _TIMEOUT_NONE
547

  
548
  if not interactive:
549
    child.stdin.close()
550
    poller.register(child.stdout, select.POLLIN)
551
    poller.register(child.stderr, select.POLLIN)
552
    fdmap = {
553
      child.stdout.fileno(): (out, child.stdout),
554
      child.stderr.fileno(): (err, child.stderr),
555
      }
556
    for fd in fdmap:
557
      SetNonblockFlag(fd, True)
558

  
559
    while fdmap:
560
      if poll_timeout:
561
        pt = poll_timeout() * 1000
562
        if pt < 0:
563
          if linger_timeout is None:
564
            logging.warning(msg_timeout)
565
            if child.poll() is None:
566
              timeout_action = _TIMEOUT_TERM
567
              IgnoreProcessNotFound(os.kill, child.pid, signal.SIGTERM)
568
            linger_timeout = RunningTimeout(_linger_timeout, True).Remaining
569
          pt = linger_timeout() * 1000
570
          if pt < 0:
571
            break
572
      else:
573
        pt = None
574

  
575
      pollresult = RetryOnSignal(poller.poll, pt)
576

  
577
      for fd, event in pollresult:
578
        if event & select.POLLIN or event & select.POLLPRI:
579
          data = fdmap[fd][1].read()
580
          # no data from read signifies EOF (the same as POLLHUP)
581
          if not data:
582
            poller.unregister(fd)
583
            del fdmap[fd]
584
            continue
585
          fdmap[fd][0].write(data)
586
        if (event & select.POLLNVAL or event & select.POLLHUP or
587
            event & select.POLLERR):
588
          poller.unregister(fd)
589
          del fdmap[fd]
590

  
591
  if timeout is not None:
592
    assert callable(poll_timeout)
593

  
594
    # We have no I/O left but it might still run
595
    if child.poll() is None:
596
      _WaitForProcess(child, poll_timeout())
597

  
598
    # Terminate if still alive after timeout
599
    if child.poll() is None:
600
      if linger_timeout is None:
601
        logging.warning(msg_timeout)
602
        timeout_action = _TIMEOUT_TERM
603
        IgnoreProcessNotFound(os.kill, child.pid, signal.SIGTERM)
604
        lt = _linger_timeout
605
      else:
606
        lt = linger_timeout()
607
      _WaitForProcess(child, lt)
608

  
609
    # Okay, still alive after timeout and linger timeout? Kill it!
610
    if child.poll() is None:
611
      timeout_action = _TIMEOUT_KILL
612
      logging.warning(msg_linger)
613
      IgnoreProcessNotFound(os.kill, child.pid, signal.SIGKILL)
614

  
615
  out = out.getvalue()
616
  err = err.getvalue()
617

  
618
  status = child.wait()
619
  return out, err, status, timeout_action
620

  
621

  
622
def _RunCmdFile(cmd, env, via_shell, output, cwd):
623
  """Run a command and save its output to a file.
624

  
625
  @type  cmd: string or list
626
  @param cmd: Command to run
627
  @type env: dict
628
  @param env: The environment to use
629
  @type via_shell: bool
630
  @param via_shell: if we should run via the shell
631
  @type output: str
632
  @param output: the filename in which to save the output
633
  @type cwd: string
634
  @param cwd: the working directory for the program
635
  @rtype: int
636
  @return: the exit status
637

  
638
  """
639
  fh = open(output, "a")
640
  try:
641
    child = subprocess.Popen(cmd, shell=via_shell,
642
                             stderr=subprocess.STDOUT,
643
                             stdout=fh,
644
                             stdin=subprocess.PIPE,
645
                             close_fds=True, env=env,
646
                             cwd=cwd)
647

  
648
    child.stdin.close()
649
    status = child.wait()
650
  finally:
651
    fh.close()
652
  return status
653

  
654

  
655
def RunParts(dir_name, env=None, reset_env=False):
656
  """Run Scripts or programs in a directory
657

  
658
  @type dir_name: string
659
  @param dir_name: absolute path to a directory
660
  @type env: dict
661
  @param env: The environment to use
662
  @type reset_env: boolean
663
  @param reset_env: whether to reset or keep the default os environment
664
  @rtype: list of tuples
665
  @return: list of (name, (one of RUNDIR_STATUS), RunResult)
666

  
667
  """
668
  rr = []
669

  
670
  try:
671
    dir_contents = ListVisibleFiles(dir_name)
672
  except OSError, err:
673
    logging.warning("RunParts: skipping %s (cannot list: %s)", dir_name, err)
674
    return rr
675

  
676
  for relname in sorted(dir_contents):
677
    fname = PathJoin(dir_name, relname)
678
    if not (os.path.isfile(fname) and os.access(fname, os.X_OK) and
679
            constants.EXT_PLUGIN_MASK.match(relname) is not None):
680
      rr.append((relname, constants.RUNPARTS_SKIP, None))
681
    else:
682
      try:
683
        result = RunCmd([fname], env=env, reset_env=reset_env)
684
      except Exception, err: # pylint: disable-msg=W0703
685
        rr.append((relname, constants.RUNPARTS_ERR, str(err)))
686
      else:
687
        rr.append((relname, constants.RUNPARTS_RUN, result))
688

  
689
  return rr
690

  
691

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

  
......
758 146
        raise errors.TypeEnforcementError(msg)
759 147

  
760 148

  
761
def _GetProcStatusPath(pid):
762
  """Returns the path for a PID's proc status file.
763

  
764
  @type pid: int
765
  @param pid: Process ID
766
  @rtype: string
767

  
768
  """
769
  return "/proc/%d/status" % pid
770

  
771

  
772
def IsProcessAlive(pid):
773
  """Check if a given pid exists on the system.
774

  
775
  @note: zombie status is not handled, so zombie processes
776
      will be returned as alive
777
  @type pid: int
778
  @param pid: the process ID to check
779
  @rtype: boolean
780
  @return: True if the process exists
781

  
782
  """
783
  def _TryStat(name):
784
    try:
785
      os.stat(name)
786
      return True
787
    except EnvironmentError, err:
788
      if err.errno in (errno.ENOENT, errno.ENOTDIR):
789
        return False
790
      elif err.errno == errno.EINVAL:
791
        raise RetryAgain(err)
792
      raise
793

  
794
  assert isinstance(pid, int), "pid must be an integer"
795
  if pid <= 0:
796
    return False
797

  
798
  # /proc in a multiprocessor environment can have strange behaviors.
799
  # Retry the os.stat a few times until we get a good result.
800
  try:
801
    return Retry(_TryStat, (0.01, 1.5, 0.1), 0.5,
802
                 args=[_GetProcStatusPath(pid)])
803
  except RetryTimeout, err:
804
    err.RaiseInner()
805

  
806

  
807
def _ParseSigsetT(sigset):
808
  """Parse a rendered sigset_t value.
809

  
810
  This is the opposite of the Linux kernel's fs/proc/array.c:render_sigset_t
811
  function.
812

  
813
  @type sigset: string
814
  @param sigset: Rendered signal set from /proc/$pid/status
815
  @rtype: set
816
  @return: Set of all enabled signal numbers
817

  
818
  """
819
  result = set()
820

  
821
  signum = 0
822
  for ch in reversed(sigset):
823
    chv = int(ch, 16)
824

  
825
    # The following could be done in a loop, but it's easier to read and
826
    # understand in the unrolled form
827
    if chv & 1:
828
      result.add(signum + 1)
829
    if chv & 2:
830
      result.add(signum + 2)
831
    if chv & 4:
832
      result.add(signum + 3)
833
    if chv & 8:
834
      result.add(signum + 4)
835

  
836
    signum += 4
837

  
838
  return result
839

  
840

  
841
def _GetProcStatusField(pstatus, field):
842
  """Retrieves a field from the contents of a proc status file.
843

  
844
  @type pstatus: string
845
  @param pstatus: Contents of /proc/$pid/status
846
  @type field: string
847
  @param field: Name of field whose value should be returned
848
  @rtype: string
849

  
850
  """
851
  for line in pstatus.splitlines():
852
    parts = line.split(":", 1)
853

  
854
    if len(parts) < 2 or parts[0] != field:
855
      continue
856

  
857
    return parts[1].strip()
858

  
859
  return None
860

  
861

  
862
def IsProcessHandlingSignal(pid, signum, status_path=None):
863
  """Checks whether a process is handling a signal.
864

  
865
  @type pid: int
866
  @param pid: Process ID
867
  @type signum: int
868
  @param signum: Signal number
869
  @rtype: bool
870

  
871
  """
872
  if status_path is None:
873
    status_path = _GetProcStatusPath(pid)
874

  
875
  try:
876
    proc_status = ReadFile(status_path)
877
  except EnvironmentError, err:
878
    # In at least one case, reading /proc/$pid/status failed with ESRCH.
879
    if err.errno in (errno.ENOENT, errno.ENOTDIR, errno.EINVAL, errno.ESRCH):
880
      return False
881
    raise
882

  
883
  sigcgt = _GetProcStatusField(proc_status, "SigCgt")
884
  if sigcgt is None:
885
    raise RuntimeError("%s is missing 'SigCgt' field" % status_path)
886

  
887
  # Now check whether signal is handled
888
  return signum in _ParseSigsetT(sigcgt)
889

  
890

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

  
......
1205 463
  return result
1206 464

  
1207 465

  
1208
def CloseFDs(noclose_fds=None):
1209
  """Close file descriptors.
1210

  
1211
  This closes all file descriptors above 2 (i.e. except
1212
  stdin/out/err).
1213

  
1214
  @type noclose_fds: list or None
1215
  @param noclose_fds: if given, it denotes a list of file descriptor
1216
      that should not be closed
1217

  
1218
  """
1219
  # Default maximum for the number of available file descriptors.
1220
  if 'SC_OPEN_MAX' in os.sysconf_names:
1221
    try:
1222
      MAXFD = os.sysconf('SC_OPEN_MAX')
1223
      if MAXFD < 0:
1224
        MAXFD = 1024
1225
    except OSError:
1226
      MAXFD = 1024
1227
  else:
1228
    MAXFD = 1024
1229
  maxfd = resource.getrlimit(resource.RLIMIT_NOFILE)[1]
1230
  if (maxfd == resource.RLIM_INFINITY):
1231
    maxfd = MAXFD
1232

  
1233
  # Iterate through and close all file descriptors (except the standard ones)
1234
  for fd in range(3, maxfd):
1235
    if noclose_fds and fd in noclose_fds:
1236
      continue
1237
    CloseFdNoError(fd)
1238

  
1239

  
1240
def Daemonize(logfile):
1241
  """Daemonize the current process.
1242

  
1243
  This detaches the current process from the controlling terminal and
1244
  runs it in the background as a daemon.
1245

  
1246
  @type logfile: str
1247
  @param logfile: the logfile to which we should redirect stdout/stderr
1248
  @rtype: int
1249
  @return: the value zero
1250

  
1251
  """
1252
  # pylint: disable-msg=W0212
1253
  # yes, we really want os._exit
1254

  
1255
  # TODO: do another attempt to merge Daemonize and StartDaemon, or at
1256
  # least abstract the pipe functionality between them
1257

  
1258
  # Create pipe for sending error messages
1259
  (rpipe, wpipe) = os.pipe()
1260

  
1261
  # this might fail
1262
  pid = os.fork()
1263
  if (pid == 0):  # The first child.
1264
    SetupDaemonEnv()
1265

  
1266
    # this might fail
1267
    pid = os.fork() # Fork a second child.
1268
    if (pid == 0):  # The second child.
1269
      CloseFdNoError(rpipe)
1270
    else:
1271
      # exit() or _exit()?  See below.
1272
      os._exit(0) # Exit parent (the first child) of the second child.
1273
  else:
1274
    CloseFdNoError(wpipe)
1275
    # Wait for daemon to be started (or an error message to
1276
    # arrive) and read up to 100 KB as an error message
1277
    errormsg = RetryOnSignal(os.read, rpipe, 100 * 1024)
1278
    if errormsg:
1279
      sys.stderr.write("Error when starting daemon process: %r\n" % errormsg)
1280
      rcode = 1
1281
    else:
1282
      rcode = 0
1283
    os._exit(rcode) # Exit parent of the first child.
1284

  
1285
  SetupDaemonFDs(logfile, None)
1286
  return wpipe
1287

  
1288

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

  
......
1312 489
  return True
1313 490

  
1314 491

  
1315
def KillProcess(pid, signal_=signal.SIGTERM, timeout=30,
1316
                waitpid=False):
1317
  """Kill a process given by its pid.
1318

  
1319
  @type pid: int
1320
  @param pid: The PID to terminate.
1321
  @type signal_: int
1322
  @param signal_: The signal to send, by default SIGTERM
1323
  @type timeout: int
1324
  @param timeout: The timeout after which, if the process is still alive,
1325
                  a SIGKILL will be sent. If not positive, no such checking
1326
                  will be done
1327
  @type waitpid: boolean
1328
  @param waitpid: If true, we should waitpid on this process after
1329
      sending signals, since it's our own child and otherwise it
1330
      would remain as zombie
1331

  
1332
  """
1333
  def _helper(pid, signal_, wait):
1334
    """Simple helper to encapsulate the kill/waitpid sequence"""
1335
    if IgnoreProcessNotFound(os.kill, pid, signal_) and wait:
1336
      try:
1337
        os.waitpid(pid, os.WNOHANG)
1338
      except OSError:
1339
        pass
1340

  
1341
  if pid <= 0:
1342
    # kill with pid=0 == suicide
1343
    raise errors.ProgrammerError("Invalid pid given '%s'" % pid)
1344

  
1345
  if not IsProcessAlive(pid):
1346
    return
1347

  
1348
  _helper(pid, signal_, waitpid)
1349

  
1350
  if timeout <= 0:
1351
    return
1352

  
1353
  def _CheckProcess():
1354
    if not IsProcessAlive(pid):
1355
      return
1356

  
1357
    try:
1358
      (result_pid, _) = os.waitpid(pid, os.WNOHANG)
1359
    except OSError:
1360
      raise RetryAgain()
1361

  
1362
    if result_pid > 0:
1363
      return
1364

  
1365
    raise RetryAgain()
1366

  
1367
  try:
1368
    # Wait up to $timeout seconds
1369
    Retry(_CheckProcess, (0.01, 1.5, 0.1), timeout)
1370
  except RetryTimeout:
1371
    pass
1372

  
1373
  if IsProcessAlive(pid):
1374
    # Kill process if it's still alive
1375
    _helper(pid, signal.SIGKILL, waitpid)
1376

  
1377

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

  
......
1483 597
  return data
1484 598

  
1485 599

  
1486
def RunInSeparateProcess(fn, *args):
1487
  """Runs a function in a separate process.
1488

  
1489
  Note: Only boolean return values are supported.
1490

  
1491
  @type fn: callable
1492
  @param fn: Function to be called
1493
  @rtype: bool
1494
  @return: Function's result
1495

  
1496
  """
1497
  pid = os.fork()
1498
  if pid == 0:
1499
    # Child process
1500
    try:
1501
      # In case the function uses temporary files
1502
      ResetTempfileModule()
1503

  
1504
      # Call function
1505
      result = int(bool(fn(*args)))
1506
      assert result in (0, 1)
1507
    except: # pylint: disable-msg=W0702
1508
      logging.exception("Error while calling function in separate process")
1509
      # 0 and 1 are reserved for the return value
1510
      result = 33
1511

  
1512
    os._exit(result) # pylint: disable-msg=W0212
1513

  
1514
  # Parent process
1515

  
1516
  # Avoid zombies and check exit code
1517
  (_, status) = os.waitpid(pid, 0)
1518

  
1519
  if os.WIFSIGNALED(status):
1520
    exitcode = None
1521
    signum = os.WTERMSIG(status)
1522
  else:
1523
    exitcode = os.WEXITSTATUS(status)
1524
    signum = None
1525

  
1526
  if not (exitcode in (0, 1) and signum is None):
1527
    raise errors.GenericError("Child program failed (code=%s, signal=%s)" %
1528
                              (exitcode, signum))
1529

  
1530
  return bool(exitcode)
1531

  
1532

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

  
b/lib/utils/io.py
569 569
def ReadLockedPidFile(path):
570 570
  """Reads a locked PID file.
571 571

  
572
  This can be used together with L{utils.StartDaemon}.
572
  This can be used together with L{utils.process.StartDaemon}.
573 573

  
574 574
  @type path: string
575 575
  @param path: Path to PID file
b/lib/utils/process.py
1
#
2
#
3

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

  
21
"""Utility functions for processes.
22

  
23
"""
24

  
25

  
26
import os
27
import sys
28
import subprocess
29
import errno
30
import select
31
import logging
32
import signal
33
import resource
34

  
35
from cStringIO import StringIO
36

  
37
from ganeti import errors
38
from ganeti import constants
39

  
40
from ganeti.utils import retry as utils_retry
41
from ganeti.utils import wrapper as utils_wrapper
42
from ganeti.utils import text as utils_text
43
from ganeti.utils import io as utils_io
44
from ganeti.utils import algo as utils_algo
45

  
46

  
47
#: when set to True, L{RunCmd} is disabled
48
_no_fork = False
49

  
50
(_TIMEOUT_NONE,
51
 _TIMEOUT_TERM,
52
 _TIMEOUT_KILL) = range(3)
53

  
54

  
55
def DisableFork():
56
  """Disables the use of fork(2).
57

  
58
  """
59
  global _no_fork # pylint: disable-msg=W0603
60

  
61
  _no_fork = True
62

  
63

  
64
class RunResult(object):
65
  """Holds the result of running external programs.
66

  
67
  @type exit_code: int
68
  @ivar exit_code: the exit code of the program, or None (if the program
69
      didn't exit())
70
  @type signal: int or None
71
  @ivar signal: the signal that caused the program to finish, or None
72
      (if the program wasn't terminated by a signal)
73
  @type stdout: str
74
  @ivar stdout: the standard output of the program
75
  @type stderr: str
76
  @ivar stderr: the standard error of the program
77
  @type failed: boolean
78
  @ivar failed: True in case the program was
79
      terminated by a signal or exited with a non-zero exit code
80
  @ivar fail_reason: a string detailing the termination reason
81

  
82
  """
83
  __slots__ = ["exit_code", "signal", "stdout", "stderr",
84
               "failed", "fail_reason", "cmd"]
85

  
86

  
87
  def __init__(self, exit_code, signal_, stdout, stderr, cmd, timeout_action,
88
               timeout):
89
    self.cmd = cmd
90
    self.exit_code = exit_code
91
    self.signal = signal_
92
    self.stdout = stdout
93
    self.stderr = stderr
94
    self.failed = (signal_ is not None or exit_code != 0)
95

  
96
    fail_msgs = []
97
    if self.signal is not None:
98
      fail_msgs.append("terminated by signal %s" % self.signal)
99
    elif self.exit_code is not None:
100
      fail_msgs.append("exited with exit code %s" % self.exit_code)
101
    else:
102
      fail_msgs.append("unable to determine termination reason")
103

  
104
    if timeout_action == _TIMEOUT_TERM:
105
      fail_msgs.append("terminated after timeout of %.2f seconds" % timeout)
106
    elif timeout_action == _TIMEOUT_KILL:
107
      fail_msgs.append(("force termination after timeout of %.2f seconds"
108
                        " and linger for another %.2f seconds") %
109
                       (timeout, constants.CHILD_LINGER_TIMEOUT))
110

  
111
    if fail_msgs and self.failed:
112
      self.fail_reason = utils_text.CommaJoin(fail_msgs)
113

  
114
    if self.failed:
115
      logging.debug("Command '%s' failed (%s); output: %s",
116
                    self.cmd, self.fail_reason, self.output)
117

  
118
  def _GetOutput(self):
119
    """Returns the combined stdout and stderr for easier usage.
120

  
121
    """
122
    return self.stdout + self.stderr
123

  
124
  output = property(_GetOutput, None, None, "Return full output")
125

  
126

  
127
def _BuildCmdEnvironment(env, reset):
128
  """Builds the environment for an external program.
129

  
130
  """
131
  if reset:
132
    cmd_env = {}
133
  else:
134
    cmd_env = os.environ.copy()
135
    cmd_env["LC_ALL"] = "C"
136

  
137
  if env is not None:
138
    cmd_env.update(env)
139

  
140
  return cmd_env
141

  
142

  
143
def RunCmd(cmd, env=None, output=None, cwd="/", reset_env=False,
144
           interactive=False, timeout=None):
145
  """Execute a (shell) command.
146

  
147
  The command should not read from its standard input, as it will be
148
  closed.
149

  
150
  @type cmd: string or list
151
  @param cmd: Command to run
152
  @type env: dict
153
  @param env: Additional environment variables
154
  @type output: str
155
  @param output: if desired, the output of the command can be
156
      saved in a file instead of the RunResult instance; this
157
      parameter denotes the file name (if not None)
158
  @type cwd: string
159
  @param cwd: if specified, will be used as the working
160
      directory for the command; the default will be /
161
  @type reset_env: boolean
162
  @param reset_env: whether to reset or keep the default os environment
163
  @type interactive: boolean
164
  @param interactive: weather we pipe stdin, stdout and stderr
165
                      (default behaviour) or run the command interactive
166
  @type timeout: int
167
  @param timeout: If not None, timeout in seconds until child process gets
168
                  killed
169
  @rtype: L{RunResult}
170
  @return: RunResult instance
171
  @raise errors.ProgrammerError: if we call this when forks are disabled
172

  
173
  """
174
  if _no_fork:
175
    raise errors.ProgrammerError("utils.RunCmd() called with fork() disabled")
176

  
177
  if output and interactive:
178
    raise errors.ProgrammerError("Parameters 'output' and 'interactive' can"
179
                                 " not be provided at the same time")
180

  
181
  if isinstance(cmd, basestring):
182
    strcmd = cmd
183
    shell = True
184
  else:
185
    cmd = [str(val) for val in cmd]
186
    strcmd = utils_text.ShellQuoteArgs(cmd)
187
    shell = False
188

  
189
  if output:
190
    logging.debug("RunCmd %s, output file '%s'", strcmd, output)
191
  else:
192
    logging.debug("RunCmd %s", strcmd)
193

  
194
  cmd_env = _BuildCmdEnvironment(env, reset_env)
195

  
196
  try:
197
    if output is None:
198
      out, err, status, timeout_action = _RunCmdPipe(cmd, cmd_env, shell, cwd,
199
                                                     interactive, timeout)
200
    else:
201
      timeout_action = _TIMEOUT_NONE
202
      status = _RunCmdFile(cmd, cmd_env, shell, output, cwd)
203
      out = err = ""
204
  except OSError, err:
205
    if err.errno == errno.ENOENT:
206
      raise errors.OpExecError("Can't execute '%s': not found (%s)" %
207
                               (strcmd, err))
208
    else:
209
      raise
210

  
211
  if status >= 0:
212
    exitcode = status
213
    signal_ = None
214
  else:
215
    exitcode = None
216
    signal_ = -status
217

  
218
  return RunResult(exitcode, signal_, out, err, strcmd, timeout_action, timeout)
219

  
220

  
221
def SetupDaemonEnv(cwd="/", umask=077):
222
  """Setup a daemon's environment.
223

  
224
  This should be called between the first and second fork, due to
225
  setsid usage.
226

  
227
  @param cwd: the directory to which to chdir
228
  @param umask: the umask to setup
229

  
230
  """
231
  os.chdir(cwd)
232
  os.umask(umask)
233
  os.setsid()
234

  
235

  
236
def SetupDaemonFDs(output_file, output_fd):
237
  """Setups up a daemon's file descriptors.
238

  
239
  @param output_file: if not None, the file to which to redirect
240
      stdout/stderr
241
  @param output_fd: if not None, the file descriptor for stdout/stderr
242

  
243
  """
244
  # check that at most one is defined
245
  assert [output_file, output_fd].count(None) >= 1
246

  
247
  # Open /dev/null (read-only, only for stdin)
248
  devnull_fd = os.open(os.devnull, os.O_RDONLY)
249

  
250
  if output_fd is not None:
251
    pass
252
  elif output_file is not None:
253
    # Open output file
254
    try:
255
      output_fd = os.open(output_file,
256
                          os.O_WRONLY | os.O_CREAT | os.O_APPEND, 0600)
257
    except EnvironmentError, err:
258
      raise Exception("Opening output file failed: %s" % err)
259
  else:
260
    output_fd = os.open(os.devnull, os.O_WRONLY)
261

  
262
  # Redirect standard I/O
263
  os.dup2(devnull_fd, 0)
264
  os.dup2(output_fd, 1)
265
  os.dup2(output_fd, 2)
266

  
267

  
268
def StartDaemon(cmd, env=None, cwd="/", output=None, output_fd=None,
269
                pidfile=None):
270
  """Start a daemon process after forking twice.
271

  
272
  @type cmd: string or list
273
  @param cmd: Command to run
274
  @type env: dict
275
  @param env: Additional environment variables
276
  @type cwd: string
277
  @param cwd: Working directory for the program
278
  @type output: string
279
  @param output: Path to file in which to save the output
280
  @type output_fd: int
281
  @param output_fd: File descriptor for output
282
  @type pidfile: string
283
  @param pidfile: Process ID file
284
  @rtype: int
285
  @return: Daemon process ID
286
  @raise errors.ProgrammerError: if we call this when forks are disabled
287

  
288
  """
289
  if _no_fork:
290
    raise errors.ProgrammerError("utils.StartDaemon() called with fork()"
291
                                 " disabled")
292

  
293
  if output and not (bool(output) ^ (output_fd is not None)):
294
    raise errors.ProgrammerError("Only one of 'output' and 'output_fd' can be"
295
                                 " specified")
296

  
297
  if isinstance(cmd, basestring):
298
    cmd = ["/bin/sh", "-c", cmd]
299

  
300
  strcmd = utils_text.ShellQuoteArgs(cmd)
301

  
302
  if output:
303
    logging.debug("StartDaemon %s, output file '%s'", strcmd, output)
304
  else:
305
    logging.debug("StartDaemon %s", strcmd)
306

  
307
  cmd_env = _BuildCmdEnvironment(env, False)
308

  
309
  # Create pipe for sending PID back
310
  (pidpipe_read, pidpipe_write) = os.pipe()
311
  try:
312
    try:
313
      # Create pipe for sending error messages
314
      (errpipe_read, errpipe_write) = os.pipe()
315
      try:
316
        try:
317
          # First fork
318
          pid = os.fork()
319
          if pid == 0:
320
            try:
321
              # Child process, won't return
322
              _StartDaemonChild(errpipe_read, errpipe_write,
323
                                pidpipe_read, pidpipe_write,
324
                                cmd, cmd_env, cwd,
325
                                output, output_fd, pidfile)
326
            finally:
327
              # Well, maybe child process failed
328
              os._exit(1) # pylint: disable-msg=W0212
329
        finally:
330
          utils_wrapper.CloseFdNoError(errpipe_write)
331

  
332
        # Wait for daemon to be started (or an error message to
333
        # arrive) and read up to 100 KB as an error message
334
        errormsg = utils_wrapper.RetryOnSignal(os.read, errpipe_read,
335
                                               100 * 1024)
336
      finally:
337
        utils_wrapper.CloseFdNoError(errpipe_read)
338
    finally:
339
      utils_wrapper.CloseFdNoError(pidpipe_write)
340

  
341
    # Read up to 128 bytes for PID
342
    pidtext = utils_wrapper.RetryOnSignal(os.read, pidpipe_read, 128)
343
  finally:
344
    utils_wrapper.CloseFdNoError(pidpipe_read)
345

  
346
  # Try to avoid zombies by waiting for child process
347
  try:
348
    os.waitpid(pid, 0)
349
  except OSError:
350
    pass
351

  
352
  if errormsg:
353
    raise errors.OpExecError("Error when starting daemon process: %r" %
354
                             errormsg)
355

  
356
  try:
357
    return int(pidtext)
358
  except (ValueError, TypeError), err:
359
    raise errors.OpExecError("Error while trying to parse PID %r: %s" %
360
                             (pidtext, err))
361

  
362

  
363
def _StartDaemonChild(errpipe_read, errpipe_write,
364
                      pidpipe_read, pidpipe_write,
365
                      args, env, cwd,
366
                      output, fd_output, pidfile):
367
  """Child process for starting daemon.
368

  
369
  """
370
  try:
371
    # Close parent's side
372
    utils_wrapper.CloseFdNoError(errpipe_read)
373
    utils_wrapper.CloseFdNoError(pidpipe_read)
374

  
375
    # First child process
376
    SetupDaemonEnv()
377

  
378
    # And fork for the second time
379
    pid = os.fork()
380
    if pid != 0:
381
      # Exit first child process
382
      os._exit(0) # pylint: disable-msg=W0212
383

  
384
    # Make sure pipe is closed on execv* (and thereby notifies
385
    # original process)
386
    utils_wrapper.SetCloseOnExecFlag(errpipe_write, True)
387

  
388
    # List of file descriptors to be left open
389
    noclose_fds = [errpipe_write]
390

  
391
    # Open PID file
392
    if pidfile:
393
      fd_pidfile = utils_io.WritePidFile(pidfile)
394

  
395
      # Keeping the file open to hold the lock
396
      noclose_fds.append(fd_pidfile)
397

  
398
      utils_wrapper.SetCloseOnExecFlag(fd_pidfile, False)
399
    else:
400
      fd_pidfile = None
401

  
402
    SetupDaemonFDs(output, fd_output)
403

  
404
    # Send daemon PID to parent
405
    utils_wrapper.RetryOnSignal(os.write, pidpipe_write, str(os.getpid()))
406

  
407
    # Close all file descriptors except stdio and error message pipe
408
    CloseFDs(noclose_fds=noclose_fds)
409

  
410
    # Change working directory
411
    os.chdir(cwd)
412

  
413
    if env is None:
414
      os.execvp(args[0], args)
415
    else:
416
      os.execvpe(args[0], args, env)
417
  except: # pylint: disable-msg=W0702
418
    try:
419
      # Report errors to original process
420
      WriteErrorToFD(errpipe_write, str(sys.exc_info()[1]))
421
    except: # pylint: disable-msg=W0702
422
      # Ignore errors in error handling
423
      pass
424

  
425
  os._exit(1) # pylint: disable-msg=W0212
426

  
427

  
428
def WriteErrorToFD(fd, err):
429
  """Possibly write an error message to a fd.
430

  
431
  @type fd: None or int (file descriptor)
432
  @param fd: if not None, the error will be written to this fd
433
  @param err: string, the error message
434

  
435
  """
436
  if fd is None:
437
    return
438

  
439
  if not err:
440
    err = "<unknown error>"
441

  
442
  utils_wrapper.RetryOnSignal(os.write, fd, err)
... This diff was truncated because it exceeds the maximum size that can be displayed.

Also available in: Unified diff