paasio 0.3.16.beta.2

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/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'