vmc 0.0.8 → 0.2.4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (63) hide show
  1. data/LICENSE +8 -3
  2. data/README.md +83 -0
  3. data/Rakefile +11 -65
  4. data/bin/vmc +3 -2
  5. data/lib/cli/commands/admin.rb +57 -0
  6. data/lib/cli/commands/apps.rb +828 -0
  7. data/lib/cli/commands/base.rb +56 -0
  8. data/lib/cli/commands/misc.rb +99 -0
  9. data/lib/cli/commands/services.rb +84 -0
  10. data/lib/cli/commands/user.rb +60 -0
  11. data/lib/cli/config.rb +109 -0
  12. data/lib/cli/core_ext.rb +119 -0
  13. data/lib/cli/errors.rb +19 -0
  14. data/lib/cli/frameworks.rb +97 -0
  15. data/lib/cli/runner.rb +437 -0
  16. data/lib/cli/services_helper.rb +74 -0
  17. data/lib/cli/usage.rb +94 -0
  18. data/lib/cli/version.rb +5 -0
  19. data/lib/cli/zip_util.rb +61 -0
  20. data/lib/cli.rb +30 -0
  21. data/lib/vmc/client.rb +415 -0
  22. data/lib/vmc/const.rb +19 -0
  23. data/lib/vmc.rb +2 -1589
  24. data/spec/assets/app_info.txt +9 -0
  25. data/spec/assets/app_listings.txt +9 -0
  26. data/spec/assets/bad_create_app.txt +9 -0
  27. data/spec/assets/delete_app.txt +9 -0
  28. data/spec/assets/global_service_listings.txt +9 -0
  29. data/spec/assets/good_create_app.txt +9 -0
  30. data/spec/assets/good_create_service.txt +9 -0
  31. data/spec/assets/info_authenticated.txt +27 -0
  32. data/spec/assets/info_return.txt +15 -0
  33. data/spec/assets/info_return_bad.txt +16 -0
  34. data/spec/assets/login_fail.txt +9 -0
  35. data/spec/assets/login_success.txt +9 -0
  36. data/spec/assets/sample_token.txt +1 -0
  37. data/spec/assets/service_already_exists.txt +9 -0
  38. data/spec/assets/service_listings.txt +9 -0
  39. data/spec/assets/service_not_found.txt +9 -0
  40. data/spec/assets/user_info.txt +9 -0
  41. data/spec/spec_helper.rb +11 -0
  42. data/spec/unit/cli_opts_spec.rb +73 -0
  43. data/spec/unit/client_spec.rb +284 -0
  44. metadata +114 -71
  45. data/README +0 -58
  46. data/lib/parse.rb +0 -719
  47. data/lib/vmc_base.rb +0 -205
  48. data/vendor/gems/httpclient/VERSION +0 -1
  49. data/vendor/gems/httpclient/lib/http-access2/cookie.rb +0 -1
  50. data/vendor/gems/httpclient/lib/http-access2/http.rb +0 -1
  51. data/vendor/gems/httpclient/lib/http-access2.rb +0 -53
  52. data/vendor/gems/httpclient/lib/httpclient/auth.rb +0 -522
  53. data/vendor/gems/httpclient/lib/httpclient/cacert.p7s +0 -1579
  54. data/vendor/gems/httpclient/lib/httpclient/cacert_sha1.p7s +0 -1579
  55. data/vendor/gems/httpclient/lib/httpclient/connection.rb +0 -84
  56. data/vendor/gems/httpclient/lib/httpclient/cookie.rb +0 -562
  57. data/vendor/gems/httpclient/lib/httpclient/http.rb +0 -867
  58. data/vendor/gems/httpclient/lib/httpclient/session.rb +0 -864
  59. data/vendor/gems/httpclient/lib/httpclient/ssl_config.rb +0 -417
  60. data/vendor/gems/httpclient/lib/httpclient/timeout.rb +0 -136
  61. data/vendor/gems/httpclient/lib/httpclient/util.rb +0 -86
  62. data/vendor/gems/httpclient/lib/httpclient.rb +0 -1020
  63. 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