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, :fs_uuid
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
22 attr_accessor :name, :dirs, :src_host, :img_path_base
26 def initialize(name, dirs, src_host: nil)
29 @src_host = src_host || name
30 @img_path_base = "#{name}_#{Time.now.strftime '%FT%T%z'}"
34 "#{img_path_base}_#{idx}.img"
38 dirs.each_with_index do |di, idx|
39 _create_disk imgpath(idx), di, idx != 0
43 def _create_disk path, di, use_gpt = false
45 raise "Disk image #{path} is already exists!" if File.exists? path
46 puts "Creating disk image #{path} (#{'%5.2f' % size_gb.to_f} GiB)..."
47 File.open(path, "w") do |f|
48 f.truncate(size_gb * GiB)
50 system("parted", "-s", path, "mklabel", use_gpt ? 'gpt' : 'msdos') or raise "Failed to create partition label"
51 system("parted", "-s", path, "mkpart", "primary", "1MiB", "#{size_gb * 1024 - 1}MiB") or raise "Failed to create partition"
53 system("parted", "-s", path, "set", "1", "boot", "on") or raise "Failed to set bios boot partition"
55 puts "Image partition has been created."
58 def with_loopdev &block
61 dirs.each_with_index do |di, idx|
62 system("kpartx", "-as", imgpath(idx)) or raise "Failed to map loop device"
63 di.device = "/dev/mapper/" + `kpartx -l #{Shellwords.escape imgpath(idx)}`.split("\n").first[/loop\d+p\d+/]
68 dirs.each_with_index do |di, idx|
69 system "kpartx", "-d", imgpath(idx), err: "/dev/null"
76 with_loopdev do |devices|
77 dirs.each_with_index do |di, index|
79 puts "Creating filesystem on #{dev}..."
80 cmd = %w(mkfs.ext4 -q)
83 cmd << '-O' << di.fs_features
86 system(*cmd) or raise "Failed to create file system on #{dev}"
87 system "e2label", dev, di.path == '/' ? 'ROOT' : di.path[1..-1].tr('/', '-')
88 di.fs_uuid = `blkid -o value -s UUID #{di.device}`.chomp("\n")
94 with_loopdev do |devices|
95 devices.each_with_index do |dev, idx|
97 mount_point = "/mnt/ci-#{$$}-#{name}-#{idx}"
98 system("mkdir", "-p", mount_point)
100 system("mount", dev, mount_point) or raise "Failed to mount file system #{dev} on #{mount_point}"
101 puts "Copying #{src_host}:#{di.srcpath} to #{dev}..."
102 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"
104 system("umount", mount_point)
105 File.directory?(mount_point) and
106 Dir.rmdir mount_point
113 puts "Fixing boot environments..."
114 Dir.mktmpdir("ci-#{$$}-#{name}") do |dir|
115 with_loopdev do |devices|
116 puts "Override grub with host version..."
117 root_dev = "/dev/#{devices.first[/loop\d+/]}"
118 rootfs_uuid = dirs.find { |d| d.path == '/'}.fs_uuid
119 puts "New rootfs UUID=#{rootfs_uuid}"
121 system("mount", devices.first, dir) or raise "Failed to mount #{devices.first} to #{dir}"
122 system("mount", "--bind", "/dev", "#{dir}/dev") or raise "Failed to mount /dev to #{dir}/dev"
123 system("mount", "--bind", "/proc", "#{dir}/proc") or raise "Failed to mount /proc to #{dir}/proc"
125 dirs[1..-1].each_with_index do |di, idx|
126 system "mkdir", "-p", "#{dir}#{di.path}"
127 system("mount", di.device, "#{dir}#{di.path}") or raise "Failed to mount #{di.device} to #{dir}#{path}"
130 system "rm", "-f", "#{dir}/etc/systemd/system/udev.service", "#{dir}/etc/systemd/system/systemd-udevd.service"
132 puts "Rewrite fstab..."
133 File.open "#{dir}/etc/fstab", "w" do |f|
134 dirs.each_with_index do |di, idx|
135 f << %W(UUID=#{di.fs_uuid} #{di.path} ext4 defaults,noatime 0 #{di.path == '/' ? 1 : 2}).join("\t")
140 unless File.exists? "#{dir}/vmlinuz"
141 system("chroot", dir, "apt-get", "-qy", "update")
142 system("chroot", dir, "apt-get", "-y", "install", "linux-image-amd64")
145 puts "Update grub..."
146 unless File.exists? "#{dir}/boot/grub/grub.cfg"
147 system("chroot", dir, "apt-get", "-qy", "update") or raise "Failed to install grub-pc"
148 system({'DEBIAN_FRONTEND' => 'noninteractive'}, "chroot", dir, "apt-get", "-y", "install", "grub-pc")
150 File.open "#{dir}/boot/grub/device.map", "w" do |f|
151 f.puts "(hd0)\t#{root_dev}"
153 system("chroot", dir, "grub-mkconfig", "-o", "/boot/grub/grub.cfg") or raise "grub-mkconfig fails."
154 system(*%W(grub-install --no-floppy --grub-mkdevicemap=#{dir}/boot/grub/device.map --root-directory=#{dir} #{root_dev})) or raise "grub-install failed."
156 system("umount", "#{dir}/dev")
157 system("umount", "#{dir}/proc")
158 dirs.reverse[0..-2].each do |di, idx|
159 system("umount", "#{dir}#{di.path}")
161 system("umount", dir)
172 puts "Image creation has been complated (#{name})"
178 list = YAML.load_file(ARGV[0] || 'image-list.yml')
179 list.each do |imgdef|
182 if imgdef.kind_of?(Hash)
183 name = imgdef['name']
184 (imgdef['dirs'] || {}).each do |path, opts|
185 dirs << SyncDirDef.new({path: path}.merge(opts.keys.map(&:to_sym).zip(opts.values).to_h))
190 dirs.empty? and dirs << SyncDirDef.new
191 ImageCreator.new(name, dirs).run