├── CODEOWNERS ├── lib ├── beaker │ └── dsl │ │ ├── helpers │ │ ├── facter_helpers.rb │ │ ├── puppet_helpers.rb │ │ └── tk_helpers.rb │ │ └── install_utils │ │ ├── aio_defaults.rb │ │ ├── ezbake_utils.rb │ │ ├── foss_defaults.rb │ │ ├── foss_utils.rb │ │ ├── module_utils.rb │ │ └── puppet_utils.rb ├── beaker-puppet │ ├── version.rb │ ├── helpers │ │ ├── rake_helpers.rb │ │ ├── host_helpers.rb │ │ ├── facter_helpers.rb │ │ └── tk_helpers.rb │ ├── wrappers.rb │ └── install_utils │ │ ├── aio_defaults.rb │ │ ├── ezbake_utils.rb │ │ ├── puppet_utils.rb │ │ ├── foss_defaults.rb │ │ ├── puppet5.rb │ │ ├── module_utils.rb │ │ └── windows_utils.rb └── beaker-puppet.rb ├── .github_changelog_generator ├── .rubocop.yml ├── setup ├── common │ ├── 025_StopFirewall.rb │ ├── 045_EnsureMasterStarted.rb │ ├── 000-delete-puppet-when-none.rb │ ├── 030_StopSssd.rb │ ├── 011_Install_Puppet_Server.rb │ ├── 005_redhat_subscription_fix.rb │ ├── 012_Finalize_Installs.rb │ ├── 040_ValidateSignCert.rb │ └── 003_solaris_cert_fix.rb ├── git │ ├── 020_PuppetUserAndGroup.rb │ ├── 060_InstallModules.rb │ ├── 010_TestSetup.rb │ ├── 070_InstallCACerts.rb │ └── 000_EnvSetup.rb ├── aio │ └── 010_Install_Puppet_Agent.rb └── gem │ └── 010_GemInstall.rb ├── acceptance ├── config │ ├── acceptance-options.rb │ ├── nodes │ │ └── vagrant-ubuntu-1404.yml │ ├── pkg │ │ └── acceptance-options.rb │ ├── gem │ │ └── acceptance-options.rb │ └── git │ │ └── acceptance-options.rb ├── tests │ ├── README.md │ ├── with_puppet_running_on.rb │ ├── backwards_compatible.rb │ ├── install_smoke_test.rb │ ├── clone_git_repo_on_test.rb │ ├── stub_host.rb │ ├── create_tmpdir_on_test.rb │ └── web_helpers_test.rb └── pre_suite │ ├── gem │ └── install.rb │ ├── pkg │ └── install.rb │ └── git │ └── install.rb ├── .simplecov ├── Gemfile ├── .gitignore ├── .github ├── dependabot.yml └── workflows │ ├── release.yml │ └── test.yml ├── release-prep.sh ├── bin └── beaker-puppet ├── spec ├── beaker-puppet │ ├── wrappers_spec.rb │ ├── helpers │ │ ├── host_helpers_spec.rb │ │ ├── tk_helpers_spec.rb │ │ └── facter_helpers_spec.rb │ └── install_utils │ │ ├── ezbake_utils_spec.rb │ │ ├── puppet_utils_spec.rb │ │ └── windows_utils_spec.rb ├── spec_helper.rb └── helpers.rb ├── beaker-puppet.gemspec ├── README.md ├── Rakefile └── LICENSE /CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @puppetlabs/phoenix @puppetlabs/unicorn -------------------------------------------------------------------------------- /lib/beaker/dsl/helpers/facter_helpers.rb: -------------------------------------------------------------------------------- 1 | require 'beaker-puppet' 2 | -------------------------------------------------------------------------------- /lib/beaker/dsl/helpers/puppet_helpers.rb: -------------------------------------------------------------------------------- 1 | require 'beaker-puppet' 2 | -------------------------------------------------------------------------------- /lib/beaker/dsl/helpers/tk_helpers.rb: -------------------------------------------------------------------------------- 1 | require 'beaker-puppet' 2 | -------------------------------------------------------------------------------- /lib/beaker/dsl/install_utils/aio_defaults.rb: -------------------------------------------------------------------------------- 1 | require 'beaker-puppet' 2 | -------------------------------------------------------------------------------- /lib/beaker/dsl/install_utils/ezbake_utils.rb: -------------------------------------------------------------------------------- 1 | require 'beaker-puppet' 2 | -------------------------------------------------------------------------------- /lib/beaker/dsl/install_utils/foss_defaults.rb: -------------------------------------------------------------------------------- 1 | require 'beaker-puppet' 2 | -------------------------------------------------------------------------------- /lib/beaker/dsl/install_utils/foss_utils.rb: -------------------------------------------------------------------------------- 1 | require 'beaker-puppet' 2 | -------------------------------------------------------------------------------- /lib/beaker/dsl/install_utils/module_utils.rb: -------------------------------------------------------------------------------- 1 | require 'beaker-puppet' 2 | -------------------------------------------------------------------------------- /lib/beaker/dsl/install_utils/puppet_utils.rb: -------------------------------------------------------------------------------- 1 | require 'beaker-puppet' 2 | -------------------------------------------------------------------------------- /lib/beaker-puppet/version.rb: -------------------------------------------------------------------------------- 1 | module BeakerPuppet 2 | VERSION = '4.2.0' 3 | end 4 | -------------------------------------------------------------------------------- /.github_changelog_generator: -------------------------------------------------------------------------------- 1 | project=beaker-puppet 2 | user=puppetlabs 3 | exclude_labels=maintenance 4 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | --- 2 | inherit_from: .rubocop_todo.yml 3 | 4 | inherit_gem: 5 | voxpupuli-rubocop: rubocop.yml 6 | -------------------------------------------------------------------------------- /setup/common/025_StopFirewall.rb: -------------------------------------------------------------------------------- 1 | test_name 'Stop firewall' do 2 | skip_test 'not testing with puppetserver' unless @options['is_puppetserver'] 3 | stop_firewall_with_puppet_on(hosts) 4 | end 5 | -------------------------------------------------------------------------------- /acceptance/config/acceptance-options.rb: -------------------------------------------------------------------------------- 1 | { 2 | load_path: File.join('acceptance', 'lib'), 3 | ssh: { 4 | keys: ['id_rsa_acceptance', "#{ENV.fetch('HOME', nil)}/.ssh/id_rsa-acceptance"], 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /.simplecov: -------------------------------------------------------------------------------- 1 | SimpleCov.configure do 2 | add_filter 'spec/' 3 | add_filter 'vendor/' 4 | add_filter do |file| 5 | file.lines_of_code < 10 6 | end 7 | end 8 | 9 | SimpleCov.start if ENV['BEAKER_PUPPET_COVERAGE'] 10 | -------------------------------------------------------------------------------- /acceptance/tests/README.md: -------------------------------------------------------------------------------- 1 | # Beaker Acceptance Tests: puppet 2 | 3 | Tests that depend upon Puppet being installed on the SUTs. 4 | 5 | As of Beaker 4.0, you must `require 'beaker-puppet'` in each of these test files in order to access beaker-puppet DSL extensions. -------------------------------------------------------------------------------- /setup/common/045_EnsureMasterStarted.rb: -------------------------------------------------------------------------------- 1 | test_name 'Ensure the master is running' 2 | skip_test 'not testing with puppetserver' unless @options['is_puppetserver'] 3 | 4 | on(master, puppet('resource', 'service', master['puppetservice'], 'ensure=running', 'enable=true')) 5 | -------------------------------------------------------------------------------- /acceptance/config/nodes/vagrant-ubuntu-1404.yml: -------------------------------------------------------------------------------- 1 | HOSTS: 2 | ubuntu-server-1404-x64: 3 | roles: 4 | - master 5 | platform: ubuntu-14.04-amd64 6 | box: puppetlabs/ubuntu-14.04-64-nocm 7 | box_url: https://vagrantcloud.com/puppetlabs/boxes/ubuntu-14.04-64-nocm 8 | hypervisor: vagrant 9 | -------------------------------------------------------------------------------- /acceptance/config/pkg/acceptance-options.rb: -------------------------------------------------------------------------------- 1 | { 2 | type: 'foss', 3 | is_puppetserver: false, 4 | puppetservice: 'puppet.service', 5 | pre_suite: 'acceptance/pre_suite/pkg/install.rb', 6 | tests: 'acceptance/tests', 7 | 'master-start-curl-retries': 30, 8 | }.merge(eval(File.read('acceptance/config/acceptance-options.rb'))) 9 | -------------------------------------------------------------------------------- /setup/common/000-delete-puppet-when-none.rb: -------------------------------------------------------------------------------- 1 | test_name 'Expunge puppet bits if hypervisor is none' 2 | 3 | # Ensure that the any previous installations of puppet 4 | # are removed from the host if it is not managed by a 5 | # provisioning hypervisor. 6 | 7 | hosts.each do |host| 8 | remove_puppet_on(host) if host[:hypervisor] == 'none' 9 | end 10 | -------------------------------------------------------------------------------- /setup/git/020_PuppetUserAndGroup.rb: -------------------------------------------------------------------------------- 1 | test_name 'Puppet User and Group' do 2 | hosts.each do |host| 3 | next if host['use_existing_container'] 4 | 5 | step 'ensure puppet user and group added to all nodes because this is what the packages do' do 6 | on host, puppet('resource user puppet ensure=present') 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /acceptance/config/gem/acceptance-options.rb: -------------------------------------------------------------------------------- 1 | { 2 | type: 'foss', 3 | add_el_extras: 'true', 4 | is_puppetserver: false, 5 | puppetservice: 'puppet.service', 6 | pre_suite: 'acceptance/pre_suite/gem/install.rb', 7 | tests: 'acceptance/tests', 8 | 'master-start-curl-retries': 30, 9 | }.merge(eval(File.read('acceptance/config/acceptance-options.rb'))) 10 | -------------------------------------------------------------------------------- /acceptance/config/git/acceptance-options.rb: -------------------------------------------------------------------------------- 1 | { 2 | type: 'foss', 3 | add_el_extras: 'true', 4 | is_puppetserver: false, 5 | puppetservice: 'puppet.service', 6 | pre_suite: 'acceptance/pre_suite/git/install.rb', 7 | tests: 'acceptance/tests', 8 | 'master-start-curl-retries': 30, 9 | }.merge(eval(File.read('acceptance/config/acceptance-options.rb'))) 10 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source ENV['GEM_SOURCE'] || 'https://rubygems.org' 2 | 3 | gemspec 4 | 5 | group :release do 6 | gem 'faraday-retry', require: false 7 | gem 'github_changelog_generator', require: false 8 | end 9 | 10 | group :coverage, optional: ENV['COVERAGE'] != 'yes' do 11 | gem 'codecov', require: false 12 | gem 'simplecov-console', require: false 13 | end 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | log/* 3 | !.gitignore 4 | junit 5 | acceptance-tests 6 | Gemfile.lock 7 | options.rb 8 | test.cfg 9 | .yardoc 10 | yard_docs 11 | coverage 12 | .bundle 13 | .vendor 14 | _vendor 15 | tmp/ 16 | doc 17 | # JetBrains IDEA 18 | *.iml 19 | .idea/ 20 | # rbenv file 21 | .ruby-version 22 | .ruby-gemset 23 | # Vagrant folder 24 | .vagrant/ 25 | .vagrant_files/ 26 | -------------------------------------------------------------------------------- /acceptance/pre_suite/gem/install.rb: -------------------------------------------------------------------------------- 1 | require 'beaker-puppet' 2 | 3 | hosts.each do |host| 4 | install_puppet_from_gem(host, { version: '3.8.7' }) 5 | next if host['platform'] =~ /windows/ 6 | 7 | on(host, "touch #{File.join(host.puppet['confdir'], 'puppet.conf')}") 8 | on(host, puppet('resource user puppet ensure=present')) 9 | on(host, puppet('resource group puppet ensure=present')) 10 | end 11 | -------------------------------------------------------------------------------- /setup/common/030_StopSssd.rb: -------------------------------------------------------------------------------- 1 | test_name 'Stop sssd' do 2 | # The sssd service causes local users/groups to be cached, 3 | # which can cause unexpected results when tests are trying 4 | # to restore state. We ensure that it is not running to 5 | # prevent such caching from occurring. 6 | hosts.each do |host| 7 | on(host, puppet('resource', 'service', 'sssd', 'ensure=stopped'), accept_all_exit_codes: true) 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | # raise PRs for gem updates 4 | - package-ecosystem: bundler 5 | directory: "/" 6 | schedule: 7 | interval: daily 8 | time: "13:00" 9 | open-pull-requests-limit: 10 10 | 11 | # Maintain dependencies for GitHub Actions 12 | - package-ecosystem: github-actions 13 | directory: "/" 14 | schedule: 15 | interval: daily 16 | time: "13:00" 17 | open-pull-requests-limit: 10 18 | -------------------------------------------------------------------------------- /setup/common/011_Install_Puppet_Server.rb: -------------------------------------------------------------------------------- 1 | test_name 'Install Puppet Server' do 2 | skip_test 'not testing with puppetserver' unless @options['is_puppetserver'] 3 | 4 | opts = { 5 | version: ENV.fetch('SERVER_VERSION', nil), 6 | release_stream: ENV.fetch('RELEASE_STREAM', nil), 7 | nightly_builds_url: ENV.fetch('NIGHTLY_BUILDS_URL', nil), 8 | dev_builds_url: ENV.fetch('DEV_BUILDS_URL', nil), 9 | } 10 | install_puppetserver_on(master, opts) unless master['use_existing_container'] 11 | end 12 | -------------------------------------------------------------------------------- /acceptance/pre_suite/pkg/install.rb: -------------------------------------------------------------------------------- 1 | require 'beaker-puppet' 2 | 3 | # the version is required on windows 4 | # all versions are required for osx 5 | hosts.each do |host| 6 | install_puppet_on(host, { 7 | version: ENV['BEAKER_PUPPET_VERSION'] || '5.5.20', 8 | puppet_agent_version: ENV['BEAKER_PUPPET_AGENT_VERSION'] || '5.5.20', 9 | }) 10 | 11 | on(host, puppet('resource user puppet ensure=present')) 12 | on(host, puppet('resource group puppet ensure=present')) 13 | end 14 | -------------------------------------------------------------------------------- /setup/common/005_redhat_subscription_fix.rb: -------------------------------------------------------------------------------- 1 | test_name 'Refresh Red Hat 8 subscription repository' 2 | 3 | # Only need to run this on Red Hat Enterprise Linux 8 on little-endian PowerPC 4 | skip_test 'Not Red Hat 8 PPCle' unless hosts.any? { |host| host.platform == 'el-8-ppc64le' } 5 | 6 | hosts.each do |host| 7 | next unless host.platform == 'el-8-ppc64le' 8 | 9 | on(host, 10 | '/usr/sbin/subscription-manager repos --disable rhel-8-for-ppc64le-baseos-rpms && /usr/sbin/subscription-manager repos --enable rhel-8-for-ppc64le-baseos-rpms') 11 | end 12 | -------------------------------------------------------------------------------- /release-prep.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Update Gemfile.lock 4 | docker run -t --rm \ 5 | -v $(pwd):/app \ 6 | ruby:3.1.4 \ 7 | /bin/bash -c 'apt-get update -qq && apt-get install -y --no-install-recommends git make netbase && cd /app && gem install bundler && bundle install --jobs 3; echo "LOCK_FILE_UPDATE_EXIT_CODE=$?"' 8 | 9 | docker run -t --rm -e CHANGELOG_GITHUB_TOKEN -v $(pwd):/usr/local/src/your-app \ 10 | githubchangeloggenerator/github-changelog-generator:1.16.2 \ 11 | github_changelog_generator --future-release $(grep VERSION lib/beaker-puppet/version.rb |rev |cut -d "'" -f 2 |rev) 12 | -------------------------------------------------------------------------------- /lib/beaker-puppet/helpers/rake_helpers.rb: -------------------------------------------------------------------------------- 1 | module Beaker 2 | module DSL 3 | module Helpers 4 | # Methods that help you interact with rake during ci setup 5 | module RakeHelpers 6 | class << self 7 | def load_tasks(beaker_root = File.expand_path("#{__dir__}/../../..")) 8 | task_dir = File.join(beaker_root, 'tasks') 9 | tasks = [ 10 | 'ci.rake', 11 | ] 12 | 13 | tasks.each do |task| 14 | load File.join(task_dir, task) 15 | end 16 | end 17 | end 18 | end 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /acceptance/tests/with_puppet_running_on.rb: -------------------------------------------------------------------------------- 1 | require 'beaker-puppet' 2 | 3 | test_name 'skip_test in with_puppet_running_on' do 4 | assert_raises SkipTest do 5 | with_puppet_running_on(master, {}) do 6 | skip_test 'skip rest' 7 | assert(false) 8 | end 9 | end 10 | end 11 | 12 | test_name 'pending_test in with_puppet_running_on' do 13 | assert_raises PendingTest do 14 | with_puppet_running_on(master, {}) do 15 | pending_test 'pending appendix prepended' 16 | assert(false) 17 | end 18 | end 19 | end 20 | 21 | test_name 'fail_test in with_puppet_running_on' do 22 | assert_raises FailTest do 23 | with_puppet_running_on(master, {}) do 24 | fail_test 'fail_test message' 25 | assert(false) 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /setup/common/012_Finalize_Installs.rb: -------------------------------------------------------------------------------- 1 | test_name 'Finalize Host Installation' 2 | 3 | step 'Verify host times' do 4 | # Get a rough estimate of clock skew among hosts 5 | times = [] 6 | hosts.each do |host| 7 | ruby = ruby_command(host) 8 | on(host, "#{ruby} -e 'puts Time.now.strftime(\"%Y-%m-%d %T.%L %z\")'") do |result| 9 | times << result.stdout.chomp 10 | end 11 | end 12 | times.map! do |time| 13 | (Time.strptime(time, '%Y-%m-%d %T.%L %z').to_f * 1000.0).to_i 14 | end 15 | diff = times.max - times.min 16 | if diff < 60_000 17 | logger.info "Host times vary #{diff} ms" 18 | else 19 | logger.warn "Host times vary #{diff} ms, tests may fail" 20 | end 21 | end 22 | 23 | step 'Configure gem mirror' do 24 | configure_gem_mirror(hosts) 25 | end 26 | -------------------------------------------------------------------------------- /acceptance/tests/backwards_compatible.rb: -------------------------------------------------------------------------------- 1 | test_name 'backwards compatible test' do 2 | step 'calls the new FOSSUtils even if I require & include the old path' do 3 | require 'beaker/dsl/install_utils/foss_utils' 4 | assert(!Beaker::DSL::InstallUtils::FOSSUtils::SourcePath.nil?) 5 | assert(Beaker::DSL::InstallUtils::FOSSUtils.method_defined?(:lookup_in_env)) 6 | end 7 | 8 | step 'require old Helpers path, get helpers from new location' do 9 | require 'beaker/dsl/helpers/puppet_helpers' 10 | assert(Beaker::DSL::Helpers::PuppetHelpers.method_defined?(:puppet_user)) 11 | assert(Beaker::DSL::Helpers::PuppetHelpers.method_defined?(:resolve_hostname_on)) 12 | end 13 | 14 | step 'require old Helpers module, get from new location' do 15 | require 'beaker/dsl/helpers' 16 | assert(Beaker::DSL::Helpers.is_a?(Module)) 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /acceptance/tests/install_smoke_test.rb: -------------------------------------------------------------------------------- 1 | require 'beaker-puppet' 2 | 3 | test_name 'puppet install smoketest' 4 | 5 | step 'puppet install smoketest: verify \'facter --help\' can be successfully called on all hosts' 6 | hosts.each do |host| 7 | on host, facter('--help') 8 | end 9 | 10 | step 'puppet install smoketest: verify \'hiera --help\' can be successfully called on all hosts' 11 | hosts.each do |host| 12 | on host, hiera('--help') 13 | end 14 | 15 | step 'puppet install smoketest: verify \'puppet help\' can be successfully called on all hosts' 16 | hosts.each do |host| 17 | on host, puppet('help') 18 | end 19 | 20 | step 'puppet install smoketest: can get a configprint of the puppet server setting on all hosts' 21 | 22 | hosts.each do |host| 23 | assert(!host.puppet['server'].empty?, 'can get a configprint of the puppet server setting') 24 | end 25 | -------------------------------------------------------------------------------- /bin/beaker-puppet: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require 'rubygems' unless defined?(Gem) 4 | require 'beaker-puppet' 5 | 6 | VERSION_STRING = 7 | " 8 | _ .--. 9 | ( ` ) 10 | beaker-puppet .-' `--, 11 | _..----.. ( )`-. 12 | .'_|` _|` _|( .__, ) 13 | /_| _| _| _( (_, .-' 14 | ;| _| _| _| '-'__,--'`--' 15 | | _| _| _| _| | 16 | _ || _| _| _| _| %s 17 | _( `--.\\_| _| _| _|/ 18 | .-' )--,| _| _|.` 19 | (__, (_ ) )_| _| / 20 | `-.__.\\ _,--'\\|__|__/ 21 | ;____; 22 | \\YT/ 23 | || 24 | |\"\"| 25 | '==' 26 | " 27 | 28 | puts format(VERSION_STRING, BeakerPuppet::VERSION) 29 | 30 | exit 0 31 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - '*' 7 | 8 | jobs: 9 | release: 10 | runs-on: ubuntu-latest 11 | if: github.repository_owner == 'puppetlabs' 12 | steps: 13 | - uses: actions/checkout@v6 14 | - name: Install Ruby 3.1 15 | uses: ruby/setup-ruby@v1 16 | with: 17 | ruby-version: '3.1' 18 | env: 19 | BUNDLE_WITHOUT: release 20 | - name: Build gem 21 | run: gem build --strict --verbose *.gemspec 22 | - name: Publish gem to rubygems.org 23 | run: gem push *.gem 24 | env: 25 | GEM_HOST_API_KEY: '${{ secrets.RUBYGEMS_AUTH_TOKEN }}' 26 | - name: Setup GitHub packages access 27 | run: | 28 | mkdir -p ~/.gem 29 | echo ":github: Bearer ${{ secrets.GITHUB_TOKEN }}" >> ~/.gem/credentials 30 | chmod 0600 ~/.gem/credentials 31 | - name: Publish gem to GitHub packages 32 | run: gem push --key github --host https://rubygems.pkg.github.com/puppetlabs *.gem 33 | -------------------------------------------------------------------------------- /spec/beaker-puppet/wrappers_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | class ClassMixedWithDSLWrappers 4 | include Beaker::DSL::Wrappers 5 | end 6 | 7 | describe ClassMixedWithDSLWrappers do 8 | let(:empty_opts) { { 'ENV' => {}, :cmdexe => true } } 9 | 10 | describe '#facter' do 11 | it 'should split out the options and pass "facter" as first arg to Command' do 12 | expect(Beaker::Command).to receive(:new) 13 | .with('facter', ['-p'], empty_opts) 14 | subject.facter('-p') 15 | end 16 | end 17 | 18 | describe '#cfacter' do 19 | it 'should split out the options and pass "cfacter" as first arg to Command' do 20 | expect(Beaker::Command).to receive(:new) 21 | .with('cfacter', ['-p'], empty_opts) 22 | subject.cfacter('-p') 23 | end 24 | end 25 | 26 | describe 'deprecated puppet wrappers' do 27 | %w[resource doc kick cert apply master agent filebucket].each do |sub| 28 | it "#{sub} delegates the proper info to #puppet" do 29 | expect(subject).to receive(:puppet).with(sub, 'blah') 30 | subject.send("puppet_#{sub}", 'blah') 31 | end 32 | end 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /setup/aio/010_Install_Puppet_Agent.rb: -------------------------------------------------------------------------------- 1 | test_name 'Install Puppet Agent Packages' do 2 | agents.each do |agent| 3 | path = ENV.fetch('DEV_BUILD_PATH', nil) 4 | if path 5 | raise ArgumentError, "The path #{path} does not exist" unless File.exist?(path) 6 | 7 | basename = File.basename(path) 8 | scp_to(agent, path, basename) 9 | 10 | # configure locations for ruby, puppet, config files, etc 11 | add_aio_defaults_on(agent) 12 | agent.install_package(basename) 13 | else 14 | opts = { 15 | nightly_builds_url: ENV.fetch('NIGHTLY_BUILDS_URL', nil), 16 | dev_builds_url: ENV.fetch('DEV_BUILDS_URL', nil), 17 | puppet_agent_version: ENV.fetch('SHA', nil), 18 | puppet_collection: ENV.fetch('RELEASE_STREAM', nil), 19 | } 20 | 21 | install_puppet_agent_on(hosts, opts) 22 | end 23 | end 24 | 25 | # make sure install is sane, beaker has already added puppet and ruby 26 | # to PATH in ~/.ssh/environment 27 | agents.each do |agent| # rubocop:disable Style/CombinableLoops 28 | on agent, puppet('--version') 29 | ruby = ruby_command(agent) 30 | on agent, "#{ruby} --version" 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /acceptance/tests/clone_git_repo_on_test.rb: -------------------------------------------------------------------------------- 1 | require 'beaker-puppet' 2 | 3 | confine :except, platform: /^solaris-10/ 4 | 5 | test_name 'Clone from git' do 6 | PACKAGES = { 7 | redhat: [ 8 | 'git', 9 | ], 10 | debian: [ 11 | %w[git git-core], 12 | ], 13 | solaris_11: [ 14 | ['git', 'developer/versioning/git'], 15 | ], 16 | solaris_10: [ 17 | 'coreutils', 18 | 'curl', # update curl to fix "CURLOPT_SSL_VERIFYHOST no longer supports 1 as value!" issue 19 | 'git', 20 | ], 21 | windows: [ 22 | 'git', 23 | ], 24 | sles: [ 25 | 'git-core', 26 | ], 27 | } 28 | 29 | install_packages_on(hosts, PACKAGES, check_if_exists: true) 30 | 31 | # implicitly tests build_giturl() and lookup_in_env() 32 | hosts.each do |host| 33 | on host, "echo #{GitHubSig} >> $HOME/.ssh/known_hosts" 34 | testdir = host.tmpdir(File.basename(__FILE__)) 35 | 36 | step 'should be able to successfully clone a git repo' do 37 | results = clone_git_repo_on(host, "#{testdir}", extract_repo_info_from(build_git_url('hiera'))) 38 | 39 | assert_match(%r{From.*github\.com[:/]puppetlabs/hiera}, result.output, 'Did not find clone') 40 | end 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /spec/beaker-puppet/helpers/host_helpers_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | class ClassMixedWithDSLHelpers 4 | include Beaker::DSL::Helpers::HostHelpers 5 | end 6 | 7 | describe ClassMixedWithDSLHelpers do 8 | let(:privatebindir) { 'C:\\Program Files\\Puppet Labs\\Puppet\\bin' } 9 | 10 | context 'when platform is windows and non cygwin' do 11 | let(:winhost) do 12 | make_host('winhost_non_cygwin', { platform: 'windows', 13 | privatebindir: privatebindir, 14 | is_cygwin: 'false', }) 15 | end 16 | 17 | it 'run the correct ruby_command' do 18 | expect(subject.ruby_command(winhost)).to eq("cmd /V /C \"set PATH=#{privatebindir};!PATH! && ruby\"") 19 | end 20 | end 21 | 22 | context 'when platform is windows and cygwin' do 23 | let(:winhost) do 24 | make_host('winhost', { platform: Beaker::Platform.new('windows-2016-a64'), 25 | privatebindir: privatebindir, 26 | is_cygwin: true, }) 27 | end 28 | 29 | it 'run the correct ruby_command' do 30 | expect(subject.ruby_command(winhost)).to eq("env PATH=\"#{privatebindir}:${PATH}\" ruby") 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | begin 2 | require 'simplecov' 3 | require 'simplecov-console' 4 | require 'codecov' 5 | rescue LoadError 6 | else 7 | SimpleCov.start do 8 | track_files 'lib/**/*.rb' 9 | 10 | add_filter '/spec' 11 | 12 | enable_coverage :branch 13 | 14 | # do not track vendored files 15 | add_filter '/vendor' 16 | add_filter '/.vendor' 17 | end 18 | 19 | SimpleCov.formatters = [ 20 | SimpleCov::Formatter::Console, 21 | SimpleCov::Formatter::Codecov, 22 | ] 23 | end 24 | 25 | # require 'pp' statement needed before fakefs, otherwise they can collide. Ref: 26 | # https://github.com/fakefs/fakefs#fakefs-----typeerror-superclass-mismatch-for-class-file 27 | require 'pp' 28 | require 'fakefs/spec_helpers' 29 | require 'beaker' 30 | require 'beaker-puppet' 31 | require 'helpers' 32 | 33 | # setup & require beaker's spec_helper.rb 34 | beaker_gem_spec = Gem::Specification.find_by_name('beaker') 35 | beaker_gem_dir = beaker_gem_spec.gem_dir 36 | beaker_spec_path = File.join(beaker_gem_dir, 'spec') 37 | $LOAD_PATH << beaker_spec_path 38 | require File.join(beaker_spec_path, 'spec_helper.rb') 39 | 40 | require 'rspec/its' 41 | 42 | RSpec.configure do |config| 43 | config.include FakeFS::SpecHelpers 44 | config.include TestFileHelpers 45 | config.include HostHelpers 46 | end 47 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: 4 | pull_request: {} 5 | push: 6 | branches: 7 | - main 8 | 9 | env: 10 | BUNDLE_WITHOUT: release 11 | 12 | jobs: 13 | rubocop: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v6 17 | - name: Install Ruby ${{ matrix.ruby }} 18 | uses: ruby/setup-ruby@v1 19 | with: 20 | ruby-version: '3.3' 21 | bundler-cache: true 22 | - name: Run Rubocop 23 | run: bundle exec rake rubocop 24 | 25 | 26 | rspec: 27 | runs-on: ubuntu-latest 28 | strategy: 29 | fail-fast: false 30 | matrix: 31 | include: 32 | - ruby: '3.1' 33 | - ruby: '3.2' 34 | - ruby: '3.3' 35 | 36 | name: RSpec - Ruby ${{ matrix.ruby }} 37 | steps: 38 | - uses: actions/checkout@v6 39 | - name: Install Ruby ${{ matrix.ruby }} 40 | uses: ruby/setup-ruby@v1 41 | with: 42 | ruby-version: ${{ matrix.ruby }} 43 | bundler-cache: true 44 | - name: spec tests 45 | run: bundle exec rake test:spec 46 | - name: Verify gem builds 47 | run: gem build --strict --verbose *.gemspec 48 | 49 | tests: 50 | needs: 51 | - rubocop 52 | - rspec 53 | runs-on: ubuntu-latest 54 | name: Test suite 55 | steps: 56 | - run: echo Test suite completed 57 | -------------------------------------------------------------------------------- /lib/beaker-puppet.rb: -------------------------------------------------------------------------------- 1 | require 'beaker' 2 | 3 | require 'in_parallel' 4 | require 'beaker-puppet/version' 5 | require 'beaker-puppet/wrappers' 6 | require 'beaker-puppet/inifile' 7 | 8 | require 'beaker-puppet/helpers/rake_helpers' 9 | 10 | %w[aio foss].each do |lib| 11 | require "beaker-puppet/install_utils/#{lib}_defaults" 12 | end 13 | %w[windows foss puppet ezbake module].each do |lib| 14 | require "beaker-puppet/install_utils/#{lib}_utils" 15 | end 16 | %w[tk facter puppet host].each do |lib| 17 | require "beaker-puppet/helpers/#{lib}_helpers" 18 | end 19 | 20 | require 'beaker-puppet/install_utils/puppet5' 21 | 22 | module BeakerPuppet 23 | include Beaker::DSL::InstallUtils::FOSSDefaults 24 | include Beaker::DSL::InstallUtils::AIODefaults 25 | 26 | include Beaker::DSL::InstallUtils::WindowsUtils 27 | include Beaker::DSL::InstallUtils::PuppetUtils 28 | include Beaker::DSL::InstallUtils::FOSSUtils 29 | include Beaker::DSL::InstallUtils::EZBakeUtils 30 | include Beaker::DSL::InstallUtils::ModuleUtils 31 | 32 | include Beaker::DSL::InstallUtils::Puppet5 33 | 34 | include Beaker::DSL::Helpers::TKHelpers 35 | include Beaker::DSL::Helpers::FacterHelpers 36 | include Beaker::DSL::Helpers::PuppetHelpers 37 | include Beaker::DSL::Helpers::HostHelpers 38 | 39 | include Beaker::DSL::Wrappers 40 | end 41 | 42 | # Register the DSL extension 43 | Beaker::DSL.register(BeakerPuppet) 44 | -------------------------------------------------------------------------------- /beaker-puppet.gemspec: -------------------------------------------------------------------------------- 1 | $LOAD_PATH.unshift File.expand_path('lib', __dir__) 2 | require 'beaker-puppet/version' 3 | 4 | Gem::Specification.new do |s| 5 | s.name = 'beaker-puppet' 6 | s.version = BeakerPuppet::VERSION 7 | s.authors = ['Vox Pupuli'] 8 | s.email = ['voxpupuli@groups.io'] 9 | s.homepage = 'https://github.com/voxpupuli/beaker-puppet' 10 | s.summary = "Beaker's Puppet DSL Extension Helpers!" 11 | s.description = 'For use for the Beaker acceptance testing tool' 12 | s.license = 'Apache-2.0' 13 | 14 | s.required_ruby_version = '>= 2.7', '< 3.4' 15 | 16 | s.files = `git ls-files`.split("\n") 17 | s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n") 18 | s.executables = `git ls-files -- bin/*`.split("\n").map { |f| File.basename(f) } 19 | s.require_paths = ['lib'] 20 | 21 | # Testing dependencies 22 | s.add_development_dependency 'fakefs', '>= 0.6', '< 3.0' 23 | s.add_development_dependency 'rake', '~> 13.0' 24 | s.add_development_dependency 'rspec', '~> 3.0' 25 | s.add_development_dependency 'rspec-its', '~> 1.3' 26 | s.add_development_dependency 'voxpupuli-rubocop', '~> 3.0' 27 | 28 | # Acceptance Testing Dependencies 29 | s.add_development_dependency 'beaker-vmpooler', '~> 1.4' 30 | 31 | # Run time dependencies 32 | s.add_runtime_dependency 'beaker', '>= 5.0', '< 7' 33 | s.add_runtime_dependency 'oga', '~> 3.4' 34 | end 35 | -------------------------------------------------------------------------------- /setup/common/040_ValidateSignCert.rb: -------------------------------------------------------------------------------- 1 | test_name 'Validate Sign Cert' do 2 | need_to_run = false 3 | hosts.each do |host| 4 | need_to_run ||= !host['use_existing_container'] 5 | end 6 | skip_test 'No new hosts to create, skipping' unless need_to_run 7 | skip_test 'not testing with puppetserver' unless @options['is_puppetserver'] 8 | hostname = on(master, 'facter hostname').stdout.strip 9 | fqdn = on(master, 'facter fqdn').stdout.strip 10 | 11 | step 'Ensure puppet is stopped' 12 | on(master, puppet('resource', 'service', master['puppetservice'], 'ensure=stopped')) 13 | 14 | step 'Clear SSL on all hosts' 15 | hosts.each do |host| 16 | ssldir = on(host, puppet('agent --configprint ssldir')).stdout.chomp 17 | # preserve permissions for master's ssldir so puppetserver can read it 18 | on(host, "rm -rf '#{ssldir}/'*") 19 | end 20 | 21 | step "Set 'server' setting" 22 | hosts.each do |host| 23 | on(host, puppet("config set server #{master.hostname} --section main")) 24 | end 25 | 26 | step 'Start puppetserver' do 27 | master_opts = { 28 | main: { 29 | dns_alt_names: "puppet,#{hostname},#{fqdn}", 30 | server: fqdn, 31 | autosign: true, 32 | }, 33 | } 34 | 35 | on(master, 'puppetserver ca setup') 36 | with_puppet_running_on(master, master_opts) do 37 | step 'Agents: Run agent --test with autosigning enabled to get cert' 38 | on(agents, puppet('agent --test'), acceptable_exit_codes: [0, 2]) 39 | end 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /setup/gem/010_GemInstall.rb: -------------------------------------------------------------------------------- 1 | test_name 'Install puppet gem' 2 | 3 | agents.each do |agent| 4 | sha = ENV.fetch('SHA', nil) 5 | base_url = "http://builds.delivery.puppetlabs.net/puppet/#{sha}/artifacts" 6 | 7 | ruby_command = ruby_command(agent) 8 | gem_command = gem_command(agent) 9 | 10 | # retrieve the build data, since the gem version is based on the short git 11 | # describe, not the full git SHA 12 | on(agent, "curl -s -o build_data.yaml #{base_url}/#{sha}.yaml") 13 | gem_version = on(agent, 14 | "#{ruby_command} -ryaml -e 'puts YAML.load_file(\"build_data.yaml\")[:gemversion]'").stdout.chomp 15 | 16 | if agent['platform'] =~ /windows/ 17 | # wipe existing gems first 18 | default_dir = on(agent, "#{ruby_command} -rrbconfig -e 'puts Gem.default_dir'").stdout.chomp 19 | on(agent, "rm -rf '#{default_dir}'") 20 | 21 | arch = agent[:ruby_arch] || 'x86' 22 | gem_arch = (arch == 'x64') ? 'x64-mingw32' : 'x86-mingw32' 23 | url = "#{base_url}/puppet-#{gem_version}-#{gem_arch}.gem" 24 | else 25 | url = "#{base_url}/puppet-#{gem_version}.gem" 26 | end 27 | 28 | step "Download puppet gem from #{url}" 29 | on(agent, "curl -s -o puppet.gem #{url}") 30 | 31 | step 'Install puppet.gem' 32 | on(agent, "#{gem_command} install puppet.gem") 33 | 34 | step "Verify it's sane" 35 | on(agent, puppet('--version')) 36 | on(agent, puppet('apply', "-e \"notify { 'hello': }\"")) do |result| 37 | assert_match(/defined 'message' as 'hello'/, result.stdout) 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /acceptance/tests/stub_host.rb: -------------------------------------------------------------------------------- 1 | require 'beaker-puppet' 2 | 3 | test_name 'validate host stubbing behavior' 4 | 5 | def get_hosts_file(host) 6 | if host['platform'] =~ /win/ 7 | 'C:\\\\Windows\\\\System32\\\\Drivers\\\\etc\\\\hosts' 8 | else 9 | '/etc/hosts' 10 | end 11 | end 12 | 13 | step 'verify stub_host_on' do 14 | step 'should add entry to hosts file' do 15 | hosts.each do |host| 16 | stub_hosts_on(host, { 'foo' => '1.1.1.1' }, { 'foo' => %w[bar baz] }) 17 | hosts_file = get_hosts_file(host) 18 | result = on host, "cat #{hosts_file}" 19 | assert_match(/foo/, result.stdout) 20 | end 21 | end 22 | 23 | step 'stubbed value should be available for other steps in the test' do 24 | hosts.each do |host| 25 | hosts_file = get_hosts_file(host) 26 | result = on host, "cat #{hosts_file}" 27 | assert_match(/foo/, result.stdout) 28 | end 29 | end 30 | end 31 | 32 | step 'verify with_stub_host_on' do 33 | step 'should add entry to hosts file' do 34 | hosts.each do |host| 35 | hosts_file = get_hosts_file(host) 36 | result = with_host_stubbed_on(host, { 'sleepy' => '1.1.1.2' }, { 'sleepy' => %w[grumpy dopey] }) do 37 | on host, "cat #{hosts_file}" 38 | end 39 | assert_match(/sleepy/, result.stdout) 40 | end 41 | end 42 | 43 | step 'stubbed value should be reverted after the execution of the block' do 44 | hosts.each do |host| 45 | hosts_file = get_hosts_file(host) 46 | result = on host, "cat #{hosts_file}" 47 | refute_match(/sleepy/, result.stdout) 48 | end 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /acceptance/tests/create_tmpdir_on_test.rb: -------------------------------------------------------------------------------- 1 | test_name 'dsl::helpers::host_helpers #create_tmpdir_on' do 2 | step '#create_tmpdir_on returns a temporary directory on the remote system' do 3 | tmpdir = create_tmpdir_on default 4 | 5 | assert_match %r{/}, tmpdir 6 | assert_equal 0, on(default, "touch #{tmpdir}/testfile").exit_code 7 | end 8 | 9 | step '#create_tmpdir_on uses the specified path prefix when provided' do 10 | tmpdir = create_tmpdir_on(default, 'mypathprefix') 11 | 12 | assert_match %r{/mypathprefix}, tmpdir 13 | assert_equal 0, on(default, "touch #{tmpdir}/testfile").exit_code 14 | end 15 | 16 | step '#create_tmpdir_on fails if a non-existent user is specified' do 17 | assert_raises Beaker::Host::CommandFailure do 18 | tmpdir = create_tmpdir_on default, '', 'fakeuser' 19 | end 20 | end 21 | 22 | step '#create_tmpdir_on sets the user if specified' do 23 | default.user_present('tmpdirtestuser') 24 | tmpdir = create_tmpdir_on(default, nil, 'tmpdirtestuser', nil) 25 | 26 | assert_match /tmpdirtestuser/, on(default, "ls -ld #{tmpdir}").output 27 | default.user_absent('tmpdirtestuser') 28 | end 29 | 30 | step '#create_tmpdir_on fails if a non-existent group is specified' do 31 | assert_raises Beaker::Host::CommandFailure do 32 | tmpdir = create_tmpdir_on default, '', nil, 'fakegroup' 33 | end 34 | end 35 | 36 | step '#create_tmpdir_on sets the group if specified' do 37 | default.group_present('tmpdirtestgroup') 38 | tmpdir = create_tmpdir_on(default, nil, nil, 'tmpdirtestgroup') 39 | 40 | assert_match /testgroup/, on(default, "ls -ld #{tmpdir}").output 41 | default.group_absent('tmpdirtestgroup') 42 | end 43 | 44 | step '#create_tmpdir_on operates on all hosts if given a hosts array' do 45 | tmpdirs = create_tmpdir_on hosts 46 | hosts.zip(tmpdirs).each do |(host, tmpdir)| 47 | assert_match %r{/}, tmpdir 48 | assert_equal 0, on(host, "touch #{tmpdir}/testfile").exit_code 49 | end 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /lib/beaker-puppet/helpers/host_helpers.rb: -------------------------------------------------------------------------------- 1 | module Beaker 2 | module DSL 3 | module Helpers 4 | # Methods that help you interact with your facter installation, facter must be installed 5 | # for these methods to execute correctly 6 | # 7 | module HostHelpers 8 | def ruby_command(host) 9 | if host['platform'] =~ /windows/ && !host.is_cygwin? 10 | "cmd /V /C \"set PATH=#{host['privatebindir']};!PATH! && ruby\"" 11 | else 12 | "env PATH=\"#{host['privatebindir']}:${PATH}\" ruby" 13 | end 14 | end 15 | 16 | # Returns an array containing the owner, group and mode of 17 | # the file specified by path. The returned mode is an integer 18 | # value containing only the file mode, excluding the type, e.g 19 | # S_IFDIR 0040000 20 | def beaker_stat(host, path) 21 | ruby = ruby_command(host) 22 | owner = on(host, 23 | "#{ruby} -e 'require \"etc\"; puts (Etc.getpwuid(File.stat(\"#{path}\").uid).name)'").stdout.chomp 24 | group = on(host, 25 | "#{ruby} -e 'require \"etc\"; puts (Etc.getgrgid(File.stat(\"#{path}\").gid).name)'").stdout.chomp 26 | mode = on(host, "#{ruby} -e 'puts (File.stat(\"#{path}\").mode & 0777).to_s(8)'").stdout.chomp.to_i 27 | 28 | [owner, group, mode] 29 | end 30 | 31 | def assert_ownership_permissions(host, location, expected_user, expected_group, expected_permissions) 32 | permissions = beaker_stat(host, location) 33 | assert_equal(expected_user, permissions[0], 34 | "Owner #{permissions[0]} does not match expected #{expected_user}") 35 | assert_equal(expected_group, permissions[1], 36 | "Group #{permissions[1]} does not match expected #{expected_group}") 37 | assert_equal(expected_permissions, permissions[2], 38 | "Permissions #{permissions[2]} does not match expected #{expected_permissions}") 39 | end 40 | end 41 | end 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /acceptance/tests/web_helpers_test.rb: -------------------------------------------------------------------------------- 1 | require 'beaker-puppet' 2 | 3 | require 'webrick' 4 | require 'webrick/https' 5 | 6 | test_name 'dsl::helpers::web_helpers #link_exists?' do 7 | cert_name = [ 8 | %w[CN localhost], 9 | ] 10 | http_cmd = "ruby -rwebrick -e'WEBrick::HTTPServer.new(:Port => 80, :DocumentRoot => \"/tmp\").start' > /tmp/mylogfile 2>&1 &" 11 | https_cmd = "ruby -rwebrick/https -e'WEBrick::HTTPServer.new(:SSLEnable => true, :SSLCertName => #{cert_name}, :Port => 555,:DocumentRoot => \"/tmp\").start' > /tmp/mylogfile 2>&1 &" 12 | on(default, http_cmd) 13 | on(default, https_cmd) 14 | # allow web servers to start up 15 | sleep(3) 16 | dir = default.tmpdir('test_dir') 17 | file = default.tmpfile('test_file') 18 | dir.slice! '/tmp' 19 | file.slice! '/tmp' 20 | dst_dir = 'web_helpers' 21 | 22 | step '#port_open_within? can tell if a port is open' do 23 | assert port_open_within?(default, 80) 24 | end 25 | 26 | step '#link_exists? can tell if a basic link exists' do 27 | assert link_exists?("http://#{default}") 28 | end 29 | 30 | step '#link_exists? can tell if a basic link does not exist' do 31 | assert !link_exists?("http://#{default}/test") 32 | end 33 | 34 | step '#link_exists? can use an ssl link' do 35 | assert link_exists?("https://#{default}:555") 36 | end 37 | 38 | step '#fetch_http_dir can fetch a dir' do 39 | assert_equal "#{dst_dir}#{dir}", fetch_http_dir("http://#{default}/#{dir}", dst_dir) 40 | end 41 | 42 | step '#fetch_http_dir will raise an error if unable fetch a dir' do 43 | exception = assert_raises(RuntimeError) { fetch_http_dir("http://#{default}/tmps", dst_dir) } 44 | 45 | assert_match /Failed to fetch_remote_dir.*/, exception.message, '#fetch_http_dir raised an unexpected RuntimeError' 46 | end 47 | 48 | step '#fetch_http_file can fetch a file' do 49 | assert_equal "#{dst_dir}#{file}", fetch_http_file("http://#{default}", file, dst_dir) 50 | end 51 | 52 | step '#fetch_http_file will raise an error if unable to fetch a file' do 53 | exception = assert_raises(RuntimeError) { fetch_http_file("http://#{default}", 'test2.txt', dst_dir) } 54 | 55 | assert_match /Failed to fetch_remote_file.*/, exception.message, '#fetch_http_dir raised an unexpected RuntimeError' 56 | end 57 | end 58 | -------------------------------------------------------------------------------- /setup/git/060_InstallModules.rb: -------------------------------------------------------------------------------- 1 | require 'pathname' 2 | 3 | # Given an array of modules specified by the --modules command line option, 4 | # Parse all of them into an array of usable hash structures. 5 | class PuppetModules 6 | attr_reader :modules 7 | 8 | def initialize(modules = []) 9 | @modules = modules 10 | end 11 | 12 | def list 13 | return [] unless modules 14 | 15 | modules.collect do |uri| 16 | git_url, git_ref = uri.split '#' 17 | folder = Pathname.new(git_url).basename('.git') 18 | name = folder.to_s.split('-', 2)[1] || folder.to_s 19 | { 20 | name: name, 21 | url: git_url, 22 | folder: folder.to_s, 23 | ref: git_ref, 24 | protocol: git_url.split(':')[0].intern, 25 | } 26 | end 27 | end 28 | end 29 | 30 | def install_git_module(mod, hosts) 31 | # The idea here is that each test can symlink the modules they want from a 32 | # temporary directory to this location. This will preserve the global 33 | # state of the system while allowing individual test cases to quickly run 34 | # with a module "installed" in the module path. 35 | moddir = '/opt/puppet-git-repos' 36 | target = "#{moddir}/#{mod[:name]}" 37 | 38 | step "Clone #{mod[:url]} if needed" 39 | on hosts, "test -d #{moddir} || mkdir -p #{moddir}" 40 | on hosts, "test -d #{target} || git clone #{mod[:url]} #{target}" 41 | step "Update #{mod[:name]} and check out revision #{mod[:ref]}" 42 | 43 | commands = ["cd #{target}", 44 | 'remote rm origin', 45 | "remote add origin #{mod[:url]}", 46 | 'fetch origin', 47 | "checkout -f #{mod[:ref]}", 48 | "reset --hard refs/remotes/origin/#{mod[:ref]}", 49 | 'clean -fdx',] 50 | 51 | on hosts, commands.join(' && git ') 52 | end 53 | 54 | def install_scp_module(mod, hosts) 55 | moddir = '/opt/puppet-git-repos' 56 | target = "#{moddir}/#{mod[:name]}" 57 | 58 | step "Purge #{target} if needed" 59 | on hosts, "test -d #{target} && rm -rf #{target} || true" 60 | 61 | step "Copy #{mod[:name]} to hosts" 62 | scp_to hosts, mod[:url].split(':', 2)[1], target 63 | end 64 | 65 | test_name 'Install Puppet Modules' 66 | 67 | skip_test 'not testing with puppetserver' unless @options['is_puppetserver'] 68 | 69 | modules = PuppetModules.new(options[:modules]).list 70 | 71 | step 'Masters: Install Puppet Modules' 72 | masters = hosts.select { |host| host['roles'].include? 'master' } 73 | 74 | modules.each do |mod| 75 | if mod[:protocol] == :scp 76 | install_scp_module(mod, masters) 77 | else 78 | install_git_module(mod, masters) 79 | end 80 | end 81 | -------------------------------------------------------------------------------- /acceptance/pre_suite/git/install.rb: -------------------------------------------------------------------------------- 1 | require 'beaker-puppet' 2 | 3 | test_name 'Puppet git pre-suite' 4 | 5 | install = [ 6 | 'facter#2.1.0', 7 | 'hiera#1.3.4', 8 | 'puppet#3.8.7', 9 | ] 10 | 11 | PACKAGES = { 12 | redhat: [ 13 | 'git', 14 | 'ruby', 15 | 'rubygem-json', # :add_el_extras is required to find this package 16 | ], 17 | debian: [ 18 | %w[git git-core], 19 | 'ruby', 20 | ], 21 | debian_ruby18: [ 22 | 'libjson-ruby', 23 | ], 24 | solaris_11: [ 25 | ['git', 'developer/versioning/git'], 26 | ['ruby', 'runtime/ruby-18'], 27 | ], 28 | solaris_10: [ 29 | 'coreutils', 30 | 'curl', # update curl to fix "CURLOPT_SSL_VERIFYHOST no longer supports 1 as value!" issue 31 | 'git', 32 | 'ruby19', 33 | 'ruby19_dev', 34 | 'gcc4core', 35 | ], 36 | windows: [ 37 | 'git', 38 | # there isn't a need for json on windows because it is bundled in ruby 1.9 39 | ], 40 | sles: [ 41 | 'git-core', 42 | ], 43 | } 44 | 45 | install_packages_on(hosts, PACKAGES, check_if_exists: true) 46 | 47 | hosts.each do |host| 48 | case host['platform'] 49 | when /windows/ 50 | arch = host[:ruby_arch] || 'x86' 51 | step "#{host} Selected architecture #{arch}" 52 | 53 | revision = if arch == 'x64' 54 | '2.1.x-x64' 55 | else 56 | '2.1.x-x86' 57 | end 58 | 59 | step "#{host} Install ruby from git using revision #{revision}" 60 | # TODO: remove this step once we are installing puppet from msi packages 61 | install_from_git(host, '/opt/puppet-git-repos', 62 | name: 'puppet-win32-ruby', 63 | path: build_giturl('puppet-win32-ruby'), 64 | rev: revision) 65 | on host, 'cd /opt/puppet-git-repos/puppet-win32-ruby; cp -r ruby/* /' 66 | on host, 'cd /lib; icacls ruby /grant "Everyone:(OI)(CI)(RX)"' 67 | on host, 'cd /lib; icacls ruby /reset /T' 68 | on host, 'cd /; icacls bin /grant "Everyone:(OI)(CI)(RX)"' 69 | on host, 'cd /; icacls bin /reset /T' 70 | on host, 'ruby --version' 71 | on host, 'cmd /c gem list' 72 | when /solaris/ 73 | on host, 'gem install json' 74 | end 75 | end 76 | 77 | tmp_repos = [] 78 | install.each do |reponame| 79 | tmp_repos << extract_repo_info_from("https://github.com/puppetlabs/#{reponame}") 80 | end 81 | 82 | repos = order_packages(tmp_repos) 83 | 84 | hosts.each do |host| 85 | repos.each do |repo| 86 | install_from_git(host, SourcePath, repo) 87 | end 88 | next if host['platform'] =~ /windows/ 89 | 90 | on(host, "touch #{File.join(host.puppet['confdir'], 'puppet.conf')}") 91 | on(host, puppet('resource user puppet ensure=present')) 92 | on(host, puppet('resource group puppet ensure=present')) 93 | end 94 | -------------------------------------------------------------------------------- /lib/beaker-puppet/wrappers.rb: -------------------------------------------------------------------------------- 1 | module Beaker 2 | module DSL 3 | module Wrappers 4 | # This is hairy and because of legacy code it will take a bit more 5 | # work to disentangle all of the things that are being passed into 6 | # this catchall param. 7 | # 8 | def facter(*args) 9 | options = args.last.is_a?(Hash) ? args.pop : {} 10 | options['ENV'] ||= {} 11 | options[:cmdexe] = true 12 | Command.new('facter', args, options) 13 | end 14 | 15 | # This is hairy and because of legacy code it will take a bit more 16 | # work to disentangle all of the things that are being passed into 17 | # this catchall param. 18 | # 19 | def cfacter(*args) 20 | options = args.last.is_a?(Hash) ? args.pop : {} 21 | options['ENV'] ||= {} 22 | options[:cmdexe] = true 23 | Command.new('cfacter', args, options) 24 | end 25 | 26 | # This is hairy and because of legacy code it will take a bit more 27 | # work to disentangle all of the things that are being passed into 28 | # this catchall param. 29 | # 30 | def hiera(*args) 31 | options = args.last.is_a?(Hash) ? args.pop : {} 32 | options['ENV'] ||= {} 33 | options[:cmdexe] = true 34 | Command.new('hiera', args, options) 35 | end 36 | 37 | # This is hairy and because of legacy code it will take a bit more 38 | # work to disentangle all of the things that are being passed into 39 | # this catchall param. 40 | # 41 | def puppet(*args) 42 | options = args.last.is_a?(Hash) ? args.pop : {} 43 | options['ENV'] ||= {} 44 | options[:cmdexe] = true 45 | # we assume that an invocation with `puppet()` will have it's first argument 46 | # a face or sub command 47 | cmd = "puppet #{args.shift}" 48 | Command.new(cmd, args, options) 49 | end 50 | 51 | # @!visibility private 52 | def puppet_resource(*args) 53 | puppet('resource', *args) 54 | end 55 | 56 | # @!visibility private 57 | def puppet_doc(*args) 58 | puppet('doc', *args) 59 | end 60 | 61 | # @!visibility private 62 | def puppet_kick(*args) 63 | puppet('kick', *args) 64 | end 65 | 66 | # @!visibility private 67 | def puppet_cert(*args) 68 | puppet('cert', *args) 69 | end 70 | 71 | # @!visibility private 72 | def puppet_apply(*args) 73 | puppet('apply', *args) 74 | end 75 | 76 | # @!visibility private 77 | def puppet_master(*args) 78 | puppet('master', *args) 79 | end 80 | 81 | # @!visibility private 82 | def puppet_agent(*args) 83 | puppet('agent', *args) 84 | end 85 | 86 | # @!visibility private 87 | def puppet_filebucket(*args) 88 | puppet('filebucket', *args) 89 | end 90 | end 91 | end 92 | end 93 | -------------------------------------------------------------------------------- /lib/beaker-puppet/helpers/facter_helpers.rb: -------------------------------------------------------------------------------- 1 | module Beaker 2 | module DSL 3 | module Helpers 4 | # Methods that help you interact with your facter installation, facter must be installed 5 | # for these methods to execute correctly 6 | # 7 | module FacterHelpers 8 | # @!macro [new] common_opts 9 | # @param [Hash{Symbol=>String}] opts Options to alter execution. 10 | # @option opts [Boolean] :silent (false) Do not produce log output 11 | # @option opts [Array] :acceptable_exit_codes ([0]) An array 12 | # (or range) of integer exit codes that should be considered 13 | # acceptable. An error will be thrown if the exit code does not 14 | # match one of the values in this list. 15 | # @option opts [Boolean] :accept_all_exit_codes (false) Consider all 16 | # exit codes as passing. 17 | # @option opts [Boolean] :dry_run (false) Do not actually execute any 18 | # commands on the SUT 19 | # @option opts [String] :stdin (nil) Input to be provided during command 20 | # execution on the SUT. 21 | # @option opts [Boolean] :pty (false) Execute this command in a pseudoterminal. 22 | # @option opts [Boolean] :expect_connection_failure (false) Expect this command 23 | # to result in a connection failure, reconnect and continue execution. 24 | # @option opts [Hash{String=>String}] :environment ({}) These will be 25 | # treated as extra environment variables that should be set before 26 | # running the command. 27 | # 28 | 29 | # Get a facter fact from a provided host 30 | # 31 | # @param [Host, Array, String, Symbol] host One or more hosts to act upon, 32 | # or a role (String or Symbol) that identifies one or more hosts. 33 | # @param [String] name The name of the fact to query for 34 | # @!macro common_opts 35 | # 36 | # @return String The value of the fact 'name' on the provided host 37 | # @raise [FailTest] Raises an exception if call to facter fails 38 | def fact_on(host, name, opts = {}) 39 | unless name.is_a?(String) 40 | raise(ArgumentError, 41 | "fact_on's `name` option must be a String. You provided a #{name.class}: '#{name}'") 42 | end 43 | 44 | if opts.is_a?(Hash) 45 | opts.merge!({ json: nil }) 46 | else 47 | opts << ' --json' 48 | end 49 | 50 | result = on host, facter("\"#{name}\"", opts) 51 | if result.is_a?(Array) 52 | result.map { |res| JSON.parse(res.stdout)[name] } 53 | else 54 | JSON.parse(result.stdout)[name] 55 | end 56 | end 57 | 58 | # Get a facter fact from the default host 59 | # @see #fact_on 60 | def fact(name, opts = {}) 61 | fact_on(default, name, opts) 62 | end 63 | end 64 | end 65 | end 66 | end 67 | -------------------------------------------------------------------------------- /spec/beaker-puppet/helpers/tk_helpers_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | class ClassMixedWithDSLHelpers 4 | include Beaker::DSL::Helpers 5 | include Beaker::DSL::Wrappers 6 | include Beaker::DSL::Roles 7 | include Beaker::DSL::Patterns 8 | include Beaker::DSL::Helpers::TKHelpers 9 | 10 | def logger 11 | RSpec::Mocks::Double.new('logger').as_null_object 12 | end 13 | end 14 | 15 | describe ClassMixedWithDSLHelpers do 16 | let(:opts) { Beaker::Options::Presets.env_vars } 17 | let(:command) { 'ls' } 18 | let(:host) { double.as_null_object } 19 | let(:result) { Beaker::Result.new(host, command) } 20 | 21 | let(:master) { make_host('master', roles: %w[master agent default]) } 22 | let(:agent) { make_host('agent', roles: %w[agent]) } 23 | let(:custom) { make_host('custom', roles: %w[custom agent]) } 24 | let(:dash) { make_host('console', roles: %w[dashboard agent]) } 25 | let(:db) { make_host('db', roles: %w[database agent]) } 26 | let(:hosts) { [master, agent, dash, db, custom] } 27 | 28 | describe 'modify_tk_config' do 29 | let(:host) { double.as_null_object } 30 | let(:config_file_path) { 'existing-file-path' } 31 | let(:invalid_config_file_path) { 'nonexisting-file-path' } 32 | let(:options_hash) { { key: 'value' } } 33 | let(:replace) { true } 34 | 35 | shared_examples 'modify-tk-config-without-error' do 36 | it 'dumps to the SUT config file path' do 37 | allow(JSON).to receive(:pretty_generate) 38 | allow(subject).to receive(:create_remote_file).with(host, config_file_path, anything) 39 | subject.modify_tk_config(host, config_file_path, options_hash, replace) 40 | end 41 | end 42 | 43 | before do 44 | allow(host).to receive(:file_exist?).with(invalid_config_file_path).and_return(false) 45 | allow(host).to receive(:file_exist?).with(config_file_path).and_return(true) 46 | end 47 | 48 | describe 'if file does not exist on SUT' do 49 | it 'raises Runtime error' do 50 | expect do 51 | subject.modify_tk_config(host, invalid_config_file_path, options_hash) 52 | end.to raise_error(RuntimeError, /.* does not exist on .*/) 53 | end 54 | end 55 | 56 | describe 'given an empty options hash' do 57 | it 'returns nil' do 58 | expect(subject.modify_tk_config(host, 'blahblah', {})).to eq(nil) 59 | end 60 | end 61 | 62 | describe 'given a non-empty options hash' do 63 | describe 'given a false value to its `replace` parameter' do 64 | let(:replace) { false } 65 | before do 66 | expect(subject).to receive(:read_tk_config_string).with(anything) 67 | end 68 | include_examples('modify-tk-config-without-error') 69 | end 70 | 71 | describe 'given a true value to its `replace` parameter' do 72 | before do 73 | expect(JSON).to receive(:pretty_generate) 74 | expect(subject).to receive(:create_remote_file).with(host, config_file_path, anything) 75 | end 76 | include_examples('modify-tk-config-without-error') 77 | end 78 | end 79 | end 80 | end 81 | -------------------------------------------------------------------------------- /lib/beaker-puppet/helpers/tk_helpers.rb: -------------------------------------------------------------------------------- 1 | require 'hocon' 2 | require 'hocon/config_error' 3 | 4 | module Beaker 5 | module DSL 6 | module Helpers 7 | # Convenience methods for modifying and reading TrapperKeeper configs 8 | module TKHelpers 9 | # Modify the given TrapperKeeper config file. 10 | # 11 | # @param [Host] host A host object 12 | # @param [OptionsHash] options_hash New hash which will be merged into 13 | # the given TrapperKeeper config. 14 | # @param [String] config_file_path Path to the TrapperKeeper config on 15 | # the given host which is to be 16 | # modified. 17 | # @param [Bool] replace If set true, instead of updating the existing 18 | # TrapperKeeper configuration, replace it entirely 19 | # with the contents of the given hash. 20 | # 21 | # @note TrapperKeeper config files can be HOCON, JSON, or Ini. We don't 22 | # particularly care which of these the file named by `config_file_path` on 23 | # the SUT actually is, just that the contents can be parsed into a map. 24 | # 25 | def modify_tk_config(host, config_file_path, options_hash, replace = false) 26 | return nil if options_hash.empty? 27 | 28 | new_hash = Beaker::Options::OptionsHash.new 29 | 30 | if replace 31 | new_hash.merge!(options_hash) 32 | else 33 | raise "Error: #{config_file_path} does not exist on #{host}" unless host.file_exist?(config_file_path) 34 | 35 | file_string = host.exec(Command.new("cat #{config_file_path}")).stdout 36 | 37 | begin 38 | tk_conf_hash = read_tk_config_string(file_string) 39 | rescue RuntimeError 40 | raise "Error reading trapperkeeper config: #{config_file_path} at host: #{host}" 41 | end 42 | 43 | new_hash.merge!(tk_conf_hash) 44 | new_hash.merge!(options_hash) 45 | end 46 | 47 | file_string = JSON.pretty_generate(new_hash) 48 | create_remote_file host, config_file_path, file_string 49 | end 50 | 51 | # The Trapperkeeper config service will accept HOCON (aka typesafe), JSON, 52 | # or Ini configuration files which means we need to safely handle the the 53 | # exceptions that might come from parsing the given string with the wrong 54 | # parser and fall back to the next valid parser in turn. We finally raise 55 | # a RuntimeException if none of the parsers succeed. 56 | # 57 | # @!visibility private 58 | def read_tk_config_string(string) 59 | begin 60 | return Hocon.parse(string) 61 | rescue Hocon::ConfigError 62 | nil 63 | end 64 | 65 | begin 66 | return JSON.parse(string) 67 | rescue JSON::JSONError 68 | nil 69 | end 70 | 71 | begin 72 | return BeakerPuppet::IniFile.new(content: string) 73 | rescue IniFile::Error 74 | nil 75 | end 76 | 77 | raise 'Failed to read TrapperKeeper config!' 78 | end 79 | end 80 | end 81 | end 82 | end 83 | -------------------------------------------------------------------------------- /spec/beaker-puppet/helpers/facter_helpers_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | class ClassMixedWithDSLHelpers 4 | include Beaker::DSL::Helpers 5 | include Beaker::DSL::Wrappers 6 | include Beaker::DSL::Roles 7 | include Beaker::DSL::Patterns 8 | include Beaker::DSL::Helpers::FacterHelpers 9 | # include Beaker::DSL::Wrappers 10 | # include BeakerTestHelpers 11 | 12 | def logger 13 | RSpec::Mocks::Double.new('logger').as_null_object 14 | end 15 | end 16 | 17 | describe ClassMixedWithDSLHelpers do 18 | let(:command) { 'ls' } 19 | let(:host) { double.as_null_object } 20 | let(:result) { Beaker::Result.new(host, command) } 21 | 22 | let(:master) { make_host('master', roles: %w[master agent default]) } 23 | let(:agent) { make_host('agent', roles: %w[agent]) } 24 | let(:custom) { make_host('custom', roles: %w[custom agent]) } 25 | let(:dash) { make_host('console', roles: %w[dashboard agent]) } 26 | let(:db) { make_host('db', roles: %w[database agent]) } 27 | let(:hosts) { [master, agent, dash, db, custom] } 28 | 29 | before :each do 30 | allow(subject).to receive(:hosts).and_return(hosts) 31 | end 32 | 33 | describe '#fact_on' do 34 | it 'retrieves a fact on a single host' do 35 | result.stdout = "{\"osfamily\": \"family\"}\n" 36 | expect(subject).to receive(:facter).with('"osfamily"', { json: nil }).once 37 | expect(subject).to receive(:on).and_return(result) 38 | 39 | expect(subject.fact_on('host', 'osfamily')).to be === JSON.parse(result.stdout)['osfamily'] 40 | end 41 | 42 | it 'converts each element to a structured fact when it receives an array of results from #on' do 43 | result.stdout = "{\"os\": {\"name\":\"name\", \"family\": \"family\"}}\n" 44 | times = hosts.length 45 | results_array = [result] * times 46 | parsed_array = [JSON.parse(result.stdout)['os']] * times 47 | allow(subject).to receive(:on).and_return(results_array) 48 | 49 | expect(subject.fact_on(hosts, 'os')).to be === parsed_array 50 | end 51 | 52 | it 'returns a single result for single host' do 53 | result.stdout = "{\"osfamily\": \"family\"}\n" 54 | parsed_result = JSON.parse(result.stdout)['osfamily'] 55 | allow(subject).to receive(:on).and_return(result) 56 | 57 | expect(subject.fact_on('host', 'osfamily')).to be === parsed_result 58 | end 59 | 60 | it 'preserves data types' do 61 | result.stdout = '{"identity": { "uid": 0, "user": "root", "privileged": true }}' 62 | allow(subject).to receive(:on).and_return(result) 63 | structured_fact = subject.fact_on('host', 'identity') 64 | 65 | expect(structured_fact['uid'].class).to be Integer 66 | expect(structured_fact['user'].class).to be String 67 | expect(structured_fact['privileged'].class).to be (TrueClass or FalseClass) 68 | end 69 | 70 | it 'raises an error when it receives a symbol for a fact' do 71 | expect { subject.fact_on('host', :osfamily) } 72 | .to raise_error(ArgumentError, /fact_on's `name` option must be a String. You provided a Symbol: 'osfamily'/) 73 | end 74 | end 75 | 76 | describe '#fact' do 77 | it 'delegates to #fact_on with the default host' do 78 | expect(subject).to receive(:fact_on).with(anything, 'osfamily', {}).once 79 | expect(subject).to receive(:default) 80 | 81 | subject.fact('osfamily') 82 | end 83 | end 84 | end 85 | -------------------------------------------------------------------------------- /setup/git/010_TestSetup.rb: -------------------------------------------------------------------------------- 1 | test_name 'Install repositories on target machines...' do 2 | repositories = options[:install].map do |url| 3 | extract_repo_info_from(build_git_url(url)) 4 | end 5 | 6 | agents.each_with_index do |host, index| 7 | on host, "echo #{GitHubSig} >> $HOME/.ssh/known_hosts" 8 | 9 | repositories.each do |repository| 10 | step "Install #{repository[:name]}" 11 | 12 | # If we're using docker, and we've mounted the puppet directory, let's run 13 | # tests from there rather than a github repo. In this case, we assume the 14 | # name of the mount corresponds to the thing we're trying to install. 15 | # 16 | # HOSTS: 17 | # hostname-1: 18 | # hypervisor: docker 19 | # mount_folders: 20 | # puppet: 21 | # host_path: ~/puppet 22 | # container_path: /build/puppet 23 | # 24 | if host[:mount_folders] 25 | mount = host[:mount_folders][repository[:name]] 26 | repository[:path] = "file://#{mount[:container_path]}" 27 | end 28 | repo_dir = host.tmpdir(repository[:name]) 29 | on(host, "chmod 755 #{repo_dir}") 30 | 31 | gem_source = ENV['GEM_SOURCE'] || 'https://rubygems.org' 32 | 33 | case repository[:path] 34 | when /^(git:|https:|git@)/ 35 | sha = ENV['SHA'] || `git rev-parse HEAD`.chomp 36 | gem_path = ":git => '#{repository[:path]}', :ref => '#{sha}'" 37 | when %r{^file://(.*)} 38 | gem_path = ":path => '#{Regexp.last_match(1)}'" 39 | else 40 | gem_path = repository[:path] 41 | end 42 | create_remote_file(host, "#{repo_dir}/Gemfile", <<~END) 43 | source '#{gem_source}' 44 | gem '#{repository[:name]}', #{gem_path} 45 | END 46 | 47 | case host['platform'] 48 | when /windows/ 49 | # bundle must be passed a Windows style path for a binstubs location 50 | bindir = host['puppetbindir'].split(':').first 51 | binstubs_dir = on(host, "cygpath -m \"#{bindir}\"").stdout.chomp 52 | # NOTE: passing --shebang to bundle is not useful because Cygwin 53 | # already finds the Ruby interpreter OK with the standard shebang of: 54 | # !/usr/bin/env ruby 55 | # the problem is a Cygwin style path is passed to the interpreter and this can't be modified: 56 | # http://cygwin.1069669.n5.nabble.com/Pass-windows-style-paths-to-the-interpreter-from-the-shebang-line-td43870.html 57 | on host, "cd #{repo_dir} && #{bundle_command(host)} install --system --binstubs '#{binstubs_dir}'" 58 | 59 | # bundler created but does not install batch files to the binstubs dir 60 | # so we have to manually copy the batch files over 61 | gemdir = on(host, "#{gem_command(host)} environment gemdir").stdout.chomp 62 | gembindir = File.join(gemdir, 'bin') 63 | on host, 64 | "cd '#{host['puppetbindir']}' && test -f ./#{repository[:name]}.bat || cp '#{gembindir}/#{repository[:name]}.bat' '#{host['puppetbindir']}/#{repository[:name]}.bat'" 65 | else 66 | on host, "cd #{repo_dir} && #{bundle_command(host)} install --system --binstubs #{host['puppetbindir']}" 67 | end 68 | puppet_bundler_install_dir ||= on(host, 69 | "cd #{repo_dir} && #{bundle_command(host)} show #{repository[:name]}").stdout.chomp 70 | host.add_env_var('RUBYLIB', File.join(puppet_bundler_install_dir, 'lib')) 71 | end 72 | end 73 | 74 | step 'Hosts: create environments directory like AIO does' do 75 | agents.each do |host| 76 | codedir = host.puppet['codedir'] 77 | on host, "mkdir -p #{codedir}/environments/production/manifests" 78 | on host, "mkdir -p #{codedir}/environments/production/modules" 79 | on host, "chmod -R 755 #{codedir}" 80 | end 81 | end 82 | end 83 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # beaker-puppet: The Puppet-Specific Beaker Library 2 | 3 | [![License](https://img.shields.io/github/license/puppetlabs/beaker-puppet.svg)](https://github.com/puppetlabs/beaker-puppet/blob/master/LICENSE) 4 | [![Test](https://github.com/puppetlabs/beaker-puppet/actions/workflows/test.yml/badge.svg)](https://github.com/puppetlabs/beaker-puppet/actions/workflows/test.yml) 5 | [![codecov](https://codecov.io/gh/puppetlabs/beaker-puppet/branch/master/graph/badge.svg?token=Mypkl78hvK)](https://codecov.io/gh/puppetlabs/beaker-puppet) 6 | [![Release](https://github.com/puppetlabs/beaker-puppet/actions/workflows/release.yml/badge.svg)](https://github.com/puppetlabs/beaker-puppet/actions/workflows/release.yml) 7 | [![RubyGem Version](https://img.shields.io/gem/v/beaker-puppet.svg)](https://rubygems.org/gems/beaker-puppet) 8 | [![RubyGem Downloads](https://img.shields.io/gem/dt/beaker-puppet.svg)](https://rubygems.org/gems/beaker-puppet) 9 | 10 | The purpose of this library is to hold all puppet-specific info & DSL methods. 11 | This includes all helper & installer methods. 12 | 13 | It might not be up to that state yet, but that's the goal for this library. If 14 | you see anything puppet-specific that you'd like to pull into this library out 15 | of beaker, please do, we would love any help that you'd like to provide. 16 | 17 | # How Do I Use This? 18 | 19 | You will need to include beaker-puppet alongside Beaker in your Gemfile or project.gemspec. E.g. 20 | 21 | ```ruby 22 | # Gemfile 23 | gem 'beaker', '~>4.0' 24 | gem 'beaker-puppet', '~>1.0' 25 | # project.gemspec 26 | s.add_runtime_dependency 'beaker', '~>4.0' 27 | s.add_runtime_dependency 'beaker-puppet', '~>1.0' 28 | ``` 29 | 30 | For DSL Extension Libraries, you must also ensure that you `require` the 31 | library in your test files. You can do this manually in individual test files 32 | or in a test helper (if you have one). You can [use 33 | `Bundler.require()`](https://bundler.io/v1.16/guides/groups.html) to require 34 | the library automatically. To explicitly require it: 35 | 36 | ```ruby 37 | require 'beaker-puppet' 38 | ``` 39 | 40 | Doing this will include (automatically) the beaker-puppet DSL methods in the 41 | beaker DSL. Then you can call beaker-puppet methods, exactly as you did before. 42 | 43 | ## Running Puppet in debug mode 44 | 45 | When using `apply_manifest` to run Puppet, it is common that you need debug 46 | output. To achieve this, the debug option can be passed. 47 | 48 | ```ruby 49 | apply_manifest_on(host, manifest, { debug: true }) 50 | ``` 51 | 52 | This has the downside that Puppet always runs in debug mode, which is very 53 | verbose. Of course you can modify the spec, but that's tedious. An easier 54 | alternative is to use the environment variable `BEAKER_PUPPET_DEBUG`. 55 | 56 | ```sh 57 | BEAKER_PUPPET_DEBUG=1 rspec spec/acceptance/my_spec.rb 58 | ``` 59 | 60 | # How Do I Test This? 61 | 62 | ### Unit / Spec Testing 63 | 64 | You can run the spec testing using our rake task `test:spec:run`. You can also run 65 | `rspec` directly. If you'd like to see a coverage report, then you can run the 66 | `test:spec:coverage` rake task. 67 | 68 | ### Acceptance Testing 69 | 70 | Acceptance testing can be run using the `acceptance` rake test namespace. For 71 | instance, to test using our package installation, you can run the 72 | `acceptance:pkg` task. 73 | 74 | Note in the above rake tasks that there are some environment variables that you 75 | can use to customize your run. For specifying your System Under Test (SUT) 76 | environment, you can use `BEAKER_HOSTS`, passing a file path to a beaker hosts 77 | file, or you can provide a beaker-hostgenerator value to the `TEST_TARGET` 78 | environment variable. You can also specify the tests that get executed with the 79 | `TESTS` environment variable. 80 | 81 | ## License 82 | 83 | This gem is licensed under the Apache-2 license. 84 | 85 | ## Release information 86 | 87 | To make a new release, please do: 88 | * update the version in `lib/beaker-puppet/version.rb` 89 | * Install gems with `bundle install --with release --path .vendor` 90 | * generate the changelog with `bundle exec rake changelog` 91 | * Check if the new version matches the closed issues/PRs in the changelog 92 | * Create a PR with it 93 | * After it got merged, push a tag. GitHub actions will do the actual release to rubygems and GitHub Packages 94 | -------------------------------------------------------------------------------- /setup/git/070_InstallCACerts.rb: -------------------------------------------------------------------------------- 1 | test_name 'Install CA Certs' 2 | confine :to, platform: 'windows' 3 | 4 | GEOTRUST_GLOBAL_CA = <<~EOM 5 | -----BEGIN CERTIFICATE----- 6 | MIIDVDCCAjygAwIBAgIDAjRWMA0GCSqGSIb3DQEBBQUAMEIxCzAJBgNVBAYTAlVT 7 | MRYwFAYDVQQKEw1HZW9UcnVzdCBJbmMuMRswGQYDVQQDExJHZW9UcnVzdCBHbG9i 8 | YWwgQ0EwHhcNMDIwNTIxMDQwMDAwWhcNMjIwNTIxMDQwMDAwWjBCMQswCQYDVQQG 9 | EwJVUzEWMBQGA1UEChMNR2VvVHJ1c3QgSW5jLjEbMBkGA1UEAxMSR2VvVHJ1c3Qg 10 | R2xvYmFsIENBMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA2swYYzD9 11 | 9BcjGlZ+W988bDjkcbd4kdS8odhM+KhDtgPpTSEHCIjaWC9mOSm9BXiLnTjoBbdq 12 | fnGk5sRgprDvgOSJKA+eJdbtg/OtppHHmMlCGDUUna2YRpIuT8rxh0PBFpVXLVDv 13 | iS2Aelet8u5fa9IAjbkU+BQVNdnARqN7csiRv8lVK83Qlz6cJmTM386DGXHKTubU 14 | 1XupGc1V3sjs0l44U+VcT4wt/lAjNvxm5suOpDkZALeVAjmRCw7+OC7RHQWa9k0+ 15 | bw8HHa8sHo9gOeL6NlMTOdReJivbPagUvTLrGAMoUgRx5aszPeE4uwc2hGKceeoW 16 | MPRfwCvocWvk+QIDAQABo1MwUTAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBTA 17 | ephojYn7qwVkDBF9qn1luMrMTjAfBgNVHSMEGDAWgBTAephojYn7qwVkDBF9qn1l 18 | uMrMTjANBgkqhkiG9w0BAQUFAAOCAQEANeMpauUvXVSOKVCUn5kaFOSPeCpilKIn 19 | Z57QzxpeR+nBsqTP3UEaBU6bS+5Kb1VSsyShNwrrZHYqLizz/Tt1kL/6cdjHPTfS 20 | tQWVYrmm3ok9Nns4d0iXrKYgjy6myQzCsplFAMfOEVEiIuCl6rYVSAlk6l5PdPcF 21 | PseKUgzbFbS9bZvlxrFUaKnjaZC2mqUPuLk/IH2uSrW4nOQdtqvmlKXBx4Ot2/Un 22 | hw4EbNX/3aBd7YdStysVAq45pmp06drE57xNNB6pXE0zX5IJL4hmXXeXxx12E6nV 23 | 5fEWCRE11azbJHFwLJhWC9kXtNHjUStedejV0NxPNO3CBWaAocvmMw== 24 | -----END CERTIFICATE----- 25 | EOM 26 | 27 | USERTRUST_NETWORK_CA = <<~EOM 28 | -----BEGIN CERTIFICATE----- 29 | MIIEdDCCA1ygAwIBAgIQRL4Mi1AAJLQR0zYq/mUK/TANBgkqhkiG9w0BAQUFADCB 30 | lzELMAkGA1UEBhMCVVMxCzAJBgNVBAgTAlVUMRcwFQYDVQQHEw5TYWx0IExha2Ug 31 | Q2l0eTEeMBwGA1UEChMVVGhlIFVTRVJUUlVTVCBOZXR3b3JrMSEwHwYDVQQLExho 32 | dHRwOi8vd3d3LnVzZXJ0cnVzdC5jb20xHzAdBgNVBAMTFlVUTi1VU0VSRmlyc3Qt 33 | SGFyZHdhcmUwHhcNOTkwNzA5MTgxMDQyWhcNMTkwNzA5MTgxOTIyWjCBlzELMAkG 34 | A1UEBhMCVVMxCzAJBgNVBAgTAlVUMRcwFQYDVQQHEw5TYWx0IExha2UgQ2l0eTEe 35 | MBwGA1UEChMVVGhlIFVTRVJUUlVTVCBOZXR3b3JrMSEwHwYDVQQLExhodHRwOi8v 36 | d3d3LnVzZXJ0cnVzdC5jb20xHzAdBgNVBAMTFlVUTi1VU0VSRmlyc3QtSGFyZHdh 37 | cmUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCx98M4P7Sof885glFn 38 | 0G2f0v9Y8+efK+wNiVSZuTiZFvfgIXlIwrthdBKWHTxqctU8EGc6Oe0rE81m65UJ 39 | M6Rsl7HoxuzBdXmcRl6Nq9Bq/bkqVRcQVLMZ8Jr28bFdtqdt++BxF2uiiPsA3/4a 40 | MXcMmgF6sTLjKwEHOG7DpV4jvEWbe1DByTCP2+UretNb+zNAHqDVmBe8i4fDidNd 41 | oI6yqqr2jmmIBsX6iSHzCJ1pLgkzmykNRg+MzEk0sGlRvfkGzWitZky8PqxhvQqI 42 | DsjfPe58BEydCl5rkdbux+0ojatNh4lz0G6k0B4WixThdkQDf2Os5M1JnMWS9Ksy 43 | oUhbAgMBAAGjgbkwgbYwCwYDVR0PBAQDAgHGMA8GA1UdEwEB/wQFMAMBAf8wHQYD 44 | VR0OBBYEFKFyXyYbKJhDlV0HN9WFlp1L0sNFMEQGA1UdHwQ9MDswOaA3oDWGM2h0 45 | dHA6Ly9jcmwudXNlcnRydXN0LmNvbS9VVE4tVVNFUkZpcnN0LUhhcmR3YXJlLmNy 46 | bDAxBgNVHSUEKjAoBggrBgEFBQcDAQYIKwYBBQUHAwUGCCsGAQUFBwMGBggrBgEF 47 | BQcDBzANBgkqhkiG9w0BAQUFAAOCAQEARxkP3nTGmZev/K0oXnWO6y1n7k57K9cM 48 | //bey1WiCuFMVGWTYGufEpytXoMs61quwOQt9ABjHbjAbPLPSbtNk28Gpgoiskli 49 | CE7/yMgUsogWXecB5BKV5UU0s4tpvc+0hY91UZ59Ojg6FEgSxvunOxqNDYJAB+gE 50 | CJChicsZUN/KHAG8HQQZexB2lzvukJDKxA4fFm517zP4029bHpbj4HR3dHuKom4t 51 | 3XbWOTCC8KucUvIqx69JXn7HaOWCgchqJ/kniCrVWFCVH/A7HFe7fRQ5YiuayZSS 52 | KqMiDP+JJn1fIytH1xUdqWqeUQ0qUZ6B+dQ7XnASfxAynB67nfhmqA== 53 | -----END CERTIFICATE----- 54 | EOM 55 | 56 | EQUIFAX_CA = <<~EOM 57 | -----BEGIN CERTIFICATE----- 58 | MIIDIDCCAomgAwIBAgIENd70zzANBgkqhkiG9w0BAQUFADBOMQswCQYDVQQGEwJV 59 | UzEQMA4GA1UEChMHRXF1aWZheDEtMCsGA1UECxMkRXF1aWZheCBTZWN1cmUgQ2Vy 60 | dGlmaWNhdGUgQXV0aG9yaXR5MB4XDTk4MDgyMjE2NDE1MVoXDTE4MDgyMjE2NDE1 61 | MVowTjELMAkGA1UEBhMCVVMxEDAOBgNVBAoTB0VxdWlmYXgxLTArBgNVBAsTJEVx 62 | dWlmYXggU2VjdXJlIENlcnRpZmljYXRlIEF1dGhvcml0eTCBnzANBgkqhkiG9w0B 63 | AQEFAAOBjQAwgYkCgYEAwV2xWGcIYu6gmi0fCG2RFGiYCh7+2gRvE4RiIcPRfM6f 64 | BeC4AfBONOziipUEZKzxa1NfBbPLZ4C/QgKO/t0BCezhABRP/PvwDN1Dulsr4R+A 65 | cJkVV5MW8Q+XarfCaCMczE1ZMKxRHjuvK9buY0V7xdlfUNLjUA86iOe/FP3gx7kC 66 | AwEAAaOCAQkwggEFMHAGA1UdHwRpMGcwZaBjoGGkXzBdMQswCQYDVQQGEwJVUzEQ 67 | MA4GA1UEChMHRXF1aWZheDEtMCsGA1UECxMkRXF1aWZheCBTZWN1cmUgQ2VydGlm 68 | aWNhdGUgQXV0aG9yaXR5MQ0wCwYDVQQDEwRDUkwxMBoGA1UdEAQTMBGBDzIwMTgw 69 | ODIyMTY0MTUxWjALBgNVHQ8EBAMCAQYwHwYDVR0jBBgwFoAUSOZo+SvSspXXR9gj 70 | IBBPM5iQn9QwHQYDVR0OBBYEFEjmaPkr0rKV10fYIyAQTzOYkJ/UMAwGA1UdEwQF 71 | MAMBAf8wGgYJKoZIhvZ9B0EABA0wCxsFVjMuMGMDAgbAMA0GCSqGSIb3DQEBBQUA 72 | A4GBAFjOKer89961zgK5F7WF0bnj4JXMJTENAKaSbn+2kmOeUJXRmm/kEd5jhW6Y 73 | 7qj/WsjTVbJmcVfewCHrPSqnI0kBBIZCe/zuf6IWUrVnZ9NA2zsmWLIodz2uFHdh 74 | 1voqZiegDfqnc1zqcPGUIWVEX/r87yloqaKHee9570+sB3c4 75 | -----END CERTIFICATE----- 76 | EOM 77 | 78 | hosts.each do |host| 79 | next if host['use_existing_container'] 80 | 81 | step 'Installing Geotrust CA cert' 82 | create_remote_file(host, 'geotrustglobal.pem', GEOTRUST_GLOBAL_CA) 83 | on host, 'chmod 644 geotrustglobal.pem' 84 | on host, 'cmd /c certutil -v -addstore Root `cygpath -w geotrustglobal.pem`' 85 | 86 | step 'Installing Usertrust Network CA cert' 87 | create_remote_file(host, 'usertrust-network.pem', USERTRUST_NETWORK_CA) 88 | on host, 'chmod 644 usertrust-network.pem' 89 | on host, 'cmd /c certutil -v -addstore Root `cygpath -w usertrust-network.pem`' 90 | 91 | step 'Installing Equifax CA cert' 92 | create_remote_file(host, 'equifax.pem', EQUIFAX_CA) 93 | on host, 'chmod 644 equifax.pem' 94 | on host, 'cmd /c certutil -v -addstore Root `cygpath -w equifax.pem`' 95 | end 96 | -------------------------------------------------------------------------------- /lib/beaker-puppet/install_utils/aio_defaults.rb: -------------------------------------------------------------------------------- 1 | module Beaker 2 | module DSL 3 | module InstallUtils 4 | # 5 | # This module contains default values for aio paths and directorys per-platform 6 | # 7 | module AIODefaults 8 | # Here be the pathing and default values for AIO installs 9 | # 10 | AIO_DEFAULTS = { 11 | 'unix' => { 12 | 'puppetbindir' => '/opt/puppetlabs/bin', 13 | 'privatebindir' => '/opt/puppetlabs/puppet/bin', 14 | 'distmoduledir' => '/etc/puppetlabs/code/modules', 15 | 'sitemoduledir' => '/opt/puppetlabs/puppet/modules', 16 | }, 17 | # sitemoduledir not included on Windows (check PUP-4049 for more info). 18 | # 19 | # Paths to the puppet's vendored ruby installation on Windows were 20 | # updated in Puppet 6 to more closely match those of *nix agents. 21 | # These path values include both the older (puppet <= 5) paths (which 22 | # include sys/ruby) and the newer versions, which have no custom ruby 23 | # directory 24 | 'windows' => { # windows with cygwin 25 | 'puppetbindir' => '/cygdrive/c/Program Files (x86)/Puppet Labs/Puppet/bin', 26 | 'privatebindir' => '/cygdrive/c/Program Files (x86)/Puppet Labs/Puppet/puppet/bin:/cygdrive/c/Program Files (x86)/Puppet Labs/Puppet/sys/ruby/bin', 27 | 'distmoduledir' => '`cygpath -smF 35`/PuppetLabs/code/modules', 28 | }, 29 | 'windows-64' => { # windows with cygwin 30 | 'puppetbindir' => '/cygdrive/c/Program Files/Puppet Labs/Puppet/bin', 31 | 'privatebindir' => '/cygdrive/c/Program Files/Puppet Labs/Puppet/puppet/bin:/cygdrive/c/Program Files/Puppet Labs/Puppet/sys/ruby/bin', 32 | 'distmoduledir' => '`cygpath -smF 35`/PuppetLabs/code/modules', 33 | }, 34 | 'pswindows' => { # pure windows 35 | 'puppetbindir' => '"C:\\Program Files (x86)\\Puppet Labs\\Puppet\\bin";"C:\\Program Files\\Puppet Labs\\Puppet\\bin"', 36 | 'privatebindir' => '"C:\\Program Files (x86)\\Puppet Labs\\Puppet\\puppet\\bin";"C:\\Program Files\\Puppet Labs\\Puppet\\puppet\\bin";"C:\\Program Files (x86)\\Puppet Labs\\Puppet\\sys\\ruby\\bin";"C:\\Program Files\\Puppet Labs\\Puppet\\sys\\ruby\\bin"', 37 | 'distmoduledir' => 'C:\\ProgramData\\PuppetLabs\\code\\modules', 38 | }, 39 | } 40 | 41 | # Add the appropriate aio defaults to the host object so that they can be accessed using host[option], set host[:type] = aio 42 | # @param [Host] host A single host to act upon 43 | # @param [String] platform The platform type of this host, one of 'windows', 'pswindows', or 'unix' 44 | def add_platform_aio_defaults(host, platform) 45 | AIO_DEFAULTS[platform].each_pair do |key, val| 46 | host[key] = val 47 | end 48 | # add group and type here for backwards compatability 49 | host['group'] = if host['platform'] =~ /windows/ 50 | 'Administrators' 51 | else 52 | 'puppet' 53 | end 54 | end 55 | 56 | # Add the appropriate aio defaults to an array of hosts 57 | # @param [Host, Array, String, Symbol] hosts One or more hosts to act upon, 58 | # or a role (String or Symbol) that identifies one or more hosts. 59 | def add_aio_defaults_on(hosts) 60 | block_on hosts do |host| 61 | if host.is_powershell? 62 | platform = 'pswindows' 63 | elsif host['platform'] =~ /windows/ 64 | ruby_arch = if host[:ruby_arch] == 'x64' 65 | /-64/ 66 | else 67 | /-32/ 68 | end 69 | platform = if host['platform'] =~ ruby_arch 70 | 'windows-64' 71 | else 72 | 'windows' 73 | end 74 | else 75 | platform = 'unix' 76 | end 77 | add_platform_aio_defaults(host, platform) 78 | end 79 | end 80 | 81 | # Remove the appropriate aio defaults from the host object so that they can no longer be accessed using host[option], set host[:type] = nil 82 | # @param [Host] host A single host to act upon 83 | # @param [String] platform The platform type of this host, one of windows, pswindows, freebsd, mac & unix 84 | def remove_platform_aio_defaults(host, platform) 85 | AIO_DEFAULTS[platform].each_pair do |key, val| 86 | host.delete(key) 87 | end 88 | host['group'] = nil 89 | end 90 | 91 | # Remove the appropriate aio defaults from an array of hosts 92 | # @param [Host, Array, String, Symbol] hosts One or more hosts to act upon, 93 | # or a role (String or Symbol) that identifies one or more hosts. 94 | def remove_aio_defaults_on(hosts) 95 | block_on hosts do |host| 96 | platform = if host.is_powershell? 97 | 'pswindows' 98 | elsif host['platform'] =~ /windows/ 99 | 'windows' 100 | else 101 | 'unix' 102 | end 103 | remove_platform_aio_defaults(host, platform) 104 | end 105 | end 106 | end 107 | end 108 | end 109 | end 110 | -------------------------------------------------------------------------------- /setup/git/000_EnvSetup.rb: -------------------------------------------------------------------------------- 1 | test_name 'Setup environment' 2 | 3 | require 'json' 4 | require 'open-uri' 5 | 6 | step 'Configure paths' do 7 | add_aio_defaults_on(hosts) 8 | add_puppet_paths_on(hosts) 9 | end 10 | 11 | step 'Install git and tar' 12 | PACKAGES = { 13 | redhat: ['git'], 14 | debian: [ 15 | %w[git git-core], 16 | ], 17 | solaris_10: %w[ 18 | coreutils 19 | git 20 | gtar 21 | ], 22 | windows: ['git'], 23 | sles: ['git'], 24 | } 25 | 26 | # We need to be able override which tar we use on solaris, which we call later 27 | # when we unpack the puppet-runtime archive 28 | tar = 'tar' 29 | 30 | agents.each do |host| 31 | case host['platform'] 32 | when /solaris/ 33 | tar = 'gtar' 34 | if host['platform'] =~ /11/ 35 | # The file allows us to non-interactively install these packages with 36 | # pkgutil on solaris 11. Solaris 10 does this as a part of the 37 | # `install_packages_on` method in beaker. Since we install packages for 38 | # solaris 11 using pkg by default, we can't use that method for sol11. 39 | # We have to override it so that we can get git from opencws, as it has 40 | # the updated ssl certs we need to access github repos. 41 | create_remote_file host, '/root/shutupsolaris', <<~END 42 | mail= 43 | # Overwrite already installed instances 44 | instance=overwrite 45 | # Do not bother checking for partially installed packages 46 | partial=nocheck 47 | # Do not bother checking the runlevel 48 | runlevel=nocheck 49 | # Do not bother checking package dependencies (We take care of this) 50 | idepend=nocheck 51 | rdepend=nocheck 52 | # DO check for available free space and abort if there isn't enough 53 | space=quit 54 | # Do not check for setuid files. 55 | setuid=nocheck 56 | # Do not check if files conflict with other packages 57 | conflict=nocheck 58 | # We have no action scripts. Do not check for them. 59 | action=nocheck 60 | # Install to the default base directory. 61 | basedir=default 62 | END 63 | on host, 'pkgadd -d http://get.opencsw.org/now -a /root/shutupsolaris -n all' 64 | on host, '/opt/csw/bin/pkgutil -U all' 65 | on host, '/opt/csw/bin/pkgutil -y -i git' 66 | on host, '/opt/csw/bin/pkgutil -y -i gtar' 67 | end 68 | host.add_env_var('PATH', '/opt/csw/bin') 69 | end 70 | end 71 | 72 | install_packages_on(agents, PACKAGES, check_if_exists: true) 73 | 74 | step 'Unpack puppet-runtime' do 75 | need_to_run = false 76 | agents.each do |host| 77 | # we only need to unpack the runtime if the host doesn't already have runtime 78 | # and if it's a not an existing container 79 | need_to_run ||= (!host['has_runtime'] && !host['use_existing_container']) 80 | end 81 | 82 | if need_to_run 83 | dev_builds_url = ENV['DEV_BUILDS_URL'] || 'http://builds.delivery.puppetlabs.net' 84 | branch = ENV['RUNTIME_BRANCH'] || 'master' 85 | 86 | # We want to grab whatever tag has been promoted most recently into the branch 87 | # of puppet-agent that corresponds to whatever component we're working on. 88 | # This will allow us to get the latest runtime package that has passed tests. 89 | runtime_json = "https://raw.githubusercontent.com/puppetlabs/puppet-agent/#{branch}/configs/components/puppet-runtime.json" 90 | runtime_tag = JSON.load(open(runtime_json))['version'] 91 | 92 | runtime_url = "#{dev_builds_url}/puppet-runtime/#{runtime_tag}/artifacts/" 93 | 94 | runtime_prefix = "agent-runtime-#{branch}-#{runtime_tag}." 95 | runtime_suffix = '.tar.gz' 96 | 97 | agents.each do |host| 98 | next if host['has_runtime'] || host['use_existing_container'] 99 | 100 | platform_tag = host['packaging_platform'] 101 | if platform_tag =~ /windows/ 102 | # the windows version is hard coded to `2012r2`. Unfortunately, 103 | # `host['packaging_platform']` is hard coded to `2012`, so we have to add 104 | # the `r2` on our own. 105 | platform, version, arch = platform_tag.split('-') 106 | platform_tag = "#{platform}-#{version}r2-#{arch}" 107 | end 108 | tarball_name = runtime_prefix + platform_tag + runtime_suffix 109 | 110 | on host, "curl -Of #{runtime_url}#{tarball_name}" 111 | 112 | case host['platform'] 113 | when /windows/ 114 | on host, "gunzip -c #{tarball_name} | tar -k -C /cygdrive/c/ -xf -" 115 | 116 | program_files = if arch == 'x64' 117 | 'ProgramFiles64Folder' 118 | else 119 | 'ProgramFilesFolder' 120 | end 121 | bindir = if branch == '5.5.x' 122 | "/cygdrive/c/#{program_files}/PuppetLabs/Puppet/sys/ruby/bin" 123 | else 124 | "/cygdrive/c/#{program_files}/PuppetLabs/Puppet/puppet/bin" 125 | end 126 | on host, "chmod 755 #{bindir}/*" 127 | 128 | # Because the runtime archive for windows gets installed in a non-standard 129 | # directory (ProgramFiles64Folder), we need to add it to the path here 130 | # rather than rely on `host['privatebindir']` like we can for other 131 | # platforms 132 | host.add_env_var('PATH', bindir) 133 | when /osx/ 134 | on host, "tar -xzf #{tarball_name}" 135 | on host, 'for d in opt var private; do rsync -ka "${d}/" "/${d}/"; done' 136 | else 137 | on host, "gunzip -c #{tarball_name} | #{tar} -k -C / -xf -" 138 | end 139 | end 140 | end 141 | end 142 | 143 | step 'Install bundler' do 144 | # Only configure gem mirror after Ruby has been installed, but before any gems are installed. 145 | configure_gem_mirror(agents) 146 | 147 | agents.each do |host| 148 | on host, "#{gem_command(host)} install bundler --no-document" 149 | end 150 | end 151 | -------------------------------------------------------------------------------- /spec/helpers.rb: -------------------------------------------------------------------------------- 1 | module TestFileHelpers 2 | def create_files(file_array) 3 | file_array.each do |f| 4 | FileUtils.mkdir_p File.dirname(f) 5 | FileUtils.touch f 6 | end 7 | end 8 | 9 | def fog_file_contents 10 | { default: { aws_access_key_id: 'IMANACCESSKEY', 11 | aws_secret_access_key: 'supersekritkey', 12 | aix_hypervisor_server: 'aix_hypervisor.labs.net', 13 | aix_hypervisor_username: 'aixer', 14 | aix_hypervisor_keyfile: '/Users/user/.ssh/id_rsa-acceptance', 15 | solaris_hypervisor_server: 'solaris_hypervisor.labs.net', 16 | solaris_hypervisor_username: 'harness', 17 | solaris_hypervisor_keyfile: '/Users/user/.ssh/id_rsa-old.private', 18 | solaris_hypervisor_vmpath: 'rpoooool/zs', 19 | solaris_hypervisor_snappaths: ['rpoooool/USER/z0'], 20 | vsphere_server: 'vsphere.labs.net', 21 | vsphere_username: 'vsphere@labs.com', 22 | vsphere_password: 'supersekritpassword', } } 23 | end 24 | end 25 | 26 | module HostHelpers 27 | HOST_DEFAULTS = { platform: 'unix', 28 | roles: ['agent'], 29 | snapshot: 'snap', 30 | ip: 'default.ip.address', 31 | private_ip: 'private.ip.address', 32 | dns_name: 'default.box.tld', 33 | box: 'default_box_name', 34 | box_url: 'http://default.box.url', 35 | image: 'default_image', 36 | flavor: 'm1.large', 37 | user_data: '#cloud-config\nmanage_etc_hosts: true\nfinal_message: "The host is finally up!"', } 38 | 39 | HOST_NAME = 'vm%d' 40 | HOST_SNAPSHOT = 'snapshot%d' 41 | HOST_IP = 'ip.address.for.%s' 42 | HOST_BOX = 'vm2%s_of_my_box' 43 | HOST_BOX_URL = 'http://address.for.my.box.%s' 44 | HOST_DNS_NAME = '%s.box.tld' 45 | HOST_TEMPLATE = '%s_has_a_template' 46 | HOST_PRIVATE_IP = 'private.ip.for.%s' 47 | 48 | def logger 49 | double('logger').as_null_object 50 | end 51 | 52 | def make_opts 53 | opts = Beaker::Options::Presets.new 54 | opts.presets.merge(opts.env_vars).merge({ logger: logger, 55 | host_config: 'sample.config', 56 | type: nil, 57 | pooling_api: 'http://vcloud.delivery.puppetlabs.net/', 58 | datastore: 'instance0', 59 | folder: 'Delivery/Quality Assurance/Staging/Dynamic', 60 | resourcepool: 'delivery/Quality Assurance/Staging/Dynamic', 61 | gce_project: 'beaker-compute', 62 | gce_keyfile: '/path/to/keyfile.p12', 63 | gce_password: 'notasecret', 64 | gce_email: '12345678910@developer.gserviceaccount.com', 65 | openstack_api_key: 'P1as$w0rd', 66 | openstack_username: 'user', 67 | openstack_auth_url: 'http://openstack_hypervisor.labs.net:5000/v2.0/tokens', 68 | openstack_tenant: 'testing', 69 | openstack_network: 'testing', 70 | openstack_keyname: 'nopass', 71 | floating_ip_pool: 'my_pool', 72 | security_group: %w[my_sg default], }) 73 | end 74 | 75 | def generate_result(name, opts) 76 | result = double('result') 77 | stdout = opts.has_key?(:stdout) ? opts[:stdout] : name 78 | stderr = opts.has_key?(:stderr) ? opts[:stderr] : name 79 | exit_code = opts.has_key?(:exit_code) ? opts[:exit_code] : 0 80 | exit_code = [exit_code].flatten 81 | allow(result).to receive(:stdout).and_return(stdout) 82 | allow(result).to receive(:stderr).and_return(stderr) 83 | allow(result).to receive(:exit_code).and_return(*exit_code) 84 | result 85 | end 86 | 87 | def make_host_opts(name, opts) 88 | make_opts.merge({ 'HOSTS' => { name => opts } }).merge(opts) 89 | end 90 | 91 | def make_host(name, host_hash) 92 | host_hash = Beaker::Options::OptionsHash.new.merge(HOST_DEFAULTS.merge(host_hash)) 93 | 94 | host = Beaker::Host.create(name, host_hash, make_opts) 95 | 96 | allow(host).to receive(:exec).and_return(generate_result(name, host_hash)) 97 | allow(host).to receive(:close) 98 | host 99 | end 100 | 101 | def make_hosts(preset_opts = {}, amt = 3) 102 | hosts = [] 103 | (1..amt).each do |num| 104 | name = HOST_NAME % num 105 | opts = { snapshot: HOST_SNAPSHOT % num, 106 | ip: HOST_IP % name, 107 | private_ip: HOST_PRIVATE_IP % name, 108 | dns_name: HOST_DNS_NAME % name, 109 | template: HOST_TEMPLATE % name, 110 | box: HOST_BOX % name, 111 | box_url: HOST_BOX_URL % name, }.merge(preset_opts) 112 | hosts << make_host(name, opts) 113 | end 114 | hosts 115 | end 116 | 117 | def make_instance(instance_data = {}) 118 | OpenStruct.new instance_data 119 | end 120 | end 121 | 122 | module PlatformHelpers 123 | DEBIANPLATFORMS = %w[debian ubuntu] 124 | 125 | FEDORASYSTEMD = (14..29).to_a.collect! { |i| "fedora-#{i}" } 126 | 127 | SYSTEMDPLATFORMS = %w[el-7 128 | centos-7 129 | redhat-7 130 | oracle-7 131 | scientific-7 132 | eos-7].concat(FEDORASYSTEMD) 133 | 134 | FEDORASYSTEMV = (1..13).to_a.collect! { |i| "fedora-#{i}" } 135 | 136 | SYSTEMVPLATFORMS = ['el-', 137 | 'centos', 138 | 'fedora', 139 | 'redhat', 140 | 'oracle', 141 | 'scientific', 142 | 'eos',].concat(FEDORASYSTEMV) 143 | end 144 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'rspec/core/rake_task' 2 | require 'securerandom' 3 | require 'beaker-hostgenerator' 4 | 5 | namespace :test do 6 | namespace :spec do 7 | desc 'Run spec tests' 8 | RSpec::Core::RakeTask.new(:run) do |t| 9 | t.rspec_opts = ['--color'] 10 | t.pattern = 'spec/' 11 | end 12 | 13 | desc 'Run spec tests with coverage' 14 | RSpec::Core::RakeTask.new(:coverage) do |t| 15 | ENV['BEAKER_PUPPET_COVERAGE'] = 'y' 16 | t.rspec_opts = ['--color'] 17 | t.pattern = 'spec/' 18 | end 19 | end 20 | 21 | namespace :acceptance do 22 | USAGE = <<~EOS 23 | You may set BEAKER_HOSTS=config/nodes/foo.yaml or include it in an acceptance-options.rb for Beaker, 24 | or specify TEST_TARGET in a form beaker-hostgenerator accepts, e.g. ubuntu1504-64a. 25 | You may override the default master test target by specifying MASTER_TEST_TARGET. 26 | You may set TESTS=path/to/test,and/more/tests. 27 | You may set additional Beaker OPTIONS='--more --options' 28 | If there is a Beaker options hash in a ./acceptance/local_options.rb, it will be included. 29 | Commandline options set through the above environment variables will override settings in this file. 30 | EOS 31 | 32 | desc <<~EOS 33 | Run the puppet beaker acceptance tests on a puppet gem install. 34 | #{USAGE} 35 | EOS 36 | task gem: 'gen_hosts' do 37 | beaker_test(:gem) 38 | end 39 | 40 | desc <<~EOS 41 | Run the puppet beaker acceptance tests on a puppet git install. 42 | #{USAGE} 43 | EOS 44 | task git: 'gen_hosts' do 45 | beaker_test(:git) 46 | end 47 | 48 | desc <<~EOS 49 | Run the puppet beaker acceptance tests on a puppet package install. 50 | #{USAGE} 51 | EOS 52 | task pkg: 'gen_hosts' do 53 | beaker_test(:pkg) 54 | end 55 | 56 | desc <<~EOS 57 | Run the puppet beaker acceptance tests on a base system (no install). 58 | #{USAGE} 59 | EOS 60 | task base: 'gen_hosts' do 61 | beaker_test 62 | end 63 | 64 | desc 'Generate Beaker Host Config File' 65 | task :gen_hosts do 66 | next if hosts_file_env 67 | 68 | cli = BeakerHostGenerator::CLI.new([test_targets]) 69 | FileUtils.mkdir_p('tmp') # -p ignores when dir already exists 70 | File.open("tmp/#{HOSTS_FILE}", 'w') do |fh| 71 | fh.print(cli.execute) 72 | end 73 | end 74 | 75 | def hosts_opt(use_preserved_hosts = false) 76 | if use_preserved_hosts 77 | "--hosts=#{HOSTS_PRESERVED}" 78 | elsif hosts_file_env 79 | "--hosts=#{hosts_file_env}" 80 | else 81 | "--hosts=tmp/#{HOSTS_FILE}" 82 | end 83 | end 84 | 85 | def hosts_file_env 86 | ENV.fetch('BEAKER_HOSTS', nil) 87 | end 88 | 89 | def agent_target 90 | ENV['TEST_TARGET'] || 'redhat7-64af' 91 | end 92 | 93 | def master_target 94 | ENV['MASTER_TEST_TARGET'] || 'redhat7-64default.mdcal' 95 | end 96 | 97 | def test_targets 98 | ENV['LAYOUT'] || "#{master_target}-#{agent_target}" 99 | end 100 | 101 | HOSTS_FILE = "#{test_targets}-#{SecureRandom.uuid}.yaml" 102 | HOSTS_PRESERVED = 'log/latest/hosts_preserved.yml' 103 | 104 | def beaker_test(mode = :base, options = {}) 105 | preserved_hosts_mode = options[:hosts] == HOSTS_PRESERVED 106 | final_options = HarnessOptions.final_options(mode, options) 107 | 108 | options_opt = '' 109 | # preserved hosts can not be used with an options file (BKR-670) 110 | # one can still use OPTIONS 111 | 112 | unless preserved_hosts_mode 113 | options_file = 'merged_options.rb' 114 | options_opt = "--options-file=#{options_file}" 115 | File.open(options_file, 'w') do |merged| 116 | merged.puts <<~EOS 117 | # Copy this file to local_options.rb and adjust as needed if you wish to run 118 | # with some local overrides. 119 | EOS 120 | merged.puts(final_options) 121 | end 122 | end 123 | 124 | tests = ENV['TESTS'] || ENV.fetch('TEST', nil) 125 | tests_opt = '' 126 | tests_opt = "--tests=#{tests}" if tests 127 | 128 | overriding_options = ENV['OPTIONS'].to_s 129 | 130 | args = [options_opt, hosts_opt(preserved_hosts_mode), tests_opt, 131 | *overriding_options.split(' '),].compact 132 | 133 | sh('beaker', *args) 134 | end 135 | 136 | module HarnessOptions 137 | defaults = { 138 | tests: ['tests'], 139 | log_level: 'debug', 140 | preserve_hosts: 'onfail', 141 | } 142 | 143 | DEFAULTS = defaults 144 | 145 | def self.get_options(file_path) 146 | puts "Attempting to merge config file: #{file_path}" 147 | if File.exist? file_path 148 | options = eval(File.read(file_path), binding) 149 | else 150 | puts "No options file found at #{File.expand_path(file_path)}... skipping" 151 | end 152 | options || {} 153 | end 154 | 155 | def self.get_mode_options(mode) 156 | get_options("./acceptance/config/#{mode}/acceptance-options.rb") 157 | end 158 | 159 | def self.get_local_options 160 | get_options('./acceptance/local_options.rb') 161 | end 162 | 163 | def self.final_options(mode, intermediary_options = {}) 164 | mode_options = get_mode_options(mode) 165 | local_overrides = get_local_options 166 | final_options = DEFAULTS.merge(mode_options) 167 | final_options.merge!(intermediary_options) 168 | final_options.merge!(local_overrides) 169 | end 170 | end 171 | end 172 | end 173 | 174 | # namespace-named default tasks. 175 | # these are the default tasks invoked when only the namespace is referenced. 176 | # they're needed because `task :default` in those blocks doesn't work as expected. 177 | task 'test:spec' => 'test:spec:run' 178 | task 'test:acceptance' => 'test:acceptance:quick' 179 | 180 | # global defaults 181 | task test: 'test:spec' 182 | task default: :test 183 | 184 | begin 185 | require 'github_changelog_generator/task' 186 | rescue LoadError 187 | # GCG is an optional group 188 | else 189 | GitHubChangelogGenerator::RakeTask.new :changelog do |config| 190 | config.header = "# Changelog\n\nAll notable changes to this project will be documented in this file." 191 | config.exclude_labels = %w[duplicate question invalid wontfix wont-fix skip-changelog] 192 | config.user = 'puppetlabs' 193 | config.project = 'beaker-puppet' 194 | config.future_release = "#{Gem::Specification.load("#{config.project}.gemspec").version}" 195 | end 196 | end 197 | 198 | begin 199 | require 'rubocop/rake_task' 200 | rescue LoadError 201 | # RuboCop is an optional group 202 | else 203 | RuboCop::RakeTask.new(:rubocop) do |task| 204 | # These make the rubocop experience maybe slightly less terrible 205 | task.options = ['--display-cop-names', '--display-style-guide', '--extra-details'] 206 | end 207 | end 208 | -------------------------------------------------------------------------------- /setup/common/003_solaris_cert_fix.rb: -------------------------------------------------------------------------------- 1 | test_name 'Add digicert to solaris keystore' 2 | 3 | # Only need to run this on soliars 11, 11.2, and sparc 4 | skip_test 'No solaris 11 version that needed keystore updating' unless hosts.any? do |host| 5 | host.platform =~ /solaris-11(\.2)?-(i386|sparc)/ 6 | end 7 | 8 | DigiCert = <<~EOM 9 | -----BEGIN CERTIFICATE----- 10 | MIIFkDCCA3igAwIBAgIQBZsbV56OITLiOQe9p3d1XDANBgkqhkiG9w0BAQwFADBi 11 | MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 12 | d3cuZGlnaWNlcnQuY29tMSEwHwYDVQQDExhEaWdpQ2VydCBUcnVzdGVkIFJvb3Qg 13 | RzQwHhcNMTMwODAxMTIwMDAwWhcNMzgwMTE1MTIwMDAwWjBiMQswCQYDVQQGEwJV 14 | UzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQu 15 | Y29tMSEwHwYDVQQDExhEaWdpQ2VydCBUcnVzdGVkIFJvb3QgRzQwggIiMA0GCSqG 16 | SIb3DQEBAQUAA4ICDwAwggIKAoICAQC/5pBzaN675F1KPDAiMGkz7MKnJS7JIT3y 17 | ithZwuEppz1Yq3aaza57G4QNxDAf8xukOBbrVsaXbR2rsnnyyhHS5F/WBTxSD1If 18 | xp4VpX6+n6lXFllVcq9ok3DCsrp1mWpzMpTREEQQLt+C8weE5nQ7bXHiLQwb7iDV 19 | ySAdYyktzuxeTsiT+CFhmzTrBcZe7FsavOvJz82sNEBfsXpm7nfISKhmV1efVFiO 20 | DCu3T6cw2Vbuyntd463JT17lNecxy9qTXtyOj4DatpGYQJB5w3jHtrHEtWoYOAMQ 21 | jdjUN6QuBX2I9YI+EJFwq1WCQTLX2wRzKm6RAXwhTNS8rhsDdV14Ztk6MUSaM0C/ 22 | CNdaSaTC5qmgZ92kJ7yhTzm1EVgX9yRcRo9k98FpiHaYdj1ZXUJ2h4mXaXpI8OCi 23 | EhtmmnTK3kse5w5jrubU75KSOp493ADkRSWJtppEGSt+wJS00mFt6zPZxd9LBADM 24 | fRyVw4/3IbKyEbe7f/LVjHAsQWCqsWMYRJUadmJ+9oCw++hkpjPRiQfhvbfmQ6QY 25 | uKZ3AeEPlAwhHbJUKSWJbOUOUlFHdL4mrLZBdd56rF+NP8m800ERElvlEFDrMcXK 26 | chYiCd98THU/Y+whX8QgUWtvsauGi0/C1kVfnSD8oR7FwI+isX4KJpn15GkvmB0t 27 | 9dmpsh3lGwIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIB 28 | hjAdBgNVHQ4EFgQU7NfjgtJxXWRM3y5nP+e6mK4cD08wDQYJKoZIhvcNAQEMBQAD 29 | ggIBALth2X2pbL4XxJEbw6GiAI3jZGgPVs93rnD5/ZpKmbnJeFwMDF/k5hQpVgs2 30 | SV1EY+CtnJYYZhsjDT156W1r1lT40jzBQ0CuHVD1UvyQO7uYmWlrx8GnqGikJ9yd 31 | +SeuMIW59mdNOj6PWTkiU0TryF0Dyu1Qen1iIQqAyHNm0aAFYF/opbSnr6j3bTWc 32 | fFqK1qI4mfN4i/RN0iAL3gTujJtHgXINwBQy7zBZLq7gcfJW5GqXb5JQbZaNaHqa 33 | sjYUegbyJLkJEVDXCLG4iXqEI2FCKeWjzaIgQdfRnGTZ6iahixTXTBmyUEFxPT9N 34 | cCOGDErcgdLMMpSEDQgJlxxPwO5rIHQw0uA5NBCFIRUBCOhVMt5xSdkoF1BN5r5N 35 | 0XWs0Mr7QbhDparTwwVETyw2m+L64kW4I1NsBm9nVX9GtUw/bihaeSbSpKhil9Ie 36 | 4u1Ki7wb/UdKDd9nZn6yW0HQO+T0O/QEY+nvwlQAUaCKKsnOeMzV6ocEGLPOr0mI 37 | r/OSmbaz5mEP0oUA51Aa5BuVnRmhuZyxm7EAHu/QD09CbMkKvO5D+jpxpchNJqU1 38 | /YldvIViHTLSoCtU7ZpXwdv6EM8Zt4tKG48BtieVU+i2iW1bvGjUI+iLUaJW+fCm 39 | gKDWHrO8Dw9TdSmq6hN35N6MgSGtBxBHEa2HPQfRdbzP82Z+ 40 | -----END CERTIFICATE----- 41 | EOM 42 | 43 | DigiCertG2 = <<-STRING 44 | -----BEGIN CERTIFICATE----- 45 | MIIDjjCCAnagAwIBAgIQAzrx5qcRqaC7KGSxHQn65TANBgkqhkiG9w0BAQsFADBh 46 | MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 47 | d3cuZGlnaWNlcnQuY29tMSAwHgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBH 48 | MjAeFw0xMzA4MDExMjAwMDBaFw0zODAxMTUxMjAwMDBaMGExCzAJBgNVBAYTAlVT 49 | MRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5j 50 | b20xIDAeBgNVBAMTF0RpZ2lDZXJ0IEdsb2JhbCBSb290IEcyMIIBIjANBgkqhkiG 51 | 9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuzfNNNx7a8myaJCtSnX/RrohCgiN9RlUyfuI 52 | 2/Ou8jqJkTx65qsGGmvPrC3oXgkkRLpimn7Wo6h+4FR1IAWsULecYxpsMNzaHxmx 53 | 1x7e/dfgy5SDN67sH0NO3Xss0r0upS/kqbitOtSZpLYl6ZtrAGCSYP9PIUkY92eQ 54 | q2EGnI/yuum06ZIya7XzV+hdG82MHauVBJVJ8zUtluNJbd134/tJS7SsVQepj5Wz 55 | tCO7TG1F8PapspUwtP1MVYwnSlcUfIKdzXOS0xZKBgyMUNGPHgm+F6HmIcr9g+UQ 56 | vIOlCsRnKPZzFBQ9RnbDhxSJITRNrw9FDKZJobq7nMWxM4MphQIDAQABo0IwQDAP 57 | BgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBhjAdBgNVHQ4EFgQUTiJUIBiV 58 | 5uNu5g/6+rkS7QYXjzkwDQYJKoZIhvcNAQELBQADggEBAGBnKJRvDkhj6zHd6mcY 59 | 1Yl9PMWLSn/pvtsrF9+wX3N3KjITOYFnQoQj8kVnNeyIv/iPsGEMNKSuIEyExtv4 60 | NeF22d+mQrvHRAiGfzZ0JFrabA0UWTW98kndth/Jsw1HKj2ZL7tcu7XUIOGZX1NG 61 | Fdtom/DzMNU+MeKNhJ7jitralj41E6Vf8PlwUHBHQRFXGU7Aj64GxJUTFy8bJZ91 62 | 8rGOmaFvE7FBcf6IKshPECBV1/MUReXgRPTqh5Uykw7+U0b6LJ3/iyK5S9kJRaTe 63 | pLiaWN0bfVKfjllDiIGknibVb63dDcY3fe0Dkhvld1927jyNxF1WW6LZZm6zNTfl 64 | MrY= 65 | -----END CERTIFICATE----- 66 | STRING 67 | 68 | USERTrust = <<~EOM 69 | -----BEGIN CERTIFICATE----- 70 | MIIF3jCCA8agAwIBAgIQAf1tMPyjylGoG7xkDjUDLTANBgkqhkiG9w0BAQwFADCBiDELMAkGA1UE 71 | BhMCVVMxEzARBgNVBAgTCk5ldyBKZXJzZXkxFDASBgNVBAcTC0plcnNleSBDaXR5MR4wHAYDVQQK 72 | ExVUaGUgVVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNVBAMTJVVTRVJUcnVzdCBSU0EgQ2VydGlmaWNh 73 | dGlvbiBBdXRob3JpdHkwHhcNMTAwMjAxMDAwMDAwWhcNMzgwMTE4MjM1OTU5WjCBiDELMAkGA1UE 74 | BhMCVVMxEzARBgNVBAgTCk5ldyBKZXJzZXkxFDASBgNVBAcTC0plcnNleSBDaXR5MR4wHAYDVQQK 75 | ExVUaGUgVVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNVBAMTJVVTRVJUcnVzdCBSU0EgQ2VydGlmaWNh 76 | dGlvbiBBdXRob3JpdHkwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCAEmUXNg7D2wiz 77 | 0KxXDXbtzSfTTK1Qg2HiqiBNCS1kCdzOiZ/MPans9s/B3PHTsdZ7NygRK0faOca8Ohm0X6a9fZ2j 78 | Y0K2dvKpOyuR+OJv0OwWIJAJPuLodMkYtJHUYmTbf6MG8YgYapAiPLz+E/CHFHv25B+O1ORRxhFn 79 | RghRy4YUVD+8M/5+bJz/Fp0YvVGONaanZshyZ9shZrHUm3gDwFA66Mzw3LyeTP6vBZY1H1dat//O 80 | +T23LLb2VN3I5xI6Ta5MirdcmrS3ID3KfyI0rn47aGYBROcBTkZTmzNg95S+UzeQc0PzMsNT79uq 81 | /nROacdrjGCT3sTHDN/hMq7MkztReJVni+49Vv4M0GkPGw/zJSZrM233bkf6c0Plfg6lZrEpfDKE 82 | Y1WJxA3Bk1QwGROs0303p+tdOmw1XNtB1xLaqUkL39iAigmTYo61Zs8liM2EuLE/pDkP2QKe6xJM 83 | lXzzawWpXhaDzLhn4ugTncxbgtNMs+1b/97lc6wjOy0AvzVVdAlJ2ElYGn+SNuZRkg7zJn0cTRe8 84 | yexDJtC/QV9AqURE9JnnV4eeUB9XVKg+/XRjL7FQZQnmWEIuQxpMtPAlR1n6BB6T1CZGSlCBst6+ 85 | eLf8ZxXhyVeEHg9j1uliutZfVS7qXMYoCAQlObgOK6nyTJccBz8NUvXt7y+CDwIDAQABo0IwQDAd 86 | BgNVHQ4EFgQUU3m/WqorSs9UgOHYm8Cd8rIDZsswDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQF 87 | MAMBAf8wDQYJKoZIhvcNAQEMBQADggIBAFzUfA3P9wF9QZllDHPFUp/L+M+ZBn8b2kMVn54CVVeW 88 | FPFSPCeHlCjtHzoBN6J2/FNQwISbxmtOuowhT6KOVWKR82kV2LyI48SqC/3vqOlLVSoGIG1VeCkZ 89 | 7l8wXEskEVX/JJpuXior7gtNn3/3ATiUFJVDBwn7YKnuHKsSjKCaXqeYalltiz8I+8jRRa8YFWSQ 90 | Eg9zKC7F4iRO/Fjs8PRF/iKz6y+O0tlFYQXBl2+odnKPi4w2r78NBc5xjeambx9spnFixdjQg3IM 91 | 8WcRiQycE0xyNN+81XHfqnHd4blsjDwSXWXavVcStkNr/+XeTWYRUc+ZruwXtuhxkYzeSf7dNXGi 92 | FSeUHM9h4ya7b6NnJSFd5t0dCy5oGzuCr+yDZ4XUmFF0sbmZgIn/f3gZXHlKYC6SQK5MNyosycdi 93 | yA5d9zZbyuAlJQG03RoHnHcAP9Dc1ew91Pq7P8yF1m9/qS3fuQL39ZeatTXaw2ewh0qpKJ4jjv9c 94 | J2vhsE/zB+4ALtRZh8tSQZXq9EfX7mRBVXyNWQKV3WKdwrnuWih0hKWbt5DHDAff9Yk2dDLWKMGw 95 | sAvgnEzDHNb842m1R0aBL6KCq9NjRHDEjf8tM7qtj3u1cIiuPhnPQCjY/MiQu12ZIvVS5ljFH4gx 96 | Q+6IHdfGjjxDah2nGN59PRbxYvnKkKj9 97 | -----END CERTIFICATE----- 98 | EOM 99 | 100 | hosts.each do |host| 101 | next unless host.platform =~ /solaris-11(\.2)?-(i386|sparc)/ 102 | 103 | create_remote_file(host, 'DigiCert_Trusted_Root_G4.pem', DigiCert) 104 | on(host, 'chmod a+r /root/DigiCert_Trusted_Root_G4.pem') 105 | on(host, 'cp -p /root/DigiCert_Trusted_Root_G4.pem /etc/certs/CA/') 106 | on(host, 'rm /root/DigiCert_Trusted_Root_G4.pem') 107 | create_remote_file(host, 'DigiCert_Trusted_Root_G2.pem', DigiCertG2) 108 | on(host, 'chmod a+r /root/DigiCert_Trusted_Root_G2.pem') 109 | on(host, 'cp -p /root/DigiCert_Trusted_Root_G2.pem /etc/certs/CA/') 110 | on(host, 'rm /root/DigiCert_Trusted_Root_G2.pem') 111 | 112 | if host.platform =~ /solaris-11-sparc/ 113 | create_remote_file(host, 'USERTrust_RSA_Certification_Authority.pem', USERTrust) 114 | on(host, 'chmod a+r /root/USERTrust_RSA_Certification_Authority.pem') 115 | on(host, 'cp -p /root/USERTrust_RSA_Certification_Authority.pem /etc/certs/CA/') 116 | on(host, 'rm /root/USERTrust_RSA_Certification_Authority.pem') 117 | end 118 | 119 | on(host, '/usr/sbin/svcadm restart /system/ca-certificates') 120 | timeout = 60 121 | counter = 0 122 | while on(host, 'svcs -x ca-certificates').output !~ /State: online/ 123 | raise 'ca-certificates services failed start up' if counter > timeout 124 | 125 | sleep 5 126 | counter += 5 127 | end 128 | end 129 | -------------------------------------------------------------------------------- /lib/beaker-puppet/install_utils/ezbake_utils.rb: -------------------------------------------------------------------------------- 1 | require 'fileutils' 2 | 3 | module Beaker 4 | module DSL 5 | module InstallUtils 6 | # This module contains methods to assist in installing projects from source 7 | # that use ezbake for packaging. 8 | # 9 | module EZBakeUtils 10 | # @!group Public DSL Methods 11 | 12 | # Installs leiningen project with given name and version on remote host. 13 | # 14 | # @param [Host] host A single remote host on which to install the 15 | # specified leiningen project. 16 | def install_from_ezbake(host) 17 | ezbake_validate_support host 18 | ezbake_tools_available? 19 | install_ezbake_tarball_on_host host 20 | ezbake_installsh host, 'service' 21 | end 22 | 23 | # Installs termini with given name and version on remote host. 24 | # 25 | # @param [Host] host A single remote host on which to install the 26 | # specified leiningen project. 27 | def install_termini_from_ezbake(host) 28 | ezbake_validate_support host 29 | ezbake_tools_available? 30 | install_ezbake_tarball_on_host host 31 | ezbake_installsh host, 'termini' 32 | end 33 | 34 | # Install a development version of ezbake into the local m2 repository 35 | # 36 | # This can be useful if you want to work on a development branch of 37 | # ezbake that hasn't been released yet. Ensure your project dependencies 38 | # in your development branch include a reference to the -SNAPSHOT 39 | # version of the project for it to successfully pickup a pre-shipped 40 | # version of ezbake. 41 | # 42 | # @param url [String] git url 43 | # @param branch [String] git branch 44 | def ezbake_dev_build(url = 'git@github.com:puppetlabs/ezbake.git', 45 | branch = 'master') 46 | ezbake_dir = 'tmp/ezbake' 47 | conditionally_clone url, ezbake_dir, branch 48 | lp = ezbake_lein_prefix 49 | 50 | Dir.chdir(ezbake_dir) do 51 | ezbake_local_cmd "#{lp} install", 52 | throw_on_failure: true 53 | end 54 | end 55 | 56 | # @!endgroup 57 | 58 | class << self 59 | attr_accessor :config 60 | end 61 | 62 | # @!group Private helpers 63 | 64 | # Test for support in one place 65 | # 66 | # @param [Host] host host to check for support 67 | # @raise [RuntimeError] if OS is not supported 68 | # @api private 69 | def ezbake_validate_support(host) 70 | variant, version, = host['platform'].to_array 71 | return if variant =~ /^(fedora|el|redhat|centos|debian|ubuntu)$/ 72 | 73 | raise "No support for #{variant} within ezbake_utils ..." 74 | end 75 | 76 | # Build, copy & unpack tarball on remote host 77 | # 78 | # @param [Host] host installation destination 79 | # @api private 80 | def install_ezbake_tarball_on_host(host) 81 | ezbake_stage unless ezbake_config 82 | 83 | # Skip installation if the remote directory exists 84 | result = on host, "test -d #{ezbake_install_dir}", acceptable_exit_codes: [0, 1] 85 | return if result.exit_code == 0 86 | 87 | ezbake_staging_dir = File.join('target', 'staging') 88 | Dir.chdir(ezbake_staging_dir) do 89 | ezbake_local_cmd 'rake package:tar' 90 | end 91 | 92 | local_tarball = ezbake_staging_dir + '/pkg/' + ezbake_install_name + '.tar.gz' 93 | remote_tarball = ezbake_install_dir + '.tar.gz' 94 | scp_to host, local_tarball, remote_tarball 95 | 96 | # untar tarball on host 97 | on host, 'tar -xzf ' + remote_tarball 98 | 99 | # Check to ensure directory exists 100 | on host, "test -d #{ezbake_install_dir}" 101 | end 102 | 103 | LOCAL_COMMANDS_REQUIRED = [ 104 | ['leiningen', 'lein --version', nil], 105 | ['lein-pprint', 'lein with-profile ci pprint :version', 106 | 'Must have lein-pprint installed under the :ci profile.',], 107 | ['java', 'java -version', nil], 108 | ['git', 'git --version', nil], 109 | ['rake', 'rake --version', nil], 110 | ] 111 | 112 | # Checks given host for the tools necessary to perform 113 | # install_from_ezbake. 114 | # 115 | # @raise [RuntimeError] if tool is not found 116 | # @api private 117 | def ezbake_tools_available? 118 | LOCAL_COMMANDS_REQUIRED.each do |software_name, command, additional_error_message| 119 | next if system command 120 | 121 | error_message = "Must have #{software_name} installed on development system.\n" 122 | error_message += additional_error_message if additional_error_message 123 | raise error_message 124 | end 125 | end 126 | 127 | # Return the ezbake config. 128 | # 129 | # @return [Hash] configuration for ezbake, usually from ezbake.rb 130 | # @api private 131 | def ezbake_config 132 | EZBakeUtils.config 133 | end 134 | 135 | # Returns a leiningen prefix with local m2 repo capability 136 | # 137 | # @return [String] lein prefix command that uses a local build 138 | # m2 repository. 139 | # @api private 140 | def ezbake_lein_prefix 141 | # Get the absolute path to the local repo 142 | m2_repo = File.join(Dir.pwd, 'tmp', 'm2-local') 143 | 144 | 'lein update-in : assoc :local-repo "\"' + m2_repo + '\"" --' 145 | end 146 | 147 | # Prepares a staging directory for the specified project. 148 | # 149 | # @api private 150 | def ezbake_stage 151 | # Install the PuppetDB jar into the local repository 152 | ezbake_local_cmd "#{ezbake_lein_prefix} install", 153 | throw_on_failure: true 154 | 155 | # Run ezbake stage 156 | ezbake_local_cmd "#{ezbake_lein_prefix} with-profile ezbake ezbake stage", 157 | throw_on_failure: true 158 | 159 | # Boostrap packaging, and grab configuration info from project 160 | staging_dir = File.join('target', 'staging') 161 | Dir.chdir(staging_dir) do 162 | ezbake_local_cmd 'rake package:bootstrap' 163 | 164 | load 'ezbake.rb' 165 | ezbake = EZBake::Config 166 | ezbake[:package_version] = `printf $(rake pl:print_build_param[ref] | tail -n 1)` 167 | EZBakeUtils.config = ezbake 168 | end 169 | end 170 | 171 | # Executes a local command using system, logging the prepared command 172 | # 173 | # @param [String] cmd command to execute 174 | # @param [Hash] opts options 175 | # @option opts [bool] :throw_on_failure If true, throws an 176 | # exception if the exit code is non-zero. Defaults to false. 177 | # @return [bool] true if exit == 0 false if otherwise 178 | # @raise [RuntimeError] if :throw_on_failure is true and 179 | # command fails 180 | # @api private 181 | def ezbake_local_cmd(cmd, opts = {}) 182 | opts = { 183 | throw_on_failure: false, 184 | }.merge(opts) 185 | 186 | logger.notify "localhost $ #{cmd}" 187 | result = system cmd 188 | raise "Command failure #{cmd}" if opts[:throw_on_failure] && result == false 189 | 190 | result 191 | end 192 | 193 | # Retrieve the tarball installation name. This is the name of 194 | # the tarball without the .tar.gz extension, and the name of the 195 | # path where it will unpack to. 196 | # 197 | # @return [String] name of the tarball and directory 198 | # @api private 199 | def ezbake_install_name 200 | ezbake = ezbake_config 201 | project_package_version = ezbake[:package_version] 202 | project_name = ezbake[:project] 203 | format('%s-%s', project_name, project_package_version) 204 | end 205 | 206 | # Returns the full path to the installed software on the remote host. 207 | # 208 | # This only returns the path, it doesn't work out if its installed or 209 | # not. 210 | # 211 | # @return [String] path to the installation dir 212 | # @api private 213 | def ezbake_install_dir 214 | "/root/#{ezbake_install_name}" 215 | end 216 | 217 | # A helper that wraps the execution of install.sh in the proper 218 | # ezbake installation directory. 219 | # 220 | # @param [Host] host Host to run install.sh on 221 | # @param [String] task Task to execute with install.sh 222 | # @api private 223 | def ezbake_installsh(host, task = '') 224 | on host, "cd #{ezbake_install_dir}; bash install.sh #{task}" 225 | end 226 | 227 | # Only clone from given git URI if there is no existing git clone at the 228 | # given local_path location. 229 | # 230 | # @param [String] upstream_uri git URI 231 | # @param [String] local_path path to conditionally install to 232 | # @param [String] branch to checkout 233 | # @api private 234 | def conditionally_clone(upstream_uri, local_path, branch = 'origin/HEAD') 235 | if ezbake_local_cmd "git --work-tree=#{local_path} --git-dir=#{local_path}/.git status" 236 | ezbake_local_cmd "git --work-tree=#{local_path} --git-dir=#{local_path}/.git fetch origin" 237 | ezbake_local_cmd "git --work-tree=#{local_path} --git-dir=#{local_path}/.git checkout #{branch}" 238 | else 239 | parent_dir = File.dirname(local_path) 240 | FileUtils.mkdir_p(parent_dir) 241 | ezbake_local_cmd "git clone #{upstream_uri} #{local_path}" 242 | ezbake_local_cmd "git --work-tree=#{local_path} --git-dir=#{local_path}/.git checkout #{branch}" 243 | end 244 | end 245 | end 246 | end 247 | end 248 | end 249 | -------------------------------------------------------------------------------- /spec/beaker-puppet/install_utils/ezbake_utils_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | EZBAKE_CONFIG_EXAMPLE = { 4 | project: 'puppetserver', 5 | real_name: 'puppetserver', 6 | user: 'puppet', 7 | group: 'puppet', 8 | uberjar_name: 'puppetserver-release.jar', 9 | config_files: [], 10 | terminus_info: {}, 11 | debian: { additional_dependencies: ['puppet (= 3.6.1-puppetlabs1)'] }, 12 | redhat: { additional_dependencies: ['puppet = 3.6.1'] }, 13 | java_args: '-Xmx192m', 14 | } 15 | 16 | class ClassMixedWithEZBakeUtils 17 | include Beaker::DSL::EZBakeUtils 18 | 19 | def initialize_ezbake_config 20 | Beaker::DSL::EZBakeUtils.config = EZBAKE_CONFIG_EXAMPLE 21 | end 22 | 23 | def wipe_out_ezbake_config 24 | Beaker::DSL::EZBakeUtils.config = nil 25 | end 26 | 27 | def logger 28 | @logger ||= RSpec::Mocks::Double.new('logger').as_null_object 29 | end 30 | end 31 | 32 | module Beaker::DSL::EZBakeUtils::EZBake 33 | Config = EZBAKE_CONFIG_EXAMPLE 34 | end 35 | 36 | describe ClassMixedWithEZBakeUtils do 37 | let(:opts) { Beaker::Options::Presets.env_vars } 38 | let(:host) { double.as_null_object } 39 | let(:local_commands) { Beaker::DSL::EZBakeUtils::LOCAL_COMMANDS_REQUIRED } 40 | 41 | describe '#install_from_ezbake' do 42 | let(:platform) { Beaker::Platform.new('el-7-i386') } 43 | let(:host) do 44 | FakeHost.create('fakevm', platform.to_s) 45 | end 46 | 47 | before do 48 | allow(subject).to receive(:ezbake_tools_available?) { true } 49 | end 50 | 51 | it 'when ran with an el-7 machine runs correct installsh command' do 52 | expect(subject).to receive(:install_ezbake_tarball_on_host) 53 | .ordered 54 | expect(subject) 55 | .to receive(:ezbake_installsh).with(host, 'service') 56 | subject.install_from_ezbake host 57 | end 58 | end 59 | 60 | describe '#install_termini_from_ezbake' do 61 | let(:platform) { Beaker::Platform.new('el-7-i386') } 62 | let(:host) do 63 | FakeHost.create('fakevm', platform.to_s) 64 | end 65 | 66 | before do 67 | allow(subject).to receive(:ezbake_tools_available?) { true } 68 | end 69 | 70 | it 'when ran with an el-7 machine runs correct installsh command' do 71 | expect(subject).to receive(:ezbake_validate_support).with(host).ordered 72 | expect(subject).to receive(:install_ezbake_tarball_on_host) 73 | .with(host).ordered 74 | expect(subject) 75 | .to receive(:ezbake_installsh).with(host, 'termini') 76 | subject.install_termini_from_ezbake host 77 | end 78 | end 79 | 80 | describe '#ezbake_validate_support' do 81 | context 'when OS supported' do 82 | let(:platform) { Beaker::Platform.new('el-7-i386') } 83 | let(:host) do 84 | FakeHost.create('fakevm', platform.to_s) 85 | end 86 | 87 | it 'should do nothing' do 88 | subject.ezbake_validate_support host 89 | end 90 | end 91 | 92 | context 'when OS not supported' do 93 | let(:platform) { Beaker::Platform.new('aix-12-ppc') } 94 | let(:host) do 95 | FakeHost.create('fakevm', platform.to_s) 96 | end 97 | 98 | it 'should throw exception' do 99 | expect do 100 | subject.ezbake_validate_support host 101 | end.to raise_error(RuntimeError, 102 | 'No support for aix within ezbake_utils ...') 103 | end 104 | end 105 | end 106 | 107 | def install_ezbake_tarball_on_host_common_expects 108 | result = object_double(Beaker::Result.new(host, 'foo'), exit_code: 1) 109 | expect(subject).to receive(:on) 110 | .with(kind_of(Beaker::Host), /test -d/, 111 | anything).ordered { result } 112 | expect(Dir).to receive(:chdir).and_yield 113 | expect(subject).to receive(:ezbake_local_cmd).with(/rake package:tar/).ordered 114 | expect(subject).to receive(:scp_to) 115 | .with(kind_of(Beaker::Host), anything, anything).ordered 116 | expect(subject).to receive(:on) 117 | .with(kind_of(Beaker::Host), /tar -xzf/).ordered 118 | expect(subject).to receive(:on) 119 | .with(kind_of(Beaker::Host), /test -d/).ordered 120 | end 121 | 122 | describe '#install_ezbake_tarball_on_host' do 123 | let(:platform) { Beaker::Platform.new('el-7-i386') } 124 | let(:host) do 125 | FakeHost.create('fakevm', platform.to_s) 126 | end 127 | 128 | it 'when invoked with configuration should run expected tasks' do 129 | subject.initialize_ezbake_config 130 | install_ezbake_tarball_on_host_common_expects 131 | subject.install_ezbake_tarball_on_host host 132 | end 133 | 134 | it 'when invoked with nil configuration runs ezbake_stage' do 135 | subject.wipe_out_ezbake_config 136 | expect(subject).to receive(:ezbake_stage) { 137 | Beaker::DSL::EZBakeUtils.config = EZBAKE_CONFIG_EXAMPLE 138 | }.ordered 139 | install_ezbake_tarball_on_host_common_expects 140 | subject.install_ezbake_tarball_on_host host 141 | end 142 | end 143 | 144 | describe '#ezbake_tools_available?' do 145 | before do 146 | allow(subject).to receive(:check_for_package) { true } 147 | allow(subject).to receive(:system) { true } 148 | end 149 | 150 | describe 'checks for local successful commands' do 151 | it 'and succeeds if all commands return successfully' do 152 | local_commands.each do |software_name, command, additional_error_messages| 153 | expect(subject).to receive(:system).with(/#{command}/) 154 | end 155 | subject.ezbake_tools_available? 156 | end 157 | 158 | it 'and raises an exception if a command returns failure' do 159 | allow(subject).to receive(:system) { false } 160 | local_commands.each do |software_name, command, additional_error_messages| 161 | expect(subject).to receive(:system).with(/#{command}/) 162 | break # just need first element 163 | end 164 | expect do 165 | subject.ezbake_tools_available? 166 | end.to raise_error(RuntimeError, /Must have .* installed on development system./) 167 | end 168 | end 169 | end 170 | 171 | describe '#ezbake_config' do 172 | it 'returns a map with ezbake configuration parameters' do 173 | subject.initialize_ezbake_config 174 | config = subject.ezbake_config 175 | expect(config).to include(EZBAKE_CONFIG_EXAMPLE) 176 | end 177 | end 178 | 179 | describe '#ezbake_stage' do 180 | before do 181 | allow(subject).to receive(:ezbake_tools_available?) { true } 182 | subject.wipe_out_ezbake_config 183 | end 184 | 185 | it 'initializes EZBakeUtils.config' do 186 | allow(Dir).to receive(:chdir).and_yield 187 | 188 | expect(subject).to receive(:ezbake_local_cmd) 189 | .with(/^lein.*install/, throw_on_failure: true).ordered 190 | expect(subject).to receive(:ezbake_local_cmd) 191 | .with(/^lein.*with-profile ezbake ezbake stage/, throw_on_failure: true).ordered 192 | expect(subject).to receive(:ezbake_local_cmd).with('rake package:bootstrap').ordered 193 | expect(subject).to receive(:load) {}.ordered 194 | expect(subject).to receive(:`).ordered 195 | 196 | config = subject.ezbake_config 197 | expect(config).to eq(nil) 198 | 199 | subject.ezbake_stage 200 | 201 | config = subject.ezbake_config 202 | expect(config).to include(EZBAKE_CONFIG_EXAMPLE) 203 | end 204 | end 205 | 206 | describe '#ezbake_local_cmd' do 207 | it 'should execute system on the command specified' do 208 | expect(subject).to receive(:system).with('my command') { true } 209 | subject.ezbake_local_cmd('my command') 210 | end 211 | 212 | it 'with :throw_on_failure should throw exeception when failed' do 213 | expect(subject).to receive(:system).with('my failure') { false } 214 | expect do 215 | subject.ezbake_local_cmd('my failure', throw_on_failure: true) 216 | end.to raise_error(RuntimeError, 'Command failure my failure') 217 | end 218 | 219 | it 'without :throw_on_failure should just fail and return false' do 220 | expect(subject).to receive(:system).with('my failure') { false } 221 | expect(subject.ezbake_local_cmd('my failure')).to eq(false) 222 | end 223 | end 224 | 225 | describe '#ezbake_install_name' do 226 | it 'should return the installation name from example configuration' do 227 | expect(subject).to receive(:ezbake_config) { 228 | { 229 | package_version: '1.1.1', 230 | project: 'myproject', 231 | } 232 | } 233 | expect(subject.ezbake_install_name).to eq 'myproject-1.1.1' 234 | end 235 | end 236 | 237 | describe '#ezbake_install_dir' do 238 | it 'should return the full path from ezbake_install_name' do 239 | expect(subject).to receive(:ezbake_install_name) { 240 | 'mynewproject-2.3.4' 241 | } 242 | expect(subject.ezbake_install_dir).to eq '/root/mynewproject-2.3.4' 243 | end 244 | end 245 | 246 | describe '#ezbake_installsh' do 247 | it 'run on command correctly when invoked' do 248 | expect(subject).to receive(:on).with(host, 249 | /install.sh my_task/) 250 | subject.ezbake_installsh host, 'my_task' 251 | end 252 | end 253 | 254 | describe '#conditionally_clone' do 255 | it 'when repo exists, just do fetch and checkout' do 256 | expect(subject).to receive(:ezbake_local_cmd) 257 | .with(/git status/) { true } 258 | expect(subject).to receive(:ezbake_local_cmd) 259 | .with(/git fetch origin/) 260 | expect(subject).to receive(:ezbake_local_cmd) 261 | .with(/git checkout/) 262 | subject.conditionally_clone('my_url', 'my_local_path') 263 | end 264 | 265 | it 'when repo does not exist, do clone and checkout' do 266 | expect(subject).to receive(:ezbake_local_cmd) 267 | .with(/git status/) { false } 268 | expect(subject).to receive(:ezbake_local_cmd) 269 | .with(/git clone/) 270 | expect(subject).to receive(:ezbake_local_cmd) 271 | .with(/git checkout/) 272 | subject.conditionally_clone('my_url', 'my_local_path') 273 | end 274 | end 275 | end 276 | -------------------------------------------------------------------------------- /spec/beaker-puppet/install_utils/puppet_utils_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | class ClassMixedWithDSLInstallUtils 4 | include Beaker::DSL::Wrappers 5 | include Beaker::DSL::Helpers 6 | include Beaker::DSL::Structure 7 | include Beaker::DSL::Roles 8 | include Beaker::DSL::Patterns 9 | include Beaker::DSL::InstallUtils 10 | 11 | def logger 12 | @logger ||= RSpec::Mocks::Double.new('logger').as_null_object 13 | end 14 | end 15 | 16 | describe ClassMixedWithDSLInstallUtils do 17 | let(:metadata) { @metadata ||= {} } 18 | let(:presets) { Beaker::Options::Presets.new } 19 | let(:opts) { presets.presets.merge(presets.env_vars) } 20 | let(:basic_hosts) do 21 | make_hosts({ pe_ver: '3.0', 22 | platform: 'linux', 23 | roles: ['agent'], }, 4) 24 | end 25 | let(:hosts) do 26 | basic_hosts[0][:roles] = %w[master database dashboard] 27 | basic_hosts[1][:platform] = 'windows' 28 | basic_hosts[2][:platform] = 'osx-10.9-x86_64' 29 | basic_hosts[3][:platform] = 'eos' 30 | basic_hosts 31 | end 32 | let(:hosts_sorted) { [hosts[1], hosts[0], hosts[2], hosts[3]] } 33 | let(:winhost) do 34 | make_host('winhost', { platform: 'windows', 35 | pe_ver: '3.0', 36 | working_dir: '/tmp', 37 | is_cygwin: true, }) 38 | end 39 | let(:winhost_non_cygwin) do 40 | make_host('winhost_non_cygwin', { platform: 'windows', 41 | pe_ver: '3.0', 42 | working_dir: '/tmp', 43 | is_cygwin: 'false', }) 44 | end 45 | let(:machost) do 46 | make_host('machost', { platform: 'osx-10.9-x86_64', 47 | pe_ver: '3.0', 48 | working_dir: '/tmp', }) 49 | end 50 | let(:unixhost) do 51 | make_host('unixhost', { platform: 'linux', 52 | pe_ver: '3.0', 53 | working_dir: '/tmp', 54 | dist: 'puppet-enterprise-3.1.0-rc0-230-g36c9e5c-debian-7-i386', }) 55 | end 56 | let(:eoshost) do 57 | make_host('eoshost', { platform: 'eos', 58 | pe_ver: '3.0', 59 | working_dir: '/tmp', 60 | dist: 'puppet-enterprise-3.7.1-rc0-78-gffc958f-eos-4-i386', }) 61 | end 62 | 63 | describe '#configure_defaults_on' do 64 | it 'can set foss defaults' do 65 | expect(subject).to receive(:add_foss_defaults_on).exactly(hosts.length).times 66 | subject.configure_defaults_on(hosts, 'foss') 67 | end 68 | 69 | it 'can set aio defaults' do 70 | expect(subject).to receive(:add_aio_defaults_on).exactly(hosts.length).times 71 | subject.configure_defaults_on(hosts, 'aio') 72 | end 73 | 74 | it 'can remove old defaults ands replace with new' do 75 | # fake the results of calling configure_pe_defaults_on 76 | hosts.each do |host| 77 | host['type'] = 'pe' 78 | end 79 | expect(subject).to receive(:remove_pe_defaults_on).exactly(hosts.length).times 80 | expect(subject).to receive(:add_foss_defaults_on).exactly(hosts.length).times 81 | subject.configure_defaults_on(hosts, 'foss') 82 | end 83 | end 84 | 85 | describe '#configure_type_defaults_on' do 86 | it 'can set foss defaults for foss type' do 87 | hosts.each do |host| 88 | host['type'] = 'foss' 89 | end 90 | expect(subject).to receive(:add_foss_defaults_on).exactly(hosts.length).times 91 | subject.configure_type_defaults_on(hosts) 92 | end 93 | 94 | it 'adds aio defaults to foss hosts when they have an aio foss puppet version' do 95 | hosts.each do |host| 96 | host[:pe_ver] = nil 97 | host[:version] = nil 98 | host['type'] = 'foss' 99 | host['version'] = '4.0' 100 | end 101 | expect(subject).to receive(:add_foss_defaults_on).exactly(hosts.length).times 102 | expect(subject).to receive(:add_aio_defaults_on).exactly(hosts.length).times 103 | subject.configure_type_defaults_on(hosts) 104 | end 105 | 106 | it 'adds aio defaults to foss hosts when they have type foss-aio' do 107 | hosts.each do |host| 108 | host[:pe_ver] = nil 109 | host[:version] = nil 110 | host['type'] = 'foss-aio' 111 | end 112 | expect(subject).to receive(:add_foss_defaults_on).exactly(hosts.length).times 113 | expect(subject).to receive(:add_aio_defaults_on).exactly(hosts.length).times 114 | subject.configure_type_defaults_on(hosts) 115 | end 116 | 117 | it 'can set aio defaults for aio type (backwards compatability)' do 118 | hosts.each do |host| 119 | host[:pe_ver] = nil 120 | host[:version] = nil 121 | host['type'] = 'aio' 122 | end 123 | expect(subject).to receive(:add_aio_defaults_on).exactly(hosts.length).times 124 | subject.configure_type_defaults_on(hosts) 125 | end 126 | end 127 | 128 | describe '#puppetserver_version_on' do 129 | it 'returns the tag on a released version' do 130 | result = object_double(Beaker::Result.new({}, 'puppetserver --version'), stdout: 'puppetserver version: 6.13.0', 131 | exit_code: 0) 132 | expect(subject).to receive(:on).with(hosts.first, 'puppetserver --version', 133 | accept_all_exit_codes: true).and_return(result) 134 | expect(subject.puppetserver_version_on(hosts.first)).to eq('6.13.0') 135 | end 136 | 137 | it 'returns the tag on a nightly version' do 138 | result = object_double(Beaker::Result.new({}, 'puppetserver --version'), 139 | stdout: 'puppetserver version: 7.0.0.SNAPSHOT.2020.10.14T0512', exit_code: 0) 140 | expect(subject).to receive(:on).with(hosts.first, 'puppetserver --version', 141 | accept_all_exit_codes: true).and_return(result) 142 | expect(subject.puppetserver_version_on(hosts.first)).to eq('7.0.0') 143 | end 144 | end 145 | 146 | describe '#puppet_collection_for' do 147 | it 'raises an error when given an invalid package' do 148 | expect do 149 | subject.puppet_collection_for(:foo, '5.5.4') 150 | end.to raise_error(RuntimeError, /package must be one of puppet_agent, puppet, puppetserver/) 151 | end 152 | 153 | context 'when the :puppet_agent package is passed in' do 154 | context 'given a valid version' do 155 | { 156 | '1.10.14' => 'pc1', 157 | '1.10.x' => 'pc1', 158 | '5.3.1' => 'puppet5', 159 | '5.3.x' => 'puppet5', 160 | '5.99.0' => 'puppet6', 161 | '6.1.99-foo' => 'puppet6', 162 | '6.99.99' => 'puppet7', 163 | '7.0.0' => 'puppet7', 164 | }.each do |version, collection| 165 | it "returns collection '#{collection}' for version '#{version}'" do 166 | expect(subject.puppet_collection_for(:puppet_agent, version)).to eq(collection) 167 | end 168 | end 169 | end 170 | 171 | it "returns the default, latest puppet collection given the version 'latest'" do 172 | expect(subject.puppet_collection_for(:puppet_agent, 'latest')).to eq('puppet') 173 | end 174 | 175 | context 'given an invalid version' do 176 | [nil, '', '0.1.0', '3.8.1', '', 'not-semver', 'not.semver.either'].each do |version| 177 | it "returns a nil collection value for version '#{version}'" do 178 | expect(subject.puppet_collection_for(:puppet_agent, version)).to be_nil 179 | end 180 | end 181 | end 182 | end 183 | 184 | context 'when the :puppet package is passed-in' do 185 | context 'given a valid version' do 186 | { 187 | '4.9.0' => 'pc1', 188 | '4.10.x' => 'pc1', 189 | '5.3.1' => 'puppet5', 190 | '5.3.x' => 'puppet5', 191 | '5.99.0' => 'puppet6', 192 | '6.1.99-foo' => 'puppet6', 193 | '6.99.99' => 'puppet7', 194 | '7.0.0' => 'puppet7', 195 | }.each do |version, collection| 196 | it "returns collection '#{collection}' for version '#{version}'" do 197 | expect(subject.puppet_collection_for(:puppet, version)).to eq(collection) 198 | end 199 | end 200 | end 201 | 202 | it "returns the default, latest puppet collection given the version 'latest'" do 203 | expect(subject.puppet_collection_for(:puppet, 'latest')).to eq('puppet') 204 | end 205 | 206 | context 'given an invalid version' do 207 | [nil, '', '0.1.0', '3.8.1', '', 'not-semver', 'not.semver.either'].each do |version| 208 | it "returns a nil collection value for version '#{version}'" do 209 | expect(subject.puppet_collection_for(:puppet, version)).to be_nil 210 | end 211 | end 212 | end 213 | end 214 | 215 | context 'when the :puppetserver package is passed in' do 216 | context 'given a valid version' do 217 | { 218 | '2.0.0' => 'pc1', 219 | '2.0.x' => 'pc1', 220 | '5.3.1' => 'puppet5', 221 | '5.3.x' => 'puppet5', 222 | '5.99.0' => 'puppet6', 223 | '6.1.99-foo' => 'puppet6', 224 | '6.99.99' => 'puppet7', 225 | '7.0.0' => 'puppet7', 226 | }.each do |version, collection| 227 | it "returns collection '#{collection}' for version '#{version}'" do 228 | expect(subject.puppet_collection_for(:puppetserver, version)).to eq(collection) 229 | end 230 | end 231 | end 232 | 233 | it "returns the default, latest puppet collection given the version 'latest'" do 234 | expect(subject.puppet_collection_for(:puppetserver, 'latest')).to eq('puppet') 235 | end 236 | 237 | context 'given an invalid version' do 238 | [nil, '', '0.1.0', '3.8.1', '', 'not-semver', 'not.semver.either'].each do |version| 239 | it "returns a nil collection value for version '#{version}'" do 240 | expect(subject.puppet_collection_for(:puppetserver, version)).to be_nil 241 | end 242 | end 243 | end 244 | end 245 | end 246 | end 247 | -------------------------------------------------------------------------------- /lib/beaker-puppet/install_utils/puppet_utils.rb: -------------------------------------------------------------------------------- 1 | module Beaker 2 | module DSL 3 | module InstallUtils 4 | # 5 | # This module contains methods useful for both foss and pe installs 6 | # 7 | module PuppetUtils 8 | # Given a type return an understood host type 9 | # @param [String] type The host type to be normalized 10 | # @return [String] The normalized type 11 | # 12 | # @example 13 | # normalize_type('pe-aio') 14 | # 'pe' 15 | # @example 16 | # normalize_type('git') 17 | # 'foss' 18 | # @example 19 | # normalize_type('foss-internal') 20 | # 'foss' 21 | def normalize_type(type) 22 | case type 23 | when /(\A|-)foss(\Z|-)/ 24 | 'foss' 25 | when /(\A|-)pe(\Z|-)/ 26 | 'pe' 27 | when /(\A|-)aio(\Z|-)/ 28 | 'aio' 29 | end 30 | end 31 | 32 | # Given a host construct a PATH that includes puppetbindir, facterbindir and hierabindir 33 | # @param [Host] host A single host to construct pathing for 34 | def construct_puppet_path(host) 35 | path = %w[puppetbindir facterbindir hierabindir privatebindir].compact.reject(&:empty?) 36 | # get the PATH defaults 37 | path.map! { |val| host[val] } 38 | path = path.compact.reject(&:empty?) 39 | # run the paths through echo to see if they have any subcommands that need processing 40 | path.map! { |val| echo_on(host, val) } 41 | 42 | separator = host['pathseparator'] 43 | separator = ':' unless host.is_powershell? 44 | path.join(separator) 45 | end 46 | 47 | # Append puppetbindir, facterbindir and hierabindir to the PATH for each host 48 | # @param [Host, Array, String, Symbol] hosts One or more hosts to act upon, 49 | # or a role (String or Symbol) that identifies one or more hosts. 50 | def add_puppet_paths_on(hosts) 51 | block_on hosts do |host| 52 | puppet_path = construct_puppet_path(host) 53 | host.add_env_var('PATH', puppet_path) 54 | end 55 | end 56 | 57 | # Remove puppetbindir, facterbindir and hierabindir to the PATH for each host 58 | # 59 | # @param [Host, Array, String, Symbol] hosts One or more hosts to act upon, 60 | # or a role (String or Symbol) that identifies one or more hosts. 61 | def remove_puppet_paths_on(hosts) 62 | block_on hosts do |host| 63 | puppet_path = construct_puppet_path(host) 64 | host.delete_env_var('PATH', puppet_path) 65 | end 66 | end 67 | 68 | # Determine the puppet collection that matches a given package version. The package 69 | # must be one of 70 | # * :puppet_agent (you can get this version from the `aio_agent_version_fact`) 71 | # * :puppet 72 | # * :puppetserver 73 | # 74 | # 75 | # @param package [Symbol] the package name. must be one of :puppet_agent, :puppet, :puppetserver 76 | # @param version [String] a version number or the string 'latest' 77 | # @returns [String|nil] the name of the corresponding puppet collection, if any 78 | def puppet_collection_for(package, version) 79 | valid_packages = %i[ 80 | puppet_agent 81 | puppet 82 | puppetserver 83 | ] 84 | 85 | raise "package must be one of #{valid_packages.join(', ')}" unless valid_packages.include?(package) 86 | 87 | case package 88 | when :puppet_agent, :puppet, :puppetserver 89 | version = version.to_s 90 | return 'puppet' if version.strip == 'latest' 91 | 92 | x, y, z = version.to_s.split('.').map(&:to_i) 93 | return nil if x.nil? || y.nil? || z.nil? 94 | 95 | pc1_x = { 96 | puppet: 4, 97 | puppet_agent: 1, 98 | puppetserver: 2, 99 | } 100 | 101 | return 'pc1' if x == pc1_x[package] 102 | 103 | # A y version >= 99 indicates a pre-release version of the next x release 104 | x += 1 if y >= 99 105 | "puppet#{x}" if x > 4 106 | end 107 | end 108 | 109 | # Report the version of puppet-agent installed on `host` 110 | # 111 | # @param [Host] host The host to act upon 112 | # @returns [String|nil] The version of puppet-agent, or nil if puppet-agent is not installed 113 | def puppet_agent_version_on(host) 114 | result = on(host, facter('aio_agent_version'), accept_all_exit_codes: true) 115 | return result.stdout.strip if result.exit_code.zero? 116 | 117 | nil 118 | end 119 | 120 | # Report the version of puppetserver installed on `host` 121 | # 122 | # @param [Host] host The host to act upon 123 | # @returns [String|nil] The version of puppetserver, or nil if puppetserver is not installed 124 | def puppetserver_version_on(host) 125 | result = on(host, 'puppetserver --version', accept_all_exit_codes: true) 126 | if result.exit_code.zero? 127 | matched = result.stdout.strip.scan(/\d+\.\d+\.\d+/) 128 | return matched.first 129 | end 130 | nil 131 | end 132 | 133 | # Configure the provided hosts to be of the provided type (one of foss, aio, pe), if the host 134 | # is already associated with a type then remove the previous settings for that type 135 | # @param [Host, Array, String, Symbol] hosts One or more hosts to act upon, 136 | # or a role (String or Symbol) that identifies one or more hosts. 137 | # @param [String] type One of 'aio', 'pe' or 'foss' 138 | def configure_defaults_on(hosts, type) 139 | block_on hosts do |host| 140 | # check to see if the host already has a type associated with it 141 | remove_defaults_on(host) 142 | 143 | add_method = "add_#{type}_defaults_on" 144 | raise "cannot add defaults of type #{type} for host #{host.name} (#{add_method} not present)" unless respond_to?( 145 | add_method, host 146 | ) 147 | 148 | send(add_method, host) 149 | 150 | # add pathing env 151 | add_puppet_paths_on(host) 152 | end 153 | end 154 | 155 | # Configure the provided hosts to be of their host[:type], it host[type] == nil do nothing 156 | def configure_type_defaults_on(hosts) 157 | block_on hosts do |host| 158 | has_defaults = false 159 | if host[:type] 160 | host_type = host[:type] 161 | # clean up the naming conventions here (some teams use foss-package, git-whatever, we need 162 | # to correctly handle that 163 | # don't worry about aio, that happens in the aio_version? check 164 | host_type = normalize_type(host_type) 165 | if host_type and host_type !~ /aio/ 166 | add_method = "add_#{host_type}_defaults_on" 167 | raise "cannot add defaults of type #{host_type} for host #{host.name} (#{add_method} not present)" unless respond_to?( 168 | add_method, host 169 | ) 170 | 171 | send(add_method, host) 172 | 173 | has_defaults = true 174 | end 175 | end 176 | if aio_version?(host) 177 | add_aio_defaults_on(host) 178 | has_defaults = true 179 | end 180 | # add pathing env 181 | add_puppet_paths_on(host) if has_defaults 182 | end 183 | end 184 | alias configure_foss_defaults_on configure_type_defaults_on 185 | alias configure_pe_defaults_on configure_type_defaults_on 186 | 187 | # If the host is associated with a type remove all defaults and environment associated with that type. 188 | # @param [Host, Array, String, Symbol] hosts One or more hosts to act upon, 189 | # or a role (String or Symbol) that identifies one or more hosts. 190 | def remove_defaults_on(hosts) 191 | block_on hosts do |host| 192 | if host['type'] 193 | # clean up the naming conventions here (some teams use foss-package, git-whatever, we need 194 | # to correctly handle that 195 | # don't worry about aio, that happens in the aio_version? check 196 | host_type = normalize_type(host['type']) 197 | remove_puppet_paths_on(hosts) 198 | remove_method = "remove_#{host_type}_defaults_on" 199 | raise "cannot remove defaults of type #{host_type} associated with host #{host.name} (#{remove_method} not present)" unless respond_to?( 200 | remove_method, host 201 | ) 202 | 203 | send(remove_method, host) 204 | 205 | remove_aio_defaults_on(host) if aio_version?(host) 206 | end 207 | end 208 | end 209 | 210 | # Uses puppet to stop the firewall on the given hosts. Puppet must be installed before calling this method. 211 | # @param [Host, Array, String, Symbol] hosts One or more hosts to act upon, or a role (String or Symbol) that identifies one or more hosts. 212 | def stop_firewall_with_puppet_on(hosts) 213 | block_on hosts do |host| 214 | case host['platform'] 215 | when /debian/ 216 | result = on(host, 'which iptables', accept_all_exit_codes: true) 217 | if result.exit_code == 0 218 | on host, 'iptables -F' 219 | else 220 | logger.notify("Unable to locate `iptables` on #{host['platform']}; not attempting to clear firewall") 221 | end 222 | when /fedora|el-7/ 223 | on host, puppet('resource', 'service', 'firewalld', 'ensure=stopped') 224 | when /el-|centos/ 225 | on host, puppet('resource', 'service', 'iptables', 'ensure=stopped') 226 | when /ubuntu/ 227 | on host, puppet('resource', 'service', 'ufw', 'ensure=stopped') 228 | else 229 | logger.notify("Not sure how to clear firewall on #{host['platform']}") 230 | end 231 | end 232 | end 233 | end 234 | end 235 | end 236 | end 237 | -------------------------------------------------------------------------------- /lib/beaker-puppet/install_utils/foss_defaults.rb: -------------------------------------------------------------------------------- 1 | module Beaker 2 | module DSL 3 | module InstallUtils 4 | # 5 | # This module contains default values for FOSS puppet paths and directorys per-platform 6 | # 7 | module FOSSDefaults 8 | # Here be the default download URLs 9 | FOSS_DEFAULT_DOWNLOAD_URLS = { 10 | win_download_url: 'https://downloads.puppet.com/windows', 11 | mac_download_url: 'https://downloads.puppet.com/mac', 12 | pe_promoted_builds_url: 'https://pm.puppet.com', 13 | release_apt_repo_url: 'https://apt.puppet.com', 14 | release_yum_repo_url: 'https://yum.puppet.com', 15 | nightly_builds_url: 'https://nightlies.puppet.com', 16 | nightly_apt_repo_url: 'https://nightlies.puppet.com/apt', 17 | nightly_yum_repo_url: 'https://nightlies.puppet.com/yum', 18 | nightly_win_download_url: 'https://nightlies.puppet.com/downloads/windows', 19 | nightly_mac_download_url: 'https://nightlies.puppet.com/downloads/mac', 20 | dev_builds_url: 'https://builds.delivery.puppetlabs.net', 21 | } 22 | 23 | # Here be the pathing and default values for FOSS installs 24 | # 25 | FOSS_DEFAULTS = { 26 | 'freebsd' => { 27 | 'puppetserver-confdir' => '/etc/puppetserver/conf.d', 28 | 'puppetservice' => 'puppetmaster', 29 | 'puppetpath' => '/usr/local/etc/puppet/modules', 30 | 'puppetvardir' => '/var/lib/puppet', 31 | 'puppetbin' => '/usr/bin/puppet', 32 | 'puppetbindir' => '/usr/bin', 33 | 'hieralibdir' => '/opt/puppet-git-repos/hiera/lib', 34 | 'hierapuppetlibdir' => '/opt/puppet-git-repos/hiera-puppet/lib', 35 | 'hierabindir' => '/opt/puppet-git-repos/hiera/bin', 36 | 'hieradatadir' => '/usr/local/etc/puppet/modules/hieradata', 37 | 'hieraconf' => '/usr/local/etc/puppet/modules/hiera.yaml', 38 | 'distmoduledir' => '/usr/local/etc/puppet/modules', 39 | 'sitemoduledir' => '/usr/share/puppet/modules', 40 | }, 41 | 'openbsd' => { 42 | 'puppetserver-confdir' => '/etc/puppetserver/conf.d', 43 | 'puppetservice' => 'puppetmaster', 44 | 'puppetpath' => '/etc/puppet/modules', 45 | 'puppetvardir' => '/var/puppet', 46 | 'puppetbin' => '/usr/local/bin/puppet', 47 | 'puppetbindir' => '/usr/local/bin', 48 | 'hieralibdir' => '/opt/puppet-git-repos/hiera/lib', 49 | 'hierapuppetlibdir' => '/opt/puppet-git-repos/hiera-puppet/lib', 50 | 'hierabindir' => '/opt/puppet-git-repos/hiera/bin', 51 | 'hieradatadir' => '/etc/puppet/hieradata', 52 | 'hieraconf' => '/etc/puppet/hiera.yaml', 53 | 'distmoduledir' => '/etc/puppet/modules', 54 | 'sitemoduledir' => '/usr/local/share/puppet/modules', 55 | }, 56 | 'mac' => { 57 | 'puppetserver-confdir' => '/etc/puppetserver/conf.d', 58 | 'puppetservice' => 'puppetmaster', 59 | 'puppetpath' => '/etc/puppet', 60 | 'puppetconfdir' => '/etc/puppet', 61 | 'puppetcodedir' => '/etc/puppet', 62 | 'puppetvardir' => '/var/lib/puppet', 63 | 'puppetbin' => '/usr/bin/puppet', 64 | 'puppetbindir' => '/usr/bin', 65 | 'hieralibdir' => '/opt/puppet-git-repos/hiera/lib', 66 | 'hierapuppetlibdir' => '/opt/puppet-git-repos/hiera-puppet/lib', 67 | 'hierabindir' => '/opt/puppet-git-repos/hiera/bin', 68 | 'hieradatadir' => '/etc/puppet/hieradata', 69 | 'hieraconf' => '/etc/puppet/hiera.yaml', 70 | 'distmoduledir' => '/etc/puppet/modules', 71 | 'sitemoduledir' => '/usr/share/puppet/modules', 72 | }, 73 | 'unix' => { 74 | 'puppetserver-confdir' => '/etc/puppetserver/conf.d', 75 | 'puppetservice' => 'puppetmaster', 76 | 'puppetpath' => '/etc/puppet', 77 | 'puppetconfdir' => '/etc/puppet', 78 | 'puppetvardir' => '/var/lib/puppet', 79 | 'puppetbin' => '/usr/bin/puppet', 80 | 'puppetbindir' => '/usr/bin', 81 | 'privatebindir' => '/usr/bin', 82 | 'hieralibdir' => '/opt/puppet-git-repos/hiera/lib', 83 | 'hierapuppetlibdir' => '/opt/puppet-git-repos/hiera-puppet/lib', 84 | 'hierabindir' => '/opt/puppet-git-repos/hiera/bin', 85 | 'hieradatadir' => '/etc/puppet/hieradata', 86 | 'hieraconf' => '/etc/puppet/hiera.yaml', 87 | 'distmoduledir' => '/etc/puppet/modules', 88 | 'sitemoduledir' => '/usr/share/puppet/modules', 89 | }, 90 | 'archlinux' => { 91 | 'puppetserver-confdir' => '/etc/puppetserver/conf.d', 92 | 'puppetservice' => 'puppetmaster', 93 | 'puppetpath' => '/etc/puppetlabs/puppet', 94 | 'puppetconfdir' => '/etc/puppetlabs/puppet', 95 | 'puppetvardir' => '/opt/puppetlabs/puppet/cache', 96 | 'puppetbin' => '/usr/bin/puppet', 97 | 'puppetbindir' => '/usr/bin', 98 | 'privatebindir' => '/usr/bin', 99 | 'hieralibdir' => '/var/lib/hiera', 100 | 'hierapuppetlibdir' => '/opt/puppet-git-repos/hiera-puppet/lib', 101 | 'hierabindir' => '/usr/bin', 102 | 'hieradatadir' => '/etc/puppetlabs/code/hiera', 103 | 'hieraconf' => '/etc/hiera.yaml', 104 | 'distmoduledir' => '/etc/puppetlabs/code/modules', 105 | 'sitemoduledir' => '/usr/share/puppet/modules', 106 | }, 107 | 'windows' => { # cygwin windows 108 | 'puppetpath' => '`cygpath -smF 35`/PuppetLabs/puppet/etc', 109 | 'puppetconfdir' => '`cygpath -smF 35`/PuppetLabs/puppet/etc', 110 | 'puppetcodedir' => '`cygpath -smF 35`/PuppetLabs/puppet/etc', 111 | 'hieraconf' => '`cygpath -smF 35`/Puppetlabs/puppet/etc/hiera.yaml', 112 | 'puppetvardir' => '`cygpath -smF 35`/PuppetLabs/puppet/var', 113 | 'distmoduledir' => '`cygpath -smF 35`/PuppetLabs/puppet/etc/modules', 114 | 'sitemoduledir' => 'C:/usr/share/puppet/modules', 115 | 'hieralibdir' => '`cygpath -w /opt/puppet-git-repos/hiera/lib`', 116 | 'hierapuppetlibdir' => '`cygpath -w /opt/puppet-git-repos/hiera-puppet/lib`', 117 | # let's just add both potential bin dirs to the path 118 | 'puppetbindir' => '/cygdrive/c/Program Files (x86)/Puppet Labs/Puppet/bin:/cygdrive/c/Program Files/Puppet Labs/Puppet/bin', 119 | 'privatebindir' => '/usr/bin', 120 | 'hierabindir' => '/opt/puppet-git-repos/hiera/bin', 121 | }, 122 | 'pswindows' => { # windows windows 123 | 'distmoduledir' => 'C:\\ProgramData\\PuppetLabs\\puppet\\etc\\modules', 124 | 'sitemoduledir' => 'C:\\usr\\share\\puppet\\modules', 125 | 'hieralibdir' => 'C:\\opt\\puppet-git-repos\\hiera\\lib', 126 | 'hierapuppetlibdir' => 'C:\\opt\\puppet-git-repos\\hiera-puppet\\lib', 127 | 'hierabindir' => 'C:\\opt\\puppet-git-repos\\hiera\\bin', 128 | 'puppetpath' => '"C:\\Program Files (x86)\\Puppet Labs\\Puppet\\etc";"C:\\Program Files\\Puppet Labs\\Puppet\\etc"', 129 | 'hieraconf' => '"C:\\Program Files (x86)\\Puppet Labs\\Puppet\\etc\\hiera.yaml";"C:\\Program Files\\Puppet Labs\\Puppet\\etc\\hiera.yaml"', 130 | 'puppetvardir' => '"C:\\Program Files (x86)\\Puppet Labs\\Puppet\\var";"C:\\Program Files\\Puppet Labs\\Puppet\\var"', 131 | 'puppetbindir' => '"C:\\Program Files (x86)\\Puppet Labs\\Puppet\\bin";"C:\\Program Files\\Puppet Labs\\Puppet\\bin"', 132 | }, 133 | } 134 | 135 | # Add the appropriate foss defaults to the host object so that they can be accessed using host[option], set host[:type] = foss 136 | # @param [Host] host A single host to act upon 137 | # @param [String] platform The platform type of this host, one of windows, pswindows, freebsd, mac & unix 138 | def add_platform_foss_defaults(host, platform) 139 | FOSS_DEFAULTS[platform].each_pair do |key, val| 140 | host[key] = val 141 | end 142 | # add the group and type for backwards compatability 143 | host['group'] = if host['platform'] =~ /windows/ 144 | 'Administrators' 145 | else 146 | 'puppet' 147 | end 148 | host['type'] = 'foss' 149 | end 150 | 151 | # Add the appropriate foss defaults to an array of hosts 152 | # @param [Host, Array, String, Symbol] hosts One or more hosts to act upon, 153 | # or a role (String or Symbol) that identifies one or more hosts. 154 | def add_foss_defaults_on(hosts) 155 | block_on hosts do |host| 156 | platform = case host.class.to_s.downcase 157 | when /aix|unix/ 158 | 'unix' 159 | when /freebsd/ 160 | 'freebsd' 161 | when /openbsd/ 162 | 'openbsd' 163 | when /mac/ 164 | 'mac' 165 | when /pswindows/ 166 | 'pswindows' 167 | when /archlinux/ 168 | 'archlinux' 169 | else 170 | 'windows' 171 | end 172 | add_platform_foss_defaults(host, platform) 173 | end 174 | end 175 | 176 | # Remove the appropriate foss defaults from the host object so that they can no longer be accessed using host[option], set host[:type] = nil 177 | # @param [Host] host A single host to act upon 178 | # @param [String] platform The platform type of this host, one of windows, pswindows, freebsd, mac & unix 179 | def remove_platform_foss_defaults(host, platform) 180 | FOSS_DEFAULTS[platform].each_pair do |key, val| 181 | host.delete(key) 182 | end 183 | host['group'] = nil 184 | host['type'] = nil 185 | end 186 | 187 | # Remove the appropriate foss defaults from an array of hosts 188 | # @param [Host, Array, String, Symbol] hosts One or more hosts to act upon, 189 | # or a role (String or Symbol) that identifies one or more hosts. 190 | def remove_foss_defaults_on(hosts) 191 | block_on hosts do |host| 192 | platform = case host.class.to_s.downcase 193 | when /aix|unix/ 194 | 'unix' 195 | when /freebsd/ 196 | 'freebsd' 197 | when /openbsd/ 198 | 'openbsd' 199 | when /mac/ 200 | 'mac' 201 | when /pswindows/ 202 | 'pswindows' 203 | else 204 | 'windows' 205 | end 206 | remove_platform_foss_defaults(host, platform) 207 | end 208 | end 209 | end 210 | end 211 | end 212 | end 213 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | 203 | -------------------------------------------------------------------------------- /lib/beaker-puppet/install_utils/puppet5.rb: -------------------------------------------------------------------------------- 1 | require 'beaker/dsl/install_utils/foss_defaults' 2 | 3 | module Beaker 4 | module DSL 5 | module InstallUtils 6 | module Puppet5 7 | include Beaker::DSL::InstallUtils::FOSSDefaults 8 | 9 | # Base URL for internal Puppet Inc. builds 10 | DEFAULT_DEV_BUILDS_URL = 'https://builds.delivery.puppetlabs.net' 11 | 12 | # grab build json from the builds server 13 | # 14 | # @param [String] sha_yaml_url URL to the .yaml file containing the 15 | # build details 16 | # 17 | # @return [Hash{String=>String}] build json parsed into a ruby hash 18 | def fetch_build_details(sha_yaml_url) 19 | dst_folder = Dir.mktmpdir 20 | 21 | at_exit do 22 | if ($!.nil? || ($!.is_a?(SystemExit) && $!.success?)) && File.directory?(dst_folder) 23 | require 'fileutils' 24 | 25 | FileUtils.rm_rf(dst_folder) 26 | end 27 | end 28 | 29 | sha_yaml_filename = File.basename(sha_yaml_url) 30 | sha_yaml_folder_url = File.dirname(sha_yaml_url) 31 | 32 | sha_yaml_file_local_path = fetch_http_file( 33 | sha_yaml_folder_url, 34 | sha_yaml_filename, 35 | dst_folder, 36 | ) 37 | 38 | file_hash = YAML.load_file(sha_yaml_file_local_path) 39 | 40 | unless file_hash.is_a?(Hash) 41 | message = <<-EOF 42 | Data fetched from #{sha_yaml_url} is invalid 43 | 44 | If the URL appears to be something that you should not normally 45 | access, your ISP may be hijacking failed web results. Updating 46 | your DNS settings to use a public DNS resolver should remedy the 47 | issue. 48 | EOF 49 | 50 | fail_test(message) 51 | end 52 | 53 | [sha_yaml_folder_url, file_hash[:platform_data]] 54 | end 55 | 56 | # Get the host's packaging platform, based on beaker-hostgenerator's 57 | # osinfo hash and the environment. Set ENV['BEAKER_PACKAGING_PLATFORMS'] 58 | # to override the default packaging platform specified by 59 | # beaker-hostgenerator. This should be a comma-separated string with 60 | # entries of the format `=` 61 | # 62 | # @param [Host] host Host whose packaging platform to determine 63 | # @return [String] The packaging platform 64 | def host_packaging_platform(host) 65 | packaging_platform = host[:packaging_platform] 66 | if ENV['BEAKER_PACKAGING_PLATFORMS'] 67 | overrides = ENV['BEAKER_PACKAGING_PLATFORMS'].split(',').map { |e| e.split('=') }.to_h 68 | logger.debug("Found packaging platform overrides: #{overrides}") 69 | if overrides[host[:platform]] 70 | platform = overrides[host[:platform]] 71 | logger.debug("Default beaker packaging platform '#{host[:packaging_platform]}' for '#{host[:platform]}' overridden as '#{platform}'") 72 | packaging_platform = platform 73 | end 74 | end 75 | packaging_platform 76 | end 77 | 78 | # Gets the artifact & repo_config URLs for this host in the build. 79 | # 80 | # @param [Host] host Host to get artifact URL for 81 | # @param [Hash] build_details Details of the build in a hash 82 | # @param [String] build_url URL to the build 83 | # 84 | # @return [String, String] URL to the build artifact, URL to the repo_config 85 | # (nil if there is no repo_config for this platform for this build) 86 | def host_urls(host, build_details, build_url) 87 | packaging_platform = host_packaging_platform(host) 88 | if packaging_platform.nil? 89 | message = <<-EOF 90 | :packaging_platform not provided for host '#{host}', platform '#{host[:platform]}' 91 | :packaging_platform should be the platform-specific key from this list: 92 | #{build_details.keys} 93 | EOF 94 | fail_test(message) 95 | end 96 | 97 | logger.debug('Platforms available for this build:') 98 | logger.debug("#{build_details.keys}") 99 | logger.debug("PLATFORM SPECIFIC INFO for #{host} (packaging name '#{packaging_platform}'):") 100 | packaging_data = build_details[packaging_platform] 101 | logger.debug("- #{packaging_data}, isnil? #{packaging_data.nil?}") 102 | if packaging_data.nil? 103 | message = <<-EOF 104 | :packaging_platform '#{packaging_platform}' for host '#{host}' not in build details 105 | :packaging_platform should be the platform-specific key from this list: 106 | #{build_details.keys} 107 | EOF 108 | fail_test(message) 109 | end 110 | 111 | artifact_buildserver_path = packaging_data[:artifact] 112 | repoconfig_buildserver_path = packaging_data[:repo_config] 113 | fail_test('no artifact_buildserver_path found') if artifact_buildserver_path.nil? 114 | 115 | artifact_url = "#{build_url}/#{artifact_buildserver_path}" 116 | repoconfig_url = "#{build_url}/#{repoconfig_buildserver_path}" unless repoconfig_buildserver_path.nil? 117 | artifact_url_correct = link_exists?(artifact_url) 118 | logger.debug("- artifact url: '#{artifact_url}'. Exists? #{artifact_url_correct}") 119 | fail_test('artifact url built incorrectly') unless artifact_url_correct 120 | 121 | [artifact_url, repoconfig_url] 122 | end 123 | 124 | # install build artifact on the given host 125 | # 126 | # @param [Host] host Host to install artifact on 127 | # @param [String] artifact_url URL of the project install artifact 128 | # @param [String] project_name Name of project for artifact. Needed for OSX installs 129 | # 130 | # @return nil 131 | def install_artifact_on(host, artifact_url, project_name) 132 | variant, version, = host[:platform].to_array 133 | case variant 134 | when 'eos' 135 | host.get_remote_file(artifact_url) 136 | onhost_package_file = File.basename(artifact_url) 137 | # TODO: Will be refactored into {Beaker::Host#install_local_package} 138 | # immediately following this work. The release timing makes it 139 | # necessary to have this here separately for a short while 140 | host.install_from_file(onhost_package_file) 141 | when 'solaris' 142 | artifact_filename = File.basename(artifact_url) 143 | artifact_folder = File.dirname(artifact_url) 144 | fetch_http_file(artifact_folder, artifact_filename, '.') 145 | onhost_package_dir = host.tmpdir('puppet_installer') 146 | scp_to host, artifact_filename, onhost_package_dir 147 | onhost_package_file = "#{onhost_package_dir}/#{artifact_filename}" 148 | host.install_local_package(onhost_package_file, '.') 149 | when 'osx' 150 | on host, "curl -O #{artifact_url}" 151 | onhost_package_file = "#{project_name}*" 152 | host.install_local_package(onhost_package_file) 153 | when 'windows' 154 | if project_name == 'puppet-agent' 155 | install_msi_on(host, artifact_url) 156 | else 157 | generic_install_msi_on(host, artifact_url) 158 | end 159 | when 'aix' 160 | artifact_filename = File.basename(artifact_url) 161 | artifact_folder = File.dirname(artifact_url) 162 | fetch_http_file(artifact_folder, artifact_filename, '.') 163 | onhost_package_dir = host.tmpdir('puppet_installer') 164 | scp_to host, artifact_filename, onhost_package_dir 165 | onhost_package_file = "#{onhost_package_dir}/#{artifact_filename}" 166 | 167 | # TODO: Will be refactored into {Beaker::Host#install_local_package} 168 | # immediately following this work. The release timing makes it 169 | # necessary to have this here seperately for a short while 170 | # NOTE: the AIX 7.1 package will only install on 7.2 with 171 | # --ignoreos. This is a bug in package building on AIX 7.1's RPM 172 | aix_72_ignoreos_hack = '--ignoreos' if version == '7.2' 173 | on host, "rpm -ivh #{aix_72_ignoreos_hack} #{onhost_package_file}" 174 | else 175 | host.install_package(artifact_url) 176 | end 177 | end 178 | 179 | # Sets up the repo_configs on the host for this build 180 | # 181 | # @param [Host] host Host to install repo_configs on 182 | # @param [String] repoconfig_url URL to the repo_config 183 | # 184 | # @return nil 185 | def install_repo_configs_on(host, repoconfig_url) 186 | if repoconfig_url.nil? 187 | logger.warn("No repo_config for host '#{host}'. Skipping repo_config install") 188 | return 189 | end 190 | 191 | install_repo_configs_from_url(host, repoconfig_url) 192 | end 193 | 194 | # Installs a specified puppet project on all hosts. Gets build information 195 | # from the provided YAML file located at the +sha_yaml_url+ parameter. 196 | # 197 | # @param [String] project_name Name of the project to install 198 | # @param [String] sha_yaml_url URL to the .yaml file containing the 199 | # build details 200 | # @param [String or Array] hosts Optional string or array of host or hosts to 201 | # install on 202 | # 203 | # @note This install method only works for Puppet versions >= 5.0 204 | # 205 | # @return nil 206 | def install_from_build_data_url(project_name, sha_yaml_url, local_hosts = nil) 207 | unless link_exists?(sha_yaml_url) 208 | message = <<-EOF 209 | Unable to locate a downloadable build of #{project_name} (tried #{sha_yaml_url}) 210 | EOF 211 | fail_test(message) 212 | end 213 | 214 | base_url, build_details = fetch_build_details(sha_yaml_url) 215 | 216 | install_targets = local_hosts.nil? ? hosts : Array(local_hosts) 217 | 218 | install_targets.each do |host| 219 | artifact_url, repoconfig_url = host_urls(host, build_details, base_url) 220 | if host.platform.variant == 'ubuntu' && host.platform.version.to_f >= 24.04 221 | # install the specific artifact we built, not based on how its repos are configured 222 | tmp_file = host.tmpfile('puppet-agent') 223 | on(host, "curl -L --output #{tmp_file} #{artifact_url}") 224 | host.install_local_package(tmp_file) 225 | elsif repoconfig_url.nil? 226 | install_artifact_on(host, artifact_url, project_name) 227 | else 228 | install_repo_configs_on(host, repoconfig_url) 229 | host.install_package(project_name) 230 | end 231 | configure_type_defaults_on(host) 232 | end 233 | end 234 | 235 | # Install puppet-agent from an internal Puppet development build. This method only 236 | # works inside Puppet's corporate VPN. 237 | # @param [Host|Array] one_or_more_hosts to install the agent on 238 | # @param [String] ref to install (this can be a tag or a long SHA) 239 | def install_puppet_agent_from_dev_builds_on(one_or_more_hosts, ref) 240 | block_on(one_or_more_hosts, run_in_parallel: true) do |host| 241 | unless dev_builds_accessible_on?(host) 242 | fail_test("Can't install puppet-agent #{ref}: unable to access Puppet's internal builds") 243 | end 244 | end 245 | sha_yaml_url = File.join(DEFAULT_DEV_BUILDS_URL, 'puppet-agent', ref, 'artifacts', "#{ref}.yaml") 246 | install_from_build_data_url('puppet-agent', sha_yaml_url, one_or_more_hosts) 247 | end 248 | end 249 | end 250 | end 251 | end 252 | -------------------------------------------------------------------------------- /lib/beaker-puppet/install_utils/module_utils.rb: -------------------------------------------------------------------------------- 1 | module Beaker 2 | module DSL 3 | module InstallUtils 4 | # 5 | # This module contains methods to help install puppet modules 6 | # 7 | # To mix this is into a class you need the following: 8 | # * a method *hosts* that yields any hosts implementing 9 | # {Beaker::Host}'s interface to act upon. 10 | # * a method *options* that provides an options hash, see {Beaker::Options::OptionsHash} 11 | # * the module {Beaker::DSL::Roles} that provides access to the various hosts implementing 12 | # {Beaker::Host}'s interface to act upon 13 | # * the module {Beaker::DSL::Wrappers} the provides convenience methods for {Beaker::DSL::Command} creation 14 | module ModuleUtils 15 | # The directories in the module directory that will not be scp-ed to the test system when using 16 | # `copy_module_to` 17 | PUPPET_MODULE_INSTALL_IGNORE = ['/.bundle', '/.git', '/.idea', '/.vagrant', '/.vendor', '/vendor', '/acceptance', 18 | '/bundle', '/spec', '/tests', '/log', '/.svn', '/junit', '/pkg', '/example', '/tmp',] 19 | 20 | # Install the desired module on all hosts using either the PMT or a 21 | # staging forge 22 | # 23 | # @see install_dev_puppet_module 24 | def install_dev_puppet_module_on(host, opts) 25 | if options[:forge_host] 26 | install_puppet_module_via_pmt_on(host, opts) 27 | else 28 | copy_module_to(host, opts) 29 | end 30 | end 31 | alias puppet_module_install_on install_dev_puppet_module_on 32 | 33 | # Install the desired module on all hosts using either the PMT or a 34 | # staging forge 35 | # 36 | # Passes options through to either `install_puppet_module_via_pmt_on` 37 | # or `copy_module_to` 38 | # 39 | # @param opts [Hash] 40 | # 41 | # @example Installing a module from the local directory 42 | # install_dev_puppet_module( :source => './', :module_name => 'concat' ) 43 | # 44 | # @example Installing a module from a staging forge 45 | # options[:forge_host] = 'my-forge-api.example.com' 46 | # install_dev_puppet_module( :source => './', :module_name => 'concat' ) 47 | # 48 | # @see install_puppet_module_via_pmt 49 | # @see copy_module_to 50 | def install_dev_puppet_module(opts) 51 | block_on(hosts) { |h| install_dev_puppet_module_on(h, opts) } 52 | end 53 | alias puppet_module_install install_dev_puppet_module 54 | 55 | # Install the desired module with the PMT on a given host 56 | # 57 | # @param opts [Hash] 58 | # @option opts [String] :module_name The short name of the module to be installed 59 | # @option opts [String] :version The version of the module to be installed 60 | def install_puppet_module_via_pmt_on(host, opts = {}) 61 | block_on host do |h| 62 | version_info = opts[:version] ? "-v #{opts[:version]}" : '' 63 | if opts[:source] 64 | author_name, module_name = parse_for_modulename(opts[:source]) 65 | modname = "#{author_name}-#{module_name}" 66 | else 67 | modname = opts[:module_name] 68 | end 69 | 70 | puppet_opts = {} 71 | if host[:default_module_install_opts].respond_to? :merge 72 | puppet_opts = host[:default_module_install_opts].merge(puppet_opts) 73 | end 74 | 75 | if options[:forge_host] 76 | puppet_opts[:module_repository] = if options[:forge_host] =~ /^http/ 77 | options[:forge_host] 78 | else 79 | "https://#{options[:forge_host]}" 80 | end 81 | end 82 | 83 | on h, puppet("module install #{modname} #{version_info}", puppet_opts) 84 | end 85 | end 86 | 87 | # Install the desired module with the PMT on all known hosts 88 | # @see #install_puppet_module_via_pmt_on 89 | def install_puppet_module_via_pmt(opts = {}) 90 | install_puppet_module_via_pmt_on(hosts, opts) 91 | end 92 | 93 | # Install local module for acceptance testing 94 | # should be used as a presuite to ensure local module is copied to the hosts you want, particularly masters 95 | # @param [Host, Array, String, Symbol] one_or_more_hosts 96 | # One or more hosts to act upon, 97 | # or a role (String or Symbol) that identifies one or more hosts. 98 | # @option opts [String] :source ('./') 99 | # The current directory where the module sits, otherwise will try 100 | # and walk the tree to figure out 101 | # @option opts [String] :module_name (nil) 102 | # Name which the module should be installed under, please do not include author, 103 | # if none is provided it will attempt to parse the metadata.json and then the Module file to determine 104 | # the name of the module 105 | # @option opts [String] :target_module_path (host['distmoduledir']/modules) 106 | # Location where the module should be installed, will default 107 | # to host['distmoduledir']/modules 108 | # @option opts [Array] :ignore_list 109 | # @option opts [String] :protocol 110 | # Name of the underlying transfer method. Valid options are 'scp' or 'rsync'. 111 | # @raise [ArgumentError] if not host is provided or module_name is not provided and can not be found in Modulefile 112 | # 113 | def copy_module_to(one_or_more_hosts, opts = {}) 114 | block_on one_or_more_hosts do |host| 115 | opts = { source: './', 116 | target_module_path: host['distmoduledir'], 117 | ignore_list: PUPPET_MODULE_INSTALL_IGNORE, }.merge(opts) 118 | 119 | ignore_list = build_ignore_list(opts) 120 | target_module_dir = get_target_module_path(host, opts[:target_module_path]) 121 | source_path = File.expand_path(opts[:source]) 122 | source_name = File.basename(source_path) 123 | if opts.has_key?(:module_name) 124 | module_name = opts[:module_name] 125 | else 126 | _, module_name = parse_for_modulename(source_path) 127 | end 128 | 129 | target_path = File.join(target_module_dir, module_name) 130 | target_path = target_path.gsub(%r{/}, '\\') if host.is_powershell? # make sure our slashes are correct 131 | 132 | opts[:protocol] ||= 'scp' 133 | case opts[:protocol] 134 | when 'scp' 135 | # move to the host 136 | logger.debug "Using scp to transfer #{source_path} to #{target_path}" 137 | scp_to host, source_path, target_module_dir, { ignore: ignore_list } 138 | 139 | # rename to the selected module name, if not correct 140 | cur_path = File.join(target_module_dir, source_name) 141 | cur_path = cur_path.gsub(%r{/}, '\\') if host.is_powershell? # make sure our slashes are correct 142 | host.mv cur_path, target_path unless cur_path == target_path 143 | when 'rsync' 144 | logger.debug "Using rsync to transfer #{source_path} to #{target_path}" 145 | rsync_to host, source_path, target_path, { ignore: ignore_list } 146 | else 147 | logger.debug 'Unsupported transfer protocol, returning nil' 148 | nil 149 | end 150 | end 151 | end 152 | alias copy_root_module_to copy_module_to 153 | 154 | def get_target_module_path(host, path = nil) 155 | if path 156 | on(host, "echo #{path}").stdout.chomp 157 | else 158 | path = host.puppet['basemodulepath'].split(':').first 159 | raise ArgumentError, 'Unable to find target module path to copy to' unless path 160 | 161 | path 162 | end 163 | end 164 | 165 | # Recursive method for finding the module root 166 | # Assumes that a Modulefile exists 167 | # @param [String] possible_module_directory 168 | # will look for Modulefile and if none found go up one level and try again until root is reached 169 | # 170 | # @return [String,nil] 171 | def parse_for_moduleroot(possible_module_directory) 172 | if File.exist?("#{possible_module_directory}/Modulefile") || File.exist?("#{possible_module_directory}/metadata.json") 173 | possible_module_directory 174 | elsif possible_module_directory === '/' 175 | logger.error "At root, can't parse for another directory" 176 | nil 177 | else 178 | logger.debug "No Modulefile or metadata.json found at #{possible_module_directory}, moving up" 179 | parse_for_moduleroot File.expand_path(File.join(possible_module_directory, '..')) 180 | end 181 | end 182 | 183 | # Parse root directory of a module for module name 184 | # Searches for metadata.json and then if none found, Modulefile and parses for the Name attribute 185 | # @param [String] root_module_dir 186 | # @return [String] module name 187 | def parse_for_modulename(root_module_dir) 188 | author_name = nil 189 | module_name = nil 190 | if File.exist?("#{root_module_dir}/metadata.json") 191 | logger.debug 'Attempting to parse Modulename from metadata.json' 192 | module_json = JSON.parse(File.read("#{root_module_dir}/metadata.json")) 193 | author_name, module_name = get_module_name(module_json['name']) if module_json.has_key?('name') 194 | end 195 | if !module_name && File.exist?("#{root_module_dir}/Modulefile") 196 | logger.debug 'Attempting to parse Modulename from Modulefile' 197 | if /^name\s+'?(\w+-\w+)'?\s*$/i.match(File.read("#{root_module_dir}/Modulefile")) 198 | author_name, module_name = get_module_name(Regexp.last_match[1]) 199 | end 200 | end 201 | logger.debug 'Unable to determine name, returning null' if !module_name && !author_name 202 | [author_name, module_name] 203 | end 204 | 205 | # Parse modulename from the pattern 'Auther-ModuleName' 206 | # 207 | # @param [String] author_module_name - pattern 208 | # 209 | # @return [String,nil] 210 | # 211 | def get_module_name(author_module_name) 212 | split_name = split_author_modulename(author_module_name) 213 | return unless split_name 214 | 215 | [split_name[:author], split_name[:module]] 216 | end 217 | 218 | # Split the Author-Name into a hash 219 | # @param [String] author_module_attr 220 | # 221 | # @return [Hash,nil] :author and :module symbols will be returned 222 | # 223 | def split_author_modulename(author_module_attr) 224 | result = /(\w+)-(\w+)/.match(author_module_attr) 225 | return unless result 226 | 227 | { author: result[1], module: result[2] } 228 | end 229 | 230 | # Build an array list of files/directories to ignore when pushing to remote host 231 | # Automatically adds '..' and '.' to array. If not opts of :ignore list is provided 232 | # it will use the static variable PUPPET_MODULE_INSTALL_IGNORE 233 | # 234 | # @param opts [Hash] 235 | # @option opts [Array] :ignore_list A list of files/directories to ignore 236 | def build_ignore_list(opts = {}) 237 | ignore_list = opts[:ignore_list] || PUPPET_MODULE_INSTALL_IGNORE 238 | raise ArgumentError 'Ignore list must be an Array' if !ignore_list.is_a?(Array) || ignore_list.nil? 239 | 240 | ignore_list << '.' unless ignore_list.include? '.' 241 | ignore_list << '..' unless ignore_list.include? '..' 242 | ignore_list 243 | end 244 | end 245 | end 246 | end 247 | end 248 | -------------------------------------------------------------------------------- /lib/beaker-puppet/install_utils/windows_utils.rb: -------------------------------------------------------------------------------- 1 | module Beaker 2 | module DSL 3 | module InstallUtils 4 | # 5 | # This module contains methods useful for Windows installs 6 | # 7 | module WindowsUtils 8 | # Given a host, returns it's system TEMP path 9 | # 10 | # @param [Host] host An object implementing {Beaker::Hosts}'s interface. 11 | # 12 | # @return [String] system temp path 13 | def get_system_temp_path(host) 14 | host.system_temp_path 15 | end 16 | alias get_temp_path get_system_temp_path 17 | 18 | # Generates commands to be inserted into a Windows batch file to launch an MSI install 19 | # @param [String] msi_path The path of the MSI - can be a local Windows style file path like 20 | # c:\temp\puppet.msi OR a url like https://download.com/puppet.msi or file://c:\temp\puppet.msi 21 | # @param [Hash{String=>String}] msi_opts MSI installer options 22 | # See https://docs.puppetlabs.com/guides/install_puppet/install_windows.html#msi-properties 23 | # @param [String] log_path The path to write the MSI log - must be a local Windows style file path 24 | # 25 | # @api private 26 | def msi_install_script(msi_path, msi_opts, log_path) 27 | # msiexec requires backslashes in file paths launched under cmd.exe start /w 28 | url_pattern = %r{^(https?|file)://} 29 | msi_path = msi_path.gsub(%r{/}, '\\') if msi_path !~ url_pattern 30 | 31 | msi_params = msi_opts.map { |k, v| "#{k}=#{v}" }.join(' ') 32 | 33 | # msiexec requires quotes around paths with backslashes - c:\ or file://c:\ 34 | # not strictly needed for http:// but it simplifies this code 35 | batch_contents = <<~BATCH 36 | start /w msiexec.exe /i "#{msi_path}" /qn /L*V #{log_path} #{msi_params} 37 | exit /B %errorlevel% 38 | BATCH 39 | end 40 | 41 | # Given a host, path to MSI and MSI options, will create a batch file 42 | # on the host, returning the path to the randomized batch file and 43 | # the randomized log file 44 | # 45 | # @param [Host] host An object implementing {Beaker::Hosts}'s interface. 46 | # @param [String] msi_path The path of the MSI - can be a local Windows 47 | # style file path like c:\temp\puppet.msi OR a url like 48 | # https://download.com/puppet.msi or file://c:\temp\puppet.msi 49 | # @param [Hash{String=>String}] msi_opts MSI installer options 50 | # See https://docs.puppetlabs.com/guides/install_puppet/install_windows.html#msi-properties 51 | # 52 | # @api private 53 | # @return [String, String] path to the batch file, patch to the log file 54 | def create_install_msi_batch_on(host, msi_path, msi_opts) 55 | timestamp = Time.new.strftime('%Y-%m-%d_%H.%M.%S') 56 | tmp_path = host.system_temp_path 57 | tmp_path.gsub!('/', '\\') 58 | 59 | batch_name = "install-puppet-msi-#{timestamp}.bat" 60 | batch_path = "#{tmp_path}#{host.scp_separator}#{batch_name}" 61 | log_path = "#{tmp_path}\\install-puppet-#{timestamp}.log" 62 | 63 | Tempfile.open(batch_name) do |tmp_file| 64 | batch_contents = msi_install_script(msi_path, msi_opts, log_path) 65 | 66 | File.open(tmp_file.path, 'w') { |file| file.puts(batch_contents) } 67 | host.do_scp_to(tmp_file.path, batch_path, {}) 68 | end 69 | 70 | [batch_path, log_path] 71 | end 72 | 73 | # Given hosts construct a PATH that includes puppetbindir, facterbindir and hierabindir 74 | # @param [Host, Array, String, Symbol] hosts One or more hosts to act upon, 75 | # or a role (String or Symbol) that identifies one or more hosts. 76 | # @param [String] msi_path The path of the MSI - can be a local Windows style file path like 77 | # c:\temp\puppet.msi OR a url like https://download.com/puppet.msi or file://c:\temp\puppet.msi 78 | # @param [Hash{String=>String}] msi_opts MSI installer options 79 | # See https://docs.puppetlabs.com/guides/install_puppet/install_windows.html#msi-properties 80 | # @option msi_opts [String] INSTALLIDIR Where Puppet and its dependencies should be installed. 81 | # (Defaults vary based on operating system and intaller architecture) 82 | # Requires Puppet 2.7.12 / PE 2.5.0 83 | # @option msi_opts [String] PUPPET_MASTER_SERVER The hostname where the puppet master server can be reached. 84 | # (Defaults to puppet) 85 | # Requires Puppet 2.7.12 / PE 2.5.0 86 | # @option msi_opts [String] PUPPET_CA_SERVER The hostname where the CA puppet master server can be reached, if you are using multiple masters and only one of them is acting as the CA. 87 | # (Defaults the value of PUPPET_MASTER_SERVER) 88 | # Requires Puppet 2.7.12 / PE 2.5.0 89 | # @option msi_opts [String] PUPPET_AGENT_CERTNAME The node’s certificate name, and the name it uses when requesting catalogs. This will set a value for 90 | # (Defaults to the node's fqdn as discovered by facter fqdn) 91 | # Requires Puppet 2.7.12 / PE 2.5.0 92 | # @option msi_opts [String] PUPPET_AGENT_ENVIRONMENT The node’s environment. 93 | # (Defaults to production) 94 | # Requires Puppet 3.3.1 / PE 3.1.0 95 | # @option msi_opts [String] PUPPET_AGENT_STARTUP_MODE Whether the puppet agent service should run (or be allowed to run) 96 | # (Defaults to Manual - valid values are Automatic, Manual or Disabled) 97 | # Requires Puppet 3.4.0 / PE 3.2.0 98 | # @option msi_opts [String] PUPPET_AGENT_ACCOUNT_USER Whether the puppet agent service should run (or be allowed to run) 99 | # (Defaults to LocalSystem) 100 | # Requires Puppet 3.4.0 / PE 3.2.0 101 | # @option msi_opts [String] PUPPET_AGENT_ACCOUNT_PASSWORD The password to use for puppet agent’s user account 102 | # (No default) 103 | # Requires Puppet 3.4.0 / PE 3.2.0 104 | # @option msi_opts [String] PUPPET_AGENT_ACCOUNT_DOMAIN The domain of puppet agent’s user account. 105 | # (Defaults to .) 106 | # Requires Puppet 3.4.0 / PE 3.2.0 107 | # @option opts [Boolean] :debug output the MSI installation log when set to true 108 | # otherwise do not output log (false; default behavior) 109 | # 110 | # @example 111 | # install_msi_on(hosts, 'c:\puppet.msi', {:debug => true}) 112 | # 113 | # @api private 114 | def install_msi_on(hosts, msi_path, msi_opts = {}, opts = {}) 115 | block_on hosts do |host| 116 | msi_opts['PUPPET_AGENT_STARTUP_MODE'] ||= 'Manual' 117 | batch_path, log_file = create_install_msi_batch_on(host, msi_path, msi_opts) 118 | # Powershell command looses an escaped slash resulting in cygwin relative path 119 | # See https://github.com/puppetlabs/beaker/pull/1626#issuecomment-621341555 120 | log_file_escaped = log_file.gsub('\\', '\\\\\\') 121 | # begin / rescue here so that we can reuse existing error msg propagation 122 | begin 123 | # 1641 = ERROR_SUCCESS_REBOOT_INITIATED 124 | # 3010 = ERROR_SUCCESS_REBOOT_REQUIRED 125 | on host, Command.new("\"#{batch_path}\"", [], { cmdexe: true }), acceptable_exit_codes: [0, 1641, 3010] 126 | rescue StandardError 127 | logger.info(file_contents_on(host, log_file_escaped)) 128 | raise 129 | end 130 | 131 | logger.info(file_contents_on(host, log_file_escaped)) if opts[:debug] 132 | 133 | unless host.is_cygwin? 134 | # Enable the PATH updates 135 | host.close 136 | 137 | # Some systems require a full reboot to trigger the enabled path 138 | host.reboot unless on(host, Command.new('puppet -h', [], { cmdexe: true }), 139 | accept_all_exit_codes: true).exit_code == 0 140 | end 141 | 142 | # verify service status post install 143 | # if puppet service exists, then pe-puppet is not queried 144 | # if puppet service does not exist, pe-puppet is queried and that exit code is used 145 | # therefore, this command will always exit 0 if either service is installed 146 | # 147 | # We also take advantage of this output to verify the startup 148 | # settings are honored as supplied to the MSI 149 | on host, Command.new('sc qc puppet || sc qc pe-puppet', [], { cmdexe: true }) do |result| 150 | output = result.stdout 151 | startup_mode = msi_opts['PUPPET_AGENT_STARTUP_MODE'].upcase 152 | 153 | search = case startup_mode 154 | when 'AUTOMATIC' 155 | { code: 2, name: 'AUTO_START' } 156 | when 'MANUAL' 157 | { code: 3, name: 'DEMAND_START' } 158 | when 'DISABLED' 159 | { code: 4, name: 'DISABLED' } 160 | end 161 | 162 | if output !~ /^\s+START_TYPE\s+:\s+#{search[:code]}\s+#{search[:name]}/ 163 | raise "puppet service startup mode did not match supplied MSI option '#{startup_mode}'" 164 | end 165 | end 166 | 167 | # (PA-514) value for PUPPET_AGENT_STARTUP_MODE should be present in 168 | # registry and honored after install/upgrade. 169 | reg_key = if host.is_x86_64? 170 | 'HKLM\\SOFTWARE\\Wow6432Node\\Puppet Labs\\PuppetInstaller' 171 | else 172 | 'HKLM\\SOFTWARE\\Puppet Labs\\PuppetInstaller' 173 | end 174 | reg_query_command = %(reg query "#{reg_key}" /v "RememberedPuppetAgentStartupMode" | findstr #{msi_opts['PUPPET_AGENT_STARTUP_MODE']}) 175 | on host, Command.new(reg_query_command, [], { cmdexe: true }) 176 | 177 | # emit the misc/versions.txt file which contains component versions for 178 | # puppet, facter, hiera, pxp-agent, packaging and vendored Ruby 179 | [ 180 | "'%PROGRAMFILES%\\Puppet Labs\\puppet\\misc\\versions.txt'", 181 | "'%PROGRAMFILES(X86)%\\Puppet Labs\\puppet\\misc\\versions.txt'", 182 | ].each do |path| 183 | result = on(host, "cmd /c type #{path}", accept_all_exit_codes: true) 184 | if result.exit_code == 0 185 | logger.info(result.stdout) 186 | break 187 | end 188 | end 189 | end 190 | end 191 | 192 | # Installs a specified msi path on given hosts 193 | # @param [Host, Array, String, Symbol] hosts One or more hosts to act upon, 194 | # or a role (String or Symbol) that identifies one or more hosts. 195 | # @param [String] msi_path The path of the MSI - can be a local Windows style file path like 196 | # c:\temp\foo.msi OR a url like https://download.com/foo.msi or file://c:\temp\foo.msi 197 | # @param [Hash{String=>String}] msi_opts MSI installer options 198 | # @option opts [Boolean] :debug output the MSI installation log when set to true 199 | # otherwise do not output log (false; default behavior) 200 | # 201 | # @example 202 | # generic_install_msi_on(hosts, 'https://releases.hashicorp.com/vagrant/1.8.4/vagrant_1.8.4.msi', {}, {:debug => true}) 203 | # 204 | # @api private 205 | def generic_install_msi_on(hosts, msi_path, msi_opts = {}, opts = {}) 206 | block_on hosts do |host| 207 | batch_path, log_file = create_install_msi_batch_on(host, msi_path, msi_opts) 208 | # Powershell command looses an escaped slash resulting in cygwin relative path 209 | # See https://github.com/puppetlabs/beaker/pull/1626#issuecomment-621341555 210 | log_file_escaped = log_file.gsub('\\', '\\\\\\') 211 | # begin / rescue here so that we can reuse existing error msg propagation 212 | begin 213 | # 1641 = ERROR_SUCCESS_REBOOT_INITIATED 214 | # 3010 = ERROR_SUCCESS_REBOOT_REQUIRED 215 | on host, Command.new("\"#{batch_path}\"", [], { cmdexe: true }), acceptable_exit_codes: [0, 1641, 3010] 216 | rescue StandardError 217 | logger.info(file_contents_on(host, log_file_escaped)) 218 | 219 | raise 220 | end 221 | 222 | logger.info(file_contents_on(host, log_file_escaped)) if opts[:debug] 223 | 224 | host.close unless host.is_cygwin? 225 | end 226 | end 227 | end 228 | end 229 | end 230 | end 231 | -------------------------------------------------------------------------------- /spec/beaker-puppet/install_utils/windows_utils_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | class ClassMixedWithDSLInstallUtils 4 | include Beaker::DSL::Helpers 5 | include Beaker::DSL::Patterns 6 | include Beaker::DSL::InstallUtils 7 | 8 | def logger 9 | @logger ||= RSpec::Mocks::Double.new('logger').as_null_object 10 | end 11 | end 12 | 13 | describe ClassMixedWithDSLInstallUtils do 14 | let(:windows_temp) { 'C:\\Windows\\Temp' } 15 | let(:batch_path) { '/fake/batch/path' } 16 | let(:msi_path) { 'c:\\foo\\puppet.msi' } 17 | let(:winhost) do 18 | make_host('winhost', 19 | { platform: Beaker::Platform.new('windows-2008r2-64'), 20 | pe_ver: '3.0', 21 | working_dir: '/tmp', 22 | is_cygwin: true, }) 23 | end 24 | let(:winhost_non_cygwin) do 25 | make_host('winhost_non_cygwin', 26 | { platform: 'windows', 27 | pe_ver: '3.0', 28 | working_dir: '/tmp', 29 | is_cygwin: 'false', }) 30 | end 31 | let(:hosts) { [winhost, winhost_non_cygwin] } 32 | 33 | def expect_install_called 34 | result = Beaker::Result.new(nil, 'temp') 35 | result.exit_code = 0 36 | 37 | hosts.each do |host| 38 | expectation = expect(subject).to receive(:on).with(host, having_attributes(command: "\"#{batch_path}\""), 39 | anything).and_return(result) 40 | if block_given? 41 | should_break = yield expectation 42 | break if should_break 43 | end 44 | end 45 | end 46 | 47 | def expect_status_called(start_type = 'DEMAND_START') 48 | result = Beaker::Result.new(nil, 'temp') 49 | result.exit_code = 0 50 | result.stdout = case start_type 51 | when 'DISABLED' 52 | ' START_TYPE : 4 DISABLED' 53 | when 'AUTOMATIC' 54 | ' START_TYPE : 2 AUTO_START' 55 | else # 'DEMAND_START' 56 | ' START_TYPE : 3 DEMAND_START' 57 | end 58 | 59 | hosts.each do |host| 60 | expect(subject).to receive(:on).with(host, 61 | having_attributes(command: 'sc qc puppet || sc qc pe-puppet')).and_yield(result) 62 | end 63 | end 64 | 65 | def expect_version_log_called(times = hosts.length) 66 | path = "'%PROGRAMFILES%\\Puppet Labs\\puppet\\misc\\versions.txt'" 67 | 68 | result = Beaker::Result.new(nil, 'temp') 69 | result.exit_code = 0 70 | 71 | hosts.each do |host| 72 | expect(subject).to receive(:on).with(host, "cmd /c type #{path}", anything).and_return(result) 73 | end 74 | end 75 | 76 | def expect_script_matches(hosts, contents) 77 | hosts.each do |host| 78 | expect(host) 79 | .to receive(:do_scp_to) do |local_path, remote_path| 80 | expect(File.read(local_path)).to match(contents) 81 | end 82 | .and_return(true) 83 | end 84 | end 85 | 86 | def expect_reg_query_called(times = hosts.length) 87 | hosts.each do |host| 88 | expect(host).to receive(:is_x86_64?).and_return(:true) 89 | end 90 | 91 | hosts.each do |host| 92 | expect(subject).to receive(:on) 93 | .with(host, having_attributes(command: /reg query "HKLM\\SOFTWARE\\Wow6432Node\\Puppet Labs\\PuppetInstaller/)) 94 | end 95 | end 96 | 97 | def expect_puppet_path_called 98 | hosts.each do |host| 99 | next if host.is_cygwin? 100 | 101 | result = Beaker::Result.new(nil, 'temp') 102 | result.exit_code = 0 103 | 104 | expect(subject).to receive(:on) 105 | .with(host, having_attributes(command: 'puppet -h'), anything) 106 | .and_return(result) 107 | end 108 | end 109 | 110 | describe '#install_msi_on' do 111 | let(:log_file) { '/fake/log/file.log' } 112 | 113 | before :each do 114 | result = Beaker::Result.new(nil, 'temp') 115 | result.exit_code = 0 116 | 117 | hosts.each do |host| 118 | allow(subject).to receive(:on) 119 | .with(host, having_attributes(command: "\"#{batch_path}\"")) 120 | .and_return(result) 121 | end 122 | 123 | allow(subject).to receive(:file_exists_on).and_return(true) 124 | allow(subject).to receive(:create_install_msi_batch_on).and_return([batch_path, log_file]) 125 | end 126 | 127 | it 'will specify a PUPPET_AGENT_STARTUP_MODE of Manual by default' do 128 | expect_install_called 129 | expect_puppet_path_called 130 | expect_status_called 131 | expect_reg_query_called 132 | expect_version_log_called 133 | expect(subject).to receive(:create_install_msi_batch_on).with( 134 | anything, anything, 135 | { 'PUPPET_AGENT_STARTUP_MODE' => 'Manual' } 136 | ) 137 | subject.install_msi_on(hosts, msi_path, {}) 138 | end 139 | 140 | it 'allows configuration of PUPPET_AGENT_STARTUP_MODE to Automatic' do 141 | expect_install_called 142 | expect_puppet_path_called 143 | expect_status_called('AUTOMATIC') 144 | expect_reg_query_called 145 | expect_version_log_called 146 | value = 'Automatic' 147 | expect(subject).to receive(:create_install_msi_batch_on).with( 148 | anything, anything, 149 | { 'PUPPET_AGENT_STARTUP_MODE' => value } 150 | ) 151 | subject.install_msi_on(hosts, msi_path, { 'PUPPET_AGENT_STARTUP_MODE' => value }) 152 | end 153 | 154 | it 'allows configuration of PUPPET_AGENT_STARTUP_MODE to Disabled' do 155 | expect_install_called 156 | expect_puppet_path_called 157 | expect_status_called('DISABLED') 158 | expect_reg_query_called 159 | expect_version_log_called 160 | value = 'Disabled' 161 | expect(subject).to receive(:create_install_msi_batch_on).with( 162 | anything, anything, 163 | { 'PUPPET_AGENT_STARTUP_MODE' => value } 164 | ) 165 | subject.install_msi_on(hosts, msi_path, { 'PUPPET_AGENT_STARTUP_MODE' => value }) 166 | end 167 | 168 | it 'will not generate a command to emit a log file without the :debug option set' do 169 | expect_install_called 170 | expect_puppet_path_called 171 | expect_status_called 172 | expect_reg_query_called 173 | expect_version_log_called 174 | 175 | expect(subject).to receive(:file_contents_on).with(anything, log_file).never 176 | 177 | subject.install_msi_on(hosts, msi_path) 178 | end 179 | 180 | it 'will generate a command to emit a log file when the install script fails' do 181 | # NOTE: a single failure aborts executing against remaining hosts 182 | expect_install_called do |e| 183 | e.and_raise 184 | true # break 185 | end 186 | 187 | expect(subject).to receive(:file_contents_on).with(anything, log_file) 188 | expect do 189 | subject.install_msi_on(hosts, msi_path) 190 | end.to raise_error(RuntimeError) 191 | end 192 | 193 | it 'will generate a command to emit a log file with the :debug option set' do 194 | expect_install_called 195 | expect_reg_query_called 196 | expect_puppet_path_called 197 | expect_status_called 198 | expect_version_log_called 199 | 200 | expect(subject).to receive(:file_contents_on).with(anything, log_file).exactly(hosts.length).times 201 | 202 | subject.install_msi_on(hosts, msi_path, {}, { debug: true }) 203 | end 204 | 205 | it 'will pass msi_path to #create_install_msi_batch_on as-is' do 206 | expect_install_called 207 | expect_reg_query_called 208 | expect_puppet_path_called 209 | expect_status_called 210 | expect_version_log_called 211 | test_path = 'test/path' 212 | expect(subject).to receive(:create_install_msi_batch_on).with( 213 | anything, test_path, anything 214 | ) 215 | subject.install_msi_on(hosts, test_path) 216 | end 217 | 218 | it 'will search in Wow6432Node for the remembered startup setting on 64-bit hosts' do 219 | expect_install_called 220 | expect_puppet_path_called 221 | expect_status_called 222 | expect_version_log_called 223 | 224 | hosts.each do |host| 225 | expect(host).to receive(:is_x86_64?).and_return(true) 226 | 227 | expect(subject).to receive(:on) 228 | .with(host, having_attributes(command: 'reg query "HKLM\\SOFTWARE\\Wow6432Node\\Puppet Labs\\PuppetInstaller" /v "RememberedPuppetAgentStartupMode" | findstr Manual')) 229 | end 230 | 231 | subject.install_msi_on(hosts, msi_path, { 'PUPPET_AGENT_STARTUP_MODE' => 'Manual' }) 232 | end 233 | 234 | it 'will omit Wow6432Node in the registry search for remembered startup setting on 32-bit hosts' do 235 | expect_install_called 236 | expect_puppet_path_called 237 | expect_status_called 238 | expect_version_log_called 239 | 240 | hosts.each do |host| 241 | expect(host).to receive(:is_x86_64?).and_return(false) 242 | 243 | expect(subject).to receive(:on) 244 | .with(host, having_attributes(command: 'reg query "HKLM\\SOFTWARE\\Puppet Labs\\PuppetInstaller" /v "RememberedPuppetAgentStartupMode" | findstr Manual')) 245 | end 246 | 247 | subject.install_msi_on(hosts, msi_path, { 'PUPPET_AGENT_STARTUP_MODE' => 'Manual' }) 248 | end 249 | end 250 | 251 | describe '#create_install_msi_batch_on' do 252 | let(:tmp) { '/tmp/create_install_msi_batch_on' } 253 | let(:tmp_slashes) { tmp.gsub('/', '\\') } 254 | 255 | before :each do 256 | FakeFS::FileSystem.add(File.expand_path(tmp)) 257 | hosts.each do |host| 258 | allow(host).to receive(:system_temp_path).and_return(tmp) 259 | end 260 | end 261 | 262 | it 'passes msi_path & msi_opts down to #msi_install_script' do 263 | allow(winhost).to receive(:do_scp_to) 264 | test_path = '/path/to/test/with/13540' 265 | test_opts = { 'key1' => 'val1', 'key2' => 'val2' } 266 | expect(subject).to receive(:msi_install_script).with( 267 | test_path, test_opts, anything 268 | ) 269 | subject.create_install_msi_batch_on(winhost, test_path, test_opts) 270 | end 271 | 272 | it 'SCPs to & returns the same batch file path, corrected for slashes' do 273 | test_time = Time.now 274 | allow(Time).to receive(:new).and_return(test_time) 275 | timestamp = test_time.strftime('%Y-%m-%d_%H.%M.%S') 276 | 277 | correct_path = "#{tmp_slashes}\\install-puppet-msi-#{timestamp}.bat" 278 | expect(winhost).to receive(:do_scp_to).with(anything, correct_path, {}) 279 | test_path, = subject.create_install_msi_batch_on(winhost, msi_path, {}) 280 | expect(test_path).to be === correct_path 281 | end 282 | 283 | it 'returns & sends log_path to #msi_install_scripts, corrected for slashes' do 284 | allow(winhost).to receive(:do_scp_to) 285 | test_time = Time.now 286 | allow(Time).to receive(:new).and_return(test_time) 287 | timestamp = test_time.strftime('%Y-%m-%d_%H.%M.%S') 288 | 289 | correct_path = "#{tmp_slashes}\\install-puppet-#{timestamp}.log" 290 | expect(subject).to receive(:msi_install_script).with( 291 | anything, anything, correct_path 292 | ) 293 | _, log_path = subject.create_install_msi_batch_on(winhost, msi_path, {}) 294 | expect(log_path).to be === correct_path 295 | end 296 | end 297 | 298 | describe '#msi_install_script' do 299 | let(:log_path) { '/log/msi_install_script' } 300 | 301 | context 'msi_params parameter' do 302 | it 'can take an empty hash' do 303 | expected_cmd = %r{^start /w msiexec\.exe /i ".*" /qn /L\*V #{log_path}\ .exit}m 304 | expect(subject.msi_install_script(msi_path, {}, log_path)).to match(expected_cmd) 305 | end 306 | 307 | it 'uses a key-value pair correctly' do 308 | params = { 'tk1' => 'tv1' } 309 | expected_cmd = %r{^start /w msiexec\.exe /i ".*" /qn /L\*V #{log_path}\ tk1=tv1} 310 | expect(subject.msi_install_script(msi_path, params, log_path)).to match(expected_cmd) 311 | end 312 | 313 | it 'uses multiple key-value pairs correctly' do 314 | params = { 'tk1' => 'tv1', 'tk2' => 'tv2' } 315 | expected_cmd = %r{^start /w msiexec\.exe /i ".*" /qn /L\*V #{log_path}\ tk1=tv1\ tk2=tv2} 316 | expect(subject.msi_install_script(msi_path, params, log_path)).to match(expected_cmd) 317 | end 318 | end 319 | 320 | context 'msi_path parameter' do 321 | it 'will generate an appropriate command with a MSI file path using non-Windows slashes' do 322 | msi_path = 'c:/foo/puppet.msi' 323 | expected_cmd = %r{^start /w msiexec\.exe /i "c:\\foo\\puppet.msi" /qn /L\*V #{log_path}} 324 | expect(subject.msi_install_script(msi_path, {}, log_path)).to match(expected_cmd) 325 | end 326 | 327 | it 'will generate an appropriate command with a MSI http(s) url' do 328 | msi_url = 'https://downloads.puppetlabs.com/puppet.msi' 329 | expected_cmd = %r{^start /w msiexec\.exe /i "https://downloads\.puppetlabs\.com/puppet\.msi" /qn /L\*V #{log_path}} 330 | expect(subject.msi_install_script(msi_url, {}, log_path)).to match(expected_cmd) 331 | end 332 | 333 | it 'will generate an appropriate command with a MSI file url' do 334 | msi_url = 'file://c:\\foo\\puppet.msi' 335 | expected_cmd = %r{^start /w msiexec\.exe /i "file://c:\\foo\\puppet\.msi" /qn /L\*V #{log_path}} 336 | expect(subject.msi_install_script(msi_url, {}, log_path)).to match(expected_cmd) 337 | end 338 | end 339 | end 340 | end 341 | --------------------------------------------------------------------------------