Git based wiki inspired by Gollum
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

462 lines
11KB

  1. from __future__ import absolute_import
  2. import json
  3. import sys
  4. import os
  5. import time
  6. import subprocess
  7. from subprocess import call, Popen
  8. from multiprocessing import cpu_count
  9. import click
  10. import pip
  11. from realms import config, create_app, db, __version__, cli, cache
  12. from realms.lib.util import random_string, in_virtualenv, green, yellow, red
  13. config = config.conf
  14. # called to discover commands in modules
  15. app = create_app()
  16. def get_user():
  17. for name in ('SUDO_USER', 'LOGNAME', 'USER', 'LNAME', 'USERNAME'):
  18. user = os.environ.get(name)
  19. if user:
  20. return user
  21. def get_pid():
  22. try:
  23. with file(config.PIDFILE) as f:
  24. return f.read().strip()
  25. except IOError:
  26. return None
  27. def is_running(pid):
  28. if not pid:
  29. return False
  30. pid = int(pid)
  31. try:
  32. os.kill(pid, 0)
  33. except OSError:
  34. return False
  35. return True
  36. def module_exists(module_name):
  37. try:
  38. __import__(module_name)
  39. except ImportError:
  40. return False
  41. else:
  42. return True
  43. def prompt_and_invoke(ctx, fn):
  44. # This is a workaround for a bug in click.
  45. # See https://github.com/mitsuhiko/click/issues/429
  46. # This isn't perfect - we are ignoring some information (type mostly)
  47. kw = {}
  48. for p in fn.params:
  49. v = click.prompt(p.prompt, p.default, p.hide_input,
  50. p.confirmation_prompt, p.type)
  51. kw[p.name] = v
  52. ctx.invoke(fn, **kw)
  53. @cli.command()
  54. @click.option('--site-title',
  55. default=config.SITE_TITLE,
  56. prompt='Enter site title.')
  57. @click.option('--base_url',
  58. default=config.BASE_URL,
  59. prompt='Enter base URL.')
  60. @click.option('--port',
  61. default=config.PORT,
  62. prompt='Enter port number.')
  63. @click.option('--secret-key',
  64. default=config.SECRET_KEY if config.SECRET_KEY != "CHANGE_ME" else random_string(64),
  65. prompt='Enter secret key.')
  66. @click.option('--wiki-path',
  67. default=config.WIKI_PATH,
  68. prompt='Enter wiki data directory.',
  69. help='Wiki Directory (git repo)')
  70. @click.option('--allow-anon',
  71. default=config.ALLOW_ANON,
  72. is_flag=True,
  73. prompt='Allow anonymous edits?')
  74. @click.option('--registration-enabled',
  75. default=config.REGISTRATION_ENABLED,
  76. is_flag=True,
  77. prompt='Enable registration?')
  78. @click.option('--cache-type',
  79. default=config.CACHE_TYPE,
  80. type=click.Choice([None, 'simple', 'redis', 'memcached']),
  81. prompt='Cache type?')
  82. @click.option('--search-type',
  83. default=config.SEARCH_TYPE,
  84. type=click.Choice(['simple', 'whoosh', 'elasticsearch']),
  85. prompt='Search type?')
  86. @click.option('--db-uri',
  87. default=config.DB_URI,
  88. prompt='Database URI? Examples: http://goo.gl/RyW0cl')
  89. @click.pass_context
  90. def setup(ctx, **kw):
  91. """ Start setup wizard
  92. """
  93. try:
  94. os.mkdir('/etc/realms-wiki')
  95. except OSError:
  96. pass
  97. conf = {}
  98. for k, v in kw.items():
  99. conf[k.upper()] = v
  100. conf_path = config.update(conf)
  101. if conf['CACHE_TYPE'] == 'redis':
  102. prompt_and_invoke(ctx, setup_redis)
  103. elif conf['CACHE_TYPE'] == 'memcached':
  104. prompt_and_invoke(ctx, setup_memcached)
  105. if conf['SEARCH_TYPE'] == 'elasticsearch':
  106. prompt_and_invoke(ctx, setup_elasticsearch)
  107. elif conf['SEARCH_TYPE'] == 'whoosh':
  108. install_whoosh()
  109. green('Config saved to %s' % conf_path)
  110. if not conf_path.startswith('/etc/realms-wiki'):
  111. yellow('Note: You can move file to /etc/realms-wiki/realms-wiki.json')
  112. click.echo()
  113. yellow('Type "realms-wiki start" to start server')
  114. yellow('Type "realms-wiki dev" to start server in development mode')
  115. yellow('Full usage: realms-wiki --help')
  116. @click.command()
  117. @click.option('--cache-redis-host',
  118. default=getattr(config, 'CACHE_REDIS_HOST', "127.0.0.1"),
  119. prompt='Redis host')
  120. @click.option('--cache-redis-port',
  121. default=getattr(config, 'CACHE_REDIS_POST', 6379),
  122. prompt='Redis port',
  123. type=int)
  124. @click.option('--cache-redis-password',
  125. default=getattr(config, 'CACHE_REDIS_PASSWORD', None),
  126. prompt='Redis password')
  127. @click.option('--cache-redis-db',
  128. default=getattr(config, 'CACHE_REDIS_DB', 0),
  129. prompt='Redis db')
  130. @click.pass_context
  131. def setup_redis(ctx, **kw):
  132. conf = config.read()
  133. for k, v in kw.items():
  134. conf[k.upper()] = v
  135. config.update(conf)
  136. install_redis()
  137. @click.command()
  138. @click.option('--elasticsearch-url',
  139. default=getattr(config, 'ELASTICSEARCH_URL', 'http://127.0.0.1:9200'),
  140. prompt='Elasticsearch URL')
  141. def setup_elasticsearch(**kw):
  142. conf = config.read()
  143. for k, v in kw.items():
  144. conf[k.upper()] = v
  145. config.update(conf)
  146. cli.add_command(setup_redis)
  147. cli.add_command(setup_elasticsearch)
  148. def get_prefix():
  149. return sys.prefix
  150. @cli.command(name='pip')
  151. @click.argument('cmd', nargs=-1)
  152. def pip_(cmd):
  153. """ Execute pip commands, useful for virtualenvs
  154. """
  155. pip.main(cmd)
  156. def install_redis():
  157. pip.main(['install', 'redis'])
  158. def install_whoosh():
  159. pip.main(['install', 'Whoosh'])
  160. def install_mysql():
  161. pip.main(['install', 'MySQL-Python'])
  162. def install_postgres():
  163. pip.main(['install', 'psycopg2'])
  164. def install_crate():
  165. pip.main(['install', 'crate'])
  166. def install_memcached():
  167. pip.main(['install', 'python-memcached'])
  168. @click.command()
  169. @click.option('--cache-memcached-servers',
  170. default=getattr(config, 'CACHE_MEMCACHED_SERVERS', ["127.0.0.1:11211"]),
  171. type=click.STRING,
  172. prompt='Memcached servers, separate with a space')
  173. def setup_memcached(**kw):
  174. conf = {}
  175. for k, v in kw.items():
  176. conf[k.upper()] = v
  177. config.update(conf)
  178. @cli.command()
  179. @click.option('--user',
  180. default=get_user(),
  181. type=click.STRING,
  182. prompt='Run as which user? (it must exist)')
  183. @click.option('--port',
  184. default=config.PORT,
  185. type=click.INT,
  186. prompt='What port to listen on?')
  187. @click.option('--workers',
  188. default=cpu_count() * 2 + 1,
  189. type=click.INT,
  190. prompt="Number of workers? (defaults to ncpu*2+1)")
  191. def setup_upstart(**kwargs):
  192. """ Start upstart conf creation wizard
  193. """
  194. from realms.lib.util import upstart_script
  195. if in_virtualenv():
  196. app_dir = get_prefix()
  197. path = '/'.join(sys.executable.split('/')[:-1])
  198. else:
  199. # Assumed root install, not sure if this matters?
  200. app_dir = '/'
  201. path = None
  202. kwargs.update(dict(app_dir=app_dir, path=path))
  203. conf_file = '/etc/init/realms-wiki.conf'
  204. script = upstart_script(**kwargs)
  205. try:
  206. with open(conf_file, 'w') as f:
  207. f.write(script)
  208. green('Wrote file to %s' % conf_file)
  209. except IOError:
  210. with open('/tmp/realms-wiki.conf', 'w') as f:
  211. f.write(script)
  212. yellow("Wrote file to /tmp/realms-wiki.conf, to install type:")
  213. yellow("sudo mv /tmp/realms-wiki.conf /etc/init/realms-wiki.conf")
  214. click.echo()
  215. click.echo("Upstart usage:")
  216. green("sudo start realms-wiki")
  217. green("sudo stop realms-wiki")
  218. green("sudo restart realms-wiki")
  219. green("sudo status realms-wiki")
  220. @cli.command()
  221. @click.argument('json_string')
  222. def configure(json_string):
  223. """ Set config, expects JSON encoded string
  224. """
  225. try:
  226. config.update(json.loads(json_string))
  227. except ValueError, e:
  228. red('Config value should be valid JSON')
  229. @cli.command()
  230. @click.option('--port', default=config.PORT)
  231. @click.option('--host', default=config.HOST)
  232. def dev(port, host):
  233. """ Run development server
  234. """
  235. green("Starting development server")
  236. config_path = config.get_path()
  237. if config_path:
  238. green("Using config: %s" % config_path)
  239. else:
  240. yellow("Using default configuration")
  241. create_app().run(host=host,
  242. port=port,
  243. debug=True)
  244. def start_server():
  245. if is_running(get_pid()):
  246. yellow("Server is already running")
  247. return
  248. try:
  249. open(config.PIDFILE, 'w')
  250. except IOError:
  251. red("PID file not writeable (%s) " % config.PIDFILE)
  252. return
  253. flags = '--daemon --pid %s' % config.PIDFILE
  254. green("Server started. Port: %s" % config.PORT)
  255. config_path = config.get_path()
  256. if config_path:
  257. green("Using config: %s" % config_path)
  258. else:
  259. yellow("Using default configuration")
  260. prefix = ''
  261. if in_virtualenv():
  262. prefix = get_prefix() + "/bin/"
  263. Popen("%sgunicorn 'realms:create_app()' -b %s:%s -k gevent %s" %
  264. (prefix, config.HOST, config.PORT, flags), shell=True, executable='/bin/bash')
  265. def stop_server():
  266. pid = get_pid()
  267. if not is_running(pid):
  268. yellow("Server is not running")
  269. else:
  270. yellow("Shutting down server")
  271. call(['kill', pid])
  272. while is_running(pid):
  273. time.sleep(1)
  274. @cli.command()
  275. def run():
  276. """ Run production server (alias for start)
  277. """
  278. start_server()
  279. @cli.command()
  280. def start():
  281. """ Run server daemon
  282. """
  283. start_server()
  284. @cli.command()
  285. def stop():
  286. """ Stop server
  287. """
  288. stop_server()
  289. @cli.command()
  290. def restart():
  291. """ Restart server
  292. """
  293. stop_server()
  294. start_server()
  295. @cli.command()
  296. def status():
  297. """ Get server status
  298. """
  299. pid = get_pid()
  300. if not is_running(pid):
  301. yellow("Server is not running")
  302. else:
  303. green("Server is running PID: %s" % pid)
  304. @cli.command()
  305. def create_db():
  306. """ Creates DB tables
  307. """
  308. green("Creating all tables")
  309. with app.app_context():
  310. green('DB_URI: %s' % app.config.get('DB_URI'))
  311. db.metadata.create_all(db.get_engine(app))
  312. @cli.command()
  313. @click.confirmation_option(help='Are you sure you want to drop the db?')
  314. def drop_db():
  315. """ Drops DB tables
  316. """
  317. yellow("Dropping all tables")
  318. with app.app_context():
  319. db.metadata.drop_all(db.get_engine(app))
  320. @cli.command()
  321. def clear_cache():
  322. """ Clears cache
  323. """
  324. yellow("Clearing the cache")
  325. with app.app_context():
  326. cache.clear()
  327. @cli.command()
  328. def test():
  329. """ Run tests
  330. """
  331. for mod in [('flask_testing', 'Flask-Testing'), ('nose', 'nose'), ('blinker', 'blinker')]:
  332. if not module_exists(mod[0]):
  333. pip.main(['install', mod[1]])
  334. nosetests = get_prefix() + "/bin/nosetests" if in_virtualenv() else "nosetests"
  335. call([nosetests, 'realms'])
  336. @cli.command()
  337. def version():
  338. """ Output version
  339. """
  340. green(__version__)
  341. @cli.command(add_help_option=False)
  342. def deploy():
  343. """ Deploy to PyPI and docker hub
  344. """
  345. call("python setup.py sdist upload", shell=True)
  346. call("sudo docker build --no-cache -t realms/realms-wiki %s/docker" % app.config['APP_PATH'], shell=True)
  347. id_ = json.loads(Popen("sudo docker inspect realms/realms-wiki".split(), stdout=subprocess.PIPE).communicate()[0])[0]['Id']
  348. call("sudo docker tag %s realms/realms-wiki:%s" % (id_, __version__), shell=True)
  349. call("sudo docker push realms/realms-wiki", shell=True)
  350. if __name__ == '__main__':
  351. cli()