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.
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