duck-installer 0.2.1
Sign up to get free protection for your applications and to get access to all the features.
- data/LICENSE +674 -0
- data/README +35 -0
- data/bin/duck +9 -0
- data/duck.yaml +93 -0
- data/files/etc/dhclient-exit-hooks.d/hostname +17 -0
- data/files/etc/fstab +1 -0
- data/files/etc/hostname +1 -0
- data/files/etc/hosts +2 -0
- data/files/etc/inittab +25 -0
- data/files/etc/mtab +1 -0
- data/files/init +9 -0
- data/files/lib/duck.d/00-splash +36 -0
- data/files/lib/duck.d/02-setup-network +30 -0
- data/files/lib/duck.d/40-debootstrap +16 -0
- data/files/lib/duck.d/41-add-policy-rc.d +14 -0
- data/files/lib/duck.d/41-update-hostname +9 -0
- data/files/lib/duck.d/98-remove-policy-rc.d +9 -0
- data/files/lib/duck.d/99-reboot +18 -0
- data/files/lib/libduck.sh +152 -0
- data/files/lib/python-duck/duck/__init__.py +2 -0
- data/files/lib/python-duck/duck/db.py +48 -0
- data/files/lib/python-duck/duck/log.py +5 -0
- data/files/sbin/duckdb +212 -0
- data/files/sbin/duckinstall +21 -0
- data/files/sbin/ducklogin +3 -0
- data/fixes/clear-persistent-udev +10 -0
- data/fixes/kernel-boot-fix +53 -0
- data/fixes/squeeze-fix +14 -0
- data/lib/duck.rb +191 -0
- data/lib/duck/build.rb +395 -0
- data/lib/duck/chroot_utils.rb +51 -0
- data/lib/duck/enter.rb +20 -0
- data/lib/duck/logging.rb +31 -0
- data/lib/duck/module_helper.rb +56 -0
- data/lib/duck/pack.rb +42 -0
- data/lib/duck/qemu.rb +34 -0
- data/lib/duck/spawn_utils.rb +83 -0
- data/lib/duck/version.rb +3 -0
- metadata +103 -0
data/lib/duck/build.rb
ADDED
@@ -0,0 +1,395 @@
|
|
1
|
+
require 'fileutils'
|
2
|
+
|
3
|
+
require 'duck/chroot_utils'
|
4
|
+
require 'duck/logging'
|
5
|
+
require 'duck/module_helper'
|
6
|
+
|
7
|
+
module Duck
|
8
|
+
class Build
|
9
|
+
include Logging
|
10
|
+
include ChrootUtils
|
11
|
+
include ModuleHelper
|
12
|
+
|
13
|
+
FixesDir = 'fixes'
|
14
|
+
FilesDir = 'files'
|
15
|
+
KeysDir = 'keys'
|
16
|
+
KeysRingsDir = 'keyrings'
|
17
|
+
BootstrapStatus = '.bootstrap'
|
18
|
+
DefaultSourceType = 'deb'
|
19
|
+
DefaultComponents = ['main']
|
20
|
+
DefaultSuite = 'squeeze'
|
21
|
+
|
22
|
+
def initialize(options)
|
23
|
+
@_roots = options[:_roots]
|
24
|
+
@target = options[:target]
|
25
|
+
@temp = options[:temp]
|
26
|
+
@chroot_env = options[:env] || {}
|
27
|
+
@packages = options[:packages] || []
|
28
|
+
@fixes = options[:fixes] || []
|
29
|
+
@sources = options[:sources]
|
30
|
+
@transports = options[:transports]
|
31
|
+
@bootstrap = validate_bootstrap options[:bootstrap]
|
32
|
+
@keyring = validate_keyring options[:keyring]
|
33
|
+
@files = validate_array [:from, :to], options[:files]
|
34
|
+
@services = validate_array [:name], options[:services]
|
35
|
+
@preferences = validate_array [:package, :pin, :priority], options[:preferences]
|
36
|
+
|
37
|
+
if @bootstrap[:tarball]
|
38
|
+
@bootstrap_tarball = File.join @temp, @bootstrap[:tarball]
|
39
|
+
end
|
40
|
+
|
41
|
+
@fixes_target = File.join @target, FixesDir
|
42
|
+
@bootstrap_status = File.join @target, BootstrapStatus
|
43
|
+
end
|
44
|
+
|
45
|
+
def validate_keyring(opts)
|
46
|
+
return nil unless opts
|
47
|
+
raise "Missing required keyring option 'keyserver'" unless opts[:keyserver]
|
48
|
+
opts[:keys] = [] unless opts[:keys]
|
49
|
+
opts
|
50
|
+
end
|
51
|
+
|
52
|
+
def validate_bootstrap(opts)
|
53
|
+
raise "Missing bootstrap options" unless opts
|
54
|
+
opts[:suite] = DefaultSuite unless opts[:suite]
|
55
|
+
return opts
|
56
|
+
end
|
57
|
+
|
58
|
+
def validate_item(keys, item)
|
59
|
+
keys.each {|k| raise "Missing '#{k}' declaration" unless item[k]}
|
60
|
+
end
|
61
|
+
|
62
|
+
def validate_array(keys, items)
|
63
|
+
items.each {|root, item| validate_item keys, item}
|
64
|
+
end
|
65
|
+
|
66
|
+
def copy_fixes
|
67
|
+
FileUtils.mkdir_p @fixes_target unless File.directory? @fixes_target
|
68
|
+
|
69
|
+
@fixes.each do |root, fix_name|
|
70
|
+
source = File.join root, FixesDir, fix_name
|
71
|
+
target = File.join @fixes_target, fix_name
|
72
|
+
|
73
|
+
next unless File.file? source
|
74
|
+
next if File.file? target and File.mtime(source) > File.mtime(target)
|
75
|
+
|
76
|
+
log.debug "copying fix #{source} to #{target}"
|
77
|
+
FileUtils.cp source, target
|
78
|
+
FileUtils.chmod 0755, target
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
def clear_fixes
|
83
|
+
FileUtils.rm_rf @fixes_target
|
84
|
+
end
|
85
|
+
|
86
|
+
def run_fixes(stage)
|
87
|
+
return unless File.directory? @fixes_target
|
88
|
+
|
89
|
+
log.info "fixes: #{stage}"
|
90
|
+
|
91
|
+
@fixes.each do |root, fix_name|
|
92
|
+
log.debug "fix: #{fix_name} #{stage}"
|
93
|
+
executable = File.join '/', FixesDir, fix_name
|
94
|
+
exitcode = chroot [@target, executable, stage]
|
95
|
+
raise "fix failed: #{fix_name} #{stage}" if exitcode != 0
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
def check_keyring
|
100
|
+
return unless @keyring
|
101
|
+
|
102
|
+
missing_keys = []
|
103
|
+
|
104
|
+
(@keyring[:keys] || []).each do |key|
|
105
|
+
key_path = File.join KeysDir, "#{key}.gpg"
|
106
|
+
next if File.file? key_path
|
107
|
+
missing_keys << {:id => key, :path => key_path}
|
108
|
+
end
|
109
|
+
|
110
|
+
return if missing_keys.empty?
|
111
|
+
|
112
|
+
log.error "Some required keys are missing from your keys directory"
|
113
|
+
|
114
|
+
missing_keys.each do |key|
|
115
|
+
log.error "Missing key: id: #{key[:id]}, path: #{key[:path]}"
|
116
|
+
end
|
117
|
+
|
118
|
+
raise StepError.new "Some required keys are missing from the keys directory"
|
119
|
+
end
|
120
|
+
|
121
|
+
def bootstrap_options
|
122
|
+
opts = {
|
123
|
+
:mirror => @bootstrap[:mirror],
|
124
|
+
:extra => [
|
125
|
+
"--variant=minbase",
|
126
|
+
] + (@bootstrap[:extra] || []),
|
127
|
+
}
|
128
|
+
|
129
|
+
unless @transports.empty?
|
130
|
+
transports = @transports.map{|r, t| "apt-transport-#{t}"}
|
131
|
+
log.debug "Installing extra transports: #{transports.join ' '}"
|
132
|
+
opts[:extra] << '--include' << transports.join(',')
|
133
|
+
end
|
134
|
+
|
135
|
+
if @bootstrap[:keyringfile]
|
136
|
+
key_path = File.join KeysRingsDir, "#{@bootstrap[:keyringfile]}"
|
137
|
+
|
138
|
+
if File.file? key_path
|
139
|
+
opts[:extra] << '--keyring' << key_path
|
140
|
+
else
|
141
|
+
log.error "Can't find key #{@bootstrap[:keyring]}"
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
opts
|
146
|
+
end
|
147
|
+
|
148
|
+
def bootstrap_tarball
|
149
|
+
return if File.file? @bootstrap_status
|
150
|
+
return unless @bootstrap_tarball
|
151
|
+
return if File.file? @bootstrap_tarball
|
152
|
+
|
153
|
+
log.debug "Building tarball: #{@bootstrap_tarball}"
|
154
|
+
|
155
|
+
opts = bootstrap_options
|
156
|
+
opts[:extra] << '--make-tarball' << @bootstrap_tarball
|
157
|
+
debootstrap @bootstrap[:suite], @target, opts
|
158
|
+
end
|
159
|
+
|
160
|
+
def bootstrap_install
|
161
|
+
return if File.file? @bootstrap_status
|
162
|
+
|
163
|
+
log.debug "Early stage bootstrap in #{@target}"
|
164
|
+
|
165
|
+
opts = bootstrap_options
|
166
|
+
|
167
|
+
if @bootstrap_tarball
|
168
|
+
opts[:extra] << "--unpack-tarball" << @bootstrap_tarball
|
169
|
+
end
|
170
|
+
|
171
|
+
opts[:extra] << "--foreign"
|
172
|
+
|
173
|
+
debootstrap @bootstrap[:suite], @target, opts
|
174
|
+
end
|
175
|
+
|
176
|
+
def bootstrap_configure
|
177
|
+
return if File.file? @bootstrap_status
|
178
|
+
|
179
|
+
log.debug "Late stage bootstrap in #{@target}"
|
180
|
+
chroot [@target, '/debootstrap/debootstrap', '--second-stage']
|
181
|
+
end
|
182
|
+
|
183
|
+
def bootstrap_end
|
184
|
+
FileUtils.touch @bootstrap_status
|
185
|
+
end
|
186
|
+
|
187
|
+
def read_file(source_dir, file)
|
188
|
+
from = file[:from]
|
189
|
+
to = file[:to]
|
190
|
+
mode = file[:mode] || 0644
|
191
|
+
mode = mode.to_i(8) if mode.is_a? String
|
192
|
+
|
193
|
+
files_pattern = File.join source_dir, from
|
194
|
+
source_files = Dir.glob files_pattern
|
195
|
+
|
196
|
+
return nil if source_files.empty?
|
197
|
+
|
198
|
+
target_to = File.join @target, to
|
199
|
+
|
200
|
+
{:files => source_files, :to => target_to, :mode => mode}
|
201
|
+
end
|
202
|
+
|
203
|
+
def files_copy
|
204
|
+
return if @files.empty?
|
205
|
+
|
206
|
+
@_roots.each do |root|
|
207
|
+
@files.each do |local_root, file|
|
208
|
+
source_dir = File.join root, FilesDir
|
209
|
+
file = read_file(source_dir, file)
|
210
|
+
next if file.nil?
|
211
|
+
|
212
|
+
FileUtils.mkdir_p file[:to]
|
213
|
+
|
214
|
+
file[:files].each do |source|
|
215
|
+
next unless File.file? source
|
216
|
+
|
217
|
+
target = File.join file[:to], File.basename(source)
|
218
|
+
|
219
|
+
# Skip if target already exists and is identical to source.
|
220
|
+
next if File.file? target and FileUtils.compare_file source, target
|
221
|
+
|
222
|
+
log.debug "Copying File: #{source} -> #{target}"
|
223
|
+
|
224
|
+
FileUtils.cp source, target
|
225
|
+
FileUtils.chmod file[:mode], target
|
226
|
+
end
|
227
|
+
end
|
228
|
+
end
|
229
|
+
end
|
230
|
+
|
231
|
+
def packages_install
|
232
|
+
return if @packages.empty?
|
233
|
+
|
234
|
+
options = []
|
235
|
+
options << 'DPkg::NoTriggers=true'
|
236
|
+
options << 'PackageManager::Configure=no'
|
237
|
+
options << 'DPkg::ConfigurePending=false'
|
238
|
+
options << 'DPkg::TriggersPending=false'
|
239
|
+
|
240
|
+
options = options.map{|option| ['-o', option]}.flatten
|
241
|
+
|
242
|
+
packages = @packages.map{|r, p| p}
|
243
|
+
|
244
|
+
log.debug "Installing Packages"
|
245
|
+
packages_repr = packages.join ' '
|
246
|
+
|
247
|
+
log.debug "Installing Packages: #{packages_repr}"
|
248
|
+
in_apt_get *(options + ['install', '--'] + packages)
|
249
|
+
end
|
250
|
+
|
251
|
+
def packages_configure
|
252
|
+
log.debug "Configuring Packages"
|
253
|
+
in_dpkg '--configure', '-a'
|
254
|
+
end
|
255
|
+
|
256
|
+
def sources_list(name, sources)
|
257
|
+
sources_dir = File.join @target, 'etc', 'apt', 'sources.list.d'
|
258
|
+
sources_list = File.join sources_dir, "#{name}.list"
|
259
|
+
|
260
|
+
log.debug "Writing Sources #{sources_list}"
|
261
|
+
|
262
|
+
File.open(sources_list, 'w', 0644) do |f|
|
263
|
+
sources.each do |source|
|
264
|
+
type = source[:type] || DefaultSourceType
|
265
|
+
components = source[:components] || DefaultComponents
|
266
|
+
suite = source[:suite]
|
267
|
+
url = source[:url]
|
268
|
+
|
269
|
+
raise "Missing 'url' in source" unless url
|
270
|
+
raise "Missing 'suite' in source" unless suite
|
271
|
+
|
272
|
+
f.write "#{type} #{url} #{suite} #{components.join ' '}\n"
|
273
|
+
end
|
274
|
+
end
|
275
|
+
end
|
276
|
+
|
277
|
+
def write_apt_preferences
|
278
|
+
apt_preferences = File.join @target, 'etc', 'apt', 'preferences'
|
279
|
+
|
280
|
+
return if File.file? apt_preferences
|
281
|
+
|
282
|
+
log.debug "Writing Preferences #{apt_preferences}"
|
283
|
+
|
284
|
+
File.open(apt_preferences, 'w', 0644) do |f|
|
285
|
+
f.write "# generated by duck\n"
|
286
|
+
|
287
|
+
@preferences.each do |root, pin|
|
288
|
+
f.write "Package: #{pin[:package]}\n"
|
289
|
+
f.write "Pin: #{pin[:pin]}\n"
|
290
|
+
f.write "Pin-Priority: #{pin[:priority]}\n"
|
291
|
+
f.write "\n"
|
292
|
+
end
|
293
|
+
end
|
294
|
+
end
|
295
|
+
|
296
|
+
def add_apt_keys
|
297
|
+
log.debug "Adding APT keys"
|
298
|
+
|
299
|
+
(@keyring[:keys] || []).each do |key|
|
300
|
+
log.debug "Adding key'#{key}'"
|
301
|
+
key_path = File.join KeysDir, "#{key}.gpg"
|
302
|
+
|
303
|
+
File.open key_path, 'r' do |f|
|
304
|
+
in_apt_key ['add', '-'], {:input_file => f}
|
305
|
+
end
|
306
|
+
end
|
307
|
+
end
|
308
|
+
|
309
|
+
def prepare_apt
|
310
|
+
add_apt_keys if @keyring
|
311
|
+
|
312
|
+
sources_list 'main', @sources.map{|r,s| s}
|
313
|
+
in_apt_get 'update'
|
314
|
+
write_apt_preferences
|
315
|
+
end
|
316
|
+
|
317
|
+
def add_policy_rcd
|
318
|
+
policy_rcd = File.join @target, 'usr', 'sbin', 'policy-rc.d'
|
319
|
+
|
320
|
+
if File.file? policy_rcd
|
321
|
+
log.debug "Policy OK: #{policy_rcd}"
|
322
|
+
return
|
323
|
+
end
|
324
|
+
|
325
|
+
log.debug "Writing Folicy: #{policy_rcd}"
|
326
|
+
|
327
|
+
File.open(policy_rcd, 'w', 0755) do |f|
|
328
|
+
f.write("#/bin/sh\n")
|
329
|
+
f.write("exit 101\n")
|
330
|
+
end
|
331
|
+
end
|
332
|
+
|
333
|
+
def remove_policy_rcd
|
334
|
+
policy_rcd = File.join @target, 'usr', 'sbin', 'policy-rc.d'
|
335
|
+
log.debug "Removing Policy: #{policy_rcd}"
|
336
|
+
FileUtils.rm_f policy_rcd
|
337
|
+
end
|
338
|
+
|
339
|
+
def disable_runlevel(runlevel)
|
340
|
+
runlevel_dir = File.join @target, 'etc', "rc#{runlevel}.d"
|
341
|
+
raise "No such runlevel: #{runlevel}" unless File.directory? runlevel_dir
|
342
|
+
|
343
|
+
Find.find(runlevel_dir) do |path|
|
344
|
+
name = File.basename path
|
345
|
+
|
346
|
+
if name =~ /^S..(.+)$/
|
347
|
+
service = $1
|
348
|
+
log.debug "Disabling Service '#{service}'"
|
349
|
+
in_update_rcd '-f', service, 'remove'
|
350
|
+
end
|
351
|
+
end
|
352
|
+
end
|
353
|
+
|
354
|
+
def configure_boot_services
|
355
|
+
disable_runlevel '2'
|
356
|
+
disable_runlevel 'S'
|
357
|
+
|
358
|
+
@services.each do |root, service|
|
359
|
+
args = [service[:name]]
|
360
|
+
args += ['start'] + service[:start].split(' ') if service[:start]
|
361
|
+
args += ['stop'] + service[:stop].split(' ') if service[:stop]
|
362
|
+
in_update_rcd '-f', *args
|
363
|
+
end
|
364
|
+
end
|
365
|
+
|
366
|
+
# define all the different steps involved.
|
367
|
+
step :check_keyring, :disable_hooks => true
|
368
|
+
step :bootstrap_tarball, :disable_hooks => true
|
369
|
+
step :bootstrap_install, :disable_hooks => true
|
370
|
+
step :copy_fixes, :disable_hooks => true
|
371
|
+
step :bootstrap_configure
|
372
|
+
step :bootstrap_end
|
373
|
+
step :add_policy_rcd
|
374
|
+
step :prepare_apt
|
375
|
+
step :packages_install
|
376
|
+
step :packages_configure
|
377
|
+
step :files_copy
|
378
|
+
step :configure_boot_services
|
379
|
+
step :remove_policy_rcd
|
380
|
+
step :clear_fixes, :disable_hooks => true
|
381
|
+
|
382
|
+
def pre_hook(name)
|
383
|
+
run_fixes "pre-#{name}"
|
384
|
+
end
|
385
|
+
|
386
|
+
def post_hook(name)
|
387
|
+
run_fixes "post-#{name}"
|
388
|
+
end
|
389
|
+
|
390
|
+
def final_hook
|
391
|
+
run_fixes "final"
|
392
|
+
clear_fixes
|
393
|
+
end
|
394
|
+
end
|
395
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
require 'duck/spawn_utils'
|
2
|
+
|
3
|
+
module ChrootUtils
|
4
|
+
include SpawnUtils
|
5
|
+
|
6
|
+
CHROOT = 'chroot'
|
7
|
+
APT_GET = 'apt-get'
|
8
|
+
APT_KEY = 'apt-key'
|
9
|
+
DPKG = 'dpkg'
|
10
|
+
UPDATE_RCD = 'update-rc.d'
|
11
|
+
SH = 'bash'
|
12
|
+
|
13
|
+
CHROOT_ENV = {
|
14
|
+
'DEBIAN_FRONTEND' => 'noninteractive',
|
15
|
+
'DEBCONF_NONINTERACTIVE_SEEN' => 'true',
|
16
|
+
'LC_ALL' => 'C',
|
17
|
+
'LANGUAGE' => 'C',
|
18
|
+
'LANG' => 'C',
|
19
|
+
}
|
20
|
+
|
21
|
+
def chroot(args, options={})
|
22
|
+
spawn [CHROOT] + args, options
|
23
|
+
end
|
24
|
+
|
25
|
+
# for doing automated tasks inside of the chroot.
|
26
|
+
def auto_chroot(args, opts={})
|
27
|
+
log.debug "chroot: #{args.join ' '}"
|
28
|
+
opts[:env] = (opts[:env] || {}).update(@chroot_env || {}).merge(CHROOT_ENV)
|
29
|
+
chroot [@target] + args, opts
|
30
|
+
end
|
31
|
+
|
32
|
+
def in_apt_get(*args)
|
33
|
+
auto_chroot [APT_GET, '-y', '--force-yes'] + args
|
34
|
+
end
|
35
|
+
|
36
|
+
def in_apt_key(args, opts)
|
37
|
+
auto_chroot [APT_KEY] + args, opts
|
38
|
+
end
|
39
|
+
|
40
|
+
def in_dpkg(*args)
|
41
|
+
auto_chroot [DPKG] + args
|
42
|
+
end
|
43
|
+
|
44
|
+
def in_shell(command)
|
45
|
+
auto_chroot [SH, '-c', command]
|
46
|
+
end
|
47
|
+
|
48
|
+
def in_update_rcd(*args)
|
49
|
+
auto_chroot [UPDATE_RCD] + args
|
50
|
+
end
|
51
|
+
end
|
data/lib/duck/enter.rb
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
require 'duck/chroot_utils'
|
2
|
+
require 'duck/logging'
|
3
|
+
|
4
|
+
module Duck
|
5
|
+
class Enter
|
6
|
+
include ChrootUtils
|
7
|
+
include Logging
|
8
|
+
|
9
|
+
def initialize(options)
|
10
|
+
@target = options[:target]
|
11
|
+
@shell = options[:shell]
|
12
|
+
@chroot_env = options[:env] || {}
|
13
|
+
end
|
14
|
+
|
15
|
+
def execute
|
16
|
+
log.info "Entering #{@target}"
|
17
|
+
chroot [@target, @shell], :env => @chroot_env
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|