nixadm 1.0.6
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/README.md +26 -0
- data/src/lib/nixadm/backup.rb +267 -0
- data/src/lib/nixadm/db/postgres.rb +9 -0
- data/src/lib/nixadm/db/postgresql.rb +217 -0
- data/src/lib/nixadm/pipeline.rb +477 -0
- data/src/lib/nixadm/util.rb +210 -0
- data/src/lib/nixadm/version.rb +11 -0
- data/src/lib/nixadm/zfs.rb +564 -0
- metadata +52 -0
@@ -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
|