diff --git a/bench/__init__.py b/bench/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/bench/app.py b/bench/app.py new file mode 100644 index 00000000..c6f87037 --- /dev/null +++ b/bench/app.py @@ -0,0 +1,37 @@ +import os +from .utils import exec_cmd, get_frappe + + +def get_apps(bench='.'): + try: + with open(os.path.join(bench, 'sites', 'apps.txt')) as f: + return f.read().strip().split('\n') + except IOError: + return [] + +def add_to_appstxt(app, bench='.'): + apps = get_apps(bench=bench) + if app not in apps: + apps.append(app) + with open(os.path.join(bench, 'sites', 'apps.txt'), 'w') as f: + return f.write('\n'.join(apps)) + +def get_app(app, git_url, bench='.'): + exec_cmd("git clone {} --origin upstream {}".format(git_url, app), cwd=os.path.join(bench, 'apps')) + install_app(app, bench=bench) + +def new_app(app, bench='.'): + exec_cmd("{frappe} --make_app {apps}".format(frappe=get_frappe(bench=bench), apps=os.path.join(bench, 'apps'))) + install_app(app, bench=bench) + +def install_app(app, bench='.'): + exec_cmd("{pip} install -e {app}".format(pip=os.path.join(bench, 'env', 'bin', 'pip'), app=os.path.join('apps', app))) + add_to_appstxt(app, bench=bench) + +def pull_all_apps(bench='.'): + apps_dir = os.path.join(bench, 'apps') + apps = [app for app in os.listdir(apps_dir) if os.path.isdir(os.path.join(apps_dir, app))] + for app in apps: + app_dir = os.path.join(apps_dir, app) + if os.path.exists(os.path.join(app_dir, '.git')): + exec_cmd("git pull --rebase upstream HEAD", cwd=app_dir) diff --git a/bench/cli.py b/bench/cli.py new file mode 100644 index 00000000..afe33060 --- /dev/null +++ b/bench/cli.py @@ -0,0 +1,103 @@ +import click +from .utils import init as _init +from .utils import setup_env as _setup_env +from .utils import new_site as _new_site +from .utils import build_assets, patch_sites, get_sites_dir, get_bench_dir, get_frappe, exec_cmd +from .app import get_app as _get_app +from .app import new_app as _new_app +from .app import pull_all_apps +from .config import generate_config +import os + +@click.group() +def bench(): + pass + +@click.command() +@click.argument('path') +def init(path): + _init(path) + click.echo('Bench {} initialized'.format(path)) + +@click.command('get-app') +@click.argument('name') +@click.argument('git-url') +def get_app(name, git_url): + _get_app(name, git_url) + +@click.command('new-app') +@click.argument('app-name') +def new_app(app_name): + _new_app(app_name) + +@click.command('new-site') +@click.argument('site') +def new_site(site): + _new_site(site) + +@click.command('update') +@click.option('--pull', flag_value=True, type=bool) +@click.option('--patch',flag_value=True, type=bool) +@click.option('--build',flag_value=True, type=bool) +def update(pull=False, patch=False, build=False): + if not (pull or patch or build): + pull, patch, build = True, True, True + if pull: + pull_all_apps() + if patch: + patch_sites() + if build: + build_assets() + +## Setup +@click.group() +def setup(): + pass + +@click.command('sudoers') +def setup_sudoers(): + pass + +@click.command('nginx') +def setup_nginx(): + generate_config('nginx', 'nginx.conf') + +@click.command('supervisor') +def setup_supervisor(): + generate_config('supervisor', 'supervisor.conf') + +@click.command('auto-update') +def setup_auto_update(): + exec_cmd('echo \"`crontab -l`\" | uniq | sed -e \"a0 10 * * * cd {bench_dir} && {bench} update\" | grep -v "^$" | uniq | crontab'.format(bench_dir=get_bench_dir(), + bench=os.path.join(get_bench_dir(), 'env', 'bin', 'bench'))) + +@click.command('backups') +def setup_backups(): + exec_cmd('echo \"`crontab -l`\" | uniq | sed -e \"a0 */6 * * * cd {sites_dir} && {frappe} --backup all\" | grep -v "^$" | uniq | crontab'.format(sites_dir=get_sites_dir(), + frappe=get_frappe())) + +@click.command('dnsmasq') +def setup_dnsmasq(): + pass + +@click.command('env') +def setup_env(): + _setup_env() + pass + +setup.add_command(setup_nginx) +setup.add_command(setup_sudoers) +setup.add_command(setup_supervisor) +setup.add_command(setup_auto_update) +setup.add_command(setup_dnsmasq) +setup.add_command(setup_backups) +setup.add_command(setup_env) + +#Bench commands + +bench.add_command(init) +bench.add_command(get_app) +bench.add_command(new_app) +bench.add_command(new_site) +bench.add_command(setup) +bench.add_command(update) diff --git a/bench/config.py b/bench/config.py new file mode 100644 index 00000000..13b95198 --- /dev/null +++ b/bench/config.py @@ -0,0 +1,25 @@ +import os +import getpass +from jinja2 import Environment, PackageLoader +from .utils import get_sites + +env = Environment(loader=PackageLoader('bench', 'templates'), trim_blocks=True) + +def generate_config(application, template_name, bench='.'): + template = env.get_template(template_name) + bench_dir = os.path.abspath(bench) + sites_dir = os.path.join(bench_dir, "sites") + sites = get_sites(bench=bench) + user = getpass.getuser() + with open("sites/currentsite.txt") as f: + default_site = f.read().strip() + + config = template.render(**{ + "bench_dir": bench_dir, + "sites_dir": sites_dir, + "user": user, + "default_site": default_site, + "sites": sites + }) + with open("config/{}.conf".format(application), 'w') as f: + f.write(config) diff --git a/bench/templates/nginx.conf b/bench/templates/nginx.conf new file mode 100644 index 00000000..e0671a25 --- /dev/null +++ b/bench/templates/nginx.conf @@ -0,0 +1,69 @@ + +upstream frappe { + server 127.0.0.1:8000 fail_timeout=0; +} + +server { + listen 80; + client_max_body_size 4G; + server_name + {% for sitename in sites %} + {{ sitename }} + {% endfor %} + ; + keepalive_timeout 5; + sendfile on; + root {{ sites_dir }}; + + location /private/ { + internal; + try_files /$uri =424; + } + + location /assets { + try_files $uri =404; + } + + location / { + try_files /$host/public/$uri @magic; + } + + location @magic { + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Use-X-Accel-Redirect True; + proxy_read_timeout 120; + proxy_redirect off; + proxy_pass http://frappe; + } +} + +server { + listen 80 default; + client_max_body_size 4G; + server_name localhost; + keepalive_timeout 5; + sendfile on; + root {{ sites_dir }}; + + location /private/ { + internal; + try_files /$uri =424; + } + + location /assets { + try_files $uri =404; + } + + location / { + try_files /{{ default_site }}/public/$uri @magic; + } + + location @magic { + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Use-X-Accel-Redirect True; + proxy_set_header Host {{ default_site }}; + proxy_read_timeout 120; + proxy_redirect off; + proxy_pass http://frappe; + } +} diff --git a/bench/templates/supervisor.conf b/bench/templates/supervisor.conf new file mode 100644 index 00000000..96723549 --- /dev/null +++ b/bench/templates/supervisor.conf @@ -0,0 +1,32 @@ +[program:frappe-web] +environment=SITES_PATH='{{ bench_dir }}/sites' +command={{ bench_dir }}/env/bin/gunicorn -b 127.0.0.1:8000 -w 2 -t 120 frappe.app:application +autostart=true +autorestart=true +stopsignal=QUIT +stdout_logfile={{ bench_dir }}/logs/web.log +stderr_logfile={{ bench_dir }}/logs/web.error.log +user={{ user }} + +[program:frappe-worker] +command={{ bench_dir }}/env/bin/python -m frappe.celery_app worker +autostart=true +autorestart=true +stopsignal=QUIT +stdout_logfile={{ bench_dir }}/logs/worker.log +stderr_logfile={{ bench_dir }}/logs/worker.error.log +user={{ user }} +directory={{ sites_dir }} + +[program:frappe-workerbeat] +command={{ bench_dir }}/env/bin/python -m frappe.celery_app beat -s test.schedule +autostart=true +autorestart=true +stopsignal=QUIT +stdout_logfile={{ bench_dir }}/logs/workerbeat.log +stderr_logfile={{ bench_dir }}/logs/workerbeat.error.log +user={{ user }} +directory={{ sites_dir }} + +[group:frappe] +programs=frappe-web,frappe-worker,frappe-workerbeat diff --git a/bench/utils.py b/bench/utils.py new file mode 100644 index 00000000..70085d93 --- /dev/null +++ b/bench/utils.py @@ -0,0 +1,53 @@ +import os +import sys +import subprocess + +def get_frappe(bench='.'): + frappe = os.path.abspath(os.path.join(bench, 'env', 'bin', 'frappe')) + if not os.path.exists(frappe): + print 'frappe app is not installed. Run the following command to install frappe' + print 'bench get-app frappe https://github.com/frappe/frappe.git' + return frappe + +def init(path): + if os.path.exists(path): + print 'Directory {} already exists!'.format(path) + sys.exit(1) + + os.mkdir(path) + for dirname in ('apps', 'sites', 'config', 'logs'): + os.mkdir(os.path.join(path, dirname)) + setup_env(bench=path) + +def exec_cmd(cmd, cwd='.'): + try: + subprocess.check_call(cmd, cwd=cwd, shell=True) + except subprocess.CalledProcessError, e: + print "Error:", e.output + raise + +def setup_env(bench='.'): + exec_cmd('virtualenv {} -p {}'.format('env', sys.executable), cwd=bench) + +def new_site(site, bench='.'): + exec_cmd("{frappe} --install {site} {site}".format(frappe=get_frappe(bench=bench), site=site), cwd=os.path.join(bench, 'sites')) + if len(get_sites(bench=bench)) == 1: + exec_cmd("{frappe} --use {site}".format(frappe=get_frappe(bench=bench), site=site), cwd=os.path.join(bench, 'sites')) + +def patch_sites(bench='.'): + exec_cmd("{frappe} --latest all".format(frappe=get_frappe(bench=bench)), cwd=os.path.join(bench, 'sites')) + +def build_assets(bench='.'): + exec_cmd("{frappe} --build".format(frappe=get_frappe(bench=bench)), cwd=os.path.join(bench, 'sites')) + +def get_sites(bench='.'): + sites_dir = os.path.join(bench, "sites") + sites = [site for site in os.listdir(sites_dir) + if os.path.isdir(os.path.join(sites_dir, site)) and site not in ('assets',)] + return sites + +def get_sites_dir(bench='.'): + return os.path.abspath(os.path.join(bench, 'sites')) + +def get_bench_dir(bench='.'): + return os.path.abspath(bench) diff --git a/setup.py b/setup.py new file mode 100644 index 00000000..60652720 --- /dev/null +++ b/setup.py @@ -0,0 +1,15 @@ +from setuptools import setup + +setup( + name='bench', + version='0.1', + py_modules=['bench'], + include_package_data=True, + install_requires=[ + 'Click', + ], + entry_points=''' +[console_scripts] +bench=bench.cli:bench +''', +)