ey-beta 0.0.4

Sign up to get free protection for your applications and to get access to all the features.
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
+