paasio 0.3.16.beta.2

Sign up to get free protection for your applications and to get access to all the features.
data/lib/vmc/client.rb ADDED
@@ -0,0 +1,481 @@
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
+ require 'open-uri'
15
+
16
+ require File.expand_path('../const', __FILE__)
17
+
18
+ class VMC::Client
19
+
20
+ def self.version
21
+ VMC::VERSION
22
+ end
23
+
24
+ attr_reader :target, :host, :user, :proxy, :auth_token
25
+ attr_accessor :trace
26
+
27
+ # Error codes
28
+ VMC_HTTP_ERROR_CODES = [ 400, 500 ]
29
+
30
+ # Errors
31
+ class BadTarget < RuntimeError; end
32
+ class AuthError < RuntimeError; end
33
+ class TargetError < RuntimeError; end
34
+ class NotFound < RuntimeError; end
35
+ class BadResponse < RuntimeError; end
36
+ class HTTPException < RuntimeError; end
37
+
38
+ # Initialize new client to the target_uri with optional auth_token
39
+ def initialize(target_url=VMC::DEFAULT_TARGET, auth_token=nil)
40
+ target_url = "http://#{target_url}" unless /^https?/ =~ target_url
41
+ target_url = target_url.gsub(/\/+$/, '')
42
+ @target = target_url
43
+ @auth_token = auth_token
44
+ end
45
+
46
+ ######################################################
47
+ # Target info
48
+ ######################################################
49
+
50
+ # Retrieves information on the target cloud, and optionally the logged in user
51
+ def info
52
+ # TODO: Should merge for new version IMO, general, services, user_account
53
+ json_get(VMC::INFO_PATH)
54
+ end
55
+
56
+ def raw_info
57
+ http_get(VMC::INFO_PATH)
58
+ end
59
+
60
+ # Global listing of services that are available on the target system
61
+ def services_info
62
+ check_login_status
63
+ json_get(path(VMC::GLOBAL_SERVICES_PATH))
64
+ end
65
+
66
+ def runtimes_info
67
+ json_get(path(VMC::GLOBAL_RUNTIMES_PATH))
68
+ end
69
+
70
+ ######################################################
71
+ # Apps
72
+ ######################################################
73
+
74
+ def apps
75
+ check_login_status
76
+ json_get(VMC::APPS_PATH)
77
+ end
78
+
79
+ def create_app(name, manifest={})
80
+ check_login_status
81
+ app = manifest.dup
82
+ app[:name] = name
83
+ app[:instances] ||= 1
84
+ json_post(VMC::APPS_PATH, app)
85
+ end
86
+
87
+ def update_app(name, manifest)
88
+ check_login_status
89
+ json_put(path(VMC::APPS_PATH, name), manifest)
90
+ end
91
+
92
+ def upload_app(name, zipfile, resource_manifest=nil)
93
+ #FIXME, manifest should be allowed to be null, here for compatability with old cc's
94
+ resource_manifest ||= []
95
+ check_login_status
96
+ upload_data = {:_method => 'put'}
97
+ if zipfile
98
+ if zipfile.is_a? File
99
+ file = zipfile
100
+ else
101
+ file = File.new(zipfile, 'rb')
102
+ end
103
+ upload_data[:application] = file
104
+ end
105
+ upload_data[:resources] = resource_manifest.to_json if resource_manifest
106
+ http_post(path(VMC::APPS_PATH, name, "application"), upload_data)
107
+ rescue RestClient::ServerBrokeConnection
108
+ retry
109
+ end
110
+
111
+ def delete_app(name)
112
+ check_login_status
113
+ http_delete(path(VMC::APPS_PATH, name))
114
+ end
115
+
116
+ def app_info(name)
117
+ check_login_status
118
+ json_get(path(VMC::APPS_PATH, name))
119
+ end
120
+
121
+ def app_update_info(name)
122
+ check_login_status
123
+ json_get(path(VMC::APPS_PATH, name, "update"))
124
+ end
125
+
126
+ def app_stats(name)
127
+ check_login_status
128
+ stats_raw = json_get(path(VMC::APPS_PATH, name, "stats"))
129
+ stats = []
130
+ stats_raw.each_pair do |k, entry|
131
+ # Skip entries with no stats
132
+ next unless entry[:stats]
133
+ entry[:instance] = k.to_s.to_i
134
+ entry[:state] = entry[:state].to_sym if entry[:state]
135
+ stats << entry
136
+ end
137
+ stats.sort { |a,b| a[:instance] - b[:instance] }
138
+ end
139
+
140
+ def app_instances(name)
141
+ check_login_status
142
+ json_get(path(VMC::APPS_PATH, name, "instances"))
143
+ end
144
+
145
+ def app_crashes(name)
146
+ check_login_status
147
+ json_get(path(VMC::APPS_PATH, name, "crashes"))
148
+ end
149
+
150
+ # List the directory or download the actual file indicated by
151
+ # the path.
152
+ def app_files(name, path, instance=0)
153
+ check_login_status
154
+ path = path.gsub('//', '/')
155
+ url = path(VMC::APPS_PATH, name, "instances", instance, "files", path)
156
+ _, body, headers = http_get(url)
157
+ body
158
+ end
159
+
160
+ def app_most_recent_deploy(name)
161
+ check_login_status
162
+ begin
163
+ json_get(path(VMC::APPS_PATH, name, 'most_recent_deploy'))
164
+ rescue BadResponse
165
+ # this is probably because there was no deploy.. it returns "null", which JSON.parse can't handle
166
+ return nil
167
+ end
168
+ end
169
+
170
+ ######################################################
171
+ # Services
172
+ ######################################################
173
+
174
+ # listing of services that are available in the system
175
+ def services
176
+ check_login_status
177
+ json_get(VMC::SERVICES_PATH)
178
+ end
179
+
180
+ def create_service(service, name)
181
+ check_login_status
182
+ services = services_info
183
+ services ||= []
184
+ service_hash = nil
185
+
186
+ service = service.to_s
187
+
188
+ # FIXME!
189
+ services.each do |service_type, value|
190
+ value.each do |vendor, version|
191
+ version.each do |version_str, service_descr|
192
+ if service == service_descr[:vendor]
193
+ service_hash = {
194
+ :type => service_descr[:type], :tier => 'free',
195
+ :vendor => service, :version => version_str
196
+ }
197
+ break
198
+ end
199
+ end
200
+ end
201
+ end
202
+
203
+ raise TargetError, "Service [#{service}] is not a valid service choice" unless service_hash
204
+ service_hash[:name] = name
205
+ json_post(path(VMC::SERVICES_PATH), service_hash)
206
+ end
207
+
208
+ def delete_service(name)
209
+ check_login_status
210
+ svcs = services || []
211
+ names = svcs.collect { |s| s[:name] }
212
+ raise TargetError, "Service [#{name}] not a valid service" unless names.include? name
213
+ http_delete(path(VMC::SERVICES_PATH, name))
214
+ end
215
+
216
+ def bind_service(service, appname)
217
+ check_login_status
218
+ app = app_info(appname)
219
+ services = app[:services] || []
220
+ app[:services] = services << service
221
+ update_app(appname, app)
222
+ end
223
+
224
+ def unbind_service(service, appname)
225
+ check_login_status
226
+ app = app_info(appname)
227
+ services = app[:services] || []
228
+ services.delete(service)
229
+ app[:services] = services
230
+ update_app(appname, app)
231
+ end
232
+
233
+ ######################################################
234
+ # Resources
235
+ ######################################################
236
+
237
+ # Send in a resources manifest array to the system to have
238
+ # it check what is needed to actually send. Returns array
239
+ # indicating what is needed. This returned manifest should be
240
+ # sent in with the upload if resources were removed.
241
+ # E.g. [{:sha1 => xxx, :size => xxx, :fn => filename}]
242
+ def check_resources(resources)
243
+ check_login_status
244
+ status, body, headers = json_post(VMC::RESOURCES_PATH, resources)
245
+ json_parse(body)
246
+ end
247
+
248
+ ######################################################
249
+ # Validation Helpers
250
+ ######################################################
251
+
252
+ # Checks that the target is valid
253
+ def target_valid?
254
+ return false unless descr = info
255
+ return false unless descr[:name]
256
+ return false unless descr[:build]
257
+ return false unless descr[:version]
258
+ return false unless descr[:support]
259
+ true
260
+ rescue
261
+ false
262
+ end
263
+
264
+ # Checks that the auth_token is valid
265
+ def logged_in?
266
+ descr = info
267
+ if descr
268
+ return false unless descr[:user]
269
+ return false unless descr[:usage]
270
+ @user = descr[:user]
271
+ true
272
+ end
273
+ end
274
+
275
+ ######################################################
276
+ # User login/password
277
+ ######################################################
278
+
279
+ # login and return an auth_token
280
+ # Auth token can be retained and used in creating
281
+ # new clients, avoiding login.
282
+ def login(user, password)
283
+ status, body, headers = json_post(path(VMC::USERS_PATH, user, "tokens"), {:password => password})
284
+ response_info = json_parse(body)
285
+ if response_info
286
+ @user = user
287
+ @auth_token = response_info[:token]
288
+ end
289
+ end
290
+
291
+ # sets the password for the current logged user
292
+ def change_password(new_password)
293
+ check_login_status
294
+ user_info = json_get(path(VMC::USERS_PATH, @user))
295
+ if user_info
296
+ user_info[:password] = new_password
297
+ json_put(path(VMC::USERS_PATH, @user), user_info)
298
+ end
299
+ end
300
+
301
+ ######################################################
302
+ # System administration
303
+ ######################################################
304
+
305
+ def proxy=(proxy)
306
+ @proxy = proxy
307
+ end
308
+
309
+ def proxy_for(proxy)
310
+ @proxy = proxy
311
+ end
312
+
313
+ def users
314
+ check_login_status
315
+ json_get(VMC::USERS_PATH)
316
+ end
317
+
318
+ def add_user(user_email, password)
319
+ json_post(VMC::USERS_PATH, { :email => user_email, :password => password })
320
+ end
321
+
322
+ def delete_user(user_email)
323
+ check_login_status
324
+ http_delete(path(VMC::USERS_PATH, user_email))
325
+ end
326
+
327
+ ######################################################
328
+
329
+ def self.path(*path)
330
+ path.flatten.collect { |x|
331
+ URI.encode x, Regexp.new("[^#{URI::PATTERN::UNRESERVED}]")
332
+ }.join("/")
333
+ end
334
+
335
+ private
336
+
337
+ def path(*args, &blk)
338
+ self.class.path(*args, &blk)
339
+ end
340
+
341
+ def json_get(url)
342
+ status, body, headers = http_get(url, 'application/json')
343
+ json_parse(body)
344
+ rescue JSON::ParserError
345
+ raise BadResponse, "Can't parse response into JSON", body
346
+ end
347
+
348
+ def json_post(url, payload)
349
+ http_post(url, payload.to_json, 'application/json')
350
+ end
351
+
352
+ def json_put(url, payload)
353
+ http_put(url, payload.to_json, 'application/json')
354
+ end
355
+
356
+ def json_parse(str)
357
+ if str
358
+ JSON.parse(str, :symbolize_names => true)
359
+ end
360
+ end
361
+
362
+ require 'rest_client'
363
+
364
+ # HTTP helpers
365
+
366
+ def http_get(path, content_type=nil)
367
+ request(:get, path, content_type)
368
+ end
369
+
370
+ def http_post(path, body, content_type=nil)
371
+ request(:post, path, content_type, body)
372
+ end
373
+
374
+ def http_put(path, body, content_type=nil)
375
+ request(:put, path, content_type, body)
376
+ end
377
+
378
+ def http_delete(path)
379
+ request(:delete, path)
380
+ end
381
+
382
+ def request(method, path, content_type = nil, payload = nil, headers = {})
383
+ headers = headers.dup
384
+ headers['AUTHORIZATION'] = @auth_token if @auth_token
385
+ headers['PROXY-USER'] = @proxy if @proxy
386
+
387
+ if content_type
388
+ headers['Content-Type'] = content_type
389
+ headers['Accept'] = content_type
390
+ end
391
+
392
+ req = {
393
+ :method => method, :url => "#{@target}/#{path}",
394
+ :payload => payload, :headers => headers, :multipart => true
395
+ }
396
+ status, body, response_headers = perform_http_request(req)
397
+
398
+ if request_failed?(status)
399
+ # FIXME, old cc returned 400 on not found for file access
400
+ err = (status == 404 || status == 400) ? NotFound : TargetError
401
+ raise err, parse_error_message(status, body)
402
+ else
403
+ return status, body, response_headers
404
+ end
405
+ rescue URI::Error, SocketError, Errno::ECONNREFUSED => e
406
+ raise BadTarget, "Cannot access target (%s)" % [ e.message ]
407
+ end
408
+
409
+ def request_failed?(status)
410
+ VMC_HTTP_ERROR_CODES.detect{|error_code| status >= error_code}
411
+ end
412
+
413
+ def perform_http_request(req)
414
+ proxy_uri = URI.parse(req[:url]).find_proxy()
415
+ RestClient.proxy = proxy_uri.to_s if proxy_uri
416
+
417
+ # Setup tracing if needed
418
+ unless trace.nil?
419
+ req[:headers]['X-VCAP-Trace'] = (trace == true ? '22' : trace)
420
+ end
421
+
422
+ result = nil
423
+ RestClient::Request.execute(req) do |response, request|
424
+ result = [ response.code, response.body, response.headers ]
425
+ unless trace.nil?
426
+ puts '>>>'
427
+ puts "PROXY: #{RestClient.proxy}" if RestClient.proxy
428
+ puts "REQUEST: #{req[:method]} #{req[:url]}"
429
+ puts "RESPONSE_HEADERS:"
430
+ response.headers.each do |key, value|
431
+ puts " #{key} : #{value}"
432
+ end
433
+ puts "REQUEST_BODY: #{req[:payload]}" if req[:payload]
434
+ puts "RESPONSE: [#{response.code}]"
435
+ begin
436
+ puts JSON.pretty_generate(JSON.parse(response.body))
437
+ rescue
438
+ puts "#{response.body}"
439
+ end
440
+ puts '<<<'
441
+ end
442
+ end
443
+ result
444
+ rescue Net::HTTPBadResponse => e
445
+ raise BadTarget "Received bad HTTP response from target: #{e}"
446
+ rescue SystemCallError, RestClient::Exception => e
447
+ raise HTTPException, "HTTP exception: #{e.class}:#{e}"
448
+ end
449
+
450
+ def truncate(str, limit = 30)
451
+ etc = '...'
452
+ stripped = str.strip[0..limit]
453
+ if stripped.length > limit
454
+ stripped + etc
455
+ else
456
+ stripped
457
+ end
458
+ end
459
+
460
+ def parse_error_message(status, body)
461
+ parsed_body = json_parse(body.to_s)
462
+ if parsed_body && parsed_body[:code] && parsed_body[:description]
463
+ desc = parsed_body[:description].gsub("\"","'")
464
+ "Error #{parsed_body[:code]}: #{desc}"
465
+ else
466
+ "Error (HTTP #{status}): #{body}"
467
+ end
468
+ rescue JSON::ParserError
469
+ if body.nil? || body.empty?
470
+ "Error (#{status}): No Response Received"
471
+ else
472
+ body_out = trace ? body : truncate(body)
473
+ "Error (JSON #{status}): #{body_out}"
474
+ end
475
+ end
476
+
477
+ def check_login_status
478
+ raise AuthError unless @user || logged_in?
479
+ end
480
+
481
+ end
data/lib/vmc/const.rb ADDED
@@ -0,0 +1,22 @@
1
+ module VMC
2
+
3
+ # This is the internal VMC version number, and is not necessarily
4
+ # the same as the RubyGem version (VMC::Cli::VERSION).
5
+ VERSION = '0.3.2'
6
+
7
+ # Targets
8
+ DEFAULT_TARGET = 'https://api.paas.io'
9
+ DEFAULT_LOCAL_TARGET = 'http://api.paas.io'
10
+
11
+ # General Paths
12
+ INFO_PATH = 'info'
13
+ GLOBAL_SERVICES_PATH = ['info', 'services']
14
+ GLOBAL_RUNTIMES_PATH = ['info', 'runtimes']
15
+ RESOURCES_PATH = 'resources'
16
+
17
+ # User specific paths
18
+ APPS_PATH = 'apps'
19
+ SERVICES_PATH = 'services'
20
+ USERS_PATH = 'users'
21
+
22
+ end
data/lib/vmc.rb ADDED
@@ -0,0 +1,3 @@
1
+ module VMC; end
2
+
3
+ require 'vmc/client'