nixadm 1.0.6

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,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