From ecef54c8bcf8821a34fa6b1ff17a61ad7367a32e Mon Sep 17 00:00:00 2001 From: Thomas Kadauke Date: Tue, 4 Jun 2013 13:25:51 +0200 Subject: [PATCH] CRUD for OpenStack heat's Stack model --- lib/fog/core.rb | 1 + lib/fog/openstack.rb | 1 + .../openstack/models/orchestration/stack.rb | 52 +++++ .../openstack/models/orchestration/stacks.rb | 21 ++ lib/fog/openstack/orchestration.rb | 181 ++++++++++++++++++ .../requests/orchestration/create_stack.rb | 43 +++++ .../requests/orchestration/delete_stack.rb | 34 ++++ .../requests/orchestration/list_stacks.rb | 46 +++++ .../requests/orchestration/update_stack.rb | 41 ++++ lib/fog/orchestration.rb | 25 +++ 10 files changed, 445 insertions(+) create mode 100644 lib/fog/openstack/models/orchestration/stack.rb create mode 100644 lib/fog/openstack/models/orchestration/stacks.rb create mode 100644 lib/fog/openstack/orchestration.rb create mode 100644 lib/fog/openstack/requests/orchestration/create_stack.rb create mode 100644 lib/fog/openstack/requests/orchestration/delete_stack.rb create mode 100644 lib/fog/openstack/requests/orchestration/list_stacks.rb create mode 100644 lib/fog/openstack/requests/orchestration/update_stack.rb create mode 100644 lib/fog/orchestration.rb diff --git a/lib/fog/core.rb b/lib/fog/core.rb index 9dc976850..f6eda6cdc 100644 --- a/lib/fog/core.rb +++ b/lib/fog/core.rb @@ -47,3 +47,4 @@ require 'fog/cdn' require 'fog/dns' require 'fog/network' require 'fog/storage' +require 'fog/orchestration' diff --git a/lib/fog/openstack.rb b/lib/fog/openstack.rb index 71d4f45b8..eeea1cecb 100644 --- a/lib/fog/openstack.rb +++ b/lib/fog/openstack.rb @@ -48,6 +48,7 @@ module Fog service(:storage, 'openstack/storage', 'Storage') service(:volume, 'openstack/volume', 'Volume') service(:metering, 'openstack/metering', 'Metering') + service(:orchestration, 'openstack/orchestration', 'Orchestration') def self.authenticate(options, connection_options = {}) case options[:openstack_auth_uri].path diff --git a/lib/fog/openstack/models/orchestration/stack.rb b/lib/fog/openstack/models/orchestration/stack.rb new file mode 100644 index 000000000..ce8ad1928 --- /dev/null +++ b/lib/fog/openstack/models/orchestration/stack.rb @@ -0,0 +1,52 @@ +require 'fog/core/model' + +module Fog + module Orchestration + class OpenStack + class Stack < Fog::Model + identity :id + + attribute :stack_name + attribute :stack_status + attribute :stack_status_reason + attribute :creation_time + attribute :updated_time + attribute :id + + attribute :template_url + attribute :template + attribute :parameters + attribute :timeout_in_minutes + + def initialize(attributes) + # Old 'connection' is renamed as service and should be used instead + prepare_service_value(attributes) + super + end + + def save + requires :stack_name + identity ? update : create + end + + def create + requires :stack_name + service.create_stack(stack_name, self.attributes) + self + end + + def update + requires :stack_name + service.update_stack(stack_name, self.attributes) + self + end + + def destroy + requires :id + service.delete_stack(self.stack_name, self.id) + true + end + end + end + end +end diff --git a/lib/fog/openstack/models/orchestration/stacks.rb b/lib/fog/openstack/models/orchestration/stacks.rb new file mode 100644 index 000000000..74eebfbaa --- /dev/null +++ b/lib/fog/openstack/models/orchestration/stacks.rb @@ -0,0 +1,21 @@ +require 'fog/core/collection' +require 'fog/openstack/models/orchestration/stack' + +module Fog + module Orchestration + class OpenStack + class Stacks < Fog::Collection + model Fog::Orchestration::OpenStack::Stack + + def all + load(service.list_stacks.body['stacks']) + end + + def find_by_id(id) + self.find {|stack| stack.id == id} + end + alias_method :get, :find_by_id + end + end + end +end diff --git a/lib/fog/openstack/orchestration.rb b/lib/fog/openstack/orchestration.rb new file mode 100644 index 000000000..dceb3895f --- /dev/null +++ b/lib/fog/openstack/orchestration.rb @@ -0,0 +1,181 @@ +require 'fog/aws/cloud_formation' + +module Fog + module Orchestration + 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, :openstack_identity_endpoint, + :current_user, :current_tenant, :openstack_region, + :openstack_endpoint_type + + model_path 'fog/openstack/models/orchestration' + model :stack + collection :stacks + + request_path 'fog/openstack/requests/orchestration' + request :create_stack + request :update_stack + request :delete_stack + request :list_stacks + + class Mock + + def initialize(options={}) + Fog::Mock.not_implemented + end + + end + + class Real + attr_reader :auth_token + attr_reader :auth_token_expiration + attr_reader :current_user + attr_reader :current_tenant + + def initialize(options={}) + @openstack_auth_token = options[:openstack_auth_token] + @auth_token = options[:openstack_auth_token] + @openstack_identity_public_endpoint = options[:openstack_identity_endpoint] + + unless @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] || ['orchestration'] + @openstack_service_name = options[:openstack_service_name] + @openstack_identity_service_type = options[:openstack_identity_service_type] || 'identity' + @openstack_endpoint_type = options[:openstack_endpoint_type] || 'publicURL' + @openstack_region = options[:openstack_region] + + @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, + :openstack_identity_endpoint => @openstack_identity_public_endpoint, + :openstack_region => @openstack_region, + :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}/#{@tenant_id}/#{params[:path]}", + :query => params[:query] + })) + 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 + + if response.status == 200 && !response.body.empty? + response.body = Fog::JSON.decode(response.body) + end + + response + end + + private + + def authenticate + if !@openstack_management_url || @openstack_must_reauthenticate + options = { + :openstack_api_key => @openstack_api_key, + :openstack_username => @openstack_username, + :openstack_auth_token => @auth_token, + :openstack_auth_uri => @openstack_auth_uri, + :openstack_region => @openstack_region, + :openstack_tenant => @openstack_tenant, + :openstack_service_type => @openstack_service_type, + :openstack_service_name => @openstack_service_name, + :openstack_identity_service_type => @openstack_identity_service_type, + :openstack_endpoint_type => @openstack_endpoint_type + } + + if @openstack_auth_uri.path =~ /\/v2.0\// + + credentials = Fog::OpenStack.authenticate_v2(options, @connection_options) + else + credentials = Fog::OpenStack.authenticate_v1(options, @connection_options) + end + + @current_user = credentials[:user] + @current_tenant = credentials[:tenant] + + @openstack_must_reauthenticate = false + @auth_token = credentials[:token] + @auth_token_expiration = credentials[:expires] + @openstack_management_url = credentials[:server_management_url] + @openstack_identity_public_endpoint = credentials[:identity_public_endpoint] + end + + uri = URI.parse(@openstack_management_url) + @host = uri.host + @path, @tenant_id = uri.path.scan(/(\/.*)\/(.*)/).flatten + + @path.sub!(/\/$/, '') + + @port = uri.port + @scheme = uri.scheme + + # Not all implementations have identity service in the catalog + if @openstack_identity_public_endpoint || @openstack_management_url + @identity_connection = Fog::Connection.new( + @openstack_identity_public_endpoint || @openstack_management_url, + false, @connection_options) + end + + true + end + + end + end + end +end diff --git a/lib/fog/openstack/requests/orchestration/create_stack.rb b/lib/fog/openstack/requests/orchestration/create_stack.rb new file mode 100644 index 000000000..641eebbde --- /dev/null +++ b/lib/fog/openstack/requests/orchestration/create_stack.rb @@ -0,0 +1,43 @@ +module Fog + module Orchestration + class OpenStack + class Real + + # Create a stack. + # + # * stack_name [String] Name of the stack to create. + # * options [Hash]: + # * :template_body [String] Structure containing the template body. + # or (one of the two Template parameters is required) + # * :template_url [String] URL of file containing the template body. + # * :disable_rollback [Boolean] Controls rollback on stack creation failure, defaults to false. + # * :parameters [Hash] Hash of providers to supply to template + # * :timeout_in_minutes [Integer] Minutes to wait before status is set to CREATE_FAILED + # + # @see http://docs.amazonwebservices.com/AWSCloudFormation/latest/APIReference/API_CreateStack.html + + def create_stack(stack_name, options = {}) + params = { + :stack_name => stack_name + }.merge(options) + + request( + :path => 'stacks', + :method => 'POST', + :body => MultiJson.encode(params) + ) + end + + end + + class Mock + def create_stack(stack_name, options = {}) + response = Excon::Response.new + response.status = 202 + response.body = {} + response + end + end + end + end +end diff --git a/lib/fog/openstack/requests/orchestration/delete_stack.rb b/lib/fog/openstack/requests/orchestration/delete_stack.rb new file mode 100644 index 000000000..106e68b38 --- /dev/null +++ b/lib/fog/openstack/requests/orchestration/delete_stack.rb @@ -0,0 +1,34 @@ +module Fog + module Orchestration + class OpenStack + class Real + + # Delete a stack. + # + # @param stack_name [String] Name of the stack to delete. + # @param stack_id [String] ID of the stack to delete. + # + # @return [Excon::Response] + # + # @see http://docs.amazonwebservices.com/AWSCloudFormation/latest/APIReference/API_DeleteStack.html + + def delete_stack(stack_name, stack_id) + request( + :path => "stacks/#{stack_name}/#{stack_id}", + :method => 'DELETE' + ) + end + + end + + class Mock + def delete_stack(stack_name, stack_id) + response = Excon::Response.new + response.status = 202 + response.body = {} + response + end + end + end + end +end diff --git a/lib/fog/openstack/requests/orchestration/list_stacks.rb b/lib/fog/openstack/requests/orchestration/list_stacks.rb new file mode 100644 index 000000000..e8b45634b --- /dev/null +++ b/lib/fog/openstack/requests/orchestration/list_stacks.rb @@ -0,0 +1,46 @@ +module Fog + module Orchestration + class OpenStack + class Real + + # List stacks. + # + # @param options [Hash] + # + # @return [Excon::Response] + # * body [Hash]: + # * stacks [Array] - Matching stacks + # * stack [Hash]: + # * id [String] - + # * stack_name [String] - + # * description [String] + # * links [Array] + # * stack_status [String] - + # * stack_status_reason [String] + # * creation_time [Time] - + # * updated_time [Time] - + # + # + # @see http://docs.aws.amazon.com/AWSCloudFormation/latest/APIReference/API_ListStacks.html + + def list_stacks(options = {}) + request( + :path => 'stacks', + :method => 'GET', + :query => options + ) + end + + end + + class Mock + def list_stacks(options = {}) + Excon::Response.new( + :body => { 'stacks' => [] }, + :status => 200 + ) + end + end + end + end +end diff --git a/lib/fog/openstack/requests/orchestration/update_stack.rb b/lib/fog/openstack/requests/orchestration/update_stack.rb new file mode 100644 index 000000000..222ca388f --- /dev/null +++ b/lib/fog/openstack/requests/orchestration/update_stack.rb @@ -0,0 +1,41 @@ +module Fog + module Orchestration + class OpenStack + class Real + + # Update a stack. + # + # @param [String] stack_name Name of the stack to update. + # @param [Hash] options + # * :template_body [String] Structure containing the template body. + # or (one of the two Template parameters is required) + # * :template_url [String] URL of file containing the template body. + # * :parameters [Hash] Hash of providers to supply to template. + # + # @see http://docs.amazonwebservices.com/AWSCloudFormation/latest/APIReference/API_UpdateStack.html + # + def update_stack(stack_name, options = {}) + params = { + :stack_name => stack_name + }.merge(options) + + request( + :path => 'stacks', + :method => 'PUT', + :body => MultiJson.encode(params) + ) + end + + end + + class Mock + def update_stack(stack_name, options = {}) + response = Excon::Response.new + response.status = 202 + response.body = {} + response + end + end + end + end +end diff --git a/lib/fog/orchestration.rb b/lib/fog/orchestration.rb new file mode 100644 index 000000000..bd641bac2 --- /dev/null +++ b/lib/fog/orchestration.rb @@ -0,0 +1,25 @@ +module Fog + module Orchestration + + 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}/network" + return Fog::Orchestration.const_get(Fog.providers[provider]).new(attributes) + end + + raise ArgumentError.new("#{provider} has no orchestration service") + end + + def self.providers + Fog.services[:orchestration] + end + + end +end