nutshell 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,5 @@
1
+ === 1.0.0 / 2010-09-28
2
+
3
+ * 1 major enhancement
4
+
5
+ * First release!
@@ -0,0 +1,12 @@
1
+ History.txt
2
+ Manifest.txt
3
+ README.txt
4
+ Rakefile
5
+ lib/nutshell.rb
6
+ lib/nutshell/remote_shell.rb
7
+ lib/nutshell/shell.rb
8
+ test/mocks/mock_object.rb
9
+ test/mocks/mock_open4.rb
10
+ test/test_helper.rb
11
+ test/test_remote_shell.rb
12
+ test/test_shell.rb
@@ -0,0 +1,76 @@
1
+ = Nutshell
2
+
3
+ * http://github.com/yaksnrainbows/nutshell
4
+
5
+ == DESCRIPTION:
6
+
7
+ A light weight ssh client that wraps the ssh and rsync commands.
8
+
9
+ == SYNOPSIS:
10
+
11
+ remote = Nutshell::RemoteShell.new "user@example.com"
12
+ remote.connected?
13
+ #=> false
14
+
15
+ remote.connect
16
+ remote.connected?
17
+ #=> <#ssh pid>
18
+
19
+ remote.call "whoami"
20
+ #=> "user"
21
+
22
+ remote.call "whoami", :sudo => true
23
+ #=> "root"
24
+
25
+ remote.upload "myfile.txt", "/tmp/myfile.txt"
26
+ remote.disconnect
27
+
28
+ remote.session do |remote|
29
+ remote.connected?
30
+ #=> <#ssh pid>
31
+
32
+ remote.tty!
33
+ # starts an interactive shell session
34
+ end
35
+
36
+ remote.connected?
37
+ #=> false
38
+
39
+
40
+ == REQUIREMENTS:
41
+
42
+ * open4 gem
43
+
44
+ * highline gem
45
+
46
+ * Unix based OS
47
+
48
+ == INSTALL:
49
+
50
+ * gem install nutshell
51
+
52
+ == LICENSE:
53
+
54
+ (The MIT License)
55
+
56
+ Copyright (c) 2010 FIX
57
+
58
+ Permission is hereby granted, free of charge, to any person obtaining
59
+ a copy of this software and associated documentation files (the
60
+ 'Software'), to deal in the Software without restriction, including
61
+ without limitation the rights to use, copy, modify, merge, publish,
62
+ distribute, sublicense, and/or sell copies of the Software, and to
63
+ permit persons to whom the Software is furnished to do so, subject to
64
+ the following conditions:
65
+
66
+ The above copyright notice and this permission notice shall be
67
+ included in all copies or substantial portions of the Software.
68
+
69
+ THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
70
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
71
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
72
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
73
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
74
+ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
75
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
76
+
@@ -0,0 +1,46 @@
1
+ # -*- ruby -*-
2
+ require 'rubygems'
3
+ require 'hoe'
4
+ require 'rake'
5
+ require 'rake/testtask'
6
+
7
+ task :manifest do
8
+ manifest_file = "Manifest.txt"
9
+
10
+ gem_files = record_files do |f|
11
+ next if f =~ /^(tmp|pkg|deploy_scripts)/
12
+ puts(f)
13
+ true
14
+ end
15
+
16
+ gem_files.push(manifest_file)
17
+ gem_files = gem_files.uniq.sort.join("\n")
18
+
19
+ File.open(manifest_file, "w+") do |file|
20
+ file.write gem_files
21
+ end
22
+ end
23
+
24
+ def record_files(path="*", file_arr=[], &block)
25
+ Dir[path].each do |child_path|
26
+ if File.file?(child_path)
27
+ next if block_given? && !yield(child_path)
28
+ file_arr << child_path
29
+ end
30
+ record_files(child_path+"/*", file_arr, &block) if
31
+ File.directory?(child_path)
32
+ end
33
+ return file_arr
34
+ end
35
+
36
+ Hoe.plugin :isolate
37
+
38
+ Hoe.spec 'nutshell' do |p|
39
+ developer('Jeremie Castagna', 'yaksnrainbows@gmail.com')
40
+ self.extra_deps << ['open4', '>= 1.0.1']
41
+ self.extra_deps << ['highline', '>= 1.5.1']
42
+ end
43
+
44
+ # vim: syntax=Ruby
45
+
46
+
@@ -0,0 +1,42 @@
1
+ require 'rubygems'
2
+ require 'open4'
3
+ require 'highline'
4
+
5
+ require 'fileutils'
6
+ require 'tmpdir'
7
+
8
+
9
+ module Nutshell
10
+
11
+ VERSION = "1.0.0"
12
+
13
+ ##
14
+ # Temp directory used by various nutshell classes
15
+ # for uploads, checkouts, etc...
16
+ TMP_DIR = File.join Dir.tmpdir, "nutshell_#{$$}"
17
+ FileUtils.mkdir_p TMP_DIR
18
+
19
+
20
+ class TimeoutError < Exception; end
21
+ class CmdError < Exception; end
22
+ class ConnectionError < Exception; end
23
+
24
+
25
+ def self.config
26
+ @config ||= {}
27
+ end
28
+
29
+
30
+ def self.timeout
31
+ config[:timeout]
32
+ end
33
+
34
+
35
+ def self.interactive?
36
+ config[:interactive]
37
+ end
38
+
39
+
40
+ require 'nutshell/shell'
41
+ require 'nutshell/remote_shell'
42
+ end
@@ -0,0 +1,268 @@
1
+ module Nutshell
2
+
3
+ ##
4
+ # Keeps an SSH connection open to a server the app will be deployed to.
5
+ # Deploy servers use the ssh command and support any ssh feature.
6
+ # By default, deploy servers use the ControlMaster feature to share
7
+ # socket connections, with the ControlPath = ~/.ssh/nutshell-%r%h:%p
8
+ #
9
+ # Setting session-persistant environment variables is supported by
10
+ # accessing the @env attribute.
11
+
12
+ class RemoteShell < Shell
13
+
14
+
15
+ ##
16
+ # The loop to keep the ssh connection open.
17
+ LOGIN_LOOP = "echo ok; echo ready; "+
18
+ "for (( ; ; )); do kill -0 $PPID && sleep 10 || exit; done;"
19
+
20
+ LOGIN_TIMEOUT = 30
21
+
22
+
23
+ ##
24
+ # Closes all remote shell connections.
25
+
26
+ def self.disconnect_all
27
+ return unless defined?(@remote_shells)
28
+ @remote_shells.each{|rs| rs.disconnect}
29
+ end
30
+
31
+
32
+ ##
33
+ # Registers a remote shell for global access from the class.
34
+ # Handled automatically on initialization.
35
+
36
+ def self.register remote_shell
37
+ (@remote_shells ||= []) << remote_shell
38
+ end
39
+
40
+
41
+ attr_reader :host, :user, :pid
42
+ attr_accessor :ssh_flags, :rsync_flags
43
+
44
+
45
+ ##
46
+ # Remote shells essentially need a host and optional user.
47
+ # Typical instantiation is done through either of these methods:
48
+ # RemoteShell.new "user@host"
49
+ # RemoteShell.new "host", :user => "user"
50
+ #
51
+ # The constructor also supports the following options:
52
+ # :env:: hash - hash of environment variables to set for the ssh session
53
+ # :password:: string - password for ssh login; if missing the deploy server
54
+ # will attempt to prompt the user for a password.
55
+
56
+ def initialize host, options={}
57
+ super $stdout, options
58
+
59
+ @host, @user = host.split("@").reverse
60
+
61
+ @user ||= options[:user]
62
+
63
+ @rsync_flags = ["-azP"]
64
+ @rsync_flags.concat [*options[:rsync_flags]] if options[:rsync_flags]
65
+
66
+ @ssh_flags = [
67
+ "-o ControlMaster=auto",
68
+ "-o ControlPath=~/.ssh/nutshell-%r@%h:%p"
69
+ ]
70
+ @ssh_flags.concat ["-l", @user] if @user
71
+ @ssh_flags.concat [*options[:ssh_flags]] if options[:ssh_flags]
72
+
73
+ @pid, @inn, @out, @err = nil
74
+
75
+ self.class.register self
76
+ end
77
+
78
+
79
+ ##
80
+ # Runs a command via SSH. Optional block is passed the
81
+ # stream(stderr, stdout) and string data.
82
+
83
+ def call command_str, options={}, &block
84
+ execute build_remote_cmd(command_str, options), &block
85
+ end
86
+
87
+
88
+ ##
89
+ # Connect to host via SSH and return process pid
90
+
91
+ def connect
92
+ return true if connected?
93
+
94
+ cmd = ssh_cmd quote_cmd(LOGIN_LOOP), :sudo => false
95
+
96
+ @pid, @inn, @out, @err = popen4 cmd.join(" ")
97
+ @inn.sync = true
98
+
99
+ data = ""
100
+ ready = nil
101
+ start_time = Time.now.to_i
102
+
103
+ until ready || @out.eof?
104
+ data << @out.readpartial(1024)
105
+ ready = data =~ /ready/
106
+
107
+ raise TimeoutError if timed_out?(start_time, LOGIN_TIMEOUT)
108
+ end
109
+
110
+ unless connected?
111
+ disconnect
112
+ host_info = [@user, @host].compact.join("@")
113
+ raise ConnectionError, "Can't connect to #{host_info}"
114
+ end
115
+
116
+ @inn.close
117
+ @pid
118
+ end
119
+
120
+
121
+ ##
122
+ # Check if SSH session is open and returns process pid
123
+
124
+ def connected?
125
+ Process.kill(0, @pid) && @pid rescue false
126
+ end
127
+
128
+
129
+ ##
130
+ # Disconnect from host
131
+
132
+ def disconnect
133
+ @inn.close rescue nil
134
+ @out.close rescue nil
135
+ @err.close rescue nil
136
+
137
+ kill_process @pid, "HUP" rescue nil
138
+
139
+ @pid = nil
140
+ end
141
+
142
+
143
+ ##
144
+ # Download a file via rsync
145
+
146
+ def download from_path, to_path, options={}, &block
147
+ from_path = "#{@host}:#{from_path}"
148
+ execute rsync_cmd(from_path, to_path, options), &block
149
+ end
150
+
151
+
152
+ ##
153
+ # Expand a path:
154
+ # shell.expand_path "~user/thing"
155
+ # #=> "/home/user/thing"
156
+
157
+ def expand_path path
158
+ dir = File.dirname path
159
+ full_dir = call "cd #{dir} && pwd"
160
+ File.join full_dir, File.basename(path)
161
+ end
162
+
163
+
164
+ ##
165
+ # Checks if the given file exists
166
+
167
+ def file? filepath
168
+ syscall "test -f #{filepath}"
169
+ end
170
+
171
+
172
+ ##
173
+ # Start an interactive shell with preset permissions and env.
174
+ # Optionally pass a command to be run first.
175
+
176
+ def tty! cmd=nil
177
+ sync do
178
+ cmd = [cmd, "sh -il"].compact.join " && "
179
+ cmd = quote_cmd cmd
180
+
181
+ pid = fork do
182
+ exec \
183
+ ssh_cmd(sudo_cmd(env_cmd(cmd)), :flags => "-t").to_a.join(" ")
184
+ end
185
+ Process.waitpid pid
186
+ end
187
+ end
188
+
189
+
190
+ ##
191
+ # Create a file remotely
192
+
193
+ def make_file filepath, content, options={}
194
+
195
+ temp_filepath =
196
+ "#{TMP_DIR}/#{File.basename(filepath)}_#{Time.now.to_i}#{rand(10000)}"
197
+
198
+ File.open(temp_filepath, "w+"){|f| f.write(content)}
199
+
200
+ self.upload temp_filepath, filepath, options
201
+
202
+ File.delete(temp_filepath)
203
+ end
204
+
205
+
206
+ ##
207
+ # Builds an ssh command with permissions, env, etc.
208
+
209
+ def build_remote_cmd cmd, options={}
210
+ cmd = sh_cmd cmd
211
+ cmd = env_cmd cmd
212
+ cmd = sudo_cmd cmd, options
213
+ cmd = ssh_cmd cmd, options
214
+ end
215
+
216
+
217
+ ##
218
+ # Uploads a file via rsync
219
+
220
+ def upload from_path, to_path, options={}, &block
221
+ to_path = "#{@host}:#{to_path}"
222
+ execute rsync_cmd(from_path, to_path, options), &block
223
+ end
224
+
225
+
226
+ ##
227
+ # Figure out which rsync flags to use.
228
+
229
+ def build_rsync_flags options
230
+ flags = @rsync_flags.dup
231
+
232
+ remote_rsync = 'rsync'
233
+ rsync_sudo = sudo_cmd remote_rsync, options
234
+
235
+ unless rsync_sudo == remote_rsync
236
+ flags << "--rsync-path='#{ rsync_sudo.join(" ") }'"
237
+ end
238
+
239
+ flags << "-e \"ssh #{@ssh_flags.join(' ')}\"" if @ssh_flags
240
+
241
+ flags.concat [*options[:flags]] if options[:flags]
242
+
243
+ flags
244
+ end
245
+
246
+
247
+ ##
248
+ # Creates an rsync command.
249
+
250
+ def rsync_cmd from_path, to_path, options={}
251
+ cmd = ["rsync", build_rsync_flags(options), from_path, to_path]
252
+ cmd.flatten.compact.join(" ")
253
+ end
254
+
255
+
256
+ ##
257
+ # Wraps the command in an ssh call.
258
+
259
+ def ssh_cmd cmd, options=nil
260
+ options ||= {}
261
+
262
+ flags = [*options[:flags]].concat @ssh_flags
263
+
264
+ ["ssh", flags, @host, cmd].flatten.compact
265
+ end
266
+ end
267
+ end
268
+