jdc 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
data/lib/jdc/client.rb ADDED
@@ -0,0 +1,457 @@
1
+ # JDC client
2
+ #
3
+ # Example:
4
+ #
5
+ # require 'jdc'
6
+ # client = JDC::Client.new('api.vcap.me')
7
+ # client.create('myapplication', manifest)
8
+ # client.create_service('redis', 'my_redis_service', opts);
9
+ #
10
+
11
+ require 'rubygems'
12
+ require 'json/pure'
13
+ require 'open-uri'
14
+
15
+ require File.expand_path('../const', __FILE__)
16
+ require File.expand_path('../timer', __FILE__)
17
+ require File.expand_path('../signature/version.rb', __FILE__)
18
+
19
+ class JDC::Client
20
+
21
+ include JDC::Timer
22
+ include JDC::Signature::Version
23
+
24
+ def self.version
25
+ JDC::VERSION
26
+ end
27
+
28
+ attr_reader :target, :host, :user, :proxy, :auth_token, :access_key_id, :secret_key
29
+ attr_accessor :trace
30
+
31
+ # Error codes
32
+ JDC_HTTP_ERROR_CODES = [ 400, 500 ]
33
+
34
+ # Errors
35
+ class BadTarget < RuntimeError; end
36
+ class AuthError < RuntimeError; end
37
+ class TargetError < RuntimeError; end
38
+ class NotFound < RuntimeError; end
39
+ class BadResponse < RuntimeError; end
40
+ class HTTPException < RuntimeError; end
41
+
42
+ # Initialize new client to the target_uri with optional auth_token
43
+ def initialize(target_url=JDC::DEFAULT_TARGET, auth_token=nil)
44
+ target_url = "http://#{target_url}" unless /^https?/ =~ target_url
45
+ target_url = target_url.gsub(/\/+$/, '')
46
+ @target = target_url
47
+ @auth_token = auth_token
48
+ @access_key_id = ENV[JDC::ACCESS_KEY_ID]
49
+ raise TargetError, "Please set the enviroment value:ACCESS_KEY_ID" unless @access_key_id
50
+ @secret_key = ENV[JDC::SECRET_KEY]
51
+ raise TargetError, "Please set the enviroment value:SECRET_KEY" unless @secret_key
52
+ end
53
+
54
+ ######################################################
55
+ # Target info
56
+ ######################################################
57
+
58
+ # Retrieves information on the target cloud, and optionally the logged in user
59
+ def info
60
+ # TODO: Should merge for new version IMO, general, services, user_account
61
+ json_get(JDC::INFO_PATH)
62
+ end
63
+
64
+ def raw_info
65
+ http_get(JDC::INFO_PATH)
66
+ end
67
+
68
+ # Global listing of services that are available on the target system
69
+ def services_info
70
+ json_get(path(JDC::GLOBAL_SERVICES_PATH))
71
+ end
72
+
73
+ def runtimes_info
74
+ json_get(path(JDC::GLOBAL_RUNTIMES_PATH))
75
+ end
76
+
77
+ ######################################################
78
+ # Apps
79
+ ######################################################
80
+
81
+ def apps
82
+ json_get(JDC::APPS_PATH)
83
+ end
84
+
85
+ def create_app(name, manifest={})
86
+ app = manifest.dup
87
+ app[:name] = name
88
+ app[:instances] ||= 1
89
+ json_post(JDC::APPS_PATH, app)
90
+ end
91
+
92
+ def update_app(name, manifest)
93
+ json_put(path(JDC::APPS_PATH, name), manifest)
94
+ end
95
+
96
+ def upload_app(name, zipfile, resource_manifest=nil)
97
+ #FIXME, manifest should be allowed to be null, here for compatability with old cc's
98
+ resource_manifest ||= []
99
+ upload_data = {:_method => 'put'}
100
+ if zipfile
101
+ if zipfile.is_a? File
102
+ file = zipfile
103
+ else
104
+ file = File.new(zipfile, 'rb')
105
+ end
106
+ upload_data[:application] = file
107
+ end
108
+ upload_data[:resources] = resource_manifest.to_json if resource_manifest
109
+ http_post(path(JDC::APPS_PATH, name, "application"), upload_data, 'multipart/form-data')
110
+ rescue RestClient::ServerBrokeConnection
111
+ retry
112
+ end
113
+
114
+ def delete_app(name)
115
+ http_delete(path(JDC::APPS_PATH, name))
116
+ end
117
+
118
+ def app_info(name)
119
+ json_get(path(JDC::APPS_PATH, name))
120
+ end
121
+
122
+ def app_update_info(name)
123
+ json_get(path(JDC::APPS_PATH, name, "update"))
124
+ end
125
+
126
+ def app_stats(name)
127
+ stats_raw = json_get(path(JDC::APPS_PATH, name, "stats"))
128
+ stats = []
129
+ stats_raw.each_pair do |k, entry|
130
+ # Skip entries with no stats
131
+ next unless entry[:stats]
132
+ entry[:instance] = k.to_s.to_i
133
+ entry[:state] = entry[:state].to_sym if entry[:state]
134
+ stats << entry
135
+ end
136
+ stats.sort { |a,b| a[:instance] - b[:instance] }
137
+ end
138
+
139
+ def app_instances(name)
140
+ json_get(path(JDC::APPS_PATH, name, "instances"))
141
+ end
142
+
143
+ def app_crashes(name)
144
+ json_get(path(JDC::APPS_PATH, name, "crashes"))
145
+ end
146
+
147
+ # List the directory or download the actual file indicated by
148
+ # the path.
149
+ def app_files(name, path, instance='0')
150
+ path = path.gsub('//', '/')
151
+ url = path(JDC::APPS_PATH, name, "instances", instance, "files", path)
152
+ _, body, headers = http_get(url)
153
+ body
154
+ end
155
+
156
+ ######################################################
157
+ # Services
158
+ ######################################################
159
+
160
+ # listing of services that are available in the system
161
+ def services
162
+ json_get(JDC::SERVICES_PATH)
163
+ end
164
+
165
+ def create_service(service, name)
166
+ services = services_info
167
+ services ||= []
168
+ service_hash = nil
169
+
170
+ service = service.to_s
171
+
172
+ # FIXME!
173
+ services.each do |service_type, value|
174
+ value.each do |vendor, version|
175
+ version.each do |version_str, service_descr|
176
+ if service == service_descr[:vendor]
177
+ service_hash = {
178
+ :type => service_descr[:type], :tier => 'free',
179
+ :vendor => service, :version => version_str
180
+ }
181
+ break
182
+ end
183
+ end
184
+ end
185
+ end
186
+
187
+ raise TargetError, "Service [#{service}] is not a valid service choice" unless service_hash
188
+ service_hash[:name] = name
189
+ json_post(path(JDC::SERVICES_PATH), service_hash)
190
+ end
191
+
192
+ def delete_service(name)
193
+ svcs = services || []
194
+ names = svcs.collect { |s| s[:name] }
195
+ raise TargetError, "Service [#{name}] not a valid service" unless names.include? name
196
+ http_delete(path(JDC::SERVICES_PATH, name))
197
+ end
198
+
199
+ def bind_service(service, appname)
200
+ app = app_info(appname)
201
+ services = app[:services] || []
202
+ app[:services] = services << service
203
+ update_app(appname, app)
204
+ end
205
+
206
+ def unbind_service(service, appname)
207
+ app = app_info(appname)
208
+ services = app[:services] || []
209
+ services.delete(service)
210
+ app[:services] = services
211
+ update_app(appname, app)
212
+ end
213
+
214
+ ######################################################
215
+ # Resources
216
+ ######################################################
217
+
218
+ # Send in a resources manifest array to the system to have
219
+ # it check what is needed to actually send. Returns array
220
+ # indicating what is needed. This returned manifest should be
221
+ # sent in with the upload if resources were removed.
222
+ # E.g. [{:sha1 => xxx, :size => xxx, :fn => filename}]
223
+ def check_resources(resources)
224
+ status, body, headers = json_post(JDC::RESOURCES_PATH, resources)
225
+ json_parse(body)
226
+ end
227
+
228
+ ######################################################
229
+ # Validation Helpers
230
+ ######################################################
231
+
232
+ # Checks that the target is valid
233
+ def target_valid?
234
+ return false unless descr = info
235
+ return false unless descr[:name]
236
+ return false unless descr[:build]
237
+ return false unless descr[:version]
238
+ return false unless descr[:support]
239
+ true
240
+ rescue
241
+ false
242
+ end
243
+
244
+ ######################################################
245
+ # User login/password
246
+ ######################################################
247
+
248
+ # login and return an auth_token
249
+ # Auth token can be retained and used in creating
250
+ # new clients, avoiding login.
251
+ def login(user, password)
252
+ status, body, headers = json_post(path(JDC::USERS_PATH, user, "tokens"), {:password => password})
253
+ response_info = json_parse(body)
254
+ if response_info
255
+ @user = user
256
+ @auth_token = response_info[:token]
257
+ end
258
+ end
259
+
260
+ # sets the password for the current logged user
261
+ def change_password(new_password)
262
+ user_info = json_get(path(JDC::USERS_PATH, @user))
263
+ if user_info
264
+ user_info[:password] = new_password
265
+ json_put(path(JDC::USERS_PATH, @user), user_info)
266
+ end
267
+ end
268
+
269
+ ######################################################
270
+ # System administration
271
+ ######################################################
272
+
273
+ def proxy=(proxy)
274
+ @proxy = proxy
275
+ end
276
+
277
+ def proxy_for(proxy)
278
+ @proxy = proxy
279
+ end
280
+
281
+ def users
282
+ json_get(JDC::USERS_PATH)
283
+ end
284
+
285
+ def add_user(user_email, password)
286
+ json_post(JDC::USERS_PATH, { :email => user_email, :password => password })
287
+ end
288
+
289
+ def delete_user(user_email)
290
+ http_delete(path(JDC::USERS_PATH, user_email))
291
+ end
292
+
293
+ ######################################################
294
+
295
+ def self.path(*path)
296
+ path.flatten.collect { |x|
297
+ URI.encode x.to_s, Regexp.new("[^#{URI::PATTERN::UNRESERVED}]")
298
+ }.join("/")
299
+ end
300
+
301
+ private
302
+
303
+ def path(*args, &blk)
304
+ self.class.path(*args, &blk)
305
+ end
306
+
307
+ def json_get(url)
308
+ status, body, headers = http_get(url, 'application/json')
309
+ json_parse(body)
310
+ rescue JSON::ParserError
311
+ raise BadResponse, "Can't parse response into JSON", body
312
+ end
313
+
314
+ def json_post(url, payload)
315
+ http_post(url, payload.to_json, 'application/json')
316
+ end
317
+
318
+ def json_put(url, payload)
319
+ http_put(url, payload.to_json, 'application/json')
320
+ end
321
+
322
+ def json_parse(str)
323
+ if str
324
+ JSON.parse(str, :symbolize_names => true)
325
+ end
326
+ end
327
+
328
+ require 'rest_client'
329
+
330
+ # HTTP helpers
331
+
332
+ def http_get(path, content_type=nil)
333
+ request(:get, path, content_type)
334
+ end
335
+
336
+ def http_post(path, body, content_type=nil)
337
+ request(:post, path, content_type, body)
338
+ end
339
+
340
+ def http_put(path, body, content_type=nil)
341
+ request(:put, path, content_type, body)
342
+ end
343
+
344
+ def http_delete(path)
345
+ request(:delete, path)
346
+ end
347
+
348
+ def request(method, path, content_type = nil, payload = nil, headers = {})
349
+ headers = headers.dup
350
+ headers['AUTHORIZATION'] = access_key_id if access_key_id
351
+ headers['PROXY-USER'] = @proxy if @proxy
352
+ headers['Version'] = JDC::VERSION
353
+
354
+ #init date time -- tibelf
355
+ headers['Date'] = get_time
356
+ headers['ACCESS-KEY-ID'] = access_key_id if access_key_id
357
+ headers['Path'] = path
358
+
359
+ if content_type
360
+ if content_type == 'application/json'
361
+ headers['Content-Type'] = content_type
362
+ headers['Accept'] = content_type
363
+ else
364
+ headers['Content-Type'] = content_type
365
+ end
366
+ end
367
+
368
+ req = {
369
+ :method => method, :url => "#{@target}/#{path}",
370
+ :payload => payload, :headers => headers, :multipart => true
371
+ }
372
+
373
+ signature = generate_signature(secret_key, req)
374
+ headers['signature'] = signature
375
+
376
+ status, body, response_headers = perform_http_request(req)
377
+
378
+ if request_failed?(status)
379
+ # FIXME, old cc returned 400 on not found for file access
380
+ err = (status == 404 || status == 400) ? NotFound : TargetError
381
+ raise err, parse_error_message(status, body)
382
+ else
383
+ return status, body, response_headers
384
+ end
385
+ rescue URI::Error, SocketError, Errno::ECONNREFUSED => e
386
+ raise BadTarget, "Cannot access target (%s)" % [ e.message ]
387
+ end
388
+
389
+ def request_failed?(status)
390
+ JDC_HTTP_ERROR_CODES.detect{|error_code| status >= error_code}
391
+ end
392
+
393
+ def perform_http_request(req)
394
+ proxy_uri = URI.parse(req[:url]).find_proxy()
395
+ RestClient.proxy = proxy_uri.to_s if proxy_uri
396
+
397
+ # Setup tracing if needed
398
+ unless trace.nil?
399
+ req[:headers]['X-VCAP-Trace'] = (trace == true ? '22' : trace)
400
+ end
401
+
402
+ result = nil
403
+ RestClient::Request.execute(req) do |response, request|
404
+ result = [ response.code, response.body, response.headers ]
405
+ unless trace.nil?
406
+ puts '>>>'
407
+ puts "PROXY: #{RestClient.proxy}" if RestClient.proxy
408
+ puts "REQUEST: #{req[:method]} #{req[:url]}"
409
+ puts "RESPONSE_HEADERS:"
410
+ response.headers.each do |key, value|
411
+ puts " #{key} : #{value}"
412
+ end
413
+ puts "REQUEST_BODY: #{req[:payload]}" if req[:payload]
414
+ puts "RESPONSE: [#{response.code}]"
415
+ begin
416
+ puts JSON.pretty_generate(JSON.parse(response.body))
417
+ rescue
418
+ puts "#{response.body}"
419
+ end
420
+ puts '<<<'
421
+ end
422
+ end
423
+ result
424
+ rescue Net::HTTPBadResponse => e
425
+ raise BadTarget "Received bad HTTP response from target: #{e}"
426
+ rescue SystemCallError, RestClient::Exception => e
427
+ raise HTTPException, "HTTP exception: #{e.class}:#{e}"
428
+ end
429
+
430
+ def truncate(str, limit = 30)
431
+ etc = '...'
432
+ stripped = str.strip[0..limit]
433
+ if stripped.length > limit
434
+ stripped + etc
435
+ else
436
+ stripped
437
+ end
438
+ end
439
+
440
+ def parse_error_message(status, body)
441
+ parsed_body = json_parse(body.to_s)
442
+ if parsed_body && parsed_body[:code] && parsed_body[:description]
443
+ desc = parsed_body[:description].gsub("\"","'")
444
+ "Error #{parsed_body[:code]}: #{desc}"
445
+ else
446
+ "Error (HTTP #{status}): #{body}"
447
+ end
448
+ rescue JSON::ParserError
449
+ if body.nil? || body.empty?
450
+ "Error (#{status}): No Response Received"
451
+ else
452
+ body_out = trace ? body : truncate(body)
453
+ "Error (JSON #{status}): #{body_out}"
454
+ end
455
+ end
456
+
457
+ end
data/lib/jdc/const.rb ADDED
@@ -0,0 +1,25 @@
1
+ module JDC
2
+
3
+ # This is the internal JDC version number, and is not necessarily
4
+ # the same as the RubyGem version (JDC::Cli::VERSION).
5
+ VERSION = '0.1.1'
6
+
7
+ # Targets
8
+ DEFAULT_TARGET = 'https://api.jd-app.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
+ #Enviroment Argument
23
+ ACCESS_KEY_ID = "ACCESS_KEY_ID"
24
+ SECRET_KEY = "SECRET_KEY"
25
+ end
@@ -0,0 +1,97 @@
1
+ require 'interact'
2
+
3
+ module JDC::Micro::Switcher
4
+ class Base
5
+ include Interactive
6
+
7
+ def initialize(config)
8
+ @config = config
9
+
10
+ @vmrun = JDC::Micro::VMrun.new(config)
11
+ unless @vmrun.running?
12
+ if ask("JingDong Cloud VM is not running. Do you want to start it?", :choices => ['y', 'n']) == 'y'
13
+ display "Starting JingDong Cloud VM: ", false
14
+ @vmrun.start
15
+ say "done".green
16
+ else
17
+ err "JingDong Cloud VM needs to be running."
18
+ end
19
+ end
20
+
21
+ err "JingDong Cloud VM initial setup needs to be completed before using 'jdc micro'" unless @vmrun.ready?
22
+ end
23
+
24
+ def offline
25
+ unless @vmrun.offline?
26
+ # save online connection type so we can restore it later
27
+ @config['online_connection_type'] = @vmrun.connection_type
28
+
29
+ if (@config['online_connection_type'] != 'nat')
30
+ if ask("Reconfigure JingDong Cloud VM network to nat mode and reboot?", :choices => ['y', 'n']) == 'y'
31
+ display "Rebooting JingDong Cloud VM: ", false
32
+ @vmrun.connection_type = 'nat'
33
+ @vmrun.reset
34
+ say "done".green
35
+ else
36
+ err "Aborted"
37
+ end
38
+ end
39
+
40
+ display "Setting JingDong Cloud VM to offline mode: ", false
41
+ @vmrun.offline!
42
+ say "done".green
43
+ display "Setting host DNS server: ", false
44
+
45
+ @config['domain'] = @vmrun.domain
46
+ @config['ip'] = @vmrun.ip
47
+ set_nameserver(@config['domain'], @config['ip'])
48
+ say "done".green
49
+ else
50
+ say "JingDong Cloud VM already in offline mode".yellow
51
+ end
52
+ end
53
+
54
+ def online
55
+ if @vmrun.offline?
56
+ current_connection_type = @vmrun.connection_type
57
+ @config['online_connection_type'] ||= current_connection_type
58
+
59
+ if (@config['online_connection_type'] != current_connection_type)
60
+ # TODO handle missing connection type in saved config
61
+ question = "Reconfigure JingDong Cloud VM network to #{@config['online_connection_type']} mode and reboot?"
62
+ if ask(question, :choices => ['y', 'n']) == 'y'
63
+ display "Rebooting JingDong Cloud VM: ", false
64
+ @vmrun.connection_type = @config['online_connection_type']
65
+ @vmrun.reset
66
+ say "done".green
67
+ else
68
+ err "Aborted"
69
+ end
70
+ end
71
+
72
+ display "Unsetting host DNS server: ", false
73
+ # TODO handle missing domain and ip in saved config (look at the VM)
74
+ @config['domain'] ||= @vmrun.domain
75
+ @config['ip'] ||= @vmrun.ip
76
+ unset_nameserver(@config['domain'], @config['ip'])
77
+ say "done".green
78
+
79
+ display "Setting JingDong Cloud VM to online mode: ", false
80
+ @vmrun.online!
81
+ say "done".green
82
+ else
83
+ say "JingDong Cloud already in online mode".yellow
84
+ end
85
+ end
86
+
87
+ def status
88
+ mode = @vmrun.offline? ? 'offline' : 'online'
89
+ say "JingDong Cloud VM currently in #{mode.green} mode"
90
+ # should the VMX path be unescaped?
91
+ say "VMX Path: #{@vmrun.vmx}"
92
+ say "Domain: #{@vmrun.domain.green}"
93
+ say "IP Address: #{@vmrun.ip.green}"
94
+ end
95
+ end
96
+
97
+ end
@@ -0,0 +1,19 @@
1
+ module JDC::Micro::Switcher
2
+
3
+ class Darwin < Base
4
+ def adminrun(command)
5
+ JDC::Micro.run_command("osascript", "-e 'do shell script \"#{command}\" with administrator privileges'")
6
+ end
7
+
8
+ def set_nameserver(domain, ip)
9
+ File.open("/tmp/#{domain}", 'w') { |file| file.write("nameserver #{ip}") }
10
+ adminrun("mkdir -p /etc/resolver;mv /tmp/#{domain} /etc/resolver/")
11
+ end
12
+
13
+ def unset_nameserver(domain, ip)
14
+ err "domain missing" unless domain
15
+ adminrun("rm -f /etc/resolver/#{domain}")
16
+ end
17
+ end
18
+
19
+ end
@@ -0,0 +1,15 @@
1
+ # only used for testing
2
+ module JDC::Micro::Switcher
3
+
4
+ class Dummy < Base
5
+ def adminrun(command)
6
+ end
7
+
8
+ def set_nameserver(domain, ip)
9
+ end
10
+
11
+ def unset_nameserver(domain, ip)
12
+ end
13
+ end
14
+
15
+ end
@@ -0,0 +1,16 @@
1
+ module JDC::Micro::Switcher
2
+
3
+ class Linux < Base
4
+ def set_nameserver(domain, ip)
5
+ JDC::Micro.run_command("sudo", "sed -i'.backup' '1 i nameserver #{ip}' /etc/resolv.conf")
6
+ # lock resolv.conf so Network Manager doesn't clear out the file when offline
7
+ JDC::Micro.run_command("sudo", "chattr +i /etc/resolv.conf")
8
+ end
9
+
10
+ def unset_nameserver(domain, ip)
11
+ JDC::Micro.run_command("sudo", "chattr -i /etc/resolv.conf")
12
+ JDC::Micro.run_command("sudo", "sed -i'.backup' '/#{ip}/d' /etc/resolv.conf")
13
+ end
14
+ end
15
+
16
+ end
@@ -0,0 +1,31 @@
1
+ module JDC::Micro::Switcher
2
+
3
+ class Windows < Base
4
+ def version?
5
+ JDC::Micro.run_command("cmd", "/c ver").to_s.scan(/\d+\.\d+/).first.to_f
6
+ end
7
+
8
+ def adminrun(command, args=nil)
9
+ if version? > 5.2
10
+ require 'win32ole'
11
+ shell = WIN32OLE.new("Shell.Application")
12
+ shell.ShellExecute(command, args, nil, "runas", 0)
13
+ else
14
+ # on older version this will try to run the command, and if you don't have
15
+ # admin privilges it will tell you so and exit
16
+ JDC::Micro.run_command(command, args)
17
+ end
18
+ end
19
+
20
+ # TODO better method to figure out the interface name is to get the NAT ip and find the
21
+ # interface with the correct subnet
22
+ def set_nameserver(domain, ip)
23
+ adminrun("netsh", "interface ip set dns \"VMware Network Adapter VMnet8\" static #{ip}")
24
+ end
25
+
26
+ def unset_nameserver(domain, ip)
27
+ adminrun("netsh", "interface ip set dns \"VMware Network Adapter VMnet8\" static none")
28
+ end
29
+ end
30
+
31
+ end