diff --git a/README.md b/README.md index bc51c475..7b934fd2 100644 --- a/README.md +++ b/README.md @@ -462,6 +462,56 @@ yum::install { 'package-name': Please note that resource name must be same as installed package name. +### Manage DNF modules streams + +> When changing from one enabled stream to another one, the provider runs `dnf module switch-to `, which replaces all installed profiles from the DNF module. Bear the consequences in mind. + +Enable default stream + +```puppet +dnf_module_stream { '': + stream => default, +} +``` + +Keep current enabled stream - if there isn't, enable default one + +```puppet +dnf_module_stream { '': + stream => present, +} +``` + +Enable a specific stream + +```puppet +dnf_module_stream { '': + stream => , +} +``` + +Disable stream (reset module) + +```puppet +dnf_module_stream { '': + stream => absent, +} +``` + +#### `dnf_module_stream` resource versus `dnfmodule` provider + +[DNF modules](https://dnf.readthedocs.io/en/latest/modularity.html) is a feature from `yum` successor, `dnf`, which allows easier and more robust selections of software versions and collections. + +As of Aug 22, 2023, [core Puppet `package` resource `dnfmodule` provider](https://www.puppet.com/docs/puppet/8/types/package.html#package-provider-dnfmodule) has some support for managing streams and profiles, but it has some issues: + +1. Setting stream is mandatory when (un)installing profiles - No way of just keeping currently enabled stream +1. It only supports installing a single profile, despite the fact `dnf` supports multi-profile installations and there are use cases for that +1. Managing two things - streams setting and profile (un)installation - in the same resource invocation is inherently messy + +One can fix 1 and 2, and add good docs to deal with 3. A compelling reason not to keep 1 and 3 is that a stream is a setting, not something one (un)installs. This makes it unsuitable for the `package` resource which, in principle, should only (un)install stuff. + +So, while one fix 2, this custom resource aims to fully and better replace `dnfmodule` provider stream support. + ### Puppet tasks The module has a puppet task that allows to run `yum update` or `yum upgrade`. diff --git a/REFERENCE.md b/REFERENCE.md index f766a026..b561298b 100644 --- a/REFERENCE.md +++ b/REFERENCE.md @@ -23,6 +23,10 @@ * [`yum::post_transaction_action`](#yum--post_transaction_action): Creates post transaction configuratons for dnf or yum. * [`yum::versionlock`](#yum--versionlock): Locks package from updates. +### Resource types + +* [`dnf_module_stream`](#dnf_module_stream): Manage DNF module streams + ### Functions * [`yum::bool2num_hash_recursive`](#yum--bool2num_hash_recursive): This functions converts the Boolean values of a Hash to Integers, either '0' or '1'. It does this recursively, decending as far as the langu @@ -825,6 +829,77 @@ Epoch of the package if CentOS 8 mechanism is used. Default value: `0` +## Resource types + +### `dnf_module_stream` + +This type allows Puppet to enable/disable streams via DNF modules + +#### Examples + +##### Enable MariaDB default stream + +```puppet +dnf_module_stream { 'mariadb': + stream => default, +} +``` + +##### Enable MariaDB 10.5 stream + +```puppet +dnf_module_stream { 'mariadb': + stream => '10.5', +} +``` + +##### Disable MariaDB streams + +```puppet +dnf_module_stream { 'mariadb': + stream => absent, +} +``` + +#### Properties + +The following properties are available in the `dnf_module_stream` type. + +##### `stream` + +Valid values: `present`, `default`, `absent`, `%r{.+}` + + Module stream that should be enabled +String - Specify stream +present - Keep current enabled stream if any, otherwise enable default one +default - Enable default stream +absent - No stream (resets module) + +#### Parameters + +The following parameters are available in the `dnf_module_stream` type. + +* [`module`](#-dnf_module_stream--module) +* [`provider`](#-dnf_module_stream--provider) +* [`title`](#-dnf_module_stream--title) + +##### `module` + +Valid values: `%r{.+}` + +DNF module to be managed + +##### `provider` + +The specific backend to use for this `dnf_module_stream` resource. You will seldom need to specify this --- Puppet will +usually discover the appropriate provider for your platform. + +##### `title` + +Valid values: `%r{.+}` + +Resource title + ## Functions ### `yum::bool2num_hash_recursive` diff --git a/lib/puppet/provider/dnf_module_stream/dnf_module_stream.rb b/lib/puppet/provider/dnf_module_stream/dnf_module_stream.rb new file mode 100644 index 00000000..0b83feb1 --- /dev/null +++ b/lib/puppet/provider/dnf_module_stream/dnf_module_stream.rb @@ -0,0 +1,100 @@ +# frozen_string_literal: true + +Puppet::Type.type(:dnf_module_stream).provide(:dnf_module_stream) do + desc 'Unique provider' + + confine package_provider: 'dnf' + + commands dnf: 'dnf' + + # Converts plain output from 'dnf module list ' to an array formatted as: + # { + # default_stream: " (if there's one)", + # enabled_stream: " (if there's one)", + # available_streams: ["", "", ...,] + # } + def dnf_output_2_hash(dnf_output) + module_hash = { available_streams: [] } + dnf_output.lines.each do |line| + line.chomp! + break if line.empty? + + # @stream_start and @stream_length: chunk of dnf output line with stream info + # Determined in elsif block below from dnf output header + if !@stream_start.nil? + # Stream string is '', ' [d][e]', or the like + stream_string = line[@stream_start, @stream_length].rstrip + stream = stream_string.split[0] + module_hash[:default_stream] = stream if stream_string.include?('[d]') + module_hash[:enabled_stream] = stream if stream_string.include?('[e]') + module_hash[:available_streams] << stream + elsif line.split[0] == 'Name' + # 'dnf module list' output header is 'NameStreamProfiles...' + # Each field has same position of data that follows + @stream_start = line[%r{Name\s+}].length + @stream_length = line[%r{Stream\s+}].length + end + end + module_hash + end + + # Gets module default, enabled and available streams + # Output formatted by function dnf_output_2_hash + def streams_state(module_name) + # This function can be called multiple times in the same resource call + return unless @streams_current_state.nil? + + dnf_output = dnf('-q', 'module', 'list', module_name) + rescue Puppet::ExecutionFailure + # Assumes any execution error happens because module doesn't exist + raise ArgumentError, "Module \"#{module_name}\" not found" + else + @streams_current_state = dnf_output_2_hash(dnf_output) + end + + def disable_stream(module_name) + dnf('-y', 'module', 'reset', module_name) + end + + def enable_stream(module_name, target_stream) + action = @streams_current_state.key?(:enabled_stream) ? 'switch-to' : 'enable' + dnf('-y', 'module', action, "#{module_name}:#{target_stream}") + end + + def stream + streams_state(resource[:module]) + case resource[:stream] + when :absent + # Act if any stream is enabled + @streams_current_state.key?(:enabled_stream) ? @streams_current_state[:enabled_stream] : :absent + when :default + # Act if default stream isn't enabled + # Specified stream = :default requires an existing default stream + raise ArgumentError, "No default stream to enable in module \"#{resource[:module]}\"" unless + @streams_current_state.key?(:default_stream) + + @streams_current_state[:enabled_stream] == @streams_current_state[:default_stream] ? :default : @streams_current_state[:enabled_stream] + when :present + # Act if no stream is enabled + # Specified stream = :default requires an existing default or enabled stream + raise ArgumentError, "No default stream to enable in module \"#{resource[:module]}\"" unless + @streams_current_state.key?(:default_stream) || @streams_current_state.key?(:enabled_stream) + + @streams_current_state.key?(:enabled_stream) ? :present : :absent + else + # Act if specified stream isn't enabled + @streams_current_state[:enabled_stream] + end + end + + def stream=(target_stream) + case target_stream + when :absent + disable_stream(resource[:module]) + when :default, :present + enable_stream(resource[:module], @streams_current_state[:default_stream]) + else + enable_stream(resource[:module], target_stream) + end + end +end diff --git a/lib/puppet/type/dnf_module_stream.rb b/lib/puppet/type/dnf_module_stream.rb new file mode 100644 index 00000000..18bdf015 --- /dev/null +++ b/lib/puppet/type/dnf_module_stream.rb @@ -0,0 +1,46 @@ +# frozen_string_literal: true + +Puppet::Type.newtype(:dnf_module_stream) do + @doc = <<-TYPE_DOC + @summary Manage DNF module streams + @example Enable MariaDB default stream + dnf_module_stream { 'mariadb': + stream => default, + } + @example Enable MariaDB 10.5 stream + dnf_module_stream { 'mariadb': + stream => '10.5', + } + @example Disable MariaDB streams + dnf_module_stream { 'mariadb': + stream => absent, + } + @param module + Module to be managed - Defaults to title + @param stream + Module stream to be enabled + + This type allows Puppet to enable/disable streams via DNF modules + TYPE_DOC + + newparam(:title, namevar: true) do + desc 'Resource title' + newvalues(%r{.+}) + end + + newparam(:module) do + desc 'DNF module to be managed' + newvalues(%r{.+}) + end + + newproperty(:stream) do + desc <<-EOS + Module stream that should be enabled + String - Specify stream + present - Keep current enabled stream if any, otherwise enable default one + default - Enable default stream + absent - No stream (resets module) + EOS + newvalues(:present, :default, :absent, %r{.+}) + end +end diff --git a/spec/acceptance/post_transaction_actions_spec.rb b/spec/acceptance/post_transaction_actions_spec.rb index df6d9ff1..59b0e530 100644 --- a/spec/acceptance/post_transaction_actions_spec.rb +++ b/spec/acceptance/post_transaction_actions_spec.rb @@ -4,6 +4,15 @@ describe 'yum::post_transaction_action define' do context 'simple parameters' do + let(:pre_condition) do + " + package{ 'vim-enhanced-absent': + name => 'vim-enhanced', + ensure => 'absent', + } + " + end + # Using puppet_apply as a helper it 'must work idempotently with no errors' do pp = <<-EOS diff --git a/spec/unit/puppet/provider/dnf_module_stream/dnf_module_stream_spec.rb b/spec/unit/puppet/provider/dnf_module_stream/dnf_module_stream_spec.rb new file mode 100644 index 00000000..7da94d76 --- /dev/null +++ b/spec/unit/puppet/provider/dnf_module_stream/dnf_module_stream_spec.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'the dnf_module_stream provider' do + it 'loads' do + expect(Puppet::Type.type(:dnf_module_stream).provide(:dnf_module_stream)).not_to be_nil + end +end diff --git a/spec/unit/puppet/type/dnf_module_stream_spec.rb b/spec/unit/puppet/type/dnf_module_stream_spec.rb new file mode 100644 index 00000000..a6500468 --- /dev/null +++ b/spec/unit/puppet/type/dnf_module_stream_spec.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +require 'spec_helper' + +dnf_module_stream = Puppet::Type.type(:dnf_module_stream) +RSpec.describe 'the dnf_module_stream type' do + it 'loads' do + expect(dnf_module_stream).not_to be_nil + end + + it 'has parameter module' do + expect(dnf_module_stream.parameters).to be_include(:module) + end + + it 'has property stream' do + expect(dnf_module_stream.properties.map(&:name)).to be_include(:stream) + end +end