sshkit 1.7.1 → 1.8.0

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 (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