512 lines
12 KiB
PHP
512 lines
12 KiB
PHP
<?php
|
|
namespace taskman;
|
|
use Exception;
|
|
use Amp;
|
|
use Amp\Process;
|
|
use phpseclib\Crypt\RSA;
|
|
use phpseclib\Net\SSH2;
|
|
use phpseclib\Net\SCP;
|
|
|
|
$GLOBALS['DEPLOY_NODES'] = array();
|
|
$GLOBALS['DEPLOY_SSH_CONNS'] = array();
|
|
|
|
define('DEPLOY_OPT_ONE_HOST', 1);
|
|
define('DEPLOY_OPT_ERR_OK', 2);
|
|
define('DEPLOY_OPT_SILENT', 4);
|
|
define('DEPLOY_OPT_IS_FILE', 8);
|
|
define('DEPLOY_OPT_NO_CACHE', 16);
|
|
|
|
class DeployNode
|
|
{
|
|
public $name;
|
|
public $props = array();
|
|
|
|
function __construct($name, $props = array())
|
|
{
|
|
$this->name = $name;
|
|
$this->_setupProps($props);
|
|
}
|
|
|
|
function get($name)
|
|
{
|
|
if(!isset($this->props[$name]))
|
|
throw new Exception("No such property '$name' in declaration '{$this->name}'");
|
|
return $this->props[$name];
|
|
}
|
|
|
|
function set($name, $val)
|
|
{
|
|
$this->props[$name] = $val;
|
|
}
|
|
|
|
function has($name)
|
|
{
|
|
return isset($this->props[$name]);
|
|
}
|
|
|
|
private function _setupProps(array $props)
|
|
{
|
|
foreach($props as $k => $v)
|
|
$this->props[$k] = $v;
|
|
}
|
|
}
|
|
|
|
function deploy_declare_node($name, array $props)
|
|
{
|
|
global $DEPLOY_NODES;
|
|
|
|
if(isset($DEPLOY_NODES[$name]))
|
|
throw new Exception("Declaration '$name' already exists");
|
|
|
|
$decl = new DeployNode($name, $props);
|
|
$deploy_dir = '/home/' . $decl->get('user') . '/' . $decl->get('dir');
|
|
//for convenience
|
|
$decl->set("deploy_dir", $deploy_dir);
|
|
|
|
//checking deploy_dir conflicts
|
|
foreach($DEPLOY_NODES as $other_name => $other_decl)
|
|
{
|
|
foreach($other_decl->get('hosts') as $other_host)
|
|
{
|
|
foreach($decl->get('hosts') as $host)
|
|
if($host == $other_host && $other_decl->get('deploy_dir') === $deploy_dir)
|
|
throw new Exception("Deploy directory '$deploy_dir' conflicts on nodes '$other_name' and '$name' for host '$host'");
|
|
}
|
|
}
|
|
|
|
$DEPLOY_NODES[$name] = $decl;
|
|
}
|
|
|
|
function deploy_get_node($name)
|
|
{
|
|
global $DEPLOY_NODES;
|
|
|
|
if(!isset($DEPLOY_NODES[$name]))
|
|
throw new Exception("Deploy node '{$name}' not found");
|
|
|
|
return $DEPLOY_NODES[$name];
|
|
}
|
|
|
|
function deploy_find_node($name)
|
|
{
|
|
global $DEPLOY_NODES;
|
|
|
|
if(isset($DEPLOY_NODES[$name]))
|
|
return $DEPLOY_NODES[$name];
|
|
return null;
|
|
}
|
|
|
|
function deploy_get_node_names()
|
|
{
|
|
global $DEPLOY_NODES;
|
|
return array_keys($DEPLOY_NODES);
|
|
}
|
|
|
|
function deploy_get($name, $key)
|
|
{
|
|
$decl = deploy_get_node($name);
|
|
return $decl->get($key);
|
|
}
|
|
|
|
function deploy_set($name, $key, $val)
|
|
{
|
|
$decl = deploy_get_node($name);
|
|
$decl->set($key, $val);
|
|
}
|
|
|
|
function deploy_exec($name, $cmd, $opts = 0, $func = null)
|
|
{
|
|
$decl = deploy_get_node($name);
|
|
|
|
$outs = array();
|
|
|
|
foreach($decl->get('hosts') as $host)
|
|
{
|
|
$ssh = deploy_get_ssh($decl, $host, $opts);
|
|
$out = deploy_node_host_exec($decl, $host, $ssh, $cmd, $status, $opts);
|
|
$outs[$host] = array($status, $out);
|
|
|
|
if($func !== null)
|
|
$func($decl, $host, $ssh, $out, $status);
|
|
if(($opts & DEPLOY_OPT_ONE_HOST) != 0)
|
|
break;
|
|
}
|
|
|
|
return $outs;
|
|
}
|
|
|
|
function deploy_node_host_exec($decl, $host, SSH2 $ssh, $cmd, &$status, $opts = 0)
|
|
{
|
|
$cmd = deploy_str($decl, $cmd);
|
|
if(($opts & DEPLOY_OPT_SILENT) == 0)
|
|
echo "[EXE] $host: $cmd\n";
|
|
$out = '';
|
|
$res = $ssh->exec($cmd, function($str) use(&$out, $opts) {
|
|
$out .= $str;
|
|
if(($opts & DEPLOY_OPT_SILENT) == 0)
|
|
echo $str;
|
|
return false;
|
|
});
|
|
$status = $ssh->getExitStatus();
|
|
// @phpstan-ignore-next-line
|
|
if($res === false)
|
|
throw new Exception("Fatal error executing($status): $cmd");
|
|
if(($opts & DEPLOY_OPT_ERR_OK) == 0 && $status !== 0)
|
|
throw new Exception("Invalid exit status($status) '$cmd': {$ssh->stdErrorLog}");
|
|
return $out;
|
|
}
|
|
|
|
function deploy_ssh_exec($name, $cmd, $opts = 0)
|
|
{
|
|
$decl = deploy_get_node($name);
|
|
$cmd = deploy_str($decl, $cmd);
|
|
$user = $decl->get('user');
|
|
|
|
$tmp_key_file = tempnam("/tmp", "ssh_");
|
|
$key_str = $decl->get('ssh_key_str');
|
|
|
|
$outs = array();
|
|
|
|
try
|
|
{
|
|
file_put_contents($tmp_key_file, $key_str);
|
|
|
|
foreach($decl->get('hosts') as $host)
|
|
{
|
|
if(($opts & DEPLOY_OPT_SILENT) == 0)
|
|
echo "[SSH] $host: $cmd\n";
|
|
|
|
$host_only = $host;
|
|
$port = 22;
|
|
if(strpos($host, ":") !== false)
|
|
list($host_only, $port) = explode(":", $host);
|
|
|
|
$proc_cmd = "ssh -p $port -o StrictHostKeyChecking=no -o ConnectTimeout=90 -o ConnectionAttempts=30 -i $tmp_key_file $user@$host_only ".escapeshellarg($cmd);
|
|
if($host === "localhost")
|
|
$proc_cmd = $cmd;
|
|
|
|
//echo "proc_cmd: $proc_cmd\n";
|
|
$out = array();
|
|
exec($proc_cmd, $out, $status);
|
|
|
|
if(($opts & DEPLOY_OPT_SILENT) == 0 && $out)
|
|
echo implode("\n", $out) . "\n";
|
|
|
|
if(($opts & DEPLOY_OPT_ERR_OK) == 0 && $status !== 0)
|
|
throw new Exception("Invalid exit status($status) '$cmd': " . implode("\n", $out));
|
|
|
|
$outs[$host] = array($status, $out);
|
|
|
|
if(($opts & DEPLOY_OPT_ONE_HOST) != 0)
|
|
break;
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
unlink($tmp_key_file);
|
|
}
|
|
|
|
return $outs;
|
|
}
|
|
|
|
function deploy_ssh_exec_async($name, $cmd, $opts = 0)
|
|
{
|
|
return Amp\call(function() use($name, $cmd, $opts) {
|
|
|
|
$decl = deploy_get_node($name);
|
|
$cmd = deploy_str($decl, $cmd);
|
|
$user = $decl->get('user');
|
|
|
|
$tmp_key_file = tempnam("/tmp", "ssh_");
|
|
$key_str = $decl->get('ssh_key_str');
|
|
|
|
$outs = array();
|
|
|
|
try
|
|
{
|
|
file_put_contents($tmp_key_file, $key_str);
|
|
|
|
foreach($decl->get('hosts') as $host)
|
|
{
|
|
if(($opts & DEPLOY_OPT_SILENT) == 0)
|
|
echo "[SSH] $host: $cmd\n";
|
|
|
|
$proc_cmd = "ssh -o StrictHostKeyChecking=no -o ConnectTimeout=90 -o ConnectionAttempts=30 -i $tmp_key_file $user@$host ".escapeshellarg($cmd);
|
|
|
|
if($host === "localhost")
|
|
$proc_cmd = $cmd;
|
|
|
|
$proc = new Amp\Process\Process($proc_cmd);
|
|
|
|
yield $proc->start();
|
|
$out = yield Amp\ByteStream\buffer($proc->getStdout());
|
|
$status = yield $proc->join();
|
|
|
|
if(($opts & DEPLOY_OPT_ERR_OK) == 0 && $status !== 0)
|
|
throw new Exception("Invalid exit status($status) '$cmd': $out");
|
|
|
|
$outs[$host] = array($status, explode("\n", $out));
|
|
|
|
if(($opts & DEPLOY_OPT_ONE_HOST) != 0)
|
|
break;
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
unlink($tmp_key_file);
|
|
}
|
|
|
|
return $outs;
|
|
|
|
});
|
|
}
|
|
|
|
function deploy_put_file($name, $path, $contents, $opts = 0)
|
|
{
|
|
$decl = deploy_get_node($name);
|
|
|
|
foreach($decl->get('hosts') as $host)
|
|
{
|
|
$ssh = deploy_get_ssh($decl, $host, $opts);
|
|
$path = deploy_str($decl, $path);
|
|
$scp = new SCP($ssh);
|
|
if(($opts & DEPLOY_OPT_SILENT) == 0)
|
|
echo "[PUT] $host: $path\n";
|
|
|
|
$fails = 0;
|
|
while($scp->put($path, $contents, ($opts & DEPLOY_OPT_IS_FILE) == 0 ? SCP::SOURCE_STRING : SCP::SOURCE_LOCAL_FILE) === false)
|
|
{
|
|
++$fails;
|
|
echo "Retrying a file put: $path...\n";
|
|
sleep(1);
|
|
if($fails > 5)
|
|
throw new Exception("Could not put file: $path");
|
|
}
|
|
}
|
|
}
|
|
|
|
function deploy_scp_put_file($name, $local_path, $remote_path, $opts = 0)
|
|
{
|
|
$decl = deploy_get_node($name);
|
|
|
|
$user = $decl->get('user');
|
|
|
|
$tmp_key_file = tempnam("/tmp", "ssh_");
|
|
$key_str = $decl->get('ssh_key_str');
|
|
|
|
try
|
|
{
|
|
file_put_contents($tmp_key_file, $key_str);
|
|
|
|
foreach($decl->get('hosts') as $host)
|
|
{
|
|
if(($opts & DEPLOY_OPT_SILENT) == 0)
|
|
echo "[PUT] $host: $local_path -> $remote_path\n";
|
|
|
|
$host_only = $host;
|
|
$port = 22;
|
|
if(strpos($host, ":") !== false)
|
|
list($host_only, $port) = explode(":", $host);
|
|
|
|
$cmd = "scp -P $port -o StrictHostKeyChecking=no -o ConnectTimeout=90 -o ConnectionAttempts=30 -i $tmp_key_file $local_path $user@$host_only:$remote_path";
|
|
if($host === "localhost")
|
|
$cmd = "cp $local_path $remote_path";
|
|
|
|
system($cmd, $ret);
|
|
|
|
if($ret != 0)
|
|
throw new Exception("Could not scp local $local_path to remote $remote_path: $ret");
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
unlink($tmp_key_file);
|
|
}
|
|
}
|
|
|
|
function deploy_scp_put_file_async($name, $local_path, $remote_path, $opts = 0)
|
|
{
|
|
return Amp\call(function() use($name, $local_path, $remote_path, $opts) {
|
|
|
|
$decl = deploy_get_node($name);
|
|
|
|
$user = $decl->get('user');
|
|
|
|
$tmp_key_file = tempnam("/tmp", "ssh_");
|
|
$key_str = $decl->get('ssh_key_str');
|
|
|
|
try
|
|
{
|
|
file_put_contents($tmp_key_file, $key_str);
|
|
|
|
foreach($decl->get('hosts') as $host)
|
|
{
|
|
if(($opts & DEPLOY_OPT_SILENT) == 0)
|
|
echo "[PUT] $host: $local_path -> $remote_path\n";
|
|
|
|
$proc_cmd = "scp -o StrictHostKeyChecking=no -o ConnectTimeout=90 -o ConnectionAttempts=30 -i $tmp_key_file $local_path $user@$host:$remote_path";
|
|
if($host === "localhost")
|
|
$proc_cmd = "cp $local_path $remote_path";
|
|
|
|
$proc = new Amp\Process\Process($proc_cmd);
|
|
|
|
yield $proc->start();
|
|
|
|
$err_stream = $proc->getStderr();
|
|
while(null !== $chunk = yield $err_stream->read())
|
|
echo $chunk;
|
|
|
|
$status = yield $proc->join();
|
|
|
|
if($status !== 0)
|
|
throw new Exception("Could not scp file $local_path to remote $remote_path: $status");
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
unlink($tmp_key_file);
|
|
}
|
|
});
|
|
}
|
|
|
|
function deploy_rsync_async($name, $src_dir, $dst_dir, $rsync_opts = '', $opts = 0)
|
|
{
|
|
return Amp\call(function() use($name, $src_dir, $dst_dir, $rsync_opts, $opts) {
|
|
|
|
$decl = deploy_get_node($name);
|
|
|
|
$tmp_key_file = tempnam("/tmp", "ssh_");
|
|
$key_str = $decl->get('ssh_key_str');
|
|
|
|
try
|
|
{
|
|
file_put_contents($tmp_key_file, $key_str);
|
|
|
|
$ssh_transport = "ssh -A -o StrictHostKeyChecking=no -o ConnectTimeout=90 -o ConnectionAttempts=30 -i $tmp_key_file";
|
|
|
|
foreach($decl->get('hosts') as $host)
|
|
{
|
|
list($ssh_host, $ssh_port) = deploy_ssh_host_port($host);
|
|
|
|
$proc_cmd = "rsync -e '" . $ssh_transport . " -p $ssh_port' -a $rsync_opts $src_dir/ " . $decl->get('user') . '@' . $ssh_host . ":$dst_dir/";
|
|
|
|
if(($opts & DEPLOY_OPT_SILENT) == 0)
|
|
echo "[RSN] $host: $proc_cmd\n";
|
|
|
|
$proc = new Amp\Process\Process($proc_cmd);
|
|
|
|
yield $proc->start();
|
|
|
|
$err_stream = $proc->getStderr();
|
|
while(null !== $chunk = yield $err_stream->read())
|
|
echo $chunk;
|
|
|
|
$status = yield $proc->join();
|
|
|
|
if($status !== 0)
|
|
throw new Exception("Could not rsync $src_dir to remote $dst_dir: $status");
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
unlink($tmp_key_file);
|
|
}
|
|
});
|
|
}
|
|
|
|
function deploy_get_file($name, $path, $opts = 0)
|
|
{
|
|
$files = array();
|
|
$decl = deploy_get_node($name);
|
|
foreach($decl->get('hosts') as $host)
|
|
{
|
|
if(($opts & DEPLOY_OPT_SILENT) == 0)
|
|
echo "[GET] $host: $path\n";
|
|
|
|
if($host !== "localhost")
|
|
{
|
|
$ssh = deploy_get_ssh($decl, $host);
|
|
$path = deploy_str($decl, $path);
|
|
$scp = new SCP($ssh);
|
|
$files[$host] = $scp->get($path);
|
|
}
|
|
else
|
|
$files[$host] = file_get_contents($path);
|
|
}
|
|
return $files;
|
|
}
|
|
|
|
function deploy_ssh_host_port($host)
|
|
{
|
|
$port = '22';
|
|
|
|
$host_parts = explode(':', $host);
|
|
if(isset($host_parts[1]))
|
|
$port = $host_parts[1];
|
|
$host = $host_parts[0];
|
|
|
|
return array($host, $port);
|
|
}
|
|
|
|
function deploy_get_ssh(DeployNode $decl, $host, $opts = 0)
|
|
{
|
|
global $DEPLOY_SSH_CONNS;
|
|
|
|
$conn_id = $decl->name.'_'.$host;
|
|
|
|
if(!isset($DEPLOY_SSH_CONNS[$conn_id]) || ($opts & DEPLOY_OPT_NO_CACHE) != 0)
|
|
{
|
|
$ssh = _deploy_make_ssh($decl, $host);
|
|
$DEPLOY_SSH_CONNS[$conn_id] = $ssh;
|
|
}
|
|
else
|
|
{
|
|
$ssh = $DEPLOY_SSH_CONNS[$conn_id];
|
|
//let's check the cached connection
|
|
try
|
|
{
|
|
$ok = $ssh->ping();
|
|
}
|
|
catch(Exception $e)
|
|
{
|
|
$ok = false;
|
|
}
|
|
//if it's broken for some reason let's just fetch the new one
|
|
if(!$ok)
|
|
{
|
|
$ssh = _deploy_make_ssh($decl, $host);
|
|
$DEPLOY_SSH_CONNS[$conn_id] = $ssh;
|
|
}
|
|
}
|
|
|
|
return $DEPLOY_SSH_CONNS[$conn_id];
|
|
}
|
|
|
|
function _deploy_make_ssh($decl, $host)
|
|
{
|
|
list($ssh_host, $ssh_port) = deploy_ssh_host_port($host);
|
|
$key = new RSA();
|
|
$key->loadKey($decl->get('ssh_key_str'));
|
|
|
|
$ssh = new SSH2($ssh_host, $ssh_port);
|
|
if(!$ssh->login($decl->get('user'), $key))
|
|
throw new Exception("Login failed");
|
|
return $ssh;
|
|
}
|
|
|
|
function deploy_str($decl, $str)
|
|
{
|
|
global $DEPLOY_NODES;
|
|
|
|
$res = preg_replace_callback(
|
|
'~%\{([^\}]+)\}%~',
|
|
function($m) use($decl)
|
|
{
|
|
return $decl->get($m[1]);
|
|
},
|
|
$str
|
|
);
|
|
|
|
return $res;
|
|
}
|