PageRenderTime 31ms CodeModel.GetById 11ms RepoModel.GetById 1ms app.codeStats 0ms

/reviewboard/cmdline/rbsite.py

https://github.com/sawatzkylindsey/reviewboard
Python | 1466 lines | 1229 code | 149 blank | 88 comment | 58 complexity | 91fff265723d19db355feb9f6931f83a MD5 | raw file
  1. #!/usr/bin/env python
  2. import getpass
  3. import imp
  4. import os
  5. import pkg_resources
  6. import platform
  7. import re
  8. import shutil
  9. import sys
  10. import textwrap
  11. import warnings
  12. from optparse import OptionGroup, OptionParser
  13. from random import choice
  14. DOCS_BASE = "http://www.reviewboard.org/docs/manual/dev/"
  15. # See if GTK is a possibility.
  16. try:
  17. # Disable the gtk warning we might hit. This is because pygtk will
  18. # yell if it can't access X.
  19. warnings.simplefilter("ignore")
  20. import pygtk
  21. pygtk.require('2.0')
  22. import gtk
  23. can_use_gtk = True
  24. gtk.init_check()
  25. except:
  26. can_use_gtk = False
  27. # Reset the warnings so we don't ignore everything.
  28. warnings.resetwarnings()
  29. # But then ignore the PendingDeprecationWarnings that we'll get from Django.
  30. # See bug 1683.
  31. warnings.filterwarnings("ignore", category=PendingDeprecationWarning)
  32. VERSION = "0.1"
  33. DEBUG = False
  34. # Global State
  35. options = None
  36. args = None
  37. site = None
  38. ui = None
  39. class Dependencies(object):
  40. memcached_modules = ["memcache"]
  41. sqlite_modules = ["pysqlite2", "sqlite3"]
  42. mysql_modules = ["MySQLdb"]
  43. postgresql_modules = ["psycopg2"]
  44. cache_dependency_info = {
  45. 'required': False,
  46. 'title': 'Server Cache',
  47. 'dependencies': [
  48. ("memcached", memcached_modules),
  49. ],
  50. }
  51. db_dependency_info = {
  52. 'required': True,
  53. 'title': 'Databases',
  54. 'dependencies': [
  55. ("sqlite3", sqlite_modules),
  56. ("MySQL", mysql_modules),
  57. ("PostgreSQL", postgresql_modules)
  58. ],
  59. }
  60. @classmethod
  61. def get_support_memcached(cls):
  62. return cls.has_modules(cls.memcached_modules)
  63. @classmethod
  64. def get_support_mysql(cls):
  65. return cls.has_modules(cls.mysql_modules)
  66. @classmethod
  67. def get_support_postgresql(cls):
  68. return cls.has_modules(cls.postgresql_modules)
  69. @classmethod
  70. def get_support_sqlite(cls):
  71. return cls.has_modules(cls.sqlite_modules)
  72. @classmethod
  73. def get_missing(cls):
  74. fatal = False
  75. missing_groups = []
  76. for dep_info in [cls.cache_dependency_info,
  77. cls.db_dependency_info]:
  78. missing_deps = []
  79. for desc, modules in dep_info['dependencies']:
  80. if not cls.has_modules(modules):
  81. missing_deps.append("%s (%s)" % (desc, ", ".join(modules)))
  82. if missing_deps:
  83. if (dep_info['required'] and
  84. len(missing_deps) == len(dep_info['dependencies'])):
  85. fatal = True
  86. text = "%s (required)" % dep_info['title']
  87. else:
  88. text = "%s (optional)" % dep_info['title']
  89. missing_groups.append({
  90. 'title': text,
  91. 'dependencies': missing_deps,
  92. })
  93. return fatal, missing_groups
  94. @classmethod
  95. def has_modules(cls, names):
  96. """
  97. Returns whether or not one of the specified modules is installed.
  98. """
  99. for name in names:
  100. try:
  101. __import__(name)
  102. return True
  103. except ImportError:
  104. continue
  105. return False
  106. class Site(object):
  107. def __init__(self, install_dir, options):
  108. self.install_dir = install_dir
  109. self.abs_install_dir = os.path.abspath(install_dir)
  110. self.site_id = \
  111. os.path.basename(install_dir).replace(" ", "_").replace(".", "_")
  112. self.options = options
  113. # State saved during installation
  114. self.domain_name = None
  115. self.site_root = None
  116. self.media_url = None
  117. self.db_type = None
  118. self.db_name = None
  119. self.db_host = None
  120. self.db_port = None
  121. self.db_user = None
  122. self.db_pass = None
  123. self.cache_type = None
  124. self.cache_info = None
  125. self.web_server_type = None
  126. self.python_loader = None
  127. self.admin_user = None
  128. self.admin_password = None
  129. def rebuild_site_directory(self):
  130. """
  131. Rebuilds the site hierarchy.
  132. """
  133. htdocs_dir = os.path.join(self.install_dir, "htdocs")
  134. media_dir = os.path.join(htdocs_dir, "media")
  135. self.mkdir(self.install_dir)
  136. self.mkdir(os.path.join(self.install_dir, "logs"))
  137. self.mkdir(os.path.join(self.install_dir, "conf"))
  138. self.mkdir(os.path.join(self.install_dir, "tmp"))
  139. os.chmod(os.path.join(self.install_dir, "tmp"), 0777)
  140. self.mkdir(os.path.join(self.install_dir, "data"))
  141. self.mkdir(htdocs_dir)
  142. self.mkdir(media_dir)
  143. # TODO: In the future, support changing ownership of these
  144. # directories.
  145. self.mkdir(os.path.join(media_dir, "uploaded"))
  146. self.mkdir(os.path.join(media_dir, "uploaded", "images"))
  147. self.link_pkg_dir("reviewboard",
  148. "htdocs/errordocs",
  149. os.path.join("htdocs", "errordocs"))
  150. media_base = os.path.join("htdocs", "media")
  151. rb_djblets_src = "htdocs/media/djblets"
  152. rb_djblets_dest = os.path.join(media_base, "djblets")
  153. for media_dir in ["admin", "rb"]:
  154. path = os.path.join(media_base, media_dir)
  155. self.link_pkg_dir("reviewboard",
  156. "htdocs/media/%s" % media_dir,
  157. os.path.join(media_base, media_dir))
  158. # Link from Djblets if available.
  159. if pkg_resources.resource_exists("djblets", "media"):
  160. self.link_pkg_dir("djblets", "media", rb_djblets_dest)
  161. elif pkg_resources.resource_exists("reviewboard", rb_djblets_src):
  162. self.link_pkg_dir("reviewboard", rb_djblets_src,
  163. rb_djblets_dest)
  164. else:
  165. ui.error("Unable to find the Djblets media path. Make sure "
  166. "Djblets is installed and try this again.")
  167. # Generate a .htaccess file that enables compression and
  168. # never expires various file types.
  169. path = os.path.join(self.install_dir, media_base, ".htaccess")
  170. fp = open(path, "w")
  171. fp.write('<IfModule mod_expires.c>\n')
  172. fp.write(' <FilesMatch "\.(jpg|gif|png|css|js|htc)">\n')
  173. fp.write(' ExpiresActive on\n')
  174. fp.write(' ExpiresDefault "access plus 1 year"\n')
  175. fp.write(' </FilesMatch>\n')
  176. fp.write('</IfModule>\n')
  177. fp.write('\n')
  178. fp.write('<IfModule mod_deflate.c>\n')
  179. for mimetype in ["text/html", "text/plain", "text/xml",
  180. "text/css", "text/javascript",
  181. "application/javascript",
  182. "application/x-javascript"]:
  183. fp.write(" AddOutputFilterByType DEFLATE %s\n" % mimetype)
  184. fp.write('</IfModule>\n')
  185. fp.close()
  186. def setup_settings(self):
  187. # Make sure that we have our settings_local.py in our path for when
  188. # we need to run manager commands.
  189. sys.path.insert(0, os.path.join(self.abs_install_dir, "conf"))
  190. def generate_config_files(self):
  191. web_conf_filename = ""
  192. enable_fastcgi = False
  193. enable_wsgi = False
  194. if self.web_server_type == "apache":
  195. if self.python_loader == "modpython":
  196. web_conf_filename = "apache-modpython.conf"
  197. elif self.python_loader == "fastcgi":
  198. web_conf_filename = "apache-fastcgi.conf"
  199. enable_fastcgi = True
  200. elif self.python_loader == "wsgi":
  201. web_conf_filename = "apache-wsgi.conf"
  202. enable_wsgi = True
  203. else:
  204. # Should never be reached.
  205. assert False
  206. elif self.web_server_type == "lighttpd":
  207. web_conf_filename = "lighttpd.conf"
  208. enable_fastcgi = True
  209. else:
  210. # Should never be reached.
  211. assert False
  212. conf_dir = os.path.join(self.install_dir, "conf")
  213. htdocs_dir = os.path.join(self.install_dir, "htdocs")
  214. self.process_template("cmdline/conf/%s.in" % web_conf_filename,
  215. os.path.join(conf_dir, web_conf_filename))
  216. self.process_template("cmdline/conf/search-cron.conf.in",
  217. os.path.join(conf_dir, "search-cron.conf"))
  218. if enable_fastcgi:
  219. fcgi_filename = os.path.join(htdocs_dir, "reviewboard.fcgi")
  220. self.process_template("cmdline/conf/reviewboard.fcgi.in",
  221. fcgi_filename)
  222. os.chmod(fcgi_filename, 0755)
  223. elif enable_wsgi:
  224. wsgi_filename = os.path.join(htdocs_dir, "reviewboard.wsgi")
  225. self.process_template("cmdline/conf/reviewboard.wsgi.in",
  226. wsgi_filename)
  227. os.chmod(wsgi_filename, 0755)
  228. # Generate a secret key based on Django's code.
  229. secret_key = ''.join([
  230. choice('abcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*(-_=+)')
  231. for i in range(50)
  232. ])
  233. # Generate the settings_local.py
  234. fp = open(os.path.join(conf_dir, "settings_local.py"), "w")
  235. fp.write("# Site-specific configuration settings for Review Board\n")
  236. fp.write("# Definitions of these settings can be found at\n")
  237. fp.write("# http://docs.djangoproject.com/en/dev/ref/settings/\n")
  238. fp.write("\n")
  239. fp.write("# Database configuration\n")
  240. db_engine = self.db_type
  241. if db_engine == "postgresql":
  242. db_engine = "postgresql_psycopg2"
  243. fp.write("DATABASE_ENGINE = '%s'\n" % db_engine)
  244. fp.write("DATABASE_NAME = '%s'\n" % self.db_name.replace("\\", "\\\\"))
  245. if self.db_type != "sqlite3":
  246. fp.write("DATABASE_USER = '%s'\n" % (self.db_user or ""))
  247. fp.write("DATABASE_PASSWORD = '%s'\n" % (self.db_pass or ""))
  248. fp.write("DATABASE_HOST = '%s'\n" % (self.db_host or ""))
  249. fp.write("DATABASE_PORT = '%s'\n" % (self.db_port or ""))
  250. fp.write("\n")
  251. fp.write("# Unique secret key. Don't share this with anybody.\n")
  252. fp.write("SECRET_KEY = '%s'\n" % secret_key)
  253. fp.write("\n")
  254. fp.write("# Cache backend settings.\n")
  255. fp.write("CACHE_BACKEND = '%s'\n" % self.cache_info)
  256. fp.write("\n")
  257. fp.write("# Extra site information.\n")
  258. fp.write("SITE_ID = 1\n")
  259. fp.write("SITE_ROOT = '%s'\n" % self.site_root)
  260. fp.write("FORCE_SCRIPT_NAME = ''\n")
  261. fp.write("DEBUG = False\n")
  262. fp.close()
  263. self.setup_settings()
  264. def sync_database(self, allow_input=False):
  265. """
  266. Synchronizes the database.
  267. """
  268. params = []
  269. if not allow_input:
  270. params.append("--noinput")
  271. self.run_manage_command("syncdb", params)
  272. self.run_manage_command("registerscmtools")
  273. def migrate_database(self):
  274. """
  275. Performs a database migration.
  276. """
  277. self.run_manage_command("evolve", ["--noinput", "--execute"])
  278. def create_admin_user(self):
  279. """
  280. Creates an administrator user account.
  281. """
  282. cwd = os.getcwd()
  283. os.chdir(self.abs_install_dir)
  284. from django.contrib.auth.models import User
  285. User.objects.create_superuser(self.admin_user, self.admin_email,
  286. self.admin_password)
  287. os.chdir(cwd)
  288. def run_manage_command(self, cmd, params=None):
  289. cwd = os.getcwd()
  290. os.chdir(self.abs_install_dir)
  291. try:
  292. from django.core.management import execute_manager, get_commands
  293. from reviewboard.admin.migration import fix_django_evolution_issues
  294. import reviewboard.settings
  295. if not params:
  296. params = []
  297. if DEBUG:
  298. params.append("--verbosity=0")
  299. fix_django_evolution_issues(reviewboard.settings)
  300. commands_dir = os.path.join(self.abs_install_dir, 'commands')
  301. if os.path.exists(commands_dir):
  302. # Pre-fetch all the available management commands.
  303. get_commands()
  304. # Insert our own management commands into this list.
  305. # Yes, this is a bit of a hack.
  306. from django.core.management import _commands
  307. for f in os.listdir(commands_dir):
  308. module_globals = {}
  309. execfile(os.path.join(commands_dir, f), module_globals)
  310. if 'Command' in module_globals:
  311. name = os.path.splitext(f)[0]
  312. _commands[name] = module_globals['Command']()
  313. execute_manager(reviewboard.settings, [__file__, cmd] + params)
  314. except ImportError, e:
  315. ui.error("Unable to execute the manager command %s: %s" %
  316. (cmd, e))
  317. os.chdir(cwd)
  318. def mkdir(self, dirname):
  319. """
  320. Creates a directory, but only if it doesn't already exist.
  321. """
  322. if not os.path.exists(dirname):
  323. os.mkdir(dirname)
  324. def link_pkg_dir(self, pkgname, src_path, dest_path, replace=True):
  325. src_dir = pkg_resources.resource_filename(pkgname, src_path)
  326. dest_dir = os.path.join(self.install_dir, dest_path)
  327. if os.path.islink(dest_dir) and not os.path.exists(dest_dir):
  328. os.unlink(dest_dir)
  329. if os.path.exists(dest_dir):
  330. if not replace:
  331. return
  332. if os.path.islink(dest_dir):
  333. os.unlink(dest_dir)
  334. else:
  335. shutil.rmtree(dest_dir)
  336. if self.options.copy_media:
  337. shutil.copytree(src_dir, dest_dir)
  338. else:
  339. os.symlink(src_dir, dest_dir)
  340. def process_template(self, template_path, dest_filename):
  341. """
  342. Generates a file from a template.
  343. """
  344. domain_name_escaped = self.domain_name.replace(".", "\\.")
  345. template = pkg_resources.resource_string("reviewboard", template_path)
  346. sitedir = os.path.abspath(self.install_dir).replace("\\", "/")
  347. # Check if this is a .exe.
  348. if (hasattr(sys, "frozen") or # new py2exe
  349. hasattr(sys, "importers") or # new py2exe
  350. imp.is_frozen("__main__")): # tools/freeze
  351. rbsite_path = sys.executable
  352. else:
  353. rbsite_path = '"%s" "%s"' % (sys.executable, sys.argv[0])
  354. data = {
  355. 'rbsite': rbsite_path,
  356. 'sitedir': sitedir,
  357. 'sitedomain': self.domain_name,
  358. 'sitedomain_escaped': domain_name_escaped,
  359. 'siteid': self.site_id,
  360. 'siteroot': self.site_root,
  361. }
  362. template = re.sub("@([a-z_]+)@", lambda m: data.get(m.group(1)),
  363. template)
  364. fp = open(dest_filename, "w")
  365. fp.write(template)
  366. fp.close()
  367. class UIToolkit(object):
  368. """
  369. An abstract class that forms the basis for all UI interaction.
  370. Subclasses can override this to provide new ways of representing the UI
  371. to the user.
  372. """
  373. def run(self):
  374. """
  375. Runs the UI.
  376. """
  377. pass
  378. def page(self, text, allow_back=True, is_visible_func=None,
  379. on_show_func=None):
  380. """
  381. Adds a new "page" to display to the user. Input and text are
  382. associated with this page and may be displayed immediately or
  383. later, depending on the toolkit.
  384. If is_visible_func is specified and returns False, this page will
  385. be skipped.
  386. """
  387. return None
  388. def prompt_input(self, page, prompt, default=None, password=False,
  389. normalize_func=None, save_obj=None, save_var=None):
  390. """
  391. Prompts the user for some text. This may contain a default value.
  392. """
  393. raise NotImplemented
  394. def prompt_choice(self, page, prompt, choices,
  395. save_obj=None, save_var=None):
  396. """
  397. Prompts the user for an item amongst a list of choices.
  398. """
  399. raise NotImplemented
  400. def text(self, page, text):
  401. """
  402. Displays a block of text to the user.
  403. """
  404. raise NotImplemented
  405. def urllink(self, page, url):
  406. """
  407. Displays a URL to the user.
  408. """
  409. raise NotImplemented
  410. def itemized_list(self, page, title, items):
  411. """
  412. Displays an itemized list.
  413. """
  414. raise NotImplemented
  415. def step(self, page, text, func):
  416. """
  417. Adds a step of a multi-step operation. This will indicate when
  418. it's starting and when it's complete.
  419. """
  420. raise NotImplemented
  421. def error(self, text, done_func=None):
  422. """
  423. Displays a block of error text to the user.
  424. """
  425. raise NotImplemented
  426. class ConsoleUI(UIToolkit):
  427. """
  428. A UI toolkit that simply prints to the console.
  429. """
  430. def __init__(self):
  431. super(UIToolkit, self).__init__()
  432. self.header_wrapper = textwrap.TextWrapper(initial_indent="* ",
  433. subsequent_indent=" ")
  434. indent_str = " " * 4
  435. self.text_wrapper = textwrap.TextWrapper(initial_indent=indent_str,
  436. subsequent_indent=indent_str,
  437. break_long_words=False)
  438. self.error_wrapper = textwrap.TextWrapper(initial_indent="[!] ",
  439. subsequent_indent=" ",
  440. break_long_words=False)
  441. def page(self, text, allow_back=True, is_visible_func=None,
  442. on_show_func=None):
  443. """
  444. Adds a new "page" to display to the user.
  445. In the console UI, we only care if we need to display or ask questions
  446. for this page. Our representation of a page in this case is simply
  447. a boolean value. If False, nothing associated with this page will
  448. be displayed to the user.
  449. """
  450. visible = not is_visible_func or is_visible_func()
  451. if not visible:
  452. return False
  453. if on_show_func:
  454. on_show_func()
  455. print
  456. print
  457. print self.header_wrapper.fill(text)
  458. return True
  459. def prompt_input(self, page, prompt, default=None, password=False,
  460. normalize_func=None, save_obj=None, save_var=None):
  461. """
  462. Prompts the user for some text. This may contain a default value.
  463. """
  464. assert save_obj
  465. assert save_var
  466. if not page:
  467. return
  468. if default:
  469. self.text(page, "The default is %s" % default)
  470. prompt = "%s [%s]" % (prompt, default)
  471. print
  472. prompt += ": "
  473. value = None
  474. while not value:
  475. if password:
  476. value = getpass.getpass(prompt)
  477. else:
  478. value = raw_input(prompt)
  479. if not value:
  480. if default:
  481. value = default
  482. else:
  483. self.error("You must answer this question.")
  484. if normalize_func:
  485. value = normalize_func(value)
  486. setattr(save_obj, save_var, value)
  487. def prompt_choice(self, page, prompt, choices,
  488. save_obj=None, save_var=None):
  489. """
  490. Prompts the user for an item amongst a list of choices.
  491. """
  492. assert save_obj
  493. assert save_var
  494. if not page:
  495. return
  496. self.text(page, "You can type either the name or the number "
  497. "from the list below.")
  498. valid_choices = []
  499. i = 0
  500. for choice in choices:
  501. if isinstance(choice, basestring):
  502. text = choice
  503. enabled = True
  504. else:
  505. text, enabled = choice
  506. if enabled:
  507. self.text(page, "(%d) %s\n" % (i + 1, text),
  508. leading_newline=(i == 0))
  509. valid_choices.append(text)
  510. i += 1
  511. print
  512. prompt += ": "
  513. choice = None
  514. while not choice:
  515. choice = raw_input(prompt)
  516. if choice not in valid_choices:
  517. try:
  518. i = int(choice) - 1
  519. if 0 <= i < len(valid_choices):
  520. choice = valid_choices[i]
  521. break
  522. except ValueError:
  523. pass
  524. self.error("'%s' is not a valid option." % choice)
  525. choice = None
  526. setattr(save_obj, save_var, choice)
  527. def text(self, page, text, leading_newline=True):
  528. """
  529. Displays a block of text to the user.
  530. This will wrap the block to fit on the user's screen.
  531. """
  532. if not page:
  533. return
  534. if leading_newline:
  535. print
  536. print self.text_wrapper.fill(text)
  537. def urllink(self, page, url):
  538. """
  539. Displays a URL to the user.
  540. """
  541. self.text(page, url)
  542. def itemized_list(self, page, title, items):
  543. """
  544. Displays an itemized list.
  545. """
  546. if title:
  547. self.text(page, "%s:" % title)
  548. for item in items:
  549. self.text(page, " * %s" % item, False)
  550. def step(self, page, text, func):
  551. """
  552. Adds a step of a multi-step operation. This will indicate when
  553. it's starting and when it's complete.
  554. """
  555. sys.stdout.write("%s ... " % text)
  556. func()
  557. print "OK"
  558. def error(self, text, done_func=None):
  559. """
  560. Displays a block of error text to the user.
  561. """
  562. print
  563. print self.error_wrapper.fill(text)
  564. if done_func:
  565. done_func()
  566. class GtkUI(UIToolkit):
  567. """
  568. A UI toolkit that uses GTK to display a wizard.
  569. """
  570. def __init__(self):
  571. self.pages = []
  572. self.page_stack = []
  573. self.window = gtk.Window(gtk.WINDOW_TOPLEVEL)
  574. self.window.set_title("Review Board Site Installer")
  575. self.window.set_default_size(300, 550)
  576. self.window.set_border_width(0)
  577. self.window.set_resizable(False)
  578. self.window.set_type_hint(gtk.gdk.WINDOW_TYPE_HINT_DIALOG)
  579. self.window.set_position(gtk.WIN_POS_CENTER_ALWAYS)
  580. self.window.set_icon_list(*[
  581. gtk.gdk.pixbuf_new_from_file(
  582. pkg_resources.resource_filename(
  583. "reviewboard", "htdocs/media/rb/images/" + filename))
  584. for filename in ["favicon.png", "logo.png"]
  585. ])
  586. vbox = gtk.VBox(False, 0)
  587. vbox.show()
  588. self.window.add(vbox)
  589. self.notebook = gtk.Notebook()
  590. self.notebook.show()
  591. vbox.pack_start(self.notebook, True, True, 0)
  592. self.notebook.set_show_border(False)
  593. self.notebook.set_show_tabs(False)
  594. sep = gtk.HSeparator()
  595. sep.show()
  596. vbox.pack_start(sep, False, True, 0)
  597. self.bbox = gtk.HButtonBox()
  598. self.bbox.show()
  599. vbox.pack_start(self.bbox, False, False, 0)
  600. self.bbox.set_border_width(12)
  601. self.bbox.set_layout(gtk.BUTTONBOX_END)
  602. self.bbox.set_spacing(6)
  603. button = gtk.Button(stock=gtk.STOCK_CANCEL)
  604. button.show()
  605. self.bbox.pack_start(button, False, False, 0)
  606. button.connect('clicked', lambda w: self.quit())
  607. self.prev_button = gtk.Button(stock=gtk.STOCK_GO_BACK)
  608. self.prev_button.show()
  609. self.bbox.pack_start(self.prev_button, False, False, 0)
  610. self.prev_button.connect('clicked', self.previous_page)
  611. self.next_button = gtk.Button("_Next")
  612. self.next_button.show()
  613. self.bbox.pack_start(self.next_button, False, False, 0)
  614. self.next_button.connect('clicked', self.next_page)
  615. self.next_button.set_image(
  616. gtk.image_new_from_stock(gtk.STOCK_GO_FORWARD,
  617. gtk.ICON_SIZE_BUTTON))
  618. self.next_button.set_flags(gtk.CAN_DEFAULT)
  619. self.next_button.grab_default()
  620. self.next_button.grab_focus()
  621. self.close_button = gtk.Button(stock=gtk.STOCK_CLOSE)
  622. self.bbox.pack_start(self.close_button, False, False, 0)
  623. self.close_button.connect('clicked', lambda w: self.quit())
  624. def run(self):
  625. if self.pages:
  626. self.window.show()
  627. self.page_stack.append(self.pages[0])
  628. self.update_buttons()
  629. gtk.main()
  630. def quit(self):
  631. gtk.main_quit()
  632. def update_buttons(self):
  633. cur_page = self.page_stack[-1]
  634. cur_page_num = self.notebook.get_current_page()
  635. self.prev_button.set_sensitive(cur_page_num > 0 and
  636. cur_page['allow_back'])
  637. if cur_page_num == len(self.pages) - 1:
  638. self.close_button.show()
  639. self.next_button.hide()
  640. else:
  641. allow_next = True
  642. for validator in cur_page['validators']:
  643. if not validator():
  644. allow_next = False
  645. break
  646. self.close_button.hide()
  647. self.next_button.show()
  648. self.next_button.set_sensitive(allow_next)
  649. def previous_page(self, widget):
  650. self.page_stack.pop()
  651. self.notebook.set_current_page(self.page_stack[-1]['index'])
  652. self.update_buttons()
  653. def next_page(self, widget):
  654. new_page_index = self.notebook.get_current_page() + 1
  655. for i in range(new_page_index, len(self.pages)):
  656. page = self.pages[i]
  657. if not page['is_visible_func'] or page['is_visible_func']():
  658. page_info = self.pages[i]
  659. self.notebook.set_current_page(i)
  660. self.page_stack.append(page)
  661. self.update_buttons()
  662. for func in page_info['on_show_funcs']:
  663. func()
  664. return
  665. def page(self, text, allow_back=True, is_visible_func=None,
  666. on_show_func=None):
  667. vbox = gtk.VBox(False, 0)
  668. vbox.show()
  669. self.notebook.append_page(vbox)
  670. hbox = gtk.HBox(False, 12)
  671. hbox.show()
  672. vbox.pack_start(hbox, False, True, 0)
  673. hbox.set_border_width(12)
  674. # Paint the title box as the base color (usually white)
  675. hbox.connect(
  676. 'expose-event',
  677. lambda w, e: w.get_window().draw_rectangle(
  678. w.get_style().base_gc[w.state], True,
  679. *w.get_allocation()))
  680. # Add the logo
  681. logo_file = pkg_resources.resource_filename(
  682. "reviewboard",
  683. "htdocs/media/rb/images/logo.png")
  684. image = gtk.image_new_from_file(logo_file)
  685. image.show()
  686. hbox.pack_start(image, False, False, 0)
  687. # Add the page title
  688. label = gtk.Label("<big><b>%s</b></big>" % text)
  689. label.show()
  690. hbox.pack_start(label, True, True, 0)
  691. label.set_alignment(0, 0.5)
  692. label.set_use_markup(True)
  693. # Add the separator
  694. sep = gtk.HSeparator()
  695. sep.show()
  696. vbox.pack_start(sep, False, True, 0)
  697. content_vbox = gtk.VBox(False, 12)
  698. content_vbox.show()
  699. vbox.pack_start(content_vbox, True, True, 0)
  700. content_vbox.set_border_width(12)
  701. page = {
  702. 'is_visible_func': is_visible_func,
  703. 'widget': content_vbox,
  704. 'index': len(self.pages),
  705. 'allow_back': allow_back,
  706. 'label_sizegroup': gtk.SizeGroup(gtk.SIZE_GROUP_HORIZONTAL),
  707. 'validators': [],
  708. 'on_show_funcs': [],
  709. }
  710. if on_show_func:
  711. page['on_show_funcs'].append(on_show_func)
  712. self.pages.append(page)
  713. return page
  714. def prompt_input(self, page, prompt, default=None, password=False,
  715. normalize_func=None, save_obj=None, save_var=None):
  716. def save_input(widget=None, event=None):
  717. value = entry.get_text()
  718. if normalize_func:
  719. value = normalize_func(value)
  720. setattr(save_obj, save_var, value)
  721. self.update_buttons()
  722. hbox = gtk.HBox(False, 6)
  723. hbox.show()
  724. page['widget'].pack_start(hbox, False, False, 0)
  725. label = gtk.Label("<b>%s:</b>" % prompt)
  726. label.show()
  727. hbox.pack_start(label, False, True, 0)
  728. label.set_alignment(0, 0.5)
  729. label.set_line_wrap(True)
  730. label.set_use_markup(True)
  731. page['label_sizegroup'].add_widget(label)
  732. entry = gtk.Entry()
  733. entry.show()
  734. hbox.pack_start(entry, True, True, 0)
  735. entry.set_activates_default(True)
  736. if password:
  737. entry.set_visibility(False)
  738. if default:
  739. entry.set_text(default)
  740. entry.connect("key_release_event", save_input)
  741. page.setdefault('entries', []).append(entry)
  742. page['validators'].append(lambda: entry.get_text() != "")
  743. page['on_show_funcs'].append(save_input)
  744. # If this is the first on the page, make sure it gets focus when
  745. # we switch to this page.
  746. if len(page['entries']) == 1:
  747. page['on_show_funcs'].append(entry.grab_focus)
  748. def prompt_choice(self, page, prompt, choices,
  749. save_obj=None, save_var=None):
  750. """
  751. Prompts the user for an item amongst a list of choices.
  752. """
  753. def on_toggled(radio_button):
  754. if radio_button.get_active():
  755. setattr(save_obj, save_var, radio_button.get_label())
  756. hbox = gtk.HBox(False, 0)
  757. hbox.show()
  758. page['widget'].pack_start(hbox, False, True, 0)
  759. label = gtk.Label(" ")
  760. label.show()
  761. hbox.pack_start(label, False, False, 0)
  762. vbox = gtk.VBox(False, 6)
  763. vbox.show()
  764. hbox.pack_start(vbox, True, True, 0)
  765. label = gtk.Label("<b>%s:</b>" % prompt)
  766. label.show()
  767. vbox.pack_start(label, False, True, 0)
  768. label.set_alignment(0, 0)
  769. label.set_line_wrap(True)
  770. label.set_use_markup(True)
  771. buttons = []
  772. for choice in choices:
  773. if isinstance(choice, basestring):
  774. text = choice
  775. enabled = True
  776. else:
  777. text, enabled = choice
  778. radio_button = gtk.RadioButton(label=text, use_underline=False)
  779. radio_button.show()
  780. vbox.pack_start(radio_button, False, True, 0)
  781. buttons.append(radio_button)
  782. radio_button.set_sensitive(enabled)
  783. radio_button.connect('toggled', on_toggled)
  784. if buttons[0] != radio_button:
  785. radio_button.set_group(buttons[0])
  786. # Force this to save.
  787. on_toggled(buttons[0])
  788. def text(self, page, text):
  789. """
  790. Displays a block of text to the user.
  791. """
  792. label = gtk.Label(textwrap.fill(text, 80))
  793. label.show()
  794. page['widget'].pack_start(label, False, True, 0)
  795. label.set_alignment(0, 0)
  796. def urllink(self, page, url):
  797. """
  798. Displays a URL to the user.
  799. """
  800. link_button = gtk.LinkButton(url, url)
  801. link_button.show()
  802. page['widget'].pack_start(link_button, False, False, 0)
  803. link_button.set_alignment(0, 0)
  804. def itemized_list(self, page, title, items):
  805. """
  806. Displays an itemized list.
  807. """
  808. if title:
  809. label = gtk.Label()
  810. label.set_markup("<b>%s:</b>" % title)
  811. label.show()
  812. page['widget'].pack_start(label, False, True, 0)
  813. label.set_alignment(0, 0)
  814. for item in items:
  815. self.text(page, u" \u2022 %s" % item)
  816. def step(self, page, text, func):
  817. """
  818. Adds a step of a multi-step operation. This will indicate when
  819. it's starting and when it's complete.
  820. """
  821. def call_func():
  822. self.bbox.set_sensitive(False)
  823. label.set_markup("<b>%s</b>" % text)
  824. while gtk.events_pending():
  825. gtk.main_iteration()
  826. func()
  827. label.set_text(text)
  828. self.bbox.set_sensitive(True)
  829. icon.set_from_stock(gtk.STOCK_APPLY, gtk.ICON_SIZE_MENU)
  830. hbox = gtk.HBox(False, 12)
  831. hbox.show()
  832. page['widget'].pack_start(hbox, False, False, 0)
  833. icon = gtk.Image()
  834. icon.show()
  835. hbox.pack_start(icon, False, False, 0)
  836. icon.set_size_request(*gtk.icon_size_lookup(gtk.ICON_SIZE_MENU))
  837. label = gtk.Label(text)
  838. label.show()
  839. hbox.pack_start(label, False, False, 0)
  840. label.set_alignment(0, 0)
  841. page['on_show_funcs'].append(call_func)
  842. def error(self, text, done_func=None):
  843. """
  844. Displays a block of error text to the user.
  845. """
  846. dlg = gtk.MessageDialog(self.window,
  847. gtk.DIALOG_MODAL |
  848. gtk.DIALOG_DESTROY_WITH_PARENT,
  849. gtk.MESSAGE_ERROR,
  850. gtk.BUTTONS_OK,
  851. text)
  852. dlg.show()
  853. if not done_func:
  854. done_func = self.quit
  855. dlg.connect('response', lambda w, e: done_func())
  856. class Command(object):
  857. needs_ui = False
  858. def add_options(self, parser):
  859. pass
  860. def run(self):
  861. pass
  862. class InstallCommand(Command):
  863. """
  864. Installs a new Review Board site tree and generates web server
  865. configuration files. This will ask several questions about the
  866. site before performing the installation.
  867. """
  868. needs_ui = True
  869. def add_options(self, parser):
  870. isWin = (platform.system() == "Windows")
  871. group = OptionGroup(parser, "'install' command",
  872. self.__doc__.strip())
  873. group.add_option("--copy-media", action="store_true",
  874. dest="copy_media", default=isWin,
  875. help="copy media files instead of symlinking")
  876. group.add_option("--noinput", action="store_true", default=False,
  877. help="run non-interactively using configuration "
  878. "provided in command-line options")
  879. group.add_option("--domain-name",
  880. help="fully-qualified host name of the site, "
  881. "excluding the http://, port or path")
  882. group.add_option("--site-root", default="/",
  883. help="path to the site relative to the domain name")
  884. group.add_option("--media-url", default="media/",
  885. help="the URL containing the media files")
  886. group.add_option("--db-type",
  887. help="database type (mysql, postgresql or sqlite3)")
  888. group.add_option("--db-name", default="reviewboard",
  889. help="database name (not for sqlite3)")
  890. group.add_option("--db-host", default="localhost",
  891. help="database host (not for sqlite3)")
  892. group.add_option("--db-user",
  893. help="database user (not for sqlite3)")
  894. group.add_option("--db-pass",
  895. help="password for the database user "
  896. "(not for sqlite3)")
  897. group.add_option("--cache-type",
  898. help="cache server type (memcached or file)")
  899. group.add_option("--cache-info",
  900. help="cache identifier (memcached connection string "
  901. "or file cache directory)")
  902. group.add_option("--web-server-type",
  903. help="web server (apache or lighttpd)")
  904. group.add_option("--python-loader",
  905. help="python loader for apache (modpython, fastcgi or wsgi)")
  906. group.add_option("--admin-user", default="admin",
  907. help="the site administrator's username")
  908. group.add_option("--admin-password",
  909. help="the site administrator's password")
  910. group.add_option("--admin-email",
  911. help="the site administrator's e-mail address")
  912. parser.add_option_group(group)
  913. def run(self):
  914. if not self.check_permissions():
  915. return
  916. site.__dict__.update(options.__dict__)
  917. self.print_introduction()
  918. if self.print_missing_dependencies():
  919. # There were required dependencies missing. Don't show any more
  920. # pages.
  921. return
  922. if not options.noinput:
  923. self.ask_domain()
  924. self.ask_site_root()
  925. self.ask_media_url()
  926. self.ask_database_type()
  927. self.ask_database_name()
  928. self.ask_database_host()
  929. self.ask_database_login()
  930. self.ask_cache_type()
  931. self.ask_cache_info()
  932. self.ask_web_server_type()
  933. self.ask_python_loader()
  934. self.ask_admin_user()
  935. self.show_install_status()
  936. self.show_finished()
  937. def normalize_root_url_path(self, path):
  938. if not path.endswith("/"):
  939. path += "/"
  940. if not path.startswith("/"):
  941. path = "/" + path
  942. return path
  943. def normalize_media_url_path(self, path):
  944. if not path.endswith("/"):
  945. path += "/"
  946. if path.startswith("/"):
  947. path = path[1:]
  948. return path
  949. def check_permissions(self):
  950. # Make sure we can create the directory first.
  951. try:
  952. # TODO: Do some chown tests too.
  953. if os.path.exists(site.install_dir):
  954. # Remove it first, to see if we own it and to handle the
  955. # case where the directory is empty as a result of a
  956. # previously canceled install.
  957. os.rmdir(site.install_dir)
  958. os.mkdir(site.install_dir)
  959. # Don't leave a mess. We'll actually do this at the end.
  960. os.rmdir(site.install_dir)
  961. return True
  962. except OSError:
  963. # Likely a permission error.
  964. ui.error("Unable to create the %s directory. Make sure "
  965. "you're running as an administrator and that the "
  966. "directory does not contain any files." % site.install_dir,
  967. done_func=lambda: sys.exit(1))
  968. return False
  969. def print_introduction(self):
  970. page = ui.page("Welcome to the Review Board site installation wizard")
  971. ui.text(page, "This will prepare a Review Board site installation in:")
  972. ui.text(page, site.abs_install_dir)
  973. ui.text(page, "We need to know a few things before we can prepare "
  974. "your site for installation. This will only take a few "
  975. "minutes.")
  976. def print_missing_dependencies(self):
  977. fatal, missing_dep_groups = Dependencies.get_missing()
  978. if missing_dep_groups:
  979. if fatal:
  980. page = ui.page("Required modules are missing")
  981. ui.text(page, "You are missing Python modules that are "
  982. "needed before the installation process. "
  983. "You will need to install the necessary "
  984. "modules and restart the install.")
  985. else:
  986. page = ui.page("Make sure you have the modules you need")
  987. ui.text(page, "Depending on your installation, you may need "
  988. "certain Python modules and servers that are "
  989. "missing.")
  990. ui.text(page, "If you need support for any of the following, "
  991. "you will need to install the necessary "
  992. "modules and restart the install.")
  993. for group in missing_dep_groups:
  994. ui.itemized_list(page, group['title'], group['dependencies'])
  995. return fatal
  996. def ask_domain(self):
  997. page = ui.page("What's the host name for this site?")
  998. ui.text(page, "This should be the fully-qualified host name without "
  999. "the http://, port or path.")
  1000. ui.prompt_input(page, "Domain Name", site.domain_name,
  1001. save_obj=site, save_var="domain_name")
  1002. def ask_site_root(self):
  1003. page = ui.page("What URL path points to Review Board?")
  1004. ui.text(page, "Typically, Review Board exists at the root of a URL. "
  1005. "For example, http://reviews.example.com/. In this "
  1006. "case, you would specify \"/\".")
  1007. ui.text(page, "However, if you want to listen to, say, "
  1008. "http://example.com/reviews/, you can specify "
  1009. '"/reviews/".')
  1010. ui.text(page, "Note that this is the path relative to the domain and "
  1011. "should not include the domain name.")
  1012. ui.prompt_input(page, "Root Path", site.site_root,
  1013. normalize_func=self.normalize_root_url_path,
  1014. save_obj=site, save_var="site_root")
  1015. def ask_media_url(self):
  1016. page = ui.page("What URL will point to the media files?")
  1017. ui.text(page, "While most installations distribute media files on "
  1018. "the same server as the rest of Review Board, some "
  1019. "custom installs may instead have a separate server "
  1020. "for this purpose.")
  1021. ui.prompt_input(page, "Media URL", site.media_url,
  1022. normalize_func=self.normalize_media_url_path,
  1023. save_obj=site, save_var="media_url")
  1024. def ask_database_type(self):
  1025. page = ui.page("What database type will you be using?")
  1026. ui.prompt_choice(page, "Database Type",
  1027. [("mysql", Dependencies.get_support_mysql()),
  1028. ("postgresql", Dependencies.get_support_postgresql()),
  1029. ("sqlite3", Dependencies.get_support_sqlite())],
  1030. save_obj=site, save_var="db_type")
  1031. def ask_database_name(self):
  1032. def determine_sqlite_path():
  1033. site.db_name = sqlite_db_name
  1034. sqlite_db_name = os.path.join(site.abs_install_dir, "data",
  1035. "reviewboard.db")
  1036. # Appears only if using sqlite.
  1037. page = ui.page("Determining database file path",
  1038. is_visible_func=lambda: site.db_type == "sqlite3",
  1039. on_show_func=determine_sqlite_path)
  1040. ui.text(page, "The sqlite database file will be stored in %s" %
  1041. sqlite_db_name)
  1042. ui.text(page, "If you are migrating from an existing "
  1043. "installation, you can move your existing "
  1044. "database there, or edit settings_local.py to "
  1045. "point to your old location.")
  1046. # Appears only if not using sqlite.
  1047. page = ui.page("What database name should Review Board use?",
  1048. is_visible_func=lambda: site.db_type != "sqlite3")
  1049. ui.text(page, "You may need to create this database and grant a "
  1050. "user modification rights before continuing.")
  1051. ui.prompt_input(page, "Database Name", site.db_name,
  1052. save_obj=site, save_var="db_name")
  1053. def ask_database_host(self):
  1054. def normalize_host_port(value):
  1055. if ":" in value:
  1056. value, site.db_port = value.split(":", 1)
  1057. return value
  1058. page = ui.page("What is the database server's address?",
  1059. is_visible_func=lambda: site.db_type != "sqlite3")
  1060. ui.text(page, "This should be specified in hostname:port form. "
  1061. "The port is optional if you're using a standard "
  1062. "port for the database type.")
  1063. ui.prompt_input(page, "Database Server", site.db_host,
  1064. normalize_func=normalize_host_port,
  1065. save_obj=site, save_var="db_host")
  1066. def ask_database_login(self):
  1067. page = ui.page("What is the login and password for this database?",
  1068. is_visible_func=lambda: site.db_type != "sqlite3")
  1069. ui.text(page, "This must be a user that has creation and modification "
  1070. "rights on the database.")
  1071. ui.prompt_input(page, "Database Username", site.db_user,
  1072. save_obj=site, save_var="db_user")
  1073. ui.prompt_input(page, "Database Password", site.db_pass, password=True,
  1074. save_obj=site, save_var="db_pass")
  1075. def ask_cache_type(self):
  1076. page = ui.page("What cache mechanism should be used?")
  1077. ui.text(page, "memcached is strongly recommended. Use it unless "
  1078. "you have a good reason not to.")
  1079. ui.prompt_choice(page, "Cache Type",
  1080. [("memcached", Dependencies.get_support_memcached()),
  1081. "file"],
  1082. save_obj=site, save_var="cache_type")
  1083. def ask_cache_info(self):
  1084. # Appears only if using memcached.
  1085. page = ui.page("What memcached connection string should be used?",
  1086. is_visible_func=lambda: site.cache_type == "memcached")
  1087. ui.text(page, "This is generally in the format of "
  1088. "memcached://hostname:port/")
  1089. ui.prompt_input(page, "Memcache Server",
  1090. site.cache_info or "memcached://localhost:11211/",
  1091. save_obj=site, save_var="cache_info")
  1092. # Appears only if using file caching.
  1093. page = ui.page("Where should the temporary cache files be stored?",
  1094. is_visible_func=lambda: site.cache_type == "file")
  1095. ui.prompt_input(page, "Cache Directory",
  1096. site.cache_info or "/tmp/reviewboard_cache",
  1097. normalize_func=lambda value: "file://" + value,
  1098. save_obj=site, save_var="cache_info")
  1099. def ask_web_server_type(self):
  1100. page = ui.page("What web server will you be using?")
  1101. ui.prompt_choice(page, "Web Server", ["apache", "lighttpd"],
  1102. save_obj=site, save_var="web_server_type")
  1103. def ask_python_loader(self):
  1104. page = ui.page("What Python loader module will you be using?",
  1105. is_visible_func=lambda: site.web_server_type == "apache")
  1106. ui.text(page, "Based on our experiences, we recommend using "
  1107. "modpython with Review Board.")
  1108. ui.prompt_choice(page, "Python Loader", ["modpython", "fastcgi", "wsgi"],
  1109. save_obj=site, save_var="python_loader")
  1110. def ask_admin_user(self):
  1111. page = ui.page("Create an administrator account")
  1112. ui.text(page, "To configure Review Board, you'll need an "
  1113. "administrator account. It is advised to have one "
  1114. "administrator and then use that account to grant "
  1115. "administrator permissions to your personal user "
  1116. "account.")
  1117. ui.text(page, "If you plan to use NIS or LDAP, use an account name "
  1118. "other than your NIS/LDAP account so as to prevent "
  1119. "conflicts.")
  1120. ui.prompt_input(page, "Username", site.admin_user,
  1121. save_obj=site, save_var="admin_user")
  1122. ui.prompt_input(page, "Password", site.admin_password, password=True,
  1123. save_obj=site, save_var="admin_password")
  1124. ui.prompt_input(page, "E-Mail Address", site.admin_email,
  1125. save_obj=site, save_var="admin_email")
  1126. def show_install_status(self):
  1127. page = ui.page("Installing the site...", allow_back=False)
  1128. ui.step(page, "Building site directories",
  1129. site.rebuild_site_directory)
  1130. ui.step(page, "Building site configuration files",
  1131. site.generate_config_files)
  1132. ui.step(page, "Creating database",
  1133. site.sync_database)
  1134. ui.step(page, "Performing migrations",
  1135. site.migrate_database)
  1136. ui.step(page, "Creating administrator account",
  1137. site.create_admin_user)
  1138. ui.step(page, "Saving site settings",
  1139. self.save_settings)
  1140. def show_finished(self):
  1141. page = ui.page("The site has been installed", allow_back=False)
  1142. ui.text(page, "The site has been installed in %s" %
  1143. site.abs_install_dir)
  1144. ui.text(page, "Sample configuration files for web servers and "
  1145. "cron are available in the conf/ directory.")
  1146. ui.text(page, "You need to modify the ownership of the "
  1147. "following directories and their contents to be owned "
  1148. "by the web server:")
  1149. ui.itemized_list(page, None, [
  1150. os.path.join(site.abs_install_dir, 'htdocs', 'media', 'uploaded'),
  1151. os.path.join(site.abs_install_dir, 'data'),
  1152. ])
  1153. ui.text(page, "For more information, visit:")
  1154. ui.urllink(page, "%sadmin/sites/creating-sites/" % DOCS_BASE)
  1155. def save_settings(self):
  1156. """
  1157. Saves some settings in the database.
  1158. """
  1159. from django.contrib.sites.models import Site
  1160. from djblets.siteconfig.models import SiteConfiguration
  1161. cur_site = Site.objects.get_current()
  1162. cur_site.domain = site.domain_name
  1163. cur_site.save()
  1164. if site.m