| #!/usr/bin/python3 -u |
| # -*- coding: utf-8 -*- |
| |
| import os, os.path, sys, stat, signal, errno, argparse, time, json, re, yaml, ast, socket, shutil, pwd, grp |
| |
| KILL_PROCESS_TIMEOUT = int(os.environ.get('KILL_PROCESS_TIMEOUT', 30)) |
| KILL_ALL_PROCESSES_TIMEOUT = int(os.environ.get('KILL_ALL_PROCESSES_TIMEOUT', 30)) |
| |
| LOG_LEVEL_NONE = 0 |
| LOG_LEVEL_ERROR = 1 |
| LOG_LEVEL_WARNING = 2 |
| LOG_LEVEL_INFO = 3 |
| LOG_LEVEL_DEBUG = 4 |
| LOG_LEVEL_TRACE = 5 |
| |
| SHENV_NAME_WHITELIST_REGEX = re.compile('\W') |
| |
| log_level = None |
| |
| environ_backup = dict(os.environ) |
| terminated_child_processes = {} |
| |
| IMPORT_STARTUP_FILENAME="startup.sh" |
| IMPORT_PROCESS_FILENAME="process.sh" |
| IMPORT_FINISH_FILENAME="finish.sh" |
| |
| IMPORT_ENVIRONMENT_DIR="/container/environment" |
| IMPORT_FIRST_STARTUP_ENVIRONMENT_DIR="/container/environment/startup" |
| |
| ENV_FILES_YAML_EXTENSIONS = ('.yaml', '.startup.yaml') |
| ENV_FILES_JSON_EXTENSIONS = ('.json', '.startup.json') |
| ENV_FILES_STARTUP_EXTENSIONS = ('.startup.yaml', '.startup.json') |
| |
| IMPORT_SERVICE_DIR="/container/service" |
| |
| RUN_DIR="/container/run" |
| RUN_STATE_DIR = RUN_DIR + "/state" |
| RUN_ENVIRONMENT_DIR = RUN_DIR + "/environment" |
| RUN_ENVIRONMENT_FILE_EXPORT = RUN_DIR + "/environment.sh" |
| RUN_STARTUP_DIR = RUN_DIR + "/startup" |
| RUN_STARTUP_FINAL_FILE = RUN_DIR + "/startup.sh" |
| RUN_PROCESS_DIR = RUN_DIR + "/process" |
| RUN_SERVICE_DIR = RUN_DIR + "/service" |
| |
| ENVIRONMENT_LOG_LEVEL_KEY = 'CONTAINER_LOG_LEVEL' |
| ENVIRONMENT_SERVICE_DIR_KEY = 'CONTAINER_SERVICE_DIR' |
| ENVIRONMENT_STATE_DIR_KEY = 'CONTAINER_STATE_DIR' |
| |
| class AlarmException(Exception): |
| pass |
| |
| def error(message): |
| if log_level >= LOG_LEVEL_ERROR: |
| sys.stderr.write("*** %s\n" % message) |
| |
| def warning(message): |
| if log_level >= LOG_LEVEL_WARNING: |
| 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 trace(message): |
| if log_level >= LOG_LEVEL_TRACE: |
| sys.stderr.write("*** %s\n" % message) |
| |
| def debug_env_dump(): |
| debug("------------ Environment dump ------------") |
| for name, value in list(os.environ.items()): |
| debug(name + " = " + value) |
| debug("------------------------------------------") |
| |
| 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 xstr(s): |
| if s is None: |
| return '' |
| return str(s) |
| |
| def set_env_hostname_to_etc_hosts(): |
| try: |
| if "HOSTNAME" in os.environ: |
| socket_hostname = socket.gethostname() |
| |
| if os.environ["HOSTNAME"] != socket_hostname: |
| ip_address = socket.gethostbyname(socket_hostname) |
| with open("/etc/hosts", "a") as myfile: |
| myfile.write(ip_address+" "+os.environ["HOSTNAME"]+"\n") |
| except: |
| trace("set_env_hostname_to_etc_hosts: failed at some point...") |
| |
| def python_dict_to_bash_envvar(name, python_dict): |
| |
| for value in python_dict: |
| python_to_bash_envvar(name+"_KEY", value) |
| python_to_bash_envvar(name+"_VALUE", python_dict.get(value)) |
| |
| values = "#COMPLEX_BASH_ENV:ROW: "+name+"_KEY "+name+"_VALUE" |
| os.environ[name] = xstr(values) |
| trace("python2bash : set : " + name + " = "+ os.environ[name]) |
| |
| def python_list_to_bash_envvar(name, python_list): |
| |
| values="#COMPLEX_BASH_ENV:TABLE:" |
| |
| i=1 |
| for value in python_list: |
| child_name = name + "_ROW_" + str(i) |
| values += " " + child_name |
| python_to_bash_envvar(child_name, value) |
| i = i +1 |
| |
| os.environ[name] = xstr(values) |
| trace("python2bash : set : " + name + " = "+ os.environ[name]) |
| |
| def python_to_bash_envvar(name, value): |
| |
| try: |
| value = ast.literal_eval(value) |
| except: |
| pass |
| |
| if isinstance(value, list): |
| python_list_to_bash_envvar(name,value) |
| |
| elif isinstance(value, dict): |
| python_dict_to_bash_envvar(name,value) |
| |
| else: |
| os.environ[name] = xstr(value) |
| trace("python2bash : set : " + name + " = "+ os.environ[name]) |
| |
| def decode_python_envvars(): |
| _environ = dict(os.environ) |
| for name, value in list(_environ.items()): |
| if value.startswith("#PYTHON2BASH:") : |
| value = value.replace("#PYTHON2BASH:","",1) |
| python_to_bash_envvar(name, value) |
| |
| def decode_json_envvars(): |
| _environ = dict(os.environ) |
| for name, value in list(_environ.items()): |
| if value.startswith("#JSON2BASH:") : |
| value = value.replace("#JSON2BASH:","",1) |
| try: |
| value = json.loads(value) |
| python_to_bash_envvar(name,value) |
| except: |
| os.environ[name] = xstr(value) |
| warning("failed to parse : " + xstr(value)) |
| trace("set : " + name + " = "+ os.environ[name]) |
| |
| def decode_envvars(): |
| decode_json_envvars() |
| decode_python_envvars() |
| |
| def generic_import_envvars(path, override_existing_environment): |
| if not os.path.exists(path): |
| trace("generic_import_envvars "+ path+ " don't exists") |
| return |
| new_env = {} |
| for envfile in listdir(path): |
| filePath = path + os.sep + envfile |
| if os.path.isfile(filePath) and "." not in envfile: |
| name = os.path.basename(envfile) |
| with open(filePath, "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 |
| trace("import " + name + " from " + filePath + " --- ") |
| |
| for name, value in list(new_env.items()): |
| if override_existing_environment or name not in os.environ: |
| os.environ[name] = value |
| trace("set : " + name + " = "+ os.environ[name]) |
| else: |
| debug("ignore : " + name + " = " + xstr(value) + " (keep " + name + " = " + os.environ[name] + " )") |
| |
| def import_run_envvars(): |
| clear_environ() |
| generic_import_envvars(RUN_ENVIRONMENT_DIR, True) |
| |
| def import_envvars(): |
| generic_import_envvars(IMPORT_ENVIRONMENT_DIR, False) |
| generic_import_envvars(IMPORT_FIRST_STARTUP_ENVIRONMENT_DIR, False) |
| |
| def export_run_envvars(to_dir = True): |
| if to_dir and not os.path.exists(RUN_ENVIRONMENT_DIR): |
| warning("export_run_envvars: "+RUN_ENVIRONMENT_DIR+" don't exists") |
| return |
| shell_dump = "" |
| for name, value in list(os.environ.items()): |
| if name in ['USER', 'GROUP', 'UID', 'GID', 'SHELL']: |
| continue |
| if to_dir: |
| with open(RUN_ENVIRONMENT_DIR + os.sep + name, "w") as f: |
| f.write(value) |
| trace("export " + name + " to " + RUN_ENVIRONMENT_DIR + os.sep + name + " --- ") |
| shell_dump += "export " + sanitize_shenvname(name) + "=" + shquote(value) + "\n" |
| |
| with open(RUN_ENVIRONMENT_FILE_EXPORT, "w") as f: |
| f.write(shell_dump) |
| trace("export "+RUN_ENVIRONMENT_FILE_EXPORT+" --- ") |
| |
| def create_run_envvars(): |
| set_dir_env() |
| set_log_level_env() |
| import_envvars() |
| import_env_files() |
| decode_envvars() |
| export_run_envvars() |
| |
| def clear_run_envvars(): |
| try: |
| shutil.rmtree(RUN_ENVIRONMENT_DIR) |
| os.makedirs(RUN_ENVIRONMENT_DIR) |
| os.chmod(RUN_ENVIRONMENT_DIR, 700) |
| except: |
| warning("clear_run_envvars: failed at some point...") |
| |
| def print_env_files_order(file_extensions): |
| |
| if not os.path.exists(IMPORT_ENVIRONMENT_DIR): |
| warning("print_env_files_order "+IMPORT_ENVIRONMENT_DIR+" don't exists") |
| return |
| |
| to_print = 'Caution: previously defined variables will not be overriden.\n' |
| |
| file_found = False |
| for subdir, dirs, files in sorted(os.walk(IMPORT_ENVIRONMENT_DIR)): |
| for file in files: |
| filepath = subdir + os.sep + file |
| if filepath.endswith(file_extensions): |
| file_found = True |
| filepath = subdir + os.sep + file |
| to_print += filepath + '\n' |
| |
| if file_found: |
| if log_level < LOG_LEVEL_DEBUG: |
| to_print+='\nTo see how this files are processed and environment variables values,\n' |
| to_print+='run this container with \'--loglevel debug\'' |
| |
| info('Environment files will be proccessed in this order : \n' + to_print) |
| |
| def import_env_files(): |
| |
| if not os.path.exists(IMPORT_ENVIRONMENT_DIR): |
| warning("import_env_files: "+IMPORT_ENVIRONMENT_DIR+" don't exists") |
| return |
| |
| file_extensions = ENV_FILES_YAML_EXTENSIONS + ENV_FILES_JSON_EXTENSIONS |
| print_env_files_order(file_extensions) |
| |
| for subdir, dirs, files in sorted(os.walk(IMPORT_ENVIRONMENT_DIR)): |
| for file in files: |
| if file.endswith(file_extensions): |
| filepath = subdir + os.sep + file |
| |
| try: |
| with open(filepath, "r") as f: |
| |
| debug("--- process file : " + filepath+ " ---") |
| |
| if file.endswith(ENV_FILES_YAML_EXTENSIONS): |
| env_vars = yaml.load(f) |
| |
| elif file.endswith(ENV_FILES_JSON_EXTENSIONS): |
| env_vars = json.load(f) |
| |
| for name, value in list(env_vars.items()): |
| if not name in os.environ: |
| if isinstance(value, list) or isinstance(value, dict): |
| os.environ[name] = '#PYTHON2BASH:' + xstr(value) |
| else: |
| os.environ[name] = xstr(value) |
| trace("set : " + name + " = "+ os.environ[name]) |
| else: |
| debug("ignore : " + name + " = " + xstr(value) + " (keep " + name + " = " + os.environ[name] + " )") |
| except: |
| warning('failed to parse: ' + filepath) |
| |
| def remove_startup_env_files(): |
| |
| if os.path.isdir(IMPORT_FIRST_STARTUP_ENVIRONMENT_DIR): |
| try: |
| shutil.rmtree(IMPORT_FIRST_STARTUP_ENVIRONMENT_DIR) |
| except: |
| warning("remove_startup_env_files: failed to remove "+IMPORT_FIRST_STARTUP_ENVIRONMENT_DIR) |
| |
| if not os.path.exists(IMPORT_ENVIRONMENT_DIR): |
| warning("remove_startup_env_files: "+IMPORT_ENVIRONMENT_DIR+" don't exists") |
| return |
| |
| for subdir, dirs, files in sorted(os.walk(IMPORT_ENVIRONMENT_DIR)): |
| for file in files: |
| filepath = subdir + os.sep + file |
| if filepath.endswith(ENV_FILES_STARTUP_EXTENSIONS): |
| try: |
| os.remove(filepath) |
| info("Remove file "+filepath) |
| except: |
| warning("remove_startup_env_files: failed to remove "+filepath) |
| |
| def restore_environ(): |
| clear_environ() |
| trace("--- Restore initial environment ---") |
| os.environ.update(environ_backup) |
| |
| def clear_environ(): |
| trace("--- Clear existing environment ---") |
| os.environ.clear() |
| |
| def set_startup_scripts_env(): |
| info("Set environment for startup files") |
| clear_run_envvars() # clear previous environment |
| create_run_envvars() # create run envvars with all env files |
| |
| def set_process_env(keep_startup_env = False): |
| info("Set environment for container process") |
| if not keep_startup_env: |
| remove_startup_env_files() |
| clear_run_envvars() |
| |
| restore_environ() |
| create_run_envvars() # recreate env var without startup env files |
| |
| def setup_run_directories(args): |
| |
| directories = (RUN_PROCESS_DIR, RUN_STARTUP_DIR, RUN_STATE_DIR, RUN_ENVIRONMENT_DIR) |
| for directory in directories: |
| if not os.path.exists(directory): |
| os.makedirs(directory) |
| |
| if directory == RUN_ENVIRONMENT_DIR: |
| os.chmod(directory, 700) |
| |
| if not os.path.exists(RUN_ENVIRONMENT_FILE_EXPORT): |
| open(RUN_ENVIRONMENT_FILE_EXPORT, 'a').close() |
| os.chmod(RUN_ENVIRONMENT_FILE_EXPORT, 640) |
| uid = pwd.getpwnam("root").pw_uid |
| gid = grp.getgrnam("docker_env").gr_gid |
| os.chown(RUN_ENVIRONMENT_FILE_EXPORT, uid, gid) |
| |
| if state_is_first_start(): |
| |
| if args.copy_service: |
| copy_service_to_run_dir() |
| |
| set_dir_env() |
| |
| base_path = os.environ[ENVIRONMENT_SERVICE_DIR_KEY] |
| nb_service = len(listdir(base_path)) |
| |
| if nb_service > 0 : |
| info("Search service in " + ENVIRONMENT_SERVICE_DIR_KEY + " = "+base_path+" :") |
| for d in listdir(base_path): |
| d_path = base_path + os.sep + d |
| if os.path.isdir(d_path): |
| if is_exe(d_path + os.sep + IMPORT_STARTUP_FILENAME): |
| info('link ' + d_path + os.sep + IMPORT_STARTUP_FILENAME + ' to ' + RUN_STARTUP_DIR + os.sep + d) |
| try: |
| os.symlink(d_path + os.sep + IMPORT_STARTUP_FILENAME, RUN_STARTUP_DIR + os.sep + d) |
| except OSError as detail: |
| warning('failed to link ' + d_path + os.sep + IMPORT_STARTUP_FILENAME + ' to ' + RUN_STARTUP_DIR + os.sep + d + ': ' + xstr(detail)) |
| |
| if is_exe(d_path + os.sep + IMPORT_PROCESS_FILENAME): |
| info('link ' + d_path + os.sep + IMPORT_PROCESS_FILENAME + ' to ' + RUN_PROCESS_DIR + os.sep + d + os.sep + 'run') |
| |
| if not os.path.exists(RUN_PROCESS_DIR + os.sep + d): |
| os.makedirs(RUN_PROCESS_DIR + os.sep + d) |
| else: |
| warning('directory ' + RUN_PROCESS_DIR + os.sep + d + ' already exists') |
| |
| try: |
| os.symlink(d_path + os.sep + IMPORT_PROCESS_FILENAME, RUN_PROCESS_DIR + os.sep + d + os.sep + 'run') |
| except OSError as detail: |
| warning('failed to link ' + d_path + os.sep + IMPORT_PROCESS_FILENAME + ' to ' + RUN_PROCESS_DIR + os.sep + d + os.sep + 'run : ' + xstr(detail)) |
| |
| if not args.skip_finish_files and is_exe(d_path + os.sep + IMPORT_FINISH_FILENAME): |
| info('link ' + d_path + os.sep + IMPORT_FINISH_FILENAME + ' to ' + RUN_PROCESS_DIR + os.sep + d + os.sep + 'finish') |
| |
| if not os.path.exists(RUN_PROCESS_DIR + os.sep + d): |
| os.makedirs(RUN_PROCESS_DIR + os.sep + d) |
| |
| try: |
| os.symlink(d_path + os.sep + IMPORT_FINISH_FILENAME, RUN_PROCESS_DIR + os.sep + d + os.sep + 'finish') |
| except OSError as detail: |
| warning('failed to link ' + d_path + os.sep + IMPORT_FINISH_FILENAME + ' to ' + RUN_PROCESS_DIR + os.sep + d + os.sep + 'finish : ' + xstr(detail)) |
| |
| def set_dir_env(): |
| if state_is_service_copied_to_run_dir(): |
| os.environ[ENVIRONMENT_SERVICE_DIR_KEY] = RUN_SERVICE_DIR |
| else: |
| os.environ[ENVIRONMENT_SERVICE_DIR_KEY] = IMPORT_SERVICE_DIR |
| trace("set : " + ENVIRONMENT_SERVICE_DIR_KEY + " = " + os.environ[ENVIRONMENT_SERVICE_DIR_KEY]) |
| |
| os.environ[ENVIRONMENT_STATE_DIR_KEY] = RUN_STATE_DIR |
| trace("set : " + ENVIRONMENT_STATE_DIR_KEY + " = " + os.environ[ENVIRONMENT_STATE_DIR_KEY]) |
| |
| def set_log_level_env(): |
| os.environ[ENVIRONMENT_LOG_LEVEL_KEY] = xstr(log_level) |
| trace("set : "+ENVIRONMENT_LOG_LEVEL_KEY+" = " + os.environ[ENVIRONMENT_LOG_LEVEL_KEY]) |
| |
| def copy_service_to_run_dir(): |
| |
| if os.path.exists(RUN_SERVICE_DIR): |
| warning("Copy "+IMPORT_SERVICE_DIR+" to "+RUN_SERVICE_DIR + " ignored") |
| warning(RUN_SERVICE_DIR + " already exists") |
| return |
| |
| info("Copy "+IMPORT_SERVICE_DIR+" to "+RUN_SERVICE_DIR) |
| |
| try: |
| shutil.copytree(IMPORT_SERVICE_DIR, RUN_SERVICE_DIR) |
| except shutil.Error as e: |
| warning(e) |
| |
| state_set_service_copied_to_run_dir() |
| |
| def state_set_service_copied_to_run_dir(): |
| open(RUN_STATE_DIR+"/service-copied-to-run-dir", 'a').close() |
| |
| def state_is_service_copied_to_run_dir(): |
| return os.path.exists(RUN_STATE_DIR+'/service-copied-to-run-dir') |
| |
| def state_set_first_startup_done(): |
| open(RUN_STATE_DIR+"/first-startup-done", 'a').close() |
| |
| def state_is_first_start(): |
| return os.path.exists(RUN_STATE_DIR+'/first-startup-done') == False |
| |
| def state_set_startup_done(): |
| open(RUN_STATE_DIR+"/startup-done", 'a').close() |
| |
| def state_reset_startup_done(): |
| try: |
| os.remove(RUN_STATE_DIR+"/startup-done") |
| except OSError: |
| pass |
| |
| def is_multiple_process_container(): |
| return len(listdir(RUN_PROCESS_DIR)) > 1 |
| |
| def is_single_process_container(): |
| return len(listdir(RUN_PROCESS_DIR)) == 1 |
| |
| def get_container_process(): |
| for p in listdir(RUN_PROCESS_DIR): |
| return RUN_PROCESS_DIR + os.sep + p + os.sep + 'run' |
| |
| def is_runit_installed(): |
| return os.path.exists('/usr/bin/sv') |
| |
| _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 |
| # the string $'b is then quoted as '$'"'"'b' |
| return "'" + s.replace("'", "'\"'\"'") + "'" |
| |
| def sanitize_shenvname(s): |
| return re.sub(SHENV_NAME_WHITELIST_REGEX, "_", s) |
| |
| # 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 |
| # processes that have terminated). |
| def waitpid_reap_other_children(pid): |
| global terminated_child_processes |
| |
| status = terminated_child_processes.get(pid) |
| if status: |
| # A previous call to waitpid_reap_other_children(), |
| # with an argument not equal to the current argument, |
| # already waited for this process. Return the status |
| # that was obtained back then. |
| del terminated_child_processes[pid] |
| return status |
| |
| done = False |
| status = None |
| while not done: |
| try: |
| # https://github.com/phusion/baseimage-docker/issues/151#issuecomment-92660569 |
| this_pid, status = os.waitpid(pid, os.WNOHANG) |
| if this_pid == 0: |
| this_pid, status = os.waitpid(-1, 0) |
| if this_pid == pid: |
| done = True |
| else: |
| # Save status for later. |
| terminated_child_processes[this_pid] = status |
| except OSError as e: |
| if e.errno == errno.ECHILD or e.errno == errno.ESRCH: |
| return None |
| else: |
| raise |
| 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: |
| warning("%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(command): |
| status = None |
| debug_env_dump() |
| pid = os.spawnvp(os.P_NOWAIT, command[0], command) |
| try: |
| status = waitpid_reap_other_children(pid) |
| except BaseException: |
| warning("An error occurred. Aborting.") |
| stop_child_process(command[0], pid) |
| raise |
| if status != 0: |
| if status is None: |
| error("%s exited with unknown status\n" % command[0]) |
| else: |
| error("%s failed with status %d\n" % (command[0], os.WEXITSTATUS(status))) |
| sys.exit(1) |
| |
| def run_command_killable_and_import_run_envvars(command): |
| run_command_killable(command) |
| import_run_envvars() |
| export_run_envvars(False) |
| |
| def kill_all_processes(time_limit): |
| info("Killing all processes...") |
| try: |
| os.kill(-1, signal.SIGTERM) |
| except OSError: |
| pass |
| signal.alarm(time_limit) |
| try: |
| # Wait until no more child processes exist. |
| done = False |
| while not done: |
| try: |
| os.waitpid(-1, 0) |
| except OSError as e: |
| if e.errno == errno.ECHILD: |
| done = True |
| else: |
| raise |
| except AlarmException: |
| warning("Not all processes have exited in time. Forcing them to exit.") |
| try: |
| os.kill(-1, signal.SIGKILL) |
| except OSError: |
| pass |
| finally: |
| signal.alarm(0) |
| |
| def container_had_startup_script(): |
| return (len(listdir(RUN_STARTUP_DIR)) > 0 or is_exe(RUN_STARTUP_FINAL_FILE)) |
| |
| def run_startup_files(args): |
| |
| # Run /container/run/startup/* |
| for name in listdir(RUN_STARTUP_DIR): |
| filename = RUN_STARTUP_DIR + os.sep + name |
| if is_exe(filename): |
| info("Running %s..." % filename) |
| run_command_killable_and_import_run_envvars([filename]) |
| |
| # Run /container/run/startup.sh. |
| if is_exe(RUN_STARTUP_FINAL_FILE): |
| info("Running "+RUN_STARTUP_FINAL_FILE+"...") |
| run_command_killable_and_import_run_envvars([RUN_STARTUP_FINAL_FILE]) |
| |
| def wait_for_process_or_interrupt(pid): |
| status = waitpid_reap_other_children(pid) |
| return (True, status) |
| |
| def run_process(args, background_process_name, background_process_command): |
| background_process_pid = run_background_process(background_process_name,background_process_command) |
| background_process_exited = False |
| exit_status = None |
| |
| if len(args.main_command) == 0: |
| background_process_exited, exit_status = wait_background_process(background_process_name, background_process_pid) |
| else: |
| exit_status = run_foreground_process(args.main_command) |
| |
| return background_process_pid, background_process_exited, exit_status |
| |
| def run_background_process(name, command): |
| info("Running "+ name +"...") |
| pid = os.spawnvp(os.P_NOWAIT, command[0], command) |
| debug("%s started as PID %d" % (name, pid)) |
| return pid |
| |
| def wait_background_process(name, pid): |
| exit_code = None |
| exit_status = None |
| process_exited = False |
| |
| process_exited, exit_code = wait_for_process_or_interrupt(pid) |
| if process_exited: |
| if exit_code is None: |
| info(name + " exited with unknown status") |
| exit_status = 1 |
| else: |
| exit_status = os.WEXITSTATUS(exit_code) |
| info("%s exited with status %d" % (name, exit_status)) |
| return (process_exited, exit_status) |
| |
| def run_foreground_process(command): |
| exit_code = None |
| exit_status = None |
| |
| info("Running %s..." % " ".join(command)) |
| pid = os.spawnvp(os.P_NOWAIT, command[0], command) |
| try: |
| exit_code = waitpid_reap_other_children(pid) |
| if exit_code is None: |
| info("%s exited with unknown status." % command[0]) |
| exit_status = 1 |
| else: |
| exit_status = os.WEXITSTATUS(exit_code) |
| info("%s exited with status %d." % (command[0], exit_status)) |
| except KeyboardInterrupt: |
| stop_child_process(command[0], pid) |
| raise |
| except BaseException: |
| warning("An error occurred. Aborting.") |
| stop_child_process(command[0], pid) |
| raise |
| |
| return exit_status |
| |
| def shutdown_runit_services(): |
| debug("Begin shutting down runit services...") |
| os.system("/usr/bin/sv -w %d force-stop %s/* > /dev/null" % (KILL_PROCESS_TIMEOUT, RUN_PROCESS_DIR)) |
| |
| def wait_for_runit_services(): |
| debug("Waiting for runit services to exit...") |
| done = False |
| while not done: |
| done = os.system("/usr/bin/sv status "+RUN_PROCESS_DIR+"/* | grep -q '^run:'") != 0 |
| if not done: |
| time.sleep(0.1) |
| shutdown_runit_services() |
| |
| def run_multiple_process_container(args): |
| if not is_runit_installed(): |
| error("Error: runit is not installed and this is a multiple process container.") |
| return |
| |
| background_process_exited=False |
| background_process_pid=None |
| |
| try: |
| runit_command=["/usr/bin/runsvdir", "-P", RUN_PROCESS_DIR] |
| background_process_pid, background_process_exited, exit_status = run_process(args, "runit daemon", runit_command) |
| |
| sys.exit(exit_status) |
| finally: |
| shutdown_runit_services() |
| if not background_process_exited: |
| stop_child_process("runit daemon", background_process_pid) |
| wait_for_runit_services() |
| |
| def run_single_process_container(args): |
| background_process_exited=False |
| background_process_pid=None |
| |
| try: |
| container_process=get_container_process(); |
| background_process_pid, background_process_exited, exit_status = run_process(args, container_process, [container_process]) |
| |
| sys.exit(exit_status) |
| finally: |
| if not background_process_exited: |
| stop_child_process(container_process, background_process_pid) |
| |
| def run_no_process_container(args): |
| if len(args.main_command) == 0: |
| args.main_command=['bash'] # run bash by default |
| |
| exit_status = run_foreground_process(args.main_command) |
| sys.exit(exit_status) |
| |
| def run_finish_files(): |
| |
| # iterate process dir to find finish files |
| for name in listdir(RUN_PROCESS_DIR): |
| filename = RUN_PROCESS_DIR + os.sep + name + os.sep + "finish" |
| if is_exe(filename): |
| info("Running %s..." % filename) |
| run_command_killable_and_import_run_envvars([filename]) |
| |
| def wait_states(states): |
| for state in states: |
| filename = RUN_STATE_DIR + os.sep + state |
| info("Wait state: " + state) |
| |
| while not os.path.exists(filename): |
| time.sleep(0.1) |
| debug("Check file " + filename) |
| pass |
| debug("Check file " + filename + " [Ok]") |
| |
| def run_cmds(args, when): |
| debug("Run commands before " + when + "...") |
| if len(args.cmds) > 0: |
| |
| for cmd in args.cmds: |
| if (len(cmd) > 1 and cmd[1] == when) or (len(cmd) == 1 and when == "startup"): |
| info("Running '"+cmd[0]+"'...") |
| run_command_killable_and_import_run_envvars(cmd[0].split()) |
| |
| def main(args): |
| |
| info(ENVIRONMENT_LOG_LEVEL_KEY + " = " + xstr(log_level) + " (" + log_level_switcher_inv.get(log_level) + ")") |
| state_reset_startup_done() |
| |
| if args.set_env_hostname_to_etc_hosts: |
| set_env_hostname_to_etc_hosts() |
| |
| wait_states(args.wait_states) |
| setup_run_directories(args) |
| |
| if not args.skip_env_files: |
| set_startup_scripts_env() |
| |
| run_cmds(args,"startup") |
| |
| if not args.skip_startup_files and container_had_startup_script(): |
| run_startup_files(args) |
| |
| state_set_startup_done() |
| state_set_first_startup_done() |
| |
| if not args.skip_env_files: |
| set_process_env(args.keep_startup_env) |
| |
| run_cmds(args,"process") |
| |
| debug_env_dump() |
| |
| if is_single_process_container() and not args.skip_process_files: |
| run_single_process_container(args) |
| |
| elif is_multiple_process_container() and not args.skip_process_files: |
| run_multiple_process_container(args) |
| |
| else: |
| run_no_process_container(args) |
| |
| # Parse options. |
| parser = argparse.ArgumentParser(description = 'Initialize the system.', epilog='Osixia! Light Baseimage: https://github.com/osixia/docker-light-baseimage') |
| parser.add_argument('main_command', metavar = 'MAIN_COMMAND', type = str, nargs = '*', |
| help = 'The main command to run, leave empty to only run container process.') |
| parser.add_argument('-e', '--skip-env-files', dest = 'skip_env_files', |
| action = 'store_const', const = True, default = False, |
| help = 'Skip getting environment values from environment file(s).') |
| parser.add_argument('-s', '--skip-startup-files', dest = 'skip_startup_files', |
| action = 'store_const', const = True, default = False, |
| help = 'Skip running '+RUN_STARTUP_DIR+'/* and '+RUN_STARTUP_FINAL_FILE + ' file(s).') |
| parser.add_argument('-p', '--skip-process-files', dest = 'skip_process_files', |
| action = 'store_const', const = True, default = False, |
| help = 'Skip running container process file(s).') |
| parser.add_argument('-f', '--skip-finish-files', dest = 'skip_finish_files', |
| action = 'store_const', const = True, default = False, |
| help = 'Skip running container finish file(s).') |
| parser.add_argument('-o', '--run-only', type=str, choices=["startup","process","finish"], dest = 'run_only', default = None, |
| help = 'Run only this file type and ignore others.') |
| parser.add_argument('-c', '--cmd', metavar=('COMMAND', 'WHEN={startup,process,finish}'), dest = 'cmds', type = str, |
| action = 'append', default = [], nargs = "+", |
| help = 'Run this command before WHEN file(s). Default before startup file(s).') |
| parser.add_argument('-k', '--no-kill-all-on-exit', dest = 'kill_all_on_exit', |
| action = 'store_const', const = False, default = True, |
| help = 'Don\'t kill all processes on the system upon exiting.') |
| parser.add_argument('--wait-state', metavar = 'FILENAME', dest = 'wait_states', type = str, |
| action = 'append', default=[], |
| help = 'Wait until the container state file exists in '+RUN_STATE_DIR+' directory before starting. Usefull when 2 containers share '+RUN_DIR+' directory via volume.') |
| parser.add_argument('--wait-first-startup', dest = 'wait_first_startup', |
| action = 'store_const', const = True, default = False, |
| help = 'Wait until the first startup is done before starting. Usefull when 2 containers share '+RUN_DIR+' directory via volume.') |
| parser.add_argument('--keep-startup-env', dest = 'keep_startup_env', |
| action = 'store_const', const = True, default = False, |
| help = 'Don\'t remove ' + xstr(ENV_FILES_STARTUP_EXTENSIONS) + ' environment files after startup scripts.') |
| parser.add_argument('--copy-service', dest = 'copy_service', |
| action = 'store_const', const = True, default = False, |
| help = 'Copy '+IMPORT_SERVICE_DIR+' to '+RUN_SERVICE_DIR+'. Help to fix docker mounted files problems.') |
| parser.add_argument('--dont-touch-etc-hosts', dest = 'set_env_hostname_to_etc_hosts', |
| action = 'store_const', const = False, default = True, |
| help = 'Don\'t add in /etc/hosts a line with the container ip and $HOSTNAME environment variable value.') |
| parser.add_argument('--keepalive', dest = 'keepalive', |
| action = 'store_const', const = True, default = False, |
| help = 'Keep alive container if all startup files and process exited without error.') |
| parser.add_argument('--keepalive-force', dest = 'keepalive_force', |
| action = 'store_const', const = True, default = False, |
| help = 'Keep alive container in all circonstancies.') |
| parser.add_argument('-l', '--loglevel', type=str, choices=["none","error","warning","info","debug","trace"], dest = 'log_level', default = "info", |
| help = 'Log level (default: info)') |
| |
| args = parser.parse_args() |
| |
| log_level_switcher = {"none": LOG_LEVEL_NONE,"error": LOG_LEVEL_ERROR,"warning": LOG_LEVEL_WARNING,"info": LOG_LEVEL_INFO,"debug": LOG_LEVEL_DEBUG, "trace": LOG_LEVEL_TRACE} |
| log_level_switcher_inv = {LOG_LEVEL_NONE: "none",LOG_LEVEL_ERROR:"error",LOG_LEVEL_WARNING:"warning",LOG_LEVEL_INFO:"info",LOG_LEVEL_DEBUG:"debug",LOG_LEVEL_TRACE:"trace"} |
| log_level = log_level_switcher.get(args.log_level) |
| |
| # Run only arg |
| if args.run_only != None: |
| if args.run_only == "startup" and args.skip_startup_files: |
| error("Error: When '--run-only startup' is set '--skip-startup-files' can't be set.") |
| sys.exit(1) |
| elif args.run_only == "process" and args.skip_startup_files: |
| error("Error: When '--run-only process' is set '--skip-process-files' can't be set.") |
| sys.exit(1) |
| elif args.run_only == "finish" and args.skip_startup_files: |
| error("Error: When '--run-only finish' is set '--skip-finish-files' can't be set.") |
| sys.exit(1) |
| |
| if args.run_only == "startup": |
| args.skip_process_files = True |
| args.skip_finish_files = True |
| elif args.run_only == "process": |
| args.skip_startup_files = True |
| args.skip_finish_files = True |
| elif args.run_only == "finish": |
| args.skip_startup_files = True |
| args.skip_process_files = True |
| |
| # wait for startup args |
| if args.wait_first_startup: |
| args.wait_states.insert(0, 'first-startup-done') |
| |
| # Run main function. |
| signal.signal(signal.SIGTERM, lambda signum, frame: ignore_signals_and_raise_keyboard_interrupt('SIGTERM')) |
| signal.signal(signal.SIGINT, lambda signum, frame: ignore_signals_and_raise_keyboard_interrupt('SIGINT')) |
| signal.signal(signal.SIGALRM, lambda signum, frame: raise_alarm_exception()) |
| |
| exit_code = 0 |
| |
| try: |
| main(args) |
| |
| except SystemExit as err: |
| exit_code = err.code |
| if args.keepalive and err.code == 0: |
| try: |
| info("All process have exited without error, keep container alive...") |
| while True: |
| time.sleep(60) |
| pass |
| except: |
| info("Keep alive process ended.") |
| |
| except KeyboardInterrupt: |
| warning("Init system aborted.") |
| exit(2) |
| |
| finally: |
| |
| run_cmds(args,"finish") |
| |
| # for multiple process images finish script are run by runit |
| if not args.skip_finish_files and not is_multiple_process_container(): |
| run_finish_files() |
| |
| if args.keepalive_force: |
| try: |
| info("All process have exited, keep container alive...") |
| while True: |
| time.sleep(60) |
| pass |
| except: |
| info("Keep alive process ended.") |
| |
| if args.kill_all_on_exit: |
| kill_all_processes(KILL_ALL_PROCESSES_TIMEOUT) |
| |
| exit(exit_code) |