├── actions ├── shell.rb └── getactions.rb ├── lib ├── ec2boot │ ├── data.rb │ ├── config.rb │ ├── userdata.rb │ ├── actions.rb │ ├── metadata.rb │ └── util.rb └── ec2boot.rb ├── process-ec-meta-data.rb ├── ec2-boot-init.rb ├── motd.unprovisioned ├── motd.provisioned ├── COPYING ├── README.markdown ├── ec2-boot-init.spec ├── ec2-boot-init.init └── Rakefile /actions/shell.rb: -------------------------------------------------------------------------------- 1 | newaction("shell") do |cmd, ud, md, config| 2 | if cmd.include?(:command) 3 | system(cmd[:command]) 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /lib/ec2boot/data.rb: -------------------------------------------------------------------------------- 1 | module EC2Boot 2 | class Data 3 | def initialize(config) 4 | @fetched = false 5 | @config = config 6 | end 7 | 8 | def fetched? 9 | @fetched 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /process-ec-meta-data.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/ruby 2 | 3 | require 'pp' 4 | require 'ec2boot' 5 | 6 | config = EC2Boot::Config.new 7 | ud = EC2Boot::UserData.new(config) 8 | md = EC2Boot::MetaData.new(config) 9 | 10 | EC2Boot::Util.write_facts(ud, md, config) 11 | 12 | config.actions.run_actions(ud, md, config) 13 | 14 | -------------------------------------------------------------------------------- /ec2-boot-init.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/ruby 2 | 3 | require 'ec2boot' 4 | 5 | config = EC2Boot::Config.new 6 | ud = EC2Boot::UserData.new(config) 7 | md = EC2Boot::MetaData.new(config) 8 | 9 | EC2Boot::Util.write_facts(ud, md, config) 10 | 11 | config.actions.run_actions(ud, md, config) 12 | 13 | EC2Boot::Util.update_motd(ud, md, config) if ud.fetched? && md.fetched? 14 | -------------------------------------------------------------------------------- /motd.unprovisioned: -------------------------------------------------------------------------------- 1 | _ _ _ _ _ 2 | | | | |_ _ _ __ _ _ _____ _(_)__(_)___ _ _ ___ __| | 3 | | |_| | ' \| '_ \ '_/ _ \ V / (_-< / _ \ ' \/ -_) _` | 4 | \___/|_||_| .__/_| \___/\_/|_/__/_\___/_||_\___\__,_| 5 | |_| 6 | 7 | Unprovisioned Amazon instance. The ec2-boot-init 8 | system failed to run or has not yet been run 9 | 10 | -------------------------------------------------------------------------------- /motd.provisioned: -------------------------------------------------------------------------------- 1 | ___ _ _ _ 2 | | _ \_ _ _____ _(_)__(_)___ _ _ ___ __| | 3 | | _/ '_/ _ \ V / (_-< / _ \ ' \/ -_) _` | 4 | |_| |_| \___/\_/|_/__/_\___/_||_\___\__,_| 5 | 6 | Successfully provisioned Amazon instance. 7 | 8 | ami: @@ami_id@@ 9 | type: @@instance_type@@ 10 | zone: @@placement_availability_zone@@ 11 | hostname: @@hostname@@ 12 | public hostname: @@public_hostname@@ 13 | 14 | System facts are in /etc/facts.txt 15 | 16 | -------------------------------------------------------------------------------- /lib/ec2boot.rb: -------------------------------------------------------------------------------- 1 | require 'net/http' 2 | require 'net/https' 3 | require 'yaml' 4 | require 'fileutils' 5 | 6 | module EC2Boot 7 | class URLFetchFailed e 29 | Util.log("Failed to run action #{type}: #{e.class}: #{e}") 30 | end 31 | else 32 | # no such method 33 | end 34 | else 35 | # no type 36 | end 37 | end 38 | else 39 | # no :actions 40 | end 41 | else 42 | # not a hash 43 | end 44 | end 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /README.markdown: -------------------------------------------------------------------------------- 1 | What is it? 2 | =========== 3 | 4 | A script to help bootstrap generic EC2 instances into specific ones. 5 | 6 | The basic idea is that you submit some user data in the form: 7 | 8 |
 9 | ---
10 | :facts:
11 |   foo: bar
12 | :write_ec2_facts: true
13 | :actions:
14 | - :type: :getactions
15 |   :url: http://your.net/ec2actions/
16 |   :list: list.txt
17 | - :type: :puppet
18 |   :master: puppet.your.net
19 | 
20 | 21 | This script when run from your init system will: 22 | 23 | * Get list of actions ("list.txt") from action root URL "http://your.net/ec2actions/" 24 | * Run action "puppet" passing the variable master into it (example only) 25 | 26 | The getactions action is shipped with this script. You can supply 27 | your own actions in the action list ("list.txt"). Actions should be 28 | supplied relative to action root URL ("http://your.net/ec2actions/") 29 | so that e.g. action "puppet.rb" in action list "list.txt" is located 30 | at "http://your.net/ec2actions/puppet.rb". 31 | 32 | You should use this to do the basic bootstrap actions like get Puppet 33 | on your node, do the basic setup etc. From there you'd configure the 34 | machine using Puppet. 35 | 36 | 37 | Actions: 38 | -------- 39 | 40 | Actions are written in ruby, here's a simple one that execute a 41 | shell command: 42 | 43 |
44 | newaction("shell") do |cmd, ud, md, config|
45 |     if cmd.include?(:command)
46 |         system(cmd[:command])
47 |     end
48 | end
49 | 
50 | 51 | Facts: 52 | ------ 53 | 54 | Facts passed in via the YAML will be written to /etc/facts.txt, if you set 55 | :write_ec2_facts it will also write all the ec2 meta data into this file 56 | 57 | Status: 58 | ------- 59 | 60 | This is very early work-in-progress chunk of code. 61 | -------------------------------------------------------------------------------- /ec2-boot-init.spec: -------------------------------------------------------------------------------- 1 | %define ruby_sitelib %(ruby -rrbconfig -e "puts Config::CONFIG['sitelibdir']") 2 | %define release %{rpm_release}%{?dist} 3 | 4 | Summary: EC2 Bootstrap System 5 | Name: ec2-boot-init 6 | Version: %{version} 7 | Release: %{release} 8 | Group: System Tools 9 | License: Apache License, Version 2 10 | URL: http://www.devco.net/ 11 | Source0: %{name}-%{version}.tgz 12 | BuildRoot: %{_tmppath}/%{name}-%{version}-%{release}-root-%(%{__id_u} -n) 13 | Requires: ruby 14 | Packager: R.I.Pienaar 15 | BuildArch: noarch 16 | 17 | %description 18 | Bootstrap system for EC2 based systems. 19 | 20 | %prep 21 | %setup -q 22 | 23 | %build 24 | 25 | %install 26 | rm -rf %{buildroot} 27 | %{__install} -d -m0755 %{buildroot}/%{ruby_sitelib}/ec2boot 28 | %{__install} -d -m0755 %{buildroot}/usr/sbin 29 | %{__install} -d -m0755 %{buildroot}/etc/init.d 30 | %{__install} -d -m0755 %{buildroot}/etc/ec2-boot-init 31 | %{__install} -d -m0755 %{buildroot}/etc/ec2-boot-init/actions 32 | %{__install} -m0755 ec2-boot-init.rb %{buildroot}/usr/sbin/ec2-boot-init 33 | %{__install} -m0755 ec2-boot-init.init %{buildroot}/etc/init.d/ec2-boot-init 34 | %{__install} -m0644 motd.provisioned %{buildroot}/etc/ec2-boot-init/motd.provisioned 35 | %{__install} -m0644 motd.unprovisioned %{buildroot}/etc/ec2-boot-init/motd.unprovisioned 36 | 37 | 38 | cp -R lib/* %{buildroot}/%{ruby_sitelib}/ 39 | cp -R actions/* %{buildroot}/etc/ec2-boot-init/actions/ 40 | 41 | %clean 42 | rm -rf %{buildroot} 43 | 44 | %post 45 | cp /etc/ec2-boot-init/motd.unprovisioned /etc/motd 46 | /sbin/chkconfig --add ec2-boot-init || : 47 | 48 | %postun 49 | if [ "$1" -ge 1 ]; then 50 | /sbin/service ec2-boot-init condrestart &>/dev/null || : 51 | fi 52 | 53 | %preun 54 | if [ "$1" = 0 ] ; then 55 | /sbin/chkconfig --del ec2-boot-init || : 56 | fi 57 | 58 | %files 59 | %doc COPYING 60 | /usr/sbin/ec2-boot-init 61 | /etc/ec2-boot-init 62 | /etc/init.d/ec2-boot-init 63 | %{ruby_sitelib}/ec2boot.rb 64 | %{ruby_sitelib}/ec2boot 65 | 66 | %changelog 67 | * Tue Nov 03 2009 R.I.Pienaar 68 | - First release 69 | -------------------------------------------------------------------------------- /ec2-boot-init.init: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # ec2-boot-init Bootstrapper for EC2 4 | # 5 | # chkconfig: 345 60 76 6 | # 7 | # description: Bootstraps EC2 instances in a way that enables you to 8 | # customise a standard image into different configurations 9 | # via structured user data 10 | # 11 | ### BEGIN INIT INFO 12 | # Provides: ec2-boot-init 13 | # Required-Start: $network $syslog 14 | # Required-Stop: $network $syslog 15 | # Default-Start: 2 3 4 5 16 | # Default-Stop: 0 1 6 17 | # Short-Description: Start daemon at boot time 18 | # Description: Enable service provided by daemon. 19 | ### END INIT INFO 20 | 21 | prog="/usr/sbin/ec2-boot-init" 22 | 23 | # Lockfile 24 | if [ -d /var/lock/subsys ]; then 25 | # RedHat/CentOS/etc who use subsys 26 | lock="/var/lock/subsys/ec2-boot-init" 27 | else 28 | # The rest of them 29 | lock="/var/lock/ec2-boot-init" 30 | fi 31 | 32 | 33 | # Source function library. 34 | . /lib/lsb/init-functions 35 | 36 | # Check that binary exists 37 | if ! [ -f $prog ] 38 | then 39 | echo "ec2-boot-init binary not found" 40 | exit 0 41 | fi 42 | 43 | # See how we were called. 44 | case "$1" in 45 | start) 46 | echo -n "Starting ec2-boot-init: " 47 | 48 | cp /etc/ec2-boot-init/motd.unprovisioned /etc/motd 49 | 50 | ${prog} 51 | 52 | if [ $? = 0 ]; then 53 | log_success_msg 54 | touch $lock 55 | exit 0 56 | else 57 | log_failure_msg 58 | exit 1 59 | fi 60 | ;; 61 | stop) 62 | log_success_msg 63 | rm -f $lock 64 | ;; 65 | restart) 66 | $0 stop 67 | sleep 2 68 | $0 start 69 | ;; 70 | condrestart) 71 | if [ -f $lock ]; then 72 | $0 stop 73 | # avoid race 74 | sleep 2 75 | $0 start 76 | fi 77 | ;; 78 | status) 79 | if [ -f ${lock} ]; then 80 | echo "ec2-boot-init ran" 81 | exit 0 82 | else 83 | echo "ec2-boot-init: service not started" 84 | exit 1 85 | fi 86 | ;; 87 | force-reload) 88 | echo "not implemented" 89 | ;; 90 | *) 91 | echo "Usage: ec2-boot-init {start|stop|restart|condrestart|status}" 92 | exit 1 93 | ;; 94 | esac 95 | exit 0 96 | -------------------------------------------------------------------------------- /lib/ec2boot/metadata.rb: -------------------------------------------------------------------------------- 1 | module EC2Boot 2 | class MetaData [:clean, :doc, :package, :rpm] 41 | 42 | # task for building docs 43 | rd = Rake::RDocTask.new(:doc) { |rdoc| 44 | announce "Building documentation for #{CURRENT_VERSION}" 45 | 46 | rdoc.rdoc_dir = 'doc' 47 | rdoc.template = 'html' 48 | rdoc.title = "#{PROJ_DOC_TITLE} version #{CURRENT_VERSION}" 49 | rdoc.options << '--line-numbers' << '--inline-source' << '--main' << 'MCollective' 50 | } 51 | 52 | desc "Create a tarball for this release" 53 | task :package => [:clean, :doc] do 54 | announce "Creating #{PROJ_NAME}-#{CURRENT_VERSION}.tgz" 55 | 56 | FileUtils.mkdir_p("build/#{PROJ_NAME}-#{CURRENT_VERSION}") 57 | system("cp -R #{PROJ_FILES.join(' ')} build/#{PROJ_NAME}-#{CURRENT_VERSION}") 58 | system("cd build && tar --exclude .svn -cvzf #{PROJ_NAME}-#{CURRENT_VERSION}.tgz #{PROJ_NAME}-#{CURRENT_VERSION}") 59 | end 60 | 61 | desc "Creates a RPM" 62 | task :rpm => [:clean, :doc, :package] do 63 | announce("Building RPM for #{PROJ_NAME}-#{CURRENT_VERSION}-#{CURRENT_RELEASE}") 64 | 65 | sourcedir = `rpm --eval '%_sourcedir'`.chomp 66 | specsdir = `rpm --eval '%_specdir'`.chomp 67 | srpmsdir = `rpm --eval '%_srcrpmdir'`.chomp 68 | rpmdir = `rpm --eval '%_rpmdir'`.chomp 69 | lsbdistrel = `lsb_release -r -s | cut -d . -f1`.chomp 70 | lsbdistro = `lsb_release -i -s`.chomp 71 | 72 | case lsbdistro 73 | when 'CentOS' 74 | rpmdist = ".el#{lsbdistrel}" 75 | when 'AmazonAMI' 76 | rpmdist = '.amzn1' 77 | else 78 | rpmdist = "" 79 | end 80 | 81 | system %{cp build/#{PROJ_NAME}-#{CURRENT_VERSION}.tgz #{sourcedir}} 82 | system %{cat #{PROJ_NAME}.spec|sed -e s/%{rpm_release}/#{CURRENT_RELEASE}/g | sed -e s/%{version}/#{CURRENT_VERSION}/g > #{specsdir}/#{PROJ_NAME}.spec} 83 | system %{cd #{specsdir} && rpmbuild -D 'version #{CURRENT_VERSION}' -D 'rpm_release #{CURRENT_RELEASE}' -D 'dist #{rpmdist}' -ba #{PROJ_NAME}.spec} 84 | 85 | system %{cp #{srpmsdir}/#{PROJ_NAME}-#{CURRENT_VERSION}-#{CURRENT_RELEASE}#{rpmdist}.src.rpm build/} 86 | 87 | system %{cp #{rpmdir}/*/#{PROJ_NAME}*-#{CURRENT_VERSION}-#{CURRENT_RELEASE}#{rpmdist}.*.rpm build/} 88 | end 89 | 90 | # vi:tabstop=4:expandtab:ai 91 | -------------------------------------------------------------------------------- /lib/ec2boot/util.rb: -------------------------------------------------------------------------------- 1 | module EC2Boot 2 | class Util 3 | # Fetches a url, it will retry 5 times if it still 4 | # failed it will return "" 5 | # 6 | # If an optional file is specified it will write 7 | # the retrieved data into the file in an efficient way 8 | # in this case return data will be true or false 9 | # 10 | # raises URLNotFound for 404s and URLFetchFailed for 11 | # other non 200 status codes 12 | def self.get_url(url, file=nil) 13 | uri = URI.parse(url) 14 | http = Net::HTTP.new(uri.host, uri.port) 15 | http.use_ssl = (uri.scheme == 'https') 16 | 17 | retries = 5 18 | 19 | begin 20 | if file 21 | dest_file = File.open(file, "w") 22 | response = http.get(uri.path) do |r| 23 | dest_file.write r 24 | end 25 | dest_file.close 26 | else 27 | response = http.get(uri.path) 28 | end 29 | 30 | raise URLNotFound if response.code == "404" 31 | raise URLFetchFailed, "#{url}: #{response.code}" unless response.code == "200" 32 | 33 | if response.code == "200" 34 | if file 35 | return true 36 | else 37 | return response.body 38 | end 39 | else 40 | if file 41 | return false 42 | else 43 | return "" 44 | end 45 | end 46 | rescue Timeout::Error => e 47 | retries -= 1 48 | sleep 1 49 | retry if retries > 0 50 | 51 | rescue URLFetchFailed => e 52 | retries -= 1 53 | sleep 1 54 | retry if retries > 0 55 | end 56 | end 57 | 58 | # Logs to stdout and syslog 59 | def self.log(msg) 60 | puts "#{Time.now}> #{msg}" 61 | if msg.strip.length > 0 62 | system("logger #{msg}") 63 | end 64 | end 65 | 66 | # updates the motd, updates all @@foo@@ variables 67 | # with data from the facts 68 | def self.update_motd(ud, md, config) 69 | templ = File.readlines(config.motd_template) 70 | 71 | File.open(config.motd_file, "w") do |motd| 72 | templ.each do |line| 73 | if md.fetched? 74 | line.gsub!(/@@ami_id@@/, md.flat_data["ami_id"]) 75 | line.gsub!(/@@instance_type@@/, md.flat_data["instance_type"]) 76 | line.gsub!(/@@placement_availability_zone@@/, md.flat_data["placement_availability_zone"]) 77 | line.gsub!(/@@hostname@@/, md.flat_data["hostname"]) 78 | line.gsub!(/@@public_hostname@@/, md.flat_data["public_hostname"]) 79 | end 80 | 81 | motd.write line 82 | end 83 | end 84 | end 85 | 86 | # writes out the facts file 87 | def self.write_facts(ud, md, config) 88 | write_ec2_facts = false 89 | 90 | File.open(config.facts_file, "w") do |facts| 91 | if ud.fetched? 92 | if ud.user_data[:facts] 93 | ud.user_data[:facts].each_pair do |k,v| 94 | facts.puts("#{k}=#{v}") 95 | end 96 | end 97 | end 98 | 99 | # only write the ec2 facts if configured to do so 100 | # since it seems puppet has fixed its broken facts 101 | if md.fetched? && ud.user_data[:write_ec2_facts] 102 | data = md.flat_data 103 | 104 | data.keys.sort.each do |k| 105 | facts.puts("ec2_#{k}=#{data[k]}") 106 | end 107 | 108 | if data.include?("placement_availability_zone") 109 | facts.puts("ec2_placement_region=" + data["placement_availability_zone"].chop) 110 | end 111 | end 112 | end 113 | end 114 | end 115 | end 116 | --------------------------------------------------------------------------------