ubalo 0.0.1 → 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- 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: []
|