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.
@@ -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,17 @@
1
+ module Bootscript
2
+
3
+ class UUWriter
4
+
5
+ attr_reader :bytes_written
6
+
7
+ def initialize(output)
8
+ @output = output
9
+ @bytes_written = 0
10
+ end
11
+
12
+ def write(data)
13
+ @bytes_written += @output.write [data].pack('m')
14
+ end
15
+ end
16
+
17
+ end
@@ -0,0 +1,3 @@
1
+ module Bootscript
2
+ VERSION = "0.2.1"
3
+ 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__
@@ -0,0 +1,7 @@
1
+ <%=
2
+ if defined? chef_attributes
3
+ JSON.pretty_generate(chef_attributes)
4
+ else
5
+ '{}'
6
+ end
7
+ %>