ey-beta 0.0.4
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/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
|
+
|