ey_recipes 0.9.1

Sign up to get free protection for your applications and to get access to all the features.
data/bin/ey-recipes ADDED
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'ey_recipes'
4
+
5
+ EY::Recipes.run(ARGV)
@@ -0,0 +1,56 @@
1
+ module EY::Recipes
2
+ class API
3
+ def self.connect(*args)
4
+ new(*args).connect
5
+ end
6
+
7
+ def initialize(api_url, aws_secret_id, aws_secret_key)
8
+ @api_url, @aws_secret_id, @aws_secret_key = api_url, aws_secret_id, aws_secret_key
9
+ end
10
+
11
+ def connect
12
+ @rest = RestClient::Resource.new(@api_url)
13
+ self
14
+ end
15
+
16
+ def get_envs
17
+ @envs ||= call_api("environments")
18
+ end
19
+
20
+ def get_json(instance_id)
21
+ call_api("json_for_instance", :instance_id => instance_id)
22
+ end
23
+
24
+ def deploy_recipes(type, env_id)
25
+ response = call_api(path_for(type), :id => env_id)
26
+ response[0] == 'working'
27
+ end
28
+
29
+ private
30
+
31
+ def path_for(type)
32
+ case type
33
+ when :main: 'deploy_main_recipes'
34
+ when :custom: 'deploy_recipes'
35
+ else
36
+ abort "Unknown recipe type: #{type}"
37
+ end
38
+ end
39
+
40
+ def keys
41
+ @keys ||= {:aws_secret_id => @aws_secret_id, :aws_secret_key => @aws_secret_key}
42
+ end
43
+
44
+ def call_api(path, opts={})
45
+ JSON.parse(@rest["/api/#{path}"].post(opts.merge(keys), {"Accept" => "application/json"}))
46
+ rescue RestClient::RequestFailed => e
47
+ case e.http_code
48
+ when 503
49
+ sleep 10 # Nanite, save us...
50
+ retry
51
+ else
52
+ raise "API call to Engine Yard failed with status #{e.http_code}."
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,108 @@
1
+ require 'open-uri'
2
+
3
+ module EY::Recipes
4
+ class BucketMinder
5
+ def initialize(opts={})
6
+ AWS::S3::Base.establish_connection!(
7
+ :access_key_id => opts[:aws_secret_id],
8
+ :secret_access_key => opts[:aws_secret_key]
9
+ )
10
+ @instance_id = opts[:instance_id]
11
+ @type = opts[:type]
12
+ @env = opts[:env]
13
+ @opts = opts
14
+ opts[:extension] ||= "tgz"
15
+ @keep = opts[:keep]
16
+ @name = "#{Time.now.strftime("%Y-%m-%dT%H:%M:%S").gsub(/:/, '-')}.#{@type}.#{opts[:extension]}"
17
+ end
18
+
19
+ def bucket
20
+ @bucket ||= begin
21
+ buck = "#{@env}-#{@type}-#{@instance_id}-#{Digest::SHA1.hexdigest(@opts[:aws_secret_id])[0..6]}"
22
+ begin
23
+ AWS::S3::Bucket.create buck
24
+ rescue AWS::S3::ResponseError
25
+ end
26
+ buck
27
+ end
28
+ end
29
+
30
+ def upload_object(file)
31
+ AWS::S3::S3Object.store(
32
+ @name,
33
+ open(file),
34
+ bucket,
35
+ :access => :private
36
+ )
37
+ FileUtils.rm file
38
+ puts "successful upload: #{@name}"
39
+ true
40
+ end
41
+
42
+ def download(index, printer = false)
43
+ obj = list[index.to_i]
44
+ puts "downloading: #{obj}" if printer
45
+ File.open(obj.key, 'wb') do |f|
46
+ print "." if printer
47
+ obj.value {|chunk| f.write chunk }
48
+ end
49
+ puts if printer
50
+ puts "finished" if printer
51
+ obj.key
52
+ end
53
+
54
+ def cleanup
55
+ begin
56
+ list[0...-(@keep)].each do |o|
57
+ puts "deleting: #{o.key}"
58
+ o.delete
59
+ end
60
+ # S3's eventual consistency sometimes causes really weird
61
+ # failures.
62
+ # Since cleanup happens every time and will clean up all stale
63
+ # objects, we can just ignore S3-interaction failures. It'll
64
+ # work next time.
65
+ rescue AWS::S3::S3Exception, AWS::S3::Error
66
+ nil
67
+ end
68
+ end
69
+
70
+ def clear_bucket
71
+ list.each do |o|
72
+ puts "deleting: #{o.key}"
73
+ o.delete
74
+ end
75
+ end
76
+
77
+ def rollback
78
+ o = list.last
79
+ puts "rolling back: #{o.key}"
80
+ o.delete
81
+ end
82
+
83
+ def list(printer = false)
84
+ objects = AWS::S3::Bucket.objects(bucket)
85
+ object_last_modified = objects.map do |o|
86
+ # NB: the last-modified value may be nil.
87
+ [o, o.about['last-modified']]
88
+ end.inject({}) do |acc, (obj, obj_last_modified)|
89
+ acc.merge(obj => obj_last_modified)
90
+ end
91
+
92
+ # sort only the remaining ones
93
+ objects = objects.find_all do |o|
94
+ object_last_modified[o]
95
+ end.sort do |a, b|
96
+ DateTime.parse(object_last_modified[a]) <=> DateTime.parse(object_last_modified[a])
97
+ end
98
+
99
+ puts "listing bucket #{bucket}" if printer && !objects.empty?
100
+ if printer
101
+ objects.each_with_index do |b,i|
102
+ puts "#{i}:#{@env} #{b.key}"
103
+ end
104
+ end
105
+ objects
106
+ end
107
+ end
108
+ end
@@ -0,0 +1,259 @@
1
+ module EY::Recipes
2
+ class CLI
3
+ def abort(message)
4
+ raise message
5
+ end
6
+
7
+ LOG_FILE = "/var/log/chef.log"
8
+ def self.run(args)
9
+ $stdout.sync = true
10
+
11
+ command = :list_envs
12
+ defaults = {
13
+ :config => '~/.ey-cloud.yml',
14
+ :type => 'recipes',
15
+ :keep => 5
16
+ }
17
+
18
+ options = {}
19
+ # Build a parser for the command line arguments
20
+ opts = OptionParser.new do |opts|
21
+ opts.version = "0.0.1"
22
+
23
+ opts.banner = "Usage: ey-recipes [-flag] [argument]"
24
+ opts.define_head "ey-recipes: managing your recipes..."
25
+ opts.separator '*'*80
26
+
27
+ opts.on("-c", "--config CONFIG", "Use config file.") do |config|
28
+ options[:config] = config
29
+ end
30
+
31
+ opts.on("-l", "--list-recipes ENV", "List recipes for ENV") do |env|
32
+ command = :list_recipes
33
+ options[:env_name] = env
34
+ end
35
+
36
+ opts.on("-u", "--upload ENV", "Upload a new recipe set so s3 but don't run it. Be sure to commit your changes to your git repo before deploying as we create a git archive of HEAD.") do |env|
37
+ command = :upload
38
+ options[:env_name] = env
39
+ end
40
+
41
+ opts.on("--clear ENV", "Clear out any customer recipes attached to ENV") do |env|
42
+ command = :clear
43
+ options[:env_name] = env
44
+ end
45
+
46
+ opts.on("-d", "--deploy ENV", "Upload a new recipe set and run them on your instances. Be sure to commit your changes to your git repo before deploying as we create a git archive of HEAD.") do |env|
47
+ command = :deploy_custom
48
+ options[:env_name] = env
49
+ end
50
+
51
+ opts.on("-r", "--rollback ENV", "Roll back to the previous recipe set for ENV and run them on your instances.") do |env|
52
+ command = :rollback
53
+ options[:env_name] = env
54
+ end
55
+
56
+ opts.on("--deploy-main ENV", "Redeploy the main EY recipe set and run it on your environment ENV") do |env|
57
+ command = :deploy_main
58
+ options[:env_name] = env
59
+ end
60
+
61
+ opts.on("--view-main-log ENV", "view the last main ey recipe run log file for ENV") do |env|
62
+ command = :view_main_logs
63
+ options[:env_name] = env
64
+ end
65
+
66
+ opts.on("--view-log ENV", "view the last custom chef recipe run log file for ENV") do |env|
67
+ command = :view_custom_logs
68
+ options[:env_name] = env
69
+ end
70
+ end
71
+
72
+ begin
73
+ opts.parse!(args)
74
+ rescue OptionParser::ParseError => e
75
+ abort(e.message)
76
+ end
77
+
78
+ ey = nil
79
+ if File.exist?(config = File.expand_path(options[:config] || defaults[:config]))
80
+ ey = new(options = defaults.merge(YAML::load(File.read(config))).merge(options))
81
+ else
82
+ puts "You need to have an ~/.ey-cloud.yml file with your credentials in it to use this tool.\nOr point it at a yaml file with -c path/to/ey-cloud.yml"
83
+ exit 1
84
+ end
85
+
86
+ if ey.respond_to?(command)
87
+ ey.send(command)
88
+ else
89
+ abort "Unknown command: #{command}"
90
+ end
91
+ end
92
+
93
+ def initialize(opts)
94
+ @opts = opts
95
+ @api = API.connect(opts[:api], opts[:aws_secret_id], opts[:aws_secret_key])
96
+ @lock = Mutex.new
97
+ end
98
+
99
+ def list_recipes
100
+ recipes_bucket.list(true)
101
+ end
102
+
103
+ def list_envs
104
+ puts "Current Environments:"
105
+ envs.each do |name,data|
106
+ puts "env: #{name} running instances: #{data['instances']}"
107
+ puts " instance_ids: #{data['instance_ids'].inspect}"
108
+ end
109
+ end
110
+
111
+ def upload
112
+ upload_recipes
113
+ end
114
+
115
+ def clear
116
+ recipes_bucket.clear_bucket
117
+ end
118
+
119
+ def deploy_custom
120
+ upload_recipes
121
+ deploy_custom_recipes
122
+ end
123
+
124
+ def deploy_custom_recipes
125
+ deploy_recipes(:custom, 'logs')
126
+ end
127
+
128
+ def deploy_main
129
+ deploy_recipes(:main, 'main.logs')
130
+ end
131
+
132
+ def rollback
133
+ recipes_bucket.rollback
134
+ deploy_custom_recipes
135
+ end
136
+
137
+ def view_main_logs
138
+ view_logs('main.logs')
139
+ end
140
+
141
+ def view_custom_logs
142
+ view_logs('logs')
143
+ end
144
+
145
+ def get_json
146
+ @api.get_json(@opts[:instance])
147
+ end
148
+
149
+ def deploy_recipes(type, log_type)
150
+ unless env['instances'] > 0
151
+ abort "There are no running instances for ENV: #{env_name}"
152
+ end
153
+
154
+ puts "deploying #{type} recipes..."
155
+ if @api.deploy_recipes(type, env['id'])
156
+ wait_for_logs(log_type)
157
+ else
158
+ abort "deploying #{type} recipes failed..."
159
+ end
160
+ end
161
+
162
+ def upload_recipes
163
+ unless File.exist?("cookbooks")
164
+ abort "you must run this command from the root of your chef recipe git repo"
165
+ end
166
+
167
+ file = "recipes.#{$$}.#{Time.now.to_i}.tmp.tgz"
168
+ tarcmd = "git archive --format=tar HEAD | gzip > #{file}"
169
+ if system(tarcmd)
170
+ recipes_bucket.upload_object(file)
171
+ recipes_bucket.cleanup
172
+ else
173
+ abort "Failed to upload recipes for #{env_name}"
174
+ end
175
+ end
176
+
177
+ def view_logs(log_type)
178
+ env['instance_ids'].each do |instance_id|
179
+ log_bucket = log_bucket_for(log_type, instance_id)
180
+ puts "Logs for: #{instance_id}"
181
+ puts display_logs(log_bucket.list.last)
182
+ end
183
+ end
184
+
185
+ private
186
+
187
+ def recipes_bucket
188
+ @recipes_bucket ||= bucket_for('recipes', 'recipes', 'tgz')
189
+ end
190
+
191
+ def envs
192
+ @envs ||= @api.get_envs
193
+ end
194
+
195
+ def env_name
196
+ @opts[:env_name] || abort("must provide environment name")
197
+ end
198
+
199
+ def env
200
+ @env ||= find_env(env_name)
201
+ end
202
+
203
+ def find_env(env_name)
204
+ unless env = envs[env_name]
205
+ abort "#{env} is not a valid environment name, your available environments are:\n#{envs.keys.join("\n")}"
206
+ end
207
+ env
208
+ end
209
+
210
+ def wait_for_logs(log_type)
211
+ threads = []
212
+ env['instance_ids'].each do |instance_id|
213
+ threads << Thread.new { wait_for_log(log_type, instance_id) }
214
+ end
215
+ threads.each {|t| t.join}
216
+ end
217
+
218
+ def wait_for_log(log_type, instance_id)
219
+ logbucket = log_bucket_for(log_type, instance_id)
220
+ newest = @lock.synchronize { logbucket.list.last }
221
+ count = 0
222
+ until @lock.synchronize { newest != logbucket.list.last }
223
+ print "."
224
+ sleep 3
225
+ count += 1
226
+ if count > 600
227
+ puts "timed out waiting for deployed logs"
228
+ exit 1
229
+ end
230
+ end
231
+ puts
232
+ puts "retrieving logs..."
233
+ puts @lock.synchronize { display_logs(logbucket.list.last) }
234
+ end
235
+
236
+ def log_bucket_for(log_type, instance_id)
237
+ bucket_for(log_type, instance_id, 'gz')
238
+ end
239
+
240
+ def bucket_for(log_type, instance_id, extension)
241
+ BucketMinder.new(:aws_secret_key => @opts[:aws_secret_key],
242
+ :aws_secret_id => @opts[:aws_secret_id],
243
+ :keep => @opts[:keep],
244
+ :env => env_name,
245
+ :type => log_type,
246
+ :extension => extension,
247
+ :instance_id => instance_id)
248
+ end
249
+
250
+ def display_logs(obj)
251
+ if obj
252
+ Zlib::GzipReader.new(StringIO.new(obj.value, 'rb')).read
253
+ else
254
+ "no logs..."
255
+ end
256
+ end
257
+
258
+ end
259
+ end
@@ -0,0 +1,3 @@
1
+ module EY::Recipes
2
+ VERSION = "0.9.1"
3
+ end
data/lib/ey_recipes.rb ADDED
@@ -0,0 +1,18 @@
1
+ require 'json'
2
+ require 'restclient'
3
+ require 'aws/s3'
4
+ require "optparse"
5
+
6
+ module EY
7
+ module Recipes
8
+ def self.run(args)
9
+ CLI.run(args)
10
+ end
11
+ end
12
+ end
13
+
14
+ current_dir = File.expand_path(File.dirname(__FILE__) + '/ey_recipes')
15
+ require current_dir + '/bucket_minder'
16
+ require current_dir + '/api'
17
+ require current_dir + '/cli'
18
+ require current_dir + '/version'
metadata ADDED
@@ -0,0 +1,88 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: ey_recipes
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.9.1
5
+ platform: ruby
6
+ authors:
7
+ - Ninjas
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2010-01-20 00:00:00 -08:00
13
+ default_executable:
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: json
17
+ type: :runtime
18
+ version_requirement:
19
+ version_requirements: !ruby/object:Gem::Requirement
20
+ requirements:
21
+ - - ">="
22
+ - !ruby/object:Gem::Version
23
+ version: "0"
24
+ version:
25
+ - !ruby/object:Gem::Dependency
26
+ name: aws-s3
27
+ type: :runtime
28
+ version_requirement:
29
+ version_requirements: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: "0"
34
+ version:
35
+ - !ruby/object:Gem::Dependency
36
+ name: rest-client
37
+ type: :runtime
38
+ version_requirement:
39
+ version_requirements: !ruby/object:Gem::Requirement
40
+ requirements:
41
+ - - ">="
42
+ - !ruby/object:Gem::Version
43
+ version: "0"
44
+ version:
45
+ description: Gem for kicking off chef recipes
46
+ email: awsmdev@engineyard.com
47
+ executables:
48
+ - ey-recipes
49
+ extensions: []
50
+
51
+ extra_rdoc_files: []
52
+
53
+ files:
54
+ - lib/ey_recipes/api.rb
55
+ - lib/ey_recipes/bucket_minder.rb
56
+ - lib/ey_recipes/cli.rb
57
+ - lib/ey_recipes/version.rb
58
+ - lib/ey_recipes.rb
59
+ has_rdoc: true
60
+ homepage: http://github.com/engineyard/ey_recipes
61
+ licenses: []
62
+
63
+ post_install_message:
64
+ rdoc_options: []
65
+
66
+ require_paths:
67
+ - lib
68
+ required_ruby_version: !ruby/object:Gem::Requirement
69
+ requirements:
70
+ - - ">="
71
+ - !ruby/object:Gem::Version
72
+ version: "0"
73
+ version:
74
+ required_rubygems_version: !ruby/object:Gem::Requirement
75
+ requirements:
76
+ - - ">="
77
+ - !ruby/object:Gem::Version
78
+ version: "0"
79
+ version:
80
+ requirements: []
81
+
82
+ rubyforge_project:
83
+ rubygems_version: 1.3.5
84
+ signing_key:
85
+ specification_version: 3
86
+ summary: Gem for kicking off chef recipes
87
+ test_files: []
88
+