sshkit 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. data/.gitignore +2 -0
  2. data/.travis.yml +5 -0
  3. data/.yardoc/checksums +13 -0
  4. data/.yardoc/object_types +0 -0
  5. data/.yardoc/objects/root.dat +0 -0
  6. data/.yardoc/proxy_types +0 -0
  7. data/.yardopts +1 -0
  8. data/CHANGELOG.md +0 -0
  9. data/EXAMPLES.md +167 -0
  10. data/FAQ.md +17 -0
  11. data/Gemfile +2 -0
  12. data/Gemfile.lock +69 -0
  13. data/LICENSE.md +674 -0
  14. data/README.md +181 -0
  15. data/Rakefile +37 -0
  16. data/Vagrantfile +18 -0
  17. data/assets/images/logo.png +0 -0
  18. data/example.rb +70 -0
  19. data/lib/core_ext/array.rb +5 -0
  20. data/lib/core_ext/hash.rb +11 -0
  21. data/lib/sshkit/all.rb +13 -0
  22. data/lib/sshkit/backends/abstract.rb +83 -0
  23. data/lib/sshkit/backends/netssh.rb +82 -0
  24. data/lib/sshkit/backends/printer.rb +28 -0
  25. data/lib/sshkit/command.rb +153 -0
  26. data/lib/sshkit/configuration.rb +29 -0
  27. data/lib/sshkit/connection_manager.rb +93 -0
  28. data/lib/sshkit/dsl.rb +15 -0
  29. data/lib/sshkit/host.rb +151 -0
  30. data/lib/sshkit/version.rb +3 -0
  31. data/lib/sshkit.rb +27 -0
  32. data/sshkit.gemspec +34 -0
  33. data/test/functional/test_connection_manager.rb +17 -0
  34. data/test/functional/test_ssh_server_comes_up_for_functional_tests.rb +23 -0
  35. data/test/helper.rb +125 -0
  36. data/test/integration/backends/test_netssh.rb +99 -0
  37. data/test/unit/backends/test_netssh.rb +14 -0
  38. data/test/unit/backends/test_printer.rb +62 -0
  39. data/test/unit/core_ext/test_string.rb +12 -0
  40. data/test/unit/test_command.rb +113 -0
  41. data/test/unit/test_configuration.rb +47 -0
  42. data/test/unit/test_connection_manager.rb +78 -0
  43. data/test/unit/test_host.rb +65 -0
  44. metadata +301 -0
@@ -0,0 +1,29 @@
1
+ module SSHKit
2
+
3
+ class Configuration
4
+
5
+ attr_writer :command_map
6
+ attr_accessor :output, :format, :runner, :backend
7
+
8
+ def initialize
9
+ @output = $stdout
10
+ @format = :dot
11
+ @runner = :parallel
12
+ @backend = SSHKit::Backend::Netssh
13
+ end
14
+
15
+ def command_map
16
+ @command_map ||= begin
17
+ Hash.new do |hash, command|
18
+ if %w{if test time}.include? command.to_s
19
+ hash[command] = command.to_s
20
+ else
21
+ hash[command] = "/usr/bin/env #{command}"
22
+ end
23
+ end
24
+ end
25
+ end
26
+
27
+ end
28
+
29
+ end
@@ -0,0 +1,93 @@
1
+ require 'timeout'
2
+
3
+ class Runner
4
+
5
+ attr_reader :hosts, :block
6
+
7
+ def initialize(hosts, &block)
8
+ @hosts = Array(hosts)
9
+ @block = block
10
+ end
11
+
12
+ end
13
+
14
+ class ParallelRunner < Runner
15
+ def execute
16
+ threads = []
17
+ hosts.each do |host|
18
+ threads << Thread.new(host) do |h|
19
+ SSHKit.config.backend.new(host, &block).run
20
+ end
21
+ end
22
+ threads.map(&:join)
23
+ end
24
+ end
25
+
26
+ class SequentialRunner < Runner
27
+ attr_writer :wait_interval
28
+ def execute
29
+ hosts.each do |host|
30
+ SSHKit.config.backend.new(host, &block).run
31
+ sleep wait_interval
32
+ end
33
+ end
34
+ private
35
+ def wait_interval
36
+ @wait_interval ||= 2
37
+ end
38
+ end
39
+
40
+ class GroupRunner < SequentialRunner
41
+ attr_writer :group_size
42
+ def execute
43
+ hosts.each_slice(group_size).collect do |group_hosts|
44
+ ParallelRunner.new(group_hosts, &block).execute
45
+ sleep wait_interval
46
+ end.flatten
47
+ end
48
+ private
49
+ def group_size
50
+ @group_size ||= 2
51
+ end
52
+ end
53
+
54
+ module SSHKit
55
+
56
+ NoValidHosts = Class.new(StandardError)
57
+
58
+ class ConnectionManager
59
+
60
+ attr_accessor :hosts
61
+
62
+ def initialize(raw_hosts)
63
+ @raw_hosts = Array(raw_hosts)
64
+ raise NoValidHosts unless Array(raw_hosts).any?
65
+ resolve_hosts!
66
+ end
67
+
68
+ def each(options={}, &block)
69
+ options = default_options.merge(options)
70
+ case options[:in]
71
+ when :parallel then ParallelRunner
72
+ when :sequence then SequentialRunner
73
+ when :groups then GroupRunner
74
+ else
75
+ raise RuntimeError, "Don't know how to handle run style #{options[:in].inspect}"
76
+ end.new(hosts, &block).execute
77
+ end
78
+
79
+ private
80
+
81
+ attr_accessor :cooldown
82
+
83
+ def default_options
84
+ { in: :parallel }
85
+ end
86
+
87
+ def resolve_hosts!
88
+ @hosts = @raw_hosts.collect { |rh| rh.is_a?(Host) ? rh : Host.new(rh) }.uniq
89
+ end
90
+
91
+ end
92
+
93
+ end
data/lib/sshkit/dsl.rb ADDED
@@ -0,0 +1,15 @@
1
+ require_relative '../sshkit'
2
+
3
+ module SSHKit
4
+
5
+ module DSL
6
+
7
+ def on(hosts, options={}, &block)
8
+ ConnectionManager.new(hosts).each(options, &block)
9
+ end
10
+
11
+ end
12
+
13
+ end
14
+
15
+ include SSHKit::DSL
@@ -0,0 +1,151 @@
1
+ module SSHKit
2
+
3
+ UnparsableHostStringError = Class.new(StandardError)
4
+
5
+ class Host
6
+
7
+ attr_reader :hostname, :port, :username
8
+
9
+ attr_accessor :password
10
+
11
+ def initialize(host_string)
12
+
13
+ suitable_parsers = [
14
+ SimpleHostParser,
15
+ HostWithPortParser,
16
+ IPv6HostWithPortParser,
17
+ HostWithUsernameParser,
18
+ HostWithUsernameAndPortParser
19
+ ].select do |p|
20
+ p.suitable?(host_string)
21
+ end
22
+
23
+ if suitable_parsers.any?
24
+ suitable_parsers.first.tap do |parser|
25
+ @username, @hostname, @port = parser.new(host_string).attributes
26
+ end
27
+ else
28
+ raise UnparsableHostStringError, "Cannot parse host string #{host_string}"
29
+ end
30
+
31
+ end
32
+
33
+ def hash
34
+ username.hash ^ hostname.hash ^ port.hash
35
+ end
36
+
37
+ def eql?(other_host)
38
+ other_host.hash == hash
39
+ end
40
+ alias :== :eql?
41
+ alias :equal? :eql?
42
+
43
+ def to_key
44
+ to_s.to_sym
45
+ end
46
+
47
+ def to_s
48
+ sprintf("%s@%s:%d", username, hostname, port)
49
+ end
50
+
51
+ end
52
+
53
+ # @private
54
+ # :nodoc:
55
+ class SimpleHostParser
56
+
57
+ def self.suitable?(host_string)
58
+ !host_string.match /[:|@]/
59
+ end
60
+
61
+ def initialize(host_string)
62
+ @host_string = host_string
63
+ end
64
+
65
+ def username
66
+ `whoami`.chomp
67
+ end
68
+
69
+ def port
70
+ 22
71
+ end
72
+
73
+ def hostname
74
+ @host_string
75
+ end
76
+
77
+ def attributes
78
+ [username, hostname, port]
79
+ end
80
+
81
+ end
82
+
83
+ # @private
84
+ # :nodoc:
85
+ class HostWithPortParser < SimpleHostParser
86
+
87
+ def self.suitable?(host_string)
88
+ !host_string.match /[@|\[|\]]/
89
+ end
90
+
91
+ def port
92
+ @host_string.split(':').last.to_i
93
+ end
94
+
95
+ def hostname
96
+ @host_string.split(':').first
97
+ end
98
+
99
+ end
100
+
101
+ # @private
102
+ # :nodoc:
103
+ class IPv6HostWithPortParser < SimpleHostParser
104
+
105
+ def self.suitable?(host_string)
106
+ host_string.match /[a-fA-F0-9:]+:\d+/
107
+ end
108
+
109
+ def port
110
+ @host_string.split(':').last.to_i
111
+ end
112
+
113
+ def hostname
114
+ @host_string.gsub!(/\[|\]/, '')
115
+ @host_string.split(':')[0..-2].join(':')
116
+ end
117
+
118
+ end
119
+
120
+ # @private
121
+ # :nodoc:
122
+ class HostWithUsernameParser < SimpleHostParser
123
+ def self.suitable?(host_string)
124
+ host_string.match(/@/) && !host_string.match(/\:/)
125
+ end
126
+ def username
127
+ @host_string.split('@').first
128
+ end
129
+ def hostname
130
+ @host_string.split('@').last
131
+ end
132
+ end
133
+
134
+ # @private
135
+ # :nodoc:
136
+ class HostWithUsernameAndPortParser < SimpleHostParser
137
+ def self.suitable?(host_string)
138
+ host_string.match /@.*:\d+/
139
+ end
140
+ def username
141
+ @host_string.split(/:|@/)[0]
142
+ end
143
+ def hostname
144
+ @host_string.split(/:|@/)[1]
145
+ end
146
+ def port
147
+ @host_string.split(/:|@/)[2].to_i
148
+ end
149
+ end
150
+
151
+ end
@@ -0,0 +1,3 @@
1
+ module SSHKit
2
+ VERSION = "0.0.1"
3
+ end
data/lib/sshkit.rb ADDED
@@ -0,0 +1,27 @@
1
+ require 'thread'
2
+ require_relative 'sshkit/all'
3
+
4
+ module SSHKit
5
+
6
+ class << self
7
+ attr_accessor :config
8
+ end
9
+
10
+ def self.capture_output(io, &block)
11
+ original_io = config.output
12
+ config.output = io
13
+ yield
14
+ ensure
15
+ config.output = original_io
16
+ end
17
+
18
+ def self.configure
19
+ @@config ||= Configuration.new
20
+ yield config
21
+ end
22
+
23
+ def self.config
24
+ @@config ||= Configuration.new
25
+ end
26
+
27
+ end
data/sshkit.gemspec ADDED
@@ -0,0 +1,34 @@
1
+ # -*- encoding: utf-8 -*-
2
+ require File.expand_path('../lib/sshkit/version', __FILE__)
3
+
4
+ Gem::Specification.new do |gem|
5
+
6
+ gem.authors = ["Lee Hambley", "Tom Clements"]
7
+ gem.email = ["lee.hambley@gmail.com", "seenmyfate@gmail.com"]
8
+ gem.summary = %q{SSHKit makes it easy to write structured, testable SSH commands in Ruby}
9
+ gem.description = %q{A comprehensive toolkit for remotely running commands in a structured manner on groups of servers.}
10
+ gem.homepage = "http://wacku.github.com/sshkit"
11
+
12
+ gem.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
13
+ gem.files = `git ls-files`.split("\n")
14
+ gem.test_files = `git ls-files -- test/*`.split("\n")
15
+ gem.name = "sshkit"
16
+ gem.require_paths = ["lib"]
17
+ gem.version = SSHKit::VERSION
18
+
19
+ gem.add_dependency('net-ssh')
20
+ gem.add_dependency('term-ansicolor')
21
+
22
+ gem.add_development_dependency('minitest', ['>= 2.11.3', '< 2.12.0'])
23
+ gem.add_development_dependency('autotest')
24
+ gem.add_development_dependency('rake')
25
+ gem.add_development_dependency('turn')
26
+ gem.add_development_dependency('unindent')
27
+ gem.add_development_dependency('mocha')
28
+ gem.add_development_dependency('debugger')
29
+ gem.add_development_dependency('vagrant')
30
+
31
+ gem.add_development_dependency('yard')
32
+ gem.add_development_dependency('redcarpet')
33
+
34
+ end
@@ -0,0 +1,17 @@
1
+ require 'helper'
2
+
3
+ module SSHKit
4
+
5
+ class TestConnectionManager < FunctionalTest
6
+
7
+ def setup
8
+
9
+ end
10
+
11
+ def test_running_commands_on_real_hosts
12
+
13
+ end
14
+
15
+ end
16
+
17
+ end
@@ -0,0 +1,23 @@
1
+ require 'helper'
2
+
3
+ module SSHKit
4
+
5
+ class TestHost < FunctionalTest
6
+
7
+ def host
8
+ @_host ||= Host.new('')
9
+ end
10
+
11
+ def test_that_it_works
12
+ assert true
13
+ end
14
+
15
+ def test_creating_a_user_gives_us_back_his_private_key_as_a_string
16
+ keys = create_user_with_key(:peter)
17
+ assert_equal [:one, :two, :three], keys.keys
18
+ assert keys.values.all?
19
+ end
20
+
21
+ end
22
+
23
+ end
data/test/helper.rb ADDED
@@ -0,0 +1,125 @@
1
+ require 'rubygems'
2
+ require 'bundler'
3
+ begin
4
+ Bundler.setup(:default, :development)
5
+ rescue Bundler::BundlerError => e
6
+ $stderr.puts e.message
7
+ $stderr.puts "Run `bundle install` to install missing gems"
8
+ exit e.status_code
9
+ end
10
+ require 'tempfile'
11
+ require 'minitest/unit'
12
+ require 'mocha'
13
+ require 'turn'
14
+ require 'unindent'
15
+ require 'debugger'
16
+ require 'vagrant'
17
+ require 'stringio'
18
+
19
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
20
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
21
+ require 'sshkit'
22
+
23
+ module Vagrant
24
+ module Communication
25
+ class SSH
26
+ def download(from)
27
+ scp_connect do |scp|
28
+ return StringIO.new.tap do |sio|
29
+ scp.download!(from, sio)
30
+ end
31
+ end
32
+ rescue RuntimeError => e
33
+ raise if e.message !~ /Permission denied/
34
+ raise Vagrant::Errors::SCPPermissionDenied, :path => from.to_s
35
+ end
36
+ private
37
+ def scp_connect
38
+ connect do |connection|
39
+ scp = Net::SCP.new(connection)
40
+ return yield scp
41
+ end
42
+ rescue Net::SCP::Error => e
43
+ raise Vagrant::Errors::SCPUnavailable if e.message =~ /\(127\)/
44
+ raise
45
+ end
46
+ end
47
+ end
48
+ end
49
+
50
+ class UnitTest < MiniTest::Unit::TestCase
51
+
52
+ end
53
+
54
+ class FunctionalTest < MiniTest::Unit::TestCase
55
+
56
+ attr_accessor :venv
57
+
58
+ def setup
59
+ @venv = Vagrant::Environment.new
60
+ venv.vms.each do |name, vm|
61
+ warn "#{name} (of #{venv.vms.size}) needs to be booted, please wait" unless vm.state == :running
62
+ end
63
+ venv.cli "up"
64
+ end
65
+
66
+ private
67
+
68
+ def vm_hosts
69
+ venv.vms.collect do |name, vm|
70
+ port = vm.env.config.for_vm(name).vm.forwarded_ports.first[:hostport]
71
+ SSHKit::Host.new("vagrant@localhost:#{port}").tap do |h|
72
+ h.password = 'vagrant'
73
+ end
74
+ end
75
+ end
76
+
77
+ def create_user_with_key(username, password = :secret)
78
+ username, password = username.to_s, password.to_s
79
+ keys = venv.vms.collect do |hostname, vm|
80
+
81
+ # Remove the user, make it again, force-generate a key for him
82
+ # short keys save us a few microseconds
83
+ vm.channel.sudo("userdel -rf #{username}; true") # The `rescue nil` of the shell world
84
+ vm.channel.sudo("useradd -m #{username}")
85
+ vm.channel.sudo("echo y | ssh-keygen -b 1024 -f #{username} -N ''")
86
+ vm.channel.sudo("chown vagrant:vagrant #{username}*")
87
+ vm.channel.sudo("echo #{username}:#{password} | chpasswd")
88
+
89
+ # Make the .ssh directory, change the ownership and the
90
+ vm.channel.sudo("mkdir -p /home/#{username}/.ssh")
91
+ vm.channel.sudo("chown #{username}:#{username} /home/#{username}/.ssh")
92
+ vm.channel.sudo("chmod 700 /home/#{username}/.ssh")
93
+
94
+ # Move the key to authorized keys and chown and chmod it
95
+ vm.channel.sudo("cat #{username}.pub > /home/#{username}/.ssh/authorized_keys")
96
+ vm.channel.sudo("chown #{username}:#{username} /home/#{username}/.ssh/authorized_keys")
97
+ vm.channel.sudo("chmod 600 /home/#{username}/.ssh/authorized_keys")
98
+
99
+ sio = vm.channel.download("/home/vagrant/#{username}")
100
+
101
+ # Clean Up Files
102
+ vm.channel.sudo("rm #{username} #{username}.pub")
103
+
104
+ # Rewind the stringio, and read it as a string
105
+ sio.tap { |s| s.rewind }.read
106
+
107
+ end
108
+
109
+ Hash[venv.vms.collect { |n,vm| n }.zip(keys)]
110
+
111
+ end
112
+
113
+ end
114
+
115
+ class IntegrationTest < MiniTest::Unit::TestCase
116
+
117
+ end
118
+
119
+ #
120
+ # Force colours in Autotest
121
+ #
122
+ Turn.config.ansi = true
123
+ Turn.config.format = :pretty
124
+
125
+ MiniTest::Unit.autorun
@@ -0,0 +1,99 @@
1
+ require 'helper'
2
+
3
+ module SSHKit
4
+
5
+ module Backend
6
+
7
+ class ToSIoFormatter < StringIO
8
+ extend Forwardable
9
+ attr_reader :original_output
10
+ def_delegators :@original_output, :read, :rewind
11
+ def initialize(oio)
12
+ @original_output = oio
13
+ end
14
+ def write(obj)
15
+ warn "What: #{obj.to_hash}"
16
+ original_output.write "> Executing #{obj}\n"
17
+ end
18
+ end
19
+
20
+ class TestPrinter < FunctionalTest
21
+
22
+ def block_to_run
23
+ lambda do |host|
24
+ execute 'date'
25
+ execute :ls, '-l', '/some/directory'
26
+ with rails_env: :production do
27
+ within '/tmp' do
28
+ as :root do
29
+ execute :touch, 'restart.txt'
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
35
+
36
+ def a_host
37
+ vm_hosts.first
38
+ end
39
+
40
+ def printer
41
+ Netssh.new(a_host, &block_to_run)
42
+ end
43
+
44
+ def simple_netssh
45
+ sio = ToSIoFormatter.new(StringIO.new)
46
+ SSHKit.capture_output(sio) do
47
+ printer.run
48
+ end
49
+ sio.rewind
50
+ result = sio.read
51
+ assert_equal <<-EOEXPECTED.unindent, result
52
+ > Executing if test ! -d /opt/sites/example.com; then echo "Directory does not exist '/opt/sites/example.com'" 2>&1; false; fi
53
+ > Executing cd /opt/sites/example.com && /usr/bin/env date
54
+ > Executing cd /opt/sites/example.com && /usr/bin/env ls -l /some/directory
55
+ > Executing if test ! -d /opt/sites/example.com/tmp; then echo "Directory does not exist '/opt/sites/example.com/tmp'" 2>&1; false; fi
56
+ > Executing if ! sudo su -u root whoami > /dev/null; then echo "You cannot switch to user 'root' using sudo, please check the sudoers file" 2>&1; false; fi
57
+ > Executing cd /opt/sites/example.com/tmp && ( RAILS_ENV=production ( sudo su -u root /usr/bin/env touch restart.txt ) )
58
+ EOEXPECTED
59
+ end
60
+
61
+ def test_capture
62
+ File.open('/dev/null', 'w') do |dnull|
63
+ SSHKit.capture_output(dnull) do
64
+ captured_command_result = ""
65
+ Netssh.new(a_host) do |host|
66
+ captured_command_result = capture(:hostname)
67
+ end.run
68
+ assert_equal "lucid32", captured_command_result
69
+ end
70
+ end
71
+ end
72
+
73
+ def test_exit_status
74
+
75
+ end
76
+
77
+ def test_raising_an_error_if_a_command_returns_a_bad_exit_status
78
+ skip "Where to implement this?"
79
+ # NOTE: I think that it might be wise to have Command raise when
80
+ # Command#exit_status=() is called, it would allow an option to
81
+ # be passed to command (raise_errors: false), which could also be
82
+ # inherited through Backend#command and specified on the (not yet
83
+ # existing) backend configurations.
84
+ assert_raises RuntimeError do
85
+ File.open('/dev/null', 'w') do |dnull|
86
+ SSHKit.capture_output(dnull) do
87
+ Netssh.new(a_host) do |host|
88
+ execute :false
89
+ end.run
90
+ end
91
+ end
92
+ end
93
+ end
94
+
95
+ end
96
+
97
+ end
98
+
99
+ end
@@ -0,0 +1,14 @@
1
+ require 'helper'
2
+
3
+ module SSHKit
4
+ module Backend
5
+ class TestNetssh < UnitTest
6
+ def backend
7
+ Netssh.new(Host.new('example.com'), Proc.new)
8
+ end
9
+ def test_net_ssh_configuration_timeout
10
+ skip "No configuration on the Netssh class yet"
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,62 @@
1
+ require 'helper'
2
+
3
+ module SSHKit
4
+
5
+ module Backend
6
+
7
+ class ToSIoFormatter < StringIO
8
+ extend Forwardable
9
+ attr_reader :original_output
10
+ def_delegators :@original_output, :read, :rewind
11
+ def initialize(oio)
12
+ @original_output = oio
13
+ end
14
+ def write(obj)
15
+ original_output.write "> Executing #{obj}\n"
16
+ end
17
+ end
18
+
19
+ class TestPrinter < UnitTest
20
+
21
+ def block_to_run
22
+ lambda do |host|
23
+ within '/opt/sites/example.com' do
24
+ execute 'date'
25
+ execute :ls, '-l', '/some/directory'
26
+ with rails_env: :production do
27
+ within :tmp do
28
+ as :root do
29
+ execute :touch, 'restart.txt'
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
36
+
37
+ def printer
38
+ Printer.new(Host.new(:'example.com'), &block_to_run)
39
+ end
40
+
41
+ def test_simple_printing
42
+ sio = ToSIoFormatter.new(StringIO.new)
43
+ SSHKit.capture_output(sio) do
44
+ printer.run
45
+ end
46
+ sio.rewind
47
+ result = sio.read
48
+ assert_equal <<-EOEXPECTED.unindent, result
49
+ > Executing if test ! -d /opt/sites/example.com; then echo "Directory does not exist '/opt/sites/example.com'" 1>&2; false; fi
50
+ > Executing cd /opt/sites/example.com && /usr/bin/env date
51
+ > Executing cd /opt/sites/example.com && /usr/bin/env ls -l /some/directory
52
+ > Executing if test ! -d /opt/sites/example.com/tmp; then echo "Directory does not exist '/opt/sites/example.com/tmp'" 1>&2; false; fi
53
+ > Executing if ! sudo su root -c whoami > /dev/null; then echo "You cannot switch to user 'root' using sudo, please check the sudoers file" 1>&2; false; fi
54
+ > Executing cd /opt/sites/example.com/tmp && ( RAILS_ENV=production sudo su root -c /usr/bin/env touch restart.txt )
55
+ EOEXPECTED
56
+ end
57
+
58
+ end
59
+
60
+ end
61
+
62
+ end