merged
authorwenzelm
Tue, 05 Nov 2019 22:56:06 +0100
changeset 71257 9b531e611d66
parent 71243 9858f391ed2d (current diff)
parent 71256 6ca9e8377613 (diff)
child 71258 1d19e844fa4d
child 71259 295609359b58
merged
--- a/Admin/Phabricator/README	Tue Nov 05 21:07:15 2019 +0100
+++ b/Admin/Phabricator/README	Tue Nov 05 22:56:06 2019 +0100
@@ -54,11 +54,11 @@
   Port 222
 
   /etc/passwd:
-  phab-daemon:x:118:126::/home/phab-daemon:/bin/bash
+  phabricator:x:118:126::/home/phabricator:/bin/bash
   vcs:x:119:125::/home/vcs:/bin/bash
 
   /etc/group:
-  phab-daemon:x:126:
+  phabricator:x:126:
   vcs:x:125:
 
   $ cp ssh/ssh-hook /usr/local/bin/.
@@ -66,24 +66,24 @@
   $ cp ssh/sshd-phabricator.service /lib/systemd/system/.
   $ cp ssh/sudoers.d/phabricator /etc/sudoers.d/.
 
-  $ ./bin/config set phd.user phab-daemon
+  $ ./bin/config set phd.user phabricator
   $ ./bin/config set diffusion.ssh-user vcs
   $ ./bin/config set diffusion.ssh-port 22
 
+  $ systemctl enable sshd-phabricator
   $ systemctl start sshd-phabricator
-  $ systemctl enable sshd-phabricator
 
   Test on local machine:
   $ echo "{}" | ssh vcs@phabricator.sketis.net conduit conduit.ping
 
 - Repository Local Path:
     mkdir -p /var/www/phabricator/repo
-    chown phab-daemon:phab-daemon /var/www/phabricator/repo
+    chown phabricator:phabricator /var/www/phabricator/repo
 
 - PHP Daemon:
   $ cp phd/phd-phabricator.service /lib/systemd/system/.
+  $ systemctl enable phd-phabricator
   $ systemctl start phd-phabricator
-  $ systemctl enable phd-phabricator
 
 - Update:
   https://secure.phabricator.com/book/phabricator/article/upgrading
--- a/Admin/Phabricator/phd/phd-phabricator.service	Tue Nov 05 21:07:15 2019 +0100
+++ b/Admin/Phabricator/phd/phd-phabricator.service	Tue Nov 05 22:56:06 2019 +0100
@@ -4,8 +4,8 @@
 
 [Service]
 Type=oneshot
-User=phab-daemon
-Group=phab-daemon
+User=phabricator
+Group=phabricator
 Environment=PATH=/sbin:/usr/sbin:/usr/local/sbin:/usr/local/bin:/usr/bin:/bin
 ExecStart=/var/www/phabricator/phabricator/bin/phd start
 ExecStop=/var/www/phabricator/phabricator/bin/phd stop
--- a/Admin/Phabricator/ssh/sudoers.d/phabricator	Tue Nov 05 21:07:15 2019 +0100
+++ b/Admin/Phabricator/ssh/sudoers.d/phabricator	Tue Nov 05 22:56:06 2019 +0100
@@ -1,2 +1,2 @@
-www-data ALL=(phab-daemon) SETENV: NOPASSWD: /usr/bin/git, /usr/bin/hg, /usr/bin/ssh, /usr/bin/id
-vcs ALL=(phab-daemon) SETENV: NOPASSWD: /usr/bin/git, /usr/bin/git-upload-pack, /usr/bin/git-receive-pack, /usr/bin/hg, /usr/bin/svnserve, /usr/bin/ssh, /usr/bin/id
+www-data ALL=(phabricator) SETENV: NOPASSWD: /usr/bin/git, /usr/bin/hg, /usr/bin/ssh, /usr/bin/id
+vcs ALL=(phabricator) SETENV: NOPASSWD: /usr/bin/git, /usr/bin/git-upload-pack, /usr/bin/git-receive-pack, /usr/bin/hg, /usr/bin/svnserve, /usr/bin/ssh, /usr/bin/id
--- a/etc/options	Tue Nov 05 21:07:15 2019 +0100
+++ b/etc/options	Tue Nov 05 22:56:06 2019 +0100
@@ -345,8 +345,6 @@
 
 section "Phabricator server"
 
-option phabricator_user : string = "phabricator"
-
 option phabricator_www_user : string = "www-data"
 option phabricator_www_root : string = "/var/www"
 
--- a/src/Pure/System/linux.scala	Tue Nov 05 21:07:15 2019 +0100
+++ b/src/Pure/System/linux.scala	Tue Nov 05 22:56:06 2019 +0100
@@ -63,4 +63,73 @@
 
   def package_install(packages: List[String], progress: Progress = No_Progress): Unit =
     progress.bash("apt-get install -y -- " + Bash.strings(packages), echo = true).check
+
+
+  /* users */
+
+  def user_exists(name: String): Boolean =
+    Isabelle_System.bash("id " + Bash.string(name)).ok
+
+  def user_entry(name: String, field: Int): String =
+  {
+    val result = Isabelle_System.bash("getent passwd " + Bash.string(name)).check
+    val fields = space_explode(':', result.out)
+
+    if (1 <= field && field <= fields.length) fields(field - 1)
+    else error("No passwd field " + field + " for user " + quote(name))
+  }
+
+  def user_description(name: String): String = user_entry(name, 5).takeWhile(_ != ',')
+
+  def user_home(name: String): String = user_entry(name, 6)
+
+  def user_add(name: String,
+    description: String = "",
+    system: Boolean = false,
+    ssh_setup: Boolean = false)
+  {
+    require(!description.contains(','))
+
+    if (user_exists(name)) error("User already exists: " + quote(name))
+
+    Isabelle_System.bash(
+      "adduser --quiet --disabled-password --gecos " + Bash.string(description) +
+        (if (system) " --system --group --shell /bin/bash " else "") +
+        " " + Bash.string(name)).check
+
+    if (ssh_setup) {
+      val id_rsa = user_home(name) + "/.ssh/id_rsa"
+      Isabelle_System.bash("""
+if [ ! -f """ + Bash.string(id_rsa) + """ ]
+then
+  yes '\n' | sudo -i -u """ + Bash.string(name) +
+    """ ssh-keygen -q -f """ + Bash.string(id_rsa) + """
+fi
+      """).check
+    }
+  }
+
+
+  /* system services */
+
+  def service_start(name: String): Unit =
+    Isabelle_System.bash("systemctl start " + Bash.string(name)).check
+
+  def service_stop(name: String): Unit =
+    Isabelle_System.bash("systemctl stop " + Bash.string(name)).check
+
+  def service_restart(name: String): Unit =
+    Isabelle_System.bash("systemctl restart " + Bash.string(name)).check
+
+  def service_install(name: String, spec: String)
+  {
+    val service_file = Path.explode("/lib/systemd/system") + Path.basic(name).ext("service")
+    File.write(service_file, spec)
+
+    Isabelle_System.bash("""
+      set -e
+      chmod 0644 """ + File.bash_path(service_file) + """
+      systemctl enable """ + Bash.string(name) + """
+      systemctl start """ + Bash.string(name)).check
+  }
 }
--- a/src/Pure/Tools/phabricator.scala	Tue Nov 05 21:07:15 2019 +0100
+++ b/src/Pure/Tools/phabricator.scala	Tue Nov 05 22:56:06 2019 +0100
@@ -16,15 +16,7 @@
 {
   /** defaults **/
 
-  val default_name = "vcs"
-
-  def default_prefix(name: String): String = "phabricator-" + name
-
-  def default_root(options: Options, name: String): Path =
-    Path.explode(options.string("phabricator_www_root")) + Path.basic(default_prefix(name))
-
-  def default_repo(options: Options, name: String): Path =
-    default_root(options, name) + Path.basic("repo")
+  /* required packages */
 
   val packages: List[String] =
     Build_Docker.packages :::
@@ -33,17 +25,44 @@
       "git", "mysql-server", "apache2", "libapache2-mod-php", "php", "php-mysql",
       "php-gd", "php-curl", "php-apcu", "php-cli", "php-json", "php-mbstring",
       // more packages
-      "php-zip", "python-pygments")
+      "php-zip", "python-pygments", "ssh")
+
+
+  /* global system resources */
+
+  val daemon_user = "phabricator"
+
+  val ssh_standard = 22
+  val ssh_alternative1 = 222
+  val ssh_alternative2 = 2222
+
+
+  /* installation parameters */
+
+  val default_name = "vcs"
+
+  def phabricator_name(name: String = "", ext: String = ""): String =
+    "phabricator" + (if (name.isEmpty) "" else "-" + name) + (if (ext.isEmpty) "" else "." + ext)
+
+  def isabelle_phabricator_name(name: String = "", ext: String = ""): String =
+    "isabelle-" + phabricator_name(name = name, ext = ext)
+
+  def default_root(options: Options, name: String): Path =
+    Path.explode(options.string("phabricator_www_root")) +
+    Path.basic(phabricator_name(name = name))
+
+  def default_repo(options: Options, name: String): Path =
+    default_root(options, name) + Path.basic("repo")
 
 
 
   /** global configuration **/
 
-  val global_config = Path.explode("/etc/isabelle-phabricator.conf")
+  val global_config = Path.explode("/etc/" + isabelle_phabricator_name(ext = "conf"))
 
   sealed case class Config(name: String, root: Path)
   {
-    def home: Path = root + Path.explode("phabricator")
+    def home: Path = root + Path.explode(phabricator_name())
 
     def execute(command: String): Process_Result =
       Isabelle_System.bash("./bin/" + command, cwd = home.file).check
@@ -77,28 +96,52 @@
 
   /** setup **/
 
+  def user_setup(name: String, description: String, ssh_setup: Boolean = false)
+  {
+    if (!Linux.user_exists(name)) {
+      Linux.user_add(name, description = description, system = true, ssh_setup = ssh_setup)
+    }
+    else if (Linux.user_description(name) != description) {
+      error("User " + quote(name) + " already exists --" +
+        " for Phabricator it should have the description:\n  " + quote(description))
+    }
+  }
+
   def phabricator_setup(
     options: Options,
     name: String = default_name,
-    prefix: String = "",
     root: String = "",
     repo: String = "",
+    package_update: Boolean = false,
     progress: Progress = No_Progress)
   {
     /* system environment */
 
     Linux.check_system_root()
 
-    Linux.package_update(progress = progress)
-    Linux.check_reboot_required()
+    if (package_update) {
+      Linux.package_update(progress = progress)
+      Linux.check_reboot_required()
+    }
 
     Linux.package_install(packages, progress = progress)
     Linux.check_reboot_required()
 
 
+    /* users */
+
+    if (name == daemon_user) {
+      error("Clash of installation name with daemon user " + quote(daemon_user))
+    }
+
+    user_setup(daemon_user, "Phabricator Daemon User", ssh_setup = true)
+    user_setup(name, "Phabricator SSH User")
+
+    val www_user = options.string("phabricator_www_user")
+
+
     /* basic installation */
 
-    val prefix_name = proper_string(prefix) getOrElse default_prefix(name)
     val root_path = if (root.nonEmpty) Path.explode(root) else default_root(options, name)
     val repo_path = if (repo.nonEmpty) Path.explode(repo) else default_repo(options, name)
 
@@ -115,7 +158,7 @@
     progress.bash(cwd = root_path.file, echo = true,
       script = """
         set -e
-        chown """ + Bash.string(options.string("phabricator_www_user")) + """ .
+        chown """ + Bash.string(www_user) + ":" + Bash.string(www_user) + """ .
         chmod 755 .
 
         git clone https://github.com/phacility/libphutil.git
@@ -126,11 +169,39 @@
     val config = Config(name, root_path)
     write_config(configs ::: List(config))
 
+    config.execute("config set pygments.enabled true")
+
+
+    /* local repository directory */
+
+    if (!Isabelle_System.bash("mkdir -p " + File.bash_path(repo_path)).ok) {
+      error("Failed to create local repository directory " + repo_path)
+    }
+
+    Isabelle_System.bash(cwd = repo_path.file,
+      script = """
+        set -e
+        chown -R """ + Bash.string(daemon_user) + ":" + Bash.string(daemon_user) + """ .
+        chmod 755 .
+      """).check
+
+    config.execute("config set repository.default-local-path " + File.bash_path(repo_path))
+
 
     /* MySQL setup */
 
     progress.echo("MySQL setup...")
 
+    File.write(Path.explode("/etc/mysql/mysql.conf.d/" + phabricator_name(ext = "cnf")),
+"""[mysqld]
+max_allowed_packet = 32M
+innodb_buffer_pool_size = 1600M
+local_infile = 0
+""")
+
+    Linux.service_restart("mysql")
+
+
     def mysql_conf(R: Regex): Option[String] =
       split_lines(File.read(Path.explode(options.string("phabricator_mysql_config")))).
         collectFirst({ case R(a) => a })
@@ -144,11 +215,47 @@
     }
 
     config.execute("config set storage.default-namespace " +
-      Bash.string(prefix_name.replace("-", "_")))
+      Bash.string(phabricator_name(name = name).replace("-", "_")))
+
+    config.execute("config set storage.mysql-engine.max-size 8388608")
 
     config.execute("storage upgrade --force")
 
 
+    /* SSH hosting */
+
+    progress.echo("SSH hosting setup...")
+
+    val ssh_port = ssh_alternative2
+
+    config.execute("config set diffusion.ssh-user " + Bash.string(name))
+    config.execute("config set diffusion.ssh-port " + ssh_port)
+
+    val sudoers_file = Path.explode("/etc/sudoers.d") + Path.basic(isabelle_phabricator_name())
+    File.write(sudoers_file,
+      www_user + " ALL=(" + daemon_user + ") SETENV: NOPASSWD: /usr/bin/git, /usr/bin/hg, /usr/bin/ssh, /usr/bin/id\n" +
+      name + " ALL=(" + daemon_user + ") SETENV: NOPASSWD: /usr/bin/git, /usr/bin/git-upload-pack, /usr/bin/git-receive-pack, /usr/bin/hg, /usr/bin/svnserve, /usr/bin/ssh, /usr/bin/id\n")
+
+    Isabelle_System.bash("chmod 0440 " + File.bash_path(sudoers_file)).check
+
+
+    /* PHP setup */
+
+    val php_version =
+      Isabelle_System.bash("""php --run 'echo PHP_MAJOR_VERSION . "." . PHP_MINOR_VERSION;'""")
+        .check.out
+
+    val php_conf =
+      Path.explode("/etc/php") + Path.basic(php_version) +  // educated guess
+        Path.explode("apache2/conf.d") +
+        Path.basic(isabelle_phabricator_name(ext = "ini"))
+
+    File.write(php_conf,
+      "post_max_size = 32M\n" +
+      "opcache.validate_timestamps = 0\n" +
+      "memory_limit = 512M\n")
+
+
     /* Apache setup */
 
     progress.echo("Apache setup...")
@@ -158,10 +265,12 @@
 
     if (!apache_sites.is_dir) error("Bad Apache sites directory " + apache_sites)
 
-    File.write(apache_sites + Path.basic(prefix_name + ".conf"),
+    val server_name = phabricator_name(name = name, ext = "lvh.me")  // alias for "localhost" for testing
+    val server_url = "http://" + server_name
+
+    File.write(apache_sites + Path.basic(isabelle_phabricator_name(name = name, ext = "conf")),
 """<VirtualHost *:80>
-    #ServerName: "lvh.me" is an alias for "localhost" for testing
-    ServerName """ + prefix_name + """.lvh.me
+    ServerName """ + server_name + """
     ServerAdmin webmaster@localhost
     DocumentRoot """ + config.home.implode + """/webroot
 
@@ -173,14 +282,42 @@
 # vim: syntax=apache ts=4 sw=4 sts=4 sr noet
 """)
 
-    Isabelle_System.bash("""
+    Isabelle_System.bash( """
       set -e
       a2enmod rewrite
-      a2ensite """ + Bash.string(prefix_name) + """
-      systemctl restart apache2
-""").check
+      a2ensite """ + Bash.string(isabelle_phabricator_name(name = name))).check
+
+    config.execute("config set phabricator.base-uri " + Bash.string(server_url))
+
+    Linux.service_restart("apache2")
+
+
+    /* PHP daemon */
+
+    progress.echo("PHP daemon setup...")
+
+    config.execute("config set phd.user " + Bash.string(daemon_user))
 
-    progress.echo("\nDONE\nWeb configuration via http://" + prefix_name + ".lvh.me")
+    Linux.service_install(isabelle_phabricator_name(name = name),
+"""[Unit]
+Description=PHP daemon for Isabelle/Phabricator """ + quote(name) + """
+After=syslog.target network.target apache2.service mysql.service
+
+[Service]
+Type=oneshot
+User=""" + daemon_user + """
+Group=""" + daemon_user + """
+Environment=PATH=/sbin:/usr/sbin:/usr/local/sbin:/usr/local/bin:/usr/bin:/bin
+ExecStart=""" + config.home.implode + """/bin/phd start
+ExecStop=""" + config.home.implode + """/bin/phd stop
+RemainAfterExit=yes
+
+[Install]
+WantedBy=multi-user.target
+""")
+
+
+    progress.echo("\nDONE\nWeb configuration via " + server_url)
   }
 
 
@@ -189,10 +326,10 @@
   val isabelle_tool1 =
     Isabelle_Tool("phabricator_setup", "setup Phabricator server on Ubuntu Linux", args =>
     {
+      var repo = ""
+      var package_update = false
       var options = Options.init()
-      var prefix = ""
       var root = ""
-      var repo = ""
 
       val getopts =
         Getopts("""
@@ -200,8 +337,8 @@
 
   Options are:
     -R DIR       repository directory (default: """ + default_repo(options, "NAME") + """)
+    -U           full update of system packages before installation
     -o OPTION    override Isabelle system OPTION (via NAME=VAL or NAME)
-    -p PREFIX    prefix for derived names (default: """ + default_prefix("NAME") + """)
     -r DIR       installation root directory (default: """ + default_root(options, "NAME") + """)
 
   Install Phabricator as Ubuntu LAMP application (Linux, Apache, MySQL, PHP).
@@ -213,8 +350,8 @@
   a regular Unix user and used for public SSH access.
 """,
           "R:" -> (arg => repo = arg),
+          "U" -> (_ => package_update = true),
           "o:" -> (arg => options = options + arg),
-          "p:" -> (arg => prefix = arg),
           "r:" -> (arg => root = arg))
 
       val more_args = getopts(args)
@@ -228,8 +365,8 @@
 
       val progress = new Console_Progress
 
-      phabricator_setup(options, name, prefix = prefix, root = root, repo = repo,
-        progress = progress)
+      phabricator_setup(options, name, root = root, repo = repo,
+        package_update = package_update, progress = progress)
     })