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 +5 -0
- data/lib/ey_recipes/api.rb +56 -0
- data/lib/ey_recipes/bucket_minder.rb +108 -0
- data/lib/ey_recipes/cli.rb +259 -0
- data/lib/ey_recipes/version.rb +3 -0
- data/lib/ey_recipes.rb +18 -0
- metadata +88 -0
data/bin/ey-recipes
ADDED
@@ -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
|
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
|
+
|