Skip to content

Commit 20038d0

Browse files
authored
Merge pull request #126 from natefoo/no-root
Rootless processmanagerless mode
2 parents 3a94ea0 + b707f8c commit 20038d0

8 files changed

Lines changed: 152 additions & 37 deletions

File tree

gravity/cli.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
from gravity import io
99
from gravity import options
10+
from gravity.settings import ProcessManager
1011

1112

1213
CONTEXT_SETTINGS = {
@@ -66,14 +67,19 @@ def get_command(self, ctx, name):
6667
@options.config_file_option()
6768
@options.state_dir_option()
6869
@options.no_log_option()
70+
@options.single_user_option()
6971
@click.pass_context
70-
def galaxy(ctx, debug, config_file, state_dir, quiet):
72+
def galaxy(ctx, debug, config_file, state_dir, quiet, single_user):
7173
"""Run Galaxy server in the foreground"""
7274
set_debug(debug)
7375
ctx.cm_kwargs = {
7476
"config_file": config_file,
7577
"state_dir": state_dir,
78+
"process_manager": ProcessManager.multiprocessing.value,
7679
}
80+
if single_user:
81+
os.environ["GALAXY_CONFIG_SINGLE_USER"] = single_user
82+
os.environ["GALAXY_CONFIG_ADMIN_USERS"] = single_user
7783
mod = __import__("gravity.commands.cmd_start", None, None, ["cli"])
7884
return ctx.invoke(mod.cli, foreground=True, quiet=quiet)
7985

gravity/config_manager.py

Lines changed: 34 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,15 @@
1414
from yaml import safe_load
1515

1616
import gravity.io
17-
from gravity.settings import Settings
18-
from gravity.state import ConfigFile, service_for_service_type
17+
from gravity.settings import (
18+
ProcessManager,
19+
Settings,
20+
)
21+
from gravity.state import (
22+
ConfigFile,
23+
service_for_service_type,
24+
galaxy_installed,
25+
)
1926
from gravity.util import recursive_update
2027

2128
log = logging.getLogger(__name__)
@@ -35,22 +42,28 @@
3542

3643

3744
@contextlib.contextmanager
38-
def config_manager(config_file=None, state_dir=None, user_mode=None):
39-
yield ConfigManager(config_file=config_file, state_dir=state_dir, user_mode=user_mode)
45+
def config_manager(config_file=None, state_dir=None, user_mode=None, process_manager=None):
46+
yield ConfigManager(
47+
config_file=config_file,
48+
state_dir=state_dir,
49+
user_mode=user_mode,
50+
process_manager=process_manager
51+
)
4052

4153

4254
class ConfigManager(object):
4355
galaxy_server_config_section = "galaxy"
4456
gravity_config_section = "gravity"
4557
app_config_file_option = "galaxy_config_file"
4658

47-
def __init__(self, config_file=None, state_dir=None, user_mode=None):
59+
def __init__(self, config_file=None, state_dir=None, user_mode=None, process_manager=None):
4860
self.__configs = {}
4961
self.state_dir = None
5062
if state_dir is not None:
5163
# convert from pathlib.Path
5264
self.state_dir = str(state_dir)
5365
self.user_mode = user_mode
66+
self.process_manager = process_manager or ProcessManager.supervisor.value
5467

5568
gravity.io.debug(f"Gravity state dir: {state_dir}")
5669

@@ -151,12 +164,13 @@ def __load_config(self, gravity_config_dict, app_config):
151164
f"{self.__configs[gravity_settings.instance_name].gravity_config_file}")
152165
gravity.io.exception(f"Duplicate instance name {gravity_settings.instance_name}, instance names must be unique")
153166

154-
gravity_config_file = gravity_config_dict["__file__"]
167+
gravity_config_file = gravity_config_dict.get("__file__")
155168
galaxy_config_file = app_config.get("__file__", gravity_config_file)
156169
galaxy_root = gravity_settings.galaxy_root or app_config.get("root")
157170

158171
# TODO: document that the default state_dir is data_dir/gravity and that setting state_dir overrides this
159-
gravity_data_dir = self.state_dir or os.path.join(app_config.get("data_dir", "database"), "gravity")
172+
default_data_dir = "data" if galaxy_installed else "database"
173+
gravity_data_dir = self.state_dir or os.path.join(app_config.get("data_dir", default_data_dir), "gravity")
160174
log_dir = gravity_settings.log_dir or os.path.join(gravity_data_dir, "log")
161175

162176
# TODO: this should use galaxy.util.properties.load_app_properties() so that env vars work
@@ -175,7 +189,7 @@ def __load_config(self, gravity_config_dict, app_config):
175189
gravity_config_file=gravity_config_file,
176190
galaxy_config_file=galaxy_config_file,
177191
instance_name=gravity_settings.instance_name,
178-
process_manager=gravity_settings.process_manager,
192+
process_manager=gravity_settings.process_manager or self.process_manager,
179193
service_command_style=gravity_settings.service_command_style,
180194
app_server=gravity_settings.app_server,
181195
virtualenv=gravity_settings.virtualenv,
@@ -212,7 +226,7 @@ def create_static_handler_services(self, config: ConfigFile, app_config: dict):
212226
job_config = app_config["job_config"]
213227
else:
214228
# config in an external file
215-
config_dir = os.path.dirname(config.galaxy_config_file)
229+
config_dir = os.path.dirname(config.galaxy_config_file or os.getcwd())
216230
job_config = app_config.get("job_config_file")
217231
if not job_config:
218232
for job_config in [os.path.abspath(os.path.join(config_dir, c)) for c in DEFAULT_JOB_CONFIG_FILES]:
@@ -383,9 +397,15 @@ def auto_load(self):
383397
*glob.glob("/etc/galaxy/gravity.d/*.yaml"),
384398
)
385399
else:
386-
configs = (os.path.join("config", "galaxy.yml"), os.path.join("config", "galaxy.yml.sample"))
400+
configs = (os.path.join("config", "galaxy.yml"), "galaxy.yml", os.path.join("config", "galaxy.yml.sample"))
401+
configs = tuple(config for config in configs if os.path.exists(config))
402+
if not configs and galaxy_installed:
403+
gravity.io.warn(
404+
"Warning: No configuration file found but Galaxy is installed in this Python environment, running with "
405+
"default config. Use -c / --config-file or set $GALAXY_CONFIG_FILE to specify a config file."
406+
)
407+
self.__load_config({}, {})
387408
for config in configs:
388-
if os.path.exists(config):
389-
self.load_config_file(os.path.abspath(config))
390-
if not load_all:
391-
return
409+
self.load_config_file(os.path.abspath(config))
410+
if not load_all:
411+
return

gravity/io.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ def error(message, *args):
3434
def warn(message, *args):
3535
if args:
3636
message = message % args
37-
click.echo(click.style(message, fg="red"), err=True)
37+
click.echo(click.style(message, fg="yellow"), err=True)
3838

3939

4040
def exception(message):

gravity/options.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,15 @@ def user_mode_option():
3131
)
3232

3333

34+
def single_user_option():
35+
return click.option(
36+
"-s",
37+
"--single-user",
38+
default=None,
39+
help="Run Galaxy in single user mode with the specified account email"
40+
)
41+
42+
3443
def no_log_option():
3544
return click.option(
3645
'--quiet', is_flag=True, default=False, help="Only output supervisor logs, do not include process logs"

gravity/process_manager/__init__.py

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ def decorator(self, *args, instance_names=None, **kwargs):
6565

6666

6767
class BaseProcessExecutionEnvironment(metaclass=ABCMeta):
68-
def __init__(self, state_dir=None, config_file=None, config_manager=None, user_mode=None):
68+
def __init__(self, state_dir=None, config_file=None, config_manager=None, user_mode=None, process_executor=None):
6969
self.config_manager = config_manager or ConfigManager(state_dir=state_dir, config_file=config_file, user_mode=user_mode)
7070
self.tail = which("tail")
7171

@@ -88,7 +88,8 @@ def _service_format_vars(self, config, service, pm_format_vars=None):
8888
"server_name": service.service_name,
8989
"galaxy_umask": service.settings.get("umask") or config.umask,
9090
"galaxy_conf": config.galaxy_config_file,
91-
"galaxy_root": config.galaxy_root,
91+
# TODO: this is used as the runtime directory, but it should probably be something else
92+
"galaxy_root": config.galaxy_root or os.getcwd(),
9293
"virtualenv_bin": virtualenv_bin,
9394
"gravity_data_dir": shlex.quote(config.gravity_data_dir),
9495
"app_config": config.app_config,
@@ -112,7 +113,9 @@ def _service_format_vars(self, config, service, pm_format_vars=None):
112113
path = environment.get("PATH", self._service_default_path())
113114
environment["PATH"] = ":".join([virtualenv_bin, path])
114115
else:
115-
config_file = shlex.quote(config.gravity_config_file)
116+
config_file_option = ""
117+
if config.gravity_config_file:
118+
config_file_option = f" --config-file {shlex.quote(config.gravity_config_file)}"
116119
# is there a click way to do this?
117120
galaxyctl = sys.argv[0]
118121
if galaxyctl.endswith(f"{os.path.sep}galaxy"):
@@ -124,7 +127,7 @@ def _service_format_vars(self, config, service, pm_format_vars=None):
124127
instance_number_opt = ""
125128
if service.count > 1:
126129
instance_number_opt = f" --service-instance {pm_format_vars['instance_number']}"
127-
format_vars["command"] = f"{galaxyctl} --config-file {config_file} exec{instance_number_opt} {config.instance_name} {service.service_name}"
130+
format_vars["command"] = f"{galaxyctl}{config_file_option} exec{instance_number_opt} {config.instance_name} {service.service_name}"
128131
environment = {}
129132
format_vars["environment"] = self._service_environment_formatter(environment, format_vars)
130133

@@ -273,7 +276,7 @@ def exec(self, config, service, service_instance_number=None, no_exec=False):
273276

274277
cmd = shlex.split(format_vars["command"])
275278
env = {**dict(os.environ), **format_vars["environment"]}
276-
cwd = format_vars["galaxy_root"]
279+
cwd = format_vars["galaxy_root"] or os.getcwd()
277280

278281
# ensure the data dir exists
279282
try:
@@ -291,10 +294,13 @@ def exec(self, config, service, service_instance_number=None, no_exec=False):
291294

292295

293296
class ProcessManagerRouter:
294-
def __init__(self, state_dir=None, config_file=None, config_manager=None, user_mode=None, **kwargs):
295-
self.config_manager = config_manager or ConfigManager(state_dir=state_dir, config_file=config_file, user_mode=user_mode)
296-
self._load_pm_modules(**kwargs)
297+
def __init__(self, state_dir=None, config_file=None, config_manager=None, user_mode=None, process_manager=None, **kwargs):
298+
self.config_manager = config_manager or ConfigManager(state_dir=state_dir,
299+
config_file=config_file,
300+
user_mode=user_mode,
301+
process_manager=process_manager)
297302
self._process_executor = ProcessExecutor(config_manager=self.config_manager)
303+
self._load_pm_modules(**kwargs)
298304

299305
def _load_pm_modules(self, *args, **kwargs):
300306
self.process_managers = {}
@@ -304,7 +310,7 @@ def _load_pm_modules(self, *args, **kwargs):
304310
for name in dir(mod):
305311
obj = getattr(mod, name)
306312
if not name.startswith("_") and inspect.isclass(obj) and issubclass(obj, BaseProcessManager) and obj != BaseProcessManager:
307-
pm = obj(*args, config_manager=self.config_manager, **kwargs)
313+
pm = obj(*args, config_manager=self.config_manager, process_executor=self._process_executor, **kwargs)
308314
self.process_managers[pm.name] = pm
309315

310316
def _instance_service_names(self, names):
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
"""
2+
"""
3+
import multiprocessing
4+
5+
from gravity.process_manager import BaseProcessManager, ProcessExecutor
6+
from gravity.settings import ProcessManager
7+
8+
9+
class MultiprocessingProcessManager(BaseProcessManager):
10+
11+
name = ProcessManager.multiprocessing
12+
13+
def __init__(self, process_executor=None, **kwargs):
14+
super().__init__(**kwargs)
15+
16+
assert process_executor is not None, f"Process executor is required for {self.__class__.__name__}"
17+
self.process_executor = process_executor
18+
self.processes = []
19+
20+
def follow(self, configs=None, service_names=None, quiet=False):
21+
""" """
22+
23+
def start(self, configs=None, service_names=None):
24+
for config in configs:
25+
for service in config.services:
26+
process = multiprocessing.Process(target=self.process_executor.exec, args=(config, service))
27+
process.start()
28+
self.processes.append(process)
29+
for process in self.processes:
30+
process.join()
31+
32+
def pm(self, *args, **kwargs):
33+
""" """
34+
35+
def stop(self, configs=None, service_names=None):
36+
""" """
37+
38+
def _present_pm_files_for_config(self, config):
39+
""" """
40+
41+
def _disable_and_remove_pm_files(self, pm_files):
42+
""" """
43+
44+
def restart(self, configs=None, service_names=None):
45+
""" """
46+
47+
def graceful(self, configs=None, service_names=None):
48+
""" """
49+
50+
def status(self, configs=None, service_names=None):
51+
""" """
52+
53+
def terminate(self):
54+
""" """
55+
56+
def shutdown(self):
57+
""" """
58+
59+
def update(self, configs=None, force=False, clean=False):
60+
""" """
61+
62+
_service_environment_formatter = ProcessExecutor._service_environment_formatter

gravity/settings.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ class LogLevel(str, Enum):
3434
class ProcessManager(str, Enum):
3535
supervisor = "supervisor"
3636
systemd = "systemd"
37+
multiprocessing = "multiprocessing"
3738

3839

3940
class ServiceCommandStyle(str, Enum):
@@ -296,12 +297,13 @@ class Settings(BaseSettings):
296297
``uwsgi:`` section will be ignored if Galaxy is started via Gravity commands (e.g ``./run.sh``, ``galaxy`` or ``galaxyctl``).
297298
"""
298299

299-
process_manager: ProcessManager = Field(
300+
process_manager: Optional[ProcessManager] = Field(
300301
None,
301302
description="""
302303
Process manager to use.
303304
``supervisor`` is the default process manager when Gravity is invoked as a non-root user.
304305
``systemd`` is the default when Gravity is invoked as root.
306+
``multiprocessing`` is the default when Gravity is invoked as the foreground shortcut ``galaxy`` instead of ``galaxyctl``
305307
""")
306308

307309
service_command_style: ServiceCommandStyle = Field(
@@ -421,8 +423,6 @@ def _process_manager_systemd_if_root(cls, v, values):
421423
if v is None:
422424
if os.geteuid() == 0:
423425
v = ProcessManager.systemd.value
424-
else:
425-
v = ProcessManager.supervisor.value
426426
return v
427427

428428
# disable service instances unless command style is gravity

gravity/state.py

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,12 @@
1010
import time
1111
from typing import Any, Dict, List, Optional
1212

13+
try:
14+
import galaxy.config
15+
import galaxy.version
16+
galaxy_installed = True
17+
except ImportError:
18+
galaxy_installed = False
1319
try:
1420
from pydantic.v1 import BaseModel, validator
1521
except ImportError:
@@ -28,7 +34,8 @@
2834

2935
def relative_to_galaxy_root(cls, v, values):
3036
if not os.path.isabs(v):
31-
v = os.path.abspath(os.path.join(values["galaxy_root"], v))
37+
galaxy_root = values.get("galaxy_root") or os.getcwd()
38+
v = os.path.abspath(os.path.join(galaxy_root, v))
3239
return v
3340

3441

@@ -41,8 +48,8 @@ class GracefulMethod(str, enum.Enum):
4148

4249
class ConfigFile(BaseModel):
4350
app_config: Dict[str, Any]
44-
gravity_config_file: str
45-
galaxy_config_file: str
51+
gravity_config_file: Optional[str]
52+
galaxy_config_file: Optional[str]
4653
instance_name: str
4754
process_manager: ProcessManager
4855
service_command_style: ServiceCommandStyle
@@ -66,18 +73,23 @@ def path_hash(self):
6673

6774
@property
6875
def galaxy_version(self):
69-
galaxy_version_file = os.path.join(self.galaxy_root, "lib", "galaxy", "version.py")
70-
with open(galaxy_version_file) as fh:
71-
locs = {}
72-
exec(fh.read(), {}, locs)
73-
return locs["VERSION"]
76+
if galaxy_installed:
77+
return galaxy.version.VERSION
78+
else:
79+
galaxy_version_file = os.path.join(self.galaxy_root, "lib", "galaxy", "version.py")
80+
with open(galaxy_version_file) as fh:
81+
locs = {}
82+
exec(fh.read(), {}, locs)
83+
return locs["VERSION"]
7484

7585
@validator("galaxy_root")
7686
def _galaxy_root_required(cls, v, values):
7787
if v is None:
78-
galaxy_config_file = values["galaxy_config_file"]
88+
galaxy_config_file = values.get("galaxy_config_file")
7989
if os.environ.get("GALAXY_ROOT_DIR"):
8090
v = os.path.abspath(os.environ["GALAXY_ROOT_DIR"])
91+
elif galaxy_installed:
92+
v = None
8193
elif os.path.exists(os.path.join(os.path.dirname(galaxy_config_file), os.pardir, "lib", "galaxy")):
8294
v = os.path.abspath(os.path.join(os.path.dirname(galaxy_config_file), os.pardir))
8395
elif galaxy_config_file.endswith(os.path.join("galaxy", "config", "sample", "galaxy.yml.sample")):

0 commit comments

Comments
 (0)