sshkit-backends-netssh_global 0.0.1

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.
data/README.md ADDED
@@ -0,0 +1,59 @@
1
+ # SSHKit Backends Netssh Global
2
+
3
+ **SSHKit Backends Netssh Global** is a backend to be used in conjunction with
4
+ Capistrano 3 and SSHKit to allow global configuration to be set. For example,
5
+ all commands can be run under a different user or folder - without modifying the
6
+ command.
7
+
8
+ This is designed to make it possible for Capistrano 3 to deploy on systems where
9
+ users login as one identity and then need to sudo to a different identity for
10
+ each command.
11
+
12
+ This works globally so that default tasks will automatically `sudo` and `cd`
13
+ without modification. This allows the default tasks to be used in this kind of
14
+ setup without them being altered.
15
+
16
+ If a task specifically `sudo`'s or `cd`'s then the global setting will not take
17
+ effect.
18
+
19
+ In some setups the ssh agent also needs to be forwarded (such as git clone).
20
+ Here the setting `ssh_commands` can be set to automatically forward the ssh
21
+ agent to the sudo user for certain commands.
22
+
23
+ ### To run tests
24
+
25
+ To setup an OSX machine to run the tests, install Homebrew then:
26
+
27
+ ```
28
+ brew tap Homebrew/bundle
29
+ brew bundle
30
+ vagrant up --provision
31
+ bundle
32
+ rake
33
+ ```
34
+
35
+ ### Usage
36
+
37
+ ```ruby
38
+ require 'sshkit/backends/netssh_global'
39
+
40
+ SSHKit::Backend::NetsshGlobal.configure do |config|
41
+ config.owner = 'bob' # Which user to sudo as for every command
42
+ config.directory = '/home/bob' # Can be specified if it is important to default commands to run in a
43
+ # certain directory. This can be used to overcome permission problems when
44
+ # sudo'ing
45
+ config.ssh_commands = [:git] # Setting for which commands require SSH forwarding
46
+ config.shell = 'bash -l' # Setting that allows the shell that sudo runs to be overriden
47
+ end
48
+
49
+ # Per host configuration
50
+ Host.new("example.com").tap do |h|
51
+ h.properties.owner = 'fred'
52
+ h.properties.directory = '/home/fred'
53
+ h.properties.ssh_commands = [:git, :bundle]
54
+ end
55
+ ```
56
+
57
+ ### Credits
58
+
59
+ The code and test suite are built on top of [SSHKit](http://github.com/capistrano/sshkit).
data/Rakefile ADDED
@@ -0,0 +1,27 @@
1
+ #!/usr/bin/env rake
2
+
3
+ require 'bundler/gem_tasks'
4
+ require 'rake/testtask'
5
+
6
+ task :default => :test
7
+
8
+ desc "Run all tests"
9
+ task :test => ['test:units', 'test:functional']
10
+
11
+ namespace :test do
12
+
13
+ Rake::TestTask.new(:units) do |t|
14
+ t.libs << "test"
15
+ t.test_files = FileList['test/unit/**/test*.rb']
16
+ end
17
+
18
+ Rake::TestTask.new(:functional) do |t|
19
+ t.libs << "test"
20
+ t.test_files = FileList['test/functional/**/test*.rb']
21
+ end
22
+
23
+ end
24
+
25
+ Rake::Task["test:functional"].enhance do
26
+ warn "Remember there are still some VMs running, kill them with `vagrant halt` if you are finished using them."
27
+ end
data/Vagrantfile ADDED
@@ -0,0 +1,20 @@
1
+ VAGRANTFILE_API_VERSION = "2"
2
+
3
+ Vagrant.configure(VAGRANTFILE_API_VERSION) do |config|
4
+ config.vm.box = 'hashicorp/precise64'
5
+
6
+ json_config_path = File.join("test", "boxes.json")
7
+ list = File.open(json_config_path).read
8
+ list = JSON.parse(list)
9
+
10
+ list.each do |vm|
11
+ config.vm.define vm["name"] do |web|
12
+ web.vm.network "forwarded_port", guest: 22, host: vm["port"]
13
+ web.vm.provision "shell", inline: "apt-get update && apt-get install --yes acl csh"
14
+
15
+ vm["users"].each do |user|
16
+ web.vm.provision "shell", inline: "useradd --create-home #{user}"
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,87 @@
1
+ require 'sshkit/command_sudo_ssh_forward'
2
+
3
+ module SSHKit
4
+ module Backend
5
+ class NetsshGlobal < Netssh
6
+ class Configuration < Netssh::Configuration
7
+ attr_accessor :owner, :directory, :shell
8
+ attr_writer :ssh_commands
9
+
10
+ def ssh_commands
11
+ @ssh_commands || [:ssh, :git, :'ssh-add', :bundle]
12
+ end
13
+ end
14
+
15
+ class << self
16
+ def config
17
+ @config ||= Configuration.new
18
+ end
19
+ end
20
+
21
+ @pool = SSHKit::Backend::ConnectionPool.new
22
+
23
+ def upload!(local, remote, options = {})
24
+ execute :setfacl, "-m u:#{ssh_user}:rwx #{File.dirname(remote)}; true"
25
+ execute :setfacl, "-m u:#{ssh_user}:rw #{remote}; true"
26
+ super
27
+ as :root do
28
+ # Required as uploaded file is owned by SSH user, not owner
29
+ execute :chown, property(:owner), remote
30
+ end
31
+ end
32
+
33
+ private
34
+
35
+ def user
36
+ @user || property(:owner)
37
+ end
38
+
39
+ def ssh_user
40
+ host.user || configure_host.ssh_options.fetch(:user)
41
+ end
42
+
43
+ def pwd
44
+ @pwd.nil? ? property(:directory) : File.join(@pwd)
45
+ end
46
+
47
+ def property(name)
48
+ host.properties.public_send(name) || self.class.config.public_send(name)
49
+ end
50
+
51
+ def with_ssh
52
+ configure_host
53
+ conn = self.class.pool.checkout(
54
+ String(host.hostname),
55
+ host.username,
56
+ host.netssh_options,
57
+ &Net::SSH.method(:start)
58
+ )
59
+ begin
60
+ yield conn.connection
61
+ ensure
62
+ self.class.pool.checkin conn
63
+ end
64
+ end
65
+
66
+ def configure_host
67
+ host.tap do |h|
68
+ h.ssh_options = self.class.config.ssh_options.merge(host.ssh_options || {})
69
+ end
70
+ end
71
+
72
+ def command(*args)
73
+ options = args.extract_options!
74
+ options.merge!(
75
+ in: pwd,
76
+ env: @env,
77
+ host: configure_host,
78
+ user: user,
79
+ group: @group,
80
+ ssh_commands: property(:ssh_commands),
81
+ shell: property(:shell)
82
+ )
83
+ SSHKit::CommandSudoSshForward.new(*[*args, options])
84
+ end
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,7 @@
1
+ module SSHKit
2
+ module Backends
3
+ class NetsshGlobal
4
+ VERSION = '0.0.1'
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,74 @@
1
+ module SSHKit
2
+ class CommandSudoSshForward < SSHKit::Command
3
+ def to_command
4
+ return command.to_s unless should_map?
5
+
6
+ within do
7
+ ssh_agent do
8
+ umask do
9
+ with do
10
+ user do
11
+ in_background do
12
+ group do
13
+ to_s
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
22
+
23
+ def environment_hash
24
+ default_env.merge(options_env)
25
+ end
26
+
27
+ def ssh_agent(&block)
28
+ return yield unless ssh_forwarding_required?
29
+ "setfacl -m #{options[:user]}:x $(dirname $SSH_AUTH_SOCK) && setfacl -m #{options[:user]}:rw $SSH_AUTH_SOCK && %s" % yield
30
+ end
31
+
32
+ def user(&block)
33
+ return yield unless options[:user]
34
+ shell = options[:shell] || 'sh'
35
+ "sudo -u #{options[:user]} #{environment_string + " " unless environment_string.empty?}-- #{shell} -c '%s'" % %Q{#{yield}}
36
+ end
37
+
38
+ def with(&block)
39
+ return yield if environment_hash.empty? || sudo_command?
40
+ "( #{environment_string} %s )" % yield
41
+ end
42
+
43
+ private
44
+
45
+ def options_env
46
+ (options[:env] || {}).merge(default_ssh_options)
47
+ end
48
+
49
+ def default_env
50
+ SSHKit.config.default_env || {}
51
+ end
52
+
53
+ def default_ssh_options
54
+ ssh_forwarding_required? ? {'SSH_AUTH_SOCK' => '$SSH_AUTH_SOCK'} : {}
55
+ end
56
+
57
+ def ssh_forwarding_required?
58
+ ssh_command? && sudo_command? && ssh_forwarding_enabled?
59
+ end
60
+
61
+ def ssh_command?
62
+ options.fetch(:ssh_commands, []).include?(command)
63
+ end
64
+
65
+ def ssh_forwarding_enabled?
66
+ options[:host] && options[:host].ssh_options[:forward_agent]
67
+ end
68
+
69
+ def sudo_command?
70
+ options[:user]
71
+ end
72
+
73
+ end
74
+ end
@@ -0,0 +1,28 @@
1
+ # -*- encoding: utf-8 -*-
2
+ require File.expand_path('../lib/sshkit/backends/version', __FILE__)
3
+
4
+ Gem::Specification.new do |gem|
5
+
6
+ gem.authors = ["Theo Cushion", "Dennis Ideler"]
7
+ gem.email = ["tcushion@pivotal.io", "dennis.ideler@fundingcircle.com"]
8
+ gem.summary = %q{SSHKit backend for globally sudoing commands}
9
+ gem.description = %q{A backend to be used in conjunction with Capistrano 3
10
+ and SSHKit to allow deployment on setups where users login as one identity and
11
+ then need to sudo to a different identity for each command.}
12
+ gem.homepage = "http://github.com/fundingcircle/sshkit-backends-netssh_global"
13
+ # gem.license = "GPL3"
14
+
15
+ gem.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
16
+ gem.files = `git ls-files`.split("\n")
17
+ gem.test_files = `git ls-files -- test/*`.split("\n")
18
+ gem.name = "sshkit-backends-netssh_global"
19
+ gem.require_paths = ["lib"]
20
+ gem.version = SSHKit::Backends::NetsshGlobal::VERSION
21
+
22
+ gem.add_runtime_dependency('sshkit', '1.7.1')
23
+
24
+ gem.add_development_dependency('minitest', ['>= 2.11.3', '< 2.12.0'])
25
+ gem.add_development_dependency('rake')
26
+ gem.add_development_dependency('turn')
27
+ gem.add_development_dependency('mocha')
28
+ end
data/test/boxes.json ADDED
@@ -0,0 +1,13 @@
1
+ [
2
+ {
3
+ "name": "one",
4
+ "port": 3001,
5
+ "user": "vagrant",
6
+ "password": "vagrant",
7
+ "hostname": "localhost",
8
+ "users": [
9
+ "owner",
10
+ "owner2"
11
+ ]
12
+ }
13
+ ]
@@ -0,0 +1,300 @@
1
+ require 'helper'
2
+ require 'securerandom'
3
+
4
+ require 'sshkit/backends/netssh_global'
5
+
6
+ module SSHKit
7
+ module Backend
8
+ class TestNetsshGlobalFunctional < FunctionalTest
9
+ def setup
10
+ super
11
+ NetsshGlobal.configure do |config|
12
+ config.owner = a_user
13
+ config.directory = nil
14
+ config.shell = nil
15
+ end
16
+ VagrantWrapper.reset!
17
+ end
18
+
19
+ def a_user
20
+ a_box.fetch('users').fetch(0)
21
+ end
22
+
23
+ def another_user
24
+ a_box.fetch('users').fetch(1)
25
+ end
26
+
27
+ def a_box
28
+ VagrantWrapper.boxes_list.first
29
+ end
30
+
31
+ def a_host
32
+ VagrantWrapper.hosts['one']
33
+ end
34
+
35
+ def test_capture
36
+ File.open('/dev/null', 'w') do |dnull|
37
+ SSHKit.capture_output(dnull) do
38
+ captured_command_result = nil
39
+ NetsshGlobal.new(a_host) do
40
+ captured_command_result = capture(:uname)
41
+ end.run
42
+
43
+ assert captured_command_result
44
+ assert_match captured_command_result, /Linux|Darwin/
45
+ end
46
+ end
47
+ end
48
+
49
+ def test_ssh_option_merge
50
+ a_host.ssh_options = { paranoid: true }
51
+ host_ssh_options = {}
52
+ SSHKit::Backend::NetsshGlobal.config.ssh_options = { forward_agent: false }
53
+ NetsshGlobal.new(a_host) do |host|
54
+ capture(:uname)
55
+ host_ssh_options = host.ssh_options
56
+ end.run
57
+ assert_equal({ forward_agent: false, paranoid: true }, host_ssh_options)
58
+ end
59
+
60
+ def test_configure_owner_via_global_config
61
+ NetsshGlobal.configure do |config|
62
+ config.owner = a_user
63
+ end
64
+
65
+ output = ''
66
+ NetsshGlobal.new(a_host) do
67
+ output = capture :whoami
68
+ end.run
69
+ assert_equal a_user, output
70
+ end
71
+
72
+ def test_configure_owner_via_host
73
+ a_host.properties.owner = another_user
74
+
75
+ output = ''
76
+ NetsshGlobal.new(a_host) do
77
+ output = capture :whoami
78
+ end.run
79
+ assert_equal another_user, output
80
+ end
81
+
82
+ def test_configure_shell_via_global_config
83
+ NetsshGlobal.configure do |config|
84
+ config.shell = "csh"
85
+ end
86
+
87
+ running_shell = ''
88
+ NetsshGlobal.new(a_host) do
89
+ running_shell = capture :echo, '$shell'
90
+ end.run
91
+
92
+ assert_equal '/bin/csh', running_shell
93
+ end
94
+
95
+ def test_configure_directory_to_nil_has_no_effect
96
+ NetsshGlobal.configure do |config|
97
+ config.directory = nil
98
+ end
99
+
100
+ output = ''
101
+ NetsshGlobal.new(a_host) do
102
+ output = capture :pwd
103
+ end.run
104
+ assert_equal "/home/#{a_host.user}", output
105
+ end
106
+
107
+ def test_configure_directory_via_global_config
108
+ NetsshGlobal.configure do |config|
109
+ config.directory = '/tmp'
110
+ end
111
+
112
+ output = ''
113
+ NetsshGlobal.new(a_host) do
114
+ output = capture :pwd
115
+ end.run
116
+ assert_equal '/tmp', output
117
+ end
118
+
119
+ def test_configure_directory_via_host
120
+ NetsshGlobal.configure do |config|
121
+ config.directory = '/usr'
122
+ end
123
+
124
+ a_host.properties.directory = '/tmp'
125
+ output = ''
126
+ NetsshGlobal.new(a_host) do
127
+ output = capture :pwd
128
+ end.run
129
+ assert_equal '/tmp', output
130
+ end
131
+
132
+ def test_execute_raises_on_non_zero_exit_status_and_captures_stdout_and_stderr
133
+ err = assert_raises SSHKit::Command::Failed do
134
+ NetsshGlobal.new(a_host) do
135
+ execute :echo, "\"Test capturing stderr\" 1>&2; false"
136
+ end.run
137
+ end
138
+ assert_equal "echo exit status: 1\necho stdout: Nothing written\necho stderr: Test capturing stderr\n", err.message
139
+ end
140
+
141
+ def test_test_does_not_raise_on_non_zero_exit_status
142
+ NetsshGlobal.new(a_host) do
143
+ test :false
144
+ end.run
145
+ end
146
+
147
+ def test_test_executes_as_owner_when_command_contains_no_spaces
148
+ result = NetsshGlobal.new(a_host) do
149
+ test 'test', '"$USER" = "owner"'
150
+ end.run
151
+
152
+ assert(result, 'Expected test to execute as "owner", but it did not')
153
+ end
154
+
155
+ def test_test_executes_as_ssh_user_when_command_contains_spaces
156
+ result = NetsshGlobal.new(a_host) do
157
+ test 'test "$USER" = "vagrant"'
158
+ end.run
159
+
160
+ assert(result, 'Expected test to execute as "vagrant", but it did not')
161
+ end
162
+
163
+ def test_upload_file
164
+ file_contents = ""
165
+ file_owner = nil
166
+ file_name = File.join("/tmp", SecureRandom.uuid)
167
+ File.open file_name, 'w+' do |f|
168
+ f.write 'example_file'
169
+ end
170
+
171
+ NetsshGlobal.new(a_host) do
172
+ upload!(file_name, file_name)
173
+ file_contents = capture(:cat, file_name)
174
+ file_owner = capture(:stat, '-c', '%U', file_name)
175
+ end.run
176
+
177
+ assert_equal 'example_file', file_contents
178
+ assert_equal a_user, file_owner
179
+ end
180
+
181
+ def test_upload_file_to_folder_owned_by_user
182
+ dir = File.join('/tmp', SecureRandom.uuid)
183
+ NetsshGlobal.new(a_host) do
184
+ execute(:mkdir, dir)
185
+ end.run
186
+
187
+ file_name = SecureRandom.uuid
188
+ local_file = File.join('/tmp', file_name)
189
+ File.open local_file, 'w+' do |f|
190
+ f.write 'example_file'
191
+ end
192
+
193
+ file_contents = ""
194
+ file_owner = nil
195
+ remote_file = File.join(dir, file_name)
196
+ NetsshGlobal.new(a_host) do
197
+ upload!(local_file, remote_file)
198
+ file_contents = capture(:cat, remote_file)
199
+ file_owner = capture(:stat, '-c', '%U', remote_file)
200
+ end.run
201
+
202
+ assert_equal 'example_file', file_contents
203
+ assert_equal a_user, file_owner
204
+ end
205
+
206
+ def test_upload_file_overtop_of_existing_file
207
+ file_name = File.join('/tmp', SecureRandom.uuid)
208
+ File.open file_name, 'w+' do |f|
209
+ f.write 'example_file'
210
+ end
211
+
212
+ NetsshGlobal.new(a_host) do
213
+ upload!(file_name, file_name)
214
+ end.run
215
+
216
+ file_contents = ""
217
+ file_owner = nil
218
+ NetsshGlobal.new(a_host) do
219
+ upload!(file_name, file_name)
220
+ file_contents = capture(:cat, file_name)
221
+ file_owner = capture(:stat, '-c', '%U', file_name)
222
+ end.run
223
+
224
+ assert_equal 'example_file', file_contents
225
+ assert_equal a_user, file_owner
226
+ end
227
+
228
+ def test_upload_string_io
229
+ file_contents = ""
230
+ file_owner = nil
231
+ NetsshGlobal.new(a_host) do
232
+ file_name = File.join("/tmp", SecureRandom.uuid)
233
+ upload!(StringIO.new('example_io'), file_name)
234
+ file_contents = download!(file_name)
235
+ file_owner = capture(:stat, '-c', '%U', file_name)
236
+ end.run
237
+ assert_equal "example_io", file_contents
238
+ assert_equal a_user, file_owner
239
+ end
240
+
241
+ def test_upload_large_file
242
+ size = 25
243
+ fills = SecureRandom.random_bytes(1024*1024)
244
+ file_name = "/tmp/file-#{SecureRandom.uuid}-#{size}.txt"
245
+ File.open(file_name, 'w') do |f|
246
+ (size).times {f.write(fills) }
247
+ end
248
+
249
+ file_contents = ""
250
+ NetsshGlobal.new(a_host) do
251
+ upload!(file_name, file_name)
252
+ file_contents = download!(file_name)
253
+ end.run
254
+
255
+ assert_equal File.open(file_name).read, file_contents
256
+ end
257
+
258
+ def test_ssh_forwarded_when_command_is_ssh_command
259
+ remote_ssh_output = ''
260
+ local_ssh_output = `ssh-add -l 2>&1`.strip
261
+ a_host.ssh_options = { forward_agent: true }
262
+ NetsshGlobal.new(a_host) do |host|
263
+ remote_ssh_output = capture 'ssh-add', '-l', '2>&1;', 'true'
264
+ end.run
265
+
266
+ assert_equal local_ssh_output, remote_ssh_output
267
+ end
268
+
269
+ def test_ssh_not_forwarded_when_command_is_not_an_ssh_command
270
+ echo_output = ''
271
+
272
+ a_host.ssh_options = { forward_agent: true }
273
+ a_host.properties.ssh_commands = [:not_echo]
274
+ NetsshGlobal.new(a_host) do |host|
275
+ echo_output = capture :echo, '$SSH_AUTH_SOCK'
276
+ end.run
277
+
278
+ assert_match '', echo_output
279
+ end
280
+
281
+ def test_can_configure_ssh_commands
282
+ echo_output = ''
283
+
284
+ a_host.ssh_options = { forward_agent: true }
285
+ a_host.properties.ssh_commands = [:echo]
286
+ NetsshGlobal.new(a_host) do |host|
287
+ echo_output = capture :echo, '$SSH_AUTH_SOCK'
288
+ end.run
289
+
290
+ assert_match /\/tmp\//, echo_output
291
+ end
292
+
293
+ def test_default_ssh_commands
294
+ ssh_commands = NetsshGlobal.config.ssh_commands
295
+
296
+ assert_equal [:ssh, :git, :'ssh-add', :bundle], ssh_commands
297
+ end
298
+ end
299
+ end
300
+ end