<commit_before>#
# Copyright (c) 2009 RightScale Inc
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

class InstanceSetup

  include RightScale::Actor

  expose :report_state

  # Amount of seconds to wait between set_r_s_version calls attemps
  RECONNECT_DELAY = 5

  # Amount of seconds to wait before shutting down if boot hasn't completed
  SUICIDE_DELAY = 45 * 60

  # Boot if and only if instance state is 'booting'
  # Prime timer for shutdown on unsuccessful boot ('suicide' functionality)
  #
  # === Parameters
  # options[:agent_identity](String):: Serialized agent identity for current agent
  # options[:auto_shutdown](TrueClass|FalseClass):: Whether instance should shutdown if it cannot boot
  #                                                 under SUICIDE_DELAY and this is the initial boot
  def initialize(options)
    agent_identity = options[:identity]
    RightScale::InstanceState.init(agent_identity)
    should_suicide = options[:auto_shutdown] && RightScale::InstanceState.initial_boot
    @suicide_timer = EM::Timer.new(SUICIDE_DELAY) do
      RightScale::RightLinkLog.error "Shutting down after having tried to boot for #{SUICIDE_DELAY / 60} minutes"
      RightScale::Platform.controller.shutdown 
    end if should_suicide 
    @boot_retries = 0
    @agent_identity = agent_identity
    RightScale::RightLinkLog.force_debug if RightScale::DevState.enabled?
    EM.threadpool_size = 1
    # Schedule boot sequence, don't run it now so agent is registered first
    if RightScale::InstanceState.value == 'booting'
      EM.next_tick { RightScale::RequestForwarder.instance.init { init_boot } }
    else
      RightScale::RequestForwarder.instance.init
    end
  end

  # Retrieve current instance state
  #
  # === Return
  # state(RightScale::OperationResult):: Success operation result containing instance state
  def report_state
    state = RightScale::OperationResult.success(RightScale::InstanceState.value)
  end

  # Handle disconnected notification from broker, enter offline mode
  #
  # === Parameters
  # status(Symbol):: Connection status, one of :connected or :disconnected
  #
  # === Return
  # true:: Always return true
  def connection_status(status)
    if status == :disconnected
      RightScale::RequestForwarder.instance.enable_offline_mode
    else
      RightScale::RequestForwarder.instance.disable_offline_mode
    end
    true
  end

  protected

  # We start off by setting the instance 'r_s_version' in the core site and
  # then proceed with the actual boot sequence
  #
  # === Return
  # true:: Always return true
  def init_boot
    options = { :agent_identity => @agent_identity, :r_s_version => RightScale::RightLinkConfig.protocol_version, :aws_id => RightScale::InstanceState.aws_id }
    RightScale::RequestForwarder.instance.request('/booter/declare', options) do |r|
      res = RightScale::OperationResult.from_results(r)
      if res.success?
        enable_managed_login
      else
        if res.retry?
          RightScale::RightLinkLog.info("RightScale not ready, retrying in #{RECONNECT_DELAY} seconds...")
        else
          RightScale::RightLinkLog.warn("Failed to contact RightScale: #{res.content}, retrying in #{RECONNECT_DELAY} seconds...")
        end
        # Retry in RECONNECT_DELAY seconds, retry forever, nothing else we can do
        EM.add_timer(RECONNECT_DELAY) { init_boot }
      end
    end
    true
  end

  # Enable managed SSH for this instance, then continue with boot. Ensures that
  # managed SSH users can login to troubleshoot stranded and other 'interesting' events
  #
  # === Return
  # true:: Always return true
  def enable_managed_login
    if RightScale::Platform.windows?
      boot
    else
      request('/booter/get_login_policy', {:agent_identity => @agent_identity}) do |r|
        res = RightScale::OperationResult.from_results(r)
        if res.success?
          policy  = res.content
          begin
            audit = RightScale::LoginManager.instance.update_policy(policy)
            if audit
              RightScale::AuditorProxy.instance.create_new_section("Managed login enabled", :audit_id => policy.audit_id)
              RightScale::AuditorProxy.instance.append_info(audit, :audit_id => policy.audit_id)
            end
          rescue Exception => e
            RightScale::AuditorProxy.instance.create_new_section('Failed to enable managed login', :audit_id => policy.audit_id)
            RightScale::AuditorProxy.instance.append_error("Error applying login policy: #{e.message}", 
                                               :category => RightScale::EventCategories::CATEGORY_ERROR,
                                               :audit_id => policy.audit_id)
            RightScale::RightLinkLog.error("#{e.class.name}: #{e.message}\n#{e.backtrace.join("\n")}")
          end
        else
          RightScale::RightLinkLog.error("Could not get login policy: #{res.content}")
        end

        boot
      end
    end
  end

  # Retrieve software repositories and configure mirrors accordingly then proceed to
  # retrieving and running boot bundle.
  #
  # === Return
  # true:: Always return true
  def boot
    request("/booter/get_repositories", @agent_identity) do |r|
      res = RightScale::OperationResult.from_results(r)
      if res.success?
        @audit_id = res.content.audit_id
        unless RightScale::Platform.windows?
          reps = res.content.repositories
          audit = "Using the following software repositories:\n"
          reps.each { |rep| audit += "  - #{rep.to_s}\n" }
          RightScale::AuditorProxy.instance.create_new_section("Configuring software repositories", :audit_id => @audit_id)
          RightScale::AuditorProxy.instance.append_info(audit, :audit_id => @audit_id)
          configure_repositories(reps)
          RightScale::AuditorProxy.instance.update_status("Software repositories configured", :audit_id => @audit_id)
        end
        RightScale::AuditorProxy.instance.create_new_section('Preparing boot bundle', :audit_id => @audit_id)
        prepare_boot_bundle do |prep_res|
          if prep_res.success?
            RightScale::AuditorProxy.instance.update_status('Boot bundle ready', :audit_id => @audit_id)
            run_boot_bundle(prep_res.content) do |boot_res|
              if boot_res.success?
                RightScale::InstanceState.value = 'operational'
              else
                strand("Failed to run boot sequence", boot_res)
              end
            end
          else
            strand("Failed to prepare boot bundle", prep_res)
          end
        end
      else
        strand("Failed to retrieve software repositories", res)
      end
    end
    true
  end

  # Log error to local log file and set instance state to stranded
  #
  # === Parameters
  # msg(String):: Error message that will be audited and logged
  # res(RightScale::OperationResult):: Operation result with additional information
  #
  # === Return
  # true:: Always return true
  def strand(msg, res=nil)
    RightScale::InstanceState.value = 'stranded'
    msg += ": #{res.content}" if res && res.content
    RightScale::AuditorProxy.instance.append_error(msg, 
                                                   :category => RightScale::EventCategories::CATEGORY_ERROR,
                                                   :audit_id => @audit_id) if @audit_id
    true
  end

  # Configure software repositories
  # Note: the configurators may return errors when the platform is not what they expect,
  # for now log error and keep going (to replicate legacy behavior).
  #
  # === Parameters
  # repositories(Array[(RepositoryInstantiation)]):: repositories to be configured
  #
  # === Return
  # true:: Always return true
  def configure_repositories(repositories)
    repositories.each do |repo|
      begin
        klass = repo.name.to_const
        unless klass.nil?
          fz = nil
          if repo.frozen_date
            # gives us date for yesterday since the mirror for today may not have been generated yet
            fz = (Date.parse(repo.frozen_date) - 1).to_s
            fz.gsub!(/-/,"")
          end
          klass.generate("none", repo.base_urls, fz)
        end
      rescue Exception => e
        RightScale::RightLinkLog.error(e.message)
      end
    end
    if system('which apt-get')
      ENV['DEBIAN_FRONTEND'] = 'noninteractive' # this prevents prompts
      RightScale::AuditorProxy.instance.append_output(`apt-get update 2>&1`, :audit_id => @audit_id)
    elsif system('which yum') 
      `yum clean metadata`
    end
    true
  end

  # Retrieve missing inputs if any
  #
  # === Block
  # Calls given block passing in one argument of type RightScale::OperationResult
  # The argument contains either a failure with associated message or success
  # with the corresponding boot bundle.
  #
  # === Return
  # true:: Always return true
  def prepare_boot_bundle(&cb)
    RightScale::AgentTagsManager.instance.tags do |tags|
      RightScale::InstanceState.startup_tags = tags
      if tags.empty?
        RightScale::AuditorProxy.instance.append_info("No tags discovered on startup", :audit_id => @audit_id)
      else
        RightScale::AuditorProxy.instance.append_info("Tags discovered on startup: '#{tags.join("', '")}'", :audit_id => @audit_id)
      end
      if @suicide_timer && !tags.include?('rs_launch:type=auto')
        # Someone removed the tag explicitely, cancel the timer
        @suicide_timer.cancel
        @suicide_timer = nil
      end
      options = { :agent_identity => @agent_identity, :audit_id => @audit_id }
      request("/booter/get_boot_bundle", options) do |r|
        res = RightScale::OperationResult.from_results(r)
        if res.success?
          bundle = res.content
          if bundle.executables.any? { |e| !e.ready }
            retrieve_missing_inputs(bundle) { cb.call(RightScale::OperationResult.success(bundle)) }
          else
            yield RightScale::OperationResult.success(bundle)
          end
        else
          msg = "Failed to retrieve boot scripts"
          msg += ": #{res.content}" if res.content
          yield RightScale::OperationResult.error(msg)
        end
      end
    end
  end

  # Retrieve missing inputs for recipes and RightScripts stored in
  # @recipes and @scripts respectively, update recipe attributes, RightScript
  # parameters and ready fields (set ready field to true if attempt was successful,
  # false otherwise).
  # This is for environment variables that we are waiting on.
  # Retries forever.
  #
  # === Block
  # Continuation block, will be called once attempt to retrieve attributes is completed
  #
  # === Return
  # true:: Always return true
  def retrieve_missing_inputs(bundle, &cb)
    scripts = bundle.executables.select { |e| e.is_a?(RightScale::RightScriptInstantiation) }
    recipes = bundle.executables.select { |e| e.is_a?(RightScale::RecipeInstantiation) }
    scripts_ids = scripts.select { |s| !s.ready }.map { |s| s.id }
    recipes_ids = recipes.select { |r| !r.ready }.map { |r| r.id }
    RightScale::RequestForwarder.instance.request('/booter/get_missing_attributes', { :agent_identity => @agent_identity,
                                                                             :scripts_ids    => scripts_ids,
                                                                             :recipes_ids    => recipes_ids }) do |r|
      res = RightScale::OperationResult.from_results(r)
      if res.success?
        res.content.each do |e|
          if e.is_a?(RightScale::RightScriptInstantiation)
            if script = scripts.detect { |s| s.id == e.id }
              script.ready = true
              script.parameters = e.parameters
            end
          else
            if recipe = recipes.detect { |s| s.id == e.id }
              recipe.ready = true
              recipe.attributes = e.attributes
            end
          end
        end
        pending_executables = bundle.executables.select { |e| !e.ready }
        if pending_executables.empty?
          yield
        else
          titles = pending_executables.map { |e| RightScale::RightScriptsCookbook.recipe_title(e.nickname) }
          RightScale::AuditorProxy.instance.append_info("Missing inputs for #{titles.join(", ")}, waiting...", :audit_id => @audit_id)
          sleep(20)
          retrieve_missing_inputs(bundle, &cb)
        end
      else
        strand("Failed to retrieve missing inputs", res)
      end
    end
  end

  # Retrieve and run boot scripts
  #
  # === Return
  # true:: Always return true
  def run_boot_bundle(bundle)
    # Cancel suicide timer if it has been primed
    @suicide_timer.cancel unless @suicide_timer.nil?

    # Force full converge on boot so that Chef state gets persisted
    sequence = RightScale::ExecutableSequenceProxy.new(bundle)
    sequence.callback do
      EM.next_tick do
        if patch = sequence.inputs_patch
          RightScale::RequestForwarder.instance.push('/updater/update_inputs', { :agent_identity => @agent_identity,
                                                                                 :patch          => patch })
        end
        RightScale::AuditorProxy.instance.update_status("boot completed: #{bundle}", :audit_id => @audit_id)
        yield RightScale::OperationResult.success
      end
    end
    sequence.errback  do
      EM.next_tick do
        RightScale::AuditorProxy.instance.update_status("boot failed: #{bundle}", :audit_id => @audit_id)
        yield RightScale::OperationResult.error("Failed to run boot bundle")
      end
    end

    # We want to be able to use Chef providers which use EM (e.g. so they can use RightScale::popen3), this means
    # that we need to synchronize the chef thread with the EM thread since providers run synchronously. So create
    # a thread here and run the sequence in it. Use EM.next_tick to switch back to EM's thread.
    EM.defer do
      begin
        sequence.run
      rescue Exception => e
        msg = "Execution of Chef boot sequence failed with exception: #{e.message}"
        RightScale::RightLinkLog.error(msg + "\n" + e.backtrace.join("\n"))
        strand(msg)
      end
    end
    
    true
  end

end
<commit_msg>Add Darwin awareness to InstanceSetup.<commit_after>#
# Copyright (c) 2009 RightScale Inc
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

class InstanceSetup

  include RightScale::Actor

  expose :report_state

  # Amount of seconds to wait between set_r_s_version calls attemps
  RECONNECT_DELAY = 5

  # Amount of seconds to wait before shutting down if boot hasn't completed
  SUICIDE_DELAY = 45 * 60

  # Boot if and only if instance state is 'booting'
  # Prime timer for shutdown on unsuccessful boot ('suicide' functionality)
  #
  # === Parameters
  # options[:agent_identity](String):: Serialized agent identity for current agent
  # options[:auto_shutdown](TrueClass|FalseClass):: Whether instance should shutdown if it cannot boot
  #                                                 under SUICIDE_DELAY and this is the initial boot
  def initialize(options)
    agent_identity = options[:identity]
    RightScale::InstanceState.init(agent_identity)
    should_suicide = options[:auto_shutdown] && RightScale::InstanceState.initial_boot
    @suicide_timer = EM::Timer.new(SUICIDE_DELAY) do
      RightScale::RightLinkLog.error "Shutting down after having tried to boot for #{SUICIDE_DELAY / 60} minutes"
      RightScale::Platform.controller.shutdown 
    end if should_suicide 
    @boot_retries = 0
    @agent_identity = agent_identity
    RightScale::RightLinkLog.force_debug if RightScale::DevState.enabled?
    EM.threadpool_size = 1
    # Schedule boot sequence, don't run it now so agent is registered first
    if RightScale::InstanceState.value == 'booting'
      EM.next_tick { RightScale::RequestForwarder.instance.init { init_boot } }
    else
      RightScale::RequestForwarder.instance.init
    end
  end

  # Retrieve current instance state
  #
  # === Return
  # state(RightScale::OperationResult):: Success operation result containing instance state
  def report_state
    state = RightScale::OperationResult.success(RightScale::InstanceState.value)
  end

  # Handle disconnected notification from broker, enter offline mode
  #
  # === Parameters
  # status(Symbol):: Connection status, one of :connected or :disconnected
  #
  # === Return
  # true:: Always return true
  def connection_status(status)
    if status == :disconnected
      RightScale::RequestForwarder.instance.enable_offline_mode
    else
      RightScale::RequestForwarder.instance.disable_offline_mode
    end
    true
  end

  protected

  # We start off by setting the instance 'r_s_version' in the core site and
  # then proceed with the actual boot sequence
  #
  # === Return
  # true:: Always return true
  def init_boot
    options = { :agent_identity => @agent_identity, :r_s_version => RightScale::RightLinkConfig.protocol_version, :aws_id => RightScale::InstanceState.aws_id }
    RightScale::RequestForwarder.instance.request('/booter/declare', options) do |r|
      res = RightScale::OperationResult.from_results(r)
      if res.success?
        enable_managed_login
      else
        if res.retry?
          RightScale::RightLinkLog.info("RightScale not ready, retrying in #{RECONNECT_DELAY} seconds...")
        else
          RightScale::RightLinkLog.warn("Failed to contact RightScale: #{res.content}, retrying in #{RECONNECT_DELAY} seconds...")
        end
        # Retry in RECONNECT_DELAY seconds, retry forever, nothing else we can do
        EM.add_timer(RECONNECT_DELAY) { init_boot }
      end
    end
    true
  end

  # Enable managed SSH for this instance, then continue with boot. Ensures that
  # managed SSH users can login to troubleshoot stranded and other 'interesting' events
  #
  # === Return
  # true:: Always return true
  def enable_managed_login
    if RightScale::Platform.windows? || RightScale::Platform.darwin? 
      boot
    else
      request('/booter/get_login_policy', {:agent_identity => @agent_identity}) do |r|
        res = RightScale::OperationResult.from_results(r)
        if res.success?
          policy  = res.content
          begin
            audit = RightScale::LoginManager.instance.update_policy(policy)
            if audit
              RightScale::AuditorProxy.instance.create_new_section("Managed login enabled", :audit_id => policy.audit_id)
              RightScale::AuditorProxy.instance.append_info(audit, :audit_id => policy.audit_id)
            end
          rescue Exception => e
            RightScale::AuditorProxy.instance.create_new_section('Failed to enable managed login', :audit_id => policy.audit_id)
            RightScale::AuditorProxy.instance.append_error("Error applying login policy: #{e.message}", 
                                               :category => RightScale::EventCategories::CATEGORY_ERROR,
                                               :audit_id => policy.audit_id)
            RightScale::RightLinkLog.error("#{e.class.name}: #{e.message}\n#{e.backtrace.join("\n")}")
          end
        else
          RightScale::RightLinkLog.error("Could not get login policy: #{res.content}")
        end

        boot
      end
    end
  end

  # Retrieve software repositories and configure mirrors accordingly then proceed to
  # retrieving and running boot bundle.
  #
  # === Return
  # true:: Always return true
  def boot
    request("/booter/get_repositories", @agent_identity) do |r|
      res = RightScale::OperationResult.from_results(r)
      if res.success?
        @audit_id = res.content.audit_id
        unless RightScale::Platform.windows? || RightScale::Platform.darwin?
          reps = res.content.repositories
          audit = "Using the following software repositories:\n"
          reps.each { |rep| audit += "  - #{rep.to_s}\n" }
          RightScale::AuditorProxy.instance.create_new_section("Configuring software repositories", :audit_id => @audit_id)
          RightScale::AuditorProxy.instance.append_info(audit, :audit_id => @audit_id)
          configure_repositories(reps)
          RightScale::AuditorProxy.instance.update_status("Software repositories configured", :audit_id => @audit_id)
        end
        RightScale::AuditorProxy.instance.create_new_section('Preparing boot bundle', :audit_id => @audit_id)
        prepare_boot_bundle do |prep_res|
          if prep_res.success?
            RightScale::AuditorProxy.instance.update_status('Boot bundle ready', :audit_id => @audit_id)
            run_boot_bundle(prep_res.content) do |boot_res|
              if boot_res.success?
                RightScale::InstanceState.value = 'operational'
              else
                strand("Failed to run boot sequence", boot_res)
              end
            end
          else
            strand("Failed to prepare boot bundle", prep_res)
          end
        end
      else
        strand("Failed to retrieve software repositories", res)
      end
    end
    true
  end

  # Log error to local log file and set instance state to stranded
  #
  # === Parameters
  # msg(String):: Error message that will be audited and logged
  # res(RightScale::OperationResult):: Operation result with additional information
  #
  # === Return
  # true:: Always return true
  def strand(msg, res=nil)
    RightScale::InstanceState.value = 'stranded'
    msg += ": #{res.content}" if res && res.content
    RightScale::AuditorProxy.instance.append_error(msg, 
                                                   :category => RightScale::EventCategories::CATEGORY_ERROR,
                                                   :audit_id => @audit_id) if @audit_id
    true
  end

  # Configure software repositories
  # Note: the configurators may return errors when the platform is not what they expect,
  # for now log error and keep going (to replicate legacy behavior).
  #
  # === Parameters
  # repositories(Array[(RepositoryInstantiation)]):: repositories to be configured
  #
  # === Return
  # true:: Always return true
  def configure_repositories(repositories)
    repositories.each do |repo|
      begin
        klass = repo.name.to_const
        unless klass.nil?
          fz = nil
          if repo.frozen_date
            # gives us date for yesterday since the mirror for today may not have been generated yet
            fz = (Date.parse(repo.frozen_date) - 1).to_s
            fz.gsub!(/-/,"")
          end
          klass.generate("none", repo.base_urls, fz)
        end
      rescue Exception => e
        RightScale::RightLinkLog.error(e.message)
      end
    end
    if system('which apt-get')
      ENV['DEBIAN_FRONTEND'] = 'noninteractive' # this prevents prompts
      RightScale::AuditorProxy.instance.append_output(`apt-get update 2>&1`, :audit_id => @audit_id)
    elsif system('which yum') 
      `yum clean metadata`
    end
    true
  end

  # Retrieve missing inputs if any
  #
  # === Block
  # Calls given block passing in one argument of type RightScale::OperationResult
  # The argument contains either a failure with associated message or success
  # with the corresponding boot bundle.
  #
  # === Return
  # true:: Always return true
  def prepare_boot_bundle(&cb)
    RightScale::AgentTagsManager.instance.tags do |tags|
      RightScale::InstanceState.startup_tags = tags
      if tags.empty?
        RightScale::AuditorProxy.instance.append_info("No tags discovered on startup", :audit_id => @audit_id)
      else
        RightScale::AuditorProxy.instance.append_info("Tags discovered on startup: '#{tags.join("', '")}'", :audit_id => @audit_id)
      end
      if @suicide_timer && !tags.include?('rs_launch:type=auto')
        # Someone removed the tag explicitely, cancel the timer
        @suicide_timer.cancel
        @suicide_timer = nil
      end
      options = { :agent_identity => @agent_identity, :audit_id => @audit_id }
      request("/booter/get_boot_bundle", options) do |r|
        res = RightScale::OperationResult.from_results(r)
        if res.success?
          bundle = res.content
          if bundle.executables.any? { |e| !e.ready }
            retrieve_missing_inputs(bundle) { cb.call(RightScale::OperationResult.success(bundle)) }
          else
            yield RightScale::OperationResult.success(bundle)
          end
        else
          msg = "Failed to retrieve boot scripts"
          msg += ": #{res.content}" if res.content
          yield RightScale::OperationResult.error(msg)
        end
      end
    end
  end

  # Retrieve missing inputs for recipes and RightScripts stored in
  # @recipes and @scripts respectively, update recipe attributes, RightScript
  # parameters and ready fields (set ready field to true if attempt was successful,
  # false otherwise).
  # This is for environment variables that we are waiting on.
  # Retries forever.
  #
  # === Block
  # Continuation block, will be called once attempt to retrieve attributes is completed
  #
  # === Return
  # true:: Always return true
  def retrieve_missing_inputs(bundle, &cb)
    scripts = bundle.executables.select { |e| e.is_a?(RightScale::RightScriptInstantiation) }
    recipes = bundle.executables.select { |e| e.is_a?(RightScale::RecipeInstantiation) }
    scripts_ids = scripts.select { |s| !s.ready }.map { |s| s.id }
    recipes_ids = recipes.select { |r| !r.ready }.map { |r| r.id }
    RightScale::RequestForwarder.instance.request('/booter/get_missing_attributes', { :agent_identity => @agent_identity,
                                                                             :scripts_ids    => scripts_ids,
                                                                             :recipes_ids    => recipes_ids }) do |r|
      res = RightScale::OperationResult.from_results(r)
      if res.success?
        res.content.each do |e|
          if e.is_a?(RightScale::RightScriptInstantiation)
            if script = scripts.detect { |s| s.id == e.id }
              script.ready = true
              script.parameters = e.parameters
            end
          else
            if recipe = recipes.detect { |s| s.id == e.id }
              recipe.ready = true
              recipe.attributes = e.attributes
            end
          end
        end
        pending_executables = bundle.executables.select { |e| !e.ready }
        if pending_executables.empty?
          yield
        else
          titles = pending_executables.map { |e| RightScale::RightScriptsCookbook.recipe_title(e.nickname) }
          RightScale::AuditorProxy.instance.append_info("Missing inputs for #{titles.join(", ")}, waiting...", :audit_id => @audit_id)
          sleep(20)
          retrieve_missing_inputs(bundle, &cb)
        end
      else
        strand("Failed to retrieve missing inputs", res)
      end
    end
  end

  # Retrieve and run boot scripts
  #
  # === Return
  # true:: Always return true
  def run_boot_bundle(bundle)
    # Cancel suicide timer if it has been primed
    @suicide_timer.cancel unless @suicide_timer.nil?

    # Force full converge on boot so that Chef state gets persisted
    sequence = RightScale::ExecutableSequenceProxy.new(bundle)
    sequence.callback do
      EM.next_tick do
        if patch = sequence.inputs_patch
          RightScale::RequestForwarder.instance.push('/updater/update_inputs', { :agent_identity => @agent_identity,
                                                                                 :patch          => patch })
        end
        RightScale::AuditorProxy.instance.update_status("boot completed: #{bundle}", :audit_id => @audit_id)
        yield RightScale::OperationResult.success
      end
    end
    sequence.errback  do
      EM.next_tick do
        RightScale::AuditorProxy.instance.update_status("boot failed: #{bundle}", :audit_id => @audit_id)
        yield RightScale::OperationResult.error("Failed to run boot bundle")
      end
    end

    # We want to be able to use Chef providers which use EM (e.g. so they can use RightScale::popen3), this means
    # that we need to synchronize the chef thread with the EM thread since providers run synchronously. So create
    # a thread here and run the sequence in it. Use EM.next_tick to switch back to EM's thread.
    EM.defer do
      begin
        sequence.run
      rescue Exception => e
        msg = "Execution of Chef boot sequence failed with exception: #{e.message}"
        RightScale::RightLinkLog.error(msg + "\n" + e.backtrace.join("\n"))
        strand(msg)
      end
    end
    
    true
  end

end

{'fraction_non_alphanumeric': 0.08915717837979321, 'fraction_numerical': 0.0012432927627273917, 'mean_word_length': 2.7115968427443837, 'pattern_counts': {'":': 4, '<': 3, '<?xml version=': 0, '>': 69, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 38, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '2856952', 'n_tokens_mistral': 8413, 'n_tokens_neox': 8222, 'n_words': 2478}