Module: ShellHelpers::SysUtils

Extended by:
SysUtils
Included in:
ShellHelpers, SysUtils
Defined in:
lib/shell_helpers/sysutils.rb

Constant Summary collapse

SysError =
Class.new(StandardError)

Instance Method Summary collapse

Instance Method Details

#blkid(*args, sudo: false) ⇒ Object

output should be the result of blkid -o export ... return a list of things like :label=>"swap", :uuid=>"82af0d2f-5ef6-418a-8656-bdfe843f19e1", :type=>"swap", :partlabel=>"swap", :partuuid=>"f4eef373-0803-4701-bd47-b968c44065a6" SH.blkid => {:devname=>"/dev/sda1", :sec_type=>"msdos", :label_fatboot=>"boot", :label=>"boot", :uuid=>"D906-BEB0", :partlabel=>"boot", :partuuid=>"...",:fstype=>"vfat", ...}



105
106
107
108
109
# File 'lib/shell_helpers/sysutils.rb', line 105

def blkid(*args, sudo: false)
	# get devname, (part)label/uuid, fstype
	fsoptions=Run.run_simple("blkid -o export #{args.shelljoin}", fail_mode: :empty, chomp: true, sudo: sudo)
	parse_blkid(fsoptions)
end

#find_device(props) ⇒ Object

like find_devices but warn out if the result is of length > 1



226
227
228
229
230
231
232
233
# File 'lib/shell_helpers/sysutils.rb', line 226

def find_device(props)
	devs=find_devices(props)
	devs=yield(devs) if block_given?
	devs=[devs].flatten
	warn "Device #{props} not found" if devs.empty?
	warn "Several devices for #{props} found: #{devs.map {|d| d&.fetch(:devname)}}" if devs.length >1
	return devs.first&.fetch(:devname)
end

#find_devices(props, method: :all) ⇒ Object

find devices matching props SH.find_devices("boot") => [:label=>"boot", :uuid=>"D906-BEB0", :partlabel=>"boot", :partuuid=>"...", :parttype=>"c12a7328-f81f-11d2-ba4b-00a0c93ec93b", :devtype=>"part", :fstype=>"vfat"]



172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
# File 'lib/shell_helpers/sysutils.rb', line 172

def find_devices(props, method: :all)
	props=props.clone
	return [{devname: props[:devname]}] unless props[:devname].nil?
	# name is both for label and partlabel
	if props.key?(:name)
		props[:label] = props[:name] unless props.key?(:label)
		props[:partlabel] = props[:name] unless props.key?(:partlabel)
	end

	if method==:blkid
		# try with UUID, then LABEL, then PARTUUID
		# as soon as we have a non empty label, we return the result of
		# blkid on it.
		#
		# Warning, since 'blkid' can only test one label, we cannot check
		# that all parameters are valid
		# search from most discriminant to less discriminant
		%i(uuid label partuuid partlabel).each do |key|
			if (label=props[key])
				return parse_blkid(%x/blkid -o export -t #{key.to_s.upcase}=#{label.shellescape}/).values
			end
		end
		# unfortunately `blkid PARTTYPE=...` does not work, so we need to parse
		# ourselves
		if props[:parttype]
			find_devices(props, method: :all)
		end
	else #method=:all
		fs=fs_infos
		# here we check all parameters (ie all defined labels are correct)
		# however, if none are defined, this return true, so we check that at least one is defined
		return [] unless %i(uuid label partuuid partlabel parttype).any? {|k| props[k]}
		return fs.keys.select do |k|
			fsprops=fs[k]
			next false if (disk=props[:disk]) && !fsprops[:devname].start_with?(disk.to_s)
			# all defined labels should match
			next false unless %i(uuid label partuuid partlabel parttype).all? do |key|
				ptype=props[key]
				ptype=partition_type(ptype) if key==:parttype and ptype.is_a?(Symbol)
				!ptype or !fsprops[key] or ptype==fsprops[key]
			end
			# their should at least be one matching label
			next false unless %i(uuid label partuuid partlabel parttype).select do |key|
				ptype=props[key]
				ptype=partition_type(ptype) if key==:parttype and ptype.is_a?(Symbol)
				ptype and fsprops[key] and ptype==fsprops[key]
			end.length > 0
			true
		end.map {|k| fs[k]}
	end
	return []
end

#findmnt(sudo: false) ⇒ Object

use findmnt to get infos about mount points SH.findmnt => {: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", ...}



139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
# File 'lib/shell_helpers/sysutils.rb', line 139

def findmnt(sudo: false)
	# get devname, mountpoint, mountoptions, (part)label/uuid, fsroot
	# only looks at mounted devices (but in comparison to lsblk also show
	# virtual mounts and bind mounts)
	fsoptions=SH::Run.run_simple("findmnt --raw -o SOURCE,TARGET,FSTYPE,OPTIONS,LABEL,UUID,PARTLABEL,PARTUUID,FSROOT", fail_mode: :empty, chomp: true, sudo: sudo)
	fs={}
	fsoptions.each_line.to_a[1..-1]&.each do |l|
		#two '	' means a missing option, so we want to split on / /, not on ' '
		source,target,fstype,options,label,uuid,partlabel,partuuid,fsroot=l.chomp.split(/ /)
		next unless source=~%r(^/dev/) #skip non dev mountpoints
		options=options.split(',')
		fs[source]={mountpoint: target, devname: source, fstype: fstype, mountoptions: options, label: label, uuid: uuid, partlabel: partlabel, partuuid: partuuid, fsroot: fsroot}
	end
	fs
end

#fs_infos(mode: :devices) ⇒ Object

we default to lsblk findmnt adds the subvolumes, the fsroot and the mountoptions



157
158
159
160
161
162
# File 'lib/shell_helpers/sysutils.rb', line 157

def fs_infos(mode: :devices)
	return findmnt if mode == :mount
	return lsblk.merge(findmnt) if mode == :all
	# :devname, :devtype, :mountpoint, [:mountoptions], :label, :uuid, :partlabel, :partuuid, :parttype, :fstype, [:fsroot]
	lsblk
end

#losetup(img) ⇒ Object

runs losetup, and returns the created disk, and a lambda to close



475
476
477
478
479
480
481
482
483
484
485
486
487
488
# File 'lib/shell_helpers/sysutils.rb', line 475

def losetup(img)
	disk = Run.run_simple("losetup -f --show #{img.shellescape}", sudo: true, chomp: true, error_mode: :nil)
	close=lambda do
		SH.sh("losetup -d #{disk.shellescape}", sudo: true) if disk
	end
	if block_given?
		begin
			yield disk
		ensure
			close.call
		end
	end
	return disk, close
end

#lsblk(sudo: false) ⇒ Object

use lsblk to get infos about devices SH.lsblk => :devtype=>"disk", "/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",...}



114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
# File 'lib/shell_helpers/sysutils.rb', line 114

def lsblk(sudo: false)
	# get devname, mountpoint, (part)label/uuid, (part/dev/fs)type
	fsoptions=Run.run_simple("lsblk -l -J -o NAME,MOUNTPOINT,LABEL,UUID,PARTLABEL,PARTUUID,PARTTYPE,TYPE,FSTYPE", fail_mode: :empty, chomp: true, sudo: sudo)
	require 'json'
	json=JSON.parse(fsoptions)
	fs={}
	json["blockdevices"]&.each do |props|
		r={}
		props.each do |k,v|
			k=k.to_sym
			k=:devtype if k==:type
			if k==:name
				k=:devname
				v="/dev/#{v}"
			end
			r[k]=v unless v.nil?
		end
		fs[r[:devname]]=r
	end
	fs
end

#make_btrfs_subvolume(dir, check: true) ⇒ Object

makes a btrfs subvolume



450
451
452
453
454
455
456
457
458
# File 'lib/shell_helpers/sysutils.rb', line 450

def make_btrfs_subvolume(dir, check: true)
	if check and dir.directory?
		raise SysError("Subvolume already exists at #{dir}") if check==:raise
		warn "Subvolume already exists at #{dir}, skipping..."
	else
		SH.sh("btrfs subvolume create #{dir.shellescape}", sudo: true)
		dir
	end
end

#make_dir_or_subvolume(dir) ⇒ Object

try to make a subvolume, else fallsback to a dir



461
462
463
464
465
466
467
468
469
470
471
472
# File 'lib/shell_helpers/sysutils.rb', line 461

def make_dir_or_subvolume(dir)
	dir=Pathname.new(dir)
	return :directory if dir.directory?
	fstype=stat_filesystem(dir, up: true)
	if fstype[:fstype]=="btrfs"
		make_btrfs_subvolume(dir)
		return :subvol
	else
		dir.sudo_mkpath
		return :directory
	end
end

#make_fs(fs, check: true) ⇒ Object

make filesystems takes a list of fs infos (where the device is specified like before, via devname, label, ...)



412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
# File 'lib/shell_helpers/sysutils.rb', line 412

def make_fs(fs, check: true)
	fs=fs.values if fs.is_a?(Hash)
	fs.each do |partfs|
		dev=SH.find_device(partfs)
		if dev and (fstype=partfs[:fstype])
			opts=partfs[:fsoptions]||[]
			bin="mkfs.#{fstype.to_s.shellescape}"
			bin="mkswap" if fstype.to_s=="swap"
			label=partfs[:label]||partfs[:name]
			if label
				labelkey="-L"
				labelkey="-n" if fstype.to_s=="vfat"
				opts+=[labelkey, label]
			end
			if check
				diskinfos=blkid(dev, sudo: true)
				unless diskinfos.dig(dev,:fstype).nil?
					raise SysError("Device #{dev} already has a filesystem: #{diskinfos[dev]}") if check==:raise
					warn "Device #{dev} already has a filesystem: #{diskinfos[dev]}"
					next
				end
			end
			SH.sh("#{bin} #{opts.shelljoin} #{dev.shellescape}", sudo: true)
		end
	end
end

#make_partitions(partitions, check: true, partprobe: true) ⇒ Object

@options:

  • check => check that no partitions exist first
  • partprobe: run partprobe if we made partitions SH.make_partitions( {:parttype=>:boot, :partlength=>"+100M", :fstype=>"vfat", :rel_mountpoint=>"boot", :mountoptions=>["fmask=133"], :slash=> :fstype=>"ext4", :rel_mountpoint=>".", ...})


354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
# File 'lib/shell_helpers/sysutils.rb', line 354

def make_partitions(partitions, check: true, partprobe: true)
	partitions=partitions.values if partitions.is_a?(Hash)
	done=[]
	disk_partitions=partitions.group_by {|p| p[:disk]}
	disk_partitions.each do |disk, dpartitions|
		next if disk.nil?
		if check
			partinfos=blkid(disk, sudo: true)
			# gpt partitions: PTUUID="652121ab-7935-403c-8b87-65a149a415ac" PTTYPE="gpt"
			# dos partitions: PTUUID="17a4a006" PTTYPE="dos"
			# others: PTTYPE="PMBR"
			unless partinfos.empty?
				raise SysError("Disk #{disk} is not empty: #{partinfos}") if check==:raise
				warn "Disk #{disk} is not empty: #{partinfos}, skipping..."
				next
			end
		end
		opts=[]
		dpartitions.each do |partition|
			next unless %i(partnum partstart partlength partlabel partattributes parttype).any? {|k| partition.key?(k)}
			num=partition[:partnum]&.to_i || 0
			start=partition[:partstart] || 0
			length=partition[:partlength] || 0
			name=partition[:partlabel] || partition[:name]
			attributes=partition[:partattributes]
			type=partition[:parttype]
			attributes=2 if type==:boot
			type=partition_type(type, mode: :hexa) if type.is_a?(Symbol)
			uuid=partition[:partuuid]
			alignment=partition[:partalignment]
			opts += ["-n", "#{num}:#{start}:#{length}"]
			opts += ["-c", "#{num}:#{name}"] if name
			opts += ["-t", "#{num}:#{type}"] if type
			opts += ["-u", "#{num}:#{uuid}"] if uuid
			opts << "--attributes=#{num}:set:#{attributes}" if attributes
			opts << ["--set-alignment=#{alignment}"] if alignment
		end
		unless opts.empty?
			Sh.sh!("sgdisk #{opts.shelljoin} #{disk.shellescape}", sudo: true)
			done << disk
		end
	end
	SH.sh("partprobe #{done.shelljoin}", sudo: true) unless done.empty? or !partprobe
	done
end

#make_raw_image(name, size = "1G") ⇒ Object

use fallocate to make a raw image



440
441
442
443
444
445
446
447
# File 'lib/shell_helpers/sysutils.rb', line 440

def make_raw_image(name, size="1G")
	raw=Pathname.new(name)
	raw.touch
	rawfs=stat_filesystem(raw)
	raw.chattr("+C") if rawfs[:fstype]=="btrfs"
	Sh.sh("fallocate -l #{size} #{raw.shellescape}")
	raw
end

#mount(paths, mkpath: true, abort_on_error: true, sort: true) ⇒ Object

Mount devices on paths

  • paths: Array (or Hash) of path => path: "/mnt", mountoptions: [], fstype: "ext4", subvol: ..., device_info where device_info is used to find the device via find_device so can be set via :devname, or uuid label partuuid partlabel parttype
  • sort: sort the mountpoints
  • abort_on_error: fail if a mount failt
  • mkpath: mkpath the mountpoints the paths and a lambda to unmount


248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
# File 'lib/shell_helpers/sysutils.rb', line 248

def mount(paths, mkpath: true, abort_on_error: true, sort: true)
	paths=paths.values if paths.is_a?(Hash)
	paths=paths.select {|p| p[:mountpoint]}
	# sort so that the mounts are in correct order
	paths=paths.sort { |p1, p2| Pathname.new(p1[:mountpoint]) <=> Pathname.new(p2[:mountpoint]) } if sort
	close=lambda do
		umount(paths, sort: sort)
	end
	paths.each do |path|
		dev=find_device(path)
		raise SysError.new("Device #{path} not found") unless dev
		options=path[:mountoptions]||[]
		options=options.split(',') if options.is_a?(String)
		options<<"subvol=#{path[:subvol].shellescape}" if path[:subvol]
		#options=options.join(',') if options.is_a?(Array)
		mntpoint=Pathname.new(path[:mountpoint])
		mntpoint.sudo_mkpath if mkpath
		cmd="mount #{(fs=path[:fstype]) && "-t #{fs.shellescape}"} #{options.empty? ? "" : "-o #{options.join(',').shellescape}"} #{dev.shellescape} #{mntpoint.shellescape}"
		abort_on_error ? Sh.sh!(cmd, sudo: true) : Sh.sh(cmd, sudo: true)
	end
	if block_given?
		begin
			yield paths
		ensure
			close.call
		end
	end
	return paths, close
end

#parse_blkid(output) ⇒ Object



73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
# File 'lib/shell_helpers/sysutils.rb', line 73

def parse_blkid(output)
	devs={}
	r=[]
	convert=lambda do |h|
		h[:type] && h[:fstype]=h.delete(:type)
		name=h[:devname]
		devs[name]=h
	end
	output=output.each_line if output.is_a?(String)
	output.each do |l|
		l=l.chomp
		if l.empty?
			convert.(Export.import_parse(r))
			r=[]
		else
			r<<l
		end
	end
	convert.(Export.import_parse(r)) unless r.empty?
	devs
end

#partition_infos(device, sudo: false) ⇒ Object

SH.partition_infos("/dev/sda", sudo: true) => [:partattributes=>"0000000000000004", :partuuid=>"00000000-0000-0000-0000-000000000000", :parttype=>"c12a7328-f81f-11d2-ba4b-00a0c93ec93b", :partattributes=>"0000000000000000", :partuuid=>"f4eef373-0803-4701-bd47-b968c44065a6", :parttype=>"0fc63daf-8483-4772-8e79-3d69d8477de4", :partattributes=>"0000000000000000", :partuuid=>"31b4cd66-39ab-4c5b-a229-d5d2010d53dd", :parttype=>"0fc63daf-8483-4772-8e79-3d69d8477de4"]



321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
# File 'lib/shell_helpers/sysutils.rb', line 321

def partition_infos(device, sudo: false)
	parts = Run.run_simple("partx -o NR --show #{device.shellescape}", sudo: sudo) { return nil }
	infos=[]
	nums=parts.each_line.count - 1
	(1..nums).each do |i|
		infos[i-1]={}
		part_options=Run.run_simple("sgdisk -i#{i} #{device.shellescape}", chomp: true, sudo: sudo)
		part_options.match(/^Partition name: '(.*)'/) do |m|
			infos[i-1][:partlabel]=m[1]
		end
		part_options.match(/^Attribute flags: (.*)/) do |m|
			infos[i-1][:partattributes]=m[1]
		end
		part_options.match(/^Partition unique GUID: (.*)/) do |m|
			infos[i-1][:partuuid]=m[1].downcase
		end
		part_options.match(/^Partition GUID code: (\S*)/) do |m|
			infos[i-1][:parttype]=m[1].downcase
		end
	end
	infos
end

#partition_type(type, mode: :guid) ⇒ Object

by default give symbol => guid can also give symbol => hexa (mode: :hexa) or hexa/guid => symbol (mode: :symbol)



291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
# File 'lib/shell_helpers/sysutils.rb', line 291

def partition_type(type, mode: :guid)
	if mode==:symbol
		%i(boot swap home x86_root x86-64_root arm64_root arm32_root linux).each do |symb|
			%i(hexa guid).each do |mode|
				partition_type(symb, mode: mode) == type.downcase and return symb
			end
		end
	end
	case type
	when :boot
		mode == :hexa ? "ef00" : "c12a7328-f81f-11d2-ba4b-00a0c93ec93b"
	when :swap
		mode == :hexa ? "8200" : "0657fd6d-a4ab-43c4-84e5-0933c84b4f4f"
	when :home
		mode == :hexa ? "8302" : "933ac7e1-2eb4-4f13-b844-0e14e2aef915"
	when :x86_root
		mode == :hexa ? "8303" : "44479540-f297-41b2-9af7-d131d5f0458a"
	when :"x86-64_root"
		mode == :hexa ? "8304" : "4f68bce3-e8cd-4db1-96e7-fbcaf984b709"
	when :arm64_root
		mode == :hexa ? "8305" : "b921b045-1df0-41c3-af44-4c6f280d3fae"
	when :arm32_root
		mode == :hexa ? "8307" : "69dad710-2ce4-4e3c-b16c-21a1d49abed3"
	when :linux
		mode == :hexa ? "8300" : "0fc63daf-8483-4772-8e79-3d69d8477de4"
	end
end

#refresh_blkid_cacheObject



164
165
166
# File 'lib/shell_helpers/sysutils.rb', line 164

def refresh_blkid_cache
	Sh.sh("blkid", sudo: true)
end

#stat_file(file) ⇒ Object

wrap 'stat' SH.stat_file("mine") => :blocknumber=>0,...



15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
# File 'lib/shell_helpers/sysutils.rb', line 15

def stat_file(file)
	require 'time'
	opts=%w(a b B f F g G h i m n N o s u U w x y z)
	stats=Run.run_simple("stat --format='#{opts.map{|o| "%#{o}\n"}.join}' #{file.shellescape}", chomp: :lines)
	r={}
	r[:access]=stats[0]
	r[:blocknumber]=stats[1].to_i
	r[:blocksize]=stats[2].to_i
	r[:rawmode]=stats[3]
	r[:filetype]=stats[4]
	r[:gid]=stats[5].to_i
	r[:group]=stats[6]
	r[:hardlinks]=stats[7].to_i
	r[:inode]=stats[8].to_i
	r[:mountpoint]=stats[9]
	r[:filename]=stats[10]
	r[:quotedfilename]=stats[11]
	r[:optimalsize]=stats[12]
	r[:size]=stats[13].to_i
	r[:uid]=stats[14].to_i
	r[:user]=stats[15]
	r[:birthtime]  = begin Time.parse(stats[16]) rescue nil end
	r[:accesstime] = begin Time.parse(stats[17]) rescue nil end
	r[:changedtime]= begin Time.parse(stats[18]) rescue nil end
	r[:statustime] = begin Time.parse(stats[19]) rescue nil end
	r
end

#stat_filesystem(file, up: true) ⇒ Object

wrap stat --file-system SH.stat_filesystem("mine") => fstype=>"btrfs"



46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
# File 'lib/shell_helpers/sysutils.rb', line 46

def stat_filesystem(file, up: true)
	if up #output the fs info of the first ascending path that exist
		# usefull to get infos of the filesystem of a file we want to create
		# but does not yet exist
		file=Pathname.new(file)
		file.ascend.each do |f|
			return stat_filesystem(f, up: false) if f.exist?
		end
	end
	opts=%w(a b c d f i l n s S T)
	stats=Run.run_simple("stat --file-system --format='#{opts.map{|o| "%#{o}\n"}.join}' #{file.shellescape}", chomp: :lines)
	#stats=stats.each_line.map {|l| l.chomp}
	r={}
	r[:userfreeblocks]=stats[0].to_i
	r[:totalblocks]=stats[1].to_i
	r[:totalnodes]=stats[2].to_i
	r[:freenodes]=stats[3].to_i
	r[:freeblocks]=stats[4].to_i
	r[:fsid]=stats[5]
	r[:maxlength]=stats[6].to_i
	r[:name]=stats[7]
	r[:blocksize]=stats[8].to_i
	r[:innerblocksize]=stats[9].to_i
	r[:fstype]=stats[10]
	r
end

#umount(paths, sort: true) ⇒ Object



278
279
280
281
282
283
284
285
286
# File 'lib/shell_helpers/sysutils.rb', line 278

def umount(paths, sort: true)
	paths=paths.values if paths.is_a?(Hash)
	paths=paths.select {|p| p[:mountpoint]}
	paths=paths.sort { |p1, p2| Pathname.new(p1[:mountpoint]) <=> Pathname.new(p2[:mountpoint]) } if sort
	paths.reverse.each do |path|
		mntpoint=path[:mountpoint]
		Sh.sh("umount #{mntpoint.shellescape}", sudo: true)
	end
end

#wipefs(disk) ⇒ Object



404
405
406
407
# File 'lib/shell_helpers/sysutils.rb', line 404

def wipefs(disk)
	# wipe all signatures
	Sh.sh("wipefs -a #{disk.shellescape}", sudo: true)
end

#zap_partitions(disk) ⇒ Object



400
401
402
403
# File 'lib/shell_helpers/sysutils.rb', line 400

def zap_partitions(disk)
	# Zap (destroy) the GPT and MBR data  structures  and  then  exit.
	Sh.sh("sgdisk --zap-all #{disk.shellescape}", sudo: true)
end