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 CHANGED
@@ -19,3 +19,4 @@ tmp
19
19
  \#*\#
20
20
  .\#*
21
21
  .*sw?
22
+ .DS_Store
data/LICENSE.txt CHANGED
@@ -1,4 +1,4 @@
1
- Copyright (c) 2013 Martin Rhoads
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
- You should create an rc file for stemcell with your standard options
30
- (and place it in the root dir as .stemcellrc?). You can see an example
31
- in examples/stemcellrc. You can get most of the options from your
32
- .chef/knife.rb but you will need to get the new chef deploy key so
33
- that instances that you launch can download code.
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 .stemcellrc
55
+ $ source ~/.stemcellrc
41
56
  ```
42
57
 
43
58
  ### Simple launch:
44
59
 
45
60
  ```bash
46
- $ ./bin/stemcell --chef-role $your_chef_role --git-branch $your_chef_branch
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
- $ ./bin/stemcell --help
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
- required_parameters = [
26
- 'aws_access_key',
27
- 'aws_secret_key',
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.size == 0
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
- ec2 = AWS::EC2.new(:access_key_id => options['aws_access_key'], :secret_access_key => options['aws_secret_key'])
42
- ec2.regions.each do |region|
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
- # kill the instances
50
- instances.each do |id, objects|
51
- if objects.count == 0
52
- puts "No instance #{id} found"
53
- next
54
- end
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 objects.count > 1
57
- puts "Found multiple instances named #{id}"
58
- next
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
- instance, region = objects[0]
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
- next
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
- puts "Terminate? (y/N)? "
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
- # -*- mode: shell -*-
4
-
5
- require_relative '../lib/stemcell'
6
-
7
- require 'trollop'
8
-
9
- options = Trollop::options do
10
- version "stemcell #{Stemcell::VERSION} (c) 2013 Airbnb"
11
- banner "Stemcell: get ready to rock"
12
-
13
- opt('aws_access_key',
14
- "aws access key",
15
- :type => String,
16
- :default => ENV['AWS_ACCESS_KEY']
17
- )
18
-
19
- opt('aws_secret_key',
20
- "aws secret key",
21
- :type => String,
22
- :default => ENV['AWS_SECRET_KEY']
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
- # these options should not need to be set
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