├── .coveralls.yml ├── .gitmodules ├── .fixtures.yml ├── .msync.yml ├── .github ├── labeler.yml ├── workflows │ ├── labeler.yml │ ├── ci.yml │ ├── release.yml │ └── prepare_release.yml ├── PULL_REQUEST_TEMPLATE.md ├── ISSUE_TEMPLATE.md └── release.yml ├── .puppet-lint.rc ├── .rubocop.yml ├── .editorconfig ├── spec ├── spec_helper_acceptance.rb ├── fixtures │ └── unit │ │ └── puppet │ │ └── provider │ │ └── kernel_parameter │ │ ├── grub2 │ │ ├── broken │ │ └── full │ │ └── grub │ │ ├── full │ │ └── broken ├── support │ └── spec │ │ └── psh_fixtures.rb ├── spec_helper.rb ├── acceptance │ ├── 06_grub2_superuser_spec.rb │ ├── 00_kernel_parameter_spec.rb │ ├── 10_grub_menuentry_spec.rb │ ├── 10_grub_config_spec.rb │ └── 05_grub2_users_spec.rb └── unit │ ├── puppet │ └── provider │ │ └── kernel_parameter │ │ └── grub2_spec.rb │ └── puppetx │ └── augeasproviders_grub │ └── util_spec.rb ├── .sync.yml ├── .gitignore ├── .pmtignore ├── Gemfile ├── Rakefile ├── .travis.sh ├── .overcommit.yml ├── .rubocop_todo.yml ├── lib ├── puppet │ ├── type │ │ ├── kernel_parameter.rb │ │ ├── grub_config.rb │ │ ├── grub_user.rb │ │ └── grub_menuentry.rb │ └── provider │ │ ├── grub_config │ │ └── grub2.rb │ │ ├── kernel_parameter │ │ └── grub2.rb │ │ ├── grub_user │ │ └── grub2.rb │ │ └── grub_menuentry │ │ └── grub2.rb └── puppetx │ └── augeasproviders_grub │ └── util.rb ├── metadata.json ├── README.md ├── LICENSE ├── REFERENCE.md └── CHANGELOG.md /.coveralls.yml: -------------------------------------------------------------------------------- 1 | service_name: travis-ci 2 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "augeas"] 2 | path = augeas 3 | url = git://github.com/hercules-team/augeas.git 4 | -------------------------------------------------------------------------------- /.fixtures.yml: -------------------------------------------------------------------------------- 1 | --- 2 | fixtures: 3 | repositories: 4 | augeasproviders_core: https://github.com/voxpupuli/puppet-augeasproviders_core 5 | -------------------------------------------------------------------------------- /.msync.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # Managed by modulesync - DO NOT EDIT 3 | # https://voxpupuli.org/docs/updating-files-managed-with-modulesync/ 4 | 5 | modulesync_config_version: '10.3.0' 6 | -------------------------------------------------------------------------------- /.github/labeler.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # Managed by modulesync - DO NOT EDIT 3 | # https://voxpupuli.org/docs/updating-files-managed-with-modulesync/ 4 | 5 | skip-changelog: 6 | - head-branch: ['^release-*', 'release'] 7 | -------------------------------------------------------------------------------- /.puppet-lint.rc: -------------------------------------------------------------------------------- 1 | # Managed by modulesync - DO NOT EDIT 2 | # https://voxpupuli.org/docs/updating-files-managed-with-modulesync/ 3 | 4 | --fail-on-warnings 5 | --no-parameter_documentation-check 6 | --no-parameter_types-check 7 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # Managed by modulesync - DO NOT EDIT 3 | # https://voxpupuli.org/docs/updating-files-managed-with-modulesync/ 4 | 5 | inherit_from: .rubocop_todo.yml 6 | inherit_gem: 7 | voxpupuli-test: rubocop.yml 8 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | 3 | # Managed by modulesync - DO NOT EDIT 4 | # https://voxpupuli.org/docs/updating-files-managed-with-modulesync/ 5 | 6 | root = true 7 | 8 | [*] 9 | charset = utf-8 10 | end_of_line = lf 11 | indent_size = 2 12 | tab_width = 2 13 | indent_style = space 14 | insert_final_newline = true 15 | trim_trailing_whitespace = true 16 | -------------------------------------------------------------------------------- /spec/spec_helper_acceptance.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Managed by modulesync - DO NOT EDIT 4 | # https://voxpupuli.org/docs/updating-files-managed-with-modulesync/ 5 | 6 | require 'voxpupuli/acceptance/spec_helper_acceptance' 7 | 8 | configure_beaker(modules: :metadata) 9 | 10 | Dir['./spec/support/acceptance/**/*.rb'].sort.each { |f| require f } 11 | -------------------------------------------------------------------------------- /spec/fixtures/unit/puppet/provider/kernel_parameter/grub2/broken: -------------------------------------------------------------------------------- 1 | GRUB_TIMEOUT = 5 2 | GRUB_DISTRIBUTOR="$(sed 's, release .*$,,g' /etc/system-release)" 3 | GRUB_DEFAULT=saved 4 | # GRUB_TERMINAL="serial console" 5 | # GRUB_SERIAL_COMMAND="serial --unit=0 --speed=9600" 6 | GRUB_CMDLINE_LINUX="quiet elevator=noop divider=10" 7 | GRUB_CMDLINE_LINUX_DEFAULT="rhgb nohz=on" 8 | GRUB_DISABLE_RECOVERY="true" 9 | GRUB_THEME=/boot/grub2/themes/system/theme.txt 10 | -------------------------------------------------------------------------------- /spec/fixtures/unit/puppet/provider/kernel_parameter/grub2/full: -------------------------------------------------------------------------------- 1 | GRUB_TIMEOUT=5 2 | GRUB_DISTRIBUTOR="$(sed 's, release .*$,,g' /etc/system-release)" 3 | GRUB_DEFAULT=saved 4 | # GRUB_TERMINAL="serial console" 5 | # GRUB_SERIAL_COMMAND="serial --unit=0 --speed=9600" 6 | GRUB_CMDLINE_LINUX="quiet elevator=noop divider=10" 7 | GRUB_CMDLINE_LINUX_DEFAULT="rhgb nohz=on" 8 | GRUB_DISABLE_RECOVERY="true" 9 | GRUB_THEME=/boot/grub2/themes/system/theme.txt 10 | -------------------------------------------------------------------------------- /.sync.yml: -------------------------------------------------------------------------------- 1 | --- 2 | .github/workflows/ci.yml: 3 | with: 4 | additional_packages: libaugeas-dev augeas-tools 5 | Gemfile: 6 | optional: 7 | ':test': 8 | - gem: ruby-augeas 9 | spec/spec_helper.rb: 10 | spec_overrides: 11 | - "require 'augeas_spec'" 12 | - "# augeasproviders: setting $LOAD_PATH to work around broken type autoloading" 13 | - "$LOAD_PATH.unshift(File.join(__dir__, 'fixtures/modules/augeasproviders_core/lib'))" 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Managed by modulesync - DO NOT EDIT 2 | # https://voxpupuli.org/docs/updating-files-managed-with-modulesync/ 3 | 4 | /pkg/ 5 | /Gemfile.lock 6 | /Gemfile.local 7 | /vendor/ 8 | /.vendor/ 9 | /spec/fixtures/manifests/ 10 | /spec/fixtures/modules/ 11 | /.vagrant/ 12 | /.bundle/ 13 | /.ruby-version 14 | /coverage/ 15 | /log/ 16 | /.idea/ 17 | /.dependencies/ 18 | /.librarian/ 19 | /Puppetfile.lock 20 | *.iml 21 | .*.sw? 22 | /.yardoc/ 23 | /Guardfile 24 | bolt-debug.log 25 | .rerun.json 26 | -------------------------------------------------------------------------------- /.github/workflows/labeler.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # Managed by modulesync - DO NOT EDIT 3 | # https://voxpupuli.org/docs/updating-files-managed-with-modulesync/ 4 | 5 | name: "Pull Request Labeler" 6 | 7 | # yamllint disable-line rule:truthy 8 | on: 9 | pull_request_target: {} 10 | 11 | permissions: 12 | contents: read 13 | pull-requests: write 14 | 15 | jobs: 16 | labeler: 17 | permissions: 18 | contents: read 19 | pull-requests: write 20 | runs-on: ubuntu-latest 21 | steps: 22 | - uses: actions/labeler@v5 23 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # Managed by modulesync - DO NOT EDIT 3 | # https://voxpupuli.org/docs/updating-files-managed-with-modulesync/ 4 | 5 | name: CI 6 | 7 | # yamllint disable-line rule:truthy 8 | on: 9 | pull_request: {} 10 | push: 11 | branches: 12 | - main 13 | - master 14 | 15 | concurrency: 16 | group: ${{ github.ref_name }} 17 | cancel-in-progress: true 18 | 19 | permissions: 20 | contents: read 21 | 22 | jobs: 23 | puppet: 24 | name: Puppet 25 | uses: voxpupuli/gha-puppet/.github/workflows/basic.yml@v3 26 | with: 27 | additional_packages: 'libaugeas-dev augeas-tools' 28 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 9 | #### Pull Request (PR) description 10 | 13 | 14 | #### This Pull Request (PR) fixes the following issues 15 | 21 | -------------------------------------------------------------------------------- /.pmtignore: -------------------------------------------------------------------------------- 1 | # Managed by modulesync - DO NOT EDIT 2 | # https://voxpupuli.org/docs/updating-files-managed-with-modulesync/ 3 | 4 | /docs/ 5 | /pkg/ 6 | /Gemfile 7 | /Gemfile.lock 8 | /Gemfile.local 9 | /vendor/ 10 | /.vendor/ 11 | /spec/ 12 | /Rakefile 13 | /.vagrant/ 14 | /.bundle/ 15 | /.ruby-version 16 | /coverage/ 17 | /log/ 18 | /.idea/ 19 | /.dependencies/ 20 | /.github/ 21 | /.librarian/ 22 | /Puppetfile.lock 23 | /Puppetfile 24 | *.iml 25 | /.editorconfig 26 | /.fixtures.yml 27 | /.gitignore 28 | /.msync.yml 29 | /.overcommit.yml 30 | /.pmtignore 31 | /.rspec 32 | /.rspec_parallel 33 | /.rubocop.yml 34 | /.sync.yml 35 | .*.sw? 36 | /.yardoc/ 37 | /.yardopts 38 | /Dockerfile 39 | /HISTORY.md 40 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # Managed by modulesync - DO NOT EDIT 3 | # https://voxpupuli.org/docs/updating-files-managed-with-modulesync/ 4 | 5 | name: Release 6 | 7 | # yamllint disable-line rule:truthy 8 | on: 9 | push: 10 | tags: 11 | - '*' 12 | 13 | permissions: 14 | contents: write 15 | 16 | jobs: 17 | release: 18 | name: Release 19 | uses: voxpupuli/gha-puppet/.github/workflows/release.yml@v3 20 | with: 21 | allowed_owner: 'voxpupuli' 22 | secrets: 23 | # Configure secrets here: 24 | # https://docs.github.com/en/actions/security-guides/encrypted-secrets 25 | username: ${{ secrets.PUPPET_FORGE_USERNAME }} 26 | api_key: ${{ secrets.PUPPET_FORGE_API_KEY }} 27 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 10 | 11 | ## Affected Puppet, Ruby, OS and module versions/distributions 12 | 13 | - Puppet: 14 | - Ruby: 15 | - Distribution: 16 | - Module version: 17 | 18 | ## How to reproduce (e.g Puppet code you use) 19 | 20 | ## What are you seeing 21 | 22 | ## What behaviour did you expect instead 23 | 24 | ## Output log 25 | 26 | ## Any additional information you'd like to impart 27 | -------------------------------------------------------------------------------- /.github/workflows/prepare_release.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # Managed by modulesync - DO NOT EDIT 3 | # https://voxpupuli.org/docs/updating-files-managed-with-modulesync/ 4 | 5 | name: 'Prepare Release' 6 | 7 | on: 8 | workflow_dispatch: 9 | inputs: 10 | version: 11 | description: 'Module version to be released. Must be a valid semver string without leading v. (1.2.3)' 12 | required: false 13 | 14 | permissions: 15 | contents: write 16 | pull-requests: write 17 | 18 | jobs: 19 | release_prep: 20 | uses: 'voxpupuli/gha-puppet/.github/workflows/prepare_release.yml@v3' 21 | with: 22 | version: ${{ github.event.inputs.version }} 23 | allowed_owner: 'voxpupuli' 24 | secrets: 25 | # Configure secrets here: 26 | # https://docs.github.com/en/actions/security-guides/encrypted-secrets 27 | github_pat: '${{ secrets.PCCI_PAT_RELEASE_PREP }}' 28 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # Managed by modulesync - DO NOT EDIT 2 | # https://voxpupuli.org/docs/updating-files-managed-with-modulesync/ 3 | 4 | source ENV['GEM_SOURCE'] || 'https://rubygems.org' 5 | 6 | group :test do 7 | gem 'voxpupuli-test', '~> 13.0', :require => false 8 | gem 'puppet_metadata', '~> 5.0', :require => false 9 | gem 'ruby-augeas', :require => false 10 | end 11 | 12 | group :development do 13 | gem 'guard-rake', :require => false 14 | gem 'overcommit', '>= 0.39.1', :require => false 15 | end 16 | 17 | group :system_tests do 18 | gem 'voxpupuli-acceptance', '~> 4.0', :require => false 19 | end 20 | 21 | group :release do 22 | gem 'voxpupuli-release', '~> 5.0', :require => false 23 | end 24 | 25 | gem 'rake', :require => false 26 | 27 | gem 'openvox', ENV.fetch('OPENVOX_GEM_VERSION', [">= 7", "< 9"]), :require => false, :groups => [:test] 28 | 29 | # vim: syntax=ruby 30 | -------------------------------------------------------------------------------- /spec/support/spec/psh_fixtures.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | def fixtures(*rest) 4 | File.join('spec', 'fixtures', *rest) 5 | end 6 | 7 | # Returns the path to your relative fixture dir. So if your spec test is 8 | # /spec/unit/facter/foo_spec.rb then your relative dir will be 9 | # /spec/fixture/unit/facter/foo 10 | def my_fixture_dir 11 | callers = caller 12 | while (line = callers.shift) 13 | next unless (found = line.match(%r{/spec/(.*)_spec\.rb:})) 14 | 15 | return fixtures(found[1]) 16 | end 17 | raise "sorry, I couldn't work out your path from the caller stack!" 18 | end 19 | 20 | # Given a name, returns the full path of a file from your relative fixture 21 | # dir as returned by my_fixture_dir. 22 | def my_fixture(name) 23 | file = File.join(my_fixture_dir, name) 24 | raise "fixture '#{name}' for #{my_fixture_dir} is not readable" unless File.readable? file 25 | 26 | file 27 | end 28 | -------------------------------------------------------------------------------- /.github/release.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # Managed by modulesync - DO NOT EDIT 3 | # https://voxpupuli.org/docs/updating-files-managed-with-modulesync/ 4 | 5 | # https://docs.github.com/en/repositories/releasing-projects-on-github/automatically-generated-release-notes 6 | 7 | changelog: 8 | exclude: 9 | labels: 10 | - duplicate 11 | - invalid 12 | - modulesync 13 | - question 14 | - skip-changelog 15 | - wont-fix 16 | - wontfix 17 | 18 | categories: 19 | - title: Breaking Changes 🛠 20 | labels: 21 | - backwards-incompatible 22 | 23 | - title: New Features 🎉 24 | labels: 25 | - enhancement 26 | 27 | - title: Bug Fixes 🐛 28 | labels: 29 | - bug 30 | 31 | - title: Documentation Updates 📚 32 | labels: 33 | - documentation 34 | - docs 35 | 36 | - title: Dependency Updates ⬆️ 37 | labels: 38 | - dependencies 39 | 40 | - title: Other Changes 41 | labels: 42 | - "*" 43 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # Managed by modulesync - DO NOT EDIT 2 | # https://voxpupuli.org/docs/updating-files-managed-with-modulesync/ 3 | 4 | begin 5 | require 'voxpupuli/test/rake' 6 | rescue LoadError 7 | # only available if gem group test is installed 8 | end 9 | 10 | begin 11 | require 'voxpupuli/acceptance/rake' 12 | rescue LoadError 13 | # only available if gem group acceptance is installed 14 | end 15 | 16 | begin 17 | require 'voxpupuli/release/rake_tasks' 18 | rescue LoadError 19 | # only available if gem group releases is installed 20 | else 21 | GCGConfig.user = 'voxpupuli' 22 | GCGConfig.project = 'puppet-augeasproviders_grub' 23 | end 24 | 25 | desc "Run main 'test' task and report merged results to coveralls" 26 | task test_with_coveralls: [:test] do 27 | if Dir.exist?(File.expand_path('../lib', __FILE__)) 28 | require 'coveralls/rake/task' 29 | Coveralls::RakeTask.new 30 | Rake::Task['coveralls:push'].invoke 31 | else 32 | puts 'Skipping reporting to coveralls. Module has no lib dir' 33 | end 34 | end 35 | 36 | # vim: syntax=ruby 37 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Managed by modulesync - DO NOT EDIT 4 | # https://voxpupuli.org/docs/updating-files-managed-with-modulesync/ 5 | 6 | # puppetlabs_spec_helper will set up coverage if the env variable is set. 7 | # We want to do this if lib exists and it hasn't been explicitly set. 8 | ENV['COVERAGE'] ||= 'yes' if Dir.exist?(File.expand_path('../lib', __dir__)) 9 | 10 | require 'voxpupuli/test/spec_helper' 11 | 12 | RSpec.configure do |c| 13 | c.facterdb_string_keys = false 14 | end 15 | 16 | add_mocked_facts! 17 | 18 | if File.exist?(File.join(__dir__, 'default_module_facts.yml')) 19 | facts = YAML.safe_load(File.read(File.join(__dir__, 'default_module_facts.yml'))) 20 | facts&.each do |name, value| 21 | add_custom_fact name.to_sym, value 22 | end 23 | end 24 | 25 | require 'augeas_spec' 26 | 27 | # augeasproviders: setting $LOAD_PATH to work around broken type autoloading 28 | 29 | $LOAD_PATH.unshift(File.join(__dir__, 'fixtures/modules/augeasproviders_core/lib')) 30 | Dir['./spec/support/spec/**/*.rb'].sort.each { |f| require f } 31 | -------------------------------------------------------------------------------- /.travis.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -xe 3 | 4 | # Clone submodules in tree 5 | git submodule update --init 6 | 7 | if [ -z $AUGEAS ]; then 8 | # Use latest version of lenses 9 | cd augeas && git pull origin master 10 | PKG_VERSION="" 11 | else 12 | if [ -z $LENSES ]; then 13 | # Use matching version of lenses 14 | cd augeas && git fetch && git checkout release-${AUGEAS} 15 | else 16 | cd augeas && git fetch && git checkout $LENSES 17 | fi 18 | 19 | PKG_VERSION="=${AUGEAS}*" 20 | # Add PPA 21 | sudo add-apt-repository -y ppa:raphink/augeas-1.0.0 22 | sudo add-apt-repository -y ppa:raphink/augeas-1.1.0 23 | sudo add-apt-repository -y ppa:raphink/augeas-1.2.0 24 | sudo add-apt-repository -y ppa:raphink/augeas-1.3.0 25 | fi 26 | sudo add-apt-repository -y ppa:raphink/augeas 27 | sudo apt-get update 28 | sudo apt-get install augeas-tools${PKG_VERSION} \ 29 | augeas-lenses${PKG_VERSION} \ 30 | libaugeas0${PKG_VERSION} \ 31 | libaugeas-dev${PKG_VERSION} \ 32 | libxml2-dev 33 | 34 | # Install gems 35 | gem install bundler 36 | bundle install 37 | 38 | # Reporting only 39 | bundle show 40 | puppet --version 41 | augtool --version 42 | -------------------------------------------------------------------------------- /spec/fixtures/unit/puppet/provider/kernel_parameter/grub/full: -------------------------------------------------------------------------------- 1 | # grub.conf generated by anaconda 2 | # 3 | # Note that you do not have to rerun grub after making changes to this file 4 | # NOTICE: You have a /boot partition. This means that 5 | # all kernel and initrd paths are relative to /boot/, eg. 6 | # root (hd0,0) 7 | # kernel /vmlinuz-version ro root=/dev/VolGroup00/LogVol00 8 | # initrd /initrd-version.img 9 | #boot=/dev/vda 10 | default=0 11 | timeout=5 12 | splashimage=(hd0,0)/grub/splash.xpm.gz 13 | hiddenmenu 14 | title Red Hat Enterprise Linux Server (2.6.18-308.13.1.el5) 15 | root (hd0,0) 16 | kernel /vmlinuz-2.6.18-308.13.1.el5 ro root=/dev/VolGroup00/LogVol00 rhgb quiet elevator=noop divider=10 rd_LVM_LV=vg/lv1 rd_LVM_LV=vg/lv2 17 | initrd /initrd-2.6.18-308.13.1.el5.img 18 | title Red Hat Enterprise Linux Server (2.6.18-308.13.1.el5) (recovery mode) 19 | root (hd0,0) 20 | kernel /vmlinuz-2.6.18-308.13.1.el5 ro root=/dev/VolGroup00/LogVol00 elevator=noop divider=10 S 21 | initrd /initrd-2.6.18-308.13.1.el5.img 22 | title Red Hat Enterprise Linux Server (2.6.18-194.el5) 23 | root (hd0,0) 24 | kernel /vmlinuz-2.6.18-194.el5 ro root=/dev/VolGroup00/LogVol00 rhgb quiet elevator=noop divider=10 splash nohz=on 25 | initrd /initrd-2.6.18-194.el5.img 26 | -------------------------------------------------------------------------------- /spec/fixtures/unit/puppet/provider/kernel_parameter/grub/broken: -------------------------------------------------------------------------------- 1 | # grub.conf generated by anaconda 2 | # 3 | # Note that you do not have to rerun grub after making changes to this file 4 | # NOTICE: You have a /boot partition. This means that 5 | # all kernel and initrd paths are relative to /boot/, eg. 6 | # root (hd0,0) 7 | # kernel /vmlinuz-version ro root=/dev/VolGroup00/LogVol00 8 | # initrd /initrd-version.img 9 | #boot=/dev/vda 10 | {{{invalidentry}}} 11 | default=0 12 | timeout=5 13 | splashimage=(hd0,0)/grub/splash.xpm.gz 14 | hiddenmenu 15 | title Red Hat Enterprise Linux Server (2.6.18-308.13.1.el5) 16 | root (hd0,0) 17 | kernel /vmlinuz-2.6.18-308.13.1.el5 ro root=/dev/VolGroup00/LogVol00 rhgb quiet elevator=noop divider=10 rd_LVM_LV=vg/lv1 rd_LVM_LV=vg/lv2 18 | initrd /initrd-2.6.18-308.13.1.el5.img 19 | title Red Hat Enterprise Linux Server (2.6.18-308.13.1.el5) (recovery mode) 20 | root (hd0,0) 21 | kernel /vmlinuz-2.6.18-308.13.1.el5 ro root=/dev/VolGroup00/LogVol00 elevator=noop divider=10 S 22 | initrd /initrd-2.6.18-308.13.1.el5.img 23 | title Red Hat Enterprise Linux Server (2.6.18-194.el5) 24 | root (hd0,0) 25 | kernel /vmlinuz-2.6.18-194.el5 ro root=/dev/VolGroup00/LogVol00 rhgb quiet elevator=noop divider=10 splash nohz=on 26 | initrd /initrd-2.6.18-194.el5.img 27 | -------------------------------------------------------------------------------- /spec/acceptance/06_grub2_superuser_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper_acceptance' 4 | 5 | test_name 'Augeasproviders Grub' 6 | 7 | describe 'GRUB2 User Tests' do 8 | hosts.each do |host| 9 | context "on #{host}" do 10 | context 'set a root superuser password' do 11 | let(:manifest) do 12 | %( 13 | grub_user { 'root': 14 | superuser => true, 15 | password => 'P@ssw0rdP@ssw0rd' 16 | } 17 | ) 18 | end 19 | 20 | it 'works with no errors' do 21 | apply_manifest_on(host, manifest, catch_failures: true) 22 | end 23 | 24 | it 'is idempotent' do 25 | apply_manifest_on(host, manifest, catch_changes: true) 26 | end 27 | end 28 | 29 | context 'with multiple superusers' do 30 | let(:manifest) do 31 | %( 32 | grub_user { 'root': 33 | superuser => true, 34 | password => 'P@ssw0rdP@ssw0rd' 35 | } 36 | 37 | grub_user { 'other_root': 38 | superuser => true, 39 | password => 'P@ssw0rdP@ssw0rd' 40 | } 41 | ) 42 | end 43 | 44 | it 'works with no errors' do 45 | apply_manifest_on(host, manifest, catch_failures: true) 46 | end 47 | 48 | it 'is idempotent' do 49 | apply_manifest_on(host, manifest, catch_changes: true) 50 | end 51 | end 52 | end 53 | end 54 | end 55 | -------------------------------------------------------------------------------- /.overcommit.yml: -------------------------------------------------------------------------------- 1 | # Managed by modulesync - DO NOT EDIT 2 | # https://voxpupuli.org/docs/updating-files-managed-with-modulesync/ 3 | # 4 | # Hooks are only enabled if you take action. 5 | # 6 | # To enable the hooks run: 7 | # 8 | # ``` 9 | # bundle exec overcommit --install 10 | # # ensure .overcommit.yml does not harm to you and then 11 | # bundle exec overcommit --sign 12 | # ``` 13 | # 14 | # (it will manage the .git/hooks directory): 15 | # 16 | # Examples howto skip a test for a commit or push: 17 | # 18 | # ``` 19 | # SKIP=RuboCop git commit 20 | # SKIP=PuppetLint git commit 21 | # SKIP=RakeTask git push 22 | # ``` 23 | # 24 | # Don't invoke overcommit at all: 25 | # 26 | # ``` 27 | # OVERCOMMIT_DISABLE=1 git commit 28 | # ``` 29 | # 30 | # Read more about overcommit: https://github.com/brigade/overcommit 31 | # 32 | # To manage this config yourself in your module add 33 | # 34 | # ``` 35 | # .overcommit.yml: 36 | # unmanaged: true 37 | # ``` 38 | # 39 | # to your modules .sync.yml config 40 | --- 41 | PreCommit: 42 | RuboCop: 43 | enabled: true 44 | description: 'Runs rubocop on modified files only' 45 | command: ['bundle', 'exec', 'rubocop'] 46 | RakeTarget: 47 | enabled: true 48 | description: 'Runs lint on modified files only' 49 | targets: 50 | - 'lint' 51 | command: ['bundle', 'exec', 'rake'] 52 | YamlSyntax: 53 | enabled: true 54 | JsonSyntax: 55 | enabled: true 56 | TrailingWhitespace: 57 | enabled: true 58 | 59 | PrePush: 60 | RakeTarget: 61 | enabled: true 62 | description: 'Run rake targets' 63 | targets: 64 | - 'validate' 65 | - 'test' 66 | - 'rubocop' 67 | command: ['bundle', 'exec', 'rake'] 68 | -------------------------------------------------------------------------------- /spec/acceptance/00_kernel_parameter_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper_acceptance' 4 | 5 | test_name 'Augeasproviders Grub' 6 | 7 | describe 'Kernel Parameter Tests' do 8 | tests = { 9 | basic: { 10 | manifest: %( 11 | kernel_parameter { 'audit': 12 | value => '1' 13 | }), 14 | test: %(grep -q "audit=1" /proc/cmdline) 15 | }, 16 | normal_bootmode: { 17 | manifest: %( 18 | kernel_parameter { 'audit': 19 | value => '1', 20 | bootmode => 'normal' 21 | }), 22 | test: %(grep -q "audit=1" /proc/cmdline) 23 | } 24 | } 25 | 26 | tests.each do |name, params| 27 | context "default parameters for #{name}" do 28 | let(:manifest) { params[:manifest] } 29 | let(:test) { params[:test] } 30 | 31 | hosts.each do |host| 32 | context "on #{host}" do 33 | # Using puppet_apply as a helper 34 | it 'works with no errors' do 35 | apply_manifest_on(host, manifest, catch_failures: true) 36 | end 37 | 38 | it 'is idempotent' do 39 | apply_manifest_on(host, manifest, catch_changes: true) 40 | end 41 | 42 | it 'is expected to have auditing enabled at boot time' do 43 | # Scrub out any custom boot entries that were added by other GRUB2 44 | # tests 45 | on(host, 'rm -rf /etc/grub.d/05_puppet_managed*') 46 | on(host, 'which grub2-mkconfig > /dev/null 2>&1 && grub2-mkconfig -o /etc/grub2.cfg', accept_all_exit_codes: true) 47 | 48 | host.reboot 49 | on(host, test) 50 | end 51 | end 52 | end 53 | end 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /spec/acceptance/10_grub_menuentry_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper_acceptance' 4 | 5 | test_name 'Augeasproviders Grub' 6 | 7 | describe 'GRUB Menuentry Tests' do 8 | hosts.each do |host| 9 | context "on #{host}" do 10 | context 'set new default kernel' do 11 | let(:manifest) do 12 | %( 13 | grub_menuentry { 'Standard': 14 | default_entry => true, 15 | root => '(hd0,msdos1)', 16 | kernel => ':preserve:', 17 | initrd => ':preserve:', 18 | kernel_options => [':preserve:', 'trogdor=BURNINATE'] 19 | } 20 | ) 21 | end 22 | 23 | # Using puppet_apply as a helper 24 | it 'works with no errors' do 25 | apply_manifest_on(host, manifest, catch_failures: true) 26 | end 27 | 28 | it 'is idempotent' do 29 | apply_manifest_on(host, manifest, catch_changes: true) 30 | end 31 | 32 | it 'has set the default to the new entry' do 33 | result = on(host, %(grubby --info=DEFAULT)).stdout 34 | result_hash = {} 35 | result.each_line do |line| 36 | line =~ %r{^\s*(.*?)=(.*)\s*$} 37 | result_hash[Regexp.last_match(1).strip] = Regexp.last_match(2).strip 38 | end 39 | 40 | expect(result_hash['title'].delete('"')).to eq('Standard') 41 | expect(result_hash['args'].delete('"')).to include('trogdor=BURNINATE') 42 | end 43 | 44 | it 'activates on reboot' do 45 | host.reboot 46 | 47 | result = on(host, %(cat /proc/cmdline)).stdout 48 | expect(result.split(%r{\s+})).to include('trogdor=BURNINATE') 49 | end 50 | end 51 | end 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /.rubocop_todo.yml: -------------------------------------------------------------------------------- 1 | # This configuration was generated by 2 | # `rubocop --auto-gen-config` 3 | # on 2023-08-17 21:29:32 UTC using RuboCop version 1.50.2. 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: 1 10 | # This cop supports unsafe autocorrection (--autocorrect-all). 11 | Lint/NonAtomicFileOperation: 12 | Exclude: 13 | - 'lib/puppet/provider/grub_menuentry/grub2.rb' 14 | 15 | # Offense count: 7 16 | RSpec/AnyInstance: 17 | Exclude: 18 | - 'spec/unit/puppet/provider/kernel_parameter/grub2_spec.rb' 19 | 20 | # Offense count: 4 21 | # This cop supports unsafe autocorrection (--autocorrect-all). 22 | RSpec/BeEq: 23 | Exclude: 24 | - 'spec/unit/puppet/provider/kernel_parameter/grub2_spec.rb' 25 | 26 | # Offense count: 4 27 | # Configuration parameters: AssignmentOnly. 28 | RSpec/InstanceVariable: 29 | Exclude: 30 | - 'spec/unit/puppet/provider/kernel_parameter/grub2_spec.rb' 31 | 32 | # Offense count: 1 33 | RSpec/MultipleDescribes: 34 | Exclude: 35 | - 'spec/unit/puppet/provider/kernel_parameter/grub2_spec.rb' 36 | 37 | # Offense count: 2 38 | RSpec/RepeatedExampleGroupDescription: 39 | Exclude: 40 | - 'spec/unit/puppet/provider/kernel_parameter/grub2_spec.rb' 41 | 42 | # Offense count: 2 43 | # This cop supports unsafe autocorrection (--autocorrect-all). 44 | Style/MapToHash: 45 | Exclude: 46 | - 'lib/puppet/provider/grub_menuentry/grub2.rb' 47 | 48 | # Offense count: 3 49 | # This cop supports unsafe autocorrection (--autocorrect-all). 50 | Style/SlicingWithRange: 51 | Exclude: 52 | - 'lib/puppet/provider/grub_menuentry/grub2.rb' 53 | - 'lib/puppetx/augeasproviders_grub/util.rb' 54 | -------------------------------------------------------------------------------- /lib/puppet/type/kernel_parameter.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Manages kernel parameters stored in bootloaders such as GRUB. 4 | # 5 | # Copyright (c) 2012 Dominic Cleal 6 | # Licensed under the Apache License, Version 2.0 7 | 8 | Puppet::Type.newtype(:kernel_parameter) do 9 | @doc = 'Manages kernel parameters stored in bootloaders.' 10 | 11 | ensurable do 12 | desc 'Whether this kernel parameter should be present on the selected boot entries.' 13 | defaultvalues 14 | defaultto :present 15 | end 16 | 17 | newparam(:name) do 18 | desc "The parameter name, e.g. 'quiet' or 'vga'." 19 | isnamevar 20 | end 21 | 22 | newproperty(:value, array_matching: :all) do 23 | desc "Value of the parameter if applicable. Many parameters are just keywords so this must be left blank, while others (e.g. 'vga') will take a value." 24 | end 25 | 26 | newparam(:target) do 27 | desc 'The bootloader configuration file, if in a non-default location for the provider.' 28 | end 29 | 30 | newparam(:bootmode) do 31 | desc "Boot mode(s) to apply the parameter to. Either 'all' (default) to use the parameter on all boots (normal and recovery mode), 'default' for just the default boot entry, 'normal' for just normal boots or 'recovery' for just recovery boots." 32 | 33 | isnamevar 34 | 35 | newvalues :all, :default, :normal, :recovery 36 | 37 | defaultto :all 38 | end 39 | 40 | autorequire(:file) do 41 | self[:target] 42 | end 43 | 44 | # title_patterns method for mapping titles to namevars for supporting 45 | # composite namevars. 46 | # https://github.com/puppetlabs/puppetlabs-java_ks/blob/5bc34745a6f86e0c4af495e0ad3559c82d57873a/lib/puppet/type/java_ks.rb#L209 47 | def self.title_patterns 48 | [ 49 | [ 50 | %r{\A([^:]+)\z}, 51 | [ 52 | [:name], 53 | ], 54 | ], 55 | [ 56 | %r{^([^:]+):([^:]+)$}, 57 | [ 58 | [:name], 59 | [:bootmode], 60 | ], 61 | ], 62 | ] 63 | end 64 | end 65 | -------------------------------------------------------------------------------- /metadata.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "puppet-augeasproviders_grub", 3 | "version": "5.1.3-rc0", 4 | "author": "Vox Pupuli", 5 | "summary": "Augeas-based grub types and providers for Puppet", 6 | "license": "Apache-2.0", 7 | "source": "https://github.com/voxpupuli/augeasproviders_grub", 8 | "project_page": "https://github.com/voxpupuli/puppet-augeasproviders_grub", 9 | "issues_url": "https://github.com/voxpupuli/augeasproviders_grub/issues", 10 | "description": "This module provides types/providers for grub configuration files using the Augeas configuration API library.", 11 | "dependencies": [ 12 | { 13 | "name": "puppet/augeasproviders_core", 14 | "version_requirement": ">=2.4.0 < 5.0.0" 15 | } 16 | ], 17 | "operatingsystem_support": [ 18 | { 19 | "operatingsystem": "Debian", 20 | "operatingsystemrelease": [ 21 | "10", 22 | "11", 23 | "12" 24 | ] 25 | }, 26 | { 27 | "operatingsystem": "Ubuntu", 28 | "operatingsystemrelease": [ 29 | "18.04", 30 | "20.04", 31 | "22.04", 32 | "24.04" 33 | ] 34 | }, 35 | { 36 | "operatingsystem": "RedHat", 37 | "operatingsystemrelease": [ 38 | "8", 39 | "9", 40 | "10" 41 | ] 42 | }, 43 | { 44 | "operatingsystem": "AlmaLinux", 45 | "operatingsystemrelease": [ 46 | "8", 47 | "9", 48 | "10" 49 | ] 50 | }, 51 | { 52 | "operatingsystem": "Rocky", 53 | "operatingsystemrelease": [ 54 | "8", 55 | "9", 56 | "10" 57 | ] 58 | }, 59 | { 60 | "operatingsystem": "CentOS", 61 | "operatingsystemrelease": [ 62 | "8", 63 | "9", 64 | "10" 65 | ] 66 | }, 67 | { 68 | "operatingsystem": "OracleLinux", 69 | "operatingsystemrelease": [ 70 | "8", 71 | "9", 72 | "10" 73 | ] 74 | } 75 | ], 76 | "requirements": [ 77 | { 78 | "name": "openvox", 79 | "version_requirement": ">= 8.19.0 < 9.0.0" 80 | } 81 | ] 82 | } 83 | -------------------------------------------------------------------------------- /lib/puppet/provider/grub_config/grub2.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # GRUB 2 support for kernel parameters, edits /etc/default/grub 4 | # 5 | # Copyright (c) 2016 Trevor Vaughan 6 | # Licensed under the Apache License, Version 2.0 7 | # Based on work by Dominic Cleal 8 | 9 | raise('Missing augeasproviders_core module dependency') if Puppet::Type.type(:augeasprovider).nil? 10 | 11 | Puppet::Type.type(:grub_config).provide(:grub2, parent: Puppet::Type.type(:augeasprovider).provider(:default)) do 12 | desc "Uses Augeas API to update kernel parameters in GRUB2's /etc/default/grub" 13 | 14 | default_file { '/etc/default/grub' } 15 | 16 | lens { 'Shellvars.lns' } 17 | 18 | def self.mkconfig_path 19 | which('grub2-mkconfig') or which('grub-mkconfig') or '/usr/sbin/grub-mkconfig' 20 | end 21 | 22 | confine feature: :augeas 23 | 24 | confine exists: mkconfig_path, for_binary: true 25 | 26 | def mkconfig 27 | execute(self.class.mkconfig_path, { failonfail: true, combine: false }) 28 | end 29 | 30 | defaultfor osfamily: :RedHat 31 | 32 | def self.instances 33 | augopen do |aug| 34 | resources = [] 35 | 36 | aug.match('$target/*').each do |key| 37 | param = key.split('/').last.strip 38 | val = aug.get(key) 39 | 40 | resource = { ensure: :present, name: param } 41 | 42 | if val 43 | val.strip! 44 | resource[:value] = val 45 | end 46 | 47 | resources << new(resource) 48 | end 49 | 50 | resources 51 | end 52 | end 53 | 54 | def exists? 55 | augopen do |aug| 56 | !aug.match("$target/#{resource[:name]}").empty? 57 | end 58 | end 59 | 60 | def create 61 | self.value = (resource[:value]) 62 | end 63 | 64 | def destroy 65 | augopen! do |aug| 66 | aug.rm("$target/#{resource[:name]}") 67 | end 68 | end 69 | 70 | def value 71 | augopen do |aug| 72 | aug.get("$target/#{resource[:name]}") 73 | end 74 | end 75 | 76 | def value=(newval) 77 | newval = %("#{newval}") if newval.is_a?(String) && !%w[' "].include?(newval[0].chr) 78 | 79 | augopen! do |aug| 80 | aug.set("$target/#{resource[:name]}", newval) 81 | end 82 | end 83 | 84 | def flush 85 | super 86 | 87 | require 'puppetx/augeasproviders_grub/util' 88 | PuppetX::AugeasprovidersGrub::Util.grub2_mkconfig(mkconfig) 89 | end 90 | end 91 | -------------------------------------------------------------------------------- /lib/puppet/type/grub_config.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Manages GRUB global parameters (non-boot entry) 4 | # 5 | # Author Trevor Vaughan 6 | # Copyright (c) 2015 Onyx Point, Inc. 7 | # Licensed under the Apache License, Version 2.0 8 | # Based on work by Dominic Cleal 9 | 10 | Puppet::Type.newtype(:grub_config) do 11 | @doc = 'Manages global GRUB configuration parameters' 12 | 13 | ensurable do 14 | desc 'Whether the GRUB configuration parameter should be present. Use absent to remove it from the target configuration file.' 15 | defaultvalues 16 | defaultto :present 17 | end 18 | 19 | newparam(:name) do 20 | desc <<-EOM 21 | The parameter that you wish to set. 22 | 23 | ## GRUB < 2 ## 24 | 25 | In the case of GRUB < 2, this will be something like 'default', 26 | 'timeout', etc... 27 | 28 | See `info grub` for additional information. 29 | 30 | ## GRUB >= 2 ## 31 | 32 | With GRUB >= 2, this will be 'GRUB_DEFAULT', 'GRUB_SAVEDEFAULT', etc.. 33 | 34 | See `info grub2` for additional information. 35 | EOM 36 | 37 | isnamevar 38 | end 39 | 40 | newparam(:target) do 41 | desc <<-EOM 42 | The bootloader configuration file, if in a non-default location for the 43 | provider. 44 | EOM 45 | end 46 | 47 | newproperty(:value) do 48 | desc <<-EOM 49 | Value of the GRUB parameter. 50 | EOM 51 | 52 | munge do |value| 53 | value.to_s unless [Hash, Array].include?(value.class) 54 | end 55 | 56 | def insync?(is) 57 | if is.is_a?(String) && should.is_a?(String) 58 | is.gsub(%r{\A("|')|("|')\Z}, '') == should.gsub(%r{\A("|')|("|')\Z}, '') 59 | else 60 | is == should 61 | end 62 | end 63 | end 64 | 65 | autorequire(:file) do 66 | reqs = [] 67 | 68 | reqs << self[:target] if self[:target] 69 | 70 | reqs 71 | end 72 | 73 | autorequire(:kernel_parameter) do 74 | reqs = [] 75 | 76 | kernel_parameters = catalog.resources.select do |r| 77 | r.is_a?(Puppet::Type.type(:kernel_parameter)) && (r[:target] == self[:target]) 78 | end 79 | 80 | # Handles conflicts with Grub >= 2 since this and kernel_parameter would edit 81 | # the same file. 82 | # 83 | # Ignored for Grub < 2 84 | kernel_parameters.each do |kparam| 85 | if kparam[:bootmode].to_s == 'all' 86 | raise Puppet::ParseError, "Conflicting resource #{kparam} defined" if self[:name].to_s == 'GRUB_CMDLINE_LINUX' 87 | elsif %w[default normal].include?(kparam[:bootmode].to_s) 88 | raise Puppet::ParseError, "Conflicting resource #{kparam} defined" if self[:name].to_s == 'GRUB_CMDLINE_LINUX_DEFAULT' 89 | end 90 | 91 | reqs << kparam 92 | end 93 | 94 | reqs 95 | end 96 | end 97 | -------------------------------------------------------------------------------- /lib/puppet/type/grub_user.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Manages GRUB2 Users - Does not apply to GRUB Legacy 4 | # 5 | # Author Trevor Vaughan 6 | # Copyright (c) 2015 Onyx Point, Inc. 7 | 8 | Puppet::Type.newtype(:grub_user) do 9 | @doc = <<-EOM 10 | Manages GRUB2 Users - Does not apply to GRUB Legacy 11 | 12 | Note: This type compares against the *active* GRUB configuration. The 13 | contents of the management file will not be updated unless the active 14 | configuration is out of sync with the expected configuration. 15 | EOM 16 | 17 | feature :grub2, 'Management of GRUB2 resources' 18 | 19 | ensurable do 20 | desc 'Whether this GRUB2 user should be present. Set to absent to remove the user definition (and superuser flag if applicable).' 21 | defaultvalues 22 | defaultto :present 23 | end 24 | 25 | newparam(:name) do 26 | desc <<-EOM 27 | The username of the GRUB2 user to be managed. 28 | EOM 29 | 30 | isnamevar 31 | 32 | validate do |value| 33 | # These are items that can separate users in the superusers string and, 34 | # therefore, should not be valid as a username. 35 | raise(Puppet::ParseError, 'Usernames may not contain spaces, commas, semicolons, ampersands, or pipes') if value =~ %r{\s|,|;|&|\|} 36 | end 37 | end 38 | 39 | newparam(:superuser, boolean: true) do 40 | desc <<-EOM 41 | If set, add this user to the 'superusers' list, if no superusers are set, 42 | but grub_user resources have been declared, a compile error will be 43 | raised. 44 | EOM 45 | 46 | newvalues(:true, :false) 47 | defaultto(:false) 48 | end 49 | 50 | newparam(:target, parent: Puppet::Parameter::Path) do 51 | desc <<-EOM 52 | The file to which to write the user information. 53 | 54 | Must be an absolute path. 55 | EOM 56 | 57 | defaultto('/etc/grub.d/02_puppet_managed_users') 58 | end 59 | 60 | newparam(:report_unmanaged, boolean: true) do 61 | desc <<-EOM 62 | Report any unmanaged users as a warning during the Puppet run. 63 | EOM 64 | 65 | newvalues(:true, :false) 66 | defaultto(:false) 67 | end 68 | 69 | newparam(:rounds) do 70 | desc <<-EOM 71 | The rounds to use when hashing the password. 72 | EOM 73 | 74 | newvalues(%r{^\d+$}) 75 | defaultto(10_000) 76 | 77 | munge(&:to_i) 78 | end 79 | 80 | newproperty(:purge, boolean: true) do 81 | desc <<-EOM 82 | Purge all unmanaged users. 83 | 84 | This does not affect any users that are not defined by Puppet! There is 85 | no way to reliably eliminate the items from all other scripts without 86 | potentially severely damaging the GRUB2 build scripts. 87 | EOM 88 | 89 | newvalues(:true, :false) 90 | defaultto(:false) 91 | end 92 | 93 | newproperty(:password) do 94 | desc <<-EOM 95 | The user's password. If the password is not already in a GRUB2 compatible 96 | form, it will be automatically converted. 97 | EOM 98 | 99 | validate do |value| 100 | raise(Puppet::ParseError, 'Passwords must be Strings') unless value.is_a?(String) 101 | end 102 | 103 | def insync?(is) 104 | provider.password?(is, should) 105 | end 106 | end 107 | end 108 | -------------------------------------------------------------------------------- /spec/acceptance/10_grub_config_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper_acceptance' 4 | 5 | test_name 'Augeasproviders Grub' 6 | 7 | describe 'Global Config Tests' do 8 | hosts.each do |host| 9 | context "on #{host}" do 10 | context 'set timeout' do 11 | let(:manifest) do 12 | %( 13 | grub_config { 'GRUB_TIMEOUT': 14 | value => 1 15 | } 16 | ) 17 | end 18 | 19 | # Using puppet_apply as a helper 20 | it 'works with no errors' do 21 | apply_manifest_on(host, manifest, catch_failures: true) 22 | end 23 | 24 | it 'is idempotent' do 25 | apply_manifest_on(host, manifest, catch_changes: true) 26 | end 27 | 28 | it 'has a timeout of 1' do 29 | on(host, %(grep 'GRUB_TIMEOUT="\\?1"\\?' /etc/default/grub)) 30 | on(host, %(grep 'timeout=1' /boot/grub2/grub.cfg)) 31 | end 32 | end 33 | 34 | context 'set arbitrary value' do 35 | let(:manifest) do 36 | %( 37 | grub_config { 'GRUB_FOOBAR': 38 | value => 'BAZ' 39 | } 40 | ) 41 | end 42 | 43 | # Using puppet_apply as a helper 44 | it 'works with no errors' do 45 | apply_manifest_on(host, manifest, catch_failures: true) 46 | end 47 | 48 | it 'is idempotent' do 49 | apply_manifest_on(host, manifest, catch_changes: true) 50 | end 51 | 52 | it 'has a GRUB_FOOBAR of BAZ' do 53 | on(host, %(grep 'GRUB_FOOBAR="BAZ"' /etc/default/grub)) 54 | end 55 | end 56 | 57 | context 'remove value' do 58 | let(:manifest) do 59 | %( 60 | grub_config { 'GRUB_FOOBAR': 61 | ensure => 'absent' 62 | } 63 | ) 64 | end 65 | 66 | # Using puppet_apply as a helper 67 | it 'works with no errors' do 68 | apply_manifest_on(host, manifest, catch_failures: true) 69 | end 70 | 71 | it 'is idempotent' do 72 | apply_manifest_on(host, manifest, catch_changes: true) 73 | end 74 | 75 | it 'does not have a GRUB_FOOBAR' do 76 | on(host, %(grep "GRUB_FOOBAR" /etc/default/grub), acceptable_exit_codes: [1]) 77 | end 78 | end 79 | 80 | context 'set Boolean value' do 81 | let(:manifest) do 82 | %( 83 | grub_config { 'GRUB_BOOLEAN_TEST': 84 | value => true 85 | } 86 | ) 87 | end 88 | 89 | # Using puppet_apply as a helper 90 | it 'works with no errors' do 91 | apply_manifest_on(host, manifest, catch_failures: true) 92 | end 93 | 94 | it 'is idempotent' do 95 | apply_manifest_on(host, manifest, catch_changes: true) 96 | end 97 | 98 | it 'has a GRUB_BOOLEAN_TEST that is true' do 99 | on(host, %(grep 'GRUB_BOOLEAN_TEST="true"' /etc/default/grub)) 100 | end 101 | end 102 | 103 | context 'set a value with spaces' do 104 | let(:manifest) do 105 | %( 106 | grub_config { 'GRUB_SPACES_TEST': 107 | value => 'this thing -has --spaces' 108 | } 109 | ) 110 | end 111 | 112 | # Using puppet_apply as a helper 113 | it 'works with no errors' do 114 | apply_manifest_on(host, manifest, catch_failures: true) 115 | end 116 | 117 | it 'is idempotent' do 118 | apply_manifest_on(host, manifest, catch_changes: true) 119 | end 120 | 121 | it 'has a valid GRUB_SPACES_TEST entry' do 122 | on(host, %(grep 'GRUB_SPACES_TEST="this thing -has --spaces"' /etc/default/grub)) 123 | end 124 | end 125 | end 126 | end 127 | end 128 | -------------------------------------------------------------------------------- /lib/puppet/provider/kernel_parameter/grub2.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # GRUB 2 support for kernel parameters, edits /etc/default/grub 4 | # 5 | # Copyright (c) 2012 Dominic Cleal 6 | # Licensed under the Apache License, Version 2.0 7 | 8 | raise('Missing augeasproviders_core module dependency') if Puppet::Type.type(:augeasprovider).nil? 9 | 10 | Puppet::Type.type(:kernel_parameter).provide(:grub2, parent: Puppet::Type.type(:augeasprovider).provider(:default)) do 11 | desc "Uses Augeas API to update kernel parameters in GRUB2's /etc/default/grub" 12 | 13 | default_file { '/etc/default/grub' } 14 | 15 | lens { 'Shellvars_list.lns' } 16 | 17 | resource_path do |resource| 18 | "$target/#{section(resource)}/value[.=~regexp('^#{resource[:name]}(=.*)?$')]" 19 | end 20 | 21 | def self.mkconfig_path 22 | which('grub2-mkconfig') or which('grub-mkconfig') or '/usr/sbin/grub-mkconfig' 23 | end 24 | 25 | confine feature: :augeas 26 | confine exists: mkconfig_path, for_binary: true 27 | 28 | # Add BLS specific option to mkconfig command if needed 29 | # 30 | # @return (String) The commandline 31 | def self.mkconfig_cmdline 32 | os = Facter.value(:os) 33 | # BLS cmdline option is only needed on RHEL 9.3+ 34 | # Fedora and Amazon Linux lack support and are excluded 35 | # since they don't have a release with major version 9 36 | needs_bls_cmdline = os.is_a?(Hash) && os['family'] == 'RedHat' && 37 | ((os['release']['major'].to_i == 9 && os['release']['minor'].to_i >= 3) || (os['release']['major'].to_i >= 10)) 38 | 39 | cmdline = [mkconfig_path] 40 | cmdline << '--update-bls-cmdline' if needs_bls_cmdline 41 | cmdline 42 | end 43 | 44 | def mkconfig 45 | execute(self.class.mkconfig_cmdline, { failonfail: true, combine: false }) 46 | end 47 | 48 | # when both grub* providers match, prefer GRUB 2 49 | def self.specificity 50 | super + 1 51 | end 52 | 53 | def self.instances 54 | augopen do |aug| 55 | resources = [] 56 | 57 | # Params are nicely separated, but no recovery-only setting (hard-coded) 58 | sections = { 'all' => 'GRUB_CMDLINE_LINUX', 59 | 'normal' => 'GRUB_CMDLINE_LINUX_DEFAULT', 60 | 'default' => 'GRUB_CMDLINE_LINUX_DEFAULT' } 61 | sections.keys.sort.each do |bootmode| 62 | key = sections[bootmode] 63 | # Get all unique param names 64 | params = aug.match("$target/#{key}/value").map do |pp| 65 | aug.get(pp).split('=')[0] 66 | end.uniq 67 | 68 | # Find all values for each param name 69 | params.each do |param| 70 | vals = aug.match("$target/#{key}/value[.=~regexp('^#{param}(=.*)?$')]").map do |vp| 71 | aug.get(vp).split('=', 2)[1] 72 | end 73 | vals = vals[0] if vals.size == 1 74 | 75 | param = { ensure: :present, name: param, value: vals, bootmode: bootmode } 76 | resources << new(param) 77 | end 78 | end 79 | resources 80 | end 81 | end 82 | 83 | def self.section(resource) 84 | case resource[:bootmode].to_s 85 | when 'default', 'normal' 86 | 'GRUB_CMDLINE_LINUX_DEFAULT' 87 | when 'all' 88 | 'GRUB_CMDLINE_LINUX' 89 | else 90 | raise("Unsupported bootmode for #{self.class} provider") 91 | end 92 | end 93 | 94 | def create 95 | self.value = (resource[:value]) 96 | end 97 | 98 | def value 99 | augopen do |aug| 100 | aug.match('$resource').map do |vp| 101 | aug.get(vp).split('=', 2)[1] 102 | end 103 | end 104 | end 105 | 106 | # If GRUB_CMDLINE_LINUX_DEFAULT does not exist, it should be set to the 107 | # present contents of GRUB_CMDLINE_LINUX. 108 | # If this is not done, you may end up with garbage on your next kernel 109 | # upgrade! 110 | def munge_grub_cmdline_linux_default(aug) 111 | src_path = '$target/GRUB_CMDLINE_LINUX/value' 112 | dest_path = '$target/GRUB_CMDLINE_LINUX_DEFAULT' 113 | 114 | return unless aug.match("#{dest_path}/value").empty? 115 | 116 | aug.match(src_path).each do |val| 117 | src_val = aug.get(val) 118 | 119 | # Need to let the rest of the code work on the actual value properly. 120 | next if src_val.split('=').first.strip == resource[:name] 121 | 122 | val_target = val.split('/').last 123 | 124 | aug.set("#{dest_path}/#{val_target}", src_val) 125 | end 126 | end 127 | 128 | def value=(newval) 129 | augopen! do |aug| 130 | # If we don't have the section at all, add it. Otherwise, any 131 | # manipulation will result in a parse error. 132 | current_section = self.class.section(resource) 133 | has_section = aug.match("$target/#{current_section}") 134 | 135 | aug.set("$target/#{current_section}/quote", '"') if !has_section || has_section.empty? 136 | 137 | munge_grub_cmdline_linux_default(aug) if current_section == 'GRUB_CMDLINE_LINUX_DEFAULT' 138 | 139 | if newval && !newval.empty? 140 | vals = newval.clone 141 | else 142 | # If no value (e.g. "quiet") then clear the value from the first and 143 | # delete the rest 144 | vals = nil 145 | aug.set("#{resource_path}[1]", resource[:name]) 146 | aug.rm("#{resource_path}[position() > 1]") 147 | end 148 | 149 | # Set any existing parameters with this name, remove excess ones 150 | if vals 151 | aug.match('$resource').each do |ppath| 152 | val = vals.shift 153 | if val.nil? 154 | aug.rm(ppath) 155 | else 156 | aug.set(ppath, "#{resource[:name]}=#{val}") 157 | end 158 | end 159 | end 160 | 161 | # Add new parameters where there are more values than existing params 162 | if vals && !vals.empty? 163 | vals.each do |val| 164 | aug.set("$target/#{current_section}/value[last()+1]", "#{resource[:name]}=#{val}") 165 | end 166 | end 167 | end 168 | end 169 | 170 | def flush 171 | super 172 | 173 | require 'puppetx/augeasproviders_grub/util' 174 | PuppetX::AugeasprovidersGrub::Util.grub2_mkconfig(mkconfig) 175 | end 176 | end 177 | -------------------------------------------------------------------------------- /spec/acceptance/05_grub2_users_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper_acceptance' 4 | 5 | test_name 'Augeasproviders Grub' 6 | 7 | describe 'GRUB2 User Tests' do 8 | let(:target_files) do 9 | [ 10 | '/etc/grub.d/02_puppet_managed_users', 11 | '/etc/grub2.cfg' 12 | ] 13 | end 14 | 15 | let(:password_info) do 16 | { 17 | plain_password: 'really bad password', 18 | hashed_password: 'grub.pbkdf2.sha512.10000.3ED7C861BA4107282E3A55FC80B549995D105324F2CB494BBF34DE86517DFCB8DCFCA3E0C3550C64F9A259B516BFDD928C0FAC4E66CFDA351A957D702EE32C3D.C589ED8757DB23957A5F946470A58CF216A7507634647E532BC68085AAA52622AB4E6E151CF60CD8409166F6581FC166CE4D4845D61353A4C439C2170CC25747', 19 | hashed_password_20k: 'grub.pbkdf2.sha512.20000.4CD886B13634E03CF533C3F4C27E59E8F67D9C62915C04E03B019651FFB1DE8BE9EBB09B0D5759CF94A502566D748C28E9AF2150E81BFF1202E66D3C417A28A1.62E1AE32B4746DCBF222EB22FA670D35E7FAAD438677D67A0A1275E79430CF4E0F31EBF2186E645E922109B973CFF9A71BD53DCA77D9E749BDEC302022FD00BE' 20 | } 21 | end 22 | 23 | hosts.each do |host| 24 | context "on #{host}" do 25 | context 'set a user on the system with a plain text password' do 26 | let(:manifest) do 27 | %( 28 | grub_user { 'test_user1': 29 | password => '#{password_info[:plain_password]}' 30 | } 31 | ) 32 | end 33 | 34 | let(:legacy_file) { '/etc/grub.d/01_puppet_managed_users' } 35 | 36 | # With a legacy file to be removed 37 | it 'has a legacy file' do 38 | create_remote_file(host, legacy_file, '# Legacy File') 39 | end 40 | 41 | # Using puppet_apply as a helper 42 | it 'works with no errors' do 43 | apply_manifest_on(host, manifest, catch_failures: true) 44 | end 45 | 46 | it 'has removed the legacy file' do 47 | expect(host.file_exist?(legacy_file)).to be false 48 | end 49 | 50 | it 'is idempotent' do 51 | apply_manifest_on(host, manifest, catch_changes: true) 52 | end 53 | 54 | it 'sets an encrypted password' do 55 | target_files.each do |target_file| 56 | result = on(host, %(grep 'password_pbkdf2 test_user1' #{target_file})).stdout 57 | 58 | _password_identifier, user, password_hash = result.split(%r{\s+}) 59 | expect(user).to eql('test_user1') 60 | expect(password_hash).to match(%r{grub\.pbkdf2\.sha512\.10000\..*}) 61 | end 62 | end 63 | end 64 | 65 | context 'set a user on the system with a hashed password' do 66 | let(:manifest) do 67 | %( 68 | grub_user { 'test_user1': 69 | password => '#{password_info[:hashed_password]}' 70 | } 71 | ) 72 | end 73 | 74 | # Using puppet_apply as a helper 75 | it 'works with no errors' do 76 | apply_manifest_on(host, manifest, catch_failures: true) 77 | end 78 | 79 | it 'is idempotent' do 80 | apply_manifest_on(host, manifest, catch_changes: true) 81 | end 82 | 83 | it 'sets an encrypted password' do 84 | target_files.each do |target_file| 85 | result = on(host, %(grep 'password_pbkdf2 test_user1' #{target_file})).stdout 86 | 87 | _password_identifier, user, password_hash = result.split(%r{\s+}) 88 | expect(user).to eql('test_user1') 89 | expect(password_hash).to eql(password_info[:hashed_password]) 90 | end 91 | end 92 | end 93 | 94 | context 'set a user on the system with a hashed password with 20000 rounds' do 95 | let(:manifest) do 96 | %( 97 | grub_user { 'test_user1': 98 | password => '#{password_info[:hashed_password_20k]}', 99 | rounds => '20000' 100 | } 101 | ) 102 | end 103 | 104 | # Using puppet_apply as a helper 105 | it 'works with no errors' do 106 | apply_manifest_on(host, manifest, catch_failures: true) 107 | end 108 | 109 | it 'is idempotent' do 110 | apply_manifest_on(host, manifest, catch_changes: true) 111 | end 112 | 113 | it 'sets an encrypted password' do 114 | target_files.each do |target_file| 115 | result = on(host, %(grep 'password_pbkdf2 test_user1' #{target_file})).stdout 116 | 117 | _password_identifier, user, password_hash = result.split(%r{\s+}) 118 | expect(user).to eql('test_user1') 119 | expect(password_hash).to eql(password_info[:hashed_password_20k]) 120 | end 121 | end 122 | end 123 | 124 | context 'should purge any users when purge is set' do 125 | let(:manifest) do 126 | %( 127 | grub_user { 'test_user1': 128 | password => '#{password_info[:hashed_password]}', 129 | purge => true 130 | } 131 | ) 132 | end 133 | 134 | # Using puppet_apply as a helper 135 | it 'works with no errors' do 136 | apply_manifest_on(host, manifest, catch_failures: true) 137 | end 138 | 139 | it 'is idempotent' do 140 | apply_manifest_on(host, manifest, catch_changes: true) 141 | end 142 | 143 | it 'purges unmanaged users' do 144 | on(host, %(puppet resource grub_user bad_user password='some password')) 145 | 146 | result = apply_manifest_on(host, manifest, catch_failures: true).stdout 147 | expect(result).to match(%r{Purged.*bad_user}) 148 | 149 | target_files.each do |target_file| 150 | result = on(host, %(grep 'password_pbkdf2 test_user1' #{target_file})).stdout 151 | 152 | _password_identifier, user, password_hash = result.split(%r{\s+}) 153 | expect(user).to eql('test_user1') 154 | expect(password_hash).to eql(password_info[:hashed_password]) 155 | 156 | result = on( 157 | host, 158 | %(grep 'password_pbkdf2 bad_user' #{target_file}), 159 | acceptable_exit_codes: [1] 160 | ).stdout 161 | expect(result).to be_empty 162 | end 163 | end 164 | end 165 | end 166 | end 167 | end 168 | -------------------------------------------------------------------------------- /lib/puppetx/augeasproviders_grub/util.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # @summary Top-level namespace for extensions to core Puppet code. 4 | # @api public 5 | module PuppetX 6 | # @summary Namespace for Augeasproviders GRUB specific helpers. 7 | # @api public 8 | module AugeasprovidersGrub 9 | # @summary Collection of pure-Ruby utility methods used by GRUB related 10 | # types and providers in this module. These helpers manipulate kernel 11 | # option arrays, locate and read GRUB2 configuration files, and safely 12 | # merge or normalize values coming from system state and desired 13 | # resources. 14 | # 15 | # None of these methods perform direct catalogue manipulation; they are 16 | # focused on idempotent transformation of data so that providers can make 17 | # consistent comparisons and updates. 18 | # @api public 19 | module Util 20 | # Return a merge of the system options and the new options. 21 | # 22 | # The following format is supported for the new options 23 | # ':defaults:' => Copy defaults from the default GRUB entry 24 | # ':preserve:' => Preserve all existing options (if present) 25 | # 26 | # ':defaults:' and ':preserve:' are mutually exclusive! 27 | # 28 | # All of the options below supersede any items affected by the above 29 | # 30 | # 'entry(=.*)?' => Ensure that `entry` exists *as entered*; replaces all 31 | # other options with the same name 32 | # '!entry(=.*)? => Add this option to the end of the arguments, 33 | # preserving any other options of the same name 34 | # '-:entry' => Ensure that all instances of `entry` do not exist 35 | # '-:entry=foo' => Ensure that only instances of `entry` with value `foo` do not exist 36 | # 37 | # @param system_opts [Array] the current system options for this entry 38 | # @param new_opts [Array] the new options for this entry 39 | # @param default_opts [Array] the default entry options (if applicable) 40 | # @return [Array] a merged/manipulated array of options 41 | def self.munge_options(system_opts, new_opts, default_opts = []) 42 | sys_opts = Array(system_opts).flatten.map(&:strip) 43 | opts = Array(new_opts).flatten.map(&:strip) 44 | def_opts = Array(default_opts).flatten.map(&:strip) 45 | 46 | result_opts = [] 47 | result_opts = def_opts.dup if opts.delete(':defaults:') 48 | 49 | if opts.delete(':preserve:') 50 | # Need to remove any result opts that are being preserved 51 | sys_opts_keys = sys_opts.map { |x| x.split('=').first.strip } 52 | 53 | result_opts.delete_if do |x| 54 | key = x.split('=').first.strip 55 | 56 | sys_opts_keys.include?(key) 57 | end 58 | 59 | result_opts += sys_opts 60 | end 61 | 62 | # Get rid of everything with a '-:' 63 | discards = Array(opts.grep(%r{^-:}).map { |x| x[2..-1] }) 64 | opts.delete_if { |x| x =~ %r{^-:} } 65 | 66 | result_opts.delete_if do |x| 67 | discards.index do |d| 68 | (d == x) || (d == x.split('=').first) 69 | end 70 | end 71 | 72 | # Prepare to append everything with a '!:' 73 | appends = Array(opts.grep(%r{^!:}).map { |x| x[2..-1] }) 74 | opts.delete_if { |x| x =~ %r{^!:} } 75 | 76 | # We only want to append items that aren't in the list already 77 | appends.delete_if { |x| result_opts.include?(x) } 78 | 79 | # Replace these items in place if possible 80 | tmp_results = [] 81 | new_results = [] 82 | 83 | opts.each do |opt| 84 | key = opt.split('=').first 85 | 86 | old_entry = result_opts.index { |x| x.split('=').first == key } 87 | 88 | if old_entry 89 | # Replace the first instance of a given item with the matching option. 90 | tmp_results[old_entry] = opt 91 | result_opts.map! { |x| nil if x.split('=').first == key } 92 | else 93 | # Keep track of everything that we aren't replacing 94 | new_results << opt 95 | end 96 | end 97 | 98 | # Copy in all of the remaining options in place 99 | result_opts.each_index do |i| 100 | tmp_results[i] = result_opts[i] if result_opts[i] 101 | end 102 | 103 | # Ensure that we're not duplicating arguments 104 | (tmp_results.compact + new_results + appends). 105 | flatten. 106 | join(' '). 107 | scan(%r{\S+=(?:(?:".+?")|(?:\S+))|\S+}). 108 | uniq 109 | end 110 | 111 | # Take care of copying ':default:' values and ensure that the leading 112 | # 'boot' entries are stripped off of passed entries. 113 | # 114 | # @value (String) The value to munge 115 | # @flavor (String) The section in the 'grubby' output to use (kernel, initrd, etc...) 116 | # @grubby_info (Hash) The broken down output of 'grubby' into a Hash 117 | # @returns (String) The cleaned value 118 | def self.munge_grubby_value(value, flavor, grubby_info) 119 | if value == ':default:' 120 | raise Puppet::Error, "No default GRUB information found for `#{flavor}`" if grubby_info.empty? || !grubby_info[flavor] 121 | 122 | value = grubby_info[flavor] 123 | 124 | # In some cases, /boot gets shoved on by GRUB and we can't compare againt 125 | # that. 126 | value = value.split('/') 127 | value.delete_at(1) if value[1] == 'boot' 128 | 129 | value = value.join('/') 130 | end 131 | 132 | value 133 | end 134 | 135 | # Return the name of the current operating system or an empty string if 136 | # not found. 137 | # 138 | # @return (String) The current operating system name 139 | def self.os_name 140 | # The usual fact 141 | (Facter.value(:os) && Facter.value(:os)['name']) || 142 | # Legacy support 143 | Facter.value(:operatingsystem) || 144 | # Fallback 145 | '' 146 | end 147 | 148 | # Return the location of all valid GRUB2 configurations on the system. 149 | # 150 | # @raise (Puppet::Error) if no path is found 151 | # 152 | # @return (Array[String]) Paths to all system GRUB2 configuration files. 153 | def self.grub2_cfg_paths 154 | paths = [ 155 | '/etc/grub2.cfg', 156 | '/etc/grub2-efi.cfg', 157 | "/boot/efi/EFI/#{os_name.downcase}/grub.cfg", 158 | '/boot/grub2/grub.cfg', 159 | '/boot/grub/grub.cfg' 160 | ] 161 | 162 | valid_paths = paths.map do |path| 163 | real_path = nil 164 | 165 | if File.readable?(path) && !File.directory?(path) 166 | real_path = File.realpath(path) 167 | 168 | # Exclude stub files which include main config (e.g. Debian OS family) 169 | File.foreach(real_path) do |line| 170 | if line.match(%r{^configfile}s) 171 | real_path = nil 172 | break 173 | end 174 | end 175 | end 176 | 177 | real_path 178 | end.compact.uniq 179 | 180 | raise(%(No grub configuration found at '#{paths.join("', '")}')) if valid_paths.empty? 181 | 182 | valid_paths 183 | end 184 | 185 | # Return the location of the first discovered GRUB2 configuration. 186 | # 187 | # @raise (Puppet::Error) if no path is found 188 | # 189 | # @return (String) The full path to the GRUB2 configuration file. 190 | def self.grub2_cfg_path 191 | paths = grub2_cfg_paths 192 | 193 | raise Puppet::Error, 'Could not find a GRUB2 configuration on the system' if paths.empty? 194 | 195 | paths.first 196 | end 197 | 198 | # Return the contents of the GRUB2 configuration on the system. 199 | # Raise an error if not found. 200 | # 201 | # @return (String) The contents of the GRUB2 configuration on the system. 202 | def self.grub2_cfg 203 | File.read(grub2_cfg_path) 204 | end 205 | 206 | # Run grub2-mkconfig on all passed configurations 207 | # 208 | # @param mkconfig_output (String) 209 | # The output of grub2-mkconfig 210 | # 211 | # @param configs (Array[String]) 212 | # The output target paths 213 | # 214 | # @raise (Puppet::Error) if an empty string is passed as the file content 215 | # 216 | # @return (Array[String]) Updated paths 217 | def self.grub2_mkconfig(mkconfig_output, configs = grub2_cfg_paths) 218 | raise('No output from grub2-mkconfig') if mkconfig_output.strip.empty? 219 | 220 | configs.each do |config_path| 221 | File.open(config_path, 'w') do |fh| 222 | fh.puts(mkconfig_output) 223 | fh.flush 224 | end 225 | end 226 | 227 | configs 228 | end 229 | 230 | # Return a list of options that have the kernel path prepended and are 231 | # formatted with all processing arguments handled. 232 | # 233 | # @param old_opts (Array[String]) An array of all old options 234 | # @param new_opts (Array[String]) An array of all new options 235 | # @param default_kernel (String) The default kernel 236 | # @param default_kernel_opts (Array[String]) The default kernel options 237 | # @param prepend_kernel_path (Boolean) If true, add the kernel itself to the default kernel options 238 | # @return (Array[String]) An Array of processed options 239 | def self.munged_options(old_opts, new_opts, default_kernel, default_kernel_opts, prepend_kernel_path = false) 240 | # You need to prepend the kernel path for the defaults if you're trying to 241 | # format for `module` lines. 242 | 243 | default_kernel_opts = Array(default_kernel_opts) 244 | 245 | default_kernel_opts = Array(default_kernel) + default_kernel_opts if default_kernel && prepend_kernel_path 246 | 247 | munge_options(old_opts, new_opts, default_kernel_opts) 248 | end 249 | end 250 | end 251 | end 252 | -------------------------------------------------------------------------------- /lib/puppet/provider/grub_user/grub2.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # GRUB2 support for User Entries 4 | # 5 | # Copyright (c) 2016 Trevor Vaughan 6 | # Licensed under the Apache License, Version 2.0 7 | 8 | Puppet::Type.type(:grub_user).provide(:grub2, parent: Puppet::Type.type(:augeasprovider).provider(:default)) do 9 | desc 'Provides for the manipulation of GRUB2 User Entries' 10 | 11 | has_feature :grub2 12 | 13 | def self.mkconfig_path 14 | which('grub2-mkconfig') or which('grub-mkconfig') or '/usr/sbin/grub-mkconfig' 15 | end 16 | 17 | confine exists: mkconfig_path, for_binary: true 18 | 19 | def mkconfig 20 | execute(self.class.mkconfig_path, { failonfail: true, combine: false }) 21 | end 22 | 23 | confine exists: '/etc/grub.d' 24 | 25 | defaultfor osfamily: :RedHat 26 | 27 | mk_resource_methods 28 | 29 | def self.grub2_cfg 30 | require 'puppetx/augeasproviders_grub/util' 31 | 32 | PuppetX::AugeasprovidersGrub::Util.grub2_cfg 33 | end 34 | 35 | def grub2_cfg 36 | self.class.grub2_cfg 37 | end 38 | 39 | def self.grub2_cfg_path 40 | require 'puppetx/augeasproviders_grub/util' 41 | 42 | PuppetX::AugeasprovidersGrub::Util.grub2_cfg_path 43 | end 44 | 45 | def grub2_cfg_path 46 | self.class.grub2_cfg_path 47 | end 48 | 49 | def self.extract_users(content) 50 | superusers = nil 51 | users = {} 52 | 53 | content.each_line do |line| 54 | case line 55 | when %r{set\s+superusers=(?:'|")(.+?)(:?'|")} 56 | superusers = Regexp.last_match(1).strip.split(%r{\s|,|;|\||&}) 57 | when %r{password(?:_pbkdf2)?\s+(.*)} 58 | user, password = Regexp.last_match(1).split(%r{\s+}) 59 | users[user] = password 60 | end 61 | end 62 | 63 | resources = [] 64 | users.each_key do |user| 65 | new_resource = {} 66 | new_resource[:name] = user 67 | new_resource[:ensure] = :present 68 | new_resource[:password] = users[user] 69 | 70 | new_resource[:superuser] = if superusers&.include?(user) 71 | :true 72 | else 73 | :false 74 | end 75 | 76 | resources << new_resource 77 | end 78 | 79 | resources 80 | end 81 | 82 | def self.instances 83 | # Short circuit if we've already gathered this information 84 | return @instance_array if @instance_array 85 | 86 | all_users = extract_users(grub2_cfg) 87 | 88 | @instance_array = all_users.map { |x| new(x) } 89 | 90 | @instance_array 91 | end 92 | 93 | def self.prefetch(resources) 94 | instances.each do |prov| 95 | if (resource = resources[prov.name]) 96 | resource.provider = prov 97 | end 98 | end 99 | end 100 | 101 | def self.report_unmanaged_users(property_hash) 102 | return if @already_reported 103 | 104 | # Report on users that aren't being managed by Puppet 105 | unmanaged_users = instances.reject do |x| 106 | property_hash[:_all_grub_resource_users].include?(x.name) 107 | end 108 | 109 | unmanaged_users.map!(&:name) 110 | 111 | warn(%(The following GRUB2 users are present but not managed by Puppet: "#{unmanaged_users.join('", "')}")) unless (property_hash[:ignore_unmanaged_users] == :true) && unmanaged_users.empty? 112 | 113 | @already_reported = true 114 | end 115 | 116 | def self.post_resource_eval 117 | # Clean up our class instance variables in case we're running in daemon mode. 118 | @instance_array = nil 119 | @already_reported = nil 120 | end 121 | 122 | def exists? 123 | # Make sure that we don't have any issues with the file. 124 | if File.exist?(resource[:target]) 125 | raise(Puppet::Error, "'#{resource[:target]}' exists but is not a readable file") unless File.file?(resource[:target]) && File.readable?(resource[:target]) 126 | 127 | # Save this for later so that we don't write the file if it doesn't need it. 128 | @property_hash[:_target_file_content] = File.read(resource[:target]).strip 129 | @property_hash[:_existing_users] = self.class.extract_users(@property_hash[:_target_file_content]).map do |x| 130 | x[:_puppet_managed] = true 131 | x 132 | end 133 | 134 | # We don't want to duplicate our current entry 135 | current_index = @property_hash[:_existing_users].index { |x| x[:name] == resource[:name] } 136 | if current_index 137 | @property_hash[:_puppet_managed] = true 138 | @property_hash[:_existing_users].delete_at(current_index) 139 | end 140 | else 141 | @property_hash[:_existing_users] = [] 142 | end 143 | 144 | # This is used later on to ensure that we retain all entries that are 145 | # actually in the catalog. 146 | # 147 | # It only applies if we actually are performing a catalog run. 148 | @property_hash[:_all_grub_resource_users] = [] 149 | @property_hash[:_all_grub_resource_users] = resource.catalog.resources.select { |x| x.type == :grub_user }.map { |x| x[:name] } if resource.catalog 150 | 151 | self.class.report_unmanaged_users(@property_hash) if resource[:report_unmanaged] == :true 152 | 153 | # Get the password into a sane format before proceeding 154 | 155 | @property_hash[:ensure] == :present 156 | end 157 | 158 | def create 159 | # Input Validation 160 | raise(Puppet::Error, '`password` is a required property') unless @property_hash[:password] || resource[:password] 161 | 162 | # End Validation 163 | 164 | @property_hash = resource.to_hash.merge(@property_hash) 165 | @property_hash[:_puppet_managed] = true 166 | end 167 | 168 | def destroy 169 | @property_hash[:_puppet_managed] = false 170 | end 171 | 172 | def password?(is, should) 173 | if is =~ %r{^grub\.pbkdf2\.\S+\.(\d+)} 174 | is_rounds = Regexp.last_match(1) 175 | 176 | if should =~ %r{^grub\.pbkdf2\.\S+\.(\d+)} 177 | should_rounds = Regexp.last_match(1) 178 | 179 | return (is == should) && (is_rounds == should_rounds) 180 | end 181 | 182 | return validate_pbkdf2(should, is) 183 | end 184 | 185 | if should =~ %r{^grub\.pbkdf2\.\S+\.(\d+)} 186 | should_rounds = Regexp.last_match(1) 187 | 188 | if is =~ %r{^grub\.pbkdf2\.\S+\.(\d+)} 189 | is_rounds = Regexp.last_match(1) 190 | 191 | return (is == should) && (is_rounds == should_rounds) 192 | end 193 | 194 | return validate_pbkdf2(is, should) 195 | else 196 | should = mkpasswd_pbkdf2(should, nil, resource[:rounds]) 197 | end 198 | 199 | is == should 200 | end 201 | 202 | def purge 203 | users_to_purge = [] 204 | if resource[:purge] == :true 205 | (@property_hash[:_existing_users] + [@property_hash]).each do |user| 206 | unless user[:_puppet_managed] && @property_hash[:_all_grub_resource_users].include?(user[:name]) 207 | # Found something to purge! 208 | users_to_purge << user[:name] 209 | end 210 | end 211 | end 212 | 213 | if users_to_purge.empty? 214 | resource[:purge] 215 | else 216 | %(Purged GRUB2 users: "#{users_to_purge.join('", "')}") 217 | end 218 | end 219 | 220 | def flush 221 | super 222 | 223 | # This is to clean up the legacy file that was put in place incorrectly 224 | # prior to the standard 01_users configuration file 225 | legacy_file = '/etc/grub.d/01_puppet_managed_users' 226 | File.unlink(legacy_file) if resource[:target] != legacy_file && File.exist?(legacy_file) 227 | 228 | output = [] 229 | 230 | output << <<~EOM 231 | #!/bin/sh 232 | ######## 233 | # This file managed by Puppet 234 | # Manual changes will be erased! 235 | ######## 236 | cat << USER_LIST 237 | EOM 238 | 239 | # Build the password file 240 | superusers = @property_hash[:_existing_users].select { |x| x[:superuser] == :true }.map { |x| x[:name] } 241 | superusers << @property_hash[:name] if @property_hash[:_puppet_managed] && (@property_hash[:superuser] == :true) 242 | 243 | # First, prepare the superusers line. 244 | output << %(set superusers="#{superusers.uniq.sort.join(',')}") unless superusers.empty? 245 | 246 | # Now, prepare our user list 247 | users = [] 248 | # Need to keep our output order consistent! 249 | (@property_hash[:_existing_users] + [@property_hash]).sort_by { |x| x[:name] }.each do |user| 250 | if resource[:purge] == :true 251 | if user[:_puppet_managed] && @property_hash[:_all_grub_resource_users].include?(user[:name]) 252 | users << format_user_entry(user) 253 | else 254 | debug("Purging GRUB2 User #{user[:name]}") 255 | end 256 | else 257 | users << format_user_entry(user) 258 | end 259 | end 260 | 261 | output += users 262 | output << 'USER_LIST' 263 | 264 | output = output.join("\n") 265 | 266 | # This really shouldn't happen but could if people start adding users in other files. 267 | if output == @property_hash[:_target_file_content] 268 | err("Please ensure that your *active* GRUB2 configuration is correct. #{self.class} thinks that you need an update, but your file content did not change") 269 | else 270 | fh = File.open(resource[:target], 'w') 271 | fh.puts(output) 272 | fh.flush 273 | fh.close 274 | 275 | FileUtils.chmod(0o755, resource[:target]) 276 | end 277 | 278 | require 'puppetx/augeasproviders_grub/util' 279 | PuppetX::AugeasprovidersGrub::Util.grub2_mkconfig(mkconfig) 280 | end 281 | 282 | private 283 | 284 | def format_user_entry(user_hash) 285 | password = user_hash[:password] 286 | 287 | password = mkpasswd_pbkdf2(password, nil, resource[:rounds]) unless password =~ %r{^grub\.pbkdf2} 288 | 289 | %(password_pbkdf2 #{user_hash[:name]} #{password}) 290 | end 291 | 292 | def pack_salt(salt) 293 | salt.scan(%r{..}).map(&:hex).pack('c*') 294 | end 295 | 296 | def unpack_salt(salt) 297 | salt.unpack1('H*').upcase 298 | end 299 | 300 | def mkpasswd_pbkdf2(password, salt, rounds = 10_000) 301 | salt ||= (0...63).map { |_x| rand(65..90).chr }.join 302 | 303 | require 'openssl' 304 | 305 | digest = OpenSSL::Digest.new('SHA512') 306 | 307 | hashed_password = OpenSSL::PKCS5.pbkdf2_hmac(password, salt, rounds, digest.digest_length, digest).unpack1('H*').upcase 308 | 309 | "grub.pbkdf2.sha512.#{rounds}.#{unpack_salt(salt)}.#{hashed_password}" 310 | end 311 | 312 | def validate_pbkdf2(password, pbkdf2_hash) 313 | if pbkdf2_hash =~ %r{(grub\.pbkdf2.*)} 314 | pbkdf2_hash = Regexp.last_match(1) 315 | raise 'Error: No valid GRUB2 PBKDF2 password hash found' unless pbkdf2_hash 316 | end 317 | 318 | _id, _type, _algorithm, rounds, hashed_salt, hashed_password = pbkdf2_hash.split('.') 319 | rounds = rounds.to_i 320 | 321 | salt = pack_salt(hashed_salt) 322 | 323 | "grub.pbkdf2.sha512.#{rounds}.#{hashed_salt}.#{hashed_password}" == mkpasswd_pbkdf2(password, salt, rounds) 324 | end 325 | end 326 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # grub: type/provider for grub files for Puppet 2 | 3 | [![Build Status](https://github.com/voxpupuli/puppet-augeasproviders_grub/workflows/CI/badge.svg)](https://github.com/voxpupuli/puppet-augeasproviders_grub/actions?query=workflow%3ACI) 4 | [![Release](https://github.com/voxpupuli/puppet-augeasproviders_grub/actions/workflows/release.yml/badge.svg)](https://github.com/voxpupuli/puppet-augeasproviders_grub/actions/workflows/release.yml) 5 | [![Puppet Forge](https://img.shields.io/puppetforge/v/puppet/augeasproviders_grub.svg)](https://forge.puppetlabs.com/puppet/augeasproviders_grub) 6 | [![Puppet Forge - downloads](https://img.shields.io/puppetforge/dt/puppet/augeasproviders_grub.svg)](https://forge.puppetlabs.com/puppet/augeasproviders_grub) 7 | [![Puppet Forge - endorsement](https://img.shields.io/puppetforge/e/puppet/augeasproviders_grub.svg)](https://forge.puppetlabs.com/puppet/augeasproviders_grub) 8 | [![Puppet Forge - scores](https://img.shields.io/puppetforge/f/puppet/augeasproviders_grub.svg)](https://forge.puppetlabs.com/puppet/augeasproviders_grub) 9 | [![puppetmodule.info docs](https://www.puppetmodule.info/images/badge.svg)](https://www.puppetmodule.info/m/puppet-augeasproviders_grub) 10 | [![Apache-2 License](https://img.shields.io/github/license/voxpupuli/puppet-augeasproviders_grub.svg)](LICENSE) 11 | [![Donated by Herculesteam](https://img.shields.io/badge/donated%20by-herculesteam-fb7047.svg)](#transfer-notice) 12 | 13 | This module provides a new type/provider for Puppet to read and modify grub 14 | config files using the Augeas configuration library. 15 | 16 | The advantage of using Augeas over the default Puppet `parsedfile` 17 | implementations is that Augeas will go to great lengths to preserve file 18 | formatting and comments, while also failing safely when needed. 19 | 20 | This provider will hide *all* of the Augeas commands etc., you don't need to 21 | know anything about Augeas to make use of it. 22 | 23 | ## Requirements 24 | 25 | Ensure both Augeas and ruby-augeas 0.3.0+ bindings are installed and working as 26 | normal. 27 | 28 | See [Puppet/Augeas pre-requisites](http://docs.puppetlabs.com/guides/augeas.html#pre-requisites). 29 | 30 | **WARNING** Your system must be able to run the grub mkconfig scripts with BLS 31 | support if you are on a systen that uses BLS! 32 | 33 | ## Installing 34 | 35 | On Puppet 2.7.14+, the module can be installed easily ([documentation](http://docs.puppetlabs.com/puppet/latest/reference/modules_installing.html)): 36 | 37 | puppet module install puppet/augeasproviders_grub 38 | 39 | You may see an error similar to this on Puppet 2.x ([#13858](http://projects.puppetlabs.com/issues/13858)): 40 | 41 | Error 400 on SERVER: Puppet::Parser::AST::Resource failed with error ArgumentError: Invalid resource type `kernel_parameter` at ... 42 | 43 | Ensure the module is present in your puppetmaster's own environment (it doesn't 44 | have to use it) and that the master has pluginsync enabled. Run the agent on 45 | the puppetmaster to cause the custom types to be synced to its local libdir 46 | (`puppet master --configprint libdir`) and then restart the puppetmaster so it 47 | loads them. 48 | 49 | ## Compatibility 50 | 51 | ### Puppet versions 52 | 53 | Minimum of Puppet 2.7. 54 | 55 | ### Augeas versions 56 | 57 | Augeas Versions | 0.10.0 | 1.0.0 | 1.1.0 | 1.2.0 | 58 | :-------------------------|:-------:|:-------:|:-------:|:-------:| 59 | **PROVIDERS** | 60 | kernel\_parameter (grub) | **yes** | **yes** | **yes** | **yes** | 61 | kernel\_parameter (grub2) | **yes** | **yes** | **yes** | **yes** | 62 | grub\_config (grub) | **yes** | **yes** | **yes** | **yes** | 63 | grub\_config (grub2) | **yes** | **yes** | **yes** | **yes** | 64 | grub\_menuentry (grub) | **yes** | **yes** | **yes** | **yes** | 65 | grub\_menuentry (grub2) | N/A | N/A | N/A | N/A | 66 | grub\_user (grub2) | N/A | N/A | N/A | N/A | 67 | 68 | **Note**: grub\_menuentry and grub\_user for GRUB2 do not use Augeas at this 69 | time due to lack of available lenses. 70 | 71 | ## Documentation and examples 72 | 73 | Type documentation can be generated with `puppet doc -r type` or viewed on the 74 | [Puppet Forge page](http://forge.puppetlabs.com/puppet/augeasproviders_grub). 75 | 76 | 77 | ### kernel_parameter provider 78 | 79 | This is a custom type and provider supplied by `augeasproviders`. It supports 80 | both GRUB Legacy (0.9x) and GRUB 2 configurations. 81 | 82 | #### manage parameter without value 83 | 84 | kernel_parameter { "quiet": 85 | ensure => present, 86 | } 87 | 88 | #### manage parameter with value 89 | 90 | kernel_parameter { "elevator": 91 | ensure => present, 92 | value => "deadline", 93 | } 94 | 95 | #### manage parameter with multiple values 96 | 97 | kernel_parameter { "rd_LVM_LV": 98 | ensure => present, 99 | value => ["vg/lvroot", "vg/lvvar"], 100 | } 101 | 102 | #### manage parameter on certain boot types 103 | 104 | Bootmode defaults to "all", so settings are applied for all boot types usually. 105 | 106 | Apply only to the default boot: 107 | 108 | kernel_parameter { "quiet": 109 | ensure => present, 110 | bootmode => "default", 111 | } 112 | 113 | Apply only to normal boots. In GRUB legacy, normal boots consist of the default boot plus non-recovery ones. In GRUB2, normal bootmode is just an alias for default. 114 | 115 | kernel_parameter { "quiet": 116 | ensure => present, 117 | bootmode => "normal", 118 | } 119 | 120 | Only recovery mode boots (unsupported with GRUB 2): 121 | 122 | kernel_parameter { "quiet": 123 | ensure => present, 124 | bootmode => "recovery", 125 | } 126 | 127 | #### manage parameter on multiple boot types 128 | 129 | kernel_parameter { 'quiet:all': 130 | ensure => absent, 131 | } 132 | kernel_parameter { 'quiet:recovery': 133 | ensure => present, 134 | } 135 | 136 | #### delete entry 137 | 138 | kernel_parameter { "rhgb": 139 | ensure => absent, 140 | } 141 | 142 | #### manage parameter in another config location 143 | 144 | kernel_parameter { "elevator": 145 | ensure => present, 146 | value => "deadline", 147 | target => "/mnt/boot/grub/menu.lst", 148 | } 149 | 150 | ### grub_config provider 151 | 152 | This custom type manages GRUB Legacy and GRUB2 global configuration parameters. 153 | 154 | In GRUB Legacy, the global items at the top of the `grub.conf` file are managed. 155 | 156 | In GRUB2, the parameters in `/etc/defaults/grub` are managed. 157 | 158 | When using GRUB2, take care that you aren't conflicting with an option later 159 | specified by `grub_menuentry`. Also, be aware that, in GRUB2, any global items 160 | here will not be referenced unless you reference them by variable name per Bash 161 | semantics. 162 | 163 | #### change the default legacy GRUB timeout 164 | 165 | This will set the `timeout` global value in the Legacy GRUB configuration. 166 | 167 | grub_config { 'timeout': 168 | value => '1' 169 | } 170 | 171 | #### change the default GRUB2 timeout 172 | 173 | This will set the `GRUB_TIMEOUT` global value in the GRUB2 configuration. 174 | 175 | grub_config { 'GRUB_TIMEOUT': 176 | value => '1' 177 | } 178 | 179 | ### grub_menuentry provider 180 | 181 | This is a custom type to manage GRUB Legacy and GRUB2 menu entries. 182 | 183 | The GRUB Legacy provider utlizes Augeas under the hood but GRUB2 did not have 184 | an available Lens and was written in Ruby. 185 | 186 | This will **not** allow for modifying dynamically generated system entries. You 187 | will need to remove some of the native GRUB2 configuration scripts to be fully 188 | independent of the default system values. 189 | 190 | The GRUB2 output of this provider will be saved, by default, in 191 | `/etc/grub.d/05_puppet_managed_` where the `random_string` is a 192 | hash of the resource `name`. 193 | 194 | #### new entry preserving all existing values 195 | 196 | This will create a new menu entry and copy over any default values if present. 197 | If the entry currently exists, it will preserve all values and not overwrite 198 | them with the default system values. 199 | 200 | grub_menuentry { 'new_entry': 201 | root => '(hd0,0)', 202 | kernel => ':preserve:', 203 | initrd => ':preserve:', 204 | kernel_options => [':preserve:'] 205 | } 206 | 207 | #### kernel option lines 208 | 209 | There are many methods for identifying and manipulating kernel option lines and 210 | so a method was developed for handling the most common scenarios. You can, of 211 | course, simply denote every option, but this is cumbersome and prone to error 212 | over time. 213 | 214 | The following format is supported for the new options: 215 | 216 | ':defaults:' => Copy defaults from the default GRUB entry 217 | ':preserve:' => Preserve all existing options (if present) 218 | 219 | Note: ':defaults:' and ':preserve:' are mutually exclusive. 220 | 221 | All of the options below supersede any items affected by the above 222 | 223 | 'entry(=.*)?' => Ensure that `entry` exists *as entered*; replaces all 224 | other options with the same name 225 | '!:entry(=.*)?' => Add this option to the end of the arguments 226 | preserving any other options of the same name 227 | '-:entry' => Ensure that all instances of `entry` do not exist 228 | '-:entry=foo' => Ensure that only instances of `entry` with value `foo` do not exist 229 | 230 | Note: Option removals and additions have higher precedence than preservation 231 | 232 | ### grub_user provider 233 | 234 | This type manages GRUB2 users and superusers. 235 | 236 | The output of this provider is stored, by default, in `/etc/grub.d/01_puppet_managed_users`. 237 | 238 | Any plain text passwords are automatically converted into the appropriate GRUB 239 | PBKDF2 format. 240 | 241 | Note: If no users are defined as superusers, then GRUB2 will not enforce user 242 | restrictions on your entries. 243 | 244 | #### user with a plain text password 245 | 246 | grub_user { 'test_user': 247 | password => 'plain text password' 248 | } 249 | 250 | #### user with a pre-hashed password 251 | 252 | grub_user { 'test_user': 253 | password => 'grub.pbkdf2.sha512.10000.REALLY_LONG_STRING' 254 | } 255 | 256 | #### user that is a superuser with a plain text password and 20000 rounds 257 | 258 | grub_user { 'test_user': 259 | password => 'plain text password', 260 | superuser => true, 261 | rounds => '20000' 262 | } 263 | 264 | ## Issues 265 | 266 | Please file any issues or suggestions 267 | [on GitHub](https://github.com/voxpupuli/augeasproviders_grub/issues). 268 | 269 | ## Transfer Notice 270 | 271 | This module was originally authored by [hercules-team](http://augeasproviders.com). 272 | The maintainer preferred that Vox Pupuli take ownership of the module for future improvement and maintenance. 273 | Existing pull requests and issues were transferred over, please fork and continue to contribute here. 274 | 275 | Previously: https://github.com/hercules-team/augeasproviders_grub 276 | -------------------------------------------------------------------------------- /lib/puppet/type/grub_menuentry.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Manages GRUB kernel menu items 4 | # 5 | # Focuses on linux-compatible menu items. 6 | # 7 | # Author Trevor Vaughan 8 | # Copyright (c) 2015 Onyx Point, Inc. 9 | # Licensed under the Apache License, Version 2.0 10 | # Based on work by Dominic Cleal 11 | 12 | Puppet::Type.newtype(:grub_menuentry) do 13 | require 'puppet/parameter/boolean' 14 | require 'puppet/property/boolean' 15 | 16 | @doc = <<-EOM 17 | Manages menu entries in the GRUB and GRUB2 systems. 18 | 19 | NOTE: This may not cover all possible options and some options may apply to 20 | either GRUB or GRUB2! 21 | EOM 22 | 23 | feature :grub, 'Can handle Legacy GRUB settings' 24 | feature :grub2, 'Can handle GRUB2 settings' 25 | 26 | ensurable do 27 | desc 'Whether this GRUB/GRUB2 menu entry should exist. Set to absent to remove the menu entry from the configuration.' 28 | defaultvalues 29 | defaultto :present 30 | end 31 | 32 | newparam(:name) do 33 | desc <<-EOM 34 | The name of the menu entry. 35 | EOM 36 | 37 | isnamevar 38 | end 39 | 40 | newparam(:target, required_features: %w[grub]) do 41 | desc <<-EOM 42 | The bootloader configuration file, if in a non-default location for the 43 | provider. 44 | EOM 45 | end 46 | 47 | newparam(:add_defaults_on_creation, parent: Puppet::Parameter::Boolean) do 48 | desc <<-EOM 49 | If set, when using the ':preserve:' option in `kernel_options` or 50 | `modules` will add the system defaults if the entry is being first 51 | created. This is the same technique that grub2-mkconfig uses when 52 | procesing entries. 53 | EOM 54 | 55 | newvalues(:true, :false) 56 | 57 | defaultto :true 58 | end 59 | 60 | # Shared Properties 61 | newproperty(:root) do 62 | desc <<-EOM 63 | The filesystem root. 64 | EOM 65 | 66 | newvalues(%r{\(.*\)}) 67 | end 68 | 69 | newproperty(:default_entry) do 70 | desc <<-EOM 71 | If set, make this menu entry the default entry. 72 | 73 | If more than one of these is set to `true` across all :menuentry 74 | resources, this is an error. 75 | 76 | In GRUB2, there is no real guarantee that this will stick since entries 77 | further down the line may have custom scripts which alter the default. 78 | 79 | NOTE: You should not use this in conjunction with using the :grub_config 80 | type to set the system default. 81 | EOM 82 | newvalues(:true, :false) 83 | 84 | def should 85 | return nil unless defined?(@should) 86 | 87 | (@should - [true, :true]).empty? 88 | end 89 | 90 | def insync?(is) 91 | is == should 92 | end 93 | end 94 | 95 | newproperty(:kernel) do 96 | desc <<-EOM 97 | The path to the kernel that you wish to boot. 98 | 99 | Set this to ':default:' to copy the default kernel if one exists. 100 | 101 | Set this to ':preserve:' to preserve the current entry. If a current 102 | entry does not exist, the default will be copied. If there is no default, 103 | this is an error. 104 | EOM 105 | 106 | newvalues(%r{^(/.*|:(default|preserve):)}) 107 | 108 | def insync?(is) 109 | provider.kernel?(is, should) 110 | end 111 | end 112 | 113 | newproperty(:kernel_options, array_matching: :all) do 114 | desc <<-EOM 115 | An array of kernel options to apply to the :kernel property. 116 | 117 | The following format is supported for the new options: 118 | ':defaults:' => Copy defaults from the default GRUB entry 119 | ':preserve:' => Preserve all existing options (if present) 120 | 121 | Note: ':defaults:' and ':preserve:' are mutually exclusive. 122 | 123 | All of the options below supersede any items affected by the above 124 | 125 | 'entry(=.*)?' => Ensure that `entry` exists *as entered*; replaces all 126 | other options with the same name 127 | '!:entry(=.*)?' => Add this option to the end of the arguments 128 | preserving any other options of the same name 129 | '-:entry' => Ensure that all instances of `entry` do not exist 130 | '-:entry=foo' => Ensure that only instances of `entry` with value `foo` do not exist 131 | 132 | Note: Option removals and additions have higher precedence than preservation 133 | EOM 134 | 135 | defaultto(':preserve:') 136 | 137 | validate do |value| 138 | raise Puppet::ParseError, 'Only one of :defaults: or :preserve: may be specified' if value.include?(':defaults:') && value.include?(':preserve:') 139 | end 140 | 141 | def insync?(is) 142 | provider.kernel_options?(is, should) 143 | end 144 | end 145 | 146 | newproperty(:modules, array_matching: :all) do 147 | desc <<-EOM 148 | An Array of module entry Arrays that apply to the given entry. 149 | Since each Multiboot format boot image is unique, you must know what you 150 | wish to pass to the module lines. 151 | 152 | The one exception to this is that many of the linux multiboot settings 153 | require the kernel and initrd to be passed to them. If you set the 154 | ':defaults:' value anywhere in the options array, the default kernel 155 | options will be copied to that location in the output. 156 | 157 | The following format is supported for the new options: 158 | ':defaults:' => Copy default options from the default *kernel* GRUB entry 159 | ':preserve:' => Preserve all existing options (if present) 160 | 161 | Note: ':defaults:' and ':preserve:' are mutually exclusive. 162 | 163 | All of the options below supersede any items affected by the above 164 | 165 | 'entry(=.*)?' => Ensure that `entry` exists *as entered*; replaces all 166 | other options with the same name 167 | '!:entry(=.*)?' => Add this option to the end of the arguments 168 | preserving any other options of the same name 169 | '-:entry' => Ensure that all instances of `entry` do not exist 170 | '-:entry=foo' => Ensure that only instances of `entry` with value `foo` do not exist 171 | 172 | Note: Option removals and additions have higher precedence than preservation 173 | 174 | Example: 175 | modules => [ 176 | ['/vmlinuz.1.2.3.4','ro'], 177 | ['/initrd.1.2.3.4'] 178 | ] 179 | EOM 180 | 181 | validate do |value| 182 | raise Puppet::ParseError, ':modules requires an Array of Arrays' unless value.is_a?(Array) && (value.first.is_a?(Array) || value.first.is_a?(String)) 183 | 184 | value.each do |val_line| 185 | raise Puppet::ParseError, 'Only one of :defaults: or :preserve: may be specified' if val_line.include?(':defaults:') && val_line.include?(':preserve:') 186 | end 187 | end 188 | 189 | def insync?(is) 190 | provider.modules?(is, should) 191 | end 192 | 193 | def is_to_s(value) 194 | "\"#{Array(Array(value).map { |x| x.join(' ') }).join("\n")}\"" 195 | end 196 | 197 | def should_to_s(value) 198 | "\"#{Array(Array(value).map { |x| x.join(' ') }).join("\n")}\"" 199 | end 200 | end 201 | 202 | newproperty(:initrd) do 203 | desc <<-EOM 204 | The path to the initrd image. 205 | 206 | Set this to ':default:' to copy the default kernel initrd if one exists. 207 | 208 | Set this to ':preserve:' to preserve the current entry. If a current 209 | entry does not exist, the default will be copied. If there is no default, 210 | this is an error. 211 | EOM 212 | 213 | newvalues(%r{^(/.*|:(default|preserve):)}) 214 | 215 | def insync?(is) 216 | provider.initrd?(is, should) 217 | end 218 | end 219 | 220 | newproperty(:makeactive, required_features: %w[grub]) do 221 | desc <<-EOM 222 | In Legacy GRUB, having this set will add a 'makeactive' entry to the menuentry. 223 | EOM 224 | newvalues(:true, :false) 225 | 226 | defaultto :false 227 | 228 | def should 229 | return nil unless defined?(@should) 230 | 231 | (@should - [true, :true]).empty? 232 | end 233 | 234 | def insync?(is) 235 | is == should 236 | end 237 | end 238 | 239 | # GRUB2 only properties 240 | newproperty(:bls, required_features: %w[grub2]) do 241 | desc <<-EOM 242 | Explicitly enable, or disable, BLS support for this resource. 243 | 244 | Has on effect on systems that are not BLS enabled. 245 | EOM 246 | 247 | newvalues(:true, :false) 248 | 249 | def should 250 | return nil unless defined?(@should) 251 | 252 | (@should - [true, :true]).empty? 253 | end 254 | 255 | def insync?(is) 256 | is == should 257 | end 258 | end 259 | 260 | newproperty(:classes, array_matching: :all, required_features: %w[grub2]) do 261 | desc <<-EOM 262 | Add this Array of classes to the menuentry. 263 | EOM 264 | end 265 | 266 | newproperty(:users, array_matching: :all, required_features: %w[grub2]) do 267 | desc <<-EOM 268 | In GRUB2, having this set will add a requirement for the listed users to 269 | authenticate to the system in order to utilize the menu entry. 270 | EOM 271 | 272 | defaultto [:unrestricted] 273 | 274 | munge do |value| 275 | value.to_s.strip.split(%r{\s|,|;|\||&}) 276 | end 277 | 278 | def should 279 | values = super.flatten 280 | values.include?('unrestricted') ? [:unrestricted] : values 281 | end 282 | 283 | def insync?(is) 284 | is.sort == should.sort 285 | end 286 | end 287 | 288 | newproperty(:load_16bit, required_featurees: %w[grub2]) do 289 | desc <<-EOM 290 | If set, ensure that `linux16` and `initrd16` are used for the kernel entries. 291 | 292 | Will default to `true` unless the entry is a BLS entry. 293 | EOM 294 | 295 | newvalues(:true, :false) 296 | 297 | def should 298 | return nil unless defined?(@should) 299 | 300 | (@should - [true, :true]).empty? 301 | end 302 | 303 | def insync?(is) 304 | is == should 305 | end 306 | end 307 | 308 | newproperty(:load_video, required_features: %w[grub2]) do 309 | desc <<-EOM 310 | If true, add the `load_video` command to the menuentry. 311 | 312 | Will default to `true` unless the entry is a BLS entry. 313 | EOM 314 | newvalues(:true, :false) 315 | 316 | def should 317 | return nil unless defined?(@should) 318 | 319 | (@should - [true, :true]).empty? 320 | end 321 | 322 | def insync?(is) 323 | is == should 324 | end 325 | end 326 | 327 | newproperty(:plugins, array_matching: :all, required_features: %w[grub2]) do 328 | desc <<-EOM 329 | An Array of plugins that should be included in this menuentry. 330 | 331 | Will default to `['gzio','part_msdos','xfs','ext2']` unless the entry is a BLS entry. 332 | EOM 333 | end 334 | 335 | # Autorequires 336 | autorequire(:file) do 337 | reqs = [] 338 | 339 | reqs << self[:target] if self[:target] 340 | 341 | reqs 342 | end 343 | 344 | autorequire(:kernel_parameter) do 345 | kernel_parameters = catalog.resources.select do |r| 346 | r.is_a?(Puppet::Type.type(:kernel_parameter)) && (r[:target] == self[:target]) 347 | end 348 | 349 | kernel_parameters 350 | end 351 | end 352 | -------------------------------------------------------------------------------- /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 | ### Resource types 8 | 9 | * [`grub_config`](#grub_config): Manages global GRUB configuration parameters 10 | * [`grub_menuentry`](#grub_menuentry): Manages menu entries in the GRUB and GRUB2 systems. NOTE: This may not cover all possible options and some options may apply to either 11 | * [`grub_user`](#grub_user): Manages GRUB2 Users - Does not apply to GRUB Legacy Note: This type compares against the *active* GRUB configuration. The contents of the ma 12 | * [`kernel_parameter`](#kernel_parameter): Manages kernel parameters stored in bootloaders. 13 | 14 | ## Resource types 15 | 16 | ### `grub_config` 17 | 18 | Manages global GRUB configuration parameters 19 | 20 | #### Properties 21 | 22 | The following properties are available in the `grub_config` type. 23 | 24 | ##### `ensure` 25 | 26 | Valid values: `present`, `absent` 27 | 28 | Whether the GRUB configuration parameter should be present. Use absent to remove it from the target configuration file. 29 | 30 | Default value: `present` 31 | 32 | ##### `value` 33 | 34 | Value of the GRUB parameter. 35 | 36 | #### Parameters 37 | 38 | The following parameters are available in the `grub_config` type. 39 | 40 | * [`name`](#-grub_config--name) 41 | * [`provider`](#-grub_config--provider) 42 | * [`target`](#-grub_config--target) 43 | 44 | ##### `name` 45 | 46 | namevar 47 | 48 | The parameter that you wish to set. 49 | 50 | ## GRUB < 2 ## 51 | 52 | In the case of GRUB < 2, this will be something like 'default', 53 | 'timeout', etc... 54 | 55 | See `info grub` for additional information. 56 | 57 | ## GRUB >= 2 ## 58 | 59 | With GRUB >= 2, this will be 'GRUB_DEFAULT', 'GRUB_SAVEDEFAULT', etc.. 60 | 61 | See `info grub2` for additional information. 62 | 63 | ##### `provider` 64 | 65 | The specific backend to use for this `grub_config` resource. You will seldom need to specify this --- Puppet will 66 | usually discover the appropriate provider for your platform. 67 | 68 | ##### `target` 69 | 70 | The bootloader configuration file, if in a non-default location for the 71 | provider. 72 | 73 | ### `grub_menuentry` 74 | 75 | Manages menu entries in the GRUB and GRUB2 systems. 76 | 77 | NOTE: This may not cover all possible options and some options may apply to 78 | either GRUB or GRUB2! 79 | 80 | #### Properties 81 | 82 | The following properties are available in the `grub_menuentry` type. 83 | 84 | ##### `bls` 85 | 86 | Valid values: `true`, `false` 87 | 88 | Explicitly enable, or disable, BLS support for this resource. 89 | 90 | Has on effect on systems that are not BLS enabled. 91 | 92 | ##### `classes` 93 | 94 | Add this Array of classes to the menuentry. 95 | 96 | ##### `default_entry` 97 | 98 | Valid values: `true`, `false` 99 | 100 | If set, make this menu entry the default entry. 101 | 102 | If more than one of these is set to `true` across all :menuentry 103 | resources, this is an error. 104 | 105 | In GRUB2, there is no real guarantee that this will stick since entries 106 | further down the line may have custom scripts which alter the default. 107 | 108 | NOTE: You should not use this in conjunction with using the :grub_config 109 | type to set the system default. 110 | 111 | ##### `ensure` 112 | 113 | Valid values: `present`, `absent` 114 | 115 | Whether this GRUB/GRUB2 menu entry should exist. Set to absent to remove the menu entry from the configuration. 116 | 117 | Default value: `present` 118 | 119 | ##### `initrd` 120 | 121 | Valid values: `%r{^(/.*|:(default|preserve):)}` 122 | 123 | The path to the initrd image. 124 | 125 | Set this to ':default:' to copy the default kernel initrd if one exists. 126 | 127 | Set this to ':preserve:' to preserve the current entry. If a current 128 | entry does not exist, the default will be copied. If there is no default, 129 | this is an error. 130 | 131 | ##### `kernel` 132 | 133 | Valid values: `%r{^(/.*|:(default|preserve):)}` 134 | 135 | The path to the kernel that you wish to boot. 136 | 137 | Set this to ':default:' to copy the default kernel if one exists. 138 | 139 | Set this to ':preserve:' to preserve the current entry. If a current 140 | entry does not exist, the default will be copied. If there is no default, 141 | this is an error. 142 | 143 | ##### `kernel_options` 144 | 145 | An array of kernel options to apply to the :kernel property. 146 | 147 | The following format is supported for the new options: 148 | ':defaults:' => Copy defaults from the default GRUB entry 149 | ':preserve:' => Preserve all existing options (if present) 150 | 151 | Note: ':defaults:' and ':preserve:' are mutually exclusive. 152 | 153 | All of the options below supersede any items affected by the above 154 | 155 | 'entry(=.*)?' => Ensure that `entry` exists *as entered*; replaces all 156 | other options with the same name 157 | '!:entry(=.*)?' => Add this option to the end of the arguments 158 | preserving any other options of the same name 159 | '-:entry' => Ensure that all instances of `entry` do not exist 160 | '-:entry=foo' => Ensure that only instances of `entry` with value `foo` do not exist 161 | 162 | Note: Option removals and additions have higher precedence than preservation 163 | 164 | Default value: `:preserve:` 165 | 166 | ##### `load_16bit` 167 | 168 | Valid values: `true`, `false` 169 | 170 | If set, ensure that `linux16` and `initrd16` are used for the kernel entries. 171 | 172 | Will default to `true` unless the entry is a BLS entry. 173 | 174 | ##### `load_video` 175 | 176 | Valid values: `true`, `false` 177 | 178 | If true, add the `load_video` command to the menuentry. 179 | 180 | Will default to `true` unless the entry is a BLS entry. 181 | 182 | ##### `makeactive` 183 | 184 | Valid values: `true`, `false` 185 | 186 | In Legacy GRUB, having this set will add a 'makeactive' entry to the menuentry. 187 | 188 | Default value: `false` 189 | 190 | ##### `modules` 191 | 192 | An Array of module entry Arrays that apply to the given entry. 193 | Since each Multiboot format boot image is unique, you must know what you 194 | wish to pass to the module lines. 195 | 196 | The one exception to this is that many of the linux multiboot settings 197 | require the kernel and initrd to be passed to them. If you set the 198 | ':defaults:' value anywhere in the options array, the default kernel 199 | options will be copied to that location in the output. 200 | 201 | The following format is supported for the new options: 202 | ':defaults:' => Copy default options from the default *kernel* GRUB entry 203 | ':preserve:' => Preserve all existing options (if present) 204 | 205 | Note: ':defaults:' and ':preserve:' are mutually exclusive. 206 | 207 | All of the options below supersede any items affected by the above 208 | 209 | 'entry(=.*)?' => Ensure that `entry` exists *as entered*; replaces all 210 | other options with the same name 211 | '!:entry(=.*)?' => Add this option to the end of the arguments 212 | preserving any other options of the same name 213 | '-:entry' => Ensure that all instances of `entry` do not exist 214 | '-:entry=foo' => Ensure that only instances of `entry` with value `foo` do not exist 215 | 216 | Note: Option removals and additions have higher precedence than preservation 217 | 218 | Example: 219 | modules => [ 220 | ['/vmlinuz.1.2.3.4','ro'], 221 | ['/initrd.1.2.3.4'] 222 | ] 223 | 224 | ##### `plugins` 225 | 226 | An Array of plugins that should be included in this menuentry. 227 | 228 | Will default to `['gzio','part_msdos','xfs','ext2']` unless the entry is a BLS entry. 229 | 230 | ##### `root` 231 | 232 | Valid values: `%r{\(.*\)}` 233 | 234 | The filesystem root. 235 | 236 | ##### `users` 237 | 238 | In GRUB2, having this set will add a requirement for the listed users to 239 | authenticate to the system in order to utilize the menu entry. 240 | 241 | Default value: `[:unrestricted]` 242 | 243 | #### Parameters 244 | 245 | The following parameters are available in the `grub_menuentry` type. 246 | 247 | * [`add_defaults_on_creation`](#-grub_menuentry--add_defaults_on_creation) 248 | * [`name`](#-grub_menuentry--name) 249 | * [`provider`](#-grub_menuentry--provider) 250 | * [`target`](#-grub_menuentry--target) 251 | 252 | ##### `add_defaults_on_creation` 253 | 254 | Valid values: `true`, `false`, `yes`, `no` 255 | 256 | If set, when using the ':preserve:' option in `kernel_options` or 257 | `modules` will add the system defaults if the entry is being first 258 | created. This is the same technique that grub2-mkconfig uses when 259 | procesing entries. 260 | 261 | Default value: `true` 262 | 263 | ##### `name` 264 | 265 | namevar 266 | 267 | The name of the menu entry. 268 | 269 | ##### `provider` 270 | 271 | The specific backend to use for this `grub_menuentry` resource. You will seldom need to specify this --- Puppet will 272 | usually discover the appropriate provider for your platform. 273 | 274 | ##### `target` 275 | 276 | The bootloader configuration file, if in a non-default location for the 277 | provider. 278 | 279 | ### `grub_user` 280 | 281 | Manages GRUB2 Users - Does not apply to GRUB Legacy 282 | 283 | Note: This type compares against the *active* GRUB configuration. The 284 | contents of the management file will not be updated unless the active 285 | configuration is out of sync with the expected configuration. 286 | 287 | #### Properties 288 | 289 | The following properties are available in the `grub_user` type. 290 | 291 | ##### `ensure` 292 | 293 | Valid values: `present`, `absent` 294 | 295 | Whether this GRUB2 user should be present. Set to absent to remove the user definition (and superuser flag if applicable). 296 | 297 | Default value: `present` 298 | 299 | ##### `password` 300 | 301 | The user's password. If the password is not already in a GRUB2 compatible 302 | form, it will be automatically converted. 303 | 304 | ##### `purge` 305 | 306 | Valid values: `true`, `false` 307 | 308 | Purge all unmanaged users. 309 | 310 | This does not affect any users that are not defined by Puppet! There is 311 | no way to reliably eliminate the items from all other scripts without 312 | potentially severely damaging the GRUB2 build scripts. 313 | 314 | Default value: `false` 315 | 316 | #### Parameters 317 | 318 | The following parameters are available in the `grub_user` type. 319 | 320 | * [`name`](#-grub_user--name) 321 | * [`provider`](#-grub_user--provider) 322 | * [`report_unmanaged`](#-grub_user--report_unmanaged) 323 | * [`rounds`](#-grub_user--rounds) 324 | * [`superuser`](#-grub_user--superuser) 325 | * [`target`](#-grub_user--target) 326 | 327 | ##### `name` 328 | 329 | namevar 330 | 331 | The username of the GRUB2 user to be managed. 332 | 333 | ##### `provider` 334 | 335 | The specific backend to use for this `grub_user` resource. You will seldom need to specify this --- Puppet will usually 336 | discover the appropriate provider for your platform. 337 | 338 | ##### `report_unmanaged` 339 | 340 | Valid values: `true`, `false` 341 | 342 | Report any unmanaged users as a warning during the Puppet run. 343 | 344 | Default value: `false` 345 | 346 | ##### `rounds` 347 | 348 | Valid values: `%r{^\d+$}` 349 | 350 | The rounds to use when hashing the password. 351 | 352 | Default value: `10_000` 353 | 354 | ##### `superuser` 355 | 356 | Valid values: `true`, `false` 357 | 358 | If set, add this user to the 'superusers' list, if no superusers are set, 359 | but grub_user resources have been declared, a compile error will be 360 | raised. 361 | 362 | Default value: `false` 363 | 364 | ##### `target` 365 | 366 | The file to which to write the user information. 367 | 368 | Must be an absolute path. 369 | 370 | Default value: `/etc/grub.d/02_puppet_managed_users` 371 | 372 | ### `kernel_parameter` 373 | 374 | Manages kernel parameters stored in bootloaders. 375 | 376 | #### Properties 377 | 378 | The following properties are available in the `kernel_parameter` type. 379 | 380 | ##### `ensure` 381 | 382 | Valid values: `present`, `absent` 383 | 384 | Whether this kernel parameter should be present on the selected boot entries. 385 | 386 | Default value: `present` 387 | 388 | ##### `value` 389 | 390 | Value of the parameter if applicable. Many parameters are just keywords so this must be left blank, while others (e.g. 'vga') will take a value. 391 | 392 | #### Parameters 393 | 394 | The following parameters are available in the `kernel_parameter` type. 395 | 396 | * [`bootmode`](#-kernel_parameter--bootmode) 397 | * [`name`](#-kernel_parameter--name) 398 | * [`provider`](#-kernel_parameter--provider) 399 | * [`target`](#-kernel_parameter--target) 400 | 401 | ##### `bootmode` 402 | 403 | Valid values: `all`, `default`, `normal`, `recovery` 404 | 405 | namevar 406 | 407 | Boot mode(s) to apply the parameter to. Either 'all' (default) to use the parameter on all boots (normal and recovery 408 | mode), 'default' for just the default boot entry, 'normal' for just normal boots or 'recovery' for just recovery boots. 409 | 410 | Default value: `all` 411 | 412 | ##### `name` 413 | 414 | namevar 415 | 416 | The parameter name, e.g. 'quiet' or 'vga'. 417 | 418 | ##### `provider` 419 | 420 | The specific backend to use for this `kernel_parameter` resource. You will seldom need to specify this --- Puppet will 421 | usually discover the appropriate provider for your platform. 422 | 423 | ##### `target` 424 | 425 | The bootloader configuration file, if in a non-default location for the provider. 426 | 427 | -------------------------------------------------------------------------------- /spec/unit/puppet/provider/kernel_parameter/grub2_spec.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env rspec 2 | # frozen_string_literal: true 3 | 4 | require 'spec_helper' 5 | provider_class = Puppet::Type.type(:kernel_parameter).provider(:grub2) 6 | 7 | LENS = 'Shellvars_list.lns' 8 | FILTER = "*[label() =~ regexp('GRUB_CMDLINE_LINUX.*')]" 9 | 10 | describe provider_class do 11 | it 'finds grub2-mkconfig' do 12 | allow(FileTest).to receive(:file?).and_return(false) 13 | allow(FileTest).to receive(:executable?).and_return(false) 14 | allow(FileTest).to receive(:file?).with('/usr/sbin/grub2-mkconfig').and_return(true) 15 | allow(FileTest).to receive(:executable?).with('/usr/sbin/grub2-mkconfig').and_return(true) 16 | expect(provider_class.mkconfig_path).to eq '/usr/sbin/grub2-mkconfig' 17 | end 18 | 19 | it 'finds grub-mkconfig' do 20 | allow(FileTest).to receive(:file?).and_return(false) 21 | allow(FileTest).to receive(:executable?).and_return(false) 22 | allow(FileTest).to receive(:file?).with('/usr/sbin/grub-mkconfig').and_return(true) 23 | allow(FileTest).to receive(:executable?).with('/usr/sbin/grub-mkconfig').and_return(true) 24 | expect(provider_class.mkconfig_path).to eq '/usr/sbin/grub-mkconfig' 25 | end 26 | end 27 | 28 | describe provider_class do 29 | before do 30 | allow_any_instance_of(provider_class).to receive(:default?).and_return(true) 31 | allow(FileTest).to receive(:exist?).and_return(false) 32 | allow(FileTest).to receive(:file?).and_return(false) 33 | allow(FileTest).to receive(:executable?).and_return(false) 34 | ['/usr/sbin/grub2-mkconfig', '/usr/sbin/grub-mkconfig'].each do |path| 35 | allow(FileTest).to receive(:file?).with(path).and_return(true) 36 | allow(FileTest).to receive(:exist?).with(path).and_return(true) 37 | allow(FileTest).to receive(:executable?).with(path).and_return(true) 38 | end 39 | allow(FileTest).to receive(:file?).with('/etc/grub2-efi.cfg').and_return(true) 40 | allow(FileTest).to receive(:file?).with('/boot/grub2/grub.cfg').and_return(true) 41 | allow(FileTest).to receive(:exist?).with('/etc/default/grub').and_return(true) 42 | 43 | require 'puppetx/augeasproviders_grub/util' 44 | allow(PuppetX::AugeasprovidersGrub::Util).to receive(:grub2_cfg_paths).and_return(['/dev/null']) 45 | end 46 | 47 | context 'with full file' do 48 | let(:tmptarget) { aug_fixture('full') } 49 | let(:target) { tmptarget.path } 50 | 51 | it 'lists instances' do 52 | allow(provider_class).to receive(:target).and_return(target) 53 | inst = provider_class.instances.map do |p| 54 | { 55 | name: p.get(:name), 56 | ensure: p.get(:ensure), 57 | value: p.get(:value), 58 | bootmode: p.get(:bootmode), 59 | } 60 | end 61 | 62 | expect(inst.size).to eq 7 63 | expect(inst[0]).to include(name: 'quiet', ensure: :present, value: :absent, bootmode: 'all') 64 | expect(inst[1]).to include(name: 'elevator', ensure: :present, value: 'noop', bootmode: 'all') 65 | expect(inst[2]).to include(name: 'divider', ensure: :present, value: '10', bootmode: 'all') 66 | expect(inst[3]).to include(name: 'rhgb', ensure: :present, value: :absent, bootmode: 'default') 67 | expect(inst[4]).to include(name: 'nohz', ensure: :present, value: 'on', bootmode: 'default') 68 | expect(inst[5]).to include(name: 'rhgb', ensure: :present, value: :absent, bootmode: 'normal') 69 | expect(inst[6]).to include(name: 'nohz', ensure: :present, value: 'on', bootmode: 'normal') 70 | end 71 | 72 | describe 'when creating entries' do 73 | before do 74 | allow_any_instance_of(provider_class).to receive(:mkconfig).and_return('OK') 75 | end 76 | 77 | it 'creates no-value entries' do 78 | apply!(Puppet::Type.type(:kernel_parameter).new( 79 | name: 'foo', 80 | ensure: :present, 81 | target: target, 82 | provider: 'grub2' 83 | )) 84 | 85 | augparse_filter(target, LENS, FILTER, ' 86 | { "GRUB_CMDLINE_LINUX" 87 | { "quote" = "\"" } 88 | { "value" = "quiet" } 89 | { "value" = "elevator=noop" } 90 | { "value" = "divider=10" } 91 | { "value" = "foo" } 92 | } 93 | { "GRUB_CMDLINE_LINUX_DEFAULT" 94 | { "quote" = "\"" } 95 | { "value" = "rhgb" } 96 | { "value" = "nohz=on" } 97 | } 98 | ') 99 | end 100 | 101 | it 'creates entry with value' do 102 | apply!(Puppet::Type.type(:kernel_parameter).new( 103 | name: 'foo', 104 | ensure: :present, 105 | value: 'bar', 106 | target: target, 107 | provider: 'grub2' 108 | )) 109 | 110 | augparse_filter(target, LENS, FILTER, ' 111 | { "GRUB_CMDLINE_LINUX" 112 | { "quote" = "\"" } 113 | { "value" = "quiet" } 114 | { "value" = "elevator=noop" } 115 | { "value" = "divider=10" } 116 | { "value" = "foo=bar" } 117 | } 118 | { "GRUB_CMDLINE_LINUX_DEFAULT" 119 | { "quote" = "\"" } 120 | { "value" = "rhgb" } 121 | { "value" = "nohz=on" } 122 | } 123 | ') 124 | end 125 | 126 | it 'creates entries with multiple values' do 127 | apply!(Puppet::Type.type(:kernel_parameter).new( 128 | name: 'foo', 129 | ensure: :present, 130 | value: %w[bar baz], 131 | target: target, 132 | provider: 'grub2' 133 | )) 134 | 135 | augparse_filter(target, LENS, FILTER, ' 136 | { "GRUB_CMDLINE_LINUX" 137 | { "quote" = "\"" } 138 | { "value" = "quiet" } 139 | { "value" = "elevator=noop" } 140 | { "value" = "divider=10" } 141 | { "value" = "foo=bar" } 142 | { "value" = "foo=baz" } 143 | } 144 | { "GRUB_CMDLINE_LINUX_DEFAULT" 145 | { "quote" = "\"" } 146 | { "value" = "rhgb" } 147 | { "value" = "nohz=on" } 148 | } 149 | ') 150 | end 151 | 152 | it 'creates normal boot-only entries' do 153 | apply!(Puppet::Type.type(:kernel_parameter).new( 154 | name: 'foo', 155 | ensure: :present, 156 | bootmode: :normal, 157 | target: target, 158 | provider: 'grub2' 159 | )) 160 | 161 | augparse_filter(target, LENS, FILTER, ' 162 | { "GRUB_CMDLINE_LINUX" 163 | { "quote" = "\"" } 164 | { "value" = "quiet" } 165 | { "value" = "elevator=noop" } 166 | { "value" = "divider=10" } 167 | } 168 | { "GRUB_CMDLINE_LINUX_DEFAULT" 169 | { "quote" = "\"" } 170 | { "value" = "rhgb" } 171 | { "value" = "nohz=on" } 172 | { "value" = "foo" } 173 | } 174 | ') 175 | end 176 | 177 | it 'creates default boot-only entries' do 178 | apply!(Puppet::Type.type(:kernel_parameter).new( 179 | name: 'foo', 180 | ensure: :present, 181 | bootmode: :default, 182 | target: target, 183 | provider: 'grub2' 184 | )) 185 | 186 | augparse_filter(target, LENS, FILTER, ' 187 | { "GRUB_CMDLINE_LINUX" 188 | { "quote" = "\"" } 189 | { "value" = "quiet" } 190 | { "value" = "elevator=noop" } 191 | { "value" = "divider=10" } 192 | } 193 | { "GRUB_CMDLINE_LINUX_DEFAULT" 194 | { "quote" = "\"" } 195 | { "value" = "rhgb" } 196 | { "value" = "nohz=on" } 197 | { "value" = "foo" } 198 | } 199 | ') 200 | end 201 | end 202 | 203 | it 'errors on recovery-only entries' do 204 | txn = apply(Puppet::Type.type(:kernel_parameter).new( 205 | name: 'foo', 206 | ensure: :present, 207 | bootmode: :recovery, 208 | target: target, 209 | provider: 'grub2' 210 | )) 211 | 212 | expect(txn.any_failed?).not_to eq nil 213 | expect(@logs.first.level).to eq :err 214 | expect(@logs.first.message.include?('Unsupported bootmode')).to eq true 215 | end 216 | 217 | it 'deletes entries' do 218 | allow_any_instance_of(provider_class).to receive(:mkconfig).and_return('OK') 219 | 220 | apply!(Puppet::Type.type(:kernel_parameter).new( 221 | name: 'divider', 222 | ensure: 'absent', 223 | target: target, 224 | provider: 'grub2' 225 | )) 226 | 227 | augparse_filter(target, LENS, FILTER, ' 228 | { "GRUB_CMDLINE_LINUX" 229 | { "quote" = "\"" } 230 | { "value" = "quiet" } 231 | { "value" = "elevator=noop" } 232 | } 233 | { "GRUB_CMDLINE_LINUX_DEFAULT" 234 | { "quote" = "\"" } 235 | { "value" = "rhgb" } 236 | { "value" = "nohz=on" } 237 | } 238 | ') 239 | end 240 | 241 | it 'deletes specific entries' do 242 | allow_any_instance_of(provider_class).to receive(:mkconfig).and_return('OK') 243 | 244 | apply!(Puppet::Type.type(:kernel_parameter).new( 245 | title: 'rhgb:normal', 246 | ensure: 'absent', 247 | target: target, 248 | provider: 'grub2' 249 | )) 250 | 251 | augparse_filter(target, LENS, FILTER, ' 252 | { "GRUB_CMDLINE_LINUX" 253 | { "quote" = "\"" } 254 | { "value" = "quiet" } 255 | { "value" = "elevator=noop" } 256 | { "value" = "divider=10" } 257 | } 258 | { "GRUB_CMDLINE_LINUX_DEFAULT" 259 | { "quote" = "\"" } 260 | { "value" = "nohz=on" } 261 | } 262 | ') 263 | end 264 | 265 | it 'creates specific entries' do 266 | allow_any_instance_of(provider_class).to receive(:mkconfig).and_return('OK') 267 | 268 | apply!(Puppet::Type.type(:kernel_parameter).new( 269 | title: 'splash:default', 270 | ensure: 'present', 271 | target: target, 272 | provider: 'grub2' 273 | )) 274 | 275 | augparse_filter(target, LENS, FILTER, ' 276 | { "GRUB_CMDLINE_LINUX" 277 | { "quote" = "\"" } 278 | { "value" = "quiet" } 279 | { "value" = "elevator=noop" } 280 | { "value" = "divider=10" } 281 | } 282 | { "GRUB_CMDLINE_LINUX_DEFAULT" 283 | { "quote" = "\"" } 284 | { "value" = "rhgb" } 285 | { "value" = "nohz=on" } 286 | { "value" = "splash" } 287 | } 288 | ') 289 | end 290 | 291 | describe 'when modifying values' do 292 | before do 293 | allow_any_instance_of(provider_class).to receive(:create).and_raise('nope') 294 | end 295 | 296 | it 'changes existing values' do 297 | allow_any_instance_of(provider_class).to receive(:mkconfig).and_return('OK') 298 | apply!(Puppet::Type.type(:kernel_parameter).new( 299 | name: 'elevator', 300 | ensure: :present, 301 | value: 'deadline', 302 | target: target, 303 | provider: 'grub2' 304 | )) 305 | 306 | augparse_filter(target, LENS, FILTER, ' 307 | { "GRUB_CMDLINE_LINUX" 308 | { "quote" = "\"" } 309 | { "value" = "quiet" } 310 | { "value" = "elevator=deadline" } 311 | { "value" = "divider=10" } 312 | } 313 | { "GRUB_CMDLINE_LINUX_DEFAULT" 314 | { "quote" = "\"" } 315 | { "value" = "rhgb" } 316 | { "value" = "nohz=on" } 317 | } 318 | ') 319 | end 320 | 321 | it 'adds value to entry' do 322 | allow_any_instance_of(provider_class).to receive(:mkconfig).and_return('OK') 323 | apply!(Puppet::Type.type(:kernel_parameter).new( 324 | name: 'quiet', 325 | ensure: :present, 326 | value: 'foo', 327 | target: target, 328 | provider: 'grub2' 329 | )) 330 | 331 | augparse_filter(target, LENS, FILTER, ' 332 | { "GRUB_CMDLINE_LINUX" 333 | { "quote" = "\"" } 334 | { "value" = "quiet=foo" } 335 | { "value" = "elevator=noop" } 336 | { "value" = "divider=10" } 337 | } 338 | { "GRUB_CMDLINE_LINUX_DEFAULT" 339 | { "quote" = "\"" } 340 | { "value" = "rhgb" } 341 | { "value" = "nohz=on" } 342 | } 343 | ') 344 | end 345 | 346 | it 'adds and remove entries for multiple values' do 347 | # This will run once for each parameter resource 348 | allow_any_instance_of(provider_class).to receive(:mkconfig).and_return('OK') 349 | 350 | # Add multiple entries 351 | apply!(Puppet::Type.type(:kernel_parameter).new( 352 | name: 'elevator', 353 | ensure: :present, 354 | value: %w[noop deadline], 355 | target: target, 356 | provider: 'grub2' 357 | )) 358 | 359 | augparse_filter(target, LENS, FILTER, ' 360 | { "GRUB_CMDLINE_LINUX" 361 | { "quote" = "\"" } 362 | { "value" = "quiet" } 363 | { "value" = "elevator=noop" } 364 | { "value" = "divider=10" } 365 | { "value" = "elevator=deadline" } 366 | } 367 | { "GRUB_CMDLINE_LINUX_DEFAULT" 368 | { "quote" = "\"" } 369 | { "value" = "rhgb" } 370 | { "value" = "nohz=on" } 371 | } 372 | ') 373 | 374 | # Remove one excess entry 375 | apply!(Puppet::Type.type(:kernel_parameter).new( 376 | name: 'elevator', 377 | ensure: :present, 378 | value: ['deadline'], 379 | target: target, 380 | provider: 'grub2' 381 | )) 382 | 383 | augparse_filter(target, LENS, FILTER, ' 384 | { "GRUB_CMDLINE_LINUX" 385 | { "quote" = "\"" } 386 | { "value" = "quiet" } 387 | { "value" = "elevator=deadline" } 388 | { "value" = "divider=10" } 389 | } 390 | { "GRUB_CMDLINE_LINUX_DEFAULT" 391 | { "quote" = "\"" } 392 | { "value" = "rhgb" } 393 | { "value" = "nohz=on" } 394 | } 395 | ') 396 | end 397 | end 398 | end 399 | 400 | context 'with broken file' do 401 | let(:tmptarget) { aug_fixture('broken') } 402 | let(:target) { tmptarget.path } 403 | 404 | it 'fails to load' do 405 | txn = apply(Puppet::Type.type(:kernel_parameter).new( 406 | name: 'foo', 407 | ensure: :present, 408 | target: target, 409 | provider: 'grub2' 410 | )) 411 | 412 | expect(txn.any_failed?).not_to eq nil 413 | expect(@logs.first.level).to eq :err 414 | expect(@logs.first.message.include?(target)).to eq true 415 | end 416 | end 417 | end 418 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | Each new release typically also includes the latest modulesync defaults. 5 | These should not affect the functionality of the module. 6 | 7 | ## [Unreleased](https://github.com/voxpupuli/puppet-augeasproviders_grub/tree/HEAD) 8 | 9 | [Full Changelog](https://github.com/voxpupuli/puppet-augeasproviders_grub/compare/v5.1.2...HEAD) 10 | 11 | **Breaking changes:** 12 | 13 | - Drop Grub \< 2 remains [\#110](https://github.com/voxpupuli/puppet-augeasproviders_grub/pull/110) ([ekohl](https://github.com/ekohl)) 14 | - Drop EoL EL7 support [\#106](https://github.com/voxpupuli/puppet-augeasproviders_grub/pull/106) ([bastelfreak](https://github.com/bastelfreak)) 15 | 16 | **Implemented enhancements:** 17 | 18 | - can alter any or all sections of /etc/default/grub by using a Composite namevar [\#120](https://github.com/voxpupuli/puppet-augeasproviders_grub/pull/120) ([marcusdots](https://github.com/marcusdots)) 19 | - Add support for Ubuntu 24.04 [\#117](https://github.com/voxpupuli/puppet-augeasproviders_grub/pull/117) ([bwitt](https://github.com/bwitt)) 20 | - Add support for EL10 [\#113](https://github.com/voxpupuli/puppet-augeasproviders_grub/pull/113) ([michael-riddle](https://github.com/michael-riddle)) 21 | - metadata.json: Add OpenVox [\#109](https://github.com/voxpupuli/puppet-augeasproviders_grub/pull/109) ([jstraw](https://github.com/jstraw)) 22 | 23 | **Fixed bugs:** 24 | 25 | - `/boot/efi/EFI/redhat/grub.cfg` incorrectly being updated on EL9 [\#107](https://github.com/voxpupuli/puppet-augeasproviders_grub/issues/107) 26 | - Skip stub grub.cfg files \(e.g. used on Debian OS family\). [\#65](https://github.com/voxpupuli/puppet-augeasproviders_grub/pull/65) ([olifre](https://github.com/olifre)) 27 | 28 | **Closed issues:** 29 | 30 | - Support Ubuntu 24.04 [\#116](https://github.com/voxpupuli/puppet-augeasproviders_grub/issues/116) 31 | - Add support for kernel parameters on el10, specifically when determining if the mkconfig command needs --update-bls-cmdline [\#112](https://github.com/voxpupuli/puppet-augeasproviders_grub/issues/112) 32 | - Ubuntu ESP grub.cfg config broken by grub-mkconfig [\#51](https://github.com/voxpupuli/puppet-augeasproviders_grub/issues/51) 33 | - Modify `GRUB_CMDLINE_LINUX` and `GRUB_CMDLINE_LINUX_DEFAULT` [\#23](https://github.com/voxpupuli/puppet-augeasproviders_grub/issues/23) 34 | 35 | **Merged pull requests:** 36 | 37 | - Add some missing docs [\#119](https://github.com/voxpupuli/puppet-augeasproviders_grub/pull/119) ([bwitt](https://github.com/bwitt)) 38 | 39 | ## [v5.1.2](https://github.com/voxpupuli/puppet-augeasproviders_grub/tree/v5.1.2) (2024-08-22) 40 | 41 | [Full Changelog](https://github.com/voxpupuli/puppet-augeasproviders_grub/compare/v5.1.1...v5.1.2) 42 | 43 | **Fixed bugs:** 44 | 45 | - fix: limit scope of update-bls-cmdline to RHEL9 [\#104](https://github.com/voxpupuli/puppet-augeasproviders_grub/pull/104) ([vchepkov](https://github.com/vchepkov)) 46 | 47 | ## [v5.1.1](https://github.com/voxpupuli/puppet-augeasproviders_grub/tree/v5.1.1) (2024-07-09) 48 | 49 | [Full Changelog](https://github.com/voxpupuli/puppet-augeasproviders_grub/compare/v5.1.0...v5.1.1) 50 | 51 | **Fixed bugs:** 52 | 53 | - RHEL \>= 9.3 - `grub2-mkconfig` does not update BLS kernel options anymore per default [\#95](https://github.com/voxpupuli/puppet-augeasproviders_grub/issues/95) 54 | - Update BLS kernel options on EL \>= 9.3 [\#98](https://github.com/voxpupuli/puppet-augeasproviders_grub/pull/98) ([silug](https://github.com/silug)) 55 | 56 | **Merged pull requests:** 57 | 58 | - README.md: Add badges + transfer notice [\#102](https://github.com/voxpupuli/puppet-augeasproviders_grub/pull/102) ([bastelfreak](https://github.com/bastelfreak)) 59 | 60 | ## [v5.1.0](https://github.com/voxpupuli/puppet-augeasproviders_grub/tree/v5.1.0) (2023-10-30) 61 | 62 | [Full Changelog](https://github.com/voxpupuli/puppet-augeasproviders_grub/compare/v5.0.1...v5.1.0) 63 | 64 | **Implemented enhancements:** 65 | 66 | - Add EL9 support [\#92](https://github.com/voxpupuli/puppet-augeasproviders_grub/pull/92) ([bastelfreak](https://github.com/bastelfreak)) 67 | - Add AlmaLinux/Rocky support [\#91](https://github.com/voxpupuli/puppet-augeasproviders_grub/pull/91) ([bastelfreak](https://github.com/bastelfreak)) 68 | - Add Ubuntu 22.04 support [\#90](https://github.com/voxpupuli/puppet-augeasproviders_grub/pull/90) ([bastelfreak](https://github.com/bastelfreak)) 69 | - Add Debian 12 support [\#89](https://github.com/voxpupuli/puppet-augeasproviders_grub/pull/89) ([bastelfreak](https://github.com/bastelfreak)) 70 | 71 | **Merged pull requests:** 72 | 73 | - Update project page to point to github repo [\#88](https://github.com/voxpupuli/puppet-augeasproviders_grub/pull/88) ([tuxmea](https://github.com/tuxmea)) 74 | 75 | ## [v5.0.1](https://github.com/voxpupuli/puppet-augeasproviders_grub/tree/v5.0.1) (2023-10-15) 76 | 77 | [Full Changelog](https://github.com/voxpupuli/puppet-augeasproviders_grub/compare/v5.0.0...v5.0.1) 78 | 79 | **Fixed bugs:** 80 | 81 | - v4.0.0: Standard error of `grub-mkconfig` written to `grub.cfg` [\#74](https://github.com/voxpupuli/puppet-augeasproviders_grub/issues/74) 82 | 83 | **Merged pull requests:** 84 | 85 | - Drop stderr from mkconfig output when updating grub [\#84](https://github.com/voxpupuli/puppet-augeasproviders_grub/pull/84) ([glangloi](https://github.com/glangloi)) 86 | 87 | ## [v5.0.0](https://github.com/voxpupuli/puppet-augeasproviders_grub/tree/v5.0.0) (2023-06-22) 88 | 89 | [Full Changelog](https://github.com/voxpupuli/puppet-augeasproviders_grub/compare/v4.0.0...v5.0.0) 90 | 91 | **Breaking changes:** 92 | 93 | - Debian: Drop 9, add support for 10 & 11 [\#82](https://github.com/voxpupuli/puppet-augeasproviders_grub/pull/82) ([bastelfreak](https://github.com/bastelfreak)) 94 | - Drop Puppet 6 support [\#77](https://github.com/voxpupuli/puppet-augeasproviders_grub/pull/77) ([bastelfreak](https://github.com/bastelfreak)) 95 | 96 | **Implemented enhancements:** 97 | 98 | - puppet/augeasproviders\_core: Allow 4.x [\#80](https://github.com/voxpupuli/puppet-augeasproviders_grub/pull/80) ([bastelfreak](https://github.com/bastelfreak)) 99 | 100 | **Merged pull requests:** 101 | 102 | - Add puppet 8 support [\#79](https://github.com/voxpupuli/puppet-augeasproviders_grub/pull/79) ([bastelfreak](https://github.com/bastelfreak)) 103 | - Add RHEL 9 to supported OS [\#76](https://github.com/voxpupuli/puppet-augeasproviders_grub/pull/76) ([tuxmea](https://github.com/tuxmea)) 104 | - Fix broken Apache-2 license [\#73](https://github.com/voxpupuli/puppet-augeasproviders_grub/pull/73) ([bastelfreak](https://github.com/bastelfreak)) 105 | 106 | ## [v4.0.0](https://github.com/voxpupuli/puppet-augeasproviders_grub/tree/v4.0.0) (2022-07-29) 107 | 108 | [Full Changelog](https://github.com/voxpupuli/puppet-augeasproviders_grub/compare/3.2.0...v4.0.0) 109 | 110 | **Breaking changes:** 111 | 112 | - Call grub2-mkconfig on all targets [\#57](https://github.com/voxpupuli/puppet-augeasproviders_grub/pull/57) ([traylenator](https://github.com/traylenator)) 113 | 114 | **Fixed bugs:** 115 | 116 | - grub\_menuentry resource fail if directory /boot/grub doe not exist [\#53](https://github.com/voxpupuli/puppet-augeasproviders_grub/issues/53) 117 | - grub.cfg isn't being properly updated on EFI systems running CentOS 7 [\#4](https://github.com/voxpupuli/puppet-augeasproviders_grub/issues/4) 118 | - Fix grub\_menuentry issues [\#56](https://github.com/voxpupuli/puppet-augeasproviders_grub/pull/56) ([trevor-vaughan](https://github.com/trevor-vaughan)) 119 | 120 | **Closed issues:** 121 | 122 | - The mkconfig update in \#4 needs to be ported to the other providers [\#63](https://github.com/voxpupuli/puppet-augeasproviders_grub/issues/63) 123 | - kernel\_parameters set incorrectly on CentOS 8 [\#58](https://github.com/voxpupuli/puppet-augeasproviders_grub/issues/58) 124 | - Typo in provider grub for custom type grub\_menuentry [\#54](https://github.com/voxpupuli/puppet-augeasproviders_grub/issues/54) 125 | - Kernel\_parameter subscribe executes on every run [\#41](https://github.com/voxpupuli/puppet-augeasproviders_grub/issues/41) 126 | - More informative error message for missing dependency [\#34](https://github.com/voxpupuli/puppet-augeasproviders_grub/issues/34) 127 | - Support for Puppet 4 [\#26](https://github.com/voxpupuli/puppet-augeasproviders_grub/issues/26) 128 | - Issue w/Puppet 2016.x [\#24](https://github.com/voxpupuli/puppet-augeasproviders_grub/issues/24) 129 | 130 | **Merged pull requests:** 131 | 132 | - Update augeasproviders\_core version [\#67](https://github.com/voxpupuli/puppet-augeasproviders_grub/pull/67) ([sazzle2611](https://github.com/sazzle2611)) 133 | - Fix mkconfig calls [\#64](https://github.com/voxpupuli/puppet-augeasproviders_grub/pull/64) ([trevor-vaughan](https://github.com/trevor-vaughan)) 134 | - error message improvement: specify that it's a missing module [\#61](https://github.com/voxpupuli/puppet-augeasproviders_grub/pull/61) ([kenyon](https://github.com/kenyon)) 135 | - Fix typo in grub\_menuentry provider [\#55](https://github.com/voxpupuli/puppet-augeasproviders_grub/pull/55) ([trevor-vaughan](https://github.com/trevor-vaughan)) 136 | 137 | ## [3.2.0](https://github.com/voxpupuli/puppet-augeasproviders_grub/tree/3.2.0) (2020-03-31) 138 | 139 | [Full Changelog](https://github.com/voxpupuli/puppet-augeasproviders_grub/compare/3.1.0...3.2.0) 140 | 141 | **Fixed bugs:** 142 | 143 | - grub\_menuentry is broken in EL8 and Fedora 30+ [\#49](https://github.com/voxpupuli/puppet-augeasproviders_grub/issues/49) 144 | 145 | **Closed issues:** 146 | 147 | - grub\_config values with spaces cause augeas errors [\#44](https://github.com/voxpupuli/puppet-augeasproviders_grub/issues/44) 148 | - Absent GRUB\_CMDLINE\_LINUX\_DEFAULT can result in duplicated kernel parameters. [\#38](https://github.com/voxpupuli/puppet-augeasproviders_grub/issues/38) 149 | - The grub2 system should update both the EFI and non-EFI configurations when triggered [\#37](https://github.com/voxpupuli/puppet-augeasproviders_grub/issues/37) 150 | 151 | **Merged pull requests:** 152 | 153 | - Add BLS support to grub\_menuentry [\#50](https://github.com/voxpupuli/puppet-augeasproviders_grub/pull/50) ([trevor-vaughan](https://github.com/trevor-vaughan)) 154 | - Fixed the EFI code for grub\_config and grub\_menuentry [\#48](https://github.com/voxpupuli/puppet-augeasproviders_grub/pull/48) ([tparkercbn](https://github.com/tparkercbn)) 155 | - Fix String value issues in grub\_config [\#46](https://github.com/voxpupuli/puppet-augeasproviders_grub/pull/46) ([trevor-vaughan](https://github.com/trevor-vaughan)) 156 | - Puppet6 [\#42](https://github.com/voxpupuli/puppet-augeasproviders_grub/pull/42) ([raphink](https://github.com/raphink)) 157 | 158 | ## [3.1.0](https://github.com/voxpupuli/puppet-augeasproviders_grub/tree/3.1.0) (2019-02-28) 159 | 160 | [Full Changelog](https://github.com/voxpupuli/puppet-augeasproviders_grub/compare/3.0.1...3.1.0) 161 | 162 | **Closed issues:** 163 | 164 | - Hard dependency on grub2-tools on CentOS7 missing [\#20](https://github.com/voxpupuli/puppet-augeasproviders_grub/issues/20) 165 | 166 | **Merged pull requests:** 167 | 168 | - Add back path for grub.cfg on Debian OS. [\#36](https://github.com/voxpupuli/puppet-augeasproviders_grub/pull/36) ([olifre](https://github.com/olifre)) 169 | 170 | ## [3.0.1](https://github.com/voxpupuli/puppet-augeasproviders_grub/tree/3.0.1) (2018-05-09) 171 | 172 | [Full Changelog](https://github.com/voxpupuli/puppet-augeasproviders_grub/compare/simp6.0.0-3.0.1...3.0.1) 173 | 174 | ## [simp6.0.0-3.0.1](https://github.com/voxpupuli/puppet-augeasproviders_grub/tree/simp6.0.0-3.0.1) (2018-05-09) 175 | 176 | [Full Changelog](https://github.com/voxpupuli/puppet-augeasproviders_grub/compare/3.0.0...simp6.0.0-3.0.1) 177 | 178 | **Closed issues:** 179 | 180 | - EFI support for all oses, not only fedora [\#27](https://github.com/voxpupuli/puppet-augeasproviders_grub/issues/27) 181 | 182 | **Merged pull requests:** 183 | 184 | - Grub2 grub\_user fix [\#32](https://github.com/voxpupuli/puppet-augeasproviders_grub/pull/32) ([trevor-vaughan](https://github.com/trevor-vaughan)) 185 | - Update grub2.rb for EFI systems [\#29](https://github.com/voxpupuli/puppet-augeasproviders_grub/pull/29) ([cohdjn](https://github.com/cohdjn)) 186 | 187 | ## [3.0.0](https://github.com/voxpupuli/puppet-augeasproviders_grub/tree/3.0.0) (2017-08-29) 188 | 189 | [Full Changelog](https://github.com/voxpupuli/puppet-augeasproviders_grub/compare/simp6.0.0-2.4.0...3.0.0) 190 | 191 | **Merged pull requests:** 192 | 193 | - Add Global EFI support [\#28](https://github.com/voxpupuli/puppet-augeasproviders_grub/pull/28) ([trevor-vaughan](https://github.com/trevor-vaughan)) 194 | - Raise exception on missing augeasproviders\_core [\#25](https://github.com/voxpupuli/puppet-augeasproviders_grub/pull/25) ([igalic](https://github.com/igalic)) 195 | 196 | ## [simp6.0.0-2.4.0](https://github.com/voxpupuli/puppet-augeasproviders_grub/tree/simp6.0.0-2.4.0) (2017-01-05) 197 | 198 | [Full Changelog](https://github.com/voxpupuli/puppet-augeasproviders_grub/compare/2.4.0...simp6.0.0-2.4.0) 199 | 200 | **Closed issues:** 201 | 202 | - Unable to set/determine correct provider on Arch Linux [\#22](https://github.com/voxpupuli/puppet-augeasproviders_grub/issues/22) 203 | 204 | ## [2.4.0](https://github.com/voxpupuli/puppet-augeasproviders_grub/tree/2.4.0) (2016-05-03) 205 | 206 | [Full Changelog](https://github.com/voxpupuli/puppet-augeasproviders_grub/compare/2.3.0...2.4.0) 207 | 208 | **Implemented enhancements:** 209 | 210 | - Requesting support for grub 'module' statements [\#10](https://github.com/voxpupuli/puppet-augeasproviders_grub/issues/10) 211 | - Confine GRUB providers to presence of menus, prefer GRUB 2 [\#8](https://github.com/voxpupuli/puppet-augeasproviders_grub/pull/8) ([ckoenig](https://github.com/ckoenig)) 212 | 213 | **Closed issues:** 214 | 215 | - Fails on CentOS 6 [\#6](https://github.com/voxpupuli/puppet-augeasproviders_grub/issues/6) 216 | 217 | **Merged pull requests:** 218 | 219 | - Update grub2.rb to add On UEFI Systems, grub.cfg [\#21](https://github.com/voxpupuli/puppet-augeasproviders_grub/pull/21) ([stivesso](https://github.com/stivesso)) 220 | - Updated the Changelog [\#19](https://github.com/voxpupuli/puppet-augeasproviders_grub/pull/19) ([trevor-vaughan](https://github.com/trevor-vaughan)) 221 | - Added support for global GRUB configuration [\#18](https://github.com/voxpupuli/puppet-augeasproviders_grub/pull/18) ([trevor-vaughan](https://github.com/trevor-vaughan)) 222 | 223 | ## [2.3.0](https://github.com/voxpupuli/puppet-augeasproviders_grub/tree/2.3.0) (2016-02-18) 224 | 225 | [Full Changelog](https://github.com/voxpupuli/puppet-augeasproviders_grub/compare/2.2.0...2.3.0) 226 | 227 | **Closed issues:** 228 | 229 | - wrong version of grub detection on Ubuntu Trusty [\#13](https://github.com/voxpupuli/puppet-augeasproviders_grub/issues/13) 230 | - Grub2 does not add the /files/etc/default/grub/GRUB\_CMDLINE\_LINUX\_DEFAULT path if it is missing [\#11](https://github.com/voxpupuli/puppet-augeasproviders_grub/issues/11) 231 | 232 | **Merged pull requests:** 233 | 234 | - adding 2 defaults for grub 2 [\#17](https://github.com/voxpupuli/puppet-augeasproviders_grub/pull/17) ([wanix](https://github.com/wanix)) 235 | - add grub.cfg location for grub2 on UEFI systems [\#16](https://github.com/voxpupuli/puppet-augeasproviders_grub/pull/16) ([tedwardia](https://github.com/tedwardia)) 236 | - Fix GRUB\_CMDLINE\_LINUX\_DEFAULT [\#14](https://github.com/voxpupuli/puppet-augeasproviders_grub/pull/14) ([trevor-vaughan](https://github.com/trevor-vaughan)) 237 | 238 | ## [2.2.0](https://github.com/voxpupuli/puppet-augeasproviders_grub/tree/2.2.0) (2016-01-04) 239 | 240 | [Full Changelog](https://github.com/voxpupuli/puppet-augeasproviders_grub/compare/2.1.0...2.2.0) 241 | 242 | ## [2.1.0](https://github.com/voxpupuli/puppet-augeasproviders_grub/tree/2.1.0) (2015-11-17) 243 | 244 | [Full Changelog](https://github.com/voxpupuli/puppet-augeasproviders_grub/compare/2.0.1...2.1.0) 245 | 246 | **Closed issues:** 247 | 248 | - undefined method `provider' for nil:NilClass [\#12](https://github.com/voxpupuli/puppet-augeasproviders_grub/issues/12) 249 | - Wrong provider selected in Centos7 [\#7](https://github.com/voxpupuli/puppet-augeasproviders_grub/issues/7) 250 | 251 | **Merged pull requests:** 252 | 253 | - Set default to grub2 provider on el7 based systems [\#9](https://github.com/voxpupuli/puppet-augeasproviders_grub/pull/9) ([vinzent](https://github.com/vinzent)) 254 | 255 | ## [2.0.1](https://github.com/voxpupuli/puppet-augeasproviders_grub/tree/2.0.1) (2014-12-10) 256 | 257 | [Full Changelog](https://github.com/voxpupuli/puppet-augeasproviders_grub/compare/2.0.0...2.0.1) 258 | 259 | **Closed issues:** 260 | 261 | - Undefined method "provider" on Centos 6.5 [\#2](https://github.com/voxpupuli/puppet-augeasproviders_grub/issues/2) 262 | 263 | ## [2.0.0](https://github.com/voxpupuli/puppet-augeasproviders_grub/tree/2.0.0) (2014-08-11) 264 | 265 | [Full Changelog](https://github.com/voxpupuli/puppet-augeasproviders_grub/compare/a9a2ad1f0685c21d9f0e5fd222c12f21b029a40b...2.0.0) 266 | 267 | 268 | 269 | \* *This Changelog was automatically generated by [github_changelog_generator](https://github.com/github-changelog-generator/github-changelog-generator)* 270 | -------------------------------------------------------------------------------- /spec/unit/puppetx/augeasproviders_grub/util_spec.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env rspec 2 | # frozen_string_literal: true 3 | 4 | require 'rspec' 5 | require 'puppet' 6 | require 'facter' 7 | 8 | # Load the main module we're testing 9 | require 'puppetx/augeasproviders_grub/util' 10 | 11 | util_class = PuppetX::AugeasprovidersGrub::Util 12 | 13 | describe util_class do 14 | before do 15 | # Clear Facter facts similar to grub2_spec.rb 16 | Facter.clear 17 | end 18 | 19 | describe '.grub2_cfg_paths' do 20 | let(:paths) do 21 | [ 22 | '/etc/grub2.cfg', 23 | '/etc/grub2-efi.cfg', 24 | '/boot/efi/EFI/centos/grub.cfg', 25 | '/boot/grub2/grub.cfg', 26 | '/boot/grub/grub.cfg' 27 | ] 28 | end 29 | 30 | before do 31 | # Mock os_name to return 'centos' for predictable path generation 32 | allow(described_class).to receive(:os_name).and_return('centos') 33 | 34 | # Reset all file system checks 35 | allow(File).to receive(:readable?).and_return(false) 36 | allow(File).to receive(:directory?).and_return(false) 37 | allow(File).to receive(:realpath).and_call_original 38 | allow(File).to receive(:foreach).and_call_original 39 | end 40 | 41 | context 'when no grub config files exist' do 42 | it 'raises an error' do 43 | expect { described_class.grub2_cfg_paths }.to raise_error( 44 | RuntimeError, 45 | %r{No grub configuration found at} 46 | ) 47 | end 48 | end 49 | 50 | context 'when grub config files exist' do 51 | before do 52 | allow(File).to receive(:readable?).with('/etc/grub2.cfg').and_return(true) 53 | allow(File).to receive(:realpath).with('/etc/grub2.cfg').and_return('/etc/grub2.cfg') 54 | end 55 | 56 | context 'and contains regular grub configuration' do 57 | before do 58 | # Mock file content without configfile directive 59 | allow(File).to receive(:foreach).with('/etc/grub2.cfg').and_yield('# GRUB2 configuration file'). 60 | and_yield('set timeout=5'). 61 | and_yield('menuentry "Linux" {'). 62 | and_yield(' linux /vmlinuz'). 63 | and_yield('}') 64 | end 65 | 66 | it 'returns the valid config path' do 67 | expect(described_class.grub2_cfg_paths).to eq(['/etc/grub2.cfg']) 68 | end 69 | end 70 | 71 | context 'and contains stub configuration with configfile directive' do 72 | before do 73 | # Mock file content with configfile directive (Debian-style stub) 74 | allow(File).to receive(:foreach).with('/etc/grub2.cfg').and_yield('# GRUB2 stub configuration'). 75 | and_yield('set prefix=/boot/grub'). 76 | and_yield('configfile /boot/grub/grub.cfg') 77 | end 78 | 79 | it 'excludes the stub file and raises error when no other files exist' do 80 | expect { described_class.grub2_cfg_paths }.to raise_error( 81 | RuntimeError, 82 | %r{No grub configuration found at} 83 | ) 84 | end 85 | end 86 | 87 | context 'and contains configfile directive at start of line' do 88 | before do 89 | # Mock file content with configfile directive at start of line 90 | allow(File).to receive(:foreach).with('/etc/grub2.cfg').and_yield('# GRUB2 stub'). 91 | and_yield('configfile /boot/grub/grub.cfg') 92 | end 93 | 94 | it 'excludes the stub file when configfile is at start of line' do 95 | expect { described_class.grub2_cfg_paths }.to raise_error( 96 | RuntimeError, 97 | %r{No grub configuration found at} 98 | ) 99 | end 100 | end 101 | end 102 | 103 | context 'when multiple grub config files exist' do 104 | before do 105 | # Mock multiple files as readable 106 | allow(File).to receive(:readable?).with('/etc/grub2.cfg').and_return(true) 107 | allow(File).to receive(:readable?).with('/boot/grub2/grub.cfg').and_return(true) 108 | allow(File).to receive(:realpath).with('/etc/grub2.cfg').and_return('/etc/grub2.cfg') 109 | allow(File).to receive(:realpath).with('/boot/grub2/grub.cfg').and_return('/boot/grub2/grub.cfg') 110 | end 111 | 112 | context 'with one stub and one regular config' do 113 | before do 114 | # First file is a stub 115 | allow(File).to receive(:foreach).with('/etc/grub2.cfg').and_yield('configfile /boot/grub/grub.cfg') 116 | 117 | # Second file is regular config 118 | allow(File).to receive(:foreach).with('/boot/grub2/grub.cfg').and_yield('# Regular GRUB config'). 119 | and_yield('menuentry "Linux" {}') 120 | end 121 | 122 | it 'returns only the non-stub config' do 123 | expect(described_class.grub2_cfg_paths).to eq(['/boot/grub2/grub.cfg']) 124 | end 125 | end 126 | 127 | context 'with multiple regular configs' do 128 | before do 129 | # Both files are regular configs 130 | allow(File).to receive(:foreach).with('/etc/grub2.cfg').and_yield('# Regular GRUB config'). 131 | and_yield('menuentry "Linux" {}') 132 | 133 | allow(File).to receive(:foreach).with('/boot/grub2/grub.cfg').and_yield('# Another regular config'). 134 | and_yield('set timeout=10') 135 | end 136 | 137 | it 'returns both configs' do 138 | result = described_class.grub2_cfg_paths 139 | expect(result).to include('/etc/grub2.cfg') 140 | expect(result).to include('/boot/grub2/grub.cfg') 141 | expect(result.length).to eq(2) 142 | end 143 | end 144 | 145 | context 'with symlinked files' do 146 | before do 147 | allow(File).to receive(:readable?).with('/etc/grub2.cfg').and_return(true) 148 | allow(File).to receive(:readable?).with('/etc/grub2-efi.cfg').and_return(true) 149 | 150 | # Both symlink to the same real file 151 | allow(File).to receive(:realpath).with('/etc/grub2.cfg').and_return('/boot/grub2/grub.cfg') 152 | allow(File).to receive(:realpath).with('/etc/grub2-efi.cfg').and_return('/boot/grub2/grub.cfg') 153 | 154 | allow(File).to receive(:foreach).with('/boot/grub2/grub.cfg').and_yield('# Real config'). 155 | and_yield('menuentry "Linux" {}') 156 | end 157 | 158 | it 'returns unique paths only' do 159 | expect(described_class.grub2_cfg_paths).to eq(['/boot/grub2/grub.cfg']) 160 | end 161 | end 162 | end 163 | 164 | context 'when checking file readability and type' do 165 | before do 166 | allow(File).to receive(:realpath).with('/etc/grub2.cfg').and_return('/etc/grub2.cfg') 167 | end 168 | 169 | context 'when file is not readable' do 170 | before do 171 | allow(File).to receive(:readable?).with('/etc/grub2.cfg').and_return(false) 172 | end 173 | 174 | it 'skips the file' do 175 | expect { described_class.grub2_cfg_paths }.to raise_error( 176 | RuntimeError, 177 | %r{No grub configuration found at} 178 | ) 179 | end 180 | end 181 | 182 | context 'when path is a directory' do 183 | before do 184 | allow(File).to receive(:readable?).with('/etc/grub2.cfg').and_return(true) 185 | allow(File).to receive(:directory?).with('/etc/grub2.cfg').and_return(true) 186 | end 187 | 188 | it 'skips the directory' do 189 | expect { described_class.grub2_cfg_paths }.to raise_error( 190 | RuntimeError, 191 | %r{No grub configuration found at} 192 | ) 193 | end 194 | end 195 | end 196 | 197 | context 'with Debian-style configuration scenario' do 198 | before do 199 | # Simulate typical Debian setup where /etc/grub2.cfg is a stub 200 | # and /boot/grub/grub.cfg contains the real configuration 201 | allow(File).to receive(:readable?).with('/etc/grub2.cfg').and_return(true) 202 | allow(File).to receive(:readable?).with('/boot/grub/grub.cfg').and_return(true) 203 | allow(File).to receive(:realpath).with('/etc/grub2.cfg').and_return('/etc/grub2.cfg') 204 | allow(File).to receive(:realpath).with('/boot/grub/grub.cfg').and_return('/boot/grub/grub.cfg') 205 | 206 | # /etc/grub2.cfg is a stub file 207 | allow(File).to receive(:foreach).with('/etc/grub2.cfg').and_yield('# Stub file pointing to real config'). 208 | and_yield('search --no-floppy --fs-uuid --set=root abcd-efgh'). 209 | and_yield('configfile ($root)/boot/grub/grub.cfg') 210 | 211 | # /boot/grub/grub.cfg is the real config 212 | allow(File).to receive(:foreach).with('/boot/grub/grub.cfg').and_yield('# This file provides configuration for GRUB'). 213 | and_yield('set timeout=5'). 214 | and_yield('menuentry "Debian GNU/Linux" {'). 215 | and_yield(' linux /vmlinuz root=/dev/sda1'). 216 | and_yield('}') 217 | end 218 | 219 | it 'correctly identifies and uses the real config file' do 220 | expect(described_class.grub2_cfg_paths).to eq(['/boot/grub/grub.cfg']) 221 | end 222 | end 223 | 224 | context 'with edge case configfile patterns' do 225 | before do 226 | allow(File).to receive(:readable?).with('/etc/grub2.cfg').and_return(true) 227 | allow(File).to receive(:realpath).with('/etc/grub2.cfg').and_return('/etc/grub2.cfg') 228 | end 229 | 230 | context 'when configfile appears in a comment' do 231 | before do 232 | allow(File).to receive(:foreach).with('/etc/grub2.cfg').and_yield('# This file uses configfile directive'). 233 | and_yield('set timeout=5'). 234 | and_yield('menuentry "Test" {}') 235 | end 236 | 237 | it 'does not exclude the file' do 238 | expect(described_class.grub2_cfg_paths).to eq(['/etc/grub2.cfg']) 239 | end 240 | end 241 | 242 | context 'when configfile appears at start of line only' do 243 | before do 244 | allow(File).to receive(:foreach).with('/etc/grub2.cfg').and_yield('set root=hd0,1'). 245 | and_yield(' # not configfile at start'). 246 | and_yield('echo "configfile mentioned here"'). 247 | and_yield('configfile /real/config') 248 | end 249 | 250 | it 'excludes the file when configfile is at line start' do 251 | expect { described_class.grub2_cfg_paths }.to raise_error( 252 | RuntimeError, 253 | %r{No grub configuration found at} 254 | ) 255 | end 256 | end 257 | 258 | context 'when configfile appears with leading whitespace' do 259 | before do 260 | allow(File).to receive(:foreach).with('/etc/grub2.cfg').and_yield('set root=hd0,1'). 261 | and_yield(' # leading spaces example'). 262 | and_yield(' configfile /real/config') 263 | end 264 | 265 | it 'includes the file when configfile has leading whitespace (not detected as stub)' do 266 | expect(described_class.grub2_cfg_paths).to eq(['/etc/grub2.cfg']) 267 | end 268 | end 269 | 270 | context 'when configfile appears with tabs' do 271 | before do 272 | allow(File).to receive(:foreach).with('/etc/grub2.cfg').and_yield('set root=hd0,1'). 273 | and_yield("\tconfigfile /real/config") 274 | end 275 | 276 | it 'includes the file when configfile has leading tabs (not detected as stub)' do 277 | expect(described_class.grub2_cfg_paths).to eq(['/etc/grub2.cfg']) 278 | end 279 | end 280 | 281 | context 'when configfile appears with mixed whitespace' do 282 | before do 283 | allow(File).to receive(:foreach).with('/etc/grub2.cfg').and_yield('set root=hd0,1'). 284 | and_yield(" \t configfile\t/real/config") 285 | end 286 | 287 | it 'includes the file when configfile has mixed whitespace (not detected as stub)' do 288 | expect(described_class.grub2_cfg_paths).to eq(['/etc/grub2.cfg']) 289 | end 290 | end 291 | 292 | context 'when file is empty' do 293 | before do 294 | allow(File).to receive(:foreach).with('/etc/grub2.cfg') 295 | end 296 | 297 | it 'includes the empty file as valid' do 298 | expect(described_class.grub2_cfg_paths).to eq(['/etc/grub2.cfg']) 299 | end 300 | end 301 | 302 | context 'when file contains only comments' do 303 | before do 304 | allow(File).to receive(:foreach).with('/etc/grub2.cfg').and_yield('# This is a comment'). 305 | and_yield('# Another comment'). 306 | and_yield('# configfile is mentioned here but in comment') 307 | end 308 | 309 | it 'includes the file as valid when configfile only appears in comments' do 310 | expect(described_class.grub2_cfg_paths).to eq(['/etc/grub2.cfg']) 311 | end 312 | end 313 | 314 | context 'when configfile is part of another command' do 315 | before do 316 | allow(File).to receive(:foreach).with('/etc/grub2.cfg').and_yield('set root=hd0,1'). 317 | and_yield('echo "Using configfile command"'). 318 | and_yield('load_configfile_module') 319 | end 320 | 321 | it 'includes the file when configfile is not at start of line' do 322 | expect(described_class.grub2_cfg_paths).to eq(['/etc/grub2.cfg']) 323 | end 324 | end 325 | 326 | context 'when configfile has different capitalization' do 327 | before do 328 | allow(File).to receive(:foreach).with('/etc/grub2.cfg').and_yield('set root=hd0,1'). 329 | and_yield('ConfigFile /real/config'). 330 | and_yield('CONFIGFILE /another/config') 331 | end 332 | 333 | it 'includes the file when configfile has different case (case-sensitive match)' do 334 | expect(described_class.grub2_cfg_paths).to eq(['/etc/grub2.cfg']) 335 | end 336 | end 337 | 338 | context 'when configfile is the only content' do 339 | before do 340 | allow(File).to receive(:foreach).with('/etc/grub2.cfg').and_yield('configfile /boot/grub/grub.cfg') 341 | end 342 | 343 | it 'excludes the file when configfile is the only line' do 344 | expect { described_class.grub2_cfg_paths }.to raise_error( 345 | RuntimeError, 346 | %r{No grub configuration found at} 347 | ) 348 | end 349 | end 350 | 351 | context 'when multiple configfile directives exist' do 352 | before do 353 | allow(File).to receive(:foreach).with('/etc/grub2.cfg').and_yield('# Stub file'). 354 | and_yield('configfile /boot/grub/grub.cfg'). 355 | and_yield('configfile /another/grub.cfg') 356 | end 357 | 358 | it 'excludes the file on first configfile match' do 359 | expect { described_class.grub2_cfg_paths }.to raise_error( 360 | RuntimeError, 361 | %r{No grub configuration found at} 362 | ) 363 | end 364 | end 365 | 366 | context 'when configfile has various arguments' do 367 | before do 368 | allow(File).to receive(:foreach).with('/etc/grub2.cfg').and_yield('set prefix=/boot/grub'). 369 | and_yield('configfile ($root)/boot/grub/grub.cfg') 370 | end 371 | 372 | it 'excludes the file when configfile has complex arguments' do 373 | expect { described_class.grub2_cfg_paths }.to raise_error( 374 | RuntimeError, 375 | %r{No grub configuration found at} 376 | ) 377 | end 378 | end 379 | end 380 | 381 | context 'with different operating system names' do 382 | before do 383 | allow(File).to receive(:readable?).and_return(false) 384 | allow(File).to receive(:directory?).and_return(false) 385 | end 386 | 387 | context 'when os_name returns Ubuntu' do 388 | before do 389 | allow(described_class).to receive(:os_name).and_return('Ubuntu') 390 | allow(File).to receive(:readable?).with('/boot/efi/EFI/ubuntu/grub.cfg').and_return(true) 391 | allow(File).to receive(:realpath).with('/boot/efi/EFI/ubuntu/grub.cfg').and_return('/boot/efi/EFI/ubuntu/grub.cfg') 392 | allow(File).to receive(:foreach).with('/boot/efi/EFI/ubuntu/grub.cfg').and_yield('# Ubuntu GRUB config'). 393 | and_yield('menuentry "Ubuntu" {}') 394 | end 395 | 396 | it 'includes the Ubuntu-specific EFI path' do 397 | expect(described_class.grub2_cfg_paths).to eq(['/boot/efi/EFI/ubuntu/grub.cfg']) 398 | end 399 | end 400 | 401 | context 'when os_name returns empty string' do 402 | before do 403 | allow(described_class).to receive(:os_name).and_return('') 404 | allow(File).to receive(:readable?).with('/boot/efi/EFI//grub.cfg').and_return(true) 405 | allow(File).to receive(:realpath).with('/boot/efi/EFI//grub.cfg').and_return('/boot/efi/EFI//grub.cfg') 406 | allow(File).to receive(:foreach).with('/boot/efi/EFI//grub.cfg').and_yield('# Generic EFI config'). 407 | and_yield('menuentry "Linux" {}') 408 | end 409 | 410 | it 'includes the path with empty OS name' do 411 | expect(described_class.grub2_cfg_paths).to eq(['/boot/efi/EFI//grub.cfg']) 412 | end 413 | end 414 | end 415 | end 416 | 417 | describe '.grub2_cfg_path' do 418 | context 'when grub2_cfg_paths returns multiple paths' do 419 | before do 420 | allow(described_class).to receive(:grub2_cfg_paths).and_return([ 421 | '/etc/grub2.cfg', 422 | '/boot/grub2/grub.cfg' 423 | ]) 424 | end 425 | 426 | it 'returns the first path' do 427 | expect(described_class.grub2_cfg_path).to eq('/etc/grub2.cfg') 428 | end 429 | end 430 | 431 | context 'when grub2_cfg_paths returns empty array' do 432 | before do 433 | allow(described_class).to receive(:grub2_cfg_paths).and_return([]) 434 | end 435 | 436 | it 'raises an error' do 437 | expect { described_class.grub2_cfg_path }.to raise_error( 438 | Puppet::Error, 439 | 'Could not find a GRUB2 configuration on the system' 440 | ) 441 | end 442 | end 443 | end 444 | 445 | describe '.munge_options' do 446 | # Include some basic tests for the existing functionality to ensure no regression 447 | it 'handles basic option merging' do 448 | system_opts = %w[quiet splash] 449 | new_opts = ['debug'] 450 | 451 | result = described_class.munge_options(system_opts, new_opts) 452 | expect(result).to include('debug') 453 | end 454 | 455 | it 'handles :preserve: option' do 456 | system_opts = %w[quiet splash] 457 | new_opts = [':preserve:', 'debug'] 458 | 459 | result = described_class.munge_options(system_opts, new_opts) 460 | expect(result).to include('quiet', 'splash', 'debug') 461 | end 462 | 463 | it 'handles :defaults: option' do 464 | system_opts = %w[quiet splash] 465 | new_opts = [':defaults:', 'debug'] 466 | default_opts = ['console=tty0'] 467 | 468 | result = described_class.munge_options(system_opts, new_opts, default_opts) 469 | expect(result).to include('console=tty0', 'debug') 470 | end 471 | end 472 | 473 | describe '.os_name' do 474 | context 'when modern Facter is available' do 475 | before do 476 | allow(Facter).to receive(:value).with(:os).and_return({ 'name' => 'CentOS' }) 477 | end 478 | 479 | it 'returns the OS name from modern fact' do 480 | expect(described_class.os_name).to eq('CentOS') 481 | end 482 | end 483 | 484 | context 'when legacy Facter is available' do 485 | before do 486 | allow(Facter).to receive(:value).with(:os).and_return(nil) 487 | allow(Facter).to receive(:value).with(:operatingsystem).and_return('Ubuntu') 488 | end 489 | 490 | it 'returns the OS name from legacy fact' do 491 | expect(described_class.os_name).to eq('Ubuntu') 492 | end 493 | end 494 | 495 | context 'when no OS facts are available' do 496 | before do 497 | allow(Facter).to receive(:value).with(:os).and_return(nil) 498 | allow(Facter).to receive(:value).with(:operatingsystem).and_return(nil) 499 | end 500 | 501 | it 'returns empty string as fallback' do 502 | expect(described_class.os_name).to eq('') 503 | end 504 | end 505 | end 506 | end 507 | -------------------------------------------------------------------------------- /lib/puppet/provider/grub_menuentry/grub2.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # GRUB 2 support for menuentries, adds material to /etc/grub.d/05_puppet_controlled_* 4 | # 5 | # Copyright (c) 2016 Trevor Vaughan 6 | # Licensed under the Apache License, Version 2.0 7 | 8 | Puppet::Type.type(:grub_menuentry).provide(:grub2, parent: Puppet::Type.type(:augeasprovider).provider(:default)) do 9 | desc 'Provides for the manipulation of GRUB2 menuentries' 10 | 11 | has_feature :grub2 12 | 13 | # Populate the default methods 14 | mk_resource_methods 15 | 16 | #### Class Methods 17 | def self.mkconfig_path 18 | which('grub2-mkconfig') or which('grub-mkconfig') or '/usr/sbin/grub-mkconfig' 19 | end 20 | 21 | # Return an Array of system resources culled from a full GRUB2 22 | # mkconfig-generated configuration 23 | # 24 | # @param config (String) The output of grub2-mkconfig or the grub2.cfg file 25 | # @param current_default (String) The name of the current default GRUB entry 26 | # @return (Array) An Array of resource Hashes 27 | def self.grub2_mkconfig_menuentries(config, current_default) 28 | resources = [] 29 | 30 | # Pull out the menuentries into our resources 31 | in_menuentry = false 32 | 33 | # We need to track these to set the default entry 34 | submenus = [] 35 | resource = {} 36 | config.to_s.each_line do |line| 37 | submenus << Regexp.last_match(1) if line =~ %r{^\s*submenu (?:'|")(.*)(?:':")\s*\{} 38 | 39 | # The main menuentry line 40 | if line =~ %r{^\s*menuentry '(.+?)'} 41 | resource = { 42 | name: Regexp.last_match(1), 43 | bls: false 44 | } 45 | resource[:default_entry] = (resource[:name] == current_default) 46 | 47 | raise Puppet::Error, 'Malformed config file received' if in_menuentry 48 | 49 | in_menuentry = true 50 | 51 | menuentry_components = line.strip.split(%r{\s+})[1..-1] 52 | 53 | classes = [] 54 | menuentry_components.each_index do |i| 55 | classes << menuentry_components[i + 1] if menuentry_components[i] == '--class' 56 | end 57 | 58 | resource[:classes] = classes unless classes.empty? 59 | 60 | users = [] 61 | if menuentry_components.include?('--unrestricted') 62 | users = [:unrestricted] 63 | else 64 | menuentry_components.each_index do |i| 65 | if menuentry_components[i] == '--users' 66 | # This insanity per the GRUB2 user's guide 67 | users = menuentry_components[i + 1].strip.split(%r{\s|,|;|\||&}) 68 | end 69 | end 70 | end 71 | 72 | resource[:users] = users unless users.empty? 73 | 74 | resource[:load_video] = false 75 | resource[:load_16bit] = false 76 | resource[:puppet_managed] = false 77 | resource[:modules] ||= [] 78 | 79 | elsif in_menuentry 80 | case line 81 | when %r{^\s*load_video\s*$} 82 | resource[:load_video] = true 83 | 84 | when %r{^\s*insmod\s+(\S+)$} 85 | resource[:plugins] ||= [] 86 | 87 | resource[:plugins] << Regexp.last_match(1).strip 88 | 89 | when %r{^\s*(?:set\s+)?root='(.+)'$} 90 | resource[:root] = Regexp.last_match(1).strip 91 | 92 | when %r{^\s*#### PUPPET MANAGED ####\s*$} 93 | resource[:puppet_managed] = true 94 | 95 | when %r{^\s*(?:multiboot|linux(16)?)(.+)} 96 | resource[:load_16bit] = true if Regexp.last_match(1) == '16' 97 | 98 | kernel_line = Regexp.last_match(2).strip.split(%r{\s+}) 99 | resource[:kernel] = kernel_line.shift 100 | resource[:kernel_options] = kernel_line 101 | 102 | when %r{^\s*initrd(?:16)?(.+)} 103 | resource[:initrd] = Regexp.last_match(1).strip 104 | 105 | when %r{^\s*module\s+(.+)} 106 | resource[:modules] << Regexp.last_match(1).strip.split(%r{\s+}) 107 | 108 | when %r{^\s*\}\s*$} 109 | in_menuentry = false 110 | if resource.empty? 111 | debug('Warning: menuentry resource was empty') 112 | else 113 | resource[:submenus] = submenus 114 | resources << resource 115 | end 116 | 117 | submenus = [] 118 | end 119 | end 120 | end 121 | 122 | resources 123 | end 124 | 125 | # Return an Array of system resources based on processing the BLS stack 126 | # 127 | # This is required for systems that use the BLS Grub2 module 128 | # 129 | # @param current_default (String) The name of the current default GRUB entry 130 | # @return (Array) An Array of resource Hashes 131 | def self.grub2_bls_menuentries(current_default) 132 | resources = [] 133 | 134 | begin 135 | grubenv = File.read('/boot/grub2/grubenv').lines. 136 | # Remove comments 137 | reject { |l| l.start_with?('#') }. 138 | # Create a hash of all of the key/value entries in the file 139 | map { |l| l.strip.split(%r{^(.+?)=(.+)$}).reject(&:empty?) }.to_h 140 | 141 | require 'puppet/util/package' 142 | # This is how grubby does it 143 | # See https://fedoraproject.org/wiki/Changes/BootLoaderSpecByDefault 144 | Dir.glob('/boot/loader/entries/*.conf').sort { |x, y| Puppet::Util::Package.versioncmp(y, x) }.each do |file| 145 | config = File.read(file) 146 | 147 | puppet_managed = config.include?('### PUPPET MANAGED ###') 148 | 149 | # Before we begin processing, replace all environment variables with their 150 | # corresponding setting from `grubenv` above 151 | # 152 | # This doesn't catch everything because we aren't processing the entire 153 | # grub2.cfg file but it's about the best we can do 154 | grubenv.each do |k, v| 155 | config.gsub!(%r{\$#{k}(\s+|$)}) { "#{v}#{Regexp.last_match(1)}" } 156 | end 157 | 158 | config = config.lines.map(&:strip) 159 | 160 | # Process out args that could occur multiple times 161 | # 162 | # The specs are completely unclear on how this actually works, so we're 163 | # taking a wild stab and assuming that plural things can be space 164 | # separated and non-plural things can only have one item. 165 | # 166 | users, config = config.partition { |x| x.match?(%r{^grub_users\s+}) } 167 | users.map! { |x| x.split(%r{\s+}) } 168 | users.flatten! 169 | users.delete('grub_users') 170 | users.sort 171 | 172 | args, config = config.partition { |x| x.match?(%r{^grub_arg\s+}) } 173 | args.map! { |x| x.split(%r{\s+}) } 174 | args.flatten! 175 | args.delete('grub_arg') 176 | args.sort 177 | 178 | classes, config = config.partition { |x| x.match?(%r{^grub_class\s+}) } 179 | classes.map! { |x| x.split(%r{\s+}) } 180 | classes.flatten! 181 | classes.delete('grub_class') 182 | classes.sort 183 | 184 | # Convert the rest of the config to a Hash for ease of use 185 | config = config.reject { |l| l.start_with?('#') }.map { |l| l.split(%r{^(.+?)\s+(.+)$}).reject(&:empty?) }.to_h 186 | 187 | resource = { 188 | name: config['title'], 189 | bls: true, 190 | bls_target: file, 191 | puppet_managed: puppet_managed, 192 | default_entry: false 193 | } 194 | 195 | resource[:default_entry] = (resource[:name] == current_default) 196 | 197 | if args.include?('--unrestricted') 198 | resource[:users] = [:unrestricted] 199 | args.delete('--unrestricted') 200 | elsif !users.empty? 201 | resource[:users] = users 202 | end 203 | 204 | resource[:args] = args unless args.empty? 205 | resource[:classes] = classes unless classes.empty? 206 | resource[:kernel] = config['linux'] 207 | resource[:kernel_options] = config['options'] 208 | resource[:initrd] = config['initrd'] 209 | 210 | resources << resource 211 | end 212 | rescue StandardError => e 213 | debug("Exception while processing BLS entries => #{e}") 214 | end 215 | 216 | resources 217 | end 218 | 219 | # Return an Array of system resources culled from the system GRUB2 220 | # configuration 221 | # 222 | # @param config (String) The output of grub2-mkconfig or the grub2.cfg file 223 | # @param current_default (String) The name of the current default GRUB entry 224 | # @return (Array) An Array of resource Hashes 225 | def self.grub2_menuentries(config, current_default) 226 | resources = grub2_mkconfig_menuentries(config, current_default) 227 | resources += grub2_bls_menuentries(current_default) if config.match?(%r{^blscfg$}) 228 | 229 | resources 230 | end 231 | 232 | def self.instances 233 | require 'puppetx/augeasproviders_grub/util' 234 | 235 | @grubby_default_index ||= (grubby '--default-index').strip 236 | 237 | current_default = nil 238 | current_default = Regexp.last_match(1).delete('"') if (grubby "--info=#{@grubby_default_index}") =~ %r{^\s*title=(.+)\s*$} 239 | 240 | grub2_menuentries(PuppetX::AugeasprovidersGrub::Util.grub2_cfg, current_default).map { |x| new(x) } 241 | end 242 | 243 | def self.prefetch(resources) 244 | instances.each do |prov| 245 | if (resource = resources[prov.name]) 246 | resource.provider = prov 247 | end 248 | end 249 | end 250 | 251 | #### End Class Methods 252 | 253 | confine exists: mkconfig_path, for_binary: true 254 | 255 | def mkconfig 256 | execute(self.class.mkconfig_path, { failonfail: true, combine: false }) 257 | end 258 | 259 | commands grubby: 'grubby' 260 | commands grub_set_default: 'grub2-set-default' 261 | 262 | confine exists: '/etc/grub.d' 263 | 264 | defaultfor osfamily: :RedHat 265 | 266 | def initialize(*args) 267 | super(*args) 268 | 269 | require 'puppetx/augeasproviders_grub/util' 270 | 271 | @grubby_info = {} 272 | begin 273 | @grubby_default_index = (grubby '--default-index').strip 274 | grubby_raw = grubby "--info=#{@grubby_default_index}" 275 | 276 | grubby_raw.each_line do |opt| 277 | key, val = opt.to_s.split('=') 278 | 279 | @grubby_info[key.strip] = val.strip.delete('"') 280 | end 281 | rescue Puppet::ExecutionFailure 282 | @grubby_info = {} 283 | end 284 | 285 | current_default = (@grubby_info['title']) if @grubby_info['title'] 286 | 287 | # Things that we really only want to do once... 288 | menu_entries = self.class.grub2_menuentries(PuppetX::AugeasprovidersGrub::Util.grub2_cfg, current_default) 289 | 290 | # These only matter if we're trying to manipulate a resource 291 | 292 | # Extract the default entry for reference later 293 | @default_entry = menu_entries.select { |x| x[:default_entry] }.first 294 | raise(Puppet::Error, 'Could not find a default GRUB2 entry. Check your system grub configuration using `grubby --info=`grubby --default-index``') unless @default_entry 295 | 296 | @bls_system = (menu_entries.find { |x| x[:bls] } ? true : false) 297 | end 298 | 299 | # Prepping material here for use in other functions since this is always 300 | # called first. 301 | def exists? 302 | require 'openssl' 303 | 304 | @property_hash[:id] = OpenSSL::Digest.new('SHA256').digest(name).unpack1('H*') 305 | 306 | @property_hash[:target] = resource[:target] if resource[:target] 307 | @property_hash[:add_defaults_on_creation] = resource[:add_defaults_on_creation] 308 | @property_hash[:classes] ||= resource[:classes] || [] 309 | 310 | @property_hash[:bls] = @bls_system && (@property_hash[:bls] || (@property_hash[:bls].nil? && (resource[:bls].nil? || resource[:bls]))) 311 | 312 | # BLS and non-BLS systems must be treated differently 313 | if @property_hash[:bls] || resource[:bls] 314 | @property_hash[:args] ||= [] 315 | else 316 | @property_hash[:load_16bit] ||= resource[:load_16bit].nil? ? true : resource[:load_16bit] 317 | @property_hash[:load_video] ||= resource[:load_video].nil? ? true : resource[:load_video] 318 | @property_hash[:plugins] ||= resource[:plugins].nil? ? %w[gzio part_msdos xfs ext2] : resource[:plugins] 319 | end 320 | 321 | retval = @property_hash[:name] == resource[:name] 322 | unless resource[:bls].nil? 323 | retval &&= (resource[:bls] == @property_hash[:bls]) 324 | @property_hash[:bls] = resource[:bls] 325 | end 326 | 327 | retval 328 | end 329 | 330 | def create 331 | # Input Validation 332 | raise Puppet::Error, '`root` is a required property' if !bls && !resource[:root] 333 | raise Puppet::Error, '`kernel` is a required property' unless resource[:kernel] 334 | raise Puppet::Error, '`initrd` is a required parameter' unless resource[:modules] || resource[:initrd] 335 | 336 | # End Validation 337 | 338 | @property_hash[:create_output] = true 339 | @property_hash[:users] = resource[:users] || [] 340 | @property_hash[:initrd] = get_initrd('', resource[:initrd]) 341 | @property_hash[:kernel] = get_kernel('', resource[:kernel]) 342 | @property_hash[:kernel_options] = get_kernel_options([], resource[:kernel_options])[:new_kernel_options] 343 | @property_hash[:modules] = get_module_options([], resource[:modules])[:new_module_options] 344 | @property_hash[:default_entry] = resource[:default_entry] 345 | 346 | if bls 347 | @property_hash[:classes] = resource[:classes] || ['kernel'] 348 | else 349 | @property_hash[:classes] = resource[:classes] || [] 350 | @property_hash[:root] = resource[:root] 351 | @property_hash[:load_16bit] = resource[:load_16bit] 352 | @property_hash[:load_video] = resource[:load_video] 353 | @property_hash[:plugins] ||= resource[:plugins].nil? ? %w[gzio part_msdos xfs ext2] : resource[:plugins] 354 | end 355 | end 356 | 357 | def destroy 358 | @property_hash[:destroy] = true 359 | end 360 | 361 | def classes=(newval) 362 | @property_hash[:classes] = newval 363 | end 364 | 365 | def users=(newval) 366 | @property_hash[:users] = newval 367 | end 368 | 369 | def load_16bit=(newval) 370 | @property_hash[:load_16bit] = newval 371 | end 372 | 373 | def load_video 374 | bls ? resource[:load_video] : @property_hash[:load_video] 375 | end 376 | 377 | def load_video=(newval) 378 | @property_hash[:load_video] = newval 379 | end 380 | 381 | def plugins 382 | bls ? resource[:plugins] : @property_hash[:plugins] 383 | end 384 | 385 | def plugins=(newval) 386 | @property_hash[:plugins] = newval 387 | end 388 | 389 | def root 390 | bls ? resource[:root] : @property_hash[:root] 391 | end 392 | 393 | def root=(newval) 394 | @property_hash[:root] = newval 395 | end 396 | 397 | def kernel?(is, should) 398 | return true unless should 399 | 400 | @new_kernel = get_kernel(is, should) 401 | 402 | is == @new_kernel 403 | end 404 | 405 | def kernel=(_newval) 406 | @property_hash[:kernel] = @new_kernel 407 | end 408 | 409 | def kernel_options?(is, should) 410 | kernel_options = get_kernel_options(is, should) 411 | 412 | @new_kernel_options = kernel_options[:new_kernel_options] 413 | kernel_options[:old_kernel_options] == @new_kernel_options 414 | end 415 | 416 | def kernel_options=(newval) 417 | @new_kernel_options = Array(newval) if @new_kernel_options.empty? 418 | 419 | default_kernel_options = @default_entry[:kernel_options] 420 | if resource[:modules] 421 | # We don't want to pick up any default kernel options if this is a multiboot entry. 422 | default_kernel_options = {} 423 | end 424 | 425 | @new_kernel_options = PuppetX::AugeasprovidersGrub::Util.munged_options([], @new_kernel_options, @default_entry[:kernel], default_kernel_options) 426 | 427 | @property_hash[:kernel_options] = @new_kernel_options.join(' ') 428 | end 429 | 430 | def modules?(is, should) 431 | module_options = get_module_options(is, should) 432 | 433 | @new_module_options = module_options[:new_module_options] 434 | 435 | module_options[:old_module_options] == @new_module_options 436 | end 437 | 438 | def get_module_options(is, should, default_entry = @default_entry) 439 | new_module_options = [] 440 | old_module_options = [] 441 | 442 | i = 0 443 | Array(should).each do |module_set| 444 | module_set << ':defaults:' if @property_hash[:create_output] && @property_hash[:add_defaults_on_creation] && module_set.include?(':preserve:') 445 | 446 | current_val = Array(Array(is)[i]) 447 | new_module_options << PuppetX::AugeasprovidersGrub::Util.munged_options(current_val, module_set, default_entry[:kernel], default_entry[:kernel_options], true) 448 | 449 | old_module_options << PuppetX::AugeasprovidersGrub::Util.munged_options(current_val, ':preserve:', default_entry[:kernel], default_entry[:kernel_options], true) unless current_val.empty? 450 | 451 | i += 1 452 | end 453 | 454 | { new_module_options: new_module_options, old_module_options: old_module_options } 455 | end 456 | 457 | def modules=(newval) 458 | new_module_options = @new_module_options 459 | 460 | if Array(new_module_options).empty? 461 | new_module_options = [] 462 | Array(newval).each do |module_set| 463 | new_module_options << PuppetX::AugeasprovidersGrub::Util.munged_options([], module_set, @default_entry[:kernel], @default_entry[:kernel_options]) 464 | end 465 | end 466 | 467 | @property_hash[:modules] = new_module_options 468 | end 469 | 470 | def initrd?(is, should) 471 | return true unless should 472 | 473 | @new_initrd = get_initrd(is, should) 474 | 475 | is == @new_initrd 476 | end 477 | 478 | def initrd=(_newval) 479 | @property_hash[:initrd] = @new_initrd 480 | end 481 | 482 | def flush 483 | super 484 | 485 | require 'puppetx/augeasproviders_grub/util' 486 | 487 | @property_hash[:name] = name 488 | 489 | if @property_hash[:kernel] =~ %r{(\d.+)} 490 | bls_version = Regexp.last_match(1) 491 | else 492 | bls_version = @property_hash[:kernel].dup 493 | bls_version.delete('/') 494 | end 495 | 496 | bls_target = @property_hash[:bls_target] || 497 | File.join('', 498 | 'boot', 499 | 'loader', 500 | 'entries', 501 | "#{@property_hash[:id][0...32]}-#{bls_version}.conf") 502 | 503 | legacy_target = @property_hash[:target] || 504 | "/etc/grub.d/05_puppet_managed_#{@property_hash[:id].upcase[0...10]}" 505 | 506 | output = [] 507 | 508 | if bls 509 | if @property_hash[:destroy] 510 | FileUtils.rm_f(property_hash[:bls_target]) 511 | else 512 | 513 | @property_hash[:target] = if @property_hash[:bls_target] && !@property_hash[:target] 514 | @property_hash[:bls_target] 515 | else 516 | bls_target 517 | end 518 | 519 | output = construct_bls_entry(@property_hash, bls_version) 520 | 521 | File.open(@property_hash[:target], 'w') do |fh| 522 | fh.puts(output.join("\n")) 523 | fh.flush 524 | end 525 | 526 | FileUtils.chmod(0o644, @property_hash[:target]) 527 | 528 | if File.exist?(legacy_target) 529 | FileUtils.rm_f(legacy_target) 530 | 531 | # Need to rebuild the full grub config if we removed a legacy target 532 | PuppetX::AugeasprovidersGrub::Util.grub2_mkconfig(mkconfig) 533 | end 534 | end 535 | elsif @property_hash[:destroy] 536 | FileUtils.rm_f(legacy_target) 537 | return 538 | else 539 | @property_hash[:load_16bit] = resource[:load_16bit] 540 | @property_hash[:add_defaults_on_creation] = resource[:add_defaults_on_creation] 541 | 542 | if !@property_hash[:create_output] && !File.exist?(legacy_target) 543 | # If we have a modification request, but the target file does not exist, 544 | # this means that the entry was picked up from something that is not 545 | # Puppet managed and is an error. 546 | raise Puppet::Error, 'Cannot modify a stock system resource; please change your resource :name' 547 | end 548 | 549 | output = construct_grub2cfg_entry(@property_hash) 550 | 551 | File.open(legacy_target, 'w') do |fh| 552 | fh.puts(output.join("\n")) 553 | fh.flush 554 | end 555 | 556 | FileUtils.chmod(0o755, legacy_target) 557 | 558 | # Need to remove the BLS config if we moved it to be legacy 559 | FileUtils.rm_f(bls_target) if File.exist?(bls_target) 560 | 561 | PuppetX::AugeasprovidersGrub::Util.grub2_mkconfig(mkconfig) 562 | end 563 | 564 | grub_set_default (Array(@property_hash[:submenus]) + Array(@property_hash[:name])).compact.join('>').to_s if @property_hash[:default_entry] 565 | end 566 | 567 | private 568 | 569 | def get_kernel(is, should, grubby_info = @grubby_info) 570 | new_kernel = should 571 | 572 | if new_kernel == ':preserve:' 573 | new_kernel = is 574 | new_kernel = ':default:' if !new_kernel || new_kernel.empty? 575 | end 576 | 577 | new_kernel = PuppetX::AugeasprovidersGrub::Util.munge_grubby_value(new_kernel, 'kernel', grubby_info) if new_kernel == ':default:' 578 | 579 | raise Puppet::Error, 'Could not find a valid kernel value to set' unless new_kernel 580 | 581 | new_kernel 582 | end 583 | 584 | def get_kernel_options(is, should, default_entry = @default_entry) 585 | should << ':defaults:' if @property_hash[:create_output] && @property_hash[:add_defaults_on_creation] && should.include?(':preserve:') 586 | 587 | default_kernel_options = default_entry[:kernel_options] 588 | if resource[:modules] 589 | # We don't want to pick up any default kernel options if this is a multiboot entry. 590 | default_kernel_options = {} 591 | end 592 | 593 | new_kernel_options = PuppetX::AugeasprovidersGrub::Util.munged_options(is, should, default_entry[:kernel], default_kernel_options) 594 | 595 | old_kernel_options = PuppetX::AugeasprovidersGrub::Util.munged_options(is, ':preserve:', default_entry[:kernel], default_entry[:kernel_options]) 596 | 597 | { new_kernel_options: new_kernel_options, old_kernel_options: old_kernel_options } 598 | end 599 | 600 | def get_initrd(is, should, grubby_info = @grubby_info) 601 | new_initrd = should 602 | 603 | if new_initrd == ':preserve:' 604 | new_initrd = is 605 | new_initrd = ':default:' if !new_initrd || new_initrd.empty? 606 | end 607 | 608 | new_initrd = PuppetX::AugeasprovidersGrub::Util.munge_grubby_value(new_initrd, 'initrd', grubby_info) if new_initrd == ':default:' 609 | 610 | raise Puppet::Error, 'Could not find a valid initrd value to set' unless new_initrd 611 | 612 | new_initrd 613 | end 614 | 615 | def construct_bls_entry(property_hash, bls_version, header_comment = '### PUPPET MANAGED ###') 616 | output = [] 617 | 618 | output << header_comment 619 | output << "title #{property_hash[:name]}" 620 | output << "version #{bls_version}" 621 | output << "linux #{property_hash[:kernel]}" 622 | output << "initrd #{property_hash[:initrd]}" 623 | output << "options #{Array(property_hash[:kernel_options]).join(' ')}" 624 | output << "id #{property_hash[:id]}-#{bls_version}" 625 | 626 | if property_hash[:users].include?('unrestricted') || property_hash[:users].include?(:unrestricted) 627 | property_hash[:users].delete(:unrestricted) 628 | property_hash[:users].delete('unrestricted') 629 | 630 | output << 'grub_arg --unrestricted' 631 | end 632 | 633 | output << if property_hash[:users].empty? 634 | 'grub_users $grub_users' 635 | else 636 | "grub_users #{property_hash[:users].join(' ')}" 637 | end 638 | 639 | property_hash[:args].each do |arg| 640 | output << "grub_arg #{arg}" 641 | end 642 | 643 | property_hash[:classes].each do |cls| 644 | output << "grub_class #{cls}" 645 | end 646 | 647 | output 648 | end 649 | 650 | def construct_grub2cfg_entry(_property_hash, header_comment = '### PUPPET MANAGED ###') 651 | output = [] 652 | 653 | output << '#!/bin/sh' 654 | 655 | # We use this to determine if we're puppet managed or not 656 | output << header_comment 657 | output << 'exec tail -n +3 $0' 658 | 659 | # Build the main menuentry line 660 | menuentry_line = ["menuentry '#{@property_hash[:name]}'"] 661 | 662 | menuentry_line << @property_hash[:classes].map { |x| "--class #{x}" }.join(' ') 663 | 664 | unless @property_hash[:users].empty? 665 | menuentry_line << if @property_hash[:users].include?('unrestricted') || @property_hash[:users].include?(:unrestricted) 666 | '--unrestricted' 667 | else 668 | "--users #{@property_hash[:users].join(',')}" 669 | end 670 | end 671 | 672 | menuentry_line << '{' 673 | 674 | output << menuentry_line.join(' ') 675 | # Main menuentry line complete 676 | 677 | output << ' load_video' if @property_hash[:load_video] 678 | 679 | output += @property_hash[:plugins].map { |x| %( insmod #{x}) } if @property_hash[:plugins] 680 | 681 | output << %( set root='#{@property_hash[:root]}') 682 | 683 | # Build the main kernel line 684 | kernel_line = [] 685 | 686 | # If we have modules defined, we're in multiboot mode 687 | if @property_hash[:modules] && !@property_hash[:modules].empty? 688 | output << ' insmod multiboot2' if File.exist?('/sys/firmware/efi') 689 | 690 | kernel_line << ' multiboot' 691 | else 692 | kernel_line << if @property_hash[:load_16bit] 693 | ' linux16' 694 | else 695 | ' linux' 696 | end 697 | end 698 | 699 | kernel_line << @property_hash[:kernel] 700 | kernel_line << @property_hash[:kernel_options] 701 | 702 | output << kernel_line.flatten.compact.join(' ') 703 | 704 | if @property_hash[:modules].empty? 705 | output << if @property_hash[:load_16bit] 706 | %( initrd16 #{@property_hash[:initrd]}) 707 | else 708 | %( initrd #{@property_hash[:initrd]}) 709 | end 710 | else 711 | @property_hash[:modules].each do |mod| 712 | output << %( module #{mod.compact.join(' ')}) 713 | end 714 | end 715 | 716 | output << '}' 717 | 718 | output 719 | end 720 | end 721 | --------------------------------------------------------------------------------