OSDN Git Service

Use environment variable for json output.
[osdn-codes/image-creator.git] / create-image
1 #!/usr/bin/ruby
2 require 'pp'
3 require 'shellwords'
4 require 'tmpdir'
5 require 'yaml'
6 require 'json'
7
8 class SyncDirDef
9   DEFAULT_EXCLUDE = %w[/proc/* /sys/* /dev/mqueue /dev/hugepages /run/* /var/lib/os-prober/mount /swap /dev/shm/* /var/lib/lxcfs/*]
10   attr_accessor :path, :size, :exclude, :size, :srcpath, :fs_features, :device, :fs_uuid
11   def initialize(path: '/', size: 8, exclude: DEFAULT_EXCLUDE, srcpath: nil, fs_features: nil)
12     @path = path
13     @size = size.to_f
14     @exclude = (DEFAULT_EXCLUDE + [*exclude]).uniq
15     @srcpath = srcpath || path
16     @fs_features = fs_features
17     @device = nil
18     @fs_uuid = nil
19   end
20 end
21
22 class ImageCreator
23   attr_accessor :name, :dirs, :src_host, :img_path_base
24   MiB = 1024 ** 2
25   GiB = 1024 ** 3
26
27   def initialize(name, dirs, src_host: nil)
28     @name = name
29     @dirs = dirs
30     @src_host = src_host || name
31     @img_path_base = "#{name}_#{Time.now.strftime '%FT%T%z'}"
32   end
33
34   def imgpath(idx)
35     "#{img_path_base}_#{idx}.img"
36   end
37
38   def create_disk
39     dirs.each_with_index do |di, idx|
40       _create_disk imgpath(idx), di, idx != 0
41     end
42   end
43
44   def _create_disk path, di, use_gpt = false
45     size_gb = di.size
46     raise "Disk image #{path} is already exists!" if File.exists? path
47     puts "Creating disk image #{path} (#{'%5.2f' % size_gb.to_f} GiB)..."
48     File.open(path, "w") do |f|
49       f.truncate(size_gb * GiB)
50     end
51     system("parted", "-s", path, "mklabel", use_gpt ? 'gpt' : 'msdos') or raise "Failed to create partition label"
52     system("parted", "-s", path, "mkpart", "primary", "1MiB", "#{size_gb * 1024 - 1}MiB") or raise "Failed to create partition"
53     if !use_gpt
54       system("parted", "-s", path, "set", "1", "boot", "on") or raise "Failed to set bios boot partition"
55     end
56     puts "Image partition has been created."
57   end
58   
59   def with_loopdev &block
60     begin
61       devices = []
62       dirs.each_with_index do |di, idx|
63         system("kpartx", "-as", imgpath(idx)) or raise "Failed to map loop device"
64         di.device = "/dev/mapper/" + `kpartx -l #{Shellwords.escape imgpath(idx)}`.split("\n").first[/loop\d+p\d+/]
65         devices << di.device
66       end
67       yield devices
68     ensure
69       dirs.each_with_index do |di, idx|
70         system "kpartx", "-d", imgpath(idx), err: "/dev/null"
71         di.device = nil
72       end
73     end
74   end
75
76   def create_fs
77     with_loopdev do |devices|
78       dirs.each_with_index do |di, index|
79         dev = di.device
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         system "e2label", dev, di.path == '/' ? 'ROOT' : di.path[1..-1].tr('/', '-')
89         di.fs_uuid = `blkid -o value -s UUID #{di.device}`.chomp("\n")
90       end
91     end
92   end
93
94   def sync_dirs
95     with_loopdev do |devices|
96       devices.each_with_index do |dev, idx|
97         di = dirs[idx]
98         mount_point = "/mnt/ci-#{$$}-#{name}-#{idx}"
99         system("mkdir", "-p", mount_point)
100         begin
101           system("mount", dev, mount_point) or raise "Failed to mount file system #{dev} on #{mount_point}"
102           puts "Copying #{src_host}:#{di.srcpath} to #{dev}..."
103           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         ensure
105           system("umount", mount_point)
106           File.directory?(mount_point) and
107             Dir.rmdir mount_point
108         end
109       end
110     end
111   end
112
113   def fix_boot
114     puts "Fixing boot environments..."
115     Dir.mktmpdir("ci-#{$$}-#{name}") do |dir|
116       with_loopdev do |devices|
117         puts "Override grub with host version..."
118         root_dev = "/dev/#{devices.first[/loop\d+/]}"
119         rootfs_uuid = dirs.find { |d| d.path == '/'}.fs_uuid
120         puts "New rootfs UUID=#{rootfs_uuid}"
121         begin
122           system("mount", devices.first, dir) or raise "Failed to mount #{devices.first} to #{dir}"
123           system("mount", "--bind", "/dev", "#{dir}/dev") or raise "Failed to mount /dev to #{dir}/dev"
124           system("mount", "--bind", "/proc", "#{dir}/proc") or raise "Failed to mount /proc to #{dir}/proc"
125
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           system "rm", "-f", "#{dir}/etc/systemd/system/udev.service", "#{dir}/etc/systemd/system/systemd-udevd.service"
132
133           puts "Rewrite fstab..."
134           File.open "#{dir}/etc/fstab", "w" do |f|
135             dirs.each_with_index do |di, idx|
136               f << %W(UUID=#{di.fs_uuid} #{di.path} ext4 defaults,noatime 0 #{di.path == '/' ? 1 : 2}).join("\t")
137               f << "\n"
138             end
139           end
140
141           system("chroot", dir, "apt-get", "-qy", "update")
142           system("chroot", dir, "apt-get", "-y", "install", "linux-image-amd64")
143
144           puts "Update grub..."
145           unless File.exists? "#{dir}/usr/sbin/grub-mkconfig"
146             system("chroot", dir, "apt-get", "-qy", "update") or raise "Failed to install grub-pc"
147             system({'DEBIAN_FRONTEND' => 'noninteractive'}, "chroot", dir, "apt-get", "-y", "install", "grub-pc")
148           end
149           File.open "#{dir}/boot/grub/device.map", "w" do |f|
150             f.puts "(hd0)\t#{root_dev}"
151           end
152           system("chroot", dir, "grub-mkconfig", "-o", "/boot/grub/grub.cfg") or raise "grub-mkconfig fails."
153           system(*%W(grub-install --no-floppy --grub-mkdevicemap=#{dir}/boot/grub/device.map --root-directory=#{dir} #{root_dev})) or raise "grub-install failed."
154         ensure
155           system("umount", "#{dir}/dev")
156           system("umount", "#{dir}/proc")
157           dirs.reverse[0..-2].each do |di, idx|
158             system("umount", "#{dir}#{di.path}")
159           end
160           system("umount", dir)
161         end
162       end
163     end
164   end
165
166   def write_json
167     jdef = []
168     dirs.each_with_index do |dir, idx|
169       jdef.push({
170         "Description" => dir.path == '/' ? 'root' : dir.path[1..-1].tr('/', '-'),
171         "Format" => "raw",
172         "UserBucket" => {
173           "S3Bucket" => ENV.fetch("S3_BUCKET", "osdn-base-images"),
174           "S3Key" => "#{ENV.fetch("S3_KEY_PREFIX", "src-disks/")}#{img_path_base}_#{idx}.img"
175         }
176       })
177     end
178     File.write "#{img_path_base}.json", JSON.pretty_generate(jdef)
179   end
180
181
182   def run
183     create_disk
184     create_fs
185     sync_dirs
186     fix_boot
187     write_json
188     puts "Image creation has been complated (#{name})"
189   end
190 end
191
192 if $0 == __FILE__
193   list = YAML.load_file(ARGV[0] || 'image-list.yml')
194   list.each do |imgdef|
195     name = nil
196     dirs = []
197     if imgdef.kind_of?(Hash)
198       name = imgdef['name']
199       (imgdef['dirs'] || {}).each do |path, opts|
200         opts.kind_of?(Hash) or opts = {size: opts}
201         dirs << SyncDirDef.new({path: path}.merge(opts.keys.map(&:to_sym).zip(opts.values).to_h))
202       end
203     else
204       name = imgdef
205     end
206     dirs.empty? and dirs << SyncDirDef.new
207     ImageCreator.new(name, dirs).run
208   end
209 end