OSDN Git Service

Support for lxc host without kernel & grub.
[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
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   end
17 end
18
19
20 class ImageCreator
21   attr_accessor :name, :dirs, :src_host
22
23   def initialize(name, dirs, src_host: nil)
24     @name = name
25     @dirs = dirs
26     @src_host = src_host || name
27   end
28
29   def size
30     @size and return @size 
31     @size = 3 * (1024**2) # alignment + BIOS boot partition + gap
32     dirs.each do |di|
33       @size += di.size * (1024**3)
34     end
35     @size
36   end
37
38   def imgpath
39     @imgpath ||= "#{name}_#{Time.now.strftime '%FT%T%z'}.img"
40   end
41
42   def create_disk
43     raise "Disk image #{imgpath} is already exists!" if File.exists? imgpath
44     puts "Creating disk image #{imgpath} (#{'%5.2f' % (size.to_f/(1024**3))} GiB)..."
45     last_mb = 1
46     File.open(imgpath, "w") do |f|
47       f.truncate size
48     end
49     system("parted", "-s", imgpath, "mklabel", "gpt") or raise "Failed to create partition label"
50     system("parted", "-s", imgpath, "mkpart", "primary", "#{last_mb}MiB", "#{last_mb+1}MiB") or raise "Failed to create boot partition"
51     system("parted", "-s", imgpath, "set", "1", "bios_grub", "on") or raise "Failed to set bios boot partition"
52     last_mb += 1
53     dirs.each do |di|
54       system("parted", "-s", imgpath, "mkpart", "primary", "#{last_mb}MiB", "#{di.size * 1024 + last_mb}MiB") or raise "Failed to create partition: #{l}"
55       last_mb += di.size * 1024
56     end
57     puts "Image partition created."
58     #system "sfdisk", "-l", imgpath
59   end
60   
61   def with_loopdev &block
62     begin
63       system("kpartx", "-as", imgpath) or raise "Failed to map loop device"
64       maplines = `kpartx -l #{Shellwords.escape imgpath}`.split("\n")
65       devices = []
66       maplines.each do |map|
67         devices << "/dev/mapper/#{map[/loop\d+p\d+/]}"
68       end
69       yield devices, maplines.first[%r{/dev/loop\d+}]
70     ensure
71       system "kpartx", "-d", imgpath, err: "/dev/null"
72     end
73   end
74
75   def create_fs
76     with_loopdev do |devices, root|
77       devices[1..-1].each_with_index do |(dev, _), index|
78         puts "Creating filesystem on #{dev}..."
79         cmd = %w(mkfs.ext4 -q)
80         di = dirs[index]
81         if di.fs_features
82           cmd << '-O' << di.fs_features
83         end
84         cmd << dev
85         system(*cmd) or raise "Failed to create file system on #{dev}"
86       end
87     end
88   end
89
90   def sync_dirs
91     with_loopdev do |devices, root|
92       devices[1..-1].each_with_index do |dev, idx|
93         di = dirs[idx]
94         mount_point = "/mnt/ci-#{$$}-#{name}-#{idx}"
95         system("mkdir", "-p", mount_point)
96         begin
97           system("mount", dev, mount_point) or raise "Failed to mount file system #{dev} on #{mount_point}"
98           puts "Copying #{src_host}:#{di.srcpath} to #{dev}..."
99           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"
100         ensure
101           system("umount", mount_point)
102           File.directory?(mount_point) and
103             Dir.rmdir mount_point
104         end
105       end
106     end
107   end
108
109   def fix_boot
110     puts "Fixing boot environments..."
111     Dir.mktmpdir("ci-#{$$}-#{name}") do |dir|
112       system("cp", "-a", "/usr/lib/grub/i386-pc/boot.img", dir) or raise "Failed to copy boot.img"
113       coreimg = "#{dir}/core.img"
114       system("grub-mkimage", "-o", coreimg, "-O", "i386-pc", "-p", "(hd0,gpt2)/boot/grub", "biosdisk", "part_gpt", "ext2", "gzio", "xzio", "lzopio") or raise "Failed to create grub core image."
115       with_loopdev do |devices, root|
116         puts "Override grub with host version..."
117         system("grub-bios-setup", "-d", dir, root) or raise "Failed to run grub-bios-setup"
118         rootfs_uuid=`blkid -o value -s UUID #{devices[1]}`.chomp("\n")
119         puts "New rootfs UUID=#{rootfs_uuid}"
120         begin
121           system("mount", devices[1], dir)
122
123           puts "Rewrite fstab..."
124           fstab = File.read "#{dir}/etc/fstab"
125           fstab.gsub!(%r{^(UUID=|/)\S+(\s+/\s+)}, "UUID=#{rootfs_uuid}\\2")
126           fstab.gsub!(%r{^(\S+\s+\S+\s+\S+\s+sw(?=\b))}, '#\1')
127           File.write "#{dir}/etc/fstab", fstab
128
129           unless File.exists? "#{dir}/vmlinuz"
130             system("chroot", dir, "apt-get", "-qy", "update")
131             system("chroot", dir, "apt-get", "-y", "install", "linux-image-amd64")
132           end
133
134           puts "Update grub.cfg..."
135           system("mkdir", "-p", "#{dir}/boot/grub/i386-pc") or raise "Failed to create grub dir"
136           system("cp -a /usr/lib/grub/i386-pc/*.mod #{dir}/boot/grub/i386-pc/") or raise "Failed to copy grub modules"
137           if File.exists? "#{dir}/boot/grub/grub.cfg"
138             grubconf = File.read "#{dir}/boot/grub/grub.cfg"
139             if old_uuid = grubconf[/root=UUID=(\S+)/, 1]
140               File.write "#{dir}/boot/grub/grub.cfg", grubconf.gsub(/#{old_uuid}/, rootfs_uuid)
141             end
142           else
143             File.write "#{dir}/boot/grub/grub.cfg", <<-EOC
144               set timeout=5
145               insmod part_gpt
146               insmod ext2
147               insmod linux
148               search --no-floppy --fs-uuid --set=root #{rootfs_uuid}
149               menuentry 'Linux' {
150                 linux   /vmlinuz root=UUID=#{rootfs_uuid} ro
151                 initrd  /initrd.img
152               }
153             EOC
154           end
155         ensure
156           system("umount", dir)
157         end
158       end
159     end
160   end
161
162   def run
163     create_disk
164     create_fs
165     sync_dirs
166     fix_boot
167     puts "Image creation has been complated (#{name})"
168   end
169
170 end
171
172 if $0 == __FILE__
173   list = YAML.load_file(ARGV[0] || 'image-list.yml')
174   list.each do |imgdef|
175     name = nil
176     dirs = []
177     if imgdef.kind_of?(Hash)
178       name = imgdef['name']
179       (imgdef['dirs'] || {}).each do |path, opts|
180         dirs << SyncDirDef.new({path: path}.merge(opts.keys.map(&:to_sym).zip(opts.values).to_h))
181       end
182     else
183       name = imgdef
184     end
185     dirs.empty? and dirs << SyncDirDef.new
186     ImageCreator.new(name, dirs).run
187   end
188 end