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 /\/, 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