realms-wiki/realms/commands.py
Lars Immisch 644929c4c8 Subtle bug in parameter handling
The parameter names must not be converted to upper case before calling
context.invoke, because that works with the lowercase names.

Before this bugfix,, we had kw like this in the various setup_* functions:
{'DB_URI': 'sqlite:////tmp/wiki.db', 'db_uri': 'sqlite:////real/path.db'}
and whichever won was pretty much random (dict sort order)
2015-09-24 21:49:18 +02:00

442 lines
11 KiB
Python

from realms import config, create_app, db, __version__, flask_cli as cli
from realms.lib.util import random_string, in_virtualenv, green, yellow, red
from subprocess import call, Popen
from multiprocessing import cpu_count
import click
import json
import sys
import os
import pip
import time
import subprocess
# 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)
def dev(port):
""" 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="0.0.0.0",
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 0.0.0.0:%s -k gevent %s" %
(prefix, 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 test():
""" Run tests
"""
for mod in [('flask.ext.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()