require "rest_client" require "json" require "cgi" module Shelly class Client class APIException < Exception attr_reader :status_code, :body, :request_id def initialize(body = {}, status_code = nil, request_id = nil) @status_code = status_code @body = body @request_id = request_id end def [](key) body[key.to_s] end end class UnauthorizedException < APIException; end class ForbiddenException < APIException; end class ConflictException < APIException; end class GemVersionException < APIException; end class GatewayTimeoutException < APIException; end class LockedException < APIException; end class ValidationException < APIException def errors self[:errors] end def each_error errors.each do |field, message| yield [field.gsub('_',' ').capitalize, message].join(" ") end end end class NotFoundException < APIException def resource self[:resource].to_sym end def id self[:id] end end attr_reader :email, :password def initialize(email = nil, password = nil) @email = email @password = password end def api_url ENV["SHELLY_URL"] || "https://api.shellycloud.com/apiv2" end def shellyapp_url get("/shellyapp")["url"] end def register_user(email, password, ssh_key) post("/users", :user => {:email => email, :password => password, :ssh_key => ssh_key}) end def token get("/token") end def app_configs(cloud) get("/apps/#{cloud}/configs") end def app_config(cloud, path) get("/apps/#{cloud}/configs/#{CGI.escape(path)}") end def app_create_config(cloud, path, content) post("/apps/#{cloud}/configs", :config => {:path => path, :content => content}) end def app_update_config(cloud, path, content) put("/apps/#{cloud}/configs/#{CGI.escape(path)}", :config => {:content => content}) end def app_delete_config(cloud, path) delete("/apps/#{cloud}/configs/#{CGI.escape(path)}") end def send_invitation(name, email, owner = false) post("/organizations/#{name}/memberships", :email => email, :owner => owner) end def delete_member(name, email) delete("/organizations/#{name}/memberships/#{email}") end def create_app(attributes) organization = attributes.delete(:organization_name) post("/apps", :app => attributes, :organization_name => organization) end def delete_app(code_name) delete("/apps/#{code_name}") end def add_ssh_key(ssh_key) post("/ssh_keys", :ssh_key => ssh_key) end def logout(ssh_key) delete("/ssh_keys", :ssh_key => ssh_key) end def start_cloud(cloud) put("/apps/#{cloud}/start") end def stop_cloud(cloud) put("/apps/#{cloud}/stop") end def apps get("/apps") end def app(code_name) get("/apps/#{code_name}") end def organizations get("/organizations") end def organization(name) get("/organizations/#{name}") end def statistics(code_name) get("/apps/#{code_name}/statistics") end def command(cloud, body, type) post("/apps/#{cloud}/command", {:body => body, :type => type}) end def console(code_name) get("/apps/#{code_name}/console") end def deploy_logs(cloud) get("/apps/#{cloud}/deployment_logs") end def deploy_log(cloud, log) get("/apps/#{cloud}/deployment_logs/#{log}") end def application_logs(cloud, options = {}) get("/apps/#{cloud}/application_logs#{query(options)}") end def application_logs_tail(cloud) url = get("/apps/#{cloud}/application_logs/tail")["url"] options = { :url => url, :method => :get, :timeout => 60 * 60 * 24, :block_response => Proc.new { |r| r.read_body { |c| yield(c) } } }.merge(http_basic_auth_options) RestClient::Request.execute(options) end def database_backups(code_name) get("/apps/#{code_name}/database_backups") end def database_backup(code_name, handler) get("/apps/#{code_name}/database_backups/#{handler}") end def restore_backup(code_name, filename) put("/apps/#{code_name}/database_backups/#{filename}/restore") end def request_backup(code_name, kind = nil) post("/apps/#{code_name}/database_backups", :kind => kind) end def download_backup_url(code_name, filename) get("/apps/#{code_name}/database_backups/#{filename}/download_url")["url"] end def members(name) get("/organizations/#{name}/memberships") end def redeploy(cloud) post("/apps/#{cloud}/deploys") end def query(options = {}) "?" + options.map { |k, v| URI.escape(k.to_s) + "=" + URI.escape(v.to_s) }.join("&") end def post(path, params = {}) request(path, :post, params) end def put(path, params = {}) request(path, :put, params) end def get(path, params = {}) request(path, :get, params) end def delete(path, params = {}) request(path, :delete, params) end def download_backup(cloud, filename, progress_callback = nil) File.open(filename, "wb") do |out| process_response = lambda do |response| response.read_body do |chunk| out.write(chunk) progress_callback.call(chunk.size) if progress_callback end end options = { :url => download_backup_url(cloud, filename), :method => :get, :block_response => process_response, :headers => {:accept => "application/x-gzip"} }.merge(http_basic_auth_options) RestClient::Request.execute(options) end end def request(path, method, params = {}) options = request_parameters(path, method, params) RestClient::Request.execute(options) do |response, request| process_response(response) end end def headers {:accept => :json, :content_type => :json, "shelly-version" => Shelly::VERSION} end def http_basic_auth_options @email ? {:user => @email, :password => @password} : {} end def request_parameters(path, method, params = {}) {:method => method, :url => "#{api_url}#{path}", :headers => headers, :payload => params.to_json }.merge(http_basic_auth_options) end def process_response(response) body = JSON.parse(response.body) rescue JSON::ParserError && {} code = response.code if (400..599).include?(code) exception_class = case response.code when 401; UnauthorizedException when 403; ForbiddenException when 404; NotFoundException when 409; ConflictException when 412; GemVersionException when 422; ValidationException when 423; LockedException when 504; GatewayTimeoutException else; APIException end raise exception_class.new(body, code, response.headers[:x_request_id]) end response.return! body end end end