diff --git a/src/IDF/Middleware.php b/src/IDF/Middleware.php index cee2270..424cc01 100644 --- a/src/IDF/Middleware.php +++ b/src/IDF/Middleware.php @@ -111,7 +111,7 @@ function IDF_Middleware_ContextPreProcessor($request) $request->project); $c = array_merge($c, $request->rights); } - $c['usherConfigured'] = Pluf::f("mtn_usher", null) !== null; + $c['usherConfigured'] = Pluf::f("mtn_usher_conf", null) !== null; return $c; } diff --git a/src/IDF/Plugin/SyncMonotone.php b/src/IDF/Plugin/SyncMonotone.php index 2280a0b..6f37fb8 100644 --- a/src/IDF/Plugin/SyncMonotone.php +++ b/src/IDF/Plugin/SyncMonotone.php @@ -41,8 +41,15 @@ class IDF_Plugin_SyncMonotone } /** - * Run mtn init command to create the corresponding monotone - * repository and add the database to the configured usher instance + * Four steps to setup a new monotone project: + * + * 1) run mtn db init to initialize a new database underknees + * 'mtn_repositories' + * 2) create a new server key in the same directory + * 3) write monotonerc for access control + * 4) add the database as new local server in the usher configuration + * 5) reload the running usher instance so it acknowledges the new + * server * * @param IDF_Project */ @@ -52,45 +59,113 @@ class IDF_Plugin_SyncMonotone return; } - $repotempl = Pluf::f('mtn_repositories', false); - if ($repotempl === false) { + $projecttempl = Pluf::f('mtn_repositories', false); + if ($projecttempl === false) { throw new IDF_Scm_Exception( '"mtn_repositories" must be defined in your configuration file.' ); } - $usher_config = Pluf::f('mtn_usher', array()); - if (!array_key_exists('rcfile', $usher_config) || - !is_writable($usher_config['rcfile'])) { + $usher_config = Pluf::f('mtn_usher_conf', false); + if (!$usher_config || !is_writable($usher_config)) { throw new IDF_Scm_Exception( - '"rcfile" in "mtn_usher" does not exist or is not writable.' + '"mtn_usher_conf" does not exist or is not writable.' ); } $shortname = $project->shortname; - $dbfile = sprintf($repotempl, $shortname); - if (file_exists($dbfile)) { + $projectpath = sprintf($projecttempl, $shortname); + if (file_exists($projectpath)) { + throw new IDF_Scm_Exception(sprintf( + __('The project path %s already exists.'), $projectpath + )); + } + + if (!mkdir($projectpath)) { throw new IDF_Scm_Exception(sprintf( - __('The repository %s already exists.'), $dbfile + __('The project path %s could not be created.'), $projectpath )); } - $return = 0; - $output = array(); + + // + // step 1) create a new database + // + $dbfile = $projectpath.'/database.mtn'; $cmd = sprintf( Pluf::f('mtn_path', 'mtn').' db init -d %s', escapeshellarg($dbfile) ); $cmd = Pluf::f('idf_exec_cmd_prefix', '').$cmd; - $ll = exec($cmd, $output, $return); + $ll = exec($cmd, $output = array(), $return = 0); + if ($return != 0) { + throw new IDF_Scm_Exception(sprintf( + __('The database file %s could not be created.'), $dbfile + )); + } + + // + // step 2) create a server key + // + // try to parse the key's domain part from the remote_url's host + // name, otherwise fall back to the configured Apache server name + $server = $_SERVER['SERVER_NAME']; + $remote_url = Pluf::f('mtn_remote_url'); + if (($parsed = parse_url($remote_url)) !== false && + !empty($parsed['host'])) { + $server = $parsed['host']; + } + + $keyname = $shortname.'-server@'.$server; + $cmd = sprintf( + Pluf::f('mtn_path', 'mtn').' au genkey --confdir=%s %s ""', + escapeshellarg($projectpath), + escapeshellarg($keyname) + ); + $cmd = Pluf::f('idf_exec_cmd_prefix', '').$cmd; + $ll = exec($cmd, $output = array(), $return = 0); if ($return != 0) { throw new IDF_Scm_Exception(sprintf( - __('Could not create repository %s - please check '. - 'your error log for details.'), - $dbfile + __('The server key %s could not be created.'), $keyname )); } - $usher_rc = file_get_contents($usher_config['rcfile']); + // + // step 3) write monotonerc for access control + // FIXME: netsync access control is still missing! + // + $monotonerc =<<getMessage() + $usher_config, $e->getMessage() )); } @@ -121,17 +196,23 @@ class IDF_Plugin_SyncMonotone $new_server = array( array('key' => 'server', 'values' => array($shortname)), - array('key' => 'local', 'values' => array('-d', $dbfile)), + array('key' => 'local', 'values' => array( + '--confdir', $projectpath, + '-d', $dbfile + )), ); $parsed_config[] = $new_server; $usher_rc = IDF_Scm_Monotone_BasicIO::compile($parsed_config); // FIXME: more sanity - what happens on failing writes? - $fp = fopen($usher_config['rcfile'], 'w'); + $fp = fopen($usher_config, 'w'); fwrite($fp, $usher_rc); fclose($fp); + // + // step 5) reload usher to pick up the new configuration + // IDF_Scm_Monotone_Usher::reload(); } } diff --git a/src/IDF/Scm/Monotone/Usher.php b/src/IDF/Scm/Monotone/Usher.php index 5be2e8e..5e55e08 100644 --- a/src/IDF/Scm/Monotone/Usher.php +++ b/src/IDF/Scm/Monotone/Usher.php @@ -21,6 +21,8 @@ # # ***** END LICENSE BLOCK ***** */ +require_once(dirname(__FILE__) . "/BasicIO.php"); + /** * Connects with the admininistrative interface of usher, * the monotone proxy. This class contains only static methods because @@ -190,36 +192,56 @@ class IDF_Scm_Monotone_Usher private static function _triggerCommand($cmd) { - $uc = Pluf::f('mtn_usher'); - if (empty($uc['host'])) { + $uc = Pluf::f('mtn_usher_conf', false); + if (!$uc || !is_readable($uc)) { + throw new IDF_Scm_Exception( + '"mtn_usher_conf" is not configured or not readable' + ); + } + + $parsed_config = + IDF_Scm_Monotone_BasicIO::parse(file_get_contents($uc)); + $host = $port = $user = $pass = null; + foreach ($parsed_config as $stanza) { + foreach ($stanza as $line) { + if ($line['key'] == 'adminaddr') { + list($host, $port) = explode(":", @$line['values'][0]); + break; + } + if ($line['key'] == 'userpass') { + $user = @$line['values'][0]; + $pass = @$line['values'][1]; + } + } + } + + if (empty($host)) { throw new IDF_Scm_Exception('usher host is empty'); } - if (!preg_match('/^\d+$/', $uc['port']) || - $uc['port'] == 0) + if (!preg_match('/^\d+$/', $port)) { throw new IDF_Scm_Exception('usher port is invalid'); } - if (empty($uc['user'])) { + if (empty($user)) { throw new IDF_Scm_Exception('usher user is empty'); } - if (empty($uc['pass'])) { + if (empty($pass)) { throw new IDF_Scm_Exception('usher pass is empty'); } - $sock = @fsockopen($uc['host'], $uc['port'], $errno, $errstr); + $sock = @fsockopen($host, $port, $errno, $errstr); if (!$sock) { throw new IDF_Scm_Exception( "could not connect to usher: $errstr ($errno)" ); } - fwrite($sock, 'USERPASS '.$uc['user'].' '.$uc['pass']."\n"); + fwrite($sock, 'USERPASS '.$user.' '.$pass."\n"); if (feof($sock)) { throw new IDF_Scm_Exception( - 'usher closed the connection - probably wrong admin '. - 'username or password' + 'usher closed the connection - this should not happen' ); } @@ -232,7 +254,7 @@ class IDF_Scm_Monotone_Usher $out = rtrim($out); if ($out == 'unknown command') { - throw new IDF_Scm_Exception("unknown command: $cmd"); + throw new IDF_Scm_Exception('unknown command: '.$cmd); } return $out; diff --git a/src/IDF/conf/idf.php-dist b/src/IDF/conf/idf.php-dist index 31c4661..57d3e95 100644 --- a/src/IDF/conf/idf.php-dist +++ b/src/IDF/conf/idf.php-dist @@ -78,9 +78,11 @@ $cfg['mtn_path'] = 'mtn'; # Additional options for the started monotone process $cfg['mtn_opts'] = array('--no-workspace', '--norc'); # -# You can setup monotone for use with indefero in two ways: +# You can setup monotone for use with indefero in several ways. The +# two most-used should be: # # 1) One database for everything: +# # Set 'mtn_repositories' below to a fixed database path, such as # '/home/mtn/repositories/all_projects.mtn' # @@ -94,26 +96,39 @@ $cfg['mtn_opts'] = array('--no-workspace', '--norc'); # the database # # 2) One database for every project with 'usher': -# Set 'mtn_remote_url' below to a string which matches your setup. +# +# Download and configure 'usher' +# (mtn clone mtn://monotone.ca?net.venge.monotone.contrib.usher) +# which acts as proxy in front of all single project databases. +# Create a basic configuration file for it and add a secret admin +# username and password. Finally, point the below variable +# 'mtn_usher_conf' to this configuration file. +# +# Then set 'mtn_remote_url' below to a string which matches your setup. # Again, the '%s' placeholder will be expanded to the project's # short name. Note that 'mtn_remote_url' is used as internal # URI (to access the data for indefero) as well as external URI -# (for end users) at the same time. +# (for end users) at the same time. 'mtn_repositories' should then +# point to a directory where all project-related files (databases, +# keys, configurations) are kept, as these are automatically created +# on project creation by IDF. # -# Then download and configure 'usher' -# (mtn clone mtn://monotone.ca?net.venge.monotone.contrib.usher) -# which acts as proxy in front of all single project databases. -# Usher's server names should be mapped to the project's short names, -# so you end up with something like this for every project: +# Example: 'mtn_repositories' is configured to be '/var/monotone/%s' +# +# - IDF tries to create /var/monotone/ as root directory +# - The database is placed in as /var/monotone//database.mtn +# - The server key is put into /var/monotone//keys and +# is named "-server@", where host is the host part +# of 'mtn_remote_url' # -# server "project" -# local "-d" "/home/mtn/repositories/project.mtn" "*" +# therefor /var/monotone MUST be read/writable for the www user and all +# files which are created underknees MUST be read/writable by the user +# who is executing the usher instance! The best way to achieve this is with +# default (POSIX) ACLs on /var/monotone. # -# Alternatively if you assign every project a unique DNS such as -# 'project.my-hosting.biz', you can also configure it like this: # -# host "project.my-hosting.biz" -# local "-d" "/home/mtn/repositories/project.mtn" "*" +# You could also choose to setup usher by hand, i.e. with individual +# databases, in this case leave 'mtn_usher_conf' below commented out. # # Pro: - read and write access can be granted per project # - no database locking issues @@ -143,22 +158,11 @@ $cfg['mtn_remote_url'] = 'mtn://my-host.biz/%s'; $cfg['mtn_db_access'] = 'remote'; # # If configured, this allows basic control of a running usher process -# via the forge administration -# -# 'host' and 'port' must be set to the specific bits from usher's -# configured 'adminaddr', 'user' and 'pass' must match the values set for -# the configured 'userpass' combination. The 'rcfile' variable must point -# to the full (writable) path of the usher configuration file which gets -# updated when new projects are added -# -#$cfg['mtn_usher'] = array( -# 'host' => 'localhost', -# 'port' => 12345, -# 'user' => 'admin', -# 'pass' => 'admin', -# 'rcfile' => '/path/to/usher.conf', -#); +# via the forge administration. The variable must point to the full (writable) +# path of the usher configuration file which gets updated when new projects +# are added # +#$cfg['mtn_usher_conf'] = '/path/to/usher.conf'; # Mercurial repositories path #$cfg['mercurial_repositories'] = '/home/mercurial/repositories/%s'; diff --git a/src/IDF/conf/urls.php b/src/IDF/conf/urls.php index 1d9b56f..b4f6a4c 100644 --- a/src/IDF/conf/urls.php +++ b/src/IDF/conf/urls.php @@ -386,7 +386,7 @@ $ctl[] = array('regex' => '#^/admin/users/(\d+)/$#', 'model' => 'IDF_Views_Admin', 'method' => 'userUpdate'); -if (Pluf::f("mtn_usher", null) !== null) +if (Pluf::f("mtn_usher_conf", null) !== null) { $ctl[] = array('regex' => '#^/admin/usher/$#', 'base' => $base,