stemcell 0.2.9 → 0.4.4
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +1 -0
- data/LICENSE.txt +2 -2
- data/README.md +24 -12
- data/bin/necrosis +60 -26
- data/bin/stemcell +23 -180
- data/examples/stemcell.json +26 -0
- data/examples/stemcellrc +21 -2
- data/lib/stemcell/command_line.rb +222 -0
- data/lib/stemcell/errors.rb +25 -0
- data/lib/stemcell/launcher.rb +258 -0
- data/lib/stemcell/log_tailer.rb +143 -0
- data/lib/stemcell/metadata_launcher.rb +108 -0
- data/lib/stemcell/metadata_source.rb +139 -0
- data/lib/stemcell/option_parser.rb +292 -0
- data/lib/stemcell/templates/bootstrap.sh.erb +40 -3
- data/lib/stemcell/utility/deep_merge.rb +13 -0
- data/lib/stemcell/utility.rb +1 -0
- data/lib/stemcell/version.rb +1 -1
- data/lib/stemcell.rb +8 -187
- data/stemcell.gemspec +22 -17
- metadata +107 -13
data/.gitignore
CHANGED
data/LICENSE.txt
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
Copyright (c) 2013
|
1
|
+
Copyright (c) 2013 Airbnb, Inc.
|
2
2
|
|
3
3
|
MIT License
|
4
4
|
|
@@ -19,4 +19,4 @@ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
19
19
|
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
20
|
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
21
|
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
-
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
CHANGED
@@ -24,35 +24,49 @@ Or install it yourself as:
|
|
24
24
|
$ gem install stemcell
|
25
25
|
```
|
26
26
|
|
27
|
+
Or build the gem locally and install it:
|
28
|
+
|
29
|
+
```bash
|
30
|
+
$ gem build stemcell.gemspec
|
31
|
+
$ gem install ./stemcell-0.4.3.gem
|
32
|
+
```
|
33
|
+
|
27
34
|
## Configuration
|
28
35
|
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
36
|
+
If you're using the command line tool, you'll need to add a `stemcell.json` to the root of your chef repo.
|
37
|
+
It contains default attributes for launching instances as well as the mapping from backing stores to images.
|
38
|
+
For an example, see `examples/stemcell.json`.
|
39
|
+
|
40
|
+
You should create an RC file for stemcell with your standard options (and place it in the root dir as .stemcellrc?).
|
41
|
+
You can see an example in `examples/stemcellrc`.
|
42
|
+
As documented in that file, you will need:
|
43
|
+
* a mono-repo for chef (like the kind described [here](https://github.com/opscode/chef-repo))
|
44
|
+
* a local checkout of the repo
|
45
|
+
* an ssh key that's allowed to read that repo
|
46
|
+
* AWS credentials with permission to create instances
|
47
|
+
* an AWS [ssh key pair](http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ec2-key-pairs.html)
|
48
|
+
* an encrypted data bag secret (optional)
|
34
49
|
|
35
50
|
## Usage
|
36
51
|
|
37
52
|
### Include your base config:
|
38
53
|
|
39
54
|
```bash
|
40
|
-
$ source
|
55
|
+
$ source ~/.stemcellrc
|
41
56
|
```
|
42
57
|
|
43
58
|
### Simple launch:
|
44
59
|
|
45
60
|
```bash
|
46
|
-
$
|
61
|
+
$ stemcell $your_chef_role --git-branch $your_chef_branch
|
47
62
|
```
|
48
63
|
|
49
|
-
This will cause instance(s) to be launched and their ip's and instance
|
50
|
-
id to be printed to the screen.
|
64
|
+
This will cause instance(s) to be launched and their ip's and instance id to be printed to the screen.
|
51
65
|
|
52
66
|
### More options:
|
53
67
|
|
54
68
|
```bash
|
55
|
-
$
|
69
|
+
$ stemcell --help
|
56
70
|
```
|
57
71
|
|
58
72
|
### Watching install:
|
@@ -75,8 +89,6 @@ This README presents `stemcell` as a tool for administrators to use to create in
|
|
75
89
|
However, we designed `stemcell` to be easily useful for automated systems which control server infrastructure.
|
76
90
|
These automated systems can call out to `stemcell` on the command-line or use the ruby classes directly.
|
77
91
|
|
78
|
-
To see how we manage our cloud, check out the rest of SmartStack, especially Cortex, our auto-scaling system.
|
79
|
-
|
80
92
|
## Similar Tools ##
|
81
93
|
|
82
94
|
There are a few additional tools which bootstrap EC2 instances with chef-solo.
|
data/bin/necrosis
CHANGED
@@ -20,12 +20,20 @@ options = Trollop::options do
|
|
20
20
|
:type => String,
|
21
21
|
:default => ENV['AWS_SECRET_KEY']
|
22
22
|
)
|
23
|
+
|
24
|
+
opt('aws_regions',
|
25
|
+
"comma-separated list of aws regions to search",
|
26
|
+
:type => String,
|
27
|
+
:default => ENV['AWS_REGIONS'],
|
28
|
+
:short => :r
|
29
|
+
)
|
30
|
+
|
31
|
+
opt :non_interactive, 'Do not ask confirmation', :short => :f
|
23
32
|
end
|
24
33
|
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
]
|
34
|
+
NON_INTERACTIVE = options[:non_interactive]
|
35
|
+
|
36
|
+
required_parameters = %w(aws_access_key aws_secret_key)
|
29
37
|
|
30
38
|
required_parameters.each do |arg|
|
31
39
|
raise ArgumentError, "--#{arg.gsub('_','-')} needs to be specified on the commandline or set \
|
@@ -33,35 +41,38 @@ by the #{arg.upcase.gsub('-','_')} environment variable" if
|
|
33
41
|
options[arg].nil? or ! options[arg]
|
34
42
|
end
|
35
43
|
|
36
|
-
raise ArgumentError, "you did not provide any instance ids to kill" if ARGV.
|
44
|
+
raise ArgumentError, "you did not provide any instance ids to kill" if ARGV.empty?
|
37
45
|
|
38
46
|
# a hash from instance_id => [ec2 instance objects]
|
39
47
|
instances = ARGV.inject({}) { |h, n| h[n] = []; h }
|
40
48
|
|
41
|
-
|
42
|
-
|
43
|
-
instances.each do |id, objects|
|
44
|
-
instance = region.instances[id]
|
45
|
-
objects << [instance, region] if instance.exists?
|
46
|
-
end
|
47
|
-
end
|
49
|
+
# convert comma-separated list to Array
|
50
|
+
regions = (options['aws_regions'] || '').split(',')
|
48
51
|
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
52
|
+
def authorized? instance
|
53
|
+
return true if NON_INTERACTIVE
|
54
|
+
|
55
|
+
puts 'Terminate? (y/N)'
|
56
|
+
confirm = $stdin.gets
|
57
|
+
confirmed = confirm && confirm.chomp.downcase == 'y'
|
55
58
|
|
56
|
-
if
|
57
|
-
|
58
|
-
|
59
|
+
if confirmed
|
60
|
+
age = Time.now - instance.launch_time
|
61
|
+
days = age / (24*3600)
|
62
|
+
if days > 2
|
63
|
+
puts "Instance is #{days} days old. REALLY terminate? (y/N)"
|
64
|
+
confirm = $stdin.gets
|
65
|
+
confirmed = confirm && confirm.chomp.downcase == 'y'
|
66
|
+
end
|
59
67
|
end
|
60
68
|
|
61
|
-
|
69
|
+
return confirmed
|
70
|
+
end
|
71
|
+
|
72
|
+
def delete_instance id, instance, region
|
62
73
|
if instance.api_termination_disabled?
|
63
74
|
puts "Cannot terminate instance #{id} -- termination protection enabled"
|
64
|
-
|
75
|
+
return
|
65
76
|
end
|
66
77
|
|
67
78
|
puts "Instance #{id} (#{instance.status} in #{region.name})"
|
@@ -71,10 +82,33 @@ instances.each do |id, objects|
|
|
71
82
|
puts "\t#{k} : #{v}"
|
72
83
|
end
|
73
84
|
|
74
|
-
|
75
|
-
confirm = $stdin.gets
|
76
|
-
if confirm && confirm.chomp.downcase == 'y'
|
85
|
+
if authorized? instance
|
77
86
|
instance.terminate
|
78
87
|
puts "Instance #{id} terminated"
|
79
88
|
end
|
80
89
|
end
|
90
|
+
|
91
|
+
AWS.memoize do
|
92
|
+
ec2 = AWS::EC2.new(:access_key_id => options['aws_access_key'], :secret_access_key => options['aws_secret_key'])
|
93
|
+
ec2.regions.each do |region|
|
94
|
+
next unless regions.empty? || regions.include?(region.name)
|
95
|
+
instances.each do |id, objects|
|
96
|
+
instance = region.instances[id]
|
97
|
+
objects << [instance, region] if instance.exists?
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
instances.each do |id, objects|
|
102
|
+
case objects.count
|
103
|
+
when 0
|
104
|
+
STDERR.puts "No instance #{id} found"
|
105
|
+
next
|
106
|
+
when 1
|
107
|
+
instance, region = objects.first
|
108
|
+
delete_instance id, instance, region
|
109
|
+
else
|
110
|
+
puts "Found multiple instances named #{id}"
|
111
|
+
next
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
data/bin/stemcell
CHANGED
@@ -1,181 +1,24 @@
|
|
1
1
|
#!/usr/bin/env ruby
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
options
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
opt('region',
|
26
|
-
'ec2 region to launch in',
|
27
|
-
:type => String,
|
28
|
-
:default => ENV['REGION'] ? ENV['REGION'] : 'us-east-1',
|
29
|
-
)
|
30
|
-
|
31
|
-
opt('instance_type',
|
32
|
-
'machine type to launch',
|
33
|
-
:type => String,
|
34
|
-
:default => ENV['INSTANCE_TYPE'] ? ENV['INSTANCE_TYPE'] : 'm1.small',
|
35
|
-
)
|
36
|
-
|
37
|
-
opt('image_id',
|
38
|
-
'ami to use for launch',
|
39
|
-
:type => String,
|
40
|
-
:default => ENV['IMAGE_ID'] ? ENV['IMAGE_ID'] : 'ami-d726abbe',
|
41
|
-
)
|
42
|
-
|
43
|
-
opt('security_groups',
|
44
|
-
'security groups to launch instance with',
|
45
|
-
:type => String,
|
46
|
-
:default => ENV['SECURITY_GROUPS'] ? ENV['SECURITY_GROUPS'] : 'default',
|
47
|
-
)
|
48
|
-
|
49
|
-
opt('availability_zone',
|
50
|
-
'zone in which to launch instances',
|
51
|
-
:type => String,
|
52
|
-
:default => ENV['AVAILABILITY_ZONE'],
|
53
|
-
)
|
54
|
-
|
55
|
-
opt('tags',
|
56
|
-
'comma-separated list of key=value pairs to apply',
|
57
|
-
:type => String,
|
58
|
-
:default => ENV['TAGS'],
|
59
|
-
)
|
60
|
-
|
61
|
-
opt('key_name',
|
62
|
-
'aws ssh key name for the ubuntu user',
|
63
|
-
:type => String,
|
64
|
-
:default => ENV['KEY_NAME'],
|
65
|
-
)
|
66
|
-
|
67
|
-
opt('iam_role',
|
68
|
-
'IAM role to associate with the instance',
|
69
|
-
:type => String,
|
70
|
-
:default => ENV['IAM_ROLE'],
|
71
|
-
)
|
72
|
-
|
73
|
-
opt('ebs_optimized',
|
74
|
-
'launch an EBS-Optimized instance',
|
75
|
-
:type => :flag,
|
76
|
-
)
|
77
|
-
|
78
|
-
opt('chef_data_bag_secret',
|
79
|
-
'path to secret file (or the string containing the secret)',
|
80
|
-
:type => String,
|
81
|
-
:default => ENV['CHEF_DATA_BAG_SECRET'],
|
82
|
-
)
|
83
|
-
|
84
|
-
opt('chef_role',
|
85
|
-
'chef role of instance to be launched',
|
86
|
-
:type => String,
|
87
|
-
:default => ENV['CHEF_ROLE'],
|
88
|
-
)
|
89
|
-
|
90
|
-
opt('chef_environment',
|
91
|
-
'chef environment in which this instance will run',
|
92
|
-
:type => String,
|
93
|
-
:default => ENV['CHEF_ENVIRONMENT'],
|
94
|
-
)
|
95
|
-
|
96
|
-
opt('git_origin',
|
97
|
-
'git origin to use',
|
98
|
-
:type => String,
|
99
|
-
:default => ENV['GIT_ORIGIN'],
|
100
|
-
)
|
101
|
-
|
102
|
-
opt('git_branch',
|
103
|
-
'git branch to run off',
|
104
|
-
:type => String,
|
105
|
-
:default => ENV['GIT_BRANCH'] ? ENV['GIT_BRANCH'] : 'production',
|
106
|
-
)
|
107
|
-
|
108
|
-
opt('git_key',
|
109
|
-
'path to the git repo deploy key (or the string containing the key)',
|
110
|
-
:type => String,
|
111
|
-
:default => ENV['GIT_KEY'],
|
112
|
-
)
|
113
|
-
|
114
|
-
opt('count',
|
115
|
-
'number of instances to launch',
|
116
|
-
:type => Integer,
|
117
|
-
:default => ENV['COUNT'] ? ENV['COUNT'] : 1,
|
118
|
-
)
|
119
|
-
|
120
|
-
end
|
121
|
-
|
122
|
-
required_parameters = [
|
123
|
-
'aws_access_key',
|
124
|
-
'aws_secret_key',
|
125
|
-
'chef_role',
|
126
|
-
'chef_environment',
|
127
|
-
'chef_data_bag_secret',
|
128
|
-
'git_branch',
|
129
|
-
'git_key',
|
130
|
-
'git_origin',
|
131
|
-
'key_name',
|
132
|
-
]
|
133
|
-
|
134
|
-
required_parameters.each do |arg|
|
135
|
-
raise ArgumentError, "--#{arg.gsub('_','-')} needs to be specified on the commandline or set \
|
136
|
-
by the #{arg.upcase.gsub('-','_')} environment variable" if
|
137
|
-
options[arg].nil? or ! options[arg]
|
138
|
-
end
|
139
|
-
|
140
|
-
|
141
|
-
# convert tags from string to ruby hash
|
142
|
-
tags = {}
|
143
|
-
if options['tags']
|
144
|
-
options['tags'].split(',').each do |tag_set|
|
145
|
-
key, value = tag_set.split('=')
|
146
|
-
tags[key] = value
|
147
|
-
end
|
148
|
-
end
|
149
|
-
options['tags'] = tags
|
150
|
-
|
151
|
-
|
152
|
-
# convert security_groups from comma seperated string to ruby array
|
153
|
-
options['security_groups'] = options['security_groups'].split(',')
|
154
|
-
|
155
|
-
|
156
|
-
# create stemcell object
|
157
|
-
stemcell = Stemcell::Stemcell.new({
|
158
|
-
'aws_access_key' => options['aws_access_key'],
|
159
|
-
'aws_secret_key' => options['aws_secret_key'],
|
160
|
-
'region' => options['region'],
|
161
|
-
})
|
162
|
-
|
163
|
-
|
164
|
-
# launch instance(s)
|
165
|
-
stemcell.launch({
|
166
|
-
'availability_zone' => options['availability_zone'],
|
167
|
-
'instance_type' => options['instance_type'],
|
168
|
-
'image_id' => options['image_id'],
|
169
|
-
'security_groups' => options['security_groups'],
|
170
|
-
'chef_role' => options['chef_role'],
|
171
|
-
'chef_environment' => options['chef_environment'],
|
172
|
-
'chef_data_bag_secret' => options['chef_data_bag_secret'],
|
173
|
-
'git_branch' => options['git_branch'],
|
174
|
-
'git_key' => options['git_key'],
|
175
|
-
'git_origin' => options['git_origin'],
|
176
|
-
'key_name' => options['key_name'],
|
177
|
-
'tags' => options['tags'],
|
178
|
-
'count' => options['count'],
|
179
|
-
'iam_role' => options['iam_role'],
|
180
|
-
'ebs_optimized' => options['ebs_optimized'],
|
181
|
-
})
|
2
|
+
require 'stemcell'
|
3
|
+
|
4
|
+
# Stemcell (c) 2012-2013 Airbnb
|
5
|
+
#
|
6
|
+
# This script starts an EC2 instance using attributes stored in the expanded
|
7
|
+
# runlist of a given role, respecting default and overriden attributes. To use
|
8
|
+
# stemcell with your role, specify a new attribute called "instance_metadata",
|
9
|
+
# the options therein will get merged against the default options (stored in
|
10
|
+
# the stemcell.json file in your local chef root), as well as any options
|
11
|
+
# passed in the command line.
|
12
|
+
#
|
13
|
+
# Usage: stemcell [chef role] [options]
|
14
|
+
#
|
15
|
+
# The priority order for options (highest to lowest):
|
16
|
+
#
|
17
|
+
# 1) Arguments passed to launch on the command line
|
18
|
+
# 2) Environment variables
|
19
|
+
# 3) Options derived from the runlist
|
20
|
+
# 4) Default instance store options (stemcell.json)
|
21
|
+
# 5) Default launch options (stemcell.json)
|
22
|
+
# 6) Base options (see below)
|
23
|
+
|
24
|
+
Stemcell::CommandLine.run!
|
@@ -0,0 +1,26 @@
|
|
1
|
+
{
|
2
|
+
"defaults": {
|
3
|
+
"region":"us-east-1",
|
4
|
+
"security_groups": ["default"],
|
5
|
+
"instance_type": "m1.small",
|
6
|
+
"backing_store": "instance_store"
|
7
|
+
},
|
8
|
+
|
9
|
+
"backing_store": {
|
10
|
+
"ebs": {
|
11
|
+
"image_id": "ami-23d9a94a"
|
12
|
+
},
|
13
|
+
"instance_store": {
|
14
|
+
"image_id": "ami-d9d6a6b0"
|
15
|
+
}
|
16
|
+
},
|
17
|
+
|
18
|
+
"availability_zones": {
|
19
|
+
"us-east-1": [
|
20
|
+
"us-east-1a",
|
21
|
+
"us-east-1b",
|
22
|
+
"us-east-1c",
|
23
|
+
"us-east-1e"
|
24
|
+
]
|
25
|
+
}
|
26
|
+
}
|
data/examples/stemcellrc
CHANGED
@@ -1,10 +1,29 @@
|
|
1
1
|
# these options need to be set
|
2
2
|
export AWS_ACCESS_KEY=your_aws_access_key
|
3
3
|
export AWS_SECRET_KEY=your_aws_secret_key
|
4
|
+
|
5
|
+
# AWC keys can be managed from the AWS console by going
|
6
|
+
# to EC2 and then clicking 'Key Pairs'. Every EC2 instance
|
7
|
+
# needs a key pair to launch; you can use the private component
|
8
|
+
# of this key pair to get into your machine before chef has run
|
4
9
|
export KEY_NAME=your_aws_ssh_key_name
|
10
|
+
|
11
|
+
# We use encrypted data bags; to generate an ecrypted data bag
|
12
|
+
# secret, follow instructions here:
|
13
|
+
# http://docs.opscode.com/chef/essentials_data_bags.html#secret-keys
|
5
14
|
export CHEF_DATA_BAG_SECRET=/path/to/encrypted/data/bag/secret
|
15
|
+
|
16
|
+
# This expects a standard opscode mono-repo, like the one
|
17
|
+
# modeled here: https://github.com/opscode/chef-repo
|
18
|
+
export LOCAL_CHEF_ROOT=/path/to/your/local/chef/repository
|
19
|
+
|
20
|
+
# The git origin is the publically-accessible version of the
|
21
|
+
# local chef root; we use a github enterprise server
|
22
|
+
export GIT_ORIGIN=git@git.airbnb.com:airbnb/chef.git
|
23
|
+
|
24
|
+
# The git key is an SSH key which has pull permissions on
|
25
|
+
# the GIT_ORIGIN repo
|
6
26
|
export GIT_KEY=/path/to/chef/deploy/key
|
7
27
|
|
8
|
-
#
|
28
|
+
# Which security group should you launch
|
9
29
|
export SECURITY_GROUP=default
|
10
|
-
export GIT_ORIGIN=git@github.com:airbnb/chef.git
|
@@ -0,0 +1,222 @@
|
|
1
|
+
require 'aws/creds'
|
2
|
+
require 'colored'
|
3
|
+
|
4
|
+
module Stemcell
|
5
|
+
class CommandLine
|
6
|
+
attr_reader :launcher
|
7
|
+
attr_reader :showing_help
|
8
|
+
attr_reader :chef_root
|
9
|
+
attr_reader :chef_role
|
10
|
+
|
11
|
+
VERSION_STRING = "Stemcell (c) 2012-2013 Airbnb."
|
12
|
+
BANNER_STRING = "Launch instances from metadata stored in roles!\n" \
|
13
|
+
"Usage: stemcell [chef role] [options]"
|
14
|
+
|
15
|
+
OPTION_PARSER_DEFAULTS = {
|
16
|
+
'non_interactive' => false,
|
17
|
+
'aws_creds' => 'default',
|
18
|
+
'ssh_user' => 'ubuntu'
|
19
|
+
}
|
20
|
+
|
21
|
+
def self.run!
|
22
|
+
CommandLine.new.run!
|
23
|
+
end
|
24
|
+
|
25
|
+
def run!
|
26
|
+
determine_root_and_showing_help
|
27
|
+
|
28
|
+
initialize_launcher
|
29
|
+
retrieve_defaults
|
30
|
+
configure_option_parser
|
31
|
+
|
32
|
+
options = parse_options_or_print_help
|
33
|
+
determine_role(options)
|
34
|
+
|
35
|
+
print_usage_and_exit unless chef_role
|
36
|
+
# If we didn't successfully initialize the launcher, we should exit.
|
37
|
+
exit unless @launcher
|
38
|
+
|
39
|
+
print_version
|
40
|
+
display_chef_repo_warnings
|
41
|
+
|
42
|
+
# Retrieve AWS credentials via the aws-creds gems (if available)
|
43
|
+
awc_name = options.delete('aws_creds')
|
44
|
+
credentials = retrieve_credentials_from_awc(awc_name)
|
45
|
+
options.merge!(credentials) if credentials
|
46
|
+
# Move launcher in the non-interactive mode (if requested)
|
47
|
+
launcher.interactive = !options.delete('non_interactive')
|
48
|
+
|
49
|
+
instances = launch_instance(chef_role, options)
|
50
|
+
tail_converge(instances, options) if options['tail']
|
51
|
+
|
52
|
+
puts "\nDone.\n".green
|
53
|
+
end
|
54
|
+
|
55
|
+
private
|
56
|
+
|
57
|
+
def determine_root_and_showing_help
|
58
|
+
# First pass at parsing the options to find the local chef root and
|
59
|
+
# whether or not we're showing the help screen.
|
60
|
+
provisional_option_parser = OptionParser.new(:override_help => true)
|
61
|
+
initial_options = provisional_option_parser.parse!(ARGV.dup)
|
62
|
+
|
63
|
+
# Remove the role before parsing any remaining options.
|
64
|
+
@showing_help = !!initial_options['help']
|
65
|
+
@chef_root = initial_options['local_chef_root']
|
66
|
+
|
67
|
+
# If we didn't receive a chef root, assume the current directory.
|
68
|
+
@chef_root ||= File.expand_path('.')
|
69
|
+
end
|
70
|
+
|
71
|
+
def initialize_launcher
|
72
|
+
@launcher = MetadataLauncher.new(:chef_root => chef_root)
|
73
|
+
rescue NoTemplateError
|
74
|
+
puts "Couldn't find `stemcell.json` in the local chef repo.".red
|
75
|
+
puts "You must specify the root of the local checkout of your chef " \
|
76
|
+
"respository by using the --local-chef-root options or " \
|
77
|
+
"setting the LOCAL_CHEF_ROOT environment variable."
|
78
|
+
rescue TemplateParseError => e
|
79
|
+
error "Couldn't parse the `stemcell.json` file: #{e.message}"
|
80
|
+
end
|
81
|
+
|
82
|
+
def launch_instance(role, options={})
|
83
|
+
launcher.run!(role, options)
|
84
|
+
rescue RoleExpansionError
|
85
|
+
error "There was a problem expanding the #{chef_role} role. " \
|
86
|
+
"Perhaps it or one of its dependencies does not exist."
|
87
|
+
rescue MissingStemcellOptionError => e
|
88
|
+
error "The '#{e.option}' attribute needs to be specified on the " \
|
89
|
+
"command line, in the role, in the stemcell.json defaults, " \
|
90
|
+
"or set by the #{e.option.upcase.gsub('-','_')} environment variable."
|
91
|
+
rescue UnknownBackingStoreError => e
|
92
|
+
error "Unknown backing store type: #{e.backing_store}."
|
93
|
+
end
|
94
|
+
|
95
|
+
def tail_converge(instances, options={})
|
96
|
+
puts "\nTailing the initial converge. Press Ctrl-C to exit...".green
|
97
|
+
|
98
|
+
if instances.count > 1
|
99
|
+
puts "\nYou're launching more than one instance."
|
100
|
+
puts "Showing you on the output from #{instances.first.instance_id}."
|
101
|
+
end
|
102
|
+
|
103
|
+
puts "\n"
|
104
|
+
tailer = LogTailer.new(instances.first.public_dns_name, options['ssh_user'])
|
105
|
+
tailer.run!
|
106
|
+
end
|
107
|
+
|
108
|
+
def retrieve_defaults
|
109
|
+
@default_options = launcher ? launcher.default_options : {}
|
110
|
+
@default_branch = @default_options['git_branch']
|
111
|
+
end
|
112
|
+
|
113
|
+
def configure_option_parser
|
114
|
+
parser_defaults = OPTION_PARSER_DEFAULTS
|
115
|
+
if showing_help
|
116
|
+
parser_defaults = parser_defaults.merge!(@default_options)
|
117
|
+
transform_parser_defaults(parser_defaults)
|
118
|
+
end
|
119
|
+
|
120
|
+
@option_parser = OptionParser.new({
|
121
|
+
:defaults => parser_defaults,
|
122
|
+
:version => VERSION_STRING,
|
123
|
+
:banner => BANNER_STRING
|
124
|
+
})
|
125
|
+
end
|
126
|
+
|
127
|
+
def transform_parser_defaults(pd)
|
128
|
+
# There are two special cases: security groups and tags. Security groups
|
129
|
+
# are an array in the template, but trollop expects a string. Likewise,
|
130
|
+
# in the case of tags, they are represented as a hash, but trollop wants
|
131
|
+
# a string once again. In both cases, the data types are encoded as a
|
132
|
+
# comma-separated list when presented as defaults.
|
133
|
+
pd['security_groups'] &&= pd['security_groups'].join(',')
|
134
|
+
pd['tags'] &&= pd['tags'].to_a.map { |p| p.join('=') }.join(',')
|
135
|
+
end
|
136
|
+
|
137
|
+
def parse_options_or_print_help
|
138
|
+
parsed_options = @option_parser.parse!(ARGV)
|
139
|
+
# Parsed options will contain nil values, strip them out.
|
140
|
+
parsed_options.keys.each do |key|
|
141
|
+
value = parsed_options[key]
|
142
|
+
parsed_options.delete(key) unless key.is_a?(String) && value
|
143
|
+
end
|
144
|
+
parsed_options
|
145
|
+
end
|
146
|
+
|
147
|
+
def determine_role(options)
|
148
|
+
role = ARGV.shift
|
149
|
+
@chef_role = role if role && role.length > 0
|
150
|
+
@chef_role ||= options['chef_role']
|
151
|
+
end
|
152
|
+
|
153
|
+
def retrieve_credentials_from_awc(name)
|
154
|
+
creds = AWS::Creds[name || 'default']
|
155
|
+
return creds && {
|
156
|
+
'aws_access_key' => creds.access_key_id,
|
157
|
+
'aws_secret_key' => creds.secret_access_key
|
158
|
+
}
|
159
|
+
rescue AWS::Creds::InvalidKeyTab,
|
160
|
+
AWS::Creds::InvalidKeyPair
|
161
|
+
end
|
162
|
+
|
163
|
+
def validate_chef_root
|
164
|
+
unless chef_root_valid?
|
165
|
+
error "This isn't a chef repository: no roles folder."
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
169
|
+
def display_chef_repo_warnings
|
170
|
+
# Print a series of warnings about the chef repo state.
|
171
|
+
if not_default_branch?
|
172
|
+
warning "You are not on the '#{@default_branch}' branch!"
|
173
|
+
end
|
174
|
+
if has_unstaged_changes?
|
175
|
+
warning "You have unstaged changes."
|
176
|
+
end
|
177
|
+
if has_uncommitted_changes?
|
178
|
+
warning "Your index contains uncommitted changes."
|
179
|
+
end
|
180
|
+
end
|
181
|
+
|
182
|
+
def chef_root_valid?
|
183
|
+
File.directory?(File.join(chef_root, 'roles'))
|
184
|
+
end
|
185
|
+
|
186
|
+
def not_default_branch?
|
187
|
+
run_in_chef("git rev-parse --abbrev-ref HEAD").strip != @default_branch
|
188
|
+
end
|
189
|
+
|
190
|
+
def has_unstaged_changes?
|
191
|
+
run_in_chef("git diff-files --quiet --ignore-submodules")
|
192
|
+
$? != 0
|
193
|
+
end
|
194
|
+
|
195
|
+
def has_uncommitted_changes?
|
196
|
+
run_in_chef("git diff-index --cached --quiet HEAD --ignore-submodules")
|
197
|
+
$? != 0
|
198
|
+
end
|
199
|
+
|
200
|
+
def run_in_chef(command)
|
201
|
+
`cd #{chef_root}; #{command}`
|
202
|
+
end
|
203
|
+
|
204
|
+
def error(string)
|
205
|
+
warn "\nERROR: #{string}\n".red
|
206
|
+
exit 1
|
207
|
+
end
|
208
|
+
|
209
|
+
def warning(string)
|
210
|
+
warn "\nWARNING: #{string}".red
|
211
|
+
end
|
212
|
+
|
213
|
+
def print_version
|
214
|
+
puts "#{VERSION_STRING}\n"
|
215
|
+
end
|
216
|
+
|
217
|
+
def print_usage_and_exit
|
218
|
+
puts "#{BANNER_STRING}\n"
|
219
|
+
exit
|
220
|
+
end
|
221
|
+
end
|
222
|
+
end
|