bootscript 0.2.1

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