sshkit-backends-netssh_global 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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