require "miasma" module Miasma module Models class Orchestration # AWS Orchestration API class Aws < Orchestration # Extended stack model to provide AWS specific stack options class Stack < Orchestration::Stack attribute :stack_policy_body, Hash, :coerce => lambda { |v| MultiJson.load(v).to_smash } attribute :stack_policy_url, String attribute :last_event_token, String end # Service name of the API API_SERVICE = "cloudformation".freeze # Service name of the eucalyptus API EUCA_API_SERVICE = "CloudFormation".freeze # Supported version of the AutoScaling API API_VERSION = "2010-05-15".freeze # Valid stack lookup states STACK_STATES = [ "CREATE_COMPLETE", "CREATE_FAILED", "CREATE_IN_PROGRESS", "DELETE_FAILED", "DELETE_IN_PROGRESS", "ROLLBACK_COMPLETE", "ROLLBACK_FAILED", "ROLLBACK_IN_PROGRESS", "UPDATE_COMPLETE", "UPDATE_COMPLETE_CLEANUP_IN_PROGRESS", "UPDATE_IN_PROGRESS", "UPDATE_ROLLBACK_COMPLETE", "UPDATE_ROLLBACK_COMPLETE_CLEANUP_IN_PROGRESS", "UPDATE_ROLLBACK_FAILED", "UPDATE_ROLLBACK_IN_PROGRESS", ].map(&:freeze).freeze include Contrib::AwsApiCore::ApiCommon include Contrib::AwsApiCore::RequestUtils # @return [Smash] external to internal resource mapping RESOURCE_MAPPING = Smash.new( "AWS::EC2::Instance" => Smash.new( :api => :compute, :collection => :servers, ), "AWS::ElasticLoadBalancing::LoadBalancer" => Smash.new( :api => :load_balancer, :collection => :balancers, ), "AWS::AutoScaling::AutoScalingGroup" => Smash.new( :api => :auto_scale, :collection => :groups, ), "AWS::CloudFormation::Stack" => Smash.new( :api => :orchestration, :collection => :stacks, ), ).to_smash(:freeze) # Fetch stacks or update provided stack data # # @param stack [Models::Orchestration::Stack] # @return [Array<Models::Orchestration::Stack>] def load_stack_data(stack = nil) d_params = Smash.new("Action" => "DescribeStacks") l_params = Smash.new("Action" => "ListStacks") STACK_STATES.each_with_index do |state, idx| l_params["StackStatusFilter.member.#{idx + 1}"] = state.to_s.upcase end if stack d_params["StackName"] = stack.id descriptions = all_result_pages(nil, :body, "DescribeStacksResponse", "DescribeStacksResult", "Stacks", "member") do |options| request( :method => :post, :path => "/", :form => options.merge(d_params), ) end else lists = all_result_pages(nil, :body, "ListStacksResponse", "ListStacksResult", "StackSummaries", "member") do |options| request( :method => :post, :path => "/", :form => options.merge(l_params), ) end descriptions = [] end (lists || descriptions).map do |stk| if lists desc = descriptions.detect do |d_stk| d_stk["StackId"] == stk["StackId"] end || Smash.new stk.merge!(desc) end if stack next if stack.id != stk["StackId"] && stk["StackId"].split("/")[1] != stack.id end state = stk["StackStatus"].downcase.to_sym unless Miasma::Models::Orchestration::VALID_RESOURCE_STATES.include?(state) parts = state.to_s.split("_") state = [parts.first, *parts.slice(-2, parts.size)].join("_").to_sym unless Miasma::Models::Orchestration::VALID_RESOURCE_STATES.include?(parts) state = :unknown end end new_stack = stack || Stack.new(self) new_stack.load_data( :id => stk["StackId"], :name => stk["StackName"], :capabilities => [stk.get("Capabilities", "member")].flatten(1).compact, :description => stk["Description"], :created => stk["CreationTime"], :updated => stk["LastUpdatedTime"], :notification_topics => [stk.get("NotificationARNs", "member")].flatten(1).compact, :timeout_in_minutes => stk["TimeoutInMinutes"] ? stk["TimeoutInMinutes"].to_i : nil, :status => stk["StackStatus"], :status_reason => stk["StackStatusReason"], :state => state, :template_description => stk["TemplateDescription"], :disable_rollback => !!stk["DisableRollback"], :outputs => [stk.get("Outputs", "member")].flatten(1).compact.map { |o| Smash.new( :key => o["OutputKey"], :value => o["OutputValue"], :description => o["Description"], ) }, :tags => Smash[ [stk.fetch("Tags", "member", [])].flatten(1).map { |param| [param["Key"], param["Value"]] } ], :parameters => Smash[ [stk.fetch("Parameters", "member", [])].flatten(1).map { |param| [param["ParameterKey"], param["ParameterValue"]] } ], :custom => Smash.new( :stack_policy => stk["StackPolicyBody"], :stack_policy_url => stk["StackPolicyURL"], ), ).valid_state end end # Save the stack # # @param stack [Models::Orchestration::Stack] # @return [Models::Orchestration::Stack] def stack_save(stack) params = Smash.new("StackName" => stack.name) if stack.dirty?(:parameters) initial_parameters = stack.data[:parameters] || {} else initial_parameters = {} end (stack.parameters || {}).each_with_index do |pair, idx| params["Parameters.member.#{idx + 1}.ParameterKey"] = pair.first if initial_parameters[pair.first] == pair.last params["Parameters.member.#{idx + 1}.UsePreviousValue"] = true else params["Parameters.member.#{idx + 1}.ParameterValue"] = pair.last end end (stack.capabilities || []).each_with_index do |cap, idx| params["Capabilities.member.#{idx + 1}"] = cap end (stack.notification_topics || []).each_with_index do |topic, idx| params["NotificationARNs.member.#{idx + 1}"] = topic end (stack.tags || {}).each_with_index do |tag, idx| params["Tags.member.#{idx + 1}.Key"] = tag.first params["Tags.member.#{idx + 1}.Value"] = tag.last end if stack.custom[:stack_policy_body] params["StackPolicyBody"] = MultiJson.dump(stack.custom[:stack_policy_body]) end if stack.custom[:stack_policy_url] params["StackPolicyURL"] = stack.custom[:stack_policy_url] end unless stack.disable_rollback.nil? params["OnFailure"] = stack.disable_rollback ? "DO_NOTHING" : "ROLLBACK" end if stack.on_failure params["OnFailure"] = stack.on_failure == "nothing" ? "DO_NOTHING" : stack.on_failure.upcase end if stack.template_url params["TemplateURL"] = stack.template_url elsif !stack.dirty?(:template) && stack.persisted? params["UsePreviousTemplate"] = true else params["TemplateBody"] = MultiJson.dump(stack.template) end if stack.persisted? result = request( :path => "/", :method => :post, :form => Smash.new( "Action" => "UpdateStack", ).merge(params), ) stack else if stack.timeout_in_minutes params["TimeoutInMinutes"] = stack.timeout_in_minutes end result = request( :path => "/", :method => :post, :form => Smash.new( "Action" => "CreateStack", ).merge(params), ) stack.id = result.get(:body, "CreateStackResponse", "CreateStackResult", "StackId") stack.valid_state end end # Reload the stack data from the API # # @param stack [Models::Orchestration::Stack] # @return [Models::Orchestration::Stack] def stack_reload(stack) if stack.persisted? ustack = Stack.new(self) ustack.id = stack.id load_stack_data(ustack) if ustack.data[:name] stack.load_data(ustack.attributes).valid_state else stack.status = "DELETE_COMPLETE" stack.state = :delete_complete stack.valid_state end end stack end # Delete the stack # # @param stack [Models::Orchestration::Stack] # @return [TrueClass, FalseClass] def stack_destroy(stack) if stack.persisted? request( :method => :post, :path => "/", :form => Smash.new( "Action" => "DeleteStack", "StackName" => stack.id, ), ) true else false end end # Fetch stack template # # @param stack [Stack] # @return [Smash] stack template def stack_template_load(stack) if stack.persisted? result = request( :method => :post, :path => "/", :form => Smash.new( "Action" => "GetTemplate", "StackName" => stack.id, ), ) MultiJson.load( result.get(:body, "GetTemplateResponse", "GetTemplateResult", "TemplateBody") ).to_smash else Smash.new end end # Validate stack template # # @param stack [Stack] # @return [NilClass, String] nil if valid, string error message if invalid def stack_template_validate(stack) begin if stack.template_url params = Smash.new("TemplateURL" => stack.template_url) else params = Smash.new("TemplateBody" => MultiJson.dump(stack.template)) end result = request( :method => :post, :path => "/", :form => params.merge( "Action" => "ValidateTemplate", ), ) nil rescue Error::ApiError::RequestError => e MultiXml.parse(e.response.body.to_s).to_smash.get( "ErrorResponse", "Error", "Message" ) end end # Return single stack # # @param ident [String] name or ID # @return [Stack] def stack_get(ident) i = Stack.new(self) i.id = ident i.reload i.name ? i : nil end # Return all stacks # # @param options [Hash] filter # @return [Array<Models::Orchestration::Stack>] # @todo check if we need any mappings on state set def stack_all load_stack_data end # Return all resources for stack # # @param stack [Models::Orchestration::Stack] # @return [Array<Models::Orchestration::Stack::Resource>] def resource_all(stack) all_result_pages(nil, :body, "ListStackResourcesResponse", "ListStackResourcesResult", "StackResourceSummaries", "member") do |options| request( :method => :post, :path => "/", :form => options.merge( Smash.new( "Action" => "ListStackResources", "StackName" => stack.id, ) ), ) end.map do |res| Stack::Resource.new( stack, :id => res["PhysicalResourceId"], :name => res["LogicalResourceId"], :logical_id => res["LogicalResourceId"], :type => res["ResourceType"], :state => res["ResourceStatus"].downcase.to_sym, :status => res["ResourceStatus"], :updated => res["LastUpdatedTimestamp"], ).valid_state end end # Reload the stack resource data from the API # # @param resource [Models::Orchestration::Stack::Resource] # @return [Models::Orchestration::Resource] def resource_reload(resource) result = request( :method => :post, :path => "/", :form => Smash.new( "LogicalResourceId" => resource.logical_id, "StackName" => resource.stack.name, ), ).get(:body, "DescribeStackResourceResponse", "DescribeStackResourceResult", "StackResourceDetail") resource.updated = result["LastUpdatedTimestamp"] resource.type = result["ResourceType"] resource.state = result["ResourceStatus"].downcase.to_sym resource.status = result["ResourceStatus"] resource.status_reason = result["ResourceStatusReason"] resource.valid_state resource end # Return all events for stack # # @param stack [Models::Orchestration::Stack] # @return [Array<Models::Orchestration::Stack::Event>] def event_all(stack, evt_id = nil) evt_id = stack.last_event_token if evt_id results = all_result_pages(evt_id, :body, "DescribeStackEventsResponse", "DescribeStackEventsResult", "StackEvents", "member") do |options| request( :method => :post, :path => "/", :form => options.merge( "Action" => "DescribeStackEvents", "StackName" => stack.id, ), ) end events = results.map do |event| stack.last_event_token = event["NextToken"] if event["NextToken"] Stack::Event.new( stack, :id => event["EventId"], :resource_id => event["PhysicalResourceId"], :resource_name => event["LogicalResourceId"], :resource_logical_id => event["LogicalResourceId"], :resource_state => event["ResourceStatus"].downcase.to_sym, :resource_status => event["ResourceStatus"], :resource_status_reason => event["ResourceStatusReason"], :time => Time.parse(event["Timestamp"]), ).valid_state end if evt_id idx = events.index { |d| d.id == evt_id } idx ? events.slice(0, idx) : events else events end end # Return all new events for event collection # # @param events [Models::Orchestration::Stack::Events] # @return [Array<Models::Orchestration::Stack::Event>] def event_all_new(events) event_all(events.stack, events.all.first.id) end # Reload the stack event data from the API # # @param resource [Models::Orchestration::Stack::Event] # @return [Models::Orchestration::Event] def event_reload(event) event.stack.events.reload event.stack.events.get(event.id) end end end end end