mirror of
https://github.com/frappe/bench.git
synced 2024-11-14 17:24:04 +00:00
refactor: Simplify Bench with OOP
Goals: - Commonify bench operations in a way that intuitive - Get rid of the multiple duplicate functions because it's so hard to understand flow in the codebase ;) - Eliminate the need to guess, re-guess and pass bench info in each function that needs to do literally anything - Increase my happiness index because I just realised that I'm the top contributor of this project and I'd like to make it my own. - Adopt the principles of Least Surprise & The Principle of The Bigger Smile (of DHH) [inspired by ruby & ror] Changes: - The bench module has Bench and the action classes that can be accessed through the bench object - Used the Bench class to access properties like sites, apps & run (to execute commands in context) to reduce effort and clutter - Style improvements & minor changes included
This commit is contained in:
parent
b59379c5a9
commit
153546afd7
43
bench/app.py
43
bench/app.py
@ -102,13 +102,15 @@ class App(AppMeta):
|
|||||||
|
|
||||||
|
|
||||||
def add_to_appstxt(app, bench_path='.'):
|
def add_to_appstxt(app, bench_path='.'):
|
||||||
apps = get_apps(bench_path=bench_path)
|
apps = Bench(bench_path).apps
|
||||||
|
|
||||||
if app not in apps:
|
if app not in apps:
|
||||||
apps.append(app)
|
apps.append(app)
|
||||||
return write_appstxt(apps, bench_path=bench_path)
|
return write_appstxt(apps, bench_path=bench_path)
|
||||||
|
|
||||||
def remove_from_appstxt(app, bench_path='.'):
|
def remove_from_appstxt(app, bench_path='.'):
|
||||||
apps = get_apps(bench_path=bench_path)
|
apps = Bench(bench_path).apps
|
||||||
|
|
||||||
if app in apps:
|
if app in apps:
|
||||||
apps.remove(app)
|
apps.remove(app)
|
||||||
return write_appstxt(apps, bench_path=bench_path)
|
return write_appstxt(apps, bench_path=bench_path)
|
||||||
@ -189,7 +191,7 @@ def setup_app_dependencies(repo_name, bench_path='.', branch=None):
|
|||||||
required_apps = eval(lines[0].strip('required_apps').strip().lstrip('=').strip())
|
required_apps = eval(lines[0].strip('required_apps').strip().lstrip('=').strip())
|
||||||
# TODO: when the time comes, add version check here
|
# TODO: when the time comes, add version check here
|
||||||
for app in required_apps:
|
for app in required_apps:
|
||||||
if app not in get_apps(bench_path=bench_path):
|
if app not in Bench(bench_path).apps:
|
||||||
get_app(app, bench_path=bench_path, branch=branch)
|
get_app(app, bench_path=bench_path, branch=branch)
|
||||||
|
|
||||||
def get_app(git_url, branch=None, bench_path='.', skip_assets=False, verbose=False, overwrite=False):
|
def get_app(git_url, branch=None, bench_path='.', skip_assets=False, verbose=False, overwrite=False):
|
||||||
@ -219,12 +221,14 @@ def get_app(git_url, branch=None, bench_path='.', skip_assets=False, verbose=Fal
|
|||||||
if dir_already_exists:
|
if dir_already_exists:
|
||||||
# application directory already exists
|
# application directory already exists
|
||||||
# prompt user to overwrite it
|
# prompt user to overwrite it
|
||||||
if overwrite or click.confirm(f'''A directory for the application "{repo_name}" already exists.
|
if overwrite or click.confirm(
|
||||||
Do you want to continue and overwrite it?'''):
|
f"A directory for the application '{repo_name}' already exists."
|
||||||
|
"Do you want to continue and overwrite it?"
|
||||||
|
):
|
||||||
import shutil
|
import shutil
|
||||||
shutil.rmtree(cloned_path)
|
shutil.rmtree(cloned_path)
|
||||||
to_clone = True
|
to_clone = True
|
||||||
elif click.confirm('''Do you want to reinstall the existing application?''', abort=True):
|
elif click.confirm("Do you want to reinstall the existing application?", abort=True):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
if to_clone:
|
if to_clone:
|
||||||
@ -282,7 +286,6 @@ def new_app(app, bench_path='.'):
|
|||||||
|
|
||||||
def install_app(app, bench_path=".", verbose=False, no_cache=False, restart_bench=True, skip_assets=False):
|
def install_app(app, bench_path=".", verbose=False, no_cache=False, restart_bench=True, skip_assets=False):
|
||||||
from bench.utils import get_env_cmd
|
from bench.utils import get_env_cmd
|
||||||
from bench.config.common_site_config import get_config
|
|
||||||
|
|
||||||
install_text = f'Installing {app}'
|
install_text = f'Installing {app}'
|
||||||
click.secho(install_text, fg="yellow")
|
click.secho(install_text, fg="yellow")
|
||||||
@ -300,7 +303,7 @@ def install_app(app, bench_path=".", verbose=False, no_cache=False, restart_benc
|
|||||||
|
|
||||||
add_to_appstxt(app, bench_path=bench_path)
|
add_to_appstxt(app, bench_path=bench_path)
|
||||||
|
|
||||||
conf = get_config(bench_path=bench_path)
|
conf = Bench(bench_path).conf
|
||||||
|
|
||||||
if conf.get("developer_mode"):
|
if conf.get("developer_mode"):
|
||||||
from bench.utils import install_python_dev_dependencies
|
from bench.utils import install_python_dev_dependencies
|
||||||
@ -318,13 +321,13 @@ def install_app(app, bench_path=".", verbose=False, no_cache=False, restart_benc
|
|||||||
|
|
||||||
def remove_app(app, bench_path='.'):
|
def remove_app(app, bench_path='.'):
|
||||||
import shutil
|
import shutil
|
||||||
from bench.config.common_site_config import get_config
|
|
||||||
|
|
||||||
|
bench = Bench(bench_path)
|
||||||
app_path = os.path.join(bench_path, 'apps', app)
|
app_path = os.path.join(bench_path, 'apps', app)
|
||||||
py = os.path.join(bench_path, 'env', 'bin', 'python')
|
py = os.path.join(bench_path, 'env', 'bin', 'python')
|
||||||
|
|
||||||
# validate app removal
|
# validate app removal
|
||||||
if app not in get_apps(bench_path):
|
if app not in bench.apps:
|
||||||
print(f"No app named {app}")
|
print(f"No app named {app}")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
@ -337,9 +340,10 @@ def remove_app(app, bench_path='.'):
|
|||||||
|
|
||||||
# re-build assets and restart processes
|
# re-build assets and restart processes
|
||||||
run_frappe_cmd("build", bench_path=bench_path)
|
run_frappe_cmd("build", bench_path=bench_path)
|
||||||
if get_config(bench_path).get('restart_supervisor_on_update'):
|
|
||||||
|
if bench.conf.get('restart_supervisor_on_update'):
|
||||||
restart_supervisor_processes(bench_path=bench_path)
|
restart_supervisor_processes(bench_path=bench_path)
|
||||||
if get_config(bench_path).get('restart_systemd_on_update'):
|
if bench.conf.get('restart_systemd_on_update'):
|
||||||
restart_systemd_processes(bench_path=bench_path)
|
restart_systemd_processes(bench_path=bench_path)
|
||||||
|
|
||||||
|
|
||||||
@ -388,11 +392,10 @@ def check_app_installed_legacy(app, bench_path="."):
|
|||||||
|
|
||||||
def pull_apps(apps=None, bench_path='.', reset=False):
|
def pull_apps(apps=None, bench_path='.', reset=False):
|
||||||
'''Check all apps if there no local changes, pull'''
|
'''Check all apps if there no local changes, pull'''
|
||||||
from bench.config.common_site_config import get_config
|
bench = Bench(bench_path)
|
||||||
|
rebase = '--rebase' if bench.conf.get('rebase_on_pull') else ''
|
||||||
|
apps = apps or bench.apps
|
||||||
|
|
||||||
rebase = '--rebase' if get_config(bench_path).get('rebase_on_pull') else ''
|
|
||||||
|
|
||||||
apps = apps or get_apps(bench_path=bench_path)
|
|
||||||
# check for local changes
|
# check for local changes
|
||||||
if not reset:
|
if not reset:
|
||||||
for app in apps:
|
for app in apps:
|
||||||
@ -432,7 +435,7 @@ Here are your choices:
|
|||||||
print(f"Skipping pull for app {app}, since remote doesn't exist, and adding it to excluded apps")
|
print(f"Skipping pull for app {app}, since remote doesn't exist, and adding it to excluded apps")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if not get_config(bench_path).get('shallow_clone') or not reset:
|
if not bench.conf.get('shallow_clone') or not reset:
|
||||||
is_shallow = os.path.exists(os.path.join(app_dir, ".git", "shallow"))
|
is_shallow = os.path.exists(os.path.join(app_dir, ".git", "shallow"))
|
||||||
if is_shallow:
|
if is_shallow:
|
||||||
s = " to safely pull remote changes." if not reset else ""
|
s = " to safely pull remote changes." if not reset else ""
|
||||||
@ -443,7 +446,7 @@ Here are your choices:
|
|||||||
logger.log(f'pulling {app}')
|
logger.log(f'pulling {app}')
|
||||||
if reset:
|
if reset:
|
||||||
reset_cmd = f"git reset --hard {remote}/{branch}"
|
reset_cmd = f"git reset --hard {remote}/{branch}"
|
||||||
if get_config(bench_path).get('shallow_clone'):
|
if bench.conf.get('shallow_clone'):
|
||||||
exec_cmd(f"git fetch --depth=1 --no-tags {remote} {branch}",
|
exec_cmd(f"git fetch --depth=1 --no-tags {remote} {branch}",
|
||||||
cwd=app_dir)
|
cwd=app_dir)
|
||||||
exec_cmd(reset_cmd, cwd=app_dir)
|
exec_cmd(reset_cmd, cwd=app_dir)
|
||||||
@ -638,7 +641,9 @@ def get_apps_json(path):
|
|||||||
return json.load(f)
|
return json.load(f)
|
||||||
|
|
||||||
def validate_branch():
|
def validate_branch():
|
||||||
installed_apps = set(get_apps())
|
apps = Bench(".").apps
|
||||||
|
|
||||||
|
installed_apps = set(apps)
|
||||||
check_apps = set(['frappe', 'erpnext'])
|
check_apps = set(['frappe', 'erpnext'])
|
||||||
intersection_apps = installed_apps.intersection(check_apps)
|
intersection_apps = installed_apps.intersection(check_apps)
|
||||||
|
|
||||||
|
203
bench/bench.py
Normal file
203
bench/bench.py
Normal file
@ -0,0 +1,203 @@
|
|||||||
|
import os
|
||||||
|
import shutil
|
||||||
|
import sys
|
||||||
|
import logging
|
||||||
|
from typing import MutableSequence
|
||||||
|
|
||||||
|
import bench
|
||||||
|
from bench.utils import remove_backups_crontab, folders_in_bench, get_venv_path, exec_cmd, get_env_cmd
|
||||||
|
from bench.config.common_site_config import setup_config
|
||||||
|
|
||||||
|
|
||||||
|
logger = logging.getLogger(bench.PROJECT_NAME)
|
||||||
|
|
||||||
|
|
||||||
|
class Base:
|
||||||
|
def run(self, cmd):
|
||||||
|
return exec_cmd(cmd, cwd=self.cwd)
|
||||||
|
|
||||||
|
|
||||||
|
class Bench(Base):
|
||||||
|
def __init__(self, path):
|
||||||
|
self.name = path
|
||||||
|
self.cwd = os.path.abspath(path)
|
||||||
|
self.exists = os.path.exists(self.name)
|
||||||
|
self.setup = BenchSetup(self)
|
||||||
|
self.teardown = BenchTearDown(self)
|
||||||
|
self.apps = BenchApps(self)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def sites(self):
|
||||||
|
return [
|
||||||
|
path for path in os.listdir(os.path.join(self.name, 'sites'))
|
||||||
|
if os.path.exists(
|
||||||
|
os.path.join("sites", path, "site_config.json")
|
||||||
|
)
|
||||||
|
]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def conf(self):
|
||||||
|
from bench.config.common_site_config import get_config
|
||||||
|
return get_config(self.name)
|
||||||
|
|
||||||
|
def init(self):
|
||||||
|
self.setup.dirs()
|
||||||
|
self.setup.env()
|
||||||
|
self.setup.backups()
|
||||||
|
|
||||||
|
def drop(self):
|
||||||
|
self.teardown.backups()
|
||||||
|
self.teardown.dirs()
|
||||||
|
|
||||||
|
def get_app(self, app, version=None):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def drop_app(self, app, version=None):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def install(self, app, branch=None):
|
||||||
|
from bench.app import App
|
||||||
|
|
||||||
|
app = App(app, branch=branch)
|
||||||
|
|
||||||
|
# get app?
|
||||||
|
# install app to env
|
||||||
|
# add to apps.txt
|
||||||
|
return
|
||||||
|
|
||||||
|
def uninstall(self, app):
|
||||||
|
# remove from apps.txt
|
||||||
|
# uninstall app from env
|
||||||
|
# remove app?
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
|
class BenchApps(MutableSequence):
|
||||||
|
def __init__(self, bench : Bench):
|
||||||
|
self.bench = bench
|
||||||
|
self.initialize_apps()
|
||||||
|
|
||||||
|
def initialize_apps(self):
|
||||||
|
try:
|
||||||
|
self.apps = open(
|
||||||
|
os.path.join(self.bench.name, "sites", "apps.txt")
|
||||||
|
).read().splitlines()
|
||||||
|
except FileNotFoundError:
|
||||||
|
self.apps = []
|
||||||
|
|
||||||
|
def __getitem__(self, key):
|
||||||
|
''' retrieves an item by its index, key'''
|
||||||
|
return self.apps[key]
|
||||||
|
|
||||||
|
def __setitem__(self, key, value):
|
||||||
|
''' set the item at index, key, to value '''
|
||||||
|
# should probably not be allowed
|
||||||
|
# self.apps[key] = value
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def __delitem__(self, key):
|
||||||
|
''' removes the item at index, key '''
|
||||||
|
# TODO: uninstall and delete app from bench
|
||||||
|
del self.apps[key]
|
||||||
|
|
||||||
|
def __len__(self):
|
||||||
|
return len(self.apps)
|
||||||
|
|
||||||
|
def insert(self, key, value):
|
||||||
|
''' add an item, value, at index, key. '''
|
||||||
|
# TODO: fetch and install app to bench
|
||||||
|
self.apps.insert(key, value)
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return self.__str__()
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return str([x for x in self.apps])
|
||||||
|
|
||||||
|
|
||||||
|
class BenchSetup(Base):
|
||||||
|
def __init__(self, bench : Bench):
|
||||||
|
self.bench = bench
|
||||||
|
self.cwd = self.bench.cwd
|
||||||
|
|
||||||
|
def dirs(self):
|
||||||
|
os.makedirs(self.bench.name, exist_ok=True)
|
||||||
|
|
||||||
|
for dirname in folders_in_bench:
|
||||||
|
os.makedirs(os.path.join(self.bench.name, dirname), exist_ok=True)
|
||||||
|
|
||||||
|
def env(self, python="python3"):
|
||||||
|
"""Setup env folder
|
||||||
|
- create env if not exists
|
||||||
|
- upgrade env pip
|
||||||
|
- install frappe python dependencies
|
||||||
|
"""
|
||||||
|
frappe = os.path.join(self.bench.name, "apps", "frappe")
|
||||||
|
env_python = get_env_cmd("python", bench_path=self.bench.name)
|
||||||
|
virtualenv = get_venv_path()
|
||||||
|
|
||||||
|
if not os.path.exists(env_python):
|
||||||
|
self.run(f"{virtualenv} -q env -p {python}")
|
||||||
|
|
||||||
|
self.run(f"{env_python} -m pip install -q -U pip")
|
||||||
|
|
||||||
|
if os.path.exists(frappe):
|
||||||
|
self.run(f"{env_python} -m pip install -q -U -e {frappe}")
|
||||||
|
|
||||||
|
def config(self, redis=True, procfile=True):
|
||||||
|
"""Setup config folder
|
||||||
|
- create pids folder
|
||||||
|
- generate sites/common_site_config.json
|
||||||
|
"""
|
||||||
|
setup_config(self.bench.name)
|
||||||
|
|
||||||
|
if redis:
|
||||||
|
from bench.config.redis import generate_config
|
||||||
|
generate_config(self.bench.name)
|
||||||
|
|
||||||
|
if procfile:
|
||||||
|
from bench.config.procfile import setup_procfile
|
||||||
|
setup_procfile(self.bench.name, skip_redis=not redis)
|
||||||
|
|
||||||
|
def logging(self):
|
||||||
|
from bench.utils import setup_logging
|
||||||
|
return setup_logging(bench_path=self.bench.name)
|
||||||
|
|
||||||
|
def patches(self):
|
||||||
|
import shutil
|
||||||
|
|
||||||
|
shutil.copy(
|
||||||
|
os.path.join(os.path.dirname(os.path.abspath(__file__)), 'patches', 'patches.txt'),
|
||||||
|
os.path.join(self.bench.name, 'patches.txt')
|
||||||
|
)
|
||||||
|
|
||||||
|
def backups(self):
|
||||||
|
# TODO: to something better for logging data? - maybe a wrapper that auto-logs with more context
|
||||||
|
logger.log('setting up backups')
|
||||||
|
|
||||||
|
from crontab import CronTab
|
||||||
|
|
||||||
|
bench_dir = os.path.abspath(self.bench.name)
|
||||||
|
user = self.bench.conf.get('frappe_user')
|
||||||
|
logfile = os.path.join(bench_dir, 'logs', 'backup.log')
|
||||||
|
system_crontab = CronTab(user=user)
|
||||||
|
backup_command = f"cd {bench_dir} && {sys.argv[0]} --verbose --site all backup"
|
||||||
|
job_command = f"{backup_command} >> {logfile} 2>&1"
|
||||||
|
|
||||||
|
if job_command not in str(system_crontab):
|
||||||
|
job = system_crontab.new(command=job_command, comment="bench auto backups set for every 6 hours")
|
||||||
|
job.every(6).hours()
|
||||||
|
system_crontab.write()
|
||||||
|
|
||||||
|
logger.log('backups were set up')
|
||||||
|
|
||||||
|
|
||||||
|
class BenchTearDown:
|
||||||
|
def __init__(self, bench):
|
||||||
|
self.bench = bench
|
||||||
|
|
||||||
|
def backups(self):
|
||||||
|
remove_backups_crontab(self.bench.name)
|
||||||
|
|
||||||
|
def dirs(self):
|
||||||
|
shutil.rmtree(self.bench.name)
|
@ -10,7 +10,7 @@ import click
|
|||||||
|
|
||||||
# imports - module imports
|
# imports - module imports
|
||||||
import bench
|
import bench
|
||||||
from bench.app import get_apps
|
from bench.bench import Bench
|
||||||
from bench.commands import bench_command
|
from bench.commands import bench_command
|
||||||
from bench.config.common_site_config import get_config
|
from bench.config.common_site_config import get_config
|
||||||
from bench.utils import (
|
from bench.utils import (
|
||||||
@ -87,7 +87,7 @@ def cli():
|
|||||||
if sys.argv[1] in get_frappe_commands():
|
if sys.argv[1] in get_frappe_commands():
|
||||||
frappe_cmd()
|
frappe_cmd()
|
||||||
|
|
||||||
if sys.argv[1] in get_apps():
|
if sys.argv[1] in Bench(".").apps:
|
||||||
app_cmd()
|
app_cmd()
|
||||||
|
|
||||||
if not (len(sys.argv) > 1 and sys.argv[1] == "src"):
|
if not (len(sys.argv) > 1 and sys.argv[1] == "src"):
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
# imports - module imports
|
# imports - module imports
|
||||||
from bench.config.common_site_config import update_config, get_config, put_config
|
from bench.config.common_site_config import update_config, put_config
|
||||||
|
|
||||||
# imports - third party imports
|
# imports - third party imports
|
||||||
import click
|
import click
|
||||||
@ -68,7 +68,8 @@ def set_common_config(configs):
|
|||||||
@click.command('remove-common-config', help='Remove specific keys from current bench\'s common config')
|
@click.command('remove-common-config', help='Remove specific keys from current bench\'s common config')
|
||||||
@click.argument('keys', nargs=-1)
|
@click.argument('keys', nargs=-1)
|
||||||
def remove_common_config(keys):
|
def remove_common_config(keys):
|
||||||
common_site_config = get_config('.')
|
from bench.bench import Bench
|
||||||
|
common_site_config = Bench('.').conf
|
||||||
for key in keys:
|
for key in keys:
|
||||||
if key in common_site_config:
|
if key in common_site_config:
|
||||||
del common_site_config[key]
|
del common_site_config[key]
|
||||||
|
@ -3,7 +3,8 @@ import os
|
|||||||
import subprocess
|
import subprocess
|
||||||
|
|
||||||
# imports - module imports
|
# imports - module imports
|
||||||
from bench.app import get_repo_dir, get_apps, get_remote
|
from bench.bench import Bench
|
||||||
|
from bench.app import get_repo_dir, get_remote
|
||||||
from bench.utils import set_git_remote_url
|
from bench.utils import set_git_remote_url
|
||||||
|
|
||||||
# imports - third party imports
|
# imports - third party imports
|
||||||
@ -25,7 +26,7 @@ def remote_reset_url(app):
|
|||||||
|
|
||||||
@click.command('remote-urls', help="Show apps remote url")
|
@click.command('remote-urls', help="Show apps remote url")
|
||||||
def remote_urls():
|
def remote_urls():
|
||||||
for app in get_apps():
|
for app in Bench(".").apps:
|
||||||
repo_dir = get_repo_dir(app)
|
repo_dir = get_repo_dir(app)
|
||||||
|
|
||||||
if os.path.exists(os.path.join(repo_dir, '.git')):
|
if os.path.exists(os.path.join(repo_dir, '.git')):
|
||||||
|
@ -70,13 +70,15 @@ def setup_production(user, yes=False):
|
|||||||
|
|
||||||
@click.command("backups", help="Add cronjob for bench backups")
|
@click.command("backups", help="Add cronjob for bench backups")
|
||||||
def setup_backups():
|
def setup_backups():
|
||||||
bench.utils.setup_backups()
|
from bench.bench import Bench
|
||||||
|
Bench(".").setup.backups()
|
||||||
|
|
||||||
|
|
||||||
@click.command("env", help="Setup virtualenv for bench")
|
@click.command("env", help="Setup virtualenv for bench")
|
||||||
@click.option("--python", type = str, default = "python3", help = "Path to Python Executable.")
|
@click.option("--python", type = str, default = "python3", help = "Path to Python Executable.")
|
||||||
def setup_env(python="python3"):
|
def setup_env(python="python3"):
|
||||||
bench.utils.setup_env(python=python)
|
from bench.bench import Bench
|
||||||
|
return Bench(".").setup.env(python=python)
|
||||||
|
|
||||||
|
|
||||||
@click.command("firewall", help="Setup firewall for system")
|
@click.command("firewall", help="Setup firewall for system")
|
||||||
@ -162,8 +164,7 @@ def setup_requirements(node=False, python=False, dev=False):
|
|||||||
@click.option("--port", help="Port on which you want to run bench manager", default=23624)
|
@click.option("--port", help="Port on which you want to run bench manager", default=23624)
|
||||||
@click.option("--domain", help="Domain on which you want to run bench manager")
|
@click.option("--domain", help="Domain on which you want to run bench manager")
|
||||||
def setup_manager(yes=False, port=23624, domain=None):
|
def setup_manager(yes=False, port=23624, domain=None):
|
||||||
from bench.utils import get_sites
|
from bench.bench import Bench
|
||||||
from bench.config.common_site_config import get_config
|
|
||||||
from bench.config.nginx import make_bench_manager_nginx_conf
|
from bench.config.nginx import make_bench_manager_nginx_conf
|
||||||
|
|
||||||
create_new_site = True
|
create_new_site = True
|
||||||
@ -182,15 +183,15 @@ def setup_manager(yes=False, port=23624, domain=None):
|
|||||||
exec_cmd("bench --site bench-manager.local install-app bench_manager")
|
exec_cmd("bench --site bench-manager.local install-app bench_manager")
|
||||||
|
|
||||||
bench_path = "."
|
bench_path = "."
|
||||||
conf = get_config(bench_path)
|
bench = Bench(bench_path)
|
||||||
|
|
||||||
if conf.get("restart_supervisor_on_update") or conf.get("restart_systemd_on_update"):
|
if bench.conf.get("restart_supervisor_on_update") or bench.conf.get("restart_systemd_on_update"):
|
||||||
# implicates a production setup or so I presume
|
# implicates a production setup or so I presume
|
||||||
if not domain:
|
if not domain:
|
||||||
print("Please specify the site name on which you want to host bench-manager using the 'domain' flag")
|
print("Please specify the site name on which you want to host bench-manager using the 'domain' flag")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
if domain not in get_sites(bench_path):
|
if domain not in bench.sites:
|
||||||
raise Exception("No such site")
|
raise Exception("No such site")
|
||||||
|
|
||||||
make_bench_manager_nginx_conf(bench_path, yes=yes, port=port, domain=domain)
|
make_bench_manager_nginx_conf(bench_path, yes=yes, port=port, domain=domain)
|
||||||
|
@ -22,10 +22,13 @@ def start(no_dev, concurrency, procfile, no_prefix):
|
|||||||
@click.option('--systemd', is_flag=True, default=False)
|
@click.option('--systemd', is_flag=True, default=False)
|
||||||
def restart(web, supervisor, systemd):
|
def restart(web, supervisor, systemd):
|
||||||
from bench.utils import restart_supervisor_processes, restart_systemd_processes
|
from bench.utils import restart_supervisor_processes, restart_systemd_processes
|
||||||
from bench.config.common_site_config import get_config
|
from bench.bench import Bench
|
||||||
if get_config('.').get('restart_supervisor_on_update') or supervisor:
|
|
||||||
|
bench = Bench(".")
|
||||||
|
|
||||||
|
if bench.conf.get('restart_supervisor_on_update') or supervisor:
|
||||||
restart_supervisor_processes(bench_path='.', web_workers=web)
|
restart_supervisor_processes(bench_path='.', web_workers=web)
|
||||||
if get_config('.').get('restart_systemd_on_update') or systemd:
|
if bench.conf.get('restart_systemd_on_update') or systemd:
|
||||||
restart_systemd_processes(bench_path='.', web_workers=web)
|
restart_systemd_processes(bench_path='.', web_workers=web)
|
||||||
|
|
||||||
|
|
||||||
@ -114,8 +117,9 @@ def renew_lets_encrypt():
|
|||||||
@click.command('backup', help="Backup single site")
|
@click.command('backup', help="Backup single site")
|
||||||
@click.argument('site')
|
@click.argument('site')
|
||||||
def backup_site(site):
|
def backup_site(site):
|
||||||
from bench.utils import get_sites, backup_site
|
from bench.bench import Bench
|
||||||
if site not in get_sites(bench_path='.'):
|
from bench.utils import backup_site
|
||||||
|
if site not in Bench(".").sites:
|
||||||
print(f'Site `{site}` not found')
|
print(f'Site `{site}` not found')
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
backup_site(site, bench_path='.')
|
backup_site(site, bench_path='.')
|
||||||
|
@ -6,10 +6,10 @@ import click
|
|||||||
|
|
||||||
# imports - module imports
|
# imports - module imports
|
||||||
import bench
|
import bench
|
||||||
from bench.config.common_site_config import get_config
|
|
||||||
from bench.config.nginx import make_nginx_conf
|
from bench.config.nginx import make_nginx_conf
|
||||||
from bench.config.production_setup import service
|
from bench.config.production_setup import service
|
||||||
from bench.config.site_config import get_domains, remove_domain, update_site_config
|
from bench.config.site_config import get_domains, remove_domain, update_site_config
|
||||||
|
from bench.bench import Bench
|
||||||
from bench.utils import exec_cmd, update_common_site_config
|
from bench.utils import exec_cmd, update_common_site_config
|
||||||
from bench.exceptions import CommandFailedError
|
from bench.exceptions import CommandFailedError
|
||||||
|
|
||||||
@ -37,7 +37,7 @@ def setup_letsencrypt(site, custom_domain, bench_path, interactive):
|
|||||||
'Do you want to continue?',
|
'Do you want to continue?',
|
||||||
abort=True)
|
abort=True)
|
||||||
|
|
||||||
if not get_config(bench_path).get("dns_multitenant"):
|
if not Bench(bench_path).conf.get("dns_multitenant"):
|
||||||
print("You cannot setup SSL without DNS Multitenancy")
|
print("You cannot setup SSL without DNS Multitenancy")
|
||||||
return
|
return
|
||||||
|
|
||||||
@ -151,7 +151,7 @@ def setup_wildcard_ssl(domain, email, bench_path, exclude_base_domain):
|
|||||||
|
|
||||||
return domain_list
|
return domain_list
|
||||||
|
|
||||||
if not get_config(bench_path).get("dns_multitenant"):
|
if not Bench(bench_path).conf.get("dns_multitenant"):
|
||||||
print("You cannot setup SSL without DNS Multitenancy")
|
print("You cannot setup SSL without DNS Multitenancy")
|
||||||
return
|
return
|
||||||
|
|
||||||
|
@ -9,7 +9,8 @@ import click
|
|||||||
|
|
||||||
# imports - module imports
|
# imports - module imports
|
||||||
import bench
|
import bench
|
||||||
from bench.utils import get_bench_name, get_sites
|
from bench.bench import Bench
|
||||||
|
from bench.utils import get_bench_name
|
||||||
|
|
||||||
|
|
||||||
def make_nginx_conf(bench_path, yes=False):
|
def make_nginx_conf(bench_path, yes=False):
|
||||||
@ -23,7 +24,7 @@ def make_nginx_conf(bench_path, yes=False):
|
|||||||
bench_path = os.path.abspath(bench_path)
|
bench_path = os.path.abspath(bench_path)
|
||||||
sites_path = os.path.join(bench_path, "sites")
|
sites_path = os.path.join(bench_path, "sites")
|
||||||
|
|
||||||
config = bench.config.common_site_config.get_config(bench_path)
|
config = Bench(bench_path).conf
|
||||||
sites = prepare_sites(config, bench_path)
|
sites = prepare_sites(config, bench_path)
|
||||||
bench_name = get_bench_name(bench_path)
|
bench_name = get_bench_name(bench_path)
|
||||||
|
|
||||||
@ -56,13 +57,12 @@ def make_nginx_conf(bench_path, yes=False):
|
|||||||
|
|
||||||
def make_bench_manager_nginx_conf(bench_path, yes=False, port=23624, domain=None):
|
def make_bench_manager_nginx_conf(bench_path, yes=False, port=23624, domain=None):
|
||||||
from bench.config.site_config import get_site_config
|
from bench.config.site_config import get_site_config
|
||||||
from bench.config.common_site_config import get_config
|
|
||||||
|
|
||||||
template = bench.config.env().get_template('bench_manager_nginx.conf')
|
template = bench.config.env().get_template('bench_manager_nginx.conf')
|
||||||
bench_path = os.path.abspath(bench_path)
|
bench_path = os.path.abspath(bench_path)
|
||||||
sites_path = os.path.join(bench_path, "sites")
|
sites_path = os.path.join(bench_path, "sites")
|
||||||
|
|
||||||
config = get_config(bench_path)
|
config = Bench(bench_path).conf
|
||||||
site_config = get_site_config(domain, bench_path=bench_path)
|
site_config = get_site_config(domain, bench_path=bench_path)
|
||||||
bench_name = get_bench_name(bench_path)
|
bench_name = get_bench_name(bench_path)
|
||||||
|
|
||||||
@ -182,18 +182,20 @@ def prepare_sites(config, bench_path):
|
|||||||
return sites
|
return sites
|
||||||
|
|
||||||
def get_sites_with_config(bench_path):
|
def get_sites_with_config(bench_path):
|
||||||
from bench.config.common_site_config import get_config
|
from bench.bench import Bench
|
||||||
from bench.config.site_config import get_site_config
|
from bench.config.site_config import get_site_config
|
||||||
|
|
||||||
sites = get_sites(bench_path=bench_path)
|
bench = Bench(bench_path)
|
||||||
dns_multitenant = get_config(bench_path).get('dns_multitenant')
|
sites = bench.sites
|
||||||
|
conf = bench.conf
|
||||||
|
dns_multitenant = conf.get('dns_multitenant')
|
||||||
|
|
||||||
ret = []
|
ret = []
|
||||||
for site in sites:
|
for site in sites:
|
||||||
try:
|
try:
|
||||||
site_config = get_site_config(site, bench_path=bench_path)
|
site_config = get_site_config(site, bench_path=bench_path)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
strict_nginx = get_config(bench_path).get('strict_nginx')
|
strict_nginx = conf.get('strict_nginx')
|
||||||
if strict_nginx:
|
if strict_nginx:
|
||||||
print(f"\n\nERROR: The site config for the site {site} is broken.",
|
print(f"\n\nERROR: The site config for the site {site} is broken.",
|
||||||
"If you want this command to pass, instead of just throwing an error,",
|
"If you want this command to pass, instead of just throwing an error,",
|
||||||
@ -236,8 +238,8 @@ def use_wildcard_certificate(bench_path, ret):
|
|||||||
"ssl_certificate_key": "/path/to/erpnext.com.key"
|
"ssl_certificate_key": "/path/to/erpnext.com.key"
|
||||||
}
|
}
|
||||||
'''
|
'''
|
||||||
from bench.config.common_site_config import get_config
|
from bench.bench import Bench
|
||||||
config = get_config(bench_path=bench_path)
|
config = Bench(bench_path).conf
|
||||||
wildcard = config.get('wildcard')
|
wildcard = config.get('wildcard')
|
||||||
|
|
||||||
if not wildcard:
|
if not wildcard:
|
||||||
|
@ -7,12 +7,12 @@ import click
|
|||||||
# imports - module imports
|
# imports - module imports
|
||||||
import bench
|
import bench
|
||||||
from bench.app import use_rq
|
from bench.app import use_rq
|
||||||
from bench.config.common_site_config import get_config
|
|
||||||
from bench.utils import which
|
from bench.utils import which
|
||||||
|
from bench.bench import Bench
|
||||||
|
|
||||||
|
|
||||||
def setup_procfile(bench_path, yes=False, skip_redis=False):
|
def setup_procfile(bench_path, yes=False, skip_redis=False):
|
||||||
config = get_config(bench_path=bench_path)
|
config = Bench(bench_path).conf
|
||||||
procfile_path = os.path.join(bench_path, 'Procfile')
|
procfile_path = os.path.join(bench_path, 'Procfile')
|
||||||
if not yes and os.path.exists(procfile_path):
|
if not yes and os.path.exists(procfile_path):
|
||||||
click.confirm('A Procfile already exists and this will overwrite it. Do you want to continue?',
|
click.confirm('A Procfile already exists and this will overwrite it. Do you want to continue?',
|
||||||
|
@ -5,10 +5,10 @@ import sys
|
|||||||
|
|
||||||
# imports - module imports
|
# imports - module imports
|
||||||
import bench
|
import bench
|
||||||
from bench.config.common_site_config import get_config
|
|
||||||
from bench.config.nginx import make_nginx_conf
|
from bench.config.nginx import make_nginx_conf
|
||||||
from bench.config.supervisor import generate_supervisor_config, update_supervisord_config
|
from bench.config.supervisor import generate_supervisor_config, update_supervisord_config
|
||||||
from bench.config.systemd import generate_systemd_config
|
from bench.config.systemd import generate_systemd_config
|
||||||
|
from bench.bench import Bench
|
||||||
from bench.utils import exec_cmd, which, fix_prod_setup_perms, get_bench_name, get_cmd_output, log
|
from bench.utils import exec_cmd, which, fix_prod_setup_perms, get_bench_name, get_cmd_output, log
|
||||||
from bench.exceptions import CommandFailedError
|
from bench.exceptions import CommandFailedError
|
||||||
|
|
||||||
@ -30,10 +30,13 @@ def setup_production_prerequisites():
|
|||||||
def setup_production(user, bench_path='.', yes=False):
|
def setup_production(user, bench_path='.', yes=False):
|
||||||
print("Setting Up prerequisites...")
|
print("Setting Up prerequisites...")
|
||||||
setup_production_prerequisites()
|
setup_production_prerequisites()
|
||||||
if get_config(bench_path).get('restart_supervisor_on_update') and get_config(bench_path).get('restart_systemd_on_update'):
|
|
||||||
|
conf = Bench(bench_path).conf
|
||||||
|
|
||||||
|
if conf.get('restart_supervisor_on_update') and conf.get('restart_systemd_on_update'):
|
||||||
raise Exception("You cannot use supervisor and systemd at the same time. Modify your common_site_config accordingly." )
|
raise Exception("You cannot use supervisor and systemd at the same time. Modify your common_site_config accordingly." )
|
||||||
|
|
||||||
if get_config(bench_path).get('restart_systemd_on_update'):
|
if conf.get('restart_systemd_on_update'):
|
||||||
print("Setting Up systemd...")
|
print("Setting Up systemd...")
|
||||||
generate_systemd_config(bench_path=bench_path, user=user, yes=yes)
|
generate_systemd_config(bench_path=bench_path, user=user, yes=yes)
|
||||||
else:
|
else:
|
||||||
@ -50,7 +53,7 @@ def setup_production(user, bench_path='.', yes=False):
|
|||||||
nginx_conf = f'/etc/nginx/conf.d/{bench_name}.conf'
|
nginx_conf = f'/etc/nginx/conf.d/{bench_name}.conf'
|
||||||
|
|
||||||
print("Setting Up symlinks and reloading services...")
|
print("Setting Up symlinks and reloading services...")
|
||||||
if get_config(bench_path).get('restart_supervisor_on_update'):
|
if conf.get('restart_supervisor_on_update'):
|
||||||
supervisor_conf_extn = "ini" if is_centos7() else "conf"
|
supervisor_conf_extn = "ini" if is_centos7() else "conf"
|
||||||
supervisor_conf = os.path.join(get_supervisor_confdir(), f'{bench_name}.{supervisor_conf_extn}')
|
supervisor_conf = os.path.join(get_supervisor_confdir(), f'{bench_name}.{supervisor_conf_extn}')
|
||||||
|
|
||||||
@ -61,7 +64,7 @@ def setup_production(user, bench_path='.', yes=False):
|
|||||||
if not os.path.islink(nginx_conf):
|
if not os.path.islink(nginx_conf):
|
||||||
os.symlink(os.path.abspath(os.path.join(bench_path, 'config', 'nginx.conf')), nginx_conf)
|
os.symlink(os.path.abspath(os.path.join(bench_path, 'config', 'nginx.conf')), nginx_conf)
|
||||||
|
|
||||||
if get_config(bench_path).get('restart_supervisor_on_update'):
|
if conf.get('restart_supervisor_on_update'):
|
||||||
reload_supervisor()
|
reload_supervisor()
|
||||||
|
|
||||||
if os.environ.get('NO_SERVICE_RESTART'):
|
if os.environ.get('NO_SERVICE_RESTART'):
|
||||||
@ -72,6 +75,7 @@ def setup_production(user, bench_path='.', yes=False):
|
|||||||
|
|
||||||
def disable_production(bench_path='.'):
|
def disable_production(bench_path='.'):
|
||||||
bench_name = get_bench_name(bench_path)
|
bench_name = get_bench_name(bench_path)
|
||||||
|
conf = Bench(bench_path).conf
|
||||||
|
|
||||||
# supervisorctl
|
# supervisorctl
|
||||||
supervisor_conf_extn = "ini" if is_centos7() else "conf"
|
supervisor_conf_extn = "ini" if is_centos7() else "conf"
|
||||||
@ -80,7 +84,7 @@ def disable_production(bench_path='.'):
|
|||||||
if os.path.islink(supervisor_conf):
|
if os.path.islink(supervisor_conf):
|
||||||
os.unlink(supervisor_conf)
|
os.unlink(supervisor_conf)
|
||||||
|
|
||||||
if get_config(bench_path).get('restart_supervisor_on_update'):
|
if conf.get('restart_supervisor_on_update'):
|
||||||
reload_supervisor()
|
reload_supervisor()
|
||||||
|
|
||||||
# nginx
|
# nginx
|
||||||
|
@ -5,13 +5,13 @@ import subprocess
|
|||||||
|
|
||||||
# imports - module imports
|
# imports - module imports
|
||||||
import bench
|
import bench
|
||||||
from bench.config.common_site_config import get_config
|
|
||||||
|
|
||||||
|
|
||||||
def generate_config(bench_path):
|
def generate_config(bench_path):
|
||||||
from urllib.parse import urlparse
|
from urllib.parse import urlparse
|
||||||
|
from bench.bench import Bench
|
||||||
|
|
||||||
config = get_config(bench_path)
|
config = Bench(bench_path).conf
|
||||||
redis_version = get_redis_version()
|
redis_version = get_redis_version()
|
||||||
|
|
||||||
ports = {}
|
ports = {}
|
||||||
|
@ -3,9 +3,6 @@ import json
|
|||||||
import os
|
import os
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
|
|
||||||
# imports - module imports
|
|
||||||
from bench.utils import get_sites
|
|
||||||
|
|
||||||
|
|
||||||
def get_site_config(site, bench_path='.'):
|
def get_site_config(site, bench_path='.'):
|
||||||
config_path = os.path.join(bench_path, 'sites', site, 'site_config.json')
|
config_path = os.path.join(bench_path, 'sites', site, 'site_config.json')
|
||||||
@ -35,8 +32,9 @@ def set_ssl_certificate_key(site, ssl_certificate_key, bench_path='.', gen_confi
|
|||||||
|
|
||||||
def set_site_config_nginx_property(site, config, bench_path='.', gen_config=True):
|
def set_site_config_nginx_property(site, config, bench_path='.', gen_config=True):
|
||||||
from bench.config.nginx import make_nginx_conf
|
from bench.config.nginx import make_nginx_conf
|
||||||
|
from bench.bench import Bench
|
||||||
|
|
||||||
if site not in get_sites(bench_path=bench_path):
|
if site not in Bench(bench_path).sites:
|
||||||
raise Exception("No such site")
|
raise Exception("No such site")
|
||||||
update_site_config(site, config, bench_path=bench_path)
|
update_site_config(site, config, bench_path=bench_path)
|
||||||
if gen_config:
|
if gen_config:
|
||||||
|
@ -7,7 +7,8 @@ import os
|
|||||||
import bench
|
import bench
|
||||||
from bench.app import use_rq
|
from bench.app import use_rq
|
||||||
from bench.utils import get_bench_name, which
|
from bench.utils import get_bench_name, which
|
||||||
from bench.config.common_site_config import get_config, update_config, get_gunicorn_workers
|
from bench.bench import Bench
|
||||||
|
from bench.config.common_site_config import update_config, get_gunicorn_workers
|
||||||
|
|
||||||
# imports - third party imports
|
# imports - third party imports
|
||||||
import click
|
import click
|
||||||
@ -21,8 +22,8 @@ def generate_supervisor_config(bench_path, user=None, yes=False, skip_redis=Fals
|
|||||||
if not user:
|
if not user:
|
||||||
user = getpass.getuser()
|
user = getpass.getuser()
|
||||||
|
|
||||||
|
config = Bench(bench_path).conf
|
||||||
template = bench.config.env().get_template('supervisor.conf')
|
template = bench.config.env().get_template('supervisor.conf')
|
||||||
config = get_config(bench_path=bench_path)
|
|
||||||
bench_dir = os.path.abspath(bench_path)
|
bench_dir = os.path.abspath(bench_path)
|
||||||
|
|
||||||
config = template.render(**{
|
config = template.render(**{
|
||||||
|
@ -8,7 +8,8 @@ import click
|
|||||||
# imports - module imports
|
# imports - module imports
|
||||||
import bench
|
import bench
|
||||||
from bench.app import use_rq
|
from bench.app import use_rq
|
||||||
from bench.config.common_site_config import get_config, get_gunicorn_workers, update_config
|
from bench.bench import Bench
|
||||||
|
from bench.config.common_site_config import get_gunicorn_workers, update_config
|
||||||
from bench.utils import exec_cmd, which, get_bench_name
|
from bench.utils import exec_cmd, which, get_bench_name
|
||||||
|
|
||||||
|
|
||||||
@ -19,7 +20,7 @@ def generate_systemd_config(bench_path, user=None, yes=False,
|
|||||||
if not user:
|
if not user:
|
||||||
user = getpass.getuser()
|
user = getpass.getuser()
|
||||||
|
|
||||||
config = get_config(bench_path=bench_path)
|
config = Bench(bench_path).conf
|
||||||
|
|
||||||
bench_dir = os.path.abspath(bench_path)
|
bench_dir = os.path.abspath(bench_path)
|
||||||
bench_name = get_bench_name(bench_path)
|
bench_name = get_bench_name(bench_path)
|
||||||
|
@ -395,7 +395,10 @@ def setup_socketio(bench_path='.'):
|
|||||||
|
|
||||||
|
|
||||||
def patch_sites(bench_path='.'):
|
def patch_sites(bench_path='.'):
|
||||||
for site in get_sites(bench_path=bench_path):
|
from bench.bench import Bench
|
||||||
|
bench = Bench(bench_path)
|
||||||
|
|
||||||
|
for site in bench.sites:
|
||||||
try:
|
try:
|
||||||
migrate_site(site, bench_path=bench_path)
|
migrate_site(site, bench_path=bench_path)
|
||||||
except subprocess.CalledProcessError:
|
except subprocess.CalledProcessError:
|
||||||
@ -417,21 +420,7 @@ def get_sites(bench_path='.'):
|
|||||||
|
|
||||||
def setup_backups(bench_path='.'):
|
def setup_backups(bench_path='.'):
|
||||||
from crontab import CronTab
|
from crontab import CronTab
|
||||||
from bench.config.common_site_config import get_config
|
from bench.bench import Bench
|
||||||
logger.log('setting up backups')
|
|
||||||
|
|
||||||
bench_dir = os.path.abspath(bench_path)
|
|
||||||
user = get_config(bench_path=bench_dir).get('frappe_user')
|
|
||||||
logfile = os.path.join(bench_dir, 'logs', 'backup.log')
|
|
||||||
system_crontab = CronTab(user=user)
|
|
||||||
backup_command = f"cd {bench_dir} && {sys.argv[0]} --verbose --site all backup"
|
|
||||||
job_command = f"{backup_command} >> {logfile} 2>&1"
|
|
||||||
|
|
||||||
if job_command not in str(system_crontab):
|
|
||||||
job = system_crontab.new(command=job_command, comment="bench auto backups set for every 6 hours")
|
|
||||||
job.every(6).hours()
|
|
||||||
system_crontab.write()
|
|
||||||
|
|
||||||
|
|
||||||
def remove_backups_crontab(bench_path='.'):
|
def remove_backups_crontab(bench_path='.'):
|
||||||
from crontab import CronTab, CronItem
|
from crontab import CronTab, CronItem
|
||||||
@ -439,7 +428,7 @@ def remove_backups_crontab(bench_path='.'):
|
|||||||
logger.log('removing backup cronjob')
|
logger.log('removing backup cronjob')
|
||||||
|
|
||||||
bench_dir = os.path.abspath(bench_path)
|
bench_dir = os.path.abspath(bench_path)
|
||||||
user = get_config(bench_path=bench_dir).get('frappe_user')
|
user = Bench(bench_dir).conf.get('frappe_user')
|
||||||
logfile = os.path.join(bench_dir, 'logs', 'backup.log')
|
logfile = os.path.join(bench_dir, 'logs', 'backup.log')
|
||||||
system_crontab = CronTab(user=user)
|
system_crontab = CronTab(user=user)
|
||||||
backup_command = f"cd {bench_dir} && {sys.argv[0]} --verbose --site all backup"
|
backup_command = f"cd {bench_dir} && {sys.argv[0]} --verbose --site all backup"
|
||||||
@ -540,8 +529,8 @@ def get_git_version():
|
|||||||
|
|
||||||
|
|
||||||
def check_git_for_shallow_clone():
|
def check_git_for_shallow_clone():
|
||||||
from bench.config.common_site_config import get_config
|
from bench.bench import Bench
|
||||||
config = get_config('.')
|
config = Bench('.').conf
|
||||||
|
|
||||||
if config:
|
if config:
|
||||||
if config.get('release_bench'):
|
if config.get('release_bench'):
|
||||||
|
Loading…
Reference in New Issue
Block a user