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

Returns:



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