shell_helpers 0.6.0 → 0.7.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/ChangeLog.md +236 -2
- data/LICENSE.txt +1 -1
- data/Rakefile +7 -10
- data/gemspec.yml +1 -0
- data/lib/shell_helpers.rb +61 -23
- data/lib/shell_helpers/export.rb +4 -4
- data/lib/shell_helpers/logger.bak.rb +481 -0
- data/lib/shell_helpers/logger.rb +274 -56
- data/lib/shell_helpers/pathname.rb +16 -3
- data/lib/shell_helpers/run.rb +48 -19
- data/lib/shell_helpers/sh.rb +97 -30
- data/lib/shell_helpers/sysutils.rb +71 -8
- data/lib/shell_helpers/utils.rb +12 -8
- data/lib/shell_helpers/version.rb +1 -1
- data/test/test_logger.rb +48 -0
- metadata +20 -5
data/lib/shell_helpers/run.rb
CHANGED
@@ -7,11 +7,7 @@ module ShellHelpers
|
|
7
7
|
extend(self)
|
8
8
|
RunError=Class.new(StandardError)
|
9
9
|
|
10
|
-
|
11
|
-
return "" unless sudoarg
|
12
|
-
return sudoarg.shellsplit if sudoarg.is_a?(String)
|
13
|
-
["sudo"]
|
14
|
-
end
|
10
|
+
## Trivial wrappers around Open3
|
15
11
|
|
16
12
|
#the run_* commands here capture all the output
|
17
13
|
def run_command(*command)
|
@@ -44,6 +40,19 @@ module ShellHelpers
|
|
44
40
|
r
|
45
41
|
end
|
46
42
|
|
43
|
+
## Launching a command
|
44
|
+
#handle sudo arguments
|
45
|
+
|
46
|
+
def sudo_args(sudoarg)
|
47
|
+
if sudoarg.respond_to?(:sudo_loop)
|
48
|
+
sudoarg.sudo_loop
|
49
|
+
end
|
50
|
+
return [] unless sudoarg
|
51
|
+
return sudoarg.shellsplit if sudoarg.is_a?(String)
|
52
|
+
["sudo"]
|
53
|
+
end
|
54
|
+
|
55
|
+
# get the args, environment and spawning options
|
47
56
|
def process_command(*args, **opts)
|
48
57
|
spawn_opts={}
|
49
58
|
if args.last.kind_of?(Hash)
|
@@ -56,7 +65,7 @@ module ShellHelpers
|
|
56
65
|
env,*args=*args
|
57
66
|
end
|
58
67
|
env.merge!(opts.delete(:env)||{})
|
59
|
-
args=args.map {|arg| arg.to_s} if args.length > 1
|
68
|
+
args=args.map.with_index {|arg, i| i == 0 && arg.is_a?(Array) ? arg : arg.to_s} if args.length > 1
|
60
69
|
spawn_opts.merge!(opts)
|
61
70
|
if sudo
|
62
71
|
if args.length > 1
|
@@ -68,22 +77,32 @@ module ShellHelpers
|
|
68
77
|
return env, args, spawn_opts
|
69
78
|
end
|
70
79
|
|
80
|
+
## Run a command
|
71
81
|
#by default capture stdout and status
|
82
|
+
# callbacks: yield status.success?, out, err, status if block_given?
|
83
|
+
# if status.success? => on_success.call(status, out, err)
|
84
|
+
# else error_mode.call(status, out, error)
|
85
|
+
# rescue ... => fail_mode.call(e)
|
86
|
+
# return status, out, error
|
72
87
|
def run(*args, output: :capture, error: nil, fail_mode: :error, chomp: false, sudo: false, error_mode: nil, expected: nil, on_success: nil, quiet: nil, **opts)
|
73
|
-
env, args, spawn_opts=Run.process_command(*args, **opts)
|
88
|
+
env, args, spawn_opts=Run.process_command(*args, sudo: sudo, **opts)
|
74
89
|
|
75
|
-
if args.
|
76
|
-
|
90
|
+
if args.is_a?(Array)
|
91
|
+
if args.length > 1
|
92
|
+
launch=args.shelljoin
|
93
|
+
else
|
94
|
+
launch=args.first #assume it has already been escaped
|
95
|
+
end
|
77
96
|
else
|
78
|
-
launch=args.
|
97
|
+
launch=args.to_s
|
79
98
|
end
|
80
99
|
launch+=" 2>/dev/null" if error==:quiet or quiet
|
81
100
|
launch+=" >/dev/null" if output==:quiet
|
82
|
-
out=
|
101
|
+
out=err=nil
|
83
102
|
|
84
103
|
begin
|
85
104
|
if error==:capture
|
86
|
-
out,
|
105
|
+
out, err, status=Open3.capture3(env, launch, spawn_opts)
|
87
106
|
elsif output==:capture
|
88
107
|
out, status=Open3.capture2(env, launch, spawn_opts)
|
89
108
|
else
|
@@ -104,8 +123,9 @@ module ShellHelpers
|
|
104
123
|
end
|
105
124
|
end
|
106
125
|
status=ProcessStatus.new(status, expected) if expected
|
107
|
-
|
108
|
-
if
|
126
|
+
status_success=status.success? if status.respond_to? :success?
|
127
|
+
yield status_success, out, err, status if block_given?
|
128
|
+
if status_success
|
109
129
|
# this block is called in case of success
|
110
130
|
on_success.call(status, out, err) if on_success.is_a?(Proc)
|
111
131
|
else # the command failed
|
@@ -117,7 +137,7 @@ module ShellHelpers
|
|
117
137
|
when :error
|
118
138
|
raise RunError.new("Error running command '#{launch}': #{status}")
|
119
139
|
when Proc
|
120
|
-
error_mode.call(status, out,
|
140
|
+
error_mode.call(status, out, err)
|
121
141
|
end
|
122
142
|
end
|
123
143
|
if chomp and out
|
@@ -130,18 +150,27 @@ module ShellHelpers
|
|
130
150
|
end
|
131
151
|
end
|
132
152
|
|
133
|
-
return out, error, status if error
|
134
|
-
return out,
|
153
|
+
# return out, error, status if error
|
154
|
+
return status, out, err
|
135
155
|
end
|
136
156
|
|
137
157
|
#a simple wrapper for %x//
|
158
|
+
#return the output; the block is called in case of an error
|
159
|
+
#eg SH.run_simple("echo foo; false") do p "error" end
|
160
|
+
#=> returns "foo\n" and prints error
|
138
161
|
def run_simple(*command, **opts, &b)
|
139
|
-
# here the block is called in case of
|
162
|
+
# here the block is called in case of error
|
140
163
|
opts[:error_mode]=b if b
|
141
|
-
out,
|
164
|
+
_status, out, _error = run(*command, **opts)
|
142
165
|
return out
|
143
166
|
end
|
144
167
|
|
168
|
+
# like run, but only returns status.success?
|
169
|
+
def run_success(*command, **opts, &b)
|
170
|
+
status, _out, _error = run(*command, **opts, &b)
|
171
|
+
status.success?
|
172
|
+
end
|
173
|
+
|
145
174
|
#same as Run, but if we get interrupted once, we don't want to launch any more commands
|
146
175
|
module Interrupt #{{{
|
147
176
|
extend(self)
|
data/lib/shell_helpers/sh.rb
CHANGED
@@ -57,6 +57,42 @@ module ShellHelpers
|
|
57
57
|
end
|
58
58
|
end
|
59
59
|
# }}}
|
60
|
+
|
61
|
+
# SudoLoop {{{
|
62
|
+
# extend SudoLoop.configure to run a sudo loop
|
63
|
+
# SH.sh("ls /", sudo: "sudo".extend(SH::SudoLoop.configure(**opts))
|
64
|
+
module SudoLoop
|
65
|
+
extend self
|
66
|
+
def configure(**opts)
|
67
|
+
return Module.new do |m|
|
68
|
+
m.define_method(:sudo_loop) do
|
69
|
+
SudoLoop.instance_method(:run_sudo_loop).bind(self).call(**opts)
|
70
|
+
end
|
71
|
+
m.define_method(:stop_sudo_loop) do
|
72
|
+
SudoLoop.instance_method(:stop_sudo_loop).bind(self).call
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
def run_sudo_loop(command: "sudo -v", interval: 30, init_command: command)
|
78
|
+
if @sudo_loop_thread.nil? or !@sudo_loop_thread.alive?
|
79
|
+
Sh.sh_or_proc(init_command, log: false)
|
80
|
+
#require 'pry'; binding.pry
|
81
|
+
@sudo_loop_thread = Thread.new do
|
82
|
+
loop do
|
83
|
+
Sh.sh_or_proc(command, log: false)
|
84
|
+
sleep(interval)
|
85
|
+
end
|
86
|
+
end
|
87
|
+
@sudo_loop_thread.run
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
def stop_sudo_loop
|
92
|
+
@sudo_loop_thread&.kill
|
93
|
+
end
|
94
|
+
end #}}}
|
95
|
+
|
60
96
|
# Sh {{{
|
61
97
|
# Module with various helper methods for executing external commands.
|
62
98
|
# In most cases, you can use #sh to run commands and have decent logging
|
@@ -82,10 +118,10 @@ module ShellHelpers
|
|
82
118
|
# # This block isn't called, since the command failed
|
83
119
|
# end
|
84
120
|
#
|
85
|
-
# sh 'ls -l /tmp/' do |stdout|
|
121
|
+
# sh 'ls -l /tmp/', capture: true do |status, stdout|
|
86
122
|
# # stdout contains the output of the command
|
87
123
|
# end
|
88
|
-
# sh 'ls -l /tmp/ /non_existent_dir' do |stdout,stderr|
|
124
|
+
# sh 'ls -l /tmp/ /non_existent_dir', capture: true do |status, stdout,stderr|
|
89
125
|
# # stdout contains the output of the command,
|
90
126
|
# # stderr contains the standard error output.
|
91
127
|
# end
|
@@ -108,16 +144,18 @@ module ShellHelpers
|
|
108
144
|
end
|
109
145
|
# }}}
|
110
146
|
|
147
|
+
ShError=Class.new(StandardError)
|
111
148
|
module Sh
|
112
149
|
include CLILogging
|
113
150
|
extend self
|
114
151
|
attr_writer :default_sh_options
|
115
152
|
def default_sh_options
|
116
|
-
@default_sh_options||={log: true, capture: false, on_success: nil,
|
153
|
+
@default_sh_options||={log: true, capture: false, on_success: nil, on_error: nil, expected:0, dryrun: false, escape: false,
|
117
154
|
log_level_execute_debug: :debug,
|
118
155
|
log_level_execute: :info, log_level_error: :error,
|
119
156
|
log_level_stderr: :error, log_level_stdout_success: :info,
|
120
|
-
log_level_stdout_fail: :warn, detach: false
|
157
|
+
log_level_stdout_fail: :warn, detach: false,
|
158
|
+
mode: :system}
|
121
159
|
end
|
122
160
|
|
123
161
|
attr_writer :spawned
|
@@ -131,14 +169,14 @@ module ShellHelpers
|
|
131
169
|
# callback called by sh to select the exec mode
|
132
170
|
# mode: :system,:spawn,:exec,:capture
|
133
171
|
# opts: sudo, env
|
134
|
-
def shrun(*args,mode: :system, **opts)
|
172
|
+
def shrun(*args,mode: :system, block: nil, **opts)
|
135
173
|
env, args, spawn_opts=Run.process_command(*args, **opts)
|
136
174
|
# p env, args, spawn_opts
|
137
175
|
case mode
|
138
176
|
when :system
|
139
177
|
system(env,*args,spawn_opts)
|
140
178
|
when :spawn, :detach
|
141
|
-
pid=spawn(env,*args,spawn_opts)
|
179
|
+
pid=spawn(env,*args,spawn_opts, &block)
|
142
180
|
if mode==:detach
|
143
181
|
Process.detach(pid)
|
144
182
|
else
|
@@ -151,11 +189,13 @@ module ShellHelpers
|
|
151
189
|
end
|
152
190
|
end
|
153
191
|
when :exec
|
154
|
-
exec(env,*args,spawn_opts)
|
192
|
+
exec(env,*args,spawn_opts, &block)
|
155
193
|
when :capture
|
156
|
-
Run.run_command(env,*args,spawn_opts)
|
194
|
+
Run.run_command(env,*args,spawn_opts, &block)
|
157
195
|
when :run
|
158
|
-
Run.run(env,*args,spawn_opts)
|
196
|
+
Run.run(env,*args,spawn_opts, &block)
|
197
|
+
else
|
198
|
+
raise ShError.new("In shrun, mode #{mode} not understood")
|
159
199
|
end
|
160
200
|
end
|
161
201
|
|
@@ -166,7 +206,7 @@ module ShellHelpers
|
|
166
206
|
# error output is logged at WARN.
|
167
207
|
# +:expected+:: an Int or Array of Int representing error codes, <b>in addition to 0</b>, that are expected and therefore constitute success. Useful for commands that don't use exit codes the way you'd like
|
168
208
|
# name: pretty name of command
|
169
|
-
# on_success,
|
209
|
+
# on_success,on_error: blocks to call on success/failure
|
170
210
|
# block:: if provided, will be called if the command exited nonzero. The block may take 0, 1, 2, or 3 arguments.
|
171
211
|
# The arguments provided are the standard output as a string, standard error as a string, and the processstatus as SH::ProcessStatus
|
172
212
|
# You should be safe to pass in a lambda instead of a block, as long as your lambda doesn't take more than three arguments
|
@@ -180,9 +220,13 @@ module ShellHelpers
|
|
180
220
|
# # ...
|
181
221
|
# end
|
182
222
|
#
|
183
|
-
# Returns the exit status of the command
|
223
|
+
# Returns the exit status of the command (Note that if the command doesn't exist, this returns 127.), stdout, stderr and the full status of the command
|
184
224
|
|
185
|
-
|
225
|
+
# callbacks: on_success, on_error
|
226
|
+
# yield process_status.success?,stdout,stderr,process_status if block_given?
|
227
|
+
# returns success; except in capture mode where it returns success,
|
228
|
+
# stdout, stderr, process_status
|
229
|
+
def sh(*command, argv0: nil, **opts)
|
186
230
|
defaults=default_sh_options
|
187
231
|
curopts=defaults.dup
|
188
232
|
defaults.keys.each do |k|
|
@@ -191,13 +235,19 @@ module ShellHelpers
|
|
191
235
|
end
|
192
236
|
|
193
237
|
log=curopts[:log]
|
194
|
-
|
238
|
+
command=[[command.first, argv0], *command[1..-1]] if argv0 and command.length > 1 and !curopts[:escape]
|
239
|
+
|
240
|
+
if command.length==1 and command.first.kind_of?(Array) #so that sh(["ls", "-a"]) works
|
241
|
+
command=command.first
|
242
|
+
command=[[command.first, argv0], *command[1..-1]] if argv0 and !curopts[:escape]
|
243
|
+
end
|
195
244
|
command_name = curopts[:name] || command_name(command) #this keep the options
|
245
|
+
# this should not be needed
|
196
246
|
command=command.shelljoin if curopts[:escape]
|
197
247
|
if log
|
198
248
|
sh_logger.send(curopts[:log_level_execute], SimpleColor.color("Executing '#{command_name}'",:bold))
|
199
249
|
p_env, p_args, p_opts= Run.process_command(*command, **opts)
|
200
|
-
sh_logger.send(curopts[:log_level_execute_debug], SimpleColor.color("Debug execute:
|
250
|
+
sh_logger.send(curopts[:log_level_execute_debug], SimpleColor.color("Debug execute: #{[p_env, *p_args, p_opts]}", :bold))
|
201
251
|
end
|
202
252
|
|
203
253
|
if !curopts[:dryrun]
|
@@ -208,15 +258,9 @@ module ShellHelpers
|
|
208
258
|
_pid = shrun(*command,**opts, mode: mode)
|
209
259
|
status=0; stdout=nil; stderr=nil
|
210
260
|
elsif curopts[:mode]==:run
|
211
|
-
|
212
|
-
case res.length
|
213
|
-
when 2
|
214
|
-
stdout, status=res; stderr=nil
|
215
|
-
when 3
|
216
|
-
stdout, stderr, status=res
|
217
|
-
end
|
261
|
+
status, stdout, stderr=shrun(*command,mode: curopts[:mode], **opts)
|
218
262
|
else
|
219
|
-
mode=curopts[:mode]
|
263
|
+
mode = curopts[:mode] || :system
|
220
264
|
shrun(*command,mode: mode, **opts)
|
221
265
|
status=$?; stdout=nil; stderr=nil
|
222
266
|
end
|
@@ -230,17 +274,26 @@ module ShellHelpers
|
|
230
274
|
if process_status.success?
|
231
275
|
sh_logger.send(curopts[:log_level_stdout_success], SimpleColor.color("stdout output of '#{command_name}':\n",:bold,:green)+stdout) unless stdout.nil? or stdout.strip.length == 0 or !log
|
232
276
|
curopts[:on_success].call(stdout,stderr,process_status) unless curopts[:on_success].nil?
|
233
|
-
block.call(stdout,stderr,process_status) unless block.nil?
|
277
|
+
# block.call(stdout,stderr,process_status) unless block.nil?
|
234
278
|
else
|
235
279
|
sh_logger.send(curopts[:log_level_stdout_fail], SimpleColor.color("stdout output of '#{command_name}':\n",:bold,:yellow)+stdout) unless stdout.nil? or stdout.strip.length == 0 or !log
|
236
280
|
sh_logger.send(curopts[:log_level_error], SimpleColor.color("Error running '#{command_name}': #{process_status.status}",:red,:bold)) if log
|
237
|
-
curopts[:
|
281
|
+
curopts[:on_error].call(stdout,stderr,process_status) unless curopts[:on_error].nil?
|
282
|
+
end
|
283
|
+
yield process_status.success?,stdout,stderr,process_status if block_given?
|
284
|
+
if curopts[:capture] || curopts[:mode]==:capture || curopts[:mode]==:run
|
285
|
+
return process_status.success?,stdout,stderr,process_status
|
286
|
+
else
|
287
|
+
return process_status.success?
|
238
288
|
end
|
239
|
-
return process_status.success?,stdout,stderr,process_status
|
240
289
|
|
241
290
|
rescue SystemCallError => ex
|
242
291
|
sh_logger.send(curopts[:log_level_error], SimpleColor.color("Error running '#{command_name}': #{ex.message}",:red,:bold)) if log
|
243
|
-
|
292
|
+
if block_given?
|
293
|
+
yield 127, nil, nil, nil
|
294
|
+
else
|
295
|
+
return 127, nil, nil, nil
|
296
|
+
end
|
244
297
|
end
|
245
298
|
|
246
299
|
# Run a command, throwing an exception if the command exited nonzero.
|
@@ -253,11 +306,11 @@ module ShellHelpers
|
|
253
306
|
# sh!("rsync foo bar", :failure_msg => "Couldn't rsync, check log for details")
|
254
307
|
# # => if command fails, app exits and user sees: "error: Couldn't rsync, check log for details
|
255
308
|
def sh!(*args,failure_msg: nil,**opts, &block)
|
256
|
-
|
309
|
+
on_error=Proc.new do |*blockargs|
|
257
310
|
process_status=blockargs.last
|
258
311
|
raise FailedCommandError.new(process_status.exitstatus,command_name(args),failure_msg: failure_msg)
|
259
312
|
end
|
260
|
-
sh(*args,**opts,
|
313
|
+
sh(*args,**opts,on_error: on_error,&block)
|
261
314
|
end
|
262
315
|
|
263
316
|
# Override the default logger (which is the one provided by CLILogging).
|
@@ -277,6 +330,20 @@ module ShellHelpers
|
|
277
330
|
end
|
278
331
|
end
|
279
332
|
|
333
|
+
# returns only the success or failure
|
334
|
+
def sh_or_proc(cmd, *args, **opts, &b)
|
335
|
+
case cmd
|
336
|
+
when Proc
|
337
|
+
cmd.call(*args, **opts, &b)
|
338
|
+
when Array
|
339
|
+
suc, _r=SH.sh(*cmd, *args, **opts, &b)
|
340
|
+
suc
|
341
|
+
when String
|
342
|
+
suc, _r=SH.sh(cmd + " #{args.shelljoin}", **opts, &b)
|
343
|
+
suc
|
344
|
+
end
|
345
|
+
end
|
346
|
+
|
280
347
|
private
|
281
348
|
def command_name(command)
|
282
349
|
if command.size == 1
|
@@ -369,8 +436,8 @@ ng or provide your own via #change_sh_logger." unless self.respond_to?(:logger)
|
|
369
436
|
cargs=Array(cmd_prepend) + [cmd] + dopts + Array(opts) + args + Array(cmd_postpone)
|
370
437
|
if !b
|
371
438
|
if method
|
372
|
-
b=lambda do |*args|
|
373
|
-
SH.public_send(method, *args)
|
439
|
+
b=lambda do |*args, **kw|
|
440
|
+
SH.public_send(method, *args, **kw)
|
374
441
|
end
|
375
442
|
else
|
376
443
|
b=lambda do |*args|
|
@@ -10,6 +10,8 @@ module ShellHelpers
|
|
10
10
|
SysError=Class.new(StandardError)
|
11
11
|
|
12
12
|
# wrap 'stat'
|
13
|
+
# @example
|
14
|
+
# SH.stat_file("mine") => {:access=>"700", :blocknumber=>0,...}
|
13
15
|
def stat_file(file)
|
14
16
|
require 'time'
|
15
17
|
opts=%w(a b B f F g G h i m n N o s u U w x y z)
|
@@ -37,9 +39,14 @@ module ShellHelpers
|
|
37
39
|
r[:statustime] = begin Time.parse(stats[19]) rescue nil end
|
38
40
|
r
|
39
41
|
end
|
42
|
+
|
40
43
|
# wrap stat --file-system
|
44
|
+
# @example
|
45
|
+
# SH.stat_filesystem("mine") => {:userfreeblocks=>..., fstype=>"btrfs"}
|
41
46
|
def stat_filesystem(file, up: true)
|
42
|
-
if up
|
47
|
+
if up #output the fs info of the first ascending path that exist
|
48
|
+
# usefull to get infos of the filesystem of a file we want to create
|
49
|
+
# but does not yet exist
|
43
50
|
file=Pathname.new(file)
|
44
51
|
file.ascend.each do |f|
|
45
52
|
return stat_filesystem(f, up: false) if f.exist?
|
@@ -47,7 +54,7 @@ module ShellHelpers
|
|
47
54
|
end
|
48
55
|
opts=%w(a b c d f i l n s S T)
|
49
56
|
stats=Run.run_simple("stat --file-system --format='#{opts.map{|o| "%#{o}\n"}.join}' #{file.shellescape}", chomp: :lines)
|
50
|
-
stats=stats.each_line.map {|l| l.chomp}
|
57
|
+
#stats=stats.each_line.map {|l| l.chomp}
|
51
58
|
r={}
|
52
59
|
r[:userfreeblocks]=stats[0].to_i
|
53
60
|
r[:totalblocks]=stats[1].to_i
|
@@ -93,6 +100,8 @@ module ShellHelpers
|
|
93
100
|
# :type=>"swap",
|
94
101
|
# :partlabel=>"swap",
|
95
102
|
# :partuuid=>"f4eef373-0803-4701-bd47-b968c44065a6"}
|
103
|
+
# @exemple
|
104
|
+
# SH.blkid => {"/dev/sda1"=> {:devname=>"/dev/sda1", :sec_type=>"msdos", :label_fatboot=>"boot", :label=>"boot", :uuid=>"D906-BEB0", :partlabel=>"boot", :partuuid=>"...",:fstype=>"vfat"}, ...}
|
96
105
|
def blkid(*args, sudo: false)
|
97
106
|
# get devname, (part)label/uuid, fstype
|
98
107
|
fsoptions=Run.run_simple("blkid -o export #{args.shelljoin}", fail_mode: :empty, chomp: true, sudo: sudo)
|
@@ -100,6 +109,8 @@ module ShellHelpers
|
|
100
109
|
end
|
101
110
|
|
102
111
|
# use lsblk to get infos about devices
|
112
|
+
# @exemple
|
113
|
+
# SH.lsblk => {"/dev/sda"=>{:devname=>"/dev/sda", :devtype=>"disk"}, "/dev/sda1"=> {:devname=>"/dev/sda1", :label=>"boot", :uuid=>"D906-BEB0", :partlabel=>"boot", :partuuid=>"00000000-0000-0000-0000-000000000000", :parttype=>"c12a7328-f81f-11d2-ba4b-00a0c93ec93b", :devtype=>"part", :fstype=>"vfat"},...}
|
103
114
|
def lsblk(sudo: false)
|
104
115
|
# get devname, mountpoint, (part)label/uuid, (part/dev/fs)type
|
105
116
|
fsoptions=Run.run_simple("lsblk -l -J -o NAME,MOUNTPOINT,LABEL,UUID,PARTLABEL,PARTUUID,PARTTYPE,TYPE,FSTYPE", fail_mode: :empty, chomp: true, sudo: sudo)
|
@@ -123,6 +134,8 @@ module ShellHelpers
|
|
123
134
|
end
|
124
135
|
|
125
136
|
# use findmnt to get infos about mount points
|
137
|
+
# @exemple
|
138
|
+
# SH.findmnt => {"/dev/bcache0[/slash]"=> {:mountpoint=>"/", :devname=>"/dev/bcache0[/slash]", :fstype=>"btrfs", :mountoptions=> ["rw", "noatime", "compress=lzo", "ssd", "space_cache", "autodefrag", "subvolid=257", "subvol=/slash"], :label=>"rootleaf", :uuid=>"1db5b600-df3e-4d1e-9eef-6a0a7fda491d", :partlabel=>"", :partuuid=>"", :fsroot=>"/slash"}, ...}
|
126
139
|
def findmnt(sudo: false)
|
127
140
|
# get devname, mountpoint, mountoptions, (part)label/uuid, fsroot
|
128
141
|
# only looks at mounted devices (but in comparison to lsblk also show
|
@@ -139,6 +152,8 @@ module ShellHelpers
|
|
139
152
|
fs
|
140
153
|
end
|
141
154
|
|
155
|
+
# we default to lsblk
|
156
|
+
# findmnt adds the subvolumes, the fsroot and the mountoptions
|
142
157
|
def fs_infos(mode: :devices)
|
143
158
|
return findmnt if mode == :mount
|
144
159
|
return lsblk.merge(findmnt) if mode == :all
|
@@ -151,6 +166,9 @@ module ShellHelpers
|
|
151
166
|
end
|
152
167
|
|
153
168
|
# find devices matching props
|
169
|
+
# @exemple
|
170
|
+
# SH.find_devices({label: "boot"})
|
171
|
+
# => [{:devname=>"/dev/sda1", :label=>"boot", :uuid=>"D906-BEB0", :partlabel=>"boot", :partuuid=>"...", :parttype=>"c12a7328-f81f-11d2-ba4b-00a0c93ec93b", :devtype=>"part", :fstype=>"vfat"}]
|
154
172
|
def find_devices(props, method: :all)
|
155
173
|
props=props.clone
|
156
174
|
return [{devname: props[:devname]}] unless props[:devname].nil?
|
@@ -161,6 +179,10 @@ module ShellHelpers
|
|
161
179
|
end
|
162
180
|
|
163
181
|
if method==:blkid
|
182
|
+
# try with UUID, then LABEL, then PARTUUID
|
183
|
+
# as soon as we have a non empty label, we return the result of
|
184
|
+
# blkid on it.
|
185
|
+
#
|
164
186
|
# Warning, since 'blkid' can only test one label, we cannot check
|
165
187
|
# that all parameters are valid
|
166
188
|
# search from most discriminant to less discriminant
|
@@ -174,21 +196,27 @@ module ShellHelpers
|
|
174
196
|
if props[:parttype]
|
175
197
|
find_devices(props, method: :all)
|
176
198
|
end
|
177
|
-
else
|
199
|
+
else #method=:all
|
178
200
|
fs=fs_infos
|
179
|
-
# here we check all parameters
|
201
|
+
# here we check all parameters (ie all defined labels are correct)
|
180
202
|
# however, if none are defined, this return true, so we check that at least one is defined
|
181
203
|
return [] unless %i(uuid label partuuid partlabel parttype).any? {|k| props[k]}
|
182
204
|
return fs.keys.select do |k|
|
183
205
|
fsprops=fs[k]
|
184
|
-
# the fsinfos should have one of this parameters defined
|
185
|
-
next false unless %i(uuid label partuuid partlabel parttype).any? {|k| fsprops[k]}
|
186
206
|
next false if (disk=props[:disk]) && !fsprops[:devname].start_with?(disk.to_s)
|
187
|
-
|
207
|
+
# all defined labels should match
|
208
|
+
next false unless %i(uuid label partuuid partlabel parttype).all? do |key|
|
188
209
|
ptype=props[key]
|
189
210
|
ptype=partition_type(ptype) if key==:parttype and ptype.is_a?(Symbol)
|
190
211
|
!ptype or !fsprops[key] or ptype==fsprops[key]
|
191
212
|
end
|
213
|
+
# their should at least be one matching label
|
214
|
+
next false unless %i(uuid label partuuid partlabel parttype).select do |key|
|
215
|
+
ptype=props[key]
|
216
|
+
ptype=partition_type(ptype) if key==:parttype and ptype.is_a?(Symbol)
|
217
|
+
ptype and fsprops[key] and ptype==fsprops[key]
|
218
|
+
end.length > 0
|
219
|
+
true
|
192
220
|
end.map {|k| fs[k]}
|
193
221
|
end
|
194
222
|
return []
|
@@ -204,6 +232,19 @@ module ShellHelpers
|
|
204
232
|
return devs.first&.fetch(:devname)
|
205
233
|
end
|
206
234
|
|
235
|
+
# Mount devices on paths
|
236
|
+
# @argument
|
237
|
+
# - paths: Array (or Hash) of path
|
238
|
+
# => path: {mountpoint: "/mnt", mountoptions: [], fstype: "ext4",
|
239
|
+
# subvol: ..., device_info}
|
240
|
+
# where device_info is used to find the device via find_device
|
241
|
+
# so can be set via :devname, or uuid label partuuid partlabel parttype
|
242
|
+
# @opts
|
243
|
+
# - sort: sort the mountpoints
|
244
|
+
# - abort_on_error: fail if a mount failt
|
245
|
+
# - mkpath: mkpath the mountpoints
|
246
|
+
# @return
|
247
|
+
# the paths and a lambda to unmount
|
207
248
|
def mount(paths, mkpath: true, abort_on_error: true, sort: true)
|
208
249
|
paths=paths.values if paths.is_a?(Hash)
|
209
250
|
paths=paths.select {|p| p[:mountpoint]}
|
@@ -244,6 +285,9 @@ module ShellHelpers
|
|
244
285
|
end
|
245
286
|
end
|
246
287
|
|
288
|
+
# by default give symbol => guid
|
289
|
+
# can also give symbol => hexa (mode: :hexa)
|
290
|
+
# or hexa/guid => symbol (mode: :symbol)
|
247
291
|
def partition_type(type, mode: :guid)
|
248
292
|
if mode==:symbol
|
249
293
|
%i(boot swap home x86_root x86-64_root arm64_root arm32_root linux).each do |symb|
|
@@ -272,6 +316,8 @@ module ShellHelpers
|
|
272
316
|
end
|
273
317
|
end
|
274
318
|
|
319
|
+
# @exemple
|
320
|
+
# SH.partition_infos("/dev/sda", sudo: true) => [{:partlabel=>"boot", :partattributes=>"0000000000000004", :partuuid=>"00000000-0000-0000-0000-000000000000", :parttype=>"c12a7328-f81f-11d2-ba4b-00a0c93ec93b"}, {:partlabel=>"swap", :partattributes=>"0000000000000000", :partuuid=>"f4eef373-0803-4701-bd47-b968c44065a6", :parttype=>"0fc63daf-8483-4772-8e79-3d69d8477de4"}, {:partlabel=>"slash", :partattributes=>"0000000000000000", :partuuid=>"31b4cd66-39ab-4c5b-a229-d5d2010d53dd", :parttype=>"0fc63daf-8483-4772-8e79-3d69d8477de4"}]
|
275
321
|
def partition_infos(device, sudo: false)
|
276
322
|
parts = Run.run_simple("partx -o NR --show #{device.shellescape}", sudo: sudo) { return nil }
|
277
323
|
infos=[]
|
@@ -295,7 +341,16 @@ module ShellHelpers
|
|
295
341
|
infos
|
296
342
|
end
|
297
343
|
|
298
|
-
|
344
|
+
#@argument
|
345
|
+
# A list of partitions
|
346
|
+
# Note that here the device has to be specified with :disk; looking at
|
347
|
+
# PARTLABEL or things like that don't make sense if the partitions
|
348
|
+
# don't exist
|
349
|
+
#@options:
|
350
|
+
# - check => check that no partitions exist first
|
351
|
+
# - partprobe: run partprobe if we made partitions
|
352
|
+
# @exemple
|
353
|
+
# SH.make_partitions( {:boot=> {:parttype=>:boot, :partlength=>"+100M", :fstype=>"vfat", :rel_mountpoint=>"boot", :mountoptions=>["fmask=133"]}, :slash=> {:parttype=>:"x86-64_root", :fstype=>"ext4", :rel_mountpoint=>"."}, ...})
|
299
354
|
def make_partitions(partitions, check: true, partprobe: true)
|
300
355
|
partitions=partitions.values if partitions.is_a?(Hash)
|
301
356
|
done=[]
|
@@ -351,6 +406,9 @@ module ShellHelpers
|
|
351
406
|
Sh.sh("wipefs -a #{disk.shellescape}", sudo: true)
|
352
407
|
end
|
353
408
|
|
409
|
+
# make filesystems
|
410
|
+
# takes a list of fs infos (where the device is specified like before,
|
411
|
+
# via devname, label, ...)
|
354
412
|
def make_fs(fs, check: true)
|
355
413
|
fs=fs.values if fs.is_a?(Hash)
|
356
414
|
fs.each do |partfs|
|
@@ -378,6 +436,7 @@ module ShellHelpers
|
|
378
436
|
end
|
379
437
|
end
|
380
438
|
|
439
|
+
# use fallocate to make a raw image
|
381
440
|
def make_raw_image(name, size="1G")
|
382
441
|
raw=Pathname.new(name)
|
383
442
|
raw.touch
|
@@ -387,6 +446,7 @@ module ShellHelpers
|
|
387
446
|
raw
|
388
447
|
end
|
389
448
|
|
449
|
+
# makes a btrfs subvolume
|
390
450
|
def make_btrfs_subvolume(dir, check: true)
|
391
451
|
if check and dir.directory?
|
392
452
|
raise SysError("Subvolume already exists at #{dir}") if check==:raise
|
@@ -396,6 +456,8 @@ module ShellHelpers
|
|
396
456
|
dir
|
397
457
|
end
|
398
458
|
end
|
459
|
+
|
460
|
+
# try to make a subvolume, else fallsback to a dir
|
399
461
|
def make_dir_or_subvolume(dir)
|
400
462
|
dir=Pathname.new(dir)
|
401
463
|
return :directory if dir.directory?
|
@@ -409,6 +471,7 @@ module ShellHelpers
|
|
409
471
|
end
|
410
472
|
end
|
411
473
|
|
474
|
+
# runs losetup, and returns the created disk, and a lambda to close
|
412
475
|
def losetup(img)
|
413
476
|
disk = Run.run_simple("losetup -f --show #{img.shellescape}", sudo: true, chomp: true, error_mode: :nil)
|
414
477
|
close=lambda do
|