diff --git a/lib/fog/bin/rackspace.rb b/lib/fog/bin/rackspace.rb index 73a34d783..8e53cea96 100644 --- a/lib/fog/bin/rackspace.rb +++ b/lib/fog/bin/rackspace.rb @@ -3,6 +3,8 @@ class Rackspace < Fog::Bin def class_for(key) case key + when :auto_scale + Fog::Rackspace::AutoScale when :block_storage Fog::Rackspace::BlockStorage when :cdn @@ -31,6 +33,8 @@ class Rackspace < Fog::Bin def [](service) @@connections ||= Hash.new do |hash, key| hash[key] = case key + when :auto_scale + Fog::Rackspace::AutoScale.new when :cdn Fog::Logger.warning("Rackspace[:cdn] is not recommended, use CDN[:rackspace] for portability") Fog::CDN.new(:provider => 'Rackspace') diff --git a/lib/fog/rackspace.rb b/lib/fog/rackspace.rb index 7d5f9de91..62e9e3ca2 100644 --- a/lib/fog/rackspace.rb +++ b/lib/fog/rackspace.rb @@ -81,6 +81,7 @@ module Fog end end + service(:auto_scale, 'rackspace/auto_scale', 'AutoScale') service(:block_storage, 'rackspace/block_storage', 'BlockStorage') service(:cdn, 'rackspace/cdn', 'CDN') service(:compute, 'rackspace/compute', 'Compute') diff --git a/lib/fog/rackspace/auto_scale.rb b/lib/fog/rackspace/auto_scale.rb new file mode 100644 index 000000000..7efd83c2d --- /dev/null +++ b/lib/fog/rackspace/auto_scale.rb @@ -0,0 +1,143 @@ +require File.expand_path(File.join(File.dirname(__FILE__), '..', 'rackspace')) + +module Fog + module Rackspace + class AutoScale < Fog::Service + include Fog::Rackspace::Errors + + class ServiceError < Fog::Rackspace::Errors::ServiceError; end + class InternalServerError < Fog::Rackspace::Errors::InternalServerError; end + + class MissingArgumentException < InvalidStateException + def initialize(resource, argument) + @resource = resource + @argument = argument + end + def to_s + "This #{resource} resource requires the #{argument} argument" + end + end + + class InvalidImageStateException < InvalidStateException + def to_s + "Image should have transitioned to '#{desired_state}' not '#{current_state}'" + end + end + + class BadRequest < Fog::Rackspace::Errors::BadRequest + attr_reader :validation_errors + + def self.slurp(error) + if error && error.response + status_code = error.response.status + if error.response.body + body = Fog::JSON.decode(error.response.body) + message = "#{body['type']} - #{body['message']}" + details = error.response.body['details'] + end + end + + new_error = new(message) + new_error.set_backtrace(error.backtrace) + new_error.instance_variable_set(:@validation_errors, details) + new_error.instance_variable_set(:@status_code, status_code) + + new_error + end + end + + requires :rackspace_username, :rackspace_api_key + recognizes :rackspace_auth_url + recognizes :rackspace_region + recognizes :rackspace_auto_scale_url + + model_path 'fog/rackspace/models/auto_scale' + model :group + collection :groups + model :policy + collection :policies + model :group_config + model :launch_config + model :webhook + + request_path 'fog/rackspace/requests/auto_scale' + request :list_groups + request :create_group + request :get_group + request :delete_group + request :get_group_state + request :pause_group_state + request :resume_group_state + + request :get_group_config + request :update_group_config + request :get_launch_config + request :replace_launch_config + + request :get_policies + request :create_policy + request :get_policy + request :update_policy + request :delete_policy + request :execute_policy + + request :execute_anonymous_webhook + + request :get_webhook + request :update_webhook + request :delete_webhook + + class Mock < Fog::Rackspace::Service + def initialize(options) + @rackspace_api_key = options[:rackspace_api_key] + end + + def request(params) + Fog::Mock.not_implemented + end + end + + + class Real < Fog::Rackspace::Service + + def initialize(options = {}) + @options = options + @options[:connection_options] ||= {} + @options[:persistent] ||= false + + authenticate + + @connection = Fog::Connection.new(endpoint_uri.to_s, @options[:persistent], @options[:connection_options]) + end + + def request(params, parse_json = true, &block) + super(params, parse_json, &block) + rescue Excon::Errors::NotFound => error + raise NotFound.slurp(error, region) + rescue Excon::Errors::BadRequest => error + raise BadRequest.slurp error + rescue Excon::Errors::InternalServerError => error + raise InternalServerError.slurp error + rescue Excon::Errors::HTTPStatusError => error + raise ServiceError.slurp error + end + + def endpoint_uri(service_endpoint_url=nil) + @uri = super(@options[:rackspace_auto_scale_url], :rackspace_auto_scale_url) + end + + def authenticate(options={}) + super(select_options([:rackspace_username, :rackspace_api_key, :rackspace_auth_url, :connection_options])) + end + + def service_name + :autoscale + end + + def region + @options[:rackspace_region] + end + end + end + end +end \ No newline at end of file diff --git a/lib/fog/rackspace/models/auto_scale/config.rb b/lib/fog/rackspace/models/auto_scale/config.rb new file mode 100644 index 000000000..fc6f5f15b --- /dev/null +++ b/lib/fog/rackspace/models/auto_scale/config.rb @@ -0,0 +1,17 @@ +require 'fog/core/model' + +module Fog + module Rackspace + class AutoScale + class Config < Fog::Model + + def pause + end + + def resume + end + + end + end + end +end \ No newline at end of file diff --git a/lib/fog/rackspace/models/auto_scale/group.rb b/lib/fog/rackspace/models/auto_scale/group.rb new file mode 100644 index 000000000..4f987da1d --- /dev/null +++ b/lib/fog/rackspace/models/auto_scale/group.rb @@ -0,0 +1,73 @@ +require 'fog/core/model' + +module Fog + module Rackspace + class AutoScale + class Group < Fog::Model + + identity :id + + attribute :links + + attribute :group_config + + attribute :launch_config + + attribute :policies + + def initialize(attributes={}) + @service = attributes[:service] + super + end + + def create(options) + requires :launch_config, :group_config, :policies + + data = service.create_group(launch_config, group_config, policies) + merge_attributes(data.body['group']) + true + end + + def destroy + requires :identity + service.delete_server(identity) + true + end + + def group_config + @group_config ||= begin + Fog::Rackspace::AutoScale::GroupConfig.new({ + :service => service, + :group => self + }) + end + end + + def launch_config + @launch_config ||= begin + Fog::Rackspace::AutoScale::LaunchConfig.new({ + :service => service, + :group => self + }) + end + end + + def policies + @policies ||= begin + Fog::Rackspace::Autoscale::Policies.new({ + :service => service, + :group => self + }) + end + end + + def state + requires :identity + data = service.get_group_state(identity) + merge_attributes(data.body['group']) + end + + end + end + end +end \ No newline at end of file diff --git a/lib/fog/rackspace/models/auto_scale/group_config.rb b/lib/fog/rackspace/models/auto_scale/group_config.rb new file mode 100644 index 000000000..12a194f5d --- /dev/null +++ b/lib/fog/rackspace/models/auto_scale/group_config.rb @@ -0,0 +1,34 @@ +require 'fog/core/model' + +module Fog + module Rackspace + class AutoScale + class GroupConfig < Fog::AutoScale::Rackspace::Config + + identity :id + + attribute :name + attribute :cooldown + attribute :min_entities + attribute :max_entities + attribute :metadata + + def update + requires :identity + options = { + 'name' => name, + 'cooldown' => cooldown, + 'min_entities' => min_entities, + 'max_entities' => max_entities, + 'metadata' => metadata + } + + data = service.update_group_config(identity, options) + merge_attributes(data.body) + true + end + + end + end + end +end \ No newline at end of file diff --git a/lib/fog/rackspace/models/auto_scale/groups.rb b/lib/fog/rackspace/models/auto_scale/groups.rb new file mode 100644 index 000000000..a7ea34a29 --- /dev/null +++ b/lib/fog/rackspace/models/auto_scale/groups.rb @@ -0,0 +1,25 @@ +require 'fog/core/collection' +require 'fog/rackspace/models/auto_scale/group' + +module Fog + module Rackspace + class AutoScale + class Groups < Fog::Collection + + model Fog::Rackspace::AutoScale::Group + + def all + data = service.list_groups.body['groups'] + load(data) + end + + def get(group_id) + data = service.get_group(group_id).body['group'] + new(data) + rescue Fog::Rackspace::AutoScale::NotFound + nil + end + end + end + end +end diff --git a/lib/fog/rackspace/models/auto_scale/launch_config.rb b/lib/fog/rackspace/models/auto_scale/launch_config.rb new file mode 100644 index 000000000..55bcb00c9 --- /dev/null +++ b/lib/fog/rackspace/models/auto_scale/launch_config.rb @@ -0,0 +1,28 @@ +require 'fog/core/model' + +module Fog + module Rackspace + class AutoScale + class LaunchConfig < Fog::AutoScale::Rackspace::Config + + identity :id + + attribute :type + attribute :args + + def update + requires :identity + options = { + 'type' => type, + 'args' => args + } + + data = service.update_launch_config(identity, options) + merge_attributes(data.body['launchConfiguration']) + true + end + + end + end + end +end \ No newline at end of file diff --git a/lib/fog/rackspace/models/auto_scale/policies.rb b/lib/fog/rackspace/models/auto_scale/policies.rb new file mode 100644 index 000000000..ddea70e64 --- /dev/null +++ b/lib/fog/rackspace/models/auto_scale/policies.rb @@ -0,0 +1,25 @@ +require 'fog/core/collection' +require 'fog/rackspace/models/auto_scale/policy' + +module Fog + module Rackspace + class AutoScale + class Policies < Fog::Collection + + model Fog::Rackspace::AutoScale::Policy + + def all + data = service.list_policies.body['policies'] + load(data) + end + + def get(policy_id) + data = service.get_policy(policy_id).body['policy'] + new(data) + rescue Fog::Rackspace::AutoScale::NotFound + nil + end + end + end + end +end \ No newline at end of file diff --git a/lib/fog/rackspace/models/auto_scale/policy.rb b/lib/fog/rackspace/models/auto_scale/policy.rb new file mode 100644 index 000000000..dd45bb0e1 --- /dev/null +++ b/lib/fog/rackspace/models/auto_scale/policy.rb @@ -0,0 +1,98 @@ +require 'fog/core/model' + +module Fog + module Rackspace + class AutoScale + class Policy < Fog::Model + + identity :id + + attribute :group_id + + attribute :links + attribute :name + + # integer + attribute :change + attribute :changePercent + + # integer + attribute :cooldown + + # webhook|schedule|cloud_monitoring + attribute :type + + # hash depending on the type chosen + # - "cron": "23 * * * *" + # - "at": "2013-06-05T03:12Z" + # - "check": { + # "label": "Website check 1", + # "type": "remote.http", + # "details": { + # "url": "http://www.example.com", + # "method": "GET" + # }, + # "monitoring_zones_poll": [ + # "mzA" + # ], + # "timeout": 30, + # "period": 100, + # "target_alias": "default" + # }, + # "alarm_criteria": { + # "criteria": "if (metric[\"duration\"] >= 2) { return new AlarmStatus(OK); } return new AlarmStatus(CRITICAL);" + # } + attribute :args + + attribute :desiredCapacity + + def check_options(options) + if options[:type] == 'schedule' + args = options['args'] + raise MissingArgumentException(self.name, "cron OR at") if args['cron'].nil? && args['at'].nil? + end + true + end + + def create(options) + requires :name, :type, :cooldown + + check_options + + data = service.create_policy(group_id, options) + merge_attributes(data.body['group']) + true + end + + def update + requires :identity + + options = { + 'name' => name, + 'change' => change, + 'changePercent' => changePercent, + 'cooldown' => cooldown, + 'type' => type, + 'args' => args, + 'desiredCapacity' => desiredCapacity + } + + data = service.update_policy(identity, options) + merge_attributes(data.body) + true + end + + def destroy + requires :identity + service.delete_policy(identity) + end + + def execute + requires :identity + service.execute_policy(identity) + end + + end + end + end +end \ No newline at end of file diff --git a/lib/fog/rackspace/models/auto_scale/webhook.rb b/lib/fog/rackspace/models/auto_scale/webhook.rb new file mode 100644 index 000000000..5b91892df --- /dev/null +++ b/lib/fog/rackspace/models/auto_scale/webhook.rb @@ -0,0 +1,34 @@ +require 'fog/core/model' + +module Fog + module Rackspace + class AutoScale + class Webhook < Fog::Model + + identity :id + + attribute :name + attribute :metadata + attribute :links + + def update + required :identity + + options = { + 'name' => name, + 'metadata' => metadata + } + + data = service.update_webhook(identity, options) + merge_attribute(data.body) + end + + def destroy + required :identity + service.delete_webhook(identity) + end + + end + end + end +end \ No newline at end of file diff --git a/lib/fog/rackspace/requests/auto_scale/create_group.rb b/lib/fog/rackspace/requests/auto_scale/create_group.rb new file mode 100644 index 000000000..54fb1a9dd --- /dev/null +++ b/lib/fog/rackspace/requests/auto_scale/create_group.rb @@ -0,0 +1,40 @@ +module Fog + module Rackspace + class AutoScale + class Real + + def create_group(launch_config, group_config, policies) + + body['launchConfiguration'] = { + 'args' => launch_config.args, + 'type' => launch_config.type + } + + body['groupConfiguration'] = { + 'name' => group_config.name, + 'cooldown' => group_config.cooldown, + 'maxEntities' => group_config.max_entities, + 'minEntities' => group_config.min_entities, + 'metadata' => group_config.metadata + } + + body['scalingPolicies'] = policies.collect { |p| p.to_a } + + request( + :expects => [201], + :method => 'POST', + :path => 'groups', + :body => Fog::JSON.encode(body) + ) + end + end + + class Mock + def create_group + Fog::Mock.not_implemented + + end + end + end + end +end diff --git a/lib/fog/rackspace/requests/auto_scale/delete_group.rb b/lib/fog/rackspace/requests/auto_scale/delete_group.rb new file mode 100644 index 000000000..698f815f7 --- /dev/null +++ b/lib/fog/rackspace/requests/auto_scale/delete_group.rb @@ -0,0 +1,22 @@ +module Fog + module Rackspace + class AutoScale + class Real + + def delete_group(group_id) + request( + :expects => [204], + :method => 'DELETE', + :path => "groups/#{group_id}" + ) + end + end + + class Mock + def delete_group(group_id) + Fog::Mock.not_implemented + end + end + end + end +end diff --git a/lib/fog/rackspace/requests/auto_scale/get_config.rb b/lib/fog/rackspace/requests/auto_scale/get_config.rb new file mode 100644 index 000000000..ca9be79bc --- /dev/null +++ b/lib/fog/rackspace/requests/auto_scale/get_config.rb @@ -0,0 +1,24 @@ +module Fog + module Rackspace + class AutoScale + + class Real + + def get_config(group_id) + request( + :expects => [200], + :method => 'GET', + :path => "groups/#{group_id}/config", + ) + end + end + + class Mock + def get_config(group_id) + Fog::Mock.not_implemented + end + end + + end + end +end \ No newline at end of file diff --git a/lib/fog/rackspace/requests/auto_scale/get_group.rb b/lib/fog/rackspace/requests/auto_scale/get_group.rb new file mode 100644 index 000000000..7faae3f90 --- /dev/null +++ b/lib/fog/rackspace/requests/auto_scale/get_group.rb @@ -0,0 +1,24 @@ +module Fog + module Rackspace + class AutoScale + + class Real + + def get_group(group_id) + request( + :expects => [200], + :method => 'GET', + :path => "groups/#{group_id}", + ) + end + end + + class Mock + def get_group(group_id) + Fog::Mock.not_implemented + end + end + + end + end +end \ No newline at end of file diff --git a/lib/fog/rackspace/requests/auto_scale/get_group_state.rb b/lib/fog/rackspace/requests/auto_scale/get_group_state.rb new file mode 100644 index 000000000..66ca58b8b --- /dev/null +++ b/lib/fog/rackspace/requests/auto_scale/get_group_state.rb @@ -0,0 +1,24 @@ +module Fog + module Rackspace + class AutoScale + + class Real + + def get_group_state(group_id) + request( + :expects => [200], + :method => 'GET', + :path => "groups/#{group_id}/state", + ) + end + end + + class Mock + def get_group_state(group_id) + Fog::Mock.not_implemented + end + end + + end + end +end \ No newline at end of file diff --git a/lib/fog/rackspace/requests/auto_scale/list_groups.rb b/lib/fog/rackspace/requests/auto_scale/list_groups.rb new file mode 100644 index 000000000..806a4ef72 --- /dev/null +++ b/lib/fog/rackspace/requests/auto_scale/list_groups.rb @@ -0,0 +1,35 @@ +module Fog + module Rackspace + class AutoScale + class Real + + # Retrieves a list of images + # @return [Excon::Response] response: + # * body [Hash]: + # * images [Array]: + # * [Hash]: + # * id [String] - flavor id + # * links [Array] - image links + # * name [String] - image name + # @raise [Fog::Compute::RackspaceV2::NotFound] - HTTP 404 + # @raise [Fog::Compute::RackspaceV2::BadRequest] - HTTP 400 + # @raise [Fog::Compute::RackspaceV2::InternalServerError] - HTTP 500 + # @raise [Fog::Compute::RackspaceV2::ServiceError] + # @see http://docs.rackspace.com/servers/api/v2/cs-devguide/content/List_Flavors-d1e4188.html + def list_groups + request( + :expects => [200], + :method => 'GET', + :path => 'groups' + ) + end + end + + class Mock + def list_groups + Fog::Mock.not_implemented + end + end + end + end +end diff --git a/lib/fog/rackspace/requests/auto_scale/update_config.rb b/lib/fog/rackspace/requests/auto_scale/update_config.rb new file mode 100644 index 000000000..6cdf577bd --- /dev/null +++ b/lib/fog/rackspace/requests/auto_scale/update_config.rb @@ -0,0 +1,37 @@ +module Fog + module Rackspace + class AutoScale + + class Real + + def update_config(group_id) + + h = { + "name" => "workers", + "cooldown" => 60, + "minEntities" => 0, + "maxEntities" => 0, + "metadata" => { + "firstkey" => "this is a string", + "secondkey" => "1" + } + } + + request( + :expects => [204], + :method => 'PUT', + :path => "groups/#{group_id}/config", + :body => Fog::JSON.encode(h) + ) + end + end + + class Mock + def update_config(group_id) + Fog::Mock.not_implemented + end + end + + end + end +end \ No newline at end of file diff --git a/lib/fog/rackspace/service.rb b/lib/fog/rackspace/service.rb index 8649cf273..a60d7dfaa 100644 --- a/lib/fog/rackspace/service.rb +++ b/lib/fog/rackspace/service.rb @@ -116,6 +116,11 @@ module Fog @auth_token || @identity_service.auth_token end + def select_options(keys) + return nil unless @options && keys + @options.select {|k,v| keys.include?(k)} + end + end end end diff --git a/tests/rackspace/auto_scale_tests.rb b/tests/rackspace/auto_scale_tests.rb new file mode 100644 index 000000000..6b41614a7 --- /dev/null +++ b/tests/rackspace/auto_scale_tests.rb @@ -0,0 +1,84 @@ +Shindo.tests('Fog::Rackspace::AutoScale', ['rackspace']) do + + def assert_method(url, method) + @service.instance_variable_set "@rackspace_auth_url", url + returns(method) { @service.send :authentication_method } + end + + tests('#authentication_method') do + @service = Fog::Rackspace::AutoScale.new :rackspace_region => :dfw + + assert_method nil, :authenticate_v2 + + assert_method 'auth.api.rackspacecloud.com', :authenticate_v1 # chef's default auth endpoint + + assert_method 'https://identity.api.rackspacecloud.com', :authenticate_v1 + assert_method 'https://identity.api.rackspacecloud.com/v1', :authenticate_v1 + assert_method 'https://identity.api.rackspacecloud.com/v1.1', :authenticate_v1 + assert_method 'https://identity.api.rackspacecloud.com/v2.0', :authenticate_v2 + + assert_method 'https://lon.identity.api.rackspacecloud.com', :authenticate_v1 + assert_method 'https://lon.identity.api.rackspacecloud.com/v1', :authenticate_v1 + assert_method 'https://lon.identity.api.rackspacecloud.com/v1.1', :authenticate_v1 + assert_method 'https://lon.identity.api.rackspacecloud.com/v2.0', :authenticate_v2 + end + + + tests('current authentation') do + pending if Fog.mocking? + + tests('variables populated').succeeds do + @service = Fog::Rackspace::AutoScale.new :rackspace_auth_url => 'https://identity.api.rackspacecloud.com/v2.0', :connection_options => {:ssl_verify_peer => true}, :rackspace_region => :dfw + returns(true, "auth token populated") { !@service.send(:auth_token).nil? } + returns(false, "path populated") { @service.instance_variable_get("@uri").host.nil? } + identity_service = @service.instance_variable_get("@identity_service") + returns(false, "identity service was used") { identity_service.nil? } + returns(true, "connection_options are passed") { identity_service.instance_variable_get("@connection_options").has_key?(:ssl_verify_peer) } + @service.list_groups + end + tests('dfw region').succeeds do + @service = Fog::Rackspace::AutoScale.new :rackspace_auth_url => 'https://identity.api.rackspacecloud.com/v2.0', :rackspace_region => :dfw + returns(true, "auth token populated") { !@service.send(:auth_token).nil? } + returns(true) { (@service.instance_variable_get("@uri").host =~ /dfw/) != nil } + @service.list_groups + end + tests('ord region').succeeds do + @service = Fog::Rackspace::AutoScale.new :rackspace_auth_url => 'https://identity.api.rackspacecloud.com/v2.0', :rackspace_region => :ord + returns(true, "auth token populated") { !@service.send(:auth_token).nil? } + returns(true) { (@service.instance_variable_get("@uri").host =~ /ord/) != nil } + @service.list_groups + end + tests('custom endpoint') do + @service = Fog::Rackspace::AutoScale.new :rackspace_auth_url => 'https://identity.api.rackspacecloud.com/v2.0', + :rackspace_auto_scale_url => 'https://my-custom-endpoint.com' + returns(true, "auth token populated") { !@service.send(:auth_token).nil? } + returns(true, "uses custom endpoint") { (@service.instance_variable_get("@uri").host =~ /my-custom-endpoint\.com/) != nil } + end + end + + tests('default auth') do + pending if Fog.mocking? + + tests('specify region').succeeds do + @service = Fog::Rackspace::AutoScale.new :rackspace_region => :ord + returns(true, "auth token populated") { !@service.send(:auth_token).nil? } + returns(true) { (@service.instance_variable_get("@uri").host =~ /ord/ ) != nil } + @service.list_groups + end + tests('custom endpoint') do + @service = Fog::Rackspace::AutoScale.new :rackspace_auto_scale_url => 'https://my-custom-endpoint.com' + returns(true, "auth token populated") { !@service.send(:auth_token).nil? } + returns(true, "uses custom endpoint") { (@service.instance_variable_get("@uri").host =~ /my-custom-endpoint\.com/) != nil } + end + end + + tests('reauthentication') do + pending if Fog.mocking? + + @service = Fog::Rackspace::AutoScale.new :rackspace_region => :ord + returns(true, "auth token populated") { !@service.send(:auth_token).nil? } + @service.instance_variable_set("@auth_token", "bad_token") + returns(true) { [200, 203].include? @service.list_groups.status } + end + +end \ No newline at end of file