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
@@ -1,54 +1,16 @@
1
-
2
1
  module SSHKit
3
2
 
4
3
  module Formatter
5
4
 
6
- class SimpleText < Abstract
7
-
8
- def write(obj)
9
- return if obj.verbosity < SSHKit.config.output_verbosity
10
- case obj
11
- when SSHKit::Command then write_command(obj)
12
- when SSHKit::LogMessage then write_log_message(obj)
13
- else
14
- original_output << "Output formatter doesn't know how to handle #{obj.class}\n"
15
- end
16
- end
17
- alias :<< :write
18
-
19
- private
20
-
21
- def write_command(command)
22
- unless command.started?
23
- original_output << "Running #{String(command)} #{command.host.user ? "as #{command.host.user}@" : "on "}#{command.host}\n"
24
- if SSHKit.config.output_verbosity == Logger::DEBUG
25
- original_output << "Command: #{command.to_command}" + "\n"
26
- end
27
- end
28
-
29
- if SSHKit.config.output_verbosity == Logger::DEBUG
30
- unless command.stdout.empty?
31
- command.stdout.lines.each do |line|
32
- original_output << "\t" + line
33
- original_output << "\n" unless line[-1] == "\n"
34
- end
35
- end
36
-
37
- unless command.stderr.empty?
38
- command.stderr.lines.each do |line|
39
- original_output << "\t" + line
40
- original_output << "\n" unless line[-1] == "\n"
41
- end
42
- end
43
- end
5
+ class SimpleText < Pretty
44
6
 
45
- if command.finished?
46
- original_output << "Finished in #{sprintf('%5.3f seconds', command.runtime)} with exit status #{command.exit_status} (#{ command.failure? ? 'failed' : 'successful' }).\n"
47
- end
7
+ # Historically, SimpleText formatter was used to disable coloring, so we maintain that behaviour
8
+ def colorize(obj, _color, _mode=nil)
9
+ obj.to_s
48
10
  end
49
11
 
50
- def write_log_message(log_message)
51
- original_output << log_message.to_s + "\n"
12
+ def format_message(_verbosity, message, _uuid=nil)
13
+ message
52
14
  end
53
15
 
54
16
  end
data/lib/sshkit/host.rb CHANGED
@@ -1,5 +1,4 @@
1
1
  require 'ostruct'
2
- require 'etc'
3
2
 
4
3
  module SSHKit
5
4
 
@@ -26,7 +25,7 @@ module SSHKit
26
25
  if host_string_or_options_hash == :local
27
26
  @local = true
28
27
  @hostname = "localhost"
29
- @user = Etc.getpwuid.name
28
+ @user = ENV['USER'] || ENV['LOGNAME'] || ENV['USERNAME']
30
29
  elsif !host_string_or_options_hash.is_a?(Hash)
31
30
  suitable_parsers = [
32
31
  SimpleHostParser,
@@ -100,7 +99,7 @@ module SSHKit
100
99
  class SimpleHostParser
101
100
 
102
101
  def self.suitable?(host_string)
103
- !host_string.match /[:|@]/
102
+ !host_string.match(/[:|@]/)
104
103
  end
105
104
 
106
105
  def initialize(host_string)
@@ -128,7 +127,7 @@ module SSHKit
128
127
  class HostWithPortParser < SimpleHostParser
129
128
 
130
129
  def self.suitable?(host_string)
131
- !host_string.match /[@|\[|\]]/
130
+ !host_string.match(/[@|\[|\]]/)
132
131
  end
133
132
 
134
133
  def port
@@ -145,7 +144,7 @@ module SSHKit
145
144
  # :nodoc:
146
145
  class HostWithUsernameAndPortParser < SimpleHostParser
147
146
  def self.suitable?(host_string)
148
- host_string.match /@.*:\d+/
147
+ host_string.match(/@.*:\d+/)
149
148
  end
150
149
  def username
151
150
  @host_string.split(/:|@/)[0]
@@ -163,7 +162,7 @@ module SSHKit
163
162
  class IPv6HostWithPortParser < SimpleHostParser
164
163
 
165
164
  def self.suitable?(host_string)
166
- host_string.match /[a-fA-F0-9:]+:\d+/
165
+ host_string.match(/[a-fA-F0-9:]+:\d+/)
167
166
  end
168
167
 
169
168
  def port
data/lib/sshkit/logger.rb CHANGED
@@ -1,6 +1,5 @@
1
1
  module SSHKit
2
2
  class Logger
3
- TRACE = -1
4
3
  DEBUG = 0
5
4
  INFO = 1
6
5
  WARN = 2
@@ -0,0 +1,47 @@
1
+ module SSHKit
2
+
3
+ class MappingInteractionHandler
4
+
5
+ def initialize(mapping, log_level=nil)
6
+ @log_level = log_level
7
+ @mapping_proc = case mapping
8
+ when Hash
9
+ lambda do |server_output|
10
+ first_matching_key_value = mapping.find { |k, _v| k === server_output }
11
+ first_matching_key_value.nil? ? nil : first_matching_key_value.last
12
+ end
13
+ when Proc
14
+ mapping
15
+ else
16
+ raise "Unsupported mapping type: #{mapping.class} - only Hash and Proc mappings are supported"
17
+ end
18
+ end
19
+
20
+ def on_data(_command, stream_name, data, channel)
21
+ log("Looking up response for #{stream_name} message #{data.inspect}")
22
+
23
+ response_data = @mapping_proc.call(data)
24
+
25
+ if response_data.nil?
26
+ log("Unable to find interaction handler mapping for #{stream_name}: #{data.inspect} so no response was sent")
27
+ else
28
+ log("Sending #{response_data.inspect}")
29
+ if channel.respond_to?(:send_data) # Net SSH Channel
30
+ channel.send_data(response_data)
31
+ elsif channel.respond_to?(:write) # Local IO
32
+ channel.write(response_data)
33
+ else
34
+ raise "Unable to write response data to channel #{channel.inspect} - does not support 'send_data' or 'write'"
35
+ end
36
+ end
37
+ end
38
+
39
+ private
40
+
41
+ def log(message)
42
+ SSHKit.config.output.send(@log_level, message) unless @log_level.nil?
43
+ end
44
+
45
+ end
46
+
47
+ end
@@ -11,7 +11,7 @@ module SSHKit
11
11
  threads << Thread.new(host) do |h|
12
12
  begin
13
13
  backend(h, &block).run
14
- rescue Exception => e
14
+ rescue StandardError => e
15
15
  e2 = ExecuteError.new e
16
16
  raise e2, "Exception while executing #{host.user ? "as #{host.user}@" : "on host "}#{host}: #{e.message}"
17
17
  end
@@ -19,7 +19,7 @@ module SSHKit
19
19
  private
20
20
  def run_backend(host, &block)
21
21
  backend(host, &block).run
22
- rescue Exception => e
22
+ rescue StandardError => e
23
23
  e2 = ExecuteError.new e
24
24
  raise e2, "Exception while executing #{host.user ? "as #{host.user}@" : "on host "}#{host}: #{e.message}"
25
25
  end
@@ -1,3 +1,3 @@
1
1
  module SSHKit
2
- VERSION = "1.7.1"
2
+ VERSION = "1.8.0"
3
3
  end
data/sshkit.gemspec CHANGED
@@ -19,7 +19,6 @@ Gem::Specification.new do |gem|
19
19
 
20
20
  gem.add_runtime_dependency('net-ssh', '>= 2.8.0')
21
21
  gem.add_runtime_dependency('net-scp', '>= 1.1.2')
22
- gem.add_runtime_dependency('colorize', '>= 0.7.0')
23
22
 
24
23
  gem.add_development_dependency('minitest', ['>= 2.11.3', '< 2.12.0'])
25
24
  gem.add_development_dependency('rake')
@@ -6,13 +6,14 @@ module SSHKit
6
6
  class TestLocal < MiniTest::Unit::TestCase
7
7
 
8
8
  def setup
9
+ super
9
10
  SSHKit.config.output = SSHKit::Formatter::BlackHole.new($stdout)
10
11
  end
11
12
 
12
13
  def test_capture
13
14
  captured_command_result = ''
14
15
  Local.new do
15
- captured_command_result = capture(:echo, 'foo')
16
+ captured_command_result = capture(:echo, 'foo', strip: false)
16
17
  end.run
17
18
  assert_equal "foo\n", captured_command_result
18
19
  end
@@ -35,6 +36,18 @@ module SSHKit
35
36
  assert_equal true, succeeded_test_result
36
37
  assert_equal false, failed_test_result
37
38
  end
39
+
40
+ def test_interaction_handler
41
+ captured_command_result = nil
42
+ Local.new do
43
+ command = 'echo Enter Data; read the_data; echo Captured $the_data;'
44
+ captured_command_result = capture(command, interaction_handler: {
45
+ "Enter Data\n" => "SOME DATA\n",
46
+ "Captured SOME DATA\n" => nil
47
+ })
48
+ end.run
49
+ assert_equal("Enter Data\nCaptured SOME DATA", captured_command_result)
50
+ end
38
51
  end
39
52
  end
40
53
  end
@@ -10,59 +10,45 @@ module SSHKit
10
10
 
11
11
  def setup
12
12
  super
13
- SSHKit.config.output = SSHKit::Formatter::BlackHole.new($stdout)
14
- end
15
-
16
- def block_to_run
17
- lambda do |host|
18
- execute 'date'
19
- execute :ls, '-l', '/some/directory'
20
- with rails_env: :production do
21
- within '/tmp' do
22
- as :root do
23
- execute :touch, 'restart.txt'
24
- end
25
- end
26
- end
27
- end
13
+ @output = String.new
14
+ SSHKit.config.output_verbosity = :debug
15
+ SSHKit.config.output = SSHKit::Formatter::SimpleText.new(@output)
28
16
  end
29
17
 
30
18
  def a_host
31
19
  VagrantWrapper.hosts['one']
32
20
  end
33
21
 
34
- def printer
35
- Netssh.new(a_host, &block_to_run)
36
- end
22
+ def test_simple_netssh
23
+ Netssh.new(a_host) do
24
+ execute 'date'
25
+ execute :ls, '-l'
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.run
37
34
 
38
- def simple_netssh
39
- SSHKit.capture_output(sio) do
40
- printer.run
41
- end
42
- sio.rewind
43
- result = sio.read
44
- assert_equal <<-EOEXPECTED.unindent, result
45
- if test ! -d /opt/sites/example.com; then echo "Directory does not exist '/opt/sites/example.com'" 2>&1; false; fi
46
- cd /opt/sites/example.com && /usr/bin/env date
47
- cd /opt/sites/example.com && /usr/bin/env ls -l /some/directory
48
- if test ! -d /opt/sites/example.com/tmp; then echo "Directory does not exist '/opt/sites/example.com/tmp'" 2>&1; false; fi
49
- 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
50
- cd /opt/sites/example.com/tmp && ( RAILS_ENV=production ( sudo su -u root /usr/bin/env touch restart.txt ) )
35
+ command_lines = @output.lines.select { |line| line.start_with?('Command:') }
36
+ assert_equal <<-EOEXPECTED.unindent, command_lines.join
37
+ Command: /usr/bin/env date
38
+ Command: /usr/bin/env ls -l
39
+ Command: if test ! -d /tmp; then echo \"Directory does not exist '/tmp'\" 1>&2; false; fi
40
+ Command: if ! sudo -u root whoami > /dev/null; then echo \"You cannot switch to user 'root' using sudo, please check the sudoers file\" 1>&2; false; fi
41
+ Command: cd /tmp && ( export RAILS_ENV="production" ; sudo -u root RAILS_ENV="production" -- sh -c '/usr/bin/env touch restart.txt' )
51
42
  EOEXPECTED
52
43
  end
53
44
 
54
45
  def test_capture
55
- File.open('/dev/null', 'w') do |dnull|
56
- SSHKit.capture_output(dnull) do
57
- captured_command_result = nil
58
- Netssh.new(a_host) do |host|
59
- captured_command_result = capture(:uname)
60
- end.run
61
-
62
- assert captured_command_result
63
- assert_match captured_command_result, /Linux|Darwin/
64
- end
65
- end
46
+ captured_command_result = nil
47
+ Netssh.new(a_host) do |_host|
48
+ captured_command_result = capture(:uname)
49
+ end.run
50
+
51
+ assert_includes %W(Linux Darwin), captured_command_result
66
52
  end
67
53
 
68
54
  def test_ssh_option_merge
@@ -76,9 +62,19 @@ module SSHKit
76
62
  assert_equal({ forward_agent: false, paranoid: true }, host_ssh_options)
77
63
  end
78
64
 
65
+ def test_env_vars_substituion_in_subshell
66
+ captured_command_result = nil
67
+ Netssh.new(a_host) do |_host|
68
+ with some_env_var: :some_value do
69
+ captured_command_result = capture(:echo, '$SOME_ENV_VAR')
70
+ end
71
+ end.run
72
+ assert_equal "some_value", captured_command_result
73
+ end
74
+
79
75
  def test_execute_raises_on_non_zero_exit_status_and_captures_stdout_and_stderr
80
76
  err = assert_raises SSHKit::Command::Failed do
81
- Netssh.new(a_host) do |host|
77
+ Netssh.new(a_host) do |_host|
82
78
  execute :echo, "'Test capturing stderr' 1>&2; false"
83
79
  end.run
84
80
  end
@@ -86,27 +82,27 @@ module SSHKit
86
82
  end
87
83
 
88
84
  def test_test_does_not_raise_on_non_zero_exit_status
89
- Netssh.new(a_host) do |host|
85
+ Netssh.new(a_host) do |_host|
90
86
  test :false
91
87
  end.run
92
88
  end
93
89
 
94
- def test_upload_file
95
- file_contents = ""
90
+ def test_upload_and_then_capture_file_contents
91
+ actual_file_contents = ""
96
92
  file_name = File.join("/tmp", SecureRandom.uuid)
97
93
  File.open file_name, 'w+' do |f|
98
- f.write 'example_file'
94
+ f.write "Some Content\nWith a newline and trailing spaces \n "
99
95
  end
100
96
  Netssh.new(a_host) do
101
97
  upload!(file_name, file_name)
102
- file_contents = capture(:cat, file_name)
98
+ actual_file_contents = capture(:cat, file_name, strip: false)
103
99
  end.run
104
- assert_equal "example_file", file_contents
100
+ assert_equal "Some Content\nWith a newline and trailing spaces \n ", actual_file_contents
105
101
  end
106
102
 
107
103
  def test_upload_string_io
108
104
  file_contents = ""
109
- Netssh.new(a_host) do |host|
105
+ Netssh.new(a_host) do |_host|
110
106
  file_name = File.join("/tmp", SecureRandom.uuid)
111
107
  upload!(StringIO.new('example_io'), file_name)
112
108
  file_contents = download!(file_name)
@@ -128,6 +124,18 @@ module SSHKit
128
124
  end.run
129
125
  assert_equal File.open(file_name).read, file_contents
130
126
  end
127
+
128
+ def test_interaction_handler
129
+ captured_command_result = nil
130
+ Netssh.new(a_host) do
131
+ command = 'echo Enter Data; read the_data; echo Captured $the_data;'
132
+ captured_command_result = capture(command, interaction_handler: {
133
+ "Enter Data\n" => "SOME DATA\n",
134
+ "Captured SOME DATA\n" => nil
135
+ })
136
+ end.run
137
+ assert_equal("Enter Data\nCaptured SOME DATA", captured_command_result)
138
+ end
131
139
  end
132
140
 
133
141
  end
data/test/helper.rb CHANGED
@@ -40,7 +40,7 @@ class FunctionalTest < MiniTest::Unit::TestCase
40
40
  def create_user_with_key(username, password = :secret)
41
41
  username, password = username.to_s, password.to_s
42
42
 
43
- keys = VagrantWrapper.hosts.collect do |name, host|
43
+ keys = VagrantWrapper.hosts.collect do |_name, host|
44
44
  Net::SSH.start(host.hostname, host.user, port: host.port, password: host.password) do |ssh|
45
45
 
46
46
  # Remove the user, make it again, force-generate a key for him
@@ -70,7 +70,7 @@ class FunctionalTest < MiniTest::Unit::TestCase
70
70
  end
71
71
  end
72
72
 
73
- Hash[VagrantWrapper.hosts.collect { |n, h| n.to_sym }.zip(keys)]
73
+ Hash[VagrantWrapper.hosts.collect { |n, _h| n.to_sym }.zip(keys)]
74
74
  end
75
75
 
76
76
  end
@@ -0,0 +1,145 @@
1
+ require 'helper'
2
+
3
+ module SSHKit
4
+
5
+ module Backend
6
+
7
+ class TestAbstract < UnitTest
8
+
9
+ def test_make
10
+ backend = ExampleBackend.new do
11
+ make %w(some command)
12
+ end
13
+
14
+ backend.run
15
+
16
+ assert_equal '/usr/bin/env make some command', backend.executed_command.to_command
17
+ end
18
+
19
+ def test_rake
20
+ backend = ExampleBackend.new do
21
+ rake %w(a command)
22
+ end
23
+
24
+ backend.run
25
+
26
+ assert_equal '/usr/bin/env rake a command', backend.executed_command.to_command
27
+ end
28
+
29
+ def test_execute_creates_and_executes_command_with_default_options
30
+ backend = ExampleBackend.new do
31
+ execute :ls, '-l', '/some/directory'
32
+ end
33
+
34
+ backend.run
35
+
36
+ assert_equal '/usr/bin/env ls -l /some/directory', backend.executed_command.to_command
37
+ assert_equal(
38
+ {:raise_on_non_zero_exit=>true, :run_in_background=>false, :in=>nil, :env=>nil, :host=>ExampleBackend.example_host, :user=>nil, :group=>nil},
39
+ backend.executed_command.options
40
+ )
41
+ end
42
+
43
+ def test_test_method_creates_and_executes_command_with_false_raise_on_non_zero_exit
44
+ backend = ExampleBackend.new do
45
+ test '[ -d /some/file ]'
46
+ end
47
+
48
+ backend.run
49
+
50
+ assert_equal '[ -d /some/file ]', backend.executed_command.to_command
51
+ assert_equal false, backend.executed_command.options[:raise_on_non_zero_exit], 'raise_on_non_zero_exit option'
52
+ end
53
+
54
+ def test_capture_creates_and_executes_command_and_returns_stripped_output
55
+ output = nil
56
+ backend = ExampleBackend.new do
57
+ output = capture :cat, '/a/file'
58
+ end
59
+ backend.full_stdout = "Some stdout\n "
60
+
61
+ backend.run
62
+
63
+ assert_equal '/usr/bin/env cat /a/file', backend.executed_command.to_command
64
+ assert_equal 'Some stdout', output
65
+ end
66
+
67
+ def test_capture_supports_disabling_strip
68
+ output = nil
69
+ backend = ExampleBackend.new do
70
+ output = capture :cat, '/a/file', :strip => false
71
+ end
72
+ backend.full_stdout = "Some stdout\n "
73
+
74
+ backend.run
75
+
76
+ assert_equal '/usr/bin/env cat /a/file', backend.executed_command.to_command
77
+ assert_equal "Some stdout\n ", output
78
+ end
79
+
80
+ def test_background_logs_deprecation_warnings
81
+ deprecation_out = ''
82
+ SSHKit.config.deprecation_output = deprecation_out
83
+
84
+ ExampleBackend.new do
85
+ background :ls
86
+ end.run
87
+
88
+ lines = deprecation_out.lines.to_a
89
+
90
+ assert_equal 2, lines.length
91
+
92
+ assert_equal("[Deprecated] The background method is deprecated. Blame badly behaved pseudo-daemons!\n", lines[0])
93
+ assert_match(/ \(Called from.*test_abstract.rb:\d+:in `block in test_background_logs_deprecation_warnings'\)\n/, lines[1])
94
+ end
95
+
96
+ def test_calling_abstract_with_undefined_execute_command_raises_exception
97
+ abstract = Abstract.new(ExampleBackend.example_host) do
98
+ execute(:some_command)
99
+ end
100
+
101
+ assert_raises(SSHKit::Backend::MethodUnavailableError) do
102
+ abstract.run
103
+ end
104
+ end
105
+
106
+ def test_abstract_backend_can_be_configured
107
+ Abstract.configure do |config|
108
+ config.some_option = 100
109
+ end
110
+
111
+ assert_equal 100, Abstract.config.some_option
112
+ end
113
+
114
+ def test_invoke_raises_no_method_error
115
+ assert_raises NoMethodError do
116
+ ExampleBackend.new.invoke :echo
117
+ end
118
+ end
119
+
120
+ # Use a concrete ExampleBackend rather than a mock for improved assertion granularity
121
+ class ExampleBackend < Abstract
122
+ attr_writer :full_stdout
123
+ attr_reader :executed_command
124
+
125
+ def initialize(&block)
126
+ block = block.nil? ? lambda {} : block
127
+ super(ExampleBackend.example_host, &block)
128
+ end
129
+
130
+ def execute_command(command)
131
+ @executed_command = command
132
+ command.on_stdout(nil, @full_stdout) unless @full_stdout.nil?
133
+ end
134
+
135
+ def ExampleBackend.example_host
136
+ Host.new(:'example.com')
137
+ end
138
+
139
+ end
140
+
141
+ end
142
+
143
+ end
144
+
145
+ end