ey-beta 0.0.4
Sign up to get free protection for your applications and to get access to all the features.
- data/LICENSE +20 -0
- data/README.rdoc +3 -0
- data/Rakefile +48 -0
- data/TODO +4 -0
- data/bin/ey-recipes +111 -0
- data/lib/bucket_minder.rb +92 -0
- data/lib/ey.rb +273 -0
- data/lib/snapshot_minder.rb +123 -0
- data/spec/ey_spec.rb +7 -0
- data/spec/spec_helper.rb +2 -0
- metadata +63 -0
data/LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2009 Engine Yard Inc.
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.rdoc
ADDED
data/Rakefile
ADDED
@@ -0,0 +1,48 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'rake/gempackagetask'
|
3
|
+
require 'rubygems/specification'
|
4
|
+
require 'date'
|
5
|
+
|
6
|
+
GEM = "ey-beta"
|
7
|
+
GEM_VERSION = "0.0.4"
|
8
|
+
AUTHOR = "Ezra Zygmuntowicz"
|
9
|
+
EMAIL = "ez@engineyard.com"
|
10
|
+
HOMEPAGE = "http://engineyard.com/solo"
|
11
|
+
SUMMARY = "Command line interface to Engine Yard's cloud"
|
12
|
+
|
13
|
+
spec = Gem::Specification.new do |s|
|
14
|
+
s.name = GEM
|
15
|
+
s.version = GEM_VERSION
|
16
|
+
s.platform = Gem::Platform::RUBY
|
17
|
+
s.has_rdoc = true
|
18
|
+
s.extra_rdoc_files = ["README.rdoc", "LICENSE", 'TODO']
|
19
|
+
s.summary = SUMMARY
|
20
|
+
s.description = s.summary
|
21
|
+
s.author = AUTHOR
|
22
|
+
s.email = EMAIL
|
23
|
+
s.homepage = HOMEPAGE
|
24
|
+
s.bindir = "bin"
|
25
|
+
s.executables = %w( ey-recipes )
|
26
|
+
# Uncomment this to add a dependency
|
27
|
+
# s.add_dependency "foo"
|
28
|
+
|
29
|
+
s.require_path = 'lib'
|
30
|
+
s.autorequire = GEM
|
31
|
+
s.files = %w(LICENSE README.rdoc Rakefile TODO) + Dir.glob("{lib,spec}/**/*")
|
32
|
+
end
|
33
|
+
|
34
|
+
Rake::GemPackageTask.new(spec) do |pkg|
|
35
|
+
pkg.gem_spec = spec
|
36
|
+
end
|
37
|
+
|
38
|
+
desc "install the gem locally"
|
39
|
+
task :install => [:package] do
|
40
|
+
sh %{sudo gem install pkg/#{GEM}-#{GEM_VERSION}}
|
41
|
+
end
|
42
|
+
|
43
|
+
desc "create a gemspec file"
|
44
|
+
task :make_spec do
|
45
|
+
File.open("#{GEM}.gemspec", "w") do |file|
|
46
|
+
file.puts spec.to_ruby
|
47
|
+
end
|
48
|
+
end
|
data/TODO
ADDED
data/bin/ey-recipes
ADDED
@@ -0,0 +1,111 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
require 'rubygems'
|
3
|
+
require 'yaml'
|
4
|
+
require "optparse"
|
5
|
+
require "json"
|
6
|
+
require 'ey'
|
7
|
+
|
8
|
+
defaults = {:config => '~/.ey-cloud.yml',
|
9
|
+
:command => :get_envs,
|
10
|
+
:type => 'recipes',
|
11
|
+
:keep => 5}
|
12
|
+
options = {}
|
13
|
+
# Build a parser for the command line arguments
|
14
|
+
opts = OptionParser.new do |opts|
|
15
|
+
opts.version = "0.0.1"
|
16
|
+
|
17
|
+
opts.banner = "Usage: ey-recipes [-flag] [argument]"
|
18
|
+
opts.define_head "ey-recipes: managing your recipes..."
|
19
|
+
opts.separator '*'*80
|
20
|
+
|
21
|
+
opts.on("-l", "--list-recipes ENV", "List recipes for ENV") do |env|
|
22
|
+
options[:env] = env
|
23
|
+
options[:command] = :list
|
24
|
+
end
|
25
|
+
|
26
|
+
opts.on("-j", "--json ENV", "Get The DNA JSON for ENV") do |env|
|
27
|
+
options[:env] = env
|
28
|
+
options[:command] = :get_json
|
29
|
+
end
|
30
|
+
|
31
|
+
opts.on("-c", "--config CONFIG", "Use config file.") do |config|
|
32
|
+
options[:config] = config
|
33
|
+
end
|
34
|
+
|
35
|
+
opts.on("-r", "--rollback ENV", "Roll back to the previous recipe set for ENV and run them on your instances.") do |env|
|
36
|
+
options[:env] = env
|
37
|
+
options[:command] = :rollback
|
38
|
+
end
|
39
|
+
|
40
|
+
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|
|
41
|
+
options[:command] = :deploy
|
42
|
+
options[:env] = env
|
43
|
+
end
|
44
|
+
|
45
|
+
opts.on("--deploy-main ENV", "Redeploy the main EY recipe set and run it on your environment ENV") do |env|
|
46
|
+
options[:command] = :deploy_main
|
47
|
+
options[:env] = env
|
48
|
+
end
|
49
|
+
|
50
|
+
opts.on("-n", "--newest ENV", "download, install and run the current custom recipe set for ENV (THIS COMMAND CAN ONLY BE RUN ON YOUR EC2 INSTANCE)") do |env|
|
51
|
+
options[:command] = :converge
|
52
|
+
options[:env] = env
|
53
|
+
end
|
54
|
+
|
55
|
+
opts.on("--view-log ENV", "view the last custom chef recipe run log file for ENV") do |env|
|
56
|
+
options[:command] = :view_logs
|
57
|
+
options[:env] = env
|
58
|
+
end
|
59
|
+
|
60
|
+
opts.on("--view-main-log ENV", "view the last main ey recipe run log file for ENV") do |env|
|
61
|
+
options[:command] = :view_logs
|
62
|
+
options[:env] = env
|
63
|
+
options[:main] = true
|
64
|
+
end
|
65
|
+
|
66
|
+
opts.on("--main ENV", "download, install and run the main ey recipe set for ENV (THIS COMMAND CAN ONLY BE RUN ON YOUR EC2 INSTANCE)") do |env|
|
67
|
+
options[:command] = :converge
|
68
|
+
options[:env] = env
|
69
|
+
options[:main] = true
|
70
|
+
options[:recipeloc] = "/etc/chef/recipes"
|
71
|
+
end
|
72
|
+
|
73
|
+
opts.on("--logs", "set the type to logs") do
|
74
|
+
options[:type] = 'logs'
|
75
|
+
options[:extension] = 'gz'
|
76
|
+
end
|
77
|
+
|
78
|
+
end
|
79
|
+
|
80
|
+
opts.parse!
|
81
|
+
|
82
|
+
ey = nil
|
83
|
+
if File.exist?(config = File.expand_path(defaults[:config]))
|
84
|
+
ey = EY::ChefRecipes.new(options = defaults.merge(YAML::load(IO.read(config))).merge(options))
|
85
|
+
elsif File.exist?(config = "/etc/.ey-cloud.yml")
|
86
|
+
ey = EY::ChefRecipes.new(options = defaults.merge(YAML::load(IO.read(config))).merge(options))
|
87
|
+
else
|
88
|
+
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"
|
89
|
+
exit 1
|
90
|
+
end
|
91
|
+
|
92
|
+
case options[:command]
|
93
|
+
when :list
|
94
|
+
ey.list true
|
95
|
+
when :rollback
|
96
|
+
ey.rollback
|
97
|
+
when :deploy
|
98
|
+
ey.deploy
|
99
|
+
when :get_json
|
100
|
+
jj ey.get_json
|
101
|
+
when :deploy_main
|
102
|
+
ey.deploy_main
|
103
|
+
when :view_logs
|
104
|
+
ey.view_logs
|
105
|
+
when :get_envs
|
106
|
+
envs = ey.get_envs
|
107
|
+
puts "Current Environments:"
|
108
|
+
envs.each {|k,v| puts "env: #{k} running instances: #{v['instances']}" }
|
109
|
+
when :converge
|
110
|
+
ey.converge
|
111
|
+
end
|
@@ -0,0 +1,92 @@
|
|
1
|
+
module AWS::S3
|
2
|
+
class S3Object
|
3
|
+
def <=>(other)
|
4
|
+
DateTime.parse(self.about['last-modified']) <=> DateTime.parse(other.about['last-modified'])
|
5
|
+
end
|
6
|
+
end
|
7
|
+
end
|
8
|
+
|
9
|
+
module EY
|
10
|
+
|
11
|
+
class BucketMinder
|
12
|
+
|
13
|
+
def initialize(opts={})
|
14
|
+
AWS::S3::Base.establish_connection!(
|
15
|
+
:access_key_id => opts[:aws_secret_id],
|
16
|
+
:secret_access_key => opts[:aws_secret_key]
|
17
|
+
)
|
18
|
+
@type = opts[:type]
|
19
|
+
@env = opts[:env]
|
20
|
+
opts[:extension] ||= "tgz"
|
21
|
+
@keep = opts[:keep]
|
22
|
+
@bucket = "#{@env}-#{@type}-#{Digest::SHA1.hexdigest(opts[:aws_secret_id])[0..6]}"
|
23
|
+
@name = "#{Time.now.strftime("%Y-%m-%dT%H:%M:%S").gsub(/:/, '-')}.#{@type}.#{opts[:extension]}"
|
24
|
+
begin
|
25
|
+
AWS::S3::Bucket.create @bucket
|
26
|
+
rescue AWS::S3::ResponseError
|
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
|
+
list[0...-(@keep)].each{|o|
|
56
|
+
puts "deleting: #{o.key}"
|
57
|
+
o.delete
|
58
|
+
}
|
59
|
+
end
|
60
|
+
|
61
|
+
def get_current
|
62
|
+
name = download(list.size - 1)
|
63
|
+
File.expand_path(name)
|
64
|
+
end
|
65
|
+
|
66
|
+
def clear_bucket
|
67
|
+
list.each do |o|
|
68
|
+
puts "deleting: #{o.key}"
|
69
|
+
o.delete
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
def rollback
|
74
|
+
o = list.last
|
75
|
+
puts "rolling back: #{o.key}"
|
76
|
+
o.delete
|
77
|
+
end
|
78
|
+
|
79
|
+
def list(printer = false)
|
80
|
+
objects = AWS::S3::Bucket.objects(@bucket).sort
|
81
|
+
puts "listing bucket #{@bucket}" if printer && !objects.empty?
|
82
|
+
if printer
|
83
|
+
objects.each_with_index do |b,i|
|
84
|
+
puts "#{i}:#{@env} #{b.key}"
|
85
|
+
end
|
86
|
+
end
|
87
|
+
objects
|
88
|
+
end
|
89
|
+
|
90
|
+
end
|
91
|
+
|
92
|
+
end
|
data/lib/ey.rb
ADDED
@@ -0,0 +1,273 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'aws/s3'
|
3
|
+
require 'date'
|
4
|
+
require 'digest'
|
5
|
+
require 'fileutils'
|
6
|
+
require File.join(File.dirname(__FILE__), 'bucket_minder')
|
7
|
+
require 'rest_client'
|
8
|
+
require 'json'
|
9
|
+
require 'open-uri'
|
10
|
+
require 'zlib'
|
11
|
+
require 'stringio'
|
12
|
+
|
13
|
+
$stdout.sync = true
|
14
|
+
|
15
|
+
module EY
|
16
|
+
|
17
|
+
class ChefRecipes
|
18
|
+
def initialize(opts={})
|
19
|
+
raise ArgumentError.new("must provide environment name") unless opts[:env] or opts[:command] == :get_envs
|
20
|
+
@opts = opts
|
21
|
+
@eyenv = opts[:eyenv] || 'production'
|
22
|
+
@env = opts[:env]
|
23
|
+
@recipeloc = opts[:recipeloc] || "/etc/chef-custom/recipes"
|
24
|
+
@rest = RestClient::Resource.new(opts[:api])
|
25
|
+
@keys = {:aws_secret_id => @opts[:aws_secret_id], :aws_secret_key => @opts[:aws_secret_key]}
|
26
|
+
@bucket = BucketMinder.new(@opts)
|
27
|
+
unless get_envs[@env] or opts[:command] == :get_envs
|
28
|
+
puts %Q{#{@env} is not a valid environment name, your available environments are:\n#{get_envs.keys.join("\n")}}
|
29
|
+
exit 1
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def call_api(path, opts={})
|
34
|
+
JSON.parse(@rest["/api/#{path}"].post(@keys.merge(opts)))
|
35
|
+
rescue RestClient::RequestFailed
|
36
|
+
puts "API call to Engine Yard failed. Are there any running instances for #{@env}"
|
37
|
+
exit 1
|
38
|
+
end
|
39
|
+
|
40
|
+
def get_envs
|
41
|
+
@_envs ||= call_api("environments")
|
42
|
+
end
|
43
|
+
|
44
|
+
def get_json(instance_id = nil)
|
45
|
+
env = get_envs[@env]
|
46
|
+
call_api("json_for_instance", :id => env['id'], :instance_id => instance_id)
|
47
|
+
rescue
|
48
|
+
{}
|
49
|
+
end
|
50
|
+
|
51
|
+
def converge
|
52
|
+
require 'chef'
|
53
|
+
require 'chef/client'
|
54
|
+
FileUtils.mkdir_p @recipeloc
|
55
|
+
logtype = nil
|
56
|
+
build_problem = false
|
57
|
+
out = log_to_string do
|
58
|
+
begin
|
59
|
+
instance_id = open("http://169.254.169.254/latest/meta-data/instance-id").gets
|
60
|
+
defaults = {
|
61
|
+
:log_level => :info,
|
62
|
+
:solo => true,
|
63
|
+
:cookbook_path => "#{@recipeloc}/cookbooks",
|
64
|
+
:file_store_path => "#{@recipeloc}/",
|
65
|
+
:file_cache_path => "#{@recipeloc}/",
|
66
|
+
:node_name => instance_id
|
67
|
+
}
|
68
|
+
Chef::Config.configure { |c| c.merge!(defaults) }
|
69
|
+
Chef::Log::Formatter.show_time = false
|
70
|
+
Chef::Log.level(Chef::Config[:log_level])
|
71
|
+
|
72
|
+
if File.exist?("#{@recipeloc}/cookbooks")
|
73
|
+
FileUtils.rm_rf("#{@recipeloc}/cookbooks")
|
74
|
+
end
|
75
|
+
|
76
|
+
if @opts[:main]
|
77
|
+
logtype = "main.logs"
|
78
|
+
install_main_recipes
|
79
|
+
else
|
80
|
+
logtype = "logs"
|
81
|
+
install_recipes
|
82
|
+
end
|
83
|
+
json = get_json(instance_id)
|
84
|
+
File.open("/etc/chef/dna.json", 'w'){|f| f.puts JSON.pretty_generate(json)}
|
85
|
+
c = Chef::Client.new
|
86
|
+
c.json_attribs = json
|
87
|
+
c.run_solo
|
88
|
+
rescue Object => e
|
89
|
+
build_problem = true
|
90
|
+
Chef::Log.error(describe_error(e))
|
91
|
+
end
|
92
|
+
end
|
93
|
+
file = "/tmp/chef-#{rand(1000)}.log"
|
94
|
+
File.open(file, 'w'){ |f| f.write out }
|
95
|
+
|
96
|
+
@bucket = BucketMinder.new(@opts.merge(:type => logtype, :extension => 'gz'))
|
97
|
+
upload_logs(file)
|
98
|
+
@bucket.cleanup
|
99
|
+
FileUtils.rm(file)
|
100
|
+
exit 1 if build_problem
|
101
|
+
end
|
102
|
+
|
103
|
+
def install_main_recipes
|
104
|
+
unless @opts[:main_recipes]
|
105
|
+
puts "you must specify :main_recipes: in your ey-cloud.yml"
|
106
|
+
exit 1
|
107
|
+
end
|
108
|
+
recipes_path = Chef::Config[:cookbook_path].gsub(/cookbooks/, '')
|
109
|
+
FileUtils.mkdir_p recipes_path
|
110
|
+
path = File.join(recipes_path, 'recipes.tgz')
|
111
|
+
File.open(path, 'wb') do |f|
|
112
|
+
f.write open(@opts[:main_recipes]).read
|
113
|
+
end
|
114
|
+
system("cd #{recipes_path} && tar xzf #{path}")
|
115
|
+
FileUtils.rm path
|
116
|
+
end
|
117
|
+
|
118
|
+
def deploy
|
119
|
+
unless File.exist?("cookbooks")
|
120
|
+
puts "you must run this command from the root of your chef recipe git repo"
|
121
|
+
exit 1
|
122
|
+
end
|
123
|
+
env = get_envs[@env]
|
124
|
+
unless env['instances'] > 0
|
125
|
+
puts "There are no running instances for ENV: #{@env}"
|
126
|
+
exit 1
|
127
|
+
end
|
128
|
+
if upload_recipes
|
129
|
+
if env
|
130
|
+
puts "deploying recipes..."
|
131
|
+
if call_api("deploy_recipes", :id => env['id'] )[0] == 'working'
|
132
|
+
wait_for_logs('logs')
|
133
|
+
else
|
134
|
+
puts "deploying main recipes failed..."
|
135
|
+
end
|
136
|
+
else
|
137
|
+
puts "No matching environments"
|
138
|
+
end
|
139
|
+
else
|
140
|
+
puts "Failed to deploy: #{@env}"
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
def rollback
|
145
|
+
@bucket.rollback
|
146
|
+
env = get_envs[@env]
|
147
|
+
if env
|
148
|
+
puts "rolling back recipes..."
|
149
|
+
call_api("deploy_recipes", :id => env['id'] )
|
150
|
+
wait_for_logs('logs')
|
151
|
+
else
|
152
|
+
puts "No matching environments for #{@env}"
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
156
|
+
def wait_for_logs(logtype)
|
157
|
+
logbucket = BucketMinder.new(@opts.merge(:type => logtype, :extension => 'gz'))
|
158
|
+
newest = logbucket.list.last
|
159
|
+
count = 0
|
160
|
+
until newest != logbucket.list.last
|
161
|
+
print "."
|
162
|
+
sleep 3
|
163
|
+
count += 1
|
164
|
+
if count > 600
|
165
|
+
puts "timed out waiting for deployed logs"
|
166
|
+
exit 1
|
167
|
+
end
|
168
|
+
end
|
169
|
+
puts
|
170
|
+
puts "retrieving logs..."
|
171
|
+
puts display_logs(logbucket.list.last)
|
172
|
+
end
|
173
|
+
|
174
|
+
def deploy_main
|
175
|
+
env = get_envs[@env]
|
176
|
+
if env
|
177
|
+
unless env['instances'] > 0
|
178
|
+
puts "There are no running instances for ENV: #{@env}"
|
179
|
+
exit 1
|
180
|
+
end
|
181
|
+
puts "deploying main EY recipes..."
|
182
|
+
if call_api("deploy_main_recipes", :id => env['id'] )[0] == 'working'
|
183
|
+
wait_for_logs('main.logs')
|
184
|
+
else
|
185
|
+
puts "deploying main recipes failed..."
|
186
|
+
end
|
187
|
+
else
|
188
|
+
puts "No matching environments"
|
189
|
+
end
|
190
|
+
end
|
191
|
+
|
192
|
+
def display_logs(obj)
|
193
|
+
if obj
|
194
|
+
Zlib::GzipReader.new(StringIO.new(obj.value, 'rb')).read
|
195
|
+
else
|
196
|
+
"no logs..."
|
197
|
+
end
|
198
|
+
end
|
199
|
+
|
200
|
+
def view_logs
|
201
|
+
logtype = "#{@opts[:main] ? 'main.' : ''}logs"
|
202
|
+
logbucket = BucketMinder.new(@opts.merge(:type => logtype, :extension => 'gz'))
|
203
|
+
puts display_logs(logbucket.list.last)
|
204
|
+
end
|
205
|
+
|
206
|
+
def upload_recipes
|
207
|
+
file = "recipes.#{rand(1000)}.tmp.tgz"
|
208
|
+
tarcmd = "git archive --format=tar HEAD | gzip > #{file}"
|
209
|
+
if system(tarcmd)
|
210
|
+
@bucket.upload_object(file)
|
211
|
+
@bucket.cleanup
|
212
|
+
true
|
213
|
+
else
|
214
|
+
puts "Unable to tar up recipes for #{@opts[:env]} wtf?"
|
215
|
+
false
|
216
|
+
end
|
217
|
+
end
|
218
|
+
|
219
|
+
def upload_logs(file)
|
220
|
+
name = "#{file}.#{rand(1000)}.tgz"
|
221
|
+
tarcmd = "cat #{file} | gzip > #{name}"
|
222
|
+
if system(tarcmd)
|
223
|
+
@bucket.upload_object(name)
|
224
|
+
@bucket.cleanup
|
225
|
+
true
|
226
|
+
else
|
227
|
+
puts "Unable to tar up log files for #{@opts[:env]} wtf?"
|
228
|
+
false
|
229
|
+
end
|
230
|
+
end
|
231
|
+
|
232
|
+
def cleanup
|
233
|
+
@bucket.cleanup
|
234
|
+
end
|
235
|
+
|
236
|
+
def list(*args)
|
237
|
+
@bucket.list(*args)
|
238
|
+
end
|
239
|
+
|
240
|
+
def download(*args)
|
241
|
+
@bucket.download(*args)
|
242
|
+
end
|
243
|
+
|
244
|
+
def install_recipes
|
245
|
+
file = get_current
|
246
|
+
Dir.chdir(@recipeloc) {
|
247
|
+
system("tar xzf #{file}")
|
248
|
+
}
|
249
|
+
FileUtils.rm file
|
250
|
+
end
|
251
|
+
|
252
|
+
def get_current
|
253
|
+
@bucket.get_current
|
254
|
+
end
|
255
|
+
|
256
|
+
def clear_bucket
|
257
|
+
@bucket.clear_bucket
|
258
|
+
end
|
259
|
+
|
260
|
+
def describe_error(e)
|
261
|
+
"#{e.class.name}: #{e.message}\n #{e.backtrace.join("\n ")}"
|
262
|
+
end
|
263
|
+
|
264
|
+
def log_to_string(&block)
|
265
|
+
output = StringIO.new
|
266
|
+
Chef::Log.init(output)
|
267
|
+
block.call
|
268
|
+
output.string
|
269
|
+
end
|
270
|
+
|
271
|
+
end
|
272
|
+
|
273
|
+
end
|
@@ -0,0 +1,123 @@
|
|
1
|
+
require 'right_aws'
|
2
|
+
require 'dbi'
|
3
|
+
require 'open-uri'
|
4
|
+
module EY
|
5
|
+
class SnapshotMinder
|
6
|
+
def initialize(opts={})
|
7
|
+
@opts = opts
|
8
|
+
@instance_id = opts[:instance_id]
|
9
|
+
@db = Mysql.new('root', opts[:dbpass])
|
10
|
+
@ec2 = RightAws::Ec2.new(opts[:aws_secret_id], opts[:aws_secret_key])
|
11
|
+
get_instance_id
|
12
|
+
find_volume_ids
|
13
|
+
end
|
14
|
+
|
15
|
+
def find_volume_ids
|
16
|
+
@volume_ids = {}
|
17
|
+
@ec2.describe_volumes.each do |volume|
|
18
|
+
if volume[:aws_instance_id] == @instance_id
|
19
|
+
if volume[:aws_device] == "/dev/sdz1"
|
20
|
+
@volume_ids[:data] = volume[:aws_id]
|
21
|
+
elsif volume[:aws_device] == "/dev/sdz2"
|
22
|
+
@volume_ids[:db] = volume[:aws_id]
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
puts("Volume IDs are #{@volume_ids.inspect}")
|
27
|
+
@volume_ids
|
28
|
+
end
|
29
|
+
|
30
|
+
def list_snapshots
|
31
|
+
@snapshot_ids = {}
|
32
|
+
@ec2.describe_snapshots.sort { |a,b| b[:aws_started_at] <=> a[:aws_started_at] }.each do |snapshot|
|
33
|
+
@volume_ids.each do |mnt, vol|
|
34
|
+
if snapshot[:aws_volume_id] == vol
|
35
|
+
(@snapshot_ids[mnt] ||= []) << snapshot[:aws_id]
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
puts("Snapshots #{@snapshot_ids.inspect}")
|
40
|
+
@snapshot_ids
|
41
|
+
end
|
42
|
+
|
43
|
+
def clean_snapshots(keep=5)
|
44
|
+
list_snapshots
|
45
|
+
@snapshot_ids.each do |mnt, ids|
|
46
|
+
snaps = []
|
47
|
+
@ec2.describe_snapshots(ids).sort { |a,b| b[:aws_started_at] <=> a[:aws_started_at] }.each do |snapshot|
|
48
|
+
snaps << snapshot
|
49
|
+
end
|
50
|
+
snaps[keep..-1].each do |snapshot|
|
51
|
+
puts "deleting snapshot of /#{mnt}: #{snapshot[:aws_id]}"
|
52
|
+
@ec2.delete_snapshot(snapshot[:aws_id])
|
53
|
+
end
|
54
|
+
end
|
55
|
+
list_snapshots
|
56
|
+
end
|
57
|
+
|
58
|
+
def snapshot_volumes
|
59
|
+
snaps = []
|
60
|
+
@volume_ids.each do |vol, vid|
|
61
|
+
case vol
|
62
|
+
when :data
|
63
|
+
snaps << create_snapshot(vid)
|
64
|
+
when :db
|
65
|
+
@db.flush_tables_with_read_lock
|
66
|
+
snaps << create_snapshot(vid)
|
67
|
+
@db.unlock_tables
|
68
|
+
end
|
69
|
+
end
|
70
|
+
snaps
|
71
|
+
end
|
72
|
+
|
73
|
+
def get_instance_id
|
74
|
+
return @instance_id if @instance_id
|
75
|
+
|
76
|
+
open('http://169.254.169.254/latest/meta-data/instance-id') do |f|
|
77
|
+
@instance_id = f.gets
|
78
|
+
end
|
79
|
+
raise "Cannot find instance id!" unless @instance_id
|
80
|
+
puts("Instance ID is #{@instance_id}")
|
81
|
+
@instance_id
|
82
|
+
end
|
83
|
+
|
84
|
+
|
85
|
+
def create_snapshot(volume_id)
|
86
|
+
snap = @ec2.create_snapshot(volume_id)
|
87
|
+
puts("Created snapshot of #{volume_id} as #{snap[:aws_id]}")
|
88
|
+
snap
|
89
|
+
end
|
90
|
+
|
91
|
+
end
|
92
|
+
|
93
|
+
class Mysql
|
94
|
+
|
95
|
+
attr_accessor :dbh
|
96
|
+
|
97
|
+
def initialize(username, password)
|
98
|
+
@username = username
|
99
|
+
@password = password
|
100
|
+
puts("Connecting to MySQL")
|
101
|
+
@dbh = DBI.connect("DBI:Mysql:mysql", username, password)
|
102
|
+
end
|
103
|
+
|
104
|
+
def flush_tables_with_read_lock
|
105
|
+
puts("Flushing tables with read lock")
|
106
|
+
@dbh.do("flush tables with read lock")
|
107
|
+
true
|
108
|
+
end
|
109
|
+
|
110
|
+
def unlock_tables
|
111
|
+
puts("Unlocking tables")
|
112
|
+
@dbh.do("unlock tables")
|
113
|
+
true
|
114
|
+
end
|
115
|
+
|
116
|
+
def disconnect
|
117
|
+
puts("Disconnecting from MySQL")
|
118
|
+
@dbh.disconnect
|
119
|
+
end
|
120
|
+
|
121
|
+
end
|
122
|
+
|
123
|
+
end
|
data/spec/ey_spec.rb
ADDED
data/spec/spec_helper.rb
ADDED
metadata
ADDED
@@ -0,0 +1,63 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: ey-beta
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.4
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Ezra Zygmuntowicz
|
8
|
+
autorequire: ey-beta
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
|
12
|
+
date: 2009-03-04 00:00:00 -08:00
|
13
|
+
default_executable:
|
14
|
+
dependencies: []
|
15
|
+
|
16
|
+
description: Command line interface to Engine Yard's cloud
|
17
|
+
email: ez@engineyard.com
|
18
|
+
executables:
|
19
|
+
- ey-recipes
|
20
|
+
extensions: []
|
21
|
+
|
22
|
+
extra_rdoc_files:
|
23
|
+
- README.rdoc
|
24
|
+
- LICENSE
|
25
|
+
- TODO
|
26
|
+
files:
|
27
|
+
- LICENSE
|
28
|
+
- README.rdoc
|
29
|
+
- Rakefile
|
30
|
+
- TODO
|
31
|
+
- lib/bucket_minder.rb
|
32
|
+
- lib/ey.rb
|
33
|
+
- lib/snapshot_minder.rb
|
34
|
+
- spec/ey_spec.rb
|
35
|
+
- spec/spec_helper.rb
|
36
|
+
has_rdoc: true
|
37
|
+
homepage: http://engineyard.com/solo
|
38
|
+
post_install_message:
|
39
|
+
rdoc_options: []
|
40
|
+
|
41
|
+
require_paths:
|
42
|
+
- lib
|
43
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: "0"
|
48
|
+
version:
|
49
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
50
|
+
requirements:
|
51
|
+
- - ">="
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: "0"
|
54
|
+
version:
|
55
|
+
requirements: []
|
56
|
+
|
57
|
+
rubyforge_project:
|
58
|
+
rubygems_version: 1.3.1
|
59
|
+
signing_key:
|
60
|
+
specification_version: 2
|
61
|
+
summary: Command line interface to Engine Yard's cloud
|
62
|
+
test_files: []
|
63
|
+
|