PageRenderTime 51ms CodeModel.GetById 14ms RepoModel.GetById 0ms app.codeStats 0ms

/lib/vmc/client.rb

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