pybiolib 0.2.951__py3-none-any.whl → 1.2.1890__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (262) hide show
  1. biolib/__init__.py +357 -11
  2. biolib/_data_record/data_record.py +380 -0
  3. biolib/_index/__init__.py +0 -0
  4. biolib/_index/index.py +55 -0
  5. biolib/_index/query_result.py +103 -0
  6. biolib/_internal/__init__.py +0 -0
  7. biolib/_internal/add_copilot_prompts.py +58 -0
  8. biolib/_internal/add_gui_files.py +81 -0
  9. biolib/_internal/data_record/__init__.py +1 -0
  10. biolib/_internal/data_record/data_record.py +85 -0
  11. biolib/_internal/data_record/push_data.py +116 -0
  12. biolib/_internal/data_record/remote_storage_endpoint.py +43 -0
  13. biolib/_internal/errors.py +5 -0
  14. biolib/_internal/file_utils.py +125 -0
  15. biolib/_internal/fuse_mount/__init__.py +1 -0
  16. biolib/_internal/fuse_mount/experiment_fuse_mount.py +209 -0
  17. biolib/_internal/http_client.py +159 -0
  18. biolib/_internal/lfs/__init__.py +1 -0
  19. biolib/_internal/lfs/cache.py +51 -0
  20. biolib/_internal/libs/__init__.py +1 -0
  21. biolib/_internal/libs/fusepy/__init__.py +1257 -0
  22. biolib/_internal/push_application.py +488 -0
  23. biolib/_internal/runtime.py +22 -0
  24. biolib/_internal/string_utils.py +13 -0
  25. biolib/_internal/templates/__init__.py +1 -0
  26. biolib/_internal/templates/copilot_template/.github/instructions/general-app-knowledge.instructions.md +10 -0
  27. biolib/_internal/templates/copilot_template/.github/instructions/style-general.instructions.md +20 -0
  28. biolib/_internal/templates/copilot_template/.github/instructions/style-python.instructions.md +16 -0
  29. biolib/_internal/templates/copilot_template/.github/instructions/style-react-ts.instructions.md +47 -0
  30. biolib/_internal/templates/copilot_template/.github/prompts/biolib_app_inputs.prompt.md +11 -0
  31. biolib/_internal/templates/copilot_template/.github/prompts/biolib_onboard_repo.prompt.md +19 -0
  32. biolib/_internal/templates/copilot_template/.github/prompts/biolib_run_apps.prompt.md +12 -0
  33. biolib/_internal/templates/dashboard_template/.biolib/config.yml +5 -0
  34. biolib/_internal/templates/github_workflow_template/.github/workflows/biolib.yml +21 -0
  35. biolib/_internal/templates/gitignore_template/.gitignore +10 -0
  36. biolib/_internal/templates/gui_template/.yarnrc.yml +1 -0
  37. biolib/_internal/templates/gui_template/App.tsx +53 -0
  38. biolib/_internal/templates/gui_template/Dockerfile +27 -0
  39. biolib/_internal/templates/gui_template/biolib-sdk.ts +82 -0
  40. biolib/_internal/templates/gui_template/dev-data/output.json +7 -0
  41. biolib/_internal/templates/gui_template/index.css +5 -0
  42. biolib/_internal/templates/gui_template/index.html +13 -0
  43. biolib/_internal/templates/gui_template/index.tsx +10 -0
  44. biolib/_internal/templates/gui_template/package.json +27 -0
  45. biolib/_internal/templates/gui_template/tsconfig.json +24 -0
  46. biolib/_internal/templates/gui_template/vite-plugin-dev-data.ts +50 -0
  47. biolib/_internal/templates/gui_template/vite.config.mts +10 -0
  48. biolib/_internal/templates/init_template/.biolib/config.yml +19 -0
  49. biolib/_internal/templates/init_template/Dockerfile +14 -0
  50. biolib/_internal/templates/init_template/requirements.txt +1 -0
  51. biolib/_internal/templates/init_template/run.py +12 -0
  52. biolib/_internal/templates/init_template/run.sh +4 -0
  53. biolib/_internal/templates/templates.py +25 -0
  54. biolib/_internal/tree_utils.py +106 -0
  55. biolib/_internal/utils/__init__.py +65 -0
  56. biolib/_internal/utils/auth.py +46 -0
  57. biolib/_internal/utils/job_url.py +33 -0
  58. biolib/_internal/utils/multinode.py +263 -0
  59. biolib/_runtime/runtime.py +157 -0
  60. biolib/_session/session.py +44 -0
  61. biolib/_shared/__init__.py +0 -0
  62. biolib/_shared/types/__init__.py +74 -0
  63. biolib/_shared/types/account.py +12 -0
  64. biolib/_shared/types/account_member.py +8 -0
  65. biolib/_shared/types/app.py +9 -0
  66. biolib/_shared/types/data_record.py +40 -0
  67. biolib/_shared/types/experiment.py +32 -0
  68. biolib/_shared/types/file_node.py +17 -0
  69. biolib/_shared/types/push.py +6 -0
  70. biolib/_shared/types/resource.py +37 -0
  71. biolib/_shared/types/resource_deploy_key.py +11 -0
  72. biolib/_shared/types/resource_permission.py +14 -0
  73. biolib/_shared/types/resource_version.py +19 -0
  74. biolib/_shared/types/result.py +14 -0
  75. biolib/_shared/types/typing.py +10 -0
  76. biolib/_shared/types/user.py +19 -0
  77. biolib/_shared/utils/__init__.py +7 -0
  78. biolib/_shared/utils/resource_uri.py +75 -0
  79. biolib/api/__init__.py +6 -0
  80. biolib/api/client.py +168 -0
  81. biolib/app/app.py +252 -49
  82. biolib/app/search_apps.py +45 -0
  83. biolib/biolib_api_client/api_client.py +126 -31
  84. biolib/biolib_api_client/app_types.py +24 -4
  85. biolib/biolib_api_client/auth.py +31 -8
  86. biolib/biolib_api_client/biolib_app_api.py +147 -52
  87. biolib/biolib_api_client/biolib_job_api.py +161 -141
  88. biolib/biolib_api_client/job_types.py +21 -5
  89. biolib/biolib_api_client/lfs_types.py +7 -23
  90. biolib/biolib_api_client/user_state.py +56 -0
  91. biolib/biolib_binary_format/__init__.py +1 -4
  92. biolib/biolib_binary_format/file_in_container.py +105 -0
  93. biolib/biolib_binary_format/module_input.py +24 -7
  94. biolib/biolib_binary_format/module_output_v2.py +149 -0
  95. biolib/biolib_binary_format/remote_endpoints.py +34 -0
  96. biolib/biolib_binary_format/remote_stream_seeker.py +59 -0
  97. biolib/biolib_binary_format/saved_job.py +3 -2
  98. biolib/biolib_binary_format/{attestation_document.py → stdout_and_stderr.py} +8 -8
  99. biolib/biolib_binary_format/system_status_update.py +3 -2
  100. biolib/biolib_binary_format/utils.py +175 -0
  101. biolib/biolib_docker_client/__init__.py +11 -2
  102. biolib/biolib_errors.py +36 -0
  103. biolib/biolib_logging.py +27 -10
  104. biolib/cli/__init__.py +38 -0
  105. biolib/cli/auth.py +46 -0
  106. biolib/cli/data_record.py +164 -0
  107. biolib/cli/index.py +32 -0
  108. biolib/cli/init.py +421 -0
  109. biolib/cli/lfs.py +101 -0
  110. biolib/cli/push.py +50 -0
  111. biolib/cli/run.py +63 -0
  112. biolib/cli/runtime.py +14 -0
  113. biolib/cli/sdk.py +16 -0
  114. biolib/cli/start.py +56 -0
  115. biolib/compute_node/cloud_utils/cloud_utils.py +110 -161
  116. biolib/compute_node/job_worker/cache_state.py +66 -88
  117. biolib/compute_node/job_worker/cache_types.py +1 -6
  118. biolib/compute_node/job_worker/docker_image_cache.py +112 -37
  119. biolib/compute_node/job_worker/executors/__init__.py +0 -3
  120. biolib/compute_node/job_worker/executors/docker_executor.py +532 -199
  121. biolib/compute_node/job_worker/executors/docker_types.py +9 -1
  122. biolib/compute_node/job_worker/executors/types.py +19 -9
  123. biolib/compute_node/job_worker/job_legacy_input_wait_timeout_thread.py +30 -0
  124. biolib/compute_node/job_worker/job_max_runtime_timer_thread.py +3 -5
  125. biolib/compute_node/job_worker/job_storage.py +108 -0
  126. biolib/compute_node/job_worker/job_worker.py +397 -212
  127. biolib/compute_node/job_worker/large_file_system.py +87 -38
  128. biolib/compute_node/job_worker/network_alloc.py +99 -0
  129. biolib/compute_node/job_worker/network_buffer.py +240 -0
  130. biolib/compute_node/job_worker/utilization_reporter_thread.py +197 -0
  131. biolib/compute_node/job_worker/utils.py +9 -24
  132. biolib/compute_node/remote_host_proxy.py +400 -98
  133. biolib/compute_node/utils.py +31 -9
  134. biolib/compute_node/webserver/compute_node_results_proxy.py +189 -0
  135. biolib/compute_node/webserver/proxy_utils.py +28 -0
  136. biolib/compute_node/webserver/webserver.py +130 -44
  137. biolib/compute_node/webserver/webserver_types.py +2 -6
  138. biolib/compute_node/webserver/webserver_utils.py +77 -12
  139. biolib/compute_node/webserver/worker_thread.py +183 -42
  140. biolib/experiments/__init__.py +0 -0
  141. biolib/experiments/experiment.py +356 -0
  142. biolib/jobs/__init__.py +1 -0
  143. biolib/jobs/job.py +741 -0
  144. biolib/jobs/job_result.py +185 -0
  145. biolib/jobs/types.py +50 -0
  146. biolib/py.typed +0 -0
  147. biolib/runtime/__init__.py +14 -0
  148. biolib/sdk/__init__.py +91 -0
  149. biolib/tables.py +34 -0
  150. biolib/typing_utils.py +2 -7
  151. biolib/user/__init__.py +1 -0
  152. biolib/user/sign_in.py +54 -0
  153. biolib/utils/__init__.py +162 -0
  154. biolib/utils/cache_state.py +94 -0
  155. biolib/utils/multipart_uploader.py +194 -0
  156. biolib/utils/seq_util.py +150 -0
  157. biolib/utils/zip/remote_zip.py +640 -0
  158. pybiolib-1.2.1890.dist-info/METADATA +41 -0
  159. pybiolib-1.2.1890.dist-info/RECORD +177 -0
  160. {pybiolib-0.2.951.dist-info → pybiolib-1.2.1890.dist-info}/WHEEL +1 -1
  161. pybiolib-1.2.1890.dist-info/entry_points.txt +2 -0
  162. README.md +0 -17
  163. biolib/app/app_result.py +0 -68
  164. biolib/app/utils.py +0 -62
  165. biolib/biolib-js/0-biolib.worker.js +0 -1
  166. biolib/biolib-js/1-biolib.worker.js +0 -1
  167. biolib/biolib-js/2-biolib.worker.js +0 -1
  168. biolib/biolib-js/3-biolib.worker.js +0 -1
  169. biolib/biolib-js/4-biolib.worker.js +0 -1
  170. biolib/biolib-js/5-biolib.worker.js +0 -1
  171. biolib/biolib-js/6-biolib.worker.js +0 -1
  172. biolib/biolib-js/index.html +0 -10
  173. biolib/biolib-js/main-biolib.js +0 -1
  174. biolib/biolib_api_client/biolib_account_api.py +0 -21
  175. biolib/biolib_api_client/biolib_large_file_system_api.py +0 -108
  176. biolib/biolib_binary_format/aes_encrypted_package.py +0 -42
  177. biolib/biolib_binary_format/module_output.py +0 -58
  178. biolib/biolib_binary_format/rsa_encrypted_aes_package.py +0 -57
  179. biolib/biolib_push.py +0 -114
  180. biolib/cli.py +0 -203
  181. biolib/cli_utils.py +0 -273
  182. biolib/compute_node/cloud_utils/enclave_parent_types.py +0 -7
  183. biolib/compute_node/enclave/__init__.py +0 -2
  184. biolib/compute_node/enclave/enclave_remote_hosts.py +0 -53
  185. biolib/compute_node/enclave/nitro_secure_module_utils.py +0 -64
  186. biolib/compute_node/job_worker/executors/base_executor.py +0 -18
  187. biolib/compute_node/job_worker/executors/pyppeteer_executor.py +0 -173
  188. biolib/compute_node/job_worker/executors/remote/__init__.py +0 -1
  189. biolib/compute_node/job_worker/executors/remote/nitro_enclave_utils.py +0 -81
  190. biolib/compute_node/job_worker/executors/remote/remote_executor.py +0 -51
  191. biolib/lfs.py +0 -196
  192. biolib/pyppeteer/.circleci/config.yml +0 -100
  193. biolib/pyppeteer/.coveragerc +0 -3
  194. biolib/pyppeteer/.gitignore +0 -89
  195. biolib/pyppeteer/.pre-commit-config.yaml +0 -28
  196. biolib/pyppeteer/CHANGES.md +0 -253
  197. biolib/pyppeteer/CONTRIBUTING.md +0 -26
  198. biolib/pyppeteer/LICENSE +0 -12
  199. biolib/pyppeteer/README.md +0 -137
  200. biolib/pyppeteer/docs/Makefile +0 -177
  201. biolib/pyppeteer/docs/_static/custom.css +0 -28
  202. biolib/pyppeteer/docs/_templates/layout.html +0 -10
  203. biolib/pyppeteer/docs/changes.md +0 -1
  204. biolib/pyppeteer/docs/conf.py +0 -299
  205. biolib/pyppeteer/docs/index.md +0 -21
  206. biolib/pyppeteer/docs/make.bat +0 -242
  207. biolib/pyppeteer/docs/reference.md +0 -211
  208. biolib/pyppeteer/docs/server.py +0 -60
  209. biolib/pyppeteer/poetry.lock +0 -1699
  210. biolib/pyppeteer/pyppeteer/__init__.py +0 -135
  211. biolib/pyppeteer/pyppeteer/accessibility.py +0 -286
  212. biolib/pyppeteer/pyppeteer/browser.py +0 -401
  213. biolib/pyppeteer/pyppeteer/browser_fetcher.py +0 -194
  214. biolib/pyppeteer/pyppeteer/command.py +0 -22
  215. biolib/pyppeteer/pyppeteer/connection/__init__.py +0 -242
  216. biolib/pyppeteer/pyppeteer/connection/cdpsession.py +0 -101
  217. biolib/pyppeteer/pyppeteer/coverage.py +0 -346
  218. biolib/pyppeteer/pyppeteer/device_descriptors.py +0 -787
  219. biolib/pyppeteer/pyppeteer/dialog.py +0 -79
  220. biolib/pyppeteer/pyppeteer/domworld.py +0 -597
  221. biolib/pyppeteer/pyppeteer/emulation_manager.py +0 -53
  222. biolib/pyppeteer/pyppeteer/errors.py +0 -48
  223. biolib/pyppeteer/pyppeteer/events.py +0 -63
  224. biolib/pyppeteer/pyppeteer/execution_context.py +0 -156
  225. biolib/pyppeteer/pyppeteer/frame/__init__.py +0 -299
  226. biolib/pyppeteer/pyppeteer/frame/frame_manager.py +0 -306
  227. biolib/pyppeteer/pyppeteer/helpers.py +0 -245
  228. biolib/pyppeteer/pyppeteer/input.py +0 -371
  229. biolib/pyppeteer/pyppeteer/jshandle.py +0 -598
  230. biolib/pyppeteer/pyppeteer/launcher.py +0 -683
  231. biolib/pyppeteer/pyppeteer/lifecycle_watcher.py +0 -169
  232. biolib/pyppeteer/pyppeteer/models/__init__.py +0 -103
  233. biolib/pyppeteer/pyppeteer/models/_protocol.py +0 -12460
  234. biolib/pyppeteer/pyppeteer/multimap.py +0 -82
  235. biolib/pyppeteer/pyppeteer/network_manager.py +0 -678
  236. biolib/pyppeteer/pyppeteer/options.py +0 -8
  237. biolib/pyppeteer/pyppeteer/page.py +0 -1728
  238. biolib/pyppeteer/pyppeteer/pipe_transport.py +0 -59
  239. biolib/pyppeteer/pyppeteer/target.py +0 -147
  240. biolib/pyppeteer/pyppeteer/task_queue.py +0 -24
  241. biolib/pyppeteer/pyppeteer/timeout_settings.py +0 -36
  242. biolib/pyppeteer/pyppeteer/tracing.py +0 -93
  243. biolib/pyppeteer/pyppeteer/us_keyboard_layout.py +0 -305
  244. biolib/pyppeteer/pyppeteer/util.py +0 -18
  245. biolib/pyppeteer/pyppeteer/websocket_transport.py +0 -47
  246. biolib/pyppeteer/pyppeteer/worker.py +0 -101
  247. biolib/pyppeteer/pyproject.toml +0 -97
  248. biolib/pyppeteer/spell.txt +0 -137
  249. biolib/pyppeteer/tox.ini +0 -72
  250. biolib/pyppeteer/utils/generate_protocol_types.py +0 -603
  251. biolib/start_cli.py +0 -7
  252. biolib/utils.py +0 -47
  253. biolib/validators/validate_app_version.py +0 -183
  254. biolib/validators/validate_argument.py +0 -134
  255. biolib/validators/validate_module.py +0 -323
  256. biolib/validators/validate_zip_file.py +0 -40
  257. biolib/validators/validator_utils.py +0 -103
  258. pybiolib-0.2.951.dist-info/LICENSE +0 -21
  259. pybiolib-0.2.951.dist-info/METADATA +0 -61
  260. pybiolib-0.2.951.dist-info/RECORD +0 -153
  261. pybiolib-0.2.951.dist-info/entry_points.txt +0 -3
  262. /LICENSE → /pybiolib-1.2.1890.dist-info/licenses/LICENSE +0 -0
@@ -0,0 +1,189 @@
1
+ import io
2
+ import os
3
+ import tarfile
4
+ import threading
5
+ import time
6
+
7
+ from docker.models.containers import Container
8
+
9
+ from biolib.biolib_docker_client import BiolibDockerClient
10
+ from biolib.biolib_errors import BioLibError
11
+ from biolib.biolib_logging import logger, logger_no_user_data
12
+ from biolib.compute_node.utils import BIOLIB_PROXY_NETWORK_NAME
13
+ from biolib.compute_node.webserver.proxy_utils import get_biolib_nginx_proxy_image
14
+ from biolib.typing_utils import Optional
15
+
16
+
17
+ class LogStreamingThread(threading.Thread):
18
+ def __init__(self, container, container_name: str):
19
+ super().__init__(daemon=True)
20
+ self._container = container
21
+ self._container_name = container_name
22
+ self._stop_event = threading.Event()
23
+
24
+ def run(self) -> None:
25
+ try:
26
+ logger_no_user_data.debug(f'Starting log streaming for container "{self._container_name}"')
27
+ log_stream = self._container.logs(follow=True, stream=True)
28
+ for log_line in log_stream:
29
+ if self._stop_event.is_set():
30
+ break
31
+ if log_line:
32
+ logger.debug(f'ComputeNodeResultsProxy | {log_line.decode("utf-8", errors="replace").rstrip()}')
33
+ except Exception as error:
34
+ logger_no_user_data.debug(f'Log streaming for container "{self._container_name}" ended: {error}')
35
+
36
+ def stop(self) -> None:
37
+ self._stop_event.set()
38
+
39
+
40
+ class ComputeNodeResultsProxy:
41
+ _instance: Optional['ComputeNodeResultsProxy'] = None
42
+
43
+ def __init__(self, tls_pem_certificate_path: str, tls_pem_key_path: str):
44
+ assert tls_pem_certificate_path, 'tls_pem_certificate_path is required'
45
+ assert tls_pem_key_path, 'tls_pem_key_path is required'
46
+ self._name = 'biolib-compute-node-results-proxy'
47
+ self._container: Optional[Container] = None
48
+ self._docker = BiolibDockerClient().get_docker_client()
49
+ self._tls_pem_certificate_path = tls_pem_certificate_path
50
+ self._tls_pem_key_path = tls_pem_key_path
51
+ self._log_streaming_thread: Optional[LogStreamingThread] = None
52
+
53
+ @staticmethod
54
+ def start_proxy(tls_pem_certificate_path: str, tls_pem_key_path: str) -> None:
55
+ abs_cert_path = os.path.abspath(tls_pem_certificate_path)
56
+ abs_key_path = os.path.abspath(tls_pem_key_path)
57
+ assert os.path.exists(abs_cert_path), f'TLS certificate file does not exist: {abs_cert_path}'
58
+ assert os.path.exists(abs_key_path), f'TLS key file does not exist: {abs_key_path}'
59
+
60
+ if ComputeNodeResultsProxy._instance is None:
61
+ logger_no_user_data.debug(
62
+ f'Creating ComputeNodeResultsProxy instance with cert: {abs_cert_path}, key: {abs_key_path}'
63
+ )
64
+ ComputeNodeResultsProxy._instance = ComputeNodeResultsProxy(abs_cert_path, abs_key_path)
65
+ ComputeNodeResultsProxy._instance._start() # pylint: disable=protected-access
66
+
67
+ @staticmethod
68
+ def stop_proxy() -> None:
69
+ if ComputeNodeResultsProxy._instance is not None:
70
+ ComputeNodeResultsProxy._instance._terminate() # pylint: disable=protected-access
71
+ ComputeNodeResultsProxy._instance = None
72
+
73
+ def _start(self) -> None:
74
+ docker = BiolibDockerClient.get_docker_client()
75
+
76
+ for index in range(3):
77
+ logger_no_user_data.debug(
78
+ f'Attempt {index} at creating ComputeNodeResultsProxy container "{self._name}"...'
79
+ )
80
+ try:
81
+ self._container = docker.containers.create(
82
+ detach=True,
83
+ image=get_biolib_nginx_proxy_image(),
84
+ name=self._name,
85
+ network=BIOLIB_PROXY_NETWORK_NAME,
86
+ ports={'443/tcp': 20443},
87
+ volumes={
88
+ self._tls_pem_certificate_path: {'bind': '/etc/ssl/certs/certificate.pem', 'mode': 'ro'},
89
+ self._tls_pem_key_path: {'bind': '/etc/ssl/private/private_key.pem', 'mode': 'ro'},
90
+ },
91
+ )
92
+ break
93
+ except Exception as error:
94
+ logger_no_user_data.exception(f'Failed to create container "{self._name}" hit error: {error}')
95
+
96
+ logger_no_user_data.debug('Sleeping before re-trying container creation...')
97
+ time.sleep(3)
98
+
99
+ if not self._container or not self._container.id:
100
+ raise BioLibError(f'Exceeded re-try limit for creating container {self._name}')
101
+
102
+ self._write_nginx_config_to_container()
103
+ self._container.start()
104
+
105
+ logger_no_user_data.debug(f'Waiting for container "{self._name}" to be ready...')
106
+ proxy_is_ready = False
107
+ for retry_count in range(1, 5):
108
+ time.sleep(0.5 * retry_count)
109
+ container_logs = self._container.logs()
110
+ if b'ready for start up\n' in container_logs or b'start worker process ' in container_logs:
111
+ proxy_is_ready = True
112
+ break
113
+
114
+ if not proxy_is_ready:
115
+ logger_no_user_data.error('ComputeNodeResultsProxy did not start properly.')
116
+ self._terminate()
117
+ raise Exception('ComputeNodeResultsProxy did not start properly')
118
+
119
+ self._container.reload()
120
+ logger.debug(f'ComputeNodeResultsProxy container "{self._name}" started with ID: {self._container.id}')
121
+
122
+ self._log_streaming_thread = LogStreamingThread(self._container, self._name)
123
+ self._log_streaming_thread.start()
124
+ logger_no_user_data.debug(f'Started log streaming for container "{self._name}"')
125
+
126
+ def _terminate(self):
127
+ logger_no_user_data.debug(f'Terminating ComputeNodeResultsProxy container "{self._name}"')
128
+ if self._log_streaming_thread:
129
+ self._log_streaming_thread.stop()
130
+ self._log_streaming_thread = None
131
+
132
+ logger.debug(f'Docker container removal temporarily disabled for debugging purposes (container "{self._name}")')
133
+ # TODO: Figure if we need to remove the container or keep it for debugging purposes
134
+ # if self._container:
135
+ # self._container.remove(force=True)
136
+
137
+ def _write_nginx_config_to_container(self) -> None:
138
+ if not self._container:
139
+ raise Exception('ComputeNodeResultsProxy container not defined when attempting to write NGINX config')
140
+
141
+ docker = BiolibDockerClient.get_docker_client()
142
+
143
+ nginx_config = """
144
+ events {
145
+ worker_connections 1024;
146
+ }
147
+
148
+ http {
149
+ client_max_body_size 0;
150
+ resolver 127.0.0.11 ipv6=off valid=30s;
151
+
152
+ server {
153
+ listen 443 ssl http2 default_server;
154
+
155
+ ssl_certificate /etc/ssl/certs/certificate.pem;
156
+ ssl_certificate_key /etc/ssl/private/private_key.pem;
157
+ ssl_protocols TLSv1.2 TLSv1.3;
158
+
159
+ location / {
160
+ if ($http_biolib_result_uuid = "") {
161
+ return 400 "Missing biolib-result-uuid header";
162
+ }
163
+
164
+ if ($http_biolib_result_port = "") {
165
+ return 400 "Missing biolib-result-port header";
166
+ }
167
+
168
+ set $target_host "biolib-app-caller-proxy-$http_biolib_result_uuid";
169
+ proxy_pass http://$target_host:1080$request_uri;
170
+ proxy_set_header Host $http_host;
171
+ proxy_set_header biolib-result-uuid $http_biolib_result_uuid;
172
+ proxy_set_header biolib-result-port $http_biolib_result_port;
173
+ proxy_pass_request_headers on;
174
+ }
175
+ }
176
+ }
177
+ """
178
+
179
+ nginx_config_bytes = nginx_config.encode()
180
+ tarfile_in_memory = io.BytesIO()
181
+ with tarfile.open(fileobj=tarfile_in_memory, mode='w:gz') as tar:
182
+ info = tarfile.TarInfo('/nginx.conf')
183
+ info.size = len(nginx_config_bytes)
184
+ tar.addfile(info, io.BytesIO(nginx_config_bytes))
185
+
186
+ tarfile_bytes = tarfile_in_memory.getvalue()
187
+ tarfile_in_memory.close()
188
+ logger_no_user_data.debug('Writing NGINX configuration to ComputeNodeResultsProxy container')
189
+ docker.api.put_archive(self._container.id, '/etc/nginx', tarfile_bytes)
@@ -0,0 +1,28 @@
1
+ from docker.errors import ImageNotFound
2
+ from docker.models.images import Image
3
+
4
+ from biolib import utils
5
+ from biolib.biolib_docker_client import BiolibDockerClient
6
+ from biolib.biolib_logging import logger_no_user_data
7
+ from biolib.typing_utils import cast
8
+
9
+
10
+ def get_biolib_nginx_proxy_image() -> Image:
11
+ docker = BiolibDockerClient().get_docker_client()
12
+
13
+ if utils.IS_RUNNING_IN_CLOUD:
14
+ try:
15
+ logger_no_user_data.debug('Getting local Docker image for nginx proxy')
16
+ return cast(Image, docker.images.get('biolib-remote-host-proxy:latest'))
17
+ except ImageNotFound:
18
+ logger_no_user_data.debug(
19
+ 'Local Docker image for nginx proxy not available. Falling back to public image...'
20
+ )
21
+
22
+ public_image_uri = 'public.ecr.aws/h5y4b3l1/biolib-remote-host-proxy:latest'
23
+ try:
24
+ logger_no_user_data.debug('Getting public Docker image for nginx proxy')
25
+ return cast(Image, docker.images.get(public_image_uri))
26
+ except ImageNotFound:
27
+ logger_no_user_data.debug('Pulling public Docker image for nginx proxy')
28
+ return cast(Image, docker.images.pull(public_image_uri))
@@ -1,23 +1,34 @@
1
1
  # pylint: disable=unsubscriptable-object
2
2
 
3
3
  import json
4
- import time
5
- import base64
6
4
  import logging
7
- from flask import Flask, request, Response, jsonify
8
- from flask_cors import CORS # type: ignore
5
+ import os
6
+ import tempfile
7
+ import time
8
+
9
+ from docker.models.networks import Network # type: ignore
10
+ from flask import Flask, Response, jsonify, request
9
11
 
10
12
  from biolib import utils
11
13
  from biolib.biolib_api_client import BiolibApiClient
12
14
  from biolib.biolib_binary_format import SavedJob
13
- from biolib.compute_node.enclave.enclave_remote_hosts import start_enclave_remote_hosts
14
- from biolib.compute_node.webserver import webserver_utils
15
+ from biolib.biolib_docker_client import BiolibDockerClient
16
+ from biolib.biolib_logging import TRACE, logger, logger_no_user_data
15
17
  from biolib.compute_node.cloud_utils.cloud_utils import CloudUtils
18
+ from biolib.compute_node.utils import BIOLIB_PROXY_NETWORK_NAME
19
+ from biolib.compute_node.webserver import webserver_utils
20
+ from biolib.compute_node.webserver.compute_node_results_proxy import ComputeNodeResultsProxy
16
21
  from biolib.compute_node.webserver.gunicorn_flask_application import GunicornFlaskApplication
17
- from biolib.biolib_logging import logger, TRACE
22
+ from biolib.compute_node.webserver.webserver_utils import get_job_compute_state_or_404
23
+ from biolib.typing_utils import Optional
18
24
 
19
25
  app = Flask(__name__)
20
- CORS(app)
26
+
27
+ if utils.IS_RUNNING_IN_CLOUD:
28
+ _BIOLIB_TMP_DIR = '/biolib/tmp'
29
+ os.makedirs(_BIOLIB_TMP_DIR, exist_ok=True)
30
+ else:
31
+ _BIOLIB_TMP_DIR = tempfile.TemporaryDirectory().name
21
32
 
22
33
 
23
34
  @app.route('/hello/')
@@ -25,6 +36,11 @@ def hello():
25
36
  return 'Hello'
26
37
 
27
38
 
39
+ @app.route('/health/')
40
+ def health():
41
+ return 'biolib-compute-node is running', 200
42
+
43
+
28
44
  @app.route('/v1/job/', methods=['POST'])
29
45
  def save_job():
30
46
  saved_job = json.loads(request.data.decode())
@@ -34,79 +50,123 @@ def save_job():
34
50
  return jsonify({'job': 'Invalid job'}), 400
35
51
 
36
52
  job_id = saved_job['job']['public_id']
53
+ job_temporary_dir = os.path.join(_BIOLIB_TMP_DIR, job_id)
54
+ os.makedirs(job_temporary_dir)
37
55
  saved_job['BASE_URL'] = BiolibApiClient.get().base_url
56
+ saved_job['job_temporary_dir'] = job_temporary_dir
38
57
 
39
58
  compute_state = webserver_utils.get_compute_state(webserver_utils.UNASSIGNED_COMPUTE_PROCESSES)
40
59
  compute_state['job_id'] = job_id
60
+ compute_state['job'] = saved_job['job']
61
+ compute_state['job_temporary_dir'] = job_temporary_dir
62
+
41
63
  webserver_utils.JOB_ID_TO_COMPUTE_STATE_DICT[job_id] = compute_state
42
64
 
43
65
  if utils.IS_RUNNING_IN_CLOUD:
44
66
  config = CloudUtils.get_webserver_config()
45
67
  saved_job['compute_node_info'] = config['compute_node_info']
68
+ compute_state['cloud_job_id'] = saved_job['cloud_job']['public_id']
69
+ compute_state['cloud_job'] = saved_job['cloud_job']
70
+
71
+ webserver_utils.update_auto_shutdown_time()
46
72
 
47
73
  saved_job_bbf_package = SavedJob().serialize(json.dumps(saved_job))
48
74
  send_package_to_compute_process(job_id, saved_job_bbf_package)
49
75
 
50
- if utils.BIOLIB_IS_RUNNING_IN_ENCLAVE:
51
- return Response(base64.b64encode(compute_state['attestation_document']), status=201)
52
- else:
53
- return '', 201
76
+ return '', 201
54
77
 
55
78
 
56
79
  @app.route('/v1/job/<job_id>/start/', methods=['POST'])
57
80
  def start_compute(job_id):
58
81
  module_input_package = request.data
82
+
83
+ if 'AES-Key-String' in request.headers:
84
+ compute_state = webserver_utils.JOB_ID_TO_COMPUTE_STATE_DICT[job_id]
85
+ compute_state['aes_key_string_b64'] = request.headers['AES-Key-String']
86
+
59
87
  send_package_to_compute_process(job_id, module_input_package)
60
88
  return '', 201
61
89
 
62
90
 
91
+ @app.route('/v1/job/<job_id>/', methods=['DELETE'])
92
+ def terminate_job(job_id: str) -> Response:
93
+ compute_state = get_job_compute_state_or_404(job_id)
94
+ # TODO: Consider BBF package
95
+ compute_state['received_messages_queue'].put(b'CANCEL_JOB')
96
+ return Response()
97
+
98
+
63
99
  @app.route('/v1/job/<job_id>/status/')
64
100
  def status(job_id):
65
101
  # TODO Implement auth token
66
- current_status = webserver_utils.JOB_ID_TO_COMPUTE_STATE_DICT[job_id]['status'].copy()
67
- response = jsonify(current_status)
102
+ return_full_logs = request.args.get('logs') == 'full'
68
103
 
69
- if current_status['status_updates']:
70
- webserver_utils.JOB_ID_TO_COMPUTE_STATE_DICT[job_id]['status']['status_updates'] = []
104
+ compute_state = get_job_compute_state_or_404(job_id)
105
+ current_status = compute_state['status'].copy()
106
+ response_data = current_status
107
+ response_data['is_completed'] = compute_state['is_completed']
71
108
 
72
- # Check if any error occurred
73
- if 'error_code' in current_status:
74
- response.call_on_close(lambda: webserver_utils.finalize_and_clean_up_compute_job(job_id))
109
+ if current_status['stdout_and_stderr_packages_b64']:
110
+ compute_state['streamed_logs_packages_b64'] = (
111
+ compute_state['streamed_logs_packages_b64'] + current_status['stdout_and_stderr_packages_b64']
112
+ )
75
113
 
76
- return response
114
+ compute_state['status']['stdout_and_stderr_packages_b64'] = []
77
115
 
116
+ if current_status['status_updates']:
117
+ compute_state['previous_status_updates'].extend(current_status['status_updates'])
118
+ compute_state['status']['status_updates'] = []
78
119
 
79
- @app.route('/v1/job/<job_id>/result/')
80
- def result(job_id):
81
- if webserver_utils.JOB_ID_TO_COMPUTE_STATE_DICT[job_id]['result']:
82
- result_data = webserver_utils.JOB_ID_TO_COMPUTE_STATE_DICT[job_id]['result']
83
- # remove result from state dict, so we know the user has started the download
84
- webserver_utils.JOB_ID_TO_COMPUTE_STATE_DICT[job_id]['result'] = None
85
- response = Response(result_data)
86
- response.call_on_close(lambda: webserver_utils.finalize_and_clean_up_compute_job(job_id))
120
+ if return_full_logs:
121
+ response_data['streamed_logs_packages_b64'] = compute_state['streamed_logs_packages_b64']
122
+ response_data['previous_status_updates'] = compute_state['previous_status_updates']
87
123
 
88
- return response
124
+ return jsonify(response_data)
89
125
 
90
- else:
91
- return '', 404
126
+
127
+ @app.route('/v1/job/<job_id>/result/')
128
+ def result():
129
+ return 410, 'This endpoint is no longer available. Please update pybiolib by running `pip3 install -U pybiolib`'
92
130
 
93
131
 
94
132
  def send_package_to_compute_process(job_id, package_bytes):
95
- message_queue = webserver_utils.JOB_ID_TO_COMPUTE_STATE_DICT[job_id]['messages_to_send_queue']
133
+ compute_state = get_job_compute_state_or_404(job_id)
134
+ message_queue = compute_state['messages_to_send_queue']
96
135
  message_queue.put(package_bytes)
97
136
 
98
137
 
99
- def start_webserver(port, host):
138
+ def start_webserver(
139
+ host: str,
140
+ port: int,
141
+ tls_pem_certificate_path: Optional[str] = None,
142
+ tls_pem_key_path: Optional[str] = None,
143
+ ) -> None:
100
144
  def worker_exit(server, worker): # pylint: disable=unused-argument
101
- active_compute_states = list(
102
- webserver_utils.JOB_ID_TO_COMPUTE_STATE_DICT.values()) + webserver_utils.UNASSIGNED_COMPUTE_PROCESSES
145
+ active_compute_states = (
146
+ list(webserver_utils.JOB_ID_TO_COMPUTE_STATE_DICT.values()) + webserver_utils.UNASSIGNED_COMPUTE_PROCESSES
147
+ )
103
148
  logger.debug(f'Sending terminate signal to {len(active_compute_states)} compute processes')
104
149
  if active_compute_states:
105
150
  for compute_state in active_compute_states:
106
151
  if compute_state['worker_thread']:
107
152
  compute_state['worker_thread'].terminate()
108
153
  time.sleep(2)
109
- return
154
+
155
+ if utils.IS_RUNNING_IN_CLOUD:
156
+ try:
157
+ logger_no_user_data.debug('Stopping ComputeNodeResultsProxy...')
158
+ ComputeNodeResultsProxy.stop_proxy()
159
+ except BaseException:
160
+ logger_no_user_data.exception('Failed to stop ComputeNodeResultsProxy')
161
+
162
+ try:
163
+ logger_no_user_data.debug(f'Removing Docker network {BIOLIB_PROXY_NETWORK_NAME}')
164
+ docker_client = BiolibDockerClient.get_docker_client()
165
+ biolib_proxy_network: Network = docker_client.networks.get(BIOLIB_PROXY_NETWORK_NAME)
166
+ biolib_proxy_network.remove()
167
+ logger_no_user_data.debug(f'Successfully removed Docker network {BIOLIB_PROXY_NETWORK_NAME}')
168
+ except BaseException:
169
+ logger_no_user_data.exception(f'Failed to clean up network {BIOLIB_PROXY_NETWORK_NAME}')
110
170
 
111
171
  def post_fork(server, worker): # pylint: disable=unused-argument
112
172
  logger.info('Started compute node')
@@ -117,10 +177,33 @@ def start_webserver(port, host):
117
177
  utils.IS_DEV = config['is_dev']
118
178
  BiolibApiClient.initialize(config['base_url'])
119
179
 
120
- if utils.BIOLIB_IS_RUNNING_IN_ENCLAVE:
121
- start_enclave_remote_hosts(config)
122
-
123
- CloudUtils.initialize()
180
+ biolib_proxy_network: Optional[Network] = None
181
+ try:
182
+ logger_no_user_data.debug(f'Creating Docker network {BIOLIB_PROXY_NETWORK_NAME}')
183
+ docker_client = BiolibDockerClient.get_docker_client()
184
+ biolib_proxy_network = docker_client.networks.create(
185
+ name=BIOLIB_PROXY_NETWORK_NAME,
186
+ internal=False,
187
+ driver='bridge',
188
+ )
189
+ logger_no_user_data.debug(f'Successfully created Docker network {BIOLIB_PROXY_NETWORK_NAME}')
190
+ except BaseException:
191
+ logger_no_user_data.exception(f'Failed to create Docker network {BIOLIB_PROXY_NETWORK_NAME}')
192
+
193
+ if biolib_proxy_network:
194
+ try:
195
+ logger_no_user_data.debug('Starting ComputeNodeResultsProxy...')
196
+ ComputeNodeResultsProxy.start_proxy(tls_pem_certificate_path, tls_pem_key_path)
197
+ except BaseException:
198
+ logger_no_user_data.exception('Failed to start ComputeNodeResultsProxy')
199
+
200
+ CloudUtils.initialize()
201
+ webserver_utils.start_auto_shutdown_timer()
202
+ else:
203
+ logger_no_user_data.error(
204
+ f'Docker network {BIOLIB_PROXY_NETWORK_NAME} was not created, shutting down...'
205
+ )
206
+ CloudUtils.deregister_and_shutdown()
124
207
 
125
208
  if logger.level == TRACE:
126
209
  gunicorn_log_level_name = 'DEBUG'
@@ -133,12 +216,15 @@ def start_webserver(port, host):
133
216
 
134
217
  options = {
135
218
  'bind': f'{host}:{port}',
136
- 'workers': 1,
137
- 'post_fork': post_fork,
138
- 'worker_exit': worker_exit,
139
- 'timeout': '300',
219
+ 'certfile': tls_pem_certificate_path,
140
220
  'graceful_timeout': 4,
221
+ 'keyfile': tls_pem_key_path,
141
222
  'loglevel': gunicorn_log_level_name,
223
+ 'post_fork': post_fork,
224
+ 'ssl_version': 'TLSv1_2',
225
+ 'timeout': '7200', # Reduce to 300 when frontend no longer downloads from webserver
226
+ 'worker_exit': worker_exit,
227
+ 'workers': 1,
142
228
  }
143
229
 
144
230
  GunicornFlaskApplication(app, options).run()
@@ -3,21 +3,17 @@ from biolib.typing_utils import TypedDict
3
3
 
4
4
  class ComputeNodeInfo(TypedDict):
5
5
  auth_token: str
6
- public_id: str
7
6
  ip_address: str
7
+ public_id: str
8
+ pybiolib_version: str
8
9
 
9
10
 
10
11
  class ShutdownTimes(TypedDict):
11
12
  auto_shutdown_time_in_seconds: int
12
- job_max_runtime_shutdown_time_in_seconds: int
13
13
 
14
14
 
15
15
  class WebserverConfig(TypedDict):
16
16
  base_url: str
17
- ecr_region_name: str
18
- max_docker_image_cache_size_bytes: int
19
- s3_general_storage_bucket_name: str
20
- s3_lfs_bucket_name: str
21
17
  compute_node_info: ComputeNodeInfo
22
18
  is_dev: bool
23
19
  shutdown_times: ShutdownTimes
@@ -1,6 +1,11 @@
1
1
  import time
2
+ from datetime import datetime, timedelta
3
+ from multiprocessing import Process
4
+
5
+ from werkzeug.exceptions import NotFound
2
6
 
3
7
  from biolib import utils
8
+ from biolib.biolib_logging import logger_no_user_data
4
9
  from biolib.compute_node.cloud_utils import CloudUtils
5
10
  from biolib.typing_utils import Dict, List
6
11
  from biolib.compute_node.webserver.worker_thread import WorkerThread
@@ -10,10 +15,12 @@ JOB_ID_TO_COMPUTE_STATE_DICT: Dict = {}
10
15
  UNASSIGNED_COMPUTE_PROCESSES: List = []
11
16
 
12
17
 
13
- def finalize_and_clean_up_compute_job(job_id: str):
14
- JOB_ID_TO_COMPUTE_STATE_DICT.pop(job_id)
15
- if utils.IS_RUNNING_IN_CLOUD:
16
- CloudUtils.finish_job(job_id)
18
+ def get_job_compute_state_or_404(job_id: str):
19
+ compute_state = JOB_ID_TO_COMPUTE_STATE_DICT.get(job_id)
20
+ if compute_state:
21
+ return compute_state
22
+
23
+ raise NotFound('Job not found')
17
24
 
18
25
 
19
26
  def get_compute_state(unassigned_compute_processes):
@@ -26,6 +33,12 @@ def get_compute_state(unassigned_compute_processes):
26
33
  def start_compute_process(unassigned_compute_processes):
27
34
  compute_state = {
28
35
  'job_id': None,
36
+ 'job': None,
37
+ 'cloud_job_id': None,
38
+ 'cloud_job': None,
39
+ 'streamed_logs_packages_b64': [],
40
+ 'previous_status_updates': [],
41
+ 'is_completed': False,
29
42
  'status': {
30
43
  'status_updates': [
31
44
  {
@@ -33,21 +46,16 @@ def start_compute_process(unassigned_compute_processes):
33
46
  'log_message': 'Initializing'
34
47
  }
35
48
  ],
49
+ 'stdout_and_stderr_packages_b64': []
36
50
  },
37
- 'result': None,
38
- 'attestation_document': b'',
51
+ 'progress': 0,
52
+ 'job_temporary_dir': '',
39
53
  'received_messages_queue': None,
40
54
  'messages_to_send_queue': None,
41
55
  'worker_process': None
42
56
  }
43
57
 
44
58
  WorkerThread(compute_state).start()
45
-
46
- while True:
47
- if compute_state['attestation_document']:
48
- break
49
- time.sleep(0.25)
50
-
51
59
  unassigned_compute_processes.append(compute_state)
52
60
 
53
61
 
@@ -62,3 +70,60 @@ def validate_saved_job(saved_job):
62
70
  return False
63
71
 
64
72
  return True
73
+
74
+
75
+ def start_auto_shutdown_timer() -> None:
76
+ if not utils.IS_RUNNING_IN_CLOUD:
77
+ logger_no_user_data.error('Not running in cloud so skipping auto shutdown time start')
78
+ return
79
+
80
+ update_auto_shutdown_time()
81
+
82
+ timer = CloudAutoShutdownTimer()
83
+ timer.start()
84
+ logger_no_user_data.debug(f"Started auto shutdown timer on pid: {timer.pid}")
85
+
86
+
87
+ def update_auto_shutdown_time() -> None:
88
+ if not utils.IS_RUNNING_IN_CLOUD:
89
+ logger_no_user_data.error('Not running in cloud so skipping auto shutdown time update')
90
+ return
91
+
92
+ webserver_config = CloudUtils.get_webserver_config()
93
+ auto_shutdown_buffer = webserver_config['shutdown_times']['auto_shutdown_time_in_seconds'] # pylint: disable=unsubscriptable-object
94
+
95
+ if JOB_ID_TO_COMPUTE_STATE_DICT:
96
+ highest_max_job_compute_state = max(
97
+ JOB_ID_TO_COMPUTE_STATE_DICT.values(),
98
+ key=lambda compute_state: compute_state['cloud_job']['max_runtime_in_seconds'] # type: ignore
99
+ )
100
+ highest_max_job_runtime = highest_max_job_compute_state['cloud_job']['max_runtime_in_seconds']
101
+ else:
102
+ highest_max_job_runtime = 0
103
+
104
+ auto_shutdown_time = datetime.now() + timedelta(seconds=highest_max_job_runtime) + timedelta(
105
+ seconds=auto_shutdown_buffer
106
+ )
107
+ auto_shutdown_time_isoformat = datetime.isoformat(auto_shutdown_time)
108
+
109
+ with open(CloudAutoShutdownTimer.SHUTDOWN_TIMESTAMP_FILE_PATH, 'w') as auto_shutdown_time_file:
110
+ auto_shutdown_time_file.write(auto_shutdown_time_isoformat)
111
+
112
+ logger_no_user_data.debug(f'Extending auto shutdown timer to: {auto_shutdown_time_isoformat}')
113
+
114
+
115
+ class CloudAutoShutdownTimer(Process):
116
+ SHUTDOWN_TIMESTAMP_FILE_PATH = '/tmp/auto_shutdown_time.timestamp'
117
+
118
+ def _get_auto_shutdown_time(self) -> datetime:
119
+ with open(self.SHUTDOWN_TIMESTAMP_FILE_PATH, mode='r') as file:
120
+ return datetime.fromisoformat(file.read())
121
+
122
+ def run(self) -> None:
123
+ auto_shutdown_time = self._get_auto_shutdown_time()
124
+ while datetime.now() < auto_shutdown_time:
125
+ time.sleep(60)
126
+ auto_shutdown_time = self._get_auto_shutdown_time()
127
+
128
+ logger_no_user_data.debug(f'Hit auto shutdown timer since {datetime.now()} > {auto_shutdown_time}')
129
+ CloudUtils.deregister_and_shutdown()