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 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.
@@ -0,0 +1,3 @@
1
+ == ey
2
+
3
+ A gem that provides...
@@ -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
@@ -0,0 +1,4 @@
1
+ TODO:
2
+ Fix LICENSE with your name
3
+ Fix Rakefile with your name and contact info
4
+ Add your code to lib/<%= name %>.rb
@@ -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
@@ -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
@@ -0,0 +1,7 @@
1
+ require File.dirname(__FILE__) + '/spec_helper'
2
+
3
+ describe "ey" do
4
+ it "should do nothing" do
5
+ true.should == true
6
+ end
7
+ end
@@ -0,0 +1,2 @@
1
+ $TESTING=true
2
+ $:.push File.join(File.dirname(__FILE__), '..', 'lib')
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
+