sshkit 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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