class Kitchen::Provisioner::SaltSolo

Basic Salt Masterless Provisioner, based on work by

@author Chris Lundquist (<chris.ludnquist@github.com>)

Constants

DEFAULT_CONFIG
RETCODE_VERSION

salt-call version that supports the undocumented –retcode-passthrough command

WIN_DEFAULT_CONFIG

Public Instance Methods

create_sandbox() click to toggle source
Calls superclass method
# File lib/kitchen/provisioner/salt_solo.rb, line 204
def create_sandbox
  super
  prepare_data
  prepare_gpg_key
  prepare_install
  prepare_minion
  prepare_pillars
  prepare_grains
  prepare_states
  prepare_state_top
  prepare_cache_commands
  # upload scripts, cached formulas, and setup system repositories
  prepare_dependencies
end
init_command() click to toggle source
# File lib/kitchen/provisioner/salt_solo.rb, line 246
      def init_command
        debug("Initialising Driver #{name}")
        cmd = if windows_os?
                'mkdir -Force -Path '"#{config[:root_path]}""\n"
              else
                "mkdir -p '#{config[:root_path]}';"
              end
        cmd += <<-INSTALL
          #{config[:init_environment]}
        INSTALL
        cmd
      end
install_chef() click to toggle source
# File lib/kitchen/provisioner/salt_solo.rb, line 163
      def install_chef
        return unless config[:require_chef]
        chef_url = config[:chef_bootstrap_url]
        if windows_os?
          <<-POWERSHELL
            if (-Not $(test-path c:\\opscode\\chef)) {
              if (-Not $(Test-Path c:\\temp)) {
                New-Item -Path c:\\temp -itemtype directory
              }
              [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
              (New-Object net.webclient).DownloadFile("#{chef_url}", "c:\\temp\\chef_bootstrap.ps1")
              write-host "-----> Installing Chef Omnibus (for busser/serverspec ruby support)"
              #{sudo('powershell')} c:\\temp\\chef_bootstrap.ps1
            }
          POWERSHELL
        else
          omnibus_download_dir = config[:omnibus_cachier] ? '/tmp/vagrant-cache/omnibus_chef' : '/tmp'
          bootstrap_url = config[:bootstrap_url]
          bootstrap_download_dir = '/tmp'
          <<-INSTALL
              echo "-----> Trying to install ruby(-dev) using assets.sh from kitchen-salt"
                mkdir -p #{bootstrap_download_dir}
                if [ ! -x #{bootstrap_download_dir}/install.sh ]
                then
                  do_download #{bootstrap_url} #{bootstrap_download_dir}/install.sh
                fi
                #{sudo('sh')} #{bootstrap_download_dir}/install.sh -d #{bootstrap_download_dir}
              if [ $? -ne 0 ] || [ ! -d "/opt/chef" ]
              then
                echo "Failed install ruby(-dev) using assets.sh from kitchen-salt"
                echo "-----> Fallback to Chef Bootstrap script (for busser/serverspec ruby support)"
                mkdir -p "#{omnibus_download_dir}"
                if [ ! -x #{omnibus_download_dir}/install.sh ]
                then
                    #{sudo('sh')} #{omnibus_download_dir}/install.sh -d #{omnibus_download_dir}
                fi;
              fi
          INSTALL
        end
      end
install_command() click to toggle source
# File lib/kitchen/provisioner/salt_solo.rb, line 110
def install_command
  return unless config[:salt_install]
  unless config[:salt_install] == 'pip' || config[:install_after_init_environment]
    setup_salt
  end
end
os_join(*args) click to toggle source
# File lib/kitchen/provisioner/salt_solo.rb, line 259
def os_join(*args)
  if windows_os?
     File.join(*args).tr('/', '\\')
  else
     File.join(*args)
  end
end
prepare_command() click to toggle source
# File lib/kitchen/provisioner/salt_solo.rb, line 117
      def prepare_command
        cmd = ''
        unless windows_os?
          cmd += <<-CHOWN
            #{sudo('chown')} -R "${SUDO_USER:-$USER}" "#{config[:root_path]}/#{config[:salt_file_root]}"
          CHOWN
        end
        if config[:prepare_salt_environment]
          cmd += <<-PREPARE
            #{config[:prepare_salt_environment]}
          PREPARE
        end
        return cmd unless config[:salt_install]
        if config[:salt_install] == 'pip' || config[:install_after_init_environment]
          cmd << setup_salt
        end
        cmd
      end
prepare_install() click to toggle source
# File lib/kitchen/provisioner/salt_solo.rb, line 219
def prepare_install
  return unless config[:salt_install]
  salt_version = config[:salt_version]
  if config[:salt_install] == 'pip'
    debug('Using pip to install')
    if File.exist?(config[:pip_pkg])
      debug('Installing with pip from sdist')
      sandbox_pip_path = File.join(sandbox_path, 'pip')
      FileUtils.mkdir_p(sandbox_pip_path)
      FileUtils.cp_r(config[:pip_pkg], sandbox_pip_path)
      config[:pip_install] = File.join(config[:root_path], 'pip', File.basename(config[:pip_pkg]))
    else
      debug('Installing with pip from download')
      if salt_version != 'latest'
        config[:pip_install] = format(config[:pip_pkg], salt_version)
      else
        config[:pip_pkg].slice!('==%s')
        config[:pip_install] = config[:pip_pkg]
      end
    end
  elsif config[:salt_install] == 'bootstrap'
    if File.exist?(config[:salt_bootstrap_url])
      FileUtils.cp_r(config[:salt_bootstrap_url], File.join(sandbox_path, 'bootstrap.sh'))
    end
  end
end
run_command() click to toggle source
# File lib/kitchen/provisioner/salt_solo.rb, line 307
def run_command
  debug("running driver #{name}")
  debug(diagnose)

  return unless config[:run_salt_call]

  # config[:salt_version] can be 'latest' or 'x.y.z', 'YYYY.M.x' etc
  # error return codes are a mess in salt:
  #  https://github.com/saltstack/salt/pull/11337
  # Unless we know we have a version that supports --retcode-passthrough
  # attempt to scan the output for signs of failure
  if "#{config[:salt_version]}" <= RETCODE_VERSION
    # scan the output for signs of failure, there is a risk of false negatives
    fail_grep = 'grep -e Result.*False -e Data.failed.to.compile -e No.matching.sls.found.for'
    # capture any non-zero exit codes from the salt-call | tee pipe
    cmd = 'set -o pipefail ; ' << salt_command
    # Capture the salt-call output & exit code
    cmd << ' 2>&1 | tee /tmp/salt-call-output ; SC=$? ; echo salt-call exit code: $SC ;'
    # check the salt-call output for fail messages
    cmd << " (sed '/#{fail_grep}/d' /tmp/salt-call-output | #{fail_grep} ; EC=$? ; echo salt-call output grep exit code ${EC} ;"
    # use the non-zer exit code from salt-call, then invert the results of the grep for failures
    cmd << ' [ ${SC} -ne 0 ] && exit ${SC} ; [ ${EC} -eq 0 ] && exit 1 ; [ ${EC} -eq 1 ] && exit 0)'
    cmd
  else
    salt_command
  end
end
salt_command() click to toggle source
# File lib/kitchen/provisioner/salt_solo.rb, line 267
def salt_command
  salt_version = config[:salt_version]

  cmd = ''
  if windows_os?
    salt_config_path = config[:salt_config]
    cmd << "(get-content #{os_join(config[:root_path], salt_config_path, 'minion')}) -replace '\\$env:TEMP', $env:TEMP | set-content #{os_join(config[:root_path], salt_config_path, 'minion')} ;"
  else
    # install/update dependencies
    cmd << sudo("chmod +x #{config[:root_path]}/*.sh;")
    cmd << sudo("#{config[:root_path]}/dependencies.sh;")
    cmd << sudo("#{config[:root_path]}/gpgkey.sh;") if config[:gpg_key]
    salt_config_path = config[:salt_config]
  end

  if config[:pre_salt_command]
    cmd << "#{config[:pre_salt_command]} && "
  end
  cmd << sudo("#{salt_call}")
  state_output = config[:salt_minion_extra_config][:state_output]
  if state_output
    cmd << " --state-output=#{state_output}"
  else
    cmd << " --state-output=changes"
  end
  cmd << " --config-dir=#{os_join(config[:root_path], salt_config_path)}"
  cmd << " state.highstate"
  cmd << " --log-level=#{config[:log_level]}" if config[:log_level]
  cmd << " --id=#{config[:salt_minion_id]}" if config[:salt_minion_id]
  cmd << " test=#{config[:dry_run]}" if config[:dry_run]
  cmd << ' --force-color' if config[:salt_force_color]
  cmd << ' --no-color' if not config[:salt_enable_color]
  if "#{salt_version}" > RETCODE_VERSION || salt_version == 'latest'
    # hope for the best and hope it works eventually
    cmd << ' --retcode-passthrough'
  end
  cmd << ' 2>&1 ; exit $LASTEXITCODE' if windows_os?
  cmd
end
setup_salt() click to toggle source
# File lib/kitchen/provisioner/salt_solo.rb, line 136
def setup_salt
  debug(diagnose)
  salt_version = config[:salt_version]

  # if salt_verison is set, bootstrap is being used & bootstrap_options is empty,
  # set the bootstrap_options string to git install the requested version
  if (salt_version != 'latest') && (config[:salt_install] == 'bootstrap') && config[:salt_bootstrap_options].empty?
    if windows_os?
      debug("Using bootstrap to install #{salt_version}")
      config[:salt_bootstrap_options] = "-version #{salt_version}"
    else
      debug("Using bootstrap git to install #{salt_version}")
      config[:salt_bootstrap_options] = "-P git v#{salt_version}"
    end
  end

  install_template = if windows_os?
                       File.expand_path('./../install_win.erb', __FILE__)
                     else
                       File.expand_path('./../install.erb', __FILE__)
                     end

  erb = ERB.new(File.read(install_template)).result(binding)
  debug('Install Command:' + erb.to_s)
  erb
end

Protected Instance Methods

insert_minion_config_dropins() click to toggle source
# File lib/kitchen/provisioner/salt_solo.rb, line 393
def insert_minion_config_dropins
  sandbox_dropin_path = File.join(sandbox_path, 'etc/salt/minion.d')
  FileUtils.mkdir_p(sandbox_dropin_path)

  config[:salt_minion_config_dropin_files].each_index do |i|
    filename = File.basename(config[:salt_minion_config_dropin_files][i])
    index = (99 - config[:salt_minion_config_dropin_files].count + i).to_s.rjust(2, '0')

    file = File.expand_path(config[:salt_minion_config_dropin_files][i])
    data = File.read(file)

    write_raw_file(File.join(sandbox_dropin_path, [index, filename].join('-')), data)
  end
end
prepare_cache_commands() click to toggle source
# File lib/kitchen/provisioner/salt_solo.rb, line 471
def prepare_cache_commands
  ctx = to_hash
  config[:cache_commands].each do |cmd|
    system(cmd % ctx)
    if $?.exitstatus.nonzero?
      raise ActionFailed,
        "cache_command '#{cmd}' failed to execute (exit status #{$?.exitstatus})"
    end
  end
end
prepare_data() click to toggle source
# File lib/kitchen/provisioner/salt_solo.rb, line 337
def prepare_data
  return unless config[:data_path]

  info('Preparing data')
  debug("Using data from #{config[:data_path]}")

  tmpdata_dir = File.join(sandbox_path, 'data')
  FileUtils.mkdir_p(tmpdata_dir)
  cp_r_with_filter(config[:data_path], tmpdata_dir, config[:salt_copy_filter])
end
prepare_dependencies() click to toggle source
# File lib/kitchen/provisioner/salt_solo.rb, line 429
      def prepare_dependencies
        # Dependency scripts are bash scripts only
        # Copying them clobbers the kitchen temp directory
        # with a file named `kitchen`. If adding Windows
        # support for dependencies, relocate into a
        # sub-directory
        return if windows_os?

        # upload scripts
        sandbox_scripts_path = File.join(sandbox_path, config[:salt_config], 'scripts')
        info("Preparing scripts into #{config[:salt_config]}/scripts")

        # PLACEHOLDER, git formulas might be fetched locally to temp and uploaded

        # setup spm
        spm_template = File.expand_path('./../spm.erb', __FILE__)
        spm_config_content = ERB.new(File.read(spm_template)).result(binding)
        sandbox_spm_config_path = File.join(sandbox_path, config[:salt_config], 'spm')
        write_raw_file(sandbox_spm_config_path, spm_config_content)

        spm_repos = config[:vendor_repo].select { |x| x[:type] == 'spm' }.each { |x| x[:url] }.map { |x| x[:url] }
        spm_repos.each do |url|
          id = url.gsub(/[htp:\/.]/, '')
          spmreposd = File.join(sandbox_path, 'etc', 'salt', 'spm.repos.d')
          repo_spec = File.join(spmreposd, 'spm.repo')
          FileUtils.mkdir_p(spmreposd)
          repo_content = "
#{id}:
  url: #{url}
"
          write_raw_file(repo_spec, repo_content)
        end

        # upload scripts
        %w[formula-fetch.sh repository-setup.sh].each do |script|
          write_raw_file(File.join(sandbox_path, script), File.read(File.expand_path("../#{script}", __FILE__)))
        end
        dependencies_script = File.expand_path('./../dependencies.erb', __FILE__)
        dependencies_content = ERB.new(File.read(dependencies_script)).result(binding)
        write_raw_file(File.join(sandbox_path, 'dependencies.sh'), dependencies_content)
      end
prepare_gpg_key() click to toggle source
# File lib/kitchen/provisioner/salt_solo.rb, line 348
def prepare_gpg_key
  return unless config[:gpg_key]

  info('Preparing gpg_key')
  debug("Using gpg key: #{config[:gpg_key]}")

  system("gpg --homedir #{config[:gpg_home]} -o #{File.join(sandbox_path, 'gpgkey.txt')} --armor --export-secret-keys #{config[:gpg_key]}")

  gpg_template = File.expand_path('./../gpgkey.erb', __FILE__)
  erb = ERB.new(File.read(gpg_template)).result(binding)
  debug('Install Command:' + erb.to_s)
  write_raw_file(File.join(sandbox_path, 'gpgkey.sh'), erb)
end
prepare_grains() click to toggle source
# File lib/kitchen/provisioner/salt_solo.rb, line 415
def prepare_grains
  debug("Grains Hash: #{config[:grains]}")

  return if config[:grains].nil?

  info("Preparing grains into #{config[:salt_config]}/grains")

  # generate the filename
  sandbox_grains_path = File.join(sandbox_path, config[:salt_config], 'grains')
  debug("sandbox_grains_path: #{sandbox_grains_path}")

  write_hash_file(sandbox_grains_path, config[:grains])
end
prepare_minion() click to toggle source
# File lib/kitchen/provisioner/salt_solo.rb, line 408
def prepare_minion
  info('Preparing salt-minion')
  prepare_minion_base_config
  prepare_minion_extra_config if config[:salt_minion_extra_config].keys.any?
  insert_minion_config_dropins if config[:salt_minion_config_dropin_files].any?
end
prepare_minion_base_config() click to toggle source
# File lib/kitchen/provisioner/salt_solo.rb, line 362
def prepare_minion_base_config
  if config[:salt_minion_config_template]
    minion_template = File.expand_path(config[:salt_minion_config_template], Kitchen::Config.new.kitchen_root)
  else
    minion_template = File.expand_path('./../minion.erb', __FILE__)
  end

  minion_config_content = if File.extname(minion_template) == '.erb'
                            ERB.new(File.read(minion_template)).result(binding)
                          else
                            File.read(minion_template)
                          end

  # create the temporary path for the salt-minion config file
  debug("sandbox is #{sandbox_path}")
  sandbox_minion_config_path = File.join(sandbox_path, config[:salt_minion_config])

  write_raw_file(sandbox_minion_config_path, minion_config_content)
end
prepare_minion_extra_config() click to toggle source
# File lib/kitchen/provisioner/salt_solo.rb, line 382
def prepare_minion_extra_config
  minion_template = File.expand_path('./../99-minion.conf.erb', __FILE__)

  safe_hash = Hashie.stringify_keys(config[:salt_minion_extra_config])
  minion_extra_config_content = ERB.new(File.read(minion_template)).result(binding)

  sandbox_dropin_path = File.join(sandbox_path, 'etc/salt/minion.d')

  write_raw_file(File.join(sandbox_dropin_path, '99-minion.conf'), minion_extra_config_content)
end
to_hash() click to toggle source
# File lib/kitchen/provisioner/salt_solo.rb, line 482
def to_hash
  hash = Hash.new
  instance_variables.each {|var| hash[var[1..-1]] = instance_variable_get(var) }
  hash.map{|k,v| [k.to_s.to_sym,v]}.to_h
end