docker_helper.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306
  1. import os
  2. import socket
  3. import fnmatch
  4. import subprocess
  5. import multiprocessing
  6. import json
  7. import docker
  8. from threading import Thread
  9. from toolset.utils import setup_util
  10. from toolset.utils.output_helper import tee_output
  11. from toolset.utils.metadata_helper import gather_tests
  12. from toolset.utils.ordered_set import OrderedSet
  13. def clean():
  14. '''
  15. Cleans all the docker images from the system
  16. '''
  17. subprocess.check_call(["docker", "image", "prune", "-f"])
  18. docker_ids = subprocess.check_output(["docker", "images",
  19. "-q"]).splitlines()
  20. for docker_id in docker_ids:
  21. subprocess.check_call(["docker", "image", "rmi", "-f", docker_id])
  22. subprocess.check_call(["docker", "system", "prune", "-a", "-f"])
  23. def build(benchmarker_config, test_names, out):
  24. '''
  25. Builds the dependency chain as well as the test implementation docker images
  26. for the given tests.
  27. '''
  28. tests = gather_tests(test_names)
  29. for test in tests:
  30. docker_buildargs = {
  31. 'CPU_COUNT': str(multiprocessing.cpu_count()),
  32. 'MAX_CONCURRENCY': str(max(benchmarker_config.concurrency_levels)),
  33. 'TFB_DATABASE': str(benchmarker_config.database_host)
  34. }
  35. test_docker_files = ["%s.dockerfile" % test.name]
  36. if test.docker_files is not None:
  37. if type(test.docker_files) is list:
  38. test_docker_files.extend(test.docker_files)
  39. else:
  40. raise Exception(
  41. "docker_files in benchmark_config.json must be an array")
  42. for test_docker_file in test_docker_files:
  43. deps = OrderedSet(
  44. list(
  45. reversed(
  46. __gather_dependencies(
  47. os.path.join(test.directory, test_docker_file)))))
  48. docker_dir = os.path.join(setup_util.get_fwroot(), "toolset",
  49. "setup", "docker")
  50. for dependency in deps:
  51. docker_file = os.path.join(test.directory,
  52. dependency + ".dockerfile")
  53. if not docker_file or not os.path.exists(docker_file):
  54. docker_file = find(docker_dir, dependency + ".dockerfile")
  55. if not docker_file:
  56. tee_output(
  57. out,
  58. "Docker build failed; %s could not be found; terminating\n"
  59. % (dependency + ".dockerfile"))
  60. return 1
  61. # Build the dependency image
  62. try:
  63. for line in docker.APIClient(
  64. base_url='unix://var/run/docker.sock').build(
  65. path=os.path.dirname(docker_file),
  66. dockerfile="%s.dockerfile" % dependency,
  67. tag="tfb/%s" % dependency,
  68. buildargs=docker_buildargs,
  69. forcerm=True):
  70. prev_line = os.linesep
  71. if line.startswith('{"stream":'):
  72. line = json.loads(line)
  73. line = line[line.keys()[0]].encode('utf-8')
  74. if prev_line.endswith(os.linesep):
  75. tee_output(out, line)
  76. else:
  77. tee_output(out, line)
  78. prev_line = line
  79. except Exception as e:
  80. tee_output(out,
  81. "Docker dependency build failed; terminating\n")
  82. print(e)
  83. return 1
  84. # Build the test images
  85. for test_docker_file in test_docker_files:
  86. try:
  87. for line in docker.APIClient(
  88. base_url='unix://var/run/docker.sock').build(
  89. path=test.directory,
  90. dockerfile=test_docker_file,
  91. tag="tfb/test/%s" % test_docker_file.replace(
  92. ".dockerfile", ""),
  93. buildargs=docker_buildargs,
  94. forcerm=True):
  95. prev_line = os.linesep
  96. if line.startswith('{"stream":'):
  97. line = json.loads(line)
  98. line = line[line.keys()[0]].encode('utf-8')
  99. if prev_line.endswith(os.linesep):
  100. tee_output(out, line)
  101. else:
  102. tee_output(out, line)
  103. prev_line = line
  104. except Exception as e:
  105. tee_output(out, "Docker build failed; terminating\n")
  106. print(e)
  107. return 1
  108. return 0
  109. def run(benchmarker_config, docker_files, database_container_id, out):
  110. '''
  111. Run the given Docker container(s)
  112. '''
  113. client = docker.from_env()
  114. for docker_file in docker_files:
  115. try:
  116. def watch_container(container):
  117. for line in container.logs(stream=True):
  118. tee_output(out, line)
  119. extra_hosts = {
  120. socket.gethostname(): str(benchmarker_config.server_host),
  121. 'TFB-SERVER': str(benchmarker_config.server_host),
  122. 'TFB-DATABASE': str(benchmarker_config.database_host),
  123. 'TFB-CLIENT': str(benchmarker_config.client_host)
  124. }
  125. container = client.containers.run(
  126. "tfb/test/%s" % docker_file.replace(".dockerfile", ""),
  127. network_mode="host",
  128. privileged=True,
  129. stderr=True,
  130. detach=True,
  131. init=True,
  132. extra_hosts=extra_hosts)
  133. watch_thread = Thread(target=watch_container, args=(container, ))
  134. watch_thread.daemon = True
  135. watch_thread.start()
  136. except Exception as e:
  137. tee_output(out,
  138. "Running docker cointainer: %s failed" % docker_file)
  139. print(e)
  140. return 1
  141. running_container_length = len(
  142. client.containers.list(filters={'status': 'running'}))
  143. expected_length = len(docker_files)
  144. if database_container_id is not None:
  145. expected_length = expected_length + 1
  146. if (running_container_length != expected_length):
  147. tee_output(out, "Running Containers (id, name):" + os.linesep)
  148. for running_container in client.containers.list():
  149. tee_output(out, "%s, %s%s" % (running_container.short_id,
  150. running_container.image, os.linesep))
  151. tee_output(out, "Excepted %s running containers; saw %s%s" %
  152. (running_container_length, expected_length, os.linesep))
  153. return 1
  154. return 0
  155. def stop(config, database_container_id, test, out):
  156. '''
  157. Attempts to stop the running test container.
  158. '''
  159. client = docker.from_env()
  160. # Stop all the containers
  161. for container in client.containers.list():
  162. if container.status == "running" and container.id != database_container_id:
  163. container.stop()
  164. # Remove only the tfb/test image for this test
  165. try:
  166. client.images.remove("tfb/test/%s" % test.name, force=True)
  167. except:
  168. # This can be okay if the user hit ctrl+c before the image built/ran
  169. pass
  170. # Stop the database container
  171. if database_container_id:
  172. command = list(config.database_ssh_command)
  173. command.extend(['docker', 'stop', database_container_id])
  174. subprocess.check_call(command)
  175. client.images.prune()
  176. def find(path, pattern):
  177. '''
  178. Finds and returns all the the files matching the given pattern recursively in
  179. the given path.
  180. '''
  181. for root, dirs, files in os.walk(path):
  182. for name in files:
  183. if fnmatch.fnmatch(name, pattern):
  184. return os.path.join(root, name)
  185. def start_database(config, database):
  186. '''
  187. Sets up a container for the given database and port, and starts said docker
  188. container.
  189. '''
  190. def __is_hex(s):
  191. try:
  192. int(s, 16)
  193. except ValueError:
  194. return False
  195. return len(s) % 2 == 0
  196. command = list(config.database_ssh_command)
  197. command.extend(['docker', 'images', '-q', database])
  198. out = subprocess.check_output(command)
  199. dbid = ''
  200. if len(out.splitlines()) > 0:
  201. dbid = out.splitlines()[len(out.splitlines()) - 1]
  202. # If the database image exists, then dbid will look like
  203. # fe12ca519b47, and we do not want to rebuild if it exists
  204. if len(dbid) != 12 and not __is_hex(dbid):
  205. def __scp_command(files):
  206. scpstr = ["scp", "-i", config.database_identity_file]
  207. for file in files:
  208. scpstr.append(file)
  209. scpstr.append("%s@%s:~/%s/" % (config.database_user,
  210. config.database_host, database))
  211. return scpstr
  212. command = list(config.database_ssh_command)
  213. command.extend(['mkdir', '-p', database])
  214. subprocess.check_call(command)
  215. dbpath = os.path.join(config.fwroot, "toolset", "setup", "docker",
  216. "databases", database)
  217. dbfiles = ""
  218. for dbfile in os.listdir(dbpath):
  219. dbfiles += "%s " % os.path.join(dbpath, dbfile)
  220. subprocess.check_call(__scp_command(dbfiles.split()))
  221. command = list(config.database_ssh_command)
  222. command.extend([
  223. 'docker', 'build', '-f',
  224. '~/%s/%s.dockerfile' % (database, database), '-t', database,
  225. '~/%s' % database
  226. ])
  227. subprocess.check_call(command)
  228. command = list(config.database_ssh_command)
  229. command.extend(
  230. ['docker', 'run', '-d', '--rm', '--init', '--network=host', database])
  231. pid = subprocess.check_output(command).strip()
  232. return pid
  233. def __gather_dependencies(docker_file):
  234. '''
  235. Gathers all the known docker dependencies for the given docker image.
  236. '''
  237. # Avoid setting up a circular import
  238. from toolset.utils import setup_util
  239. deps = []
  240. docker_dir = os.path.join(setup_util.get_fwroot(), "toolset", "setup",
  241. "docker")
  242. if os.path.exists(docker_file):
  243. with open(docker_file) as fp:
  244. for line in fp.readlines():
  245. tokens = line.strip().split(' ')
  246. if tokens[0] == "FROM":
  247. # This is magic that our base image points to
  248. if tokens[1] != "ubuntu:16.04":
  249. dep_ref = tokens[1].strip().split(':')[0].strip()
  250. if '/' not in dep_ref:
  251. raise AttributeError(
  252. "Could not find docker FROM dependency: %s" %
  253. dep_ref)
  254. depToken = dep_ref.split('/')[1]
  255. deps.append(depToken)
  256. dep_docker_file = os.path.join(
  257. os.path.dirname(docker_file),
  258. depToken + ".dockerfile")
  259. if not os.path.exists(dep_docker_file):
  260. dep_docker_file = find(docker_dir,
  261. depToken + ".dockerfile")
  262. deps.extend(__gather_dependencies(dep_docker_file))
  263. return deps