mirror of
https://github.com/deployphp/deployer.git
synced 2025-02-24 09:12:51 +01:00
Implement ssh multiplexing the right way
This commit is contained in:
parent
d2b29cfe73
commit
7ebe90e9e4
182
src/Host/Host.php
Normal file
182
src/Host/Host.php
Normal file
@ -0,0 +1,182 @@
|
||||
<?php
|
||||
/* (c) Anton Medvedev <anton@medv.io>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Deployer\Host;
|
||||
|
||||
class Host
|
||||
{
|
||||
private $hostname;
|
||||
private $user;
|
||||
private $port;
|
||||
private $configFile;
|
||||
private $identityFile;
|
||||
private $forwardAgent = true;
|
||||
private $multiplexing = true;
|
||||
private $options = [];
|
||||
|
||||
/**
|
||||
* Host constructor.
|
||||
* @param string $hostname
|
||||
*/
|
||||
public function __construct(string $hostname)
|
||||
{
|
||||
$this->hostname = $hostname;
|
||||
}
|
||||
|
||||
public function generateOptionsString()
|
||||
{
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns pair user/hostname
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function __toString()
|
||||
{
|
||||
$user = empty($this->user) ? '' : "{$this->user}@";
|
||||
$hostname = $this->hostname;
|
||||
return "$user$hostname";
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getHostname()
|
||||
{
|
||||
return $this->hostname;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $hostname
|
||||
*/
|
||||
public function setHostname(string $hostname)
|
||||
{
|
||||
$this->hostname = $hostname;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getUser()
|
||||
{
|
||||
return $this->user;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $user
|
||||
*/
|
||||
public function setUser(string $user)
|
||||
{
|
||||
$this->user = $user;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function getPort()
|
||||
{
|
||||
return $this->port;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $port
|
||||
*/
|
||||
public function setPort(int $port)
|
||||
{
|
||||
$this->port = $port;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getConfigFile()
|
||||
{
|
||||
return $this->configFile;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $configFile
|
||||
*/
|
||||
public function setConfigFile(string $configFile)
|
||||
{
|
||||
$this->configFile = $configFile;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getIdentityFile()
|
||||
{
|
||||
return $this->identityFile;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $identityFile
|
||||
*/
|
||||
public function setIdentityFile(string $identityFile)
|
||||
{
|
||||
$this->identityFile = $identityFile;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function isForwardAgent()
|
||||
{
|
||||
return $this->forwardAgent;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param bool $forwardAgent
|
||||
*/
|
||||
public function setForwardAgent(bool $forwardAgent)
|
||||
{
|
||||
$this->forwardAgent = $forwardAgent;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function isMultiplexing()
|
||||
{
|
||||
return $this->multiplexing;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param bool $multiplexing
|
||||
*/
|
||||
public function setMultiplexing(bool $multiplexing)
|
||||
{
|
||||
$this->multiplexing = $multiplexing;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function getOptions()
|
||||
{
|
||||
return $this->options;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $options
|
||||
*/
|
||||
public function setOptions(array $options)
|
||||
{
|
||||
$this->options = $options;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $option
|
||||
*/
|
||||
public function addOption(string $option)
|
||||
{
|
||||
$this->options[] = $option;
|
||||
}
|
||||
}
|
@ -7,7 +7,9 @@
|
||||
|
||||
namespace Deployer\Ssh;
|
||||
|
||||
use Deployer\Exception\Exception;
|
||||
use Deployer\Exception\RuntimeException;
|
||||
use Deployer\Host\Host;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Process\Process;
|
||||
|
||||
@ -27,23 +29,39 @@ class Client
|
||||
$this->output = $output;
|
||||
}
|
||||
|
||||
|
||||
public function run($host, $command, $options = [])
|
||||
/**
|
||||
* @param Host $host
|
||||
* @param string $command
|
||||
* @param array $config
|
||||
* @return string
|
||||
* @throws RuntimeException
|
||||
*/
|
||||
public function run(Host $host, string $command, array $config = [])
|
||||
{
|
||||
$hostname = $host->getHostname();
|
||||
$defaults = [
|
||||
'timeout' => 300,
|
||||
'tty' => false,
|
||||
];
|
||||
$options = array_merge($defaults, $options);
|
||||
$config = array_merge($defaults, $config);
|
||||
|
||||
$hostname = $host;
|
||||
$ssh = "ssh $hostname 'bash -s; printf \"[exit_code:%s]\" $?;'";
|
||||
$options = $host->generateOptionsString();
|
||||
|
||||
if ($host->isMultiplexing()) {
|
||||
$options = $this->initMultiplexing($host);
|
||||
}
|
||||
|
||||
if ($config['tty']) {
|
||||
$options .= ' -tt';
|
||||
}
|
||||
|
||||
$ssh = "ssh $options $host 'bash -s; printf \"[exit_code:%s]\" $?;'";
|
||||
|
||||
$process = new Process($ssh);
|
||||
$process
|
||||
->setInput($command)
|
||||
->setTimeout($options['timeout'])
|
||||
->setTty($options['tty']);
|
||||
->setTimeout($config['timeout'])
|
||||
->setTty($config['tty']);
|
||||
|
||||
$callback = function ($type, $buffer) use ($hostname) {
|
||||
if ($this->output->isDebug()) {
|
||||
@ -105,4 +123,76 @@ class Client
|
||||
$exitCode = (int)$match[1];
|
||||
return $exitCode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Init multiplexing by adding options for ssh command
|
||||
*
|
||||
* @param Host $host
|
||||
* @return string Host options
|
||||
*/
|
||||
private function initMultiplexing(Host $host)
|
||||
{
|
||||
$options = $host->generateOptionsString();
|
||||
$controlPath = $this->generateControlPath($host);
|
||||
|
||||
$options .= " -o ControlMaster=auto";
|
||||
$options .= " -o ControlPersist=60";
|
||||
$options .= " -o ControlPath=$controlPath";
|
||||
|
||||
$process = new Process("ssh $options -O check -S $controlPath $host 2>&1");
|
||||
$process->run();
|
||||
|
||||
if (!preg_match('/Master running/', $process->getOutput())) {
|
||||
$this->writeln(Process::OUT, $host->getHostname(), 'ssh multiplexing initialization');
|
||||
}
|
||||
|
||||
return $options;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return SSH multiplexing control path
|
||||
*
|
||||
* When ControlPath is longer than 104 chars we can get:
|
||||
*
|
||||
* SSH Error: unix_listener: too long for Unix domain socket
|
||||
*
|
||||
* So try to get as descriptive path as possible.
|
||||
* %C is for creating hash out of connection attributes.
|
||||
*
|
||||
* @param Host $host
|
||||
* @return string ControlPath
|
||||
* @throws Exception
|
||||
*/
|
||||
private function generateControlPath(Host $host)
|
||||
{
|
||||
$connectionData = "{$host->getUser()}_{$host->getHostname()}_{$host->getPort()}";
|
||||
$tryLongestPossible = 0;
|
||||
$controlPath = '';
|
||||
do {
|
||||
switch ($tryLongestPossible) {
|
||||
case 1:
|
||||
$controlPath = "~/.ssh/deployer_mux_$connectionData";
|
||||
break;
|
||||
case 2:
|
||||
$controlPath = "~/.ssh/deployer_mux_%C";
|
||||
break;
|
||||
case 3:
|
||||
$controlPath = "~/deployer_mux_$connectionData";
|
||||
break;
|
||||
case 4:
|
||||
$controlPath = "~/deployer_mux_%C";
|
||||
break;
|
||||
case 5:
|
||||
$controlPath = "~/mux_%C";
|
||||
break;
|
||||
case 6:
|
||||
throw new Exception("The multiplexing control path is too long. Control path is: $controlPath");
|
||||
default:
|
||||
$controlPath = "~/.ssh/deployer_mux_$connectionData";
|
||||
}
|
||||
$tryLongestPossible++;
|
||||
} while (strlen($controlPath) > 104); // Unix socket max length
|
||||
|
||||
return $controlPath;
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user