sshkit 1.18.1 → 1.21.0

Sign up to get free protection for your applications and to get access to all the features.
data/Dangerfile CHANGED
@@ -1 +1 @@
1
- danger.import_dangerfile(github: "capistrano/danger")
1
+ danger.import_dangerfile(github: "capistrano/danger", branch: "no-changelog")
data/Gemfile CHANGED
@@ -7,13 +7,6 @@ platforms :rbx do
7
7
  gem 'json'
8
8
  end
9
9
 
10
- # Chandler requires Ruby >= 2.1.0, but depending on the Travis environment,
11
- # we may not meet that requirement. Only include the chandler gem if the Ruby
12
- # requirement is met. (Chandler is used only for `rake release`; see Rakefile.)
13
- if Gem::Requirement.new('>= 2.1.0').satisfied_by?(Gem::Version.new(RUBY_VERSION))
14
- gem 'chandler', '>= 0.1.1'
15
- end
16
-
17
10
  # public_suffix 3+ requires ruby 2.1+
18
11
  if Gem::Requirement.new('< 2.1').satisfied_by?(Gem::Version.new(RUBY_VERSION))
19
12
  gem 'public_suffix', '< 3'
data/README.md CHANGED
@@ -6,29 +6,31 @@ more servers.
6
6
  [![Gem Version](https://badge.fury.io/rb/sshkit.svg)](https://rubygems.org/gems/sshkit)
7
7
  [![Build Status](https://travis-ci.org/capistrano/sshkit.svg?branch=master)](https://travis-ci.org/capistrano/sshkit)
8
8
 
9
- ## How might it work?
9
+ ## Example
10
10
 
11
- The typical use-case looks something like this:
11
+ - Connect to 2 servers
12
+ - Execute commands as `deploy` user with `RAILS_ENV=production`
13
+ - Execute commands in serial (default is `:parallel`)
12
14
 
13
15
  ```ruby
14
16
  require 'sshkit'
15
17
  require 'sshkit/dsl'
16
18
  include SSHKit::DSL
17
19
 
18
- on %w{1.example.com 2.example.com}, in: :sequence, wait: 5 do |host|
20
+ on ["1.example.com", "2.example.com"], in: :sequence do |host|
21
+ puts "Now executing on #{host}"
19
22
  within "/opt/sites/example.com" do
20
23
  as :deploy do
21
- with rails_env: :production do
22
- rake "assets:precompile"
23
- runner "S3::Sync.notify"
24
- execute :node, "socket_server.js"
24
+ with RAILS_ENV: 'production' do
25
+ execute :rake, "assets:precompile"
26
+ execute :rails, "runner", "S3::Sync.notify"
25
27
  end
26
28
  end
27
29
  end
28
30
  end
29
31
  ```
30
32
 
31
- You can find many other examples of how to use SSHKit over in [EXAMPLES.md](EXAMPLES.md).
33
+ Many other examples are in [EXAMPLES.md](EXAMPLES.md).
32
34
 
33
35
  ## Basic usage
34
36
 
@@ -37,7 +39,7 @@ You can pass one or more hosts as parameters; this runs commands via SSH. Altern
37
39
  pass `:local` to run commands locally. By default SSKit will run the commands on all hosts in
38
40
  parallel.
39
41
 
40
- #### Running commands
42
+ ### Running commands
41
43
 
42
44
  All backends support the `execute(*args)`, `test(*args)` & `capture(*args)` methods
43
45
  for executing a command. You can call any of these methods in the context of an `on()`
@@ -63,7 +65,7 @@ end
63
65
  By default the `capture` methods strips whitespace. If you need to preserve whitespace
64
66
  you can pass the `strip: false` option: `capture(:ls, '-l', strip: false)`
65
67
 
66
- #### Transferring files
68
+ ### Transferring files
67
69
 
68
70
  All backends also support the `upload!` and `download!` methods for transferring files.
69
71
  For the remote backend, the file is transferred with scp.
@@ -75,7 +77,7 @@ on '1.example.com' do
75
77
  end
76
78
  ```
77
79
 
78
- #### Users, working directories, environment variables and umask
80
+ ### Users, working directories, environment variables and umask
79
81
 
80
82
  When running commands, you can tell SSHKit to set up the context for those
81
83
  commands using the following methods:
@@ -113,6 +115,11 @@ the raised error.
113
115
  Helpers such as `runner()` and `rake()` which expand to `execute(:rails, "runner", ...)` and
114
116
  `execute(:rake, ...)` are convenience helpers for Ruby, and Rails based apps.
115
117
 
118
+ ### Verbosity / Silence
119
+
120
+ - raise verbosity of a command: `execute "echo DEAD", verbosity: :ERROR`
121
+ - hide a command from output: `execute "echo HIDDEN", verbosity: :DEBUG`
122
+
116
123
  ## Parallel
117
124
 
118
125
  Notice on the `on()` call the `in: :sequence` option, the following will do
@@ -567,10 +574,20 @@ In order to do special gymnastics with SSH, tunneling, aliasing, complex options
567
574
 
568
575
  These system level files are the preferred way of setting up tunneling and proxies because the system implementations of these things are faster and better than the Ruby implementations you would get if you were to configure them through Net::SSH. In cases where it's not possible (Windows?), it should be possible to make use of the Net::SSH APIs to setup tunnels and proxy commands before deferring control to Capistrano/SSHKit..
569
576
 
570
- ## SSHKit Related Blog Posts
577
+ ## Proxying
571
578
 
572
- [SSHKit Gem Basics](http://www.rubyplus.com/articles/591)
579
+ To connect to the target host via a jump/bastion host, use a `Net::SSH::Proxy::Jump`
573
580
 
574
- [SSHKit Gem Part 2](http://www.rubyplus.com/articles/601)
581
+ ```ruby
582
+ host = SSHKit::Host.new(
583
+ hostname: 'target.host.com',
584
+ ssh_options: { proxy: Net::SSH::Proxy::Jump.new("proxy.bar.com") }
585
+ )
586
+ on [host] do
587
+ execute :echo, '1'
588
+ end
589
+ ```
590
+
591
+ ## SSHKit Related Blog Posts
575
592
 
576
593
  [Embedded Capistrano with SSHKit](http://ryandoyle.net/posts/embedded-capistrano-with-sshkit/)
data/RELEASING.md CHANGED
@@ -5,7 +5,6 @@
5
5
  * You must have commit rights to the SSHKit repository.
6
6
  * You must have push rights for the sshkit gem on rubygems.org.
7
7
  * You must be using Ruby >= 2.1.0.
8
- * Your `~/.netrc` must be configured with your GitHub credentials, [as explained here](https://github.com/mattbrictson/chandler#2-configure-netrc).
9
8
 
10
9
  ## How to release
11
10
 
@@ -13,6 +12,6 @@
13
12
  2. **Ensure the tests are passing by running `rake test`.** If functional tests fail, ensure you have [Vagrant](https://www.vagrantup.com) installed and have started it with `vagrant up`.
14
13
  3. Determine which would be the correct next version number according to [semver](http://semver.org/).
15
14
  4. Update the version in `./lib/sshkit/version.rb`.
16
- 5. Update the `CHANGELOG`.
17
- 6. Commit the changelog and version in a single commit, the message should be "Preparing vX.Y.Z"
18
- 7. Run `rake release`; this will tag, push to GitHub, publish to rubygems.org, and upload the latest changelog entry to the [GitHub releases page](https://github.com/capistrano/sshkit/releases).
15
+ 5. Commit the `version.rb` change with a message like "Preparing vX.Y.Z"
16
+ 6. Run `rake release`; this will tag, push to GitHub, and publish to rubygems.org
17
+ 7. Update the draft release on the [GitHub releases page](https://github.com/capistrano/sshkit/releases) to point to the new tag and publish the release
data/Rakefile CHANGED
@@ -30,10 +30,7 @@ RuboCop::RakeTask.new(:lint) do |task|
30
30
  task.options = ['--lint']
31
31
  end
32
32
 
33
- task "release:rubygem_push" do
34
- # Delay loading Chandler until this point, since it requires Ruby >= 2.1,
35
- # which may not be available in all environments (e.g. Travis).
36
- # We assume that the person doing `rake release` has Ruby >= 2.1.
37
- require "chandler/tasks"
38
- Rake.application.invoke_task("chandler:push")
33
+ Rake::Task["release"].enhance do
34
+ puts "Don't forget to publish the release on GitHub!"
35
+ system "open https://github.com/capistrano/sshkit/releases"
39
36
  end
@@ -0,0 +1,9 @@
1
+ # tiny example so you can play with the sshkit or make a failing example for an issue
2
+ require 'bundler/setup'
3
+ require 'sshkit'
4
+ require 'sshkit/dsl'
5
+ include SSHKit::DSL
6
+
7
+ on [ENV.fetch("HOST")] do
8
+ execute "echo hello"
9
+ end
@@ -1,3 +1,5 @@
1
+ require 'shellwords'
2
+
1
3
  module SSHKit
2
4
 
3
5
  module Backend
@@ -80,13 +82,14 @@ module SSHKit
80
82
 
81
83
  def within(directory, &_block)
82
84
  (@pwd ||= []).push directory.to_s
85
+ escaped = Command.shellescape_except_tilde(pwd_path)
83
86
  execute <<-EOTEST, verbosity: Logger::DEBUG
84
- if test ! -d #{File.join(@pwd)}
85
- then echo "Directory does not exist '#{File.join(@pwd)}'" 1>&2
87
+ if test ! -d #{escaped}
88
+ then echo "Directory does not exist '#{escaped}'" 1>&2
86
89
  false
87
90
  fi
88
- EOTEST
89
- yield
91
+ EOTEST
92
+ yield
90
93
  ensure
91
94
  @pwd.pop
92
95
  end
@@ -108,8 +111,8 @@ module SSHKit
108
111
  @group = nil
109
112
  end
110
113
  execute <<-EOTEST, verbosity: Logger::DEBUG
111
- if ! sudo -u #{@user} whoami > /dev/null
112
- then echo "You cannot switch to user '#{@user}' using sudo, please check the sudoers file" 1>&2
114
+ if ! sudo -u #{@user.to_s.shellescape} whoami > /dev/null
115
+ then echo "You cannot switch to user '#{@user.to_s.shellescape}' using sudo, please check the sudoers file" 1>&2
113
116
  false
114
117
  fi
115
118
  EOTEST
@@ -21,7 +21,7 @@ end
21
21
 
22
22
  # The ConnectionPool caches connections and allows them to be reused, so long as
23
23
  # the reuse happens within the `idle_timeout` period. Timed out connections are
24
- # closed, forcing a new connection to be used in that case.
24
+ # eventually closed, forcing a new connection to be used in that case.
25
25
  #
26
26
  # Additionally, a background thread is started to check for abandoned
27
27
  # connections that have timed out without any attempt at being reused. These
@@ -46,7 +46,11 @@ class SSHKit::Backend::ConnectionPool
46
46
  @caches = {}
47
47
  @caches.extend(MonitorMixin)
48
48
  @timed_out_connections = Queue.new
49
- Thread.new { run_eviction_loop }
49
+
50
+ # Spin up eviction loop only if caching is enabled
51
+ if cache_enabled?
52
+ Thread.new { run_eviction_loop }
53
+ end
50
54
  end
51
55
 
52
56
  # Creates a new connection or reuses a cached connection (if possible) and
@@ -133,7 +137,7 @@ class SSHKit::Backend::ConnectionPool
133
137
  process_deferred_close
134
138
 
135
139
  # Periodically sweep all Caches to evict stale connections
136
- sleep([idle_timeout, 5].min)
140
+ sleep(5)
137
141
  caches.values.each(&:evict)
138
142
  end
139
143
  end
@@ -109,7 +109,8 @@ module SSHKit
109
109
  message = "#{action} #{name} #{percentage.round(2)}%"
110
110
  percentage_r = (percentage / log_percent).truncate * log_percent
111
111
  if percentage_r > 0 && (last_name != name || last_percentage != percentage_r)
112
- info message
112
+ verbosity = (options[:verbosity] || :INFO).downcase # TODO: ideally reuse command.rb logic
113
+ public_send verbosity, message
113
114
  last_name = name
114
115
  last_percentage = percentage_r
115
116
  else
@@ -1,5 +1,6 @@
1
1
  require 'digest/sha1'
2
2
  require 'securerandom'
3
+ require 'shellwords'
3
4
 
4
5
  # @author Lee Hambley
5
6
  module SSHKit
@@ -9,7 +10,7 @@ module SSHKit
9
10
 
10
11
  Failed = Class.new(SSHKit::StandardError)
11
12
 
12
- attr_reader :command, :args, :options, :started_at, :started, :exit_status, :full_stdout, :full_stderr
13
+ attr_reader :command, :args, :options, :started_at, :started, :exit_status, :full_stdout, :full_stderr, :uuid
13
14
 
14
15
  # Initialize a new Command object
15
16
  #
@@ -25,6 +26,7 @@ module SSHKit
25
26
  @args = args
26
27
  @options.symbolize_keys!
27
28
  @stdout, @stderr, @full_stdout, @full_stderr = String.new, String.new, String.new, String.new
29
+ @uuid = Digest::SHA1.hexdigest(SecureRandom.random_bytes(10))[0..7]
28
30
  end
29
31
 
30
32
  def complete?
@@ -41,10 +43,6 @@ module SSHKit
41
43
  @started = new_started
42
44
  end
43
45
 
44
- def uuid
45
- @uuid ||= Digest::SHA1.hexdigest(SecureRandom.random_bytes(10))[0..7]
46
- end
47
-
48
46
  def success?
49
47
  exit_status.nil? ? false : exit_status.to_i == 0
50
48
  end
@@ -145,7 +143,7 @@ module SSHKit
145
143
 
146
144
  def within(&_block)
147
145
  return yield unless options[:in]
148
- sprintf("cd #{options[:in]} && %s", yield)
146
+ "cd #{self.class.shellescape_except_tilde(options[:in])} && #{yield}"
149
147
  end
150
148
 
151
149
  def environment_hash
@@ -161,28 +159,30 @@ module SSHKit
161
159
  end
162
160
 
163
161
  def with(&_block)
164
- return yield unless environment_hash.any?
165
- "( export #{environment_string} ; #{yield} )"
162
+ env_string = environment_string
163
+ return yield if env_string.empty?
164
+ "( export #{env_string} ; #{yield} )"
166
165
  end
167
166
 
168
167
  def user(&_block)
169
168
  return yield unless options[:user]
170
- "sudo -u #{options[:user]} #{environment_string + " " unless environment_string.empty?}-- sh -c '#{yield}'"
169
+ env_string = environment_string
170
+ "sudo -u #{options[:user].to_s.shellescape} #{env_string + " " unless env_string.empty?}-- sh -c #{yield.shellescape}"
171
171
  end
172
172
 
173
173
  def in_background(&_block)
174
174
  return yield unless options[:run_in_background]
175
- sprintf("( nohup %s > /dev/null & )", yield)
175
+ "( nohup #{yield} > /dev/null & )"
176
176
  end
177
177
 
178
178
  def umask(&_block)
179
179
  return yield unless SSHKit.config.umask
180
- sprintf("umask #{SSHKit.config.umask} && %s", yield)
180
+ "umask #{SSHKit.config.umask} && #{yield}"
181
181
  end
182
182
 
183
183
  def group(&_block)
184
184
  return yield unless options[:group]
185
- %Q(sg #{options[:group]} -c "#{yield}")
185
+ "sg #{options[:group].to_s.shellescape} -c #{yield.shellescape}"
186
186
  # We could also use the so-called heredoc format perhaps:
187
187
  #"newgrp #{options[:group]} <<EOC \\\"%s\\\" EOC" % %Q{#{yield}}
188
188
  end
@@ -219,6 +219,11 @@ module SSHKit
219
219
  end
220
220
  end
221
221
 
222
+ # allow using home directory but escape everything else like spaces etc
223
+ def self.shellescape_except_tilde(file)
224
+ file.shellescape.gsub("\\~", "~")
225
+ end
226
+
222
227
  private
223
228
 
224
229
  def default_options
@@ -234,7 +239,7 @@ module SSHKit
234
239
 
235
240
  def call_interaction_handler(stream_name, data, channel)
236
241
  interaction_handler = options[:interaction_handler]
237
- interaction_handler = MappingInteractionHandler.new(interaction_handler) if interaction_handler.kind_of?(Hash)
242
+ interaction_handler = MappingInteractionHandler.new(interaction_handler) if interaction_handler.kind_of?(Hash) or interaction_handler.kind_of?(Proc)
238
243
  interaction_handler.on_data(self, stream_name, data, channel) if interaction_handler.respond_to?(:on_data)
239
244
  end
240
245
 
data/lib/sshkit/host.rb CHANGED
@@ -151,18 +151,20 @@ module SSHKit
151
151
  # @private
152
152
  # :nodoc:
153
153
  class IPv6HostWithPortParser < SimpleHostParser
154
+ IPV6_REGEX = /\[([a-fA-F0-9:]+)\](?:\:(\d+))?/
154
155
 
155
156
  def self.suitable?(host_string)
156
- host_string.match(/[a-fA-F0-9:]+:\d+/)
157
+ host_string.match(IPV6_REGEX)
157
158
  end
158
159
 
159
160
  def port
160
- @host_string.split(':').last.to_i
161
+ prt = @host_string.match(IPV6_REGEX)[2]
162
+ prt = prt.to_i unless prt.nil?
163
+ prt
161
164
  end
162
165
 
163
166
  def hostname
164
- @host_string.gsub!(/\[|\]/, '')
165
- @host_string.split(':')[0..-2].join(':')
167
+ @host_string.match(IPV6_REGEX)[1]
166
168
  end
167
169
 
168
170
  end
@@ -1,3 +1,3 @@
1
1
  module SSHKit
2
- VERSION = "1.18.1".freeze
2
+ VERSION = "1.21.0".freeze
3
3
  end
data/sshkit.gemspec CHANGED
@@ -9,6 +9,9 @@ Gem::Specification.new do |gem|
9
9
  gem.description = %q{A comprehensive toolkit for remotely running commands in a structured manner on groups of servers.}
10
10
  gem.homepage = "http://github.com/capistrano/sshkit"
11
11
  gem.license = "MIT"
12
+ gem.metadata = {
13
+ "changelog_uri" => "https://github.com/capistrano/sshkit/releases"
14
+ }
12
15
 
13
16
  gem.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
14
17
  gem.files = `git ls-files`.split("\n")
@@ -99,6 +99,24 @@ module SSHKit
99
99
  end.run
100
100
  assert_equal("Enter Data\nCaptured SOME DATA", captured_command_result)
101
101
  end
102
+
103
+ def test_interaction_handler_with_proc
104
+ captured_command_result = nil
105
+ Local.new do
106
+ command = 'echo Enter Data; read the_data; echo Captured $the_data;'
107
+ captured_command_result = capture(command, interaction_handler:
108
+ lambda { |data|
109
+ case data
110
+ when "Enter Data\n"
111
+ "SOME DATA\n"
112
+ when "Captured SOME DATA\n"
113
+ nil
114
+ end
115
+ }
116
+ )
117
+ end.run
118
+ assert_equal("Enter Data\nCaptured SOME DATA", captured_command_result)
119
+ end
102
120
  end
103
121
  end
104
122
  end
@@ -38,7 +38,7 @@ module SSHKit
38
38
  "Command: /usr/bin/env ls -l\n",
39
39
  "Command: if test ! -d /tmp; then echo \"Directory does not exist '/tmp'\" 1>&2; false; fi\n",
40
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\n",
41
- "Command: cd /tmp && ( export RAILS_ENV=\"production\" ; sudo -u root RAILS_ENV=\"production\" -- sh -c '/usr/bin/env touch restart.txt' )\n"
41
+ "Command: cd /tmp && ( export RAILS_ENV=\"production\" ; sudo -u root RAILS_ENV=\"production\" -- sh -c /usr/bin/env\\ touch\\ restart.txt )\n"
42
42
  ], command_lines
43
43
  end
44
44
 
@@ -82,7 +82,7 @@ module SSHKit
82
82
  command_lines = @output.lines.select { |line| line.start_with?('Command:') }
83
83
  assert_equal [
84
84
  "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\n",
85
- "Command: sudo -u root -- sh -c 'sg admin -c \"/usr/bin/env touch restart.txt\"'\n"
85
+ "Command: sudo -u root -- sh -c sg\\ admin\\ -c\\ /usr/bin/env\\\\\\ touch\\\\\\ restart.txt\n"
86
86
  ], command_lines
87
87
  end
88
88
 
@@ -96,21 +96,18 @@ module SSHKit
96
96
  end
97
97
 
98
98
  def test_ssh_option_merge
99
- verify_host_opt = if Net::SSH::Version::MAJOR >= 5
100
- { verify_host_key: :always }
101
- else
102
- { paranoid: true }
103
- end
104
- a_host.ssh_options = verify_host_opt
99
+ keepalive_opt = { keepalive: true }
100
+ test_host = a_host.dup
101
+ test_host.ssh_options = keepalive_opt
105
102
  host_ssh_options = {}
106
103
  SSHKit::Backend::Netssh.config.ssh_options = { forward_agent: false }
107
- Netssh.new(a_host) do |host|
104
+ Netssh.new(test_host) do |host|
108
105
  capture(:uname)
109
106
  host_ssh_options = host.ssh_options
110
107
  end.run
111
- assert_equal [:forward_agent, *verify_host_opt.keys, :known_hosts, :logger, :password_prompt].sort, host_ssh_options.keys.sort
108
+ assert_equal [:forward_agent, *keepalive_opt.keys, :known_hosts, :logger, :password_prompt].sort, host_ssh_options.keys.sort
112
109
  assert_equal false, host_ssh_options[:forward_agent]
113
- assert_equal verify_host_opt.values.first, host_ssh_options[verify_host_opt.keys.first]
110
+ assert_equal keepalive_opt.values.first, host_ssh_options[keepalive_opt.keys.first]
114
111
  assert_instance_of SSHKit::Backend::Netssh::KnownHosts, host_ssh_options[:known_hosts]
115
112
  end
116
113