vmc-tsuru 0.1.alpha

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