OSDN Git Service

Change to use mbr for boot disk, split dirs for each disk.
[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
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   end
18 end
19
20 class ImageCreator
21   attr_accessor :name, :dirs, :src_host, :img_path_base
22   MiB = 1024 ** 2
23   GiB = 1024 ** 3
24
25   def initialize(name, dirs, src_host: nil)
26     @name = name
27     @dirs = dirs
28     @src_host = src_host || name
29     @img_path_base = "#{name}_#{Time.now.strftime '%FT%T%z'}"
30   end
31
32   def imgpath(idx)
33     "#{img_path_base}_#{idx}.img"
34   end
35
36   def create_disk
37     dirs.each_with_index do |di, idx|
38       _create_disk imgpath(idx), di, idx != 0
39     end
40   end
41
42   def _create_disk path, di, use_gpt = false
43     size_gb = di.size
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)
48     end
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"
51     if use_gpt
52       system("parted", "-s", path, "name", "1", di.path) or raise "Failed to set part label"
53     else
54       system("parted", "-s", path, "set", "1", "boot", "on") or raise "Failed to set bios boot partition"
55     end
56     puts "Image partition created."
57     #system "sfdisk", "-l", path
58   end
59   
60   def with_loopdev &block
61     begin
62       devices = []
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+/]
66         devices << di.device
67       end
68       yield devices
69     ensure
70       dirs.each_with_index do |di, idx|
71         system "kpartx", "-d", imgpath(idx), err: "/dev/null"
72         di.device = nil
73       end
74     end
75   end
76
77   def create_fs
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)
82         di = dirs[index]
83         if di.fs_features
84           cmd << '-O' << di.fs_features
85         end
86         cmd << dev
87         system(*cmd) or raise "Failed to create file system on #{dev}"
88       end
89     end
90   end
91
92   def sync_dirs
93     with_loopdev do |devices|
94       devices.each_with_index do |dev, idx|
95         di = dirs[idx]
96         mount_point = "/mnt/ci-#{$$}-#{name}-#{idx}"
97         system("mkdir", "-p", mount_point)
98         begin
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"
102         ensure
103           system("umount", mount_point)
104           File.directory?(mount_point) and
105             Dir.rmdir mount_point
106         end
107       end
108     end
109   end
110
111   def fix_boot
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}"
124         begin
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}"
129           end
130
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") 
135               f << "\n"
136             }
137           end
138
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")
142           end
143
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)
151             end
152           else
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
156               set timeout=5
157               insmod part_msdos
158               insmod ext2
159               insmod linux
160               search --no-floppy --fs-uuid --set=root #{rootfs_uuid}
161               menuentry 'Linux' {
162                 linux   /vmlinuz root=UUID=#{rootfs_uuid} ro
163                 initrd  /initrd.img
164               }
165             EOC
166           end
167         ensure
168           dirs.reverse[0..-2].each do |di, idx|
169             system("umount", "#{dir}#{di.path}")
170           end
171           system("umount", dir)
172         end
173       end
174     end
175   end
176
177   def run
178     create_disk
179     create_fs
180     sync_dirs
181     fix_boot
182     puts "Image creation has been complated (#{name})"
183   end
184
185 end
186
187 if $0 == __FILE__
188   list = YAML.load_file(ARGV[0] || 'image-list.yml')
189   list.each do |imgdef|
190     name = nil
191     dirs = []
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))
196       end
197     else
198       name = imgdef
199     end
200     dirs.empty? and dirs << SyncDirDef.new
201     ImageCreator.new(name, dirs).run
202   end
203 end