tolvmxfs/lib/to_lvm_xfs/base.rb

400 lines
10 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'].each do |path|
path = Pathname.new( path) + cmd
return path if path.executable?
end
end
end
class Base
def self.run *args, &exe
new( *args).instance_eval &exe
end
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 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[31;1m#{e} (#{e.class}):"
e.backtrace.each {|s| STDERR.puts " #{s}" }
STDERR.print "\e[0m"
end
def check_program cmd
path = Pathname.which cmd
raise Error, "#{cmd} not found." unless path&.executable?
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_program cmd
end
@qemu_bin_src = check_program 'qemu-arm-static'
raise Error, "@qemu-arm-static not found. Please install qemu-user-static" unless @qemu_bin_srci&.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 = [], []
@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
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 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]
sh.alias_command :mount_ro, *%w[mount -oro]
sh.alias_command :fs_uuid, *%w[blkid -o value -s UUID], return: :line
sh.alias_command :fs_type, *%w[blkid -o value -s TYPE], return: :line
end
def getopts opts
opts.on '-h', '--help' do
STDERR.puts opts
exit 1
end
opts.on '-bIMAGE', '--baseimage=IMAGE', 'Write image to 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 run
nok = false
build
if @qemu_bin.exist?
msg :remove, "/usr/bin/qemu-arm-static"
@qemu_bin.unlink
end
rescue ProgrammError
nok = true
err $!
raise
ensure
STDERR.puts "\e[1;36m#{"<"*80}\e[0m"
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 from, to, *opts, &exe
@mounted.push to
sh.mount *opts, from, to
if block_given?
begin
yield
ensure
umount to
end
end
end
def umount mp, *opts
r = sh.umount *opts, mp
@mounted.delete to
r
end
def umount_all ignore_exceptions: nil
until @mounted.empty?
if ignore_exceptions
capsulated_rescue { sh.umount @mounted.pop }
else
sh.umount @mounted.pop
end
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
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