diff --git a/BeSimpleDeploymentBundle.php b/BeSimpleDeploymentBundle.php index f783299..06f27a3 100755 --- a/BeSimpleDeploymentBundle.php +++ b/BeSimpleDeploymentBundle.php @@ -6,4 +6,4 @@ class BeSimpleDeploymentBundle extends Bundle { -} +} \ No newline at end of file diff --git a/Command/DeploymentCommand.php b/Command/DeploymentCommand.php index 49c52a8..1d8e84c 100644 --- a/Command/DeploymentCommand.php +++ b/Command/DeploymentCommand.php @@ -27,6 +27,7 @@ protected function configure() $this ->setDefinition(array( new InputArgument('server', InputArgument::OPTIONAL, 'The target server name', null), + new InputOption('tag', 't', InputOption::VALUE_NONE, 'Tag with git if real deployment') )) ; } @@ -58,6 +59,13 @@ protected function execute(InputInterface $input, OutputInterface $output) )); }); + $eventDispatcher->addListener(Events::onDeploymentSshStart, function (CommandEvent $event) use ($self) { + $self->write(sprintf( + '[Try SSH Command] %s', + $event->getCommand() + )); + }); + // Feedback events if ($output->getVerbosity() > Output::VERBOSITY_NORMAL) { @@ -86,6 +94,49 @@ protected function execute(InputInterface $input, OutputInterface $output) ), 'info'); }); + if($input->getOption('tag')){ + $eventDispatcher->addListener(Events::onDeploymentSuccess, function (DeployerEvent $event) use ($self) { + if(!$event->isTest()){ + $path = realpath($this->getApplication()->getKernel()->getRootDir().'/..'); + if(is_dir($path)){ + $tag = 'deploy-'. strtolower($event->getServer()); + + $self->write(sprintf( + 'Trying to set GIT tag %s in %s', + $tag, + $path + ), 'comment'); + + $gitCmd = sprintf( + 'git --git-dir=%s --work-tree=%s', + $path.'/.git', + $path + ); + + $status = shell_exec($gitCmd.' status -s'); + if($status !== null){ + $self->write(sprintf('There are uncommitted changes - wont set the tag: %s', $status), 'error'); + return; + } + + $commit = shell_exec($gitCmd.' rev-parse HEAD'); + if(!$commit){ + $self->write('Last commit-hash not found for HEAD', 'error'); + return; + } + + $tagResult = shell_exec($gitCmd.' tag -f '. $tag .' '. $commit); + if(!$tagResult){ + $self->write('Tagging seems ok. Try "git push --tags"', 'info'); + return; + } + + $self->write($tagResult, 'info'); + } + } + }); + } + $eventDispatcher->addListener(Events::onDeploymentRsyncSuccess, function (CommandEvent $event) use ($self) { $self->write(sprintf( '[Rsync success] %s', diff --git a/DependencyInjection/BeSimpleDeploymentExtension.php b/DependencyInjection/BeSimpleDeploymentExtension.php index af459a9..5798846 100755 --- a/DependencyInjection/BeSimpleDeploymentExtension.php +++ b/DependencyInjection/BeSimpleDeploymentExtension.php @@ -24,11 +24,11 @@ public function load(array $configs, ContainerBuilder $container) $loader = new XmlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config')); $loader->load('deployment.xml'); - $container->setParameter('be_simple_deployment.config.rsync', $config['rsync']); - $container->setParameter('be_simple_deployment.config.ssh', $config['rsync']); - $container->setParameter('be_simple_deployment.config.rules', $config['rules']); - $container->setParameter('be_simple_deployment.config.commands', $config['commands']); - $container->setParameter('be_simple_deployment.config.servers', $config['servers']); + $container->setParameter('be_simple_deployment.config.rsync', isset($config['rsync']) ? $config['rsync'] : array()); + $container->setParameter('be_simple_deployment.config.ssh', isset($config['ssh']) ? $config['ssh'] : array()); + $container->setParameter('be_simple_deployment.config.rules', isset($config['rules']) ? $config['rules'] : array()); + $container->setParameter('be_simple_deployment.config.commands', isset($config['commands']) ? $config['commands'] : array()); + $container->setParameter('be_simple_deployment.config.servers', isset($config['servers']) ? $config['servers'] : array()); } /** diff --git a/DependencyInjection/Configuration.php b/DependencyInjection/Configuration.php index ed27bda..8108e1a 100644 --- a/DependencyInjection/Configuration.php +++ b/DependencyInjection/Configuration.php @@ -39,6 +39,7 @@ protected function addRsyncSection(ArrayNodeDefinition $node) ->scalarNode('command')->defaultValue('rsync')->cannotBeEmpty()->end() ->booleanNode('delete')->defaultFalse()->end() ->scalarNode('options')->defaultValue('-Cva')->end() + ->scalarNode('timeout')->defaultValue(120)->end() ->scalarNode('root')->defaultValue('%kernel.root_dir%/..')->cannotBeEmpty()->end() ->end() ; @@ -52,9 +53,34 @@ protected function addSshSection(ArrayNodeDefinition $node) { $node->children() ->arrayNode('ssh')->children() + ->scalarNode('username')->defaultNull()->end() + ->scalarNode('password')->defaultNull()->end() + ->scalarNode('pubkey_file')->defaultNull()->end() ->scalarNode('privkey_file')->defaultNull()->end() ->scalarNode('passphrase')->defaultNull()->end() + ->arrayNode('connect_methods') + ->addDefaultsIfNotSet() + ->children() + ->scalarNode('kex')->defaultNull()->end() + ->scalarNode('hostkey')->defaultNull()->end() + ->arrayNode('client_to_server') + ->addDefaultsIfNotSet() + ->children() + ->scalarNode('crypt')->defaultNull()->end() + ->scalarNode('comp')->defaultNull()->end() + ->scalarNode('mac')->defaultNull()->end() + ->end() + ->end() + ->arrayNode('server_to_client') + ->addDefaultsIfNotSet() + ->children() + ->scalarNode('crypt')->defaultNull()->end() + ->scalarNode('comp')->defaultNull()->end() + ->scalarNode('mac')->defaultNull()->end() + ->end() + ->end() + ->end() ->end() ; } @@ -96,7 +122,7 @@ protected function addCommandsSection(ArrayNodeDefinition $node) ->prototype('array')->children() ->scalarNode('type')->defaultValue('symfony')->end() ->scalarNode('command')->isRequired()->cannotBeEmpty()->end() - ->scalarNode('env')->defaultNull()->end() + ->arrayNode('env')->prototype('scalar')->end() ->end() ; } @@ -114,8 +140,36 @@ protected function addServersSection(ArrayNodeDefinition $node) ->scalarNode('host')->defaultValue('localhost')->end() ->scalarNode('rsync_port')->defaultNull()->end() ->scalarNode('ssh_port')->defaultValue(22)->cannotBeEmpty()->end() + ->scalarNode('username')->defaultNull()->end() ->scalarNode('password')->defaultNull()->end() + + ->scalarNode('symfony_command')->defaultValue('php app/console')->end() + + ->scalarNode('pubkey_file')->defaultNull()->end() + ->scalarNode('privkey_file')->defaultNull()->end() + ->scalarNode('passphrase')->defaultNull()->end() + + ->arrayNode('connect_methods') + ->children() + ->scalarNode('kex')->end() + ->scalarNode('hostkey')->end() + ->arrayNode('client_to_server') + ->children() + ->scalarNode('crypt')->end() + ->scalarNode('comp')->end() + ->scalarNode('mac')->end() + ->end() + ->end() + ->arrayNode('server_to_client') + ->children() + ->scalarNode('crypt')->end() + ->scalarNode('comp')->end() + ->scalarNode('mac')->end() + ->end() + ->end() + ->end() + ->end() ->scalarNode('path')->isRequired()->cannotBeEmpty()->end() ->arrayNode('rules') ->useAttributeAsKey('rules')->prototype('scalar')->end() diff --git a/Deployer/Deployer.php b/Deployer/Deployer.php index e3a8be8..c7b5096 100755 --- a/Deployer/Deployer.php +++ b/Deployer/Deployer.php @@ -67,12 +67,10 @@ public function test($server = null) */ protected function deploy($server = null, $real = false) { - if(is_null($server)) { - foreach($this->config->getServerNames() as $server) { - $this->deploy($server, $real); - } - - return; + if(is_null($server)){ + $serverNames = $this->config->getServerNames(); + reset($serverNames); + return $this->deploy(current($serverNames), $real); } $this->dispatcher->dispatch(Events::onDeploymentStart, new DeployerEvent($server, $real)); diff --git a/Deployer/Rsync.php b/Deployer/Rsync.php index 11df8ea..e7ab4f0 100644 --- a/Deployer/Rsync.php +++ b/Deployer/Rsync.php @@ -99,6 +99,7 @@ public function run(array $connection, array $rules, $real = false) $command = $this->buildCommand($connection, $rules, $real); $process = new Process($command, $root); + $process->setTimeout($this->config['timeout']); $this->stderr = array(); $this->stdout = array(); @@ -150,6 +151,10 @@ protected function buildCommand(array $connection, array $rules, $real = false) $options[] = '-p '.$connection['port']; } + if($connection['ssh_port']!=22) { + $options[] = sprintf('--rsh="ssh -p%d"', $connection['ssh_port']); + } + if ($this->config['delete']) { $options[] = '--delete'; } diff --git a/Deployer/Ssh.php b/Deployer/Ssh.php index 653f2ce..bd43411 100644 --- a/Deployer/Ssh.php +++ b/Deployer/Ssh.php @@ -2,6 +2,8 @@ namespace BeSimple\DeploymentBundle\Deployer; +use Sensio\Bundle\GeneratorBundle\Command\Helper\DialogHelper; +use Symfony\Component\Console\Output\ConsoleOutput; use Symfony\Component\EventDispatcher\EventDispatcherInterface; use BeSimple\DeploymentBundle\Event\CommandEvent; use BeSimple\DeploymentBundle\Event\FeedbackEvent; @@ -96,16 +98,13 @@ public function getStderr($glue = "\n") public function run(array $connection, array $commands, $real = false) { $this->connect($connection); - $this->execute(array('type' => 'shell', 'command' => sprintf('cd %s', $connection['path']))); - if ($real) { - foreach ($commands as $command) { - $this->execute($command); + if($real){ + foreach($commands as $command){ + $this->execute($command, $connection); } } - $this->disconnect(); - return $this->stdout; } @@ -116,26 +115,41 @@ public function run(array $connection, array $commands, $real = false) */ protected function connect(array $connection) { - $this->session = ssh2_connect($connection['host'], $connection['ssh_port']); + $methods = @$connection['connect_methods'] ?: @$this->config['connect_methods']; + $this->session = @ssh2_connect($connection['host'], $connection['ssh_port'], $methods); if (!$this->session) { throw new \InvalidArgumentException(sprintf('SSH connection failed on "%s:%s"', $connection['host'], $connection['ssh_port'])); } - if (isset($connection['username']) && isset($connection['pubkey_file']) && isset($connection['privkey_file'])) { - if (!ssh2_auth_pubkey_file($connection['username'], $connection['pubkey_file'], $connection['privkey_file'], $connection['passphrase'])) { - throw new \InvalidArgumentException(sprintf('SSH authentication failed for user "%s" with public key "%s"', $connection['username'], $connection['pubkey_file'])); + $username = @$connection['username'] ?: @$this->config['username']; + $password = @$connection['password'] ?: @$this->config['password']; + + if($username && $password){ + if(!ssh2_auth_password($this->session, $username, $password)){ + throw new \InvalidArgumentException(sprintf('SSH authentication failed for user "%s"', $username)); } - } else if ($connection['username'] && $connection['password']) { - if (!ssh2_auth_password($this->session, $connection['username'], $connection['password'])) { - throw new \InvalidArgumentException(sprintf('SSH authentication failed for user "%s"', $connection['username'])); + }elseif($username){ + $pubkey = @$connection['pubkey_file'] ?: @$this->config['pubkey_file']; + $privkey = @$connection['privkey_file'] ?: @$this->config['privkey_file']; + $passphrase = @$connection['passphrase'] ?: @$this->config['passphrase']; + + $co = new ConsoleOutput(); + $helper = new DialogHelper(); + $helper->setInputStream($this->stdin); + if(!$pubkey){ + $pubkey = $helper->ask($co,'pubkey_file?'); + } + if(!$privkey){ + $privkey = $helper->ask($co,'privkey_file?'); + } + if(!$passphrase){ + $passphrase = $helper->askHiddenResponse($co,'passphrase?'); } - } - - $this->shell = ssh2_shell($this->session); - if (!$this->shell) { - throw new \RuntimeException(sprintf('Failed opening "%s" shell', $this->config['shell'])); + if(!ssh2_auth_pubkey_file($this->session, $username, $pubkey, $privkey, $passphrase)){ + throw new \InvalidArgumentException(sprintf('SSH authentication failed for user "%s" with public key "%s"', $username, $pubkey)); + } } $this->stdout = array(); @@ -143,21 +157,12 @@ protected function connect(array $connection) } /** - * @return void - */ - protected function disconnect() - { - fclose($this->shell); - } - - /** - * @param array $command - * @return void + * @param array $commandArray + * @param array $connection */ - protected function execute(array $command) + protected function execute(array $commandArray, array $connection) { - $command = $this->buildCommand($command); - + $command = $this->buildCommand($commandArray, $connection); $this->dispatcher->dispatch(Events::onDeploymentSshStart, new CommandEvent($command)); $outStream = ssh2_exec($this->session, $command); @@ -166,22 +171,18 @@ protected function execute(array $command) stream_set_blocking($outStream, true); stream_set_blocking($errStream, true); - $stdout = explode("\n", stream_get_contents($outStream)); - $stderr = explode("\n", stream_get_contents($errStream)); + $stdout = stream_get_contents($outStream); + $stderr = stream_get_contents($errStream); - if (count($stdout)) { - $this->dispatcher->dispatch(Events::onDeploymentRsyncFeedback, new FeedbackEvent('out', implode("\n", $stdout))); + if($stdout){ + $this->dispatcher->dispatch(Events::onDeploymentSshFeedback, new FeedbackEvent('out', $stdout)); } - if (count($stdout)) { - $this->dispatcher->dispatch(Events::onDeploymentRsyncFeedback, new FeedbackEvent('err', implode("\n", $stderr))); + if($stderr){ + $this->dispatcher->dispatch(Events::onDeploymentSshFeedback, new FeedbackEvent('err', $stderr)); } - $this->stdout = array_merge($this->stdout, $stdout); - - if (is_array($stderr)) { - $this->stderr = array_merge($this->stderr, $stderr); - } else { + if(!$stderr){ $this->dispatcher->dispatch(Events::onDeploymentSshSuccess, new CommandEvent($command)); } @@ -190,18 +191,27 @@ protected function execute(array $command) } /** - * @param array $command + * @param array $commandArray + * @param array $connection * @return string + * @throws \InvalidArgumentException */ - protected function buildCommand(array $command) + protected function buildCommand(array $commandArray, array $connection) { - if ($command['type'] === 'shell') { - return $command['command']; + $envs = ''; + foreach($commandArray['env'] as $key => $value){ + $envs .= 'declare -x '.$key.'='.$value.' && '; } - $symfony = $this->config['symfony_command']; - $env = $command['env'] ?: $this->env; + switch($commandArray['type']){ + case 'shell': + return sprintf("%s%s", $envs, $commandArray['command']); + break; + case 'symfony': + return sprintf('%scd %s && %s %s', $envs, $connection['path'], $connection['symfony_command'], $commandArray['command']); + break; + } - return sprintf('%s %s --env="%s"', $symfony, $command['command'], $env); + throw new \InvalidArgumentException(sprintf('CommandType "%s" invalid', $commandArray['type'])); } } diff --git a/README.md b/README.md index 2f36e3c..4a570d6 100755 --- a/README.md +++ b/README.md @@ -1,14 +1,11 @@ Symfony2 applications deployment made easy ========================================== - *Up to date thanks to jonaswouters* - A few words ----------- - ###Description - Deploy your project usinc rsync (must be installed) in ssh mode. @@ -16,24 +13,20 @@ A few words - Easily create rules for rsync (ignore / force files). - Schedule commands to run on ditant server via ssh (SSH2 PHP extension required). - ###Links - Stable releases : [https://github.com/besimple/DeploymentBundle](https://github.com/besimple/DeploymentBundle) - Nightly builds : [https://github.com/jfsimon/DeploymentBundle](https://github.com/jfsimon/DeploymentBundle) - Rest documentation : *will come later* - ###Requirements - Rsync package : [http://samba.anu.edu.au/rsync/](http://samba.anu.edu.au/rsync/) - SSH2 extension : [http://fr.php.net/manual/en/book.ssh2.php](http://fr.php.net/manual/en/book.ssh2.php) - How to install -------------- - 1. Get the sources via GIT - Use clone method if not using GIT for your project @@ -44,7 +37,6 @@ How to install git submodule add git://github.com/besimple/DeploymentBundle.git vendor/BeSimple/DeploymentBundle - 2. Register bundle in `AppKernel` class // app/AppKernel.php @@ -55,7 +47,6 @@ How to install // ... ); - 3. Add `besimple_deployment` entry to your config file # app/config.yml @@ -67,7 +58,6 @@ How to install commands: ~ servers: ~ - 4. Add `BeSimple` namespace to autoload // app/autoload.php @@ -78,88 +68,78 @@ How to install // ... )); - How to configure ---------------- - ###An example be_simple_deployment: - - rsync: - delete: true - - ssh: - pubkey_file: /home/me/.ssh/id_rsa.pub - privkey_file: /home/me/.ssh/id_rsa - passwphrase: secret - - rules: - eclipse: - ignore: [.settings, .buildpath, .project] - git: - ignore: [.git, .git*, .svn] - symfony: - ignore: [/app/logs/*, /app/cache/*, /web/uploads/*, /web/*_dev.php] - - commands: - cache_warmup: - type: symfony - command: cache:warmup - fix_perms: - type: shell - command: ./bin/fix_perms.sh - - servers: - staging: - host: localhost - username: login - password: passwd - path: /path/to/project - rules: [eclipse, symfony] - commands: [cache_warmup, fix_perms] - production: - # ... - - -###Rsync configuration - -To be continued. - - -###SSH configuration - -To be continued. - - -###Rules configuration - -Rules can be declared as templates for reuse in your servers configuration. -Some templates are already bundled by default. The following parameters can be used : - -- ignore : masks for the files to be ignored -- force : ignored files can be forced this way - - -###Servers configuration - -Here is the full list of parameters : - -- host : -- rsync_port : -- ssh_port : -- username -- password : -- path : the path for your application root on the remote server -- rules : list of rules templates to apply -- commands : list of commands to trigger on destination server - + rsync: + delete: true + rules: + eclipse: + ignore: [.settings, .buildpath, .project] + netbeans: + ignore: [nbproject] + phpstorm: + ignore: [.idea] + git: + ignore: [.git, .git*] + svn: + ignore: [.svn] + symfony: + ignore: [/app/cache/*, /app/logs/*, /app/config/parameters.yml, /web/bundles/*, /web/uploads/*, /web/js/*, /web/css/*] + hosting: + ignore: [/.htaccess, /.htpasswd, /web/.htaccess, /web/.user.ini, /web/manage.php, /web/phpinfo.php, /web/ntunnel_mysql.php] + system: + ignore: [._*, .DS_Store] + commands: + cache_clear: + type: symfony + command: cache:clear + assetic_dump: + type: symfony + command: assetic:dump + assets_install: + type: symfony + command: assets:install + ssh: + connect_methods: + server_to_client: + crypt: rijndael-cbc@lysator.liu.se, aes256-cbc, aes192-cbc, aes128-cbc, 3des-cbc, blowfish-cbc, cast128-cbc, arcfour + client_to_server: + crypt: rijndael-cbc@lysator.liu.se, aes256-cbc, aes192-cbc, aes128-cbc, 3des-cbc, blowfish-cbc, cast128-cbc, arcfour + servers: + dev: + host: dev.server.ch + username: username + + pubkey_file: %deploy_dev_pubkey_file% + privkey_file: %deploy_dev_privkey_file% + passphrase: %deploy_dev_passphrase% + + path: /home/user/www/dev.project.com/ + rules: [eclipse, netbeans, phpstorm, git, svn, symfony, hosting, system] + commands: [cache_clear, assetic_dump, assets_install] + + symfony_command: php -c web/.user.ini app/console --env=dev + prod: + host: prod.server.ch + username: username + + pubkey_file: %deploy_prod_pubkey_file% + privkey_file: %deploy_prod_privkey_file% + passphrase: %deploy_prod_passphrase% + + path: /home/user/www/prod.project.com/ + rules: [eclipse, netbeans, phpstorm, git, svn, symfony, hosting, system] + commands: [cache_clear_dev, assetic_dump, assets_install] + + symfony_command: php -c web/.user.ini app/console --env=prod How to use ---------- - ###Using the commands The simpliest way to deploy your application is to use the command line, @@ -174,7 +154,6 @@ go into your project root folder and type the following commands : You can use the verbose option (`-v`) to get all feedback from rsync and remote ssh commands. - ###Using the service You can also use the deployment feature within your controller @@ -193,16 +172,14 @@ You can connect many events to know what's happening. - **onDeploymentStart** : fired on deployment start. - **onDeploymentSuccess** : fired on deployment success. - ###Rsync events - **onDeploymentRsyncStart** : fired when rsync is started. - **onDeploymentRsyncFeedback** : fired on each rsync `stdout` or `stderr` line. - **onDeploymentRsyncSuccess** : fired on rsync success. - ###SSH events - **onDeploymentSshStart** : fired when SSH command run. - **onDeploymentSshFeedback** : fired on each SSH `stdout` or `stderr` line. -- **onDeploymentSshSuccess** : fired on SSH command success. +- **onDeploymentSshSuccess** : fired on SSH command success. \ No newline at end of file diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..be9e3ca --- /dev/null +++ b/composer.json @@ -0,0 +1,20 @@ +{ + "name": "ibrows/be-simple-deployment-bundle", + "description": "Symfony2 applications deployment made easy", + "keywords": ["ibrows", "deployment", "deploy"], + "type": "symfony-bundle", + "license": "MIT", + "authors": [ + { + "name": "Mike Meier", + "email": "mike.meier@ibrows.ch" + } + ], + "require": { + "php": ">=5.3.0", + "symfony/framework-bundle": ">=2.0" + }, + "autoload": { + "psr-4": { "BeSimple\\DeploymentBundle\\": "" } + } +}