run 30 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936
  1. #!/usr/bin/python3 -u
  2. # -*- coding: utf-8 -*-
  3. import os, os.path, sys, stat, signal, errno, argparse, time, json, re, yaml, ast, socket, shutil, pwd, grp
  4. from datetime import datetime
  5. KILL_PROCESS_TIMEOUT = int(os.environ.get('KILL_PROCESS_TIMEOUT', 30))
  6. KILL_ALL_PROCESSES_TIMEOUT = int(os.environ.get('KILL_ALL_PROCESSES_TIMEOUT', 30))
  7. LOG_LEVEL_NONE = 0
  8. LOG_LEVEL_ERROR = 1
  9. LOG_LEVEL_WARNING = 2
  10. LOG_LEVEL_INFO = 3
  11. LOG_LEVEL_DEBUG = 4
  12. LOG_LEVEL_TRACE = 5
  13. SHENV_NAME_WHITELIST_REGEX = re.compile('\W')
  14. log_level = None
  15. environ_backup = dict(os.environ)
  16. terminated_child_processes = {}
  17. IMPORT_STARTUP_FILENAME="startup.sh"
  18. IMPORT_PROCESS_FILENAME="process.sh"
  19. IMPORT_FINISH_FILENAME="finish.sh"
  20. IMPORT_ENVIRONMENT_DIR="/container/environment"
  21. IMPORT_FIRST_STARTUP_ENVIRONMENT_DIR="/container/environment/startup"
  22. ENV_FILES_YAML_EXTENSIONS = ('.yaml', '.startup.yaml')
  23. ENV_FILES_JSON_EXTENSIONS = ('.json', '.startup.json')
  24. ENV_FILES_STARTUP_EXTENSIONS = ('.startup.yaml', '.startup.json')
  25. IMPORT_SERVICE_DIR="/container/service"
  26. RUN_DIR="/container/run"
  27. RUN_STATE_DIR = RUN_DIR + "/state"
  28. RUN_ENVIRONMENT_DIR = RUN_DIR + "/environment"
  29. RUN_ENVIRONMENT_FILE_EXPORT = RUN_DIR + "/environment.sh"
  30. RUN_STARTUP_DIR = RUN_DIR + "/startup"
  31. RUN_STARTUP_FINAL_FILE = RUN_DIR + "/startup.sh"
  32. RUN_PROCESS_DIR = RUN_DIR + "/process"
  33. RUN_SERVICE_DIR = RUN_DIR + "/service"
  34. ENVIRONMENT_LOG_LEVEL_KEY = 'CONTAINER_LOG_LEVEL'
  35. ENVIRONMENT_SERVICE_DIR_KEY = 'CONTAINER_SERVICE_DIR'
  36. ENVIRONMENT_STATE_DIR_KEY = 'CONTAINER_STATE_DIR'
  37. class AlarmException(Exception):
  38. pass
  39. def write_log(level, message):
  40. now = datetime.now()
  41. for line in message.splitlines():
  42. sys.stderr.write("*** %s | %s | %s\n" % (level, now.strftime("%Y-%m-%d %H:%M:%S"), line))
  43. def error(message):
  44. if log_level >= LOG_LEVEL_ERROR:
  45. write_log(" ERROR ", message)
  46. def warning(message):
  47. if log_level >= LOG_LEVEL_WARNING:
  48. write_log("WARNING", message)
  49. def info(message):
  50. if log_level >= LOG_LEVEL_INFO:
  51. write_log(" INFO ", message)
  52. def debug(message):
  53. if log_level >= LOG_LEVEL_DEBUG:
  54. write_log(" DEBUG ", message)
  55. def trace(message):
  56. if log_level >= LOG_LEVEL_TRACE:
  57. write_log(" TRACE ", message)
  58. def debug_env_dump():
  59. debug("------------ Environment dump ------------")
  60. for name, value in list(os.environ.items()):
  61. debug(name + " = " + value)
  62. debug("------------------------------------------")
  63. def ignore_signals_and_raise_keyboard_interrupt(signame):
  64. signal.signal(signal.SIGTERM, signal.SIG_IGN)
  65. signal.signal(signal.SIGINT, signal.SIG_IGN)
  66. raise KeyboardInterrupt(signame)
  67. def raise_alarm_exception():
  68. raise AlarmException('Alarm')
  69. def listdir(path):
  70. try:
  71. result = os.stat(path)
  72. except OSError:
  73. return []
  74. if stat.S_ISDIR(result.st_mode):
  75. return sorted(os.listdir(path))
  76. else:
  77. return []
  78. def is_exe(path):
  79. try:
  80. return os.path.isfile(path) and os.access(path, os.X_OK)
  81. except OSError:
  82. return False
  83. def xstr(s):
  84. if s is None:
  85. return ''
  86. return str(s)
  87. def set_env_hostname_to_etc_hosts():
  88. try:
  89. if "HOSTNAME" in os.environ:
  90. socket_hostname = socket.gethostname()
  91. if os.environ["HOSTNAME"] != socket_hostname:
  92. ip_address = socket.gethostbyname(socket_hostname)
  93. with open("/etc/hosts", "a") as myfile:
  94. myfile.write(ip_address+" "+os.environ["HOSTNAME"]+"\n")
  95. except:
  96. warning("set_env_hostname_to_etc_hosts: failed at some point...")
  97. def python_dict_to_bash_envvar(name, python_dict):
  98. for value in python_dict:
  99. python_to_bash_envvar(name+"_KEY", value)
  100. python_to_bash_envvar(name+"_VALUE", python_dict.get(value))
  101. values = "#COMPLEX_BASH_ENV:ROW: "+name+"_KEY "+name+"_VALUE"
  102. os.environ[name] = xstr(values)
  103. trace("python2bash : set : " + name + " = "+ os.environ[name])
  104. def python_list_to_bash_envvar(name, python_list):
  105. values="#COMPLEX_BASH_ENV:TABLE:"
  106. i=1
  107. for value in python_list:
  108. child_name = name + "_ROW_" + str(i)
  109. values += " " + child_name
  110. python_to_bash_envvar(child_name, value)
  111. i = i +1
  112. os.environ[name] = xstr(values)
  113. trace("python2bash : set : " + name + " = "+ os.environ[name])
  114. def python_to_bash_envvar(name, value):
  115. try:
  116. value = ast.literal_eval(value)
  117. except:
  118. pass
  119. if isinstance(value, list):
  120. python_list_to_bash_envvar(name,value)
  121. elif isinstance(value, dict):
  122. python_dict_to_bash_envvar(name,value)
  123. else:
  124. os.environ[name] = xstr(value)
  125. trace("python2bash : set : " + name + " = "+ os.environ[name])
  126. def decode_python_envvars():
  127. _environ = dict(os.environ)
  128. for name, value in list(_environ.items()):
  129. if value.startswith("#PYTHON2BASH:") :
  130. value = value.replace("#PYTHON2BASH:","",1)
  131. python_to_bash_envvar(name, value)
  132. def decode_json_envvars():
  133. _environ = dict(os.environ)
  134. for name, value in list(_environ.items()):
  135. if value.startswith("#JSON2BASH:") :
  136. value = value.replace("#JSON2BASH:","",1)
  137. try:
  138. value = json.loads(value)
  139. python_to_bash_envvar(name,value)
  140. except:
  141. os.environ[name] = xstr(value)
  142. warning("failed to parse : " + xstr(value))
  143. trace("set : " + name + " = "+ os.environ[name])
  144. def decode_envvars():
  145. decode_json_envvars()
  146. decode_python_envvars()
  147. def generic_import_envvars(path, override_existing_environment):
  148. if not os.path.exists(path):
  149. trace("generic_import_envvars "+ path+ " don't exists")
  150. return
  151. new_env = {}
  152. for envfile in listdir(path):
  153. filePath = path + os.sep + envfile
  154. if os.path.isfile(filePath) and "." not in envfile:
  155. name = os.path.basename(envfile)
  156. with open(filePath, "r") as f:
  157. # Text files often end with a trailing newline, which we
  158. # don't want to include in the env variable value. See
  159. # https://github.com/phusion/baseimage-docker/pull/49
  160. value = re.sub('\n\Z', '', f.read())
  161. new_env[name] = value
  162. trace("import " + name + " from " + filePath)
  163. for name, value in list(new_env.items()):
  164. if override_existing_environment or name not in os.environ:
  165. os.environ[name] = value
  166. trace("set : " + name + " = "+ os.environ[name])
  167. else:
  168. debug("ignore : " + name + " = " + xstr(value) + " (keep " + name + " = " + os.environ[name] + " )")
  169. def import_run_envvars():
  170. clear_environ()
  171. generic_import_envvars(RUN_ENVIRONMENT_DIR, True)
  172. def import_envvars():
  173. generic_import_envvars(IMPORT_ENVIRONMENT_DIR, False)
  174. generic_import_envvars(IMPORT_FIRST_STARTUP_ENVIRONMENT_DIR, False)
  175. def export_run_envvars(to_dir = True):
  176. if to_dir and not os.path.exists(RUN_ENVIRONMENT_DIR):
  177. warning("export_run_envvars: "+RUN_ENVIRONMENT_DIR+" don't exists")
  178. return
  179. shell_dump = ""
  180. for name, value in list(os.environ.items()):
  181. if name in ['USER', 'GROUP', 'UID', 'GID', 'SHELL']:
  182. continue
  183. if to_dir:
  184. with open(RUN_ENVIRONMENT_DIR + os.sep + name, "w") as f:
  185. f.write(value)
  186. trace("export " + name + " to " + RUN_ENVIRONMENT_DIR + os.sep + name)
  187. shell_dump += "export " + sanitize_shenvname(name) + "=" + shquote(value) + "\n"
  188. with open(RUN_ENVIRONMENT_FILE_EXPORT, "w") as f:
  189. f.write(shell_dump)
  190. trace("export "+RUN_ENVIRONMENT_FILE_EXPORT)
  191. def create_run_envvars():
  192. set_dir_env()
  193. set_log_level_env()
  194. import_envvars()
  195. import_env_files()
  196. decode_envvars()
  197. export_run_envvars()
  198. def clear_run_envvars():
  199. try:
  200. shutil.rmtree(RUN_ENVIRONMENT_DIR)
  201. os.makedirs(RUN_ENVIRONMENT_DIR)
  202. os.chmod(RUN_ENVIRONMENT_DIR, 700)
  203. except:
  204. warning("clear_run_envvars: failed at some point...")
  205. def print_env_files_order(file_extensions):
  206. if not os.path.exists(IMPORT_ENVIRONMENT_DIR):
  207. warning("print_env_files_order "+IMPORT_ENVIRONMENT_DIR+" don't exists")
  208. return
  209. to_print = 'Caution: previously defined variables will not be overriden.\n'
  210. file_found = False
  211. for subdir, _, files in sorted(os.walk(IMPORT_ENVIRONMENT_DIR)):
  212. for file in files:
  213. filepath = subdir + os.sep + file
  214. if filepath.endswith(file_extensions):
  215. file_found = True
  216. filepath = subdir + os.sep + file
  217. to_print += filepath + '\n'
  218. if file_found:
  219. if log_level < LOG_LEVEL_DEBUG:
  220. to_print+='\nTo see how this files are processed and environment variables values,\n'
  221. to_print+='run this container with \'--loglevel debug\''
  222. info('Environment files will be proccessed in this order : \n' + to_print)
  223. def import_env_files():
  224. if not os.path.exists(IMPORT_ENVIRONMENT_DIR):
  225. warning("import_env_files: "+IMPORT_ENVIRONMENT_DIR+" don't exists")
  226. return
  227. file_extensions = ENV_FILES_YAML_EXTENSIONS + ENV_FILES_JSON_EXTENSIONS
  228. print_env_files_order(file_extensions)
  229. for subdir, _, files in sorted(os.walk(IMPORT_ENVIRONMENT_DIR)):
  230. for file in files:
  231. if file.endswith(file_extensions):
  232. filepath = subdir + os.sep + file
  233. try:
  234. with open(filepath, "r") as f:
  235. debug("process environment file : " + filepath)
  236. if file.endswith(ENV_FILES_YAML_EXTENSIONS):
  237. env_vars = yaml.load(f)
  238. elif file.endswith(ENV_FILES_JSON_EXTENSIONS):
  239. env_vars = json.load(f)
  240. for name, value in list(env_vars.items()):
  241. if not name in os.environ:
  242. if isinstance(value, list) or isinstance(value, dict):
  243. os.environ[name] = '#PYTHON2BASH:' + xstr(value)
  244. else:
  245. os.environ[name] = xstr(value)
  246. trace("set : " + name + " = "+ os.environ[name])
  247. else:
  248. debug("ignore : " + name + " = " + xstr(value) + " (keep " + name + " = " + os.environ[name] + " )")
  249. except:
  250. warning('failed to parse: ' + filepath)
  251. def remove_startup_env_files():
  252. if os.path.isdir(IMPORT_FIRST_STARTUP_ENVIRONMENT_DIR):
  253. try:
  254. shutil.rmtree(IMPORT_FIRST_STARTUP_ENVIRONMENT_DIR)
  255. except:
  256. warning("remove_startup_env_files: failed to remove "+IMPORT_FIRST_STARTUP_ENVIRONMENT_DIR)
  257. if not os.path.exists(IMPORT_ENVIRONMENT_DIR):
  258. warning("remove_startup_env_files: "+IMPORT_ENVIRONMENT_DIR+" don't exists")
  259. return
  260. for subdir, _, files in sorted(os.walk(IMPORT_ENVIRONMENT_DIR)):
  261. for file in files:
  262. filepath = subdir + os.sep + file
  263. if filepath.endswith(ENV_FILES_STARTUP_EXTENSIONS):
  264. try:
  265. os.remove(filepath)
  266. info("Remove file "+filepath)
  267. except:
  268. warning("remove_startup_env_files: failed to remove "+filepath)
  269. def restore_environ():
  270. clear_environ()
  271. trace("Restore initial environment")
  272. os.environ.update(environ_backup)
  273. def clear_environ():
  274. trace("Clear existing environment")
  275. os.environ.clear()
  276. def set_startup_scripts_env():
  277. debug("Set environment for startup files")
  278. clear_run_envvars() # clear previous environment
  279. create_run_envvars() # create run envvars with all env files
  280. def set_process_env(keep_startup_env = False):
  281. debug("Set environment for container process")
  282. if not keep_startup_env:
  283. remove_startup_env_files()
  284. clear_run_envvars()
  285. restore_environ()
  286. create_run_envvars() # recreate env var without startup env files
  287. def setup_run_directories(args):
  288. directories = (RUN_PROCESS_DIR, RUN_STARTUP_DIR, RUN_STATE_DIR, RUN_ENVIRONMENT_DIR)
  289. for directory in directories:
  290. if not os.path.exists(directory):
  291. os.makedirs(directory)
  292. if directory == RUN_ENVIRONMENT_DIR:
  293. os.chmod(directory, 700)
  294. if not os.path.exists(RUN_ENVIRONMENT_FILE_EXPORT):
  295. open(RUN_ENVIRONMENT_FILE_EXPORT, 'a').close()
  296. os.chmod(RUN_ENVIRONMENT_FILE_EXPORT, 640)
  297. uid = pwd.getpwnam("root").pw_uid
  298. gid = grp.getgrnam("docker_env").gr_gid
  299. os.chown(RUN_ENVIRONMENT_FILE_EXPORT, uid, gid)
  300. if state_is_first_start():
  301. if args.copy_service:
  302. copy_service_to_run_dir()
  303. set_dir_env()
  304. base_path = os.environ[ENVIRONMENT_SERVICE_DIR_KEY]
  305. nb_service = len(listdir(base_path))
  306. if nb_service > 0 :
  307. info("Search service in " + ENVIRONMENT_SERVICE_DIR_KEY + " = "+base_path+" :")
  308. for d in listdir(base_path):
  309. d_path = base_path + os.sep + d
  310. if os.path.isdir(d_path):
  311. if is_exe(d_path + os.sep + IMPORT_STARTUP_FILENAME):
  312. info('link ' + d_path + os.sep + IMPORT_STARTUP_FILENAME + ' to ' + RUN_STARTUP_DIR + os.sep + d)
  313. try:
  314. os.symlink(d_path + os.sep + IMPORT_STARTUP_FILENAME, RUN_STARTUP_DIR + os.sep + d)
  315. except OSError as detail:
  316. warning('failed to link ' + d_path + os.sep + IMPORT_STARTUP_FILENAME + ' to ' + RUN_STARTUP_DIR + os.sep + d + ': ' + xstr(detail))
  317. if is_exe(d_path + os.sep + IMPORT_PROCESS_FILENAME):
  318. info('link ' + d_path + os.sep + IMPORT_PROCESS_FILENAME + ' to ' + RUN_PROCESS_DIR + os.sep + d + os.sep + 'run')
  319. if not os.path.exists(RUN_PROCESS_DIR + os.sep + d):
  320. os.makedirs(RUN_PROCESS_DIR + os.sep + d)
  321. else:
  322. warning('directory ' + RUN_PROCESS_DIR + os.sep + d + ' already exists')
  323. try:
  324. os.symlink(d_path + os.sep + IMPORT_PROCESS_FILENAME, RUN_PROCESS_DIR + os.sep + d + os.sep + 'run')
  325. except OSError as detail:
  326. warning('failed to link ' + d_path + os.sep + IMPORT_PROCESS_FILENAME + ' to ' + RUN_PROCESS_DIR + os.sep + d + os.sep + 'run : ' + xstr(detail))
  327. if not args.skip_finish_files and is_exe(d_path + os.sep + IMPORT_FINISH_FILENAME):
  328. info('link ' + d_path + os.sep + IMPORT_FINISH_FILENAME + ' to ' + RUN_PROCESS_DIR + os.sep + d + os.sep + 'finish')
  329. if not os.path.exists(RUN_PROCESS_DIR + os.sep + d):
  330. os.makedirs(RUN_PROCESS_DIR + os.sep + d)
  331. try:
  332. os.symlink(d_path + os.sep + IMPORT_FINISH_FILENAME, RUN_PROCESS_DIR + os.sep + d + os.sep + 'finish')
  333. except OSError as detail:
  334. warning('failed to link ' + d_path + os.sep + IMPORT_FINISH_FILENAME + ' to ' + RUN_PROCESS_DIR + os.sep + d + os.sep + 'finish : ' + xstr(detail))
  335. def set_dir_env():
  336. if state_is_service_copied_to_run_dir():
  337. os.environ[ENVIRONMENT_SERVICE_DIR_KEY] = RUN_SERVICE_DIR
  338. else:
  339. os.environ[ENVIRONMENT_SERVICE_DIR_KEY] = IMPORT_SERVICE_DIR
  340. trace("set : " + ENVIRONMENT_SERVICE_DIR_KEY + " = " + os.environ[ENVIRONMENT_SERVICE_DIR_KEY])
  341. os.environ[ENVIRONMENT_STATE_DIR_KEY] = RUN_STATE_DIR
  342. trace("set : " + ENVIRONMENT_STATE_DIR_KEY + " = " + os.environ[ENVIRONMENT_STATE_DIR_KEY])
  343. def set_log_level_env():
  344. os.environ[ENVIRONMENT_LOG_LEVEL_KEY] = xstr(log_level)
  345. trace("set : "+ENVIRONMENT_LOG_LEVEL_KEY+" = " + os.environ[ENVIRONMENT_LOG_LEVEL_KEY])
  346. def copy_service_to_run_dir():
  347. if os.path.exists(RUN_SERVICE_DIR):
  348. warning("Copy "+IMPORT_SERVICE_DIR+" to "+RUN_SERVICE_DIR + " ignored")
  349. warning(RUN_SERVICE_DIR + " already exists")
  350. return
  351. info("Copy "+IMPORT_SERVICE_DIR+" to "+RUN_SERVICE_DIR)
  352. try:
  353. shutil.copytree(IMPORT_SERVICE_DIR, RUN_SERVICE_DIR)
  354. except shutil.Error as e:
  355. warning(e)
  356. state_set_service_copied_to_run_dir()
  357. def state_set_service_copied_to_run_dir():
  358. open(RUN_STATE_DIR+"/service-copied-to-run-dir", 'a').close()
  359. def state_is_service_copied_to_run_dir():
  360. return os.path.exists(RUN_STATE_DIR+'/service-copied-to-run-dir')
  361. def state_set_first_startup_done():
  362. open(RUN_STATE_DIR+"/first-startup-done", 'a').close()
  363. def state_is_first_start():
  364. return os.path.exists(RUN_STATE_DIR+'/first-startup-done') == False
  365. def state_set_startup_done():
  366. open(RUN_STATE_DIR+"/startup-done", 'a').close()
  367. def state_reset_startup_done():
  368. try:
  369. os.remove(RUN_STATE_DIR+"/startup-done")
  370. except OSError:
  371. pass
  372. def is_multiple_process_container():
  373. return len(listdir(RUN_PROCESS_DIR)) > 1
  374. def is_single_process_container():
  375. return len(listdir(RUN_PROCESS_DIR)) == 1
  376. def get_container_process():
  377. for p in listdir(RUN_PROCESS_DIR):
  378. return RUN_PROCESS_DIR + os.sep + p + os.sep + 'run'
  379. def is_runit_installed():
  380. return os.path.exists('/sbin/sv')
  381. _find_unsafe = re.compile(r'[^\w@%+=:,./-]').search
  382. def shquote(s):
  383. """Return a shell-escaped version of the string *s*."""
  384. if not s:
  385. return "''"
  386. if _find_unsafe(s) is None:
  387. return s
  388. # use single quotes, and put single quotes into double quotes
  389. # the string $'b is then quoted as '$'"'"'b'
  390. return "'" + s.replace("'", "'\"'\"'") + "'"
  391. def sanitize_shenvname(s):
  392. return re.sub(SHENV_NAME_WHITELIST_REGEX, "_", s)
  393. # Waits for the child process with the given PID, while at the same time
  394. # reaping any other child processes that have exited (e.g. adopted child
  395. # processes that have terminated).
  396. def waitpid_reap_other_children(pid):
  397. global terminated_child_processes
  398. status = terminated_child_processes.get(pid)
  399. if status:
  400. # A previous call to waitpid_reap_other_children(),
  401. # with an argument not equal to the current argument,
  402. # already waited for this process. Return the status
  403. # that was obtained back then.
  404. del terminated_child_processes[pid]
  405. return status
  406. done = False
  407. status = None
  408. while not done:
  409. try:
  410. # https://github.com/phusion/baseimage-docker/issues/151#issuecomment-92660569
  411. this_pid, status = os.waitpid(pid, os.WNOHANG)
  412. if this_pid == 0:
  413. this_pid, status = os.waitpid(-1, 0)
  414. if this_pid == pid:
  415. done = True
  416. else:
  417. # Save status for later.
  418. terminated_child_processes[this_pid] = status
  419. except OSError as e:
  420. if e.errno == errno.ECHILD or e.errno == errno.ESRCH:
  421. return None
  422. else:
  423. raise
  424. return status
  425. def stop_child_process(name, pid, signo = signal.SIGTERM, time_limit = KILL_PROCESS_TIMEOUT):
  426. info("Shutting down %s (PID %d)..." % (name, pid))
  427. try:
  428. os.kill(pid, signo)
  429. except OSError:
  430. pass
  431. signal.alarm(time_limit)
  432. try:
  433. try:
  434. waitpid_reap_other_children(pid)
  435. except OSError:
  436. pass
  437. except AlarmException:
  438. warning("%s (PID %d) did not shut down in time. Forcing it to exit." % (name, pid))
  439. try:
  440. os.kill(pid, signal.SIGKILL)
  441. except OSError:
  442. pass
  443. try:
  444. waitpid_reap_other_children(pid)
  445. except OSError:
  446. pass
  447. finally:
  448. signal.alarm(0)
  449. def run_command_killable(command):
  450. status = None
  451. debug_env_dump()
  452. pid = os.spawnvp(os.P_NOWAIT, command[0], command)
  453. try:
  454. status = waitpid_reap_other_children(pid)
  455. except BaseException:
  456. warning("An error occurred. Aborting.")
  457. stop_child_process(command[0], pid)
  458. raise
  459. if status != 0:
  460. if status is None:
  461. error("%s exited with unknown status\n" % command[0])
  462. else:
  463. error("%s failed with status %d\n" % (command[0], os.WEXITSTATUS(status)))
  464. sys.exit(1)
  465. def run_command_killable_and_import_run_envvars(command):
  466. run_command_killable(command)
  467. import_run_envvars()
  468. export_run_envvars(False)
  469. def kill_all_processes(time_limit):
  470. info("Killing all processes...")
  471. try:
  472. os.kill(-1, signal.SIGTERM)
  473. except OSError:
  474. pass
  475. signal.alarm(time_limit)
  476. try:
  477. # Wait until no more child processes exist.
  478. done = False
  479. while not done:
  480. try:
  481. os.waitpid(-1, 0)
  482. except OSError as e:
  483. if e.errno == errno.ECHILD:
  484. done = True
  485. else:
  486. raise
  487. except AlarmException:
  488. warning("Not all processes have exited in time. Forcing them to exit.")
  489. try:
  490. os.kill(-1, signal.SIGKILL)
  491. except OSError:
  492. pass
  493. finally:
  494. signal.alarm(0)
  495. def container_had_startup_script():
  496. return (len(listdir(RUN_STARTUP_DIR)) > 0 or is_exe(RUN_STARTUP_FINAL_FILE))
  497. def run_startup_files(args):
  498. # Run /container/run/startup/*
  499. for name in listdir(RUN_STARTUP_DIR):
  500. filename = RUN_STARTUP_DIR + os.sep + name
  501. if is_exe(filename):
  502. info("Running %s..." % filename)
  503. run_command_killable_and_import_run_envvars([filename])
  504. # Run /container/run/startup.sh.
  505. if is_exe(RUN_STARTUP_FINAL_FILE):
  506. info("Running "+RUN_STARTUP_FINAL_FILE+"...")
  507. run_command_killable_and_import_run_envvars([RUN_STARTUP_FINAL_FILE])
  508. def wait_for_process_or_interrupt(pid):
  509. status = waitpid_reap_other_children(pid)
  510. return (True, status)
  511. def run_process(args, background_process_name, background_process_command):
  512. background_process_pid = run_background_process(background_process_name,background_process_command)
  513. background_process_exited = False
  514. exit_status = None
  515. if len(args.main_command) == 0:
  516. background_process_exited, exit_status = wait_background_process(background_process_name, background_process_pid)
  517. else:
  518. exit_status = run_foreground_process(args.main_command)
  519. return background_process_pid, background_process_exited, exit_status
  520. def run_background_process(name, command):
  521. info("Running "+ name +"...")
  522. pid = os.spawnvp(os.P_NOWAIT, command[0], command)
  523. debug("%s started as PID %d" % (name, pid))
  524. return pid
  525. def wait_background_process(name, pid):
  526. exit_code = None
  527. exit_status = None
  528. process_exited = False
  529. process_exited, exit_code = wait_for_process_or_interrupt(pid)
  530. if process_exited:
  531. if exit_code is None:
  532. info(name + " exited with unknown status")
  533. exit_status = 1
  534. else:
  535. exit_status = os.WEXITSTATUS(exit_code)
  536. info("%s exited with status %d" % (name, exit_status))
  537. return (process_exited, exit_status)
  538. def run_foreground_process(command):
  539. exit_code = None
  540. exit_status = None
  541. info("Running %s..." % " ".join(command))
  542. pid = os.spawnvp(os.P_NOWAIT, command[0], command)
  543. try:
  544. exit_code = waitpid_reap_other_children(pid)
  545. if exit_code is None:
  546. info("%s exited with unknown status." % command[0])
  547. exit_status = 1
  548. else:
  549. exit_status = os.WEXITSTATUS(exit_code)
  550. info("%s exited with status %d." % (command[0], exit_status))
  551. except KeyboardInterrupt:
  552. stop_child_process(command[0], pid)
  553. raise
  554. except BaseException:
  555. error("An error occurred. Aborting.")
  556. stop_child_process(command[0], pid)
  557. raise
  558. return exit_status
  559. def shutdown_runit_services():
  560. debug("Begin shutting down runit services...")
  561. os.system("/sbin/sv -w %d force-stop %s/* > /dev/null" % (KILL_PROCESS_TIMEOUT, RUN_PROCESS_DIR))
  562. def wait_for_runit_services():
  563. debug("Waiting for runit services to exit...")
  564. done = False
  565. while not done:
  566. done = os.system("/sbin/sv status "+RUN_PROCESS_DIR+"/* | grep -q '^run:'") != 0
  567. if not done:
  568. time.sleep(0.1)
  569. shutdown_runit_services()
  570. def run_multiple_process_container(args):
  571. if not is_runit_installed():
  572. error("Error: runit is not installed and this is a multiple process container.")
  573. return
  574. background_process_exited=False
  575. background_process_pid=None
  576. try:
  577. runit_command=["/sbin/runsvdir", "-P", RUN_PROCESS_DIR]
  578. background_process_pid, background_process_exited, exit_status = run_process(args, "runit daemon", runit_command)
  579. sys.exit(exit_status)
  580. finally:
  581. shutdown_runit_services()
  582. if not background_process_exited:
  583. stop_child_process("runit daemon", background_process_pid)
  584. wait_for_runit_services()
  585. def run_single_process_container(args):
  586. background_process_exited=False
  587. background_process_pid=None
  588. try:
  589. container_process=get_container_process()
  590. background_process_pid, background_process_exited, exit_status = run_process(args, container_process, [container_process])
  591. sys.exit(exit_status)
  592. finally:
  593. if not background_process_exited:
  594. stop_child_process(container_process, background_process_pid)
  595. def run_no_process_container(args):
  596. if len(args.main_command) == 0:
  597. args.main_command=['bash'] # run bash by default
  598. exit_status = run_foreground_process(args.main_command)
  599. sys.exit(exit_status)
  600. def run_finish_files():
  601. # iterate process dir to find finish files
  602. for name in listdir(RUN_PROCESS_DIR):
  603. filename = RUN_PROCESS_DIR + os.sep + name + os.sep + "finish"
  604. if is_exe(filename):
  605. info("Running %s..." % filename)
  606. run_command_killable_and_import_run_envvars([filename])
  607. def wait_states(states):
  608. for state in states:
  609. filename = RUN_STATE_DIR + os.sep + state
  610. info("Wait state: " + state)
  611. while not os.path.exists(filename):
  612. time.sleep(0.1)
  613. debug("Check file " + filename)
  614. pass
  615. debug("Check file " + filename + " [Ok]")
  616. def run_cmds(args, when):
  617. debug("Run commands before " + when + "...")
  618. if len(args.cmds) > 0:
  619. for cmd in args.cmds:
  620. if (len(cmd) > 1 and cmd[1] == when) or (len(cmd) == 1 and when == "startup"):
  621. info("Running '"+cmd[0]+"'...")
  622. run_command_killable_and_import_run_envvars(cmd[0].split())
  623. def main(args):
  624. info(ENVIRONMENT_LOG_LEVEL_KEY + " = " + xstr(log_level) + " (" + log_level_switcher_inv.get(log_level) + ")")
  625. state_reset_startup_done()
  626. if args.set_env_hostname_to_etc_hosts:
  627. set_env_hostname_to_etc_hosts()
  628. wait_states(args.wait_states)
  629. setup_run_directories(args)
  630. if not args.skip_env_files:
  631. set_startup_scripts_env()
  632. run_cmds(args,"startup")
  633. if not args.skip_startup_files and container_had_startup_script():
  634. run_startup_files(args)
  635. state_set_startup_done()
  636. state_set_first_startup_done()
  637. if not args.skip_env_files:
  638. set_process_env(args.keep_startup_env)
  639. run_cmds(args,"process")
  640. debug_env_dump()
  641. if is_single_process_container() and not args.skip_process_files:
  642. run_single_process_container(args)
  643. elif is_multiple_process_container() and not args.skip_process_files:
  644. run_multiple_process_container(args)
  645. else:
  646. run_no_process_container(args)
  647. # Parse options.
  648. parser = argparse.ArgumentParser(description = 'Initialize the system.', epilog='Osixia! Light Baseimage: https://github.com/osixia/docker-light-baseimage')
  649. parser.add_argument('main_command', metavar = 'MAIN_COMMAND', type = str, nargs = '*',
  650. help = 'The main command to run, leave empty to only run container process.')
  651. parser.add_argument('-e', '--skip-env-files', dest = 'skip_env_files',
  652. action = 'store_const', const = True, default = False,
  653. help = 'Skip getting environment values from environment file(s).')
  654. parser.add_argument('-s', '--skip-startup-files', dest = 'skip_startup_files',
  655. action = 'store_const', const = True, default = False,
  656. help = 'Skip running '+RUN_STARTUP_DIR+'/* and '+RUN_STARTUP_FINAL_FILE + ' file(s).')
  657. parser.add_argument('-p', '--skip-process-files', dest = 'skip_process_files',
  658. action = 'store_const', const = True, default = False,
  659. help = 'Skip running container process file(s).')
  660. parser.add_argument('-f', '--skip-finish-files', dest = 'skip_finish_files',
  661. action = 'store_const', const = True, default = False,
  662. help = 'Skip running container finish file(s).')
  663. parser.add_argument('-o', '--run-only', type=str, choices=["startup","process","finish"], dest = 'run_only', default = None,
  664. help = 'Run only this file type and ignore others.')
  665. parser.add_argument('-c', '--cmd', metavar=('COMMAND', 'WHEN={startup,process,finish}'), dest = 'cmds', type = str,
  666. action = 'append', default = [], nargs = "+",
  667. help = 'Run COMMAND before WHEN file(s). Default before startup file(s).')
  668. parser.add_argument('-k', '--no-kill-all-on-exit', dest = 'kill_all_on_exit',
  669. action = 'store_const', const = False, default = True,
  670. help = 'Don\'t kill all processes on the system upon exiting.')
  671. parser.add_argument('--wait-state', metavar = 'FILENAME', dest = 'wait_states', type = str,
  672. action = 'append', default=[],
  673. help = 'Wait until the container FILENAME file exists in '+RUN_STATE_DIR+' directory before starting. Usefull when 2 containers share '+RUN_DIR+' directory via volume.')
  674. parser.add_argument('--wait-first-startup', dest = 'wait_first_startup',
  675. action = 'store_const', const = True, default = False,
  676. help = 'Wait until the first startup is done before starting. Usefull when 2 containers share '+RUN_DIR+' directory via volume.')
  677. parser.add_argument('--keep-startup-env', dest = 'keep_startup_env',
  678. action = 'store_const', const = True, default = False,
  679. help = 'Don\'t remove ' + xstr(ENV_FILES_STARTUP_EXTENSIONS) + ' environment files after startup scripts.')
  680. parser.add_argument('--copy-service', dest = 'copy_service',
  681. action = 'store_const', const = True, default = False,
  682. help = 'Copy '+IMPORT_SERVICE_DIR+' to '+RUN_SERVICE_DIR+'. Help to fix docker mounted files problems.')
  683. parser.add_argument('--dont-touch-etc-hosts', dest = 'set_env_hostname_to_etc_hosts',
  684. action = 'store_const', const = False, default = True,
  685. help = 'Don\'t add in /etc/hosts a line with the container ip and $HOSTNAME environment variable value.')
  686. parser.add_argument('--keepalive', dest = 'keepalive',
  687. action = 'store_const', const = True, default = False,
  688. help = 'Keep alive container if all startup files and process exited without error.')
  689. parser.add_argument('--keepalive-force', dest = 'keepalive_force',
  690. action = 'store_const', const = True, default = False,
  691. help = 'Keep alive container in all circonstancies.')
  692. parser.add_argument('-l', '--loglevel', type=str, choices=["none","error","warning","info","debug","trace"], dest = 'log_level', default = "info",
  693. help = 'Log level (default: info)')
  694. args = parser.parse_args()
  695. 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}
  696. 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"}
  697. log_level = log_level_switcher.get(args.log_level)
  698. # Run only arg
  699. if args.run_only != None:
  700. if args.run_only == "startup" and args.skip_startup_files:
  701. error("Error: When '--run-only startup' is set '--skip-startup-files' can't be set.")
  702. sys.exit(1)
  703. elif args.run_only == "process" and args.skip_startup_files:
  704. error("Error: When '--run-only process' is set '--skip-process-files' can't be set.")
  705. sys.exit(1)
  706. elif args.run_only == "finish" and args.skip_startup_files:
  707. error("Error: When '--run-only finish' is set '--skip-finish-files' can't be set.")
  708. sys.exit(1)
  709. if args.run_only == "startup":
  710. args.skip_process_files = True
  711. args.skip_finish_files = True
  712. elif args.run_only == "process":
  713. args.skip_startup_files = True
  714. args.skip_finish_files = True
  715. elif args.run_only == "finish":
  716. args.skip_startup_files = True
  717. args.skip_process_files = True
  718. # wait for startup args
  719. if args.wait_first_startup:
  720. args.wait_states.insert(0, 'first-startup-done')
  721. # Run main function.
  722. signal.signal(signal.SIGTERM, lambda signum, frame: ignore_signals_and_raise_keyboard_interrupt('SIGTERM'))
  723. signal.signal(signal.SIGINT, lambda signum, frame: ignore_signals_and_raise_keyboard_interrupt('SIGINT'))
  724. signal.signal(signal.SIGALRM, lambda signum, frame: raise_alarm_exception())
  725. exit_code = 0
  726. try:
  727. main(args)
  728. except SystemExit as err:
  729. exit_code = err.code
  730. if args.keepalive and err.code == 0:
  731. try:
  732. info("All process have exited without error, keep container alive...")
  733. while True:
  734. time.sleep(60)
  735. pass
  736. except:
  737. error("Keep alive process ended.")
  738. except KeyboardInterrupt:
  739. warning("Init system aborted.")
  740. exit(2)
  741. finally:
  742. run_cmds(args,"finish")
  743. # for multiple process images finish script are run by runit
  744. if not args.skip_finish_files and not is_multiple_process_container():
  745. run_finish_files()
  746. if args.keepalive_force:
  747. try:
  748. info("All process have exited, keep container alive...")
  749. while True:
  750. time.sleep(60)
  751. pass
  752. except:
  753. error("Keep alive process ended.")
  754. if args.kill_all_on_exit:
  755. kill_all_processes(KILL_ALL_PROCESSES_TIMEOUT)
  756. exit(exit_code)