vmc 0.0.8 → 0.2.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 +8 -3
- data/README.md +83 -0
- data/Rakefile +11 -65
- data/bin/vmc +3 -2
- data/lib/cli/commands/admin.rb +57 -0
- data/lib/cli/commands/apps.rb +828 -0
- data/lib/cli/commands/base.rb +56 -0
- data/lib/cli/commands/misc.rb +99 -0
- data/lib/cli/commands/services.rb +84 -0
- data/lib/cli/commands/user.rb +60 -0
- data/lib/cli/config.rb +109 -0
- data/lib/cli/core_ext.rb +119 -0
- data/lib/cli/errors.rb +19 -0
- data/lib/cli/frameworks.rb +97 -0
- data/lib/cli/runner.rb +437 -0
- data/lib/cli/services_helper.rb +74 -0
- data/lib/cli/usage.rb +94 -0
- data/lib/cli/version.rb +5 -0
- data/lib/cli/zip_util.rb +61 -0
- data/lib/cli.rb +30 -0
- data/lib/vmc/client.rb +415 -0
- data/lib/vmc/const.rb +19 -0
- data/lib/vmc.rb +2 -1589
- data/spec/assets/app_info.txt +9 -0
- data/spec/assets/app_listings.txt +9 -0
- data/spec/assets/bad_create_app.txt +9 -0
- data/spec/assets/delete_app.txt +9 -0
- data/spec/assets/global_service_listings.txt +9 -0
- data/spec/assets/good_create_app.txt +9 -0
- data/spec/assets/good_create_service.txt +9 -0
- data/spec/assets/info_authenticated.txt +27 -0
- data/spec/assets/info_return.txt +15 -0
- data/spec/assets/info_return_bad.txt +16 -0
- data/spec/assets/login_fail.txt +9 -0
- data/spec/assets/login_success.txt +9 -0
- data/spec/assets/sample_token.txt +1 -0
- data/spec/assets/service_already_exists.txt +9 -0
- data/spec/assets/service_listings.txt +9 -0
- data/spec/assets/service_not_found.txt +9 -0
- data/spec/assets/user_info.txt +9 -0
- data/spec/spec_helper.rb +11 -0
- data/spec/unit/cli_opts_spec.rb +73 -0
- data/spec/unit/client_spec.rb +284 -0
- metadata +114 -71
- data/README +0 -58
- data/lib/parse.rb +0 -719
- data/lib/vmc_base.rb +0 -205
- data/vendor/gems/httpclient/VERSION +0 -1
- data/vendor/gems/httpclient/lib/http-access2/cookie.rb +0 -1
- data/vendor/gems/httpclient/lib/http-access2/http.rb +0 -1
- data/vendor/gems/httpclient/lib/http-access2.rb +0 -53
- data/vendor/gems/httpclient/lib/httpclient/auth.rb +0 -522
- data/vendor/gems/httpclient/lib/httpclient/cacert.p7s +0 -1579
- data/vendor/gems/httpclient/lib/httpclient/cacert_sha1.p7s +0 -1579
- data/vendor/gems/httpclient/lib/httpclient/connection.rb +0 -84
- data/vendor/gems/httpclient/lib/httpclient/cookie.rb +0 -562
- data/vendor/gems/httpclient/lib/httpclient/http.rb +0 -867
- data/vendor/gems/httpclient/lib/httpclient/session.rb +0 -864
- data/vendor/gems/httpclient/lib/httpclient/ssl_config.rb +0 -417
- data/vendor/gems/httpclient/lib/httpclient/timeout.rb +0 -136
- data/vendor/gems/httpclient/lib/httpclient/util.rb +0 -86
- data/vendor/gems/httpclient/lib/httpclient.rb +0 -1020
- data/vendor/gems/httpclient/lib/tags +0 -908
data/lib/vmc/client.rb
ADDED
@@ -0,0 +1,415 @@
|
|
1
|
+
# VMC client
|
2
|
+
#
|
3
|
+
# Example:
|
4
|
+
#
|
5
|
+
# require 'vmc'
|
6
|
+
# client = VMC::Client.new('api.vcap.me')
|
7
|
+
# client.login(:user, :pass)
|
8
|
+
# client.create('myapplication', manifest)
|
9
|
+
# client.create_service('redis', 'my_redis_service', opts);
|
10
|
+
#
|
11
|
+
|
12
|
+
require 'rubygems'
|
13
|
+
require 'json/pure'
|
14
|
+
|
15
|
+
require File.dirname(__FILE__) + '/const'
|
16
|
+
|
17
|
+
class VMC::Client
|
18
|
+
|
19
|
+
def self.version
|
20
|
+
VMC::VERSION
|
21
|
+
end
|
22
|
+
|
23
|
+
attr_reader :target, :host, :user, :proxy, :auth_token
|
24
|
+
attr_accessor :trace
|
25
|
+
|
26
|
+
# Error codes
|
27
|
+
VMC_HTTP_ERROR_CODES = [ 400, 403, 404, 500 ]
|
28
|
+
|
29
|
+
# Errors
|
30
|
+
class BadTarget < RuntimeError; end
|
31
|
+
class AuthError < RuntimeError; end
|
32
|
+
class TargetError < RuntimeError; end
|
33
|
+
class NotFound < RuntimeError; end
|
34
|
+
class BadResponse < RuntimeError; end
|
35
|
+
class HTTPException < RuntimeError; end
|
36
|
+
|
37
|
+
# Initialize new client to the target_uri with optional auth_token
|
38
|
+
def initialize(target_url=VMC::DEFAULT_TARGET, auth_token=nil)
|
39
|
+
target_url = "http://#{target_url}" unless /^https?/ =~ target_url
|
40
|
+
@target = target_url
|
41
|
+
@auth_token = auth_token
|
42
|
+
end
|
43
|
+
|
44
|
+
######################################################
|
45
|
+
# Target info
|
46
|
+
######################################################
|
47
|
+
|
48
|
+
# Retrieves information on the target cloud, and optionally the logged in user
|
49
|
+
def info
|
50
|
+
# TODO: Should merge for new version IMO, general, services, user_account
|
51
|
+
json_get(VMC::INFO_PATH)
|
52
|
+
end
|
53
|
+
|
54
|
+
def raw_info
|
55
|
+
http_get(VMC::INFO_PATH)
|
56
|
+
end
|
57
|
+
|
58
|
+
# Global listing of services that are available on the target system
|
59
|
+
def services_info
|
60
|
+
check_login_status
|
61
|
+
json_get(VMC::GLOBAL_SERVICES_PATH)
|
62
|
+
end
|
63
|
+
|
64
|
+
######################################################
|
65
|
+
# Apps
|
66
|
+
######################################################
|
67
|
+
|
68
|
+
def apps
|
69
|
+
check_login_status
|
70
|
+
json_get(VMC::APPS_PATH)
|
71
|
+
end
|
72
|
+
|
73
|
+
def create_app(name, manifest={})
|
74
|
+
check_login_status
|
75
|
+
app = manifest.dup
|
76
|
+
app[:name] = name
|
77
|
+
app[:instances] ||= 1
|
78
|
+
json_post(VMC::APPS_PATH, app)
|
79
|
+
end
|
80
|
+
|
81
|
+
def update_app(name, manifest)
|
82
|
+
check_login_status
|
83
|
+
json_put("#{VMC::APPS_PATH}/#{name}", manifest)
|
84
|
+
end
|
85
|
+
|
86
|
+
def upload_app(name, zipfile, resource_manifest=nil)
|
87
|
+
#FIXME, manifest should be allowed to be null, here for compatability with old cc's
|
88
|
+
resource_manifest ||= []
|
89
|
+
check_login_status
|
90
|
+
if zipfile.is_a? File
|
91
|
+
file = zipfile
|
92
|
+
else
|
93
|
+
file = File.new(zipfile, 'rb')
|
94
|
+
end
|
95
|
+
upload_data = {:application => file, :_method => 'put'}
|
96
|
+
upload_data[:resources] = resource_manifest.to_json if resource_manifest
|
97
|
+
http_post("#{VMC::APPS_PATH}/#{name}/application", upload_data)
|
98
|
+
end
|
99
|
+
|
100
|
+
def delete_app(name)
|
101
|
+
check_login_status
|
102
|
+
http_delete("#{VMC::APPS_PATH}/#{name}")
|
103
|
+
end
|
104
|
+
|
105
|
+
def app_info(name)
|
106
|
+
check_login_status
|
107
|
+
json_get("#{VMC::APPS_PATH}/#{name}")
|
108
|
+
end
|
109
|
+
|
110
|
+
def app_stats(name)
|
111
|
+
check_login_status
|
112
|
+
stats_raw = json_get("#{VMC::APPS_PATH}/#{name}/stats")
|
113
|
+
stats = []
|
114
|
+
stats_raw.each_pair do |k, entry|
|
115
|
+
# Skip entries with no stats
|
116
|
+
next unless entry[:stats]
|
117
|
+
entry[:instance] = k.to_s.to_i
|
118
|
+
entry[:state] = entry[:state].to_sym if entry[:state]
|
119
|
+
stats << entry
|
120
|
+
end
|
121
|
+
stats.sort { |a,b| a[:instance] - b[:instance] }
|
122
|
+
end
|
123
|
+
|
124
|
+
def app_instances(name)
|
125
|
+
check_login_status
|
126
|
+
json_get("#{VMC::APPS_PATH}/#{name}/instances")
|
127
|
+
end
|
128
|
+
|
129
|
+
def app_crashes(name)
|
130
|
+
check_login_status
|
131
|
+
json_get("#{VMC::APPS_PATH}/#{name}/crashes")
|
132
|
+
end
|
133
|
+
|
134
|
+
# List the directory or download the actual file indicated by
|
135
|
+
# the path.
|
136
|
+
def app_files(name, path, instance=0)
|
137
|
+
check_login_status
|
138
|
+
url = "#{VMC::APPS_PATH}/#{name}/instances/#{instance}/files/#{path}"
|
139
|
+
url.gsub!('//', '/')
|
140
|
+
_, body, headers = http_get(url)
|
141
|
+
body
|
142
|
+
end
|
143
|
+
|
144
|
+
######################################################
|
145
|
+
# Services
|
146
|
+
######################################################
|
147
|
+
|
148
|
+
# listing of services that are available in the system
|
149
|
+
def services
|
150
|
+
check_login_status
|
151
|
+
json_get(VMC::SERVICES_PATH)
|
152
|
+
end
|
153
|
+
|
154
|
+
def create_service(service, name)
|
155
|
+
check_login_status
|
156
|
+
services = services_info
|
157
|
+
services ||= []
|
158
|
+
service_hash = nil
|
159
|
+
|
160
|
+
service = service.to_s
|
161
|
+
|
162
|
+
# FIXME!
|
163
|
+
services.each do |service_type, value|
|
164
|
+
value.each do |vendor, version|
|
165
|
+
version.each do |version_str, service_descr|
|
166
|
+
if service == service_descr[:vendor]
|
167
|
+
service_hash = {
|
168
|
+
:type => service_descr[:type], :tier => 'free',
|
169
|
+
:vendor => service, :version => version_str
|
170
|
+
}
|
171
|
+
break
|
172
|
+
end
|
173
|
+
end
|
174
|
+
end
|
175
|
+
end
|
176
|
+
|
177
|
+
raise TargetError, "Service [#{service}] is not a valid service choice" unless service_hash
|
178
|
+
service_hash[:name] = name
|
179
|
+
json_post(VMC::SERVICES_PATH, service_hash)
|
180
|
+
end
|
181
|
+
|
182
|
+
def delete_service(name)
|
183
|
+
check_login_status
|
184
|
+
svcs = services || []
|
185
|
+
names = svcs.collect { |s| s[:name] }
|
186
|
+
raise TargetError, "Service [#{name}] not a valid service" unless names.include? name
|
187
|
+
http_delete("#{VMC::SERVICES_PATH}/#{name}")
|
188
|
+
end
|
189
|
+
|
190
|
+
def bind_service(service, appname)
|
191
|
+
check_login_status
|
192
|
+
app = app_info(appname)
|
193
|
+
services = app[:services] || []
|
194
|
+
app[:services] = services << service
|
195
|
+
update_app(appname, app)
|
196
|
+
end
|
197
|
+
|
198
|
+
def unbind_service(service, appname)
|
199
|
+
check_login_status
|
200
|
+
app = app_info(appname)
|
201
|
+
services = app[:services] || []
|
202
|
+
services.delete(service)
|
203
|
+
app[:services] = services
|
204
|
+
update_app(appname, app)
|
205
|
+
end
|
206
|
+
|
207
|
+
######################################################
|
208
|
+
# Resources
|
209
|
+
######################################################
|
210
|
+
|
211
|
+
# Send in a resources manifest array to the system to have
|
212
|
+
# it check what is needed to actually send. Returns array
|
213
|
+
# indicating what is needed. This returned manifest should be
|
214
|
+
# sent in with the upload if resources were removed.
|
215
|
+
# E.g. [{:sha1 => xxx, :size => xxx, :fn => filename}]
|
216
|
+
def check_resources(resources)
|
217
|
+
check_login_status
|
218
|
+
status, body, headers = json_post(VMC::RESOURCES_PATH, resources)
|
219
|
+
json_parse(body)
|
220
|
+
end
|
221
|
+
|
222
|
+
######################################################
|
223
|
+
# Validation Helpers
|
224
|
+
######################################################
|
225
|
+
|
226
|
+
# Checks that the target is valid
|
227
|
+
def target_valid?
|
228
|
+
return false unless descr = info
|
229
|
+
return false unless descr[:name]
|
230
|
+
return false unless descr[:build]
|
231
|
+
return false unless descr[:version]
|
232
|
+
return false unless descr[:support]
|
233
|
+
true
|
234
|
+
rescue
|
235
|
+
false
|
236
|
+
end
|
237
|
+
|
238
|
+
# Checks that the auth_token is valid
|
239
|
+
def logged_in?
|
240
|
+
descr = info
|
241
|
+
if descr
|
242
|
+
return false unless descr[:user]
|
243
|
+
return false unless descr[:usage]
|
244
|
+
@user = descr[:user]
|
245
|
+
true
|
246
|
+
end
|
247
|
+
end
|
248
|
+
|
249
|
+
######################################################
|
250
|
+
# User login/password
|
251
|
+
######################################################
|
252
|
+
|
253
|
+
# login and return an auth_token
|
254
|
+
# Auth token can be retained and used in creating
|
255
|
+
# new clients, avoiding login.
|
256
|
+
def login(user, password)
|
257
|
+
status, body, headers = json_post("#{VMC::USERS_PATH}/#{user}/tokens", {:password => password})
|
258
|
+
response_info = json_parse(body)
|
259
|
+
if response_info
|
260
|
+
@user = user
|
261
|
+
@auth_token = response_info[:token]
|
262
|
+
end
|
263
|
+
end
|
264
|
+
|
265
|
+
# sets the password for the current logged user
|
266
|
+
def change_password(new_password)
|
267
|
+
check_login_status
|
268
|
+
user_info = json_get("#{VMC::USERS_PATH}/#{@user}")
|
269
|
+
if user_info
|
270
|
+
user_info[:password] = new_password
|
271
|
+
json_put("#{VMC::USERS_PATH}/#{@user}", user_info)
|
272
|
+
end
|
273
|
+
end
|
274
|
+
|
275
|
+
######################################################
|
276
|
+
# System administration
|
277
|
+
######################################################
|
278
|
+
|
279
|
+
def proxy=(proxy)
|
280
|
+
@proxy = proxy
|
281
|
+
end
|
282
|
+
|
283
|
+
def proxy_for(proxy)
|
284
|
+
@proxy = proxy
|
285
|
+
end
|
286
|
+
|
287
|
+
def add_user(user_email, password)
|
288
|
+
json_post(VMC::USERS_PATH, { :email => user_email, :password => password })
|
289
|
+
end
|
290
|
+
|
291
|
+
def delete_user(user_email)
|
292
|
+
http_delete("#{VMC::USERS_PATH}/#{user_email}")
|
293
|
+
end
|
294
|
+
|
295
|
+
######################################################
|
296
|
+
|
297
|
+
private
|
298
|
+
|
299
|
+
def json_get(url)
|
300
|
+
status, body, headers = http_get(url, 'application/json')
|
301
|
+
json_parse(body)
|
302
|
+
rescue JSON::ParserError
|
303
|
+
raise BadResponse, "Can't parse response into JSON", body
|
304
|
+
end
|
305
|
+
|
306
|
+
def json_post(url, payload)
|
307
|
+
http_post(url, payload.to_json, 'application/json')
|
308
|
+
end
|
309
|
+
|
310
|
+
def json_put(url, payload)
|
311
|
+
http_put(url, payload.to_json, 'application/json')
|
312
|
+
end
|
313
|
+
|
314
|
+
def json_parse(str)
|
315
|
+
if str
|
316
|
+
JSON.parse(str, :symbolize_names => true)
|
317
|
+
end
|
318
|
+
end
|
319
|
+
|
320
|
+
require 'rest_client'
|
321
|
+
|
322
|
+
# HTTP helpers
|
323
|
+
|
324
|
+
def http_get(path, content_type=nil)
|
325
|
+
request(:get, path, content_type)
|
326
|
+
end
|
327
|
+
|
328
|
+
def http_post(path, body, content_type=nil)
|
329
|
+
request(:post, path, content_type, body)
|
330
|
+
end
|
331
|
+
|
332
|
+
def http_put(path, body, content_type=nil)
|
333
|
+
request(:put, path, content_type, body)
|
334
|
+
end
|
335
|
+
|
336
|
+
def http_delete(path)
|
337
|
+
request(:delete, path)
|
338
|
+
end
|
339
|
+
|
340
|
+
def request(method, path, content_type = nil, payload = nil, headers = {})
|
341
|
+
headers = headers.dup
|
342
|
+
headers['AUTHORIZATION'] = @auth_token if @auth_token
|
343
|
+
headers['PROXY-USER'] = @proxy if @proxy
|
344
|
+
headers['Content-Type'] = content_type if content_type
|
345
|
+
|
346
|
+
req = {
|
347
|
+
:method => method, :url => "#{@target}#{path}",
|
348
|
+
:payload => payload, :headers => headers
|
349
|
+
}
|
350
|
+
status, body, response_headers = perform_http_request(req)
|
351
|
+
|
352
|
+
if VMC_HTTP_ERROR_CODES.include?(status)
|
353
|
+
# FIXME, old cc returned 400 on not found for file access
|
354
|
+
err = (status == 404 || status == 400) ? NotFound : TargetError
|
355
|
+
raise err, parse_error_message(status, body)
|
356
|
+
else
|
357
|
+
return status, body, response_headers
|
358
|
+
end
|
359
|
+
rescue URI::Error, SocketError, Errno::ECONNREFUSED => e
|
360
|
+
raise BadTarget, "Cannot access target (%s)" % [ e.message ]
|
361
|
+
end
|
362
|
+
|
363
|
+
def perform_http_request(req)
|
364
|
+
RestClient.proxy = ENV['https_proxy'] || ENV['http_proxy']
|
365
|
+
|
366
|
+
result = nil
|
367
|
+
RestClient::Request.execute(req) do |response, request|
|
368
|
+
result = [ response.code, response.body, response.headers ]
|
369
|
+
if trace
|
370
|
+
puts '>>>'
|
371
|
+
puts "PROXY: #{RestClient.proxy}" if RestClient.proxy
|
372
|
+
puts "REQUEST: #{req[:method]} #{req[:url]}"
|
373
|
+
puts "REQUEST_BODY: #{req[:payload]}" if req[:payload]
|
374
|
+
puts "RESPONSE: [#{response.code}] #{response.body}"
|
375
|
+
puts '<<<'
|
376
|
+
end
|
377
|
+
end
|
378
|
+
result
|
379
|
+
rescue Net::HTTPBadResponse => e
|
380
|
+
raise BadTarget "Received bad HTTP response from target: #{e}"
|
381
|
+
rescue RestClient::Exception => e
|
382
|
+
raise HTTPException, "HTTP exception: #{e}"
|
383
|
+
end
|
384
|
+
|
385
|
+
def truncate(str, limit = 30)
|
386
|
+
etc = '...'
|
387
|
+
stripped = str.strip[0..limit]
|
388
|
+
if stripped.length > limit
|
389
|
+
stripped + etc
|
390
|
+
else
|
391
|
+
stripped
|
392
|
+
end
|
393
|
+
end
|
394
|
+
|
395
|
+
def parse_error_message(status, body)
|
396
|
+
parsed_body = json_parse(body.to_s)
|
397
|
+
if parsed_body && parsed_body[:code] && parsed_body[:description]
|
398
|
+
desc = parsed_body[:description].gsub("\"","'")
|
399
|
+
"Error #{parsed_body[:code]}: #{desc}"
|
400
|
+
else
|
401
|
+
"Error (HTTP #{status}): #{body}"
|
402
|
+
end
|
403
|
+
rescue JSON::ParserError
|
404
|
+
if body.nil? || body.empty?
|
405
|
+
"Error (#{status}): No Response Received"
|
406
|
+
else
|
407
|
+
"Error (JSON #{status}): #{truncate(body)}"
|
408
|
+
end
|
409
|
+
end
|
410
|
+
|
411
|
+
def check_login_status
|
412
|
+
raise AuthError unless @user || logged_in?
|
413
|
+
end
|
414
|
+
|
415
|
+
end
|
data/lib/vmc/const.rb
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
module VMC
|
2
|
+
|
3
|
+
VERSION = '0.2.4'
|
4
|
+
|
5
|
+
# Targets
|
6
|
+
DEFAULT_TARGET = 'http://api.cloudfoundry.com'
|
7
|
+
DEFAULT_LOCAL_TARGET = 'http://api.vcap.me'
|
8
|
+
|
9
|
+
# General Paths
|
10
|
+
INFO_PATH = '/info'
|
11
|
+
GLOBAL_SERVICES_PATH = '/info/services'
|
12
|
+
RESOURCES_PATH = '/resources'
|
13
|
+
|
14
|
+
# User specific paths
|
15
|
+
APPS_PATH = '/apps'
|
16
|
+
SERVICES_PATH = '/services'
|
17
|
+
USERS_PATH = '/users'
|
18
|
+
|
19
|
+
end
|