sshkit 1.7.1 → 1.8.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (56) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +2 -2
  3. data/BREAKING_API_WISHLIST.md +14 -0
  4. data/CHANGELOG.md +74 -0
  5. data/CONTRIBUTING.md +43 -0
  6. data/EXAMPLES.md +265 -169
  7. data/Gemfile +7 -0
  8. data/README.md +274 -9
  9. data/RELEASING.md +16 -8
  10. data/Rakefile +8 -0
  11. data/lib/sshkit.rb +0 -9
  12. data/lib/sshkit/all.rb +6 -4
  13. data/lib/sshkit/backends/abstract.rb +42 -42
  14. data/lib/sshkit/backends/connection_pool.rb +57 -8
  15. data/lib/sshkit/backends/local.rb +21 -50
  16. data/lib/sshkit/backends/netssh.rb +45 -98
  17. data/lib/sshkit/backends/printer.rb +3 -23
  18. data/lib/sshkit/backends/skipper.rb +4 -8
  19. data/lib/sshkit/color.rb +51 -20
  20. data/lib/sshkit/command.rb +68 -47
  21. data/lib/sshkit/configuration.rb +38 -5
  22. data/lib/sshkit/deprecation_logger.rb +17 -0
  23. data/lib/sshkit/formatters/abstract.rb +28 -4
  24. data/lib/sshkit/formatters/black_hole.rb +1 -2
  25. data/lib/sshkit/formatters/dot.rb +3 -10
  26. data/lib/sshkit/formatters/pretty.rb +31 -56
  27. data/lib/sshkit/formatters/simple_text.rb +6 -44
  28. data/lib/sshkit/host.rb +5 -6
  29. data/lib/sshkit/logger.rb +0 -1
  30. data/lib/sshkit/mapping_interaction_handler.rb +47 -0
  31. data/lib/sshkit/runners/parallel.rb +1 -1
  32. data/lib/sshkit/runners/sequential.rb +1 -1
  33. data/lib/sshkit/version.rb +1 -1
  34. data/sshkit.gemspec +0 -1
  35. data/test/functional/backends/test_local.rb +14 -1
  36. data/test/functional/backends/test_netssh.rb +58 -50
  37. data/test/helper.rb +2 -2
  38. data/test/unit/backends/test_abstract.rb +145 -0
  39. data/test/unit/backends/test_connection_pool.rb +27 -2
  40. data/test/unit/backends/test_printer.rb +47 -47
  41. data/test/unit/formatters/test_custom.rb +65 -0
  42. data/test/unit/formatters/test_dot.rb +25 -32
  43. data/test/unit/formatters/test_pretty.rb +114 -22
  44. data/test/unit/formatters/test_simple_text.rb +83 -0
  45. data/test/unit/test_color.rb +69 -5
  46. data/test/unit/test_command.rb +53 -18
  47. data/test/unit/test_command_map.rb +0 -4
  48. data/test/unit/test_configuration.rb +47 -7
  49. data/test/unit/test_coordinator.rb +45 -52
  50. data/test/unit/test_deprecation_logger.rb +38 -0
  51. data/test/unit/test_host.rb +3 -4
  52. data/test/unit/test_logger.rb +0 -1
  53. data/test/unit/test_mapping_interaction_handler.rb +101 -0
  54. metadata +37 -41
  55. data/lib/sshkit/utils/capture_output_methods.rb +0 -13
  56. data/test/functional/test_coordinator.rb +0 -17
data/lib/sshkit/color.rb CHANGED
@@ -1,27 +1,58 @@
1
- require 'colorize'
1
+ module SSHKit
2
+ # Very basic support for ANSI color, so that we don't have to rely on
3
+ # any external dependencies. This class handles colorizing strings, and
4
+ # automatically disabling color if the underlying output is not a tty.
5
+ #
6
+ class Color
7
+ COLOR_CODES = {
8
+ :black => 30,
9
+ :red => 31,
10
+ :green => 32,
11
+ :yellow => 33,
12
+ :blue => 34,
13
+ :magenta => 35,
14
+ :cyan => 36,
15
+ :white => 37,
16
+ :light_black => 90,
17
+ :light_red => 91,
18
+ :light_green => 92,
19
+ :light_yellow => 93,
20
+ :light_blue => 94,
21
+ :light_magenta => 95,
22
+ :light_cyan => 96,
23
+ :light_white => 97
24
+ }.freeze
2
25
 
3
- module Color
4
- String.colors.each do |color|
5
- instance_eval <<-RUBY, __FILE__, __LINE__
6
- def #{color}(string = '')
7
- string = yield if block_given?
8
- colorize? ? string.colorize(:color => :#{color}) : string
9
- end
10
- RUBY
11
- end
26
+ def initialize(output, env=ENV)
27
+ @output, @env = output, env
28
+ end
12
29
 
13
- String.modes.each do |mode|
14
- instance_eval <<-RUBY, __FILE__, __LINE__
15
- def #{mode}(string = '')
16
- string = yield if block_given?
17
- colorize? ? string.colorize(:mode => :#{mode}) : string
18
- end
19
- RUBY
20
- end
30
+ # Converts the given obj to string and surrounds in the appropriate ANSI
31
+ # color escape sequence, based on the specified color and mode. The color
32
+ # must be a symbol (see COLOR_CODES for a complete list).
33
+ #
34
+ # If the underlying output does not support ANSI color (see `colorize?),
35
+ # the string will be not be colorized. Likewise if the specified color
36
+ # symbol is unrecognized, the string will not be colorized.
37
+ #
38
+ # Note that the only mode currently support is :bold. All other values
39
+ # will be silently ignored (i.e. treated the same as mode=nil).
40
+ #
41
+ def colorize(obj, color, mode=nil)
42
+ string = obj.to_s
43
+ return string unless colorize?
44
+ return string unless COLOR_CODES.key?(color)
45
+
46
+ result = mode == :bold ? "\e[1;" : "\e[0;"
47
+ result << COLOR_CODES.fetch(color).to_s
48
+ result << ";49m#{string}\e[0m"
49
+ end
21
50
 
22
- class << self
51
+ # Returns `true` if the underlying output is a tty, or if the SSHKIT_COLOR
52
+ # environment variable is set.
53
+ #
23
54
  def colorize?
24
- ENV['SSHKIT_COLOR'] || $stdout.tty?
55
+ @env['SSHKIT_COLOR'] || (@output.respond_to?(:tty?) && @output.tty?)
25
56
  end
26
57
  end
27
58
  end
@@ -4,38 +4,12 @@ require 'securerandom'
4
4
  # @author Lee Hambley
5
5
  module SSHKit
6
6
 
7
- # @author Lee Hambley
8
- module CommandHelper
9
-
10
- def rake(tasks=[])
11
- execute :rake, tasks
12
- end
13
-
14
- def make(tasks=[])
15
- execute :make, tasks
16
- end
17
-
18
- def execute(command, args=[])
19
- Command.new(command, args)
20
- end
21
-
22
- private
23
-
24
- def map(command)
25
- SSHKit.config.command_map[command.to_sym]
26
- end
27
-
28
- end
29
-
30
7
  # @author Lee Hambley
31
8
  class Command
32
9
 
33
10
  Failed = Class.new(SSHKit::StandardError)
34
11
 
35
- attr_reader :command, :args, :options, :started_at, :started, :exit_status
36
-
37
- attr_accessor :stdout, :stderr
38
- attr_accessor :full_stdout, :full_stderr
12
+ attr_reader :command, :args, :options, :started_at, :started, :exit_status, :full_stdout, :full_stderr
39
13
 
40
14
  # Initialize a new Command object
41
15
  #
@@ -45,14 +19,13 @@ module SSHKit
45
19
  # nothing in stdin or stdout
46
20
  #
47
21
  def initialize(*args)
48
- raise ArgumentError, "May not pass no arguments to Command.new" if args.empty?
22
+ raise ArgumentError, "Must pass arguments to Command.new" if args.empty?
49
23
  @options = default_options.merge(args.extract_options!)
50
24
  @command = args.shift.to_s.strip.to_sym
51
25
  @args = args
52
26
  @options.symbolize_keys!
53
27
  sanitize_command!
54
- @stdout, @stderr = String.new, String.new
55
- @full_stdout, @full_stderr = String.new, String.new
28
+ @stdout, @stderr, @full_stdout, @full_stderr = String.new, String.new, String.new, String.new
56
29
  end
57
30
 
58
31
  def complete?
@@ -83,6 +56,38 @@ module SSHKit
83
56
  end
84
57
  alias :failed? :failure?
85
58
 
59
+ def stdout
60
+ log_reader_deprecation('stdout')
61
+ @stdout
62
+ end
63
+
64
+ def stdout=(new_value)
65
+ log_writer_deprecation('stdout')
66
+ @stdout = new_value
67
+ end
68
+
69
+ def stderr
70
+ log_reader_deprecation('stderr')
71
+ @stderr
72
+ end
73
+
74
+ def stderr=(new_value)
75
+ log_writer_deprecation('stderr')
76
+ @stderr = new_value
77
+ end
78
+
79
+ def on_stdout(channel, data)
80
+ @stdout = data
81
+ @full_stdout += data
82
+ call_interaction_handler(:stdout, data, channel)
83
+ end
84
+
85
+ def on_stderr(channel, data)
86
+ @stderr = data
87
+ @full_stderr += data
88
+ call_interaction_handler(:stderr, data, channel)
89
+ end
90
+
86
91
  def exit_status=(new_exit_status)
87
92
  @finished_at = Time.now
88
93
  @exit_status = new_exit_status
@@ -125,7 +130,7 @@ module SSHKit
125
130
  end
126
131
 
127
132
  def verbosity
128
- if vb = options[:verbosity]
133
+ if (vb = options[:verbosity])
129
134
  case vb.class.name
130
135
  when 'Symbol' then return Logger.const_get(vb.to_s.upcase)
131
136
  when 'Fixnum' then return vb
@@ -136,12 +141,12 @@ module SSHKit
136
141
  end
137
142
 
138
143
  def should_map?
139
- !command.match /\s/
144
+ !command.match(/\s/)
140
145
  end
141
146
 
142
- def within(&block)
147
+ def within(&_block)
143
148
  return yield unless options[:in]
144
- "cd #{options[:in]} && %s" % yield
149
+ sprintf("cd #{options[:in]} && %s", yield)
145
150
  end
146
151
 
147
152
  def environment_hash
@@ -150,35 +155,33 @@ module SSHKit
150
155
 
151
156
  def environment_string
152
157
  environment_hash.collect do |key,value|
153
- if key.is_a? Symbol
154
- "#{key.to_s.upcase}=#{value}"
155
- else
156
- "#{key.to_s}=#{value}"
157
- end
158
+ key_string = key.is_a?(Symbol) ? key.to_s.upcase : key.to_s
159
+ escaped_value = value.to_s.gsub(/"/, '\"')
160
+ %{#{key_string}="#{escaped_value}"}
158
161
  end.join(' ')
159
162
  end
160
163
 
161
- def with(&block)
164
+ def with(&_block)
162
165
  return yield unless environment_hash.any?
163
- "( #{environment_string} %s )" % yield
166
+ sprintf("( export #{environment_string} ; %s )", yield)
164
167
  end
165
168
 
166
- def user(&block)
169
+ def user(&_block)
167
170
  return yield unless options[:user]
168
171
  "sudo -u #{options[:user]} #{environment_string + " " unless environment_string.empty?}-- sh -c '%s'" % %Q{#{yield}}
169
172
  end
170
173
 
171
- def in_background(&block)
174
+ def in_background(&_block)
172
175
  return yield unless options[:run_in_background]
173
- "( nohup %s > /dev/null & )" % yield
176
+ sprintf("( nohup %s > /dev/null & )", yield)
174
177
  end
175
178
 
176
- def umask(&block)
179
+ def umask(&_block)
177
180
  return yield unless SSHKit.config.umask
178
- "umask #{SSHKit.config.umask} && %s" % yield
181
+ sprintf("umask #{SSHKit.config.umask} && %s", yield)
179
182
  end
180
183
 
181
- def group(&block)
184
+ def group(&_block)
182
185
  return yield unless options[:group]
183
186
  "sg #{options[:group]} -c \\\"%s\\\"" % %Q{#{yield}}
184
187
  # We could also use the so-called heredoc format perhaps:
@@ -227,6 +230,24 @@ module SSHKit
227
230
  end
228
231
  end
229
232
 
233
+ def call_interaction_handler(stream_name, data, channel)
234
+ interaction_handler = options[:interaction_handler]
235
+ interaction_handler = MappingInteractionHandler.new(interaction_handler) if interaction_handler.kind_of?(Hash)
236
+ interaction_handler.on_data(self, stream_name, data, channel) if interaction_handler.respond_to?(:on_data)
237
+ end
238
+
239
+ def log_reader_deprecation(stream)
240
+ SSHKit.config.deprecation_logger.log(
241
+ "The #{stream} method on Command is deprecated. " \
242
+ "The @#{stream} attribute will be removed in a future release. Use full_#{stream}() instead."
243
+ )
244
+ end
245
+
246
+ def log_writer_deprecation(stream)
247
+ SSHKit.config.deprecation_logger.log(
248
+ "The #{stream}= method on Command is deprecated. The @#{stream} attribute will be removed in a future release."
249
+ )
250
+ end
230
251
  end
231
252
 
232
253
  end
@@ -6,7 +6,16 @@ module SSHKit
6
6
  attr_writer :output, :backend, :default_env
7
7
 
8
8
  def output
9
- @output ||= formatter(:pretty)
9
+ @output ||= use_format(:pretty)
10
+ end
11
+
12
+ def deprecation_logger
13
+ self.deprecation_output = $stderr if @deprecation_logger.nil?
14
+ @deprecation_logger
15
+ end
16
+
17
+ def deprecation_output=(out)
18
+ @deprecation_logger = DeprecationLogger.new(out)
10
19
  end
11
20
 
12
21
  def default_env
@@ -25,8 +34,29 @@ module SSHKit
25
34
  @output_verbosity = logger(verbosity)
26
35
  end
27
36
 
37
+ # TODO: deprecate in favor of `use_format`
28
38
  def format=(format)
29
- self.output = formatter(format)
39
+ use_format(format)
40
+ end
41
+
42
+ # Tell SSHKit to use the specified `formatter` for stdout. The formatter
43
+ # can be the name of a built-in SSHKit formatter, like `:pretty`, a
44
+ # formatter class, like `SSHKit::Formatter::Pretty`, or a custom formatter
45
+ # class you've written yourself.
46
+ #
47
+ # Additional arguments will be passed to the formatter's constructor.
48
+ #
49
+ # Example:
50
+ #
51
+ # config.use_format(:pretty)
52
+ #
53
+ # Is equivalent to:
54
+ #
55
+ # config.output = SSHKit::Formatter::Pretty.new($stdout)
56
+ #
57
+ def use_format(formatter, *args)
58
+ klass = formatter.is_a?(Class) ? formatter : formatter_class(formatter)
59
+ self.output = klass.new($stdout, *args)
30
60
  end
31
61
 
32
62
  def command_map
@@ -43,10 +73,13 @@ module SSHKit
43
73
  verbosity.is_a?(Integer) ? verbosity : Logger.const_get(verbosity.upcase)
44
74
  end
45
75
 
46
- def formatter(format)
47
- SSHKit::Formatter.constants.each do |const|
48
- return SSHKit::Formatter.const_get(const).new($stdout) if const.downcase.eql?(format.downcase)
76
+ def formatter_class(symbol)
77
+ name = symbol.to_s.downcase
78
+ found = SSHKit::Formatter.constants.find do |const|
79
+ const.to_s.downcase == name
49
80
  end
81
+ fail NameError, 'Unrecognized SSHKit::Formatter "#{symbol}"' if found.nil?
82
+ SSHKit::Formatter.const_get(found)
50
83
  end
51
84
 
52
85
  end
@@ -0,0 +1,17 @@
1
+ module SSHKit
2
+ class DeprecationLogger
3
+ def initialize(out)
4
+ @out = out
5
+ @previous_warnings = Set.new
6
+ end
7
+
8
+ def log(message)
9
+ return if @out.nil?
10
+ warning_msg = "[Deprecated] #{message}\n"
11
+ caller_line = caller.find { |line| !line.include?('lib/sshkit') }
12
+ warning_msg << " (Called from #{caller_line})\n" unless caller_line.nil?
13
+ @out << warning_msg unless @previous_warnings.include?(warning_msg)
14
+ @previous_warnings << warning_msg
15
+ end
16
+ end
17
+ end
@@ -9,15 +9,39 @@ module SSHKit
9
9
  extend Forwardable
10
10
  attr_reader :original_output
11
11
  def_delegators :@original_output, :read, :rewind
12
+ def_delegators :@color, :colorize
12
13
 
13
- def initialize(oio)
14
- @original_output = oio
14
+ def initialize(output)
15
+ @original_output = output
16
+ @color = SSHKit::Color.new(output)
15
17
  end
16
18
 
17
- def write(obj)
19
+ %w(fatal error warn info debug).each do |level|
20
+ define_method(level) do |message|
21
+ write(LogMessage.new(Logger.const_get(level.upcase), message))
22
+ end
23
+ end
24
+ alias :log :info
25
+
26
+ def log_command_start(command)
27
+ write(command)
28
+ end
29
+
30
+ def log_command_data(command, _stream_type, _stream_data)
31
+ write(command)
32
+ end
33
+
34
+ def log_command_exit(command)
35
+ write(command)
36
+ end
37
+
38
+ def <<(obj)
39
+ write(obj)
40
+ end
41
+
42
+ def write(_obj)
18
43
  raise "Abstract formatter should not be used directly, maybe you want SSHKit::Formatter::BlackHole"
19
44
  end
20
- alias :<< :write
21
45
 
22
46
  end
23
47
 
@@ -4,10 +4,9 @@ module SSHKit
4
4
 
5
5
  class BlackHole < Abstract
6
6
 
7
- def write(obj)
7
+ def write(_obj)
8
8
  # Nothing, nothing to do
9
9
  end
10
- alias :<< :write
11
10
 
12
11
  end
13
12
 
@@ -4,18 +4,11 @@ module SSHKit
4
4
 
5
5
  class Dot < Abstract
6
6
 
7
- def write(obj)
8
- return unless obj.is_a? SSHKit::Command
9
- if obj.finished?
10
- original_output << (obj.failure? ? c.red('.') : c.green('.'))
11
- end
7
+ def log_command_exit(command)
8
+ original_output << colorize('.', command.failure? ? :red : :green)
12
9
  end
13
- alias :<< :write
14
10
 
15
- private
16
-
17
- def c
18
- @c ||= Color
11
+ def write(_obj)
19
12
  end
20
13
 
21
14
  end
@@ -4,77 +4,52 @@ module SSHKit
4
4
 
5
5
  class Pretty < Abstract
6
6
 
7
+ LEVEL_NAMES = %w{ DEBUG INFO WARN ERROR FATAL }.freeze
8
+ LEVEL_COLORS = [:black, :blue, :yellow, :red, :red].freeze
9
+
7
10
  def write(obj)
8
- return if obj.verbosity < SSHKit.config.output_verbosity
9
- case obj
10
- when SSHKit::Command then write_command(obj)
11
- when SSHKit::LogMessage then write_log_message(obj)
11
+ if obj.kind_of?(SSHKit::LogMessage)
12
+ write_message(obj.verbosity, obj.to_s)
12
13
  else
13
- original_output << c.black(c.on_yellow("Output formatter doesn't know how to handle #{obj.class}\n"))
14
+ raise "write only supports formatting SSHKit::LogMessage, called with #{obj.class}: #{obj.inspect}"
14
15
  end
15
16
  end
16
- alias :<< :write
17
-
18
- private
19
17
 
20
- def write_command(command)
21
- unless command.started?
22
- original_output << "%6s %s\n" % [level(command.verbosity),
23
- uuid(command) + "Running #{c.yellow(c.bold(String(command)))} #{command.host.user ? "as #{c.blue(command.host.user)}@" : "on "}#{c.blue(command.host.to_s)}"]
24
- if SSHKit.config.output_verbosity == Logger::DEBUG
25
- original_output << "%6s %s\n" % [level(Logger::DEBUG),
26
- uuid(command) + "Command: #{c.blue(command.to_command)}"]
27
- end
28
- end
29
-
30
- if SSHKit.config.output_verbosity == Logger::DEBUG
31
- unless command.stdout.empty?
32
- command.stdout.lines.each do |line|
33
- original_output << "%6s %s" % [level(Logger::DEBUG),
34
- uuid(command) + c.green("\t" + line)]
35
- original_output << "\n" unless line[-1] == "\n"
36
- end
37
- command.stdout = ''
38
- end
39
-
40
- unless command.stderr.empty?
41
- command.stderr.lines.each do |line|
42
- original_output << "%6s %s" % [level(Logger::DEBUG),
43
- uuid(command) + c.red("\t" + line)]
44
- original_output << "\n" unless line[-1] == "\n"
45
- end
46
- command.stderr = ''
47
- end
48
- end
49
-
50
- if command.finished?
51
- original_output << "%6s %s\n" % [level(command.verbosity),
52
- uuid(command) + "Finished in #{sprintf('%5.3f seconds', command.runtime)} with exit status #{command.exit_status} (#{c.bold { command.failure? ? c.red('failed') : c.green('successful') }})."]
53
- end
18
+ def log_command_start(command)
19
+ host_prefix = command.host.user ? "as #{colorize(command.host.user, :blue)}@" : 'on '
20
+ message = "Running #{colorize(command, :yellow, :bold)} #{host_prefix}#{colorize(command.host, :blue)}"
21
+ write_message(command.verbosity, message, command.uuid)
22
+ write_message(Logger::DEBUG, "Command: #{colorize(command.to_command, :blue)}", command.uuid)
54
23
  end
55
24
 
56
- def write_log_message(log_message)
57
- original_output << "%6s %s\n" % [level(log_message.verbosity), log_message.to_s]
25
+ def log_command_data(command, stream_type, stream_data)
26
+ color = case stream_type
27
+ when :stdout then :green
28
+ when :stderr then :red
29
+ else raise "Unrecognised stream_type #{stream_type}, expected :stdout or :stderr"
30
+ end
31
+ write_message(Logger::DEBUG, colorize("\t#{stream_data}".chomp, color), command.uuid)
58
32
  end
59
33
 
60
- def c
61
- @c ||= Color
34
+ def log_command_exit(command)
35
+ runtime = sprintf('%5.3f seconds', command.runtime)
36
+ successful_or_failed = command.failure? ? colorize('failed', :red, :bold) : colorize('successful', :green, :bold)
37
+ message = "Finished in #{runtime} with exit status #{command.exit_status} (#{successful_or_failed})."
38
+ write_message(command.verbosity, message, command.uuid)
62
39
  end
63
40
 
64
- def uuid(obj)
65
- "[#{c.green(obj.uuid)}] "
66
- end
41
+ protected
67
42
 
68
- def level(verbosity)
69
- c.send(level_formatting(verbosity), level_names(verbosity))
43
+ def format_message(verbosity, message, uuid=nil)
44
+ message = "[#{colorize(uuid, :green)}] #{message}" unless uuid.nil?
45
+ level = colorize(Pretty::LEVEL_NAMES[verbosity], Pretty::LEVEL_COLORS[verbosity])
46
+ '%6s %s' % [level, message]
70
47
  end
71
48
 
72
- def level_formatting(level_num)
73
- %w{ black blue yellow red red }[level_num]
74
- end
49
+ private
75
50
 
76
- def level_names(level_num)
77
- %w{ DEBUG INFO WARN ERROR FATAL }[level_num]
51
+ def write_message(verbosity, message, uuid=nil)
52
+ original_output << "#{format_message(verbosity, message, uuid)}\n" if verbosity >= SSHKit.config.output_verbosity
78
53
  end
79
54
 
80
55
  end