Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support managing gx-it-proxy via gravity #32

Merged
merged 6 commits into from
Mar 4, 2022
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions gravity/commands/cmd_update.py
Original file line number Diff line number Diff line change
@@ -4,8 +4,9 @@


@click.command("update")
@click.option("--force", is_flag=True, help="Force rewriting of process config files")
@click.pass_context
def cli(ctx):
def cli(ctx, force):
"""Update process manager from config changes."""
with process_manager.process_manager(state_dir=ctx.parent.state_dir, start_daemon=False) as pm:
pm.update()
pm.update(force)
18 changes: 17 additions & 1 deletion gravity/config_manager.py
Original file line number Diff line number Diff line change
@@ -16,6 +16,9 @@
CELERY_DEFAULT_CONFIG,
DEFAULT_INSTANCE_NAME,
GUNICORN_DEFAULT_CONFIG,
GXIT_DEFAULT_IP,
GXIT_DEFAULT_PORT,
GXIT_DEFAULT_SESSIONS,
)
from gravity.io import debug, error, exception, info, warn
from gravity.state import (
@@ -85,6 +88,7 @@ def get_config(self, conf, defaults=None):
"app_server": "gunicorn",
"gunicorn": GUNICORN_DEFAULT_CONFIG,
"celery": CELERY_DEFAULT_CONFIG,
"gx_it_proxy": {},
"handlers": {},
}
if defaults is not None:
@@ -109,6 +113,7 @@ def get_config(self, conf, defaults=None):
config.attribs["gunicorn"] = gravity_config["gunicorn"]
config.attribs["celery"] = gravity_config["celery"]
config.attribs["handlers"] = gravity_config["handlers"]
config.attribs["gx_it_proxy"] = gravity_config["gx_it_proxy"]
# Store gravity version, in case we need to convert old setting
config.attribs['gravity_version'] = __version__
webapp_service_names = []
@@ -129,7 +134,6 @@ def get_config(self, conf, defaults=None):
config.services.append(service_for_service_type("celery")(config_type=config.config_type))
config.services.append(service_for_service_type("celery-beat")(config_type=config.config_type))
# If this is a Galaxy config, parse job_conf.xml for any *static* standalone handlers
# Marius: Don't think that's gonna work if job config file not defined!
# TODO: use galaxy config parsing ?
# TODO: if not, need yaml job config parsing
job_conf_xml = app_config.get("job_config_file", DEFAULT_JOB_CONFIG_FILE)
@@ -149,6 +153,7 @@ def get_config(self, conf, defaults=None):
# doesn't parse that part of the job config. See logic in lib/galaxy/web_stack/handlers.py _get_is_handler() to
# see how this is determined.
self.create_handler_services(gravity_config, config)
self.create_gxit_services(gravity_config, app_config, config)
return config

def create_handler_services(self, gravity_config, config):
@@ -158,6 +163,17 @@ def create_handler_services(self, gravity_config, config):
config.services.append(
service_for_service_type("standalone")(config_type=config.config_type, service_name=service_name, server_pools=pools))

def create_gxit_services(self, gravity_config, app_config, config):
if app_config.get("interactivetools_enable") and gravity_config["gx_it_proxy"].get("enable"):
# TODO: resolve against data_dir, or bring in galaxy-config ?
# CWD in supervisor template is galaxy_root, so this should work for simple cases as is
gxit_config = gravity_config['gx_it_proxy']
gxit_config["sessions"] = app_config.get("interactivetools_map", GXIT_DEFAULT_SESSIONS)
gxit_config["ip"] = gxit_config.get("ip", GXIT_DEFAULT_IP)
gxit_config["port"] = gxit_config.get("port", GXIT_DEFAULT_PORT)
gxit_config["verbose"] = '--verbose' if gxit_config.get("verbose") else ''
config.services.append(service_for_service_type("gx-it-proxy")(config_type=config.config_type, gxit=gxit_config))

@staticmethod
def expand_handlers(gravity_config, config):
handlers = gravity_config.get("handlers", {})
3 changes: 3 additions & 0 deletions gravity/defaults.py
Original file line number Diff line number Diff line change
@@ -14,3 +14,6 @@
"concurrency": 2,
"extra_args": ""
}
GXIT_DEFAULT_IP = "localhost"
GXIT_DEFAULT_PORT = 4002
GXIT_DEFAULT_SESSIONS = "database/interactivetools_map.sqlite"
30 changes: 26 additions & 4 deletions gravity/process_manager/supervisor_manager.py
Original file line number Diff line number Diff line change
@@ -116,6 +116,27 @@
{process_name_opt}
"""


SUPERVISORD_SERVICE_TEMPLATES["gx-it-proxy"] = """;
; This file is maintained by Galaxy - CHANGES WILL BE OVERWRITTEN
;

[program:{program_name}]
command = {command}
directory = {galaxy_root}
umask = {galaxy_umask}
autostart = true
autorestart = true
startsecs = 10
stopwaitsecs = 10
environment = npm_config_yes=true
numprocs = 1
stdout_logfile = {log_file}
redirect_stderr = true
{process_name_opt}
"""


SUPERVISORD_SERVICE_TEMPLATES["standalone"] = """;
; This file is maintained by Galaxy - CHANGES WILL BE OVERWRITTEN
;
@@ -237,6 +258,7 @@ def __update_service(self, config_file, config, attribs, service, instance_conf_
"attach_to_pool_opt": attach_to_pool_opt,
"gunicorn": attribs["gunicorn"],
"celery": attribs["celery"],
"gx_it_proxy": attribs["gx_it_proxy"],
"galaxy_umask": service.get("umask", "022"),
"program_name": program_name,
"process_name_opt": process_name_opt,
@@ -258,7 +280,7 @@ def __update_service(self, config_file, config, attribs, service, instance_conf_
with open(conf, "w") as out:
out.write(template.format(**format_vars))

def _process_config_changes(self, configs, meta_changes):
def _process_config_changes(self, configs, meta_changes, force=False):
# remove the services of any configs which have been removed
for config in meta_changes["remove_configs"].values():
instance_name = config["instance_name"]
@@ -273,7 +295,7 @@ def _process_config_changes(self, configs, meta_changes):
for config_file, config in configs.items():
instance_name = config["instance_name"]
attribs = config["attribs"]
update_all_configs = False
update_all_configs = False or force

# config attribs have changed (galaxy_root, virtualenv, etc.)
if "update_attribs" in config:
@@ -410,10 +432,10 @@ def shutdown(self):
time.sleep(0.5)
info("supervisord has terminated")

def update(self):
def update(self, force=False):
"""Add newly defined servers, remove any that are no longer present"""
configs, meta_changes = self.config_manager.determine_config_changes()
self._process_config_changes(configs, meta_changes)
self._process_config_changes(configs, meta_changes, force)
# only need to update if supervisord is running, otherwise changes will be picked up at next start
if self.__supervisord_is_running():
self.supervisorctl("update")
8 changes: 8 additions & 0 deletions gravity/state.py
Original file line number Diff line number Diff line change
@@ -81,6 +81,13 @@ class GalaxyCeleryBeatService(Service):
command_template = "{virtualenv_bin}celery --app galaxy.celery beat --loglevel {celery[loglevel]}"


class GalaxyGxItProxyService(Service):
service_type = "gx-it-proxy"
service_name = "gx-it-proxy"
command_template = "{virtualenv_bin}npx gx-it-proxy --ip {gx_it_proxy[ip]} --port {gx_it_proxy[port]}" \
" --sessions {gx_it_proxy[sessions]} {gx_it_proxy[verbose]}"


class GalaxyStandaloneService(Service):
service_type = "standalone"
service_name = "standalone"
@@ -164,5 +171,6 @@ def service_for_service_type(service_type):
"unicornherder": GalaxyUnicornHerderService,
"celery": GalaxyCeleryService,
"celery-beat": GalaxyCeleryBeatService,
"gx-it-proxy": GalaxyGxItProxyService,
"standalone": GalaxyStandaloneService,
}
32 changes: 32 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
@@ -7,9 +7,26 @@
from pathlib import Path

import pytest
import yaml
from gravity import config_manager

TEST_DIR = Path(os.path.dirname(__file__))
GXIT_CONFIG = """
gravity:
gunicorn:
bind: 'localhost:{gx_port}'
gx_it_proxy:
enable: true
port: {gxit_port}
verbose: true
galaxy:
conda_auto_init: false
interactivetools_enable: true
interactivetools_map: database/interactivetools_map.sqlite
galaxy_infrastructure_url: http://localhost:{gx_port}
interactivetools_upstream_proxy: false
interactivetools_proxy_host: localhost:{gxit_port}
"""


@pytest.fixture(scope='session')
@@ -77,6 +94,9 @@ def free_port():
return portnum


another_free_port = free_port


@pytest.fixture()
def startup_config(galaxy_virtualenv, free_port):
return {
@@ -91,6 +111,18 @@ def startup_config(galaxy_virtualenv, free_port):
}


@pytest.fixture
def gxit_config(free_port, another_free_port):
config_yaml = GXIT_CONFIG.format(gxit_port=another_free_port, gx_port=free_port)
return yaml.safe_load(config_yaml)


@pytest.fixture
def gxit_startup_config(galaxy_virtualenv, gxit_config):
gxit_config['gravity']['virtualenv'] = galaxy_virtualenv
return gxit_config


@pytest.fixture(scope="session")
def galaxy_virtualenv(galaxy_root_dir):
virtual_env_dir = str(TEST_DIR / "galaxy_venv")
55 changes: 40 additions & 15 deletions tests/test_operations.py
Original file line number Diff line number Diff line change
@@ -14,15 +14,15 @@
def test_cmd_register(state_dir, galaxy_yml):
runner = CliRunner()
result = runner.invoke(galaxyctl, ['--state-dir', state_dir, 'register', str(galaxy_yml)])
assert result.exit_code == 0
assert result.exit_code == 0, result.output
assert 'Registered galaxy config:' in result.output


def test_cmd_deregister(state_dir, galaxy_yml):
test_cmd_register(state_dir, galaxy_yml)
runner = CliRunner()
result = runner.invoke(galaxyctl, ['--state-dir', state_dir, 'deregister', str(galaxy_yml)])
assert result.exit_code == 0
assert result.exit_code == 0, result.output
assert 'Deregistered config:' in result.output


@@ -38,11 +38,22 @@ def wait_for_startup(state_dir, free_port, prefix="/"):
return startup_logs


def wait_for_gxit_proxy(state_dir):
startup_logs = ""
with open(state_dir / "log" / 'gx-it-proxy.log') as fh:
for _ in range(STARTUP_TIMEOUT * 4):
startup_logs = fh.read()
if 'Listening' in startup_logs:
return True
time.sleep(0.25)
return startup_logs


def start_instance(state_dir, free_port):
runner = CliRunner()
result = runner.invoke(galaxyctl, ['--state-dir', state_dir, 'start'])
assert re.search(r"gunicorn\s*STARTING", result.output)
assert result.exit_code == 0
assert result.exit_code == 0, result.output
startup_done = wait_for_startup(state_dir, free_port)
assert startup_done is True, f"Startup failed. Application startup logs:\n {startup_done}"

@@ -51,29 +62,43 @@ def test_cmd_start(state_dir, galaxy_yml, startup_config, free_port):
galaxy_yml.write(json.dumps(startup_config))
runner = CliRunner()
result = runner.invoke(galaxyctl, ['--state-dir', state_dir, 'register', str(galaxy_yml)])
assert result.exit_code == 0
assert result.exit_code == 0, result.output
result = runner.invoke(galaxyctl, ['--state-dir', state_dir, 'update'])
assert result.exit_code == 0
assert result.exit_code == 0, result.output
start_instance(state_dir, free_port)
result = runner.invoke(galaxyctl, ['--state-dir', state_dir, 'stop'])
assert result.exit_code == 0
assert result.exit_code == 0, result.output
assert "All processes stopped, supervisord will exit" in result.output


def test_cmd_start_with_gxit(state_dir, galaxy_yml, gxit_startup_config, free_port):
galaxy_yml.write(json.dumps(gxit_startup_config))
runner = CliRunner()
result = runner.invoke(galaxyctl, ['--state-dir', state_dir, 'register', str(galaxy_yml)])
assert result.exit_code == 0, result.output
result = runner.invoke(galaxyctl, ['--state-dir', state_dir, 'update'])
assert result.exit_code == 0, result.output
start_instance(state_dir, free_port)
result = runner.invoke(galaxyctl, ['--state-dir', state_dir, 'status'])
assert result.exit_code == 0, result.output
startup_done = wait_for_gxit_proxy(state_dir)
assert startup_done is True, f"gx-it-proxy startup failed. gx-it-proxy startup logs:\n {startup_done}"


def test_cmd_restart_with_update(state_dir, galaxy_yml, startup_config, free_port):
galaxy_yml.write(json.dumps(startup_config))
runner = CliRunner()
result = runner.invoke(galaxyctl, ['--state-dir', state_dir, 'register', str(galaxy_yml)])
assert result.exit_code == 0
assert result.exit_code == 0, result.output
result = runner.invoke(galaxyctl, ['--state-dir', state_dir, 'update'])
assert result.exit_code == 0
assert result.exit_code == 0, result.output
start_instance(state_dir, free_port)
# change prefix
prefix = '/galaxypf/'
startup_config['galaxy']['galaxy_url_prefix'] = prefix
galaxy_yml.write(json.dumps(startup_config))
result = runner.invoke(galaxyctl, ['--state-dir', state_dir, 'restart'])
assert result.exit_code == 0
assert result.exit_code == 0, result.output
startup_done = wait_for_startup(state_dir=state_dir, free_port=free_port, prefix=prefix)
assert startup_done is True, f"Startup failed. Application startup logs:\n {startup_done}"

@@ -82,7 +107,7 @@ def test_cmd_show(state_dir, galaxy_yml):
test_cmd_register(state_dir, galaxy_yml)
runner = CliRunner()
result = runner.invoke(galaxyctl, ['--state-dir', state_dir, 'show', str(galaxy_yml)])
assert result.exit_code == 0
assert result.exit_code == 0, result.output
details = safe_load(result.output)
assert details['config_type'] == 'galaxy'

@@ -97,7 +122,7 @@ def test_cmd_show_config_does_not_exist(state_dir, galaxy_yml):
assert f'To register this config file run "galaxyctl register {str(galaxy_yml)}"' in result.output
# register the sample file, but ask for galaxy.yml
result = runner.invoke(galaxyctl, ['--state-dir', state_dir, 'register', str(galaxy_yml + '.sample')])
assert result.exit_code == 0
assert result.exit_code == 0, result.output
result = runner.invoke(galaxyctl, ['--state-dir', state_dir, 'show', str(galaxy_yml)])
assert result.exit_code == 1
assert f"{str(galaxy_yml)} is not a registered config file." in result.output
@@ -108,21 +133,21 @@ def test_cmd_show_config_does_not_exist(state_dir, galaxy_yml):
def test_cmd_instances(state_dir, galaxy_yml):
runner = CliRunner()
result = runner.invoke(galaxyctl, ['--state-dir', state_dir, 'instances'])
assert result.exit_code == 0
assert result.exit_code == 0, result.output
assert not result.output
test_cmd_register(state_dir, galaxy_yml)
result = runner.invoke(galaxyctl, ['--state-dir', state_dir, 'instances'])
assert result.exit_code == 0
assert result.exit_code == 0, result.output
assert "_default_" in result.output


def test_cmd_configs(state_dir, galaxy_yml):
runner = CliRunner()
result = runner.invoke(galaxyctl, ['--state-dir', state_dir, 'configs'])
assert result.exit_code == 0
assert result.exit_code == 0, result.output
assert 'No config files registered' in result.output
test_cmd_register(state_dir, galaxy_yml)
result = runner.invoke(galaxyctl, ['--state-dir', state_dir, 'configs'])
assert result.exit_code == 0
assert result.exit_code == 0, result.output
assert result.output.startswith("TYPE")
assert str(galaxy_yml) in result.output
27 changes: 27 additions & 0 deletions tests/test_process_manager.py
Original file line number Diff line number Diff line change
@@ -55,6 +55,20 @@ def test_update(galaxy_yml, default_config_manager):
pm.update()


def test_update_force(galaxy_yml, default_config_manager):
test_update(galaxy_yml, default_config_manager)
instance_conf_dir = Path(default_config_manager.state_dir) / 'supervisor' / 'supervisord.conf.d' / '_default_.d'
gunicorn_conf_path = instance_conf_dir / "galaxy_gunicorn_gunicorn.conf"
assert gunicorn_conf_path.exists()
update_time = gunicorn_conf_path.stat().st_mtime
with process_manager.process_manager(state_dir=default_config_manager.state_dir) as pm:
pm.update()
assert gunicorn_conf_path.stat().st_mtime == update_time
with process_manager.process_manager(state_dir=default_config_manager.state_dir) as pm:
pm.update(force=True)
assert gunicorn_conf_path.stat().st_mtime != update_time


@pytest.mark.parametrize('job_conf', [[JOB_CONF_XML_DYNAMIC_HANDLERS]], indirect=True)
def test_dynamic_handlers(default_config_manager, galaxy_yml, job_conf):
galaxy_yml.write(DYNAMIC_HANDLER_CONFIG)
@@ -97,3 +111,16 @@ def test_static_handlers(default_config_manager, galaxy_yml, job_conf):
handler1_config_path = instance_conf_dir / 'galaxy_standalone_handler1.conf'
assert handler1_config_path.exists()
assert 'galaxy.yml --server-name=handler1 --pid-file=' in handler1_config_path.open().read()


def test_gxit_handler(default_config_manager, galaxy_yml, gxit_config):
galaxy_yml.write(json.dumps(gxit_config))
default_config_manager.add([str(galaxy_yml)])
with process_manager.process_manager(state_dir=default_config_manager.state_dir) as pm:
pm.update()
instance_conf_dir = Path(default_config_manager.state_dir) / 'supervisor' / 'supervisord.conf.d' / '_default_.d'
gxit_config_path = instance_conf_dir / 'galaxy_gx-it-proxy_gx-it-proxy.conf'
assert gxit_config_path.exists()
gxit_port = gxit_config["gravity"]["gx_it_proxy"]["port"]
sessions = "database/interactivetools_map.sqlite"
assert f'npx gx-it-proxy --ip localhost --port {gxit_port} --sessions {sessions}' in gxit_config_path.read_text()