duck-installer 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|