matthewtodd-captain 0.1.0
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/README.rdoc +65 -0
- data/Rakefile +20 -0
- data/bin/captain +6 -0
- data/features/installer_image.feature +8 -0
- data/features/steps/captain_steps.rb +17 -0
- data/features/support/env.rb +11 -0
- data/features/support/helpers.rb +46 -0
- data/lib/captain/application.rb +111 -0
- data/lib/captain/image.rb +28 -0
- data/lib/captain/package.rb +68 -0
- data/lib/captain/package_list.rb +102 -0
- data/lib/captain/release.rb +101 -0
- data/lib/captain/remote.rb +232 -0
- data/lib/captain/resource.rb +43 -0
- data/lib/captain.rb +10 -0
- data/resources/disk_base_components.erb +1 -0
- data/resources/disk_base_installable.erb +0 -0
- data/resources/disk_cd_type.erb +1 -0
- data/resources/disk_info.erb +1 -0
- data/resources/disk_udeb_include.erb +4 -0
- data/resources/isolinux.bin +0 -0
- data/resources/isolinux.cfg +9 -0
- data/resources/preseed.seed.erb +58 -0
- data/resources/release.erb +23 -0
- data/resources/release_component.erb +2 -0
- metadata +104 -0
data/README.rdoc
ADDED
@@ -0,0 +1,65 @@
|
|
1
|
+
= Captain
|
2
|
+
|
3
|
+
Captain builds an Ubuntu installation CD just as you like it.
|
4
|
+
|
5
|
+
It gathers packages from any number of repositories, preseeds almost all of the installer questions, and runs on any platform with <tt>mkisofs</tt> (including OSX).
|
6
|
+
|
7
|
+
== Behold
|
8
|
+
|
9
|
+
With no external configuration, Captain builds an i386 Jaunty CD:
|
10
|
+
|
11
|
+
$ captain
|
12
|
+
...
|
13
|
+
$ ls
|
14
|
+
ubuntu-9.04-captain-i386.iso
|
15
|
+
$
|
16
|
+
|
17
|
+
== Install
|
18
|
+
|
19
|
+
Captain uses <tt>mkisofs</tt> to burn the final image.
|
20
|
+
|
21
|
+
On OSX, you can get this with
|
22
|
+
|
23
|
+
port install cdrtools
|
24
|
+
|
25
|
+
And then you're good to go:
|
26
|
+
|
27
|
+
gem sources --add http://gems.github.com
|
28
|
+
gem install matthewtodd-captain
|
29
|
+
|
30
|
+
== Configure
|
31
|
+
|
32
|
+
Captain looks for <tt>config/captain.rb</tt> under the current directory.
|
33
|
+
|
34
|
+
Here are the defaults:
|
35
|
+
|
36
|
+
architecture 'i386'
|
37
|
+
include_packages ['linux-server', 'language-support-en', 'grub']
|
38
|
+
install_packages []
|
39
|
+
label 'Ubuntu'
|
40
|
+
output_directory '.'
|
41
|
+
post_install_commands []
|
42
|
+
repositories ['http://us.archive.ubuntu.com/ubuntu jaunty main restricted']
|
43
|
+
tasks ['minimal', 'standard']
|
44
|
+
tag 'captain'
|
45
|
+
version '9.04'
|
46
|
+
working_directory temporary_directory
|
47
|
+
|
48
|
+
There are a few things you'll need to be careful with, for now:
|
49
|
+
|
50
|
+
+repositories+:: By convention, the installation system and udeb packages will be pulled from the first repository in the list. So you'll want to make sure the first repository is a full-fledged Ubuntu repository.
|
51
|
+
+tasks+:: <b>MUST include <tt>'minimal'</tt></b>, else the installer will die with the maddeningly not-at-all-the-problem <tt>"failure trying to run: chroot /target mount -t proc proc /proc"</tt>
|
52
|
+
+include_packages+:: MUST include a kernel, some language-support package, and grub; else the installer will complain.
|
53
|
+
+version+:: Could be determined automatically from, say, the <tt>Release</tt> file in the first repository. But it isn't yet, so beware of the duplication.
|
54
|
+
|
55
|
+
See the examples[http://github.com/matthewtodd/captain/tree/master/examples] folder for more.
|
56
|
+
|
57
|
+
== Preseeding
|
58
|
+
|
59
|
+
See resources/preseed.seed.erb[http://github.com/matthewtodd/captain/blob/master/resources/preseed.seed.erb]. There may be a few controversial decisions in there that could use some configuring: there's just one monolithic disk partition, no "regular" user created, and no http mirrors included in <tt>/etc/apt/sources.list</tt>.
|
60
|
+
|
61
|
+
Other than the disk partitioning, I suppose most of these things could be handled post-install, though I'm glad to accept patches / suggestions adding the configuration options you need.
|
62
|
+
|
63
|
+
== Network Cache
|
64
|
+
|
65
|
+
Captain caches everything it downloads from the network in <tt>$HOME/.captain</tt>.
|
data/Rakefile
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
this_rakefile_uses_shoe = <<END
|
2
|
+
----------------------------------------
|
3
|
+
Please install Shoe:
|
4
|
+
gem sources --add http://gems.github.com
|
5
|
+
gem install matthewtodd-shoe
|
6
|
+
----------------------------------------
|
7
|
+
END
|
8
|
+
|
9
|
+
begin
|
10
|
+
gem 'matthewtodd-shoe'
|
11
|
+
rescue Gem::LoadError
|
12
|
+
abort this_rakefile_uses_shoe
|
13
|
+
else
|
14
|
+
require 'shoe'
|
15
|
+
end
|
16
|
+
|
17
|
+
Shoe.tie('captain', '0.1.0', 'Builds an Ubuntu installation CD just as you like it.') do |spec|
|
18
|
+
spec.requirements = ['mkisofs']
|
19
|
+
spec.add_development_dependency 'cucumber'
|
20
|
+
end
|
data/bin/captain
ADDED
@@ -0,0 +1,8 @@
|
|
1
|
+
Feature: Installer Image
|
2
|
+
In order to install a custom Ubuntu system
|
3
|
+
As a perennial yak-shaver
|
4
|
+
I want to assemble my own installer CD
|
5
|
+
|
6
|
+
Scenario: Default Configuration
|
7
|
+
When I run captain
|
8
|
+
Then I should be able to launch the resulting image "ubuntu-9.04-captain-i386.iso"
|
@@ -0,0 +1,17 @@
|
|
1
|
+
When /^I run captain$/ do
|
2
|
+
@app.run
|
3
|
+
end
|
4
|
+
|
5
|
+
# Before, I had stubbed out all sorts of stuff with ShamRack. That got to be a
|
6
|
+
# lot to maintain. So, now I just make sure I can actually boot up the
|
7
|
+
# installation system.
|
8
|
+
Then /^I should be able to launch the resulting image "(.+)"$/ do |image_name|
|
9
|
+
vmware_directory = Pathname.pwd.join(@app.output_directory)
|
10
|
+
config_path = vmware_directory.join('captain.vmx')
|
11
|
+
hard_disk_path = vmware_directory.join('captain.vmdk')
|
12
|
+
cdrom_iso_path = vmware_directory.join(image_name)
|
13
|
+
|
14
|
+
create_a_vmware_configuration_file(config_path, hard_disk_path, cdrom_iso_path)
|
15
|
+
create_an_empty_hard_disk_image(hard_disk_path)
|
16
|
+
launch_vmware(config_path)
|
17
|
+
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
require 'pathname'
|
2
|
+
root = Pathname.new(__FILE__).dirname.parent.parent
|
3
|
+
|
4
|
+
$LOAD_PATH.unshift(root.join('lib'))
|
5
|
+
require 'captain'
|
6
|
+
|
7
|
+
Before do
|
8
|
+
root.join('tmp').rmtree if root.join('tmp').directory?
|
9
|
+
@app = Captain::Application.new
|
10
|
+
@app.output_directory('tmp')
|
11
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
require 'erb'
|
2
|
+
|
3
|
+
module Helpers
|
4
|
+
def append_to_path(directory)
|
5
|
+
ENV['PATH'] = "#{ENV['PATH']}:#{directory}" if File.directory?(directory)
|
6
|
+
end
|
7
|
+
|
8
|
+
def create_a_vmware_configuration_file(path, hard_disk_path, cdrom_iso_path)
|
9
|
+
template = File.read(__FILE__).split('__END__').last.strip
|
10
|
+
|
11
|
+
File.open(path, 'w') do |config|
|
12
|
+
config.write ERB.new(template).result(binding)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def create_an_empty_hard_disk_image(path)
|
17
|
+
append_to_path('/Applications/Q.app/Contents/MacOS')
|
18
|
+
system("qemu-img create -f vmdk #{path} 2G > /dev/null") || raise('Could not create image.')
|
19
|
+
end
|
20
|
+
|
21
|
+
def launch_vmware(config_path)
|
22
|
+
append_to_path('/Library/Application Support/VMware Fusion')
|
23
|
+
system("vmrun -T fusion start #{config_path} gui") || raise('VMware Error.')
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
World(Helpers)
|
28
|
+
|
29
|
+
__END__
|
30
|
+
config.version = "8"
|
31
|
+
virtualHW.version = "7"
|
32
|
+
scsi0.present = "FALSE"
|
33
|
+
memsize = "512"
|
34
|
+
ide0:0.present = "TRUE"
|
35
|
+
ide0:0.fileName = "<%= hard_disk_path %>"
|
36
|
+
ide1:0.present = "TRUE"
|
37
|
+
ide1:0.fileName = "<%= cdrom_iso_path %>"
|
38
|
+
ide1:0.deviceType = "cdrom-image"
|
39
|
+
floppy0.present = "FALSE"
|
40
|
+
ethernet0.present = "TRUE"
|
41
|
+
usb.present = "TRUE"
|
42
|
+
sound.present = "FALSE"
|
43
|
+
displayName = "Captain - Cucumber"
|
44
|
+
guestOS = "ubuntu"
|
45
|
+
nvram = "captain.nvram"
|
46
|
+
workingDir = "<%= path.dirname %>"
|
@@ -0,0 +1,111 @@
|
|
1
|
+
require 'tmpdir'
|
2
|
+
|
3
|
+
module Captain
|
4
|
+
class Application
|
5
|
+
def self.run
|
6
|
+
new.run
|
7
|
+
end
|
8
|
+
|
9
|
+
def initialize
|
10
|
+
load_default_configuration
|
11
|
+
load_configuration
|
12
|
+
end
|
13
|
+
|
14
|
+
def run
|
15
|
+
create_bundle_directory
|
16
|
+
create_packages
|
17
|
+
create_installer_and_its_supporting_files
|
18
|
+
create_boot_loader
|
19
|
+
create_ubuntu_symlink
|
20
|
+
create_iso_image
|
21
|
+
end
|
22
|
+
|
23
|
+
def method_missing(symbol, *args)
|
24
|
+
if args.length > 0
|
25
|
+
@configuration[symbol] = args.first
|
26
|
+
elsif @configuration.has_key?(symbol)
|
27
|
+
@configuration[symbol]
|
28
|
+
else
|
29
|
+
super
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
|
35
|
+
# This is a convenient way to put arbitrary content on the disk.
|
36
|
+
def create_bundle_directory
|
37
|
+
FileUtils.cp_r('bundle', working_directory) if File.directory?('bundle')
|
38
|
+
end
|
39
|
+
|
40
|
+
def create_packages
|
41
|
+
PackageList.new(repositories, architecture, tasks, include_packages.concat(install_packages)).copy_to(working_directory, self)
|
42
|
+
end
|
43
|
+
|
44
|
+
def create_installer_and_its_supporting_files
|
45
|
+
mirror, codename = installer_repository_mirror_and_codename
|
46
|
+
|
47
|
+
Remote.installer_file(mirror, codename, architecture, 'cdrom', 'vmlinuz' ).copy_to(working_directory, 'install', 'vmlinuz')
|
48
|
+
Remote.installer_file(mirror, codename, architecture, 'cdrom', 'initrd.gz').copy_to(working_directory, 'install', 'initrd.gz')
|
49
|
+
|
50
|
+
Resource.template('preseed.seed.erb', template_binding).copy_to(working_directory, 'install', 'preseed.seed')
|
51
|
+
Resource.template('disk_base_components.erb', template_binding).copy_to(working_directory, '.disk', 'base_components')
|
52
|
+
Resource.template('disk_base_installable.erb', template_binding).copy_to(working_directory, '.disk', 'base_installable')
|
53
|
+
Resource.template('disk_cd_type.erb', template_binding).copy_to(working_directory, '.disk', 'cd_type')
|
54
|
+
Resource.template('disk_info.erb', template_binding).copy_to(working_directory, '.disk', 'info')
|
55
|
+
Resource.template('disk_udeb_include.erb', template_binding).copy_to(working_directory, '.disk', 'udeb_include')
|
56
|
+
end
|
57
|
+
|
58
|
+
def create_boot_loader
|
59
|
+
Resource.file('isolinux.bin').copy_to(working_directory, 'isolinux', 'isolinux.bin')
|
60
|
+
Resource.file('isolinux.cfg').copy_to(working_directory, 'isolinux', 'isolinux.cfg')
|
61
|
+
end
|
62
|
+
|
63
|
+
def create_ubuntu_symlink
|
64
|
+
Dir.chdir(working_directory) do
|
65
|
+
FileUtils.symlink('.', 'ubuntu')
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
def create_iso_image
|
70
|
+
Image.new(working_directory).burn(iso_image_path)
|
71
|
+
end
|
72
|
+
|
73
|
+
def installer_repository_mirror_and_codename
|
74
|
+
repositories.first.split(' ').slice(0, 2)
|
75
|
+
end
|
76
|
+
|
77
|
+
def iso_image_path
|
78
|
+
File.join(output_directory, "#{label}-#{version}-#{tag}-#{architecture}.iso".downcase)
|
79
|
+
end
|
80
|
+
|
81
|
+
def template_binding
|
82
|
+
binding
|
83
|
+
end
|
84
|
+
|
85
|
+
def load_default_configuration
|
86
|
+
@configuration = Hash.new
|
87
|
+
|
88
|
+
architecture 'i386'
|
89
|
+
include_packages ['linux-server', 'language-support-en', 'grub']
|
90
|
+
install_packages []
|
91
|
+
label 'Ubuntu'
|
92
|
+
output_directory '.'
|
93
|
+
post_install_commands []
|
94
|
+
repositories ['http://us.archive.ubuntu.com/ubuntu jaunty main restricted']
|
95
|
+
tasks ['minimal', 'standard']
|
96
|
+
tag 'captain'
|
97
|
+
version '9.04'
|
98
|
+
working_directory temporary_directory
|
99
|
+
end
|
100
|
+
|
101
|
+
def load_configuration
|
102
|
+
instance_eval(File.read('config/captain.rb')) if File.exist?('config/captain.rb')
|
103
|
+
end
|
104
|
+
|
105
|
+
def temporary_directory
|
106
|
+
temporary_directory = Dir.mktmpdir('captain')
|
107
|
+
at_exit { FileUtils.remove_entry_secure(temporary_directory) }
|
108
|
+
temporary_directory
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
require 'pathname'
|
2
|
+
|
3
|
+
module Captain
|
4
|
+
class Image
|
5
|
+
def initialize(base_directory)
|
6
|
+
@base_directory = base_directory
|
7
|
+
end
|
8
|
+
|
9
|
+
def burn(path)
|
10
|
+
path = Pathname.new(path)
|
11
|
+
path.parent.mkpath unless path.parent.directory?
|
12
|
+
|
13
|
+
system('mkisofs',
|
14
|
+
'-boot-info-table',
|
15
|
+
'-boot-load-size', '4',
|
16
|
+
'-cache-inodes',
|
17
|
+
'-eltorito-boot', 'isolinux/isolinux.bin',
|
18
|
+
'-eltorito-catalog', 'isolinux/boot.cat',
|
19
|
+
'-full-iso9660-filenames',
|
20
|
+
'-joliet',
|
21
|
+
'-no-emul-boot',
|
22
|
+
'-output', path,
|
23
|
+
'-rational-rock',
|
24
|
+
'-volid', 'Name',
|
25
|
+
@base_directory) || raise('Error creating iso image.')
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
require 'set'
|
2
|
+
|
3
|
+
module Captain
|
4
|
+
class Package
|
5
|
+
attr_reader :codename
|
6
|
+
attr_reader :component
|
7
|
+
attr_reader :dependencies
|
8
|
+
attr_reader :filename
|
9
|
+
attr_reader :md5sum
|
10
|
+
attr_reader :mirror
|
11
|
+
attr_reader :name
|
12
|
+
|
13
|
+
def initialize(mirror, codename, component, manifest)
|
14
|
+
@mirror = mirror
|
15
|
+
@codename = codename
|
16
|
+
@component = component
|
17
|
+
@manifest = manifest
|
18
|
+
|
19
|
+
@dependencies = Set.new
|
20
|
+
@provides = Set.new
|
21
|
+
@recommends = Set.new
|
22
|
+
@tasks = Set.new
|
23
|
+
|
24
|
+
@manifest.each_line do |line|
|
25
|
+
case line
|
26
|
+
when /^Depends:(.*)$/
|
27
|
+
@dependencies.merge(parse_list($1))
|
28
|
+
when /^Filename:(.*)$/
|
29
|
+
@filename = $1.strip
|
30
|
+
when /^MD5sum:(.*)$/
|
31
|
+
@md5sum = $1.strip
|
32
|
+
when /^Package:(.*)$/
|
33
|
+
@name = $1.strip
|
34
|
+
when /^Provides:(.*)$/
|
35
|
+
@provides.merge(parse_list($1))
|
36
|
+
when /^Recommends:(.*)$/
|
37
|
+
@recommends.merge(parse_list($1))
|
38
|
+
when /^Task:(.*)$/
|
39
|
+
@tasks.merge(parse_list($1))
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def copy_to(directory)
|
45
|
+
Remote.package_file(mirror, filename, md5sum).copy_to(directory, filename)
|
46
|
+
end
|
47
|
+
|
48
|
+
def copy_manifest_to(stream)
|
49
|
+
# Just making sure we don't end up with extra newlines. Postel's law and all that.
|
50
|
+
stream.puts(@manifest.strip)
|
51
|
+
stream.puts
|
52
|
+
end
|
53
|
+
|
54
|
+
def tasks
|
55
|
+
@tasks
|
56
|
+
end
|
57
|
+
|
58
|
+
def name_and_provides
|
59
|
+
@name_and_provides ||= @provides.dup.add(@name)
|
60
|
+
end
|
61
|
+
|
62
|
+
private
|
63
|
+
|
64
|
+
def parse_list(string)
|
65
|
+
string.split(/[,|]/).map { |versioned| versioned.strip.split(' ').first }
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
@@ -0,0 +1,102 @@
|
|
1
|
+
module Captain
|
2
|
+
class PackageList
|
3
|
+
def initialize(sources, architecture, tasks, selectors)
|
4
|
+
@sources = sources
|
5
|
+
@architecture = architecture
|
6
|
+
@tasks = tasks
|
7
|
+
@selectors = selectors
|
8
|
+
end
|
9
|
+
|
10
|
+
def copy_to(directory, configuration)
|
11
|
+
each_release { |release| release.copy_to(directory, configuration) }
|
12
|
+
end
|
13
|
+
|
14
|
+
private
|
15
|
+
|
16
|
+
def each_release
|
17
|
+
winnow_down(deb_packages).concat(udeb_packages).group_by { |package| package.codename }.each do |codename, packages|
|
18
|
+
yield Release.new(codename, @architecture, packages)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def winnow_down(packages)
|
23
|
+
selected_by_task, remaining_by_task = select_packages_by_task(packages)
|
24
|
+
selected_by_name, remaining_by_name = select_packages_by_name(remaining_by_task)
|
25
|
+
selected_by_dependencies = select_packages_by_dependencies(remaining_by_name, selected_by_name)
|
26
|
+
|
27
|
+
selected_by_task.concat(selected_by_name).concat(selected_by_dependencies)
|
28
|
+
end
|
29
|
+
|
30
|
+
def select_packages_by_task(pool)
|
31
|
+
pool.partition { |package| !package.tasks.intersection(@tasks).empty? }
|
32
|
+
end
|
33
|
+
|
34
|
+
def select_packages_by_name(pool, names=@selectors)
|
35
|
+
pool.partition { |package| !package.name_and_provides.intersection(names).empty? }
|
36
|
+
end
|
37
|
+
|
38
|
+
def select_packages_by_dependencies(pool, dependent_packages)
|
39
|
+
if dependent_packages.empty?
|
40
|
+
[]
|
41
|
+
else
|
42
|
+
selected, remaining = select_packages_by_name(pool, dependencies_of(dependent_packages))
|
43
|
+
selected + select_packages_by_dependencies(remaining, selected)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def dependencies_of(packages)
|
48
|
+
# We have todo this goofy inject instead of a simple flatten because the dependencies are returned as Sets.
|
49
|
+
packages.map { |package| package.dependencies }.inject { |all, each| all.merge(each) }
|
50
|
+
end
|
51
|
+
|
52
|
+
def deb_packages
|
53
|
+
deb_components.map { |component| component.packages }.flatten
|
54
|
+
end
|
55
|
+
|
56
|
+
def deb_components
|
57
|
+
@sources.map do |source|
|
58
|
+
mirror, codename, *components = source.split(' ')
|
59
|
+
components.map do |component|
|
60
|
+
ComponentManifest.new(mirror, codename, component, @architecture)
|
61
|
+
end
|
62
|
+
end.flatten
|
63
|
+
end
|
64
|
+
|
65
|
+
def udeb_packages
|
66
|
+
mirror, codename, *components = @sources.first.split(' ')
|
67
|
+
ComponentManifest.new(mirror, codename, 'main/debian-installer', @architecture).packages
|
68
|
+
end
|
69
|
+
|
70
|
+
class ComponentManifest
|
71
|
+
include Enumerable
|
72
|
+
|
73
|
+
def initialize(mirror, codename, component, architecture)
|
74
|
+
@mirror = mirror
|
75
|
+
@codename = codename
|
76
|
+
@component = component
|
77
|
+
@architecture = architecture
|
78
|
+
end
|
79
|
+
|
80
|
+
def each
|
81
|
+
buffer = []
|
82
|
+
open_stream.each_line do |line|
|
83
|
+
if line == "\n"
|
84
|
+
yield Package.new(@mirror, @codename, @component, buffer.join)
|
85
|
+
buffer.clear
|
86
|
+
else
|
87
|
+
buffer.push(line)
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
alias_method :packages, :to_a
|
93
|
+
|
94
|
+
private
|
95
|
+
|
96
|
+
def open_stream
|
97
|
+
Remote.component_file(@mirror, @codename, @component, @architecture, 'Packages.gz').gunzipped
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
end
|
102
|
+
end
|
@@ -0,0 +1,101 @@
|
|
1
|
+
require 'digest'
|
2
|
+
require 'pathname'
|
3
|
+
require 'zlib'
|
4
|
+
|
5
|
+
module Captain
|
6
|
+
class Release
|
7
|
+
attr_reader :architecture
|
8
|
+
attr_reader :codename
|
9
|
+
attr_reader :components
|
10
|
+
|
11
|
+
def initialize(codename, architecture, packages)
|
12
|
+
@codename = codename
|
13
|
+
@architecture = architecture
|
14
|
+
@components = organize_into_components(packages)
|
15
|
+
@packages = packages.sort_by { |p| p.filename }
|
16
|
+
end
|
17
|
+
|
18
|
+
def copy_to(directory, config)
|
19
|
+
directory = Pathname.new(directory)
|
20
|
+
|
21
|
+
@packages.each { |p| p.copy_to(directory) }
|
22
|
+
@components.each { |c| c.copy_to(directory.join('dists', @codename)) }
|
23
|
+
|
24
|
+
Resource.template('release.erb', binding).copy_to(directory.join('dists', @codename, 'Release'))
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
def organize_into_components(packages)
|
30
|
+
packages.group_by { |package| package.component }.map do |name, packages|
|
31
|
+
Component.new(name, @architecture, packages)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def deb_components
|
36
|
+
@components.reject { |c| c.udeb? }
|
37
|
+
end
|
38
|
+
|
39
|
+
class Component
|
40
|
+
attr_reader :name
|
41
|
+
attr_reader :files
|
42
|
+
|
43
|
+
def initialize(name, architecture, packages)
|
44
|
+
@name = name
|
45
|
+
manifest = manifest(packages)
|
46
|
+
@files = []
|
47
|
+
@files.push Manifest.new("#{name}/binary-#{architecture}/Release", Resource.template('release_component.erb', binding).contents)
|
48
|
+
@files.push Manifest.new("#{name}/binary-#{architecture}/Packages", manifest)
|
49
|
+
@files.push Manifest.new("#{name}/binary-#{architecture}/Packages.gz", gzip(manifest))
|
50
|
+
end
|
51
|
+
|
52
|
+
def copy_to(directory)
|
53
|
+
files.each { |file| file.copy_to(directory) }
|
54
|
+
end
|
55
|
+
|
56
|
+
def udeb?
|
57
|
+
name =~ /debian-installer/
|
58
|
+
end
|
59
|
+
|
60
|
+
private
|
61
|
+
|
62
|
+
def manifest(packages)
|
63
|
+
io = StringIO.new
|
64
|
+
packages.each { |package| package.copy_manifest_to(io) }
|
65
|
+
io.string
|
66
|
+
end
|
67
|
+
|
68
|
+
def gzip(string)
|
69
|
+
io = StringIO.new
|
70
|
+
gzip = Zlib::GzipWriter.new(io)
|
71
|
+
gzip.write(string)
|
72
|
+
gzip.close
|
73
|
+
io.string
|
74
|
+
end
|
75
|
+
|
76
|
+
class Manifest
|
77
|
+
attr_reader :path
|
78
|
+
|
79
|
+
def initialize(path, contents)
|
80
|
+
@path = path
|
81
|
+
@contents = contents
|
82
|
+
end
|
83
|
+
|
84
|
+
def copy_to(directory)
|
85
|
+
file = Pathname.new(directory).join(path)
|
86
|
+
file.dirname.mkpath
|
87
|
+
file.open('w') { |io| io.write(@contents) }
|
88
|
+
end
|
89
|
+
|
90
|
+
def checksum(algorithm)
|
91
|
+
Digest(algorithm).hexdigest(@contents)
|
92
|
+
end
|
93
|
+
|
94
|
+
def size
|
95
|
+
@contents.length
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
end
|
101
|
+
end
|
@@ -0,0 +1,232 @@
|
|
1
|
+
require 'digest'
|
2
|
+
require 'open-uri'
|
3
|
+
require 'pathname'
|
4
|
+
require 'uri'
|
5
|
+
require 'zlib'
|
6
|
+
|
7
|
+
module Captain
|
8
|
+
class Remote
|
9
|
+
def self.release_file(mirror, codename)
|
10
|
+
# TODO verify the Release file with GPG
|
11
|
+
new("#{mirror}/dists/#{codename}/Release")
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.component_file(mirror, codename, component, architecture, *rest)
|
15
|
+
uri = component_uri(mirror, codename, component, architecture, *rest)
|
16
|
+
path = [component, "binary-#{architecture}", *rest].join('/')
|
17
|
+
md5sum = release_file(mirror, codename).grep(%r{^ \w{32}\s+\d+\s+#{path}}).first.split(' ').first
|
18
|
+
new(uri, Verifier::MD5.new(md5sum))
|
19
|
+
end
|
20
|
+
|
21
|
+
def self.installer_file(mirror, codename, architecture, *rest)
|
22
|
+
uri = installer_uri(mirror, codename, architecture, *rest)
|
23
|
+
md5sum_uri = installer_uri(mirror, codename, architecture, 'MD5SUMS')
|
24
|
+
md5sum = new(md5sum_uri).grep(%r{#{rest.join('/')}}).first.split(' ').first
|
25
|
+
new(uri, Verifier::MD5.new(md5sum))
|
26
|
+
end
|
27
|
+
|
28
|
+
def self.package_file(mirror, filename, md5sum)
|
29
|
+
new("#{mirror}/#{filename}", Verifier::MD5.new(md5sum))
|
30
|
+
end
|
31
|
+
|
32
|
+
def self.component_uri(mirror, codename, component, architecture, *rest)
|
33
|
+
["#{mirror}/dists/#{codename}/#{component}/binary-#{architecture}", *rest].join('/')
|
34
|
+
end
|
35
|
+
|
36
|
+
def self.installer_uri(mirror, codename, architecture, *rest)
|
37
|
+
["#{mirror}/dists/#{codename}/main/installer-#{architecture}/current/images", *rest].join('/')
|
38
|
+
end
|
39
|
+
|
40
|
+
include Enumerable
|
41
|
+
|
42
|
+
def initialize(uri, verifier=Verifier::Content.new)
|
43
|
+
@uri = URI.parse(uri)
|
44
|
+
@verifier = verifier
|
45
|
+
@cache = Cache::Persistent.new
|
46
|
+
end
|
47
|
+
|
48
|
+
def copy_to(*paths)
|
49
|
+
path = Pathname.new(File.join(paths))
|
50
|
+
path.dirname.mkpath
|
51
|
+
|
52
|
+
open_stream do |stream|
|
53
|
+
File.open(path, 'w') do |file|
|
54
|
+
Stream.copy(stream, file)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def each_line
|
60
|
+
open_stream do |stream|
|
61
|
+
stream.each_line do |line|
|
62
|
+
yield line
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
alias_method :each, :each_line
|
67
|
+
|
68
|
+
def gunzipped
|
69
|
+
Adapter::Gunzip.new(self)
|
70
|
+
end
|
71
|
+
|
72
|
+
private
|
73
|
+
|
74
|
+
def open_stream(retry_count=4)
|
75
|
+
@cache.open(@uri) do |cache|
|
76
|
+
begin
|
77
|
+
@verifier.verify(cache)
|
78
|
+
yield(cache)
|
79
|
+
rescue
|
80
|
+
begin
|
81
|
+
@uri.open(ProgressMeter.new(@uri).to_open_uri_hash) do |stream|
|
82
|
+
@verifier.verify(stream)
|
83
|
+
cache.populate(stream)
|
84
|
+
yield(cache)
|
85
|
+
end
|
86
|
+
rescue Errno::ECONNRESET, OpenURI::HTTPError, SocketError, Timeout::Error, Verifier::Error
|
87
|
+
retry_count -= 1
|
88
|
+
raise if retry_count.zero?
|
89
|
+
puts $!.message
|
90
|
+
puts "Trying again... (#{retry_count} more)"
|
91
|
+
retry
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
module Verifier
|
98
|
+
class Error < RuntimeError
|
99
|
+
end
|
100
|
+
|
101
|
+
class Content
|
102
|
+
def verify(stream)
|
103
|
+
raise(Verifier::Error.new("No content.")) unless stream.read(1)
|
104
|
+
ensure
|
105
|
+
stream.rewind
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
class MD5
|
110
|
+
def initialize(expected)
|
111
|
+
@expected = expected
|
112
|
+
raise(Verifier::Error.new("No expected MD5Sum given.")) unless @expected
|
113
|
+
end
|
114
|
+
|
115
|
+
def verify(stream)
|
116
|
+
actual = md5sum(stream)
|
117
|
+
raise(Verifier::Error.new("MD5Sum mismatch: expected #{@expected} but was #{actual}")) unless @expected == actual
|
118
|
+
end
|
119
|
+
|
120
|
+
private
|
121
|
+
|
122
|
+
def md5sum(stream)
|
123
|
+
digest = Digest::MD5.new
|
124
|
+
Stream.copy(stream, digest, :update)
|
125
|
+
digest.hexdigest
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
module Cache
|
131
|
+
class Persistent
|
132
|
+
PATH = Pathname.new(ENV['HOME']).join('.captain')
|
133
|
+
|
134
|
+
def open(uri)
|
135
|
+
# I didn't expect to have to use string concatenation here, but PATH
|
136
|
+
# gets confused when uri.path starts with a /.
|
137
|
+
path = PATH.join("#{uri.host}#{uri.path}")
|
138
|
+
if path.exist?
|
139
|
+
path.open('r+') { |cache| yield populatable(cache) }
|
140
|
+
else
|
141
|
+
path.dirname.mkpath
|
142
|
+
path.open('w+') { |cache| yield populatable(cache) }
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
private
|
147
|
+
|
148
|
+
def populatable(stream)
|
149
|
+
def stream.populate(other)
|
150
|
+
Stream.copy(other, self)
|
151
|
+
end
|
152
|
+
stream
|
153
|
+
end
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
class ProgressMeter
|
158
|
+
# For these ANSI escape sequences and more, see http://en.wikipedia.org/wiki/ANSI_escape_code
|
159
|
+
MOVE_CURSOR_UP_1_LINE = "\e[1A"
|
160
|
+
ERASE_ENTIRE_LINE = "\e[2K"
|
161
|
+
MOVE_CURSOR_TO_COLUMN_1 = "\e[1G" # columns are 1-based, oddly enough
|
162
|
+
OVERWRITE_PREVIOUS_LINE = "#{MOVE_CURSOR_UP_1_LINE}#{ERASE_ENTIRE_LINE}#{MOVE_CURSOR_TO_COLUMN_1}"
|
163
|
+
|
164
|
+
def initialize(uri)
|
165
|
+
puts uri
|
166
|
+
puts
|
167
|
+
end
|
168
|
+
|
169
|
+
def to_open_uri_hash
|
170
|
+
{ :content_length_proc => method(:the_total_size_is), :progress_proc => method(:the_currently_downloaded_size_is) }
|
171
|
+
end
|
172
|
+
|
173
|
+
def the_total_size_is(size)
|
174
|
+
@total_size = size
|
175
|
+
end
|
176
|
+
|
177
|
+
def the_currently_downloaded_size_is(size)
|
178
|
+
@current = size
|
179
|
+
report
|
180
|
+
end
|
181
|
+
|
182
|
+
private
|
183
|
+
|
184
|
+
# TODO report percent complete
|
185
|
+
# TODO report time spent
|
186
|
+
# TODO report time remaining
|
187
|
+
def report
|
188
|
+
puts "#{OVERWRITE_PREVIOUS_LINE} #{@current} of #{@total_size}"
|
189
|
+
$stdout.flush
|
190
|
+
end
|
191
|
+
end
|
192
|
+
|
193
|
+
class Stream
|
194
|
+
def self.copy(from, to, method = :write)
|
195
|
+
buffer = ''
|
196
|
+
to.truncate(0) if to.respond_to?(:truncate)
|
197
|
+
to.send(method, buffer) while from.read(16384, buffer)
|
198
|
+
ensure
|
199
|
+
from.rewind
|
200
|
+
to.rewind if to.respond_to?(:rewind)
|
201
|
+
end
|
202
|
+
end
|
203
|
+
|
204
|
+
module Adapter
|
205
|
+
class Gunzip
|
206
|
+
include Enumerable
|
207
|
+
|
208
|
+
def initialize(stream)
|
209
|
+
@stream = stream
|
210
|
+
end
|
211
|
+
|
212
|
+
def each_line
|
213
|
+
open_stream do |stream|
|
214
|
+
stream.each_line do |line|
|
215
|
+
yield line
|
216
|
+
end
|
217
|
+
end
|
218
|
+
end
|
219
|
+
alias_method :each, :each_line
|
220
|
+
|
221
|
+
private
|
222
|
+
|
223
|
+
def open_stream
|
224
|
+
@stream.send(:open_stream) do |stream|
|
225
|
+
yield Zlib::GzipReader.new(stream)
|
226
|
+
end
|
227
|
+
end
|
228
|
+
end
|
229
|
+
end
|
230
|
+
|
231
|
+
end
|
232
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
require 'erb'
|
2
|
+
require 'pathname'
|
3
|
+
|
4
|
+
module Captain
|
5
|
+
class Resource
|
6
|
+
PATH = Pathname.new(File.dirname(__FILE__)).join('..', '..', 'resources')
|
7
|
+
|
8
|
+
def self.file(name)
|
9
|
+
new(name)
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.template(name, binding)
|
13
|
+
Template.new(name, binding)
|
14
|
+
end
|
15
|
+
|
16
|
+
def initialize(name)
|
17
|
+
@name = name
|
18
|
+
end
|
19
|
+
|
20
|
+
def contents
|
21
|
+
PATH.join(@name).read
|
22
|
+
end
|
23
|
+
|
24
|
+
def copy_to(*paths)
|
25
|
+
path = File.join(paths)
|
26
|
+
FileUtils.mkpath(File.dirname(path))
|
27
|
+
File.open(path, 'w') { |f| f.write(contents) }
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
class Template < Resource
|
33
|
+
def initialize(name, binding)
|
34
|
+
super(name)
|
35
|
+
@binding = binding
|
36
|
+
end
|
37
|
+
|
38
|
+
def contents
|
39
|
+
ERB.new(super, nil, '<>').result(@binding)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
data/lib/captain.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
main
|
File without changes
|
@@ -0,0 +1 @@
|
|
1
|
+
full_cd/single
|
@@ -0,0 +1 @@
|
|
1
|
+
<%= label %> <%= version %> <%= tag.capitalize %>
|
Binary file
|
@@ -0,0 +1,9 @@
|
|
1
|
+
# Don't show the boot: prompt
|
2
|
+
prompt 0
|
3
|
+
|
4
|
+
# Automatically boot the linux kernel after 1 (= 10 10ths of a) second
|
5
|
+
timeout 10
|
6
|
+
|
7
|
+
label linux
|
8
|
+
kernel /install/vmlinuz
|
9
|
+
append file=/cdrom/install/preseed.seed initrd=/install/initrd.gz quiet locale=en_US console-setup/layoutcode=us netcfg/choose_interface=auto --
|
@@ -0,0 +1,58 @@
|
|
1
|
+
### Localization
|
2
|
+
d-i debian-installer/locale string en_US
|
3
|
+
d-i console-setup/ask_detect boolean false
|
4
|
+
d-i console-setup/layoutcode string us
|
5
|
+
|
6
|
+
### Network configuration
|
7
|
+
d-i netcfg/dhcp_timeout string 10
|
8
|
+
d-i netcfg/dhcp_failed note
|
9
|
+
d-i netcfg/dhcp_options select Configure network manually
|
10
|
+
|
11
|
+
### Mirror settings
|
12
|
+
d-i mirror/country string US
|
13
|
+
|
14
|
+
### Clock and time zone setup
|
15
|
+
d-i clock-setup/utc boolean true
|
16
|
+
d-i time/zone string UTC
|
17
|
+
d-i clock-setup/ntp boolean false
|
18
|
+
|
19
|
+
### Partitioning
|
20
|
+
d-i partman-auto/init_automatically_partition select biggest_free
|
21
|
+
d-i partman-auto/method string regular
|
22
|
+
d-i partman-auto/choose_recipe select atomic
|
23
|
+
d-i partman/confirm_write_new_label boolean true
|
24
|
+
d-i partman/choose_partition select finish
|
25
|
+
d-i partman/confirm boolean true
|
26
|
+
|
27
|
+
### Base system installation
|
28
|
+
|
29
|
+
### Account setup
|
30
|
+
d-i passwd/root-login boolean true
|
31
|
+
d-i passwd/make-user boolean false
|
32
|
+
|
33
|
+
### Apt setup
|
34
|
+
d-i apt-setup/use_mirror boolean false
|
35
|
+
d-i apt-setup/services-select multiselect
|
36
|
+
d-i debian-installer/allow_unauthenticated string true
|
37
|
+
|
38
|
+
### Package selection
|
39
|
+
tasksel tasksel/first multiselect standard
|
40
|
+
d-i pkgsel/include string <%= install_packages.join(' ') %>
|
41
|
+
d-i pkgsel/update-policy select none
|
42
|
+
|
43
|
+
### Boot loader installation
|
44
|
+
d-i grub-installer/only_debian boolean true
|
45
|
+
d-i grub-installer/with_other_os boolean true
|
46
|
+
|
47
|
+
### Finishing up the installation
|
48
|
+
d-i finish-install/reboot_in_progress note
|
49
|
+
d-i cdrom-detect/eject boolean false
|
50
|
+
|
51
|
+
### X configuration
|
52
|
+
|
53
|
+
### Preseeding other packages
|
54
|
+
user-setup-udeb user-setup/encrypted-private boolean false
|
55
|
+
|
56
|
+
#### Advanced options
|
57
|
+
### Running custom commands during the installation
|
58
|
+
d-i preseed/late_command string <%= post_install_commands.join('; ') %>
|
@@ -0,0 +1,23 @@
|
|
1
|
+
Suite: <%= codename %>
|
2
|
+
Codename: <%= codename %>
|
3
|
+
Date: <%= Time.now.utc.strftime('%a, %e %b %Y %H:%M:%S UTC') %>
|
4
|
+
Architectures: <%= architecture %>
|
5
|
+
Components: <%= deb_components.map { |c| c.name }.join(' ') %>
|
6
|
+
MD5Sum:
|
7
|
+
<% components.each do |component| %>
|
8
|
+
<% component.files.each do |file| %>
|
9
|
+
<%= file.checksum('MD5') %> <%= file.size %> <%= file.path %>
|
10
|
+
<% end %>
|
11
|
+
<% end %>
|
12
|
+
SHA1:
|
13
|
+
<% components.each do |component| %>
|
14
|
+
<% component.files.each do |file| %>
|
15
|
+
<%= file.checksum('SHA1') %> <%= file.size %> <%= file.path %>
|
16
|
+
<% end %>
|
17
|
+
<% end %>
|
18
|
+
SHA256:
|
19
|
+
<% components.each do |component| %>
|
20
|
+
<% component.files.each do |file| %>
|
21
|
+
<%= file.checksum('SHA256') %> <%= file.size %> <%= file.path %>
|
22
|
+
<% end %>
|
23
|
+
<% end %>
|
metadata
ADDED
@@ -0,0 +1,104 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: matthewtodd-captain
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Matthew Todd
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
|
12
|
+
date: 2009-07-30 00:00:00 -07:00
|
13
|
+
default_executable: captain
|
14
|
+
dependencies:
|
15
|
+
- !ruby/object:Gem::Dependency
|
16
|
+
name: matthewtodd-shoe
|
17
|
+
type: :development
|
18
|
+
version_requirement:
|
19
|
+
version_requirements: !ruby/object:Gem::Requirement
|
20
|
+
requirements:
|
21
|
+
- - ">="
|
22
|
+
- !ruby/object:Gem::Version
|
23
|
+
version: "0"
|
24
|
+
version:
|
25
|
+
- !ruby/object:Gem::Dependency
|
26
|
+
name: cucumber
|
27
|
+
type: :development
|
28
|
+
version_requirement:
|
29
|
+
version_requirements: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: "0"
|
34
|
+
version:
|
35
|
+
description:
|
36
|
+
email: matthew.todd@gmail.com
|
37
|
+
executables:
|
38
|
+
- captain
|
39
|
+
extensions: []
|
40
|
+
|
41
|
+
extra_rdoc_files:
|
42
|
+
- README.rdoc
|
43
|
+
files:
|
44
|
+
- Rakefile
|
45
|
+
- README.rdoc
|
46
|
+
- bin/captain
|
47
|
+
- features/installer_image.feature
|
48
|
+
- features/steps
|
49
|
+
- features/steps/captain_steps.rb
|
50
|
+
- features/support
|
51
|
+
- features/support/env.rb
|
52
|
+
- features/support/helpers.rb
|
53
|
+
- lib/captain
|
54
|
+
- lib/captain/application.rb
|
55
|
+
- lib/captain/image.rb
|
56
|
+
- lib/captain/package.rb
|
57
|
+
- lib/captain/package_list.rb
|
58
|
+
- lib/captain/release.rb
|
59
|
+
- lib/captain/remote.rb
|
60
|
+
- lib/captain/resource.rb
|
61
|
+
- lib/captain.rb
|
62
|
+
- resources/disk_base_components.erb
|
63
|
+
- resources/disk_base_installable.erb
|
64
|
+
- resources/disk_cd_type.erb
|
65
|
+
- resources/disk_info.erb
|
66
|
+
- resources/disk_udeb_include.erb
|
67
|
+
- resources/isolinux.bin
|
68
|
+
- resources/isolinux.cfg
|
69
|
+
- resources/preseed.seed.erb
|
70
|
+
- resources/release.erb
|
71
|
+
- resources/release_component.erb
|
72
|
+
has_rdoc: false
|
73
|
+
homepage:
|
74
|
+
licenses:
|
75
|
+
post_install_message:
|
76
|
+
rdoc_options:
|
77
|
+
- --main
|
78
|
+
- README.rdoc
|
79
|
+
- --title
|
80
|
+
- captain-0.1.0
|
81
|
+
- --inline-source
|
82
|
+
require_paths:
|
83
|
+
- lib
|
84
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
85
|
+
requirements:
|
86
|
+
- - ">="
|
87
|
+
- !ruby/object:Gem::Version
|
88
|
+
version: "0"
|
89
|
+
version:
|
90
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
91
|
+
requirements:
|
92
|
+
- - ">="
|
93
|
+
- !ruby/object:Gem::Version
|
94
|
+
version: "0"
|
95
|
+
version:
|
96
|
+
requirements:
|
97
|
+
- mkisofs
|
98
|
+
rubyforge_project:
|
99
|
+
rubygems_version: 1.3.5
|
100
|
+
signing_key:
|
101
|
+
specification_version: 3
|
102
|
+
summary: Builds an Ubuntu installation CD just as you like it.
|
103
|
+
test_files: []
|
104
|
+
|