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