from __future__ import absolute_import import json import sys import os import time import subprocess from subprocess import call, Popen from multiprocessing import cpu_count import click import pip from realms import config, create_app, db, __version__, cli, cache from realms.lib.util import random_string, in_virtualenv, green, yellow, red config = config.conf # called to discover commands in modules app = create_app() def get_user(): for name in ('SUDO_USER', 'LOGNAME', 'USER', 'LNAME', 'USERNAME'): user = os.environ.get(name) if user: return user def get_pid(): try: with file(config.PIDFILE) as f: return f.read().strip() except IOError: return None def is_running(pid): if not pid: return False pid = int(pid) try: os.kill(pid, 0) except OSError: return False return True def module_exists(module_name): try: __import__(module_name) except ImportError: return False else: return True def prompt_and_invoke(ctx, fn): # This is a workaround for a bug in click. # See https://github.com/mitsuhiko/click/issues/429 # This isn't perfect - we are ignoring some information (type mostly) kw = {} for p in fn.params: v = click.prompt(p.prompt, p.default, p.hide_input, p.confirmation_prompt, p.type) kw[p.name] = v ctx.invoke(fn, **kw) @cli.command() @click.option('--site-title', default=config.SITE_TITLE, prompt='Enter site title.') @click.option('--base_url', default=config.BASE_URL, prompt='Enter base URL.') @click.option('--port', default=config.PORT, prompt='Enter port number.') @click.option('--secret-key', default=config.SECRET_KEY if config.SECRET_KEY != "CHANGE_ME" else random_string(64), prompt='Enter secret key.') @click.option('--wiki-path', default=config.WIKI_PATH, prompt='Enter wiki data directory.', help='Wiki Directory (git repo)') @click.option('--allow-anon', default=config.ALLOW_ANON, is_flag=True, prompt='Allow anonymous edits?') @click.option('--registration-enabled', default=config.REGISTRATION_ENABLED, is_flag=True, prompt='Enable registration?') @click.option('--cache-type', default=config.CACHE_TYPE, type=click.Choice([None, 'simple', 'redis', 'memcached']), prompt='Cache type?') @click.option('--search-type', default=config.SEARCH_TYPE, type=click.Choice(['simple', 'whoosh', 'elasticsearch']), prompt='Search type?') @click.option('--db-uri', default=config.DB_URI, prompt='Database URI? Examples: http://goo.gl/RyW0cl') @click.pass_context def setup(ctx, **kw): """ Start setup wizard """ try: os.mkdir('/etc/realms-wiki') except OSError: pass conf = {} for k, v in kw.items(): conf[k.upper()] = v conf_path = config.update(conf) if conf['CACHE_TYPE'] == 'redis': prompt_and_invoke(ctx, setup_redis) elif conf['CACHE_TYPE'] == 'memcached': prompt_and_invoke(ctx, setup_memcached) if conf['SEARCH_TYPE'] == 'elasticsearch': prompt_and_invoke(ctx, setup_elasticsearch) elif conf['SEARCH_TYPE'] == 'whoosh': install_whoosh() green('Config saved to %s' % conf_path) if not conf_path.startswith('/etc/realms-wiki'): yellow('Note: You can move file to /etc/realms-wiki/realms-wiki.json') click.echo() yellow('Type "realms-wiki start" to start server') yellow('Type "realms-wiki dev" to start server in development mode') yellow('Full usage: realms-wiki --help') @click.command() @click.option('--cache-redis-host', default=getattr(config, 'CACHE_REDIS_HOST', "127.0.0.1"), prompt='Redis host') @click.option('--cache-redis-port', default=getattr(config, 'CACHE_REDIS_POST', 6379), prompt='Redis port', type=int) @click.option('--cache-redis-password', default=getattr(config, 'CACHE_REDIS_PASSWORD', None), prompt='Redis password') @click.option('--cache-redis-db', default=getattr(config, 'CACHE_REDIS_DB', 0), prompt='Redis db') @click.pass_context def setup_redis(ctx, **kw): conf = config.read() for k, v in kw.items(): conf[k.upper()] = v config.update(conf) install_redis() @click.command() @click.option('--elasticsearch-url', default=getattr(config, 'ELASTICSEARCH_URL', 'http://127.0.0.1:9200'), prompt='Elasticsearch URL') def setup_elasticsearch(**kw): conf = config.read() for k, v in kw.items(): conf[k.upper()] = v config.update(conf) cli.add_command(setup_redis) cli.add_command(setup_elasticsearch) def get_prefix(): return sys.prefix @cli.command(name='pip') @click.argument('cmd', nargs=-1) def pip_(cmd): """ Execute pip commands, useful for virtualenvs """ pip.main(cmd) def install_redis(): pip.main(['install', 'redis']) def install_whoosh(): pip.main(['install', 'Whoosh']) def install_mysql(): pip.main(['install', 'MySQL-Python']) def install_postgres(): pip.main(['install', 'psycopg2']) def install_crate(): pip.main(['install', 'crate']) def install_memcached(): pip.main(['install', 'python-memcached']) @click.command() @click.option('--cache-memcached-servers', default=getattr(config, 'CACHE_MEMCACHED_SERVERS', ["127.0.0.1:11211"]), type=click.STRING, prompt='Memcached servers, separate with a space') def setup_memcached(**kw): conf = {} for k, v in kw.items(): conf[k.upper()] = v config.update(conf) @cli.command() @click.option('--user', default=get_user(), type=click.STRING, prompt='Run as which user? (it must exist)') @click.option('--port', default=config.PORT, type=click.INT, prompt='What port to listen on?') @click.option('--workers', default=cpu_count() * 2 + 1, type=click.INT, prompt="Number of workers? (defaults to ncpu*2+1)") def setup_upstart(**kwargs): """ Start upstart conf creation wizard """ from realms.lib.util import upstart_script if in_virtualenv(): app_dir = get_prefix() path = '/'.join(sys.executable.split('/')[:-1]) else: # Assumed root install, not sure if this matters? app_dir = '/' path = None kwargs.update(dict(app_dir=app_dir, path=path)) conf_file = '/etc/init/realms-wiki.conf' script = upstart_script(**kwargs) try: with open(conf_file, 'w') as f: f.write(script) green('Wrote file to %s' % conf_file) except IOError: with open('/tmp/realms-wiki.conf', 'w') as f: f.write(script) yellow("Wrote file to /tmp/realms-wiki.conf, to install type:") yellow("sudo mv /tmp/realms-wiki.conf /etc/init/realms-wiki.conf") click.echo() click.echo("Upstart usage:") green("sudo start realms-wiki") green("sudo stop realms-wiki") green("sudo restart realms-wiki") green("sudo status realms-wiki") @cli.command() @click.argument('json_string') def configure(json_string): """ Set config, expects JSON encoded string """ try: config.update(json.loads(json_string)) except ValueError, e: red('Config value should be valid JSON') @cli.command() @click.option('--port', default=config.PORT) @click.option('--host', default=config.HOST) def dev(port, host): """ Run development server """ green("Starting development server") config_path = config.get_path() if config_path: green("Using config: %s" % config_path) else: yellow("Using default configuration") create_app().run(host=host, port=port, debug=True) def start_server(): if is_running(get_pid()): yellow("Server is already running") return try: open(config.PIDFILE, 'w') except IOError: red("PID file not writeable (%s) " % config.PIDFILE) return flags = '--daemon --pid %s' % config.PIDFILE green("Server started. Port: %s" % config.PORT) config_path = config.get_path() if config_path: green("Using config: %s" % config_path) else: yellow("Using default configuration") prefix = '' if in_virtualenv(): prefix = get_prefix() + "/bin/" Popen("%sgunicorn 'realms:create_app()' -b %s:%s -k gevent %s" % (prefix, config.HOST, config.PORT, flags), shell=True, executable='/bin/bash') def stop_server(): pid = get_pid() if not is_running(pid): yellow("Server is not running") else: yellow("Shutting down server") call(['kill', pid]) while is_running(pid): time.sleep(1) @cli.command() def run(): """ Run production server (alias for start) """ start_server() @cli.command() def start(): """ Run server daemon """ start_server() @cli.command() def stop(): """ Stop server """ stop_server() @cli.command() def restart(): """ Restart server """ stop_server() start_server() @cli.command() def status(): """ Get server status """ pid = get_pid() if not is_running(pid): yellow("Server is not running") else: green("Server is running PID: %s" % pid) @cli.command() def create_db(): """ Creates DB tables """ green("Creating all tables") with app.app_context(): green('DB_URI: %s' % app.config.get('DB_URI')) db.metadata.create_all(db.get_engine(app)) @cli.command() @click.confirmation_option(help='Are you sure you want to drop the db?') def drop_db(): """ Drops DB tables """ yellow("Dropping all tables") with app.app_context(): db.metadata.drop_all(db.get_engine(app)) @cli.command() def clear_cache(): """ Clears cache """ yellow("Clearing the cache") with app.app_context(): cache.clear() @cli.command() def test(): """ Run tests """ for mod in [('flask_testing', 'Flask-Testing'), ('nose', 'nose'), ('blinker', 'blinker')]: if not module_exists(mod[0]): pip.main(['install', mod[1]]) nosetests = get_prefix() + "/bin/nosetests" if in_virtualenv() else "nosetests" call([nosetests, 'realms']) @cli.command() def version(): """ Output version """ green(__version__) @cli.command(add_help_option=False) def deploy(): """ Deploy to PyPI and docker hub """ call("python setup.py sdist upload", shell=True) call("sudo docker build --no-cache -t realms/realms-wiki %s/docker" % app.config['APP_PATH'], shell=True) id_ = json.loads(Popen("sudo docker inspect realms/realms-wiki".split(), stdout=subprocess.PIPE).communicate()[0])[0]['Id'] call("sudo docker tag %s realms/realms-wiki:%s" % (id_, __version__), shell=True) call("sudo docker push realms/realms-wiki", shell=True) if __name__ == '__main__': cli()