docker_helper.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376
  1. import os
  2. import socket
  3. import fnmatch
  4. import subprocess
  5. import multiprocessing
  6. import json
  7. import docker
  8. import time
  9. import re
  10. import traceback
  11. from threading import Thread
  12. from colorama import Fore, Style
  13. from toolset.utils.output_helper import log, FNULL
  14. from toolset.utils.metadata_helper import gather_tests
  15. from toolset.utils.ordered_set import OrderedSet
  16. from toolset.utils.database_helper import test_database
  17. def clean(config):
  18. '''
  19. Cleans all the docker images from the system
  20. '''
  21. # Clean the app server images
  22. subprocess.check_call(["docker", "image", "prune", "-f"])
  23. docker_ids = subprocess.check_output(["docker", "images",
  24. "-q"]).splitlines()
  25. for docker_id in docker_ids:
  26. subprocess.check_call(["docker", "image", "rmi", "-f", docker_id])
  27. subprocess.check_call(["docker", "system", "prune", "-a", "-f"])
  28. # Clean the database server images
  29. command = list(config.database_ssh_command)
  30. command.extend(["docker", "image", "prune", "-f"])
  31. subprocess.check_call(command)
  32. command = list(config.database_ssh_command)
  33. command.extend(["docker", "images", "-q"])
  34. docker_ids = subprocess.check_output(command).splitlines()
  35. for docker_id in docker_ids:
  36. command = list(config.database_ssh_command)
  37. command.extend(["docker", "image", "rmi", "-f", docker_id])
  38. subprocess.check_call(command)
  39. command = list(config.database_ssh_command)
  40. command.extend(["docker", "system", "prune", "-a", "-f"])
  41. subprocess.check_call(command)
  42. def build(benchmarker_config, test_names, build_log_dir=os.devnull):
  43. '''
  44. Builds the dependency chain as well as the test implementation docker images
  45. for the given tests.
  46. '''
  47. tests = gather_tests(test_names)
  48. for test in tests:
  49. log_prefix = "%s: " % test.name
  50. docker_buildargs = {
  51. 'CPU_COUNT': str(multiprocessing.cpu_count()),
  52. 'MAX_CONCURRENCY': str(max(benchmarker_config.concurrency_levels)),
  53. 'TFB_DATABASE': str(benchmarker_config.database_host)
  54. }
  55. test_docker_files = ["%s.dockerfile" % test.name]
  56. if test.docker_files is not None:
  57. if type(test.docker_files) is list:
  58. test_docker_files.extend(test.docker_files)
  59. else:
  60. raise Exception(
  61. "docker_files in benchmark_config.json must be an array")
  62. for test_docker_file in test_docker_files:
  63. deps = OrderedSet(
  64. list(
  65. reversed(
  66. __gather_dependencies(
  67. os.path.join(test.directory, test_docker_file)))))
  68. docker_dir = os.path.join(
  69. os.getenv('FWROOT'), "toolset", "setup", "docker")
  70. for dependency in deps:
  71. build_log_file = build_log_dir
  72. if build_log_dir is not os.devnull:
  73. build_log_file = os.path.join(
  74. build_log_dir, "%s.log" % dependency.lower())
  75. with open(build_log_file, 'w') as build_log:
  76. docker_file = os.path.join(test.directory,
  77. dependency + ".dockerfile")
  78. if not docker_file or not os.path.exists(docker_file):
  79. docker_file = find(docker_dir,
  80. dependency + ".dockerfile")
  81. if not docker_file:
  82. log("Docker build failed; %s could not be found; terminating"
  83. % (dependency + ".dockerfile"),
  84. prefix=log_prefix, file=build_log, color=Fore.RED)
  85. return 1
  86. # Build the dependency image
  87. try:
  88. for line in docker.APIClient(
  89. base_url='unix://var/run/docker.sock').build(
  90. path=os.path.dirname(docker_file),
  91. dockerfile="%s.dockerfile" % dependency,
  92. tag="tfb/%s" % dependency,
  93. buildargs=docker_buildargs,
  94. forcerm=True):
  95. if line.startswith('{"stream":'):
  96. line = json.loads(line)
  97. line = line[line.keys()[0]].encode('utf-8')
  98. log(line,
  99. prefix=log_prefix,
  100. file=build_log,
  101. color=Fore.WHITE + Style.BRIGHT \
  102. if re.match(r'^Step \d+\/\d+', line) else '')
  103. except Exception:
  104. tb = traceback.format_exc()
  105. log("Docker dependency build failed; terminating",
  106. prefix=log_prefix, file=build_log, color=Fore.RED)
  107. log(tb, prefix=log_prefix, file=build_log)
  108. return 1
  109. # Build the test images
  110. for test_docker_file in test_docker_files:
  111. build_log_file = build_log_dir
  112. if build_log_dir is not os.devnull:
  113. build_log_file = os.path.join(
  114. build_log_dir, "%s.log" % test_docker_file.replace(
  115. ".dockerfile", "").lower())
  116. with open(build_log_file, 'w') as build_log:
  117. try:
  118. for line in docker.APIClient(
  119. base_url='unix://var/run/docker.sock').build(
  120. path=test.directory,
  121. dockerfile=test_docker_file,
  122. tag="tfb/test/%s" % test_docker_file.replace(
  123. ".dockerfile", ""),
  124. buildargs=docker_buildargs,
  125. forcerm=True):
  126. if line.startswith('{"stream":'):
  127. line = json.loads(line)
  128. line = line[line.keys()[0]].encode('utf-8')
  129. log(line,
  130. prefix=log_prefix,
  131. file=build_log,
  132. color=Fore.WHITE + Style.BRIGHT \
  133. if re.match(r'^Step \d+\/\d+', line) else '')
  134. except Exception:
  135. tb = traceback.format_exc()
  136. log("Docker build failed; terminating",
  137. prefix=log_prefix, file=build_log, color=Fore.RED)
  138. log(tb, prefix=log_prefix, file=build_log)
  139. return 1
  140. return 0
  141. def run(benchmarker_config, docker_files, run_log_dir):
  142. '''
  143. Run the given Docker container(s)
  144. '''
  145. client = docker.from_env()
  146. for docker_file in docker_files:
  147. log_prefix = "%s: " % docker_file.replace(".dockerfile", "")
  148. try:
  149. def watch_container(container, docker_file):
  150. with open(
  151. os.path.join(
  152. run_log_dir, "%s.log" % docker_file.replace(
  153. ".dockerfile", "").lower()), 'w') as run_log:
  154. for line in container.logs(stream=True):
  155. log(line, prefix=log_prefix, file=run_log)
  156. extra_hosts = {
  157. socket.gethostname(): str(benchmarker_config.server_host),
  158. 'TFB-SERVER': str(benchmarker_config.server_host),
  159. 'TFB-DATABASE': str(benchmarker_config.database_host),
  160. 'TFB-CLIENT': str(benchmarker_config.client_host)
  161. }
  162. container = client.containers.run(
  163. "tfb/test/%s" % docker_file.replace(".dockerfile", ""),
  164. network_mode="host",
  165. privileged=True,
  166. stderr=True,
  167. detach=True,
  168. init=True,
  169. extra_hosts=extra_hosts)
  170. watch_thread = Thread(
  171. target=watch_container, args=(
  172. container,
  173. docker_file,
  174. ))
  175. watch_thread.daemon = True
  176. watch_thread.start()
  177. except Exception:
  178. with open(
  179. os.path.join(run_log_dir, "%s.log" % docker_file.replace(
  180. ".dockerfile", "").lower()), 'w') as run_log:
  181. tb = traceback.format_exc()
  182. log("Running docker cointainer: %s failed" % docker_file,
  183. prefix=log_prefix, file=run_log)
  184. log(tb, prefix=log_prefix, file=run_log)
  185. return 1
  186. return 0
  187. def successfully_running_containers(docker_files, out):
  188. '''
  189. Returns whether all the expected containers for the given docker_files are
  190. running.
  191. '''
  192. client = docker.from_env()
  193. expected_running_container_images = []
  194. for docker_file in docker_files:
  195. # 'gemini.dockerfile' -> 'gemini'
  196. image_tag = docker_file.split('.')[0]
  197. expected_running_container_images.append(image_tag)
  198. running_container_images = []
  199. for container in client.containers.list():
  200. # 'tfb/test/gemini:latest' -> 'gemini'
  201. image_tag = container.image.tags[0].split(':')[0][9:]
  202. running_container_images.append(image_tag)
  203. for image_name in expected_running_container_images:
  204. if image_name not in running_container_images:
  205. log_prefix = "%s: " % image_name
  206. log("ERROR: Expected tfb/test/%s to be running container" %
  207. image_name, prefix=log_prefix, file=out)
  208. return False
  209. return True
  210. def stop(config=None, database_container_id=None, test=None):
  211. '''
  212. Attempts to stop the running test container.
  213. '''
  214. client = docker.from_env()
  215. # Stop all the containers
  216. for container in client.containers.list():
  217. if container.status == "running" and container.id != database_container_id:
  218. container.stop()
  219. # Remove only the tfb/test image for this test
  220. try:
  221. client.images.remove("tfb/test/%s" % test.name, force=True)
  222. except:
  223. # This can be okay if the user hit ctrl+c before the image built/ran
  224. pass
  225. # Stop the database container
  226. if database_container_id:
  227. command = list(config.database_ssh_command)
  228. command.extend(['docker', 'stop', database_container_id])
  229. subprocess.check_call(command, stdout=FNULL, stderr=subprocess.STDOUT)
  230. client.images.prune()
  231. client.containers.prune()
  232. client.networks.prune()
  233. client.volumes.prune()
  234. def find(path, pattern):
  235. '''
  236. Finds and returns all the the files matching the given pattern recursively in
  237. the given path.
  238. '''
  239. for root, dirs, files in os.walk(path):
  240. for name in files:
  241. if fnmatch.fnmatch(name, pattern):
  242. return os.path.join(root, name)
  243. def start_database(config, database):
  244. '''
  245. Sets up a container for the given database and port, and starts said docker
  246. container.
  247. '''
  248. def __is_hex(s):
  249. try:
  250. int(s, 16)
  251. except ValueError:
  252. return False
  253. return len(s) % 2 == 0
  254. command = list(config.database_ssh_command)
  255. command.extend(['docker', 'images', '-q', database])
  256. out = subprocess.check_output(command)
  257. dbid = ''
  258. if len(out.splitlines()) > 0:
  259. dbid = out.splitlines()[len(out.splitlines()) - 1]
  260. # If the database image exists, then dbid will look like
  261. # fe12ca519b47, and we do not want to rebuild if it exists
  262. if len(dbid) != 12 and not __is_hex(dbid):
  263. def __scp_command(files):
  264. scpstr = ["scp", "-i", config.database_identity_file]
  265. for file in files:
  266. scpstr.append(file)
  267. scpstr.append("%s@%s:~/%s/" % (config.database_user,
  268. config.database_host, database))
  269. return scpstr
  270. command = list(config.database_ssh_command)
  271. command.extend(['mkdir', '-p', database])
  272. subprocess.check_call(command)
  273. dbpath = os.path.join(config.fwroot, "toolset", "setup", "docker",
  274. "databases", database)
  275. dbfiles = ""
  276. for dbfile in os.listdir(dbpath):
  277. dbfiles += "%s " % os.path.join(dbpath, dbfile)
  278. subprocess.check_call(__scp_command(dbfiles.split()))
  279. command = list(config.database_ssh_command)
  280. command.extend([
  281. 'docker', 'build', '-f',
  282. '~/%s/%s.dockerfile' % (database, database), '-t', database,
  283. '~/%s' % database
  284. ])
  285. subprocess.check_call(command)
  286. command = list(config.database_ssh_command)
  287. command.extend(
  288. ['docker', 'run', '-d', '--rm', '--init', '--network=host', database])
  289. docker_id = subprocess.check_output(command).strip()
  290. # Sleep until the database accepts connections
  291. slept = 0
  292. max_sleep = 60
  293. while not test_database(config, database) and slept < max_sleep:
  294. time.sleep(1)
  295. slept += 1
  296. return docker_id
  297. def __gather_dependencies(docker_file):
  298. '''
  299. Gathers all the known docker dependencies for the given docker image.
  300. '''
  301. deps = []
  302. docker_dir = os.path.join(
  303. os.getenv('FWROOT'), "toolset", "setup", "docker")
  304. if os.path.exists(docker_file):
  305. with open(docker_file) as fp:
  306. for line in fp.readlines():
  307. tokens = line.strip().split(' ')
  308. if tokens[0] == "FROM":
  309. # This is magic that our base image points to
  310. if tokens[1] != "ubuntu:16.04":
  311. dep_ref = tokens[1].strip().split(':')[0].strip()
  312. if '/' not in dep_ref:
  313. raise AttributeError(
  314. "Could not find docker FROM dependency: %s" %
  315. dep_ref)
  316. depToken = dep_ref.split('/')[1]
  317. deps.append(depToken)
  318. dep_docker_file = os.path.join(
  319. os.path.dirname(docker_file),
  320. depToken + ".dockerfile")
  321. if not os.path.exists(dep_docker_file):
  322. dep_docker_file = find(docker_dir,
  323. depToken + ".dockerfile")
  324. deps.extend(__gather_dependencies(dep_docker_file))
  325. return deps