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