aws-carb 0.0.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,78 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ # monkeypatch shell spinner to fix some bugs..
4
+
5
+ module ShellSpinner
6
+ class Runner
7
+ def wrap_block(text = nil, colorize = true, &block)
8
+ with_message(text) { with_spinner(&block) }
9
+ end
10
+
11
+ private
12
+
13
+ # FIXME: better way to disable colours?
14
+ #colorize = colorize ? lambda { |s,c| s.colorize(c) } : lambda { |s,c| s }
15
+ #colorize.call(s, :red)
16
+
17
+ def with_message(text = nil, colorize = false)
18
+ begin
19
+ print "#{text}... " unless text.nil?
20
+
21
+ catch_user_output { yield }
22
+
23
+ print "done\n".colorize(:green) unless text.nil?
24
+
25
+ print user_output.colorize(:blue)
26
+
27
+ rescue Exception => e
28
+ print "\bfail\n".colorize(:red) unless text.nil?
29
+
30
+ print user_output.colorize(:blue)
31
+
32
+ re_raise_exception e
33
+ end
34
+ end
35
+ end
36
+ end
37
+
38
+ def ShellSpinner(text = nil, colorize = true, &block)
39
+ runner = ShellSpinner::Runner.new
40
+
41
+ runner.wrap_block(text, colorize, &block)
42
+ end
43
+
44
+ # override the output from optparse to be a bit more aesthetically pleasing
45
+ module Subcommands
46
+ def print_actions
47
+ subcommand_help = "#{'SUBCOMMAND'.colorize({:color => :white, :mode => :bold})}\n"
48
+
49
+ @commands.each_pair do |c, opt|
50
+ subcommand_help << "\n #{c} - #{opt.call.description}"
51
+ end
52
+
53
+ unless @aliases.empty?
54
+ subcommand_help << "\n\naliases: \n"
55
+ @aliases.each_pair { |name, val| subcommand_help << " #{name} - #{val}\n" }
56
+ end
57
+
58
+ subcommand_help << "\n\n help <command> - for more information on a specific command\n\n"
59
+ end
60
+
61
+ def command *names
62
+ name = names.shift
63
+
64
+ @commands ||= {}
65
+ @aliases ||= {}
66
+
67
+ names.each { |n| @aliases[n.to_s] = name.to_s } if names.length > 0
68
+
69
+ opt = lambda do
70
+ OptionParser.new do |opts|
71
+ yield opts
72
+ opts.banner << "OPTIONS"
73
+ end
74
+ end
75
+
76
+ @commands[name.to_s] = opt
77
+ end
78
+ end
@@ -0,0 +1,97 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ module AWSCarb
4
+ module Services
5
+ class Ec2
6
+
7
+ include Singleton
8
+
9
+ attr_reader :instance
10
+
11
+ def client(config)
12
+ @config = config
13
+ @instance = nil
14
+
15
+ ShellSpinner "# configuring ec2 session", false do
16
+ begin
17
+ @client = AWS::EC2.new(config[:ec2])
18
+ @client.regions[@config.find_with_context(:region, :ec2)]
19
+ puts
20
+ rescue => e
21
+ puts "error: failed to create ec2 session, check that you're using a valid region!"
22
+ die e
23
+ end
24
+ end
25
+ end
26
+
27
+ def create_instance
28
+
29
+ instance = nil
30
+
31
+ ShellSpinner "# creating instance", false do
32
+
33
+ # FIXME: this is naff
34
+
35
+ begin
36
+ allowed_ec2_parameters = [
37
+ :count,
38
+ :iam_instance_profile,
39
+ :block_device_mappings,
40
+ :virtual_name,
41
+ :device_name,
42
+ :ebs,
43
+ :snapshot_id,
44
+ :volume_size,
45
+ :delete_on_termination,
46
+ :volume_type,
47
+ :iops,
48
+ :no_device,
49
+ :monitoring_enabled,
50
+ :availability_zone,
51
+ :image_id,
52
+ :key_name,
53
+ :key_pair,
54
+ :security_groups,
55
+ :security_group_ids,
56
+ :user_data,
57
+ :instance_type,
58
+ :kernel_id,
59
+ :ramdisk_id,
60
+ :disable_api_termination,
61
+ :instance_initiated_shutdown_behavior,
62
+ :subnet,
63
+ :private_ip_address,
64
+ :dedicated_tenancy,
65
+ :ebs_optimized,
66
+ ]
67
+
68
+ ec2_config = {}
69
+
70
+ allowed_ec2_parameters.each do |param|
71
+ ec2_config[param] = @config[:ec2][param] if @config[:ec2][param]
72
+ end
73
+
74
+ @instance = @client.instances.create(ec2_config)
75
+ rescue => e
76
+ puts "# failed to create new ec2 instance:"
77
+ die e
78
+ end
79
+ end
80
+
81
+ puts
82
+
83
+ ShellSpinner "# awaiting build completion", false do
84
+ sleep 1 until @instance.status != :pending
85
+ end
86
+
87
+ puts
88
+
89
+ ShellSpinner "# awaiting running state", false do
90
+ sleep 1 until @instance.status == :running
91
+ end
92
+
93
+ puts
94
+ end
95
+ end
96
+ end
97
+ end
@@ -0,0 +1,86 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ module AWSCarb
4
+ # named so as not to clash with AWS module from aws-sdk
5
+ module Services
6
+ class Route53
7
+
8
+ include Singleton
9
+
10
+ def client(config)
11
+ @config = config
12
+
13
+ begin
14
+ ShellSpinner "# configuring route53 session", false do
15
+ @client = ::AWS::Route53.new(@config[:route53])
16
+ end
17
+
18
+ puts
19
+ rescue => e
20
+ puts "error: failed to create route53 session"
21
+ die e
22
+ end
23
+ end
24
+
25
+ def check_hostname_and_domain_availability
26
+
27
+ ShellSpinner "# checking to see if hostname and domain have been configured", false do
28
+
29
+ if @config[:route53].andand[:new_dns_records]
30
+
31
+ else
32
+ debug "# skipping route53 check since either hostname or domain wasn't found:"
33
+ debug "hostname not found" if hostname.nil?
34
+ debug "domain not found" if domain.nil?
35
+ debug
36
+ end
37
+ end
38
+
39
+ puts
40
+
41
+ return unless @config[:route53].andand[:new_dns_records]
42
+
43
+ ShellSpinner "# checking to see if record exists", false do
44
+ begin
45
+ record_sets = @client.hosted_zones[@config[:route53][:zone]].resource_record_sets
46
+
47
+ @config[:route53][:new_dns_records].each_value do |record|
48
+ die "error: record already exists: #{record[:alias]}" if record_sets[record[:alias], 'CNAME'].exists?
49
+ end
50
+ rescue => e
51
+ puts "# could not check to see if DNS records exist:"
52
+ die e
53
+ end
54
+ end
55
+
56
+ puts
57
+ end
58
+
59
+ def create_records(ec2)
60
+ if @config[:route53][:new_dns_records].nil?
61
+ debug "# skipping creation of new records on route53"
62
+ debug
63
+ return
64
+ end
65
+
66
+ ShellSpinner "# updating route53 with new CNAMES for host", false do
67
+
68
+ @config[:route53][:new_dns_records][:public][:target] = ec2.instance.public_dns_name
69
+ @config[:route53][:new_dns_records][:private][:target] = ec2.instance.private_dns_name
70
+
71
+ record_sets = @client.hosted_zones[@config[:route53][:zone]].resource_record_sets
72
+
73
+ @config[:route53][:new_dns_records].each do |record_scope, record|
74
+ new_record = record_sets[record[:alias], 'CNAME']
75
+
76
+ raise "error: '#{record_scope}' record already exists: #{record[:alias]}" if new_record.exists?
77
+
78
+ record_sets.create(record[:alias], 'CNAME', :ttl => @config[:route53][:ttl], :resource_records => [{:value => record[:target]}])
79
+ end
80
+ end
81
+
82
+ puts
83
+ end
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,103 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ module AWSCarb
4
+ class UserData
5
+
6
+ include Singleton
7
+
8
+ attr_accessor :combined_user_data
9
+
10
+ def create(config)
11
+ user_data_template_resolved = resolve_template(config)
12
+ @combined_user_data = combine_user_data(config, user_data_template_resolved)
13
+ @user_data_template = nil
14
+ @resolved_template = nil
15
+ end
16
+
17
+ def resolve_template(config)
18
+
19
+ # FIXME: blank templates / empty templates / no template should work..
20
+
21
+ return nil unless config[:ec2] and config[:user_data_template][:file]
22
+
23
+ ShellSpinner "# loading template", false do
24
+ begin
25
+ template_file = config[:user_data_template][:file]
26
+
27
+ raise ArgumentError, "no such file: #{template_file}" unless File.exist?(template_file)
28
+
29
+ @user_data_template = File.read(template_file)
30
+ rescue => e
31
+ puts "# unable to open template file:"
32
+ die e
33
+ end
34
+ end
35
+
36
+ puts
37
+
38
+ ShellSpinner "# parsing template" do
39
+ begin
40
+ @resolved_template = Erubis::Eruby.new(@user_data_template).result(config[:user_data_template_variables])
41
+ rescue => e
42
+ puts "# failed to resolve variables in user_data_template:"
43
+ ap e.class
44
+ die e
45
+ end
46
+ end
47
+
48
+ puts
49
+
50
+ return @resolved_template
51
+ end
52
+
53
+ def combine_user_data(config, user_data_template_resolved)
54
+
55
+ # if user_data_template and user_data are supplied then combine them, otherwise just
56
+ # use user_data (which is empty by default)
57
+ begin
58
+ if config[:ec2].andand[:user_data]
59
+ user_data = config[:ec2][:user_data]
60
+ end
61
+
62
+ if ! user_data_template_resolved.nil? and ! user_data.nil?
63
+ puts "# combining user_data with user_data_template"
64
+ user_data = user_data_template_resolved + user_data
65
+ puts
66
+ elsif ! user_data_template_resolved.nil? and user_data.nil?
67
+ debug "# no raw user_data parsed in"
68
+ user_data = user_data_template_resolved
69
+ debug
70
+ elsif user_data.nil?
71
+ debug "# no user_data or user_data_template specified on the command line"
72
+ user_data = ""
73
+ debug
74
+ else
75
+ debug "# using user_data from cli argument"
76
+ debug
77
+ end
78
+
79
+ rescue => e
80
+ puts "# failed to combine user_data!"
81
+ die e
82
+ end
83
+
84
+ return user_data
85
+ end
86
+
87
+ def display
88
+ return if @combined_user_data.nil?
89
+
90
+ puts "# --- beginning of user_data ---"
91
+ puts
92
+ begin
93
+ puts @combined_user_data
94
+ rescue => e
95
+ puts "error: could not display user_data!"
96
+ puts e
97
+ end
98
+ puts
99
+ puts "# --- end of user_data ---"
100
+ puts
101
+ end
102
+ end
103
+ end
@@ -0,0 +1,3 @@
1
+ module AWSCarb
2
+ VERSION = "0.0.3"
3
+ end
data/lib/aws-carb.rb ADDED
@@ -0,0 +1,129 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'aws-sdk'
4
+ require 'yaml'
5
+ require 'erubis'
6
+ require 'awesome_print'
7
+ require 'securerandom'
8
+ require 'shell-spinner'
9
+ require 'active_support/core_ext/string/strip'
10
+ require 'active_support/core_ext/hash/keys'
11
+ require 'ostruct'
12
+ require 'subcommand'
13
+ require 'singleton'
14
+ require 'andand'
15
+ require 'colorize'
16
+
17
+ # module is broken up into:
18
+ #
19
+ # AWSCarb.* - main methods
20
+ # AWSCarb::CliArugmentParser - argument parsing
21
+ # AWSCarb::Config - argument checking / config checking
22
+ # AWSCarb::UserData - parse user data template and possibly combine with user_data cli arg
23
+ # AWSCarb::Services::Ec2 - build an ec2 instance
24
+ # AWSCarb::Services::Route53 - create dns records in route53
25
+ #
26
+
27
+ if ! $stdout.tty?
28
+ String.class_eval do
29
+ def colorize(args)
30
+ self
31
+ end
32
+ end
33
+ end
34
+
35
+ module AWSCarb
36
+ def self.banner
37
+ banner = <<-HEREDOC.strip_heredoc
38
+
39
+ :::::::: ::: ::::::::: :::::::::
40
+ :+: :+: :+: :+: :+: :+: :+: :+:
41
+ +:+ +:+ +:+ +:+ +:+ +:+ +:+
42
+ +#+ +#++:++#++: +#++:++#: +#++:++#+
43
+ +#+ +#+ +#+ +#+ +#+ +#+ +#+
44
+ #+# #+# #+# #+# #+# #+# #+# #+#
45
+ ######## ### ### ### ### #########
46
+
47
+ - cloudinit and route53 bootstrap -
48
+
49
+ HEREDOC
50
+
51
+ indent = ' ' * 6
52
+
53
+ puts banner.each_line.map { |line| indent + line }
54
+ end
55
+
56
+ def self.run
57
+
58
+ #
59
+ # configuration
60
+ #
61
+
62
+ # parse cli args
63
+ cli_arguments = CliArgumentParser.parse
64
+
65
+ # display banner on successful cli argument parsing..
66
+ banner
67
+
68
+ # create a configuration based on our various data sources..
69
+ @config = Config.instance
70
+ @config.create(cli_arguments)
71
+ @config.display if $GLOBAL_VERBOSE
72
+
73
+ # load erb template and parse the template with user_data_template_variables
74
+ # then merge user_data template with raw user_data (if provided) -
75
+ # end up single user_data ready to pass into ec2 instance..
76
+ @user_data = UserData.instance
77
+ @user_data.create(@config)
78
+ @user_data.display if @config[:user_data_template][:file] and ($GLOBAL_VERBOSE or @config[:show_parsed_template])
79
+
80
+ #
81
+ # aws interaction
82
+ #
83
+
84
+ if @config[:route53].andand[:new_dns_records]
85
+ @route53 = Services::Route53.instance
86
+ @route53.client(@config)
87
+ @route53.check_hostname_and_domain_availability
88
+ end
89
+
90
+ ## initialize ec2 object with credentials
91
+ @ec2 = Services::Ec2.instance
92
+ @ec2.client(@config)
93
+ @ec2.create_instance
94
+
95
+ if @config[:route53].andand[:new_dns_records]
96
+ @route53.create_records(@ec2)
97
+ end
98
+
99
+ #
100
+ # summary
101
+ #
102
+
103
+ show_instance_details
104
+ end
105
+
106
+ def self.show_instance_details
107
+ puts <<-HEREDOC.strip_heredoc
108
+ # instance details:
109
+ id: #{@ec2.instance.id}
110
+ public ip: #{@ec2.instance.public_ip_address}
111
+ public aws fqdn: #{@ec2.instance.public_dns_name}
112
+ private ip: #{@ec2.instance.private_ip_address}
113
+ private aws fqdn: #{@ec2.instance.private_dns_name}
114
+ HEREDOC
115
+
116
+ unless @config[:route53][:new_dns_records].nil?
117
+ puts <<-HEREDOC.strip_heredoc
118
+ public fqdn: #{@config[:route53][:new_dns_records][:public][:alias]}
119
+ private fqdn: #{@config[:route53][:new_dns_records][:private][:alias]}
120
+ HEREDOC
121
+ end
122
+
123
+ puts <<-HEREDOC.strip_heredoc
124
+
125
+ # connect:
126
+ ssh #{@ec2.instance.dns_name} -l ubuntu
127
+ HEREDOC
128
+ end
129
+ end
@@ -0,0 +1,84 @@
1
+ #cloud-config
2
+
3
+ # the above line is required by cloud init
4
+
5
+ <%- if defined?(hostname) %>
6
+ hostname: <%= hostname %>
7
+ fqdn: <%= hostname -%>.<%= domain %>
8
+ <% end %>
9
+
10
+ <%- if defined?(ssh_keys) %>
11
+ ssh_authorized_keys:
12
+ <% ssh_keys.each do |key| %>
13
+ - <%= key %>
14
+ <% end %>
15
+ <% end %>
16
+
17
+ bootcmd:
18
+ - echo "deb http://<%= repository_server -%>/debian/ <%= debian_distro -%> main" >> /etc/apt/<%= apt_source_name -%>.list.d/<% apt_source_name -%>.list
19
+
20
+ package_upgrade: true
21
+
22
+ manage_etc_hosts: true
23
+
24
+ packages:
25
+ - puppet
26
+ - facter
27
+ - lsb-release
28
+
29
+ # required by aws-sdk
30
+ - ruby1.9.1
31
+ - ruby1.9.1-dev
32
+ - libxml2-dev
33
+ - build-essential
34
+
35
+ # required by awscli
36
+ - python-pip
37
+
38
+
39
+ puppet:
40
+ conf:
41
+ main:
42
+ logdir: /var/log/puppet
43
+ vardir: /var/lib/puppet
44
+ rundir: /var/run/puppet
45
+ ssldir: /etc/puppet/ssl
46
+ confdir: /etc/puppet
47
+ pluginsync: true
48
+ factpath: /var/lib/puppet/lib/facter:/var/lib/puppet/facts
49
+ agent:
50
+ server: <%= puppet_master %>
51
+ ca_server: <%= ca_server %>
52
+ configtimeout: 600
53
+ preferred_serialization_format: marshal
54
+
55
+ runcmd:
56
+ - service puppet stop
57
+ - puppet agent --onetime --no-daemonize -v > /var/log/puppet-initial_run.log
58
+ - update-alternatives --set ruby /usr/bin/ruby1.9.1
59
+ - update-alternatives --set gem /usr/bin/gem1.9.1
60
+
61
+ # building the native extensions for this gem takes a long time,
62
+ # if you reboot the machine soon after machine creation
63
+ # then this gem may not have finished installing and your
64
+ # DNS will not get updated on boot.
65
+ - gem install aws-sdk facter --no-ri --no-rdoc
66
+
67
+ # script to update route53 dynamic dns configuration on boot..
68
+
69
+ - pip install awscli
70
+
71
+ # filthy hack..
72
+ - mkdir /root/.aws
73
+ - echo '[profile s3]' >> /root/.aws/config
74
+ - echo 'aws_access_key_id = <%= s3_access_key_id %>' >> /root/.aws/config
75
+ - echo 'aws_secret_access_key = <%= s3_secret_access_key %>' >> /root/.aws/config
76
+ - echo 'region = <%= s3_region %>' >> /root/.aws/config
77
+
78
+ # update route53 on boot
79
+ # available at: https://github.com/roobert/route53-dynamic_dns_update
80
+ - aws s3 --profile s3 cp --region us-east-1 s3://<%= s3_bucket -%>.cloudinit/aws-route53.conf /etc/aws-route53.conf
81
+ - aws s3 --profile s3 cp --region us-east-1 s3://<%= s3_bucket -%>.cloudinit/route53-dynamic_dns_update.rb /usr/local/bin/route53-dynamic_dns_update.rb
82
+ - chmod 400 /etc/aws-route53.conf
83
+ - chmod 755 /usr/local/bin/route53-dynamic_dns_update.rb
84
+ - echo '/usr/local/bin/route53-dynamic_dns_update.rb' > /etc/rc.local