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