OSDN Git Service

Change grub install method, run gru-install.
[osdn-codes/image-creator.git] / create-image
1 #!/usr/bin/ruby
2 require 'pp'
3 require 'shellwords'
4 require 'tmpdir'
5 require 'yaml'
6
7 class SyncDirDef
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)
11     @path = path
12     @size = size.to_f
13     @exclude = (DEFAULT_EXCLUDE + [*exclude]).uniq
14     @srcpath = srcpath || path
15     @fs_features = fs_features
16     @device = nil
17     @fs_uuid = nil
18   end
19 end
20
21 class ImageCreator
22   attr_accessor :name, :dirs, :src_host, :img_path_base
23   MiB = 1024 ** 2
24   GiB = 1024 ** 3
25
26   def initialize(name, dirs, src_host: nil)
27     @name = name
28     @dirs = dirs
29     @src_host = src_host || name
30     @img_path_base = "#{name}_#{Time.now.strftime '%FT%T%z'}"
31   end
32
33   def imgpath(idx)
34     "#{img_path_base}_#{idx}.img"
35   end
36
37   def create_disk
38     dirs.each_with_index do |di, idx|
39       _create_disk imgpath(idx), di, idx != 0
40     end
41   end
42
43   def _create_disk path, di, use_gpt = false
44     size_gb = di.size
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)
49     end
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"
52     if !use_gpt
53       system("parted", "-s", path, "set", "1", "boot", "on") or raise "Failed to set bios boot partition"
54     end
55     puts "Image partition has been created."
56   end
57   
58   def with_loopdev &block
59     begin
60       devices = []
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+/]
64         devices << di.device
65       end
66       yield devices
67     ensure
68       dirs.each_with_index do |di, idx|
69         system "kpartx", "-d", imgpath(idx), err: "/dev/null"
70         di.device = nil
71       end
72     end
73   end
74
75   def create_fs
76     with_loopdev do |devices|
77       dirs.each_with_index do |di, index|
78         dev = di.device
79         puts "Creating filesystem on #{dev}..."
80         cmd = %w(mkfs.ext4 -q)
81         di = dirs[index]
82         if di.fs_features
83           cmd << '-O' << di.fs_features
84         end
85         cmd << dev
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")
89       end
90     end
91   end
92
93   def sync_dirs
94     with_loopdev do |devices|
95       devices.each_with_index do |dev, idx|
96         di = dirs[idx]
97         mount_point = "/mnt/ci-#{$$}-#{name}-#{idx}"
98         system("mkdir", "-p", mount_point)
99         begin
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"
103         ensure
104           system("umount", mount_point)
105           File.directory?(mount_point) and
106             Dir.rmdir mount_point
107         end
108       end
109     end
110   end
111
112   def fix_boot
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}"
120         begin
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"
124
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}"
128           end
129
130           system "rm", "-f", "#{dir}/etc/systemd/system/udev.service", "#{dir}/etc/systemd/system/systemd-udevd.service"
131
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")
136               f << "\n"
137             end
138           end
139
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")
143           end
144
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")
149           end
150           File.open "#{dir}/boot/grub/device.map", "w" do |f|
151             f.puts "(hd0)\t#{root_dev}"
152           end
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."
155         ensure
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}")
160           end
161           system("umount", dir)
162         end
163       end
164     end
165   end
166
167   def run
168     create_disk
169     create_fs
170     sync_dirs
171     fix_boot
172     puts "Image creation has been complated (#{name})"
173   end
174
175 end
176
177 if $0 == __FILE__
178   list = YAML.load_file(ARGV[0] || 'image-list.yml')
179   list.each do |imgdef|
180     name = nil
181     dirs = []
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))
186       end
187     else
188       name = imgdef
189     end
190     dirs.empty? and dirs << SyncDirDef.new
191     ImageCreator.new(name, dirs).run
192   end
193 end