diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 55d524d671..6a222f205b 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -6,6 +6,8 @@ before_script: - docker info - cat /etc/hosts - export PYTHONIOENCODING=utf-8 + - mkdir -p ~/.kube && cp "$GITLAB_SECRET_FILE_KUBE_CONFIG" ~/.kube/config + - mkdir -p ~/.aws && cp "$GITLAB_SECRET_FILE_AWS_CREDENTIALS" ~/.aws/credentials after_script: # We need to clean up any files that Toil may have made via Docker that # aren't deletable by the Gitlab user. If we don't do this, Gitlab will try @@ -29,11 +31,6 @@ py2_batch_systems: - pwd - apt update && DEBIAN_FRONTEND=noninteractive apt install -y tzdata && apt install -y jq - virtualenv -p python2.7 venv && . venv/bin/activate && make prepare && make develop extras=[all] && pip install htcondor awscli==1.16.272 - # Get Kubernetes credentials - - mkdir -p ~/.kube - - cp "$GITLAB_SECRET_FILE_KUBE_CONFIG" ~/.kube/config - - mkdir -p ~/.aws - - cp "$GITLAB_SECRET_FILE_AWS_CREDENTIALS" ~/.aws/credentials - python -m pytest -r s src/toil/test/batchSystems/batchSystemTest.py - python -m pytest -r s src/toil/test/mesos/MesosDataStructuresTest.py @@ -92,8 +89,6 @@ py2_integration_jobstore: - export TOIL_AWS_ZONE=us-west-2a # This reads GITLAB_SECRET_FILE_SSH_KEYS - python setup_gitlab_ssh.py - - mkdir -p ~/.aws - - cp "$GITLAB_SECRET_FILE_AWS_CREDENTIALS" ~/.aws/credentials - python -m pytest src/toil/test/jobStores/jobStoreTest.py py2_integration_sort: @@ -107,8 +102,6 @@ py2_integration_sort: - export TOIL_AWS_ZONE=us-west-2a # This reads GITLAB_SECRET_FILE_SSH_KEYS - python setup_gitlab_ssh.py - - mkdir -p ~/.aws - - cp "$GITLAB_SECRET_FILE_AWS_CREDENTIALS" ~/.aws/credentials - python -m pytest src/toil/test/sort/sortTest.py - python -m pytest src/toil/test/provisioners/clusterScalerTest.py @@ -123,8 +116,6 @@ py2_integration_sort: # - export TOIL_AWS_ZONE=us-west-2a # # This reads GITLAB_SECRET_FILE_SSH_KEYS # - python setup_gitlab_ssh.py -# - mkdir -p ~/.aws -# - cp "$GITLAB_SECRET_FILE_AWS_CREDENTIALS" ~/.aws/credentials # - python -m pytest src/toil/test/provisioners/aws/awsProvisionerTest.py @@ -171,6 +162,18 @@ py3_main: - python -m pytest src/toil/test/src - python -m pytest src/toil/test/utils +py3_appliance_build: + stage: main_tests + script: + - pwd + - apt update && DEBIAN_FRONTEND=noninteractive apt install -y tzdata && apt install -y jq + - virtualenv -p python3.6 venv && . venv/bin/activate && make prepare && make develop extras=[all] && pip install htcondor awscli==1.16.272 + # This reads GITLAB_SECRET_FILE_QUAY_CREDENTIALS + - python2.7 setup_gitlab_docker.py + - export TOIL_APPLIANCE_SELF=quay.io/ucsc_cgl/toil:$(python version_template.py dockerTag) + - echo $TOIL_APPLIANCE_SELF + - make push_docker + #py3_integration: # stage: integration # script: @@ -182,6 +185,4 @@ py3_main: # - export TOIL_AWS_ZONE=us-west-2a # # This reads GITLAB_SECRET_FILE_SSH_KEYS # - python setup_gitlab_ssh.py -# - mkdir -p ~/.aws -# - cp "$GITLAB_SECRET_FILE_AWS_CREDENTIALS" ~/.aws/credentials # - python -m pytest src/toil/test/jobStores/jobStoreTest.py diff --git a/README.rst b/README.rst index 712dad4e3e..34ff55164a 100644 --- a/README.rst +++ b/README.rst @@ -7,13 +7,14 @@ programming. * Check the `website`_ for a description of Toil and its features. * Full documentation for the latest stable release can be found at `Read the Docs`_. +* Please subscribe to low-volume `announce`_ mailing list so we keep you informed +* Google Groups discussion `forum`_ * See our occasional `blog`_ for tutorials. -* Google Groups discussion `forum`_ and videochat `invite list`_. .. _website: http://toil.ucsc-cgl.org/ .. _Read the Docs: https://toil.readthedocs.io/en/latest +.. _announce: https://groups.google.com/forum/#!forum/toil-announce .. _forum: https://groups.google.com/forum/#!forum/toil-community -.. _invite list: https://groups.google.com/forum/#!forum/toil-community-videochats .. _blog: https://toilpipelines.wordpress.com/ .. image:: https://badges.gitter.im/bd2k-genomics-toil/Lobby.svg diff --git a/docker/Dockerfile.py b/docker/Dockerfile.py index f70246589e..7acee3263e 100644 --- a/docker/Dockerfile.py +++ b/docker/Dockerfile.py @@ -14,11 +14,16 @@ from __future__ import print_function import os +import sys import textwrap applianceSelf = os.environ['TOIL_APPLIANCE_SELF'] sdistName = os.environ['_TOIL_SDIST_NAME'] +# Make sure to install packages into the pip for the version of Python we are +# building for. +pip = 'pip{}'.format(sys.version_info[0]) + dependencies = ' '.join(['libffi-dev', # For client side encryption for extras with PyNACL 'python3.6', @@ -56,7 +61,7 @@ def heredoc(s): Run toil .py --help to see all options for running your workflow. For more information see http://toil.readthedocs.io/en/latest/ - Copyright (C) 2015-2018 Regents of the University of California + Copyright (C) 2015-2020 Regents of the University of California Version: {applianceSelf} @@ -85,8 +90,9 @@ def heredoc(s): apt-get clean && \ rm -rf /var/lib/apt/lists/* - RUN wget https://dl.google.com/go/go1.13.3.linux-amd64.tar.gz && \ - tar xvf go1.13.3.linux-amd64.tar.gz && \ + RUN wget -q https://dl.google.com/go/go1.13.3.linux-amd64.tar.gz && \ + tar xf go1.13.3.linux-amd64.tar.gz && \ + rm go1.13.3.linux-amd64.tar.gz && \ mv go/bin/* /usr/bin/ && \ mv go /usr/local/ @@ -110,16 +116,16 @@ def heredoc(s): RUN chmod 777 /usr/bin/waitForKey.sh && chmod 777 /usr/bin/customDockerInit.sh # The stock pip is too old and can't install from sdist with extras - RUN pip install --upgrade pip==9.0.1 + RUN {pip} install --upgrade pip==9.0.1 # Default setuptools is too old - RUN pip install --upgrade setuptools==36.5.0 + RUN {pip} install --upgrade setuptools==36.5.0 # Include virtualenv, as it is still the recommended way to deploy pipelines - RUN pip install --upgrade virtualenv==15.0.3 + RUN {pip} install --upgrade virtualenv==15.0.3 # Install s3am (--never-download prevents silent upgrades to pip, wheel and setuptools) - RUN virtualenv --never-download /home/s3am \ + RUN virtualenv --python python3 --never-download /home/s3am \ && /home/s3am/bin/pip install s3am==2.0 \ && ln -s /home/s3am/bin/s3am /usr/local/bin/ @@ -129,7 +135,7 @@ def heredoc(s): && chmod u+x /usr/local/bin/docker # Fix for Mesos interface dependency missing on ubuntu - RUN pip install protobuf==3.0.0 + RUN {pip} install protobuf==3.0.0 # Fix for https://issues.apache.org/jira/browse/MESOS-3793 ENV MESOS_LAUNCHER=posix @@ -151,7 +157,7 @@ def heredoc(s): # This component changes most frequently and keeping it last maximizes Docker cache hits. COPY {sdistName} . - RUN pip install {sdistName}[all] + RUN {pip} install {sdistName}[all] RUN rm {sdistName} # We intentionally inherit the default ENTRYPOINT and CMD from the base image, to the effect diff --git a/docs/index.rst b/docs/index.rst index 7184271ec2..f4b41213d0 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -4,7 +4,7 @@ Toil Documentation Toil is an open-source pure-Python workflow engine that lets people write better pipelines. Check out our `website`_ for a comprehensive list of Toil's features and read our `paper`_ to learn what Toil can do -in the real world. Feel free to also join us on `GitHub`_ and `Gitter`_. +in the real world. Please subscribe to our low-volume `announce`_ mailing list and feel free to also join us on `GitHub`_ and `Gitter`_. If using Toil for your research, please cite @@ -13,6 +13,7 @@ If using Toil for your research, please cite http://doi.org/10.1038/nbt.3772 .. _website: http://toil.ucsc-cgl.org/ +.. _announce: https://groups.google.com/forum/#!forum/toil-announce .. _GridEngine: http://gridscheduler.sourceforge.net/ .. _Parasol: http://genecats.soe.ucsc.edu/eng/parasol.html .. _Apache Mesos: http://mesos.apache.org/ diff --git a/setup.py b/setup.py index b203c316b3..ae472f79f9 100644 --- a/setup.py +++ b/setup.py @@ -20,12 +20,12 @@ # toil issue: https://github.com/DataBiosphere/toil/issues/2924 # very similar to this issue: https://github.com/mcfletch/pyopengl/issues/11 # the "right way" is waiting for a fix from "http-parser", but this fixes things in the meantime since that might take a while -cppflags_args = [i.strip() for i in os.environ.get('CPPFLAGS', '').split(' ') if i.strip()] -python_version = float('{}.{}'.format(str(sys.version_info.major), str(sys.version_info.minor))) -if python_version >= 3.7 and '-DPYPY_VERSION' not in cppflags_args: - raise RuntimeError('Toil requires the environment variable "CPPFLAGS" to contain "-DPYPY_VERSION" when installed ' - 'on python3.7 or higher. This can be set with:\n\n' - ' export CPPFLAGS=$CPPFLAGS" -DPYPY_VERSION"\n\n') +cppflags = os.environ.get('CPPFLAGS') +if cppflags: + # note, duplicate options don't affect things here so we don't check - Mark D + os.environ['CPPFLAGS'] = ' '.join([cppflags, '-DPYPY_VERSION']) +else: + os.environ['CPPFLAGS'] = '-DPYPY_VERSION' def runSetup(): @@ -98,7 +98,6 @@ def runSetup(): kubernetes_reqs = [ kubernetes] mesos_reqs = [ - http_parser, pymesos, psutil] wdl_reqs = [] diff --git a/src/toil/__init__.py b/src/toil/__init__.py index 5d8d732769..de500ade43 100644 --- a/src/toil/__init__.py +++ b/src/toil/__init__.py @@ -48,7 +48,6 @@ log = logging.getLogger(__name__) - def which(cmd, mode=os.F_OK | os.X_OK, path=None): """ Copy-pasted in from python3.6's shutil.which(). diff --git a/src/toil/batchSystems/kubernetes.py b/src/toil/batchSystems/kubernetes.py index 5d88a5312a..170ed082b7 100644 --- a/src/toil/batchSystems/kubernetes.py +++ b/src/toil/batchSystems/kubernetes.py @@ -568,8 +568,15 @@ def getUpdatedBatchJob(self, maxWait): if containerStatuses is None or len(containerStatuses) == 0: logger.debug("No job container statuses for job %s" % (pod.metadata.owner_references[0].name)) return (int(pod.metadata.owner_references[0].name[len(self.jobPrefix):]), -1, 0) - logger.info("REASON: %s Exit Code: %s" % (pod.status.container_statuses[0].state.terminated.reason, - pod.status.container_statuses[0].state.terminated.exit_code)) + + # Get termination onformation from the pod + termination = pod.status.container_statuses[0].state.terminated + logger.info("REASON: %s Exit Code: %s", termination.reason, termination.exit_code) + + if termination.exit_code != 0: + # The pod failed. Dump information about it. + logger.debug('Failed pod information: %s', str(pod)) + logger.warning('Log from failed pod: %s', self._getLogForPod(pod)) jobID = int(pod.metadata.owner_references[0].name[len(self.jobPrefix):]) terminated = pod.status.container_statuses[0].state.terminated runtime = slow_down((terminated.finished_at - terminated.started_at).total_seconds()) diff --git a/src/toil/deferred.py b/src/toil/deferred.py index 2efa30ddad..9e7e885770 100644 --- a/src/toil/deferred.py +++ b/src/toil/deferred.py @@ -99,6 +99,15 @@ class DeferredFunctionManager(object): be locked, and will take them over. """ + # Define what directory the state directory should actaully be, under the base + STATE_DIR_STEM = 'deferred' + # Have a prefix to distinguish our deferred functions from e.g. NFS + # "silly rename" files, or other garbage that people put in our + # directory + PREFIX = 'func' + # And a suffix to distingusidh in-progress from completed files + WIP_SUFFIX = '.tmp' + def __init__(self, stateDirBase): """ Create a new DeferredFunctionManager, sharing state with other @@ -115,12 +124,14 @@ def __init__(self, stateDirBase): """ # Work out where state files live - self.stateDir = os.path.join(stateDirBase, "deferred") + self.stateDir = os.path.join(stateDirBase, self.STATE_DIR_STEM) mkdir_p(self.stateDir) # We need to get a state file, locked by us and not somebody scanning for abandoned state files. - # So we suffix not-yet-ready ones with .tmp - self.stateFD, self.stateFileName = tempfile.mkstemp(dir=self.stateDir, suffix='.tmp') + # So we suffix not-yet-ready ones with our suffix + self.stateFD, self.stateFileName = tempfile.mkstemp(dir=self.stateDir, + prefix=self.PREFIX, + suffix=self.WIP_SUFFIX) # Lock the state file. The lock will automatically go away if our process does. try: @@ -129,9 +140,9 @@ def __init__(self, stateDirBase): # Someone else might have locked it even though they should not have. raise RuntimeError("Could not lock deferred function state file %s: %s" % (self.stateFileName, str(e))) - # Rename it to remove the ".tmp" - os.rename(self.stateFileName, self.stateFileName[:-4]) - self.stateFileName = self.stateFileName[:-4] + # Rename it to remove the suffix + os.rename(self.stateFileName, self.stateFileName[:-len(self.WIP_SUFFIX)]) + self.stateFileName = self.stateFileName[:-len(self.WIP_SUFFIX)] # Wrap the FD in a Python file object, which we will use to actually use it. # Problem: we can't be readable and writable at the same time. So we need two file objects. @@ -208,7 +219,7 @@ def cleanupWorker(cls, stateDirBase): # Clean up the directory we have been using. # It might not be empty if .tmp files escaped: nobody can tell they # aren't just waiting to be locked. - shutil.rmtree(os.path.join(stateDirBase, "deferred")) + shutil.rmtree(os.path.join(stateDirBase, cls.STATE_DIR_STEM)) @@ -280,10 +291,14 @@ def _runOrphanedDeferredFunctions(self): for filename in os.listdir(self.stateDir): # Scan the whole directory for work nobody else owns. - if filename.endswith(".tmp"): + if filename.endswith(self.WIP_SUFFIX): # Skip files from instances that are still being set up continue + if not filename.startswith(self.PREFIX): + # Skip NFS deleted files and any other contaminants + continue + fullFilename = os.path.join(self.stateDir, filename) if fullFilename == self.stateFileName: @@ -332,7 +347,8 @@ def _runOrphanedDeferredFunctions(self): # Unlock it fcntl.lockf(fd, fcntl.LOCK_UN) - # Now close it. + # Now close it. This closes the backing file descriptor. See + # fileObj.close() diff --git a/src/toil/lib/threading.py b/src/toil/lib/threading.py index e10fbf56d1..68550ac19a 100644 --- a/src/toil/lib/threading.py +++ b/src/toil/lib/threading.py @@ -17,9 +17,11 @@ from __future__ import absolute_import from future.utils import raise_ from builtins import range +import logging import math import sys import threading +import traceback if sys.version_info >= (3, 0): from threading import BoundedSemaphore else: @@ -27,6 +29,8 @@ import psutil +log = logging.getLogger(__name__) + class BoundedEmptySemaphore( BoundedSemaphore ): """ A bounded semaphore that is initially empty. @@ -118,17 +122,28 @@ def cpu_count(): Ignores the cgroup's cpu shares value, because it's extremely difficult to interpret. See https://github.com/kubernetes/kubernetes/issues/81021. + Caches result for efficiency. + :return: Integer count of available CPUs, minimum 1. :rtype: int """ + cached = getattr(cpu_count, 'result', None) + if cached is not None: + # We already got a CPU count. + return cached + # Get the fallback answer of all the CPUs on the machine total_machine_size = psutil.cpu_count(logical=True) + log.debug('Total machine size: %d cores', total_machine_size) + try: with open('/sys/fs/cgroup/cpu/cpu.cfs_quota_us', 'r') as stream: # Read the quota - quota = int(stream.read) + quota = int(stream.read()) + + log.debug('CPU quota: %d', quota) if quota == -1: # Assume we can use the whole machine @@ -136,14 +151,24 @@ def cpu_count(): with open('/sys/fs/cgroup/cpu/cpu.cfs_period_us', 'r') as stream: # Read the period in which we are allowed to burn the quota - period = int(stream.read) + period = int(stream.read()) + + log.debug('CPU quota period: %d', period) # The thread count is how many multiples of a wall clcok period we can burn in that period. cgroup_size = int(math.ceil(float(quota)/float(period))) + + log.debug('Cgroup size in cores: %d', cgroup_size) + except: # We can't actually read these cgroup fields. Maybe we are a mac or something. + log.debug('Could not inspect cgroup: %s', traceback.format_exc()) cgroup_size = float('inf') # Return the smaller of the actual thread count and the cgroup's limit, minimum 1. - return max(1, min(cgroup_size, total_machine_size)) + result = max(1, min(cgroup_size, total_machine_size)) + log.debug('cpu_count: %s', str(result)) + # Make sure to remember it for the next call + setattr(cpu_count, 'result', result) + return result diff --git a/src/toil/resource.py b/src/toil/resource.py index 13813f826e..a21418e771 100644 --- a/src/toil/resource.py +++ b/src/toil/resource.py @@ -40,6 +40,8 @@ from toil.lib.memoize import strict_bool from toil.lib.iterables import concat +from toil.version import exactPython + from toil import inVirtualEnv log = logging.getLogger(__name__) @@ -565,8 +567,8 @@ def _resourcePath(self): raise ResourceException( "Toil does not support loading a user script from a package directory. You " "may want to remove %s from %s or invoke the user script as a module via " - "'PYTHONPATH=\"%s\" python -m %s.%s'." % - tuple(concat(initName, self.dirPath, os.path.split(self.dirPath), self.name))) + "'PYTHONPATH=\"%s\" %s -m %s.%s'." % + tuple(concat(initName, self.dirPath, exactPython, os.path.split(self.dirPath), self.name))) return self.dirPath moduleExtensions = ('.py', '.pyc', '.pyo') diff --git a/src/toil/test/__init__.py b/src/toil/test/__init__.py index 627b928067..3cee0c4c2f 100644 --- a/src/toil/test/__init__.py +++ b/src/toil/test/__init__.py @@ -19,6 +19,7 @@ import re import shutil import signal +import sys import tempfile import threading import time @@ -399,7 +400,6 @@ def needs_docker(test_item): else: return unittest.skip("Install docker to include this test.")(test_item) - def needs_encryption(test_item): """ Use as a decorator before test classes or methods to only run them if PyNaCl is installed @@ -432,6 +432,10 @@ def needs_cwl(test_item): def needs_appliance(test_item): + """ + Use as a decorator before test classes or methods to only run them if + the Toil appliance Docker image is downloaded. + """ import json test_item = _mark_test('appliance', test_item) if os.getenv('TOIL_SKIP_DOCKER', '').lower() == 'true': @@ -454,6 +458,23 @@ def needs_appliance(test_item): assert False, 'Expected `docker inspect` to return zero or one image.' else: return unittest.skip('Install Docker to include this test.')(test_item) + +def needs_downloadable_appliance(test_item): + """ + Use as a decorator before test classes or methods to only run them if + the Toil appliance Docker image ought to be available for download. + + For now, this just skips if running on Python 3. + TODO: When the appliance build is set up for Python 3 (when + https://github.com/DataBiosphere/toil/issues/2742 is fixed), this behavior + should be changed. + """ + + if sys.version_info[0] == 2: + return test_item + else: + return unittest.skip("Skipping test that needs Toil Appliance, available only for Python 2")(test_item) + def integrative(test_item): diff --git a/src/toil/test/batchSystems/batchSystemTest.py b/src/toil/test/batchSystems/batchSystemTest.py index 01492f6429..54ba86370b 100644 --- a/src/toil/test/batchSystems/batchSystemTest.py +++ b/src/toil/test/batchSystems/batchSystemTest.py @@ -55,6 +55,7 @@ needs_slurm, needs_torque, needs_htcondor, + needs_downloadable_appliance, slow, tempFileContaining, travis_test) @@ -345,6 +346,7 @@ def _createConfig(self): @needs_kubernetes @needs_aws_s3 +@needs_downloadable_appliance class KubernetesBatchSystemTest(hidden.AbstractBatchSystemTest): """ Tests against the Kubernetes batch system diff --git a/src/toil/test/docs/scriptsTest.py b/src/toil/test/docs/scriptsTest.py index cef4644f25..0271c10770 100644 --- a/src/toil/test/docs/scriptsTest.py +++ b/src/toil/test/docs/scriptsTest.py @@ -11,6 +11,7 @@ from toil import subprocess from toil.test import ToilTest from toil.test import needs_cwl +from toil.version import python class ToilDocumentationTest(ToilTest): @@ -37,7 +38,7 @@ def tearDown(self): """Just check the exit code""" def checkExitCode(self, script): program = os.path.join(self.directory, "scripts", script) - process = subprocess.Popen(["python", program, "file:my-jobstore", "--clean=always"], + process = subprocess.Popen([python, program, "file:my-jobstore", "--clean=always"], stdout=subprocess.PIPE, stderr=subprocess.PIPE) stdout, stderr = process.communicate() assert process.returncode == 0, stderr @@ -51,7 +52,7 @@ def checkExpectedOut(self, script, expectedOutput): # Check that the expected output is there index = outerr.find(expectedOutput) - self.assertGreater(index, -1, index) + self.assertGreater(index, -1, "Expected:\n{}\nOutput:\n{}".format(expectedOutput, outerr)) """Check the exit code and look for a pattern""" def checkExpectedPattern(self, script, expectedPattern): @@ -60,7 +61,7 @@ def checkExpectedPattern(self, script, expectedPattern): # Check that the expected output pattern is there pattern = re.compile(expectedPattern, re.DOTALL) n = re.search(pattern, outerr) - self.assertNotEqual(n, None, n) + self.assertNotEqual(n, None, "Pattern:\n{}\nOutput:\n{}".format(expectedPattern, outerr)) @needs_cwl def testCwlexample(self): diff --git a/src/toil/test/provisioners/aws/awsProvisionerTest.py b/src/toil/test/provisioners/aws/awsProvisionerTest.py index 8f80079578..01e14578f7 100644 --- a/src/toil/test/provisioners/aws/awsProvisionerTest.py +++ b/src/toil/test/provisioners/aws/awsProvisionerTest.py @@ -26,6 +26,7 @@ from toil import subprocess from toil.provisioners import clusterFactory +from toil.version import exactPython from toil.test import needs_aws_ec2, integrative, ToilTest, needs_appliance, timeLimit, slow log = logging.getLogger(__name__) @@ -113,7 +114,7 @@ def _test(self, preemptableJobs=False): assert len(self.getMatchingRoles()) == 1 # --never-download prevents silent upgrades to pip, wheel and setuptools - venv_command = ['virtualenv', '--system-site-packages', '--never-download', '/home/venv'] + venv_command = ['virtualenv', '--system-site-packages', '--python', exactPython, '--never-download', '/home/venv'] self.sshUtil(venv_command) upgrade_command = ['/home/venv/bin/pip', 'install', 'setuptools==28.7.1'] diff --git a/src/toil/test/provisioners/gceProvisionerTest.py b/src/toil/test/provisioners/gceProvisionerTest.py index 5af0424efc..f3c932f510 100644 --- a/src/toil/test/provisioners/gceProvisionerTest.py +++ b/src/toil/test/provisioners/gceProvisionerTest.py @@ -15,6 +15,7 @@ import logging import os from toil import subprocess +from toil.version import exactPython from abc import abstractmethod import pytest @@ -127,7 +128,7 @@ def _test(self, preemptableJobs=False): # --never-download prevents silent upgrades to pip, wheel and setuptools venv_command = ['virtualenv', '--system-site-packages', '--never-download', - '/home/venv'] + '--python', exactPython, '/home/venv'] self.sshUtil(venv_command) upgrade_command = ['/home/venv/bin/pip', 'install', 'setuptools==28.7.1'] diff --git a/src/toil/test/src/autoDeploymentTest.py b/src/toil/test/src/autoDeploymentTest.py index 12812d41af..f2effc2f7d 100644 --- a/src/toil/test/src/autoDeploymentTest.py +++ b/src/toil/test/src/autoDeploymentTest.py @@ -2,11 +2,13 @@ from builtins import str from builtins import object import logging +import sys from contextlib import contextmanager from toil.lib.iterables import concat from toil import subprocess +from toil.version import exactPython from toil.test import needs_mesos, ApplianceTestSupport, needs_appliance, slow log = logging.getLogger(__name__) @@ -38,11 +40,14 @@ def _venvApplianceCluster(self): leader.runOnAppliance('virtualenv', '--system-site-packages', '--never-download', # prevent silent upgrades to pip etc + '--python', exactPython, 'venv') leader.runOnAppliance('venv/bin/pip', 'list') # For diagnostic purposes yield leader, worker - sitePackages = 'venv/lib/python2.7/site-packages' + # TODO: Are we sure the python in the appliance we are testing is the same + # as the one we are testing from? If not, how can we get the version it is? + sitePackages = 'venv/lib/{}/site-packages'.format(exactPython) def testRestart(self): """ diff --git a/src/toil/test/src/promisedRequirementTest.py b/src/toil/test/src/promisedRequirementTest.py index 57b0852607..18a2f81d65 100644 --- a/src/toil/test/src/promisedRequirementTest.py +++ b/src/toil/test/src/promisedRequirementTest.py @@ -48,6 +48,7 @@ def testConcurrencyDynamic(self): Asserts that promised core resources are allocated properly using a dynamic Toil workflow """ for coresPerJob in self.allocatedCores: + log.debug('Testing %d cores per job with CPU count %d', coresPerJob, self.cpuCount) tempDir = self._createTempDir('testFiles') counterPath = self.getCounterPath(tempDir) @@ -63,6 +64,7 @@ def testConcurrencyStatic(self): Asserts that promised core resources are allocated properly using a static DAG """ for coresPerJob in self.allocatedCores: + log.debug('Testing %d cores per job with CPU count %d', coresPerJob, self.cpuCount) tempDir = self._createTempDir('testFiles') counterPath = self.getCounterPath(tempDir) diff --git a/src/toil/test/src/resourceTest.py b/src/toil/test/src/resourceTest.py index 94b5a326f0..d60944cc48 100644 --- a/src/toil/test/src/resourceTest.py +++ b/src/toil/test/src/resourceTest.py @@ -29,6 +29,7 @@ from toil import inVirtualEnv from toil.resource import ModuleDescriptor, Resource, ResourceException from toil.test import ToilTest, tempFileContaining, travis_test +from toil.version import exactPython class ResourceTest(ToilTest): @@ -72,8 +73,10 @@ def _testExternal(self, moduleName, pyFiles, virtualenv=False): if virtualenv: self.assertTrue(inVirtualEnv()) # --never-download prevents silent upgrades to pip, wheel and setuptools - subprocess.check_call(['virtualenv', '--never-download', dirPath]) - sitePackages = os.path.join(dirPath, 'lib', 'python2.7', 'site-packages') + subprocess.check_call(['virtualenv', '--never-download', '--python', exactPython, dirPath]) + sitePackages = os.path.join(dirPath, 'lib', + exactPython, + 'site-packages') # tuple assignment is necessary to make this line immediately precede the try: oldPrefix, sys.prefix, dirPath = sys.prefix, dirPath, sitePackages else: diff --git a/src/toil/test/utils/ABCWorkflowDebug/debugWorkflow.py b/src/toil/test/utils/ABCWorkflowDebug/debugWorkflow.py index 9709ff8e32..6acf519202 100644 --- a/src/toil/test/utils/ABCWorkflowDebug/debugWorkflow.py +++ b/src/toil/test/utils/ABCWorkflowDebug/debugWorkflow.py @@ -1,5 +1,6 @@ from toil.job import Job from toil.common import Toil +from toil.version import python from toil import subprocess import os import logging @@ -34,7 +35,7 @@ def writeA(job, mkFile): # make a file (A.txt) and writes a string 'A' into it using 'mkFile.py' content = 'A' - cmd = 'python' + ' ' + mkFile_fs + ' ' + 'A.txt' + ' ' + content + cmd = python + ' ' + mkFile_fs + ' ' + 'A.txt' + ' ' + content this_process = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE) this_process.wait() @@ -71,7 +72,7 @@ def writeB(job, mkFile, B_file): for line in f: file_contents = file_contents + line - cmd = 'python' + ' ' + mkFile_fs + ' ' + 'B.txt' + ' ' + file_contents + cmd = python + ' ' + mkFile_fs + ' ' + 'B.txt' + ' ' + file_contents this_process = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE) this_process.wait() diff --git a/src/toil/test/utils/utilsTest.py b/src/toil/test/utils/utilsTest.py index 8da7c1ca96..34d1a1ac6a 100644 --- a/src/toil/test/utils/utilsTest.py +++ b/src/toil/test/utils/utilsTest.py @@ -39,6 +39,7 @@ from toil.utils.toilStats import getStats, processData from toil.common import Toil, Config from toil.provisioners import clusterFactory +from toil.version import python logger = logging.getLogger(__name__) @@ -65,7 +66,7 @@ def setUp(self): self.correctSort = fileHandle.readlines() self.correctSort.sort() - self.sort_workflow_cmd = ['python', '-m', 'toil.test.sort.sort', + self.sort_workflow_cmd = [python, '-m', 'toil.test.sort.sort', 'file:' + self.toilDir, '--clean=never', '--numLines=1', '--lineLength=1'] @@ -168,7 +169,7 @@ def testAWSProvisionerUtils(self): for test in testStrings: logger.debug('Testing SSH with special string: %s', test) compareTo = "import sys; assert sys.argv[1]==%r" % test - leader.sshAppliance('python', '-', test, input=compareTo) + leader.sshAppliance(python, '-', test, input=compareTo) try: leader.sshAppliance('nonsenseShouldFail') @@ -177,7 +178,7 @@ def testAWSProvisionerUtils(self): else: self.fail('The remote command failed silently where it should have raised an error') - leader.sshAppliance('python', '-c', "import os; assert os.environ['TOIL_WORKDIR']=='/var/lib/toil'") + leader.sshAppliance(python, '-c', "import os; assert os.environ['TOIL_WORKDIR']=='/var/lib/toil'") # `toil rsync-cluster` # Testing special characters - string.punctuation diff --git a/src/toil/test/wdl/toilwdlTest.py b/src/toil/test/wdl/toilwdlTest.py index 28b67c474d..75c8a1fbde 100644 --- a/src/toil/test/wdl/toilwdlTest.py +++ b/src/toil/test/wdl/toilwdlTest.py @@ -3,6 +3,7 @@ import unittest import os from toil import subprocess +from toil.version import exactPython import toil.wdl.wdl_parser as wdl_parser from toil.wdl.wdl_analysis import AnalyzeWDL from toil.wdl.wdl_synthesis import SynthesizeWDL @@ -121,7 +122,7 @@ def testMD5sum(self): inputfile = os.path.abspath('src/toil/test/wdl/md5sum/md5sum.input') json = os.path.abspath('src/toil/test/wdl/md5sum/md5sum.json') - subprocess.check_call(['python', self.program, wdl, json, '-o', self.output_dir]) + subprocess.check_call([exactPython, self.program, wdl, json, '-o', self.output_dir]) md5sum_output = os.path.join(self.output_dir, 'md5sum.txt') assert os.path.exists(md5sum_output) os.unlink(md5sum_output) @@ -274,7 +275,7 @@ def testTut01(self): json = os.path.abspath("src/toil/test/wdl/wdl_templates/t01/helloHaplotypeCaller_inputs.json") ref_dir = os.path.abspath("src/toil/test/wdl/wdl_templates/t01/output/") - subprocess.check_call(['python', self.program, wdl, json, '-o', self.output_dir]) + subprocess.check_call([exactPython, self.program, wdl, json, '-o', self.output_dir]) compare_runs(self.output_dir, ref_dir) @@ -288,7 +289,7 @@ def testTut02(self): json = os.path.abspath("src/toil/test/wdl/wdl_templates/t02/simpleVariantSelection_inputs.json") ref_dir = os.path.abspath("src/toil/test/wdl/wdl_templates/t02/output/") - subprocess.check_call(['python', self.program, wdl, json, '-o', self.output_dir]) + subprocess.check_call([exactPython, self.program, wdl, json, '-o', self.output_dir]) compare_runs(self.output_dir, ref_dir) @@ -302,7 +303,7 @@ def testTut03(self): json = os.path.abspath("src/toil/test/wdl/wdl_templates/t03/simpleVariantDiscovery_inputs.json") ref_dir = os.path.abspath("src/toil/test/wdl/wdl_templates/t03/output/") - subprocess.check_call(['python', self.program, wdl, json, '-o', self.output_dir]) + subprocess.check_call([exactPython, self.program, wdl, json, '-o', self.output_dir]) compare_runs(self.output_dir, ref_dir) @@ -316,7 +317,7 @@ def testTut04(self): json = os.path.abspath("src/toil/test/wdl/wdl_templates/t04/jointCallingGenotypes_inputs.json") ref_dir = os.path.abspath("src/toil/test/wdl/wdl_templates/t04/output/") - subprocess.check_call(['python', self.program, wdl, json, '-o', self.output_dir]) + subprocess.check_call([exactPython, self.program, wdl, json, '-o', self.output_dir]) compare_runs(self.output_dir, ref_dir) @@ -334,7 +335,7 @@ def testENCODE(self): "src/toil/test/wdl/wdl_templates/testENCODE/output/") subprocess.check_call( - ['python', self.program, wdl, json, '--docker_user=None', '--out_dir', self.output_dir]) + [exactPython, self.program, wdl, json, '--docker_user=None', '--out_dir', self.output_dir]) compare_runs(self.output_dir, ref_dir) @@ -349,7 +350,7 @@ def testPipe(self): "src/toil/test/wdl/wdl_templates/testPipe/output/") subprocess.check_call( - ['python', self.program, wdl, json, '--out_dir', self.output_dir]) + [exactPython, self.program, wdl, json, '--out_dir', self.output_dir]) compare_runs(self.output_dir, ref_dir) diff --git a/version_template.py b/version_template.py index 7fda17fc2a..158a1b72fb 100644 --- a/version_template.py +++ b/version_template.py @@ -24,9 +24,10 @@ # # - don't import at module level unless you want the imported value to be included in the output # - only import from the Python standard run-time library (you can't have any dependencies) +# - don't import even standard modules at global scope without renaming them +# to have leading/trailing underscores - -baseVersion = '3.22.1a6' +baseVersion = '3.23.1' cgcloudVersion = '1.6.0a1.dev393' dockerName = 'toil' @@ -61,15 +62,59 @@ def distVersion(): "'pip install setuptools --upgrade'") return baseVersion +def exactPython(): + """ + Returns the Python command for the exact version of Python we are installed + for. Something like 'python2.7' or 'python3.6'. + """ + + import sys + + return 'python{}.{}'.format(sys.version_info[0], sys.version_info[1]) + +def python(): + """ + Returns the Python command for the general version of Python we are + installed for. Something like 'python2.7' or 'python3'. + + We assume all Python 3s are sufficiently intercompatible that we can just + use 'python3' here for all of them. This is useful because the Toil Docker + appliance is only built for particular Python versions, and we would like + workflows to work with a variety of leader Python versions. + """ + + import sys + + if sys.version_info[0] == 3: + # Ignore minor version + return 'python3' + else: + return exactPython() + +def _pythonVersionSuffix(): + """ + Returns a short string identifying the running version of Python. Toil + appliances running the same Toil version but on different versions of + Python as returned by this function are not compatible. + """ + + import sys + + # For now, we assume all Python 3 releases are intercompatible. + # We also only tag the Python 2 releases specially, since Python 2 is old and busted. + if sys.version_info[0] == 3: + return '' + else: + return '-py{}'.format(sys.version_info[0]) def dockerTag(): """The primary tag of the Docker image for the appliance. This uniquely identifies the appliance image.""" - return version() + return version() + _pythonVersionSuffix() def dockerShortTag(): """A secondary, shortened form of :func:`dockerTag` with which to tag the appliance image for convenience.""" - return shortVersion() + return shortVersion() + _pythonVersionSuffix() def dockerMinimalTag(): @@ -77,7 +122,7 @@ def dockerMinimalTag(): A minimal tag with which to tag the appliance image for convenience. Does not include information about the git commit or working copy dirtyness. """ - return distVersion() + return distVersion() + _pythonVersionSuffix() def currentCommit():