8 DEFAULT_EXCLUDE = %w[/proc/* /sys/* /dev/mqueue /dev/hugepages /run/* /var/lib/os-prober/mount /swap /dev/shm/* /var/lib/lxcfs/*]
9 attr_accessor :path, :size, :exclude, :size, :srcpath, :fs_features, :device
10 def initialize(path: '/', size: 8, exclude: DEFAULT_EXCLUDE, srcpath: nil, fs_features: nil)
13 @exclude = (DEFAULT_EXCLUDE + [*exclude]).uniq
14 @srcpath = srcpath || path
15 @fs_features = fs_features
21 attr_accessor :name, :dirs, :src_host, :img_path_base
25 def initialize(name, dirs, src_host: nil)
28 @src_host = src_host || name
29 @img_path_base = "#{name}_#{Time.now.strftime '%FT%T%z'}"
33 "#{img_path_base}_#{idx}.img"
37 dirs.each_with_index do |di, idx|
38 _create_disk imgpath(idx), di, idx != 0
42 def _create_disk path, di, use_gpt = false
44 raise "Disk image #{path} is already exists!" if File.exists? path
45 puts "Creating disk image #{path} (#{'%5.2f' % size_gb.to_f} GiB)..."
46 File.open(path, "w") do |f|
47 f.truncate(size_gb * GiB)
49 system("parted", "-s", path, "mklabel", use_gpt ? 'gpt' : 'msdos') or raise "Failed to create partition label"
50 system("parted", "-s", path, "mkpart", "primary", "1MiB", "#{size_gb * 1024 - 1}MiB") or raise "Failed to create partition"
52 system("parted", "-s", path, "name", "1", di.path) or raise "Failed to set part label"
54 system("parted", "-s", path, "set", "1", "boot", "on") or raise "Failed to set bios boot partition"
56 puts "Image partition created."
57 #system "sfdisk", "-l", path
60 def with_loopdev &block
63 dirs.each_with_index do |di, idx|
64 system("kpartx", "-as", imgpath(idx)) or raise "Failed to map loop device"
65 di.device = "/dev/mapper/" + `kpartx -l #{Shellwords.escape imgpath(idx)}`.split("\n").first[/loop\d+p\d+/]
70 dirs.each_with_index do |di, idx|
71 system "kpartx", "-d", imgpath(idx), err: "/dev/null"
78 with_loopdev do |devices|
79 devices.each_with_index do |dev, index|
80 puts "Creating filesystem on #{dev}..."
81 cmd = %w(mkfs.ext4 -q)
84 cmd << '-O' << di.fs_features
87 system(*cmd) or raise "Failed to create file system on #{dev}"
93 with_loopdev do |devices|
94 devices.each_with_index do |dev, idx|
96 mount_point = "/mnt/ci-#{$$}-#{name}-#{idx}"
97 system("mkdir", "-p", mount_point)
99 system("mount", dev, mount_point) or raise "Failed to mount file system #{dev} on #{mount_point}"
100 puts "Copying #{src_host}:#{di.srcpath} to #{dev}..."
101 system("rsync", "-azHSAX", "--numeric-ids", "--info=progress2", "#{src_host}:#{di.srcpath}/", "#{mount_point}/", *((["--exclude"] * di.exclude.size).zip(di.exclude).flatten)) or raise "rsync fails"
103 system("umount", mount_point)
104 File.directory?(mount_point) and
105 Dir.rmdir mount_point
112 puts "Fixing boot environments..."
113 Dir.mktmpdir("ci-#{$$}-#{name}") do |dir|
114 system("cp", "-a", "/usr/lib/grub/i386-pc/boot.img", dir) or raise "Failed to copy boot.img"
115 coreimg = "#{dir}/core.img"
116 system("grub-mkimage", "-o", coreimg, "-O", "i386-pc", "-p", "(hd0,msdos1)/boot/grub", "biosdisk", "part_msdos", "ext2", "gzio", "xzio", "lzopio") or raise "Failed to create grub core image."
117 with_loopdev do |devices|
118 root_dev = "/dev/#{devices.first[/loop\d+/]}"
119 puts "Override grub with host version..."
120 system("grub-bios-setup", "-d", dir, root_dev) or raise "Failed to run grub-bios-setup"
121 fs_uuids = devices.map { |d| `blkid -o value -s UUID #{d}`.chomp("\n") }
122 rootfs_uuid = fs_uuids.first
123 puts "New rootfs UUID=#{rootfs_uuid}"
125 system("mount", devices.first, dir) or raise "Failed to mount #{devices.first} to #{dir}"
126 dirs[1..-1].each_with_index do |di, idx|
127 system "mkdir", "-p", "#{dir}#{di.path}"
128 system("mount", di.device, "#{dir}#{di.path}") or raise "Failed to mount #{di.device} to #{dir}#{path}"
131 puts "Rewrite fstab..."
132 File.open "#{dir}/etc/fstab", "w" do |f|
133 devices.map.with_index { |d, idx|
134 f << %W(UUID=#{fs_uuids[idx]} #{dirs[idx].path} ext4 defaults,noatime 0 #{dirs[idx].path == '/' ? 1 : 2}).join("\t")
139 unless File.exists? "#{dir}/vmlinuz"
140 system("chroot", dir, "apt-get", "-qy", "update")
141 system("chroot", dir, "apt-get", "-y", "install", "linux-image-amd64")
144 puts "Update grub.cfg..."
145 system("mkdir", "-p", "#{dir}/boot/grub/i386-pc") or raise "Failed to create grub dir"
146 system("cp -a /usr/lib/grub/i386-pc/*.mod #{dir}/boot/grub/i386-pc/") or raise "Failed to copy grub modules"
147 if File.exists? "#{dir}/boot/grub/grub.cfg"
148 grubconf = File.read "#{dir}/boot/grub/grub.cfg"
149 if old_uuid = grubconf[/root=UUID=(\S+)/, 1]
150 File.write "#{dir}/boot/grub/grub.cfg", grubconf.gsub(/#{old_uuid}/, rootfs_uuid)
153 system("chroot", dir, "apt-get", "-qy", "update")
154 system({'DEBIAN_FRONTEND' => 'noninteractive'}, "chroot", dir, "apt-get", "-y", "install", "grub-pc")
155 File.write "#{dir}/boot/grub/grub.cfg", <<-EOC
160 search --no-floppy --fs-uuid --set=root #{rootfs_uuid}
162 linux /vmlinuz root=UUID=#{rootfs_uuid} ro
168 dirs.reverse[0..-2].each do |di, idx|
169 system("umount", "#{dir}#{di.path}")
171 system("umount", dir)
182 puts "Image creation has been complated (#{name})"
188 list = YAML.load_file(ARGV[0] || 'image-list.yml')
189 list.each do |imgdef|
192 if imgdef.kind_of?(Hash)
193 name = imgdef['name']
194 (imgdef['dirs'] || {}).each do |path, opts|
195 dirs << SyncDirDef.new({path: path}.merge(opts.keys.map(&:to_sym).zip(opts.values).to_h))
200 dirs.empty? and dirs << SyncDirDef.new
201 ImageCreator.new(name, dirs).run