aws-carb 0.0.3

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,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