Statistics
| Branch: | Tag: | Revision:

root / daemons / import-export @ 1e915b86

History | View | Annotate | Download (21.7 kB)

1
#!/usr/bin/python
2
#
3

    
4
# Copyright (C) 2010 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
"""Import/export daemon.
23

    
24
"""
25

    
26
# pylint: disable-msg=C0103
27
# C0103: Invalid name import-export
28

    
29
import errno
30
import logging
31
import optparse
32
import os
33
import re
34
import select
35
import signal
36
import socket
37
import subprocess
38
import sys
39
import time
40
from cStringIO import StringIO
41

    
42
from ganeti import constants
43
from ganeti import cli
44
from ganeti import utils
45
from ganeti import serializer
46
from ganeti import objects
47
from ganeti import locking
48

    
49

    
50
#: Used to recognize point at which socat(1) starts to listen on its socket.
51
#: The local address is required for the remote peer to connect (in particular
52
#: the port number).
53
LISTENING_RE = re.compile(r"^listening on\s+"
54
                          r"AF=(?P<family>\d+)\s+"
55
                          r"(?P<address>.+):(?P<port>\d+)$", re.I)
56

    
57
#: Used to recognize point at which socat(1) is sending data over the wire
58
TRANSFER_LOOP_RE = re.compile(r"^starting data transfer loop with FDs\s+.*$",
59
                              re.I)
60

    
61
SOCAT_LOG_DEBUG = "D"
62
SOCAT_LOG_INFO = "I"
63
SOCAT_LOG_NOTICE = "N"
64
SOCAT_LOG_WARNING = "W"
65
SOCAT_LOG_ERROR = "E"
66
SOCAT_LOG_FATAL = "F"
67

    
68
SOCAT_LOG_IGNORE = frozenset([
69
  SOCAT_LOG_DEBUG,
70
  SOCAT_LOG_INFO,
71
  SOCAT_LOG_NOTICE,
72
  ])
73

    
74
#: Socat buffer size: at most this many bytes are transferred per step
75
SOCAT_BUFSIZE = 1024 * 1024
76

    
77
#: How many lines to keep in the status file
78
MAX_RECENT_OUTPUT_LINES = 20
79

    
80
#: Don't update status file more than once every 5 seconds (unless forced)
81
MIN_UPDATE_INTERVAL = 5.0
82

    
83
#: Give child process up to 5 seconds to exit after sending a signal
84
CHILD_LINGER_TIMEOUT = 5.0
85

    
86
#: How long to wait for a connection to be established
87
DEFAULT_CONNECT_TIMEOUT = 60
88

    
89
# Common options for socat
90
SOCAT_TCP_OPTS = ["keepalive", "keepidle=60", "keepintvl=10", "keepcnt=5"]
91
SOCAT_OPENSSL_OPTS = ["verify=1", "cipher=HIGH", "method=TLSv1"]
92

    
93

    
94
# Global variable for options
95
options = None
96

    
97

    
98
class Error(Exception):
99
  """Generic exception"""
100

    
101

    
102
def SetupLogging():
103
  """Configures the logging module.
104

    
105
  """
106
  formatter = logging.Formatter("%(asctime)s: %(message)s")
107

    
108
  stderr_handler = logging.StreamHandler()
109
  stderr_handler.setFormatter(formatter)
110
  stderr_handler.setLevel(logging.NOTSET)
111

    
112
  root_logger = logging.getLogger("")
113
  root_logger.addHandler(stderr_handler)
114

    
115
  if options.debug:
116
    root_logger.setLevel(logging.NOTSET)
117
  elif options.verbose:
118
    root_logger.setLevel(logging.INFO)
119
  else:
120
    root_logger.setLevel(logging.ERROR)
121

    
122
  # Create special logger for child process output
123
  child_logger = logging.Logger("child output")
124
  child_logger.addHandler(stderr_handler)
125
  child_logger.setLevel(logging.NOTSET)
126

    
127
  return child_logger
128

    
129

    
130
def _VerifyListening(family, address, port):
131
  """Verify address given as listening address by socat.
132

    
133
  """
134
  # TODO: Implement IPv6 support
135
  if family != socket.AF_INET:
136
    raise Error("Address family %r not supported" % family)
137

    
138
  try:
139
    packed_address = socket.inet_pton(family, address)
140
  except socket.error:
141
    raise Error("Invalid address %r for family %s" % (address, family))
142

    
143
  return (socket.inet_ntop(family, packed_address), port)
144

    
145

    
146
class StatusFile:
147
  """Status file manager.
148

    
149
  """
150
  def __init__(self, path):
151
    """Initializes class.
152

    
153
    """
154
    self._path = path
155
    self._data = objects.ImportExportStatus(ctime=time.time(),
156
                                            mtime=None,
157
                                            recent_output=[])
158

    
159
  def AddRecentOutput(self, line):
160
    """Adds a new line of recent output.
161

    
162
    """
163
    self._data.recent_output.append(line)
164

    
165
    # Remove old lines
166
    del self._data.recent_output[:-MAX_RECENT_OUTPUT_LINES]
167

    
168
  def SetListenPort(self, port):
169
    """Sets the port the daemon is listening on.
170

    
171
    @type port: int
172
    @param port: TCP/UDP port
173

    
174
    """
175
    assert isinstance(port, (int, long)) and 0 < port < 2**16
176
    self._data.listen_port = port
177

    
178
  def GetListenPort(self):
179
    """Returns the port the daemon is listening on.
180

    
181
    """
182
    return self._data.listen_port
183

    
184
  def SetConnected(self):
185
    """Sets the connected flag.
186

    
187
    """
188
    self._data.connected = True
189

    
190
  def GetConnected(self):
191
    """Determines whether the daemon is connected.
192

    
193
    """
194
    return self._data.connected
195

    
196
  def SetExitStatus(self, exit_status, error_message):
197
    """Sets the exit status and an error message.
198

    
199
    """
200
    # Require error message when status isn't 0
201
    assert exit_status == 0 or error_message
202

    
203
    self._data.exit_status = exit_status
204
    self._data.error_message = error_message
205

    
206
  def ExitStatusIsSuccess(self):
207
    """Returns whether the exit status means "success".
208

    
209
    """
210
    return not bool(self._data.error_message)
211

    
212
  def Update(self, force):
213
    """Updates the status file.
214

    
215
    @type force: bool
216
    @param force: Write status file in any case, not only when minimum interval
217
                  is expired
218

    
219
    """
220
    if not (force or
221
            self._data.mtime is None or
222
            time.time() > (self._data.mtime + MIN_UPDATE_INTERVAL)):
223
      return
224

    
225
    logging.debug("Updating status file %s", self._path)
226

    
227
    self._data.mtime = time.time()
228
    utils.WriteFile(self._path,
229
                    data=serializer.DumpJson(self._data.ToDict(), indent=True),
230
                    mode=0400)
231

    
232

    
233
def _ProcessSocatOutput(status_file, level, msg):
234
  """Interprets socat log output.
235

    
236
  """
237
  if level == SOCAT_LOG_NOTICE:
238
    if status_file.GetListenPort() is None:
239
      # TODO: Maybe implement timeout to not listen forever
240
      m = LISTENING_RE.match(msg)
241
      if m:
242
        (_, port) = _VerifyListening(int(m.group("family")), m.group("address"),
243
                                     int(m.group("port")))
244

    
245
        status_file.SetListenPort(port)
246
        return True
247

    
248
    if not status_file.GetConnected():
249
      m = TRANSFER_LOOP_RE.match(msg)
250
      if m:
251
        status_file.SetConnected()
252
        return True
253

    
254
  return False
255

    
256

    
257
def ProcessOutput(line, status_file, logger, socat):
258
  """Takes care of child process output.
259

    
260
  @param status_file: Status file manager
261
  @param logger: Child output logger
262
  @type socat: bool
263
  @param socat: Whether it's a socat output line
264
  @type line: string
265
  @param line: Child output line
266

    
267
  """
268
  force_update = False
269
  forward_line = line
270

    
271
  if socat:
272
    level = None
273
    parts = line.split(None, 4)
274

    
275
    if len(parts) == 5:
276
      (_, _, _, level, msg) = parts
277

    
278
      force_update = _ProcessSocatOutput(status_file, level, msg)
279

    
280
      if options.debug or (level and level not in SOCAT_LOG_IGNORE):
281
        forward_line = "socat: %s %s" % (level, msg)
282
      else:
283
        forward_line = None
284
    else:
285
      forward_line = "socat: %s" % line
286

    
287
  if forward_line:
288
    logger.info(forward_line)
289
    status_file.AddRecentOutput(forward_line)
290

    
291
  status_file.Update(force_update)
292

    
293

    
294
class CommandBuilder(object):
295
  def __init__(self, mode, opts, socat_stderr_fd):
296
    """Initializes this class.
297

    
298
    @param mode: Daemon mode (import or export)
299
    @param opts: Options object
300
    @type socat_stderr_fd: int
301
    @param socat_stderr_fd: File descriptor socat should write its stderr to
302

    
303
    """
304
    self._opts = opts
305
    self._mode = mode
306
    self._socat_stderr_fd = socat_stderr_fd
307

    
308
  @staticmethod
309
  def GetBashCommand(cmd):
310
    """Prepares a command to be run in Bash.
311

    
312
    """
313
    return ["bash", "-o", "errexit", "-o", "pipefail", "-c", cmd]
314

    
315
  def _GetSocatCommand(self):
316
    """Returns the socat command.
317

    
318
    """
319
    common_addr_opts = SOCAT_TCP_OPTS + SOCAT_OPENSSL_OPTS + [
320
      "key=%s" % self._opts.key,
321
      "cert=%s" % self._opts.cert,
322
      "cafile=%s" % self._opts.ca,
323
      ]
324

    
325
    if self._opts.bind is not None:
326
      common_addr_opts.append("bind=%s" % self._opts.bind)
327

    
328
    if self._mode == constants.IEM_IMPORT:
329
      if self._opts.port is None:
330
        port = 0
331
      else:
332
        port = self._opts.port
333

    
334
      addr1 = [
335
        "OPENSSL-LISTEN:%s" % port,
336
        "reuseaddr",
337

    
338
        # Retry to listen if connection wasn't established successfully, up to
339
        # 100 times a second. Note that this still leaves room for DoS attacks.
340
        "forever",
341
        "intervall=0.01",
342
        ] + common_addr_opts
343
      addr2 = ["stdout"]
344

    
345
    elif self._mode == constants.IEM_EXPORT:
346
      addr1 = ["stdin"]
347
      addr2 = [
348
        "OPENSSL:%s:%s" % (self._opts.host, self._opts.port),
349

    
350
        # How long to wait per connection attempt
351
        "connect-timeout=%s" % self._opts.connect_timeout,
352

    
353
        # Retry a few times before giving up to connect (once per second)
354
        "retry=%s" % self._opts.connect_retries,
355
        "intervall=1",
356
        ] + common_addr_opts
357

    
358
    else:
359
      raise Error("Invalid mode '%s'" % self._mode)
360

    
361
    for i in [addr1, addr2]:
362
      for value in i:
363
        if "," in value:
364
          raise Error("Comma not allowed in socat option value: %r" % value)
365

    
366
    return [
367
      constants.SOCAT_PATH,
368

    
369
      # Log to stderr
370
      "-ls",
371

    
372
      # Log level
373
      "-d", "-d",
374

    
375
      # Buffer size
376
      "-b%s" % SOCAT_BUFSIZE,
377

    
378
      # Unidirectional mode, the first address is only used for reading, and the
379
      # second address is only used for writing
380
      "-u",
381

    
382
      ",".join(addr1), ",".join(addr2)
383
      ]
384

    
385
  def _GetTransportCommand(self):
386
    """Returns the command for the transport part of the daemon.
387

    
388
    """
389
    socat_cmd = ("%s 2>&%d" %
390
                 (utils.ShellQuoteArgs(self._GetSocatCommand()),
391
                  self._socat_stderr_fd))
392

    
393
    compr = self._opts.compress
394

    
395
    assert compr in constants.IEC_ALL
396

    
397
    if self._mode == constants.IEM_IMPORT:
398
      if compr == constants.IEC_GZIP:
399
        transport_cmd = "%s | gunzip -c" % socat_cmd
400
      else:
401
        transport_cmd = socat_cmd
402
    elif self._mode == constants.IEM_EXPORT:
403
      if compr == constants.IEC_GZIP:
404
        transport_cmd = "gzip -c | %s" % socat_cmd
405
      else:
406
        transport_cmd = socat_cmd
407
    else:
408
      raise Error("Invalid mode '%s'" % self._mode)
409

    
410
    # TODO: Use "dd" to measure processed data (allows to give an ETA)
411

    
412
    # TODO: Run transport as separate user
413
    # The transport uses its own shell to simplify running it as a separate user
414
    # in the future.
415
    return self.GetBashCommand(transport_cmd)
416

    
417
  def GetCommand(self):
418
    """Returns the complete child process command.
419

    
420
    """
421
    transport_cmd = self._GetTransportCommand()
422

    
423
    buf = StringIO()
424

    
425
    if self._opts.cmd_prefix:
426
      buf.write(self._opts.cmd_prefix)
427
      buf.write(" ")
428

    
429
    buf.write(utils.ShellQuoteArgs(transport_cmd))
430

    
431
    if self._opts.cmd_suffix:
432
      buf.write(" ")
433
      buf.write(self._opts.cmd_suffix)
434

    
435
    return self.GetBashCommand(buf.getvalue())
436

    
437

    
438
def ProcessChildIO(child, socat_stderr_read_fd, status_file, child_logger,
439
                   signal_notify, signal_handler, mode):
440
  """Handles the child processes' output.
441

    
442
  """
443
  assert not (signal_handler.signum - set([signal.SIGTERM, signal.SIGINT])), \
444
         "Other signals are not handled in this function"
445

    
446
  # Buffer size 0 is important, otherwise .read() with a specified length
447
  # might buffer data while poll(2) won't mark its file descriptor as
448
  # readable again.
449
  socat_stderr_read = os.fdopen(socat_stderr_read_fd, "r", 0)
450

    
451
  script_stderr_lines = utils.LineSplitter(ProcessOutput, status_file,
452
                                           child_logger, False)
453
  try:
454
    socat_stderr_lines = utils.LineSplitter(ProcessOutput, status_file,
455
                                            child_logger, True)
456
    try:
457
      fdmap = {
458
        child.stderr.fileno(): (child.stderr, script_stderr_lines),
459
        socat_stderr_read.fileno(): (socat_stderr_read, socat_stderr_lines),
460
        signal_notify.fileno(): (signal_notify, None),
461
        }
462

    
463
      poller = select.poll()
464
      for fd in fdmap:
465
        utils.SetNonblockFlag(fd, True)
466
        poller.register(fd, select.POLLIN)
467

    
468
      if options.connect_timeout and mode == constants.IEM_IMPORT:
469
        listen_timeout = locking.RunningTimeout(options.connect_timeout, True)
470
      else:
471
        listen_timeout = None
472

    
473
      exit_timeout = None
474

    
475
      while True:
476
        # Break out of loop if only signal notify FD is left
477
        if len(fdmap) == 1 and signal_notify.fileno() in fdmap:
478
          break
479

    
480
        timeout = None
481

    
482
        if listen_timeout and not exit_timeout:
483
          if status_file.GetConnected():
484
            listen_timeout = None
485
          elif listen_timeout.Remaining() < 0:
486
            logging.info("Child process didn't establish connection in time")
487
            child.Kill(signal.SIGTERM)
488
            exit_timeout = \
489
              locking.RunningTimeout(CHILD_LINGER_TIMEOUT, True)
490
            # Next block will calculate timeout
491
          else:
492
            # Not yet connected, check again in a second
493
            timeout = 1000
494

    
495
        if exit_timeout:
496
          timeout = exit_timeout.Remaining() * 1000
497
          if timeout < 0:
498
            logging.info("Child process didn't exit in time")
499
            break
500

    
501
        for fd, event in utils.RetryOnSignal(poller.poll, timeout):
502
          if event & (select.POLLIN | event & select.POLLPRI):
503
            (from_, to) = fdmap[fd]
504

    
505
            # Read up to 1 KB of data
506
            data = from_.read(1024)
507
            if data:
508
              if to:
509
                to.write(data)
510
              elif fd == signal_notify.fileno():
511
                # Signal handling
512
                if signal_handler.called:
513
                  signal_handler.Clear()
514
                  if exit_timeout:
515
                    logging.info("Child process still has about %0.2f seconds"
516
                                 " to exit", exit_timeout.Remaining())
517
                  else:
518
                    logging.info("Giving child process %0.2f seconds to exit",
519
                                 CHILD_LINGER_TIMEOUT)
520
                    exit_timeout = \
521
                      locking.RunningTimeout(CHILD_LINGER_TIMEOUT, True)
522
            else:
523
              poller.unregister(fd)
524
              del fdmap[fd]
525

    
526
          elif event & (select.POLLNVAL | select.POLLHUP |
527
                        select.POLLERR):
528
            poller.unregister(fd)
529
            del fdmap[fd]
530

    
531
        script_stderr_lines.flush()
532
        socat_stderr_lines.flush()
533

    
534
      # If there was a timeout calculator, we were waiting for the child to
535
      # finish, e.g. due to a signal
536
      return not bool(exit_timeout)
537
    finally:
538
      socat_stderr_lines.close()
539
  finally:
540
    script_stderr_lines.close()
541

    
542

    
543
def ParseOptions():
544
  """Parses the options passed to the program.
545

    
546
  @return: Arguments to program
547

    
548
  """
549
  global options # pylint: disable-msg=W0603
550

    
551
  parser = optparse.OptionParser(usage=("%%prog <status-file> {%s|%s}" %
552
                                        (constants.IEM_IMPORT,
553
                                         constants.IEM_EXPORT)))
554
  parser.add_option(cli.DEBUG_OPT)
555
  parser.add_option(cli.VERBOSE_OPT)
556
  parser.add_option("--key", dest="key", action="store", type="string",
557
                    help="RSA key file")
558
  parser.add_option("--cert", dest="cert", action="store", type="string",
559
                    help="X509 certificate file")
560
  parser.add_option("--ca", dest="ca", action="store", type="string",
561
                    help="X509 CA file")
562
  parser.add_option("--bind", dest="bind", action="store", type="string",
563
                    help="Bind address")
564
  parser.add_option("--host", dest="host", action="store", type="string",
565
                    help="Remote hostname")
566
  parser.add_option("--port", dest="port", action="store", type="int",
567
                    help="Remote port")
568
  parser.add_option("--connect-retries", dest="connect_retries", action="store",
569
                    type="int", default=0,
570
                    help=("How many times the connection should be retried"
571
                          " (export only)"))
572
  parser.add_option("--connect-timeout", dest="connect_timeout", action="store",
573
                    type="int", default=DEFAULT_CONNECT_TIMEOUT,
574
                    help="Timeout for connection to be established (seconds)")
575
  parser.add_option("--compress", dest="compress", action="store",
576
                    type="choice", help="Compression method",
577
                    metavar="[%s]" % "|".join(constants.IEC_ALL),
578
                    choices=list(constants.IEC_ALL), default=constants.IEC_GZIP)
579
  parser.add_option("--cmd-prefix", dest="cmd_prefix", action="store",
580
                    type="string", help="Command prefix")
581
  parser.add_option("--cmd-suffix", dest="cmd_suffix", action="store",
582
                    type="string", help="Command suffix")
583

    
584
  (options, args) = parser.parse_args()
585

    
586
  if len(args) != 2:
587
    # Won't return
588
    parser.error("Expected exactly two arguments")
589

    
590
  (status_file_path, mode) = args
591

    
592
  if mode not in (constants.IEM_IMPORT,
593
                  constants.IEM_EXPORT):
594
    # Won't return
595
    parser.error("Invalid mode: %s" % mode)
596

    
597
  return (status_file_path, mode)
598

    
599

    
600
class ChildProcess(subprocess.Popen):
601
  def __init__(self, cmd, noclose_fds):
602
    """Initializes this class.
603

    
604
    """
605
    self._noclose_fds = noclose_fds
606

    
607
    # Not using close_fds because doing so would also close the socat stderr
608
    # pipe, which we still need.
609
    subprocess.Popen.__init__(self, cmd, shell=False, close_fds=False,
610
                              stderr=subprocess.PIPE, stdout=None, stdin=None,
611
                              preexec_fn=self._ChildPreexec)
612
    self._SetProcessGroup()
613

    
614
  def _ChildPreexec(self):
615
    """Called before child executable is execve'd.
616

    
617
    """
618
    # Move to separate process group. By sending a signal to its process group
619
    # we can kill the child process and all grandchildren.
620
    os.setpgid(0, 0)
621

    
622
    # Close almost all file descriptors
623
    utils.CloseFDs(noclose_fds=self._noclose_fds)
624

    
625
  def _SetProcessGroup(self):
626
    """Sets the child's process group.
627

    
628
    """
629
    assert self.pid, "Can't be called in child process"
630

    
631
    # Avoid race condition by setting child's process group (as good as
632
    # possible in Python) before sending signals to child. For an
633
    # explanation, see preexec function for child.
634
    try:
635
      os.setpgid(self.pid, self.pid)
636
    except EnvironmentError, err:
637
      # If the child process was faster we receive EPERM or EACCES
638
      if err.errno not in (errno.EPERM, errno.EACCES):
639
        raise
640

    
641
  def Kill(self, signum):
642
    """Sends signal to child process.
643

    
644
    """
645
    logging.info("Sending signal %s to child process", signum)
646
    os.killpg(self.pid, signum)
647

    
648
  def ForceQuit(self):
649
    """Ensure child process is no longer running.
650

    
651
    """
652
    # Final check if child process is still alive
653
    if utils.RetryOnSignal(self.poll) is None:
654
      logging.error("Child process still alive, sending SIGKILL")
655
      self.Kill(signal.SIGKILL)
656
      utils.RetryOnSignal(self.wait)
657

    
658

    
659
def main():
660
  """Main function.
661

    
662
  """
663
  # Option parsing
664
  (status_file_path, mode) = ParseOptions()
665

    
666
  # Configure logging
667
  child_logger = SetupLogging()
668

    
669
  status_file = StatusFile(status_file_path)
670
  try:
671
    try:
672
      # Pipe to receive socat's stderr output
673
      (socat_stderr_read_fd, socat_stderr_write_fd) = os.pipe()
674

    
675
      # Get child process command
676
      cmd = CommandBuilder(mode, options, socat_stderr_write_fd).GetCommand()
677

    
678
      logging.debug("Starting command %r", cmd)
679

    
680
      # Start child process
681
      child = ChildProcess(cmd, [socat_stderr_write_fd])
682
      try:
683
        def _ForwardSignal(signum, _):
684
          """Forwards signals to child process.
685

    
686
          """
687
          child.Kill(signum)
688

    
689
        signal_wakeup = utils.SignalWakeupFd()
690
        try:
691
          # TODO: There is a race condition between starting the child and
692
          # handling the signals here. While there might be a way to work around
693
          # it by registering the handlers before starting the child and
694
          # deferring sent signals until the child is available, doing so can be
695
          # complicated.
696
          signal_handler = utils.SignalHandler([signal.SIGTERM, signal.SIGINT],
697
                                               handler_fn=_ForwardSignal,
698
                                               wakeup=signal_wakeup)
699
          try:
700
            # Close child's side
701
            utils.RetryOnSignal(os.close, socat_stderr_write_fd)
702

    
703
            if ProcessChildIO(child, socat_stderr_read_fd, status_file,
704
                              child_logger, signal_wakeup, signal_handler,
705
                              mode):
706
              # The child closed all its file descriptors and there was no
707
              # signal
708
              # TODO: Implement timeout instead of waiting indefinitely
709
              utils.RetryOnSignal(child.wait)
710
          finally:
711
            signal_handler.Reset()
712
        finally:
713
          signal_wakeup.Reset()
714
      finally:
715
        child.ForceQuit()
716

    
717
      if child.returncode == 0:
718
        errmsg = None
719
      elif child.returncode < 0:
720
        errmsg = "Exited due to signal %s" % (-child.returncode, )
721
      else:
722
        errmsg = "Exited with status %s" % (child.returncode, )
723

    
724
      status_file.SetExitStatus(child.returncode, errmsg)
725
    except Exception, err: # pylint: disable-msg=W0703
726
      logging.exception("Unhandled error occurred")
727
      status_file.SetExitStatus(constants.EXIT_FAILURE,
728
                                "Unhandled error occurred: %s" % (err, ))
729

    
730
    if status_file.ExitStatusIsSuccess():
731
      sys.exit(constants.EXIT_SUCCESS)
732

    
733
    sys.exit(constants.EXIT_FAILURE)
734
  finally:
735
    status_file.Update(True)
736

    
737

    
738
if __name__ == "__main__":
739
  main()