381 lines
9.5 KiB
Ruby
381 lines
9.5 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 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 = Pathname.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
|
|
|
|
attr_reader :sh, :mounted, :looped, :base, :dest, :vgname
|
|
def initialize *args
|
|
OptionParser.new do |opts|
|
|
getopts opts
|
|
opts.parse! args
|
|
end
|
|
|
|
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
|
|
|
|
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[dmsetup lvcreate vgcreate pvcreate vgchange mkswap vgscan]
|
|
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 -ms]
|
|
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 -aHAX]
|
|
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 = Pathname.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 = Pathname.new f
|
|
d "#{f} not found.", f.exist?
|
|
@root_authorized_keys = f
|
|
end
|
|
end
|
|
|
|
def run
|
|
build
|
|
|
|
qemu_bin = dest.root + "usr/bin/qemu-arm-static"
|
|
if qemu_bin.exist?
|
|
msg :remove, "/usr/bin/qemu-arm-static"
|
|
qemu_bin.unlink
|
|
end
|
|
|
|
rescue ProgrammError
|
|
err $!
|
|
raise
|
|
ensure
|
|
STDERR.puts "="*80
|
|
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 "="*80
|
|
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
|
|
akf = home+'.ssh/authorized_keys'
|
|
akd = akf.dirname
|
|
akd.mkdir
|
|
akd.chown uid, gid
|
|
akd.chmod 0700
|
|
akf.copy src
|
|
akf.chown uid, gid
|
|
akf.chmod 0600
|
|
end
|
|
|
|
def rename_user
|
|
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
|
|
|
|
if @username
|
|
dest.root.join( 'etc/passwd').replace_i do |f|
|
|
f.each_line.flat_map do |l|
|
|
user, pwd, uid, gid, name, home, shell = l.split( ':')
|
|
user, home = @username, "/home/#{@username}" if 'pi' == user
|
|
[user, pwd, uid, gid, name, home, shell].join ':'
|
|
end
|
|
end
|
|
|
|
dest.root.join( 'etc/group').replace_i do |f|
|
|
f.each_line.flat_map do |l|
|
|
group, pwd, gid, users = l.split( ':')
|
|
group = @username if 'pi' == group
|
|
users = users.split( ',').map {|user| 'pi' == user ? @username : user }
|
|
[group, pwd, gid, users.join( ',')].join ':'
|
|
end
|
|
end
|
|
|
|
dest.root.join( 'home/pi').rename dest.root.join( 'home', @username)
|
|
end
|
|
|
|
if @password or @username
|
|
dest.root.join( 'etc/shadow').replace_i do |f|
|
|
f.each_line.flat_map do |l|
|
|
user, crypt, lastchange, minage, maxage, warn, inact, expiry, reserved = l.split( ':')
|
|
if 'pi' == user
|
|
crypt = @password if @password
|
|
user = @username if @username
|
|
end
|
|
[user, crypt, lastchange, minage, maxage, warn, inact, expiry, reserved].join ':'
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
def set_hostname hostname = nil
|
|
hostname = @hostname
|
|
return if hostname.nil? or hostname.empty?
|
|
msg :patch, 'etc/hosts', 'set hostname to', hostname
|
|
dest.root.join( 'etc/hosts').replace_i do |f|
|
|
f.each_line.map {|l| l.gsub /\<raspberrypi\>/, hostname }
|
|
end
|
|
msg :write, hostname, :to, 'etc/hostname'
|
|
dest.root.join( 'etc/hostname').open 'w' do |f|
|
|
f.puts hostname
|
|
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 = Pathname.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
|