ey_recipes 0.9.1

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/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
+