dysinger-rush 0.4

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 (47) hide show
  1. data/Rakefile +65 -0
  2. data/bin/rush +13 -0
  3. data/bin/rushd +7 -0
  4. data/lib/rush.rb +27 -0
  5. data/lib/rush/access.rb +130 -0
  6. data/lib/rush/array_ext.rb +19 -0
  7. data/lib/rush/box.rb +112 -0
  8. data/lib/rush/commands.rb +55 -0
  9. data/lib/rush/config.rb +154 -0
  10. data/lib/rush/dir.rb +158 -0
  11. data/lib/rush/embeddable_shell.rb +26 -0
  12. data/lib/rush/entry.rb +178 -0
  13. data/lib/rush/exceptions.rb +31 -0
  14. data/lib/rush/file.rb +77 -0
  15. data/lib/rush/find_by.rb +39 -0
  16. data/lib/rush/fixnum_ext.rb +18 -0
  17. data/lib/rush/head_tail.rb +11 -0
  18. data/lib/rush/local.rb +374 -0
  19. data/lib/rush/process.rb +55 -0
  20. data/lib/rush/process_set.rb +62 -0
  21. data/lib/rush/remote.rb +152 -0
  22. data/lib/rush/search_results.rb +58 -0
  23. data/lib/rush/server.rb +117 -0
  24. data/lib/rush/shell.rb +148 -0
  25. data/lib/rush/ssh_tunnel.rb +122 -0
  26. data/lib/rush/string_ext.rb +3 -0
  27. data/spec/access_spec.rb +134 -0
  28. data/spec/array_ext_spec.rb +15 -0
  29. data/spec/base.rb +24 -0
  30. data/spec/box_spec.rb +64 -0
  31. data/spec/commands_spec.rb +47 -0
  32. data/spec/config_spec.rb +108 -0
  33. data/spec/dir_spec.rb +159 -0
  34. data/spec/embeddable_shell_spec.rb +17 -0
  35. data/spec/entry_spec.rb +129 -0
  36. data/spec/file_spec.rb +79 -0
  37. data/spec/find_by_spec.rb +58 -0
  38. data/spec/fixnum_ext_spec.rb +19 -0
  39. data/spec/local_spec.rb +313 -0
  40. data/spec/process_set_spec.rb +50 -0
  41. data/spec/process_spec.rb +73 -0
  42. data/spec/remote_spec.rb +135 -0
  43. data/spec/search_results_spec.rb +44 -0
  44. data/spec/shell_spec.rb +12 -0
  45. data/spec/ssh_tunnel_spec.rb +122 -0
  46. data/spec/string_ext_spec.rb +23 -0
  47. metadata +126 -0
@@ -0,0 +1,122 @@
1
+ # Internal class for managing an ssh tunnel, across which relatively insecure
2
+ # HTTP commands can be sent by Rush::Connection::Remote.
3
+ class Rush::SshTunnel
4
+ def initialize(real_host)
5
+ @real_host = real_host
6
+ end
7
+
8
+ def host
9
+ 'localhost'
10
+ end
11
+
12
+ def port
13
+ @port
14
+ end
15
+
16
+ def ensure_tunnel(options={})
17
+ return if @port and tunnel_alive?
18
+
19
+ @port = config.tunnels[@real_host]
20
+
21
+ if !@port or !tunnel_alive?
22
+ setup_everything(options)
23
+ end
24
+ end
25
+
26
+ def setup_everything(options={})
27
+ display "Connecting to #{@real_host}..."
28
+ push_credentials
29
+ launch_rushd
30
+ establish_tunnel(options)
31
+ end
32
+
33
+ def push_credentials
34
+ display "Pushing credentials"
35
+ config.ensure_credentials_exist
36
+ ssh_append_to_credentials(config.credentials_file.contents.strip)
37
+ end
38
+
39
+ def ssh_append_to_credentials(string)
40
+ # the following horror is exactly why rush is needed
41
+ passwords_file = "~/.rush/passwords"
42
+ string = "'#{string}'"
43
+ ssh "M=`grep #{string} #{passwords_file} 2>/dev/null | wc -l`; if [ $M = 0 ]; then mkdir -p .rush; chmod 700 .rush; echo #{string} >> #{passwords_file}; chmod 600 #{passwords_file}; fi"
44
+ end
45
+
46
+ def launch_rushd
47
+ display "Launching rushd"
48
+ ssh("if [ `ps aux | grep rushd | grep -v grep | wc -l` -ge 1 ]; then exit; fi; rushd > /dev/null 2>&1 &")
49
+ end
50
+
51
+ def establish_tunnel(options={})
52
+ display "Establishing ssh tunnel"
53
+ @port = next_available_port
54
+
55
+ make_ssh_tunnel(options)
56
+
57
+ tunnels = config.tunnels
58
+ tunnels[@real_host] = @port
59
+ config.save_tunnels tunnels
60
+
61
+ sleep 0.5
62
+ end
63
+
64
+ def tunnel_options
65
+ {
66
+ :local_port => @port,
67
+ :remote_port => Rush::Config::DefaultPort,
68
+ :ssh_host => @real_host,
69
+ }
70
+ end
71
+
72
+ def tunnel_alive?
73
+ `#{tunnel_count_command}`.to_i > 0
74
+ end
75
+
76
+ def tunnel_count_command
77
+ "ps x | grep '#{ssh_tunnel_command_without_stall}' | grep -v grep | wc -l"
78
+ end
79
+
80
+ class SshFailed < Exception; end
81
+ class NoPortSelectedYet < Exception; end
82
+
83
+ def ssh(command)
84
+ raise SshFailed unless system("ssh #{@real_host} '#{command}'")
85
+ end
86
+
87
+ def make_ssh_tunnel(options={})
88
+ raise SshFailed unless system(ssh_tunnel_command(options))
89
+ end
90
+
91
+ def ssh_tunnel_command_without_stall
92
+ options = tunnel_options
93
+ raise NoPortSelectedYet unless options[:local_port]
94
+ "ssh -f -L #{options[:local_port]}:127.0.0.1:#{options[:remote_port]} #{options[:ssh_host]}"
95
+ end
96
+
97
+ def ssh_stall_command(options={})
98
+ if options[:timeout] == :infinite
99
+ "while [ 1 ]; do sleep 1000; done"
100
+ elsif options[:timeout].to_i > 10
101
+ "sleep #{options[:timeout].to_i}"
102
+ else
103
+ "sleep 9000"
104
+ end
105
+ end
106
+
107
+ def ssh_tunnel_command(options={})
108
+ ssh_tunnel_command_without_stall + ' "' + ssh_stall_command(options) + '"'
109
+ end
110
+
111
+ def next_available_port
112
+ (config.tunnels.values.max || Rush::Config::DefaultPort) + 1
113
+ end
114
+
115
+ def config
116
+ @config ||= Rush::Config.new
117
+ end
118
+
119
+ def display(msg)
120
+ puts msg
121
+ end
122
+ end
@@ -0,0 +1,3 @@
1
+ class String
2
+ include Rush::HeadTail
3
+ end
@@ -0,0 +1,134 @@
1
+ require File.dirname(__FILE__) + '/base'
2
+
3
+ describe Rush::Access do
4
+ before do
5
+ @access = Rush::Access.new
6
+ end
7
+
8
+ it "has roles: user, group, other" do
9
+ @access.class.roles == %w(user group other)
10
+ end
11
+
12
+ it "has permissions: read, write, execute" do
13
+ @access.class.permissions == %w(read write execute)
14
+ end
15
+
16
+ it "gets parts from a one-part symbol like :user" do
17
+ @access.parts_from(:user).should == %w(user)
18
+ end
19
+
20
+ it "gets parts from a two-part symbol like :read_write" do
21
+ @access.parts_from(:read_write).should == %w(read write)
22
+ end
23
+
24
+ it "allows use of 'and' in multipart symbols, like :user_and_group" do
25
+ @access.parts_from(:user_and_group).should == %w(user group)
26
+ end
27
+
28
+ it "extract_list verifies that all the parts among the valid choices" do
29
+ @access.should_receive(:parts_from).with(:red_green).and_return(%w(red green))
30
+ @access.extract_list('type', :red_green, %w(red blue green)).should == %w(red green)
31
+ end
32
+
33
+ it "extract_list raises a BadAccessSpecifier when there is part not in the list of choices" do
34
+ lambda do
35
+ @access.extract_list('role', :user_bork, %w(user group))
36
+ end.should raise_error(Rush::BadAccessSpecifier, "Unrecognized role: bork")
37
+ end
38
+
39
+ it "sets one value in the matrix of permissions and roles" do
40
+ @access.set_matrix(%w(read), %w(user))
41
+ @access.user_can_read.should == true
42
+ end
43
+
44
+ it "sets two values in the matrix of permissions and roles" do
45
+ @access.set_matrix(%w(read), %w(user group))
46
+ @access.user_can_read.should == true
47
+ @access.group_can_read.should == true
48
+ end
49
+
50
+ it "sets four values in the matrix of permissions and roles" do
51
+ @access.set_matrix(%w(read write), %w(user group))
52
+ @access.user_can_read.should == true
53
+ @access.group_can_read.should == true
54
+ @access.user_can_write.should == true
55
+ @access.group_can_write.should == true
56
+ end
57
+
58
+ it "parse options hash" do
59
+ @access.parse(:user_can => :read)
60
+ @access.user_can_read.should == true
61
+ end
62
+
63
+ it "generates octal permissions from its member vars" do
64
+ @access.user_can_read = true
65
+ @access.octal_permissions.should == 0400
66
+ end
67
+
68
+ it "generates octal permissions from its member vars" do
69
+ @access.user_can_read = true
70
+ @access.user_can_write = true
71
+ @access.user_can_execute = true
72
+ @access.group_can_read = true
73
+ @access.group_can_execute = true
74
+ @access.octal_permissions.should == 0750
75
+ end
76
+
77
+ it "applies its settings to a file" do
78
+ file = "/tmp/rush_spec_#{Process.pid}"
79
+ begin
80
+ system "rm -rf #{file}; touch #{file}; chmod 770 #{file}"
81
+ @access.user_can_read = true
82
+ @access.apply(file)
83
+ `ls -l #{file}`.should match(/^-r--------/)
84
+ ensure
85
+ system "rm -rf #{file}; touch #{file}"
86
+ end
87
+ end
88
+
89
+ it "serializes itself to a hash" do
90
+ @access.user_can_read = true
91
+ @access.to_hash.should == {
92
+ :user_can_read => 1, :user_can_write => 0, :user_can_execute => 0,
93
+ :group_can_read => 0, :group_can_write => 0, :group_can_execute => 0,
94
+ :other_can_read => 0, :other_can_write => 0, :other_can_execute => 0,
95
+ }
96
+ end
97
+
98
+ it "unserializes from a hash" do
99
+ @access.from_hash(:user_can_read => '1')
100
+ @access.user_can_read.should == true
101
+ end
102
+
103
+ it "initializes from a serialized hash" do
104
+ @access.class.should_receive(:new).and_return(@access)
105
+ @access.class.from_hash(:user_can_read => '1').should == @access
106
+ @access.user_can_read.should == true
107
+ end
108
+
109
+ it "initializes from a parsed options hash" do
110
+ @access.class.should_receive(:new).and_return(@access)
111
+ @access.class.parse(:user_and_group_can => :read).should == @access
112
+ @access.user_can_read.should == true
113
+ end
114
+
115
+ it "converts and octal integer into an array of integers" do
116
+ @access.octal_integer_array(0740).should == [ 7, 4, 0 ]
117
+ end
118
+
119
+ it "filters out anything above the top three digits (File.stat returns some extra data there)" do
120
+ @access.octal_integer_array(0100644).should == [ 6, 4, 4 ]
121
+ end
122
+
123
+ it "taskes permissions from an octal representation" do
124
+ @access.from_octal(0644)
125
+ @access.user_can_read.should == true
126
+ @access.user_can_write.should == true
127
+ @access.user_can_execute.should == false
128
+ end
129
+
130
+ it "computes a display hash by dropping false keys and converting the 1s to trues" do
131
+ @access.should_receive(:to_hash).and_return(:red => 1, :green => 0, :blue => 1)
132
+ @access.display_hash.should == { :red => true, :blue => true }
133
+ end
134
+ end
@@ -0,0 +1,15 @@
1
+ require File.dirname(__FILE__) + '/base'
2
+
3
+ describe Array do
4
+ it "mixes commands into array" do
5
+ [ 1,2,3 ].entries.should == [ 1, 2, 3 ]
6
+ end
7
+
8
+ it "can call head" do
9
+ [ 1,2,3 ].head(1).should == [ 1 ]
10
+ end
11
+
12
+ it "can call tail" do
13
+ [ 1,2,3 ].tail(1).should == [ 3 ]
14
+ end
15
+ end
data/spec/base.rb ADDED
@@ -0,0 +1,24 @@
1
+ require 'rubygems'
2
+ require 'spec'
3
+
4
+ $LOAD_PATH.unshift(File.dirname(__FILE__) + '/../lib')
5
+ require 'rush'
6
+
7
+ def mock_config(&block)
8
+ mock_config_start
9
+ block.call(config)
10
+ mock_config_end
11
+ end
12
+
13
+ def mock_config_sandbox_dir
14
+ "/tmp/fake_config.#{Process.pid}"
15
+ end
16
+
17
+ def mock_config_start
18
+ mock_config_cleanup
19
+ Rush::Config.new(mock_config_sandbox_dir)
20
+ end
21
+
22
+ def mock_config_cleanup
23
+ FileUtils.rm_rf(mock_config_sandbox_dir)
24
+ end
data/spec/box_spec.rb ADDED
@@ -0,0 +1,64 @@
1
+ require File.dirname(__FILE__) + '/base'
2
+
3
+ describe Rush::Box do
4
+ before do
5
+ @sandbox_dir = "/tmp/rush_spec.#{Process.pid}"
6
+ system "rm -rf #{@sandbox_dir}; mkdir -p #{@sandbox_dir}"
7
+
8
+ @box = Rush::Box.new('localhost')
9
+ end
10
+
11
+ after do
12
+ system "rm -rf #{@sandbox_dir}"
13
+ end
14
+
15
+ it "looks up entries with [] syntax" do
16
+ @box['/'].should == Rush::Dir.new('/', @box)
17
+ end
18
+
19
+ it "looks up processes" do
20
+ @box.connection.should_receive(:processes).and_return([ { :pid => 123 } ])
21
+ @box.processes.should == [ Rush::Process.new({ :pid => 123 }, @box) ]
22
+ end
23
+
24
+ it "executes bash commands" do
25
+ @box.connection.should_receive(:bash).with('cmd', nil, false).and_return('output')
26
+ @box.bash('cmd').should == 'output'
27
+ end
28
+
29
+ it "executes bash commands with an optional user" do
30
+ @box.connection.should_receive(:bash).with('cmd', 'user', false)
31
+ @box.bash('cmd', :user => 'user')
32
+ end
33
+
34
+ it "executes bash commands in the background, returning a Rush::Process" do
35
+ @box.connection.should_receive(:bash).with('cmd', nil, true).and_return(123)
36
+ @box.stub!(:processes).and_return([ mock('ps', :pid => 123) ])
37
+ @box.bash('cmd', :background => true).pid.should == 123
38
+ end
39
+
40
+ it "builds a script of environment variables to prefix the bash command" do
41
+ @box.command_with_environment('cmd', { :a => 'b' }).should == "export a='b'\ncmd"
42
+ end
43
+
44
+ it "sets the environment variables from the provided hash" do
45
+ @box.connection.stub!(:bash)
46
+ @box.should_receive(:command_with_environment).with('cmd', { 1 => 2 })
47
+ @box.bash('cmd', :env => { 1 => 2 })
48
+ end
49
+
50
+ it "checks the connection to determine if it is alive" do
51
+ @box.connection.should_receive(:alive?).and_return(true)
52
+ @box.should be_alive
53
+ end
54
+
55
+ it "establish_connection to set up the connection manually" do
56
+ @box.connection.should_receive(:ensure_tunnel)
57
+ @box.establish_connection
58
+ end
59
+
60
+ it "establish_connection can take a hash of options" do
61
+ @box.connection.should_receive(:ensure_tunnel).with(:timeout => :infinite)
62
+ @box.establish_connection(:timeout => :infinite)
63
+ end
64
+ end
@@ -0,0 +1,47 @@
1
+ require File.dirname(__FILE__) + '/base'
2
+
3
+ describe Rush::Commands do
4
+ before do
5
+ @sandbox_dir = "/tmp/rush_spec.#{Process.pid}"
6
+ system "rm -rf #{@sandbox_dir}; mkdir -p #{@sandbox_dir}"
7
+
8
+ @filename = "test_file"
9
+ system "echo thing_to_find > #{@sandbox_dir}/#{@filename}"
10
+ system "echo dont_find_me > #{@sandbox_dir}/some_other_file"
11
+
12
+ @dir = Rush::Dir.new(@sandbox_dir)
13
+ @array = @dir.files
14
+ end
15
+
16
+ after do
17
+ system "rm -rf #{@sandbox_dir}"
18
+ end
19
+
20
+ it "searches a list of files" do
21
+ results = @dir.files.search(/thing_to_find/)
22
+ results.should be_kind_of(Rush::SearchResults)
23
+ results.entries.should == [ @dir[@filename] ]
24
+ results.lines.should == [ "thing_to_find" ]
25
+ end
26
+
27
+ it "searches a dir" do
28
+ @dir.search(/thing_to_find/).entries.should == [ @dir[@filename] ]
29
+ end
30
+
31
+ it "searchs a dir's nested files" do
32
+ @dir.create_dir('sub').create_file('file').write('nested')
33
+ @dir['**'].search(/nested/).entries.should == [ @dir['sub/file'] ]
34
+ end
35
+
36
+ it "search and replace contents on all files in the glob" do
37
+ @dir['1'].create.write('xax')
38
+ @dir['2'].create.write('-a-')
39
+ @dir.replace_contents!(/a/, 'b')
40
+ @dir['1'].contents.should == "xbx"
41
+ @dir['2'].contents.should == "-b-"
42
+ end
43
+
44
+ it "counts lines of the contained files" do
45
+ @dir.files.line_count.should == 2
46
+ end
47
+ end
@@ -0,0 +1,108 @@
1
+ require File.dirname(__FILE__) + '/base'
2
+
3
+ describe Rush::Config do
4
+ before do
5
+ @sandbox_dir = "/tmp/rush_config_spec.#{Process.pid}"
6
+ system "rm -rf #{@sandbox_dir}"
7
+ @config = Rush::Config.new(@sandbox_dir)
8
+ end
9
+
10
+ after do
11
+ system "rm -rf #{@sandbox_dir}"
12
+ end
13
+
14
+ it "creates the dir" do
15
+ File.directory?(@sandbox_dir).should be_true
16
+ end
17
+
18
+ it "can access the history file" do
19
+ @config.history_file.class.should == Rush::File
20
+ end
21
+
22
+ it "saves the shell command history" do
23
+ @config.save_history(%w(1 2 3))
24
+ @config.history_file.contents.should == "1\n2\n3\n"
25
+ end
26
+
27
+ it "loads the shell command history" do
28
+ @config.save_history(%w(1 2 3))
29
+ @config.load_history.should == %w(1 2 3)
30
+ end
31
+
32
+ it "loads a blank history if no history file" do
33
+ @config.load_history.should == []
34
+ end
35
+
36
+ it "loads the env file" do
37
+ @config.env_file.write('abc')
38
+ @config.load_env.should == 'abc'
39
+ end
40
+
41
+ it "loads nothing if env file does not exist" do
42
+ @config.load_env.should == ""
43
+ end
44
+
45
+ it "loads the commands file" do
46
+ @config.commands_file.write('abc')
47
+ @config.load_commands.should == 'abc'
48
+ end
49
+
50
+ it "loads nothing if commands file does not exist" do
51
+ @config.load_commands.should == ""
52
+ end
53
+
54
+ it "loads usernames and password for rushd" do
55
+ system "echo 1:2 > #{@sandbox_dir}/passwords"
56
+ system "echo a:b >> #{@sandbox_dir}/passwords"
57
+ @config.passwords.should == { '1' => '2', 'a' => 'b' }
58
+ end
59
+
60
+ it "loads blank hash if no passwords file" do
61
+ @config.passwords.should == { }
62
+ end
63
+
64
+ it "loads credentials for client connecting to server" do
65
+ system "echo user:pass > #{@sandbox_dir}/credentials"
66
+ @config.credentials_user.should == 'user'
67
+ @config.credentials_password.should == 'pass'
68
+ end
69
+
70
+ it "loads list of ssh tunnels" do
71
+ system "echo host.example.com:123 > #{@sandbox_dir}/tunnels"
72
+ @config.tunnels.should == { 'host.example.com' => 123 }
73
+ end
74
+
75
+ it "returns an empty hash if tunnels file is blank" do
76
+ @config.tunnels.should == { }
77
+ end
78
+
79
+ it "saves a list of ssh tunnels" do
80
+ @config.save_tunnels({ 'my.example.com' => 4000 })
81
+ @config.tunnels_file.contents.should == "my.example.com:4000\n"
82
+ end
83
+
84
+ it "ensure_credentials_exist doesn't do anything if credentials already exist" do
85
+ @config.credentials_file.write "dummy"
86
+ @config.should_receive(:generate_credentials).exactly(0).times
87
+ @config.ensure_credentials_exist
88
+ end
89
+
90
+ it "ensure_credentials_exist generates credentials file if they don't exist" do
91
+ @config.should_receive(:generate_credentials)
92
+ @config.ensure_credentials_exist
93
+ end
94
+
95
+ it "secret_characters returns valid characters for username or password" do
96
+ @config.secret_characters.should be_kind_of(Array)
97
+ end
98
+
99
+ it "generate_secret products a random string for use in username and password" do
100
+ @config.should_receive(:secret_characters).and_return(%w(a))
101
+ @config.generate_secret(2, 2).should == "aa"
102
+ end
103
+
104
+ it "generate_credentials saves credentials" do
105
+ @config.generate_credentials
106
+ @config.credentials_file.contents.should match(/^.+:.+$/)
107
+ end
108
+ end