Browse files

Lots of changes around the API and some example file

  • Loading branch information...
leehambley committed Jan 16, 2013
1 parent def3df1 commit 4d46db67bc900fc0444234b7c39ea64e110f2ae8
@@ -0,0 +1,12 @@
+lib/deploy.rb 8f584561b611345114f38176ec53bf00f9d5550f
+lib/deploy/all.rb 3684714987ca5ae9b29d7a1882e1df3891b2c211
+lib/deploy/host.rb 777a8deedcdd5b41dceab8773c992bafa5ee9f92
+lib/core_ext/hash.rb b7a0f0d1ab3b83f6b251e2f865ad6fa3766124a0
+lib/deploy/command.rb b2e81d81444f3bd6f7295d57162a5bc892c336bf
+lib/core_ext/array.rb 3d495a96a0d1566877bf2ebb70ab9ea10a7d32e1
+lib/deploy/version.rb 30e41688e07f7ee74377aaef147250340df4a3f0
+lib/deploy/configuration.rb 1bcbd1936a85a34370aee4ad0db2c92c5b84278e
+lib/deploy/backends/netssh.rb 6b7b3cdf4fc900a61502e1a1076820429e0d992b
+lib/deploy/backends/printer.rb db41b51e9624105efd7cb7f1fef426b506ebadaa
+lib/deploy/backends/abstract.rb e5de7e91d236cb14b5ed9071c20ad6f04abb1d60
+lib/deploy/connection_manager.rb a8066c7d400b10269f3c22b6fa942100430edf01
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1 @@
+--no-private -
No changes.
@@ -0,0 +1,17 @@
+## Is it better than Capistrano?
+*SSHKit* is designed to solve a different problem than Capistrano. *SSHKit* is
+a toolkit for performing structured commands on groups of servers in a
+repeatable way.
+It provides concurrency handling, sane error checking and control flow that
+would otherwise be difficult to achive with pure *Net::SSH*.
+Since *Capistrano v3.0*, *SSHKit* is used by *Capistrano* to communicate with
+backend servers. Whilst Capistrano provides the structure for repeatable
+## Production Ready?
+It's in private Beta use, and the documentation could use more work, but this
+is open source, that's more or less how it works.
@@ -3,6 +3,7 @@ PATH
deploy (0.0.1)
+ term-ansicolor
@@ -35,6 +36,8 @@ GEM
net-ssh (>= 1.99.1)
net-ssh (2.2.2)
rake (10.0.3)
+ redcarpet (2.2.2)
+ term-ansicolor (1.0.7)
turn (0.9.3)
unindent (1.0)
@@ -47,6 +50,7 @@ GEM
log4r (~> 1.1.9)
net-scp (~> 1.0.4)
net-ssh (~> 2.2.2)
+ yard (0.8.3)
@@ -58,6 +62,8 @@ DEPENDENCIES
minitest (>= 2.11.3, < 2.12.0)
+ redcarpet
+ yard

Large diffs are not rendered by default.

Oops, something went wrong.
@@ -1,106 +1,156 @@
-## Deploy.rb
+# SSHKit
-This is a work in progress alternative backend for what may become Capistrano
+**SSHKit** is a toolkit for running commands in a structured way on one or
+more servers.
-## Ready?
+## How might it work?
-Nowhere near, it's barely more than a collection of tests, and classes to
-prove some concepts; there's nothing you could even use in production even if
-you wanted!
+The typical use-case looks something like this:
-## How might it work?
+ require 'sshkit/dsl'
-The typical use-case will look something like this:
-``` ruby
-Deploy::ConnectionManager.backend = :net_ssh
-on(%{}, in: :parallel) do
- in("/opt/sites/") do
- as("deploy") do
- with({rails_env: :production}) do
- puts capture "ls -lr public/assets/"
- rake "assets:precompile"
- ensure
- runner "S3::Sync.notify"
+ on %w{}, in: :sequence, wait: 5 do
+ within "/opt/sites/" do
+ as :deploy do
+ with rails_env: :production do
+ rake "assets:precompile"
+ runner "S3::Sync.notify"
+ end
+ end
- end
One will notice that it's quite low level, but exposes a convenient API, the
-`as()`/`in()`/`with()` are nestable in any order, repeatable, and stackable.
+`as()`/`within()`/`with()` are nestable in any order, repeatable, and stackable.
+When used inside a block in this way, `as()` and `within()` will guard
+the block they are given with a check.
+In the case of `within()`, an error-raising check will be made that the directory
+exists; for `as()` a simple call to `sudo su -<user> whoami` wrapped in a check for
+success, raising an error if unsuccessful.
+The directory check is implemented like this:
+ if test ! -d <directory>; then echo "Directory doesn't exist" 2>&1; false; fi
+And the user switching test implemented like this:
-Helpers such as `runner()` and `rake()` which expand to `run("rails runner", ...)` and
-`run("rake", ...)` are convenience helpers for Rails based apps.
+ if ! sudo su -u <user> whoami > /dev/null; then echo "Can't switch user" 2>&1; false; fi
+According to the defaults, any command that exits with a status other than 0
+raises an error (this can be changed). The body of the message is whatever was
+written to *stdout* by the process.
+Helpers such as `runner()` and `rake()` which expand to `execute(:rails, "runner", ...)` and
+`execute(:rake, ...)` are convenience helpers for Ruby, and Rails based apps.
## Parallel
Notice on the `on()` call the `in: :parallel` option, the following will do
what you might expect:
-on(in: :parallel, limit: 2) { ...}
-on(in: :sequence, wait: 5) { ... }
-on(in: :parallel, limit: 2, wait: 5) { ... }
+ on(in: :parallel, limit: 2) { ...}
+ on(in: :sequence, wait: 5) { ... }
+ on(in: :groups, limit: 2, wait: 5) { ... }
-## Shell Escaping
+## Synchronisation
-We've not talked about this extensively, but sufficed to say that we'll test
-for, and document the most sane behaviour.
+The `on()` block is the unit of synchronisation, one `on()` block will wait
+for all servers to complete before it returns.
-## Output Handling
+For example:
-The output will work very much like MiniTest, in that result and event objects
-will be emitted to an IOStream, these classes are emitted at various times,
-for example
+ all_servers = %w{}
+ site_dir = '/opt/sites/'
-1. A Command is emitted from each `run()` `rake()` `runner()` etc, this
- command has a handful of instance variables the output formatter can call
- on such as `host` and `command`, the example above might emit something
- like this:
+ # Let's simulate a backup task, assuming that some servers take longer
+ # then others to complete
+ on servers do |host|
+ in site_dir do
+ execute :tar, '-czf', "backup-#{host.hostname}.tar.gz", 'current'
+ # Will run: "/usr/bin/env tar -czf current"
+ end
+ end
- {
- host: ""
- command: "su deploy 'cd /opt/sites/ && RAILS_ENV=production ls -lr public_assets'"
- }
- {
- host: ""
- command: "su deploy 'cd /opt/sites/ && RAILS_ENV=production rake assets:precompile '"
- }
- {
- host: ""
- command: "su deploy 'cd /opt/sites/ && RAILS_ENV=production rails runner \'S3::Sync.notify\''"
- }
+ # Now we can do something with those backups, safe in the knowledge that
+ # they will all exist (all tar commands exited with a success status, or
+ # that we will have raised an exception if one of them failed.
+ on servers do |host|
+ in site_dir do
+ backup_filename = "backup-#{host.hostname}.tar.gz"
+ target_filename = "backups/#{}/#{host.hostname}.tar.gz"
+ puts capture(:s3cmd, 'put', backup_filename, target_filename)
+ end
+ end
-2. When the command results are finished, or in progress (not implemented, streaming responses, such as tail) then
- there is emitted every time a CommandStatus object (might end up being called CommandResult) this will encapsulate
- the logic around success, or error conditions, capturing stderr/out of the result instance.
+## The Command Map
-3. *Responders* might be made available to command objects, which allow you to interact with a command, an example might beL
+It's often a problem that programatic SSH sessions don't share the same environmental
+variables as sessions that are started interactively.
- run "git checkout", responder: lambda { |prompt| "fullysecret" if prompt =~ /^Password/ }
+This problem often comes when calling out to executables, expected to be on
+the `$PATH` which, under conditions without dotfiles or other environmental
+configuration are not where they are expected to be.
- The responder needs only to respond to call, and take the prompt (that will be the last line of the standard output of
- the process, if it returns something, that will be written to the processes stdin.
+To try and solve this there is the `with()` helper which takes a hash of variables and makes them
+available to the environment.
-The final command Result object will have fields covering start and end times, host, time waiting for mutexes, time waiting for
-a connection, the processes stdin, stdout, stderr and exit status, as well as convenience methods which will make implemeting
+ with path: '/usr/local/bin/rbenv/shims:$PATH' do
+ execute :ruby, '--version'
+ end
+Will execute:
+ ( PATH=/usr/local/bin/rbenv/shims:$PATH /usr/bin/env ruby --version )
+**Often more preferable is to use the *command map*.**
+The *command map* is used by default when instantiating a *Command* object
+The *command map* exists on the configuration object, and in principle is
+quite simple, it's a *Hash* structure with a default key factory block
+specified, for example:
+ puts SSHKit.config.command_map[:ruby]
+ # => /usr/bin/env ruby
-## ToDo
+The `/usr/bin/env` prefix is applied to all commands, to make clear that the
+environment is being deferred to to make the decision, this is what happens
+anyway when one would simply attempt to execute `ruby`, however by making it
+explicit, it was hoped that it might lead people to explore the documentation.
-* Assertive backend (also logging backend)
+One can override the hash map for individual commands:
-* Capture helper
-* Run helper should be useable in an if statement value (simply return the
- command result)
+ SSHKit.config.command_map[:rake] = "/usr/local/rbenv/shims/rake"
+ puts SSHKit.config.command_map[:rake]
+ # => /usr/local/rbenv/shims/rake
+One can also override the command map completely, this may not be wise, but it
+would be possible, for example:
+ SSHKit.config.command_map = do |hash, command|
+ hash[command] = "/usr/local/rbenv/shims/#{command}"
+ end
+This would effectively make it impossible to call any commands which didn't
+provide an executable in that directory, but in some cases that might be
+*Note:* All keys should be symbolised, as the *Command* object will symbolize it's
+first argument before attempting to find it in the *command map*.
+## Output Handling
-## Better Error Messages
+The output handling comprises two objects, first is the output itself, by
+default this is *$stdout*, but can be any object responding to a
+*StringIO*-like interface. The second part is the *formatter*.
-By encapsulating things such as `as()` into helper methods, we can contextualise what went wrong, a message such as:
+The *formatter* and *output* have a strange relationship:
- "Tried to run `su - deploy` as `root` failed with status `0` *No askpass program was provided*"
+ SSHKit.config.output =$stdout)
-would be much more helpful than what we have now, and the check can be made on the connection object when as() is called,
-before executing commands.
+The *formatter* will typically delegate all calls to the *output*, depending
+on it's implementation it will almost certainly override the implementation of
+`write()` (alias `<<()`) and query the objects it receives to determine what
+should be printed.
@@ -16,6 +16,7 @@ do |gem|
gem.version = Deploy::VERSION
+ gem.add_dependency('term-ansicolor')
gem.add_development_dependency('minitest', ['>= 2.11.3', '< 2.12.0'])
@@ -26,4 +27,7 @@ do |gem|
+ gem.add_development_dependency('yard')
+ gem.add_development_dependency('redcarpet')
@@ -0,0 +1,67 @@
+#!/usr/bin/env ruby
+# Ruby 1.9 doesn't include the current
+# working directory on the load path.
+$: << Dir.pwd + '/lib/'
+# Automatically sucks in the `deploy`
+# files so that you don't need to.
+require 'deploy/dsl'
+require 'forwardable'
+require 'term/ansicolor'
+directory = '/opt/sites/web_application'
+hosts ="")
+# Custom output formatter!
+class ColorizedFormatter < StringIO
+ extend Forwardable
+ attr_reader :original_output
+ def_delegators :@original_output, :read, :rewind
+ def initialize(oio)
+ @original_output = oio
+ end
+ def write(obj)
+ if obj.is_a? Deploy::Command
+ unless obj.started?
+ original_output << "[#{}] Running #{c.yellow(c.bold(String(obj)))} on #{c.yellow(}\n"
+ end
+ if obj.complete? && !obj.stdout.empty?
+ obj.stdout.lines.each do |line|
+ original_output <<"\t" + line)
+ end
+ end
+ if obj.complete? && !obj.stderr.empty?
+ obj.stderr.lines.each do |line|
+ original_output <<"\t" + line)
+ end
+ end
+ if obj.finished?
+ original_output << "[#{}] Finished in #{sprintf('%5.3f seconds', obj.runtime)} command #{c.bold { obj.failure? ?'failed') :'successful') }}.\n"
+ end
+ else
+ original_output <<"Output formatter doesn't know how to handle #{obj.inspect}\n"))
+ end
+ end
+ private
+ def c
+ @c ||= Term::ANSIColor
+ end
+Deploy.config.output =$stdout)
+on hosts do |host|
+ target = '/opt/rack-rack-repository'
+ if execute(:test, "-d #{target}")
+ within target do
+ execute :git, :pull
+ end
+ else
+ execute :git, :clone, 'git://', target
+ end
@@ -1,10 +1,13 @@
require_relative '../core_ext/array'
require_relative '../core_ext/hash'
-require_relative 'command'
+require_relative 'dsl'
require_relative 'host'
-require_relative 'connection_manager'
+require_relative 'command'
require_relative 'configuration'
+require_relative 'connection_manager'
require_relative 'backends/abstract'
require_relative 'backends/printer'
require_relative 'backends/netssh'
Oops, something went wrong.

0 comments on commit 4d46db6

Please sign in to comment.