diff --git a/lib/fog/bin/openstack.rb b/lib/fog/bin/openstack.rb index 8780b4160..8b837c2ab 100644 --- a/lib/fog/bin/openstack.rb +++ b/lib/fog/bin/openstack.rb @@ -15,6 +15,8 @@ class OpenStack < Fog::Bin Fog::Storage::OpenStack when :volume Fog::Volume::OpenStack + when :metering + Fog::Metering::OpenStack else raise ArgumentError, "Unrecognized service: #{key}" end @@ -41,6 +43,9 @@ class OpenStack < Fog::Bin when :volume Fog::Logger.warning("OpenStack[:volume] is not recommended, use Volume[:openstack] for portability") Fog::Volume.new(:provider => 'OpenStack') + when :metering + Fog::Logger.warning("OpenStack[:metering] is not recommended, use Metering[:openstack] for portability") + Fog::Metering.new(:provider => 'OpenStack') else raise ArgumentError, "Unrecognized service: #{key.inspect}" end diff --git a/lib/fog/metering.rb b/lib/fog/metering.rb new file mode 100644 index 000000000..3dc6244c6 --- /dev/null +++ b/lib/fog/metering.rb @@ -0,0 +1,25 @@ +module Fog + module Metering + + def self.[](provider) + self.new(:provider => provider) + end + + def self.new(attributes) + attributes = attributes.dup # Prevent delete from having side effects + provider = attributes.delete(:provider).to_s.downcase.to_sym + if self.providers.include?(provider) + require "fog/#{provider}/metering" + return Fog::Metering.const_get(Fog.providers[provider]).new(attributes) + end + + raise ArgumentError.new("#{provider} has no identity service") + end + + def self.providers + Fog.services[:metering] + end + + end +end + diff --git a/lib/fog/openstack.rb b/lib/fog/openstack.rb index 774ebc8bd..f5c5d295e 100644 --- a/lib/fog/openstack.rb +++ b/lib/fog/openstack.rb @@ -47,6 +47,7 @@ module Fog service(:network, 'openstack/network', 'Network') service(:storage, 'openstack/storage', 'Storage') service(:volume, 'openstack/volume', 'Volume') + service(:metering, 'openstack/metering', 'Metering') # legacy v1.0 style auth def self.authenticate_v1(options, connection_options = {}) diff --git a/lib/fog/openstack/metering.rb b/lib/fog/openstack/metering.rb new file mode 100644 index 000000000..e46c790dc --- /dev/null +++ b/lib/fog/openstack/metering.rb @@ -0,0 +1,215 @@ +require 'fog/metering' +require 'fog/openstack' + +module Fog + module Metering + class OpenStack < Fog::Service + + requires :openstack_auth_url + recognizes :openstack_auth_token, :openstack_management_url, :persistent, + :openstack_service_type, :openstack_service_name, :openstack_tenant, + :openstack_api_key, :openstack_username, + :current_user, :current_tenant, + :openstack_endpoint_type + + model_path 'fog/openstack/models/metering' + + model :resource + collection :resources + + + request_path 'fog/openstack/requests/metering' + + # Metering + request :get_resource + request :get_samples + request :get_statistics + request :list_meters + request :list_resources + + + class Mock + def self.data + @data ||= Hash.new do |hash, key| + hash[key] = { + :users => {}, + :tenants => {} + } + end + end + + def self.reset + @data = nil + end + + def initialize(options={}) + require 'multi_json' + @openstack_username = options[:openstack_username] + @openstack_tenant = options[:openstack_tenant] + @openstack_auth_uri = URI.parse(options[:openstack_auth_url]) + + @auth_token = Fog::Mock.random_base64(64) + @auth_token_expiration = (Time.now.utc + 86400).iso8601 + + management_url = URI.parse(options[:openstack_auth_url]) + management_url.port = 8776 + management_url.path = '/v1' + @openstack_management_url = management_url.to_s + + @data ||= { :users => {} } + unless @data[:users].find {|u| u['name'] == options[:openstack_username]} + id = Fog::Mock.random_numbers(6).to_s + @data[:users][id] = { + 'id' => id, + 'name' => options[:openstack_username], + 'email' => "#{options[:openstack_username]}@mock.com", + 'tenantId' => Fog::Mock.random_numbers(6).to_s, + 'enabled' => true + } + end + end + + def data + self.class.data[@openstack_username] + end + + def reset_data + self.class.data.delete(@openstack_username) + end + + def credentials + { :provider => 'openstack', + :openstack_auth_url => @openstack_auth_uri.to_s, + :openstack_auth_token => @auth_token, + :openstack_management_url => @openstack_management_url } + end + end + + class Real + attr_reader :current_user + attr_reader :current_tenant + + def initialize(options={}) + require 'multi_json' + + @openstack_auth_token = options[:openstack_auth_token] + + unless @openstack_auth_token + missing_credentials = Array.new + @openstack_api_key = options[:openstack_api_key] + @openstack_username = options[:openstack_username] + + missing_credentials << :openstack_api_key unless @openstack_api_key + missing_credentials << :openstack_username unless @openstack_username + raise ArgumentError, "Missing required arguments: #{missing_credentials.join(', ')}" unless missing_credentials.empty? + end + + @openstack_tenant = options[:openstack_tenant] + @openstack_auth_uri = URI.parse(options[:openstack_auth_url]) + @openstack_management_url = options[:openstack_management_url] + @openstack_must_reauthenticate = false + @openstack_service_type = options[:openstack_service_type] || ['metering'] + @openstack_service_name = options[:openstack_service_name] + + @openstack_endpoint_type = options[:openstack_endpoint_type] || 'adminURL' + @connection_options = options[:connection_options] || {} + + @current_user = options[:current_user] + @current_tenant = options[:current_tenant] + + authenticate + + @persistent = options[:persistent] || false + @connection = Fog::Connection.new("#{@scheme}://#{@host}:#{@port}", @persistent, @connection_options) + end + + def credentials + { :provider => 'openstack', + :openstack_auth_url => @openstack_auth_uri.to_s, + :openstack_auth_token => @auth_token, + :openstack_management_url => @openstack_management_url, + :current_user => @current_user, + :current_tenant => @current_tenant } + end + + def reload + @connection.reset + end + + def request(params) + begin + response = @connection.request(params.merge({ + :headers => { + 'Content-Type' => 'application/json', + 'Accept' => 'application/json', + 'X-Auth-Token' => @auth_token + }.merge!(params[:headers] || {}), + :host => @host, + :path => "#{@path}/v2/#{params[:path]}"#, + # Causes errors for some requests like tenants?limit=1 + # :query => ('ignore_awful_caching' << Time.now.to_i.to_s) + })) + rescue Excon::Errors::Unauthorized => error + if error.response.body != 'Bad username or password' # token expiration + @openstack_must_reauthenticate = true + authenticate + retry + else # bad credentials + raise error + end + rescue Excon::Errors::HTTPStatusError => error + raise case error + when Excon::Errors::NotFound + Fog::Compute::OpenStack::NotFound.slurp(error) + else + error + end + end + unless response.body.empty? + response.body = MultiJson.decode(response.body) + end + response + end + + private + + def authenticate + if !@openstack_management_url || @openstack_must_reauthenticate + options = { + :openstack_tenant => @openstack_tenant, + :openstack_api_key => @openstack_api_key, + :openstack_username => @openstack_username, + :openstack_auth_uri => @openstack_auth_uri, + :openstack_auth_token => @openstack_auth_token, + :openstack_service_type => @openstack_service_type, + :openstack_service_name => @openstack_service_name, + :openstack_endpoint_type => @openstack_endpoint_type + } + + credentials = Fog::OpenStack.authenticate_v2(options, @connection_options) + + @current_user = credentials[:user] + @current_tenant = credentials[:tenant] + + @openstack_must_reauthenticate = false + @auth_token = credentials[:token] + @openstack_management_url = credentials[:server_management_url] + uri = URI.parse(@openstack_management_url) + else + @auth_token = @openstack_auth_token + uri = URI.parse(@openstack_management_url) + end + + @host = uri.host + @path = uri.path + @path.sub!(/\/$/, '') + @port = uri.port + @scheme = uri.scheme + true + end + + end + end + end +end + diff --git a/lib/fog/openstack/models/metering/meter.rb b/lib/fog/openstack/models/metering/meter.rb new file mode 100644 index 000000000..e69de29bb diff --git a/lib/fog/openstack/models/metering/meters.rb b/lib/fog/openstack/models/metering/meters.rb new file mode 100644 index 000000000..e69de29bb diff --git a/lib/fog/openstack/models/metering/resource.rb b/lib/fog/openstack/models/metering/resource.rb new file mode 100644 index 000000000..9857cfe23 --- /dev/null +++ b/lib/fog/openstack/models/metering/resource.rb @@ -0,0 +1,24 @@ +require 'fog/core/model' + +module Fog + module Metering + class OpenStack + + class Resource < Fog::Model + + identity :resource_id + + attribute :project_id + attribute :user_id + attribute :metadata + + def initialize(attributes) + prepare_service_value(attributes) + super + end + + end + end + end +end + diff --git a/lib/fog/openstack/models/metering/resources.rb b/lib/fog/openstack/models/metering/resources.rb new file mode 100644 index 000000000..6108b7bb9 --- /dev/null +++ b/lib/fog/openstack/models/metering/resources.rb @@ -0,0 +1,25 @@ +require 'fog/core/collection' +require 'fog/openstack/models/metering/resource' + +module Fog + module Metering + class OpenStack + + class Resources < Fog::Collection + model Fog::Metering::OpenStack::Resource + + def all(detailed=true) + load(service.list_resources.body) + end + + def find_by_id(resource_id) + resource = service.get_resource(resource_id).body + new(resource) + rescue Fog::Metering::OpenStack::NotFound + nil + end + end + + end + end +end diff --git a/lib/fog/openstack/requests/metering/get_resource.rb b/lib/fog/openstack/requests/metering/get_resource.rb new file mode 100644 index 000000000..f398fea14 --- /dev/null +++ b/lib/fog/openstack/requests/metering/get_resource.rb @@ -0,0 +1,32 @@ +module Fog + module Metering + class OpenStack + class Real + + def get_resource(resource_id) + request( + :expects => 200, + :method => 'GET', + :path => "resources/#{resource_id}" + ) + end + + end + + class Mock + + def get_resource(resource_id) + response = Excon::Response.new + response.status = 200 + response.body = { + 'resource_id'=>'glance', + 'project_id'=>'d646b40dea6347dfb8caee2da1484c56', + 'user_id'=>'1d5fd9eda19142289a60ed9330b5d284', + 'metadata'=>{}} + response + end + end + + end + end +end diff --git a/lib/fog/openstack/requests/metering/get_samples.rb b/lib/fog/openstack/requests/metering/get_samples.rb new file mode 100644 index 000000000..c6688a447 --- /dev/null +++ b/lib/fog/openstack/requests/metering/get_samples.rb @@ -0,0 +1,53 @@ +module Fog + module Metering + class OpenStack + class Real + + def get_samples(meter_id, options = {}) + + data = { + 'q' => Array.new + } + + filters = ['field', 'op', 'value'] + filter = {} + filters.select{|o| options[o]}.each do |key| + filter[key] = options[key] + end + + data['q'] << filter unless filter.empty? + + request( + :body => Fog::JSON.encode(data), + :expects => 200, + :method => 'GET', + :path => "meters/#{meter_id}" + ) + end + + end + + class Mock + + def get_samples(meter_id) + response = Excon::Response.new + response.status = 200 + response.body = [{ + 'counter_name'=>'image.size', + 'user_id'=>'1d5fd9eda19142289a60ed9330b5d284', + 'resource_id'=>'glance', + 'timestamp'=>'2013-04-03T23:44:21', + 'resource_metadata'=>{}, + 'source'=>'artificial', + 'counter_unit'=>'bytes', + 'counter_volume'=>10.0, + 'project_id'=>'d646b40dea6347dfb8caee2da1484c56', + 'message_id'=>'14e4a902-9cf3-11e2-a054-003048f5eafc', + 'counter_type'=>'gauge'}] + response + end + end + + end + end +end diff --git a/lib/fog/openstack/requests/metering/get_statistics.rb b/lib/fog/openstack/requests/metering/get_statistics.rb new file mode 100644 index 000000000..749749a9f --- /dev/null +++ b/lib/fog/openstack/requests/metering/get_statistics.rb @@ -0,0 +1,54 @@ +module Fog + module Metering + class OpenStack + class Real + + def get_statistics(meter_id, options={}) + + data = { + 'period' => options['period'], + 'q' => Array.new + } + + filters = ['field', 'op', 'value'] + filter = {} + filters.select{|o| options[o]}.each do |key| + filter[key] = options[key] + end + + data['q'] << filter unless filter.empty? + + request( + :body => Fog::JSON.encode(data), + :expects => 200, + :method => 'GET', + :path => "meters/#{meter_id}/statistics" + ) + end + + end + + class Mock + + def get_statistics(meter_id) + response = Excon::Response.new + response.status = 200 + response.body = [{ + 'count'=>143, + 'duration_start'=>'2013-04-03T23:44:21', + 'min'=>10.0, + 'max'=>10.0, + 'duration_end'=>'2013-04-04T23:24:21', + 'period'=>0, + 'period_end'=>'2013-04-04T23:24:21', + 'duration'=>85200.0, + 'period_start'=>'2013-04-03T23:44:21', + 'avg'=>10.0, + 'sum'=>1430.0}] + response + end + end + + end + end +end diff --git a/lib/fog/openstack/requests/metering/list_meters.rb b/lib/fog/openstack/requests/metering/list_meters.rb new file mode 100644 index 000000000..cd719d442 --- /dev/null +++ b/lib/fog/openstack/requests/metering/list_meters.rb @@ -0,0 +1,48 @@ +module Fog + module Metering + class OpenStack + class Real + + def list_meters(options={}) + + data = { + 'q' => Array.new + } + + filters = ['field', 'op', 'value'] + filter = {} + filters.select{|o| options[o]}.each do |key| + filter[key] = options[key] + end + + data['q'] << filter unless filter.empty? + + request( + :body => Fog::JSON.encode(data), + :expects => 200, + :method => 'GET', + :path => 'meters' + ) + end + + end + + class Mock + + def list_meters(options={}) + response = Excon::Response.new + response.status = 200 + response.body = [{ + 'user_id'=>'1d5fd9eda19142289a60ed9330b5d284', + 'name'=>'image.size', + 'resource_id'=>'glance', + 'project_id'=>'d646b40dea6347dfb8caee2da1484c56', + 'type'=>'gauge', + 'unit'=>'bytes'}] + response + end + end + + end + end +end diff --git a/lib/fog/openstack/requests/metering/list_resources.rb b/lib/fog/openstack/requests/metering/list_resources.rb new file mode 100644 index 000000000..8fdd0d8cb --- /dev/null +++ b/lib/fog/openstack/requests/metering/list_resources.rb @@ -0,0 +1,32 @@ +module Fog + module Metering + class OpenStack + class Real + + def list_resources(options = {}) + request( + :expects => 200, + :method => 'GET', + :path => 'resources' + ) + end + + end + + class Mock + + def list_resources(options = {}) + response = Excon::Response.new + response.status = 200 + response.body = [{ + 'resource_id'=>'glance', + 'project_id'=>'d646b40dea6347dfb8caee2da1484c56', + 'user_id'=>'1d5fd9eda19142289a60ed9330b5d284', + 'metadata'=>{}}] + response + end + end + + end + end +end diff --git a/tests/openstack/requests/metering/meter_tests.rb b/tests/openstack/requests/metering/meter_tests.rb new file mode 100644 index 000000000..1ae6c3fe5 --- /dev/null +++ b/tests/openstack/requests/metering/meter_tests.rb @@ -0,0 +1,52 @@ +Shindo.tests('Fog::Metering[:openstack] | meter requests', ['openstack']) do + + @sample_format = { + 'counter_name' => String, + 'user_id' => String, + 'resource_id' => String, + 'timestamp' => String, + 'resource_metadata' => Hash, + 'source' => String, + 'counter_unit' => String, + 'counter_volume' => Float, + 'project_id' => String, + 'message_id' => String, + 'counter_type' => String + } + + @meter_format = { + 'user_id' => String, + 'name' => String, + 'resource_id' => String, + 'project_id' => String, + 'type' => String, + 'unit' => String + } + + @statistics_format = { + 'count' => Integer, + 'duration_start' => String, + 'min' => Float, + 'max' => Float, + 'duration_end' => String, + 'period' => Integer, + 'period_end' => String, + 'duration' => Float, + 'period_start' => String, + 'avg' => Float, + 'sum' => Float + } + tests('success') do + tests('#list_meters').formats([@meter_format]) do + Fog::Metering[:openstack].list_meters.body + end + + tests('#get_samples').formats([@sample_format]) do + Fog::Metering[:openstack].get_samples('test').body + end + + tests('#get_statistics').formats([@statistics_format]) do + Fog::Metering[:openstack].get_statistics('test').body + end + end +end diff --git a/tests/openstack/requests/metering/resource_tests.rb b/tests/openstack/requests/metering/resource_tests.rb new file mode 100644 index 000000000..fc1c85d88 --- /dev/null +++ b/tests/openstack/requests/metering/resource_tests.rb @@ -0,0 +1,19 @@ +Shindo.tests('Fog::Metering[:openstack] | resource requests', ['openstack']) do + + @resource_format = { + 'resource_id' => String, + 'project_id' => String, + 'user_id' => String, + 'metadata' => Hash, + } + + tests('success') do + tests('#list_resource').formats([@resource_format]) do + Fog::Metering[:openstack].list_resources.body + end + + tests('#get_resource').formats(@resource_format) do + Fog::Metering[:openstack].get_resource('test').body + end + end +end