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