diff env/lib/python3.7/site-packages/planemo/galaxy/config.py @ 0:26e78fe6e8c4 draft

"planemo upload commit c699937486c35866861690329de38ec1a5d9f783"
author shellac
date Sat, 02 May 2020 07:14:21 -0400
parents
children
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/env/lib/python3.7/site-packages/planemo/galaxy/config.py	Sat May 02 07:14:21 2020 -0400
@@ -0,0 +1,1270 @@
+"""Abstractions for setting up a Galaxy instance."""
+from __future__ import absolute_import
+from __future__ import print_function
+
+import abc
+import contextlib
+import os
+import random
+import shutil
+from string import Template
+from tempfile import mkdtemp
+
+from galaxy.containers.docker_model import DockerVolume
+from galaxy.tool_util.deps import docker_util
+from galaxy.tool_util.deps.commands import argv_to_str
+from pkg_resources import parse_version
+from six import (
+    add_metaclass,
+    iteritems
+)
+from six.moves import shlex_quote
+
+from planemo import git
+from planemo.config import OptionSource
+from planemo.deps import ensure_dependency_resolvers_conf_configured
+from planemo.docker import docker_host_args
+from planemo.io import (
+    communicate,
+    kill_pid_file,
+    shell,
+    shell_join,
+    untar_to,
+    wait_on,
+    warn,
+    write_file,
+)
+from planemo.mulled import build_involucro_context
+from planemo.shed import tool_shed_url
+from planemo.virtualenv import DEFAULT_PYTHON_VERSION
+from .api import (
+    DEFAULT_MASTER_API_KEY,
+    gi,
+    user_api_key,
+)
+from .distro_tools import (
+    DISTRO_TOOLS_ID_TO_PATH
+)
+from .run import (
+    setup_common_startup_args,
+    setup_venv,
+)
+from .workflows import (
+    find_tool_ids,
+    import_workflow,
+    install_shed_repos,
+)
+
+
+NO_TEST_DATA_MESSAGE = (
+    "planemo couldn't find a target test-data directory, you should likely "
+    "create a test-data directory or pass an explicit path using --test_data."
+)
+
+WEB_SERVER_CONFIG_TEMPLATE = """
+[server:${server_name}]
+use = egg:Paste#http
+port = ${port}
+host = ${host}
+use_threadpool = True
+threadpool_kill_thread_limit = 10800
+[app:main]
+paste.app_factory = galaxy.web.buildapp:app_factory
+"""
+
+TOOL_CONF_TEMPLATE = """<toolbox>
+  <tool file="data_source/upload.xml" />
+  ${tool_definition}
+</toolbox>
+"""
+
+SHED_TOOL_CONF_TEMPLATE = """<?xml version="1.0"?>
+<toolbox tool_path="${shed_tool_path}">
+</toolbox>
+"""
+
+SHED_DATA_MANAGER_CONF_TEMPLATE = """<?xml version="1.0"?>
+<data_managers>
+</data_managers>
+"""
+
+EMPTY_JOB_METRICS_TEMPLATE = """<?xml version="1.0"?>
+<job_metrics>
+</job_metrics>
+"""
+
+TOOL_SHEDS_CONF = """<tool_sheds>
+  <tool_shed name="Target Shed" url="${shed_target_url}" />
+</tool_sheds>
+"""
+
+JOB_CONFIG_LOCAL = """<job_conf>
+    <plugins>
+        <plugin id="planemo_runner" type="runner" load="galaxy.jobs.runners.local:LocalJobRunner" workers="4"/>
+    </plugins>
+    <handlers>
+        <handler id="main"/>
+    </handlers>
+    <destinations default="planemo_dest">
+        <destination id="planemo_dest" runner="planemo_runner">
+            <param id="require_container">${require_container}</param>
+            <param id="docker_enabled">${docker_enable}</param>
+            <param id="docker_sudo">${docker_sudo}</param>
+            <param id="docker_sudo_cmd">${docker_sudo_cmd}</param>
+            <param id="docker_cmd">${docker_cmd}</param>
+            ${docker_host_param}
+        </destination>
+        <destination id="upload_dest" runner="planemo_runner">
+            <param id="docker_enable">false</param>
+        </destination>
+    </destinations>
+    <tools>
+        <tool id="upload1" destination="upload_dest" />
+    </tools>
+</job_conf>
+"""
+
+LOGGING_TEMPLATE = """
+## Configure Python loggers.
+[loggers]
+keys = root,paste,displayapperrors,galaxydeps,galaxymasterapikey,galaxy
+
+[handlers]
+keys = console
+
+[formatters]
+keys = generic
+
+[logger_root]
+level = WARN
+handlers = console
+
+[logger_paste]
+level = WARN
+handlers = console
+qualname = paste
+propagate = 0
+
+[logger_galaxydeps]
+level = DEBUG
+handlers = console
+qualname = galaxy.tools.deps
+propagate = 0
+
+[logger_galaxymasterapikey]
+level = WARN
+handlers = console
+qualname = galaxy.web.framework.webapp
+propagate = 0
+
+[logger_displayapperrors]
+level = ERROR
+handlers =
+qualname = galaxy.datatypes.display_applications.application
+propagate = 0
+
+[logger_galaxy]
+level = ${log_level}
+handlers = console
+qualname = galaxy
+propagate = 0
+
+[handler_console]
+class = StreamHandler
+args = (sys.stderr,)
+level = DEBUG
+formatter = generic
+
+[formatter_generic]
+format = %(asctime)s %(levelname)-5.5s [%(name)s] %(message)s
+"""
+
+
+EMPTY_TOOL_CONF_TEMPLATE = """<toolbox></toolbox>"""
+
+DEFAULT_GALAXY_BRANCH = "master"
+DEFAULT_GALAXY_SOURCE = "https://github.com/galaxyproject/galaxy"
+CWL_GALAXY_SOURCE = "https://github.com/common-workflow-language/galaxy"
+
+DATABASE_LOCATION_TEMPLATE = "sqlite:///%s?isolation_level=IMMEDIATE"
+
+COMMAND_STARTUP_COMMAND = "./scripts/common_startup.sh ${COMMON_STARTUP_ARGS}"
+
+CLEANUP_IGNORE_ERRORS = True
+DEFAULT_GALAXY_BRAND = 'Configured by Planemo'
+
+
+@contextlib.contextmanager
+def galaxy_config(ctx, runnables, **kwds):
+    """Set up a ``GalaxyConfig`` in an auto-cleaned context."""
+    c = local_galaxy_config
+    if kwds.get("dockerize", False):
+        c = docker_galaxy_config
+    elif kwds.get("external", False):
+        c = external_galaxy_config
+
+    with c(ctx, runnables, **kwds) as config:
+        yield config
+
+
+def simple_docker_volume(path):
+    path = os.path.abspath(path)
+    return DockerVolume("%s:%s:rw" % (path, path))
+
+
+@contextlib.contextmanager
+def docker_galaxy_config(ctx, runnables, for_tests=False, **kwds):
+    """Set up a ``GalaxyConfig`` for Docker container."""
+    test_data_dir = _find_test_data(runnables, **kwds)
+
+    with _config_directory(ctx, **kwds) as config_directory:
+        def config_join(*args):
+            return os.path.join(config_directory, *args)
+
+        ensure_dependency_resolvers_conf_configured(ctx, kwds, os.path.join(config_directory, "resolvers_conf.xml"))
+        _handle_job_metrics(config_directory, kwds)
+
+        shed_tool_conf = "config/shed_tool_conf.xml"
+        all_tool_paths = _all_tool_paths(runnables, **kwds)
+
+        tool_directories = set([])  # Things to mount...
+        for tool_path in all_tool_paths:
+            directory = os.path.dirname(os.path.normpath(tool_path))
+            if os.path.exists(directory):
+                tool_directories.add(directory)
+
+        # TODO: remap these.
+        tool_volumes = []
+        for tool_directory in tool_directories:
+            volume = simple_docker_volume(tool_directory)
+            tool_volumes.append(volume)
+
+        empty_tool_conf = config_join("empty_tool_conf.xml")
+
+        tool_conf = config_join("tool_conf.xml")
+
+        shed_tool_path = kwds.get("shed_tool_path") or config_join("shed_tools")
+        _ensure_directory(shed_tool_path)
+
+        sheds_config_path = _configure_sheds_config_file(
+            ctx, config_directory, **kwds
+        )
+        port = _get_port(kwds)
+        properties = _shared_galaxy_properties(config_directory, kwds, for_tests=for_tests)
+        _handle_container_resolution(ctx, kwds, properties)
+        master_api_key = _get_master_api_key(kwds)
+
+        template_args = dict(
+            shed_tool_path=shed_tool_path,
+            tool_conf=tool_conf,
+        )
+        tool_config_file = "%s,%s" % (tool_conf, shed_tool_conf)
+
+        _write_tool_conf(ctx, all_tool_paths, tool_conf)
+        write_file(empty_tool_conf, EMPTY_TOOL_CONF_TEMPLATE)
+
+        properties.update(dict(
+            tool_config_file=tool_config_file,
+            tool_sheds_config_file=sheds_config_path,
+            migrated_tools_config=empty_tool_conf,
+        ))
+
+        server_name = "planemo%d" % random.randint(0, 100000)
+
+        # Value substitutions in Galaxy properties - for consistency with
+        # non-Dockerized version.
+        template_args = dict(
+        )
+        env = _build_env_for_galaxy(properties, template_args)
+        env["NONUSE"] = "nodejs,proftp,reports"
+        if ctx.verbose:
+            env["GALAXY_LOGGING"] = "full"
+
+        # TODO: setup FTP upload dir and disable FTP server in container.
+        _build_test_env(properties, env)
+
+        docker_target_kwds = docker_host_args(**kwds)
+        volumes = tool_volumes + [simple_docker_volume(config_directory)]
+        export_directory = kwds.get("export_directory", None)
+        if export_directory is not None:
+            volumes.append(DockerVolume("%s:/export:rw" % export_directory))
+
+        # TODO: Allow this to real Docker volumes and allow multiple.
+        extra_volume = kwds.get("docker_extra_volume")
+        if extra_volume:
+            volumes.append(simple_docker_volume(extra_volume))
+        yield DockerGalaxyConfig(
+            ctx,
+            config_directory,
+            env,
+            test_data_dir,
+            port,
+            server_name,
+            master_api_key,
+            runnables,
+            docker_target_kwds=docker_target_kwds,
+            volumes=volumes,
+            export_directory=export_directory,
+            kwds=kwds,
+        )
+
+
+@contextlib.contextmanager
+def local_galaxy_config(ctx, runnables, for_tests=False, **kwds):
+    """Set up a ``GalaxyConfig`` in an auto-cleaned context."""
+    test_data_dir = _find_test_data(runnables, **kwds)
+    tool_data_table = _find_tool_data_table(
+        runnables,
+        test_data_dir=test_data_dir,
+        **kwds
+    )
+    data_manager_config_paths = [r.data_manager_conf_path for r in runnables if r.data_manager_conf_path]
+    galaxy_root = _find_galaxy_root(ctx, **kwds)
+    install_galaxy = kwds.get("install_galaxy", False)
+    if galaxy_root is not None:
+        if os.path.isdir(galaxy_root) and not os.listdir(galaxy_root):
+            os.rmdir(galaxy_root)
+        if os.path.isdir(galaxy_root) and install_galaxy:
+            raise Exception("%s is an existing non-empty directory, cannot install Galaxy again" % galaxy_root)
+
+    # Duplicate block in docker variant above.
+    if kwds.get("mulled_containers", False) and not kwds.get("docker", False):
+        if ctx.get_option_source("docker") != OptionSource.cli:
+            kwds["docker"] = True
+        else:
+            raise Exception("Specified no docker and mulled containers together.")
+
+    with _config_directory(ctx, **kwds) as config_directory:
+        def config_join(*args):
+            return os.path.join(config_directory, *args)
+
+        install_env = {}
+        if kwds.get('galaxy_skip_client_build', True):
+            install_env['GALAXY_SKIP_CLIENT_BUILD'] = '1'
+        if galaxy_root is None:
+            galaxy_root = config_join("galaxy-dev")
+        if not os.path.isdir(galaxy_root):
+            _build_eggs_cache(ctx, install_env, kwds)
+            _install_galaxy(ctx, galaxy_root, install_env, kwds)
+
+        if parse_version(kwds.get('galaxy_python_version') or DEFAULT_PYTHON_VERSION) >= parse_version('3'):
+            # on python 3 we use gunicorn,
+            # which requires 'main' as server name
+            server_name = 'main'
+        else:
+            server_name = "planemo%d" % random.randint(0, 100000)
+        # Once we don't have to support earlier than 18.01 - try putting these files
+        # somewhere better than with Galaxy.
+        log_file = "%s.log" % server_name
+        pid_file = "%s.pid" % server_name
+        ensure_dependency_resolvers_conf_configured(ctx, kwds, os.path.join(config_directory, "resolvers_conf.xml"))
+        _handle_job_config_file(config_directory, server_name, kwds)
+        _handle_job_metrics(config_directory, kwds)
+        file_path = kwds.get("file_path") or config_join("files")
+        _ensure_directory(file_path)
+
+        tool_dependency_dir = kwds.get("tool_dependency_dir") or config_join("deps")
+        _ensure_directory(tool_dependency_dir)
+
+        shed_tool_conf = kwds.get("shed_tool_conf") or config_join("shed_tools_conf.xml")
+        all_tool_paths = _all_tool_paths(runnables, **kwds)
+        empty_tool_conf = config_join("empty_tool_conf.xml")
+
+        tool_conf = config_join("tool_conf.xml")
+
+        shed_data_manager_config_file = config_join("shed_data_manager_conf.xml")
+
+        shed_tool_path = kwds.get("shed_tool_path") or config_join("shed_tools")
+        _ensure_directory(shed_tool_path)
+
+        sheds_config_path = _configure_sheds_config_file(
+            ctx, config_directory, **kwds
+        )
+
+        database_location = config_join("galaxy.sqlite")
+        master_api_key = _get_master_api_key(kwds)
+        dependency_dir = os.path.join(config_directory, "deps")
+        _ensure_directory(shed_tool_path)
+        port = _get_port(kwds)
+        template_args = dict(
+            port=port,
+            host=kwds.get("host", "127.0.0.1"),
+            server_name=server_name,
+            temp_directory=config_directory,
+            shed_tool_path=shed_tool_path,
+            database_location=database_location,
+            tool_conf=tool_conf,
+            debug=kwds.get("debug", "true"),
+            id_secret=kwds.get("id_secret", "test_secret"),
+            log_level="DEBUG" if ctx.verbose else "INFO",
+        )
+        tool_config_file = "%s,%s" % (tool_conf, shed_tool_conf)
+        # Setup both galaxy_email and older test user test@bx.psu.edu
+        # as admins for command_line, etc...
+        properties = _shared_galaxy_properties(config_directory, kwds, for_tests=for_tests)
+        properties.update(dict(
+            server_name="main",
+            ftp_upload_dir_template="${ftp_upload_dir}",
+            ftp_upload_purge="False",
+            ftp_upload_dir=test_data_dir or os.path.abspath('.'),
+            ftp_upload_site="Test Data",
+            check_upload_content="False",
+            tool_dependency_dir=dependency_dir,
+            file_path=file_path,
+            new_file_path="${temp_directory}/tmp",
+            tool_config_file=tool_config_file,
+            tool_sheds_config_file=sheds_config_path,
+            manage_dependency_relationships="False",
+            job_working_directory="${temp_directory}/job_working_directory",
+            template_cache_path="${temp_directory}/compiled_templates",
+            citation_cache_type="file",
+            citation_cache_data_dir="${temp_directory}/citations/data",
+            citation_cache_lock_dir="${temp_directory}/citations/lock",
+            database_auto_migrate="True",
+            enable_beta_tool_formats="True",
+            id_secret="${id_secret}",
+            log_level="${log_level}",
+            debug="${debug}",
+            watch_tools="auto",
+            default_job_shell="/bin/bash",  # For conda dependency resolution
+            tool_data_table_config_path=tool_data_table,
+            data_manager_config_file=",".join(data_manager_config_paths) or None,  # without 'or None' may raise IOError in galaxy (see #946)
+            integrated_tool_panel_config=("${temp_directory}/"
+                                          "integrated_tool_panel_conf.xml"),
+            migrated_tools_config=empty_tool_conf,
+            test_data_dir=test_data_dir,  # TODO: make gx respect this
+            shed_data_manager_config_file=shed_data_manager_config_file,
+        ))
+        _handle_container_resolution(ctx, kwds, properties)
+        write_file(config_join("logging.ini"), _sub(LOGGING_TEMPLATE, template_args))
+        properties["database_connection"] = _database_connection(database_location, **kwds)
+
+        _handle_kwd_overrides(properties, kwds)
+
+        # TODO: consider following property
+        # watch_tool = False
+        # datatypes_config_file = config/datatypes_conf.xml
+        # welcome_url = /static/welcome.html
+        # logo_url = /
+        # sanitize_all_html = True
+        # serve_xss_vulnerable_mimetypes = False
+        # track_jobs_in_database = None
+        # outputs_to_working_directory = False
+        # retry_job_output_collection = 0
+
+        env = _build_env_for_galaxy(properties, template_args)
+        env.update(install_env)
+        _build_test_env(properties, env)
+        env['GALAXY_TEST_SHED_TOOL_CONF'] = shed_tool_conf
+        env['GALAXY_TEST_DBURI'] = properties["database_connection"]
+
+        env["GALAXY_TEST_UPLOAD_ASYNC"] = "false"
+        env["GALAXY_TEST_LOGGING_CONFIG"] = config_join("logging.ini")
+        env["GALAXY_DEVELOPMENT_ENVIRONMENT"] = "1"
+        # Following are needed in 18.01 to prevent Galaxy from changing log and pid.
+        # https://github.com/galaxyproject/planemo/issues/788
+        env["GALAXY_LOG"] = log_file
+        env["GALAXY_PID"] = pid_file
+        web_config = _sub(WEB_SERVER_CONFIG_TEMPLATE, template_args)
+        write_file(config_join("galaxy.ini"), web_config)
+        _write_tool_conf(ctx, all_tool_paths, tool_conf)
+        write_file(empty_tool_conf, EMPTY_TOOL_CONF_TEMPLATE)
+
+        shed_tool_conf_contents = _sub(SHED_TOOL_CONF_TEMPLATE, template_args)
+        # Write a new shed_tool_conf.xml if needed.
+        write_file(shed_tool_conf, shed_tool_conf_contents, force=False)
+
+        write_file(shed_data_manager_config_file, SHED_DATA_MANAGER_CONF_TEMPLATE)
+
+        yield LocalGalaxyConfig(
+            ctx,
+            config_directory,
+            env,
+            test_data_dir,
+            port,
+            server_name,
+            master_api_key,
+            runnables,
+            galaxy_root,
+            kwds,
+        )
+
+
+def _all_tool_paths(runnables, **kwds):
+    tool_paths = [r.path for r in runnables if r.has_tools and not r.data_manager_conf_path]
+    all_tool_paths = list(tool_paths) + list(kwds.get("extra_tools", []))
+    for runnable in runnables:
+        if runnable.type.name == "galaxy_workflow":
+            tool_ids = find_tool_ids(runnable.path)
+            for tool_id in tool_ids:
+                if tool_id in DISTRO_TOOLS_ID_TO_PATH:
+                    all_tool_paths.append(DISTRO_TOOLS_ID_TO_PATH[tool_id])
+
+    return all_tool_paths
+
+
+def _shared_galaxy_properties(config_directory, kwds, for_tests):
+    """Setup properties useful for local and Docker Galaxy instances.
+
+    Most things related to paths, etc... are very different between Galaxy
+    modalities and many taken care of internally to the container in that mode.
+    But this method sets up API stuff, tool, and job stuff that can be shared.
+    """
+    master_api_key = _get_master_api_key(kwds)
+    user_email = _user_email(kwds)
+    properties = {
+        'master_api_key': master_api_key,
+        'admin_users': "%s,test@bx.psu.edu" % user_email,
+        'expose_dataset_path': "True",
+        'cleanup_job': 'never',
+        'collect_outputs_from': "job_working_directory",
+        'allow_path_paste': "True",
+        'check_migrate_tools': "False",
+        'use_cached_dependency_manager': str(kwds.get("conda_auto_install", False)),
+        'brand': kwds.get("galaxy_brand", DEFAULT_GALAXY_BRAND),
+        'strict_cwl_validation': str(not kwds.get("non_strict_cwl", False)),
+    }
+    if kwds.get("galaxy_single_user", True):
+        properties['single_user'] = user_email
+
+    if for_tests:
+        empty_dir = os.path.join(config_directory, "empty")
+        _ensure_directory(empty_dir)
+        properties["tour_config_dir"] = empty_dir
+        properties["interactive_environment_plugins_directory"] = empty_dir
+        properties["visualization_plugins_directory"] = empty_dir
+    return properties
+
+
+@contextlib.contextmanager
+def external_galaxy_config(ctx, runnables, for_tests=False, **kwds):
+    yield BaseGalaxyConfig(
+        ctx=ctx,
+        galaxy_url=kwds.get("galaxy_url", None),
+        master_api_key=_get_master_api_key(kwds),
+        user_api_key=kwds.get("galaxy_user_key", None),
+        runnables=runnables,
+        kwds=kwds
+    )
+
+
+def _get_master_api_key(kwds):
+    master_api_key = kwds.get("galaxy_admin_key") or DEFAULT_MASTER_API_KEY
+    return master_api_key
+
+
+def _get_port(kwds):
+    port = int(kwds.get("port", 9090))
+    return port
+
+
+def _user_email(kwds):
+    user_email = kwds.get("galaxy_email")
+    return user_email
+
+
+@contextlib.contextmanager
+def _config_directory(ctx, **kwds):
+    config_directory = kwds.get("config_directory", None)
+    created_config_directory = False
+    if not config_directory:
+        created_config_directory = True
+        config_directory = os.path.realpath(mkdtemp())
+        ctx.vlog("Created directory for Galaxy configuration [%s]" % config_directory)
+    try:
+        yield config_directory
+    finally:
+        cleanup = not kwds.get("no_cleanup", False)
+        if created_config_directory and cleanup:
+            shutil.rmtree(config_directory)
+
+
+@add_metaclass(abc.ABCMeta)
+class GalaxyInterface(object):
+    """Abstraction around a Galaxy instance.
+
+    Description of a Galaxy instance and how to interact with it - this could
+    potentially be a remote, already running instance or an instance Planemo manages
+    to execute some task(s).
+    """
+
+    @abc.abstractproperty
+    def gi(self):
+        """Return an admin bioblend Galaxy instance for API interactions."""
+
+    @abc.abstractproperty
+    def user_gi(self):
+        """Return a user-backed bioblend Galaxy instance for API interactions."""
+
+    @abc.abstractmethod
+    def install_repo(self, *args, **kwds):
+        """Install specified tool shed repository."""
+
+    @abc.abstractproperty
+    def tool_shed_client(self):
+        """Return a admin bioblend tool shed client."""
+
+    @abc.abstractmethod
+    def wait_for_all_installed(self):
+        """Wait for all queued up repositories installs to complete."""
+
+    @abc.abstractmethod
+    def install_workflows(self):
+        """Install all workflows configured with these planemo arguments."""
+
+    @abc.abstractmethod
+    def workflow_id(self, path):
+        """Get installed workflow API ID for input path."""
+
+
+@add_metaclass(abc.ABCMeta)
+class GalaxyConfig(GalaxyInterface):
+    """Specialization of GalaxyInterface for Galaxy instances Planemo manages itself.
+
+    This assumes more than an API connection is available - Planemo needs to be able to
+    start and stop the Galaxy instance, recover logs, etc... There are currently two
+    implementations - a locally executed Galaxy and one running inside a Docker containe
+    """
+
+    @abc.abstractproperty
+    def kill(self):
+        """Stop the running instance."""
+
+    @abc.abstractmethod
+    def startup_command(self, ctx, **kwds):
+        """Return a shell command used to startup this instance.
+
+        Among other common planmo kwds, this should respect the
+        ``daemon`` keyword.
+        """
+
+    @abc.abstractproperty
+    def log_contents(self):
+        """Retrieve text of log for running Galaxy instance."""
+
+    @abc.abstractmethod
+    def cleanup(self):
+        """Cleanup allocated resources to run this instance."""
+
+    @abc.abstractproperty
+    def use_path_paste(self):
+        """Use path paste to upload data."""
+
+
+class BaseGalaxyConfig(GalaxyInterface):
+
+    def __init__(
+        self,
+        ctx,
+        galaxy_url,
+        master_api_key,
+        user_api_key,
+        runnables,
+        kwds,
+    ):
+        self._ctx = ctx
+        self.galaxy_url = galaxy_url
+        self.master_api_key = master_api_key
+        self._user_api_key = user_api_key
+        self.runnables = runnables
+        self._kwds = kwds
+        self._workflow_ids = {}
+
+    @property
+    def gi(self):
+        assert self.galaxy_url
+        return gi(url=self.galaxy_url, key=self.master_api_key)
+
+    @property
+    def user_gi(self):
+        user_api_key = self.user_api_key
+        assert user_api_key
+        return self._gi_for_key(user_api_key)
+
+    @property
+    def user_api_key(self):
+        # TODO: thread-safe
+        if self._user_api_key is None:
+            # TODO: respect --galaxy_email - seems like a real bug
+            self._user_api_key = user_api_key(self.gi)
+
+        return self._user_api_key
+
+    def _gi_for_key(self, key):
+        assert self.galaxy_url
+        return gi(url=self.galaxy_url, key=key)
+
+    def install_repo(self, *args, **kwds):
+        self.tool_shed_client.install_repository_revision(
+            *args, **kwds
+        )
+
+    @property
+    def tool_shed_client(self):
+        return self.gi.toolShed
+
+    def wait_for_all_installed(self):
+        def status_ready(repo):
+            status = repo["status"]
+            if status in ["Installing", "New"]:
+                return None
+            if status == "Installed":
+                return True
+            raise Exception("Error installing repo status is %s" % status)
+
+        def ready():
+            repos = self.tool_shed_client.get_repositories()
+            ready = all(map(status_ready, repos))
+            return ready or None
+
+        wait_on(ready, "galaxy tool installation", timeout=60 * 60 * 1)
+
+    def install_workflows(self):
+        for runnable in self.runnables:
+            if runnable.type.name in ["galaxy_workflow", "cwl_workflow"]:
+                self._install_workflow(runnable)
+
+    def _install_workflow(self, runnable):
+        if self._kwds["shed_install"]:
+            install_shed_repos(runnable, self.gi, self._kwds.get("ignore_dependency_problems", False))
+
+        default_from_path = self._kwds.get("workflows_from_path", False)
+        # TODO: Allow serialization so this doesn't need to assume a
+        # shared filesystem with Galaxy server.
+        from_path = default_from_path or (runnable.type.name == "cwl_workflow")
+        workflow = import_workflow(
+            runnable.path, admin_gi=self.gi, user_gi=self.user_gi, from_path=from_path
+        )
+        self._workflow_ids[runnable.path] = workflow["id"]
+
+    def workflow_id(self, path):
+        return self._workflow_ids[path]
+
+    @property
+    def use_path_paste(self):
+        option = self._kwds.get("paste_test_data_paths")
+        if option is None:
+            return self.default_use_path_paste
+        else:
+            return option
+
+    @property
+    def default_use_path_paste(self):
+        return False
+
+
+class BaseManagedGalaxyConfig(BaseGalaxyConfig):
+
+    def __init__(
+        self,
+        ctx,
+        config_directory,
+        env,
+        test_data_dir,
+        port,
+        server_name,
+        master_api_key,
+        runnables,
+        kwds,
+    ):
+        galaxy_url = "http://localhost:%d" % port
+        super(BaseManagedGalaxyConfig, self).__init__(
+            ctx=ctx,
+            galaxy_url=galaxy_url,
+            master_api_key=master_api_key,
+            user_api_key=None,
+            runnables=runnables,
+            kwds=kwds
+        )
+        self.config_directory = config_directory
+        self.env = env
+        self.test_data_dir = test_data_dir
+        self.port = port
+        self.server_name = server_name
+
+
+class DockerGalaxyConfig(BaseManagedGalaxyConfig):
+    """A :class:`GalaxyConfig` description of a Dockerized Galaxy instance."""
+
+    def __init__(
+        self,
+        ctx,
+        config_directory,
+        env,
+        test_data_dir,
+        port,
+        server_name,
+        master_api_key,
+        runnables,
+        docker_target_kwds,
+        volumes,
+        export_directory,
+        kwds,
+    ):
+        super(DockerGalaxyConfig, self).__init__(
+            ctx,
+            config_directory,
+            env,
+            test_data_dir,
+            port,
+            server_name,
+            master_api_key,
+            runnables,
+            kwds,
+        )
+        self.docker_target_kwds = docker_target_kwds
+        self.volumes = volumes
+        self.export_directory = export_directory
+
+    def kill(self):
+        """Kill planemo container..."""
+        kill_command = docker_util.kill_command(
+            self.server_name,
+            **self.docker_target_kwds
+        )
+        return shell(kill_command)
+
+    def startup_command(self, ctx, **kwds):
+        """Return a shell command used to startup this instance.
+
+        Among other common planmo kwds, this should respect the
+        ``daemon`` keyword.
+        """
+        daemon = kwds.get("daemon", False)
+        daemon_str = "" if not daemon else " -d"
+        docker_run_extras = "-p %s:80%s" % (self.port, daemon_str)
+        env_directives = ["%s='%s'" % item for item in self.env.items()]
+        image = kwds.get("docker_galaxy_image", "bgruening/galaxy-stable")
+        run_command = docker_util.build_docker_run_command(
+            "", image,
+            interactive=False,
+            env_directives=env_directives,
+            working_directory=None,
+            name=self.server_name,
+            run_extra_arguments=docker_run_extras,
+            set_user=False,
+            volumes=self.volumes,
+            **self.docker_target_kwds
+        )
+        chmod_command = [
+            "chmod",
+            "-R",
+            "o+rwx",
+            self.config_directory,
+        ]
+        if self.export_directory:
+            chmod_command.append(self.export_directory)
+
+        return shell_join(
+            argv_to_str(chmod_command),
+            run_command,
+        )
+
+    @property
+    def log_contents(self):
+        logs_command = docker_util.logs_command(
+            self.server_name,
+            **self.docker_target_kwds
+        )
+        output, _ = communicate(
+            logs_command
+        )
+        return output
+
+    def cleanup(self):
+        shutil.rmtree(self.config_directory, CLEANUP_IGNORE_ERRORS)
+
+
+class LocalGalaxyConfig(BaseManagedGalaxyConfig):
+    """A local, non-containerized implementation of :class:`GalaxyConfig`."""
+
+    def __init__(
+        self,
+        ctx,
+        config_directory,
+        env,
+        test_data_dir,
+        port,
+        server_name,
+        master_api_key,
+        runnables,
+        galaxy_root,
+        kwds,
+    ):
+        super(LocalGalaxyConfig, self).__init__(
+            ctx,
+            config_directory,
+            env,
+            test_data_dir,
+            port,
+            server_name,
+            master_api_key,
+            runnables,
+            kwds,
+        )
+        self.galaxy_root = galaxy_root
+
+    def kill(self):
+        kill_pid_file(self.pid_file)
+
+    def startup_command(self, ctx, **kwds):
+        """Return a shell command used to startup this instance.
+
+        Among other common planemo kwds, this should respect the
+        ``daemon`` keyword.
+        """
+        daemon = kwds.get("daemon", False)
+        # TODO: Allow running dockerized Galaxy here instead.
+        setup_venv_command = setup_venv(ctx, kwds)
+        run_script = "%s $COMMON_STARTUP_ARGS" % shlex_quote(os.path.join(self.galaxy_root, "run.sh"))
+        if daemon:
+            run_script += " --daemon"
+            self.env["GALAXY_RUN_ALL"] = "1"
+        else:
+            run_script += " --server-name %s" % shlex_quote(self.server_name)
+        server_ini = os.path.join(self.config_directory, "galaxy.ini")
+        self.env["GALAXY_CONFIG_FILE"] = server_ini
+        if parse_version(kwds.get('galaxy_python_version') or DEFAULT_PYTHON_VERSION) >= parse_version('3'):
+            # We need to start under gunicorn
+            self.env['APP_WEBSERVER'] = 'gunicorn'
+            self.env['GUNICORN_CMD_ARGS'] = "--bind={host}:{port} --name={server_name}".format(
+                host=kwds.get('host', '127.0.0.1'),
+                port=kwds['port'],
+                server_name=self.server_name,
+            )
+        cd_to_galaxy_command = ['cd', self.galaxy_root]
+        return shell_join(
+            cd_to_galaxy_command,
+            setup_venv_command,
+            setup_common_startup_args(),
+            run_script,
+        )
+
+    @property
+    def log_file(self):
+        """Log file used when planemo serves this Galaxy instance."""
+        file_name = "%s.log" % self.server_name
+        return os.path.join(self.galaxy_root, file_name)
+
+    @property
+    def pid_file(self):
+        pid_file_name = "%s.pid" % self.server_name
+        return os.path.join(self.galaxy_root, pid_file_name)
+
+    @property
+    def log_contents(self):
+        if not os.path.exists(self.log_file):
+            return ""
+        with open(self.log_file, "r") as f:
+            return f.read()
+
+    def cleanup(self):
+        shutil.rmtree(self.config_directory, CLEANUP_IGNORE_ERRORS)
+
+    @property
+    def default_use_path_paste(self):
+        # If Planemo started a local, native Galaxy instance assume files URLs can be
+        # pasted.
+        return True
+
+
+def _database_connection(database_location, **kwds):
+    default_connection = DATABASE_LOCATION_TEMPLATE % database_location
+    database_connection = kwds.get("database_connection") or default_connection
+    return database_connection
+
+
+def _find_galaxy_root(ctx, **kwds):
+    root_prop = "galaxy_root"
+    cwl = kwds.get("cwl", False)
+    if cwl:
+        root_prop = "cwl_galaxy_root"
+    galaxy_root = kwds.get(root_prop, None)
+    if galaxy_root:
+        return galaxy_root
+    else:
+        par_dir = os.getcwd()
+        while True:
+            run = os.path.join(par_dir, "run.sh")
+            config = os.path.join(par_dir, "config")
+            if os.path.isfile(run) and os.path.isdir(config):
+                return par_dir
+            new_par_dir = os.path.dirname(par_dir)
+            if new_par_dir == par_dir:
+                break
+            par_dir = new_par_dir
+    return None
+
+
+def _find_test_data(runnables, **kwds):
+    test_data_search_path = "."
+    runnables = [r for r in runnables if r.has_tools]
+    if len(runnables) > 0:
+        test_data_search_path = runnables[0].test_data_search_path
+
+    # Find test data directory associated with path.
+    test_data = kwds.get("test_data", None)
+    if test_data:
+        return os.path.abspath(test_data)
+    else:
+        test_data = _search_tool_path_for(test_data_search_path, "test-data")
+        if test_data:
+            return test_data
+    warn(NO_TEST_DATA_MESSAGE)
+    return None
+
+
+def _find_tool_data_table(runnables, test_data_dir, **kwds):
+    tool_data_search_path = "."
+    runnables = [r for r in runnables if r.has_tools]
+    if len(runnables) > 0:
+        tool_data_search_path = runnables[0].tool_data_search_path
+
+    tool_data_table = kwds.get("tool_data_table", None)
+    if tool_data_table:
+        return os.path.abspath(tool_data_table)
+    else:
+        extra_paths = [test_data_dir] if test_data_dir else []
+        return _search_tool_path_for(
+            tool_data_search_path,
+            "tool_data_table_conf.xml.test",
+            extra_paths,
+        ) or _search_tool_path_for(  # if all else fails just use sample
+            tool_data_search_path,
+            "tool_data_table_conf.xml.sample"
+        )
+
+
+def _search_tool_path_for(path, target, extra_paths=[]):
+    """Check for presence of a target in different artifact directories."""
+    if not os.path.isdir(path):
+        tool_dir = os.path.dirname(path)
+    else:
+        tool_dir = path
+    possible_dirs = [tool_dir, "."] + extra_paths
+    for possible_dir in possible_dirs:
+        possible_path = os.path.join(possible_dir, target)
+        if os.path.exists(possible_path):
+            return os.path.abspath(possible_path)
+    return None
+
+
+def _configure_sheds_config_file(ctx, config_directory, **kwds):
+    if "shed_target" not in kwds:
+        kwds = kwds.copy()
+        kwds["shed_target"] = "toolshed"
+    shed_target_url = tool_shed_url(ctx, **kwds)
+    contents = _sub(TOOL_SHEDS_CONF, {"shed_target_url": shed_target_url})
+    tool_sheds_conf = os.path.join(config_directory, "tool_sheds_conf.xml")
+    write_file(tool_sheds_conf, contents)
+    return tool_sheds_conf
+
+
+def _tool_conf_entry_for(tool_paths):
+    tool_definitions = ""
+    for tool_path in tool_paths:
+        if os.path.isdir(tool_path):
+            tool_definitions += '''<tool_dir dir="%s" />''' % tool_path
+        else:
+            tool_definitions += '''<tool file="%s" />''' % tool_path
+    return tool_definitions
+
+
+def _install_galaxy(ctx, galaxy_root, env, kwds):
+    if not kwds.get("no_cache_galaxy", False):
+        _install_galaxy_via_git(ctx, galaxy_root, env, kwds)
+    else:
+        _install_galaxy_via_download(ctx, galaxy_root, env, kwds)
+
+
+def _install_galaxy_via_download(ctx, galaxy_root, env, kwds):
+    branch = _galaxy_branch(kwds)
+    untar_to("https://codeload.github.com/galaxyproject/galaxy/tar.gz/" + branch, tar_args=['-xvzf', '-', 'galaxy-' + branch], dest_dir=galaxy_root)
+    _install_with_command(ctx, galaxy_root, env, kwds)
+
+
+def _install_galaxy_via_git(ctx, galaxy_root, env, kwds):
+    gx_repo = _ensure_galaxy_repository_available(ctx, kwds)
+    branch = _galaxy_branch(kwds)
+    command = git.command_clone(ctx, gx_repo, galaxy_root, branch=branch)
+    shell(command, env=env)
+    _install_with_command(ctx, galaxy_root, env, kwds)
+
+
+def _build_eggs_cache(ctx, env, kwds):
+    if kwds.get("no_cache_galaxy", False):
+        return None
+    workspace = ctx.workspace
+    eggs_path = os.path.join(workspace, "gx_eggs")
+    if not os.path.exists(eggs_path):
+        os.makedirs(eggs_path)
+    env["GALAXY_EGGS_PATH"] = eggs_path
+
+
+def _galaxy_branch(kwds):
+    branch = kwds.get("galaxy_branch", None)
+    if branch is None:
+        cwl = kwds.get("cwl", False)
+        branch = "cwl-1.0" if cwl else None
+    if branch is None:
+        branch = DEFAULT_GALAXY_BRANCH
+
+    return branch
+
+
+def _galaxy_source(kwds):
+    source = kwds.get("galaxy_source", None)
+    if source is None:
+        cwl = kwds.get("cwl", False)
+        source = CWL_GALAXY_SOURCE if cwl else None
+    if source is None:
+        source = DEFAULT_GALAXY_SOURCE
+
+    return source
+
+
+def _install_with_command(ctx, galaxy_root, env, kwds):
+    setup_venv_command = setup_venv(ctx, kwds)
+    env['__PYVENV_LAUNCHER__'] = ''
+    install_cmd = shell_join(
+        setup_venv_command,
+        setup_common_startup_args(),
+        COMMAND_STARTUP_COMMAND,
+    )
+    shell(install_cmd, cwd=galaxy_root, env=env)
+
+
+def _ensure_galaxy_repository_available(ctx, kwds):
+    workspace = ctx.workspace
+    cwl = kwds.get("cwl", False)
+    galaxy_source = kwds.get('galaxy_source')
+    if galaxy_source and galaxy_source != DEFAULT_GALAXY_SOURCE:
+        sanitized_repo_name = "".join(c if c.isalnum() else '_' for c in kwds['galaxy_source']).rstrip()[:255]
+        gx_repo = os.path.join(workspace, "gx_repo_%s" % sanitized_repo_name)
+    else:
+        gx_repo = os.path.join(workspace, "gx_repo")
+    if cwl:
+        gx_repo += "_cwl"
+    if os.path.exists(gx_repo):
+        # Convert the git repository from bare to mirror, if needed
+        shell(['git', '--git-dir', gx_repo, 'config', 'remote.origin.fetch', '+refs/*:refs/*'])
+        shell(['git', '--git-dir', gx_repo, 'config', 'remote.origin.mirror', 'true'])
+        # Attempt remote update - but don't fail if not interweb, etc...
+        shell("git --git-dir %s remote update >/dev/null 2>&1" % gx_repo)
+    else:
+        remote_repo = _galaxy_source(kwds)
+        command = git.command_clone(ctx, remote_repo, gx_repo, mirror=True)
+        shell(command)
+    return gx_repo
+
+
+def _build_env_for_galaxy(properties, template_args):
+    env = {}
+    for key, value in iteritems(properties):
+        if value is not None:  # Do not override None with empty string
+            var = "GALAXY_CONFIG_OVERRIDE_%s" % key.upper()
+            value = _sub(value, template_args)
+            env[var] = value
+    return env
+
+
+def _build_test_env(properties, env):
+    # Keeping these environment variables around for a little while but
+    # many are probably not needed as of the following commit.
+    # https://bitbucket.org/galaxy/galaxy-central/commits/d7dd1f9
+    test_property_variants = {
+        'GALAXY_TEST_JOB_CONFIG_FILE': 'job_config_file',
+        'GALAXY_TEST_MIGRATED_TOOL_CONF': 'migrated_tools_config',
+        'GALAXY_TEST_TOOL_CONF': 'tool_config_file',
+        'GALAXY_TEST_FILE_DIR': 'test_data_dir',
+        'GALAXY_TOOL_DEPENDENCY_DIR': 'tool_dependency_dir',
+        # Next line would be required for tool shed tests.
+        # 'GALAXY_TEST_TOOL_DEPENDENCY_DIR': 'tool_dependency_dir',
+    }
+    for test_key, gx_key in test_property_variants.items():
+        value = properties.get(gx_key, None)
+        if value is not None:
+            env[test_key] = value
+
+
+def _handle_job_config_file(config_directory, server_name, kwds):
+    job_config_file = kwds.get("job_config_file", None)
+    if not job_config_file:
+        template_str = JOB_CONFIG_LOCAL
+        job_config_file = os.path.join(
+            config_directory,
+            "job_conf.xml",
+        )
+        docker_enable = str(kwds.get("docker", False))
+        docker_host = str(kwds.get("docker_host", docker_util.DEFAULT_HOST))
+        docker_host_param = ""
+        if docker_host:
+            docker_host_param = """<param id="docker_host">%s</param>""" % docker_host
+
+        conf_contents = Template(template_str).safe_substitute({
+            "server_name": server_name,
+            "docker_enable": docker_enable,
+            "require_container": "false",
+            "docker_sudo": str(kwds.get("docker_sudo", False)),
+            "docker_sudo_cmd": str(kwds.get("docker_sudo_cmd", docker_util.DEFAULT_SUDO_COMMAND)),
+            "docker_cmd": str(kwds.get("docker_cmd", docker_util.DEFAULT_DOCKER_COMMAND)),
+            "docker_host": docker_host_param,
+        })
+        write_file(job_config_file, conf_contents)
+    kwds["job_config_file"] = job_config_file
+
+
+def _write_tool_conf(ctx, tool_paths, tool_conf_path):
+    tool_definition = _tool_conf_entry_for(tool_paths)
+    tool_conf_template_kwds = dict(tool_definition=tool_definition)
+    tool_conf_contents = _sub(TOOL_CONF_TEMPLATE, tool_conf_template_kwds)
+    write_file(tool_conf_path, tool_conf_contents)
+    ctx.vlog(
+        "Writing tool_conf to path %s with contents [%s]",
+        tool_conf_path,
+        tool_conf_contents,
+    )
+
+
+def _handle_container_resolution(ctx, kwds, galaxy_properties):
+    if kwds.get("mulled_containers", False):
+        galaxy_properties["enable_beta_mulled_containers"] = "True"
+        involucro_context = build_involucro_context(ctx, **kwds)
+        galaxy_properties["involucro_auto_init"] = "False"  # Use planemo's
+        galaxy_properties["involucro_path"] = involucro_context.involucro_bin
+
+
+def _handle_job_metrics(config_directory, kwds):
+    metrics_conf = os.path.join(config_directory, "job_metrics_conf.xml")
+    with open(metrics_conf, "w") as fh:
+        fh.write(EMPTY_JOB_METRICS_TEMPLATE)
+    kwds["job_metrics_config_file"] = metrics_conf
+
+
+def _handle_kwd_overrides(properties, kwds):
+    kwds_gx_properties = [
+        'job_config_file',
+        'job_metrics_config_file',
+        'dependency_resolvers_config_file',
+    ]
+    for prop in kwds_gx_properties:
+        val = kwds.get(prop, None)
+        if val:
+            properties[prop] = val
+
+
+def _sub(template, args):
+    if template is None:
+        return ''
+    return Template(template).safe_substitute(args)
+
+
+def _ensure_directory(path):
+    if path is not None and not os.path.exists(path):
+        os.makedirs(path)
+
+
+__all__ = (
+    "DATABASE_LOCATION_TEMPLATE",
+    "galaxy_config",
+)