Implement ssh multiplexing the right way

This commit is contained in:
Anton Medvedev 2017-03-12 19:30:48 +07:00
parent d2b29cfe73
commit 7ebe90e9e4
2 changed files with 279 additions and 7 deletions

182
src/Host/Host.php Normal file
View 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;
}
}

View File

@ -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;
}
}