digipolisgent/robo-digipolis-deploy

View on GitHub
src/Ssh.php

Summary

Maintainability
A
3 hrs
Test Coverage
C
78%
<?php

namespace DigipolisGent\Robo\Task\Deploy;

use DigipolisGent\CommandBuilder\CommandBuilder;
use DigipolisGent\Robo\Task\Deploy\Ssh\Auth\AbstractAuth;
use DigipolisGent\Robo\Task\Deploy\Ssh\Command;
use DigipolisGent\Robo\Task\Deploy\Ssh\Factory\SshFactoryInterface;
use DigipolisGent\Robo\Task\Deploy\Ssh\Factory\SshPhpseclibFactory;
use Robo\Result;
use Robo\Task\BaseTask;

class Ssh extends BaseTask
{
    use \Robo\Common\CommandReceiver;

    /**
     * The server to ssh to/from.
     *
     * @var string
     */
    protected $host;

    /**
     * The ssh port of the server. Defaults to 22.
     *
     * @var int
     */
    protected $port = 22;

    /**
     * The ssh timeout. Defaults to 10 seconds.
     *
     * @var int
     */
    protected $timeout = 10;

    /**
     * The command stack to execute.
     *
     * @var array
     */
    protected $commandStack = [];

    /**
     * Whether or not to stop on fail.
     *
     * @var bool
     */
    protected $stopOnFail = false;

    /**
     * The authentication for the ssh connection.
     *
     * @var AbstractAuth
     */
    protected $auth;

    /**
     * The fully qualified classname of the ssh factory.
     *
     * @var string
     */
    protected $sshFactory = SshPhpseclibFactory::class;

    /**
     * The remote directory to execute the commands in.
     *
     * @var string
     */
    protected $remoteDir;

    /**
     * Whether or not to use the physical directory structure without following
     * symbolic links.
     *
     * @var bool
     */
    protected $physicalRemoteDir = false;

    /**
     * Stores ssh command output.
     *
     * @var string
     */
    protected $sshOutput = '';

    /**
     * Enables or disables verbose output.
     *
     * @var bool
     */
    protected $verbose = false;

    /**
     * Creates a new Ssh task.
     *
     * @param string $host
     *   The host.
     * @param AbstractAuth $auth
     *   Authentication data.
     */
    public function __construct($host, AbstractAuth $auth)
    {
        $this->host = $host;
        $this->auth = $auth;
    }

    /**
     * Sets the port to connect on.
     *
     * @param int $port
     *   The port to connect on.
     *
     * @return $this
     */
    public function port($port)
    {
        $this->port = $port;

        return $this;
    }

    /**
     * Sets the ssh timeout.
     *
     * @param int $timeout
     *   Timeout in seconds.
     *
     * @return $this
     */
    public function timeout($timeout)
    {
        $this->timeout = $timeout;

        return $this;
    }

    /**
     * @param string|CommandInterface|CommandBuilder $command
     * @param callable $callback
     *
     * @return $this
     */
    public function exec($command, $callback = null)
    {
        $this->commandStack[] = [
            'command' => new Command(
                $this->receiveCommand(
                    $command instanceof CommandBuilder
                    ? (string) $command
                    : $command
                )
            ),
            'callback' => $callback,
        ];

        return $this;
    }

    /**
     * Sets the remote directory.
     *
     * @param string $directory
     *   The remote directory.
     * @param bool $physical
     *   Use the physical directory structure without following symbolic links
     *   (-P argument for cd).
     *
     * @return $this
     */
    public function remoteDirectory($directory, $physical = false)
    {
        $this->remoteDir = $directory;
        $this->physicalRemoteDir = $physical;

        return $this;
    }

    /**
     * {@inheritdoc}
     */
    public function progressIndicatorSteps()
    {
        return count($this->commandStack);
    }

    /**
     * Should we stop up- or downloading files once one has failed?
     *
     * @param bool $stopOnFail
     *   Whether or not we should stop on fail.
     *
     * @return $this
     */
    public function stopOnFail($stopOnFail = true)
    {
        $this->stopOnFail = $stopOnFail;

        return $this;
    }

    /**
     * Set the SshFactory.
     *
     * @param string|\DigipolisGent\Robo\Task\Deploy\Ssh\Factory\SshFactoryInterface $class
     *   A factory instance or the fully qualified classname of the ssh factory.
     *   The given class (whether it's a classname or instance) must implement
     *   \DigipolisGent\Robo\Task\Deploy\Ssh\Factory\SshFactoryInterface.
     *
     * @throws InvalidArgumentException
     *   If the class is not an instance of
     *   \DigipolisGent\Robo\Task\Deploy\Ssh\Factory\SshFactoryInterface.
     *
     * @return $this
     */
    public function sshFactory($class)
    {
        if (!is_subclass_of($class, SshFactoryInterface::class)) {
            throw new \InvalidArgumentException(sprintf(
                'SSH Factory %s does not implement %s.',
                $class,
                SshFactoryInterface::class
            ));
        }
        $this->sshFactory = $class;

        return $this;
    }

    public function verbose($verbose = true)
    {
        $this->verbose = $verbose;
    }

    /**
     * {@inheritdoc}
     */
    public function run()
    {
        try {
            $ssh = call_user_func(
                [$this->sshFactory, 'create'],
                $this->host,
                $this->port,
                $this->timeout
            );
            $this->startTimer();
            $ssh->login($this->auth);
            $errorMessage = '';
            foreach ($this->commandStack as $command) {
                $command['command']
                    ->setDirectory($this->remoteDir)
                    ->setPhysicalDirectory($this->physicalRemoteDir);

                $this->printTaskInfo(sprintf(
                    '%s@%s:%s$ %s',
                    $this->auth->getUser(),
                    $this->host,
                    $this->remoteDir ? $this->remoteDir : '~',
                    $command['command']->getCommand()
                ));
                $result = call_user_func_array(
                    [
                        $ssh,
                        'exec',
                    ],
                    [
                        (string) $command['command'],
                        $this->commandCallback($command['callback']),
                    ]
                );
                $this->printTaskInfo($this->sshOutput);
                $this->sshOutput = '';
                if ($result === false || $ssh->getExitStatus() !== 0) {
                    $errorMessage .= sprintf(
                        'Could not execute %s on %s on port %s in folder %s with message: %s.',
                        $command['command']->getCommand(),
                        $this->host,
                        $this->port,
                        $this->remoteDir,
                        $ssh->getStdError()
                    );
                    if ($ssh->isTimeout()) {
                        $errorMessage .= ' ';
                        $errorMessage .= sprintf(
                            'Connection timed out. Execution took %s, timeout is set at %s seconds.',
                            $this->getExecutionTime(),
                            $this->timeout
                        );
                    }
                    if ($this->stopOnFail) {
                        return Result::error($this, $errorMessage);
                    }
                    $errorMessage .= "\n";
                }
            }
        } catch (\Exception $e) {
            $errorMessage = $e->getMessage();
        }
        $this->stopTimer();
        return $errorMessage
            ? Result::error($this, $errorMessage . ($this->verbose ?  "\nVerbose log:\n" . $ssh->getLog() : ''))
            : Result::success($this, ($this->verbose ?  "Verbose log:\n" . $ssh->getLog() : ''));
    }

    /**
     * Wrap the callback so we can print the output.
     *
     * @param callable $callback
     *   The callback to wrap.
     */
    protected function commandCallback($callback)
    {
        return (
            function ($output) use ($callback) {
                $this->sshOutput .= $output;
                if (is_callable($callback)) {
                    return call_user_func($callback, $output);
                }
            }
        );
    }
}