docker_helper.py 11 KB

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