Skip to content

Commit

Permalink
Merge pull request #320 from EmersonPrado/issue_310
Browse files Browse the repository at this point in the history
Support dnf module management - Fix #310
  • Loading branch information
bastelfreak committed Apr 4, 2024
2 parents aa0bfce + 49e8b42 commit 56edfc9
Show file tree
Hide file tree
Showing 7 changed files with 307 additions and 0 deletions.
50 changes: 50 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <Stream>`, which replaces all installed profiles from the DNF module. Bear the consequences in mind.
Enable default stream

```puppet
dnf_module_stream { '<Module>':
stream => default,
}
```

Keep current enabled stream - if there isn't, enable default one

```puppet
dnf_module_stream { '<Module>':
stream => present,
}
```

Enable a specific stream

```puppet
dnf_module_stream { '<Module>':
stream => <Stream name>,
}
```

Disable stream (reset module)

```puppet
dnf_module_stream { '<Module>':
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`.
Expand Down
75 changes: 75 additions & 0 deletions REFERENCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -825,6 +829,77 @@ Epoch of the package if CentOS 8 mechanism is used.

Default value: `0`

## Resource types

### <a name="dnf_module_stream"></a>`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)

##### <a name="-dnf_module_stream--module"></a>`module`

Valid values: `%r{.+}`

DNF module to be managed

##### <a name="-dnf_module_stream--provider"></a>`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.

##### <a name="-dnf_module_stream--title"></a>`title`

Valid values: `%r{.+}`

Resource title

## Functions

### <a name="yum--bool2num_hash_recursive"></a>`yum::bool2num_hash_recursive`
Expand Down
100 changes: 100 additions & 0 deletions lib/puppet/provider/dnf_module_stream/dnf_module_stream.rb
Original file line number Diff line number Diff line change
@@ -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 <Module>' to an array formatted as:
# {
# default_stream: "<Default stream> (if there's one)",
# enabled_stream: "<Enabled stream> (if there's one)",
# available_streams: ["<Stream>", "<Stream>", ...,]
# }
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 '<Stream>', '<Stream> [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 'Name<Spaces>Stream<Spaces>Profiles<Spaces>...'
# 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
46 changes: 46 additions & 0 deletions lib/puppet/type/dnf_module_stream.rb
Original file line number Diff line number Diff line change
@@ -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
9 changes: 9 additions & 0 deletions spec/acceptance/post_transaction_actions_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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
18 changes: 18 additions & 0 deletions spec/unit/puppet/type/dnf_module_stream_spec.rb
Original file line number Diff line number Diff line change
@@ -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

0 comments on commit 56edfc9

Please sign in to comment.