stemcell 0.2.9 → 0.4.4

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