GithubHelp home page GithubHelp logo

capistrano / sshkit Goto Github PK

View Code? Open in Web Editor NEW
1.1K 1.1K 253.0 1.5 MB

A toolkit for deploying code and assets to servers in a repeatable, testable, reliable way.

License: MIT License

Ruby 100.00%
devops ruby ssh

sshkit's Introduction

Capistrano: A deployment automation tool built on Ruby, Rake, and SSH.

Gem Version Build Status Code Climate CodersClan

Capistrano is a framework for building automated deployment scripts. Although Capistrano itself is written in Ruby, it can easily be used to deploy projects of any language or framework, be it Rails, Java, or PHP.

Once installed, Capistrano gives you a cap tool to perform your deployments from the comfort of your command line.

$ cd my-capistrano-enabled-project
$ cap production deploy

When you run cap, Capistrano dutifully connects to your server(s) via SSH and executes the steps necessary to deploy your project. You can define those steps yourself by writing Rake tasks, or by using pre-built task libraries provided by the Capistrano community.

Tasks are simple to make. Here's an example:

task :restart_sidekiq do
  on roles(:worker) do
    execute :service, "sidekiq restart"
  end
end
after "deploy:published", "restart_sidekiq"

Note: This documentation is for the current version of Capistrano (3.x). If you are looking for Capistrano 2.x documentation, you can find it in this archive.


Contents

Features

There are many ways to automate deployments, from simple rsync bash scripts to complex containerized toolchains. Capistrano sits somewhere in the middle: it automates what you already know how to do manually with SSH, but in a repeatable, scalable fashion. There is no magic here!

Here's what makes Capistrano great:

Strong conventions

Capistrano defines a standard deployment process that all Capistrano-enabled projects follow by default. You don't have to decide how to structure your scripts, where deployed files should be placed on the server, or how to perform common tasks: Capistrano has done this work for you.

Multiple stages

Define your deployment once, and then easily parameterize it for multiple stages (environments), e.g. qa, staging, and production. No copy-and-paste necessary: you only need to specify what is different for each stage, like IP addresses.

Parallel execution

Deploying to a fleet of app servers? Capistrano can run each deployment task concurrently across those servers and uses connection pooling for speed.

Server roles

Your application may need many different types of servers: a database server, an app server, two web servers, and a job queue work server, for example. Capistrano lets you tag each server with one or more roles, so you can control what tasks are executed where.

Community driven

Capistrano is easily extensible using the rubygems package manager. Deploying a Rails app? Wordpress? Laravel? Chances are, someone has already written Capistrano tasks for your framework of choice and has distributed it as a gem. Many Ruby projects also come with Capistrano tasks built-in.

It's just SSH

Everything in Capistrano comes down to running SSH commands on remote servers. On the one hand, that makes Capistrano simple. On the other hand, if you aren't comfortable SSH-ing into a Linux box and doing stuff on the command-line, then Capistrano is probably not for you.

Gotchas

While Capistrano ships with a strong set of conventions that are common for all types of deployments, it needs help understanding the specifics of your project, and there are some things Capistrano is not suited to do.

Project specifics

Out of the box, Capistrano can deploy your code to server(s), but it does not know how to execute your code. Does foreman need to be run? Does Apache need to be restarted? You'll need to tell Capistrano how to do this part by writing these deployment steps yourself, or by finding a gem in the Capistrano community that does it for you.

Key-based SSH

Capistrano depends on connecting to your server(s) with SSH using key-based (i.e. password-less) authentication. You'll need this working before you can use Capistrano.

Provisioning

Likewise, your server(s) will likely need supporting software installed before you can perform a deployment. Capistrano itself has no requirements other than SSH, but your application probably needs database software, a web server like Apache or Nginx, and a language runtime like Java, Ruby, or PHP. These server provisioning steps are not done by Capistrano.

sudo, etc.

Capistrano is designed to deploy using a single, non-privileged SSH user, using a non-interactive SSH session. If your deployment requires sudo, interactive prompts, authenticating as one user but running commands as another, you can probably accomplish this with Capistrano, but it may be difficult. Your automated deployments will be much smoother if you can avoid such requirements.

Shells

Capistrano 3 expects a POSIX shell like Bash or Sh. Shells like tcsh, csh, and such may work, but probably will not.

Quick start

Requirements

  • Ruby version 2.0 or higher on your local machine (MRI or Rubinius)
  • A project that uses source control (Git, Mercurial, and Subversion support is built-in)
  • The SCM binaries (e.g. git, hg) needed to check out your project must be installed on the server(s) you are deploying to
  • Bundler, along with a Gemfile for your project, are recommended

Install the Capistrano gem

Add Capistrano to your project's Gemfile using require: false:

group :development do
  gem "capistrano", "~> 3.17", require: false
end

Then run Bundler to ensure Capistrano is downloaded and installed:

$ bundle install

"Capify" your project

Make sure your project doesn't already have a "Capfile" or "capfile" present. Then run:

$ bundle exec cap install

This creates all the necessary configuration files and directory structure for a Capistrano-enabled project with two stages, staging and production:

├── Capfile
├── config
│   ├── deploy
│   │   ├── production.rb
│   │   └── staging.rb
│   └── deploy.rb
└── lib
    └── capistrano
            └── tasks

To customize the stages that are created, use:

$ bundle exec cap install STAGES=local,sandbox,qa,production

Note that the files that Capistrano creates are simply templates to get you started. Make sure to edit the deploy.rb and stage files so that they contain values appropriate for your project and your target servers.

Command-line usage

# list all available tasks
$ bundle exec cap -T

# deploy to the staging environment
$ bundle exec cap staging deploy

# deploy to the production environment
$ bundle exec cap production deploy

# simulate deploying to the production environment
# does not actually do anything
$ bundle exec cap production deploy --dry-run

# list task dependencies
$ bundle exec cap production deploy --prereqs

# trace through task invocations
$ bundle exec cap production deploy --trace

# lists all config variable before deployment tasks
$ bundle exec cap production deploy --print-config-variables

Finding help and documentation

Capistrano is a large project encompassing multiple GitHub repositories and a community of plugins, and it can be overwhelming when you are just getting started. Here are resources that can help:

Related GitHub repositories:

  • capistrano/sshkit provides the SSH behavior that underlies Capistrano (when you use execute in a Capistrano task, you are using SSHKit)
  • capistrano/rails is a very popular gem that adds Ruby on Rails deployment tasks
  • mattbrictson/airbrussh provides Capistrano's default log formatting

GitHub issues are for bug reports and feature requests. Please refer to the CONTRIBUTING document for guidelines on submitting GitHub issues.

If you think you may have discovered a security vulnerability in Capistrano, do not open a GitHub issue. Instead, please send a report to [email protected].

How to contribute

Contributions to Capistrano, in the form of code, documentation or idea, are gladly accepted. Read the DEVELOPMENT document to learn how to hack on Capistrano's code, run the tests, and contribute your first pull request.

License

MIT License (MIT)

Copyright (c) 2012-2020 Tom Clements, Lee Hambley

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

sshkit's People

Contributors

akm avatar azrle avatar betesh avatar bobziuchkovski avatar bretweinraub avatar byroot avatar cheald avatar colorbox avatar dependabot[bot] avatar fjan avatar grosser avatar hab278 avatar kirs avatar leehambley avatar mattbrictson avatar miry avatar nikolayrys avatar norsegaud avatar rgo avatar robd avatar seanhandley avatar seenmyfate avatar shirosaki avatar sinjo avatar sj26 avatar steved avatar tisba avatar townsen avatar will-in-wi avatar wmontgomery-splunk avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

sshkit's Issues

Nesting 'with' fails

I have some example code below from my Capistrano v3 tasks for Django. It seems that nesting 'with' calls is broken. Is this functionality supposed to be supported? The docs state "One will notice that it's quite low level, but exposes a convenient API, the as()/within()/with() are nestable in any order, repeatable, and stackable."

My current work around is to wrap the body of with_virtualenv in a begin-rescue block that captures the NameError and check for the message "instance variable @_env not defined".

Note that my intention is to keep the Django and virtualenv tasks in separate files/gems, hence my not simply merging the with calls.

def with_virtualenv(&block)
  with path: "#{fetch(:virtualenv_dir)}/bin:$PATH" do
    block.call
  end
end

def django_manage(*args)
  within release_path do
    with_virtualenv do
      with app_environment: fetch(:stage) do
        execute :python, 'manage.py', args
      end
    end
  end
end

# Results in "instance variable @_env not defined"
namespace :django do
  task :migrate do
    on primary fetch(:migration_role) do
      django_manage 'syncdb', '--noinput', '--migrate'
    end
  end
end

`#as` and `#with` don't work together

The following script triggers the error:

require 'sshkit/dsl'

SSHKit.config.output_verbosity = Logger::DEBUG

run_locally do
  as :root do
    with foo: 'bar' do
      execute :env
    end
  end
end

It doesn't print the FOO environment variable. This happens because environment variables should be set after calling /usr/bin/env, not before everything else in the command, as it is.

For example, the command generated by the script above is:

$ FOO=bar sudo su root -c "/usr/bin/env env"

It should be:

$ sudo su root -c "/usr/bin/env FOO=bar env"

For this reason, I believe the first example in README is broken:

require 'sshkit/dsl'

on %w{1.example.com 2.example.com}, in: :sequence, wait: 5 do
  within "/opt/sites/example.com" do
    as :deploy  do
      with rails_env: :production do
        rake   "assets:precompile"
        runner "S3::Sync.notify"
      end
    end
  end
end

Maybe this line in README is about this issue:

No environment handling (sshkit might not need to care)

If that is the case, I believe it would be nice to make it clear.

If this is really an issue, please let me know so I can work to fix it.

within(dir) not applied for text commands

I'm trying to run "foreman export" command on a server. This seems to be an intersection where capistrano 3, rvm and bundler aren't coming together for me.

I'd like to just do this in a capistrano "on" block:

on roles(:thingy) do
  execute :foreman, "export ...etc..."
end

The interactive session in the server works just fine using 'bundle exec foreman', and ruby is set up via the login shell.

Whenever I use execute with a raw string, then all of the environment variables (with) and current directories (within) are not applied. Is this normal?

What I want it to generate is this command (as an example):

ssh user@server "cd /apps/mobiledataanywhere/current && ~/.rvm/bin/rvm 2.0.0 do bundle exec foreman help export"

This works, but I cannot find how to prefix a command by bundle exec, prefix that by dvm do, and prefix that by environment and prefix that by put it in a directory.

Is there a way???

Anyway, the point of this issue was that in a within(dir) block, commands that are mapped (such as :rake) get the directory prefixed into the command correctly, but string only commands do not. e.g.:

This code:

          within(release_path) do
              execute :ruby, '-e', %Q["puts 'hello'"]
              set :rvm_do, -> { "#{fetch(:rvm_path)}/bin/rvm #{fetch(:rvm_ruby_version)} do" }
              execute %Q[#{fetch(:rvm_do)} ruby -e "puts 'hello'"]
          end

Generate these commands:

DEBUG [849c50cc] Command: cd /my/release/path && ~/.rvm/bin/rvm 2.0.0 do ruby -e "puts 'hello'"
DEBUG [7a309cb5] Command: ~/.rvm/bin/rvm 2.0.0 do ruby -e "puts 'hello'"

Notice the directory

Re-using connections between ok() calls.

Seems like a recurring problem that Capistrano is tripping warnings about opening too many connections.

Things to consider are:

  • What if the cached connection has died (re-open?)
  • Closing connection at teardown?

GIT_SSH Wrapper (Prevents strict host checking error on first host connect)

I've seen this technique for the first time today, it's really really smooth, it's used in Chef's deployment resource, which works quite smoothly, but this Ruby implementation is also really neat.

This delivers two features:

  1. Specify the key explicitly, misses out the chance that the ssh sub system will figure out the wrong key, or fail on agent forwarding/etc
  2. Enables the user to disable strict host key checking, which is actually the point of these things in Chef, at least.

Resources:

Support project-local known hosts file.

I didn't know (until reading the docs) that SSH supports multiple known hosts files, how cool would it be if we could check in to source control (with all the integrity benefits that come with it) the list of known hosts for a given project, and refer to that list when deploying/working with sshkit?

Bundler is broken

root@Ubuntu-1204-precise-64-minimal ~ # bundle --trace
Unknown switches '--trace'
root@Ubuntu-1204-precise-64-minimal ~ # echo $?
0

That is written on stderr, and then it exits with a success status.

Net::SSH::Shell

I think we should consider using this to offer stateful shells to the on() block. Otherwise this doesn't do what a user expects

on 'example.com' do
  cd "/tmp"
  run "rm -rf *"
end

In this first example, the user will have just blown away the whole server, as the state of cd isn't preserved, the shell is non-interactive.

The problem is slightly mitigated by the following"

on 'example.com' do
  in "/tmp" do
    run "rm -rf *"
  end
end

Which will in effect run cd /tmp && rm -rf * as one command, but it might be saner to look at keeping the shell state using Net::SSH::Shell in order to allow the other use-case (although that said, I think the on(), in(), as(), with() API is very powerful for almost every case.

with() escapes paths too aggressively

Trying to augment the path with:

with path: '/usr/bin/weird:$PATH' do
  #  ....
end

becomes:

( PATH=/usr/bin/weird:\$PATH  ..... )

(note the backslashed $ in the $PATH)

SSHKit::DSL breaks RSpec feature specs

After adding the sshkit gem in a Rails project Gemfile and running RSpec feature specs, I get really strange errors.

group :test do
  gem 'sshkit'
end

If I comment the following line https://github.com/leehambley/sshkit/blob/master/lib/sshkit/dsl.rb#L19 and use the non-dsl API then everything starts working again. I imagine that somehow, the on method overrides some other classes/modules on method.

Here the error when running the specs:

An error occurred in an after hook
  ActionView::Template::Error: no block given
  occurred at /Users/damselem/.rvm/gems/ruby-2.0.0-p247/gems/sshkit-0.0.34/lib/sshkit/backends/netssh.rb:42:in `instance_exec'`

On exit<>0 netssh backend deletes all command output

_execute() in the netssh backend assigns '' to stdout and stderr when a non zero exit code which automatically produces the "command stderr: Nothing written" & "command stdout: Nothing written" logs. Is there a reason stdout/stderr are nixed?

This is confusing especially if the command has been dutifully printing things on stdout.

Also throwing an exception on exit<>0 by default totally destroys the usefulness of capture(). capture() should probably add raise_on_non_zero_exit: false in the arguments like test() does.

The point is that in most cases capturing the output is significant when there is an error - the successful case is the boring one. The way SSHKit's API is at the moment it seems it does not provide an easy way to grab the log and determine that a command failed.

The naive approach would be to let capture() return a tuple: [bool,output]

Basic Sanity test

Is there an example we can run to validate both connection and receipt of output via commands? I'm connecting fine, and actually have fully deployments for aws instances with nginx/rails/unicorn working just fine. But whenever I change anything I have to debug from server logs.

I have no 'output' coming back from any commands.
So if for instance I enter bundle exec cap production console, I enter the console, and can execute commands, but I get no output from them. This makes it hard to debug things like setting environment vars or other little deployment helpers.

If there is a test suite of verification step I can run that would tell me, I 'should' be getting output from commands, that would be very helpful.

in: :sequence not working when used with capistrano roles

Not sure if I am doing this right and if it is capistrano or ssh kit related:

 on (roles :app), in: :sequence do |host|

does execute on all servers in parallel, while

 on %w{appserver-1, appserver-2, appserver-3}, in: :sequence do |host|

does execute sequentially.

Not sure if I am doing something wrong here...

GPL copyleft affects any software deployed with Capistrano?

Hi,

Perhaps this is a question for a lawyer rather than a GitHub issue, but I thought I would ask here anyway:

If my (proprietary) software requires Capistrano for deployment, and Capistrano (v3) in turn requires SSHKit, which is GPL software, does that mean that my software is bound by the terms of the GPL? After all, SSHKit is in my Gemfile.lock and "incorporated" into my program for the purposes of deployment.

For some perspective, popular deployment tools like Chef and Puppet are notably not GPL, and in fact Puppet switched from GPL to Apache license in 2011 specifically to address concerns from companies wanting to use Puppet but worried about licensing.

Maybe I am being paranoid here. What is your stance on this?

Thanks.

command_map for piped commands

Given:
The SSHKit (capistrano) command in question is:
execute :curl, "-s", fetch(:composer_download_url), "|", :php

My PHP binary is in the following path and set so:
SSHKit.config.command_map[:php] = '/usr/local/bin/php54'

Problem:
The :curl symbol is correctly replaced from the command map, but the :php symbol is not.

I've tested replacement by changing the :curl value in the command_map and succeeded, so I'm guessing it's because :php is in a pipe or is the second substitution that it is not replaced?

Please explain command.rb:94:in `exit_status=' in Capistrano run

Sorry, for posting capistrano issue here, but it really goes to SSHKit, imho.
So, i've made custom capistrano task, for running background delayed_job processes:

namespace :delayed_job do
  def rails_env
    fetch(:stage) ? "RAILS_ENV=#{fetch(:stage)}" : ''
  end

  desc "Restarts all delayed jobs"
  task :restart do
    on roles(:all) do
      SSHKit.config.command_map[:delayed_job] = "#{ rails_env } script/delayed_job"
      # It's just for running this simple command
      # cd /srv/myapp/current && RAILS_ENV=parsing script/delayed_job --queues=mailer restart -i "mailers"
      within current_path do
            execute :delayed_job, "--queues=mailer restart -i \"mailers\""
      end
    end
  end  

end

As I've mentioned, I just want to run cd /srv/myapp/current && RAILS_ENV=parsing script/delayed_job --queues=mailer restart -i "mailers" and it works as needed on server. But not with capistrano, and I keep getting such error:

 INFO [2ddcee9f] Running RAILS_ENV=parsing script/delayed_job --queues=mailer restart -i "mailers" on 123.123.123.123
DEBUG [2ddcee9f] Command: cd /srv/myapp/current && RAILS_ENV=parsing script/delayed_job --queues=mailer restart -i "mailers"
cap aborted!
delayed_job stdout: Nothing written
delayed_job stderr: Nothing written
.../vendor/bundle/gems/sshkit-1.0.0/lib/sshkit/command.rb:94:in `exit_status='

How I can remap this command?

Please note, that I just want to migrate such Capistrano v2 construction, to v3: run "cd #{current_path};#{rails_env} script/delayed_job --queues=mailer restart -i \"mailers\""

Thanks in advance.

Block functionality lost when using a heredoc (or string)

In the execute, if you have a string or heredoc, the surrounding blocks are ignored.
Was that done intentionally? Kind of like if you don't use a symbol to start the command line statement, you don't get any syntactical sugar? I wasn't sure if this is really an issue that you're interested in but I have a usecase for this and it seems like others could too.

My case is like below:

on 'google.com' do
  within release_path do
    execute <<-SCRIPT
      bundle exec build.mysql2 --with-mysql-include=/blah/blah
      bundle exec build.nokogiri --with-xml-lib=/blah/blah
    SCRIPT
  end
end

Thanks for taking a look at this.

Multiple ENV vars don't stack

with foo: 'bar', baz: 'boo' do
  # .....
end

The command is expanded to ( FOO=barBAZ=boo ........ ) which is obviously wrong.

SSHKit::Configuration.format doesn't pick up on "BlackHole"

Gives the following error.

uninitialized constant SSHKit::Formatter::Blackhole
/sshkit/lib/sshkit/configuration.rb:47:in `const_get'
/sshkit/lib/sshkit/configuration.rb:47:in `formatter'
/sshkit/lib/sshkit/configuration.rb:29:in `format='

`test()` in `within(dir)` should run after cd into dir

Given a test() in a within(),

within('/home/vagrant/app1/current') do
  if test '[ -e tmp/pids/server.pid ]'
     execute :kill, '-INT', 'tmp/pids/server.pid'
  end
end

Does it make sense to generate the following commands?

cd /home/vagrant/app1/current && [ -e tmp/pids/server.pid ]
cd /home/vagrant/app1/current && (/usr/bin/env kill -INT tmp/pids/server.pid)

However, currently it doesn't cd into the given directory when generate test() command:

[ -e tmp/pids/server.pid ]
cd /home/vagrant/app1/current && (/usr/bin/env kill -INT tmp/pids/server.pid)

on(hosts, options = {}, &block) Command

This is the main entry point for doing anything with deploy.rb, here's a run-down of how it should work:

Given the following

hosts = (0..20).collect { |n| "#{n}.example.com" }
on( hosts , in: :sequence, limit: 2) do
   # ... snip ...
end
  1. All addresses should be resolved, ahead of time. In the case of unreachable hosts, an exception should be raised of a unique, catchable type.

  2. A new instance of connection pool should be instantiated, this component may be responsible for connecting to the hosts, and raising the exception.

  3. A new instance of something to manage the connection pool (worker pool?) should be instantiated, this should be responsible for enforcing the limit of two (limit: 2) hosts, and operating in sequence (in: :sequence). Other options might be in: :parallel (the default) and (limit: nil).

  4. In the context of this piece of code, the on(), in(), as(), and with() commands should be executed once per host. Importantly the block cannot be somehow resolved and then passed in completion (as a string to run) to it's host, the block given should be yielded in this context, so that the block may do something such as:

    on(hosts) do
      if capture('uptime').split(" ")[2] > 365 # we've been up too long, fake some downtime
        as :root { run "reboot -h now" }
      end
    end
    
  5. At the creation of the connection pool, it should be added to a list of connection pools to be closed, an at_exit handler somewhere should be responsible for hanging up the connections before the program terminates.

warning: already initialized constant StandardError

I'm using Capistrano 3.0.0 with jRuby 1.7.4 and I'm getting the warning below every time I run the cap command.

  $ cap install
  /gems/sshkit-0.0.34/lib/sshkit.rb:4 warning: already initialized constant StandardError
  mkdir -p config/deploy
  create config/deploy.rb
  create config/deploy/staging.rb
  create config/deploy/production.rb
  mkdir -p lib/capistrano/tasks
  Capified

Any ideas?

A case for assert_shell_equal

I'm working on the CommandContext spike in a branch right now, and notice that the formatting specifics of the shell commands is getting in the way of real work.

I'd propose that we write something like assert_shell_equal (_equivalent might be better?) implementation something like this:

class ShellCommand < String
  def initialize(string)
    @string = string
  end
  # Naïve implementation, probably need to build a stateful parser to
  # do a decent job of this without blowing up file paths/etc
  def to_minified_s
     string.gsub!(/\n/, '; ') #New lines for semicolon
     string.gsub!(/;\s?;/, ';') # Strip double semi-colons (empty commands)
     string.gsub(/\s+/, ' ') # Strip duplicate space
  end
  def to_formatted_s
    # imagine this makes it beautiful and highlights for ANSI with pygments, or something
  end
end

Usage via assert_shell_equal

def assert_shell_equal(a, b)
  assert_equal ShellCommand.new(a).to_minified_s, ShellCommand.new(b).to_minified_s
end

Then one would be able to write a test such as:

def test_equal_things_are_equal
   assert_shell_equal "echo 'Hello World'\n\ntrue", "echo 'Hello World'; true"
end

Support project-local SSH options.

This can probably be done with the :config option for Net::SSH.start.

From their documentation:

:config => set to true to load the default OpenSSH config files (~/.ssh/config, /etc/ssh_config), or to false to not load them, or to a file-name (or array of file-names) to load those specific configuration files. Defaults to true.

That sounds like we should override that to always be Net::SSH::Config.default_files + [__dir_+"/config/ssh"]. (Or something.)

Fix handling of output streams, including in cases of error.

In SSHKit we do some weird stuff with stream handling depending on the packets (sic) we receive back from Net::SSH.

I'd like to write a test case script that produces buffered, and unbuffered output, line- and character-wise, on standard error, and standard out to test the correct usage.

With a pathologically badly behaved script, we could find a way to make this work reliably, I am sure.

multiline bash with if

If have the following bash block:

  execute <<-EOBLOCK
    set -e
    if [ -L #{current_path} ]; then
      service #{service_name} stop
    fi
    rm -f #{current_path}
    ln -s #{release_path} #{current_path}
    service #{service_name} start
  EOBLOCK

what sshkit/capistrano runs on the server is

/usr/bin/env if [ -L /var/play/hello-b/release/CURRENT ]; then; service play-hello-b stop; fi; rm -f /var/play/hello-b/release/CURRENT; ln -s /var/play/hello-b/release/hello-b-1.0-SNAPSHOT /var/play/hello-b/release/CURRENT; service play-hello-b start

the problem seems to be the added semicolon after the then statement then;, this causes an error.

I am not sure, if I am doing something wrong or if this is a problem with the muliline command conversion in sshkit.

Running multiple commands simultaneously on a single role/server

In the capistrano-resque gem, we were previously using threads to start multiple Resque workers at the same time:

# This code is in a loop for X number of pids
threads << Thread.new(pid) do |pid|
  on roles(role) do
    info "Starting worker for QUEUE: #{queue}" 
    within current_path do
      execute :rake, %{RAILS_ENV=#{fetch(:rails_env)} QUEUE="#{queue}" PIDFILE=#{pid} BACKGROUND=yes VERBOSE=1 INTERVAL=#{fetch(:interval)} #{"environment" if fetch(:resque_environment_task)} resque:work}
    end
  end
end

# After the loop, we wait on the threads
threads.each(&:join)

Assuming 2 workers, it starts 2 threads. It's now resulting in output like this, and capistrano hangs waiting for the commands to finish.

 INFO Starting worker(s) with QUEUE: foo
DEBUG [88e9068c] Running /usr/bin/env if test ! -d /data/www/capistrano-resque-test-app/current; then echo "Directory does not exist '/data/www/capistrano-resque-test-app/current'" 1>&2; false; fi on ec2-2.petefowler.com
 INFO Starting worker(s) with QUEUE: foo
DEBUG [88e9068c] Command: if test ! -d /data/www/capistrano-resque-test-app/current; then echo "Directory does not exist '/data/www/capistrano-resque-test-app/current'" 1>&2; false; fi
DEBUG [d78b4902] Running /usr/bin/env if test ! -d /data/www/capistrano-resque-test-app/current; then echo "Directory does not exist '/data/www/capistrano-resque-test-app/current'" 1>&2; false; fi on ec2-2.petefowler.com
DEBUG [d78b4902] Command: if test ! -d /data/www/capistrano-resque-test-app/current; then echo "Directory does not exist '/data/www/capistrano-resque-test-app/current'" 1>&2; false; fi

As far as I can tell, the within block first tests the current_path, then executes the code within. However, that directory test doesn't seem to be completing.

This worked fine with SSHKit 1.2.0, so I assume it has something to do with reusing SSH connections in 1.3.0? Is there a "correct" way to be handling simultaneous commands, or are we stuck running them sequentially for now?

High level API

This is a summary of what I've had in mind for this:

End user can proceed with:

    > deploy my_app production master

    > setup my_app staging develop

So lib/deploy/production.rb

    require 'deploy/rails' # standard deploy included in gem
    require 'deploy/my_data_centre' # my own gem extensions

    module Deploy
      class Production
        include Deploy::Rails
        include MyDataCentre::Roles
        include Deploy::Notifier::Twitter
      end
    end

Rails users could get a version of the file above with:

    rails g install deploy

Which would generate:

    require 'deploy/rails'
    # comments/instructions here
    module Deploy
      class Production
        include Deploy::Rails
      end
    end

Allowing either rake task or executable to call

    configuration = Deploy::Configuration.new(parse_config)
    Deploy::Production.new(configuration).deploy

I would imagine configuration to be used for over-riding default configuration for symlinks, and setting up shared folders.

            {
              :name => 'my_app',
              :shared_directories => ['system', 'config', 'bundle', 'bin'],
              :normal_symlinks => ['config/database.yml', 'bin', 'log'],
              :weird_symlinks =>  {
                'system' => 'public/system',
                'pids'   => 'tmp/pids',
                'bundle' => 'vendor/bundle',
                'cache' => 'tmp/cache',
                'sockets'    => 'tmp/sockets'
              }
            }

But when using defaults, it could be as simple as

    { :name => 'my_app' }

Allowing the code above to become:

    config = Deploy::Configuration.new(:name => 'my_app')
    Deploy::Production.new(config).deploy

Going back to the first bit of code, I imagine the Deploy::Rails module to looks something like this:

    module Deploy
      # class per env
      class Rails
        include Deploy::SCM::Git
        include Deploy::DependencyMangement::Bundler
        include Deploy::AppServer::Unicorn
        include Deploy::Command::Rake

        def deploy
          update
          bundle
          symlink
          migrate
          restart
        end

        # replace 'cold'
        # prompt for yml configuration not in repo
        # or allow 'accept defaults' which can be read from a local file
        def setup
          build_directory_structure
          clone
          symlink
          bundle
          migrate
          cleanup #(fix permissions/ownership, touch assets)
          notify
        end
      end
    end

A rack app version could implement deploy with just update and restart.

You could imagine a scenario where PAAS customers could deploy with a lib/deploy/production.rb file that just contains:

    require 'deploy/engine_yard'

I imagine the roles to be something like this, allowing the module to be shared between applications

module MyDataCentre
  module Roles
    def web
      %w{10.1.1.1 10.1.1.2}
    end

    def app

    end

    def database

    end 
  end
end

All the modules would need to confirm to an interface per type, so switch unicorn for mongrel, or nginx for apache is trivial.

    module Deploy
      class SCM::Git < SCM
        def update

        end

        def clone

        end
      end
    end

    module Deploy
      class Unicorn < AppServer
        def start

        end

        def stop

        end

        def status

        end

        def restart

        end
      end
    end

Or maybe something like this

module Deploy

    class Production
      include SomeComannds
      include Dispatchable

      def deploy
        Dispatcher.new(self).start!
      end
    end

    class Dispatcher
      attr_accessor :state

      def initialize(deploy)
      end

      def start!
        commands.each { command.perform }
      end
    end

    class Command
      #as, on, in
      attr_accessor :identity, :target, :context, :command

     def perform
       #
     end
    end
  end

Capistrano: run with options --dry-run

cap local deploy --dry-run --trace
** Invoke local (first_time)
** Execute local
** Invoke load:defaults (first_time)
** Execute load:defaults
** Invoke deploy (first_time)
** Execute deploy
** Invoke deploy:starting (first_time)
** Execute deploy:starting
** Invoke deploy:check (first_time)
** Execute deploy:check
** Invoke git:check (first_time)
** Invoke git:wrapper (first_time)
** Execute git:wrapper
cap aborted!
undefined method `verbosity' for "/usr/bin/env #<StringIO:0x007ff7a9827118> /tmp/git-ssh.sh":String
/Users/stamm/.rbenv/versions/2.0.0-p247/lib/ruby/gems/2.0.0/gems/sshkit-1.1.0/lib/sshkit/formatters/pretty.rb:10:in `write'
/Users/stamm/.rbenv/versions/2.0.0-p247/lib/ruby/gems/2.0.0/gems/sshkit-1.1.0/lib/sshkit/backends/printer.rb:14:in `block in execute'
/Users/stamm/.rbenv/versions/2.0.0-p247/lib/ruby/gems/2.0.0/gems/sshkit-1.1.0/lib/sshkit/backends/printer.rb:13:in `tap'
/Users/stamm/.rbenv/versions/2.0.0-p247/lib/ruby/gems/2.0.0/gems/sshkit-1.1.0/lib/sshkit/backends/printer.rb:13:in `execute'
/Users/stamm/.rbenv/versions/2.0.0-p247/lib/ruby/gems/2.0.0/gems/capistrano-3.0.0/lib/capistrano/tasks/git.rake:11:in `block (3 levels) in <top (required)>'
/Users/stamm/.rbenv/versions/2.0.0-p247/lib/ruby/gems/2.0.0/gems/sshkit-1.1.0/lib/sshkit/backends/printer.rb:9:in `instance_exec'
/Users/stamm/.rbenv/versions/2.0.0-p247/lib/ruby/gems/2.0.0/gems/sshkit-1.1.0/lib/sshkit/backends/printer.rb:9:in `run'
/Users/stamm/.rbenv/versions/2.0.0-p247/lib/ruby/gems/2.0.0/gems/sshkit-1.1.0/lib/sshkit/runners/parallel.rb:12:in `block (2 levels) in execute'
Tasks: TOP => git:check => git:wrapper

In https://github.com/leehambley/sshkit/blob/ffbce7622ae57bb960d4a75375f5afb18b9228b7/lib/sshkit/backends/printer.rb#L14

output << cmd.to_s

This line call SSHKit::Formatter::Pretty.write() with object String

If temporary comment this line
https://github.com/leehambley/sshkit/blob/ffbce7622ae57bb960d4a75375f5afb18b9228b7/lib/sshkit/formatters/pretty.rb#L10
I will get messages:

Output formatter doesn't know how to handle String

In 5 minutes I have change line to:

return if obj.respond_to?(:verbosity) && obj.verbosity < SSHKit.config.output_verbosity

And add more condition:

when ::String then original_output << obj + "\n"

It's look like what I want.

cap local deploy --dry-run
/usr/bin/env #<StringIO:0x007f92cb827368> /tmp/git-ssh.sh
/usr/bin/env chmod +x /tmp/git-ssh.sh
/usr/bin/env git ls-remote [email protected]:stamm/grape.git
/usr/bin/env mkdir -pv /var/www/shared /var/www/releases
/usr/bin/env mkdir -pv /var/www/shared/log /var/www/shared/tmp/pids /var/www/shared/tmp/cache /var/www/shared/tmp/sockets
/usr/bin/env mkdir -pv /var/www/shared/config
/usr/bin/env [ -f /var/www/shared/config/thin.yml ]
/usr/bin/env [ -f /var/www/repo/HEAD ]
 INFO The repository mirror is at /var/www/repo
/usr/bin/env if test ! -d /var/www/repo; then echo "Directory does not exist '/var/www/repo'" 1>&2; false; fi
/usr/bin/env git remote update
/usr/bin/env if test ! -d /var/www/repo; then echo "Directory does not exist '/var/www/repo'" 1>&2; false; fi
/usr/bin/env mkdir -p /var/www/releases/20131028002432
/usr/bin/env git archive master | tar -x -C /var/www/releases/20131028002432
/usr/bin/env mkdir -pv /var/www/releases/20131028002432/config
/usr/bin/env [ -L /var/www/releases/20131028002432/config/thin.yml ]
/usr/bin/env mkdir -pv /var/www/releases/20131028002432 /var/www/releases/20131028002432/tmp /var/www/releases/20131028002432/tmp /var/www/releases/20131028002432/tmp
/usr/bin/env [ -L /var/www/releases/20131028002432/log ]
/usr/bin/env [ -L /var/www/releases/20131028002432/tmp/pids ]
/usr/bin/env [ -L /var/www/releases/20131028002432/tmp/cache ]
/usr/bin/env [ -L /var/www/releases/20131028002432/tmp/sockets ]
/usr/bin/env if test ! -d /var/www/releases/20131028002432; then echo "Directory does not exist '/var/www/releases/20131028002432'" 1>&2; false; fi
/usr/bin/env bundle --gemfile /var/www/releases/20131028002432/Gemfile --path /var/www/shared/bundle --deployment --quiet --binstubs /var/www/shared/bin --without development test
/usr/bin/env rm -rf /var/www/current
/usr/bin/env ln -s /var/www/releases/20131028002432 /var/www/current
/usr/bin/env if test ! -d /var/www/releases/20131028002432; then echo "Directory does not exist '/var/www/releases/20131028002432'" 1>&2; false; fi
/usr/bin/env /var/www/shared/bin/thin restart -C /var/www/releases/20131028002432/config/thin.yml
/usr/bin/env ls -x /var/www/releases
/usr/bin/env if test ! -d /var/www/releases; then echo "Directory does not exist '/var/www/releases'" 1>&2; false; fi
/usr/bin/env echo "Branch master deployed as release 20131028002432 by stamm; " >> /var/www/revisions.log

I knew that is bad idea.
What you think about this problem?

:wait option is broken

Using 1.1.0. The wait: n option for in: :sequence/:groups is being ignored. I lookd over the code and can't see on's options being used anywhere, except for :in. I'm happy to submit a patch, but I wanted to verify that I wasn't overlooking something first.

within should wrap command in parentheses

When we use

within '/my/directory'
  background(:sleep, 10)
end

it won't return immediately since it will generate this command
cd /my/directory && /usr/bin/env nohup sleep 10 &> /dev/null &
and the && take precedence over the background &

We can make this work by just simply add parentheses over the command in the within block to be something like
cd /my/directory && (/usr/bin/env nohup sleep 10 &> /dev/null &)

Connection Pool Implementation

I've been trying to consider how to implement the worker pools, given the following:

class InstallBundlerRubyGem
  attr_reader :all_hosts
  def initialize(hosts)
    @all_hosts = hosts
  end
  def perform
    on hosts_which_need_bundler do
      as 'root' { run "gem install bundler --no-rdoc --no-ri" }
    end
  end
  private
    def hosts_which_need_bundler
        [].tap do |hosts_which_need_bundler|
          on all_hosts do |host|
            if (capture("gem list bundler") ~= bundler) != nil
              hosts_which_need_bundler.push host
            end
          end
        end
    end
end

Usage Example:

hosts = (0..20).collect { |n| "#{n}.example.com" }
InstallBundlerRubyGem.new(hosts).perform

There's a few things going on in this that I'd like to draw attention to:

  1. Should on() always make a new connection pool for the given hosts, if it should, it should also hang them up at the end of the block, the overhead of opening all these connections shouldn't be underestimated, but it makes the implementation clearer than the alternative, which is finding some global space in which to store connection pools, and attempting to find a connection pool which has the suitable hosts, and duplicating those connections into a new pool with the valid subset.
  2. I think on() should yield the current host to the block, given the nature of the implementation I'd like to achieve, I think this would be a win, and needn't complicate the implementation.
  3. In writing this contrived example, have I stumbled upon something sane for a plugin architecture? Maybe in this example the class could < Deploy::Extension, which could inherit settings such as default environment, default user, etc. (just a thought)

upload!() should honor within()

The upload!() (and download()) methods should both honor within() for relative paths.

Example:

within '/tmp/' do
  upload! '/etc/hosts', 'should-be-in-tmp'
end

I expect the file should-be-in-tmp to be in /tmp/ but instead it is in the user's home directory.

Ciao!

Mapped commands aren't re-mapped

Ran into a weird one today, I had something like:

SSHKit.config.command_map[:rake]   = "/usr/local/rbenv/shims/bundle exec rake"
SSHKit.config.command_map[:ruby]   = "/usr/local/rbenv/shims/ruby"

I had a problem with Rake x.x.x is already activated, bundle exec might help so I tried mapping:

SSHKit.config.command_map[:bundle] = "/usr/local/rbenv/shims/bundle"

Then I realised it would have been nice to be able to have have mapped:

SSHKit.config.command_map[:rake]   = "bundle exec rake"

and have the bundle part of that be somehow re-expanded.

In the end I worked around by using:

SSHKit.config.command_map[:bundle] = "/usr/local/rbenv/shims/bundle"
SSHKit.config.command_map[:rake]   = "/usr/local/rbenv/shims/bundle exec rake"
SSHKit.config.command_map[:ruby]   = "/usr/local/rbenv/shims/ruby"

Which is still absolutely OK, but weird that I never thought of this when designing the command map.

run(command, options = {}, &block) Command

Note: This should only be available inside the scope of on() (also yet to be written).

This command should work like this:

  1. Block until there is a free worker (dependent on the implementation of the on() feature, expect that it's implemented with a worker/connection-pool)
  2. Immediately execute the command, honoring where suitable the in(), as(), with() settings. It remains to be seen whether on() will really be a command context such as the others are, but I think that would be a reasonable assumption.
  3. run() should wait for the command to finish, I'm not excluding the idea of run('something', nohup: true), but I don't know enough about how that might work (it would be another command context, to wrap it in a no-hup script) to confidently make it a requirement.
  4. run() should yield to a block, passing the stdin, stdout and stderr to the block, for each line of input (on any I/O stream), the block should be executed (if given) - in the block anything written to stdin should be funneled back to the running process.

Method signature: run('command', options = {}, &block)

Similar commands, not implemented yet: stream() and capture().

`background` hangs when ran in `within` blocks

Hi,

Thanks a lot for the great software!
I'm getting more and more excited as I see how flexible SSHKit (and of course Capistrano 3) is :)

I have found that background hangs when used within within blocks.
I'm running Capistrano 3.0.1 with SSHKit on Ubuntu 12.04.

With the code below, run cap test will_hang1 to reproduce the hangs. You can also run cap test wont_hang to see how to fix those.

As far as I know, the problem here is that nohup seems to hang when executed after cd.
We can probably avoid the hangs by changing background to emit shell commands like cd the_dir; ( ( nohup the_command &>/dev/null ) & ).

I suspect that the issue is arrising from Net::SSH(ref). So I believe that we can do a work-around on SSHKit side or just document it in Usage Examples.

# config/deploy/test.rb

task :will_hang1 do
  on roles(:local) do
    within '/home/vagrant' do
      background 'nc -l 12345'
    end
  end
end

# FYI, Don't do the below, too!
task :will_hang2 do
  on roles(:local) do
    execute "( ( cd /home/vagrant; nohup nc -l 12345 &>/dev/null ) & )"
  end
end

task :wont_hang do
  on roles(:local) do
    execute "cd /home/vagrant; ( ( nohup nc -l 12345 &>/dev/null ) & )"
  end
end

Regards,
Yusuke

FloatDomainError downloading empty file

Eg.

testfile='/tmp/arandomfoobarfile'
on 'somehost' do
  execute "rm -f #{testfile}; touch #{testfile}"
  download! testfile, testfile
end

raises a FloatDomainError:

lib/sshkit/backends/netssh.rb:84:in `to_i': NaN (FloatDomainError)

Thanks!

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.