class Miasma::Models::Orchestration::Aws

AWS Orchestration API

Constants

API_SERVICE

Service name of the API

API_VERSION

Supported version of the AutoScaling API

EUCA_API_SERVICE

Service name of the eucalyptus API

RESOURCE_MAPPING

@return [Smash] external to internal resource mapping

STACK_STATES

Valid stack lookup states

Public Instance Methods

changeset_name(stack) click to toggle source

Generate changeset name given stack. This is a unique name for miasma and ensures only one changeset is used/persisted for miasma interactions.

@param stack [Models::Orchestration::Stack] @return [String]

# File lib/miasma/contrib/aws/orchestration.rb, line 464
def changeset_name(stack)
  stack.custom.fetch(:plan_name, "miasma-changeset-#{stack.name}")
end
common_stack_params(stack) click to toggle source

Common parameters used for stack creation/update requests. This is currently shared between stack creation and plan creation

@param stack [Model::Orchestration::Stack] @return [Smash]

# File lib/miasma/contrib/aws/orchestration.rb, line 700
def common_stack_params(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.template_url
    params["TemplateURL"] = stack.template_url
  elsif !stack.dirty?(:template) && stack.persisted?
    params["UsePreviousTemplate"] = true
  else
    params["TemplateBody"] = MultiJson.dump(stack.template)
  end
  params
end
event_all(stack, evt_id = nil) click to toggle source

Return all events for stack

@param stack [Models::Orchestration::Stack] @return [Array<Models::Orchestration::Stack::Event>]

# File lib/miasma/contrib/aws/orchestration.rb, line 641
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"]).localtime,
    ).valid_state
  end
  if evt_id
    idx = events.index { |d| d.id == evt_id }
    idx ? events.slice(0, idx) : events
  else
    events
  end
end
event_all_new(events) click to toggle source

Return all new events for event collection

@param events [Models::Orchestration::Stack::Events] @return [Array<Models::Orchestration::Stack::Event>]

# File lib/miasma/contrib/aws/orchestration.rb, line 681
def event_all_new(events)
  event_all(events.stack, events.all.first.id)
end
event_reload(event) click to toggle source

Reload the stack event data from the API

@param resource [Models::Orchestration::Stack::Event] @return [Models::Orchestration::Event]

# File lib/miasma/contrib/aws/orchestration.rb, line 689
def event_reload(event)
  event.stack.events.reload
  event.stack.events.get(event.id)
end
load_stack_data(stack = nil) click to toggle source

Fetch stacks or update provided stack data

@param stack [Models::Orchestration::Stack] @return [Array<Models::Orchestration::Stack>]

# File lib/miasma/contrib/aws/orchestration.rb, line 65
def load_stack_data(stack = nil)
  d_params = Smash.new("Action" => "DescribeStacks")
  l_params = Smash.new("Action" => "ListStacks")
  # TODO: Need to update to allow passing in list of desired
  #       states for lookup or to remove state filtering. This
  #       used to work with earlier versions with sfn but something
  #       has changed breaking the integration.
  STACK_STATES.each_with_index do |state, idx|
    l_params["StackStatusFilter.member.#{idx + 1}"] = state.to_s.upcase
  end
  if stack
    logger.debug("loading stack information for `#{stack.id}`")
    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
    logger.debug("loading stack listing information")
    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
    logger.debug("loaded stack information `#{new_stack.inspect}`")
    new_stack
  end
end
resource_all(stack) click to toggle source

Return all resources for stack

@param stack [Models::Orchestration::Stack] @return [Array<Models::Orchestration::Stack::Resource>]

# File lib/miasma/contrib/aws/orchestration.rb, line 585
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
resource_reload(resource) click to toggle source

Reload the stack resource data from the API

@param resource [Models::Orchestration::Stack::Resource] @return [Models::Orchestration::Resource]

# File lib/miasma/contrib/aws/orchestration.rb, line 617
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
stack_all() click to toggle source

Return all stacks

@param options [Hash] filter @return [Array<Models::Orchestration::Stack>] @todo check if we need any mappings on state set

# File lib/miasma/contrib/aws/orchestration.rb, line 577
def stack_all
  load_stack_data
end
stack_destroy(stack) click to toggle source

Delete the stack

@param stack [Models::Orchestration::Stack] @return [TrueClass, FalseClass]

# File lib/miasma/contrib/aws/orchestration.rb, line 493
def stack_destroy(stack)
  if stack.persisted?
    logger.debug("deleting stack `#{stack.id}`")
    request(
      :method => :post,
      :path => "/",
      :form => Smash.new(
        "Action" => "DeleteStack",
        "StackName" => stack.id,
      ),
    )
    true
  else
    logger.debug("stack not persisted. delete is no-op `#{stack.name}`")
    false
  end
end
stack_get(ident) click to toggle source

Return single stack

@param ident [String] name or ID @return [Stack]

# File lib/miasma/contrib/aws/orchestration.rb, line 565
def stack_get(ident)
  i = Stack.new(self)
  i.id = ident
  i.reload
  i.name ? i : nil
end
stack_plan(stack) click to toggle source

Generate a new stack plan from the API

@param stack [Models::Orchestration::Stack] @return [Models::Orchestration::Stack] @todo Needs to include the rolearn and resourcetypes

at some point but more thought on how to integrate
# File lib/miasma/contrib/aws/orchestration.rb, line 210
def stack_plan(stack)
  logger.debug("generating plan for stack `#{stack.id}`")
  params = common_stack_params(stack)
  plan_name = changeset_name(stack)
  if stack.persisted? && stack.state != :unknown
    logger.debug("plan will update stack")
    changeset_type = "UPDATE"
  else
    logger.debug("plan will create stack")
    changeset_type = "CREATE"
  end
  result = request(
    :path => "/",
    :method => :post,
    :form => params.merge(Smash.new(
      "Action" => "CreateChangeSet",
      "ChangeSetName" => plan_name,
      "StackName" => stack.name,
      "ChangeSetType" => changeset_type,
    )),
  )
  stack.reload
  # Ensure we have the same plan name in use after reload
  stack.custom = stack.custom.dup
  stack.custom[:plan_name] = plan_name
  stack.plan
end
stack_plan_all(stack) click to toggle source

Load all plans associated to given stack

@param stack [Models::Orchestration::Stack] @return [Array<Models::Orchestration::Stack::Plan>]

# File lib/miasma/contrib/aws/orchestration.rb, line 432
def stack_plan_all(stack)
  logger.debug("loading all plans for stack `#{stack.id}`")
  all_result_pages(nil, :body,
                   "ListChangeSetsResponse", "ListChangeSetsResult",
                   "Summaries", "member") do |options|
    request(
      :method => :post,
      :path => "/",
      :form => options.merge(
        Smash.new(
          "Action" => "ListChangeSets",
          "StackName" => stack.id || stack.name,
        )
      ),
    )
  end.map do |res|
    stack = Stack.new(self,
                      id: res["StackId"],
                      name: res["StackName"])
    stack.custom = {:plan_name => res["ChangeSetName"],
                    :plan_id => res["ChangeSetId"]}
    stack.plan
  end
end
stack_plan_destroy(stack) click to toggle source

Delete the plan attached to the stack

@param stack [Models::Orchestration::Stack] @return [Models::Orchestration::Stack]

# File lib/miasma/contrib/aws/orchestration.rb, line 378
def stack_plan_destroy(stack)
  logger.debug("deleting plan `#{stack.plan.id}` for stack `#{stack.id}`")
  request(
    :path => "/",
    :method => :post,
    :form => Smash.new(
      "Action" => "DeleteChangeSet",
      "ChangeSetName" => stack.plan.id,
      "StackName" => stack.name,
    ),
  )
  stack.plan = nil
  stack.valid_state
end
stack_plan_execute(stack) click to toggle source

Apply the plan attached to the stack

@param stack [Model::Orchestration::Stack] @return [Model::Orchestration::Stack]

# File lib/miasma/contrib/aws/orchestration.rb, line 397
def stack_plan_execute(stack)
  logger.debug("applying plan `#{stack.plan.id}` to stack `#{stack.id}`")
  request(
    :path => "/",
    :method => :post,
    :form => Smash.new(
      "Action" => "ExecuteChangeSet",
      "ChangeSetName" => stack.plan.id,
      "StackName" => stack.name,
    ),
  )
  stack.reload
end
stack_plan_load(stack) click to toggle source

Load the plan for the stack

@param stack [Models::Orchestration::Stack] @return [Models::Orchestration::Stack::Plan]

# File lib/miasma/contrib/aws/orchestration.rb, line 242
def stack_plan_load(stack)
  if stack.attributes[:plan]
    plan = stack.attributes[:plan]
  else
    plan = Stack::Plan.new(stack, name: changeset_name(stack))
  end
  if stack.custom[:plan_name]
    if stack.custom[:plan_name] != plan.name
      plan.name = stack.custom[:plan_name]
    else
      plan.name = changeset_name(stack)
    end
  end
  logger.debug("loading plan `#{plan.name}` for stack `#{stack.id}`")
  result = nil
  Bogo::Retry.build(:linear, max_attempts: 10, wait_interval: 5, ui: Bogo::Ui.new) do
    begin
      result = request(
        :path => "/",
        :method => :post,
        :form => Smash.new(
          "Action" => "DescribeChangeSet",
          "ChangeSetName" => plan.name,
          "StackName" => stack.name,
        ),
      )
    rescue Error::ApiError::RequestError => e
      # Plan does not exist
      if e.response.code == 404
        logger.warn("plan `#{plan.name}` does not exist for stack `#{stack.id}`")
        return nil
      end
      # Stack does not exist
      if e.response.code == 400 && e.message.include?("ValidationError: Stack")
        logger.warn("stack `#{stack.id}` does not exist")
        return nil
      end
      raise
    end
    status = result.get(:body, "DescribeChangeSetResponse", "DescribeChangeSetResult", "ExecutionStatus")
    if status != "AVAILABLE"
      logger.debug("plan `#{plan.name}` is not available (status: `#{status}`)")
      raise "Plan execution is not yet available"
    end
  end.run!
  res = result.get(:body, "DescribeChangeSetResponse", "DescribeChangeSetResult")
  plan.id = res["ChangeSetId"]
  plan.name = res["ChangeSetName"]
  plan.custom = {
    :execution_status => res["ExecutionStatus"],
    :stack_name => res["StackName"],
    :stack_id => res["StackId"],
    :status => res["Status"],
  }
  plan.state = res["ExecutionStatus"].downcase.to_sym
  plan.parameters = Smash[
    [res.get("Parameters", "member")].compact.flatten.map { |param|
      [param["ParameterKey"], param["ParameterValue"]]
    }
  ]
  plan.created_at = res["CreationTime"]
  plan.template = stack_plan_template(plan, :processed)
  items = {:add => [], :replace => [], :remove => [], :unknown => [], :interrupt => []}
  [res.get("Changes", "member")].compact.flatten.each do |chng|
    if chng["Type"] == "Resource"
      item_diffs = []
      [chng.get("ResourceChange", "Details", "member")].compact.flatten.each do |d|
        item_path = [
          d.get("Target", "Attribute"),
          d.get("Target", "Name"),
        ].compact
        original_value = stack.template.get("Resources", chng.get("ResourceChange", "LogicalResourceId"), *item_path)
        if original_value.is_a?(Hash) && (stack.parameters || {}).key?(original_value["Ref"])
          original_value = stack.parameters[original_value["Ref"]]
        end
        new_value = plan.template.get("Resources", chng.get("ResourceChange", "LogicalResourceId"), *item_path)
        if new_value.is_a?(Hash) && plan.parameters.key?(new_value["Ref"])
          new_value = plan.parameters[new_value["Ref"]]
        end
        diff = Stack::Plan::Diff.new(
          :name => item_path.join("."),
          :current => original_value.inspect,
          :proposed => new_value.inspect,
        )

        unless item_diffs.detect { |d| d.name == diff.name && d.current == diff.current && d.proposed == diff.proposed }
          item_diffs << diff
        end
      end
      type = case chng.get("ResourceChange", "Action").to_s.downcase
             when "add"
               :add
             when "modify"
               chng.get("ResourceChange", "Replacement") == "True" ?
                 :replace : :interrupt
             when "remove"
               :remove
             else
               :unknown
             end
      items[type] << Stack::Plan::Item.new(
        :name => chng.get("ResourceChange", "LogicalResourceId"),
        :type => chng.get("ResourceChange", "ResourceType"),
        :diffs => item_diffs.sort_by(&:name),
      )
    end
  end.compact
  items.each do |type, list|
    plan.send("#{type}=", list.sort_by(&:name))
  end
  if plan.custom[:stack_id]
    stack.id = plan.custom[:stack_id]
    stack.valid_state
  end
  logger.debug("plan `#{plan.name}` loaded for stack `#{stack.id}` - `#{plan.inspect}`")
  stack.plan = plan.valid_state
end
stack_plan_reload(plan) click to toggle source

Reload the plan

@param plan [Model::Orchestration::Stack::Plan] @return [Model::Orchestration::Stack::Plan]

# File lib/miasma/contrib/aws/orchestration.rb, line 415
def stack_plan_reload(plan)
  logger.debug("reloading plan `#{plan.id}`")
  if plan.stack.plan == plan
    stack_plan_load(plan.stack)
  else
    stack = Stack.new(self,
                      id: plan.custom[:stack_id],
                      name: plan.custom[:stack_name])
    stack.dirty[:plan] = plan
    stack_plan_load(stack)
  end
end
stack_plan_template(plan, state) click to toggle source
# File lib/miasma/contrib/aws/orchestration.rb, line 360
def stack_plan_template(plan, state)
  logger.debug("loading template for plan `#{plan.name}`")
  result = request(
    :path => "/",
    :method => :post,
    :form => Smash.new(
      "Action" => "GetTemplate",
      "ChangeSetName" => plan.id,
      "TemplateStage" => state.to_s.capitalize,
    ),
  )
  MultiJson.load(result.get(:body, "GetTemplateResponse", "GetTemplateResult", "TemplateBody")).to_smash
end
stack_reload(stack) click to toggle source

Reload the stack data from the API

@param stack [Models::Orchestration::Stack] @return [Models::Orchestration::Stack]

# File lib/miasma/contrib/aws/orchestration.rb, line 472
def stack_reload(stack)
  logger.debug("reloading stack `#{stack.id}`")
  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
stack_save(stack) click to toggle source

Save the stack

@param stack [Models::Orchestration::Stack] @return [Models::Orchestration::Stack]

# File lib/miasma/contrib/aws/orchestration.rb, line 164
def stack_save(stack)
  logger.debug("saving stack information `#{stack.inspect}`")
  params = common_stack_params(stack)
  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.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
stack_template_load(stack) click to toggle source

Fetch stack template

@param stack [Stack] @return [Smash] stack template

# File lib/miasma/contrib/aws/orchestration.rb, line 515
def stack_template_load(stack)
  if stack.persisted?
    logger.debug("loading template for stack `#{stack.id}`")
    result = request(
      :method => :post,
      :path => "/",
      :form => Smash.new(
        "Action" => "GetTemplate",
        "StackName" => stack.id,
      ),
    )
    template = result.get(:body, "GetTemplateResponse", "GetTemplateResult", "TemplateBody")
    template.nil? ? Smash.new : MultiJson.load(template)
  else
    logger.debug("no template for non-persisted stack `#{stack.name}`")
    Smash.new
  end
end
stack_template_validate(stack) click to toggle source

Validate stack template

@param stack [Stack] @return [NilClass, String] nil if valid, string error message if invalid

# File lib/miasma/contrib/aws/orchestration.rb, line 538
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
    logger.error("template validate error - #{e.response.body}")
    MultiXml.parse(e.response.body.to_s).to_smash.get(
      "ErrorResponse", "Error", "Message"
    )
  end
end