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) |
