Add --await-child option to glance-control.

Fixes bug 817032

Previously an immediate non-zero exit status from service
launch was not reflected in the exit status returned from
glance-control.

Now the parent glance-control process configurably waits for
the child to exit ungracefully and if this occurs, it inherits
the non-zero status code from the child.

Change-Id: Ibbe92a5bf40d095951a572d78ae07026d8a9313d
This commit is contained in:
Eoghan Glynn 2012-02-02 15:37:13 +00:00
parent 16b682d273
commit 593e8c2fa7
4 changed files with 49 additions and 8 deletions

View File

@ -111,6 +111,18 @@ def do_start(server, conf, args):
fp.write('%d\n' % pid)
fp.close()
def await_child(pid):
if conf.await_child:
bail_time = time.time() + conf.await_child
while time.time() < bail_time:
reported_pid, status = os.waitpid(pid, os.WNOHANG)
if reported_pid == pid:
global exitcode
# the exit code is encoded in 2nd least significant byte
exitcode = status >> 8
break
time.sleep(0.05)
def launch(pid_file, conf_file=None):
args = [server]
print 'Starting %s' % server,
@ -136,6 +148,7 @@ def do_start(server, conf, args):
sys.exit(0)
else:
write_pid_file(pid_file, pid)
await_child(pid)
if not conf.pid_file:
pid_file = '/var/run/glance/%s.pid' % server
@ -181,11 +194,19 @@ def do_stop(server, conf, args, graceful=False):
if __name__ == '__main__':
exitcode = 0
conf = config.GlanceConfigOpts(usage=USAGE)
conf.register_cli_opt(cfg.StrOpt('pid-file',
metavar='PATH',
help='File to use as pid file. Default: '
'/var/run/glance/$server.pid'))
conf.register_cli_opt(cfg.IntOpt('await-child',
metavar='DELAY',
default=0,
help='Period to wait for service death '
'in order to report exit code '
'(default is to not wait at all)'))
args = conf()
if len(args) < 2:
@ -235,3 +256,5 @@ if __name__ == '__main__':
for server in servers:
do_stop(server, conf, args, graceful=True)
do_start(server, conf, args)
sys.exit(exitcode)

View File

@ -87,6 +87,7 @@ class Server(object):
self.server_control = './bin/glance-control'
self.exec_env = None
self.deployment_flavor = ''
self.server_control_options = ''
def write_conf(self, **kwargs):
"""
@ -128,7 +129,7 @@ class Server(object):
return self.conf_file_name
def start(self, **kwargs):
def start(self, expected_exitcode=0, **kwargs):
"""
Starts the server.
@ -140,9 +141,13 @@ class Server(object):
self.write_conf(**kwargs)
cmd = ("%(server_control)s %(server_name)s start "
"%(conf_file_name)s --pid-file=%(pid_file)s"
"%(conf_file_name)s --pid-file=%(pid_file)s "
"%(server_control_options)s"
% self.__dict__)
return execute(cmd, no_venv=self.no_venv, exec_env=self.exec_env)
return execute(cmd,
no_venv=self.no_venv,
exec_env=self.exec_env,
expected_exitcode=expected_exitcode)
def stop(self):
"""
@ -439,7 +444,11 @@ class FunctionalTest(unittest.TestCase):
if os.path.exists(f):
os.unlink(f)
def start_server(self, server, expect_launch, **kwargs):
def start_server(self,
server,
expect_launch,
expected_exitcode=0,
**kwargs):
"""
Starts a server on an unused port.
@ -449,13 +458,15 @@ class FunctionalTest(unittest.TestCase):
:param server: the server to launch
:param expect_launch: true iff the server is expected to
successfully start
:param expected_exitcode: expected exitcode from the launcher
"""
self.cleanup()
# Start up the requested server
exitcode, out, err = server.start(**kwargs)
exitcode, out, err = server.start(expected_exitcode=expected_exitcode,
**kwargs)
self.assertEqual(0, exitcode,
self.assertEqual(expected_exitcode, exitcode,
"Failed to spin up the requested server. "
"Got: %s" % err)
self.assertTrue(re.search("Starting glance-[a-z]+ with", out))

View File

@ -1248,8 +1248,10 @@ class TestApi(functional.FunctionalTest):
"""
self.cleanup()
self.api_server.default_store = 'shouldnotexist'
self.api_server.server_control_options += ' --await-child=1'
# ensure that the API server fails to launch
self.start_server(self.api_server,
expect_launch=False,
expected_exitcode=255,
**self.__dict__.copy())

View File

@ -168,7 +168,11 @@ def skip_if_disabled(func):
return wrapped
def execute(cmd, raise_error=True, no_venv=False, exec_env=None):
def execute(cmd,
raise_error=True,
no_venv=False,
exec_env=None,
expected_exitcode=0):
"""
Executes a command in a subprocess. Returns a tuple
of (exitcode, out, err), where out is the string output
@ -183,6 +187,7 @@ def execute(cmd, raise_error=True, no_venv=False, exec_env=None):
variables; values may be callables, which will
be passed the current value of the named
environment variable
:param expected_exitcode: expected exitcode from the launcher
"""
env = os.environ.copy()
@ -219,7 +224,7 @@ def execute(cmd, raise_error=True, no_venv=False, exec_env=None):
result = process.communicate()
(out, err) = result
exitcode = process.returncode
if process.returncode != 0 and raise_error:
if process.returncode != expected_exitcode and raise_error:
msg = "Command %(cmd)s did not succeed. Returned an exit "\
"code of %(exitcode)d."\
"\n\nSTDOUT: %(out)s"\