duck-installer 0.2.1

Sign up to get free protection for your applications and to get access to all the features.
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