rsynconrails 0.1.0

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.
@@ -0,0 +1,351 @@
1
+ require 'system_config'
2
+ require 'cli_funcs'
3
+
4
+ class Rsync < CliFuncs
5
+ attr_accessor :flags_all, :cmd_run, :source, :destination
6
+ attr_reader :uptodate, :deleted, :modified, :created, :excluded, :ignored, :duplicates, :base_dir, :data_dir, :output, :transfer_stats
7
+
8
+ def initialize
9
+ super
10
+ @utility = CliUtils.new("rsync").utility_path
11
+ @uptodate = Array.new
12
+ @deleted = Array.new
13
+ @modified = Array.new
14
+ @created = Array.new
15
+ @excluded = Hash.new
16
+ @ignored = Array.new
17
+ @duplicates = Array.new
18
+ @transfer_stats = Hash.new
19
+ @source = Array.new
20
+ @destination = String
21
+ @output_filter_junk = Regexp
22
+ @output_filter_excluded = Regexp
23
+ @output_filter_warn_err = Regexp
24
+ @output_filter_stats = Regexp
25
+ @output_filter_duplicates = Regexp
26
+ @output_filter_created = Regexp
27
+ set_flags_base
28
+ set_output_filters
29
+ end
30
+
31
+ def rsync
32
+ run_and_capture(cmd_run)
33
+ end
34
+
35
+ def set_output_filters
36
+ set_output_filter_junk
37
+ set_output_filter_excluded
38
+ set_output_filter_warn_err
39
+ set_output_filter_stats
40
+ set_output_filter_duplicates
41
+ set_output_filter_created
42
+ end
43
+
44
+ def output_process
45
+ @output.each do |line|
46
+ if line.match @output_filter_junk
47
+ next
48
+ elsif line.match @output_filter_duplicates
49
+ puts "#{line.chomp} DUPLICATE!" if DEBUG
50
+ @duplicates.push(line)
51
+ next
52
+ elsif line.match @output_filter_excluded
53
+ puts "#{line.chomp} EXCLUDED!" if DEBUG
54
+ @excluded[$3] = $4
55
+ next
56
+ elsif line.match @output_filter_warn_err
57
+ # Capture warnings / errors here
58
+ next
59
+ elsif line.match @output_filter_stats
60
+ # Set hash of stats
61
+ @transfer_stats[$1] = $2
62
+ @transfer_stats[$3] = $4
63
+ @transfer_stats[$5] = $6
64
+ @transfer_stats[$7] = $8
65
+ @transfer_stats[$9] = $10
66
+ @transfer_stats[$11] = $12
67
+ @transfer_stats[$13] = $14
68
+ @transfer_stats[$15] = $16
69
+ @transfer_stats[$17] = $18
70
+ @transfer_stats[$19] = $20
71
+ @transfer_stats[$21] = $22
72
+ next
73
+ elsif line.match @output_filter_created
74
+ #@created.push(line)
75
+ next
76
+ else
77
+ # catch all, this is the main content
78
+ process_itemized(line)
79
+ end
80
+ end
81
+ puts @transfer_stats.inspect if DEBUG
82
+ end
83
+
84
+ def process_itemized(line)
85
+ # Break apart the line by spaces (e.g. ".f 9/file9")
86
+ attrs,item = line.split(/\s+/, 2)
87
+ # Break apart itemized attrs on each character 0 = . 1 = f 2 = nil, 3 = nil ...
88
+ attrs_p = attrs.split("")
89
+ # Begin check's for file/directory disposition according to rsync
90
+ # If element 0 contains a '.', it means no update has occurred, but may have attribute changes
91
+ if(attrs_p[0] == ".")
92
+ # This first check ignores directories which have had their timestamp changed
93
+ if(
94
+ attrs_p[1] == "d" and
95
+ attrs_p[2] == "." and
96
+ attrs_p[3] == "." and
97
+ attrs_p[4] == "t" and
98
+ attrs_p[5] == "." and
99
+ attrs_p[6] == "." and
100
+ attrs_p[7] == "." and
101
+ attrs_p[8] == "." and
102
+ attrs_p[9] == "." and
103
+ attrs_p[10] == "."
104
+ )
105
+ puts "#{line.chomp} IGNORED!" if DEBUG
106
+ @ignored.push(line)
107
+ #return
108
+ # checks if nothing has changed with this item
109
+ elsif(
110
+ attrs_p[1] =~ /f|d|L|D|S/ and
111
+ attrs_p[2] == nil and
112
+ attrs_p[3] == nil and
113
+ attrs_p[4] == nil and
114
+ attrs_p[5] == nil and
115
+ attrs_p[6] == nil and
116
+ attrs_p[7] == nil and
117
+ attrs_p[8] == nil and
118
+ attrs_p[9] == nil and
119
+ attrs_p[10] == nil
120
+ )
121
+ puts "#{line.chomp} UPTODATE!" if DEBUG
122
+ @uptodate.push(line)
123
+ # something must have changed, like an attribute (e.g. ownership or mode)
124
+ else
125
+ puts "#{line.chomp} MODIFIED OWNERSHIP OR MODE!" if DEBUG
126
+ @modified.push(line)
127
+ end
128
+ elsif(attrs_p[0] =~ /\*|<|>|c|h/)
129
+ # checks if item is being deleted
130
+ if(
131
+ attrs_p[1] == "d" and
132
+ attrs_p[2] == "e" and
133
+ attrs_p[3] == "l" and
134
+ attrs_p[4] == "e" and
135
+ attrs_p[5] == "t" and
136
+ attrs_p[6] == "i" and
137
+ attrs_p[7] == "n" and
138
+ attrs_p[8] == "g" and
139
+ attrs_p[9] == nil and
140
+ attrs_p[10] == nil
141
+ )
142
+ puts "#{line.chomp} DELETED!" if DEBUG
143
+ @deleted.push(line)
144
+
145
+ # checks if item is being created (i.e. new file/dir)
146
+ elsif(
147
+ attrs_p[1] =~ /f|d/ and
148
+ attrs_p[2] == "+" and
149
+ attrs_p[3] == "+" and
150
+ attrs_p[4] == "+" and
151
+ attrs_p[5] == "+" and
152
+ attrs_p[6] == "+" and
153
+ attrs_p[7] == "+" and
154
+ attrs_p[8] == "+" and
155
+ attrs_p[9] == "+" and
156
+ attrs_p[10] == "+"
157
+ )
158
+ puts "#{line.chomp} CREATED!" if DEBUG
159
+ @created.push(line)
160
+
161
+ # everthing else is considered a modification
162
+ else
163
+ puts "#{line.chomp} MODIFIED CATCH ALL 1!" if DEBUG
164
+ @modified.push(line)
165
+ end
166
+ else
167
+ puts "#{line.chomp} MODIFIED CATCH ALL 2!" if DEBUG
168
+ @modified.push(line)
169
+ end
170
+ end
171
+
172
+ def set_output_filter_warn_err
173
+ filter = Array.new
174
+
175
+ # These are warnings and errors kicked out by rsync during it's run
176
+ filter.push("WARNING: .* failed verification -- update discarded \(will try again\)\.")
177
+ filter.push("IO error encountered -- skipping file deletion")
178
+ filter.push("file has vanished: .*")
179
+ filter.push("rsync (error|warning): .*")
180
+ filter.push("cannot delete non-empty directory: .*")
181
+
182
+ @output_filter_warn_err = /#{filter.join("|")}/
183
+ end
184
+
185
+ def set_output_filter_created
186
+ filter = Array.new
187
+
188
+ # These should be rare, for some reason it doesn't show up in the itemized list
189
+ # and rsync doesn't count it as a 'created file', it seems like the only time
190
+ # this happens is if the destination root directory doesn't already exist.
191
+ filter.push("^created directory .*")
192
+
193
+ @output_filter_created = /#{filter[0]}/
194
+ end
195
+
196
+ def set_output_filter_stats
197
+ filter = Array.new
198
+
199
+ # These are for rsync stats which are output after the transfer is complete
200
+ filter.push("(Number of files): (\\d+)")
201
+ filter.push("(Number of files transferred): (\\d+)")
202
+ filter.push("(Total file size): (\\d+) bytes")
203
+ filter.push("(Total transferred file size): (\\d+) bytes")
204
+ filter.push("(Literal data): (\\d+) bytes")
205
+ filter.push("(Matched data): (\\d+) bytes")
206
+ filter.push("(File list size): (\\d+)")
207
+ filter.push("(File list generation time): (.*) seconds")
208
+ filter.push("(File list transfer time): (.*) seconds")
209
+ filter.push("(Total bytes sent): (\\d+)")
210
+ filter.push("(Total bytes received): (\\d+)")
211
+
212
+ @output_filter_stats = /#{filter.join("|")}/
213
+ end
214
+
215
+ def set_output_filter_excluded
216
+ # These are files/directories which have been excluded by a pattern we passed to rsync
217
+ @output_filter_excluded = /^\[generator\] (excluding|protecting) (file|directory) (.*) because of pattern (.*)$/
218
+ end
219
+
220
+ def set_output_filter_duplicates
221
+ filter = Array.new
222
+
223
+ # These are file/directories which are the same in the sources list
224
+ filter.push("^removing duplicate name .* from file list .*")
225
+
226
+ @output_filter_duplicates = /#{filter[0]}/
227
+ end
228
+
229
+ def set_output_filter_junk
230
+ filter = Array.new
231
+
232
+ # blank line
233
+ filter.push("^$")
234
+ # ignore line
235
+ filter.push("^sending incremental file list")
236
+ # ignore line
237
+ filter.push("^building file list ...")
238
+ # ignore line
239
+ filter.push("^expand file_list\s\w+")
240
+ # ignore line
241
+ filter.push("^rsync: expand\s\w+")
242
+ # ignore line
243
+ filter.push("^opening connection\s\w+")
244
+ # ignore line - should probably capture this info
245
+ filter.push("^total")
246
+ # ignore line - should probably capture this info
247
+ filter.push("^wrote")
248
+ # ignore line - should probably capture this info
249
+ filter.push("^sent")
250
+ # ignore line
251
+ filter.push("^done")
252
+ # ignore line
253
+ filter.push("^excluding")
254
+ # ignore line
255
+ filter.push("^hiding")
256
+ # ignore line
257
+ filter.push("^delta( |-)transmission (dis|en)abled")
258
+ # ignore line
259
+ filter.push("^deleting in \./")
260
+ # ignore line
261
+ filter.push("^opening connection using: ssh")
262
+ # ignore line
263
+ filter.push("^rsync\\[\\d+\\] \\(sender\\) heap statistics:")
264
+ # ignore line
265
+ filter.push("^rsync\\[\\d+\\] \\(server receiver\\) heap statistics:")
266
+ # ignore line
267
+ filter.push("^rsync\\[\\d+\\] \\(server generator\\) heap statistics:")
268
+ # ignore line
269
+ filter.push("^ arena:")
270
+ # ignore line
271
+ filter.push("^ ordblks:")
272
+ # ignore line
273
+ filter.push("^ smblks:")
274
+ # ignore line
275
+ filter.push("^ hblks:")
276
+ # ignore line
277
+ filter.push("^ hblkhd:")
278
+ # ignore line
279
+ filter.push("^ allmem:")
280
+ # ignore line
281
+ filter.push("^ usmblks:")
282
+ # ignore line
283
+ filter.push("^ fsmblks:")
284
+ # ignore line
285
+ filter.push("^ uordblks:")
286
+ # ignore line
287
+ filter.push("^ fordblks:")
288
+ # ignore line
289
+ filter.push("^ keepcost:")
290
+
291
+ @output_filter_junk = /#{filter.join("|")}/
292
+ end
293
+
294
+ def cmd_run
295
+ u = CliUtils.new(@utility)
296
+ puts [u.utility_path, flags_run, @source, @destination].flatten.inspect if DEBUG
297
+ [u.utility_path, flags_run, @source, @destination].flatten
298
+ end
299
+
300
+ def flag_delete
301
+ flag_add("--delete")
302
+ end
303
+
304
+ def flag_compress
305
+ flag_add("-z")
306
+ end
307
+
308
+ def flag_dryrun
309
+ flag_add("-n")
310
+ end
311
+
312
+ def flag_verbose
313
+ flag_add("-vv")
314
+ end
315
+
316
+ def flag_archive
317
+ flag_add("-a")
318
+ end
319
+
320
+ def flag_itemized
321
+ flag_add("-i")
322
+ end
323
+
324
+ def flag_checksum
325
+ flag_add("-c")
326
+ end
327
+
328
+ def flag_stats
329
+ flag_add("--stats")
330
+ end
331
+
332
+ def flag_bwlimit(kbps)
333
+ flag_add("--bwlimit=#{kbps}")
334
+ end
335
+
336
+ def flag_rsync_path(path)
337
+ flag_add("--rsync-path=#{path}")
338
+ end
339
+
340
+ def flag_exclude(pattern)
341
+ flag_add("--exclude=#{pattern[1..-1]}")
342
+ end
343
+
344
+ def set_flags_base
345
+ flag_archive
346
+ flag_verbose
347
+ flag_itemized
348
+ flag_delete
349
+ flag_stats
350
+ end
351
+ end
data/lib/cli_utils.rb ADDED
@@ -0,0 +1,70 @@
1
+ require 'open3'
2
+
3
+ class CliUtils
4
+ attr_accessor :utility, :utility_path
5
+
6
+ def initialize(utility)
7
+ @utility = utility
8
+ @utility_path
9
+ @version
10
+ valid?
11
+ end
12
+
13
+ def valid?
14
+ find_utility
15
+ get_version
16
+ supp_util_versions
17
+ end
18
+
19
+ def find_utility
20
+ cmd = "which #{@utility}"
21
+ ph = IO.popen(cmd)
22
+ output = ph.readlines()
23
+ ph.close
24
+ if($? != 0)
25
+ puts "Cannot find required utility #{@utility} in path."
26
+ false
27
+ else
28
+ @utility_path = output[0].chomp
29
+ end
30
+ end
31
+
32
+ def get_version
33
+ version = Array.new
34
+ if(@utility_path == nil)
35
+ find_utility
36
+ end
37
+ cmd = "#{@utility_path} --version"
38
+ Open3.popen2e(cmd) { |i,oe,t|
39
+ oe.each do |line|
40
+ version.push(line)
41
+ end
42
+ }
43
+
44
+ @version = version.to_s
45
+ end
46
+
47
+ def supp_util_versions
48
+ case @utility
49
+ when "rsync"
50
+ supp_version = 3
51
+ version_pattern.match(@version)
52
+ if($1.to_s < supp_version.to_s)
53
+ puts "Unsupported version of 'rsync', use version #{supp_version} or higher."
54
+ false
55
+ else
56
+ true
57
+ end
58
+ end
59
+ end
60
+
61
+ def version_pattern
62
+ case @utility
63
+ when "rsync"
64
+ Regexp.new(/rsync version ([\d\.]+) protocol version (\d+)/)
65
+ else
66
+ puts "Not a supported utility: #{utility}"
67
+ exit 1
68
+ end
69
+ end
70
+ end
data/lib/host.rb ADDED
@@ -0,0 +1,194 @@
1
+ require 'system_config'
2
+ require 'find'
3
+
4
+ class Host
5
+ attr_accessor :host, :domain, :host_yml, :autocreate
6
+ attr_reader :hosts_dir, :host_dir, :found_host, :host_yml_values
7
+
8
+ def initialize(hostname="")
9
+ @autocreate = false
10
+ @hostname = hostname
11
+ @host = ""
12
+ @domain = ""
13
+ @host_dir = ""
14
+ @host_yml = ""
15
+ @found_host = ""
16
+ @host_yml_values = Hash.new
17
+
18
+ @hosts_dir = SYSTEM_CONFIG['hosts_dir']
19
+ prepare_hosts_dir
20
+ set_hostname_domain
21
+ end
22
+
23
+ def set_hostname_domain
24
+ if @hostname.include? "."
25
+ @host = @hostname.split(".").shift
26
+ @domain = @hostname.split(".")[1..-1].join(".")
27
+ else
28
+ @host = @hostname
29
+ @domain = ""
30
+ end
31
+ end
32
+
33
+ def find_host
34
+ begin
35
+ Find.find(@hosts_dir) do |path|
36
+ # These are the 3 things which define a 'host' in the system, 2 if there's no domain
37
+ if path =~ /#{@domain}/ and path =~ /#{@host}$/ and File.directory? "#{path}/overrides"
38
+ @host_dir = path
39
+ @host_yml = "#{@host_dir}/host.yml"
40
+ # Since this is a path with more than just the domain and hostname in it,
41
+ # we need to test if this is a hostname without a domain in front. If so
42
+ # we need to set @found_host differently. We test to see if the full path
43
+ # minus the hostname is equal to the '@hosts_dir' because someone might
44
+ # name create a host without it having a domain component.
45
+ if "/#{path.split('/')[1..-2].join('/')}" == @hosts_dir
46
+ # If we get here, it means this path did not have a domain component
47
+ @found_host = "#{path.split('/')[-1]}"
48
+ else
49
+ # This path did have a domain component
50
+ @found_host = "#{path.split('/')[-2]}/#{path.split('/')[-1]}"
51
+ end
52
+ # This should set @host_yml_values
53
+ unless host_valid?
54
+ return false
55
+ end
56
+ else
57
+ prepare_host
58
+ end
59
+ end
60
+ rescue Exception => e
61
+ puts "Tried to find host: #{@hostname} in #{@hosts_dir} during Host.find_host, received exception #{e}"
62
+ exit 1
63
+ end
64
+ end
65
+
66
+ def prepare_host
67
+ if @autocreate
68
+ @host_dir = "#{@hosts_dir}/#{@domain}/#{@host}"
69
+ @host_yml = "#{@host_dir}/host.yml"
70
+ # This should set @host_yml_values
71
+ unless host_valid?
72
+ return false
73
+ end
74
+ end
75
+ end
76
+
77
+ def host_valid?
78
+ if host_dir?
79
+ if host_yml?
80
+ return true
81
+ else
82
+ puts "Error: @host_yml: #{@host_yml} isn't valid!"
83
+ return false
84
+ end
85
+ else
86
+ puts "Error: @host_dir: #{@host_dir} does not exist!"
87
+ return false
88
+ end
89
+ end
90
+
91
+ def host_yml?
92
+ unless File.exists? @host_yml then
93
+ prepare_host_yml
94
+ yml_valid?
95
+ else
96
+ yml_valid?
97
+ end
98
+ end
99
+
100
+ def prepare_host_yml
101
+ begin
102
+ f = open(@host_yml, 'w+')
103
+ f.puts "config:"
104
+ f.puts " package_base: test_dist"
105
+ f.puts " release_tag: current"
106
+ f.puts " rsync_path:"
107
+ f.puts " ssh_port:"
108
+ f.puts " session_mode:"
109
+ f.puts "include:"
110
+ f.puts " # - section/packagename"
111
+ f.puts "exclude:"
112
+ f.puts " # - /somefile"
113
+ f.puts "exclude_backup:"
114
+ f.puts " # - /somefile"
115
+ f.puts "execute:"
116
+ f.puts " # - some arbitrary command"
117
+ f.close
118
+ rescue Exception => e
119
+ puts "Tried to open #{@host_yml} during 'Host.prepare_host_yml', received exception: #{e}"
120
+ false
121
+ end
122
+ end
123
+
124
+ def yml_valid?
125
+ begin
126
+ @host_yml_values = YAML.load_file(@host_yml)
127
+
128
+ valid_keys = ['config','include','exclude','exclude_backup','execute']
129
+ config_valid_subkeys = ['package_base','release_tag','rsync_path','ssh_port','session_mode']
130
+
131
+ # Begin check for basic elements
132
+ @host_yml_values.each_pair do |key,val|
133
+ if key == 'config' and val.class == Hash
134
+ val.each_pair do |skey,sval|
135
+ unless config_valid_subkeys.include? skey
136
+ puts "Invalid sub-option for 'config:' => #{skey} detected in #{@host_yml}. Exiting..."
137
+ return false
138
+ end
139
+ end
140
+ else
141
+ unless valid_keys.include? key
142
+ puts "Invalid option => #{key} detected in #{@host_yml}. Exiting..."
143
+ return false
144
+ end
145
+ end
146
+ end
147
+ rescue Exception => e
148
+ puts "Tried to load #{@host_yml} during 'Host.yml_valid?', received exception: #{e}"
149
+ end
150
+ end
151
+
152
+ def host_dir?
153
+ if File.directory? @host_dir
154
+ true
155
+ else
156
+ prepare_host_dir
157
+ end
158
+ end
159
+
160
+ def hosts_dir?
161
+ if File.directory? @hosts_dir
162
+ true
163
+ else
164
+ prepare_hosts_dir
165
+ end
166
+ end
167
+
168
+ def prompt_host_dir
169
+ # This isn't used right now
170
+ print "Configuration directory doesn't exist for #{@host}, create? (N/y): "
171
+ input = gets.chomp
172
+ if input =~ /[yY]/ then true else exit end
173
+ end
174
+
175
+ def prepare_host_dir
176
+ begin
177
+ FileUtils.mkdir_p "#{@host_dir}/overrides"
178
+ true
179
+ rescue Exception => e
180
+ puts "Tried to create #{@host_dir} during 'Host.prepare_host_dir', received exception: #{e}"
181
+ false
182
+ end
183
+ end
184
+
185
+ def prepare_hosts_dir
186
+ begin
187
+ FileUtils.mkdir_p @hosts_dir
188
+ true
189
+ rescue Exception => e
190
+ puts "Tried to create #{@hosts_dir} during 'Host.prepare_hosts_dir', received exception: #{e}"
191
+ false
192
+ end
193
+ end
194
+ end
data/lib/package.rb ADDED
@@ -0,0 +1,66 @@
1
+ require 'system_config'
2
+ require 'find'
3
+
4
+ class Package
5
+ attr_accessor :package, :package_yml, :package_base, :package_release_tag, :package_dir
6
+ attr_reader :packages_dir
7
+
8
+ def initialize(package="")
9
+ @package = package
10
+ @package_base = ""
11
+ @package_release_tag = ""
12
+ @package_yml = ""
13
+ @package_dir = ""
14
+
15
+ @packages_dir = SYSTEM_CONFIG['packages_dir']
16
+ end
17
+
18
+ def set_properties(obj)
19
+ @package_base = obj.host_yml_values['config']['package_base']
20
+ @package_release_tag = obj.host_yml_values['config']['release_tag']
21
+ @package_dir = "#{@packages_dir}/#{@package_base}/#{package}/#{@package_release_tag}"
22
+ @package_yml = "#{@package_dir}/package.yml"
23
+ end
24
+
25
+ def package_valid?
26
+ if package_dir?
27
+ if package_yml?
28
+ return true
29
+ else
30
+ puts "Error: @package_yml: #{@package_yml} isn't valid!"
31
+ return false
32
+ end
33
+ else
34
+ puts "Error: @package_dir: #{@package_dir} does not exist!"
35
+ return false
36
+ end
37
+ end
38
+
39
+ def package_yml?
40
+ File.exists? @package_yml
41
+ yml_valid?
42
+ end
43
+
44
+ def yml_valid?
45
+ begin
46
+ package_yml = YAML.load_file(@package_yml)
47
+
48
+ valid_keys = ['rank','include','exclude','exclude_backup','execute']
49
+
50
+ # Begin check for basic elements
51
+ package_yml.each_pair do |key,val|
52
+ unless valid_keys.include? key
53
+ puts "Invalid option => #{key} detected in #{@package_yml}. Exiting..."
54
+ return false
55
+ end
56
+ end
57
+ rescue Exception => e
58
+ puts "Tried to load #{@package_yml} during 'Package.yml_valid?', received exception: #{e}"
59
+ end
60
+ end
61
+
62
+ def package_dir?
63
+ # Package isn't valid unless it has a 'files' directory under it
64
+ File.directory? "#{@package_dir}/files"
65
+ end
66
+ end
data/lib/ssh_funcs.rb ADDED
@@ -0,0 +1,29 @@
1
+ require 'system_config'
2
+ require 'net/ssh'
3
+
4
+ class SSHFuncs
5
+ attr_reader :output
6
+ attr_accessor :hostname, :remote_cmd
7
+
8
+ def initialize(hostname)
9
+ @hostname = hostname
10
+ @output
11
+ @run_cmd
12
+ @ssh
13
+ ssh_channel
14
+ end
15
+
16
+ def ssh_channel
17
+ begin
18
+ @ssh = Net::SSH.start(@hostname, 'root')
19
+ rescue Exception => e
20
+ puts "Couldn't establish SSH session with #{@hostname}, received exception: #{e}"
21
+ end
22
+ end
23
+
24
+ def run_cmd(cmd)
25
+ # capture all stderr and stdout output from a remote process
26
+ @output = @ssh.exec!(cmd)
27
+ puts "SSH CMD OUTPUT: #{@output}" if DEBUG
28
+ end
29
+ end