jdc 0.1.1

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