70967
|
1 |
/* Title: Pure/Tools/phabricator.scala
|
|
2 |
Author: Makarius
|
|
3 |
|
|
4 |
Support for Phabricator server. See also:
|
|
5 |
- https://www.phacility.com/phabricator
|
|
6 |
- https://secure.phabricator.com/book/phabricator
|
|
7 |
*/
|
|
8 |
|
|
9 |
package isabelle
|
|
10 |
|
|
11 |
|
70969
|
12 |
import scala.util.matching.Regex
|
|
13 |
|
|
14 |
|
70967
|
15 |
object Phabricator
|
|
16 |
{
|
|
17 |
/** defaults **/
|
|
18 |
|
71049
|
19 |
/* required packages */
|
|
20 |
|
|
21 |
val packages: List[String] =
|
|
22 |
Build_Docker.packages :::
|
|
23 |
List(
|
|
24 |
// https://secure.phabricator.com/source/phabricator/browse/master/scripts/install/install_ubuntu.sh 15e6e2adea61
|
|
25 |
"git", "mysql-server", "apache2", "libapache2-mod-php", "php", "php-mysql",
|
|
26 |
"php-gd", "php-curl", "php-apcu", "php-cli", "php-json", "php-mbstring",
|
|
27 |
// more packages
|
|
28 |
"php-zip", "python-pygments", "ssh")
|
|
29 |
|
|
30 |
|
|
31 |
/* global system resources */
|
|
32 |
|
|
33 |
val daemon_user = "phabricator"
|
|
34 |
|
|
35 |
val ssh_standard = 22
|
|
36 |
val ssh_alternative1 = 222
|
|
37 |
val ssh_alternative2 = 2222
|
|
38 |
|
|
39 |
|
|
40 |
/* installation parameters */
|
|
41 |
|
70967
|
42 |
val default_name = "vcs"
|
|
43 |
|
70968
|
44 |
def default_prefix(name: String): String = "phabricator-" + name
|
70967
|
45 |
|
|
46 |
def default_root(options: Options, name: String): Path =
|
|
47 |
Path.explode(options.string("phabricator_www_root")) + Path.basic(default_prefix(name))
|
|
48 |
|
|
49 |
def default_repo(options: Options, name: String): Path =
|
|
50 |
default_root(options, name) + Path.basic("repo")
|
|
51 |
|
|
52 |
|
|
53 |
|
|
54 |
/** global configuration **/
|
|
55 |
|
|
56 |
val global_config = Path.explode("/etc/isabelle-phabricator.conf")
|
|
57 |
|
|
58 |
sealed case class Config(name: String, root: Path)
|
70968
|
59 |
{
|
70969
|
60 |
def home: Path = root + Path.explode("phabricator")
|
|
61 |
|
|
62 |
def execute(command: String): Process_Result =
|
|
63 |
Isabelle_System.bash("./bin/" + command, cwd = home.file).check
|
70968
|
64 |
}
|
70967
|
65 |
|
|
66 |
def read_config(): List[Config] =
|
|
67 |
{
|
|
68 |
if (global_config.is_file) {
|
|
69 |
for (entry <- Library.trim_split_lines(File.read(global_config)) if entry.nonEmpty)
|
|
70 |
yield {
|
|
71 |
space_explode(':', entry) match {
|
|
72 |
case List(name, root) => Config(name, Path.explode(root))
|
|
73 |
case _ => error("Malformed config file " + global_config + "\nentry " + quote(entry))
|
|
74 |
}
|
|
75 |
}
|
|
76 |
}
|
|
77 |
else Nil
|
|
78 |
}
|
|
79 |
|
|
80 |
def write_config(configs: List[Config])
|
|
81 |
{
|
|
82 |
File.write(global_config,
|
|
83 |
configs.map(config => config.name + ":" + config.root.implode).mkString("", "\n", "\n"))
|
|
84 |
}
|
|
85 |
|
|
86 |
def get_config(name: String): Config =
|
|
87 |
read_config().find(config => config.name == name) getOrElse
|
|
88 |
error("Bad Isabelle/Phabricator installation " + quote(name))
|
|
89 |
|
|
90 |
|
|
91 |
|
|
92 |
/** setup **/
|
|
93 |
|
71049
|
94 |
def user_setup(name: String, description: String, ssh_setup: Boolean = false)
|
|
95 |
{
|
|
96 |
if (!Linux.user_exists(name)) {
|
|
97 |
Linux.user_add(name, description = description, ssh_setup = ssh_setup)
|
|
98 |
}
|
|
99 |
else if (Linux.user_description(name) != description) {
|
|
100 |
error("User " + quote(name) + " already exists --" +
|
|
101 |
" for Phabricator it should have the description:\n " + quote(description))
|
|
102 |
}
|
|
103 |
}
|
|
104 |
|
70967
|
105 |
def phabricator_setup(
|
|
106 |
options: Options,
|
|
107 |
name: String = default_name,
|
|
108 |
prefix: String = "",
|
|
109 |
root: String = "",
|
|
110 |
repo: String = "",
|
71047
|
111 |
package_update: Boolean = false,
|
70967
|
112 |
progress: Progress = No_Progress)
|
|
113 |
{
|
|
114 |
/* system environment */
|
|
115 |
|
|
116 |
Linux.check_system_root()
|
|
117 |
|
71047
|
118 |
if (package_update) {
|
|
119 |
Linux.package_update(progress = progress)
|
|
120 |
Linux.check_reboot_required()
|
|
121 |
}
|
70967
|
122 |
|
|
123 |
Linux.package_install(packages, progress = progress)
|
|
124 |
Linux.check_reboot_required()
|
|
125 |
|
|
126 |
|
71049
|
127 |
/* users */
|
|
128 |
|
|
129 |
if (name == daemon_user) {
|
|
130 |
error("Clash of installation name with daemon user " + quote(daemon_user))
|
|
131 |
}
|
|
132 |
|
|
133 |
user_setup(daemon_user, "Phabricator Daemon User", ssh_setup = true)
|
|
134 |
user_setup(name, "Phabricator SSH User")
|
|
135 |
|
|
136 |
val www_user = options.string("phabricator_www_user")
|
|
137 |
|
|
138 |
|
70967
|
139 |
/* basic installation */
|
|
140 |
|
|
141 |
val prefix_name = proper_string(prefix) getOrElse default_prefix(name)
|
|
142 |
val root_path = if (root.nonEmpty) Path.explode(root) else default_root(options, name)
|
|
143 |
val repo_path = if (repo.nonEmpty) Path.explode(repo) else default_repo(options, name)
|
|
144 |
|
|
145 |
val configs = read_config()
|
|
146 |
|
|
147 |
for (config <- configs if config.name == name) {
|
|
148 |
error("Duplicate Phabricator installation " + quote(name) + " in " + config.root)
|
|
149 |
}
|
|
150 |
|
|
151 |
if (!Isabelle_System.bash("mkdir -p " + File.bash_path(root_path)).ok) {
|
|
152 |
error("Failed to create root directory " + root_path)
|
|
153 |
}
|
|
154 |
|
|
155 |
progress.bash(cwd = root_path.file, echo = true,
|
|
156 |
script = """
|
|
157 |
set -e
|
71049
|
158 |
chown """ + Bash.string(www_user) + """ .
|
70967
|
159 |
chmod 755 .
|
|
160 |
|
|
161 |
git clone https://github.com/phacility/libphutil.git
|
|
162 |
git clone https://github.com/phacility/arcanist.git
|
|
163 |
git clone https://github.com/phacility/phabricator.git
|
|
164 |
""").check
|
|
165 |
|
|
166 |
val config = Config(name, root_path)
|
|
167 |
write_config(configs ::: List(config))
|
70968
|
168 |
|
|
169 |
|
70969
|
170 |
/* MySQL setup */
|
|
171 |
|
|
172 |
progress.echo("MySQL setup...")
|
|
173 |
|
|
174 |
def mysql_conf(R: Regex): Option[String] =
|
|
175 |
split_lines(File.read(Path.explode(options.string("phabricator_mysql_config")))).
|
|
176 |
collectFirst({ case R(a) => a })
|
|
177 |
|
|
178 |
for (user <- mysql_conf("""^user\s*=\s*(\S*)\s*$""".r)) {
|
|
179 |
config.execute("config set mysql.user " + Bash.string(user))
|
|
180 |
}
|
|
181 |
|
|
182 |
for (pass <- mysql_conf("""^password\s*=\s*(\S*)\s*$""".r)) {
|
|
183 |
config.execute("config set mysql.pass " + Bash.string(pass))
|
|
184 |
}
|
|
185 |
|
|
186 |
config.execute("config set storage.default-namespace " +
|
|
187 |
Bash.string(prefix_name.replace("-", "_")))
|
|
188 |
|
|
189 |
config.execute("storage upgrade --force")
|
|
190 |
|
|
191 |
|
71049
|
192 |
/* PHP daemon */
|
|
193 |
|
|
194 |
progress.echo("PHP daemon setup...")
|
|
195 |
|
|
196 |
config.execute("config set phd.user " + Bash.string(daemon_user))
|
|
197 |
|
|
198 |
Linux.service_install("phd-" + prefix_name,
|
|
199 |
"""[Unit]
|
|
200 |
Description=PHP daemon (Phabricator """ + quote(name) + """)
|
|
201 |
After=syslog.target network.target apache2.service mysql.service
|
|
202 |
|
|
203 |
[Service]
|
|
204 |
Type=oneshot
|
|
205 |
User=""" + daemon_user + """
|
|
206 |
Group=""" + daemon_user + """
|
|
207 |
Environment=PATH=/sbin:/usr/sbin:/usr/local/sbin:/usr/local/bin:/usr/bin:/bin
|
|
208 |
ExecStart=""" + root_path.expand.implode + """/phabricator/bin/phd start
|
|
209 |
ExecStop=""" + root_path.expand.implode + """/phabricator/bin/phd stop
|
|
210 |
RemainAfterExit=yes
|
|
211 |
|
|
212 |
[Install]
|
|
213 |
WantedBy=multi-user.target
|
|
214 |
""")
|
|
215 |
|
|
216 |
|
|
217 |
/* SSH hosting */
|
|
218 |
|
|
219 |
progress.echo("SSH hosting setup...")
|
|
220 |
|
|
221 |
val ssh_port = ssh_alternative2
|
|
222 |
|
|
223 |
config.execute("config set diffusion.ssh-user " + Bash.string(name))
|
|
224 |
config.execute("config set diffusion.ssh-port " + ssh_port)
|
|
225 |
|
|
226 |
val sudoers_file = Path.explode("/etc/sudoers.d") + Path.basic(prefix_name)
|
|
227 |
File.write(sudoers_file,
|
|
228 |
www_user + " ALL=(" + daemon_user + ") SETENV: NOPASSWD: /usr/bin/git, /usr/bin/hg, /usr/bin/ssh, /usr/bin/id\n" +
|
|
229 |
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")
|
|
230 |
|
|
231 |
Isabelle_System.bash("chmod 0440 " + File.bash_path(sudoers_file)).check
|
|
232 |
|
|
233 |
|
70968
|
234 |
/* Apache setup */
|
|
235 |
|
|
236 |
progress.echo("Apache setup...")
|
|
237 |
|
|
238 |
val apache_root = Path.explode(options.string("phabricator_apache_root"))
|
|
239 |
val apache_sites = apache_root + Path.explode("sites-available")
|
|
240 |
|
|
241 |
if (!apache_sites.is_dir) error("Bad Apache sites directory " + apache_sites)
|
|
242 |
|
|
243 |
File.write(apache_sites + Path.basic(prefix_name + ".conf"),
|
|
244 |
"""<VirtualHost *:80>
|
|
245 |
#ServerName: "lvh.me" is an alias for "localhost" for testing
|
|
246 |
ServerName """ + prefix_name + """.lvh.me
|
|
247 |
ServerAdmin webmaster@localhost
|
70969
|
248 |
DocumentRoot """ + config.home.implode + """/webroot
|
70968
|
249 |
|
|
250 |
ErrorLog ${APACHE_LOG_DIR}/error.log
|
|
251 |
RewriteEngine on
|
|
252 |
RewriteRule ^(.*)$ /index.php?__path__=$1 [B,L,QSA]
|
|
253 |
</VirtualHost>
|
|
254 |
|
|
255 |
# vim: syntax=apache ts=4 sw=4 sts=4 sr noet
|
|
256 |
""")
|
|
257 |
|
|
258 |
Isabelle_System.bash("""
|
|
259 |
set -e
|
|
260 |
a2enmod rewrite
|
|
261 |
a2ensite """ + Bash.string(prefix_name) + """
|
|
262 |
systemctl restart apache2
|
|
263 |
""").check
|
|
264 |
|
|
265 |
progress.echo("\nDONE\nWeb configuration via http://" + prefix_name + ".lvh.me")
|
70967
|
266 |
}
|
|
267 |
|
|
268 |
|
|
269 |
/* Isabelle tool wrapper */
|
|
270 |
|
|
271 |
val isabelle_tool1 =
|
|
272 |
Isabelle_Tool("phabricator_setup", "setup Phabricator server on Ubuntu Linux", args =>
|
|
273 |
{
|
71047
|
274 |
var repo = ""
|
|
275 |
var package_update = false
|
70967
|
276 |
var options = Options.init()
|
|
277 |
var prefix = ""
|
|
278 |
var root = ""
|
|
279 |
|
|
280 |
val getopts =
|
|
281 |
Getopts("""
|
|
282 |
Usage: isabelle phabricator_setup [OPTIONS] [NAME]
|
|
283 |
|
|
284 |
Options are:
|
|
285 |
-R DIR repository directory (default: """ + default_repo(options, "NAME") + """)
|
71047
|
286 |
-U full update of system packages before installation
|
70967
|
287 |
-o OPTION override Isabelle system OPTION (via NAME=VAL or NAME)
|
|
288 |
-p PREFIX prefix for derived names (default: """ + default_prefix("NAME") + """)
|
|
289 |
-r DIR installation root directory (default: """ + default_root(options, "NAME") + """)
|
|
290 |
|
|
291 |
Install Phabricator as Ubuntu LAMP application (Linux, Apache, MySQL, PHP).
|
|
292 |
|
|
293 |
Slogan: "Discuss. Plan. Code. Review. Test.
|
|
294 |
Every application your project needs, all in one tool."
|
|
295 |
|
|
296 |
The installation NAME (default: """ + quote(default_name) + """) is mapped to
|
|
297 |
a regular Unix user and used for public SSH access.
|
|
298 |
""",
|
|
299 |
"R:" -> (arg => repo = arg),
|
71047
|
300 |
"U" -> (_ => package_update = true),
|
70967
|
301 |
"o:" -> (arg => options = options + arg),
|
|
302 |
"p:" -> (arg => prefix = arg),
|
|
303 |
"r:" -> (arg => root = arg))
|
|
304 |
|
|
305 |
val more_args = getopts(args)
|
|
306 |
|
|
307 |
val name =
|
|
308 |
more_args match {
|
|
309 |
case Nil => default_name
|
|
310 |
case List(name) => name
|
|
311 |
case _ => getopts.usage()
|
|
312 |
}
|
|
313 |
|
|
314 |
val progress = new Console_Progress
|
|
315 |
|
|
316 |
phabricator_setup(options, name, prefix = prefix, root = root, repo = repo,
|
71047
|
317 |
package_update = package_update, progress = progress)
|
70967
|
318 |
})
|
|
319 |
|
|
320 |
|
|
321 |
|
|
322 |
/** update **/
|
|
323 |
|
|
324 |
def phabricator_update(name: String, progress: Progress = No_Progress)
|
|
325 |
{
|
|
326 |
Linux.check_system_root()
|
|
327 |
|
|
328 |
???
|
|
329 |
}
|
|
330 |
|
|
331 |
|
|
332 |
/* Isabelle tool wrapper */
|
|
333 |
|
|
334 |
val isabelle_tool2 =
|
|
335 |
Isabelle_Tool("phabricator_update", "update Phabricator server installation", args =>
|
|
336 |
{
|
|
337 |
val getopts =
|
|
338 |
Getopts("""
|
|
339 |
Usage: isabelle phabricator_update [NAME]
|
|
340 |
|
|
341 |
Update Phabricator installation, with lookup of NAME (default + """ + quote(default_name) + """)
|
|
342 |
in """ + global_config + "\n")
|
|
343 |
|
|
344 |
val more_args = getopts(args)
|
|
345 |
val name =
|
|
346 |
more_args match {
|
|
347 |
case Nil => default_name
|
|
348 |
case List(name) => name
|
|
349 |
case _ => getopts.usage()
|
|
350 |
}
|
|
351 |
|
|
352 |
val progress = new Console_Progress
|
|
353 |
|
|
354 |
phabricator_update(name, progress = progress)
|
|
355 |
})
|
|
356 |
}
|