nixadm 1.0.6

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,477 @@
1
+ require 'open3'
2
+ require 'tempfile'
3
+
4
+ module NixAdm
5
+
6
+ #--------------------------------------------------------------------------------
7
+ # Support Code
8
+ #--------------------------------------------------------------------------------
9
+
10
+ class Status
11
+
12
+ attr_reader :status
13
+
14
+ class Result
15
+ attr_accessor :val, :msg, :data
16
+
17
+ def initialize()
18
+ clear()
19
+ end
20
+
21
+ def clear()
22
+ @val = 0
23
+ @msg = nil
24
+ @data = {}
25
+ end
26
+
27
+ def toJson()
28
+ return {
29
+ 'rc' => @val,
30
+ 'msg' => @msg,
31
+ 'data' => @data
32
+ }
33
+ end
34
+
35
+ def ==(that)
36
+ if that.class == TrueClass
37
+ return @val == 0
38
+ end
39
+
40
+ if that.class == FalseClass
41
+ return @val != 0
42
+ end
43
+
44
+ if that.class == Result
45
+ return @val == that.val
46
+ end
47
+
48
+ return false
49
+ end
50
+
51
+ end
52
+
53
+ def initialize()
54
+ @status = Result.new()
55
+ end
56
+
57
+ def success()
58
+ @status.clear()
59
+
60
+ return @status
61
+ end
62
+
63
+ def failure(val=nil, msg=nil, data={})
64
+ val = -1 if val.nil?
65
+ msg = 'failed' if msg.nil?
66
+
67
+ @status.val = val
68
+ @status.msg = msg
69
+ @status.data = data
70
+
71
+ return @status
72
+ end
73
+
74
+ def status(val=nil, msg=nil, data={})
75
+ return @status if val.nil?
76
+
77
+ @status.val = val
78
+ @status.msg = msg
79
+ @status.data = data
80
+
81
+ return @status
82
+ end
83
+
84
+ end # class Status
85
+
86
+ #--------------------------------------------------------------------------------
87
+ # Pipeline
88
+ #--------------------------------------------------------------------------------
89
+
90
+ # This class represents a UNIX pipeline. It provides two different
91
+ # implementations: a classic version which works exacly like the shell
92
+ # (Pipeline#classic()) and a conservative version that ensures all commands in
93
+ # the pipeline must succeed in order to continue (Pipeline#strict()). Ultimately
94
+ # both implementations work using the Process.spawn() method. They differ in how
95
+ # data flows between processes and how the pipeline behaves in the event of
96
+ # error.
97
+ #
98
+ # Examples:
99
+ #
100
+ # sys = NixAdm::Pipeline.new()
101
+ # sys.run('ssh jan zfs list -t snapshot')
102
+ # puts sys.out
103
+ # puts sys.stats
104
+ #
105
+ # # Strict mode:
106
+ # sys.mode = :strict
107
+ # sys.run('ssh jan zfs list -t snapshot')
108
+ # puts sys.out
109
+ #
110
+ # # Tempfile
111
+ # file = Tempfile.new('test')
112
+ # file.write('tempfile')
113
+ # sys.run([ './one.rb', './two.rb' ], file)
114
+ # puts pipeline.out
115
+ #
116
+ # # OS File
117
+ # file = File.open('/tmp/data.txt')
118
+ # pipeline.run([ './one.rb', './two.rb' ], file)
119
+ # puts pipeline.out
120
+
121
+ class Pipeline
122
+
123
+ attr_accessor :opts, :mode, :debug, :throw_on_fail
124
+ attr_reader :stats, :out, :error, :testing
125
+
126
+ def initialize()
127
+ @opts = {}
128
+ @stats = []
129
+ @mode = :classic
130
+ @debug = false
131
+ @error = nil
132
+ @throw_on_fail = true
133
+ end
134
+
135
+ # Run a UNIX pipeline. This selects between the two pipeline implementations:
136
+ # sequential or parallel depending on the @mode value. If @mode is :classic,
137
+ # it runs the classic() implementation, otherwise it runs the strict()
138
+ # implementation. Ultimately both implementations work using the
139
+ # Process.spawn() method. They differ in how data flows between processes and
140
+ # how the pipeline behaves in the event of error.
141
+
142
+ def run(commands, input=nil, &block)
143
+ if @mode == :classic
144
+ return classic(commands, input, &block)
145
+ else
146
+ return strict(commands, input, &block)
147
+ end
148
+ end
149
+
150
+ # Runs one or more commands sequentially in a class UNIX pipeline. This is a
151
+ # true UNIX pipeline. It starts each command in parallel and data flows
152
+ # through each using UNIX pipes. If a command fails, the pipeline will
153
+ # continue until the last command exists.
154
+ #
155
+ # @param commands A string containing a single command or an array of multiple
156
+ # commands.
157
+ #
158
+ # @param input Standard in to the first process in pipeline. This can be the
159
+ # a string or IO object.
160
+ #
161
+ # @return Returns true if all commands in pipeline completed successfully,
162
+ # false otherwise. Standard out of the last command it stored in the @out
163
+ # member while standard error is stored in @error. Process status info for
164
+ # each command is stored in @stats.
165
+
166
+ def classic(args, input=nil)
167
+
168
+ if args.is_a? String
169
+ args = [ args ]
170
+ end
171
+
172
+ if @debug
173
+ $stderr.puts args
174
+ end
175
+
176
+ stderr, ewrite = IO.pipe
177
+ args << { :err=>ewrite }
178
+
179
+ Open3.pipeline_rw(*args) do |stdin, stdout, threads|
180
+ if block_given?
181
+ yield stdin, stdout
182
+ else
183
+ if not input.nil?
184
+ if input.is_a?(Tempfile) or input.is_a?(IO)
185
+ input.seek(0)
186
+ stdin.write(input.read())
187
+ end
188
+
189
+ if input.is_a?(String)
190
+ stdin.write(input)
191
+ end
192
+ end
193
+
194
+ stdin.close()
195
+
196
+ # Collect stdout
197
+ @out = ''
198
+ while true
199
+ line = stdout.gets
200
+ break if line.nil?
201
+ @out += line
202
+ end
203
+
204
+ end
205
+
206
+ threads.each do |t|
207
+ @stats << t.value
208
+ end
209
+ end
210
+
211
+ ewrite.close()
212
+
213
+ # Collect stderr
214
+ @error = ''
215
+ while true
216
+ line = stderr.gets
217
+ break if line.nil?
218
+ @error += line
219
+ end
220
+
221
+ result = @stats.all? { |s| s.exitstatus == 0 }
222
+
223
+ if result == false and @throw_on_fail
224
+ # Assemble command-line representation of pipeline
225
+ cmd_line = []
226
+ args.each do |e|
227
+ if e.class == String
228
+ cmd_line << e
229
+ elsif e.class == Array
230
+ cmd_line << e.join(' ')
231
+ end
232
+ end
233
+
234
+ raise "Command failed: #{cmd_line.join(' | ')}\n #{error()}"
235
+ end
236
+
237
+ return result
238
+ end
239
+
240
+ # Runs one or more commands sequentially in a pipeline. This is not a true
241
+ # UNIX pipeline in that it's more conservative. It runs each command fully to
242
+ # exit and ensures that it completed successfully (with 0 exit status). If a
243
+ # command fails, it stops the pipeline and return false. Use this form when
244
+ # you need to ensure that every command in the pipeline must succeed before
245
+ # calling the next.
246
+ #
247
+ # Rather than using UNIX pipes to connect commands, this uses temporary
248
+ # files. The standard out of each process is piped to a temporary file which
249
+ # in turn is used as the standard in for the next command.
250
+ #
251
+ # @param commands A string containing a single command or an array of multiple
252
+ # commands.
253
+ #
254
+ # @param input Standard in to the first process in pipeline. This can be the
255
+ # a string or IO object.
256
+ #
257
+ # @return Returns true if all commands in pipeline completed successfully,
258
+ # false otherwise. Standard out of the last command it stored in the @out
259
+ # member while standard error is stored in @error. Process status info for
260
+ # each command is stored in @stats.
261
+
262
+ def strict(commands, input=nil)
263
+
264
+ opts_base = @opts.dup
265
+ in_read, in_write = IO.pipe()
266
+
267
+ r = nil
268
+ w = nil
269
+ e = nil
270
+
271
+ if commands.is_a? String
272
+ commands = [ commands ]
273
+ end
274
+
275
+ commands.each_with_index do |cmd, i|
276
+ cmd_opts = opts_base.dup
277
+
278
+ if String === cmd
279
+ cmd = [cmd]
280
+ else
281
+ cmd_opts.update cmd.pop if Hash === cmd.last
282
+ end
283
+
284
+ # Read stream
285
+ if i == 0
286
+ r = in_read
287
+ else
288
+ if r != in_read
289
+ # Close previous tempfile
290
+ r.close()
291
+ r.unlink() if r.is_a?(Tempfile)
292
+ end
293
+
294
+ r = w
295
+
296
+ if r.is_a?(Tempfile)
297
+ r.seek(0)
298
+ end
299
+ end
300
+
301
+ # Write stream
302
+ w = Tempfile.new(tempfile_name())
303
+
304
+ # Close previous stderr tempfile
305
+ if not e.nil?
306
+ e.close()
307
+ e.unlink()
308
+ end
309
+
310
+ e = Tempfile.new(tempfile_name())
311
+
312
+ cmd_opts[:in] = r
313
+ cmd_opts[:out] = w
314
+ cmd_opts[:err] = e
315
+
316
+ if @debug
317
+ $stderr.puts cmd
318
+ end
319
+
320
+ pid = Process.spawn(*cmd, cmd_opts)
321
+
322
+ if i == 0
323
+ if block_given?
324
+ yield in_write
325
+ else
326
+ if not input.nil?
327
+ if input.is_a?(Tempfile) or input.is_a?(IO)
328
+ input.seek(0)
329
+ in_write.write(input.read())
330
+ end
331
+
332
+ if input.is_a?(String)
333
+ in_write.write(input)
334
+ end
335
+ end
336
+ end
337
+
338
+ in_read.close()
339
+ in_write.close()
340
+ end
341
+
342
+ Process.wait(pid)
343
+
344
+ @stats << $?
345
+
346
+ if $? != 0
347
+ e.seek(0)
348
+ @error = "Process failed #{cmd}: #{e.read()}"
349
+ e.close()
350
+ e.unlink()
351
+
352
+ closeTempfiles([r,w,e])
353
+
354
+ return false
355
+ end
356
+ end
357
+
358
+ # Collect output
359
+ w.seek(0)
360
+ @out = w.read()
361
+ w.close()
362
+ w.unlink()
363
+
364
+ closeTempfiles([r,w,e])
365
+
366
+ return true
367
+ end
368
+
369
+ def tempfile_name()
370
+ value = '';
371
+ 8.times{value << (65 + rand(25)).chr}
372
+ value
373
+ end
374
+
375
+ def closeTempfiles(files)
376
+ files.each do |file|
377
+ if not file.nil? and file.is_a?(Tempfile)
378
+ file.close()
379
+ file.unlink()
380
+ end
381
+ end
382
+ end
383
+
384
+ end # class Pipeline
385
+
386
+ class Command < Status
387
+
388
+ attr_accessor :throw_on_fail
389
+ attr_reader :sys, :port, :user
390
+
391
+ def initialize(host, port=22, user='root', options: {})
392
+ super()
393
+
394
+ @sys = NixAdm::Pipeline.new()
395
+ @host = host
396
+ @user = user
397
+ @port = port
398
+ @throw_on_fail = true
399
+
400
+ @sys.debug = options[:debug] || false
401
+ end
402
+
403
+ def debug()
404
+ return @sys.debug
405
+ end
406
+
407
+ def debug=(value)
408
+ @sys.debug = value
409
+ end
410
+
411
+ def run(*args)
412
+ if @sys.run(*args) == false
413
+ failure -1, "Command failed: #{args}"
414
+ if @throw_on_fail == true
415
+ raise @status.msg
416
+ else
417
+ return status()
418
+ end
419
+ end
420
+
421
+ if block_given?
422
+ @sys.out.each_line do |line|
423
+ yield line
424
+ end
425
+ end
426
+
427
+ return success()
428
+ end
429
+
430
+ # Some derivative classes have their own separate notion of run(). Make exec()
431
+ # and alias to help with this.
432
+ alias exec run
433
+
434
+ # This command wraps a command in an SSH call. It checks for the existence of
435
+ # the host_to_run_on argument as well as the value in @host member (which
436
+ # takes precedence) and if either exist, it prepends the command with 'ssh' to
437
+ # run command remotely on that machine.
438
+ def resolveCommand(command, host_to_run_on = nil)
439
+
440
+ # Convert array of commands into pipeline then into string
441
+ if command.is_a?(Array)
442
+ command = command.join(' | ')
443
+ end
444
+
445
+ # You have to put quotes around command otherwise shell might
446
+ # misinterpret. For example, if we have:
447
+ #
448
+ # command = "zfs send data/jails/cron@1 | zfs receive root@storage -d data"
449
+ #
450
+ # then without double quotes around command it would be interpreted as
451
+ #
452
+ # ssh root@jan zfs send data/jails/cron@1 | zfs receive root@storage -d data
453
+ #
454
+ # This would set up the pipline is run locally on this the current machine
455
+ # the script is running and the "zfs send" output would be piped here and
456
+ # then from here on to storage. This is a waste of bandwidth. By putting
457
+ # command in double quotes it will be as follows:
458
+ #
459
+ # ssh root@jan "zfs send data/jails/cron@1 | zfs receive root@storage -d data"
460
+ #
461
+ # Here the entire pipeline is run remotely on jan and is piped directly to
462
+ # storage. This is what we want.
463
+
464
+ if not host_to_run_on.nil?
465
+ command = %Q{ssh -p #{@port} #{@user}@#{host_to_run_on} "#{command}"}
466
+ end
467
+
468
+ if not @host.nil?
469
+ command = %Q{ssh -p #{@port} #{@user}@#{@host} "#{command}"}
470
+ end
471
+
472
+ return command.gsub("\n", ' ').strip()
473
+ end
474
+
475
+ end # class Command
476
+
477
+ end # module NixAdm