added deploy.inc.php

This commit is contained in:
Bimmy 2022-07-28 17:37:42 +03:00
commit 70cba8fd1a
2 changed files with 522 additions and 0 deletions

11
composer.json Normal file
View File

@ -0,0 +1,11 @@
{
"name": "bit/taskman_deploy",
"description": "android test farm",
"homepage": "https://git.bit5.ru/composer/taskman_deploy",
"require": {
"php": ">=7.4"
},
"autoload": {
"classmap": ["deploy.inc.php"]
}
}

511
deploy.inc.php Normal file
View File

@ -0,0 +1,511 @@
<?php
namespace taskman;
use Amp;
require_once('composer/vendor/autoload.php');
$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);
use phpseclib\Crypt\RSA;
use phpseclib\Net\SSH2;
use phpseclib\Net\SCP;
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();
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;
}