bootscript 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.
- checksums.yaml +15 -0
- data/.gitignore +17 -0
- data/.rspec +1 -0
- data/ERB_VARS.md +30 -0
- data/Gemfile +2 -0
- data/LICENSE.txt +22 -0
- data/README.md +176 -0
- data/Rakefile +8 -0
- data/bootscript.gemspec +31 -0
- data/lib/bootscript.rb +80 -0
- data/lib/bootscript/chef.rb +70 -0
- data/lib/bootscript/script.rb +178 -0
- data/lib/bootscript/uu_writer.rb +17 -0
- data/lib/bootscript/version.rb +3 -0
- data/lib/templates/bootscript.ps1.erb +91 -0
- data/lib/templates/bootscript.sh.erb +154 -0
- data/lib/templates/chef/attributes.json.erb +7 -0
- data/lib/templates/chef/chef-install.ps1.erb +126 -0
- data/lib/templates/chef/chef-install.sh.erb +72 -0
- data/lib/templates/chef/chef_client.conf.erb +9 -0
- data/lib/templates/chef/json_attributes.rb.erb +4 -0
- data/lib/templates/chef/ramdisk_secrets.rb.erb +2 -0
- data/lib/templates/windows_footer.bat.erb +4 -0
- data/lib/templates/windows_header.bat.erb +18 -0
- data/spec/bootscript/chef_spec.rb +48 -0
- data/spec/bootscript/script_spec.rb +126 -0
- data/spec/bootscript/uu_writer_spec.rb +30 -0
- data/spec/bootscript_spec.rb +71 -0
- data/spec/spec_helper.rb +8 -0
- data/spec/unpacker.rb +40 -0
- metadata +208 -0
@@ -0,0 +1,178 @@
|
|
1
|
+
module Bootscript
|
2
|
+
|
3
|
+
require 'fileutils'
|
4
|
+
require 'erubis'
|
5
|
+
require 'json'
|
6
|
+
require 'tmpdir'
|
7
|
+
require 'zlib'
|
8
|
+
require 'archive/tar/minitar'
|
9
|
+
require 'zip'
|
10
|
+
|
11
|
+
# Main functional class. Models and builds a self-extracting Bash/TAR file.
|
12
|
+
class Script
|
13
|
+
|
14
|
+
# A Hash of data sources to be written onto the boot target's filesystem.
|
15
|
+
# Each (String) key is a path to the desired file on the boot target.
|
16
|
+
# Each value can be a String (treated as ERB), or Object with a read method.
|
17
|
+
# Any Ruby File objects with extension ".ERB" are also processed as ERB.
|
18
|
+
attr_accessor :data_map
|
19
|
+
|
20
|
+
# Standard Ruby Logger, overridden by passing :logger to {#initialize}
|
21
|
+
attr_reader :log
|
22
|
+
|
23
|
+
# constructor - configures the AWS S3 connection and logging
|
24
|
+
# @param logger [::Logger] - a standard Ruby logger
|
25
|
+
def initialize(logger = nil)
|
26
|
+
@log ||= logger || Bootscript.default_logger
|
27
|
+
@data_map = Hash.new
|
28
|
+
@vars = Hash.new
|
29
|
+
end
|
30
|
+
|
31
|
+
# Generates the BootScript contents by interpreting the @data_map
|
32
|
+
# based on erb_vars. If destination has a write() method,
|
33
|
+
# the data is streamed there line-by-line, and the number of bytes written
|
34
|
+
# is returned. Otherwise, the BootScript contents are returned as a String.
|
35
|
+
# In the case of streaming output, the destination must be already opened.
|
36
|
+
# @param erb_vars [Hash] Ruby variables to interpolate into all templates
|
37
|
+
# @param destination [IO] a Ruby object that responds to write(String)
|
38
|
+
# @return [Fixnum] the number of bytes written to the destination, or
|
39
|
+
# @return [String] the text of the rendered script, if destination is nil
|
40
|
+
def generate(erb_vars = {}, destination = nil)
|
41
|
+
# Set state / instance variables, used by publish() and helper methods
|
42
|
+
@vars = Bootscript.merge_platform_defaults(erb_vars)
|
43
|
+
output = destination || StringIO.open(@script_data = "")
|
44
|
+
@bytes_written = 0
|
45
|
+
if Bootscript.windows?(@vars)
|
46
|
+
@bytes_written += output.write(render_erb_text(File.read(
|
47
|
+
"#{File.dirname(__FILE__)}/../templates/windows_header.bat.erb"
|
48
|
+
)))
|
49
|
+
end
|
50
|
+
write_bootscript(output) # streams the script part line-by-line
|
51
|
+
write_uuencoded_archive(output) # streams the archive line-by-line
|
52
|
+
if Bootscript.windows?(@vars)
|
53
|
+
@bytes_written += output.write(render_erb_text(File.read(
|
54
|
+
"#{File.dirname(__FILE__)}/../templates/windows_footer.bat.erb"
|
55
|
+
)))
|
56
|
+
end
|
57
|
+
output.close unless destination # (close StringIO if it was opened)
|
58
|
+
return (destination ? @bytes_written : @script_data)
|
59
|
+
end
|
60
|
+
|
61
|
+
private
|
62
|
+
|
63
|
+
# Streams the bootscript to destination and updates @bytes_written
|
64
|
+
def write_bootscript(destination)
|
65
|
+
# If streaming, send the top-level script line-by-line from memory
|
66
|
+
if Bootscript.windows?(@vars)
|
67
|
+
template_path = Bootscript::WINDOWS_TEMPLATE
|
68
|
+
else
|
69
|
+
template_path = Bootscript::UNIX_TEMPLATE
|
70
|
+
end
|
71
|
+
template = File.read(template_path)
|
72
|
+
template = strip_shell_comments(template) if @vars[:strip_comments]
|
73
|
+
@log.debug "Rendering boot script to #{destination}..."
|
74
|
+
render_erb_text(template).each_line do |ln|
|
75
|
+
destination.write ln
|
76
|
+
@bytes_written += ln.bytes.count
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
# Streams the uuencoded archive to destination, updating @bytes_written
|
81
|
+
def write_uuencoded_archive(destination)
|
82
|
+
@log.debug "Writing #{@vars[:platform]} archive to #{destination}..."
|
83
|
+
if Bootscript.windows?(@vars)
|
84
|
+
@bytes_written += destination.write("$archive = @'\n")
|
85
|
+
write_windows_archive(destination)
|
86
|
+
@bytes_written += destination.write("'@\n")
|
87
|
+
else # :platform = 'unix'
|
88
|
+
@bytes_written += destination.write("begin-base64 0600 bootstrap.tbz\n")
|
89
|
+
write_unix_archive(destination)
|
90
|
+
@bytes_written += destination.write("====\n") # (base64 footer)
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
# Streams a uuencoded TGZ archive to destination, updating @bytes_written
|
95
|
+
def write_unix_archive(destination)
|
96
|
+
begin
|
97
|
+
uuencode = UUWriter.new(destination)
|
98
|
+
gz = Zlib::GzipWriter.new(uuencode)
|
99
|
+
tar = Archive::Tar::Minitar::Writer.open(gz)
|
100
|
+
render_data_map_into(tar)
|
101
|
+
ensure
|
102
|
+
tar.close
|
103
|
+
gz.close
|
104
|
+
@bytes_written += uuencode.bytes_written
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
# Streams a uuencoded ZIP archive to destination, updating @bytes_written
|
109
|
+
def write_windows_archive(destination)
|
110
|
+
Dir.mktmpdir do |dir|
|
111
|
+
zip_path = "#{dir}/archive.zip"
|
112
|
+
zipfile = File.open(zip_path, 'wb')
|
113
|
+
Zip::OutputStream.open(zipfile) {|zip| render_data_map_into(zip)}
|
114
|
+
zipfile.close
|
115
|
+
@log.debug "zipfile = #{zip_path}, length = #{File.size zip_path}"
|
116
|
+
File.open(zip_path, 'rb') do |zipfile|
|
117
|
+
@bytes_written += destination.write([zipfile.read].pack 'm')
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
# renders each data map item into an 'archive', which must be either an
|
123
|
+
# Archive::Tar::Minitar::Writer (if unix), or a Zip::OutputStream (windows)
|
124
|
+
def render_data_map_into(archive)
|
125
|
+
full_data_map.each do |remote_path, item|
|
126
|
+
if item.is_a? String # case 1: data item is a String
|
127
|
+
@log.debug "Rendering ERB data (#{item[0..16]}...) into archive"
|
128
|
+
data = render_erb_text(item)
|
129
|
+
input = StringIO.open(data, 'r')
|
130
|
+
size = data.bytes.count
|
131
|
+
elsif item.is_a?(File) # case 2: data item is an ERB file
|
132
|
+
if item.path.upcase.sub(/\A.*\./,'') == 'ERB'
|
133
|
+
@log.debug "Rendering ERB file #{item.path} into archive"
|
134
|
+
data = render_erb_text(item.read)
|
135
|
+
input = StringIO.open(data, 'r')
|
136
|
+
size = data.bytes.count
|
137
|
+
else # case 3: data item is a regular File
|
138
|
+
@log.debug "Copying data from #{item.inspect} into archive"
|
139
|
+
input = item
|
140
|
+
size = File.stat(item).size
|
141
|
+
end
|
142
|
+
else # case 4: Error
|
143
|
+
raise ArgumentError.new("cannot process item: #{item}")
|
144
|
+
end
|
145
|
+
if Bootscript.windows?(@vars)
|
146
|
+
archive.put_next_entry remote_path
|
147
|
+
archive.write input.read
|
148
|
+
else
|
149
|
+
opts = {mode: 0600, size: size, mtime: Time.now}
|
150
|
+
archive.add_file_simple(remote_path, opts) do |output|
|
151
|
+
while data = input.read(512) ; output.write data end
|
152
|
+
end
|
153
|
+
end
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
# merges the @data_map with the Chef built-ins, as-needed
|
158
|
+
def full_data_map
|
159
|
+
Bootscript::Chef.included?(@vars) ?
|
160
|
+
@data_map.merge(Bootscript::Chef.files(@vars)) : @data_map
|
161
|
+
end
|
162
|
+
|
163
|
+
# renders erb_text, using @vars
|
164
|
+
def render_erb_text(erb_text)
|
165
|
+
Erubis::Eruby.new(erb_text).result(@vars)
|
166
|
+
end
|
167
|
+
|
168
|
+
# strips all empty lines and lines beginning with # from text
|
169
|
+
# does NOT touch the first line of text
|
170
|
+
def strip_shell_comments(text)
|
171
|
+
lines = text.lines.to_a
|
172
|
+
return text if lines.count < 2
|
173
|
+
lines.first + lines[1..lines.count].
|
174
|
+
reject{|l| (l =~ /^\s*#/) || (l =~ /^\s+$/)}.join('')
|
175
|
+
end
|
176
|
+
end
|
177
|
+
|
178
|
+
end
|
@@ -0,0 +1,91 @@
|
|
1
|
+
$startup_cmd = '<%= startup_command %>'
|
2
|
+
$createRAMDisk = $<%= create_ramdisk.to_s.upcase %>
|
3
|
+
$RAMDiskSize = "<%= ramdisk_size %>M"
|
4
|
+
$RAMDiskMount = "<%= ramdisk_mount %>"
|
5
|
+
$IMDiskURL = "<%= imdisk_url %>"
|
6
|
+
$IMDisk = "C:\windows\system32\imdisk.exe"
|
7
|
+
$IMDIskInstaller = "C:\imdiskinst.exe"
|
8
|
+
|
9
|
+
function main()
|
10
|
+
{
|
11
|
+
echo "Starting bootstrap..."
|
12
|
+
Decrypt-Archive
|
13
|
+
if ($createRAMDisk)
|
14
|
+
{
|
15
|
+
Download-ImDisk
|
16
|
+
Install-ImDisk
|
17
|
+
Execute-ImDisk
|
18
|
+
echo "RAMDisk setup complete."
|
19
|
+
}
|
20
|
+
Execute-Command($startup_cmd)
|
21
|
+
echo "Bootstrap complete."
|
22
|
+
}
|
23
|
+
|
24
|
+
function Download-ImDisk()
|
25
|
+
{
|
26
|
+
if ((test-path $IMDIskInstaller) -ne $true)
|
27
|
+
{
|
28
|
+
echo "Downloading ImDisk utility..."
|
29
|
+
$wc = new-object System.Net.WebClient
|
30
|
+
try { $wc.DownloadFile($IMDiskURL, $IMDIskInstaller) }
|
31
|
+
catch { throw $error[0] }
|
32
|
+
}
|
33
|
+
}
|
34
|
+
|
35
|
+
function Install-ImDisk()
|
36
|
+
{
|
37
|
+
if ((test-path $IMDisk) -ne $true)
|
38
|
+
{
|
39
|
+
$Env:IMDISK_SILENT_SETUP = 1
|
40
|
+
Execute-Command("$IMDIskInstaller -y")
|
41
|
+
}
|
42
|
+
}
|
43
|
+
|
44
|
+
function Execute-ImDisk()
|
45
|
+
{
|
46
|
+
if ((test-path $RAMDiskMount) -ne $true)
|
47
|
+
{
|
48
|
+
echo "Setting up $RAMDiskSize RAMDisk at $RAMDiskMount..."
|
49
|
+
$fsArgs = '"/fs:ntfs /q /y"'
|
50
|
+
Execute-Command("$IMDisk -a -t vm -m $RAMDiskMount -s $RAMDiskSize -p $fsArgs")
|
51
|
+
}
|
52
|
+
}
|
53
|
+
|
54
|
+
function Decrypt-Archive()
|
55
|
+
{
|
56
|
+
$archive_path = "C:\bootscript_archive.zip"
|
57
|
+
[io.file]::WriteAllBytes($archive_path,
|
58
|
+
[System.Convert]::FromBase64String($archive))
|
59
|
+
Expand-ZIPFile $archive_path "C:\"
|
60
|
+
Remove-Item $archive_path
|
61
|
+
}
|
62
|
+
|
63
|
+
function Expand-ZIPFile($file, $dest)
|
64
|
+
{
|
65
|
+
$shell = new-object -com shell.application
|
66
|
+
$zip = $shell.NameSpace($file)
|
67
|
+
foreach($item in $zip.items())
|
68
|
+
{ $shell.Namespace($dest).copyhere($item) }
|
69
|
+
}
|
70
|
+
|
71
|
+
function Execute-Command($cmd)
|
72
|
+
{
|
73
|
+
if ($cmd -ne "")
|
74
|
+
{
|
75
|
+
Try {
|
76
|
+
echo "Running: $cmd"
|
77
|
+
$Env:_THIS_CMD = $cmd
|
78
|
+
$proc = Start-Process -FilePath c:\windows\system32\cmd.exe `
|
79
|
+
-ArgumentList "/C", "%_THIS_CMD%" `
|
80
|
+
-Verbose -Debug -Wait -Passthru
|
81
|
+
do { start-sleep -Milliseconds 500 }
|
82
|
+
until ($proc.HasExited)
|
83
|
+
echo "Finished: $cmd"
|
84
|
+
}
|
85
|
+
Catch {
|
86
|
+
echo "Failed: $cmd"
|
87
|
+
echo "Error was: $error[0]"
|
88
|
+
throw $error[0]
|
89
|
+
}
|
90
|
+
}
|
91
|
+
}
|
@@ -0,0 +1,154 @@
|
|
1
|
+
#!/usr/bin/env bash
|
2
|
+
# Top-level bootscript
|
3
|
+
set -e # always stop on errors
|
4
|
+
test $UID == 0 || (echo "ERROR: must run as root"; exit 1)
|
5
|
+
|
6
|
+
####################################
|
7
|
+
#### Step 0 - Configuration
|
8
|
+
UPDATE_OS='<%= update_os %>'
|
9
|
+
CREATE_RAMDISK='<%= create_ramdisk %>'
|
10
|
+
RAMDISK_SIZE='<%= ramdisk_size %>'
|
11
|
+
RAMDISK_MOUNT='<%= ramdisk_mount %>'
|
12
|
+
LOG="/var/log/bootscript.log"
|
13
|
+
|
14
|
+
|
15
|
+
####################################
|
16
|
+
#### Step 1 - Logging control
|
17
|
+
# Log all output to the log file, in addition to STDOUT
|
18
|
+
npipe=/tmp/$$.tmp
|
19
|
+
trap "rm -f $npipe" EXIT
|
20
|
+
mknod $npipe p
|
21
|
+
tee <$npipe $LOG &
|
22
|
+
exec 1>&-
|
23
|
+
exec 1>$npipe 2>&1
|
24
|
+
|
25
|
+
|
26
|
+
####################################
|
27
|
+
#### STEP 2 - Detect linux distro
|
28
|
+
lowercase(){
|
29
|
+
echo "$1" |
|
30
|
+
sed "y/ABCDEFGHIJKLMNOPQRSTUVWXYZ/abcdefghijklmnopqrstuvwxyz/"
|
31
|
+
}
|
32
|
+
OS=`lowercase \`uname\``
|
33
|
+
KERNEL=`uname -r`
|
34
|
+
MACH=`uname -m`
|
35
|
+
if [ "{$OS}" == "windowsnt" ]; then
|
36
|
+
OS=windows
|
37
|
+
elif [ "{$OS}" == "darwin" ]; then
|
38
|
+
OS=mac
|
39
|
+
else
|
40
|
+
OS=`uname`
|
41
|
+
if [ "${OS}" = "SunOS" ] ; then
|
42
|
+
OS=Solaris
|
43
|
+
ARCH=`uname -p`
|
44
|
+
OSSTR="${OS} ${REV}(${ARCH} `uname -v`)"
|
45
|
+
elif [ "${OS}" = "AIX" ] ; then
|
46
|
+
OSSTR="${OS} `oslevel` (`oslevel -r`)"
|
47
|
+
elif [ "${OS}" = "Linux" ] ; then
|
48
|
+
if [ -f /etc/redhat-release ] ; then
|
49
|
+
DistroBasedOn='RedHat'
|
50
|
+
DIST=`cat /etc/redhat-release |sed s/\ release.*//`
|
51
|
+
PSUEDONAME=`cat /etc/redhat-release | sed s/.*\(// | sed s/\)//`
|
52
|
+
REV=`cat /etc/redhat-release | sed s/.*release\ // | sed s/\ .*//`
|
53
|
+
elif [ -f /etc/SuSE-release ] ; then
|
54
|
+
DistroBasedOn='SuSe'
|
55
|
+
PSUEDONAME=`cat /etc/SuSE-release | tr "\n" ' '| sed s/VERSION.*//`
|
56
|
+
REV=`cat /etc/SuSE-release | tr "\n" ' ' | sed s/.*=\ //`
|
57
|
+
elif [ -f /etc/mandrake-release ] ; then
|
58
|
+
DistroBasedOn='Mandrake'
|
59
|
+
PSUEDONAME=`cat /etc/mandrake-release | sed s/.*\(// | sed s/\)//`
|
60
|
+
REV=`cat /etc/mandrake-release | sed s/.*release\ // | sed s/\ .*//`
|
61
|
+
elif [ -f /etc/debian_version ] ; then
|
62
|
+
DistroBasedOn='Debian'
|
63
|
+
DIST=`cat /etc/lsb-release | grep '^DISTRIB_ID' | awk -F= '{ print $2 }'`
|
64
|
+
PSUEDONAME=`cat /etc/lsb-release | grep '^DISTRIB_CODENAME' | awk -F= '{ print $2 }'`
|
65
|
+
REV=`cat /etc/lsb-release | grep '^DISTRIB_RELEASE' | awk -F= '{ print $2 }'`
|
66
|
+
fi
|
67
|
+
if [ -f /etc/UnitedLinux-release ] ; then
|
68
|
+
DIST="${DIST}[`cat /etc/UnitedLinux-release | tr "\n" ' ' | sed s/VERSION.*//`]"
|
69
|
+
fi
|
70
|
+
OS=`lowercase $OS`
|
71
|
+
DistroBasedOn=`lowercase $DistroBasedOn`
|
72
|
+
readonly OS
|
73
|
+
readonly DIST
|
74
|
+
readonly DistroBasedOn
|
75
|
+
readonly PSUEDONAME
|
76
|
+
readonly REV
|
77
|
+
readonly KERNEL
|
78
|
+
readonly MACH
|
79
|
+
fi
|
80
|
+
fi
|
81
|
+
|
82
|
+
####################################
|
83
|
+
#### STEP 3 - OS update
|
84
|
+
if [ "$DistroBasedOn" == 'debian' ] ; then
|
85
|
+
export DEBIAN_FRONTEND=noninteractive
|
86
|
+
if [ "$UPDATE_OS" == 'true' ] ; then
|
87
|
+
echo "Upgrading all OS packages..."
|
88
|
+
apt-get update
|
89
|
+
cmd='apt-get -o Dpkg::Options::="--force-all" --force-yes -y upgrade'
|
90
|
+
$cmd || echo "==> WARNING: Unable to complete operating system package upgrade!"
|
91
|
+
fi
|
92
|
+
fi
|
93
|
+
|
94
|
+
####################################
|
95
|
+
#### STEP 4 - Create RAMdisk
|
96
|
+
if [ "$CREATE_RAMDISK" == 'true' ] ; then
|
97
|
+
if $(mount | grep "$RAMDISK_MOUNT" >/dev/null 2>&1) ; then
|
98
|
+
echo "RAMdisk already exists at ${RAMDISK_MOUNT}..."
|
99
|
+
else
|
100
|
+
echo "Creating $RAMDISK_SIZE RAMdisk at ${RAMDISK_MOUNT}..."
|
101
|
+
if [ -e $RAMDISK_MOUNT ] ; then
|
102
|
+
echo "ERROR: $RAMDISK_MOUNT already exists and is not a RAMdisk!"
|
103
|
+
exit 4
|
104
|
+
fi
|
105
|
+
mkdir -p $RAMDISK_MOUNT
|
106
|
+
mount -t tmpfs -o size=${RAMDISK_SIZE}M tmpfs $RAMDISK_MOUNT
|
107
|
+
fi
|
108
|
+
fi
|
109
|
+
|
110
|
+
|
111
|
+
####################################
|
112
|
+
#### STEP 5 - Check for uudecode, and attempt to install it if needed
|
113
|
+
if ! (which uudecode >/dev/null 2>&1) ; then
|
114
|
+
flavor="$OS / $Dist / $DistroBasedOn"
|
115
|
+
echo "uudecode not found - will attempt installation for $flavor"
|
116
|
+
if [ "$OS" == 'linux' ] ; then
|
117
|
+
if [ "$DistroBasedOn" == 'debian' ] ; then
|
118
|
+
apt-get -y install sharutils
|
119
|
+
else
|
120
|
+
echo "ERROR: Only Debian-derived Linux supported for now :("
|
121
|
+
exit 3
|
122
|
+
fi
|
123
|
+
fi
|
124
|
+
fi
|
125
|
+
|
126
|
+
|
127
|
+
####################################
|
128
|
+
#### STEP 6 - Extract Archive
|
129
|
+
# Cut the trailing part of this file and pipe it to uudecode and tar
|
130
|
+
echo "Extracting the included tar archive..."
|
131
|
+
SCRIPT_PATH="$( cd "$(dirname "$0")" ; pwd -P )/$(basename $0)"
|
132
|
+
ARCHIVE=`awk '/^__ARCHIVE_FOLLOWS__/ {print NR + 1; exit 0; }' $SCRIPT_PATH`
|
133
|
+
cd /
|
134
|
+
tail -n+$ARCHIVE $SCRIPT_PATH | uudecode -o /dev/stdout | tar xovz
|
135
|
+
echo "Removing ${SCRIPT_PATH}..."
|
136
|
+
rm -f $SCRIPT_PATH # this script removes itself!
|
137
|
+
|
138
|
+
|
139
|
+
<% if defined? startup_command %>
|
140
|
+
####################################
|
141
|
+
#### STEP 7 - Execute startup command
|
142
|
+
echo "Executing user startup command..."
|
143
|
+
<% if defined? chef_validation_pem %>
|
144
|
+
chmod 0744 /usr/local/sbin/chef-install.sh
|
145
|
+
<% end %>
|
146
|
+
exec <%= startup_command %>
|
147
|
+
<% end %>
|
148
|
+
exit 0 # This is reached only if there's no user startup command
|
149
|
+
|
150
|
+
|
151
|
+
####################################
|
152
|
+
#### Boot data archive - this gets extracted with `tail`, and
|
153
|
+
#### piped through `uudecode` and `tar` in Step 5, above
|
154
|
+
__ARCHIVE_FOLLOWS__
|