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 class Base 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 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_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 = [], [] @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 <"*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 /\/, 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