some 0.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (8) hide show
  1. data/README.rdoc +123 -0
  2. data/Rakefile +25 -0
  3. data/VERSION +1 -0
  4. data/bin/some +174 -0
  5. data/lib/some.rb +297 -0
  6. data/spec/base.rb +21 -0
  7. data/spec/some_spec.rb +31 -0
  8. metadata +75 -0
@@ -0,0 +1,123 @@
1
+ = Tired of wrestling with server provisioning? Sumo!
2
+
3
+ Want to fire up a one-off EC2 instance, pronto? ec2-run-instances got you down? Try Sumo.
4
+
5
+ $ sumo launch
6
+ ---> Launching instance... i-4f809c26 (1.5s)
7
+ ---> Acquiring hostname... ec2-67-202-17-178.compute-1.amazonaws.com (26.7s)
8
+
9
+ Logging you in via ssh. Type 'exit' or Ctrl-D to return to your local system.
10
+ ------------------------------------------------------------------------------
11
+ Linux domU-12-31-39-04-31-37 2.6.21.7-2.fc8xen #1 SMP Fri Feb 15 12:39:36 EST 2008 i686
12
+ ...
13
+ root@domU-12-31-39-04-31-37:~#
14
+
15
+ Later...
16
+
17
+ $ sumo terminate
18
+ ec2-67-202-17-178.compute-1.amazonaws.com scheduled for termination
19
+
20
+ You can manage multiple instances via "sumo list" and specifying hostname or instance id as arguments to the ssh or terminate commands.
21
+
22
+ == Service installation with Chef
23
+
24
+ The launch command takes an argument, which is a server role (from roles/#{role}.json inside your cookbooks repo):
25
+
26
+ $ sumo launch redis
27
+ ---> Launch instance... i-b96c73d0 (1.3s)
28
+ ---> Acquire hostname... ec2-75-101-191-220.compute-1.amazonaws.com (36.1s)
29
+ ---> Wait for ssh... done (9.0s)
30
+ ---> Bootstrap chef... done (61.3s)
31
+ ---> Setup redis... done (11.9s)
32
+ ---> Opening firewall... ports 6379 (5.2s)
33
+
34
+ Your instance is exporting the following resources:
35
+ Redis: redis://:8452cdd98f428c972f08@ec2-75-101-191-220.compute-1.amazonaws.com:6379/0
36
+
37
+ The instance can assume multiple roles if you like:
38
+
39
+ $ sumo launch redis,solr,couchdb
40
+
41
+ == Setup
42
+
43
+ Dependencies:
44
+
45
+ $ sudo gem install amazon-ec2 thor
46
+
47
+ Then create ~/.sumo/config.yml containing:
48
+
49
+ ---
50
+ access_id: <your amazon access key id>
51
+ access_secret: <your amazon secret access key>
52
+
53
+ Optional config you can include any of the following in your config.yml:
54
+
55
+ user: root
56
+ ami: ami-ed46a784
57
+ availability_zone: us-east-1b
58
+ cookbooks_url: git://github.com/adamwiggins/chef-cookbooks.git
59
+
60
+ You'll need Bacon and Mocha if you want to run the specs, and Jewler if you want to create gems.
61
+
62
+ == Managing volumes
63
+
64
+ Create and attach a volume to your running instance:
65
+
66
+ $ sumo create_volume
67
+ ---> Create 5MB volume... vol-8a9c6ae3 (1.1s)
68
+ $ sumo volumes
69
+ vol-8a9c6ae3 5MB available
70
+ $ sumo attach
71
+ ---> Attach vol-8a9c6ae3 to i-bc32cbd4 as /dev/sdc1... done (0.6s)
72
+
73
+ Log in to format and mount the volume:
74
+
75
+ $ sumo ssh
76
+ root@ip-10-251-122-175:~# mkfs.ext3 /dev/sdc1
77
+ mke2fs 1.41.4 (27-Jan-2009)
78
+ Filesystem label=
79
+ OS type: Linux
80
+ Block size=4096 (log=2)
81
+ ...
82
+ $ mkdir /myvol
83
+ $ mount /dev/sdc1 /myvol
84
+ $ echo "I'm going to persist to a volume" > /myvol/hello.txt
85
+
86
+ To detach from a running instance (perhaps so you can attach elsewhere):
87
+
88
+ $ sumo detatch
89
+ ---> Detach vol-8a9c6ae3... done (0.6s)
90
+
91
+ Destroy it if you no longer want the data stored on it:
92
+
93
+ $ sumo destroy_volume
94
+ ---> Destroy volume... done (0.8s)
95
+
96
+ == Some details you might want to know
97
+
98
+ Sumo creates its own keypair named sumo, which is stored in ~/.ssh/keypair.pem. Amazon doesn't let you upload your own ssh public key, which is lame, so this is the best option for making the launch-and-connect process a single step.
99
+
100
+ It will also create an Amazon security group called sumo, so that it can lower the firewall for services you configure via cookbook roles.
101
+
102
+ If you run any production machines from your EC2 account, I recommend setting up a separate account for use with Sumo. It does not prompt for confirmation when terminating an instance or differentiate between instances started by it vs. instances started by other tools.
103
+
104
+ == Anti-features
105
+
106
+ Sumo is not a cloud management tool, a monitor tool, or anything more than a way to get an instance up right quick. If you're looking for a way to manage a cluster of production instances, try one of these fine tools.
107
+
108
+ * Pool Party
109
+ * RightScale
110
+ * Engine Yard Cloud
111
+ * Cloudkick
112
+
113
+ == Meta
114
+
115
+ Created by Adam Wiggins
116
+
117
+ Patches contributed by Orion Henry, Blake Mizerany, Jesse Newland, Gert Goet,
118
+ and Tim Lossen
119
+
120
+ Released under the MIT License: http://www.opensource.org/licenses/mit-license.php
121
+
122
+ http://github.com/adamwiggins/sumo
123
+
@@ -0,0 +1,25 @@
1
+ require 'jeweler'
2
+
3
+ Jeweler::Tasks.new do |s|
4
+ s.name = "some"
5
+ s.description = "sumo clone for NIFTY Cloud"
6
+ s.summary = s.description
7
+ s.author = "tily"
8
+ s.email = "tidnlyam@gmail.com"
9
+ s.homepage = "http://github.com/tily/some"
10
+ s.rubyforge_project = "some"
11
+ s.files = FileList["[A-Z]*", "{bin,lib,spec}/**/*"]
12
+ s.executables = %w(some)
13
+ s.add_dependency "nifty-cloud-sdk"
14
+ s.add_dependency "thor"
15
+ end
16
+
17
+ Jeweler::RubyforgeTasks.new
18
+
19
+ desc 'Run specs'
20
+ task :spec do
21
+ sh 'bacon -s spec/*_spec.rb'
22
+ end
23
+
24
+ task :default => :spec
25
+
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.0.0
@@ -0,0 +1,174 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'rubygems'
4
+ require File.dirname(__FILE__) + '/../lib/some'
5
+
6
+ require 'thor'
7
+
8
+ class CLI < Thor
9
+ desc "launch [<role>]", "launch an instance as role, or omit to ssh to vanilla instance"
10
+ def launch(role=nil)
11
+ id = task("Launch instance") { some.launch }
12
+ host = task("Acquire hostname") { some.wait_for_hostname(id) }
13
+ task("Wait for ssh") { some.wait_for_ssh(host) }
14
+
15
+ if role
16
+ task("Bootstrap chef") { some.bootstrap_chef(host) }
17
+ role.split(',').each do |role|
18
+ task("Setup #{role}") { some.setup_role(host, role) }
19
+ end
20
+
21
+ resources = some.resources(host)
22
+ unless resources.empty?
23
+ task("Open firewall") do
24
+ ports = resources.map { |r| r.match(/:(\d+)\//)[1] }
25
+ ports.each { |port| some.open_firewall(port) }
26
+ "ports " + ports.join(", ")
27
+ end
28
+ end
29
+
30
+ puts
31
+ display_resources(host)
32
+ else
33
+ puts "\nLogging you in via ssh. Type 'exit' or Ctrl-D to return to your local system."
34
+ puts '-' * 78
35
+ connect_ssh(host)
36
+ end
37
+ end
38
+
39
+ desc "ssh [<instance_id or hostname>]", "ssh to a specified instance or first available"
40
+ def ssh(id=nil)
41
+ inst = some.find(id) || some.running.first || abort("No running instances")
42
+ hostname = inst[:hostname] || wait_for_hostname(inst[:instance_id])
43
+ connect_ssh hostname
44
+ end
45
+
46
+ desc "resources [<instance_id or hostname>]", "show resources exported by an instance"
47
+ def resources(id=nil)
48
+ inst = some.find(id) || some.running.first || abort("No running instances")
49
+ hostname = inst[:hostname] || wait_for_hostname(inst[:instance_id])
50
+ display_resources(inst[:hostname])
51
+ end
52
+
53
+ desc "bootstrap", "bootstrap chef and cookbooks"
54
+ def bootstrap(id=nil)
55
+ inst = some.find(id) || some.running.first || abort("No running instances")
56
+ task "Bootstrap chef" do
57
+ some.bootstrap_chef(inst[:hostname])
58
+ end
59
+ end
60
+
61
+ desc "role", "setup instance as a role"
62
+ def role(role, id=nil)
63
+ inst = some.find(id) || some.running.first || abort("No running instances")
64
+ task "Setup #{role}" do
65
+ some.setup_role(inst[:hostname], role)
66
+ end
67
+ end
68
+
69
+ desc "list", "list running instances"
70
+ def list
71
+ some.list.each do |inst|
72
+ printf "%-50s %-12s %s\n", inst[:hostname], inst[:instance_id], inst[:status]
73
+ end
74
+ end
75
+
76
+ desc "console [<instance_id or hostname>]", "get console output for instance or first available"
77
+ def console(id=nil)
78
+ inst = some.find(id) || (some.running | some.pending).first || abort("No running or pending instances")
79
+
80
+ puts some.console_output(inst[:instance_id]).inspect
81
+ end
82
+
83
+ desc "terminate [<instance_id or hostname>]", "terminate specified instance or first available"
84
+ def terminate(id=nil)
85
+ inst = some.find(id) || (some.running | some.pending).first || abort("No running or pending instances")
86
+
87
+ some.terminate(inst[:instance_id])
88
+ puts "#{inst[:hostname] || inst[:instance_id]} scheduled for termination"
89
+ end
90
+
91
+ desc "terminate_all", "terminate all instances"
92
+ def terminate_all
93
+ instances = (some.running | some.pending)
94
+ abort("No running or pending instances") if instances.empty?
95
+ instances.each do |inst|
96
+ some.terminate(inst[:instance_id])
97
+ puts "#{inst[:hostname] || inst[:instance_id]} scheduled for termination"
98
+ end
99
+ end
100
+
101
+ desc "volumes", "list all volumes"
102
+ def volumes
103
+ some.volumes.each do |v|
104
+ printf "%-10s %4sGB %10s %15s %15s\n", v[:volume_id], v[:size], v[:status], v[:instance], v[:device]
105
+ end
106
+ end
107
+
108
+ desc "create_volume [<megabytes>]", "create a volume"
109
+ def create_volume(size=5)
110
+ task("Create #{size}GB volume") { some.create_volume(size) }
111
+ end
112
+
113
+ desc "destroy_volume [<volume_id>]", "destroy a volume"
114
+ def destroy_volume(volume=nil)
115
+ vol_id = (some.find_volume(volume) || some.nondestroyed_volumes.first || abort("No volumes"))[:volume_id]
116
+ task("Destroy volume") { some.destroy_volume(vol_id) }
117
+ end
118
+
119
+ desc "attach [<volume_id>] [<instance_id or hostname>] [<device>]", "attach volume to running instance"
120
+ def attach(volume=nil, inst_id=nil, device=nil)
121
+ vol_id = (some.find_volume(volume) || some.available_volumes.first || abort("No available volumes"))[:volume_id]
122
+ inst_id = (some.find(inst_id) || some.running.first || abort("No running instances"))[:instance_id]
123
+ device ||= '/dev/sdc1'
124
+ task("Attach #{vol_id} to #{inst_id} as #{device}") do
125
+ some.attach(vol_id, inst_id, device)
126
+ end
127
+ end
128
+
129
+ desc "detach [<volume_id>]", "detach volume from instance"
130
+ def detach(volume=nil)
131
+ vol_id = (some.find_volume(volume) || some.attached_volumes.first || abort("No attached volumes"))[:volume_id]
132
+ task("Detach #{vol_id}") { some.detach(vol_id) }
133
+ end
134
+
135
+ no_tasks do
136
+ def some
137
+ @some ||= Some.new
138
+ end
139
+
140
+ def config
141
+ some.config
142
+ end
143
+
144
+ def task(msg, &block)
145
+ printf "---> %-24s ", "#{msg}..."
146
+ start = Time.now
147
+ result = block.call || 'done'
148
+ finish = Time.now
149
+ time = sprintf("%0.1f", finish - start)
150
+ puts "#{result} (#{time}s)"
151
+ result
152
+ end
153
+
154
+ def connect_ssh(hostname)
155
+ some.wait_for_ssh(hostname)
156
+ system "ssh -i #{some.keypair_file} #{config['user']}@#{hostname}"
157
+ if $?.success?
158
+ puts "\nType 'some terminate' if you're done with this instance."
159
+ end
160
+ end
161
+
162
+ def display_resources(host)
163
+ resources = some.resources(host)
164
+ unless resources.empty?
165
+ puts "Your instance is exporting the following resources:"
166
+ resources.each do |resource|
167
+ puts " #{resource}"
168
+ end
169
+ end
170
+ end
171
+ end
172
+ end
173
+
174
+ CLI.start
@@ -0,0 +1,297 @@
1
+ require 'NIFTY'
2
+ require 'yaml'
3
+ require 'socket'
4
+
5
+ class Some
6
+ def launch
7
+ ami = config['ami']
8
+ raise "No AMI selected" unless ami
9
+
10
+ create_keypair unless File.exists? keypair_file
11
+
12
+ create_security_group
13
+ open_firewall(22)
14
+
15
+ result = api.run_instances(
16
+ :image_id => ami,
17
+ :instance_type => config['instance_size'] || 'mini',
18
+ :key_name => 'something',
19
+ :security_group => 'something',
20
+ :availability_zone => config['availability_zone'],
21
+ :disable_api_termination => false
22
+ )
23
+ result.instancesSet.item[0].instanceId
24
+ end
25
+
26
+ def list
27
+ @list ||= fetch_list
28
+ end
29
+
30
+ def volumes
31
+ result = api.describe_volumes
32
+ return [] unless result.volumeSet
33
+
34
+ result.volumeSet.item.map do |row|
35
+ {
36
+ :volume_id => row["volumeId"],
37
+ :size => row["size"],
38
+ :status => row["status"],
39
+ :device => (row["attachmentSet"]["item"].first["device"] rescue ""),
40
+ :instance_id => (row["attachmentSet"]["item"].first["instanceId"] rescue ""),
41
+ }
42
+ end
43
+ end
44
+
45
+ def available_volumes
46
+ volumes.select { |vol| vol[:status] == 'available' }
47
+ end
48
+
49
+ def attached_volumes
50
+ volumes.select { |vol| vol[:status] == 'in-use' }
51
+ end
52
+
53
+ def nondestroyed_volumes
54
+ volumes.select { |vol| vol[:status] != 'deleting' }
55
+ end
56
+
57
+ def attach(volume, instance, device)
58
+ result = api.attach_volume(
59
+ :volume_id => volume,
60
+ :instance_id => instance,
61
+ :device => device
62
+ )
63
+ "done"
64
+ end
65
+
66
+ def detach(volume)
67
+ result = api.detach_volume(:volume_id => volume, :force => "true")
68
+ "done"
69
+ end
70
+
71
+ def create_volume(size)
72
+ result = api.create_volume(
73
+ :availability_zone => config['availability_zone'],
74
+ :size => size.to_s
75
+ )
76
+ result["volumeId"]
77
+ end
78
+
79
+ def destroy_volume(volume)
80
+ api.delete_volume(:volume_id => volume)
81
+ "done"
82
+ end
83
+
84
+ def fetch_list
85
+ result = api.describe_instances
86
+ return [] unless result.reservationSet
87
+
88
+ instances = []
89
+ result.reservationSet.item.each do |r|
90
+ r.instancesSet.item.each do |item|
91
+ instances << {
92
+ :instance_id => item.instanceId,
93
+ :status => item.instanceState.name,
94
+ :hostname => item.dnsName
95
+ }
96
+ end
97
+ end
98
+ instances
99
+ end
100
+
101
+ def find(id_or_hostname)
102
+ return unless id_or_hostname
103
+ id_or_hostname = id_or_hostname.strip.downcase
104
+ list.detect do |inst|
105
+ inst[:hostname] == id_or_hostname or
106
+ inst[:instance_id] == id_or_hostname
107
+ end
108
+ end
109
+
110
+ def find_volume(volume_id)
111
+ return unless volume_id
112
+ volume_id = volume_id.strip.downcase
113
+ volumes.detect do |volume|
114
+ volume[:volume_id] == volume_id or
115
+ volume[:volume_id].gsub(/^vol-/, '') == volume_id
116
+ end
117
+ end
118
+
119
+ def running
120
+ list_by_status('running')
121
+ end
122
+
123
+ def pending
124
+ list_by_status('pending')
125
+ end
126
+
127
+ def list_by_status(status)
128
+ list.select { |i| i[:status] == status }
129
+ end
130
+
131
+ def instance_info(instance_id)
132
+ fetch_list.detect do |inst|
133
+ inst[:instance_id] == instance_id
134
+ end
135
+ end
136
+
137
+ def wait_for_hostname(instance_id)
138
+ raise ArgumentError unless instance_id
139
+ loop do
140
+ if inst = instance_info(instance_id)
141
+ if hostname = inst[:hostname]
142
+ return hostname
143
+ end
144
+ end
145
+ sleep 1
146
+ end
147
+ end
148
+
149
+ def wait_for_ssh(hostname)
150
+ raise ArgumentError unless hostname
151
+ loop do
152
+ begin
153
+ Timeout::timeout(4) do
154
+ TCPSocket.new(hostname, 22)
155
+ return
156
+ end
157
+ rescue SocketError, Timeout::Error, Errno::ECONNREFUSED, Errno::EHOSTUNREACH
158
+ end
159
+ end
160
+ end
161
+
162
+ def bootstrap_chef(hostname)
163
+ commands = [
164
+ "curl -L https://www.opscode.com/chef/install.sh | bash",
165
+ "mkdir -p /var/chef/cookbooks /etc/chef",
166
+ "echo json_attribs \\'/etc/chef/dna.json\\' > /etc/chef/solo.rb"
167
+ ]
168
+ ssh(hostname, commands)
169
+ end
170
+
171
+ def setup_role(hostname, role)
172
+ commands = [
173
+ "echo \'#{config['role'][role]}\' > /etc/chef/dna.json",
174
+ "chef-solo -r #{config['cookbooks_url']}"
175
+ ]
176
+ ssh(hostname, commands)
177
+ end
178
+
179
+ def ssh(hostname, cmds)
180
+ IO.popen("ssh -i #{keypair_file} #{config['user']}@#{hostname} > ~/.some/ssh.log 2>&1", "w") do |pipe|
181
+ pipe.puts cmds.join(' && ')
182
+ end
183
+ unless $?.success?
184
+ abort "failed\nCheck ~/.some/ssh.log for the output"
185
+ end
186
+ end
187
+
188
+ def resources(hostname)
189
+ @resources ||= {}
190
+ @resources[hostname] ||= fetch_resources(hostname)
191
+ end
192
+
193
+ def fetch_resources(hostname)
194
+ cmd = "ssh -i #{keypair_file} #{config['user']}@#{hostname} 'cat /root/resources' 2>&1"
195
+ out = IO.popen(cmd, 'r') { |pipe| pipe.read }
196
+ abort "failed to read resources, output:\n#{out}" unless $?.success?
197
+ parse_resources(out, hostname)
198
+ end
199
+
200
+ def parse_resources(raw, hostname)
201
+ raw.split("\n").map do |line|
202
+ line.gsub(/localhost/, hostname)
203
+ end
204
+ end
205
+
206
+ def terminate(instance_id)
207
+ inst = instance_info(instance_id)
208
+ if inst[:status] != 'stopped'
209
+ api.stop_instances(:instance_id => [ instance_id ])
210
+ puts "waiting for #{instance_id} to stop "
211
+ loop do
212
+ print '.'
213
+ if inst = instance_info(instance_id)
214
+ if inst[:status] == 'stopped'
215
+ break
216
+ end
217
+ end
218
+ sleep 5
219
+ end
220
+ end
221
+ api.terminate_instances(:instance_id => [ instance_id ])
222
+ end
223
+
224
+ def console_output(instance_id)
225
+ api.get_console_output(:instance_id => instance_id)["output"]
226
+ end
227
+
228
+ def config
229
+ @config ||= default_config.merge read_config
230
+ end
231
+
232
+ def default_config
233
+ {
234
+ 'user' => 'root',
235
+ 'ami' => 'ami-ed46a784',
236
+ 'availability_zone' => 'east-12',
237
+ 'password' => 'password'
238
+ }
239
+ end
240
+
241
+ def some_dir
242
+ "#{ENV['HOME']}/.some"
243
+ end
244
+
245
+ def read_config
246
+ YAML.load File.read("#{some_dir}/config.yml")
247
+ rescue Errno::ENOENT
248
+ raise "Some is not configured, please fill in ~/.some/config.yml"
249
+ end
250
+
251
+ def keypair_file
252
+ "#{some_dir}/keypair.pem"
253
+ end
254
+
255
+ def create_keypair
256
+ keypair = api.create_key_pair(:key_name => "something", :password => config['password']).keyMaterial
257
+ File.open(keypair_file, 'w') { |f| f.write Base64.decode64(keypair) }
258
+ File.chmod 0600, keypair_file
259
+ end
260
+
261
+ def create_security_group
262
+ api.create_security_group(:group_name => 'something', :group_description => 'Something')
263
+ rescue NIFTY::ResponseError => e
264
+ if e.message != "The groupName 'something' already exists."
265
+ raise e
266
+ end
267
+ end
268
+
269
+ def open_firewall(port)
270
+ api.authorize_security_group_ingress(
271
+ :group_name => 'something',
272
+ :ip_permissions => {
273
+ :ip_protocol => 'tcp',
274
+ :from_port => port,
275
+ :to_port => port,
276
+ :cidr_ip => '0.0.0.0/0'
277
+ }
278
+ )
279
+ rescue NIFTY::ResponseError => e
280
+ raise e
281
+ end
282
+
283
+ def api
284
+ @api ||= NIFTY::Cloud::Base.new(
285
+ :access_key => config['access_key'],
286
+ :secret_key => config['secret_key'],
287
+ :server => server,
288
+ :path => '/api'
289
+ )
290
+ end
291
+
292
+ def server
293
+ zone = config['availability_zone']
294
+ host = zone.slice(0, zone.length - 1)
295
+ "#{host}.cp.cloud.nifty.com"
296
+ end
297
+ end
@@ -0,0 +1,21 @@
1
+ require File.dirname(__FILE__) + '/../lib/some'
2
+
3
+ require 'bacon'
4
+ require 'mocha/standalone'
5
+ require 'mocha/object'
6
+
7
+ class Bacon::Context
8
+ include Mocha::API
9
+
10
+ def initialize(name, &block)
11
+ @name = name
12
+ @before, @after = [
13
+ [lambda { mocha_setup }],
14
+ [lambda { mocha_verify ; mocha_teardown }]
15
+ ]
16
+ @block = block
17
+ end
18
+
19
+ def xit(desc, &bk)
20
+ end
21
+ end
@@ -0,0 +1,31 @@
1
+ require File.dirname(__FILE__) + '/base'
2
+
3
+ require 'fileutils'
4
+
5
+ describe Some do
6
+ before do
7
+ @work_path = "/tmp/spec_#{Process.pid}/"
8
+ FileUtils.mkdir_p(@work_path)
9
+ File.open("#{@work_path}/config.yml", "w") do |f|
10
+ f.write YAML.dump({})
11
+ end
12
+
13
+ @some = Some.new
14
+ @some.stubs(:some_dir).returns(@work_path)
15
+ end
16
+
17
+ after do
18
+ FileUtils.rm_rf(@work_path)
19
+ end
20
+
21
+ it "defaults to user root if none is specified in the config" do
22
+ @some.config['user'].should == 'root'
23
+ end
24
+
25
+ it "uses specified user if one is in the config" do
26
+ File.open("#{@work_path}/config.yml", "w") do |f|
27
+ f.write YAML.dump('user' => 'joe')
28
+ end
29
+ @some.config['user'].should == 'joe'
30
+ end
31
+ end
metadata ADDED
@@ -0,0 +1,75 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: some
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.0
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - tily
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2013-04-04 00:00:00.000000000Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: nifty-cloud-sdk
16
+ requirement: &110762780 !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: '0'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: *110762780
25
+ - !ruby/object:Gem::Dependency
26
+ name: thor
27
+ requirement: &110762200 !ruby/object:Gem::Requirement
28
+ none: false
29
+ requirements:
30
+ - - ! '>='
31
+ - !ruby/object:Gem::Version
32
+ version: '0'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: *110762200
36
+ description: sumo clone for NIFTY Cloud
37
+ email: tidnlyam@gmail.com
38
+ executables:
39
+ - some
40
+ extensions: []
41
+ extra_rdoc_files:
42
+ - README.rdoc
43
+ files:
44
+ - README.rdoc
45
+ - Rakefile
46
+ - VERSION
47
+ - bin/some
48
+ - lib/some.rb
49
+ - spec/base.rb
50
+ - spec/some_spec.rb
51
+ homepage: http://github.com/tily/some
52
+ licenses: []
53
+ post_install_message:
54
+ rdoc_options: []
55
+ require_paths:
56
+ - lib
57
+ required_ruby_version: !ruby/object:Gem::Requirement
58
+ none: false
59
+ requirements:
60
+ - - ! '>='
61
+ - !ruby/object:Gem::Version
62
+ version: '0'
63
+ required_rubygems_version: !ruby/object:Gem::Requirement
64
+ none: false
65
+ requirements:
66
+ - - ! '>='
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ requirements: []
70
+ rubyforge_project: some
71
+ rubygems_version: 1.8.13
72
+ signing_key:
73
+ specification_version: 3
74
+ summary: sumo clone for NIFTY Cloud
75
+ test_files: []