tolvmxfs/lib/to_lvm_xfs/base.rb

463 lines
12 KiB
Ruby

require 'pathname'
require 'shellwords'
require 'optparse'
require 'securerandom'
require 'to_lvm_xfs/exts'
require 'to_lvm_xfs/sh'
require 'to_lvm_xfs/structs'
class ProgrammError < RuntimeError
end
class Pathname
def self.which cmd
ENV['PATH'].split(':').each do |path|
path = Pathname.new( path) + cmd
return path if path.executable?
end
nil
end
end
module OutputHelper
def d msg, eq
raise ProgrammError, msg, caller[1..-1] unless eq
end
def err *args
STDERR.puts "\e[31;1m#{args.join ' '}\e[0m"
end
def msg first, *args
STDERR.puts "\e[33m#{first}\e[35m #{args.join ' '}\e[0m"
end
def task name, &exe
STDERR.puts "\e[30;1m***\e[0m #{name} \e[30;1m#{'*' * [0,80-name.length].max}\e[0m"
if yield
STDERR.puts "\e[32;1m---\e[0m #{name} \e[32;1m#{'-' * [0,80-name.length].max}\e[0m\n"
else
STDERR.puts "\e[35;1m---\e[0m #{name} \e[35;1m#{'-' * [0,80-name.length].max}\e[0m\n"
end
rescue Object
STDERR.puts "\n\e[31;1m|||\e[0m #{name} \e[31;1m#{'|' * [0,80-name.length].max}\e[0m\n"
raise
end
end
class Base
include OutputHelper
def self.run *args, &exe
new( *args).instance_eval &exe
end
def kpartx image, read_only: nil
sh.kpartx "-sa#{read_only ? :r : ''}", image
lpartx image
end
def lpartx image
mapper = XPathname.new '/dev/mapper'
lines = sh.kpartx( -:al, image, return: :lines)[0..-1]
if lines.grep( /^loop deleted/).empty?
lines.map do |line|
d "cannot extract partition «#{line}»", line =~ /^([^ :]+) : /
mapper + $1
end
else
[]
end
end
def lsblk path, *output
output = %w[NAME TYPE MOUNTPOINT] if output.empty?
output = output.map {|o| o.to_s }
lines =
sh.lsblk %w[--output], output.join(','), path
op = output.map {|o| o.downcase.to_sym }
lines.map {|l| Hash[*op.zip(l.split( /\s+/)).flatten]}
end
def departx image
sh.kpartx -:sd, image
end
def kpartxed? image
lines = sh.kpartx -:l, image, return: :lines
not lines.grep( /^loop deleted/).empty?
end
def get_vgname_of_pv *a, &exe
return to_enum( __method__, *a) unless block_given?
sh.pvs( --:options, :vg_name, *a, return: :lines).each do |l|
yield l.chomp.sub( /^\s*/, '')
end
end
def remove_all_partitions_from image
sh[].parted( -:ms, image, :print).each do
sh.parted image, :rm, 1
end
end
def manipulate_file file, &exe
flags = 'r+'
msg :manipulate, file
File.open file.to_s, flags do |file|
lines = []
exe.call file, &lines.method( :push)
file.truncate 0
file.pos = 0
lines.each &file.method( :puts)
end
end
def capsulated_rescue
yield
rescue Object => e
STDERR.puts "\e[0;1mTraceback\e[0m (most recent call last):"
e.backtrace[1..-1].
each_with_index.
reverse_each {|s,i| STDERR.puts "\t#{i}: #{s}" }
STDERR.puts "#{e.backtrace}: \e[31;1m#{e} (#{e.class})\e[0m"
end
def check_programm cmd
path = Pathname.which cmd
raise ProgrammError, "#{cmd} not found." unless path&.executable?
path
end
attr_reader :sh, :mounted, :looped, :base, :dest, :vgname
def initialize *args
OptionParser.new do |opts|
getopts opts
opts.parse! args
end
%w[kpartx parted rsync lvm lvs pvs vgs mkfs.xfs mkfs.vfat dmsetup losetup lsblk blkid].each do |cmd|
check_programm cmd
end
@qemu_bin_src = check_programm 'qemu-arm-static'
raise ProgrammError, "qemu-arm-static not found. Please install qemu-user-static" unless @qemu_bin_src&.executable?
if :ask == @password
v = b = nil
require 'io/console'
STDERR.print "Type Password. "
v = STDIN.getpass
STDERR.print "Type password again. "
b = STDIN.getpass
d "Password missmatch", v == b
d "Password is empty", !v.empty?
@password = v.crypt "$6$#{SecureRandom.urlsafe_base64 6}$"
end
@sh = Sh.new immediately: true, expect_status: 0
#x = sh.system :echo, :hallo, :welt, as_io: true do |io|
# STDERR.puts io.inspect
# STDERR.puts '---',io.readlines,'---'
#end
#STDERR.puts x.inspect
#exit 1
@mounted, @activated_vgs = Hash.new.tap {|h| h.default = 0 }, []
@looped = Hash.new {|h,k| h[k] = [] }
@base = Image.new self, 'base', image: @baseimage
@dest = Image.new self, 'dest', image: @destination
@qemu_bin = dest.root.join 'usr/bin/qemu-arm-static'
STDERR.print <<-EOF.gsub( /^\s+#/, '')
#Settings:
# username: #{@username || '(default)'}
# password: #{@password ? '*********' : '(default)'}
# baseimage: #{@base.image}
# destination: #{@dest.image}
# vgname: #{@vgname}
# hostname: #{@hostname || '(default)'}
EOF
sh.def_system_commands *%i[echo sed mount umount partx kpartx sync rsync xz gzip bzip2 zip tar bash dpkg apt]
sh.def_system_commands *%i[losetup dmsetup lvcreate vgcreate pvcreate vgchange mkswap vgscan]
sh.alias_command :losetup_list, *%w[losetup --list --json], return: :json, could_be_empty: true
sh.alias_command :pvs, *%w[pvs --noheadings]
sh.alias_command :blkid, 'blkid', return: :line
sh.alias_command :lsblk, *%w[lsblk --noheadings --paths --list], return: :lines
sh.alias_command :parted, *%w[parted --machine --script]
sh.alias_command :mkxfs, *%w[mkfs.xfs -f]
sh.alias_command :mkext2fs, *%w[mkfs.ext2]
sh.alias_command :mkext4fs, *%w[mkfs.ext4]
sh.alias_command :mkvfat, *%w[mkfs.vfat]
sh.alias_command :rsync_all, *%w[rsync --archive --hard-links --acls --xattrs --sparse]
sh.alias_command :mount_ro, *%w[mount -oro]
sh.alias_command :fs_uuid, *%w[blkid -ovalue -sUUID], return: :line
sh.alias_command :fs_type, *%w[blkid -ovalue -sTYPE], return: :line
end
def getopts opts
opts.on '-h', '--help' do
STDERR.puts opts
exit 1
end
opts.on '-bIMAGE', '--baseimage=IMAGE', 'Read image from IMAGE. Device or file' do |v|
@baseimage = v
end
opts.on '-dIMAGE', '--destination=IMAGE', 'Write image to IMAGE. Device or file' do |v|
@destination = v
end
opts.on '-uUSER', '--user=USER', 'Change username of user pi to this USERname' do |v|
@username = v
end
opts.on '-W', '--ask-pass', 'Ask for password for user via CLI.' do
@password = :ask
end
opts.on '-wPASSWORD', '--password=PASSWORD', 'Set password for user' do |v|
@password = v.crypt "$6$#{SecureRandom.urlsafe_base64 6}$"
end
opts.on '-nVGNAME', '--vgname=VGNAME', 'Name for volume group' do |v|
@vgname = v
end
opts.on '-HNAME', '--hostname=NAME', 'Set hostname to NAME' do |v|
@hostname = v
end
opts.on '-aAUTHKEYS', '--auth-keys=AUTHKEYS', 'SSH-Keys for user' do |f|
f = XPathname.new f
d "#{f} not found.", f.exist?
@authorized_keys = f
end
opts.on '-AAUTHKEYS', '--root-auth-keys=AUTHKEYS', 'SSH-Keys for root' do |f|
f = XPathname.new f
d "#{f} not found.", f.exist?
@root_authorized_keys = f
end
end
def cleanup &exe
@cleanup ||= []
@cleanup.unshift exe
end
def after &exe
@after ||= []
@after.unshift exe
end
def run
@after ||= []
@cleanup ||= []
nok = false
build
task "After run" do
@after.each {|exe| exe.call }
if @qemu_bin.exist?
#msg :remove, "/usr/bin/qemu-arm-static"
@qemu_bin.unlink
end
end
rescue ProgrammError
nok = true
err $!
raise
ensure
STDERR.puts "\e[1;36m#{"<"*80}\e[0m"
@cleanup.each {|exe| exe.call }
umount_all ignore_exceptions: true
umount dest.root, -:R rescue Object
umount base.root, -:R rescue Object
sh.vgchange -:an, vgname rescue Object
sh.sync rescue Object
departx dest.image rescue Object
departx base.image rescue Object
sh.sync rescue Object
STDERR.puts "\e[1;#{nok ? 31: 36}m#{">"*80}\e[0m"
end
def activate_vg vgname
return nil if @activated_vgs.include? vgname
@activated_vgs.unshift vgname
sh.vgchange -:ae, vgname
end
def deactivate_vg vgname
return nil unless @activated_vgs.include? vgname
sh.vgchange -:an, vgname
@activated_vgs.delete vgname
end
def deactivate_all_vgs ignore_exceptions: nil
until @activated_vgs.empty?
if ignore_exceptions
capsulated_rescue { sh.vgchange -:an, @activated_vgs.pop }
else
sh.umount @activated_vgs.pop
end
end
end
def mount dev, mp, *opts, &exe
@mounted[mp] += 1
sh.mount *opts, dev, mp
if block_given?
begin
yield
ensure
umount mp
end
end
end
def umount mp, *opts
if 0 < @mounted[mp]
@mounted[mp] -= 1
if 0 == @mounted[mp]
r = sh.umount *opts, mp
@mounted.delete mp
r
end
end
end
def umount_all ignore_exceptions: nil
until @mounted.empty?
m = @mounted.keys.first
if ignore_exceptions
capsulated_rescue { sh.umount m }
else
sh.umount m
end
@mounted.delete m
end
end
def mkmppaths
[base, dest].each do |mp|
mp.dir.mkpath
mp.root.mkpath
end
end
def ssh_copy_id_local src, home, uid, gid = nil
akfile = home+'.ssh/authorized_keys'
akdir = akfile.dirname
akdir. mkdir
akdir. chown uid, gid
akdir. chmod 0700
akfile.copy src
akfile.chown uid, gid
akfile.chmod 0600
end
def install_authorized_keys
ssh_copy_id_local @authorized_keys, dest.root+'home/pi', 1000, 1000 if @authorized_keys
ssh_copy_id_local @root_authorized_keys, dest.root+'root', 0, 0 if @root_authorized_keys
end
def rename_user username = nil, password: nil, old: nil
username ||= @username
old ||= 'pi'
password ||= @password
if username and old != username
dest.root.join( 'etc/passwd').sed_i do |l|
user, pwd, uid, gid, name, home, shell = l.split( ':', 7)
user, home = username, "/home/#{username}" if old == user
[user, pwd, uid, gid, name, home, shell].join ':'
end
dest.root.join( 'etc/group').sed_i do |l|
group, pwd, gid, users = l.split( ':', 4)
group = username if old == group
users = users.split( ',').map {|user| old == user ? username : user }
[group, pwd, gid, users.join( ',')].join ':'
end
dest.root.join( 'home', old).rename dest.root.join( 'home', username)
end
if password or username
dest.root.join( 'etc/shadow').sed_i do |l|
user, crypt, lastchange, minage, maxage, warn, inact, expiry, reserved = l.split( ':', 9)
if old == user
crypt = password if password
user = username if username
end
[user, crypt, lastchange, minage, maxage, warn, inact, expiry, reserved].join ':'
end
end
end
def set_hostname hostname = nil
hostname ||= @hostname
return if hostname.nil? or hostname.empty?
msg "setting hostname", hostname
dest.root.join( 'etc/hosts').sed_i {|l| l.gsub /\<raspberrypi\>/, hostname }
dest.root.join( 'etc/hostname').write "#{hostname}\n"
end
FSTabMountEntry =
Struct.new :spec, :file, :type, :ops, :freq, :passno do
def to_a() [spec, file, type, ops, freq||0, passno||0] end
def to_s() to_a.join ' ' end
end
def update_fstab content
cnt = {}
content = content.each do |ent|
cnt[ent.file.to_s] = ent.to_s
end
dest.root.join( 'etc/fstab').replace_i do |f|
f.each_line.flat_map do |l|
mp = l.split( /\s+/)[1]
cnt.delete( mp) || l
end + cnt.values
end
end
def install_packages_from_dir *paths
paths.each do |pkgs|
next unless pkgs.directory?
pkgs.each_child do |pn|
next if /\A\./ =~ pn.basename.to_s # no dot-files.
install_package pn
end
end
end
def install_package path
case path.basename.to_s
when /\.deb\z/
install_deb path
when /\.tar\z/, /\.t(?:ar\.|)(?:xz|bzip2|gz|lzma|Z)\z/
sh.tar -:C, dest.root, -:xf, path
else
warn :ignore, path
end
end
def install_deb path
#sh.dpkg '--unpack', '--force-architecture', pn, chroot: dest.root
adeb = XPathname.new( 'var/cache/apt/archives')+path.basename.to_s
dest.root.join( adeb.to_s).copy path
sh.dpkg -:i, adeb, chroot: dest.root
#msg :unpack, path
# sometimes, you have to use system to do something. but it is ok, if you know, how to do it.
# this command should be safe.
#d "unpacking failed: #{path}",
# system( "ar p #{path.to_s.shellescape} data.tar.xz | tar -JxC #{dest.root.to_s.shellescape}")
end
end