Mercurial > repos > shellac > sam_consensus_v3
comparison env/lib/python3.9/site-packages/cwltool/singularity.py @ 0:4f3585e2f14b draft default tip
"planemo upload commit 60cee0fc7c0cda8592644e1aad72851dec82c959"
author | shellac |
---|---|
date | Mon, 22 Mar 2021 18:12:50 +0000 |
parents | |
children |
comparison
equal
deleted
inserted
replaced
-1:000000000000 | 0:4f3585e2f14b |
---|---|
1 """Support for executing Docker containers using the Singularity 2.x engine.""" | |
2 | |
3 import os | |
4 import os.path | |
5 import re | |
6 import shutil | |
7 import sys | |
8 from distutils import spawn | |
9 from subprocess import ( # nosec | |
10 DEVNULL, | |
11 PIPE, | |
12 Popen, | |
13 TimeoutExpired, | |
14 check_call, | |
15 check_output, | |
16 ) | |
17 from typing import Callable, Dict, List, MutableMapping, Optional, Tuple, cast | |
18 | |
19 from schema_salad.sourceline import SourceLine | |
20 | |
21 from .builder import Builder | |
22 from .context import RuntimeContext | |
23 from .errors import UnsupportedRequirement, WorkflowException | |
24 from .job import ContainerCommandLineJob | |
25 from .loghandler import _logger | |
26 from .pathmapper import MapperEnt, PathMapper | |
27 from .utils import ( | |
28 CWLObjectType, | |
29 create_tmp_dir, | |
30 docker_windows_path_adjust, | |
31 ensure_non_writable, | |
32 ensure_writable, | |
33 ) | |
34 | |
35 _USERNS = None # type: Optional[bool] | |
36 _SINGULARITY_VERSION = "" | |
37 | |
38 | |
39 def _singularity_supports_userns() -> bool: | |
40 global _USERNS # pylint: disable=global-statement | |
41 if _USERNS is None: | |
42 try: | |
43 hello_image = os.path.join(os.path.dirname(__file__), "hello.simg") | |
44 result = Popen( # nosec | |
45 ["singularity", "exec", "--userns", hello_image, "true"], | |
46 stderr=PIPE, | |
47 stdout=DEVNULL, | |
48 universal_newlines=True, | |
49 ).communicate(timeout=60)[1] | |
50 _USERNS = ( | |
51 "No valid /bin/sh" in result | |
52 or "/bin/sh doesn't exist in container" in result | |
53 or "executable file not found in" in result | |
54 ) | |
55 except TimeoutExpired: | |
56 _USERNS = False | |
57 return _USERNS | |
58 | |
59 | |
60 def get_version() -> str: | |
61 global _SINGULARITY_VERSION # pylint: disable=global-statement | |
62 if not _SINGULARITY_VERSION: | |
63 _SINGULARITY_VERSION = check_output( # nosec | |
64 ["singularity", "--version"], universal_newlines=True | |
65 ) | |
66 if _SINGULARITY_VERSION.startswith("singularity version "): | |
67 _SINGULARITY_VERSION = _SINGULARITY_VERSION[20:] | |
68 return _SINGULARITY_VERSION | |
69 | |
70 | |
71 def is_version_2_6() -> bool: | |
72 return get_version().startswith("2.6") | |
73 | |
74 | |
75 def is_version_3_or_newer() -> bool: | |
76 return int(get_version()[0]) >= 3 | |
77 | |
78 | |
79 def is_version_3_1_or_newer() -> bool: | |
80 version = get_version().split(".") | |
81 return int(version[0]) >= 4 or (int(version[0]) == 3 and int(version[1]) >= 1) | |
82 | |
83 | |
84 def _normalize_image_id(string: str) -> str: | |
85 return string.replace("/", "_") + ".img" | |
86 | |
87 | |
88 def _normalize_sif_id(string: str) -> str: | |
89 return string.replace("/", "_") + ".sif" | |
90 | |
91 | |
92 class SingularityCommandLineJob(ContainerCommandLineJob): | |
93 def __init__( | |
94 self, | |
95 builder: Builder, | |
96 joborder: CWLObjectType, | |
97 make_path_mapper: Callable[..., PathMapper], | |
98 requirements: List[CWLObjectType], | |
99 hints: List[CWLObjectType], | |
100 name: str, | |
101 ) -> None: | |
102 """Builder for invoking the Singularty software container engine.""" | |
103 super().__init__(builder, joborder, make_path_mapper, requirements, hints, name) | |
104 | |
105 @staticmethod | |
106 def get_image( | |
107 dockerRequirement: Dict[str, str], | |
108 pull_image: bool, | |
109 force_pull: bool = False, | |
110 ) -> bool: | |
111 """ | |
112 Acquire the software container image in the specified dockerRequirement. | |
113 | |
114 Uses Singularity and returns the success as a bool. Updates the | |
115 provided dockerRequirement with the specific dockerImageId to the full | |
116 path of the local image, if found. Likewise the | |
117 dockerRequirement['dockerPull'] is updated to a docker:// URI if needed. | |
118 """ | |
119 found = False | |
120 | |
121 candidates = [] | |
122 | |
123 cache_folder = None | |
124 if "CWL_SINGULARITY_CACHE" in os.environ: | |
125 cache_folder = os.environ["CWL_SINGULARITY_CACHE"] | |
126 elif is_version_2_6() and "SINGULARITY_PULLFOLDER" in os.environ: | |
127 cache_folder = os.environ["SINGULARITY_PULLFOLDER"] | |
128 | |
129 if ( | |
130 "dockerImageId" not in dockerRequirement | |
131 and "dockerPull" in dockerRequirement | |
132 ): | |
133 match = re.search( | |
134 pattern=r"([a-z]*://)", string=dockerRequirement["dockerPull"] | |
135 ) | |
136 img_name = _normalize_image_id(dockerRequirement["dockerPull"]) | |
137 candidates.append(img_name) | |
138 if is_version_3_or_newer(): | |
139 sif_name = _normalize_sif_id(dockerRequirement["dockerPull"]) | |
140 candidates.append(sif_name) | |
141 dockerRequirement["dockerImageId"] = sif_name | |
142 else: | |
143 dockerRequirement["dockerImageId"] = img_name | |
144 if not match: | |
145 dockerRequirement["dockerPull"] = ( | |
146 "docker://" + dockerRequirement["dockerPull"] | |
147 ) | |
148 elif "dockerImageId" in dockerRequirement: | |
149 if os.path.isfile(dockerRequirement["dockerImageId"]): | |
150 found = True | |
151 candidates.append(dockerRequirement["dockerImageId"]) | |
152 candidates.append(_normalize_image_id(dockerRequirement["dockerImageId"])) | |
153 if is_version_3_or_newer(): | |
154 candidates.append(_normalize_sif_id(dockerRequirement["dockerPull"])) | |
155 | |
156 targets = [os.getcwd()] | |
157 if "CWL_SINGULARITY_CACHE" in os.environ: | |
158 targets.append(os.environ["CWL_SINGULARITY_CACHE"]) | |
159 if is_version_2_6() and "SINGULARITY_PULLFOLDER" in os.environ: | |
160 targets.append(os.environ["SINGULARITY_PULLFOLDER"]) | |
161 for target in targets: | |
162 for dirpath, _subdirs, files in os.walk(target): | |
163 for entry in files: | |
164 if entry in candidates: | |
165 path = os.path.join(dirpath, entry) | |
166 if os.path.isfile(path): | |
167 _logger.info( | |
168 "Using local copy of Singularity image found in %s", | |
169 dirpath, | |
170 ) | |
171 dockerRequirement["dockerImageId"] = path | |
172 found = True | |
173 if (force_pull or not found) and pull_image: | |
174 cmd = [] # type: List[str] | |
175 if "dockerPull" in dockerRequirement: | |
176 if cache_folder: | |
177 env = os.environ.copy() | |
178 if is_version_2_6(): | |
179 env["SINGULARITY_PULLFOLDER"] = cache_folder | |
180 cmd = [ | |
181 "singularity", | |
182 "pull", | |
183 "--force", | |
184 "--name", | |
185 dockerRequirement["dockerImageId"], | |
186 str(dockerRequirement["dockerPull"]), | |
187 ] | |
188 else: | |
189 cmd = [ | |
190 "singularity", | |
191 "pull", | |
192 "--force", | |
193 "--name", | |
194 "{}/{}".format( | |
195 cache_folder, dockerRequirement["dockerImageId"] | |
196 ), | |
197 str(dockerRequirement["dockerPull"]), | |
198 ] | |
199 | |
200 _logger.info(str(cmd)) | |
201 check_call(cmd, env=env, stdout=sys.stderr) # nosec | |
202 dockerRequirement["dockerImageId"] = "{}/{}".format( | |
203 cache_folder, dockerRequirement["dockerImageId"] | |
204 ) | |
205 found = True | |
206 else: | |
207 cmd = [ | |
208 "singularity", | |
209 "pull", | |
210 "--force", | |
211 "--name", | |
212 str(dockerRequirement["dockerImageId"]), | |
213 str(dockerRequirement["dockerPull"]), | |
214 ] | |
215 _logger.info(str(cmd)) | |
216 check_call(cmd, stdout=sys.stderr) # nosec | |
217 found = True | |
218 | |
219 elif "dockerFile" in dockerRequirement: | |
220 raise WorkflowException( | |
221 SourceLine(dockerRequirement, "dockerFile").makeError( | |
222 "dockerFile is not currently supported when using the " | |
223 "Singularity runtime for Docker containers." | |
224 ) | |
225 ) | |
226 elif "dockerLoad" in dockerRequirement: | |
227 if is_version_3_1_or_newer(): | |
228 if "dockerImageId" in dockerRequirement: | |
229 name = "{}.sif".format(dockerRequirement["dockerImageId"]) | |
230 else: | |
231 name = "{}.sif".format(dockerRequirement["dockerLoad"]) | |
232 cmd = [ | |
233 "singularity", | |
234 "build", | |
235 name, | |
236 "docker-archive://{}".format(dockerRequirement["dockerLoad"]), | |
237 ] | |
238 _logger.info(str(cmd)) | |
239 check_call(cmd, stdout=sys.stderr) # nosec | |
240 found = True | |
241 dockerRequirement["dockerImageId"] = name | |
242 raise WorkflowException( | |
243 SourceLine(dockerRequirement, "dockerLoad").makeError( | |
244 "dockerLoad is not currently supported when using the " | |
245 "Singularity runtime (version less than 3.1) for Docker containers." | |
246 ) | |
247 ) | |
248 elif "dockerImport" in dockerRequirement: | |
249 raise WorkflowException( | |
250 SourceLine(dockerRequirement, "dockerImport").makeError( | |
251 "dockerImport is not currently supported when using the " | |
252 "Singularity runtime for Docker containers." | |
253 ) | |
254 ) | |
255 | |
256 return found | |
257 | |
258 def get_from_requirements( | |
259 self, | |
260 r: CWLObjectType, | |
261 pull_image: bool, | |
262 force_pull: bool, | |
263 tmp_outdir_prefix: str, | |
264 ) -> Optional[str]: | |
265 """ | |
266 Return the filename of the Singularity image. | |
267 | |
268 (e.g. hello-world-latest.{img,sif}). | |
269 """ | |
270 if not bool(spawn.find_executable("singularity")): | |
271 raise WorkflowException("singularity executable is not available") | |
272 | |
273 if not self.get_image(cast(Dict[str, str], r), pull_image, force_pull): | |
274 raise WorkflowException( | |
275 "Container image {} not found".format(r["dockerImageId"]) | |
276 ) | |
277 | |
278 return os.path.abspath(cast(str, r["dockerImageId"])) | |
279 | |
280 @staticmethod | |
281 def append_volume( | |
282 runtime: List[str], source: str, target: str, writable: bool = False | |
283 ) -> None: | |
284 runtime.append("--bind") | |
285 runtime.append( | |
286 "{}:{}:{}".format( | |
287 docker_windows_path_adjust(source), | |
288 docker_windows_path_adjust(target), | |
289 "rw" if writable else "ro", | |
290 ) | |
291 ) | |
292 | |
293 def add_file_or_directory_volume( | |
294 self, runtime: List[str], volume: MapperEnt, host_outdir_tgt: Optional[str] | |
295 ) -> None: | |
296 if host_outdir_tgt is not None: | |
297 # workaround for lack of overlapping mounts in Singularity | |
298 # revert to daa923d5b0be3819b6ed0e6440e7193e65141052 | |
299 # once https://github.com/sylabs/singularity/issues/1607 | |
300 # is fixed | |
301 if volume.type == "File": | |
302 shutil.copy(volume.resolved, host_outdir_tgt) | |
303 else: | |
304 shutil.copytree(volume.resolved, host_outdir_tgt) | |
305 ensure_non_writable(host_outdir_tgt) | |
306 elif not volume.resolved.startswith("_:"): | |
307 self.append_volume(runtime, volume.resolved, volume.target) | |
308 | |
309 def add_writable_file_volume( | |
310 self, | |
311 runtime: List[str], | |
312 volume: MapperEnt, | |
313 host_outdir_tgt: Optional[str], | |
314 tmpdir_prefix: str, | |
315 ) -> None: | |
316 if host_outdir_tgt is not None: | |
317 # workaround for lack of overlapping mounts in Singularity | |
318 # revert to daa923d5b0be3819b6ed0e6440e7193e65141052 | |
319 # once https://github.com/sylabs/singularity/issues/1607 | |
320 # is fixed | |
321 if self.inplace_update: | |
322 try: | |
323 os.link(os.path.realpath(volume.resolved), host_outdir_tgt) | |
324 except os.error: | |
325 shutil.copy(volume.resolved, host_outdir_tgt) | |
326 else: | |
327 shutil.copy(volume.resolved, host_outdir_tgt) | |
328 ensure_writable(host_outdir_tgt) | |
329 elif self.inplace_update: | |
330 self.append_volume(runtime, volume.resolved, volume.target, writable=True) | |
331 ensure_writable(volume.resolved) | |
332 else: | |
333 file_copy = os.path.join( | |
334 create_tmp_dir(tmpdir_prefix), | |
335 os.path.basename(volume.resolved), | |
336 ) | |
337 shutil.copy(volume.resolved, file_copy) | |
338 # volume.resolved = file_copy | |
339 self.append_volume(runtime, file_copy, volume.target, writable=True) | |
340 ensure_writable(file_copy) | |
341 | |
342 def add_writable_directory_volume( | |
343 self, | |
344 runtime: List[str], | |
345 volume: MapperEnt, | |
346 host_outdir_tgt: Optional[str], | |
347 tmpdir_prefix: str, | |
348 ) -> None: | |
349 if volume.resolved.startswith("_:"): | |
350 if host_outdir_tgt is not None: | |
351 new_dir = host_outdir_tgt | |
352 else: | |
353 new_dir = os.path.join( | |
354 create_tmp_dir(tmpdir_prefix), | |
355 os.path.basename(volume.resolved), | |
356 ) | |
357 os.makedirs(new_dir) | |
358 else: | |
359 if host_outdir_tgt is not None: | |
360 # workaround for lack of overlapping mounts in Singularity | |
361 # revert to daa923d5b0be3819b6ed0e6440e7193e65141052 | |
362 # once https://github.com/sylabs/singularity/issues/1607 | |
363 # is fixed | |
364 shutil.copytree(volume.resolved, host_outdir_tgt) | |
365 ensure_writable(host_outdir_tgt) | |
366 else: | |
367 if not self.inplace_update: | |
368 dir_copy = os.path.join( | |
369 create_tmp_dir(tmpdir_prefix), | |
370 os.path.basename(volume.resolved), | |
371 ) | |
372 shutil.copytree(volume.resolved, dir_copy) | |
373 source = dir_copy | |
374 # volume.resolved = dir_copy | |
375 else: | |
376 source = volume.resolved | |
377 self.append_volume(runtime, source, volume.target, writable=True) | |
378 ensure_writable(source) | |
379 | |
380 def create_runtime( | |
381 self, env: MutableMapping[str, str], runtime_context: RuntimeContext | |
382 ) -> Tuple[List[str], Optional[str]]: | |
383 """Return the Singularity runtime list of commands and options.""" | |
384 any_path_okay = self.builder.get_requirement("DockerRequirement")[1] or False | |
385 runtime = [ | |
386 "singularity", | |
387 "--quiet", | |
388 "exec", | |
389 "--contain", | |
390 "--ipc", | |
391 ] | |
392 if _singularity_supports_userns(): | |
393 runtime.append("--userns") | |
394 else: | |
395 runtime.append("--pid") | |
396 if is_version_3_1_or_newer(): | |
397 runtime.append("--home") | |
398 runtime.append( | |
399 "{}:{}".format( | |
400 docker_windows_path_adjust(os.path.realpath(self.outdir)), | |
401 self.builder.outdir, | |
402 ) | |
403 ) | |
404 else: | |
405 runtime.append("--bind") | |
406 runtime.append( | |
407 "{}:{}:rw".format( | |
408 docker_windows_path_adjust(os.path.realpath(self.outdir)), | |
409 self.builder.outdir, | |
410 ) | |
411 ) | |
412 runtime.append("--bind") | |
413 tmpdir = "/tmp" # nosec | |
414 runtime.append( | |
415 "{}:{}:rw".format( | |
416 docker_windows_path_adjust(os.path.realpath(self.tmpdir)), tmpdir | |
417 ) | |
418 ) | |
419 | |
420 self.add_volumes( | |
421 self.pathmapper, | |
422 runtime, | |
423 any_path_okay=True, | |
424 secret_store=runtime_context.secret_store, | |
425 tmpdir_prefix=runtime_context.tmpdir_prefix, | |
426 ) | |
427 if self.generatemapper is not None: | |
428 self.add_volumes( | |
429 self.generatemapper, | |
430 runtime, | |
431 any_path_okay=any_path_okay, | |
432 secret_store=runtime_context.secret_store, | |
433 tmpdir_prefix=runtime_context.tmpdir_prefix, | |
434 ) | |
435 | |
436 runtime.append("--pwd") | |
437 runtime.append("%s" % (docker_windows_path_adjust(self.builder.outdir))) | |
438 | |
439 if runtime_context.custom_net: | |
440 raise UnsupportedRequirement( | |
441 "Singularity implementation does not support custom networking" | |
442 ) | |
443 elif runtime_context.disable_net: | |
444 runtime.append("--net") | |
445 | |
446 env["SINGULARITYENV_TMPDIR"] = tmpdir | |
447 env["SINGULARITYENV_HOME"] = self.builder.outdir | |
448 | |
449 for name, value in self.environment.items(): | |
450 env[f"SINGULARITYENV_{name}"] = str(value) | |
451 return (runtime, None) |