init: Create a command to setup a new Gerrit installation

The init command uses an interactive prompting process to help the
user get a basic website configured.

The --import-projects option will automatically search for and import
any Git repositories which are discovered within gerrit.basePath.

Bug: issue 323
Bug: issue 330
Change-Id: I3d6e8f9f5fea8bfc78f6dfb1fc8f284bebfba670
Signed-off-by: Shawn O. Pearce <sop@google.com>
This commit is contained in:
Shawn O. Pearce
2009-11-17 14:52:07 -08:00
parent da85ff6cb4
commit 2fe738b4ad
10 changed files with 1289 additions and 26 deletions

View File

@@ -9,8 +9,8 @@ through the Java command line. For example:
[[programs]]Programs [[programs]]Programs
-------------------- --------------------
CreateSchema:: link:pgm-init.html[init]::
Initialize a new database schema. Initialize a new Gerrit server installation
link:pgm-daemon.html[daemon]:: link:pgm-daemon.html[daemon]::
Gerrit HTTP, SSH network server. Gerrit HTTP, SSH network server.
@@ -18,6 +18,9 @@ link:pgm-daemon.html[daemon]::
version:: version::
Display the release version of Gerrit Code Review. Display the release version of Gerrit Code Review.
CreateSchema::
Initialize a new database schema.
GERRIT GERRIT
------ ------
Part of link:index.html[Gerrit Code Review] Part of link:index.html[Gerrit Code Review]

View File

@@ -0,0 +1,51 @@
init
====
NAME
----
init - Initialize a new Gerrit server installation
SYNOPSIS
--------
[verse]
'java' -jar gerrit.war 'init'
-d <SITE_PATH>
[\--batch]
[\--import-projects]
DESCRIPTION
-----------
Creates a new Gerrit server installation, interactively prompting
for some basic setup prior to writing default configuration files
into a newly created `$site_path`.
If run an an existing `$site_path`, init will upgrade some resources
as necessary. This can be useful to import newly created projects.
OPTIONS
-------
\--batch::
Run in batch mode, skipping interactive prompts. Reasonable
configuration defaults are chosen based on the whims of
the Gerrit developers.
\--import-projects::
Recursively search
link:config-gerrit.html#gerrit.basePath[gerrit.basePath]
for any Git repositories not yet registered as a project,
and initializes a new project for them.
-d::
\--site-path::
Location of the gerrit.config file, and all other per-site
configuration data, supporting libaries and log files.
CONTEXT
-------
This command can only be run on a server which has direct
connectivity to the metadata database, and local access to the
managed Git repositories.
GERRIT
------
Part of link:index.html[Gerrit Code Review]

View File

@@ -54,6 +54,7 @@ public final class GerritLauncher {
System.err.println("usage: java -jar " + jar + " command [ARG ...]"); System.err.println("usage: java -jar " + jar + " command [ARG ...]");
System.err.println(); System.err.println();
System.err.println("The most commonly used commands are:"); System.err.println("The most commonly used commands are:");
System.err.println(" init Initialize a Gerrit installation");
System.err.println(" daemon Run the Gerrit network daemons"); System.err.println(" daemon Run the Gerrit network daemons");
System.err.println(" version Display the build version number"); System.err.println(" version Display the build version number");
System.err.println(); System.err.println();

View File

@@ -72,7 +72,12 @@ public abstract class AbstractProgram {
ProxyUtil.configureHttpProxy(); ProxyUtil.configureHttpProxy();
return run(); return run();
} catch (Die err) { } catch (Die err) {
System.err.println("fatal: " + err.getMessage()); final Throwable cause = err.getCause();
final String diemsg = err.getMessage();
if (cause != null && !cause.getMessage().equals(diemsg)) {
System.err.println("fatal: " + cause.getMessage());
}
System.err.println("fatal: " + diemsg);
return 128; return 128;
} }
} }

View File

@@ -0,0 +1,238 @@
// Copyright (C) 2009 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package com.google.gerrit.pgm;
import static org.eclipse.jgit.util.StringUtils.equalsIgnoreCase;
import java.io.Console;
import java.lang.reflect.InvocationTargetException;
/** Console based interaction with the invoking user. */
public abstract class ConsoleUI {
/** Get a UI instance, assuming interactive mode. */
public static ConsoleUI getInstance() {
return getInstance(false);
}
/** Get a UI instance, possibly forcing batch mode. */
public static ConsoleUI getInstance(final boolean batchMode) {
Console console = batchMode ? null : System.console();
return console != null ? new Interactive(console) : new Batch();
}
/** Constructs an exception indicating the user aborted the operation. */
protected static Die abort() {
return new Die("aborted by user");
}
/** Obtain all values from an enumeration. */
@SuppressWarnings("unchecked")
protected static <T extends Enum<?>> T[] all(final T value) {
try {
return (T[]) value.getClass().getMethod("values").invoke(null);
} catch (IllegalArgumentException e) {
throw new IllegalArgumentException("Cannot obtain enumeration values", e);
} catch (SecurityException e) {
throw new IllegalArgumentException("Cannot obtain enumeration values", e);
} catch (IllegalAccessException e) {
throw new IllegalArgumentException("Cannot obtain enumeration values", e);
} catch (InvocationTargetException e) {
throw new IllegalArgumentException("Cannot obtain enumeration values", e);
} catch (NoSuchMethodException e) {
throw new IllegalArgumentException("Cannot obtain enumeration values", e);
}
}
/** @return true if this is a batch UI that has no user interaction. */
public abstract boolean isBatch();
/** Display a header message before a series of prompts. */
public abstract void header(String fmt, Object... args);
/** Request the user to answer a yes/no question. */
public abstract boolean yesno(String fmt, Object... args);
/** Prints a message asking the user to let us know when its safe to continue. */
public abstract void waitForUser();
/** Prompt the user for a string, suggesting a default, and returning choice. */
public final String readString(String def, String fmt, Object... args) {
if (def != null && def.isEmpty()) {
def = null;
}
return readStringImpl(def, fmt, args);
}
/** Prompt the user for a string, suggesting a default, and returning choice. */
protected abstract String readStringImpl(String def, String fmt,
Object... args);
/** Prompt the user for a password, returning the string; null if blank. */
public abstract String password(String fmt, Object... args);
/** Prompt the user to make a choice from an enumeration's values. */
public abstract <T extends Enum<?>> T readEnum(T def, String fmt,
Object... args);
private static class Interactive extends ConsoleUI {
private final Console console;
Interactive(final Console console) {
this.console = console;
}
@Override
public boolean isBatch() {
return false;
}
@Override
public boolean yesno(String fmt, Object... args) {
final String prompt = String.format(fmt, args);
for (;;) {
final String yn = console.readLine("%-30s [y/n]? ", prompt);
if (yn == null) {
throw abort();
}
if (yn.equalsIgnoreCase("y") || yn.equalsIgnoreCase("yes")) {
return true;
}
if (yn.equalsIgnoreCase("n") || yn.equalsIgnoreCase("no")) {
return false;
}
}
}
@Override
public void waitForUser() {
if (console.readLine("Press enter to continue ") == null) {
throw abort();
}
}
@Override
protected String readStringImpl(String def, String fmt, Object... args) {
final String prompt = String.format(fmt, args);
String r;
if (def != null) {
r = console.readLine("%-30s [%s]: ", prompt, def);
} else {
r = console.readLine("%-30s : ", prompt);
}
if (r == null) {
throw abort();
}
r = r.trim();
if (r.isEmpty()) {
return def;
}
return r;
}
@Override
public String password(String fmt, Object... args) {
final String prompt = String.format(fmt, args);
for (;;) {
final char[] a1 = console.readPassword("%-30s : ", prompt);
if (a1 == null) {
throw abort();
}
final char[] a2 = console.readPassword("%30s : ", "confirm password");
if (a2 == null) {
throw abort();
}
final String s1 = new String(a1);
final String s2 = new String(a2);
if (!s1.equals(s2)) {
console.printf("error: Passwords did not match; try again\n");
continue;
}
return !s1.isEmpty() ? s1 : null;
}
}
@Override
public <T extends Enum<?>> T readEnum(T def, String fmt, Object... args) {
final String prompt = String.format(fmt, args);
final T[] options = all(def);
for (;;) {
String r = console.readLine("%-30s [%s/?]: ", prompt, def.toString());
if (r == null) {
throw abort();
}
r = r.trim();
if (r.isEmpty()) {
return def;
}
for (final T e : options) {
if (equalsIgnoreCase(e.toString(), r)) {
return e;
}
}
if (!"?".equals(r)) {
console.printf("error: '%s' is not a valid choice\n", r);
}
console.printf(" Supported options are:\n");
for (final T e : options) {
console.printf(" %s\n", e.toString().toLowerCase());
}
}
}
@Override
public void header(String fmt, Object... args) {
fmt = fmt.replaceAll("\n", "\n*** ");
console.printf("\n*** " + fmt + "\n*** \n\n", args);
}
}
private static class Batch extends ConsoleUI {
@Override
public boolean isBatch() {
return true;
}
@Override
public boolean yesno(String fmt, Object... args) {
return true;
}
@Override
protected String readStringImpl(String def, String fmt, Object... args) {
return def;
}
@Override
public void waitForUser() {
}
@Override
public String password(String fmt, Object... args) {
return null;
}
@Override
public <T extends Enum<?>> T readEnum(T def, String fmt, Object... args) {
return def;
}
@Override
public void header(String fmt, Object... args) {
}
}
}

View File

@@ -17,6 +17,7 @@ package com.google.gerrit.pgm;
import com.google.gerrit.lifecycle.LifecycleListener; import com.google.gerrit.lifecycle.LifecycleListener;
import org.apache.log4j.Appender; import org.apache.log4j.Appender;
import org.apache.log4j.ConsoleAppender;
import org.apache.log4j.DailyRollingFileAppender; import org.apache.log4j.DailyRollingFileAppender;
import org.apache.log4j.Level; import org.apache.log4j.Level;
import org.apache.log4j.LogManager; import org.apache.log4j.LogManager;
@@ -29,6 +30,22 @@ import org.apache.log4j.spi.LoggingEvent;
import java.io.File; import java.io.File;
public class ErrorLogFile { public class ErrorLogFile {
public static void errorOnlyConsole() {
LogManager.resetConfiguration();
final PatternLayout layout = new PatternLayout();
layout.setConversionPattern("%-5p %c %x: %m%n");
final ConsoleAppender dst = new ConsoleAppender();
dst.setLayout(layout);
dst.setTarget("System.err");
dst.setThreshold(Level.ERROR);
final Logger root = LogManager.getRootLogger();
root.removeAllAppenders();
root.addAppender(dst);
}
public static LifecycleListener start(final File sitePath) { public static LifecycleListener start(final File sitePath) {
final File logdir = new File(sitePath, "logs"); final File logdir = new File(sitePath, "logs");
if (!logdir.exists() && !logdir.mkdirs()) { if (!logdir.exists() && !logdir.mkdirs()) {

View File

@@ -0,0 +1,659 @@
// Copyright (C) 2009 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package com.google.gerrit.pgm;
import static com.google.gerrit.pgm.DataSourceProvider.Type.H2;
import com.google.gerrit.reviewdb.AuthType;
import com.google.gerrit.reviewdb.Project;
import com.google.gerrit.reviewdb.ReviewDb;
import com.google.gerrit.reviewdb.Project.SubmitType;
import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.gerrit.server.mail.SmtpEmailSender.Encryption;
import com.google.gwtjsonrpc.server.SignedToken;
import com.google.gwtorm.client.OrmException;
import com.google.gwtorm.client.SchemaFactory;
import com.google.inject.AbstractModule;
import com.google.inject.Inject;
import com.google.inject.Injector;
import com.google.inject.Module;
import org.apache.sshd.common.util.SecurityUtils;
import org.apache.sshd.server.keyprovider.SimpleGeneratorHostKeyProvider;
import org.eclipse.jgit.lib.Config;
import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.lib.FileBasedConfig;
import org.eclipse.jgit.lib.LockFile;
import org.eclipse.jgit.lib.RepositoryCache.FileKey;
import org.eclipse.jgit.util.SystemReader;
import org.kohsuke.args4j.Option;
import java.io.File;
import java.io.IOException;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
/** Initialize a new Gerrit installation. */
public class Init extends SiteProgram {
@Option(name = "--batch", usage = "Batch mode; skip interactive prompting")
private boolean batchMode;
@Option(name = "--import-projects", usage = "Import git repositories as projects")
private boolean importProjects;
@Inject
private GitRepositoryManager repositoryManager;
@Inject
private SchemaFactory<ReviewDb> schema;
private boolean deleteOnFailure;
private ConsoleUI ui;
private Injector dbInjector;
private Injector sysInjector;
@Override
public int run() throws Exception {
ErrorLogFile.errorOnlyConsole();
ui = ConsoleUI.getInstance(batchMode);
try {
initSitePath();
inject();
initGit();
} catch (Exception failure) {
if (deleteOnFailure) {
recursiveDelete(getSitePath());
}
throw failure;
} catch (Error failure) {
if (deleteOnFailure) {
recursiveDelete(getSitePath());
}
throw failure;
}
System.err.println("Initialized " + getSitePath().getCanonicalPath());
return 0;
}
private void initSitePath() throws IOException, InterruptedException {
final File sitePath = getSitePath();
final File gerrit_config = new File(sitePath, "gerrit.config");
final File secure_config = new File(sitePath, "secure.config");
final File replication_config = new File(sitePath, "replication.config");
final File lib_dir = new File(sitePath, "lib");
final File logs_dir = new File(sitePath, "logs");
final File static_dir = new File(sitePath, "static");
final File cache_dir = new File(sitePath, "cache");
if (gerrit_config.exists()) {
if (!gerrit_config.exists()) {
throw die("'" + sitePath + "' is not a Gerrit server site");
}
} else if (!gerrit_config.exists()) {
ui.header("Gerrit Code Review %s", version());
if (!ui.yesno("Initialize '%s'", sitePath.getCanonicalPath())) {
throw die("aborted by user");
}
if (!sitePath.mkdirs()) {
throw die("Cannot make directory " + sitePath);
}
deleteOnFailure = true;
final FileBasedConfig cfg = new FileBasedConfig(gerrit_config);
final FileBasedConfig sec = new FileBasedConfig(secure_config);
init_gerrit_basepath(cfg);
init_database(cfg, sec);
init_auth(cfg, sec);
init_sendemail(cfg, sec);
init_sshd(cfg, sec);
init_httpd(cfg, sec);
cache_dir.mkdir();
set(cfg, "cache", "directory", cache_dir.getName());
cfg.save();
saveSecureConfig(sec);
if (ui != null) {
System.err.println();
}
}
if (!secure_config.exists()) {
chmod600(secure_config);
}
if (!replication_config.exists()) {
replication_config.createNewFile();
}
lib_dir.mkdir();
logs_dir.mkdir();
static_dir.mkdir();
}
private void initGit() throws OrmException, IOException {
final File root = repositoryManager.getBasePath();
if (root != null && importProjects) {
System.err.println("Scanning projects under " + root);
final ReviewDb db = schema.open();
try {
final HashSet<String> have = new HashSet<String>();
for (Project p : db.projects().all()) {
have.add(p.getName());
}
importProjects(root, "", db, have);
} finally {
db.close();
}
}
}
private void importProjects(final File dir, final String prefix,
final ReviewDb db, final Set<String> have) throws OrmException,
IOException {
final File[] ls = dir.listFiles();
if (ls == null) {
return;
}
for (File f : ls) {
if (".".equals(f.getName()) || "..".equals(f.getName())) {
} else if (FileKey.isGitRepository(f)) {
String name = f.getName();
if (name.equals(".git")) {
name = prefix.substring(0, prefix.length() - 1);
} else if (name.endsWith(".git")) {
name = prefix + name.substring(0, name.length() - 4);
} else {
name = prefix + name;
System.err.println("Ignoring non-standard name '" + name + "'");
continue;
}
if (have.contains(name)) {
continue;
}
final Project.NameKey nameKey = new Project.NameKey(name);
final Project.Id idKey = new Project.Id(db.nextProjectId());
final Project p = new Project(nameKey, idKey);
p.setDescription(repositoryManager.getProjectDescription(name));
p.setSubmitType(SubmitType.MERGE_IF_NECESSARY);
p.setUseContributorAgreements(false);
p.setUseSignedOffBy(false);
db.projects().insert(Collections.singleton(p));
} else if (f.isDirectory()) {
importProjects(f, prefix + f.getName() + "/", db, have);
}
}
}
private void saveSecureConfig(final FileBasedConfig sec) throws IOException {
final byte[] out = Constants.encode(sec.toText());
final File path = sec.getFile();
final LockFile lf = new LockFile(path);
if (!lf.lock()) {
throw new IOException("Cannot lock " + path);
}
try {
chmod600(new File(path.getParentFile(), path.getName() + ".lock"));
lf.write(out);
if (!lf.commit()) {
throw new IOException("Cannot commit write to " + path);
}
} finally {
lf.unlock();
}
}
private static void chmod600(final File path) throws IOException {
if (!path.exists() && !path.createNewFile()) {
throw new IOException("Cannot create " + path);
}
path.setWritable(false, false /* all */);
path.setReadable(false, false /* all */);
path.setExecutable(false, false /* all */);
path.setWritable(true, true /* owner only */);
path.setReadable(true, true /* owner only */);
if (path.isDirectory()) {
path.setExecutable(true, true /* owner only */);
}
}
private void init_gerrit_basepath(final Config cfg) {
ui.header("Git Repositories");
File d = new File(ui.readString("git", "Location of Git repositories"));
set(cfg, "gerrit", "basePath", d.getPath());
if (d.exists()) {
if (!importProjects && d.list() != null && d.list().length > 0) {
importProjects = ui.yesno("Import existing repositories");
}
} else if (!d.mkdirs()) {
throw die("Cannot create " + d);
}
}
private void init_database(final Config cfg, final Config sec) {
ui.header("SQL Database");
DataSourceProvider.Type db_type = ui.readEnum(H2, "Database server type");
if (db_type == DataSourceProvider.Type.DEFAULT) {
db_type = H2;
}
set(cfg, "database", "type", db_type, null);
switch (db_type) {
case MYSQL:
createDownloader()
.setRequired(true)
.setName("MySQL Connector/J 5.1.10")
.setJarUrl(
"http://repo2.maven.org/maven2/mysql/mysql-connector-java/5.1.10/mysql-connector-java-5.1.10.jar")
.setSHA1("b83574124f1a00d6f70d56ba64aa52b8e1588e6d").download();
loadSiteLib();
break;
}
final boolean userPassAuth;
switch (db_type) {
case H2:
userPassAuth = false;
new File(getSitePath(), "db").mkdirs();
break;
case JDBC: {
userPassAuth = true;
String driver = ui.readString("", "Driver class name");
String url = ui.readString("", "url");
set(cfg, "database", "driver", driver);
set(cfg, "database", "url", url);
break;
}
case POSTGRES:
case POSTGRESQL:
case MYSQL: {
userPassAuth = true;
String def_port = "(" + db_type.toString() + " default)";
String hostname = ui.readString("localhost", "Server hostname");
String port = ui.readString(def_port, "Server port");
String database = ui.readString("reviewdb", "Database name");
set(cfg, "database", "hostname", hostname);
set(cfg, "database", "port", port != def_port ? port : null);
set(cfg, "database", "database", database);
break;
}
default:
throw die("internal bug, database " + db_type + " not supported");
}
if (userPassAuth) {
String user = ui.readString(username(), "Database username");
String pass = user != null ? ui.password("%s's password", user) : null;
set(cfg, "database", "username", user);
set(sec, "database", "password", pass);
}
}
private void init_auth(final Config cfg, final Config sec) {
ui.header("User Authentication");
AuthType auth_type = ui.readEnum(AuthType.OPENID, "Authentication method");
set(cfg, "auth", "type", auth_type, null);
switch (auth_type) {
case HTTP:
case HTTP_LDAP: {
String def_hdr = "(HTTP Basic)";
String hdr = ui.readString(def_hdr, "Username HTTP header");
String logoutUrl = ui.readString("", "Single-sign-on logout URL");
set(cfg, "auth", "httpHeader", hdr != def_hdr ? hdr : null);
set(cfg, "auth", "logoutUrl", logoutUrl);
break;
}
}
switch (auth_type) {
case LDAP:
case HTTP_LDAP: {
String server = ui.readString("ldap://localhost", "LDAP server");
if (server != null && !server.startsWith("ldap://")
&& !server.startsWith("ldaps://")) {
if (ui.yesno("Use SSL")) {
server = "ldaps://" + server;
} else {
server = "ldap://" + server;
}
}
final String def_dn = dnOf(server);
String accountBase = ui.readString(def_dn, "Account BaseDN");
String groupBase = ui.readString(accountBase, "Group BaseDN");
String user = ui.readString(null, "LDAP username");
String pass = user != null ? ui.password("%s's password", user) : null;
set(cfg, "ldap", "server", server);
set(cfg, "ldap", "username", user);
set(sec, "ldap", "password", pass);
set(cfg, "ldap", "accountBase", accountBase);
set(cfg, "ldap", "groupBase", groupBase);
break;
}
}
}
private void init_sendemail(final Config cfg, final Config sec) {
ui.header("Email Delivery");
String def_port = "(default)";
String smtpserver = ui.readString("localhost", "SMTP server hostname");
String port = ui.readString(def_port, "SMTP server port");
Encryption enc = ui.readEnum(Encryption.NONE, "SMTP encryption");
String username = null;
if (enc != Encryption.NONE || !isLocal(smtpserver)) {
username = username();
}
username = ui.readString(username, "SMTP username");
String password =
username != null ? ui.password("%s's password", username) : null;
set(cfg, "sendemail", "smtpServer", smtpserver);
set(cfg, "sendemail", "smtpServerPort", port != def_port ? port : null);
set(cfg, "sendemail", "smtpEncryption", enc, Encryption.NONE);
set(cfg, "sendemail", "smtpUser", username);
set(sec, "sendemail", "smtpPass", password);
}
private void init_sshd(final Config cfg, final Config sec)
throws IOException, InterruptedException {
ui.header("SSH Daemon");
String sshd_hostname = ui.readString("*", "Gerrit SSH listens on address");
String sshd_port = ui.readString("29418", "Gerrit SSH listens on port");
set(cfg, "sshd", "listenAddress", sshd_hostname + ":" + sshd_port);
// Download and install BouncyCastle if the user wants to use it.
//
createDownloader().setRequired(false).setName("Bouncy Castle Crypto v144")
.setJarUrl("http://www.bouncycastle.org/download/bcprov-jdk16-144.jar")
.setSHA1("6327a5f7a3dc45e0fd735adb5d08c5a74c05c20c").download();
loadSiteLib();
System.err.print("Generating SSH host key ...");
System.err.flush();
if (SecurityUtils.isBouncyCastleRegistered()) {
// Generate the SSH daemon host key using ssh-keygen.
//
final String comment = "gerrit-code-review@" + hostname();
final File rsa = new File(getSitePath(), "ssh_host_rsa_key");
final File dsa = new File(getSitePath(), "ssh_host_dsa_key");
System.err.print(" rsa...");
System.err.flush();
Runtime.getRuntime().exec(new String[] {"ssh-keygen", //
"-q" /* quiet */, //
"-t", "rsa", //
"-P", "", //
"-C", comment, //
"-f", rsa.getAbsolutePath() //
}).waitFor();
System.err.print(" dsa...");
System.err.flush();
Runtime.getRuntime().exec(new String[] {"ssh-keygen", //
"-q" /* quiet */, //
"-t", "dsa", //
"-P", "", //
"-C", comment, //
"-f", dsa.getAbsolutePath() //
}).waitFor();
} else {
// Generate the SSH daemon host key ourselves. This is complex
// because SimpleGeneratorHostKeyProvider doesn't mark the data
// file as only readable by us, exposing the private key for a
// short period of time. We try to reduce that risk by creating
// the key within a temporary directory.
//
final File tmpdir = new File(getSitePath(), "tmp.sshkeygen");
if (!tmpdir.mkdir()) {
throw die("Cannot create directory " + tmpdir);
}
chmod600(tmpdir);
final String keyname = "ssh_host_key";
final File tmpkey = new File(tmpdir, keyname);
final SimpleGeneratorHostKeyProvider p;
System.err.print(" rsa(simple)...");
System.err.flush();
p = new SimpleGeneratorHostKeyProvider();
p.setPath(tmpkey.getAbsolutePath());
p.setAlgorithm("RSA");
p.loadKeys(); // forces the key to generate.
chmod600(tmpkey);
final File key = new File(getSitePath(), keyname);
if (!tmpkey.renameTo(key)) {
throw die("Cannot rename " + tmpkey + " to " + key);
}
if (!tmpdir.delete()) {
throw die("Cannot delete " + tmpdir);
}
}
System.err.println(" done");
}
private void init_httpd(final Config cfg, final Config sec)
throws IOException, InterruptedException {
ui.header("HTTP Daemon");
final boolean reverseProxy =
ui.yesno("Behind reverse HTTP proxy (e.g. Apache mod_proxy)");
final boolean useSSL;
if (reverseProxy) {
useSSL = ui.yesno("Does the proxy server use https:// (SSL)");
} else {
useSSL = ui.yesno("Use https:// (SSL)");
}
final String scheme = useSSL ? "https" : "http";
final String port_def = useSSL ? "8443" : "8080";
String httpd_hostname = ui.readString(reverseProxy ? "localhost" : "*", //
"Gerrit HTTP listens on address");
String httpd_port = ui.readString(reverseProxy ? "8081" : port_def, //
"Gerrit HTTP listens on port");
String context = "/";
if (reverseProxy) {
context = ui.readString("/", "Gerrit's subdirectory on proxy server");
if (!context.endsWith("/")) {
context += "/";
}
}
final String httpd_url = (reverseProxy ? "proxy-" : "") //
+ scheme + "://" + httpd_hostname + ":" + httpd_port + context;
set(cfg, "httpd", "listenUrl", httpd_url);
if (useSSL && !reverseProxy
&& ui.yesno("Create self-signed SSL certificate")) {
final String certName =
ui.readString("*".equals(httpd_hostname) ? hostname()
: httpd_hostname, "Certificate server name");
final String validity =
ui.readString("365", "Certificate expires in (days)");
final String ssl_pass = SignedToken.generateRandomKey();
final String dname =
"CN=" + certName + ",OU=Gerrit Code Review,O=" + domainOf(certName);
final File tmpdir = new File(getSitePath(), "tmp.sslcertgen");
if (!tmpdir.mkdir()) {
throw die("Cannot create directory " + tmpdir);
}
chmod600(tmpdir);
final File tmpstore = new File(tmpdir, "keystore");
Runtime.getRuntime().exec(new String[] {"keytool", //
"-keystore", tmpstore.getAbsolutePath(), //
"-storepass", ssl_pass, //
"-genkeypair", //
"-alias", certName, //
"-keyalg", "RSA", //
"-validity", validity, //
"-dname", dname, //
"-keypass", ssl_pass, //
}).waitFor();
chmod600(tmpstore);
final File store = new File(getSitePath(), "keystore");
if (!tmpstore.renameTo(store)) {
throw die("Cannot rename " + tmpstore + " to " + store);
}
if (!tmpdir.delete()) {
throw die("Cannot delete " + tmpdir);
}
set(sec, "httpd", "sslKeyPassword", ssl_pass);
set(cfg, "gerrit", "canonicalWebUrl", "https://" + certName + ":"
+ httpd_port + context);
}
}
private <T extends Enum<?>> void set(Config cfg, String section, String name,
T value, T def) {
if (value != null && value != def) {
cfg.setString(section, null, name, value.toString());
} else {
cfg.unset(section, null, name);
}
}
private void set(Config cfg, String section, String name, String value) {
if (value != null && !value.isEmpty()) {
cfg.setString(section, null, name, value);
} else {
cfg.unset(section, null, name);
}
}
private void inject() {
dbInjector = createDbInjector();
sysInjector = createSysInjector();
sysInjector.injectMembers(this);
}
private Injector createSysInjector() {
final List<Module> modules = new ArrayList<Module>();
modules.add(new AbstractModule() {
@Override
protected void configure() {
bind(GitRepositoryManager.class);
}
});
return dbInjector.createChildInjector(modules);
}
private LibraryDownloader createDownloader() {
return new LibraryDownloader(ui, getSitePath());
}
private static String version() {
return com.google.gerrit.common.Version.getVersion();
}
private static String username() {
return System.getProperty("user.name");
}
private static String hostname() {
return SystemReader.getInstance().getHostname();
}
private static boolean isLocal(final String hostname) {
try {
return InetAddress.getByName(hostname).isLoopbackAddress();
} catch (UnknownHostException e) {
return false;
}
}
private static String dnOf(String name) {
if (name != null) {
int p = name.indexOf("://");
if (0 < p) {
name = name.substring(p + 3);
}
p = name.indexOf(".");
if (0 < p) {
name = name.substring(p + 1);
name = "DC=" + name.replaceAll("\\.", ",DC=");
} else {
name = null;
}
}
return name;
}
private static String domainOf(String name) {
if (name != null) {
int p = name.indexOf("://");
if (0 < p) {
name = name.substring(p + 3);
}
p = name.indexOf(".");
if (0 < p) {
name = name.substring(p + 1);
}
}
return name;
}
private static void recursiveDelete(File path) {
File[] entries = path.listFiles();
if (entries != null) {
for (File e : entries) {
recursiveDelete(e);
}
}
if (!path.delete() && path.exists()) {
System.err.println("warn: Cannot remove " + path);
}
}
}

View File

@@ -0,0 +1,233 @@
// Copyright (C) 2009 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package com.google.gerrit.pgm;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.util.HttpSupport;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.Proxy;
import java.net.ProxySelector;
import java.net.URL;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
/** Get optional or required 3rd party library files into $site_path/lib. */
class LibraryDownloader {
private final ConsoleUI console;
private final File libDirectory;
private boolean required;
private String name;
private String jarUrl;
private String sha1;
private File dst;
LibraryDownloader(final ConsoleUI console, final File sitePath) {
this.console = console;
this.libDirectory = new File(sitePath, "lib");
}
LibraryDownloader setRequired(final boolean required) {
this.required = required;
return this;
}
LibraryDownloader setName(final String name) {
this.name = name;
return this;
}
LibraryDownloader setJarUrl(final String url) {
this.jarUrl = url;
return this;
}
LibraryDownloader setSHA1(final String sha1) {
this.sha1 = sha1;
return this;
}
void download() {
if (jarUrl == null || !jarUrl.contains("/")) {
throw new IllegalStateException("Invalid JarUrl for " + name);
}
final String jarName = jarUrl.substring(jarUrl.lastIndexOf('/') + 1);
if (jarName.contains("/") || jarName.contains("\\")) {
throw new IllegalStateException("Invalid JarUrl: " + jarUrl);
}
if (name == null) {
name = jarName;
}
dst = new File(libDirectory, jarName);
if (!dst.exists() && shouldGet()) {
doGet();
}
}
private boolean shouldGet() {
if (console.isBatch()) {
return required;
} else {
final StringBuilder msg = new StringBuilder();
msg.append("\n");
msg.append("Gerrit Code Review is not shipped with %s\n");
if (required) {
msg.append("** This library is required for your configuration. **\n");
} else {
msg.append(" If available, Gerrit can take advantage of features\n");
msg.append(" in the library, but will also function without it.\n");
}
msg.append("Download and install it now");
return console.yesno(msg.toString(), name);
}
}
private void doGet() {
if (!libDirectory.exists() && !libDirectory.mkdirs()) {
throw new Die("Cannot create " + libDirectory);
}
try {
doGetByHttp();
verifyFileChecksum();
} catch (IOException err) {
dst.delete();
if (console.isBatch()) {
throw new Die("error: Cannot get " + jarUrl, err);
}
System.err.println();
System.err.println();
System.err.println("error: " + err.getMessage());
System.err.println("Please download:");
System.err.println();
System.err.println(" " + jarUrl);
System.err.println();
System.err.println("and save as:");
System.err.println();
System.err.println(" " + dst.getAbsolutePath());
System.err.println();
System.err.flush();
console.waitForUser();
if (dst.exists()) {
verifyFileChecksum();
} else if (!console.yesno("Continue without this library")) {
throw new Die("aborted by user");
}
}
}
private void doGetByHttp() throws IOException {
System.err.print("Downloading " + jarUrl + " ...");
System.err.flush();
try {
final ProxySelector proxySelector = ProxySelector.getDefault();
final URL url = new URL(jarUrl);
final Proxy proxy = HttpSupport.proxyFor(proxySelector, url);
final HttpURLConnection c = (HttpURLConnection) url.openConnection(proxy);
final InputStream in;
switch (HttpSupport.response(c)) {
case HttpURLConnection.HTTP_OK:
in = c.getInputStream();
break;
case HttpURLConnection.HTTP_NOT_FOUND:
throw new FileNotFoundException(url.toString());
default:
throw new IOException(url.toString() + ": " + HttpSupport.response(c)
+ " " + c.getResponseMessage());
}
try {
final OutputStream out = new FileOutputStream(dst);
try {
final byte[] buf = new byte[8192];
int n;
while ((n = in.read(buf)) > 0) {
out.write(buf, 0, n);
}
} finally {
out.close();
}
} finally {
in.close();
}
System.err.println(" OK");
System.err.flush();
} catch (IOException err) {
dst.delete();
System.err.println(" !! FAIL !!");
System.err.flush();
throw err;
}
}
private void verifyFileChecksum() {
if (sha1 != null) {
try {
final MessageDigest md = MessageDigest.getInstance("SHA-1");
final FileInputStream in = new FileInputStream(dst);
try {
final byte[] buf = new byte[8192];
int n;
while ((n = in.read(buf)) > 0) {
md.update(buf, 0, n);
}
} finally {
in.close();
}
if (sha1.equals(ObjectId.fromRaw(md.digest()).name())) {
System.err.println("Checksum " + dst.getName() + " OK");
System.err.flush();
} else if (console.isBatch()) {
dst.delete();
throw new Die(dst + " SHA-1 checksum does not match");
} else if (!console.yesno("error: SHA-1 checksum does not match\n"
+ "Use %s anyway", dst.getName())) {
dst.delete();
throw new Die("aborted by user");
}
} catch (IOException checksumError) {
dst.delete();
throw new Die("cannot checksum " + dst, checksumError);
} catch (NoSuchAlgorithmException checksumError) {
dst.delete();
throw new Die("cannot checksum " + dst, checksumError);
}
}
}
}

View File

@@ -31,19 +31,22 @@ import com.google.inject.name.Names;
import org.kohsuke.args4j.Option; import org.kohsuke.args4j.Option;
import java.io.File; import java.io.File;
import java.io.FileFilter;
import java.lang.reflect.InvocationTargetException; import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method; import java.lang.reflect.Method;
import java.net.MalformedURLException; import java.net.MalformedURLException;
import java.net.URL; import java.net.URL;
import java.net.URLClassLoader; import java.net.URLClassLoader;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Set;
import javax.sql.DataSource; import javax.sql.DataSource;
public abstract class SiteProgram extends AbstractProgram { public abstract class SiteProgram extends AbstractProgram {
private boolean siteLibLoaded;
@Option(name = "--site-path", aliases = {"-d"}, usage = "Local directory containing site data") @Option(name = "--site-path", aliases = {"-d"}, usage = "Local directory containing site data")
private File sitePath = new File("."); private File sitePath = new File(".");
@@ -58,30 +61,38 @@ public abstract class SiteProgram extends AbstractProgram {
/** Load extra JARs from {@code lib/} subdirectory of {@link #getSitePath()} */ /** Load extra JARs from {@code lib/} subdirectory of {@link #getSitePath()} */
protected void loadSiteLib() { protected void loadSiteLib() {
if (!siteLibLoaded) {
final File libdir = new File(getSitePath(), "lib"); final File libdir = new File(getSitePath(), "lib");
final File[] list = libdir.listFiles(); final File[] list = libdir.listFiles(new FileFilter() {
if (list != null) { @Override
final List<File> toLoad = new ArrayList<File>(); public boolean accept(File path) {
for (final File u : list) { if (!path.isFile()) {
if (u.isFile() && (u.getName().endsWith(".jar") // return false;
|| u.getName().endsWith(".zip"))) {
toLoad.add(u);
} }
return path.getName().endsWith(".jar") //
|| path.getName().endsWith(".zip");
} }
addToClassLoader(toLoad); });
if (list != null && 0 < list.length) {
Arrays.sort(list, new Comparator<File>() {
@Override
public int compare(File a, File b) {
return a.getName().compareTo(b.getName());
} }
});
siteLibLoaded = true; addToClassLoader(list);
} }
} }
private void addToClassLoader(final List<File> additionalLocations) { private void addToClassLoader(final File[] additionalLocations) {
final ClassLoader cl = getClass().getClassLoader(); final ClassLoader cl = getClass().getClassLoader();
if (!(cl instanceof URLClassLoader)) { if (!(cl instanceof URLClassLoader)) {
throw noAddURL("Not loaded by URLClassLoader", null); throw noAddURL("Not loaded by URLClassLoader", null);
} }
final URLClassLoader ucl = (URLClassLoader) cl;
final Set<URL> have = new HashSet<URL>();
have.addAll(Arrays.asList(ucl.getURLs()));
final Method m; final Method m;
try { try {
m = URLClassLoader.class.getDeclaredMethod("addURL", URL.class); m = URLClassLoader.class.getDeclaredMethod("addURL", URL.class);
@@ -92,17 +103,20 @@ public abstract class SiteProgram extends AbstractProgram {
throw noAddURL("Method addURL not available", e); throw noAddURL("Method addURL not available", e);
} }
for (final File u : additionalLocations) { for (final File path : additionalLocations) {
try { try {
m.invoke(cl, u.toURI().toURL()); final URL url = path.toURI().toURL();
if (have.add(url)) {
m.invoke(cl, url);
}
} catch (MalformedURLException e) { } catch (MalformedURLException e) {
throw noAddURL("addURL " + u + " failed", e); throw noAddURL("addURL " + path + " failed", e);
} catch (IllegalArgumentException e) { } catch (IllegalArgumentException e) {
throw noAddURL("addURL " + u + " failed", e); throw noAddURL("addURL " + path + " failed", e);
} catch (IllegalAccessException e) { } catch (IllegalAccessException e) {
throw noAddURL("addURL " + u + " failed", e); throw noAddURL("addURL " + path + " failed", e);
} catch (InvocationTargetException e) { } catch (InvocationTargetException e) {
throw noAddURL("addURL " + u + " failed", e.getCause()); throw noAddURL("addURL " + path + " failed", e.getCause());
} }
} }
} }

View File

@@ -29,10 +29,13 @@ import org.eclipse.jgit.lib.RepositoryCache;
import org.eclipse.jgit.lib.WindowCache; import org.eclipse.jgit.lib.WindowCache;
import org.eclipse.jgit.lib.WindowCacheConfig; import org.eclipse.jgit.lib.WindowCacheConfig;
import org.eclipse.jgit.lib.RepositoryCache.FileKey; import org.eclipse.jgit.lib.RepositoryCache.FileKey;
import org.eclipse.jgit.util.IO;
import org.eclipse.jgit.util.RawParseUtils;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import java.io.File; import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException; import java.io.IOException;
/** Class managing Git repositories. */ /** Class managing Git repositories. */
@@ -40,6 +43,9 @@ import java.io.IOException;
public class GitRepositoryManager { public class GitRepositoryManager {
private static final Logger log = LoggerFactory.getLogger(GitRepositoryManager.class); private static final Logger log = LoggerFactory.getLogger(GitRepositoryManager.class);
private static final String UNNAMED =
"Unnamed repository; edit this file to name it for gitweb.";
public static class Lifecycle implements LifecycleListener { public static class Lifecycle implements LifecycleListener {
private final Config cfg; private final Config cfg;
@@ -147,10 +153,46 @@ public class GitRepositoryManager {
} }
} }
/**
* Read the {@code GIT_DIR/description} file for gitweb.
* <p>
* NB: This code should really be in JGit, as a member of the Repository
* object. Until it moves there, its here.
*
* @param name the repository name, relative to the base directory.
* @return description text; null if no description has been configured.
* @throws RepositoryNotFoundException the named repository does not exist.
* @throws IOException the description file exists, but is not readable by
* this process.
*/
public String getProjectDescription(final String name)
throws RepositoryNotFoundException, IOException {
final Repository e = openRepository(name);
final File d = new File(e.getDirectory(), "description");
String description;
try {
description = RawParseUtils.decode(IO.readFully(d));
} catch (FileNotFoundException err) {
return null;
}
if (description != null) {
description = description.trim();
if (description.isEmpty()) {
description = null;
}
if (UNNAMED.equals(description)) {
description = null;
}
}
return description;
}
/** /**
* Set the {@code GIT_DIR/description} file for gitweb. * Set the {@code GIT_DIR/description} file for gitweb.
* <p> * <p>
* NB: This code should really be in JGit, as a member of the Repostiory * NB: This code should really be in JGit, as a member of the Repository
* object. Until it moves there, its here. * object. Until it moves there, its here.
* *
* @param name the repository name, relative to the base directory. * @param name the repository name, relative to the base directory.