├── data └── common.yaml ├── .yardopts ├── pdk.yaml ├── .rspec ├── .puppet-lint.rc ├── CODEOWNERS ├── .gitattributes ├── .vscode └── extensions.json ├── .gitpod.yml ├── .devcontainer ├── Dockerfile └── devcontainer.json ├── .github ├── workflows │ ├── nightly.yml │ ├── mend.yml │ ├── ci.yml │ ├── release_prep.yml │ └── release.yml └── pull_request_template.md ├── plans ├── teardown.pp ├── tests_against_agents.pp ├── tester.pp ├── provisioner.pp ├── server_setup.pp ├── agents.pp ├── puppetserver_setup.pp └── agents_setup.pp ├── tasks ├── fix_secure_path.sh ├── update_site_pp.json ├── fix_secure_path.json ├── update_node_pp.json ├── install_pe.json ├── install_puppetserver.json ├── run_tests.json ├── update_site_pp.rb ├── docker.json ├── docker_exp.json ├── run_tests.rb ├── abs.json ├── provision_service.json ├── update_node_pp.rb ├── install_pe.sh ├── lxd.json ├── vagrant.json ├── docker_exp.rb ├── install_puppetserver.sh ├── lxd.rb ├── provision_service.rb ├── abs.rb ├── docker.rb └── vagrant.rb ├── spec ├── default_facts.yml ├── spec_helper_local.rb ├── unit │ ├── task_helper_spec.rb │ ├── inventory_helper_spec.rb │ └── docker_helper_spec.rb ├── tasks │ ├── vagrant_spec.rb │ ├── abs_spec.rb │ ├── provision_service_spec.rb │ └── lxd_spec.rb ├── integration │ └── puppetcore_spec.rb └── spec_helper.rb ├── .fixtures.yml ├── Rakefile ├── .gitignore ├── .pdkignore ├── hiera.yaml ├── .sync.yml ├── .gitpod.Dockerfile ├── lib ├── docker_helper.rb ├── task_helper.rb └── inventory_helper.rb ├── metadata.json ├── .rubocop_todo.yml ├── Gemfile ├── CHANGELOG.md ├── LICENSE ├── REFERENCE.md ├── README.md └── .rubocop.yml /data/common.yaml: -------------------------------------------------------------------------------- 1 | --- {} 2 | -------------------------------------------------------------------------------- /.yardopts: -------------------------------------------------------------------------------- 1 | --markup markdown 2 | -------------------------------------------------------------------------------- /pdk.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | ignore: [] 3 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --color 2 | --format documentation 3 | -------------------------------------------------------------------------------- /.puppet-lint.rc: -------------------------------------------------------------------------------- 1 | --relative 2 | --no-140chars-check 3 | -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | # Setting ownership to the tooling team 2 | * @puppetlabs/devx 3 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.rb eol=lf 2 | *.erb eol=lf 3 | *.pp eol=lf 4 | *.sh eol=lf 5 | *.epp eol=lf 6 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "puppet.puppet-vscode", 4 | "rebornix.Ruby" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /.gitpod.yml: -------------------------------------------------------------------------------- 1 | image: 2 | file: .gitpod.Dockerfile 3 | 4 | tasks: 5 | - init: pdk bundle install 6 | 7 | vscode: 8 | extensions: 9 | - puppet.puppet-vscode@1.2.0:f5iEPbmOj6FoFTOV6q8LTg== 10 | -------------------------------------------------------------------------------- /.devcontainer/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM puppet/pdk:latest 2 | 3 | # [Optional] Uncomment this section to install additional packages. 4 | # RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \ 5 | # && apt-get -y install --no-install-recommends 6 | 7 | -------------------------------------------------------------------------------- /.github/workflows/nightly.yml: -------------------------------------------------------------------------------- 1 | name: "nightly" 2 | 3 | on: 4 | schedule: 5 | - cron: "0 0 * * *" 6 | workflow_dispatch: 7 | 8 | jobs: 9 | Spec: 10 | uses: "puppetlabs/cat-github-actions/.github/workflows/module_ci.yml@main" 11 | secrets: "inherit" 12 | -------------------------------------------------------------------------------- /plans/teardown.pp: -------------------------------------------------------------------------------- 1 | plan provision::teardown( 2 | ) { 3 | $all_nodes = get_targets('*') 4 | $all_node_names = $all_nodes.map |$n| { $n.name } 5 | $all_node_names.each |$node_name| { 6 | run_task('provision::abs', 'localhost', action=> 'tear_down', node_name=> $node_name) 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /tasks/fix_secure_path.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | pupt_path=${PT_path:-/opt/puppetlabs/bin} 4 | 5 | sed -i -r -e "/^\s*Defaults\s*secure_path\s*/ s#=+\"([^\"]+)\".*#=\"\1:${pupt_path}\"#" /etc/sudoers 6 | sed -i -r -e "/^\s*Defaults\s+secure_path/ s#=([^\"].*)#=\1:${pupt_path}#" /etc/sudoers 7 | 8 | exit 0 9 | -------------------------------------------------------------------------------- /spec/default_facts.yml: -------------------------------------------------------------------------------- 1 | # Use default_module_facts.yml for module specific facts. 2 | # 3 | # Facts specified here will override the values provided by rspec-puppet-facts. 4 | --- 5 | networking: 6 | ip: "172.16.254.254" 7 | ip6: "FE80:0000:0000:0000:AAAA:AAAA:AAAA" 8 | mac: "AA:AA:AA:AA:AA:AA" 9 | is_pe: false 10 | -------------------------------------------------------------------------------- /tasks/update_site_pp.json: -------------------------------------------------------------------------------- 1 | { 2 | "puppet_task_version": 1, 3 | "supports_noop": false, 4 | "description": "Updates the site.pp on a target", 5 | "parameters": { 6 | "manifest": { 7 | "description": "The manifest code", 8 | "type": "String[1]" 9 | } 10 | }, 11 | "private": true 12 | } 13 | -------------------------------------------------------------------------------- /.github/workflows/mend.yml: -------------------------------------------------------------------------------- 1 | name: "mend" 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - "main" 7 | schedule: 8 | - cron: "0 0 * * *" 9 | workflow_dispatch: 10 | 11 | jobs: 12 | 13 | mend: 14 | uses: "puppetlabs/cat-github-actions/.github/workflows/tooling_mend_ruby.yml@main" 15 | secrets: "inherit" 16 | -------------------------------------------------------------------------------- /.fixtures.yml: -------------------------------------------------------------------------------- 1 | fixtures: 2 | repositories: 3 | "facts": "https://github.com/puppetlabs/puppetlabs-facts.git" 4 | "puppet_conf": "https://github.com/puppetlabs/puppetlabs-puppet_conf.git" 5 | puppet_agent: 6 | repo: 'https://github.com/puppetlabs/puppetlabs-puppet_agent.git' 7 | ref: v4.13.0 8 | symlinks: 9 | "provision": "#{source_dir}" 10 | -------------------------------------------------------------------------------- /plans/tests_against_agents.pp: -------------------------------------------------------------------------------- 1 | plan provision::tests_against_agents( 2 | ) { 3 | # get agents ? 4 | $agents = get_targets('*').filter |$n| { $n.vars['role'] != 'pe' } 5 | 6 | # iterate over agents 7 | $agents.each |$sut| { 8 | # pass the hostname as the sut, as the task is run locally. 9 | run_task('provision::run_tests', 'localhost', sut => $sut.name) 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: "ci" 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - "main" 7 | workflow_dispatch: 8 | 9 | jobs: 10 | Spec: 11 | uses: "puppetlabs/cat-github-actions/.github/workflows/module_ci.yml@main" 12 | with: 13 | # This line enables shellcheck to be run on this repository 14 | run_shellcheck: true 15 | ruby_version: '3.1' 16 | secrets: "inherit" 17 | -------------------------------------------------------------------------------- /tasks/fix_secure_path.json: -------------------------------------------------------------------------------- 1 | { 2 | "puppet_task_version": 1, 3 | "supports_noop": false, 4 | "description": "Add puppet agent bin directory to sudo secure_path", 5 | "parameters": { 6 | "path": { 7 | "description": "Puppet agent bin directory path", 8 | "type": "Optional[String[1]]", 9 | "default": "/opt/puppetlabs/bin" 10 | } 11 | }, 12 | "private": true 13 | } 14 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'bundler' 4 | require 'puppet_litmus/rake_tasks' if Gem.loaded_specs.key? 'puppet_litmus' 5 | require 'puppetlabs_spec_helper/rake_tasks' 6 | require 'puppet-syntax/tasks/puppet-syntax' 7 | require 'puppet-strings/tasks' if Gem.loaded_specs.key? 'puppet-strings' 8 | 9 | PuppetLint.configuration.send('disable_relative') 10 | PuppetLint.configuration.send('disable_140chars') 11 | -------------------------------------------------------------------------------- /tasks/update_node_pp.json: -------------------------------------------------------------------------------- 1 | { 2 | "puppet_task_version": 1, 3 | "supports_noop": false, 4 | "description": "Creates a manifest file for a target node on pe server", 5 | "parameters": { 6 | "manifest": { 7 | "description": "The manifest code", 8 | "type": "String[1]" 9 | }, 10 | "target_node": { 11 | "description": "The target node", 12 | "type": "String[1]" 13 | } 14 | }, 15 | "private": true 16 | } 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .git/ 2 | .*.sw[op] 3 | .metadata 4 | .yardoc 5 | .yardwarns 6 | *.iml 7 | /.bundle/ 8 | /.idea/ 9 | /.vagrant/ 10 | /coverage/ 11 | /bin/ 12 | /doc/ 13 | /Gemfile.local 14 | /Gemfile.lock 15 | /junit/ 16 | /log/ 17 | /pkg/ 18 | /spec/fixtures/manifests/ 19 | /spec/fixtures/modules/* 20 | /tmp/ 21 | /vendor/ 22 | /convert_report.txt 23 | /update_report.txt 24 | .DS_Store 25 | .project 26 | .envrc 27 | /inventory.yaml 28 | /spec/fixtures/litmus_inventory.yaml 29 | -------------------------------------------------------------------------------- /.github/workflows/release_prep.yml: -------------------------------------------------------------------------------- 1 | name: "Release Prep" 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | version: 7 | description: "Version of module to be released. Must be a valid semver string. Ex: 1.0.0" 8 | required: true 9 | 10 | jobs: 11 | release_prep: 12 | uses: "puppetlabs/cat-github-actions/.github/workflows/module_release_prep.yml@main" 13 | with: 14 | version: "${{ github.event.inputs.version }}" 15 | secrets: "inherit" 16 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ## Summary 2 | Provide a detailed description of all the changes present in this pull request. 3 | 4 | ## Additional Context 5 | Add any additional context about the problem here. 6 | - [ ] Root cause and the steps to reproduce. (If applicable) 7 | - [ ] Thought process behind the implementation. 8 | 9 | ## Related Issues (if any) 10 | Mention any related issues or pull requests. 11 | 12 | ## Checklist 13 | - [ ] 🟢 Spec tests. 14 | - [ ] 🟢 Acceptance tests. 15 | - [ ] Manually verified. 16 | -------------------------------------------------------------------------------- /spec/spec_helper_local.rb: -------------------------------------------------------------------------------- 1 | require 'fileutils' 2 | 3 | RSpec.configure do |rspec| 4 | rspec.expect_with :rspec do |c| 5 | c.max_formatted_output_length = nil 6 | end 7 | end 8 | 9 | RSpec.shared_context('with tmpdir') do 10 | let(:tmpdir) { @tmpdir } # rubocop:disable RSpec/InstanceVariable 11 | 12 | around(:each) do |example| 13 | Dir.mktmpdir('rspec-provision_test') do |t| 14 | FileUtils.mkdir_p(File.join(t, 'spec', 'fixtures')) 15 | @tmpdir = t 16 | example.run 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /plans/tester.pp: -------------------------------------------------------------------------------- 1 | plan provision::tester( 2 | ) { 3 | # get pe_server ? 4 | $server = get_targets('*').filter |$n| { $n.vars['role'] == 'pe' } 5 | $agents = get_targets('*').filter |$n| { $n.vars['role'] != 'pe' } 6 | $agent_names = $agents.map |$n| { $n.name } 7 | 8 | $manifest = "class { 'motd':\ncontent => 'foomph\n'\n}" 9 | $agent_names.each |$agent_name| { 10 | run_task('provision::update_node_pp', $server, manifest => $manifest, target_node => $agent_name) 11 | } 12 | run_command('puppet agent -t', $agents) 13 | } 14 | -------------------------------------------------------------------------------- /tasks/install_pe.json: -------------------------------------------------------------------------------- 1 | { 2 | "puppet_task_version": 1, 3 | "supports_noop": false, 4 | "description": "Installs PE on a target", 5 | "parameters": { 6 | "version": { 7 | "description": "The release of PE you want to install e.g. 2018.1 (Default: 2019.2)", 8 | "type": "Optional[String[1]]" 9 | }, 10 | "os": { 11 | "description": "The os of PE you are installing e.g. ubuntu-22.04-amd64 (Default: el-7-x86_64)", 12 | "type": "Optional[String[1]]" 13 | } 14 | }, 15 | "private": true 16 | } 17 | -------------------------------------------------------------------------------- /plans/provisioner.pp: -------------------------------------------------------------------------------- 1 | plan provision::provisioner( 2 | ) { 3 | # bolt command run 'touch ./inventory.yaml' 4 | # provision server machine, set role 5 | run_task('provision::abs', 'localhost', action => 'provision', platform => 'centos-7-x86_64', vars => 'role: pe') 6 | # provision agents agent_linux agent_windows 7 | run_task('provision::abs', 'localhost', action => 'provision', platform => 'centos-6-x86_64', vars => 'role: agent_linux') 8 | run_task('provision::abs', 'localhost', action => 'provision', platform => 'win-2016-x86_64', vars => 'role: agent_windows') 9 | } 10 | -------------------------------------------------------------------------------- /tasks/install_puppetserver.json: -------------------------------------------------------------------------------- 1 | { 2 | "puppet_task_version": 1, 3 | "supports_noop": false, 4 | "description": "install puppetserver community edition", 5 | "parameters": { 6 | "collection": { 7 | "description": "The Puppet Server version", 8 | "type": "Optional[String[1]]" 9 | }, 10 | "platform": { 11 | "description": "The operating system and version", 12 | "type": "Optional[String[1]]" 13 | }, 14 | "retry": { 15 | "description": "The number of retries in case of network connectivity failures", 16 | "type": "Optional[Integer]" 17 | } 18 | }, 19 | "private": true 20 | } 21 | -------------------------------------------------------------------------------- /plans/server_setup.pp: -------------------------------------------------------------------------------- 1 | plan provision::server_setup( 2 | ) { 3 | # get pe-server from inventory file? eg https://puppet.com/docs/bolt/latest/writing_plans.html#collect-facts-from-the-targets 4 | $server = get_targets('*').filter |$n| { $n.vars['role'] == 'pe' } 5 | # install pe server 6 | run_task('provision::install_pe', $server) 7 | 8 | # install modules 9 | run_command('puppet module install puppetlabs-motd', $server) 10 | # update site on server 11 | $manifest = 'include motd' 12 | # run_task('provision::update_site_pp', $server, manifest => $manifest) 13 | # set the ui password 14 | run_command('puppet infra console_password --password=litmus', $server) 15 | } 16 | -------------------------------------------------------------------------------- /tasks/run_tests.json: -------------------------------------------------------------------------------- 1 | { 2 | "puppet_task_version": 1, 3 | "supports_noop": false, 4 | "description": "Run rspec tests against a target machine", 5 | "parameters": { 6 | "sut": { 7 | "description": "The target SUT to run tests against", 8 | "type": "String[1]" 9 | }, 10 | "test_path": { 11 | "description": "Location of the test files. Defaults to './spec/acceptance'", 12 | "type": "Optional[String[1]]" 13 | }, 14 | "format": { 15 | "description": "", 16 | "type": "Enum[progress, documentation]", 17 | "default": "progress" 18 | } 19 | }, 20 | "files": [ 21 | "provision/lib/task_helper.rb" 22 | ], 23 | "private": true 24 | } 25 | -------------------------------------------------------------------------------- /.pdkignore: -------------------------------------------------------------------------------- 1 | .git/ 2 | .*.sw[op] 3 | .metadata 4 | .yardoc 5 | .yardwarns 6 | *.iml 7 | /.bundle/ 8 | /.idea/ 9 | /.vagrant/ 10 | /coverage/ 11 | /bin/ 12 | /doc/ 13 | /Gemfile.local 14 | /Gemfile.lock 15 | /junit/ 16 | /log/ 17 | /pkg/ 18 | /spec/fixtures/manifests/ 19 | /spec/fixtures/modules/* 20 | /tmp/ 21 | /vendor/ 22 | /convert_report.txt 23 | /update_report.txt 24 | .DS_Store 25 | .project 26 | .envrc 27 | /inventory.yaml 28 | /spec/fixtures/litmus_inventory.yaml 29 | /.fixtures.yml 30 | /Gemfile 31 | /.gitattributes 32 | /.github/ 33 | /.gitignore 34 | /.pdkignore 35 | /.puppet-lint.rc 36 | /Rakefile 37 | /rakelib/ 38 | /.rspec 39 | /..yml 40 | /.yardopts 41 | /spec/ 42 | /.vscode/ 43 | /.sync.yml 44 | /.devcontainer/ 45 | -------------------------------------------------------------------------------- /hiera.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | version: 5 3 | 4 | defaults: # Used for any hierarchy level that omits these keys. 5 | datadir: data # This path is relative to hiera.yaml's directory. 6 | data_hash: yaml_data # Use the built-in YAML backend. 7 | 8 | hierarchy: 9 | - name: "osfamily/major release" 10 | paths: 11 | # Used to distinguish between Debian and Ubuntu 12 | - "os/%{facts.os.name}/%{facts.os.release.major}.yaml" 13 | - "os/%{facts.os.family}/%{facts.os.release.major}.yaml" 14 | # Used for Solaris 15 | - "os/%{facts.os.family}/%{facts.kernelrelease}.yaml" 16 | - name: "osfamily" 17 | paths: 18 | - "os/%{facts.os.name}.yaml" 19 | - "os/%{facts.os.family}.yaml" 20 | - name: 'common' 21 | path: 'common.yaml' 22 | -------------------------------------------------------------------------------- /tasks/update_site_pp.rb: -------------------------------------------------------------------------------- 1 | #!/opt/puppetlabs/puppet/bin/ruby 2 | # frozen_string_literal: true 3 | 4 | require 'json' 5 | require 'open3' 6 | require 'puppet' 7 | 8 | def update_file(manifest) 9 | path = '/etc/puppetlabs/code/environments/production/manifests' 10 | _stdout, stderr, status = Open3.capture3("mkdir -p #{path}") 11 | raise Puppet::Error, "stderr: ' %{stderr}')" % ({ stderr: }) if status != 0 12 | 13 | site_path = File.join(path, 'site.pp') 14 | File.open(site_path, 'w+') { |f| f.write(manifest) } 15 | 'site.pp updated' 16 | end 17 | 18 | params = JSON.parse($stdin.read) 19 | manifest = params['manifest'] 20 | 21 | begin 22 | result = update_file(manifest) 23 | puts result.to_json 24 | exit 0 25 | rescue Puppet::Error => e 26 | puts({ status: 'failure', error: e.message }.to_json) 27 | exit 1 28 | end 29 | -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | // For format details, see https://aka.ms/devcontainer.json. For config options, see the README at: 2 | // https://github.com/microsoft/vscode-dev-containers/tree/v0.140.1/containers/puppet 3 | { 4 | "name": "Puppet Development Kit (Community)", 5 | "dockerFile": "Dockerfile", 6 | 7 | // Set *default* container specific settings.json values on container create. 8 | "settings": { 9 | "terminal.integrated.shell.linux": "/bin/bash" 10 | }, 11 | 12 | // Add the IDs of extensions you want installed when the container is created. 13 | "extensions": [ 14 | "puppet.puppet-vscode", 15 | "rebornix.Ruby" 16 | ] 17 | 18 | // Use 'forwardPorts' to make a list of ports inside the container available locally. 19 | // "forwardPorts": [], 20 | 21 | // Use 'postCreateCommand' to run commands after the container is created. 22 | // "postCreateCommand": "pdk --version", 23 | } 24 | -------------------------------------------------------------------------------- /plans/agents.pp: -------------------------------------------------------------------------------- 1 | plan provision::agents( 2 | ) { 3 | # get pe_server ? 4 | $server = get_targets('*').filter |$n| { $n.vars['role'] == 'pe' } 5 | # get agents ? 6 | $agents = get_targets('*').filter |$n| { $n.vars['role'] != 'pe' } 7 | $windows_agents = get_targets('*').filter |$n| { $n.vars['role'] == 'agent_windows' } 8 | 9 | # install agents 10 | run_task('puppet_agent::install', $agents) 11 | # set the server 12 | $server_string = $server[0].name 13 | run_task('puppet_conf', $agents, action => 'set', section => 'main', setting => 'server', value => $server_string) 14 | run_command("powershell.exe -NoProfile -Nologo -Command 'Remove-Item -Path /ProgramData/PuppetLabs/puppet/etc/ssl -Force -Recurse'", $windows_agents, '_catch_errors' => true) 15 | # rm -rf /etc/puppetlabs/puppet/ssl 16 | # run agent -t 17 | run_command('puppet agent -t', $agents, '_catch_errors' => true) 18 | } 19 | -------------------------------------------------------------------------------- /tasks/docker.json: -------------------------------------------------------------------------------- 1 | { 2 | "puppet_task_version": 1, 3 | "supports_noop": false, 4 | "description": "Provision/Tear down a machine on docker", 5 | "parameters": { 6 | "action": { 7 | "description": "Action to perform, tear_down or provision", 8 | "type": "Enum[provision, tear_down]", 9 | "default": "provision" 10 | }, 11 | "inventory": { 12 | "description": "Location of the inventory file", 13 | "type": "Optional[String[1]]" 14 | }, 15 | "node_name": { 16 | "description": "The name of the node", 17 | "type": "Optional[String[1]]" 18 | }, 19 | "platform": { 20 | "description": "Platform to provision, eg ubuntu:14.04", 21 | "type": "Optional[String[1]]" 22 | }, 23 | "vars": { 24 | "description": "YAML string of key/value pairs to add to the inventory vars section", 25 | "type": "Optional[String[1]]" 26 | } 27 | }, 28 | "files": [ 29 | "provision/lib/task_helper.rb", 30 | "provision/lib/docker_helper.rb", 31 | "provision/lib/inventory_helper.rb" 32 | ] 33 | } 34 | -------------------------------------------------------------------------------- /tasks/docker_exp.json: -------------------------------------------------------------------------------- 1 | { 2 | "puppet_task_version": 1, 3 | "supports_noop": false, 4 | "description": "Provision/Tear down a machine on docker", 5 | "parameters": { 6 | "action": { 7 | "description": "Action to perform, tear_down or provision", 8 | "type": "Enum[provision, tear_down]", 9 | "default": "provision" 10 | }, 11 | "inventory": { 12 | "description": "Location of the inventory file", 13 | "type": "Optional[String[1]]" 14 | }, 15 | "node_name": { 16 | "description": "The name of the node", 17 | "type": "Optional[String[1]]" 18 | }, 19 | "platform": { 20 | "description": "Platform to provision, eg ubuntu:14.04", 21 | "type": "Optional[String[1]]" 22 | }, 23 | "vars": { 24 | "description": "YAML string of key/value pairs to add to the inventory vars section", 25 | "type": "Optional[String[1]]" 26 | } 27 | }, 28 | "files": [ 29 | "provision/lib/task_helper.rb", 30 | "provision/lib/docker_helper.rb", 31 | "provision/lib/inventory_helper.rb" 32 | ] 33 | } 34 | -------------------------------------------------------------------------------- /tasks/run_tests.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | require 'puppet_litmus' 5 | require_relative '../lib/task_helper' 6 | 7 | def run_tests(sut, test_path, format) 8 | test = "bundle exec rspec #{test_path} --format #{format}" 9 | options = { 10 | env: { 11 | 'TARGET_HOST' => sut 12 | } 13 | } 14 | env = options[:env].nil? ? {} : options[:env] 15 | stdout, stderr, status = Open3.capture3(env, test) 16 | raise "status: 'not ok'\n stdout: #{stdout}\n stderr: #{stderr}" unless status.to_i.zero? 17 | 18 | { status: 'ok', result: stdout } 19 | end 20 | 21 | params = JSON.parse($stdin.read) 22 | sut = params['sut'] 23 | test_path = if params['test_path'].nil? 24 | './spec/acceptance/' 25 | else 26 | params['test_path'] 27 | end 28 | format = params['format'] 29 | 30 | begin 31 | result = run_tests(sut, test_path, format) 32 | puts result.to_json 33 | exit 0 34 | rescue StandardError => e 35 | puts({ _error: { kind: 'run_tests/failure', msg: e.message } }.to_json) 36 | exit 1 37 | end 38 | -------------------------------------------------------------------------------- /tasks/abs.json: -------------------------------------------------------------------------------- 1 | { 2 | "puppet_task_version": 1, 3 | "supports_noop": false, 4 | "description": "Provision/Tear down a machine using abs", 5 | "parameters": { 6 | "action": { 7 | "description": "Action to perform, tear_down or provision", 8 | "type": "Enum[provision, tear_down]", 9 | "default": "provision" 10 | }, 11 | "inventory": { 12 | "description": "Location of the inventory file", 13 | "type": "Optional[String[1]]" 14 | }, 15 | "node_name": { 16 | "description": "The name of the node", 17 | "type": "Optional[String[1]]" 18 | }, 19 | "platform": { 20 | "description": "Provision a single platform or a Hash of platforms specifying the number of instances. eg 'ubuntu-1604-x86_64 or '{ \"centos-7-x86_64\":1, \"centos-6-x86_64\":2 }'", 21 | "type": "Optional[Variant[String[1],Hash]]" 22 | }, 23 | "vars": { 24 | "description": "key/value pairs to add to the vars section", 25 | "type": "Optional[String[1]]" 26 | } 27 | }, 28 | "files": [ 29 | "provision/lib/task_helper.rb", 30 | "provision/lib/inventory_helper.rb" 31 | ] 32 | } 33 | -------------------------------------------------------------------------------- /tasks/provision_service.json: -------------------------------------------------------------------------------- 1 | { 2 | "puppet_task_version": 1, 3 | "supports_noop": false, 4 | "description": "Provision/Tear down a list of machines using the provisioning service", 5 | "parameters": { 6 | "action": { 7 | "description": "Action to perform, tear_down or provision", 8 | "type": "Enum[provision, tear_down]", 9 | "default": "provision" 10 | }, 11 | "platform": { 12 | "description": "Needed by litmus", 13 | "type": "Optional[String[1]]" 14 | }, 15 | "node_name": { 16 | "description": "Needed by litmus", 17 | "type": "Optional[String[1]]" 18 | }, 19 | "inventory": { 20 | "description": "Location of the inventory file", 21 | "type": "Optional[String[1]]" 22 | }, 23 | "vars": { 24 | "description": "The address of the provisioning service", 25 | "type": "Optional[String[1]]" 26 | }, 27 | "retry_attempts": { 28 | "description": "The number of times to retry the provisioning if it fails", 29 | "type": "Optional[Integer[1]]", 30 | "default": 5 31 | } 32 | }, 33 | "files": [ 34 | "provision/lib/task_helper.rb", 35 | "provision/lib/inventory_helper.rb" 36 | ] 37 | } 38 | -------------------------------------------------------------------------------- /spec/unit/task_helper_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'task_helper' 4 | 5 | describe 'Utility Functions' do 6 | describe '.platform_is_windows?' do 7 | it 'correctly identifies Windows platforms' do 8 | expect(platform_is_windows?('somewinorg/blah-windows-2019')).to be_truthy 9 | expect(platform_is_windows?('somewinorg/blah-WinDows-2019')).to be_truthy 10 | expect(platform_is_windows?('myorg/some_image:windows-server')).to be_truthy 11 | expect(platform_is_windows?('myorg/some_image:win-server-2008')).to be_truthy 12 | expect(platform_is_windows?('myorg/win-2k8r2')).to be_truthy 13 | expect(platform_is_windows?('myorg/windows-server')).to be_truthy 14 | expect(platform_is_windows?('windows-server')).to be_truthy 15 | expect(platform_is_windows?('win-2008')).to be_truthy 16 | expect(platform_is_windows?('webserserver-windows-2008')).to be_truthy 17 | expect(platform_is_windows?('webserver-win-2008')).to be_truthy 18 | expect(platform_is_windows?('myorg/winderping')).to be_falsey 19 | expect(platform_is_windows?('2012r2')).to be_falsey 20 | expect(platform_is_windows?('redhat8')).to be_falsey 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /.sync.yml: -------------------------------------------------------------------------------- 1 | --- 2 | ".gitlab-ci.yml": 3 | delete: true 4 | ".travis.yml": 5 | simplecov: true 6 | notifications: 7 | slack: 8 | secure: >- 9 | EYcrPlgOclhyT2cdNZpSyQOZiZvgj0WYDzD0dq248cpvoAlL/5GPXpttTaAy9k1pjWoGh5azNIn+hYGmuTMxMuAT2lHwDEQotXqrQVbphynMABu45GBiuAfO6uZPFgcQtDtKaax2TjEG8sK7xJw8FdhBo+YMpxSJHtgV7IjmYs0aU89gG4BGUSgULFf4kswPfu8IxNvgXUTipzeRQUg920arjXFSsjkiefirk71NUpzS6vR7Q4DXjtUKl9jWdu5soouTzwb4J11TfZz2RvN9lZw0fzDjVdWcye5SpRsnCvXKaNjoGyVWBMY2Zo1ThlrFHR1MECyyX8lwsSMz0Uyl6rhfF35W5Fzxo5M+U9d0ANifE9Ttcmxt8a8AYpaxxgWYwVRIzMequPBGHluv+52oQ5jrOsYduxdwagyuYdgBieNQrE3NTWTos9N5xk+QCVOBoKrwVqWKvA8ldduP0doXpQllHw6uI2KEI2eUvh2Tohzu2vnuurXEocJtm9H21qqQvthMAFPCNVlzw3rDaCax3Q65raWXq160riGp42B9rs/IBdlgXkbQpVvyh9/HMExv/3oWQMJqKGpSKNAfaTjpQ4D6CGAFWi63L+nDyENGMdFZ3cyEGc+Kmz3KOszJqKE3mK3r6SECCq+zgPLKz3K73ovx7u2yUMtLS5ebYKwn94w= 10 | appveyor.yml: 11 | delete: true 12 | Gemfile: 13 | optional: 14 | ":development": 15 | - gem: github_changelog_generator 16 | version: '= 1.15.2' 17 | - gem: webmock 18 | Rakefile: 19 | changelog_project: provision 20 | default_disabled_lint_checks: 21 | - 140chars 22 | spec/spec_helper.rb: 23 | coverage_report: true 24 | .gitpod.Dockerfile: 25 | unmanaged: false 26 | .gitpod.yml: 27 | unmanaged: false 28 | -------------------------------------------------------------------------------- /.gitpod.Dockerfile: -------------------------------------------------------------------------------- 1 | FROM gitpod/workspace-full 2 | RUN sudo wget https://apt.puppet.com/puppet-tools-release-bionic.deb && \ 3 | wget https://apt.puppetlabs.com/puppet6-release-bionic.deb && \ 4 | sudo dpkg -i puppet6-release-bionic.deb && \ 5 | sudo dpkg -i puppet-tools-release-bionic.deb && \ 6 | sudo apt-get update && \ 7 | sudo apt-get install -y pdk zsh puppet-agent && \ 8 | sudo apt-get clean && \ 9 | sudo rm -rf /var/lib/apt/lists/* 10 | RUN sudo usermod -s $(which zsh) gitpod && \ 11 | sh -c "$(curl -fsSL https://raw.github.com/ohmyzsh/ohmyzsh/master/tools/install.sh)" && \ 12 | echo "plugins=(git gitignore github gem pip bundler python ruby docker docker-compose)" >> /home/gitpod/.zshrc && \ 13 | echo 'PATH="$PATH:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/opt/puppetlabs/bin:/opt/puppetlabs/puppet/bin"' >> /home/gitpod/.zshrc && \ 14 | sudo /opt/puppetlabs/puppet/bin/gem install puppet-debugger hub -N && \ 15 | mkdir -p /home/gitpod/.config/puppet && \ 16 | /opt/puppetlabs/puppet/bin/ruby -r yaml -e "puts ({'disabled' => true}).to_yaml" > /home/gitpod/.config/puppet/analytics.yml 17 | RUN rm -f puppet6-release-bionic.deb puppet-tools-release-bionic.deb 18 | ENTRYPOINT /usr/bin/zsh 19 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: "Module Release" 2 | 3 | on: 4 | workflow_dispatch: 5 | 6 | jobs: 7 | release: 8 | name: "Release" 9 | runs-on: "ubuntu-latest" 10 | if: github.repository_owner == 'puppetlabs' 11 | 12 | steps: 13 | 14 | - name: "Checkout" 15 | uses: "actions/checkout@v3" 16 | with: 17 | ref: "${{ github.ref }}" 18 | clean: true 19 | fetch-depth: 0 20 | 21 | - name: "Get version" 22 | id: "get_version" 23 | run: | 24 | echo "version=$(jq --raw-output .version metadata.json)" >> $GITHUB_OUTPUT 25 | 26 | - name: "PDK build" 27 | uses: "docker://puppet/pdk:nightly" 28 | with: 29 | args: "build" 30 | 31 | - name: "Generate release notes" 32 | run: | 33 | export GH_HOST=github.com 34 | gh extension install chelnak/gh-changelog 35 | gh changelog get --latest > OUTPUT.md 36 | env: 37 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 38 | 39 | - name: "Create release" 40 | run: | 41 | gh release create v${{ steps.get_version.outputs.version }} --title v${{ steps.get_version.outputs.version }} -F OUTPUT.md 42 | env: 43 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 44 | -------------------------------------------------------------------------------- /spec/tasks/vagrant_spec.rb: -------------------------------------------------------------------------------- 1 | require 'json' 2 | require 'rspec' 3 | require 'spec_helper' 4 | require 'net/ssh' 5 | require_relative '../../tasks/vagrant' 6 | 7 | describe 'vagrant' do 8 | let(:provider) { 'virtualbox' } 9 | let(:platform) { 'generic/debian10' } 10 | 11 | include_context('with tmpdir') 12 | 13 | before(:each) do 14 | # Stub $stdin.read to return a predefined JSON string 15 | allow($stdin).to receive(:read).and_return({ 16 | platform:, 17 | action: 'provision', 18 | vars: 'role: worker1', 19 | inventory: tmpdir, 20 | enable_synced_folder: 'true', 21 | provider:, 22 | hyperv_vswitch: 'hyperv_vswitch', 23 | hyperv_smb_username: 'hyperv_smb_username' 24 | }.to_json) 25 | allow(Open3).to receive(:capture3).with(%r{vagrant up --provider #{provider}}, any_args).and_return(['', '', 0]).once 26 | allow(File).to receive(:read).with(%r{#{tmpdir}.*\.vagrant}).and_return('some_unique_id') 27 | allow(Open3).to receive(:capture3).with(%r{vagrant ssh-config}, any_args).and_return(['', '', 0]).once 28 | allow(Net::SSH).to receive(:start).and_return(true) 29 | end 30 | 31 | it 'provisions a new vagrant box when action is provision' do 32 | expect { vagrant }.to raise_error(SystemExit).and output( 33 | include('"status":"ok"', '"platform":"generic/debian10"', '"role":"worker1"'), 34 | ).to_stdout 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /tasks/update_node_pp.rb: -------------------------------------------------------------------------------- 1 | #!/opt/puppetlabs/puppet/bin/ruby 2 | # frozen_string_literal: true 3 | 4 | require 'json' 5 | require 'open3' 6 | require 'puppet' 7 | # see https://github.com/DavidS/dasz-configuration/blob/main/manifests/nodes/backup.dasz.at.pp 8 | # see https://github.com/DavidS/dasz-configuration/blob/main/manifests/site.pp 9 | def update_file(manifest, target_node) 10 | path = '/etc/puppetlabs/code/environments/production/manifests/nodes' 11 | _stdout, stderr, status = Open3.capture3("mkdir -p #{path}") 12 | raise Puppet::Error, _("stderr: ' %{stderr}')" % { stderr: }) if status != 0 13 | 14 | site_path = File.join(path, "#{target_node}.pp") 15 | if File.file?(site_path) 16 | existing_manifest = File.readlines(site_path) 17 | # remove the last bracket in the manifest 18 | existing_manifest.pop 19 | existing_manifest.push("\n #{manifest}\n}") 20 | final_manifest = existing_manifest.join 21 | else 22 | final_manifest = "node '#{target_node}' \n{\n #{manifest} \n}" 23 | end 24 | File.open(site_path, 'w+') { |f| f.write(final_manifest) } 25 | "#{site_path} updated" 26 | end 27 | 28 | params = JSON.parse($stdin.read) 29 | manifest = params['manifest'] 30 | target_node = params['target_node'] 31 | 32 | begin 33 | result = update_file(manifest, target_node) 34 | puts result.to_json 35 | exit 0 36 | rescue Puppet::Error => e 37 | puts({ status: 'failure', error: e.message }.to_json) 38 | exit 1 39 | end 40 | -------------------------------------------------------------------------------- /tasks/install_pe.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | if [ -z ${PT_version+x} ]; then 4 | PE_RELEASE=2019.8 5 | 6 | else 7 | PE_RELEASE=$PT_version 8 | fi 9 | 10 | if [ -z ${PT_os+x} ]; then 11 | PE_OS=el-7-x86_64 12 | 13 | else 14 | PE_OS=$PT_os 15 | fi 16 | 17 | PE_LATEST=$(curl https://artifactory.delivery.puppetlabs.net/artifactory/generic_enterprise__local/"${PE_RELEASE}"/ci-ready/LATEST) 18 | PE_FILE_NAME=puppet-enterprise-${PE_LATEST}-${PE_OS} 19 | TAR_FILE=${PE_FILE_NAME}.tar 20 | DOWNLOAD_URL=https://artifactory.delivery.puppetlabs.net/artifactory/generic_enterprise__local/${PE_RELEASE}/ci-ready/${TAR_FILE} 21 | 22 | ## Download PE 23 | if ! curl -o "${TAR_FILE}" "${DOWNLOAD_URL}" ; then 24 | echo "Error: failed to download [${DOWNLOAD_URL}]" 25 | exit 2 26 | fi 27 | 28 | ## Install PE 29 | if ! tar xvf "${TAR_FILE}" ; then 30 | echo "Error: Failed to untar [${TAR_FILE}]" 31 | exit 2 32 | fi 33 | 34 | cd "${PE_FILE_NAME}" || exit 1 35 | if ! DISABLE_ANALYTICS=1 ./puppet-enterprise-installer -y -c ./conf.d/pe.conf ; then 36 | echo "Error: Failed to install Puppet Enterprise. Please check the logs and call Bryan.x" 37 | exit 2 38 | fi 39 | 40 | ## Finalize configuration 41 | echo "Finalize PE install" 42 | puppet agent -t 43 | # if [[ $? -ne 0 ]];then 44 | # echo “Error: Agent run failed. Check the logs above...” 45 | # exit 2 46 | # fi 47 | 48 | ## Create and configure Certs 49 | echo "autosign = true" >> /etc/puppetlabs/puppet/puppet.conf 50 | 51 | service pe-puppetserver restart 52 | -------------------------------------------------------------------------------- /spec/integration/puppetcore_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | require 'bundler' 5 | 6 | RSpec.describe 'Gemfile.lock verification' do 7 | let(:parser) { Bundler::LockfileParser.new(Bundler.read_file(Bundler.default_lockfile)) } 8 | let(:private_source) { 'https://rubygems-puppetcore.puppet.com/' } 9 | 10 | # Helper method to get source remotes for a specific gem 11 | def get_gem_source_remotes(gem_name) 12 | spec = parser.specs.find { |s| s.name == gem_name } 13 | return [] unless spec 14 | 15 | source = spec.source 16 | return [] unless source.is_a?(Bundler::Source::Rubygems) 17 | 18 | source.remotes.map(&:to_s) 19 | end 20 | 21 | context 'when the environment is configured with a valid PUPPET_FORGE_TOKEN' do 22 | it 'returns puppet from puppetcore' do 23 | remotes = get_gem_source_remotes('puppet') 24 | expect(remotes).to eq([private_source]), 25 | "Expected puppet to come from puppetcore, got: #{remotes.join(', ')}" 26 | end 27 | 28 | it 'returns facter from puppetcore' do 29 | remotes = get_gem_source_remotes('facter') 30 | expect(remotes).to eq([private_source]), 31 | "Expected facter to come from puppetcore, got: #{remotes.join(', ')}" 32 | end 33 | 34 | it 'has PUPPET_FORGE_TOKEN set' do 35 | expect(ENV.fetch('PUPPET_FORGE_TOKEN', nil)).not_to be_nil, 36 | 'Expected PUPPET_FORGE_TOKEN to be set' 37 | end 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /plans/puppetserver_setup.pp: -------------------------------------------------------------------------------- 1 | plan provision::puppetserver_setup( 2 | Optional[String] $collection = 'puppet7' 3 | ) { 4 | # get server 5 | $server = get_targets('*').filter |$node| { $node.vars['role'] == 'server' } 6 | 7 | # get facts 8 | $puppetserver_facts = facts($server[0]) 9 | $platform = $puppetserver_facts['platform'] 10 | 11 | # install puppetserver and start on master 12 | run_task( 13 | 'provision::install_puppetserver', 14 | $server, 15 | 'install and configure server', 16 | { 'collection' => $collection, 'platform' => $platform } 17 | ) 18 | 19 | $os_name = $puppetserver_facts['provisioner'] ? { 20 | 'docker' => split($puppetserver_facts['platform'], Regexp['[/:-]'])[1], 21 | 'docker_exp' => split($puppetserver_facts['platform'], Regexp['[/:-]'])[1], 22 | default => split($puppetserver_facts['platform'], Regexp['[/:-]'])[0] 23 | } 24 | 25 | $os_family = $os_name ? { 26 | /(^redhat|rhel|centos|scientific|oraclelinux)/ => 'redhat', 27 | /(^debian|ubuntu)/ => 'debian', 28 | default => 'unsupported' 29 | } 30 | 31 | if $os_family == 'unsupported' { 32 | fail_plan('Not supported platform!') 33 | } 34 | 35 | if $os_family == 'debian' { 36 | run_task('provision::fix_secure_path', $server, 'fix secure path') 37 | } 38 | 39 | $fqdn = run_command('facter fqdn', $server).to_data[0]['value']['stdout'] 40 | run_task('puppet_conf', $server, action => 'set', section => 'main', setting => 'server', value => $fqdn) 41 | 42 | run_command('systemctl start puppetserver', $server, '_catch_errors' => true) 43 | run_command('systemctl enable puppetserver', $server, '_catch_errors' => true) 44 | } 45 | -------------------------------------------------------------------------------- /lib/docker_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'json' 4 | 5 | def docker_exec(container_id, command) 6 | run_local_command("docker exec #{container_id} #{command}") 7 | end 8 | 9 | def docker_image_os_release_facts(image) 10 | os_release_facts = {} 11 | begin 12 | os_release = run_local_command("docker run --rm #{image} cat /etc/os-release") 13 | # The or-release file is a newline-separated list of environment-like 14 | # shell-compatible variable assignments. 15 | re = '^(.+)=(.+)' 16 | os_release.each_line do |line| 17 | line = line.strip || line 18 | next if line.nil? || line.empty? 19 | 20 | _, key, value = line.match(re).to_a 21 | # The values seems to be quoted most of the time, however debian only quotes 22 | # some of the values :/. Parse it, as if it was a JSON string. 23 | value = JSON.parse(value) unless value[0] != '"' 24 | os_release_facts[key] = value 25 | end 26 | rescue StandardError 27 | # fall through to parsing the id and version from the image if it doesn't have `/etc/os-release` 28 | id, version_id = image.split(':') 29 | id = id.sub(%r{/}, '_') 30 | os_release_facts['ID'] = id 31 | os_release_facts['VERSION_ID'] = version_id 32 | end 33 | os_release_facts 34 | end 35 | 36 | def docker_tear_down(container_id) 37 | run_local_command("docker rm -f #{container_id}") 38 | puts "Removed #{container_id}" 39 | { status: 'ok' } 40 | end 41 | 42 | # Workaround for fixing the bash message in stderr when tty is missing 43 | def docker_fix_missing_tty_error_message(container_id) 44 | system("docker exec #{container_id} sed -i 's/^mesg n/tty -s \\&\\& mesg n/g' /root/.profile") 45 | end 46 | -------------------------------------------------------------------------------- /metadata.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "puppetlabs-provision", 3 | "version": "3.0.1", 4 | "author": "puppetlabs", 5 | "summary": "Provisions and tears down containers / vms / machines through tasks.", 6 | "license": "Apache-2.0", 7 | "source": "https://github.com/puppetlabs/provision/", 8 | "project_page": "https://github.com/puppetlabs/provision/", 9 | "issues_url": "https://github.com/puppetlabs/provision/issues", 10 | "dependencies": [ 11 | 12 | ], 13 | "operatingsystem_support": [ 14 | { 15 | "operatingsystem": "CentOS", 16 | "operatingsystemrelease": [ 17 | "7", 18 | "8" 19 | ] 20 | }, 21 | { 22 | "operatingsystem": "OracleLinux", 23 | "operatingsystemrelease": [ 24 | "7" 25 | ] 26 | }, 27 | { 28 | "operatingsystem": "RedHat", 29 | "operatingsystemrelease": [ 30 | "7", 31 | "8", 32 | "9" 33 | ] 34 | }, 35 | { 36 | "operatingsystem": "Scientific", 37 | "operatingsystemrelease": [ 38 | "7" 39 | ] 40 | }, 41 | { 42 | "operatingsystem": "Debian", 43 | "operatingsystemrelease": [ 44 | "10", 45 | "11" 46 | ] 47 | }, 48 | { 49 | "operatingsystem": "Ubuntu", 50 | "operatingsystemrelease": [ 51 | "18.04", 52 | "20.04", 53 | "22.04" 54 | ] 55 | }, 56 | { 57 | "operatingsystem": "windows", 58 | "operatingsystemrelease": [ 59 | "10", 60 | "2012 R2", 61 | "2016", 62 | "2019", 63 | "2022" 64 | ] 65 | } 66 | ], 67 | "requirements": [ 68 | { 69 | "name": "puppet", 70 | "version_requirement": ">= 8.0.0 < 9.0.0" 71 | } 72 | ], 73 | "pdk-version": "3.0.1", 74 | "template-url": "https://github.com/puppetlabs/pdk-templates.git#main", 75 | "template-ref": "heads/main-0-g553bd53" 76 | } 77 | -------------------------------------------------------------------------------- /tasks/lxd.json: -------------------------------------------------------------------------------- 1 | { 2 | "puppet_task_version": 1, 3 | "supports_noop": false, 4 | "description": "Provision/Tear down an instance on LXD", 5 | "parameters": { 6 | "action": { 7 | "description": "Action to perform, tear_down or provision", 8 | "type": "Enum[provision, tear_down]", 9 | "default": "provision" 10 | }, 11 | "inventory": { 12 | "description": "Location of the inventory file", 13 | "type": "Optional[String[1]]" 14 | }, 15 | "node_name": { 16 | "description": "The name of the instance", 17 | "type": "Optional[String[1]]" 18 | }, 19 | "platform": { 20 | "description": "LXD image to use, eg images:ubuntu/22.04", 21 | "type": "Optional[String[1]]" 22 | }, 23 | "profiles": { 24 | "description": "LXD Profiles to apply", 25 | "type": "Optional[Array[String[1]]]" 26 | }, 27 | "storage": { 28 | "description": "LXD Storage pool name", 29 | "type": "Optional[String[1]]" 30 | }, 31 | "instance_type": { 32 | "description": "LXD Instance type", 33 | "type": "Optional[String[1]]" 34 | }, 35 | "vm": { 36 | "description": "Provision as a virtual-machine instead of a container", 37 | "type": "Optional[Boolean]" 38 | }, 39 | "remote": { 40 | "description": "LXD remote, defaults to the LXD client configured default remote", 41 | "type": "Optional[String]" 42 | }, 43 | "retries": { 44 | "description": "On provision check the instance is accepting commands, will be deleted if retries exceeded, 0 to disable", 45 | "type": "Integer", 46 | "default": 5 47 | }, 48 | "vars": { 49 | "description": "YAML string of key/value pairs to add to the inventory vars section", 50 | "type": "Optional[String[1]]" 51 | } 52 | }, 53 | "files": [ 54 | "provision/lib/task_helper.rb", 55 | "provision/lib/inventory_helper.rb" 56 | ] 57 | } 58 | -------------------------------------------------------------------------------- /plans/agents_setup.pp: -------------------------------------------------------------------------------- 1 | plan provision::agents_setup( 2 | Optional[String] $collection = 'puppet7' 3 | ) { 4 | # get pe_server ? 5 | $server = get_targets('*').filter |$n| { $n.vars['role'] == 'server' } 6 | 7 | # get agents ? 8 | $agents = get_targets('*').filter |$n| { $n.vars['role'] != 'server' } 9 | 10 | # install agents 11 | run_task('puppet_agent::install', $agents, { 'collection' => $collection }) 12 | 13 | # set the server 14 | $server_fqdn = run_command('facter fqdn', $server).to_data[0]['value']['stdout'] 15 | run_task('puppet_conf', $agents, action => 'set', section => 'main', setting => 'server', value => $server_fqdn) 16 | 17 | $agents.each |$node| { 18 | $puppetnode_facts = facts($node) 19 | $platform = $puppetnode_facts['platform'] 20 | 21 | $os_name = $puppetnode_facts['provisioner'] ? { 22 | 'docker' => split($puppetnode_facts['platform'], Regexp['[/:-]'])[1], 23 | 'docker_exp' => split($puppetnode_facts['platform'], Regexp['[/:-]'])[1], 24 | default => split($puppetnode_facts['platform'], Regexp['[/:-]'])[0] 25 | } 26 | 27 | $os_family = $os_name ? { 28 | /(^redhat|rhel|centos|scientific|oraclelinux)/ => 'redhat', 29 | /(^debian|ubuntu)/ => 'debian', 30 | /(^win)/ => 'windows', 31 | default => 'unsupported' 32 | } 33 | 34 | if $os_family == 'unsupported' { 35 | fail_plan('Not supported platform!') 36 | } 37 | 38 | if $os_family == 'debian' { 39 | run_task('provision::fix_secure_path', $node, 'fix secure path') 40 | } 41 | 42 | if $os_family == 'windows' { 43 | catch_errors() || { 44 | run_command('sc start puppet', $node, '_catch_errors' => true) 45 | } 46 | } else { 47 | catch_errors() || { 48 | run_command('systemctl start puppet', $node, '_catch_errors' => true) 49 | run_command('systemctl enable puppet', $node, '_catch_errors' => true) 50 | } 51 | } 52 | } 53 | 54 | # request signature 55 | run_command('puppet agent -t', $agents, '_catch_errors' => true) 56 | 57 | # wait for all certificates 58 | ctrl::sleep(5) 59 | 60 | # sign all requests 61 | run_command('puppetserver ca sign --all', $server, '_catch_errors' => true) 62 | } 63 | -------------------------------------------------------------------------------- /.rubocop_todo.yml: -------------------------------------------------------------------------------- 1 | # This configuration was generated by 2 | # `rubocop --auto-gen-config` 3 | # on 2024-01-17 17:28:36 UTC using RuboCop version 1.50.0. 4 | # The point is for the user to remove these configuration records 5 | # one by one as the offenses are removed from the code base. 6 | # Note that changes in the inspected code, or installation of new 7 | # versions of RuboCop, may require this file to be generated again. 8 | 9 | # Offense count: 13 10 | # Configuration parameters: AllowedMethods, AllowedPatterns, CountRepeatedAttributes. 11 | Metrics/AbcSize: 12 | Max: 94 13 | 14 | # Offense count: 2 15 | # Configuration parameters: CountComments, CountAsOne. 16 | Metrics/ClassLength: 17 | Max: 155 18 | 19 | # Offense count: 8 20 | # Configuration parameters: AllowedMethods, AllowedPatterns. 21 | Metrics/CyclomaticComplexity: 22 | Max: 23 23 | 24 | # Offense count: 20 25 | # Configuration parameters: CountComments, CountAsOne, AllowedMethods, AllowedPatterns. 26 | Metrics/MethodLength: 27 | Max: 70 28 | 29 | # Offense count: 3 30 | # Configuration parameters: CountKeywordArgs, MaxOptionalParameters. 31 | Metrics/ParameterLists: 32 | Max: 11 33 | 34 | # Offense count: 7 35 | # Configuration parameters: AllowedMethods, AllowedPatterns. 36 | Metrics/PerceivedComplexity: 37 | Max: 25 38 | 39 | # Offense count: 2 40 | # Configuration parameters: IgnoredMetadata. 41 | RSpec/DescribeClass: 42 | Exclude: 43 | - '**/spec/features/**/*' 44 | - '**/spec/requests/**/*' 45 | - '**/spec/routing/**/*' 46 | - '**/spec/system/**/*' 47 | - '**/spec/views/**/*' 48 | - 'spec/tasks/abs_spec.rb' 49 | - 'spec/unit/task_helper_spec.rb' 50 | 51 | # Offense count: 12 52 | # Configuration parameters: CountAsOne. 53 | RSpec/ExampleLength: 54 | Max: 27 55 | 56 | # Offense count: 13 57 | RSpec/MultipleExpectations: 58 | Max: 13 59 | 60 | # Offense count: 6 61 | # Configuration parameters: AllowSubject. 62 | RSpec/MultipleMemoizedHelpers: 63 | Max: 6 64 | 65 | # Offense count: 6 66 | RSpec/StubbedMock: 67 | Exclude: 68 | - 'spec/tasks/abs_spec.rb' 69 | 70 | # Offense count: 7 71 | Style/MixinUsage: 72 | Exclude: 73 | - 'spec/spec_helper.rb' 74 | - 'tasks/docker.rb' 75 | - 'tasks/docker_exp.rb' 76 | - 'tasks/vagrant.rb' 77 | -------------------------------------------------------------------------------- /spec/unit/inventory_helper_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | require 'inventory_helper' 5 | 6 | describe InventoryHelper, type: :class do 7 | include_context('with tmpdir') 8 | 9 | let(:inventory_file) { tmpdir } 10 | let(:inventory) { described_class.open(inventory_file) } 11 | 12 | let(:node_uri) { 'testing' } 13 | let(:node_name) { node_uri } 14 | let(:node_data) do 15 | { 16 | uri: node_uri, 17 | name: node_name, 18 | config: { 19 | transport: 'whocares' 20 | } 21 | } 22 | end 23 | 24 | describe '.open' do 25 | it 'correctly opens and saves new inventory file' do 26 | expect(inventory.save).to be_a described_class 27 | end 28 | 29 | context 'non-existent inventory path' do 30 | let(:inventory_file) { File.join(tmpdir, 'testing/testing.yaml') } 31 | 32 | it 'fails to open inventory file' do 33 | expect { inventory.save }.to raise_error(RuntimeError, %r{directory for storing inventory does not exist}) 34 | end 35 | end 36 | end 37 | 38 | describe '.lookup' do 39 | let(:node_name) { 'somethingelse' } 40 | 41 | before(:each) do 42 | inventory.add(node_data, 'whocares').save 43 | end 44 | 45 | it 'by uri' do 46 | expect(inventory.lookup(node_uri)).to be_a Hash 47 | end 48 | 49 | it 'by name' do 50 | expect(inventory.lookup(name: node_name)).to be_a Hash 51 | end 52 | 53 | it 'fallback to name' do 54 | expect(inventory.lookup(node_name)).to be_a Hash 55 | end 56 | 57 | it 'only in group' do 58 | expect(inventory.lookup(node_uri, group: 'whocares')).to be_a Hash 59 | end 60 | 61 | it 'not in group' do 62 | expect { inventory.lookup(node_uri, group: 'nogroup') }.to raise_error(RuntimeError, "Failed to lookup target #{node_uri}") 63 | end 64 | end 65 | 66 | describe '.add' do 67 | it 'add a node' do 68 | expect(inventory.add(node_data, 'whocares').save).to be_a described_class 69 | end 70 | end 71 | 72 | describe '.remove' do 73 | it 'remove a node' do 74 | expect(inventory.add(node_data, 'whocares').save).to be_a described_class 75 | expect(inventory.remove(inventory.lookup(node_uri))).to be_a described_class 76 | expect { inventory.delete(inventory.lookup(node_uri)) }.to raise_error(RuntimeError, "Failed to lookup target #{node_uri}") 77 | end 78 | end 79 | end 80 | -------------------------------------------------------------------------------- /lib/task_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | def run_local_command(command, dir = Dir.pwd) 4 | require 'open3' 5 | stdout, stderr, status = Open3.capture3(command, chdir: dir) 6 | error_message = "Attempted to run\ncommand:'#{command}'\nstdout:#{stdout}\nstderr:#{stderr}" 7 | raise error_message unless status.to_i.zero? 8 | 9 | stdout 10 | end 11 | 12 | def platform_is_windows?(platform) 13 | # TODO: This seems sub-optimal. We should be able to override/specify what the real platform is on a per target basis 14 | # (plain_windows) somewinorg/blah-windows-2019 15 | # (plain_windows) myorg/some_image:windows-server 16 | # (bare_win_with_demlimiter) myorg/some_image:win-server-2008 17 | # (bare_win_with_demlimiter) myorg/win-2k8r2 18 | # No Match myorg/winderping <--- Is this a Windows platform? 19 | # (plain_windows) myorg/windows-server 20 | # (plain_windows) windows-server 21 | # (bare_win_with_demlimiter) win-2008 22 | # (plain_windows) webserserver-windows-2008 23 | # (bare_win_with_demlimiter) webserver-win-2008 24 | windows_regex = %r{(?windows)|(?(?:^|[/:\-\\;])win(?:[/:\-\\;]|$))}i 25 | platform =~ windows_regex 26 | end 27 | 28 | def on_windows? 29 | # Stolen directly from Puppet::Util::Platform.windows? 30 | # Ruby only sets File::ALT_SEPARATOR on Windows and the Ruby standard 31 | # library uses that to test what platform it's on. In some places we 32 | # would use Puppet.features.microsoft_windows?, but this method can be 33 | # used to determine the behavior of the underlying system without 34 | # requiring features to be initialized and without side effect. 35 | !!File::ALT_SEPARATOR 36 | end 37 | 38 | def platform_uses_ssh(platform) 39 | # TODO: This seems sub-optimal. We should be able to override/specify what transport to use on a per target basis 40 | !platform_is_windows?(platform) 41 | end 42 | 43 | def token_from_fogfile(provider = 'abs') 44 | fog_file = File.join(Dir.home, '.fog') 45 | unless File.file?(fog_file) 46 | puts "Cannot file fog file at #{fog_file}" 47 | return nil 48 | end 49 | require 'yaml' 50 | contents = YAML.load_file(fog_file) 51 | token = contents.dig(:default, :abs_token) 52 | 53 | raise "Error: could not obtain #{provider} token from .fog file" if token.nil? 54 | 55 | token 56 | rescue StandardError 57 | puts 'Failed to get token from .fog file' 58 | end 59 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.configure do |c| 4 | c.mock_with :rspec 5 | end 6 | 7 | require 'puppetlabs_spec_helper/module_spec_helper' 8 | require 'rspec-puppet-facts' 9 | 10 | require 'spec_helper_local' if File.file?(File.join(File.dirname(__FILE__), 'spec_helper_local.rb')) 11 | 12 | include RspecPuppetFacts 13 | 14 | default_facts = { 15 | puppetversion: Puppet.version, 16 | facterversion: Facter.version, 17 | } 18 | 19 | default_fact_files = [ 20 | File.expand_path(File.join(File.dirname(__FILE__), 'default_facts.yml')), 21 | File.expand_path(File.join(File.dirname(__FILE__), 'default_module_facts.yml')), 22 | ] 23 | 24 | default_fact_files.each do |f| 25 | next unless File.exist?(f) && File.readable?(f) && File.size?(f) 26 | 27 | begin 28 | default_facts.merge!(YAML.safe_load(File.read(f), permitted_classes: [], permitted_symbols: [], aliases: true)) 29 | rescue StandardError => e 30 | RSpec.configuration.reporter.message "WARNING: Unable to load #{f}: #{e}" 31 | end 32 | end 33 | 34 | # read default_facts and merge them over what is provided by facterdb 35 | default_facts.each do |fact, value| 36 | add_custom_fact fact, value 37 | end 38 | 39 | RSpec.configure do |c| 40 | c.default_facts = default_facts 41 | c.before :each do 42 | # set to strictest setting for testing 43 | # by default Puppet runs at warning level 44 | Puppet.settings[:strict] = :warning 45 | Puppet.settings[:strict_variables] = true 46 | end 47 | c.filter_run_excluding(bolt: true) unless ENV['GEM_BOLT'] 48 | c.after(:suite) do 49 | RSpec::Puppet::Coverage.report!(0) 50 | end 51 | 52 | # Filter backtrace noise 53 | backtrace_exclusion_patterns = [ 54 | %r{spec_helper}, 55 | %r{gems}, 56 | ] 57 | 58 | if c.respond_to?(:backtrace_exclusion_patterns) 59 | c.backtrace_exclusion_patterns = backtrace_exclusion_patterns 60 | elsif c.respond_to?(:backtrace_clean_patterns) 61 | c.backtrace_clean_patterns = backtrace_exclusion_patterns 62 | end 63 | end 64 | 65 | # Ensures that a module is defined 66 | # @param module_name Name of the module 67 | def ensure_module_defined(module_name) 68 | module_name.split('::').reduce(Object) do |last_module, next_module| 69 | last_module.const_set(next_module, Module.new) unless last_module.const_defined?(next_module, false) 70 | last_module.const_get(next_module, false) 71 | end 72 | end 73 | 74 | # 'spec_overrides' from sync.yml will appear below this line 75 | -------------------------------------------------------------------------------- /tasks/vagrant.json: -------------------------------------------------------------------------------- 1 | { 2 | "puppet_task_version": 1, 3 | "supports_noop": false, 4 | "description": "Provision/Tear down a machine on vagrant", 5 | "parameters": { 6 | "action": { 7 | "description": "Action to perform, tear_down or provision", 8 | "type": "Enum[provision, tear_down]", 9 | "default": "provision" 10 | }, 11 | "inventory": { 12 | "description": "Location of the inventory file", 13 | "type": "Optional[String[1]]" 14 | }, 15 | "node_name": { 16 | "description": "The name of the node", 17 | "type": "Optional[String[1]]" 18 | }, 19 | "platform": { 20 | "description": "Platform to provision, eg ubuntu:14.04", 21 | "type": "Optional[String[1]]" 22 | }, 23 | "provider": { 24 | "description": "Provider to use provision, eg virtualbox", 25 | "type": "Optional[String[1]]" 26 | }, 27 | "cpus": { 28 | "description": "Number of CPUs. Eg 2", 29 | "type": "Optional[Integer]" 30 | }, 31 | "memory": { 32 | "description": "MB Memory. Eg 4000", 33 | "type": "Optional[Integer]" 34 | }, 35 | "hyperv_vswitch": { 36 | "description": "The Hyper-V virtual switch to spin the vagrant image up on", 37 | "type": "Optional[String[1]]", 38 | "default": "Default Switch" 39 | }, 40 | "hyperv_smb_username": { 41 | "description": "The username on the Hyper-V machine to use for authenticating the shared folder. Required to use Hyper-V with a synced folder.", 42 | "type": "Optional[String[1]]" 43 | }, 44 | "hyperv_smb_password": { 45 | "description": "The password on the Hyper-V machine to use for authenticating the shared folder. Required to use Hyper-V with a synced folder.", 46 | "type": "Optional[String[1]]", 47 | "sensitive": true 48 | }, 49 | "enable_synced_folder": { 50 | "description": "Whether to use the vagrant synced folder for the provisioned machine", 51 | "type": "Optional[Boolean]", 52 | "default": false 53 | }, 54 | "box_url": { 55 | "description": "Path to the Vagrant Box URL", 56 | "type": "Optional[String[1]]" 57 | }, 58 | "password": { 59 | "description": "Password to use for Vagrant boxes without the default Vagrant insecure key", 60 | "type": "Optional[String[1]]" 61 | }, 62 | "vars": { 63 | "description": "YAML string of key/value pairs to add to the inventory vars section", 64 | "type": "Optional[String[1]]" 65 | } 66 | }, 67 | "files": [ 68 | "provision/lib/task_helper.rb", 69 | "provision/lib/inventory_helper.rb" 70 | ] 71 | } 72 | -------------------------------------------------------------------------------- /tasks/docker_exp.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | require 'json' 5 | require 'yaml' 6 | require_relative '../lib/task_helper' 7 | require_relative '../lib/docker_helper' 8 | require_relative '../lib/inventory_helper' 9 | 10 | # TODO: detect what shell to use 11 | @shell_command = 'bash -lc' 12 | 13 | def provision(docker_platform, inventory, vars) 14 | os_release_facts = docker_image_os_release_facts(docker_platform) 15 | 16 | inventory_node = { 17 | 'config' => { 18 | 'transport' => 'docker', 19 | 'docker' => { 20 | 'shell-command' => @shell_command, 21 | } 22 | }, 23 | 'facts' => { 24 | 'provisioner' => 'docker_exp', 25 | 'platform' => docker_platform, 26 | 'os-release' => os_release_facts, 27 | } 28 | } 29 | 30 | docker_run_opts = '' 31 | unless vars.nil? 32 | var_hash = YAML.safe_load(vars) 33 | inventory_node['vars'] = var_hash 34 | docker_run_opts = var_hash['docker_run_opts'].flatten.join(' ') unless var_hash['docker_run_opts'].nil? 35 | end 36 | 37 | if docker_platform.match?(%r{debian|ubuntu}) 38 | docker_run_opts += ' --volume /sys/fs/cgroup:/sys/fs/cgroup:rw' unless docker_run_opts.include?('--volume /sys/fs/cgroup:/sys/fs/cgroup') 39 | docker_run_opts += ' --cgroupns=host' unless docker_run_opts.include?('--cgroupns') 40 | end 41 | 42 | creation_command = 'docker run -d -it --privileged ' 43 | creation_command += "#{docker_run_opts} " unless docker_run_opts.nil? 44 | creation_command += docker_platform 45 | 46 | container_id = run_local_command(creation_command).strip[0..11] 47 | 48 | docker_fix_missing_tty_error_message(container_id) unless platform_is_windows?(docker_platform) 49 | 50 | inventory_node['name'] = container_id 51 | inventory_node['uri'] = container_id 52 | inventory_node['facts']['container_id'] = container_id 53 | 54 | inventory.add(inventory_node, 'docker_nodes').save 55 | 56 | { status: 'ok', node_name: inventory_node['name'], node: inventory_node } 57 | end 58 | 59 | params = JSON.parse($stdin.read) 60 | action = params['action'] 61 | inventory = InventoryHelper.open(params['inventory']) 62 | node_name = params['node_name'] 63 | platform = params['platform'] 64 | vars = params['vars'] 65 | raise 'specify a node_name when tearing down' if action == 'tear_down' && node_name.nil? 66 | raise 'specify a platform when provisioning' if action == 'provision' && platform.nil? 67 | 68 | unless node_name.nil? ^ platform.nil? 69 | case action 70 | when 'tear_down' 71 | raise 'specify only a node_name, not platform, when tearing down' 72 | when 'provision' 73 | raise 'specify only a platform, not node_name, when provisioning' 74 | else 75 | raise 'specify only one of: node_name, platform' 76 | end 77 | end 78 | 79 | begin 80 | result = provision(platform, inventory, vars) if action == 'provision' 81 | if action == 'tear_down' 82 | node = inventory.lookup(node_name, group: 'docker_nodes') 83 | result = docker_tear_down(node['facts']['container_id']) 84 | inventory.remove(node).save 85 | end 86 | puts result.to_json 87 | exit 0 88 | rescue StandardError => e 89 | puts({ _error: { kind: 'provision/docker_exp_failure', msg: e.message, backtrace: e.backtrace } }.to_json) 90 | exit 1 91 | end 92 | -------------------------------------------------------------------------------- /spec/unit/docker_helper_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'docker_helper' 4 | require 'stringio' 5 | 6 | describe 'Docker Helper Functions' do 7 | let(:container_id) { 'abc12345' } 8 | let(:inventory_location) { '.' } 9 | let(:full_inventory_location) { "#{inventory_location}/spec/fixtures/litmus_inventory.yaml" } 10 | let(:inventory_yaml) do 11 | <<-YAML 12 | version: 2 13 | groups: 14 | - name: docker_nodes 15 | targets: 16 | - name: #{container_id} 17 | uri: #{container_id} 18 | config: 19 | transport: docker 20 | docker: 21 | shell-command: bash -lc 22 | connect-timeout: 120 23 | facts: 24 | provisioner: docker_exp 25 | container_id: #{container_id} 26 | platform: litmusimage/debian:12 27 | os-release: 28 | PRETTY_NAME: Debian GNU/Linux 12 (bookworm) 29 | NAME: Debian GNU/Linux 30 | VERSION_ID: '12' 31 | VERSION: 12 (bookworm) 32 | VERSION_CODENAME: bookworm 33 | ID: debian 34 | HOME_URL: https://www.debian.org/ 35 | SUPPORT_URL: https://www.debian.org/support 36 | BUG_REPORT_URL: https://bugs.debian.org/ 37 | - name: ssh_nodes 38 | targets: [] 39 | - name: winrm_nodes 40 | targets: [] 41 | - name: lxd_nodes 42 | targets: [] 43 | YAML 44 | end 45 | 46 | let(:os_release_facts) do 47 | <<-FILE 48 | PRETTY_NAME="Debian GNU/Linux 12 (bookworm)" 49 | NAME="Debian GNU/Linux" 50 | VERSION_ID="12" 51 | VERSION="12 (bookworm)" 52 | VERSION_CODENAME=bookworm 53 | ID=debian 54 | HOME_URL="https://www.debian.org/" 55 | SUPPORT_URL="https://www.debian.org/support" 56 | BUG_REPORT_URL="https://bugs.debian.org/" 57 | FILE 58 | end 59 | 60 | describe '.docker_exec' do 61 | it 'calls run_local_command' do 62 | allow(self).to receive(:run_local_command).with("docker exec #{container_id} a command").and_return('some output') 63 | expect(docker_exec(container_id, 'a command')).to eq('some output') 64 | end 65 | end 66 | 67 | describe '.docker_image_os_release_facts' do 68 | it 'returns parsed hash of /etc/os-release from container' do 69 | allow(self).to receive(:run_local_command) 70 | .with('docker run --rm litmusimage/debian:12 cat /etc/os-release') 71 | .and_return(os_release_facts) 72 | expect(docker_image_os_release_facts('litmusimage/debian:12')).to match(hash_including('PRETTY_NAME' => 'Debian GNU/Linux 12 (bookworm)')) 73 | end 74 | 75 | it 'returns minimal facts if parse fails for any reason' do 76 | allow(self).to receive(:run_local_command) 77 | .with('docker run --rm litmusimage/debian:12 cat /etc/os-release') 78 | .and_return(StandardError) 79 | expect(docker_image_os_release_facts('litmusimage/debian:12')).to match(hash_including('ID' => 'litmusimage_debian')) 80 | end 81 | end 82 | 83 | describe '.docker_tear_down' do 84 | it 'expect to return status ok' do 85 | allow(self).to receive(:run_local_command).with("docker rm -f #{container_id}") 86 | expect { 87 | expect(docker_tear_down(container_id)).to eql({ status: 'ok' }) 88 | }.to output("Removed #{container_id}\n").to_stdout 89 | end 90 | end 91 | 92 | describe '.docker_fix_missing_tty_error_message' do 93 | it 'execute command on container to disable mesg' do 94 | allow(self).to receive(:system).with("docker exec #{container_id} sed -i 's/^mesg n/tty -s \\&\\& mesg n/g' /root/.profile") 95 | expect(docker_fix_missing_tty_error_message(container_id)).to be_nil 96 | end 97 | end 98 | end 99 | -------------------------------------------------------------------------------- /lib/inventory_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'yaml' 4 | require 'delegate' 5 | require 'json' 6 | 7 | # simple bolt inventory manipulator 8 | class InventoryHelper < SimpleDelegator 9 | def initialize(location) 10 | @location = location 11 | super(refresh) 12 | end 13 | 14 | # Load inventory from location in YAML format 15 | # or generate a default structure 16 | # 17 | # @return [Hash] 18 | def refresh 19 | x = JSON.parse(YAML.load_file(@location).to_json) if File.file?(@location) 20 | { 'version' => 2, 'groups' => [] }.merge(x || {}) 21 | end 22 | 23 | # Save inventory to location in yaml format 24 | def save 25 | File.open(@location, 'wb+') { |f| f.write(to_yaml) } 26 | self 27 | end 28 | 29 | # Adds a node to a group specified, if group_name exists in inventory hash. 30 | # 31 | # @param node [Hash] node to add to the group 32 | # @param group [String] group of nodes to limit the search for the node_name in 33 | # @return [Hash] inventory_hash with node added to group if group_name exists in inventory hash. 34 | def add(node, group) 35 | # stringify keys 36 | node = JSON.parse(node.to_json) 37 | # check if group exists 38 | if self['groups'].any? { |g| g['name'] == group } 39 | self['groups'].each do |g| 40 | g['targets'].push(node) if g['name'] == group 41 | end 42 | else 43 | # add new group 44 | self['groups'].push({ 'name' => group, 'targets' => [node] }) 45 | end 46 | 47 | self 48 | end 49 | 50 | # Lookup a node 51 | # 52 | # @param either [String] uri or name of node to find 53 | # @param uri [String] uri of node to find 54 | # @param name [String] name of node to find 55 | # @param group [String] limit search to group 56 | # @return [Hash] inventory target 57 | def lookup(either = nil, uri: nil, name: nil, group: nil) 58 | value = either || uri || name 59 | 60 | keys = [] 61 | keys << 'uri' if uri || either 62 | keys << 'name' if name || either 63 | 64 | self['groups'].each do |g| 65 | next unless (group && group == g['name']) || group.nil? 66 | g['targets'].each do |t| 67 | return t if keys.include? t.key(value) 68 | end 69 | end 70 | 71 | raise "Failed to lookup target #{value}" 72 | end 73 | 74 | # Remove node 75 | # 76 | # @param node [Hash] 77 | # @return [Hash] inventory_hash with node of node_name removed. 78 | def remove(node) 79 | # stringify keys 80 | node = JSON.parse(node.to_json) 81 | self['groups'].map! do |g| 82 | g['targets'].reject! { |target| target == node } 83 | g 84 | end 85 | 86 | self 87 | end 88 | 89 | class << self 90 | attr_accessor :instances 91 | 92 | def open(location = nil) 93 | # Inventory location is an optional task parameter. 94 | location = location.nil? ? Dir.pwd : location 95 | location = if File.directory?(location) 96 | # DEPRECATED: puppet_litmus <= 1.4.0 support 97 | if Gem.loaded_specs['puppet_litmus'] && Gem.loaded_specs['puppet_litmus'].version <= Gem::Version.new('1.4.0') 98 | File.join(location, 'spec', 'fixtures', 'litmus_inventory.yaml') 99 | else 100 | File.join(location, 'inventory.yaml') 101 | end 102 | else 103 | location 104 | end 105 | 106 | raise "directory for storing inventory does not exist: #{location}" unless File.exist?(File.dirname(location)) 107 | 108 | @instances ||= {} 109 | @instances[location] = new(location) unless @instances.key? location 110 | @instances[location] 111 | end 112 | end 113 | 114 | attr_reader :location 115 | 116 | protected 117 | 118 | attr_writer :location 119 | end 120 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source ENV['GEM_SOURCE'] || 'https://rubygems.org' 2 | 3 | def location_for(place_or_version, fake_version = nil) 4 | git_url_regex = %r{\A(?(https?|git)[:@][^#]*)(#(?.*))?} 5 | file_url_regex = %r{\Afile:\/\/(?.*)} 6 | 7 | if place_or_version && (git_url = place_or_version.match(git_url_regex)) 8 | [fake_version, { git: git_url[:url], branch: git_url[:branch], require: false }].compact 9 | elsif place_or_version && (file_url = place_or_version.match(file_url_regex)) 10 | ['>= 0', { path: File.expand_path(file_url[:path]), require: false }] 11 | else 12 | [place_or_version, { require: false }] 13 | end 14 | end 15 | 16 | group :development do 17 | gem "json", '= 2.6.1', require: false if Gem::Requirement.create(['>= 3.1.0', '< 3.1.3']).satisfied_by?(Gem::Version.new(RUBY_VERSION.dup)) 18 | gem "json", '= 2.6.3', require: false if Gem::Requirement.create(['>= 3.2.0', '< 4.0.0']).satisfied_by?(Gem::Version.new(RUBY_VERSION.dup)) 19 | gem "racc", '~> 1.4.0', require: false if Gem::Requirement.create(['>= 2.7.0', '< 3.0.0']).satisfied_by?(Gem::Version.new(RUBY_VERSION.dup)) 20 | gem "voxpupuli-puppet-lint-plugins", '~> 5.0', require: false 21 | gem "facterdb", '~> 1.18', require: false 22 | gem "metadata-json-lint", '~> 4.0', require: false 23 | gem "puppetlabs_spec_helper", '~> 6.0', require: false 24 | gem "rspec-puppet-facts", '~> 2.0', require: false 25 | gem "dependency_checker", '~> 1.0.0', require: false 26 | gem "parallel_tests", '= 3.12.1', require: false 27 | gem "pry", '~> 0.10', require: false 28 | gem "simplecov-console", '~> 0.9', require: false 29 | gem "puppet-debugger", '~> 1.0', require: false 30 | gem "rubocop", '~> 1.50.0', require: false 31 | gem "rubocop-performance", '= 1.16.0', require: false 32 | gem "rubocop-rspec", '= 2.19.0', require: false 33 | gem "puppet-strings", '~> 4.0', require: false 34 | gem "rb-readline", '= 0.5.5', require: false, platforms: [:mswin, :mingw, :x64_mingw] 35 | gem "github_changelog_generator", '= 1.15.2', require: false 36 | gem "webmock", require: false 37 | end 38 | group :system_tests do 39 | gem "puppet_litmus", '~> 1.0', require: false, platforms: [:ruby, :x64_mingw] 40 | gem "serverspec", '~> 2.41', require: false 41 | end 42 | group :release_prep do 43 | gem "puppet-strings", '~> 4.0', require: false 44 | gem "puppetlabs_spec_helper", '~> 6.0', require: false 45 | end 46 | 47 | puppet_version = ENV['PUPPET_GEM_VERSION'] 48 | facter_version = ENV['FACTER_GEM_VERSION'] 49 | hiera_version = ENV['HIERA_GEM_VERSION'] 50 | 51 | gems = {} 52 | puppet_version = ENV.fetch('PUPPET_GEM_VERSION', nil) 53 | facter_version = ENV.fetch('FACTER_GEM_VERSION', nil) 54 | hiera_version = ENV.fetch('HIERA_GEM_VERSION', nil) 55 | 56 | # If PUPPET_FORGE_TOKEN is set then use authenticated source for both puppet and facter, since facter is a transitive dependency of puppet 57 | # Otherwise, do as before and use location_for to fetch gems from the default source 58 | if !ENV['PUPPET_FORGE_TOKEN'].to_s.empty? 59 | gems['puppet'] = ['~> 8.11', { require: false, source: 'https://rubygems-puppetcore.puppet.com' }] 60 | gems['facter'] = ['~> 4.11', { require: false, source: 'https://rubygems-puppetcore.puppet.com' }] 61 | else 62 | gems['puppet'] = location_for(puppet_version) 63 | gems['facter'] = location_for(facter_version) if facter_version 64 | end 65 | 66 | gems['hiera'] = location_for(hiera_version) if hiera_version 67 | 68 | gems.each do |gem_name, gem_params| 69 | gem gem_name, *gem_params 70 | end 71 | 72 | # Evaluate Gemfile.local and ~/.gemfile if they exist 73 | extra_gemfiles = [ 74 | "#{__FILE__}.local", 75 | File.join(Dir.home, '.gemfile'), 76 | ] 77 | 78 | extra_gemfiles.each do |gemfile| 79 | if File.file?(gemfile) && File.readable?(gemfile) 80 | eval(File.read(gemfile), binding) 81 | end 82 | end 83 | # vim: syntax=ruby 84 | -------------------------------------------------------------------------------- /tasks/install_puppetserver.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # get parameters provided by bolt 4 | if [ -n "$PT_collection" ]; then 5 | collection=$PT_collection 6 | else 7 | collection='none' 8 | fi 9 | 10 | if [ -n "$PT_platform" ]; then 11 | platform=$PT_platform 12 | else 13 | platform='none' 14 | fi 15 | 16 | if [ -n "$PT_retry" ]; then 17 | retry=$PT_retry 18 | else 19 | retry=5 20 | fi 21 | 22 | parse_platform() { 23 | data=() 24 | for x in $(echo "$1" | tr "/|-|-|:" "\n") 25 | do 26 | data+=("$x") 27 | done 28 | if [[ "$2" == "osname" ]]; then 29 | if [[ "${data[0]}" == "litmusimage" ]]; then 30 | echo "${data[1]}" 31 | else 32 | echo "${data[0]}" 33 | fi 34 | fi 35 | if [[ "$2" == "majorversion" ]]; then 36 | if [[ "${data[0]}" == "litmusimage" ]]; then 37 | echo "${data[2]}" 38 | else 39 | echo "${data[1]}" 40 | fi 41 | fi 42 | } 43 | 44 | fetch_osfamily() { 45 | re_debian="(^debian|ubuntu)" 46 | re_redhat="(^redhat|rhel|centos|scientific|oraclelinux)" 47 | unsupported=1 48 | if [[ $1 =~ $re_debian ]]; then 49 | echo "debian" 50 | unsupported=0 51 | fi 52 | if [[ $1 =~ $re_redhat ]]; then 53 | echo "redhat" 54 | unsupported=0 55 | fi 56 | if [[ $unsupported == 1 ]]; then 57 | echo "unsupported" 58 | fi 59 | } 60 | 61 | fetch_collection() { 62 | # Handle puppetcore8-nightly -> puppet8-nightly conversion 63 | if [[ "$1" == puppetcore8* ]]; then 64 | echo "${1/puppetcore8/puppet8}" 65 | else 66 | myarr=() 67 | for x in $(echo "$1" | tr "-" "\n") 68 | do 69 | myarr+=("$x") 70 | done 71 | echo "${myarr[0]}" 72 | fi 73 | } 74 | 75 | fetch_codename() { 76 | codename="unsupported" 77 | case $2 in 78 | "8") 79 | if [[ "$1" == "puppet6" ]]; then 80 | codename="jessie" 81 | fi 82 | ;; 83 | "9") codename="stretch";; 84 | "10") codename="buster";; 85 | "11") codename="bullseye";; 86 | "12") codename="bookworm";; 87 | "1404") 88 | if [[ "$1" == "puppet6" ]]; then 89 | codename="trusty" 90 | fi 91 | ;; 92 | "14.04") 93 | if [[ "$1" == "puppet6" ]]; then 94 | codename="trusty" 95 | fi 96 | ;; 97 | "1604") codename="xenial";; 98 | "16.04") codename="xenial";; 99 | "1804") codename="bionic";; 100 | "18.04") codename="bionic";; 101 | "2004") codename="focal";; 102 | "20.04") codename="focal";; 103 | "2204") codename="jammy";; 104 | "22.04") codename="jammy";; 105 | *) codename="unsupported" 106 | esac 107 | echo $codename 108 | } 109 | 110 | run_cmd() { 111 | eval "$1" 112 | rc=$? 113 | 114 | if test $rc -ne 0; then 115 | attempt_number=0 116 | while test $attempt_number -lt "$retry"; do 117 | echo "Retrying... [$((attempt_number + 1))/$retry]" 118 | eval "$1" 119 | rc=$? 120 | 121 | if test $rc -eq 0; then 122 | break 123 | fi 124 | 125 | echo "Return code: $rc" 126 | sleep 1s 127 | ((attempt_number=attempt_number+1)) 128 | done 129 | fi 130 | 131 | return $rc 132 | } 133 | 134 | if [[ "$platform" == "none" || "$collection" == "none" ]]; then 135 | echo "please provide both parameters(collection and platform)" 136 | exit 1 137 | fi 138 | 139 | if [[ "$platform" == "null" || "$collection" == "null" ]]; then 140 | echo "please provide both parameters(collection and platform)" 141 | exit 1 142 | fi 143 | 144 | osname=$(parse_platform "$platform" "osname") 145 | major_version=$(parse_platform "$platform" "majorversion") 146 | osfamily=$(fetch_osfamily "$osname") 147 | collection=$(fetch_collection "$collection") 148 | 149 | if [[ "$collection" == "puppet5" ]]; then 150 | echo "puppet5 eol!" 151 | exit 1 152 | fi 153 | 154 | if [[ "$osfamily" == "unsupported" ]]; then 155 | echo "No builds for $platform" 156 | exit 1 157 | fi 158 | 159 | if [[ "$osfamily" == "debian" ]]; then 160 | codename=$(fetch_codename "$collection" "$major_version") 161 | if [[ "$codename" == "unsupported" ]]; then 162 | echo "No builds for $platform" 163 | exit 1 164 | else 165 | run_cmd "curl -o puppet.deb https://artifactory.delivery.puppetlabs.net:443/artifactory/internal_nightly__local/apt/${collection}-release-${codename}.deb" 166 | dpkg -i --force-confmiss puppet.deb 167 | apt-get update -y 168 | apt-get install puppetserver -y 169 | fi 170 | fi 171 | 172 | if [[ "$osfamily" == "redhat" ]]; then 173 | run_cmd "curl -o puppet.rpm https://artifactory.delivery.puppetlabs.net:443/artifactory/internal_nightly__local/yum/${collection}-release-el-${major_version}.noarch.rpm" 174 | rpm -Uvh puppet.rpm --quiet 175 | yum install puppetserver -y --quiet 176 | fi 177 | 178 | exit 0 179 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 2 | # Changelog 3 | 4 | All notable changes to this project will be documented in this file. 5 | 6 | The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org). 7 | 8 | ## [v3.0.1](https://github.com/puppetlabs/provision/tree/v3.0.1) - 2024-10-28 9 | 10 | [Full Changelog](https://github.com/puppetlabs/provision/compare/v3.0.0...v3.0.1) 11 | 12 | ### Fixed 13 | 14 | - (MAINT) Install Puppet Server Task [#277](https://github.com/puppetlabs/provision/pull/277) ([coreymbe](https://github.com/coreymbe)) 15 | 16 | ## [v3.0.0](https://github.com/puppetlabs/provision/tree/v3.0.0) - 2024-10-28 17 | 18 | [Full Changelog](https://github.com/puppetlabs/provision/compare/v2.1.1...v3.0.0) 19 | 20 | ### Changed 21 | 22 | - (CAT-1264) - Drop Support for EOL Windows 2008 R2, Debian 8 + Ubuntu 16.04 [#231](https://github.com/puppetlabs/provision/pull/231) ([jordanbreen28](https://github.com/jordanbreen28)) 23 | 24 | ### Added 25 | 26 | - (MAINT) Support for Puppet Server on Ubuntu 22.04 [#273](https://github.com/puppetlabs/provision/pull/273) ([coreymbe](https://github.com/coreymbe)) 27 | - (CAT-372) - add var support to vagrant provisioner [#264](https://github.com/puppetlabs/provision/pull/264) ([jordanbreen28](https://github.com/jordanbreen28)) 28 | - Uncouple from the puppet_litmus gem [#260](https://github.com/puppetlabs/provision/pull/260) ([h0tw1r3](https://github.com/h0tw1r3)) 29 | - tasks require path to inventory yaml [#259](https://github.com/puppetlabs/provision/pull/259) ([h0tw1r3](https://github.com/h0tw1r3)) 30 | - LXD provisoner support [#251](https://github.com/puppetlabs/provision/pull/251) ([h0tw1r3](https://github.com/h0tw1r3)) 31 | - Add additional Docker provisioner OS support [#244](https://github.com/puppetlabs/provision/pull/244) ([seanmil](https://github.com/seanmil)) 32 | - Add flexible Linux box support for Vagrant [#242](https://github.com/puppetlabs/provision/pull/242) ([seanmil](https://github.com/seanmil)) 33 | - (CAT-1264) - Add Support for CentOS 8, RHEL 8/9, Debian 10/11, Ubuntu 18/20/22, Windows 16/19/22 [#232](https://github.com/puppetlabs/provision/pull/232) ([jordanbreen28](https://github.com/jordanbreen28)) 34 | - docker context and DOCKER_HOST env support [#200](https://github.com/puppetlabs/provision/pull/200) ([h0tw1r3](https://github.com/h0tw1r3)) 35 | 36 | ### Fixed 37 | 38 | - (bug) - Fix empty inventory [#271](https://github.com/puppetlabs/provision/pull/271) ([jordanbreen28](https://github.com/jordanbreen28)) 39 | - (CAT-1958) - Fix 404 on teardown of abs node [#270](https://github.com/puppetlabs/provision/pull/270) ([jordanbreen28](https://github.com/jordanbreen28)) 40 | - fix tear_down from puppet_litmus [#268](https://github.com/puppetlabs/provision/pull/268) ([h0tw1r3](https://github.com/h0tw1r3)) 41 | - fix provision on dnf only platforms [#261](https://github.com/puppetlabs/provision/pull/261) ([h0tw1r3](https://github.com/h0tw1r3)) 42 | - fix bolt tasks with docker_exp transport [#258](https://github.com/puppetlabs/provision/pull/258) ([h0tw1r3](https://github.com/h0tw1r3)) 43 | - fix redhat distribution not supported [#255](https://github.com/puppetlabs/provision/pull/255) ([h0tw1r3](https://github.com/h0tw1r3)) 44 | - fix sles ssh setup in docker if ssh already installed [#254](https://github.com/puppetlabs/provision/pull/254) ([h0tw1r3](https://github.com/h0tw1r3)) 45 | - (CAT-1688) - Pin rubocop to `~> 1.50.0` [#249](https://github.com/puppetlabs/provision/pull/249) ([LukasAud](https://github.com/LukasAud)) 46 | - Fix docker remote host support [#247](https://github.com/puppetlabs/provision/pull/247) ([seanmil](https://github.com/seanmil)) 47 | 48 | ## [v2.1.1](https://github.com/puppetlabs/provision/tree/v2.1.1) - 2023-07-27 49 | 50 | [Full Changelog](https://github.com/puppetlabs/provision/compare/v2.1.0...v2.1.1) 51 | 52 | ### Fixed 53 | 54 | - (CAT-1253) - Fixes undefined variable in vagrant provisioner [#228](https://github.com/puppetlabs/provision/pull/228) ([jordanbreen28](https://github.com/jordanbreen28)) 55 | 56 | ## [v2.1.0](https://github.com/puppetlabs/provision/tree/v2.1.0) - 2023-07-25 57 | 58 | [Full Changelog](https://github.com/puppetlabs/provision/compare/v2.0.0...v2.1.0) 59 | 60 | ### Added 61 | 62 | - (maint) - Add connect-timeout to transport [#216](https://github.com/puppetlabs/provision/pull/216) ([jordanbreen28](https://github.com/jordanbreen28)) 63 | 64 | ### Fixed 65 | 66 | - (CONT-1241) - Retrying when response body is nil or empty but response code is 200 [#221](https://github.com/puppetlabs/provision/pull/221) ([Ramesh7](https://github.com/Ramesh7)) 67 | 68 | ## [v2.0.0](https://github.com/puppetlabs/provision/tree/v2.0.0) - 2023-05-04 69 | 70 | [Full Changelog](https://github.com/puppetlabs/provision/compare/v1.0.0...v2.0.0) 71 | 72 | ### Changed 73 | 74 | - (CONT-809) Add Puppet 8 support [#205](https://github.com/puppetlabs/provision/pull/205) ([GSPatton](https://github.com/GSPatton)) 75 | 76 | ## [v1.0.0](https://github.com/puppetlabs/provision/tree/v1.0.0) - 2023-05-03 77 | 78 | [Full Changelog](https://github.com/puppetlabs/provision/compare/254ad83d7bea85d163c3a6399dc86025af733cd3...v1.0.0) 79 | -------------------------------------------------------------------------------- /tasks/lxd.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | require 'json' 5 | require 'yaml' 6 | require_relative '../lib/task_helper' 7 | require_relative '../lib/inventory_helper' 8 | 9 | # Provision and teardown instances on LXD 10 | class LXDProvision 11 | attr_reader :node_name, :retries 12 | attr_reader :platform, :inventory, :vars, :action, :options 13 | 14 | def provision 15 | lxd_remote = options[:remote] || lxd_default_remote 16 | 17 | lxd_flags = [] 18 | options[:profiles]&.each { |p| lxd_flags << "--profile #{p}" } 19 | lxd_flags << "--type #{options[:instance_type]}" if options[:instance_type] 20 | lxd_flags << "--storage #{options[:storage]}" if options[:storage] 21 | lxd_flags << '--vm' if options[:vm] 22 | 23 | creation_command = "lxc -q create #{platform} #{lxd_remote}: #{lxd_flags.join(' ')}" 24 | container_id = run_local_command(creation_command).chomp.split[-1] 25 | 26 | # add agent cdrom device if required 27 | container_properties = YAML.safe_load(run_local_command("lxc -q config show #{lxd_remote}:#{container_id} -e")) 28 | if container_properties['config']&.fetch('image.requirements.cdrom_agent', nil).to_s == 'true' 29 | run_local_command("lxc -q config device add #{lxd_remote}:#{container_id} agent disk source=agent:config") 30 | end 31 | 32 | begin 33 | start_command = "lxc -q start #{lxd_remote}:#{container_id}" 34 | run_local_command(start_command) 35 | 36 | # wait here for a bit until instance can accept commands 37 | state_command = "lxc -q exec #{lxd_remote}:#{container_id} uptime" 38 | attempt = 0 39 | begin 40 | run_local_command(state_command) 41 | rescue StandardError => e 42 | raise "Giving up waiting for #{lxd_remote}:#{container_id} to enter running state. Got error: #{e.message}" if retries > 0 && attempt > retries 43 | 44 | attempt += 1 45 | sleep 2**attempt 46 | retry if retries > 0 47 | end 48 | rescue StandardError 49 | run_local_command("lxc -q delete #{lxd_remote}:#{container_id} -f") 50 | raise 51 | end 52 | 53 | facts = { 54 | provisioner: 'lxd', 55 | container_id:, 56 | platform: 57 | } 58 | 59 | options.each do |option| 60 | facts[:"lxd_#{option[0]}"] = option[1] unless option[1].to_s.empty? 61 | end 62 | 63 | node = { 64 | uri: container_id, 65 | config: { 66 | transport: 'lxd', 67 | lxd: { 68 | remote: lxd_remote, 69 | 'shell-command': 'sh -lc' 70 | } 71 | }, 72 | facts: 73 | } 74 | 75 | node[:vars] = vars unless vars.nil? 76 | 77 | inventory.add(node, 'lxd_nodes').save 78 | 79 | { status: 'ok', node_name: container_id, node: } 80 | end 81 | 82 | def tear_down 83 | node = inventory.lookup(node_name, group: 'lxd_nodes') 84 | 85 | raise "node_name #{node_name} not found in inventory" unless node 86 | 87 | run_local_command("lxc -q delete #{node['config']['lxd']['remote']}:#{node['facts']['container_id']} -f") 88 | 89 | inventory.remove(node).save 90 | 91 | { status: 'ok' } 92 | end 93 | 94 | def task(**params) 95 | finalize_params!(params) 96 | 97 | @action = params.delete(:action) 98 | @retries = params.delete(:retries)&.to_i || 1 99 | @platform = params.delete(:platform) 100 | @node_name = params.delete(:node_name) 101 | @vars = YAML.safe_load(params.delete(:vars) || '~') 102 | 103 | @inventory = InventoryHelper.open(params.delete(:inventory)) 104 | 105 | @options = params.reject { |k, _v| k.start_with? '_' } 106 | method(action).call 107 | end 108 | 109 | def lxd_default_remote 110 | @lxd_default_remote ||= run_local_command('lxc -q remote get-default').chomp 111 | @lxd_default_remote 112 | end 113 | 114 | # add environment provided parameters (puppet litmus) 115 | def finalize_params!(params) 116 | ['remote', 'profiles', 'storage', 'instance_type', 'vm'].each do |p| 117 | params[p] = YAML.safe_load(ENV.fetch("LXD_#{p.upcase}", '~')) if params[p].to_s.empty? 118 | end 119 | params.compact! 120 | end 121 | 122 | class << self 123 | def run 124 | params = JSON.parse($stdin.read, symbolize_names: true) 125 | 126 | case params[:action] 127 | when 'tear_down' 128 | raise 'do not specify platform when tearing down' if params[:platform] 129 | raise 'node_name required when tearing down' unless params[:node_name] 130 | when 'provision' 131 | raise 'do not specify node_name when provisioning' if params[:node_name] 132 | raise 'platform required, when provisioning' unless params[:platform] 133 | else 134 | raise "invalid action: #{params[:action]}" if params[:action] 135 | 136 | raise 'must specify a valid action' 137 | end 138 | 139 | result = new.task(**params) 140 | puts result.to_json 141 | rescue StandardError => e 142 | puts({ _error: { kind: 'provision/lxd_failure', msg: e.message, details: { backtraces: e.backtrace } } }.to_json) 143 | exit 1 144 | end 145 | end 146 | end 147 | 148 | LXDProvision.run if __FILE__ == $PROGRAM_NAME 149 | -------------------------------------------------------------------------------- /spec/tasks/abs_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | require 'webmock/rspec' 5 | require_relative '../../tasks/abs' 6 | require 'yaml' 7 | 8 | describe 'provision::abs' do 9 | let(:abs) { ABSProvision.new } 10 | let(:inventory_dir) { "#{tmpdir}/spec/fixtures" } 11 | let(:inventory_file) { "#{inventory_dir}/litmus_inventory.yaml" } 12 | let(:empty_inventory_yaml) do 13 | <<~YAML 14 | --- 15 | version: 2 16 | groups: 17 | - name: docker_nodes 18 | targets: [] 19 | - name: ssh_nodes 20 | targets: [] 21 | - name: winrm_nodes 22 | targets: [] 23 | YAML 24 | end 25 | 26 | include_context('with tmpdir') 27 | 28 | def with_env(env_vars) 29 | env_vars.each { |k, v| ENV[k] = v } 30 | yield 31 | ensure 32 | env_vars.each { |k, _v| ENV.delete(k) } 33 | end 34 | 35 | before(:each) do 36 | FileUtils.mkdir_p(inventory_dir) 37 | end 38 | 39 | describe '.run' do 40 | it 'handles JSON parameters from stdin' do 41 | json_input = '{"action":"foo","platform":"bar"}' 42 | expect($stdin).to receive(:read).and_return(json_input) 43 | 44 | expect { ABSProvision.run }.to( 45 | raise_error(SystemExit) { |e| 46 | expect(e.status).to eq(0) 47 | }.and( 48 | output("null\n").to_stdout, 49 | ), 50 | ) 51 | end 52 | 53 | it 'raises an error when platform not given for provision' do 54 | expect($stdin).to receive(:read).and_return('{"action":"provision"}') 55 | 56 | expect { ABSProvision.run }.to raise_error(RuntimeError, %r{specify a platform when provisioning}) 57 | end 58 | 59 | it 'raises an error when node_name not given for tear_down' do 60 | expect($stdin).to receive(:read).and_return('{"action":"teardown"}') 61 | 62 | expect { ABSProvision.run }.to raise_error(RuntimeError, %r{specify only one of: node_name, platform}) 63 | end 64 | 65 | it 'raises an error if both node_name and platform are given' do 66 | expect($stdin).to receive(:read).and_return('{"action":"teardown","platform":"centos-9"}') 67 | 68 | expect { ABSProvision.run }.to( 69 | raise_error(SystemExit) do |e| 70 | expect(e.status).to eq(0) 71 | end, 72 | ) 73 | end 74 | end 75 | 76 | context 'when provisioning' do 77 | let(:params) do 78 | { 79 | action: 'provision', 80 | platform: 'redhat-8-x86_64', 81 | inventory: inventory_file, 82 | } 83 | end 84 | let(:response_body) do 85 | [ 86 | { 87 | 'type' => 'redhat-8-x86_64', 88 | 'hostname' => 'foo-bar.test' 89 | }, 90 | ] 91 | end 92 | 93 | it 'provisions the platform' do 94 | stub_request(:post, 'https://abs-prod.k8s.infracore.puppet.net/api/v2/request') 95 | .to_return({ status: 202 }, { status: 200, body: response_body.to_json }) 96 | 97 | expect(abs.task(**params)).to eq({ status: 'ok', nodes: 1 }) 98 | 99 | updated_inventory = YAML.load_file(inventory_file) 100 | ssh_targets = updated_inventory['groups'].find { |g| g['name'] == 'ssh_nodes' }['targets'] 101 | expect(ssh_targets.size).to eq(1) 102 | expect(ssh_targets.first.dig('facts', 'platform')).to eq('redhat-8-x86_64') 103 | end 104 | 105 | it 'targets a different abs host' do 106 | stub_request(:post, 'https://abs-spec.k8s.infracore.puppet.net/api/v2/request') 107 | .to_return({ status: 202 }, { status: 200, body: response_body.to_json }) 108 | 109 | with_env('ABS_SUBDOMAIN' => 'abs-spec') do 110 | expect(abs.task(**params)).to eq({ status: 'ok', nodes: 1 }) 111 | end 112 | end 113 | 114 | it 'provision with an existing inventory file' do 115 | stub_request(:post, 'https://abs-prod.k8s.infracore.puppet.net/api/v2/request') 116 | .to_return({ status: 202 }, { status: 200, body: response_body.to_json }) 117 | 118 | File.write(inventory_file, empty_inventory_yaml) 119 | 120 | expect(abs.task(**params)).to eq({ status: 'ok', nodes: 1 }) 121 | end 122 | 123 | it 'raises an error if abs returns error response' 124 | end 125 | 126 | context 'when tearing down' do 127 | let(:params) do 128 | { 129 | action: 'tear_down', 130 | node_name: 'foo-bar.test', 131 | inventory: inventory_file 132 | } 133 | end 134 | let(:inventory_yaml) do 135 | empty = YAML.safe_load(empty_inventory_yaml) 136 | groups = empty['groups'] 137 | ssh_nodes = groups.find { |g| g['name'] == 'ssh_nodes' } 138 | ssh_nodes['targets'] << { 139 | 'uri' => 'foo-bar.test', 140 | 'facts' => { 141 | 'platform' => 'redhat-8-x86_64', 142 | 'job_id' => 'a-job-id' 143 | } 144 | } 145 | empty.to_yaml 146 | end 147 | 148 | before(:each) do 149 | File.write(inventory_file, inventory_yaml) 150 | end 151 | 152 | it 'tears down a node' do 153 | expect(abs).to receive(:token_from_fogfile).and_return('fog-token') 154 | stub_request(:post, 'https://abs-prod.k8s.infracore.puppet.net/api/v2/return') 155 | .to_return(status: 200) 156 | 157 | expect(abs.task(**params)).to eq({ status: 'ok', removed: ['foo-bar.test'] }) 158 | expect(YAML.load_file(inventory_file)).to eq(YAML.safe_load(empty_inventory_yaml)) 159 | end 160 | 161 | it 'targets a different abs host' do 162 | expect(abs).to receive(:token_from_fogfile).and_return('fog-token') 163 | stub_request(:post, 'https://abs-spec.k8s.infracore.puppet.net/api/v2/return') 164 | .to_return(status: 200) 165 | 166 | with_env('ABS_SUBDOMAIN' => 'abs-spec') do 167 | expect(abs.task(**params)).to eq({ status: 'ok', removed: ['foo-bar.test'] }) 168 | end 169 | end 170 | 171 | it 'raises an error if abs returns error response' 172 | end 173 | end 174 | -------------------------------------------------------------------------------- /spec/tasks/provision_service_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | require 'webmock/rspec' 5 | require_relative '../../tasks/provision_service' 6 | 7 | ENV['GITHUB_RUN_ID'] = '1234567890' 8 | ENV['GITHUB_URL'] = 'https://api.github.com/repos/puppetlabs/puppetlabs-iis/actions/runs/1234567890' 9 | 10 | describe 'ProvisionService' do 11 | describe '.run' do 12 | context 'when inputs are invalid' do 13 | it 'return exception' do 14 | json_input = '{}' 15 | allow($stdin).to receive(:read).and_return(json_input) 16 | 17 | expect { ProvisionService.run }.to( 18 | raise_error(SystemExit) { |e| 19 | expect(e.status).to eq(0) 20 | }.and( 21 | output(%r{Unknown action}).to_stdout, 22 | ), 23 | ) 24 | end 25 | 26 | it 'return exception about invalid action' do 27 | json_input = '{"action":"foo","platform":"bar"}' 28 | allow($stdin).to receive(:read).and_return(json_input) 29 | 30 | expect { ProvisionService.run }.to( 31 | raise_error(SystemExit) { |e| 32 | expect(e.status).to eq(0) 33 | }.and( 34 | output(%r{Unknown action 'foo'}).to_stdout, 35 | ), 36 | ) 37 | end 38 | 39 | it 'return exception for missing platform' do 40 | json_input = '{"action":"provision"}' 41 | allow($stdin).to receive(:read).and_return(json_input) 42 | 43 | expect { ProvisionService.run }.to( 44 | raise_error(SystemExit) { |e| 45 | expect(e.status).to eq(1) 46 | }.and( 47 | output(%r{specify a platform when provisioning}).to_stdout, 48 | ), 49 | ) 50 | end 51 | 52 | it 'return exception for missing node_name' do 53 | json_input = '{"action":"tear_down"}' 54 | allow($stdin).to receive(:read).and_return(json_input) 55 | 56 | expect { ProvisionService.run }.to( 57 | raise_error(SystemExit) { |e| 58 | expect(e.status).to eq(1) 59 | }.and( 60 | output(%r{specify a node_name when tearing down}).to_stdout, 61 | ), 62 | ) 63 | end 64 | end 65 | end 66 | 67 | describe '#provision' do 68 | let(:inventory) { InventoryHelper.open("#{Dir.pwd}/litmus_inventory.yaml") } 69 | let(:vars) { nil } 70 | let(:platform) { 'centos-8' } 71 | let(:retry_attempts) { 8 } 72 | let(:response_body) do 73 | { 74 | 'groups' => [ 75 | 'targets' => { 76 | 'uri' => '127.0.0.1' 77 | }, 78 | ] 79 | } 80 | end 81 | let(:provision_service) { ProvisionService.new } 82 | 83 | context 'when response is empty' do 84 | it 'return exception' do 85 | stub_request(:post, 'https://facade-release-6f3kfepqcq-ew.a.run.app/v1/provision') 86 | .with( 87 | body: '{"url":"https://api.github.com/repos/puppetlabs/puppetlabs-iis/actions/runs/1234567890","VMs":[{"cloud":null,"region":null,"zone":null,"images":["centos-8"]}]}', 88 | headers: { 89 | 'Accept' => 'application/json', 90 | 'Accept-Encoding' => 'gzip;q=1.0,deflate;q=0.6,identity;q=0.3', 91 | 'Content-Type' => 'application/json', 92 | 'Host' => 'facade-release-6f3kfepqcq-ew.a.run.app', 93 | 'User-Agent' => 'Ruby' 94 | }, 95 | ) 96 | .to_return(status: 200, body: '', headers: {}) 97 | expect { provision_service.provision(platform, inventory, vars, retry_attempts) }.to raise_error(RuntimeError) 98 | end 99 | end 100 | 101 | context 'when successive retry success' do 102 | it 'return valid response' do 103 | stub_request(:post, 'https://facade-release-6f3kfepqcq-ew.a.run.app/v1/provision') 104 | .with( 105 | body: '{"url":"https://api.github.com/repos/puppetlabs/puppetlabs-iis/actions/runs/1234567890","VMs":[{"cloud":null,"region":null,"zone":null,"images":["centos-8"]}]}', 106 | headers: { 107 | 'Accept' => 'application/json', 108 | 'Accept-Encoding' => 'gzip;q=1.0,deflate;q=0.6,identity;q=0.3', 109 | 'Content-Type' => 'application/json', 110 | 'Host' => 'facade-release-6f3kfepqcq-ew.a.run.app', 111 | 'User-Agent' => 'Ruby' 112 | }, 113 | ) 114 | .to_return(status: 200, body: '', headers: {}) 115 | expect { provision_service.provision(platform, inventory, vars, retry_attempts) }.to raise_error(RuntimeError) 116 | stub_request(:post, 'https://facade-release-6f3kfepqcq-ew.a.run.app/v1/provision') 117 | .with( 118 | body: '{"url":"https://api.github.com/repos/puppetlabs/puppetlabs-iis/actions/runs/1234567890","VMs":[{"cloud":null,"region":null,"zone":null,"images":["centos-8"]}]}', 119 | headers: { 120 | 'Accept' => 'application/json', 121 | 'Accept-Encoding' => 'gzip;q=1.0,deflate;q=0.6,identity;q=0.3', 122 | 'Content-Type' => 'application/json', 123 | 'Host' => 'facade-release-6f3kfepqcq-ew.a.run.app', 124 | 'User-Agent' => 'Ruby' 125 | }, 126 | ) 127 | .to_return(status: 200, body: response_body.to_json, headers: {}) 128 | allow(File).to receive(:open) 129 | expect(provision_service.provision(platform, inventory, vars, retry_attempts)[:status]).to eq('ok') 130 | end 131 | end 132 | 133 | context 'when response is valid' do 134 | it 'return valid response' do 135 | stub_request(:post, 'https://facade-release-6f3kfepqcq-ew.a.run.app/v1/provision') 136 | .with( 137 | body: '{"url":"https://api.github.com/repos/puppetlabs/puppetlabs-iis/actions/runs/1234567890","VMs":[{"cloud":null,"region":null,"zone":null,"images":["centos-8"]}]}', 138 | headers: { 139 | 'Accept' => 'application/json', 140 | 'Accept-Encoding' => 'gzip;q=1.0,deflate;q=0.6,identity;q=0.3', 141 | 'Content-Type' => 'application/json', 142 | 'Host' => 'facade-release-6f3kfepqcq-ew.a.run.app', 143 | 'User-Agent' => 'Ruby' 144 | }, 145 | ) 146 | .to_return(status: 200, body: response_body.to_json, headers: {}) 147 | 148 | allow(File).to receive(:open) 149 | expect(provision_service.provision(platform, inventory, vars, retry_attempts)[:status]).to eq('ok') 150 | end 151 | end 152 | end 153 | end 154 | -------------------------------------------------------------------------------- /spec/tasks/lxd_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'fileutils' 4 | require 'spec_helper' 5 | require 'webmock/rspec' 6 | require_relative '../../tasks/lxd' 7 | require 'yaml' 8 | 9 | RSpec::Matchers.define_negated_matcher :not_raise_error, :raise_error 10 | 11 | describe 'provision::lxd' do 12 | include_context('with tmpdir') 13 | 14 | let(:lxd) { LXDProvision.new } 15 | 16 | let(:inventory_file) { tmpdir } 17 | let(:inventory) { InventoryHelper.open(inventory_file) } 18 | 19 | let(:provision_input) do 20 | { 21 | action: 'provision', 22 | platform: 'images:foobar/1', 23 | inventory: tmpdir 24 | } 25 | end 26 | let(:tear_down_input) do 27 | { 28 | action: 'tear_down', 29 | node_name: container_id, 30 | inventory: tmpdir 31 | } 32 | end 33 | 34 | let(:lxd_config_show) do 35 | <<-YAML 36 | architecture: x86_64 37 | config: 38 | image.architecture: amd64 39 | image.description: Almalinux 9 amd64 (20240515_23:08) 40 | image.os: Almalinux 41 | image.release: "9" 42 | image.requirements.cdrom_agent: "true" 43 | image.serial: "20240515_23:08" 44 | image.type: disk-kvm.img 45 | image.variant: default 46 | limits.cpu: "2" 47 | limits.memory: 4GB 48 | raw.idmap: |- 49 | uid 1000 1000 50 | gid 1000 1000 51 | volatile.apply_template: create 52 | volatile.base_image: 980e4586bcb618732801ee5ef36bbb7c11beaad4a56862938701354c18b6e706 53 | volatile.cloud-init.instance-id: dbecd5bc-252b-4d4a-a7c5-9fd4c5e39be0 54 | volatile.eth0.hwaddr: 00:16:3e:96:41:fb 55 | volatile.uuid: ccbca107-16bb-450e-9afe-d77e4d100f4b 56 | volatile.uuid.generation: ccbca107-16bb-450e-9afe-d77e4d100f4b 57 | devices: 58 | eth0: 59 | name: eth0 60 | network: incusbr-1000 61 | type: nic 62 | root: 63 | path: / 64 | pool: local 65 | type: disk 66 | ephemeral: false 67 | profiles: 68 | - default 69 | stateful: false 70 | description: "" 71 | YAML 72 | end 73 | 74 | let(:lxd_remote) { 'fake' } 75 | let(:lxd_flags) { [] } 76 | let(:lxd_platform) { nil } 77 | let(:container_id) { lxd_init_output } 78 | let(:lxd_init_output) { 'random-host' } 79 | 80 | let(:provision_output) do 81 | { 82 | status: 'ok', 83 | node_name: container_id, 84 | node: { 85 | uri: container_id, 86 | config: { 87 | transport: 'lxd', 88 | lxd: { 89 | remote: lxd_remote, 90 | 'shell-command': 'sh -lc' 91 | } 92 | }, 93 | facts: { 94 | provisioner: 'lxd', 95 | container_id:, 96 | platform: lxd_platform 97 | } 98 | } 99 | } 100 | end 101 | 102 | let(:tear_down_output) do 103 | { 104 | status: 'ok', 105 | } 106 | end 107 | 108 | describe '.run' do 109 | let(:task_input) { {} } 110 | let(:imposter) { instance_double('LXDProvision') } 111 | 112 | task_tests = [ 113 | [ { action: 'provision', platform: 'test' }, 'success', true ], 114 | [ { action: 'provision', platform: 'test', vm: true }, 'success', true ], 115 | [ { action: 'provision', node_name: 'test' }, 'do not specify node_name', false ], 116 | [ { action: 'provision' }, 'platform required', false ], 117 | [ { action: 'tear_down', node_name: 'test' }, 'success', true ], 118 | [ { action: 'tear_down' }, 'node_name required', false ], 119 | [ { action: 'tear_down', platform: 'test' }, 'do not specify platform', false ], 120 | ] 121 | 122 | task_tests.each do |v| 123 | it "expect arguments '#{v[0]}' return '#{v[1]}'#{v[2] ? '' : ' and raise error'}" do 124 | allow(LXDProvision).to receive(:new).and_return(imposter) 125 | allow(imposter).to receive(:task).and_return(v[1]) 126 | allow($stdin).to receive(:read).and_return(v[0].to_json) 127 | if v[2] 128 | expect { LXDProvision.run }.to output(%r{#{v[1]}}).to_stdout 129 | else 130 | expect { LXDProvision.run }.to output(%r{#{v[1]}}).to_stdout.and raise_error(SystemExit) 131 | end 132 | end 133 | end 134 | end 135 | 136 | describe '.task' do 137 | context 'action=provision' do 138 | let(:lxd_platform) { provision_input[:platform] } 139 | 140 | before(:each) do 141 | expect(lxd).to receive(:run_local_command) 142 | .with('lxc -q remote get-default').and_return(lxd_remote) 143 | expect(lxd).to receive(:run_local_command) 144 | .with("lxc -q create #{lxd_platform} #{lxd_remote}: #{lxd_flags.join(' ')}").and_return(lxd_init_output) 145 | expect(lxd).to receive(:run_local_command) 146 | .with("lxc -q config show #{lxd_remote}:#{container_id} -e").and_return(lxd_config_show) 147 | if lxd_config_show.match?(%r{image\.requirements\.cdrom_agent:.*true}) 148 | expect(lxd).to receive(:run_local_command) 149 | .with("lxc -q config device add #{lxd_remote}:#{container_id} agent disk source=agent:config").and_return(lxd_config_show) 150 | end 151 | expect(lxd).to receive(:run_local_command) 152 | .with("lxc -q start #{lxd_remote}:#{container_id}").and_return(lxd_init_output) 153 | end 154 | 155 | it 'provisions successfully' do 156 | expect(lxd).to receive(:run_local_command) 157 | .with("lxc -q exec #{lxd_remote}:#{container_id} uptime") 158 | 159 | expect(lxd.task(**provision_input)).to eq(provision_output) 160 | end 161 | 162 | it 'when retries=0 try once but ignore the raised error' do 163 | provision_input[:retries] = 0 164 | 165 | expect(lxd).to receive(:run_local_command) 166 | .with("lxc -q exec #{lxd_remote}:#{container_id} uptime").and_raise(StandardError) 167 | 168 | expect(lxd.task(**provision_input)).to eq(provision_output) 169 | end 170 | 171 | it 'max retries then deletes the instance' do 172 | expect(lxd).to receive(:run_local_command) 173 | .exactly(3).times 174 | .with("lxc -q exec #{lxd_remote}:#{container_id} uptime").and_raise(StandardError) 175 | expect(lxd).to receive(:run_local_command) 176 | .with("lxc -q delete #{lxd_remote}:#{container_id} -f") 177 | 178 | expect { lxd.task(**provision_input) }.to raise_error(StandardError, %r{Giving up waiting for #{lxd_remote}:#{container_id}}) 179 | end 180 | end 181 | 182 | context 'action=tear_down' do 183 | it 'tears down successfully' do 184 | inventory.add(provision_output[:node], 'lxd_nodes').save 185 | 186 | expect(lxd).to receive(:run_local_command) 187 | .with("lxc -q delete #{lxd_remote}:#{container_id} -f") 188 | 189 | expect(lxd.task(**tear_down_input)).to eq(tear_down_output) 190 | end 191 | 192 | it 'expect to raise error if no inventory' do 193 | expect { lxd.task(**tear_down_input) }.to raise_error(RuntimeError, %r{Failed to lookup target #{container_id}}) 194 | end 195 | 196 | it 'expect to raise error if node_name not in inventory' do 197 | inventory.save 198 | expect { lxd.task(**tear_down_input) }.to raise_error(RuntimeError, %r{Failed to lookup target #{container_id}}) 199 | end 200 | end 201 | end 202 | end 203 | -------------------------------------------------------------------------------- /tasks/provision_service.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | require 'json' 5 | require 'net/http' 6 | require 'yaml' 7 | require 'etc' 8 | require_relative '../lib/task_helper' 9 | require_relative '../lib/inventory_helper' 10 | 11 | # Provision and teardown vms through provision service. 12 | class ProvisionService 13 | RETRY_COUNT = 3 14 | 15 | def default_uri 16 | 'https://facade-release-6f3kfepqcq-ew.a.run.app/v1/provision' 17 | end 18 | 19 | def platform_to_cloud_request_parameters(platform, cloud, region, zone) 20 | case platform 21 | when String 22 | { cloud:, region:, zone:, images: [platform] } 23 | when Array 24 | { cloud:, region:, zone:, images: platform } 25 | else 26 | platform[:cloud] = cloud unless cloud.nil? 27 | platform[:images] = [platform[:images]] if platform[:images].is_a?(String) 28 | platform 29 | end 30 | end 31 | 32 | # curl -X POST https://facade-validation-6f3kfepqcq-ew.a.run.app/v1/provision --data @test_machines.json 33 | def invoke_cloud_request(params, uri, job_url, verb, retry_attempts) 34 | headers = { 35 | 'Accept' => 'application/json', 36 | 'Content-Type' => 'application/json' 37 | } 38 | 39 | case verb.downcase 40 | when 'post' 41 | request = Net::HTTP::Post.new(uri, headers) 42 | machines = [] 43 | machines << params 44 | request.body = if job_url 45 | { url: job_url, VMs: machines }.to_json 46 | else 47 | { github_token: ENV.fetch('GITHUB_TOKEN', nil), VMs: machines }.to_json 48 | end 49 | when 'delete' 50 | request = Net::HTTP::Delete.new(uri, headers) 51 | request.body = { uuid: params }.to_json 52 | else 53 | raise StandardError "Unknown verb: '#{verb}'" 54 | end 55 | 56 | if job_url 57 | File.open('request.json', 'wb') do |f| 58 | f.write(request.body) 59 | end 60 | end 61 | 62 | req_options = { 63 | use_ssl: uri.scheme == 'https', 64 | read_timeout: 60 * 5, # timeout reads after 5 minutes - that's longer than the backend service would keep the request open 65 | max_retries: retry_attempts # retry up to 5 times before throwing an error 66 | } 67 | 68 | response = Net::HTTP.start(uri.hostname, uri.port, req_options) do |http| 69 | http.request(request) 70 | end 71 | if response.code == '200' 72 | response.body 73 | else 74 | begin 75 | body = JSON.parse(response.body) 76 | body_json = true 77 | rescue JSON::ParserError 78 | body = response.body 79 | body_json = false 80 | end 81 | puts({ _error: { kind: 'provision_service/service_error', msg: 'provision service returned an error', code: response.code, body:, body_json: } }.to_json) 82 | exit 1 83 | end 84 | end 85 | 86 | def provision(platform, inventory, vars, retry_attempts) 87 | # Call the provision service with the information necessary and write the inventory file locally 88 | 89 | if ENV['GITHUB_RUN_ID'] 90 | job_url = ENV['GITHUB_URL'] || "https://api.github.com/repos/#{ENV.fetch('GITHUB_REPOSITORY', nil)}/actions/runs/#{ENV['GITHUB_RUN_ID']}" 91 | else 92 | puts 'Using GITHUB_TOKEN as no GITHUB_RUN_ID found' 93 | end 94 | uri = URI.parse(ENV['SERVICE_URL'] || default_uri) 95 | cloud = ENV.fetch('CLOUD', nil) 96 | region = ENV.fetch('REGION', nil) 97 | zone = ENV.fetch('ZONE', nil) 98 | if job_url.nil? && vars 99 | data = JSON.parse(vars.tr(';', ',')) 100 | job_url = data['job_url'] 101 | end 102 | currnet_retry_count = 0 103 | begin 104 | params = platform_to_cloud_request_parameters(platform, cloud, region, zone) 105 | response = invoke_cloud_request(params, uri, job_url, 'post', retry_attempts) 106 | response_hash = YAML.safe_load(response) 107 | # Knock the response for validity to make sure return payload is expected. 108 | # Have seen multiple occurances of nil:NilClass error where the response code is 200 but return payload is empty 109 | raise if response_hash.nil? || response_hash.empty? 110 | rescue StandardError => e 111 | currnet_retry_count += 1 112 | raise e if currnet_retry_count >= RETRY_COUNT 113 | 114 | puts "Failed while provisioning the resource with response :\n #{response_hash}\nHence retrying #{currnet_retry_count} of #{RETRY_COUNT}" 115 | retry 116 | end 117 | 118 | unless vars.nil? 119 | var_hash = YAML.safe_load(vars) 120 | end 121 | 122 | response_hash['groups'].each do |bg| 123 | bg['targets'].each do |trgts| 124 | trgts['vars'] = var_hash if var_hash 125 | inventory.add(trgts, bg['name']) 126 | end 127 | end 128 | inventory.save 129 | 130 | { 131 | status: 'ok', 132 | node_name: platform, 133 | target_names: response_hash['groups']&.each { |g| g['targets'] }&.map { |t| t['uri'] }&.flatten&.uniq 134 | } 135 | end 136 | 137 | def tear_down(node_name, inventory, _vars, retry_attempts) 138 | # remove all provisioned resources 139 | uri = URI.parse(ENV['SERVICE_URL'] || default_uri) 140 | 141 | node = inventory.lookup(node_name) 142 | facts = node['facts'] 143 | job_id = facts['uuid'] 144 | response = invoke_cloud_request(job_id, uri, '', 'delete', retry_attempts) 145 | response.to_json 146 | end 147 | 148 | # Runs the provision or tear_down action based on the provided parameters. 149 | # Expects the following parameters in JSON format from stdin: 150 | # - action: The action to perform ('provision' or 'tear_down'). 151 | # - node_name: The name of the node to provision or tear down. 152 | # - platform: The platform to provision. 153 | # - vars: Additional variables to assign to nodes. 154 | # - retry_attempts: The number of retry attempts for provisioning or tearing down. 155 | # - inventory_location: The location of the inventory file. If not provided, defaults to './spec/fixtures/litmus_inventory.yaml'. 156 | # The result of the action is printed to stdout in JSON format. 157 | # Exits with status 0 on success, or 1 on failure. 158 | def self.run 159 | params = JSON.parse($stdin.read) 160 | params.transform_keys!(&:to_sym) 161 | action, node_name, platform, vars, retry_attempts, inventory_location = params.values_at(:action, :node_name, :platform, :vars, :retry_attempts, :inventory) 162 | 163 | inventory_location ||= File.join(Dir.pwd, '/spec/fixtures/litmus_inventory.yaml') 164 | inventory = InventoryHelper.open(inventory_location) 165 | 166 | runner = new 167 | begin 168 | case action 169 | when 'provision' 170 | raise 'specify a platform when provisioning' if platform.to_s.empty? 171 | 172 | result = runner.provision(platform, inventory, vars, retry_attempts) 173 | when 'tear_down' 174 | raise 'specify a node_name when tearing down' if node_name.nil? 175 | 176 | result = runner.tear_down(node_name, inventory, vars, retry_attempts) 177 | else 178 | result = { _error: { kind: 'provision_service/argument_error', msg: "Unknown action '#{action}'" } } 179 | end 180 | puts result.to_json 181 | exit 0 182 | rescue StandardError => e 183 | puts({ _error: { kind: 'provision_service/failure', msg: e.message, details: { backtrace: e.backtrace } } }.to_json) 184 | exit 1 185 | end 186 | end 187 | end 188 | 189 | ProvisionService.run if __FILE__ == $PROGRAM_NAME 190 | -------------------------------------------------------------------------------- /tasks/abs.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | require 'json' 5 | require 'net/http' 6 | require 'yaml' 7 | require 'etc' 8 | require 'date' 9 | require_relative '../lib/task_helper' 10 | require_relative '../lib/inventory_helper' 11 | 12 | # Provision and teardown vms through ABS. 13 | class ABSProvision 14 | # Enforces a k8s.infracore.puppet.net domain, but allows selection of prod, 15 | # stage, etc hostname from the environment variable +ABS_SUBDOMAIN+ so that 16 | # CI can test vms from staging. 17 | # 18 | # Defaults to abs-prod.k8s.infracore.puppet.net. 19 | def abs_host 20 | subdomain = ENV['ABS_SUBDOMAIN'] || 'abs-prod' 21 | "#{subdomain}.k8s.infracore.puppet.net" 22 | end 23 | 24 | def provision(platform, inventory, vars) 25 | uri = URI.parse("https://#{abs_host}/api/v2/request") 26 | jenkins_build_url = if ENV['CI'] == 'true' && ENV['TRAVIS'] == 'true' 27 | ENV.fetch('TRAVIS_JOB_WEB_URL', nil) 28 | elsif ENV['CI'] == 'True' && ENV['APPVEYOR'] == 'True' 29 | "https://ci.appveyor.com/project/#{ENV.fetch('APPVEYOR_REPO_NAME', nil)}/build/job/#{ENV.fetch('APPVEYOR_JOB_ID', nil)}" 30 | elsif ENV['GITHUB_ACTIONS'] == 'true' 31 | "https://github.com/#{ENV.fetch('GITHUB_REPOSITORY', nil)}/actions/runs/#{ENV.fetch('GITHUB_RUN_ID', nil)}" 32 | else 33 | 'https://litmus_manual' 34 | end 35 | poll_duration = ENV['POLL_ABS_TIMEOUT_SECONDS'] || 600 36 | 37 | # Job ID must be unique 38 | job_id = "iac-task-pid-#{Process.pid}-#{DateTime.now.strftime('%Q')}" 39 | 40 | headers = { 'X-AUTH-TOKEN' => token_from_fogfile('abs'), 'Content-Type' => 'application/json' } 41 | priority = ENV['CI'] ? 1 : 2 42 | payload = if platform.instance_of?(String) 43 | { 'resources' => { platform => 1 }, 44 | 'priority' => priority, 45 | 'job' => { 'id' => job_id, 46 | 'tags' => { 'user' => Etc.getlogin, 'jenkins_build_url' => jenkins_build_url } } } 47 | else 48 | { 'resources' => platform, 49 | 'priority' => priority, 50 | 'job' => { 'id' => job_id, 51 | 'tags' => { 'user' => Etc.getlogin, 'jenkins_build_url' => jenkins_build_url } } } 52 | end 53 | http = Net::HTTP.new(uri.host, uri.port) 54 | http.use_ssl = true 55 | request = Net::HTTP::Post.new(uri.request_uri, headers) 56 | request.body = payload.to_json 57 | 58 | # Make an initial request - we should receive a 202 response to indicate the request is being processed 59 | reply = http.request(request) 60 | # Use this 'puts' only for debugging purposes 61 | # Do not use this in production mode because puppet_litmus will parse the STDOUT to extract the results 62 | # puts "#{Time.now.strftime('%Y/%m/%d %H:%M:%S')}: Received: #{reply.code} #{reply.message} from ABS" 63 | raise "Error: #{reply}: #{reply.message}" unless reply.is_a?(Net::HTTPAccepted) # should be a 202 64 | 65 | # We want to then poll the API until we get a 200 response, indicating the VMs have been provisioned 66 | timeout = Time.now.to_i + poll_duration.to_i # Let's poll the API for a max of poll_duration seconds 67 | sleep_time = 1 68 | 69 | # Progressively increase the sleep time by 1 second. When we hit 10 seconds, start querying every 30 seconds until we 70 | # exceed the time out. This is an attempt to strike a balance between quick provisioning and not saturating the ABS 71 | # API and network if it's taking longer to provision than usual 72 | while Time.now.to_i < timeout 73 | sleep (sleep_time <= 10) ? sleep_time : 30 74 | reply = http.request(request) 75 | # Use this 'puts' only for debugging purposes 76 | # Do not use this in production mode because puppet_litmus will parse the STDOUT to extract the results 77 | # puts "#{Time.now.strftime('%Y/%m/%d %H:%M:%S')}: Received #{reply.code} #{reply.message} from ABS" 78 | break if reply.code == '200' # Our host(s) are provisioned 79 | raise 'ABS API Error: Received a HTTP 404 response' if reply.code == '404' # Our host(s) will never be provisioned 80 | 81 | sleep_time += 1 82 | end 83 | 84 | raise "Timeout: unable to get a 200 response in #{poll_duration} seconds" if reply.code != '200' 85 | 86 | data = JSON.parse(reply.body) 87 | data.each do |host| 88 | if platform_uses_ssh(host['type']) 89 | node = { 'uri' => host['hostname'], 90 | 'config' => { 'transport' => 'ssh', 'ssh' => { 'user' => ENV.fetch('ABS_USER', nil), 'host-key-check' => false, 'connect-timeout' => 120 } }, 91 | 'facts' => { 'provisioner' => 'abs', 'platform' => host['type'], 'job_id' => job_id } } 92 | if !ENV['ABS_SSH_PRIVATE_KEY'].nil? && !ENV['ABS_SSH_PRIVATE_KEY'].empty? 93 | node['config']['ssh']['private-key'] = ENV.fetch('ABS_SSH_PRIVATE_KEY', nil) 94 | else 95 | node['config']['ssh']['password'] = ENV.fetch('ABS_PASSWORD', nil) 96 | end 97 | group_name = 'ssh_nodes' 98 | else 99 | node = { 'uri' => host['hostname'], 100 | 'config' => { 'transport' => 'winrm', 101 | 'winrm' => { 'user' => ENV.fetch('ABS_WIN_USER', nil), 'password' => ENV.fetch('ABS_PASSWORD', nil), 'ssl' => false, 'connect-timeout' => 120 } }, 102 | 'facts' => { 'provisioner' => 'abs', 'platform' => host['type'], 'job_id' => job_id } } 103 | group_name = 'winrm_nodes' 104 | end 105 | unless vars.nil? 106 | var_hash = YAML.safe_load(vars) 107 | node['vars'] = var_hash 108 | end 109 | inventory.add(node, group_name) 110 | end 111 | 112 | inventory.save 113 | { status: 'ok', nodes: data.length } 114 | end 115 | 116 | def tear_down(node_name, inventory) 117 | node = inventory.lookup(node_name, group: 'ssh_nodes') 118 | 119 | targets_to_remove = [] 120 | inventory['groups'].each do |group| 121 | group['targets'].each do |job_node| 122 | targets_to_remove.push(job_node) if job_node['facts']['job_id'] == node['facts']['job_id'] 123 | end 124 | end 125 | 126 | uri = URI.parse("https://#{abs_host}/api/v2/return") 127 | headers = { 'X-AUTH-TOKEN' => token_from_fogfile('abs'), 'Content-Type' => 'application/json' } 128 | payload = { 'job_id' => node['facts']['job_id'], 129 | 'hosts' => [{ 'hostname' => node['uri'], 'type' => node['facts']['platform'], 'engine' => 'vmpooler' }] } 130 | http = Net::HTTP.new(uri.host, uri.port) 131 | http.use_ssl = true 132 | request = Net::HTTP::Post.new(uri.request_uri, headers) 133 | request.body = payload.to_json 134 | 135 | reply = http.request(request) 136 | raise "Error: #{reply}: #{reply.message}" unless reply.code == '200' 137 | 138 | targets_to_remove.each do |target| 139 | inventory.remove(target) 140 | end 141 | inventory.save 142 | 143 | { status: 'ok', removed: targets_to_remove.map { |t| t['name'] || t['uri'] } } 144 | end 145 | 146 | def task(action:, platform: nil, node_name: nil, inventory: nil, vars: nil, **_kwargs) 147 | inventory = InventoryHelper.open(inventory) 148 | result = provision(platform, inventory, vars) if action == 'provision' 149 | result = tear_down(node_name, inventory) if action == 'tear_down' 150 | result 151 | end 152 | 153 | def self.run 154 | params = JSON.parse($stdin.read) 155 | params.transform_keys!(&:to_sym) 156 | action, node_name, platform = params.values_at(:action, :node_name, :platform) 157 | 158 | raise 'specify a node_name when tearing down' if action == 'tear_down' && node_name.nil? 159 | raise 'specify a platform when provisioning' if action == 'provision' && platform.nil? 160 | 161 | unless node_name.nil? ^ platform.nil? 162 | case action 163 | when 'tear_down' 164 | raise 'specify only a node_name, not platform, when tearing down' 165 | when 'provision' 166 | raise 'specify only a platform, not node_name, when provisioning' 167 | else 168 | raise 'specify only one of: node_name, platform' 169 | end 170 | end 171 | 172 | begin 173 | runner = new 174 | result = runner.task(**params) 175 | puts result.to_json 176 | exit 0 177 | rescue StandardError => e 178 | puts({ _error: { kind: 'provision/abs_failure', msg: e.message } }.to_json) 179 | exit 1 180 | end 181 | end 182 | end 183 | 184 | ABSProvision.run if __FILE__ == $PROGRAM_NAME 185 | -------------------------------------------------------------------------------- /tasks/docker.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | require 'json' 5 | require 'uri' 6 | require 'yaml' 7 | require_relative '../lib/task_helper' 8 | require_relative '../lib/docker_helper' 9 | require_relative '../lib/inventory_helper' 10 | 11 | def install_ssh_components(distro, version, container) 12 | case distro 13 | when %r{debian}, %r{ubuntu}, %r{cumulus} 14 | warn '!!! Disabling ESM security updates for ubuntu - no access without privilege !!!' 15 | docker_exec(container, 'rm -f /etc/apt/sources.list.d/ubuntu-esm-infra-trusty.list') 16 | docker_exec(container, 'apt-get update') 17 | docker_exec(container, 'apt-get install -y openssh-server openssh-client') 18 | when %r{centos}, %r{^el-}, %r{eos}, %r{oracle}, %r{ol}, %r{rhel|redhat}, %r{scientific}, %r{amzn}, %r{rocky}, %r{almalinux}, %r{fedora} 19 | if version == '6' 20 | # sometimes the redhat 6 variant containers like to eat their rpmdb, leading to 21 | # issues with "rpmdb: unable to join the environment" errors 22 | # This "fix" is from https://www.srv24x7.com/criticalyum-main-error-rpmdb-open-failed/ 23 | docker_exec(container, 'bash -exc "rm -f /var/lib/rpm/__db*; ' \ 24 | 'db_verify /var/lib/rpm/Packages; ' \ 25 | 'rpm --rebuilddb; ' \ 26 | 'yum clean all"') 27 | else 28 | # If systemd is running for init, ensure systemd has finished starting up before proceeding: 29 | check_init_cmd = 'if [[ "$(readlink /proc/1/exe)" == "/usr/lib/systemd/systemd" ]]; then ' \ 30 | 'count=0 ; while ! [[ "$(systemctl is-system-running)" =~ ^running|degraded$ && $count > 20 ]]; ' \ 31 | 'do sleep 0.1 ; count=$((count+1)) ; done ; fi' 32 | docker_exec(container, "bash -c '#{check_init_cmd}'") 33 | end 34 | packager = (version.to_i > 8) ? 'dnf' : 'yum' 35 | docker_exec(container, "#{packager} install -y sudo openssh-server openssh-clients") 36 | ssh_folder = docker_exec(container, 'ls /etc/ssh/') 37 | docker_exec(container, 'ssh-keygen -t rsa -f /etc/ssh/ssh_host_rsa_key -N ""') unless ssh_folder.include?('ssh_host_rsa_key') 38 | docker_exec(container, 'ssh-keygen -t dsa -f /etc/ssh/ssh_host_dsa_key -N ""') unless ssh_folder.include?('ssh_host_dsa_key') 39 | when %r{opensuse}, %r{sles} 40 | docker_exec(container, 'zypper -n in openssh') 41 | docker_exec(container, 'ssh-keygen -A') 42 | docker_exec(container, 'sed -ri "s/^#?UsePAM .*/UsePAM no/" /etc/ssh/sshd_config') 43 | else 44 | raise "distribution #{distro} not yet supported on docker" 45 | end 46 | 47 | # Make sshd directory, set root password 48 | docker_exec(container, 'mkdir -p /var/run/sshd') 49 | docker_exec(container, 'bash -c "echo root:root | /usr/sbin/chpasswd"') 50 | 51 | # fix ssh 52 | docker_exec(container, 'sed -ri "s/^#?PermitRootLogin .*/PermitRootLogin yes/" /etc/ssh/sshd_config') 53 | docker_exec(container, 'sed -ri "s/^#?PasswordAuthentication .*/PasswordAuthentication yes/" /etc/ssh/sshd_config') 54 | docker_exec(container, 'sed -ri "s/^#?UseDNS .*/UseDNS no/" /etc/ssh/sshd_config') 55 | docker_exec(container, 'sed -e "/HostKey.*ssh_host_e.*_key/ s/^#*/#/" -ri /etc/ssh/sshd_config') 56 | # Current RedHat/CentOs 7 packs an old version of pam, which are missing a 57 | # crucial patch when running unprivileged containers. See: 58 | # https://bugzilla.redhat.com/show_bug.cgi?id=1728777 59 | docker_exec(container, 'sed "s@session\s*required\s*pam_loginuid.so@session optional pam_loginuid.so@g" -i /etc/pam.d/sshd') \ 60 | if distro =~ %r{rhel|redhat|centos} && version =~ %r{^7} 61 | 62 | # install ssh 63 | case distro 64 | when %r{debian}, %r{ubuntu} 65 | docker_exec(container, 'service ssh restart') 66 | when %r{centos}, %r{^el-}, %r{eos}, %r{fedora}, %r{ol}, %r{rhel|redhat}, %r{scientific}, %r{amzn}, %r{rocky}, %r{almalinux} 67 | if %r{^(7|8|9|2)}.match?(version) 68 | docker_exec(container, '/usr/sbin/sshd') 69 | else 70 | docker_exec(container, 'service sshd restart') 71 | end 72 | when %r{opensuse}, %r{sles} 73 | docker_exec(container, '/usr/sbin/sshd') 74 | else 75 | raise "distribution #{distro} not yet supported on docker" 76 | end 77 | end 78 | 79 | # We check for a local port open by binding a raw socket to it 80 | # If the socket can successfully bind, then the port is open 81 | def local_port_open?(port) 82 | require 'socket' 83 | require 'timeout' 84 | Timeout.timeout(1) do 85 | socket = Socket.new(Socket::Constants::AF_INET, 86 | Socket::Constants::SOCK_STREAM, 87 | 0) 88 | socket.bind(Socket.pack_sockaddr_in(port, '0.0.0.0')) 89 | true 90 | rescue Errno::EADDRINUSE, Errno::ECONNREFUSED 91 | false 92 | ensure 93 | socket.close 94 | end 95 | rescue Timeout::Error 96 | false 97 | end 98 | 99 | # These defaults are arbitrary but outside the well-known range 100 | def random_ssh_forwarding_port(start_port = 52_222, end_port = 52_999) 101 | raise 'start_port must be less than end_port' if start_port >= end_port 102 | 103 | # This stops us from potentially allocating an invalid port 104 | raise 'Could not find an open port to use for SSH forwarding' if end_port > 65_535 105 | 106 | port = rand(start_port..end_port) 107 | return port if local_port_open?(port) 108 | 109 | # Try again but bump up the port ranges 110 | # Since we thrown an exception above if the end port is > 65535, 111 | # there is a hard limit to the amount of times we can retry depending 112 | # on the start port and the diff between the end port and the start port. 113 | port_diff = end_port - start_port 114 | new_start_port = start_port + port_diff + 1 115 | new_end_port = end_port + port_diff + 1 116 | random_ssh_forwarding_port(new_start_port, new_end_port) 117 | end 118 | 119 | def provision(docker_platform, inventory, vars) 120 | os_release_facts = docker_image_os_release_facts(docker_platform) 121 | distro = os_release_facts['ID'] 122 | version = os_release_facts['VERSION_ID'] 123 | 124 | # support for ssh to remote docker 125 | hostname = (ENV['DOCKER_HOST'].nil? || ENV['DOCKER_HOST'].empty?) ? 'localhost' : URI.parse(ENV.fetch('DOCKER_HOST', nil)).host || ENV.fetch('DOCKER_HOST', nil) 126 | begin 127 | # Use the current docker context to determine the docker hostname 128 | docker_context = JSON.parse(run_local_command('docker context inspect'))[0] 129 | docker_uri = URI.parse(docker_context['Endpoints']['docker']['Host']) 130 | hostname = docker_uri.host unless docker_uri.host.nil? || docker_uri.host.empty? 131 | rescue RuntimeError 132 | # old clients may not support docker context 133 | end 134 | 135 | warn '!!! Using private port forwarding!!!' 136 | front_facing_port = random_ssh_forwarding_port 137 | 138 | inventory_node = { 139 | 'uri' => "#{hostname}:#{front_facing_port}", 140 | 'alias' => "#{hostname}:#{front_facing_port}", 141 | 'config' => { 142 | 'transport' => 'ssh', 143 | 'ssh' => { 144 | 'user' => 'root', 145 | 'password' => 'root', 146 | 'port' => front_facing_port, 147 | 'host-key-check' => false, 148 | 'connect-timeout' => 120 149 | } 150 | }, 151 | 'facts' => { 152 | 'provisioner' => 'docker', 153 | 'platform' => docker_platform, 154 | 'os-release' => os_release_facts 155 | } 156 | } 157 | 158 | docker_run_opts = '' 159 | unless vars.nil? 160 | var_hash = YAML.safe_load(vars) 161 | inventory_node['vars'] = var_hash 162 | docker_run_opts = var_hash['docker_run_opts'].flatten.join(' ') unless var_hash['docker_run_opts'].nil? 163 | end 164 | 165 | if docker_platform.match?(%r{debian|ubuntu}) 166 | docker_run_opts += ' --volume /sys/fs/cgroup:/sys/fs/cgroup:rw' unless docker_run_opts.include?('--volume /sys/fs/cgroup:/sys/fs/cgroup') 167 | docker_run_opts += ' --cgroupns=host' unless docker_run_opts.include?('--cgroupns') 168 | end 169 | 170 | creation_command = 'docker run -d -it --privileged --tmpfs /tmp:exec ' 171 | creation_command += "-p #{front_facing_port}:22 " 172 | creation_command += "#{docker_run_opts} " unless docker_run_opts.nil? 173 | creation_command += docker_platform 174 | 175 | container_id = run_local_command(creation_command).strip[0..11] 176 | 177 | install_ssh_components(distro, version, container_id) 178 | 179 | inventory_node['name'] = container_id 180 | inventory_node['facts']['container_id'] = container_id 181 | 182 | inventory.add(inventory_node, 'ssh_nodes').save 183 | 184 | { status: 'ok', node_name: inventory_node['name'], node: inventory_node } 185 | end 186 | 187 | params = JSON.parse($stdin.read) 188 | platform = params['platform'] 189 | action = params['action'] 190 | node_name = params['node_name'] 191 | inventory = InventoryHelper.open(params['inventory']) 192 | vars = params['vars'] 193 | raise 'specify a node_name when tearing down' if action == 'tear_down' && node_name.nil? 194 | raise 'specify a platform when provisioning' if action == 'provision' && platform.nil? 195 | 196 | unless node_name.nil? ^ platform.nil? 197 | case action 198 | when 'tear_down' 199 | raise 'specify only a node_name, not platform, when tearing down' 200 | when 'provision' 201 | raise 'specify only a platform, not node_name, when provisioning' 202 | else 203 | raise 'specify only one of: node_name, platform' 204 | end 205 | end 206 | 207 | begin 208 | result = provision(platform, inventory, vars) if action == 'provision' 209 | if action == 'tear_down' 210 | node = inventory.lookup(node_name, group: 'ssh_nodes') 211 | result = docker_tear_down(node['facts']['container_id']) 212 | inventory.remove(node).save 213 | end 214 | puts result.to_json 215 | exit 0 216 | rescue StandardError => e 217 | puts({ _error: { kind: 'provision/docker_failure', msg: e.message, backtrace: e.backtrace } }.to_json) 218 | exit 1 219 | end 220 | -------------------------------------------------------------------------------- /tasks/vagrant.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | require 'json' 5 | require 'net/http' 6 | require 'yaml' 7 | require 'puppet_litmus' 8 | require 'fileutils' 9 | require 'net/ssh' 10 | require_relative '../lib/task_helper' 11 | require_relative '../lib/inventory_helper' 12 | 13 | def vagrant_version 14 | return @vagrant_version if defined?(@vagrant_version) 15 | 16 | @vagrant_version = begin 17 | command = 'vagrant --version' 18 | output = run_local_command(command) 19 | Gem::Version.new(output.strip.split(%r{\s+})[1]) 20 | end 21 | @vagrant_version 22 | end 23 | 24 | def supports_windows_platform? 25 | # Relies on the winrm-config command added in 2.2.0: 26 | # https://github.com/hashicorp/vagrant/blob/main/CHANGELOG.md#220-october-16-2018 27 | vagrant_version >= Gem::Version.new('2.2.0') 28 | end 29 | 30 | def generate_vagrantfile(file_path, platform, enable_synced_folder, provider, cpus, memory, hyperv_vswitch, hyperv_smb_username, hyperv_smb_password, box_url) 31 | synced_folder = 'config.vm.synced_folder ".", "/vagrant", disabled: true' unless enable_synced_folder 32 | if on_windows? 33 | # Even though this is the default value in the metadata it isn't sent along if tthe parameter is unspecified for some reason. 34 | network = "config.vm.network 'public_network', bridge: '#{hyperv_vswitch.nil? ? 'Default Switch' : hyperv_vswitch}'" 35 | if enable_synced_folder && !hyperv_smb_username.nil? && !hyperv_smb_password.nil? 36 | synced_folder = "config.vm.synced_folder '.', '/vagrant', type: 'smb', smb_username: '#{hyperv_smb_username}', smb_password: '#{hyperv_smb_password}'" 37 | end 38 | end 39 | if cpus.nil? && memory.nil? 40 | provider_config_block = '' 41 | else 42 | if provider.nil? 43 | provider = on_windows? ? 'hyperv' : 'virtualbox' 44 | end 45 | provider_config_block = <<~PCB 46 | config.vm.provider "#{provider}" do |v| 47 | #{"v.cpus = #{cpus}" unless cpus.nil?} 48 | #{"v.memory = #{memory}" unless memory.nil?} 49 | end 50 | PCB 51 | end 52 | box_url_config = if box_url 53 | "config.vm.box_url = '#{box_url.gsub('%BOX%', platform)}'" 54 | else 55 | '' 56 | end 57 | vf = <<~VF 58 | Vagrant.configure("2") do |config| 59 | config.vm.box = '#{platform}' 60 | config.vm.boot_timeout = 600 61 | config.ssh.insert_key = false 62 | #{box_url_config} 63 | #{network} 64 | #{synced_folder} 65 | #{provider_config_block} 66 | end 67 | VF 68 | File.open(file_path, 'w') do |f| 69 | f.write(vf) 70 | end 71 | end 72 | 73 | def get_vagrant_dir(platform, vagrant_dirs, int = 0) 74 | platform_dir = "#{platform}-#{int}".gsub(%r{[/\\]}, '-') # Strip slashes 75 | platform_dir = get_vagrant_dir(platform, vagrant_dirs, int + 1) if vagrant_dirs.include?(platform_dir) 76 | platform_dir 77 | end 78 | 79 | def configure_remoting(platform, remoting_config_path, password) 80 | if platform_uses_ssh(platform) 81 | command = "vagrant ssh-config > \"#{remoting_config_path}\"" 82 | run_local_command(command, @vagrant_env) 83 | remoting_config = Net::SSH::Config.load(remoting_config_path, 'default') 84 | # Pre-configure sshd on the platform prior to handing back 85 | ssh_params = { 86 | port: remoting_config['port'], 87 | keys: remoting_config['identityfile'], 88 | password:, 89 | verbose: :debug 90 | }.compact 91 | Net::SSH.start( 92 | remoting_config['hostname'], 93 | remoting_config['user'], 94 | **ssh_params, 95 | ) do |session| 96 | session.exec!('sudo su -c "cp -r .ssh /root/."') 97 | session.exec!('sudo su -c "sed -i \"s/.*PermitUserEnvironment\s.*/PermitUserEnvironment yes/g\" /etc/ssh/sshd_config"') 98 | systemctl = session.exec!('which systemctl 2>/dev/null') 99 | restart_command = if systemctl.strip.empty? 100 | # Debian and Ubuntu use 'ssh' and the EL/Suse family use 'sshd'. This will catch either. 101 | 'service ssh restart || service sshd restart' 102 | else 103 | # On Debian/Ubuntu sshd is an alias to the 'ssh' service, and on the EL/Suse families 104 | # 'sshd' is the service name, so 'sshd.service' works for all: 105 | 'systemctl restart sshd.service' 106 | end 107 | session.exec!("sudo su -c \"#{restart_command}\"") 108 | end 109 | else 110 | command = "vagrant winrm-config > \"#{remoting_config_path}\"" 111 | run_local_command(command, @vagrant_env) 112 | remoting_config = Net::SSH::Config.load(remoting_config_path, 'default') 113 | # TODO: Delete remoting_config_path as it's no longer needed 114 | # TODO: It's possible we may want to configure WinRM on the target platform beyond the defaults 115 | end 116 | remoting_config 117 | end 118 | 119 | def provision(platform, inventory, enable_synced_folder, provider, cpus, memory, hyperv_vswitch, hyperv_smb_username, hyperv_smb_password, box_url, password, vars) 120 | if platform_is_windows?(platform) && !supports_windows_platform? 121 | raise "To provision a Windows VM with this task you must have vagrant 2.2.0 or later installed; vagrant seems to be installed at v#{vagrant_version}" 122 | end 123 | 124 | if provider.nil? 125 | provider = on_windows? ? 'hyperv' : 'virtualbox' 126 | end 127 | 128 | vagrant_dirs = Dir.glob("#{File.join(File.dirname(inventory.location), '.vagrant')}/*/").map { |d| File.basename(d) } 129 | @vagrant_env = File.expand_path(File.join(File.dirname(inventory.location), '.vagrant', get_vagrant_dir(platform, vagrant_dirs))) 130 | FileUtils.mkdir_p @vagrant_env 131 | generate_vagrantfile(File.join(@vagrant_env, 'Vagrantfile'), platform, enable_synced_folder, provider, cpus, memory, hyperv_vswitch, hyperv_smb_username, hyperv_smb_password, box_url) 132 | command = "vagrant up --provider #{provider}" 133 | run_local_command(command, @vagrant_env) 134 | vm_id = File.read(File.join(@vagrant_env, '.vagrant', 'machines', 'default', provider, 'index_uuid')) 135 | 136 | remote_config_file = platform_uses_ssh(platform) ? File.join(@vagrant_env, 'ssh-config') : File.join(@vagrant_env, 'winrm-config') 137 | remote_config = configure_remoting(platform, remote_config_file, password) 138 | node_name = "#{remote_config['hostname']}:#{remote_config['port']}" 139 | 140 | if platform_uses_ssh(platform) 141 | node = { 142 | 'name' => node_name, 143 | 'uri' => node_name, 144 | 'config' => { 145 | 'transport' => 'ssh', 146 | 'ssh' => { 147 | 'user' => remote_config['user'], 148 | 'host' => remote_config['hostname'], 149 | 'host-key-check' => remote_config['stricthostkeychecking'], 150 | 'port' => remote_config['port'], 151 | 'run-as' => 'root', 152 | 'connect-timeout' => 120 153 | } 154 | }, 155 | 'facts' => { 156 | 'provisioner' => 'vagrant', 157 | 'platform' => platform, 158 | 'id' => vm_id, 159 | 'vagrant_env' => @vagrant_env 160 | } 161 | } 162 | node['config']['ssh']['private-key'] = remote_config['identityfile'][0] if remote_config['identityfile'] 163 | node['config']['ssh']['password'] = password if password 164 | group_name = 'ssh_nodes' 165 | else 166 | # TODO: Need to figure out where SSL comes from 167 | remote_config['uses_ssl'] ||= false # TODO: Is the default _actually_ false? 168 | node = { 169 | 'name' => node_name, 170 | 'uri' => node_name, 171 | 'config' => { 172 | 'transport' => 'winrm', 173 | 'winrm' => { 174 | 'user' => remote_config['user'], 175 | 'password' => remote_config['password'], 176 | 'ssl' => remote_config['uses_ssl'], 177 | 'connect-timeout' => 120 178 | } 179 | }, 180 | 'facts' => { 181 | 'provisioner' => 'vagrant', 182 | 'platform' => platform, 183 | 'id' => vm_id, 184 | 'vagrant_env' => @vagrant_env 185 | } 186 | } 187 | group_name = 'winrm_nodes' 188 | end 189 | # Add the vars hash to the node if they are passed exists 190 | unless vars.nil? 191 | var_hash = YAML.safe_load(vars) 192 | node['vars'] = var_hash 193 | end 194 | inventory.add(node, group_name).save 195 | { status: 'ok', node_name:, node: } 196 | end 197 | 198 | def tear_down(node_name, inventory) 199 | command = 'vagrant destroy -f' 200 | node = inventory.lookup(node_name, group: 'ssh_nodes') 201 | vagrant_env = node['facts']['vagrant_env'] 202 | run_local_command(command, vagrant_env) 203 | FileUtils.rm_r(vagrant_env) 204 | inventory.remote(node).save 205 | { status: 'ok' } 206 | end 207 | 208 | def vagrant 209 | params = JSON.parse($stdin.read) 210 | warn params 211 | platform = params['platform'] 212 | action = params['action'] 213 | node_name = params['node_name'] 214 | vars = params['vars'] 215 | inventory = InventoryHelper.open(params['inventory']) 216 | enable_synced_folder = params['enable_synced_folder'].nil? ? ENV.fetch('VAGRANT_ENABLE_SYNCED_FOLDER', nil) : params['enable_synced_folder'] 217 | enable_synced_folder = enable_synced_folder.casecmp('true').zero? if enable_synced_folder.is_a?(String) 218 | provider = params['provider'].nil? ? ENV.fetch('VAGRANT_PROVIDER', nil) : params['provider'] 219 | cpus = params['cpus'].nil? ? ENV.fetch('VAGRANT_CPUS', nil) : params['cpus'] 220 | memory = params['memory'].nil? ? ENV.fetch('VAGRANT_MEMORY', nil) : params['memory'] 221 | hyperv_vswitch = params['hyperv_vswitch'].nil? ? ENV.fetch('VAGRANT_HYPERV_VSWITCH', nil) : params['hyperv_vswitch'] 222 | hyperv_smb_username = params['hyperv_smb_username'].nil? ? ENV.fetch('VAGRANT_HYPERV_SMB_USERNAME', nil) : params['hyperv_smb_username'] 223 | hyperv_smb_password = params['hyperv_smb_password'].nil? ? ENV.fetch('VAGRANT_HYPERV_SMB_PASSWORD', nil) : params['hyperv_smb_password'] 224 | box_url = params['box_url'].nil? ? ENV.fetch('VAGRANT_BOX_URL', nil) : params['box_url'] 225 | password = params['password'].nil? ? ENV.fetch('VAGRANT_PASSWORD', nil) : params['password'] 226 | raise 'specify a node_name when tearing down' if action == 'tear_down' && node_name.nil? 227 | raise 'specify a platform when provisioning' if action == 'provision' && platform.nil? 228 | 229 | unless node_name.nil? ^ platform.nil? 230 | case action 231 | when 'tear_down' 232 | raise 'specify only a node_name, not platform, when tearing down' 233 | when 'provision' 234 | raise 'specify only a platform, not node_name, when provisioning' 235 | else 236 | raise 'specify only one of: node_name, platform' 237 | end 238 | end 239 | 240 | begin 241 | result = provision(platform, inventory, enable_synced_folder, provider, cpus, memory, hyperv_vswitch, hyperv_smb_username, hyperv_smb_password, box_url, password, vars) if action == 'provision' 242 | result = tear_down(node_name, inventory) if action == 'tear_down' 243 | puts result.to_json 244 | exit 0 245 | rescue StandardError => e 246 | puts({ _error: { kind: 'provision/vagrant_failure', msg: e.message } }.to_json) 247 | exit 1 248 | end 249 | end 250 | 251 | vagrant if __FILE__ == $PROGRAM_NAME 252 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /REFERENCE.md: -------------------------------------------------------------------------------- 1 | # Reference 2 | 3 | 4 | 5 | ## Table of Contents 6 | 7 | ### Tasks 8 | 9 | * [`abs`](#abs): Provision/Tear down a machine using abs 10 | * [`docker`](#docker): Provision/Tear down a machine on docker 11 | * [`docker_exp`](#docker_exp): Provision/Tear down a machine on docker 12 | * [`fix_secure_path`](#fix_secure_path): Add puppet agent bin directory to sudo secure_path 13 | * [`install_pe`](#install_pe): Installs PE on a target 14 | * [`install_puppetserver`](#install_puppetserver): install puppetserver community edition 15 | * [`lxd`](#lxd): Provision/Tear down an instance on LXD 16 | * [`provision_service`](#provision_service): Provision/Tear down a list of machines using the provisioning service 17 | * [`run_tests`](#run_tests): Run rspec tests against a target machine 18 | * [`update_node_pp`](#update_node_pp): Creates a manifest file for a target node on pe server 19 | * [`update_site_pp`](#update_site_pp): Updates the site.pp on a target 20 | * [`vagrant`](#vagrant): Provision/Tear down a machine on vagrant 21 | 22 | ### Plans 23 | 24 | * [`provision::agents`](#provision--agents) 25 | * [`provision::agents_setup`](#provision--agents_setup) 26 | * [`provision::provisioner`](#provision--provisioner) 27 | * [`provision::puppetserver_setup`](#provision--puppetserver_setup) 28 | * [`provision::server_setup`](#provision--server_setup) 29 | * [`provision::teardown`](#provision--teardown) 30 | * [`provision::tester`](#provision--tester) 31 | * [`provision::tests_against_agents`](#provision--tests_against_agents) 32 | 33 | ## Tasks 34 | 35 | ### `abs` 36 | 37 | Provision/Tear down a machine using abs 38 | 39 | **Supports noop?** false 40 | 41 | #### Parameters 42 | 43 | ##### `action` 44 | 45 | Data type: `Enum[provision, tear_down]` 46 | 47 | Action to perform, tear_down or provision 48 | 49 | ##### `inventory` 50 | 51 | Data type: `Optional[String[1]]` 52 | 53 | Location of the inventory file 54 | 55 | ##### `node_name` 56 | 57 | Data type: `Optional[String[1]]` 58 | 59 | The name of the node 60 | 61 | ##### `platform` 62 | 63 | Data type: `Optional[Variant[String[1],Hash]]` 64 | 65 | Provision a single platform or a Hash of platforms specifying the number of instances. eg 'ubuntu-1604-x86_64 or '{ "centos-7-x86_64":1, "centos-6-x86_64":2 }' 66 | 67 | ##### `vars` 68 | 69 | Data type: `Optional[String[1]]` 70 | 71 | key/value pairs to add to the vars section 72 | 73 | ### `docker` 74 | 75 | Provision/Tear down a machine on docker 76 | 77 | **Supports noop?** false 78 | 79 | #### Parameters 80 | 81 | ##### `action` 82 | 83 | Data type: `Enum[provision, tear_down]` 84 | 85 | Action to perform, tear_down or provision 86 | 87 | ##### `inventory` 88 | 89 | Data type: `Optional[String[1]]` 90 | 91 | Location of the inventory file 92 | 93 | ##### `node_name` 94 | 95 | Data type: `Optional[String[1]]` 96 | 97 | The name of the node 98 | 99 | ##### `platform` 100 | 101 | Data type: `Optional[String[1]]` 102 | 103 | Platform to provision, eg ubuntu:14.04 104 | 105 | ##### `vars` 106 | 107 | Data type: `Optional[String[1]]` 108 | 109 | YAML string of key/value pairs to add to the inventory vars section 110 | 111 | ### `docker_exp` 112 | 113 | Provision/Tear down a machine on docker 114 | 115 | **Supports noop?** false 116 | 117 | #### Parameters 118 | 119 | ##### `action` 120 | 121 | Data type: `Enum[provision, tear_down]` 122 | 123 | Action to perform, tear_down or provision 124 | 125 | ##### `inventory` 126 | 127 | Data type: `Optional[String[1]]` 128 | 129 | Location of the inventory file 130 | 131 | ##### `node_name` 132 | 133 | Data type: `Optional[String[1]]` 134 | 135 | The name of the node 136 | 137 | ##### `platform` 138 | 139 | Data type: `Optional[String[1]]` 140 | 141 | Platform to provision, eg ubuntu:14.04 142 | 143 | ##### `vars` 144 | 145 | Data type: `Optional[String[1]]` 146 | 147 | YAML string of key/value pairs to add to the inventory vars section 148 | 149 | ### `fix_secure_path` 150 | 151 | Add puppet agent bin directory to sudo secure_path 152 | 153 | **Supports noop?** false 154 | 155 | #### Parameters 156 | 157 | ##### `path` 158 | 159 | Data type: `Optional[String[1]]` 160 | 161 | Puppet agent bin directory path 162 | 163 | ### `install_pe` 164 | 165 | Installs PE on a target 166 | 167 | **Supports noop?** false 168 | 169 | #### Parameters 170 | 171 | ##### `version` 172 | 173 | Data type: `Optional[String[1]]` 174 | 175 | The release of PE you want to install e.g. 2018.1 (Default: 2019.2) 176 | 177 | ### `install_puppetserver` 178 | 179 | install puppetserver community edition 180 | 181 | **Supports noop?** false 182 | 183 | #### Parameters 184 | 185 | ##### `collection` 186 | 187 | Data type: `Optional[String[1]]` 188 | 189 | The Puppet Server version 190 | 191 | ##### `platform` 192 | 193 | Data type: `Optional[String[1]]` 194 | 195 | The operating system and version 196 | 197 | ##### `retry` 198 | 199 | Data type: `Optional[Integer]` 200 | 201 | The number of retries in case of network connectivity failures 202 | 203 | ### `lxd` 204 | 205 | Provision/Tear down an instance on LXD 206 | 207 | **Supports noop?** false 208 | 209 | #### Parameters 210 | 211 | ##### `action` 212 | 213 | Data type: `Enum[provision, tear_down]` 214 | 215 | Action to perform, tear_down or provision 216 | 217 | ##### `inventory` 218 | 219 | Data type: `Optional[String[1]]` 220 | 221 | Location of the inventory file 222 | 223 | ##### `node_name` 224 | 225 | Data type: `Optional[String[1]]` 226 | 227 | The name of the instance 228 | 229 | ##### `platform` 230 | 231 | Data type: `Optional[String[1]]` 232 | 233 | LXD image to use, eg images:ubuntu/22.04 234 | 235 | ##### `profiles` 236 | 237 | Data type: `Optional[Array[String[1]]]` 238 | 239 | LXD Profiles to apply 240 | 241 | ##### `storage` 242 | 243 | Data type: `Optional[String[1]]` 244 | 245 | LXD Storage pool name 246 | 247 | ##### `instance_type` 248 | 249 | Data type: `Optional[String[1]]` 250 | 251 | LXD Instance type 252 | 253 | ##### `vm` 254 | 255 | Data type: `Optional[Boolean]` 256 | 257 | Provision as a virtual-machine instead of a container 258 | 259 | ##### `remote` 260 | 261 | Data type: `Optional[String]` 262 | 263 | LXD remote, defaults to the LXD client configured default remote 264 | 265 | ##### `retries` 266 | 267 | Data type: `Integer` 268 | 269 | On provision check the instance is accepting commands, will be deleted if retries exceeded, 0 to disable 270 | 271 | ##### `vars` 272 | 273 | Data type: `Optional[String[1]]` 274 | 275 | YAML string of key/value pairs to add to the inventory vars section 276 | 277 | ### `provision_service` 278 | 279 | Provision/Tear down a list of machines using the provisioning service 280 | 281 | **Supports noop?** false 282 | 283 | #### Parameters 284 | 285 | ##### `action` 286 | 287 | Data type: `Enum[provision, tear_down]` 288 | 289 | Action to perform, tear_down or provision 290 | 291 | ##### `platform` 292 | 293 | Data type: `Optional[String[1]]` 294 | 295 | Needed by litmus 296 | 297 | ##### `node_name` 298 | 299 | Data type: `Optional[String[1]]` 300 | 301 | Needed by litmus 302 | 303 | ##### `inventory` 304 | 305 | Data type: `Optional[String[1]]` 306 | 307 | Location of the inventory file 308 | 309 | ##### `vars` 310 | 311 | Data type: `Optional[String[1]]` 312 | 313 | The address of the provisioning service 314 | 315 | ##### `retry_attempts` 316 | 317 | Data type: `Optional[Integer[1]]` 318 | 319 | The number of times to retry the provisioning if it fails 320 | 321 | ### `run_tests` 322 | 323 | Run rspec tests against a target machine 324 | 325 | **Supports noop?** false 326 | 327 | #### Parameters 328 | 329 | ##### `sut` 330 | 331 | Data type: `String[1]` 332 | 333 | The target SUT to run tests against 334 | 335 | ##### `test_path` 336 | 337 | Data type: `Optional[String[1]]` 338 | 339 | Location of the test files. Defaults to './spec/acceptance' 340 | 341 | ##### `format` 342 | 343 | Data type: `Enum[progress, documentation]` 344 | 345 | 346 | 347 | ### `update_node_pp` 348 | 349 | Creates a manifest file for a target node on pe server 350 | 351 | **Supports noop?** false 352 | 353 | #### Parameters 354 | 355 | ##### `manifest` 356 | 357 | Data type: `String[1]` 358 | 359 | The manifest code 360 | 361 | ##### `target_node` 362 | 363 | Data type: `String[1]` 364 | 365 | The target node 366 | 367 | ### `update_site_pp` 368 | 369 | Updates the site.pp on a target 370 | 371 | **Supports noop?** false 372 | 373 | #### Parameters 374 | 375 | ##### `manifest` 376 | 377 | Data type: `String[1]` 378 | 379 | The manifest code 380 | 381 | ### `vagrant` 382 | 383 | Provision/Tear down a machine on vagrant 384 | 385 | **Supports noop?** false 386 | 387 | #### Parameters 388 | 389 | ##### `action` 390 | 391 | Data type: `Enum[provision, tear_down]` 392 | 393 | Action to perform, tear_down or provision 394 | 395 | ##### `inventory` 396 | 397 | Data type: `Optional[String[1]]` 398 | 399 | Location of the inventory file 400 | 401 | ##### `node_name` 402 | 403 | Data type: `Optional[String[1]]` 404 | 405 | The name of the node 406 | 407 | ##### `platform` 408 | 409 | Data type: `Optional[String[1]]` 410 | 411 | Platform to provision, eg ubuntu:14.04 412 | 413 | ##### `provider` 414 | 415 | Data type: `Optional[String[1]]` 416 | 417 | Provider to use provision, eg virtualbox 418 | 419 | ##### `cpus` 420 | 421 | Data type: `Optional[Integer]` 422 | 423 | Number of CPUs. Eg 2 424 | 425 | ##### `memory` 426 | 427 | Data type: `Optional[Integer]` 428 | 429 | MB Memory. Eg 4000 430 | 431 | ##### `hyperv_vswitch` 432 | 433 | Data type: `Optional[String[1]]` 434 | 435 | The Hyper-V virtual switch to spin the vagrant image up on 436 | 437 | ##### `hyperv_smb_username` 438 | 439 | Data type: `Optional[String[1]]` 440 | 441 | The username on the Hyper-V machine to use for authenticating the shared folder. Required to use Hyper-V with a synced folder. 442 | 443 | ##### `hyperv_smb_password` 444 | 445 | Data type: `Optional[String[1]]` 446 | 447 | The password on the Hyper-V machine to use for authenticating the shared folder. Required to use Hyper-V with a synced folder. 448 | 449 | ##### `enable_synced_folder` 450 | 451 | Data type: `Optional[Boolean]` 452 | 453 | Whether to use the vagrant synced folder for the provisioned machine 454 | 455 | ##### `box_url` 456 | 457 | Data type: `Optional[String[1]]` 458 | 459 | Path to the Vagrant Box URL 460 | 461 | ##### `password` 462 | 463 | Data type: `Optional[String[1]]` 464 | 465 | Password to use for Vagrant boxes without the default Vagrant insecure key 466 | 467 | ##### `vars` 468 | 469 | Data type: `Optional[String[1]]` 470 | 471 | YAML string of key/value pairs to add to the inventory vars section 472 | 473 | ## Plans 474 | 475 | ### `provision::agents` 476 | 477 | The provision::agents class. 478 | 479 | ### `provision::agents_setup` 480 | 481 | The provision::agents_setup class. 482 | 483 | #### Parameters 484 | 485 | The following parameters are available in the `provision::agents_setup` plan: 486 | 487 | * [`collection`](#-provision--agents_setup--collection) 488 | 489 | ##### `collection` 490 | 491 | Data type: `Optional[String]` 492 | 493 | 494 | 495 | Default value: `'puppet7'` 496 | 497 | ### `provision::provisioner` 498 | 499 | The provision::provisioner class. 500 | 501 | ### `provision::puppetserver_setup` 502 | 503 | The provision::puppetserver_setup class. 504 | 505 | #### Parameters 506 | 507 | The following parameters are available in the `provision::puppetserver_setup` plan: 508 | 509 | * [`collection`](#-provision--puppetserver_setup--collection) 510 | 511 | ##### `collection` 512 | 513 | Data type: `Optional[String]` 514 | 515 | 516 | 517 | Default value: `'puppet7'` 518 | 519 | ### `provision::server_setup` 520 | 521 | The provision::server_setup class. 522 | 523 | ### `provision::teardown` 524 | 525 | The provision::teardown class. 526 | 527 | ### `provision::tester` 528 | 529 | The provision::tester class. 530 | 531 | ### `provision::tests_against_agents` 532 | 533 | The provision::tests_against_agents class. 534 | 535 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Provision 2 | 3 | [![Code Owners](https://img.shields.io/badge/owners-DevX--team-blue)](https://github.com/puppetlabs/provision/blob/main/CODEOWNERS) 4 | ![ci](https://github.com/puppetlabs/provision/actions/workflows/ci.yml/badge.svg) 5 | ![GitHub release (latest by date)](https://img.shields.io/github/v/release/puppetlabs/provision) 6 | 7 | Simple tasks to provision and tear down containers, instances and virtual machines. 8 | 9 | ## Table of Contents 10 | 11 | 1. [Description](#description) 12 | 2. [Setup - The basics of getting started with provision](#setup) 13 | * [Setup requirements](#setup-requirements) 14 | 3. [Usage - Configuration options and additional functionality](#usage) 15 | * [ABS](#abs) 16 | * [Docker](#docker) 17 | * [Vagrant](#vagrant) 18 | * [Provision Service](#provision_service) 19 | 4. [Limitations - OS compatibility, etc.](#limitations) 20 | 5. [Development - Guide for contributing to the module](#development) 21 | 6. [License - Additional information in regards to licensing](#license) 22 | 23 | ## Description 24 | 25 | Bolt tasks allowing a user to provision and tear down systems. It also maintains a Bolt inventory file. 26 | Provisioners so far: 27 | 28 | * ABS (AlwaysBeScheduling) 29 | * Docker 30 | * Vagrant 31 | * Provision Service 32 | 33 | ## Setup 34 | 35 | ### Setup Requirements 36 | 37 | Bolt has to be installed to run the tasks. Each provisioner has its own requirements. From having Docker to installed or access to private infrastructure. 38 | 39 | #### Running the tasks as part of puppet_litmus 40 | 41 | Please follow the litmus documentation [here](https://puppetlabs.github.io/content-and-tooling-team/docs/litmus/). 42 | 43 | #### Running the module stand-alone call the tasks/plans directly 44 | 45 | For provisioning to work you will need to have a number of other modules available. You can use bolt to install the modules for you from your Puppetfile, by following this [guide](https://puppet.com/docs/bolt/latest/installing_tasks_from_the_forge.html). 46 | The required modules are: 47 | 48 | ```shell 49 | cat $HOME/.puppetlabs/bolt/Puppetfile 50 | mod 'puppetlabs-puppet_agent' 51 | mod 'puppetlabs-facts' 52 | mod 'puppetlabs-puppet_conf' 53 | ``` 54 | 55 | ## Usage 56 | 57 | There is a basic workflow for the provision tasks. 58 | 59 | * provision - creates/initiates a platform and edits a bolt inventory file. 60 | * tear_down - creates/initiates a system/container and edits a bolt inventory file. 61 | 62 | For extended functionality please look at the [provision wiki](https://github.com/puppetlabs/provision/wiki). 63 | 64 | ### ABS 65 | 66 | (internal to puppet) Allows you to provision machines on puppets internal pooler. Reads the '~/.fog' file for your authentication token. 67 | 68 | #### Setting up your Token 69 | 70 | In order to run ABS you first require an access token stored within your '.fog' file. If you already have one you may skip this section, otherwise request one by running the following command, changing the username. 71 | 72 | ```shell 73 | $ curl -X POST -d '' -u tp --url https://test-example.abs.net/api/v2/token 74 | Enter host password for user 'tp': 75 | { 76 | "ok": true, 77 | "token": "0pd263lej948h28493692r07" 78 | }% 79 | ``` 80 | 81 | Now that you have your token, check that it works by running: 82 | 83 | ```shell 84 | $ curl --url https://test-example.abs.net/api/v2/token/0pd263lej948h28493692r07 85 | { 86 | "ok": true, 87 | "user": "tp", 88 | "created": "2019-01-04 14:25:55 +0000", 89 | "last_accessed": "2019-01-04 14:26:27 +0000" 90 | }% 91 | ``` 92 | 93 | Finally all that you have left to do is to place your new token into your '.fog' file as shown below: 94 | 95 | ```shell 96 | $ cat ~/.fog 97 | :default: 98 | :abs_token: 0pd263lej948h28493692r07 99 | ``` 100 | 101 | #### Running the Commands 102 | 103 | ##### Setting up a new machine 104 | 105 | ```ruby 106 | $ bundle exec bolt task run provision::abs --targets localhost action=provision platform=ubuntu-1604-x86_64 107 | 108 | Started on localhost... 109 | Finished on localhost: 110 | { 111 | "status": "ok", 112 | "node_name": "yh6f4djvz7o3te6.delivery.puppetlabs.net" 113 | } 114 | Successful on 1 node: localhost 115 | Ran on 1 node in 1.44 seconds 116 | ``` 117 | 118 | ##### Tearing down a finished machine 119 | 120 | ```ruby 121 | $ bundle exec bolt task run provision::abs --targets localhost action=tear_down node_name=yh6f4djvz7o3te6.delivery.puppetlabs.net 122 | 123 | Started on localhost... 124 | Finished on localhost: 125 | Removed yh6f4djvz7o3te6.delivery.puppetlabs.net 126 | {"status":"ok"} 127 | { 128 | } 129 | Successful on 1 node: localhost 130 | Ran on 1 node in 1.54 seconds 131 | ``` 132 | 133 | ### Docker 134 | 135 | Given an docker image name it will spin up that container and setup external ssh on that platform. For helpful docker tips look [here](https://github.com/puppetlabs/litmus_image/blob/main/README.md) 136 | 137 | Containers by default will be managed in the current [docker context](https://docs.docker.com/engine/context/working-with-contexts/), on the [DOCKER_HOST](https://docs.docker.com/engine/reference/commandline/cli/#environment-variables), or on localhost if nether are configured. 138 | 139 | #### Provision 140 | 141 | ```ruby 142 | $ bundle exec bolt task run provision::docker --targets localhost action=provision platform=ubuntu:14.04 143 | 144 | Started on localhost... 145 | Finished on localhost: 146 | Provisioning ubuntu_14.04-2222 147 | {"status":"ok","node_name":"localhost"} 148 | { 149 | } 150 | Successful on 1 node: localhost 151 | Ran on 1 node in 33.96 seconds 152 | ``` 153 | 154 | Provision allows for passing additional command line arguments to the docker run when specifying `vars['docker_run_opts']` as an array of arguments. 155 | 156 | When running Debian or Ubuntu containers, the following flags will be added to the $docker_run_opts by default. 157 | 158 | ```shell 159 | --volume /sys/fs/cgroup:/sys/fs/cgroup:rw --cgroupns=host 160 | ``` 161 | 162 | These defaults can be overriden by passing the flags with different values i.e. 163 | 164 | ```shell 165 | --volume /sys/fs/cgroup:/sys/fs/cgroup:ro --cgroupns=private 166 | ``` 167 | 168 | ```ruby 169 | bundle exec bolt task run provision::docker --targets localhost action=provision platform=ubuntu:14.04 vars='{ "docker_run_opts": ["-p 8086:8086", "-p 3000:3000"]}' 170 | ``` 171 | 172 | #### Tear down 173 | 174 | ```ruby 175 | $ bundle exec bolt task run provision::docker --targets localhost action=tear_down node_name=localhost:2222 176 | 177 | Started on localhost... 178 | Finished on localhost: 179 | Removed localhost:2222 180 | {"status":"ok"} 181 | { 182 | } 183 | Successful on 1 node: localhost 184 | Ran on 1 node in 2.02 seconds 185 | ``` 186 | 187 | ### Vagrant 188 | 189 | Tested with vagrant images: 190 | 191 | * ubuntu/trusty64 192 | * ubuntu/xenial64 193 | * ubuntu/bionic64 194 | * debian/jessie64 195 | * centos/7 196 | 197 | provision 198 | 199 | ```ruby 200 | $ bundle exec bolt task run provision::vagrant --targets localhost action=provision platform=ubuntu/xenial64 201 | 202 | Started on localhost... 203 | Finished on localhost: 204 | { 205 | "status": "ok", 206 | "node_name": "127.0.0.1:2222" 207 | } 208 | Successful on 1 node: localhost 209 | Ran on 1 node in 51.98 seconds 210 | ``` 211 | 212 | For multi-node provisioning, you can assign arbitrary tags to the nodes you deploy, by passing an optional YAML-string 'vars' to the bolt task. In the example below we are assigning the role of `k8s-controller` to the provisioned node. 213 | 214 | ```ruby 215 | $ bundle exec bolt task run provision::vagrant --targets localhost action=provision platform=ubuntu/xenial64 inventory=/Users/tp/workspace/git/provision vars='role: k8s-controller' 216 | ``` 217 | 218 | sudo secure_path fix 219 | 220 | As some Vagrant boxes do not allow ssh root logins, the **vagrant** user is used to login and *sudo* is used to execute privileged commands as root user. 221 | By default the Puppet agent installation does not change the systems' sudo *secure_path* configuration. 222 | This leads to errors when anything tries to execute `puppet` commands on the test system. 223 | To add the Puppet agent binary path to the *secure_path* please run the `provision::fix_secure_path` Bolt task: 224 | 225 | ```ruby 226 | $ bundle exec bolt task run provision::fix_secure_path path=/opt/puppetlabs/bin -i inventory.yaml -t ssh_nodes 227 | 228 | Started on 127.0.0.1:2222... 229 | Finished on 127.0.0.1:2222: 230 | Task completed successfully with no result 231 | Successful on 1 target: 127.0.0.1:2222 232 | Ran on 1 target in 0.84 sec 233 | ``` 234 | 235 | tear_down 236 | 237 | ```ruby 238 | $ bundle exec bolt task run provision::vagrant --targets localhost action=tear_down node_name=127.0.0.1:2222 239 | 240 | Started on localhost... 241 | Finished on localhost: 242 | Removed 127.0.0.1:2222 243 | {"status":"ok"} 244 | { 245 | } 246 | Successful on 1 node: localhost 247 | Ran on 1 node in 4.52 seconds 248 | ``` 249 | 250 | #### Additional Vagrant options 251 | 252 | ##### Box URL 253 | 254 | If you need to use a local/private Vagrant box requiring you to specify a URL to access, you can pass a URL by setting the `VAGRANT_BOX_URL` environment variable. If you put `%BOX%` in the URL it will be replaced with the name of the box/image. You can also set the variable by setting the `params` hash in `provision.yaml`. 255 | 256 | In `provision.yaml`: 257 | 258 | ```yaml 259 | vagrant_boxes: 260 | provisioner: vagrant 261 | images: 262 | - mybox1 263 | params: 264 | vagrant_box_url: 'https://boxes.example.com/box/%BOX%.json' 265 | ``` 266 | 267 | ##### Password 268 | 269 | If you use a custom Vagrant box which requires the use of a password instead of the standard Vagrant keypair then you can set it in the `VAGRANT_PASSWORD` environment variable, or in the `provision.yaml` params hash with the `vagrant_password` key. 270 | 271 | ### Provision_service 272 | 273 | The provision service task is meant to be used from a Github Action workflow. 274 | 275 | Example usage: 276 | Using the following provision.yaml file: 277 | 278 | ```yaml 279 | test_serv: 280 | provisioner: provision::provision_service 281 | params: 282 | cloud: gcp 283 | region: europe-west1 284 | zone: europe-west1-d 285 | images: ['centos-7-v20200618', 'windows-server-2016-dc-v20200813'] 286 | ``` 287 | 288 | In the provision step you can invoke bundle exec rake 'litmus:provision_list[test_serv]' and this will ensure the creation of two VMs in GCP. 289 | 290 | Manual invocation of the provision service task from a workflow can be done using: 291 | 292 | ```ruby 293 | bundle exec bolt task run provision::provision_service --targets localhost action=provision platform=centos-7-v20200813 inventory=/Users/tp/workspace/git/provision/inventory.yaml vars='role: puppetserver' 294 | ``` 295 | 296 | Or using Litmus: 297 | 298 | ```ruby 299 | bundle exec rake 'litmus:provision[provision_service, centos-7-v20200813, role: puppetserver]' 300 | ``` 301 | 302 | #### Synced Folders 303 | 304 | By default the task will provision a Vagrant box with the [synced folder](https://developer.hashicorp.com/vagrant/docs/synced-folders) **disabled**. 305 | To enable the synced folder you must specify the parameter `enable_synced_folder` as `true`. 306 | Instead of passing this parameter directly you can instead specify the environment variable `LITMUS_ENABLE_SYNCED_FOLDER` as `true`. 307 | 308 | #### Hyper-V Provider 309 | 310 | This task can also be used against a Windows host to utilize Hyper-V Vagrant boxes. 311 | When provisioning, a few additional parameters need to be passed: 312 | 313 | * `hyperv_vswitch`, which specifies the Hyper-V Virtual Switch to assign the VM. 314 | If you do not specify one the [`Default Switch`](https://searchenterprisedesktop.techtarget.com/blog/Windows-Enterprise-Desktop/Default-Switch-Makes-Hyper-V-Networking-Dead-Simple) will be used. 315 | * `hyperv_smb_username` and `hyperv_smb_password`, which ensure the synced folder works correctly (only necessary is `enable_synced_folder` is `true`). 316 | If these parameters are omitted when provisioning on Windows and using synced folders Vagrant will try to prompt for input and the task will hang indefinitely until it finally times out. 317 | The context in which a Bolt task is run does not allow for mid-task input. 318 | 319 | Instead of passing them as parameters directly they can also be passed as environment variables: 320 | 321 | * `LITMUS_HYPERV_VSWITCH` for `hyperv_vswitch` 322 | * `HYPERV_SMB_USERNAME` for `hyperv_smb_username` 323 | * `HYPERV_SMB_PASSWORD` for `hyperv_smb_password` 324 | 325 | provision 326 | 327 | ```powershell 328 | PS> $env:LITMUS_HYPERV_VSWITCH = 'internal_nat' 329 | PS> bundle exec bolt task run provision::vagrant --targets localhost action=provision platform=centos/7 hyperv_smb_username=tp hyperv_smb_password=notMyrealPassword 330 | 331 | Started on localhost... 332 | Finished on localhost: 333 | { 334 | "status": "ok", 335 | "node_name": "127.0.0.1:2222" 336 | } 337 | Successful on 1 node: localhost 338 | Ran on 1 node in 51.98 seconds 339 | ``` 340 | 341 | Using the `tear_down` task is the same as on Linux or MacOS. 342 | 343 | ## Limitations 344 | 345 | * The docker task only supports Linux 346 | * The docker task uses port forwarding, not internal IP addresses. This is because of limitations when running on the mac. 347 | 348 | ## Development 349 | 350 | Testing/development/debugging it is better to use ruby directly, you will need to pass the JSON parameters. Depending on how you are running (using a puppet file or as part of a puppet_litmus). The dependencies of provision will need to be available. See the setup section above. 351 | 352 | ```shell 353 | # powershell 354 | echo '{ "platform": "ubuntu-1604-x86_64", "action": "provision", "inventory": "c:\\workspace\\puppetlabs-motd\\" }' | bundle exec ruby .\spec\fixtures\modules\provision\tasks\abs.rb 355 | # bash / zshell ... 356 | echo '{ "platform": "ubuntu-1604-x86_64", "action": "provision", "inventory": "/home/tp/workspace/puppetlabs-motd/" }' | bundle exec ruby spec/fixtures/modules/provision/tasks/abs.rb 357 | ``` 358 | 359 | Testing using bolt, the second step 360 | 361 | ```ruby 362 | bundle exec bolt task run provision::docker --targets localhost action=provision platform=ubuntu:14.04 363 | ``` 364 | 365 | ## License 366 | 367 | This codebase is licensed under Apache 2.0. However, the open source dependencies included in this codebase might be subject to other software licenses such as AGPL, GPL2.0, and MIT. 368 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | --- 2 | require: 3 | - rubocop-performance 4 | - rubocop-rspec 5 | AllCops: 6 | NewCops: enable 7 | DisplayCopNames: true 8 | TargetRubyVersion: '3.1' 9 | Include: 10 | - "**/*.rb" 11 | Exclude: 12 | - bin/* 13 | - ".vendor/**/*" 14 | - "**/Gemfile" 15 | - "**/Rakefile" 16 | - pkg/**/* 17 | - spec/fixtures/**/* 18 | - vendor/**/* 19 | - "**/Puppetfile" 20 | - "**/Vagrantfile" 21 | - "**/Guardfile" 22 | Layout/LineLength: 23 | Description: People have wide screens, use them. 24 | Max: 200 25 | RSpec/BeforeAfterAll: 26 | Description: Beware of using after(:all) as it may cause state to leak between tests. 27 | A necessary evil in acceptance testing. 28 | Exclude: 29 | - spec/acceptance/**/*.rb 30 | RSpec/HookArgument: 31 | Description: Prefer explicit :each argument, matching existing module's style 32 | EnforcedStyle: each 33 | RSpec/DescribeSymbol: 34 | Exclude: 35 | - spec/unit/facter/**/*.rb 36 | Style/BlockDelimiters: 37 | Description: Prefer braces for chaining. Mostly an aesthetical choice. Better to 38 | be consistent then. 39 | EnforcedStyle: braces_for_chaining 40 | Style/ClassAndModuleChildren: 41 | Description: Compact style reduces the required amount of indentation. 42 | EnforcedStyle: compact 43 | Style/EmptyElse: 44 | Description: Enforce against empty else clauses, but allow `nil` for clarity. 45 | EnforcedStyle: empty 46 | Style/FormatString: 47 | Description: Following the main puppet project's style, prefer the % format format. 48 | EnforcedStyle: percent 49 | Style/FormatStringToken: 50 | Description: Following the main puppet project's style, prefer the simpler template 51 | tokens over annotated ones. 52 | EnforcedStyle: template 53 | Style/Lambda: 54 | Description: Prefer the keyword for easier discoverability. 55 | EnforcedStyle: literal 56 | Style/RegexpLiteral: 57 | Description: Community preference. See https://github.com/voxpupuli/modulesync_config/issues/168 58 | EnforcedStyle: percent_r 59 | Style/TernaryParentheses: 60 | Description: Checks for use of parentheses around ternary conditions. Enforce parentheses 61 | on complex expressions for better readability, but seriously consider breaking 62 | it up. 63 | EnforcedStyle: require_parentheses_when_complex 64 | Style/TrailingCommaInArguments: 65 | Description: Prefer always trailing comma on multiline argument lists. This makes 66 | diffs, and re-ordering nicer. 67 | EnforcedStyleForMultiline: comma 68 | Style/TrailingCommaInArrayLiteral: 69 | Description: Prefer always trailing comma on multiline literals. This makes diffs, 70 | and re-ordering nicer. 71 | EnforcedStyleForMultiline: comma 72 | Style/SymbolArray: 73 | Description: Using percent style obscures symbolic intent of array's contents. 74 | EnforcedStyle: brackets 75 | RSpec/MessageSpies: 76 | EnforcedStyle: receive 77 | Style/Documentation: 78 | Exclude: 79 | - lib/puppet/parser/functions/**/* 80 | - spec/**/* 81 | Style/WordArray: 82 | EnforcedStyle: brackets 83 | Performance/AncestorsInclude: 84 | Enabled: true 85 | Performance/BigDecimalWithNumericArgument: 86 | Enabled: true 87 | Performance/BlockGivenWithExplicitBlock: 88 | Enabled: true 89 | Performance/CaseWhenSplat: 90 | Enabled: true 91 | Performance/ConstantRegexp: 92 | Enabled: true 93 | Performance/MethodObjectAsBlock: 94 | Enabled: true 95 | Performance/RedundantSortBlock: 96 | Enabled: true 97 | Performance/RedundantStringChars: 98 | Enabled: true 99 | Performance/ReverseFirst: 100 | Enabled: true 101 | Performance/SortReverse: 102 | Enabled: true 103 | Performance/Squeeze: 104 | Enabled: true 105 | Performance/StringInclude: 106 | Enabled: true 107 | Performance/Sum: 108 | Enabled: true 109 | Style/CollectionMethods: 110 | Enabled: true 111 | Style/MethodCalledOnDoEndBlock: 112 | Enabled: true 113 | Style/StringMethods: 114 | Enabled: true 115 | Bundler/GemFilename: 116 | Enabled: false 117 | Bundler/InsecureProtocolSource: 118 | Enabled: false 119 | Capybara/CurrentPathExpectation: 120 | Enabled: false 121 | Capybara/VisibilityMatcher: 122 | Enabled: false 123 | Gemspec/DuplicatedAssignment: 124 | Enabled: false 125 | Gemspec/OrderedDependencies: 126 | Enabled: false 127 | Gemspec/RequiredRubyVersion: 128 | Enabled: false 129 | Gemspec/RubyVersionGlobalsUsage: 130 | Enabled: false 131 | Layout/ArgumentAlignment: 132 | Enabled: false 133 | Layout/BeginEndAlignment: 134 | Enabled: false 135 | Layout/ClosingHeredocIndentation: 136 | Enabled: false 137 | Layout/EmptyComment: 138 | Enabled: false 139 | Layout/EmptyLineAfterGuardClause: 140 | Enabled: false 141 | Layout/EmptyLinesAroundArguments: 142 | Enabled: false 143 | Layout/EmptyLinesAroundAttributeAccessor: 144 | Enabled: false 145 | Layout/EndOfLine: 146 | Enabled: false 147 | Layout/FirstArgumentIndentation: 148 | Enabled: false 149 | Layout/HashAlignment: 150 | Enabled: false 151 | Layout/HeredocIndentation: 152 | Enabled: false 153 | Layout/LeadingEmptyLines: 154 | Enabled: false 155 | Layout/SpaceAroundMethodCallOperator: 156 | Enabled: false 157 | Layout/SpaceInsideArrayLiteralBrackets: 158 | Enabled: false 159 | Layout/SpaceInsideReferenceBrackets: 160 | Enabled: false 161 | Lint/BigDecimalNew: 162 | Enabled: false 163 | Lint/BooleanSymbol: 164 | Enabled: false 165 | Lint/ConstantDefinitionInBlock: 166 | Enabled: false 167 | Lint/DeprecatedOpenSSLConstant: 168 | Enabled: false 169 | Lint/DisjunctiveAssignmentInConstructor: 170 | Enabled: false 171 | Lint/DuplicateElsifCondition: 172 | Enabled: false 173 | Lint/DuplicateRequire: 174 | Enabled: false 175 | Lint/DuplicateRescueException: 176 | Enabled: false 177 | Lint/EmptyConditionalBody: 178 | Enabled: false 179 | Lint/EmptyFile: 180 | Enabled: false 181 | Lint/ErbNewArguments: 182 | Enabled: false 183 | Lint/FloatComparison: 184 | Enabled: false 185 | Lint/HashCompareByIdentity: 186 | Enabled: false 187 | Lint/IdentityComparison: 188 | Enabled: false 189 | Lint/InterpolationCheck: 190 | Enabled: false 191 | Lint/MissingCopEnableDirective: 192 | Enabled: false 193 | Lint/MixedRegexpCaptureTypes: 194 | Enabled: false 195 | Lint/NestedPercentLiteral: 196 | Enabled: false 197 | Lint/NonDeterministicRequireOrder: 198 | Enabled: false 199 | Lint/OrderedMagicComments: 200 | Enabled: false 201 | Lint/OutOfRangeRegexpRef: 202 | Enabled: false 203 | Lint/RaiseException: 204 | Enabled: false 205 | Lint/RedundantCopEnableDirective: 206 | Enabled: false 207 | Lint/RedundantRequireStatement: 208 | Enabled: false 209 | Lint/RedundantSafeNavigation: 210 | Enabled: false 211 | Lint/RedundantWithIndex: 212 | Enabled: false 213 | Lint/RedundantWithObject: 214 | Enabled: false 215 | Lint/RegexpAsCondition: 216 | Enabled: false 217 | Lint/ReturnInVoidContext: 218 | Enabled: false 219 | Lint/SafeNavigationConsistency: 220 | Enabled: false 221 | Lint/SafeNavigationWithEmpty: 222 | Enabled: false 223 | Lint/SelfAssignment: 224 | Enabled: false 225 | Lint/SendWithMixinArgument: 226 | Enabled: false 227 | Lint/ShadowedArgument: 228 | Enabled: false 229 | Lint/StructNewOverride: 230 | Enabled: false 231 | Lint/ToJSON: 232 | Enabled: false 233 | Lint/TopLevelReturnWithArgument: 234 | Enabled: false 235 | Lint/TrailingCommaInAttributeDeclaration: 236 | Enabled: false 237 | Lint/UnreachableLoop: 238 | Enabled: false 239 | Lint/UriEscapeUnescape: 240 | Enabled: false 241 | Lint/UriRegexp: 242 | Enabled: false 243 | Lint/UselessMethodDefinition: 244 | Enabled: false 245 | Lint/UselessTimes: 246 | Enabled: false 247 | Metrics/AbcSize: 248 | Enabled: false 249 | Metrics/BlockLength: 250 | Enabled: false 251 | Metrics/BlockNesting: 252 | Enabled: false 253 | Metrics/ClassLength: 254 | Enabled: false 255 | Metrics/CyclomaticComplexity: 256 | Enabled: false 257 | Metrics/MethodLength: 258 | Enabled: false 259 | Metrics/ModuleLength: 260 | Enabled: false 261 | Metrics/ParameterLists: 262 | Enabled: false 263 | Metrics/PerceivedComplexity: 264 | Enabled: false 265 | Migration/DepartmentName: 266 | Enabled: false 267 | Naming/AccessorMethodName: 268 | Enabled: false 269 | Naming/BlockParameterName: 270 | Enabled: false 271 | Naming/HeredocDelimiterCase: 272 | Enabled: false 273 | Naming/HeredocDelimiterNaming: 274 | Enabled: false 275 | Naming/MemoizedInstanceVariableName: 276 | Enabled: false 277 | Naming/MethodParameterName: 278 | Enabled: false 279 | Naming/RescuedExceptionsVariableName: 280 | Enabled: false 281 | Naming/VariableNumber: 282 | Enabled: false 283 | Performance/BindCall: 284 | Enabled: false 285 | Performance/DeletePrefix: 286 | Enabled: false 287 | Performance/DeleteSuffix: 288 | Enabled: false 289 | Performance/InefficientHashSearch: 290 | Enabled: false 291 | Performance/UnfreezeString: 292 | Enabled: false 293 | Performance/UriDefaultParser: 294 | Enabled: false 295 | RSpec/Be: 296 | Enabled: false 297 | RSpec/Capybara/FeatureMethods: 298 | Enabled: false 299 | RSpec/ContainExactly: 300 | Enabled: false 301 | RSpec/ContextMethod: 302 | Enabled: false 303 | RSpec/ContextWording: 304 | Enabled: false 305 | RSpec/DescribeClass: 306 | Enabled: false 307 | RSpec/EmptyHook: 308 | Enabled: false 309 | RSpec/EmptyLineAfterExample: 310 | Enabled: false 311 | RSpec/EmptyLineAfterExampleGroup: 312 | Enabled: false 313 | RSpec/EmptyLineAfterHook: 314 | Enabled: false 315 | RSpec/ExampleLength: 316 | Enabled: false 317 | RSpec/ExampleWithoutDescription: 318 | Enabled: false 319 | RSpec/ExpectChange: 320 | Enabled: false 321 | RSpec/ExpectInHook: 322 | Enabled: false 323 | RSpec/FactoryBot/AttributeDefinedStatically: 324 | Enabled: false 325 | RSpec/FactoryBot/CreateList: 326 | Enabled: false 327 | RSpec/FactoryBot/FactoryClassName: 328 | Enabled: false 329 | RSpec/HooksBeforeExamples: 330 | Enabled: false 331 | RSpec/ImplicitBlockExpectation: 332 | Enabled: false 333 | RSpec/ImplicitSubject: 334 | Enabled: false 335 | RSpec/LeakyConstantDeclaration: 336 | Enabled: false 337 | RSpec/LetBeforeExamples: 338 | Enabled: false 339 | RSpec/MatchArray: 340 | Enabled: false 341 | RSpec/MissingExampleGroupArgument: 342 | Enabled: false 343 | RSpec/MultipleExpectations: 344 | Enabled: false 345 | RSpec/MultipleMemoizedHelpers: 346 | Enabled: false 347 | RSpec/MultipleSubjects: 348 | Enabled: false 349 | RSpec/NestedGroups: 350 | Enabled: false 351 | RSpec/PredicateMatcher: 352 | Enabled: false 353 | RSpec/ReceiveCounts: 354 | Enabled: false 355 | RSpec/ReceiveNever: 356 | Enabled: false 357 | RSpec/RepeatedExampleGroupBody: 358 | Enabled: false 359 | RSpec/RepeatedExampleGroupDescription: 360 | Enabled: false 361 | RSpec/RepeatedIncludeExample: 362 | Enabled: false 363 | RSpec/ReturnFromStub: 364 | Enabled: false 365 | RSpec/SharedExamples: 366 | Enabled: false 367 | RSpec/StubbedMock: 368 | Enabled: false 369 | RSpec/UnspecifiedException: 370 | Enabled: false 371 | RSpec/VariableDefinition: 372 | Enabled: false 373 | RSpec/VoidExpect: 374 | Enabled: false 375 | RSpec/Yield: 376 | Enabled: false 377 | Security/Open: 378 | Enabled: false 379 | Style/AccessModifierDeclarations: 380 | Enabled: false 381 | Style/AccessorGrouping: 382 | Enabled: false 383 | Style/BisectedAttrAccessor: 384 | Enabled: false 385 | Style/CaseLikeIf: 386 | Enabled: false 387 | Style/ClassEqualityComparison: 388 | Enabled: false 389 | Style/ColonMethodDefinition: 390 | Enabled: false 391 | Style/CombinableLoops: 392 | Enabled: false 393 | Style/CommentedKeyword: 394 | Enabled: false 395 | Style/Dir: 396 | Enabled: false 397 | Style/DoubleCopDisableDirective: 398 | Enabled: false 399 | Style/EmptyBlockParameter: 400 | Enabled: false 401 | Style/EmptyLambdaParameter: 402 | Enabled: false 403 | Style/Encoding: 404 | Enabled: false 405 | Style/EvalWithLocation: 406 | Enabled: false 407 | Style/ExpandPathArguments: 408 | Enabled: false 409 | Style/ExplicitBlockArgument: 410 | Enabled: false 411 | Style/ExponentialNotation: 412 | Enabled: false 413 | Style/FloatDivision: 414 | Enabled: false 415 | Style/FrozenStringLiteralComment: 416 | Enabled: false 417 | Style/GlobalStdStream: 418 | Enabled: false 419 | Style/HashAsLastArrayItem: 420 | Enabled: false 421 | Style/HashLikeCase: 422 | Enabled: false 423 | Style/HashTransformKeys: 424 | Enabled: false 425 | Style/HashTransformValues: 426 | Enabled: false 427 | Style/IfUnlessModifier: 428 | Enabled: false 429 | Style/KeywordParametersOrder: 430 | Enabled: false 431 | Style/MinMax: 432 | Enabled: false 433 | Style/MixinUsage: 434 | Enabled: false 435 | Style/MultilineWhenThen: 436 | Enabled: false 437 | Style/NegatedUnless: 438 | Enabled: false 439 | Style/NumericPredicate: 440 | Enabled: false 441 | Style/OptionalBooleanParameter: 442 | Enabled: false 443 | Style/OrAssignment: 444 | Enabled: false 445 | Style/RandomWithOffset: 446 | Enabled: false 447 | Style/RedundantAssignment: 448 | Enabled: false 449 | Style/RedundantCondition: 450 | Enabled: false 451 | Style/RedundantConditional: 452 | Enabled: false 453 | Style/RedundantFetchBlock: 454 | Enabled: false 455 | Style/RedundantFileExtensionInRequire: 456 | Enabled: false 457 | Style/RedundantRegexpCharacterClass: 458 | Enabled: false 459 | Style/RedundantRegexpEscape: 460 | Enabled: false 461 | Style/RedundantSelfAssignment: 462 | Enabled: false 463 | Style/RedundantSort: 464 | Enabled: false 465 | Style/RescueStandardError: 466 | Enabled: false 467 | Style/SingleArgumentDig: 468 | Enabled: false 469 | Style/SlicingWithRange: 470 | Enabled: false 471 | Style/SoleNestedConditional: 472 | Enabled: false 473 | Style/StderrPuts: 474 | Enabled: false 475 | Style/StringConcatenation: 476 | Enabled: false 477 | Style/Strip: 478 | Enabled: false 479 | Style/SymbolProc: 480 | Enabled: false 481 | Style/TrailingBodyOnClass: 482 | Enabled: false 483 | Style/TrailingBodyOnMethodDefinition: 484 | Enabled: false 485 | Style/TrailingBodyOnModule: 486 | Enabled: false 487 | Style/TrailingCommaInHashLiteral: 488 | Enabled: false 489 | Style/TrailingMethodEndStatement: 490 | Enabled: false 491 | Style/UnpackFirst: 492 | Enabled: false 493 | Capybara/MatchStyle: 494 | Enabled: false 495 | Capybara/NegationMatcher: 496 | Enabled: false 497 | Capybara/SpecificActions: 498 | Enabled: false 499 | Capybara/SpecificFinders: 500 | Enabled: false 501 | Capybara/SpecificMatcher: 502 | Enabled: false 503 | Gemspec/DeprecatedAttributeAssignment: 504 | Enabled: false 505 | Gemspec/DevelopmentDependencies: 506 | Enabled: false 507 | Gemspec/RequireMFA: 508 | Enabled: false 509 | Layout/LineContinuationLeadingSpace: 510 | Enabled: false 511 | Layout/LineContinuationSpacing: 512 | Enabled: false 513 | Layout/LineEndStringConcatenationIndentation: 514 | Enabled: false 515 | Layout/SpaceBeforeBrackets: 516 | Enabled: false 517 | Lint/AmbiguousAssignment: 518 | Enabled: false 519 | Lint/AmbiguousOperatorPrecedence: 520 | Enabled: false 521 | Lint/AmbiguousRange: 522 | Enabled: false 523 | Lint/ConstantOverwrittenInRescue: 524 | Enabled: false 525 | Lint/DeprecatedConstants: 526 | Enabled: false 527 | Lint/DuplicateBranch: 528 | Enabled: false 529 | Lint/DuplicateMagicComment: 530 | Enabled: false 531 | Lint/DuplicateMatchPattern: 532 | Enabled: false 533 | Lint/DuplicateRegexpCharacterClassElement: 534 | Enabled: false 535 | Lint/EmptyBlock: 536 | Enabled: false 537 | Lint/EmptyClass: 538 | Enabled: false 539 | Lint/EmptyInPattern: 540 | Enabled: false 541 | Lint/IncompatibleIoSelectWithFiberScheduler: 542 | Enabled: false 543 | Lint/LambdaWithoutLiteralBlock: 544 | Enabled: false 545 | Lint/NoReturnInBeginEndBlocks: 546 | Enabled: false 547 | Lint/NonAtomicFileOperation: 548 | Enabled: false 549 | Lint/NumberedParameterAssignment: 550 | Enabled: false 551 | Lint/OrAssignmentToConstant: 552 | Enabled: false 553 | Lint/RedundantDirGlobSort: 554 | Enabled: false 555 | Lint/RefinementImportMethods: 556 | Enabled: false 557 | Lint/RequireRangeParentheses: 558 | Enabled: false 559 | Lint/RequireRelativeSelfPath: 560 | Enabled: false 561 | Lint/SymbolConversion: 562 | Enabled: false 563 | Lint/ToEnumArguments: 564 | Enabled: false 565 | Lint/TripleQuotes: 566 | Enabled: false 567 | Lint/UnexpectedBlockArity: 568 | Enabled: false 569 | Lint/UnmodifiedReduceAccumulator: 570 | Enabled: false 571 | Lint/UselessRescue: 572 | Enabled: false 573 | Lint/UselessRuby2Keywords: 574 | Enabled: false 575 | Metrics/CollectionLiteralLength: 576 | Enabled: false 577 | Naming/BlockForwarding: 578 | Enabled: false 579 | Performance/CollectionLiteralInLoop: 580 | Enabled: false 581 | Performance/ConcurrentMonotonicTime: 582 | Enabled: false 583 | Performance/MapCompact: 584 | Enabled: false 585 | Performance/RedundantEqualityComparisonBlock: 586 | Enabled: false 587 | Performance/RedundantSplitRegexpArgument: 588 | Enabled: false 589 | Performance/StringIdentifierArgument: 590 | Enabled: false 591 | RSpec/BeEq: 592 | Enabled: false 593 | RSpec/BeNil: 594 | Enabled: false 595 | RSpec/ChangeByZero: 596 | Enabled: false 597 | RSpec/ClassCheck: 598 | Enabled: false 599 | RSpec/DuplicatedMetadata: 600 | Enabled: false 601 | RSpec/ExcessiveDocstringSpacing: 602 | Enabled: false 603 | RSpec/FactoryBot/ConsistentParenthesesStyle: 604 | Enabled: false 605 | RSpec/FactoryBot/FactoryNameStyle: 606 | Enabled: false 607 | RSpec/FactoryBot/SyntaxMethods: 608 | Enabled: false 609 | RSpec/IdenticalEqualityAssertion: 610 | Enabled: false 611 | RSpec/NoExpectationExample: 612 | Enabled: false 613 | RSpec/PendingWithoutReason: 614 | Enabled: false 615 | RSpec/Rails/AvoidSetupHook: 616 | Enabled: false 617 | RSpec/Rails/HaveHttpStatus: 618 | Enabled: false 619 | RSpec/Rails/InferredSpecType: 620 | Enabled: false 621 | RSpec/Rails/MinitestAssertions: 622 | Enabled: false 623 | RSpec/Rails/TravelAround: 624 | Enabled: false 625 | RSpec/RedundantAround: 626 | Enabled: false 627 | RSpec/SkipBlockInsideExample: 628 | Enabled: false 629 | RSpec/SortMetadata: 630 | Enabled: false 631 | RSpec/SubjectDeclaration: 632 | Enabled: false 633 | RSpec/VerifiedDoubleReference: 634 | Enabled: false 635 | Security/CompoundHash: 636 | Enabled: false 637 | Security/IoMethods: 638 | Enabled: false 639 | Style/ArgumentsForwarding: 640 | Enabled: false 641 | Style/ArrayIntersect: 642 | Enabled: false 643 | Style/CollectionCompact: 644 | Enabled: false 645 | Style/ComparableClamp: 646 | Enabled: false 647 | Style/ConcatArrayLiterals: 648 | Enabled: false 649 | Style/DataInheritance: 650 | Enabled: false 651 | Style/DirEmpty: 652 | Enabled: false 653 | Style/DocumentDynamicEvalDefinition: 654 | Enabled: false 655 | Style/EmptyHeredoc: 656 | Enabled: false 657 | Style/EndlessMethod: 658 | Enabled: false 659 | Style/EnvHome: 660 | Enabled: false 661 | Style/FetchEnvVar: 662 | Enabled: false 663 | Style/FileEmpty: 664 | Enabled: false 665 | Style/FileRead: 666 | Enabled: false 667 | Style/FileWrite: 668 | Enabled: false 669 | Style/HashConversion: 670 | Enabled: false 671 | Style/HashExcept: 672 | Enabled: false 673 | Style/IfWithBooleanLiteralBranches: 674 | Enabled: false 675 | Style/InPatternThen: 676 | Enabled: false 677 | Style/MagicCommentFormat: 678 | Enabled: false 679 | Style/MapCompactWithConditionalBlock: 680 | Enabled: false 681 | Style/MapToHash: 682 | Enabled: false 683 | Style/MapToSet: 684 | Enabled: false 685 | Style/MinMaxComparison: 686 | Enabled: false 687 | Style/MultilineInPatternThen: 688 | Enabled: false 689 | Style/NegatedIfElseCondition: 690 | Enabled: false 691 | Style/NestedFileDirname: 692 | Enabled: false 693 | Style/NilLambda: 694 | Enabled: false 695 | Style/NumberedParameters: 696 | Enabled: false 697 | Style/NumberedParametersLimit: 698 | Enabled: false 699 | Style/ObjectThen: 700 | Enabled: false 701 | Style/OpenStructUse: 702 | Enabled: false 703 | Style/OperatorMethodCall: 704 | Enabled: false 705 | Style/QuotedSymbols: 706 | Enabled: false 707 | Style/RedundantArgument: 708 | Enabled: false 709 | Style/RedundantConstantBase: 710 | Enabled: false 711 | Style/RedundantDoubleSplatHashBraces: 712 | Enabled: false 713 | Style/RedundantEach: 714 | Enabled: false 715 | Style/RedundantHeredocDelimiterQuotes: 716 | Enabled: false 717 | Style/RedundantInitialize: 718 | Enabled: false 719 | Style/RedundantLineContinuation: 720 | Enabled: false 721 | Style/RedundantSelfAssignmentBranch: 722 | Enabled: false 723 | Style/RedundantStringEscape: 724 | Enabled: false 725 | Style/SelectByRegexp: 726 | Enabled: false 727 | Style/StringChars: 728 | Enabled: false 729 | Style/SwapValues: 730 | Enabled: false 731 | --------------------------------------------------------------------------------