ubalo 0.0.1 → 0.0.2
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/bin/ubalo +332 -0
- data/lib/ubalo.rb +210 -0
- metadata +47 -11
data/bin/ubalo
ADDED
@@ -0,0 +1,332 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'gli'
|
4
|
+
require 'yaml'
|
5
|
+
|
6
|
+
require 'highline'
|
7
|
+
require 'highline/import'
|
8
|
+
hl = HighLine.new
|
9
|
+
|
10
|
+
require 'ubalo'
|
11
|
+
include GLI
|
12
|
+
|
13
|
+
use_openstruct true
|
14
|
+
|
15
|
+
class Ubalo
|
16
|
+
class << self
|
17
|
+
def config
|
18
|
+
if @config
|
19
|
+
@config
|
20
|
+
elsif File.exists?(config_path)
|
21
|
+
@config = YAML.load_file(config_path)
|
22
|
+
else
|
23
|
+
{}
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def write_config new_config
|
28
|
+
h = config.merge(new_config)
|
29
|
+
File.open(config_path, 'w') do |f|
|
30
|
+
YAML.dump(h, f)
|
31
|
+
end
|
32
|
+
config
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
def config_path
|
37
|
+
File.expand_path("~/.ubalorc")
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def ubalo
|
43
|
+
@ubalo
|
44
|
+
end
|
45
|
+
|
46
|
+
program_desc 'Command-line access to ubalo.com.'
|
47
|
+
version "0.0.1"
|
48
|
+
|
49
|
+
desc "Change the connect url"
|
50
|
+
flag 'connect-url'
|
51
|
+
|
52
|
+
desc 'Display authentication information'
|
53
|
+
command :whoami do |c|
|
54
|
+
c.action do |global_options,options,args|
|
55
|
+
puts ubalo.whoami
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
desc 'Display pods'
|
60
|
+
command :pods do |c|
|
61
|
+
c.desc 'show all pods you can snap (default)'
|
62
|
+
c.switch :all
|
63
|
+
|
64
|
+
c.desc 'show only your pods'
|
65
|
+
c.switch :mine
|
66
|
+
|
67
|
+
c.desc 'show only the pods you can edit and snap'
|
68
|
+
c.switch :editable
|
69
|
+
|
70
|
+
c.action do |global_options,options,args|
|
71
|
+
unless options.all or options.edit_only
|
72
|
+
# Default to showing just your pods.
|
73
|
+
options.mine = true
|
74
|
+
end
|
75
|
+
|
76
|
+
your_pods, editable_pods, snappable_pods = ubalo.pods
|
77
|
+
|
78
|
+
# Always show your pods.
|
79
|
+
your_pods.each do |pod|
|
80
|
+
puts Ubalo.format_pod(pod)
|
81
|
+
end
|
82
|
+
|
83
|
+
if options.all or options.edit_only
|
84
|
+
editable_pods.each do |pod|
|
85
|
+
puts Ubalo.format_pod(pod)
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
if options.all
|
90
|
+
snappable_pods.each do |pod|
|
91
|
+
puts Ubalo.format_pod(pod)
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
desc 'Display tasks'
|
98
|
+
command :tasks do |c|
|
99
|
+
c.desc 'filter by task state (pending, complete, etc)'
|
100
|
+
c.long_desc 'possibilities are waiting,running,pending,complete,failed,all'
|
101
|
+
c.default_value 'all'
|
102
|
+
c.flag 'state'
|
103
|
+
|
104
|
+
c.desc 'maximum number of tasks to display'
|
105
|
+
c.default_value 20
|
106
|
+
c.flag 'max'
|
107
|
+
|
108
|
+
c.action do |global_options,options,args|
|
109
|
+
unless options.pending or options.failed
|
110
|
+
# Default to showing just your pods.
|
111
|
+
options.all = true
|
112
|
+
end
|
113
|
+
|
114
|
+
ubalo.tasks(options.state, options.max).each do |task|
|
115
|
+
puts Ubalo.format_task(task)
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
desc 'Add an ssh key (defaults to ~/.ssh/id_rsa.pub)'
|
121
|
+
arg_name '<file containing ssh key>'
|
122
|
+
command :add_key do |c|
|
123
|
+
c.action do |global_options,options,args|
|
124
|
+
|
125
|
+
public_key_file, = *args
|
126
|
+
|
127
|
+
public_key_file ||= '~/.ssh/id_rsa.pub'
|
128
|
+
public_key_file = File.expand_path(public_key_file)
|
129
|
+
|
130
|
+
puts ubalo.upload_key(File.read(public_key_file))
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
desc 'Clear all ssh keys'
|
135
|
+
command :clear_keys do |c|
|
136
|
+
c.action do |global_options,options,args|
|
137
|
+
puts ubalo.clear_keys
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
desc 'Modify a pod'
|
142
|
+
arg_name '<pod name>'
|
143
|
+
command :edit do |c|
|
144
|
+
c.action do |global_options,options,args|
|
145
|
+
pod_name = args.shift
|
146
|
+
unless pod_name
|
147
|
+
raise "please specify a pod"
|
148
|
+
end
|
149
|
+
|
150
|
+
ubalo.ssh(pod_name)
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
desc 'Show the code in a pod'
|
155
|
+
arg_name '<pod name>'
|
156
|
+
command :cat do |c|
|
157
|
+
c.action do |global_options,options,args|
|
158
|
+
pod_name = args.shift
|
159
|
+
unless pod_name
|
160
|
+
raise "please specify a pod"
|
161
|
+
end
|
162
|
+
|
163
|
+
puts ubalo.cat_code(pod_name)
|
164
|
+
end
|
165
|
+
end
|
166
|
+
|
167
|
+
desc 'Set the code in a pod'
|
168
|
+
arg_name '<pod_name> <code_file>'
|
169
|
+
command :code do |c|
|
170
|
+
c.action do |global_options,options,args|
|
171
|
+
pod_name = args.shift
|
172
|
+
unless pod_name
|
173
|
+
raise "please specify a pod"
|
174
|
+
end
|
175
|
+
|
176
|
+
file_name = args.shift
|
177
|
+
if file_name
|
178
|
+
code = File.read(file_name)
|
179
|
+
elsif $stdin.tty?
|
180
|
+
raise "please give the new code as a filename or on stdin"
|
181
|
+
else
|
182
|
+
code = $stdin.read
|
183
|
+
end
|
184
|
+
|
185
|
+
puts ubalo.upload_code(pod_name, code)
|
186
|
+
end
|
187
|
+
end
|
188
|
+
|
189
|
+
desc 'Show the files in a pod environ'
|
190
|
+
arg_name '<pod name>'
|
191
|
+
command :ls do |c|
|
192
|
+
c.desc 'list in long format'
|
193
|
+
c.switch :l
|
194
|
+
|
195
|
+
c.desc 'list in human-readable form'
|
196
|
+
c.switch :h
|
197
|
+
|
198
|
+
c.action do |global_options,options,args|
|
199
|
+
opts = ""
|
200
|
+
opts << " -l" if options.l
|
201
|
+
opts << " -h" if options.h
|
202
|
+
|
203
|
+
pod_name = args.shift
|
204
|
+
unless pod_name
|
205
|
+
raise "please specify a pod"
|
206
|
+
end
|
207
|
+
puts ubalo.ls(pod_name, opts)
|
208
|
+
end
|
209
|
+
end
|
210
|
+
|
211
|
+
desc 'Copy files into and out of a pod environ'
|
212
|
+
arg_name '<pod name>'
|
213
|
+
command :cp do |c|
|
214
|
+
c.desc 'copy recursively'
|
215
|
+
c.switch :r
|
216
|
+
|
217
|
+
c.action do |global_options,options,args|
|
218
|
+
opts = ""
|
219
|
+
opts << " -r" if options.r
|
220
|
+
|
221
|
+
scp_args = args.join(" ").gsub(/[\w-]+(?=:)/) do |pod_name|
|
222
|
+
ssh_path, environ = ubalo.ssh_path(pod_name)
|
223
|
+
"#{ssh_path}:#{environ}"
|
224
|
+
end
|
225
|
+
|
226
|
+
ubalo.scp(opts, scp_args)
|
227
|
+
end
|
228
|
+
end
|
229
|
+
|
230
|
+
desc 'Snap a pod'
|
231
|
+
arg_name '<pod name>'
|
232
|
+
command :snap do |c|
|
233
|
+
c.desc 'detach, leaving task running in the background; return an id'
|
234
|
+
c.switch [:detach, :d]
|
235
|
+
|
236
|
+
c.desc 'explicitly show the complete result'
|
237
|
+
c.switch [:verbose, :v]
|
238
|
+
|
239
|
+
c.desc 'outputs as json to stdout; stdout > stderr'
|
240
|
+
c.switch [:json, :j]
|
241
|
+
|
242
|
+
c.action do |global_options,options,args|
|
243
|
+
pod_name = args.shift
|
244
|
+
unless pod_name
|
245
|
+
raise "please specify a pod"
|
246
|
+
end
|
247
|
+
|
248
|
+
h = ubalo.submit_task(pod_name, args.join(" "))
|
249
|
+
task_id = h['id']
|
250
|
+
|
251
|
+
if options.detach
|
252
|
+
puts task_id
|
253
|
+
else
|
254
|
+
result = ubalo.check_task(task_id, true)
|
255
|
+
ubalo.show_result(result, options.verbose, options.json)
|
256
|
+
end
|
257
|
+
end
|
258
|
+
end
|
259
|
+
|
260
|
+
desc 'Check or get the result of a task'
|
261
|
+
arg_name '<task id>'
|
262
|
+
command :check do |c|
|
263
|
+
c.desc 'explicitly show the complete result'
|
264
|
+
c.switch [:verbose, :v]
|
265
|
+
|
266
|
+
c.desc 'wait for the result if the task is pending'
|
267
|
+
c.switch [:wait, :w]
|
268
|
+
|
269
|
+
c.desc 'outputs as json to stdout; stdout > stderr'
|
270
|
+
c.switch [:json, :j]
|
271
|
+
|
272
|
+
c.action do |global_options,options,args|
|
273
|
+
task_id = args.shift
|
274
|
+
unless task_id
|
275
|
+
raise "please specify a task"
|
276
|
+
end
|
277
|
+
|
278
|
+
result = ubalo.check_task(task_id, true)
|
279
|
+
ubalo.show_result(result, options.verbose, options.json)
|
280
|
+
end
|
281
|
+
end
|
282
|
+
|
283
|
+
desc 'Retrieve an API token'
|
284
|
+
command :authorize do |c|
|
285
|
+
c.action do |global_options,options,args|
|
286
|
+
puts "Please enter your Ubalo username/email and password to unlock command-line access."
|
287
|
+
login = hl.ask(" login: "){|q| q.echo = true}
|
288
|
+
password = hl.ask(" password: "){|q| q.echo = false}
|
289
|
+
|
290
|
+
ubalo.token = ubalo.get_api_token(login, password)
|
291
|
+
Ubalo.write_config('token' => ubalo.token)
|
292
|
+
puts "Access details saved to ~/.ubalorc."
|
293
|
+
puts ubalo.whoami
|
294
|
+
end
|
295
|
+
end
|
296
|
+
|
297
|
+
desc 'Clear any stored API tokens'
|
298
|
+
command :deauthorize do |c|
|
299
|
+
c.action do |global_options,options,args|
|
300
|
+
Ubalo.write_config('token' => nil)
|
301
|
+
puts "Access details deleted."
|
302
|
+
end
|
303
|
+
end
|
304
|
+
|
305
|
+
pre do |global,command,options,args|
|
306
|
+
config = Ubalo.config
|
307
|
+
token = config['token']
|
308
|
+
|
309
|
+
unless token or (command && command.name == :authorize)
|
310
|
+
raise "No credentials found. Please run 'ubalo authorize'."
|
311
|
+
end
|
312
|
+
|
313
|
+
connect_url = global['connect-url'] || config['connect-url']
|
314
|
+
@ubalo ||= Ubalo.authorize(token, connect_url)
|
315
|
+
|
316
|
+
true
|
317
|
+
end
|
318
|
+
|
319
|
+
on_error do |exception|
|
320
|
+
case exception
|
321
|
+
when RestClient::BadRequest
|
322
|
+
$stderr.puts exception.inspect.sub('400 Bad Request', 'Error')
|
323
|
+
false
|
324
|
+
when UbaloExit
|
325
|
+
# Normal exit, preserving the correct exit code.
|
326
|
+
exit exception.message
|
327
|
+
else
|
328
|
+
true
|
329
|
+
end
|
330
|
+
end
|
331
|
+
|
332
|
+
exit GLI.run(ARGV)
|
data/lib/ubalo.rb
ADDED
@@ -0,0 +1,210 @@
|
|
1
|
+
require 'rest-client'
|
2
|
+
require 'json'
|
3
|
+
|
4
|
+
class String
|
5
|
+
def lfit max_length
|
6
|
+
if length - 3 <= max_length
|
7
|
+
ljust(max_length)
|
8
|
+
else
|
9
|
+
"#{self[0,max_length-3]}..."
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
def rfit max_length
|
14
|
+
if length - 3 <= max_length
|
15
|
+
rjust(max_length)
|
16
|
+
else
|
17
|
+
"#{self[0,max_length-3]}..."
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def indent amount=2
|
22
|
+
each_line.map do |l|
|
23
|
+
" "*amount + l
|
24
|
+
end.join
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
class NilClass
|
29
|
+
def lfit max_length
|
30
|
+
"".lfit(max_length)
|
31
|
+
end
|
32
|
+
|
33
|
+
def rfit max_length
|
34
|
+
"".rfit(max_length)
|
35
|
+
end
|
36
|
+
|
37
|
+
def indent amount
|
38
|
+
"".indent(amount)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
class UbaloExit < StandardError
|
43
|
+
|
44
|
+
end
|
45
|
+
|
46
|
+
class Ubalo
|
47
|
+
class << self
|
48
|
+
def format_task task
|
49
|
+
"#{task['id'].to_s[0,6]} #{task['state'].lfit(8)} #{task['pod_name'].lfit(25)} #{task['arg'].to_s.lfit(40)}"
|
50
|
+
end
|
51
|
+
|
52
|
+
def format_pod pod
|
53
|
+
"#{pod['environ'].lfit(18)} #{pod['code_lines'].to_s.rjust 3} lines #{pod['user'].rfit(10)} / #{pod['name']}"
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
attr_reader :base_url
|
58
|
+
attr_accessor :token
|
59
|
+
|
60
|
+
def self.authorize token, base_url=nil
|
61
|
+
new(token, base_url)
|
62
|
+
end
|
63
|
+
|
64
|
+
def initialize token, base_url
|
65
|
+
@token = token
|
66
|
+
@base_url = base_url || "https://ubalo.com/api"
|
67
|
+
end
|
68
|
+
|
69
|
+
def get(action, params={})
|
70
|
+
url = "#{base_url}/#{action}"
|
71
|
+
if token
|
72
|
+
params.merge!({:auth_token => token})
|
73
|
+
end
|
74
|
+
response = RestClient.get url, :params => params
|
75
|
+
response
|
76
|
+
end
|
77
|
+
|
78
|
+
def post(action, params={})
|
79
|
+
url = "#{base_url}/#{action}"
|
80
|
+
RestClient.post url, {:auth_token => token}.merge(params)
|
81
|
+
end
|
82
|
+
|
83
|
+
def username
|
84
|
+
username, role = whoami
|
85
|
+
username
|
86
|
+
end
|
87
|
+
|
88
|
+
def whoami
|
89
|
+
get(:whoami)
|
90
|
+
end
|
91
|
+
|
92
|
+
def tasks state, count
|
93
|
+
parse(get(:tasks, :count => count, :state => state))
|
94
|
+
end
|
95
|
+
|
96
|
+
def submit_task(pod_name, arg)
|
97
|
+
parse(post(:submit_task, {:pod_name => pod_name, :arg => arg}))
|
98
|
+
end
|
99
|
+
|
100
|
+
def check_task(id, blocking=false)
|
101
|
+
if blocking
|
102
|
+
10.times do
|
103
|
+
h = check_task_once(id)
|
104
|
+
return(h) if %w{complete failed}.include?(h['state'])
|
105
|
+
sleep 0.5
|
106
|
+
end
|
107
|
+
raise "timed-out waiting for task"
|
108
|
+
else
|
109
|
+
check_task_once(id)
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
def upload_key(key)
|
114
|
+
post(:upload_key, :key => key)
|
115
|
+
end
|
116
|
+
|
117
|
+
def clear_keys
|
118
|
+
post(:clear_keys)
|
119
|
+
end
|
120
|
+
|
121
|
+
def cat_code(name)
|
122
|
+
get(:cat_code, :pod_name => name)
|
123
|
+
end
|
124
|
+
|
125
|
+
def upload_code(name, code)
|
126
|
+
post(:upload_code, {:pod_name => name, :code => code})
|
127
|
+
end
|
128
|
+
|
129
|
+
def ssh_path(name)
|
130
|
+
h = parse(get(:ssh_path, :pod_name => name))
|
131
|
+
[h['ssh_path'], h['environ'], h['environ_fullname']]
|
132
|
+
end
|
133
|
+
|
134
|
+
def pods
|
135
|
+
h = parse(get(:pods))
|
136
|
+
[h['your_pods'], h['editable_pods'], h['snappable_pods']]
|
137
|
+
end
|
138
|
+
|
139
|
+
def ssh(pod_name)
|
140
|
+
ssh_path, environ = ssh_path(pod_name)
|
141
|
+
puts "Opening an ssh connection to edit the '#{environ}' environment for the '#{pod_name}' pod."
|
142
|
+
ssh_command = "ssh -tq #{ssh_path} #{environ}"
|
143
|
+
Process.exec({'TERM' => 'xterm'}, ssh_command)
|
144
|
+
end
|
145
|
+
|
146
|
+
def scp(opts, args)
|
147
|
+
Process.exec("scp#{opts} #{args}")
|
148
|
+
end
|
149
|
+
|
150
|
+
def ls(pod_name, options)
|
151
|
+
ssh_path, environ = ssh_path(pod_name)
|
152
|
+
ssh_command = "ssh -tq #{ssh_path} #{environ}"
|
153
|
+
`#{ssh_command} ls#{options}`
|
154
|
+
end
|
155
|
+
|
156
|
+
def show_result(result, verbose=false, json=false)
|
157
|
+
if result['outputs']
|
158
|
+
outputs = JSON.dump(result['outputs'])
|
159
|
+
end
|
160
|
+
|
161
|
+
if json
|
162
|
+
puts outputs
|
163
|
+
else
|
164
|
+
if verbose
|
165
|
+
puts " id: #{result['id']}"
|
166
|
+
puts " name: #{result['pod_name']}"
|
167
|
+
puts " state: #{result['state']}"
|
168
|
+
puts "status: #{result['status']}"
|
169
|
+
if result['stdout']
|
170
|
+
puts "stdout:"
|
171
|
+
puts result['stdout'].indent
|
172
|
+
end
|
173
|
+
if result['stderr']
|
174
|
+
puts "stderr:"
|
175
|
+
puts result['stderr'].indent
|
176
|
+
end
|
177
|
+
else
|
178
|
+
if result['stdout']
|
179
|
+
print result['stdout']
|
180
|
+
end
|
181
|
+
if result['stderr']
|
182
|
+
$stderr.print result['stderr']
|
183
|
+
end
|
184
|
+
|
185
|
+
exit_code = Integer(result['status'] || 0)
|
186
|
+
unless exit_code.zero?
|
187
|
+
raise UbaloExit, exit_code
|
188
|
+
end
|
189
|
+
end
|
190
|
+
|
191
|
+
if outputs
|
192
|
+
$stderr.puts "outputs:"
|
193
|
+
$stderr.puts outputs.indent
|
194
|
+
end
|
195
|
+
end
|
196
|
+
end
|
197
|
+
|
198
|
+
def get_api_token login, password
|
199
|
+
parse(post(:get_api_token, :user => {:login => login, :password => password}))['api_token']
|
200
|
+
end
|
201
|
+
|
202
|
+
private
|
203
|
+
def check_task_once id
|
204
|
+
parse(get(:check_task, :id => id))
|
205
|
+
end
|
206
|
+
|
207
|
+
def parse(result)
|
208
|
+
JSON.load(result)
|
209
|
+
end
|
210
|
+
end
|
metadata
CHANGED
@@ -1,23 +1,59 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: ubalo
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.2
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
8
|
-
-
|
8
|
+
- Ubalo Crew
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2011-
|
13
|
-
dependencies:
|
14
|
-
|
15
|
-
|
16
|
-
|
12
|
+
date: 2011-11-22 00:00:00.000000000Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: gli
|
16
|
+
requirement: &2153389080 !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: *2153389080
|
25
|
+
- !ruby/object:Gem::Dependency
|
26
|
+
name: highline
|
27
|
+
requirement: &2153388480 !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: *2153388480
|
36
|
+
- !ruby/object:Gem::Dependency
|
37
|
+
name: rest-client
|
38
|
+
requirement: &2153387840 !ruby/object:Gem::Requirement
|
39
|
+
none: false
|
40
|
+
requirements:
|
41
|
+
- - ~>
|
42
|
+
- !ruby/object:Gem::Version
|
43
|
+
version: 1.6.3
|
44
|
+
type: :runtime
|
45
|
+
prerelease: false
|
46
|
+
version_requirements: *2153387840
|
47
|
+
description: CLI and API client for Ubalo
|
48
|
+
email: dev@ubalo.com
|
49
|
+
executables:
|
50
|
+
- ubalo
|
17
51
|
extensions: []
|
18
52
|
extra_rdoc_files: []
|
19
|
-
files:
|
20
|
-
|
53
|
+
files:
|
54
|
+
- lib/ubalo.rb
|
55
|
+
- bin/ubalo
|
56
|
+
homepage: http://ubalo.com/
|
21
57
|
licenses: []
|
22
58
|
post_install_message:
|
23
59
|
rdoc_options: []
|
@@ -37,8 +73,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
37
73
|
version: '0'
|
38
74
|
requirements: []
|
39
75
|
rubyforge_project:
|
40
|
-
rubygems_version: 1.8.
|
76
|
+
rubygems_version: 1.8.6
|
41
77
|
signing_key:
|
42
78
|
specification_version: 3
|
43
|
-
summary:
|
79
|
+
summary: CLI and API client for Ubalo
|
44
80
|
test_files: []
|