1
0
mirror of https://github.com/phusion/baseimage-docker.git synced 2026-03-26 04:18:46 +00:00

refactor: improve pep8 compliance

This changeset fixes PEP8 issues, minus `E501 line too long (80 > 79 characters)`, as that should be more of a guideline than a strict rule, and harder to follow without silly refactoring.

Also removed two unused exception variables.
This commit is contained in:
Jose Diaz-Gonzalez
2017-05-08 17:07:48 -06:00
committed by GitHub
parent 8f7fcfec33
commit f7dfb05850

View File

@@ -1,12 +1,21 @@
#!/usr/bin/python3 -u #!/usr/bin/python3 -u
import os, os.path, sys, stat, signal, errno, argparse, time, json, re import argparse
import errno
import json
import os
import os.path
import re
import signal
import stat
import sys
import time
KILL_PROCESS_TIMEOUT = int(os.environ.get('KILL_PROCESS_TIMEOUT', 5)) KILL_PROCESS_TIMEOUT = int(os.environ.get('KILL_PROCESS_TIMEOUT', 5))
KILL_ALL_PROCESSES_TIMEOUT = int(os.environ.get('KILL_ALL_PROCESSES_TIMEOUT', 5)) KILL_ALL_PROCESSES_TIMEOUT = int(os.environ.get('KILL_ALL_PROCESSES_TIMEOUT', 5))
LOG_LEVEL_ERROR = 1 LOG_LEVEL_ERROR = 1
LOG_LEVEL_WARN = 1 LOG_LEVEL_WARN = 1
LOG_LEVEL_INFO = 2 LOG_LEVEL_INFO = 2
LOG_LEVEL_DEBUG = 3 LOG_LEVEL_DEBUG = 3
SHENV_NAME_WHITELIST_REGEX = re.compile('[^\w\-_\.]') SHENV_NAME_WHITELIST_REGEX = re.compile('[^\w\-_\.]')
@@ -15,345 +24,403 @@ log_level = None
terminated_child_processes = {} terminated_child_processes = {}
class AlarmException(Exception):
pass
def error(message):
if log_level >= LOG_LEVEL_ERROR:
sys.stderr.write("*** %s\n" % message)
def warn(message):
if log_level >= LOG_LEVEL_WARN:
sys.stderr.write("*** %s\n" % message)
def info(message):
if log_level >= LOG_LEVEL_INFO:
sys.stderr.write("*** %s\n" % message)
def debug(message):
if log_level >= LOG_LEVEL_DEBUG:
sys.stderr.write("*** %s\n" % message)
def ignore_signals_and_raise_keyboard_interrupt(signame):
signal.signal(signal.SIGTERM, signal.SIG_IGN)
signal.signal(signal.SIGINT, signal.SIG_IGN)
raise KeyboardInterrupt(signame)
def raise_alarm_exception():
raise AlarmException('Alarm')
def listdir(path):
try:
result = os.stat(path)
except OSError:
return []
if stat.S_ISDIR(result.st_mode):
return sorted(os.listdir(path))
else:
return []
def is_exe(path):
try:
return os.path.isfile(path) and os.access(path, os.X_OK)
except OSError:
return False
def import_envvars(clear_existing_environment = True, override_existing_environment = True):
if not os.path.exists("/etc/container_environment"):
return
new_env = {}
for envfile in listdir("/etc/container_environment"):
name = os.path.basename(envfile)
with open("/etc/container_environment/" + envfile, "r") as f:
# Text files often end with a trailing newline, which we
# don't want to include in the env variable value. See
# https://github.com/phusion/baseimage-docker/pull/49
value = re.sub('\n\Z', '', f.read())
new_env[name] = value
if clear_existing_environment:
os.environ.clear()
for name, value in new_env.items():
if override_existing_environment or not name in os.environ:
os.environ[name] = value
def export_envvars(to_dir = True):
if not os.path.exists("/etc/container_environment"):
return
shell_dump = ""
for name, value in os.environ.items():
if name in ['HOME', 'USER', 'GROUP', 'UID', 'GID', 'SHELL']:
continue
if to_dir:
with open("/etc/container_environment/" + name, "w") as f:
f.write(value)
shell_dump += "export " + sanitize_shenvname(name) + "=" + shquote(value) + "\n"
with open("/etc/container_environment.sh", "w") as f:
f.write(shell_dump)
with open("/etc/container_environment.json", "w") as f:
f.write(json.dumps(dict(os.environ)))
_find_unsafe = re.compile(r'[^\w@%+=:,./-]').search _find_unsafe = re.compile(r'[^\w@%+=:,./-]').search
def shquote(s):
"""Return a shell-escaped version of the string *s*."""
if not s:
return "''"
if _find_unsafe(s) is None:
return s
# use single quotes, and put single quotes into double quotes class AlarmException(Exception):
# the string $'b is then quoted as '$'"'"'b' pass
return "'" + s.replace("'", "'\"'\"'") + "'"
def error(message):
if log_level >= LOG_LEVEL_ERROR:
sys.stderr.write("*** %s\n" % message)
def warn(message):
if log_level >= LOG_LEVEL_WARN:
sys.stderr.write("*** %s\n" % message)
def info(message):
if log_level >= LOG_LEVEL_INFO:
sys.stderr.write("*** %s\n" % message)
def debug(message):
if log_level >= LOG_LEVEL_DEBUG:
sys.stderr.write("*** %s\n" % message)
def ignore_signals_and_raise_keyboard_interrupt(signame):
signal.signal(signal.SIGTERM, signal.SIG_IGN)
signal.signal(signal.SIGINT, signal.SIG_IGN)
raise KeyboardInterrupt(signame)
def raise_alarm_exception():
raise AlarmException('Alarm')
def listdir(path):
try:
result = os.stat(path)
except OSError:
return []
if stat.S_ISDIR(result.st_mode):
return sorted(os.listdir(path))
else:
return []
def is_exe(path):
try:
return os.path.isfile(path) and os.access(path, os.X_OK)
except OSError:
return False
def import_envvars(clear_existing_environment=True, override_existing_environment=True):
if not os.path.exists("/etc/container_environment"):
return
new_env = {}
for envfile in listdir("/etc/container_environment"):
name = os.path.basename(envfile)
with open("/etc/container_environment/" + envfile, "r") as f:
# Text files often end with a trailing newline, which we
# don't want to include in the env variable value. See
# https://github.com/phusion/baseimage-docker/pull/49
value = re.sub('\n\Z', '', f.read())
new_env[name] = value
if clear_existing_environment:
os.environ.clear()
for name, value in new_env.items():
if override_existing_environment or name not in os.environ:
os.environ[name] = value
def export_envvars(to_dir=True):
if not os.path.exists("/etc/container_environment"):
return
shell_dump = ""
for name, value in os.environ.items():
if name in ['HOME', 'USER', 'GROUP', 'UID', 'GID', 'SHELL']:
continue
if to_dir:
with open("/etc/container_environment/" + name, "w") as f:
f.write(value)
shell_dump += "export " + sanitize_shenvname(name) + "=" + shquote(value) + "\n"
with open("/etc/container_environment.sh", "w") as f:
f.write(shell_dump)
with open("/etc/container_environment.json", "w") as f:
f.write(json.dumps(dict(os.environ)))
def shquote(s):
"""Return a shell-escaped version of the string *s*."""
if not s:
return "''"
if _find_unsafe(s) is None:
return s
# use single quotes, and put single quotes into double quotes
# the string $'b is then quoted as '$'"'"'b'
return "'" + s.replace("'", "'\"'\"'") + "'"
def sanitize_shenvname(s): def sanitize_shenvname(s):
return re.sub(SHENV_NAME_WHITELIST_REGEX, "_", s) return re.sub(SHENV_NAME_WHITELIST_REGEX, "_", s)
# Waits for the child process with the given PID, while at the same time # Waits for the child process with the given PID, while at the same time
# reaping any other child processes that have exited (e.g. adopted child # reaping any other child processes that have exited (e.g. adopted child
# processes that have terminated). # processes that have terminated).
def waitpid_reap_other_children(pid): def waitpid_reap_other_children(pid):
global terminated_child_processes global terminated_child_processes
status = terminated_child_processes.get(pid) status = terminated_child_processes.get(pid)
if status: if status:
# A previous call to waitpid_reap_other_children(), # A previous call to waitpid_reap_other_children(),
# with an argument not equal to the current argument, # with an argument not equal to the current argument,
# already waited for this process. Return the status # already waited for this process. Return the status
# that was obtained back then. # that was obtained back then.
del terminated_child_processes[pid] del terminated_child_processes[pid]
return status return status
done = False done = False
status = None status = None
while not done: while not done:
try: try:
# https://github.com/phusion/baseimage-docker/issues/151#issuecomment-92660569 # https://github.com/phusion/baseimage-docker/issues/151#issuecomment-92660569
this_pid, status = os.waitpid(pid, os.WNOHANG) this_pid, status = os.waitpid(pid, os.WNOHANG)
if this_pid == 0: if this_pid == 0:
this_pid, status = os.waitpid(-1, 0) this_pid, status = os.waitpid(-1, 0)
if this_pid == pid: if this_pid == pid:
done = True done = True
else: else:
# Save status for later. # Save status for later.
terminated_child_processes[this_pid] = status terminated_child_processes[this_pid] = status
except OSError as e: except OSError as e:
if e.errno == errno.ECHILD or e.errno == errno.ESRCH: if e.errno == errno.ECHILD or e.errno == errno.ESRCH:
return None return None
else: else:
raise raise
return status return status
def stop_child_process(name, pid, signo=signal.SIGTERM, time_limit=KILL_PROCESS_TIMEOUT):
info("Shutting down %s (PID %d)..." % (name, pid))
try:
os.kill(pid, signo)
except OSError:
pass
signal.alarm(time_limit)
try:
try:
waitpid_reap_other_children(pid)
except OSError:
pass
except AlarmException:
warn("%s (PID %d) did not shut down in time. Forcing it to exit." % (name, pid))
try:
os.kill(pid, signal.SIGKILL)
except OSError:
pass
try:
waitpid_reap_other_children(pid)
except OSError:
pass
finally:
signal.alarm(0)
def stop_child_process(name, pid, signo = signal.SIGTERM, time_limit = KILL_PROCESS_TIMEOUT):
info("Shutting down %s (PID %d)..." % (name, pid))
try:
os.kill(pid, signo)
except OSError:
pass
signal.alarm(time_limit)
try:
try:
waitpid_reap_other_children(pid)
except OSError:
pass
except AlarmException:
warn("%s (PID %d) did not shut down in time. Forcing it to exit." % (name, pid))
try:
os.kill(pid, signal.SIGKILL)
except OSError:
pass
try:
waitpid_reap_other_children(pid)
except OSError:
pass
finally:
signal.alarm(0)
def run_command_killable(*argv): def run_command_killable(*argv):
filename = argv[0] filename = argv[0]
status = None status = None
pid = os.spawnvp(os.P_NOWAIT, filename, argv) pid = os.spawnvp(os.P_NOWAIT, filename, argv)
try: try:
status = waitpid_reap_other_children(pid) status = waitpid_reap_other_children(pid)
except BaseException as s: except BaseException:
warn("An error occurred. Aborting.") warn("An error occurred. Aborting.")
stop_child_process(filename, pid) stop_child_process(filename, pid)
raise raise
if status != 0: if status != 0:
if status is None: if status is None:
error("%s exited with unknown status\n" % filename) error("%s exited with unknown status\n" % filename)
else: else:
error("%s failed with status %d\n" % (filename, os.WEXITSTATUS(status))) error("%s failed with status %d\n" % (filename, os.WEXITSTATUS(status)))
sys.exit(1) sys.exit(1)
def run_command_killable_and_import_envvars(*argv): def run_command_killable_and_import_envvars(*argv):
run_command_killable(*argv) run_command_killable(*argv)
import_envvars() import_envvars()
export_envvars(False) export_envvars(False)
def kill_all_processes(time_limit): def kill_all_processes(time_limit):
info("Killing all processes...") info("Killing all processes...")
try: try:
os.kill(-1, signal.SIGTERM) os.kill(-1, signal.SIGTERM)
except OSError: except OSError:
pass pass
signal.alarm(time_limit) signal.alarm(time_limit)
try: try:
# Wait until no more child processes exist. # Wait until no more child processes exist.
done = False done = False
while not done: while not done:
try: try:
os.waitpid(-1, 0) os.waitpid(-1, 0)
except OSError as e: except OSError as e:
if e.errno == errno.ECHILD: if e.errno == errno.ECHILD:
done = True done = True
else: else:
raise raise
except AlarmException: except AlarmException:
warn("Not all processes have exited in time. Forcing them to exit.") warn("Not all processes have exited in time. Forcing them to exit.")
try: try:
os.kill(-1, signal.SIGKILL) os.kill(-1, signal.SIGKILL)
except OSError: except OSError:
pass pass
finally: finally:
signal.alarm(0) signal.alarm(0)
def run_startup_files(): def run_startup_files():
# Run /etc/my_init.d/* # Run /etc/my_init.d/*
for name in listdir("/etc/my_init.d"): for name in listdir("/etc/my_init.d"):
filename = "/etc/my_init.d/" + name filename = "/etc/my_init.d/" + name
if is_exe(filename): if is_exe(filename):
info("Running %s..." % filename) info("Running %s..." % filename)
run_command_killable_and_import_envvars(filename) run_command_killable_and_import_envvars(filename)
# Run /etc/rc.local.
if is_exe("/etc/rc.local"):
info("Running /etc/rc.local...")
run_command_killable_and_import_envvars("/etc/rc.local")
# Run /etc/rc.local.
if is_exe("/etc/rc.local"):
info("Running /etc/rc.local...")
run_command_killable_and_import_envvars("/etc/rc.local")
def start_runit(): def start_runit():
info("Booting runit daemon...") info("Booting runit daemon...")
pid = os.spawnl(os.P_NOWAIT, "/usr/bin/runsvdir", "/usr/bin/runsvdir", pid = os.spawnl(os.P_NOWAIT,
"-P", "/etc/service") "/usr/bin/runsvdir",
info("Runit started as PID %d" % pid) "/usr/bin/runsvdir",
return pid "-P",
"/etc/service")
info("Runit started as PID %d" % pid)
return pid
def wait_for_runit_or_interrupt(pid): def wait_for_runit_or_interrupt(pid):
try: try:
status = waitpid_reap_other_children(pid) status = waitpid_reap_other_children(pid)
return (True, status) return (True, status)
except KeyboardInterrupt: except KeyboardInterrupt:
return (False, None) return (False, None)
def shutdown_runit_services(quiet=False):
if not quiet:
debug("Begin shutting down runit services...")
os.system("/usr/bin/sv -w %d down /etc/service/* > /dev/null" % KILL_PROCESS_TIMEOUT)
def shutdown_runit_services(quiet = False):
if not quiet:
debug("Begin shutting down runit services...")
os.system("/usr/bin/sv -w %d down /etc/service/* > /dev/null" % KILL_PROCESS_TIMEOUT)
def wait_for_runit_services(): def wait_for_runit_services():
debug("Waiting for runit services to exit...") debug("Waiting for runit services to exit...")
done = False done = False
while not done: while not done:
done = os.system("/usr/bin/sv status /etc/service/* | grep -q '^run:'") != 0 done = os.system("/usr/bin/sv status /etc/service/* | grep -q '^run:'") != 0
if not done: if not done:
time.sleep(0.1) time.sleep(0.1)
# According to https://github.com/phusion/baseimage-docker/issues/315 # According to https://github.com/phusion/baseimage-docker/issues/315
# there is a bug or race condition in Runit, causing it # there is a bug or race condition in Runit, causing it
# not to shutdown services that are already being started. # not to shutdown services that are already being started.
# So during shutdown we repeatedly instruct Runit to shutdown # So during shutdown we repeatedly instruct Runit to shutdown
# services. # services.
shutdown_runit_services(True) shutdown_runit_services(True)
def install_insecure_key(): def install_insecure_key():
info("Installing insecure SSH key for user root") info("Installing insecure SSH key for user root")
run_command_killable("/usr/sbin/enable_insecure_key") run_command_killable("/usr/sbin/enable_insecure_key")
def main(args): def main(args):
import_envvars(False, False) import_envvars(False, False)
export_envvars() export_envvars()
if args.enable_insecure_key: if args.enable_insecure_key:
install_insecure_key() install_insecure_key()
if not args.skip_startup_files: if not args.skip_startup_files:
run_startup_files() run_startup_files()
runit_exited = False runit_exited = False
exit_code = None exit_code = None
if not args.skip_runit:
runit_pid = start_runit()
try:
exit_status = None
if len(args.main_command) == 0:
runit_exited, exit_code = wait_for_runit_or_interrupt(runit_pid)
if runit_exited:
if exit_code is None:
info("Runit exited with unknown status")
exit_status = 1
else:
exit_status = os.WEXITSTATUS(exit_code)
info("Runit exited with status %d" % exit_status)
else:
info("Running %s..." % " ".join(args.main_command))
pid = os.spawnvp(os.P_NOWAIT,
args.main_command[0],
args.main_command)
try:
exit_code = waitpid_reap_other_children(pid)
if exit_code is None:
info("%s exited with unknown status." % args.main_command[0])
exit_status = 1
else:
exit_status = os.WEXITSTATUS(exit_code)
info("%s exited with status %d." % (args.main_command[0], exit_status))
except KeyboardInterrupt:
stop_child_process(args.main_command[0], pid)
raise
except BaseException:
warn("An error occurred. Aborting.")
stop_child_process(args.main_command[0], pid)
raise
sys.exit(exit_status)
finally:
if not args.skip_runit:
shutdown_runit_services()
if not runit_exited:
stop_child_process("runit daemon", runit_pid)
wait_for_runit_services()
if not args.skip_runit:
runit_pid = start_runit()
try:
exit_status = None
if len(args.main_command) == 0:
runit_exited, exit_code = wait_for_runit_or_interrupt(runit_pid)
if runit_exited:
if exit_code is None:
info("Runit exited with unknown status")
exit_status = 1
else:
exit_status = os.WEXITSTATUS(exit_code)
info("Runit exited with status %d" % exit_status)
else:
info("Running %s..." % " ".join(args.main_command))
pid = os.spawnvp(os.P_NOWAIT, args.main_command[0], args.main_command)
try:
exit_code = waitpid_reap_other_children(pid)
if exit_code is None:
info("%s exited with unknown status." % args.main_command[0])
exit_status = 1
else:
exit_status = os.WEXITSTATUS(exit_code)
info("%s exited with status %d." % (args.main_command[0], exit_status))
except KeyboardInterrupt:
stop_child_process(args.main_command[0], pid)
raise
except BaseException as s:
warn("An error occurred. Aborting.")
stop_child_process(args.main_command[0], pid)
raise
sys.exit(exit_status)
finally:
if not args.skip_runit:
shutdown_runit_services()
if not runit_exited:
stop_child_process("runit daemon", runit_pid)
wait_for_runit_services()
# Parse options. # Parse options.
parser = argparse.ArgumentParser(description = 'Initialize the system.') parser = argparse.ArgumentParser(description='Initialize the system.')
parser.add_argument('main_command', metavar = 'MAIN_COMMAND', type = str, nargs = '*', parser.add_argument(
help = 'The main command to run. (default: runit)') 'main_command',
parser.add_argument('--enable-insecure-key', dest = 'enable_insecure_key', metavar='MAIN_COMMAND',
action = 'store_const', const = True, default = False, type=str,
help = 'Install the insecure SSH key') nargs='*',
parser.add_argument('--skip-startup-files', dest = 'skip_startup_files', help='The main command to run. (default: runit)')
action = 'store_const', const = True, default = False, parser.add_argument(
help = 'Skip running /etc/my_init.d/* and /etc/rc.local') '--enable-insecure-key',
parser.add_argument('--skip-runit', dest = 'skip_runit', dest='enable_insecure_key',
action = 'store_const', const = True, default = False, action='store_const',
help = 'Do not run runit services') const=True,
parser.add_argument('--no-kill-all-on-exit', dest = 'kill_all_on_exit', default=False,
action = 'store_const', const = False, default = True, help='Install the insecure SSH key')
help = 'Don\'t kill all processes on the system upon exiting') parser.add_argument(
parser.add_argument('--quiet', dest = 'log_level', '--skip-startup-files',
action = 'store_const', const = LOG_LEVEL_WARN, default = LOG_LEVEL_INFO, dest='skip_startup_files',
help = 'Only print warnings and errors') action='store_const',
const=True,
default=False,
help='Skip running /etc/my_init.d/* and /etc/rc.local')
parser.add_argument(
'--skip-runit',
dest='skip_runit',
action='store_const',
const=True,
default=False,
help='Do not run runit services')
parser.add_argument(
'--no-kill-all-on-exit',
dest='kill_all_on_exit',
action='store_const',
const=False,
default=True,
help='Do not kill all processes on the system upon exiting')
parser.add_argument(
'--quiet',
dest='log_level',
action='store_const',
const=LOG_LEVEL_WARN,
default=LOG_LEVEL_INFO,
help='Only print warnings and errors')
args = parser.parse_args() args = parser.parse_args()
log_level = args.log_level log_level = args.log_level
if args.skip_runit and len(args.main_command) == 0: if args.skip_runit and len(args.main_command) == 0:
error("When --skip-runit is given, you must also pass a main command.") error("When --skip-runit is given, you must also pass a main command.")
sys.exit(1) sys.exit(1)
# Run main function. # Run main function.
signal.signal(signal.SIGTERM, lambda signum, frame: ignore_signals_and_raise_keyboard_interrupt('SIGTERM')) signal.signal(signal.SIGTERM,
signal.signal(signal.SIGINT, lambda signum, frame: ignore_signals_and_raise_keyboard_interrupt('SIGINT')) lambda signum, frame: ignore_signals_and_raise_keyboard_interrupt('SIGTERM'))
signal.signal(signal.SIGALRM, lambda signum, frame: raise_alarm_exception()) signal.signal(signal.SIGINT,
lambda signum, frame: ignore_signals_and_raise_keyboard_interrupt('SIGINT'))
signal.signal(signal.SIGALRM,
lambda signum, frame: raise_alarm_exception())
try: try:
main(args) main(args)
except KeyboardInterrupt: except KeyboardInterrupt:
warn("Init system aborted.") warn("Init system aborted.")
exit(2) exit(2)
finally: finally:
if args.kill_all_on_exit: if args.kill_all_on_exit:
kill_all_processes(KILL_ALL_PROCESSES_TIMEOUT) kill_all_processes(KILL_ALL_PROCESSES_TIMEOUT)