463 lines
12 KiB
Ruby
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
|