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/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