stemcell 0.2.9 → 0.4.4

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,292 @@
1
+ require 'trollop'
2
+
3
+ module Stemcell
4
+ class OptionParser
5
+ attr_reader :options
6
+
7
+ attr_reader :defaults
8
+ attr_reader :version
9
+ attr_reader :banner
10
+ attr_reader :override_help
11
+
12
+ OPTION_DEFINITIONS = [
13
+ {
14
+ :name => 'local_chef_root',
15
+ :desc => "directory of your local chef repository",
16
+ :type => String,
17
+ :env => 'LOCAL_CHEF_ROOT'
18
+ },
19
+ {
20
+ :name => 'aws_creds',
21
+ :desc => "select the aws credentials to use, via the aws-creds gem",
22
+ :type => String,
23
+ :env => 'AWS_CREDS'
24
+ },
25
+ {
26
+ :name => 'aws_access_key',
27
+ :desc => "aws access key",
28
+ :type => String,
29
+ :env => 'AWS_ACCESS_KEY'
30
+ },
31
+ {
32
+ :name => 'aws_secret_key',
33
+ :desc => "aws secret key",
34
+ :type => String,
35
+ :env => 'AWS_SECRET_KEY'
36
+ },
37
+ {
38
+ :name => 'region',
39
+ :desc => "ec2 region to launch in",
40
+ :type => String,
41
+ :env => 'REGION'
42
+ },
43
+ {
44
+ :name => 'instance_type',
45
+ :desc => "machine type to launch",
46
+ :type => String,
47
+ :env => 'INSTANCE_TYPE'
48
+ },
49
+ {
50
+ :name => 'backing_store',
51
+ :desc => "select between the backing store templates",
52
+ :type => String,
53
+ :env => 'BACKING_STORE'
54
+ },
55
+ {
56
+ :name => 'image_id',
57
+ :desc => "ami to use for launch",
58
+ :type => String,
59
+ :env => 'IMAGE_ID'
60
+ },
61
+ {
62
+ :name => 'security_groups',
63
+ :desc => "comma-separated list of security groups to launch instance with",
64
+ :type => String,
65
+ :env => 'SECURITY_GROUPS'
66
+ },
67
+ {
68
+ :name => 'availability_zone',
69
+ :desc => "zone in which to launch instances",
70
+ :type => String,
71
+ :env => 'AVAILABILITY_ZONE'
72
+ },
73
+ {
74
+ :name => 'tags',
75
+ :desc => "comma-separated list of key=value pairs to apply",
76
+ :type => String,
77
+ :env => 'TAGS'
78
+ },
79
+ {
80
+ :name => 'key_name',
81
+ :desc => "aws ssh key name for the ubuntu user",
82
+ :type => String,
83
+ :env => 'KEY_NAME'
84
+ },
85
+ {
86
+ :name => 'iam_role',
87
+ :desc => "IAM role to associate with the instance",
88
+ :type => String,
89
+ :env => 'IAM_ROLE'
90
+ },
91
+ {
92
+ :name => 'placement_group',
93
+ :desc => "Placement group to associate with the instance",
94
+ :type => String,
95
+ :env => 'PLACEMENT_GROUP'
96
+ },
97
+ {
98
+ :name => 'ebs_optimized',
99
+ :desc => "launch an EBS-Optimized instance",
100
+ :type => String,
101
+ :env => 'EBS_OPTIMIZED'
102
+ },
103
+ {
104
+ :name => 'block_device_mappings',
105
+ :desc => 'block device mappings',
106
+ :type => String,
107
+ :env => 'BLOCK_DEVICE_MAPPINGS'
108
+ },
109
+ {
110
+ :name => 'ephemeral_devices',
111
+ :desc => "comma-separated list of block devices to map ephemeral devices to",
112
+ :type => String,
113
+ :env => 'EPHEMERAL_DEVICES'
114
+ },
115
+ {
116
+ :name => 'chef_data_bag_secret',
117
+ :desc => "path to secret file (or the string containing the secret)",
118
+ :type => String,
119
+ :env => 'CHEF_DATA_BAG_SECRET'
120
+ },
121
+ {
122
+ :name => 'chef_role',
123
+ :desc => "chef role of instance to be launched",
124
+ :type => String,
125
+ :env => 'CHEF_ROLE'
126
+ },
127
+ {
128
+ :name => 'chef_environment',
129
+ :desc => "chef environment in which this instance will run",
130
+ :type => String,
131
+ :env => 'CHEF_ENVIRONMENT'
132
+ },
133
+ {
134
+ :name => 'git_origin',
135
+ :desc => "git origin to use",
136
+ :type => String,
137
+ :env => 'GIT_ORIGIN'
138
+ },
139
+ {
140
+ :name => 'git_branch',
141
+ :desc => "git branch to run off",
142
+ :type => String,
143
+ :env => 'GIT_BRANCH'
144
+ },
145
+ {
146
+ :name => 'git_key',
147
+ :desc => "path to the git repo deploy key (or the key as a string)",
148
+ :type => String,
149
+ :env => 'GIT_KEY'
150
+ },
151
+ {
152
+ :name => 'count',
153
+ :desc => "number of instances to launch",
154
+ :type => Integer,
155
+ :env => 'COUNT'
156
+ },
157
+ {
158
+ :name => 'tail',
159
+ :desc => "interactively tail the initial converge",
160
+ :type => nil,
161
+ :env => 'TAIL',
162
+ :short => :t
163
+ },
164
+ {
165
+ :name => 'ssh_user',
166
+ :desc => "ssh username",
167
+ :type => String,
168
+ :env => 'SSH_USER',
169
+ :short => :u
170
+ },
171
+ {
172
+ :name => 'non_interactive',
173
+ :desc => "assumes an affirmative answer to all prompts",
174
+ :type => nil,
175
+ :env => 'NON_INTERACTIVE',
176
+ :short => :f
177
+ }
178
+ ]
179
+
180
+ def initialize(config={})
181
+ @defaults = config[:defaults] || {}
182
+ @version = config[:version]
183
+ @banner = config[:banner]
184
+ @override_help = config[:override_help]
185
+ end
186
+
187
+ def parse!(args)
188
+ # The block passed to Trollop#options is evaluated in the binding of the
189
+ # trollop parser itself, it doesn't have access to the this instance.
190
+ # So use a value that can be captured instead!
191
+ _this = self
192
+ _defns = OPTION_DEFINITIONS
193
+
194
+ @options = Trollop::options(args) do
195
+ version _this.version if _this.version
196
+ banner _this.banner if _this.banner
197
+
198
+ _defns.each do |defn|
199
+ # Prioritize the environment variable, then the given default
200
+ default = ENV[defn[:env]] || _this.defaults[defn[:name]]
201
+
202
+ opt(
203
+ defn[:name],
204
+ defn[:desc],
205
+ :type => defn[:type],
206
+ :short => defn[:short],
207
+ :default => default)
208
+ end
209
+
210
+ # Prevent trollop from showing its help screen
211
+ opt('help', 'help', :short => :l) if _this.override_help
212
+ end
213
+
214
+ # convert tags from string to ruby hash
215
+ if options['tags']
216
+ tags = {}
217
+ options['tags'].split(',').each do |tag_set|
218
+ key, value = tag_set.split('=')
219
+ tags[key] = value
220
+ end
221
+ options['tags'] = tags
222
+ end
223
+
224
+ # parse block_device_mappings to convert it from the standard CLI format
225
+ # to the EC2 Ruby API format.
226
+ # All of this is a bit hard to find so here are some docs links to
227
+ # understand
228
+
229
+ # CLI This format is documented by typing
230
+ # ec2-run-instances --help and looking at the -b option
231
+ # Basically, it's either
232
+
233
+ # none
234
+ # ephemeral<number>
235
+ # '[<snapshot-id>][:<size>[:<delete-on-termination>][:<type>[:<iops>]]]'
236
+
237
+ # Ruby API (that does call to the native API)
238
+ # gems/aws-sdk-1.17.0/lib/aws/ec2/instance_collection.rb
239
+ # line 91 + example line 57
240
+
241
+ if options['block_device_mappings']
242
+ block_device_mappings = []
243
+ options['block_device_mappings'].split(',').each do |device_set|
244
+ device,devparam = device_set.split('=')
245
+
246
+ mapping = {}
247
+
248
+ if devparam == 'none'
249
+ mapping = { :no_device => device }
250
+ else
251
+ mapping = { :device_name => device }
252
+ if devparam =~ /^ephemeral[0-3]/
253
+ mapping[:virtual_name] = devparam
254
+ else
255
+ # we have a more complex 'ebs' parameter
256
+ #'[<snapshot-id>][:<size>[:<delete-on-termination>][:<type>[:<iops>]]]'
257
+
258
+ mapping[:ebs] = {}
259
+
260
+ devparam = devparam.split ':'
261
+
262
+ # a bit ugly but short and won't change
263
+ # notice the to_i on volume_size parameter
264
+ mapping[:ebs][:snapshot_id] = devparam[0] unless devparam[0].blank?
265
+ mapping[:ebs][:volume_size] = devparam[1].to_i
266
+
267
+ # defaults to true - except if we have the exact string "false"
268
+ mapping[:ebs][:delete_on_termination] = (devparam[2] != "false")
269
+
270
+ # optional. notice the to_i on iops parameter
271
+ mapping[:ebs][:volume_type] = devparam[3] unless devparam[3].blank?
272
+ mapping[:ebs][:iops] = devparam[4].to_i if (devparam[4].to_i)
273
+
274
+ end
275
+ end
276
+
277
+ block_device_mappings.push mapping
278
+ end
279
+
280
+ options['block_device_mappings'] = block_device_mappings
281
+ end
282
+
283
+ # convert security_groups from comma seperated string to ruby array
284
+ options['security_groups'] &&= options['security_groups'].split(',')
285
+ # convert ephemeral_devices from comma separated string to ruby array
286
+ options['ephemeral_devices'] &&= options['ephemeral_devices'].split(',')
287
+
288
+ options
289
+ end
290
+
291
+ end
292
+ end
@@ -9,6 +9,7 @@
9
9
 
10
10
 
11
11
  set -o pipefail
12
+ PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
12
13
 
13
14
 
14
15
  # ensure we were called by root
@@ -17,6 +18,29 @@ if [ $UID != 0 ]; then
17
18
  exit 1
18
19
  fi
19
20
 
21
+ ## Only run once at a time
22
+ # http://stackoverflow.com/questions/959475/shell-fragment-to-make-sure-only-one-instance-a-shell-script-runs-at-any-given-t
23
+ # slightly modified to use $$
24
+ # to run it I must set +e
25
+
26
+ SCRIPTNAME=`basename $0`
27
+ PIDFILE=/var/run/${SCRIPTNAME}.pid
28
+
29
+ set +e
30
+ if [ -f ${PIDFILE} ]; then
31
+ #verify if the process is actually still running under this pid
32
+ OLDPID=`cat ${PIDFILE}`
33
+ RESULT=`ps -ef | grep ${OLDPID} | grep ${SCRIPTNAME}`
34
+
35
+ if [ -n "${RESULT}" ]; then
36
+ echo "Script already running! Exiting"
37
+ exit 255
38
+ fi
39
+
40
+ fi
41
+ ### ---
42
+ set -e
43
+
20
44
 
21
45
  # redirect stdout to /var/log/init
22
46
  exec > /var/log/init
@@ -24,6 +48,10 @@ exec > /var/log/init
24
48
  # redirect stderr to /var/log/init.err
25
49
  exec 2> /var/log/init.err
26
50
 
51
+ #grab pid of this process and update the pid file with it
52
+ echo $$ > ${PIDFILE}
53
+
54
+
27
55
 
28
56
  ##
29
57
  ## settings
@@ -32,7 +60,7 @@ exec 2> /var/log/init.err
32
60
 
33
61
  chef_version=11.4.0
34
62
  chef_dir=/etc/chef
35
- converge=/usr/local/bin/converge
63
+ converge=/usr/local/bin/first_converge
36
64
  role=<%= opts['chef_role'] %>
37
65
  environment=<%= opts['chef_environment'] %>
38
66
  origin=<%= opts['git_origin'] %>
@@ -118,7 +146,10 @@ log_level :info
118
146
  log_location STDOUT
119
147
  EOF
120
148
 
121
- echo -e "$data_bag_secret" > ${chef_dir}/encrypted_data_bag_secret
149
+ encrypted_data_bag_secret_file=${chef_dir}/encrypted_data_bag_secret
150
+ echo -e "$data_bag_secret" > $encrypted_data_bag_secret_file
151
+ chmod 0400 $encrypted_data_bag_secret_file
152
+
122
153
  echo "chef configured"
123
154
  }
124
155
 
@@ -213,4 +244,10 @@ configure_chef_daemon
213
244
  ##
214
245
 
215
246
 
216
- echo "ran successfully:)"
247
+ if [ -f ${PIDFILE} ]; then
248
+ rm ${PIDFILE}
249
+ fi
250
+ # I can delete the cron too
251
+ rm -f /etc/cron.d/ec2admin_get_callback1
252
+
253
+ echo "<%= last_bootstrap_line %>"
@@ -0,0 +1,13 @@
1
+ class Hash
2
+ def deep_merge!(other_hash)
3
+ merge!(other_hash) do |key, oldval, newval|
4
+ # Coerce Chef-style attribute hashes to generic ones
5
+ oldval = oldval.to_hash if oldval.respond_to?(:to_hash)
6
+ newval = newval.to_hash if newval.respond_to?(:to_hash)
7
+
8
+ oldval.class.to_s == 'Hash' &&
9
+ newval.class.to_s == 'Hash' ?
10
+ oldval.deep_merge!(newval) : newval
11
+ end
12
+ end
13
+ end
@@ -0,0 +1 @@
1
+ require 'stemcell/utility/deep_merge'
@@ -1,3 +1,3 @@
1
1
  module Stemcell
2
- VERSION = "0.2.9"
2
+ VERSION = "0.4.4"
3
3
  end
data/lib/stemcell.rb CHANGED
@@ -1,189 +1,10 @@
1
- require 'logger'
2
- require 'erb'
3
- require 'aws-sdk'
1
+ require 'stemcell/utility'
2
+ require 'stemcell/errors'
4
3
 
5
- require_relative "stemcell/version"
4
+ require 'stemcell/command_line'
5
+ require 'stemcell/option_parser'
6
+ require 'stemcell/launcher'
7
+ require 'stemcell/log_tailer'
6
8
 
7
- module Stemcell
8
- class Stemcell
9
- def initialize(opts={})
10
- @log = Logger.new(STDOUT)
11
- @log.level = Logger::INFO unless ENV['DEBUG']
12
- @log.debug "creating new stemcell object"
13
- @log.debug "opts are #{opts.inspect}"
14
- ['aws_access_key',
15
- 'aws_secret_key',
16
- 'region',
17
- ].each do |req|
18
- raise ArgumentError, "missing required param #{req}" unless opts[req]
19
- instance_variable_set("@#{req}",opts[req])
20
- end
21
-
22
- @ec2_url = "ec2.#{@region}.amazonaws.com"
23
- @timeout = 120
24
- @start_time = Time.new
25
-
26
- AWS.config({:access_key_id => @aws_access_key, :secret_access_key => @aws_secret_key})
27
- @ec2 = AWS::EC2.new(:ec2_endpoint => @ec2_url)
28
- end
29
-
30
-
31
- def launch(opts={})
32
- verify_required_options(opts,[
33
- 'image_id',
34
- 'security_groups',
35
- 'key_name',
36
- 'count',
37
- 'chef_role',
38
- 'chef_environment',
39
- 'chef_data_bag_secret',
40
- 'git_branch',
41
- 'git_key',
42
- 'git_origin',
43
- 'instance_type',
44
- ])
45
-
46
- # attempt to accept keys as file paths
47
- opts['git_key'] = try_file(opts['git_key'])
48
- opts['chef_data_bag_secret'] = try_file(opts['chef_data_bag_secret'])
49
-
50
- # generate tags and merge in any that were specefied as inputs
51
- tags = {
52
- 'Name' => "#{opts['chef_role']}-#{opts['chef_environment']}",
53
- 'Group' => "#{opts['chef_role']}-#{opts['chef_environment']}",
54
- 'created_by' => ENV['USER'],
55
- 'stemcell' => VERSION,
56
- }
57
- tags.merge!(opts['tags']) if opts['tags']
58
-
59
- # generate launch options
60
- launch_options = {
61
- :image_id => opts['image_id'],
62
- :security_groups => opts['security_groups'],
63
- :user_data => opts['user_data'],
64
- :instance_type => opts['instance_type'],
65
- :key_name => opts['key_name'],
66
- :count => opts['count'],
67
- }
68
-
69
- # specify availability zone (optional)
70
- launch_options[:availability_zone] = opts['availability_zone'] if opts['availability_zone']
71
-
72
- # specify IAM role (optional)
73
- launch_options[:iam_instance_profile] = opts['iam_role'] if opts['iam_role']
74
-
75
- # specify an EBS-optimized instance (optional)
76
- launch_options[:ebs_optimized] = true if opts['ebs_optimized']
77
-
78
- # generate user data script to bootstrap instance, include in launch optsions
79
- launch_options[:user_data] = render_template(opts)
80
-
81
- # launch instances
82
- instances = do_launch(launch_options)
83
-
84
- # wait for aws to report instance stats
85
- wait(instances)
86
-
87
- # set tags on all instances launched
88
- set_tags(instances, tags)
89
-
90
- print_run_info(instances)
91
- @log.info "launched instances successfully"
92
- return instances
93
- end
94
-
95
- def find_instance(id)
96
- return @ec2.instances[id]
97
- end
98
-
99
- def kill(instances,opts={})
100
- return if instances.nil?
101
- instances.each do |i|
102
- begin
103
- instance = find_instance(i)
104
- @log.warn "Terminating instance #{instance.instance_id}"
105
- instance.terminate
106
- rescue AWS::EC2::Errors::InvalidInstanceID::NotFound => e
107
- throw e unless opts[:ignore_not_found]
108
- end
109
- end
110
- end
111
-
112
- private
113
-
114
- def print_run_info(instances)
115
- puts "here is the info for what's launched:"
116
- instances.each do |instance|
117
- puts "\tinstance_id: #{instance.instance_id}"
118
- puts "\tpublic ip: #{instance.public_ip_address}"
119
- puts
120
- end
121
- puts "install logs will be in /var/log/init and /var/log/init.err"
122
- end
123
-
124
- def wait(instances)
125
- @log.info "Waiting up to #{@timeout} seconds for #{instances.count} instances (#{instances.inspect}):"
126
-
127
- while true
128
- sleep 5
129
- if Time.now - @start_time > @timeout
130
- kill(instances)
131
- raise TimeoutError, "exceded timeout of #{@timeout}"
132
- end
133
-
134
- if instances.select{|i| i.status != :running }.empty?
135
- break
136
- end
137
- end
138
-
139
- @log.info "all instances in running state"
140
- end
141
-
142
- def verify_required_options(params,required_options)
143
- @log.debug "params is #{params}"
144
- @log.debug "required_options are #{required_options}"
145
- required_options.each do |required|
146
- raise ArgumentError, "you need to provide option #{required}" unless params.include?(required)
147
- end
148
- end
149
-
150
- def do_launch(opts={})
151
- @log.debug "about to launch instance(s) with options #{opts}"
152
- @log.info "launching instances"
153
- instances = @ec2.instances.create(opts)
154
- instances = [instances] unless instances.class == Array
155
- instances.each do |instance|
156
- @log.info "launched instance #{instance.instance_id}"
157
- end
158
- return instances
159
- end
160
-
161
- def set_tags(instances=[],tags)
162
- @log.info "setting tags on instance(s)"
163
- instances.each do |instance|
164
- instance.tags.set(tags)
165
- end
166
- end
167
-
168
- def render_template(opts={})
169
- this_file = File.expand_path __FILE__
170
- base_dir = File.dirname this_file
171
- template_file_path = File.join(base_dir,'stemcell','templates','bootstrap.sh.erb')
172
- template_file = File.read(template_file_path)
173
- erb_template = ERB.new(template_file)
174
- generated_template = erb_template.result(binding)
175
- @log.debug "genereated template is #{generated_template}"
176
- return generated_template
177
- end
178
-
179
- # attempt to accept keys as file paths
180
- def try_file(opt="")
181
- begin
182
- return File.read(opt)
183
- rescue Object => e
184
- return opt
185
- end
186
- end
187
-
188
- end
189
- end
9
+ require 'stemcell/metadata_launcher'
10
+ require 'stemcell/metadata_source'
data/stemcell.gemspec CHANGED
@@ -1,22 +1,27 @@
1
1
  # -*- encoding: utf-8 -*-
2
- lib = File.expand_path('../lib', __FILE__)
3
- $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
2
+ $:.push File.expand_path("../lib", __FILE__)
4
3
  require 'stemcell/version'
5
4
 
6
- Gem::Specification.new do |gem|
7
- gem.name = "stemcell"
8
- gem.version = Stemcell::VERSION
9
- gem.authors = ["Martin Rhoads"]
10
- gem.email = ["martin.rhoads@airbnb.com"]
11
- gem.description = %q{stemcell launches instances}
12
- gem.summary = %q{no summary}
13
- gem.homepage = ""
5
+ Gem::Specification.new do |s|
6
+ s.name = "stemcell"
7
+ s.version = Stemcell::VERSION
8
+ s.authors = ["Martin Rhoads", "Igor Serebryany", "Nelson Gauthier", "Patrick Viet"]
9
+ s.email = ["martin.rhoads@airbnb.com", "igor.serebryany@airbnb.com"]
10
+ s.description = %q{A tool for launching and bootstrapping EC2 instances}
11
+ s.summary = %q{no summary}
12
+ s.homepage = "https://github.com/airbnb/stemcell"
14
13
 
15
- gem.files = `git ls-files`.split($/)
16
- gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
17
- gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
18
- gem.require_paths = ["lib"]
19
- gem.add_runtime_dependency 'trollop'
20
- gem.add_runtime_dependency 'aws-sdk'
21
- end
14
+ s.files = `git ls-files`.split($/)
15
+ s.executables = s.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
16
+ s.test_files = s.files.grep(%r{^(test|spec|features)/})
17
+ s.require_paths = ["lib"]
18
+
19
+ s.add_runtime_dependency 'aws-sdk', '~> 1.9'
20
+ s.add_runtime_dependency 'net-ssh', '~> 2.6'
21
+ s.add_runtime_dependency 'chef', '~> 11.4.0'
22
22
 
23
+ s.add_runtime_dependency 'trollop', '~> 2.0'
24
+ s.add_runtime_dependency 'aws-creds', '~> 0.2.2'
25
+ s.add_runtime_dependency 'colored', '~> 1.2'
26
+ s.add_runtime_dependency 'json', '~> 1.7.7'
27
+ end