Implement sysfs values management resource

Change-Id: I84cd799630a5b930f7e22cad735205d122fa0fb1
Closes-Bug: 1456587
Signed-off-by: Sergii Golovatiuk <sgolovatiuk@mirantis.com>
This commit is contained in:
Dmitry Ilyin 2015-05-19 22:11:25 +03:00
parent 47491e08d6
commit 5cde6f1668
14 changed files with 693 additions and 0 deletions

View File

@ -30,3 +30,20 @@ class { 'openstack::keepalive' :
tcpka_intvl => '3',
tcp_retries2 => '5',
}
# increase network backlog for performance on fast networks
sysctl::value { 'net.core.netdev_max_backlog': value => '261144' }
L2_port<||> -> Sysfs_config_value<||>
L3_ifconfig<||> -> Sysfs_config_value<||>
L3_route<||> -> Sysfs_config_value<||>
class { 'sysfs' :}
sysfs_config_value { 'rps_cpus' :
ensure => 'present',
name => "/etc/sysfs.d/rps_cpus.conf",
value => cpu_affinity_hex($::processorcount),
sysfs => '/sys/class/net/*/queues/rx-*/rps_cpus',
exclude => '/sys/class/net/lo/*',
}

View File

@ -30,4 +30,32 @@ class NetconfigPostTest < Test::Unit::TestCase
assert TestCommon::Network.ping?(ip), "Cannot ping the master node '#{ip}'!"
end
def processor_count
File.read('/proc/cpuinfo').split("\n").count { |line| line.start_with? 'processor' }
end
def hex_mask
return @hex_mask if @hex_mask
@hex_mask = ((2 ** processor_count) -1 ).to_s(16)
end
def rps_cpus
Dir.glob('/sys/class/net/*/queues/rx-*/rps_cpus').reject { |node| node.start_with? '/sys/class/net/lo' }
end
def test_rps_cpus_set
rps_cpus.each do |node|
assert File.read(node).chomp.end_with?(hex_mask), "Sysfs node: '#{node}' is not '#{hex_mask}'!"
end
end
def test_rps_cpus_config
assert File.exists?('/etc/sysfs.d/rps_cpus.conf'), 'RPS_CPUS sysfs config is missing!'
rps_cpus.each do |line|
line.gsub! %r(/sys/), ''
line = "#{line} = #{hex_mask}"
assert TestCommon::Config.has_line?('/etc/sysfs.d/rps_cpus.conf', line), "Line '#{line}' is missing in the rps_cpus.conf!"
end
end
end

View File

@ -0,0 +1,80 @@
#!/bin/bash
#
# sysfs Apply sysfs values from the config files
#
# Based on Debian 'sysfsutils' init script
#
# chkconfig: 345 15 85
# description: Sets sysfs values from the config file to the system on boot
### BEGIN INIT INFO
# Short-Description: Apply sysfs values from the config files
# Description: Apply sysfs values from the config files
### END INIT INFO
. '/etc/init.d/functions'
if [ -f '/etc/sysconfig/sysfs' ]; then
. '/etc/sysconfig/sysfs'
fi
if [ -z "${CONFIG_FILE}" ]; then
CONFIG_FILE='/etc/sysfs.conf'
fi
if [ -z "${CONFIG_DIR}" ]; then
CONFIG_DIR='/etc/sysfs.d'
fi
load_conffile() {
FILE="$1"
echo "Load sysfs file: ${FILE}"
sed 's/#.*$//; /^[[:space:]]*$/d;
s/^[[:space:]]*\([^=[:space:]]*\)[[:space:]]*\([^=[:space:]]*\)[[:space:]]*=[[:space:]]*\(.*\)/\1 \2 \3/' \
"${FILE}" | {
while read f1 f2 f3; do
if [ "$f1" = "mode" -a -n "$f2" -a -n "$f3" ]; then
if [ -f "/sys/$f2" ] || [ -d "/sys/$f2" ]; then
chmod "$f3" "/sys/$f2"
else
failure "unknown attribute $f2"
fi
elif [ "$f1" = "owner" -a -n "$f2" -a -n "$f3" ]; then
if [ -f "/sys/$f2" ]; then
chown "$f3" "/sys/$f2"
else
failure "unknown attribute $f2"
fi
elif [ "$f1" -a -n "$f2" -a -z "$f3" ]; then
if [ -f "/sys/$f1" ]; then
# Some fields need a terminating newline, others
# need the terminating newline to be absent :-(
echo -n "$f2" > "/sys/$f1" 2>/dev/null ||
echo "$f2" > "/sys/$f1"
else
echo "unknown attribute $f1"
fi
else
failure "syntax error: '$f1' '$f2' '$f3'"
exit 1
fi
done
}
}
######################################################################
case "$1" in
start|restart|reload)
echo "Settings sysfs values..."
for file in ${CONFIG_FILE} ${CONFIG_DIR}/*.conf; do
[ -r "${file}" ] || continue
load_conffile "${file}"
done
;;
stop)
exit 0
;;
*)
echo "Usage: $0 {start|stop|restart|reload}"
exit 2
esac

View File

@ -0,0 +1,10 @@
module Puppet::Parser::Functions
newfunction(:cpu_affinity_hex, :type => :rvalue, :doc => <<-EOS
Generate a HEX value used to set network device rsp_cpus value
EOS
) do |argv|
number = argv[0].to_i
fail "Argument should be the CPU number - integer value!" unless number.to_s == argv[0]
((2 ** number) - 1).to_s(16)
end
end

View File

@ -0,0 +1,140 @@
require 'fileutils'
require 'digest/md5'
Puppet::Type.type(:sysfs_config_value).provide(:ruby) do
def glob(path)
Dir.glob path
end
def included_sysfs_nodes
return @included_sysfs_nodes if @included_sysfs_nodes
return [] unless @resource[:sysfs]
@included_sysfs_nodes = []
return @included_sysfs_nodes unless @resource[:sysfs]
@resource[:sysfs].each do |path|
@included_sysfs_nodes += glob path
end
@included_sysfs_nodes
end
def excluded_sysfs_nodes
return @excluded_sysfs_nodes if @excluded_sysfs_nodes
return [] unless @resource[:exclude]
@excluded_sysfs_nodes = []
return @excluded_sysfs_nodes unless @resource[:exclude]
@resource[:exclude].each do |path|
@excluded_sysfs_nodes += glob path
end
@excluded_sysfs_nodes
end
def sysfs_nodes
return @sysfs_nodes if @sysfs_nodes
@sysfs_nodes = included_sysfs_nodes.reject do |included_node|
excluded_sysfs_nodes.find do |excluded_node|
included_node.start_with? excluded_node
end
end
end
def sysfs_node_value(node)
value = @resource[:value]
if @resource[:value].is_a? Hash
value = @resource[:value].fetch 'default', nil
@resource[:value].each do |override_node, override_value|
if node.include? override_node
value = override_value
break
end
end
end
value
end
def generate_file_content
return unless @resource.generate_content?
content = ''
sysfs_nodes.each do |node|
node = node.gsub %r(^/sys/), ''
content += "#{node} = #{sysfs_node_value node}\n"
end
debug 'Generated config content'
@resource[:content] = content
end
def reset
@included_sysfs_nodes = nil
@excluded_sysfs_nodes = nil
@sysfs_nodes = nil
end
######################################################
def file_name
@resource[:name]
end
def file_base_dir
File.dirname file_name
end
def file_mkdir
FileUtils.mkdir_p file_base_dir unless File.directory? file_base_dir
fail "Could not create the base directory for file: '#{file_name}'" unless File.directory? file_base_dir
end
def file_write(value)
File.open(file_name, 'w') do |f|
f.write value
end
fail "Error writing file: '#{file_name}'!" unless file_read == value
end
def file_exists?
File.file? file_name
end
def file_remove
File.delete file_name if file_exists?
end
def file_read
return unless file_exists?
File.read file_name
end
######################################################
def exists?
debug 'Call: exists?'
generate_file_content
out = file_exists?
debug "Return: '#{out}'"
out
end
def create
debug 'Call: create'
file_mkdir
file_write @resource[:content]
end
def destroy
debug 'Call: destroy'
file_remove
end
def content
debug 'Call: content'
out = file_read
debug "Return: '(md5)#{Digest::MD5.hexdigest out}'"
out
end
def content=(value)
debug "Call: content='(md5)#{Digest::MD5.hexdigest value}'"
file_write value
end
end

View File

@ -0,0 +1,87 @@
require 'digest/md5'
Puppet::Type.newtype(:sysfs_config_value, :doc => <<-eos
This resource creates a single file in the sysfs config directory. The content
of this files can either be passed directly by the "content" property the same
way as usual "file" resource works or it can be autogenerated.
For example, it you want to set the scheduler of all your SSDs to "noop" you
can specify "/sys/block/sd*/queue/scheduler" as the "sysfs" property and "noop"
as the "value" property. The configuration file will contain:
block/sda/queue/scheduler = noop
block/sdb/queue/scheduler = noop
The globulation will be opened to match every drive and the value will be set
for every line.
You can exclude sysfs path elements by specifying "sdb" to the "exclude"
parameter to exclude matching lines from the file.
If you need to pass different values for different lines you can pass a hash
to the "value" parameter instead of a string:
value => { 'sdb' => 'deadline', 'default' => 'noop' }
The matching lines will be set to their values and the "default" values will be
used for the lines that have not matched any of the hash keys.
Parameters "sysfs" and "exclude" can actually accept several patterns as an
array, but, perhaps, it would be better to use several instances of this
resources if you need to set a lot of different values.
eos
) do
ensurable
newparam :name, :namevar => true do
desc 'The path to the config file'
end
newparam :sysfs do
desc 'Path to the SysFS nodes to be updated'
munge do |value|
break unless value
break value if value.is_a? Array
[value]
end
end
newparam :exclude do
desc 'Path to the SysFS nodes to be excluded'
munge do |value|
break unless value
break value if value.is_a? Array
[value]
end
end
newparam :value do
desc 'Set the SysFS nodes to this value'
validate do |value|
fail "The value should be either a string or a hash!" unless value.is_a? String or value.is_a? Hash
end
end
newproperty :content do
desc 'Content of the config file. Should be autogenerated unless overriden.'
def should_to_s(value)
"(md5)#{Digest::MD5.hexdigest value}"
end
def is_to_s(value)
"(md5)#{Digest::MD5.hexdigest value}"
end
end
def generate_content?
return false if self[:content]
!self[:sysfs].nil? and !self[:value].nil?
end
def validate
fail 'You should privide either "sysfs" and "value" to generate content or the "content" itself!' unless self[:content] or generate_content?
end
end

View File

@ -0,0 +1,12 @@
# == Class: sysfs
#
# This module manages Linux sysfs values using sysfsutils init script which
# can take values stored in /etc/sysfs.conf and /etc/sysfs.d/*.conf snipplets.
#
class sysfs {
class { 'sysfs::install' :}
class { 'sysfs::service' :}
Class['sysfs::install'] ~> Class['sysfs::service']
}

View File

@ -0,0 +1,38 @@
# == Class: sysfs::install
#
# This class installs the sysfsutils packages and prepares the config directory
#
class sysfs::install inherits sysfs::params {
File {
owner => 'root',
group => 'root',
mode => '0755',
}
#TODO: should be moved to the fuel-library package or sysfsutils package
if $::osfamily == 'RedHat' {
file { 'sysfsutils.init' :
ensure => 'present',
name => "/etc/init.d/${service}",
source => 'puppet:///modules/sysfs/centos-sysfsutils.init.sh',
}
}
package { 'sysfsutils' :
ensure => 'installed',
name => $package,
}
tweaks::ubuntu_service_override { 'sysfsutils' :
package_name => $package,
}
file { 'sysfs.d' :
ensure => 'directory',
name => $config_dir,
}
Class['sysfs::install'] -> Sysfs_config_value <||>
}

View File

@ -0,0 +1,5 @@
class sysfs::params {
$package = 'sysfsutils'
$service = 'sysfsutils'
$config_dir = '/etc/sysfs.d'
}

View File

@ -0,0 +1,15 @@
# == Class: sysfs::service
#
# This class actually enables and runs the sysfsutils service to
# apply any configuration found in the config files
#
class sysfs::service inherits sysfs::params {
service { 'sysfsutils' :
ensure => 'running',
enable => true,
hasstatus => false,
hasrestart => true,
}
Sysfs_config_value <||> ~> Service['sysfsutils']
}

View File

@ -0,0 +1,33 @@
require 'spec_helper'
describe 'the cpu_affinity_hex function' do
let(:scope) { PuppetlabsSpec::PuppetInternals.scope }
it 'should exist' do
expect(
Puppet::Parser::Functions.function('cpu_affinity_hex')
).to eq('function_cpu_affinity_hex')
end
it 'should calculate HEX affinity value' do
expect(
scope.function_cpu_affinity_hex(%w(12))
).to eq 'fff'
expect(
scope.function_cpu_affinity_hex(%w(2))
).to eq '3'
end
it 'should raise an error if there is less than 1 arguments' do
expect {
scope.function_cpu_affinity_hexs([])
}.to raise_error
end
it 'should raise an error if value is not integer' do
expect {
scope.function_cpu_affinity_hex(%w(abc))
}.to raise_error
end
end

View File

@ -0,0 +1,7 @@
require 'puppetlabs_spec_helper/module_spec_helper'
RSpec.configure do |config|
config.mock_with :rspec do |mock|
mock.syntax = :expect
end
end

View File

@ -0,0 +1,167 @@
require 'spec_helper'
describe Puppet::Type.type(:sysfs_config_value).provider(:ruby) do
let(:resource) do
Puppet::Type.type(:sysfs_config_value).new(
:name => '/etc/sysfs.d/scheduler.conf',
:sysfs => '/sys/block/sd*/queue/scheduler',
:exclude => '/sys/block/sdc/queue/scheduler',
:value => 'noop',
)
end
let(:provider) do
provider = resource.provider
if ENV['SPEC_PUPPET_DEBUG']
class << provider
def debug(str)
puts str
end
end
end
provider
end
subject { provider }
before(:each) do
allow(subject).to receive(:file_mkdir)
allow(subject).to receive(:file_write)
allow(subject).to receive(:file_exists?)
allow(subject).to receive(:file_remove)
allow(subject).to receive(:file_read)
end
it 'should exist' do
expect(subject).to be_a Puppet::Provider
end
context 'ensurable' do
it 'should check if file exists' do
expect(subject).to receive(:file_exists?).and_return(true)
expect(subject.exists?).to be true
end
it 'can create a file and parent directory' do
resource[:content] = '123'
expect(subject).to receive(:file_write).with('123')
expect(subject).to receive(:file_mkdir)
expect(subject.file_base_dir).to eq '/etc/sysfs.d'
subject.create
end
it 'can remove a file' do
expect(subject).to receive(:file_remove)
subject.destroy
end
end
context 'content' do
it 'can read file content' do
expect(subject).to receive(:file_read).and_return('123')
expect(subject.content).to eq '123'
end
it 'can write file content' do
expect(subject).to receive(:file_write).with('123')
subject.content = '123'
end
end
context 'content generation' do
before(:each) do
allow(subject).to receive(:glob).with('/sys/block/sd*/queue/scheduler').and_return %w(
/sys/block/sda/queue/scheduler
/sys/block/sdb/queue/scheduler
/sys/block/sdc/queue/scheduler
)
allow(subject).to receive(:glob).with('/sys/block/sda/queue/scheduler').and_return %w(
/sys/block/sda/queue/scheduler
)
allow(subject).to receive(:glob).with('/sys/block/sdb/queue/scheduler').and_return %w(
/sys/block/sdb/queue/scheduler
)
allow(subject).to receive(:glob).with('/sys/block/sdc/queue/scheduler').and_return %w(
/sys/block/sdc/queue/scheduler
)
end
it 'can get a list of sysfs nodes using "sysfs" and "exclude"' do
expect(subject.sysfs_nodes).to match_array %w(
/sys/block/sda/queue/scheduler
/sys/block/sdb/queue/scheduler
)
end
it 'can get a sysfs node value either as a string or as a hash' do
resource[:value] = '123'
expect(subject.sysfs_node_value '/sys/block/sda/queue/scheduler').to eq '123'
resource[:value] = { 'sda' => '234' }
expect(subject.sysfs_node_value '/sys/block/sda/queue/scheduler').to eq '234'
resource[:value] = { 'default' => '345' }
expect(subject.sysfs_node_value '/sys/block/sda/queue/scheduler').to eq '345'
end
it 'can generate new config file content' do
expect(resource).to receive(:generate_content?).and_return(true)
resource[:content] = ''
subject.generate_file_content
expect(resource[:content]).to eq <<-eos
block/sda/queue/scheduler = noop
block/sdb/queue/scheduler = noop
eos
end
it 'can use values provided as a hash' do
expect(resource).to receive(:generate_content?).and_return(true)
resource[:content] = ''
resource[:value] = { 'sdb' => 'deadline', 'default' => 'noop' }
subject.generate_file_content
expect(resource[:content]).to eq <<-eos
block/sda/queue/scheduler = noop
block/sdb/queue/scheduler = deadline
eos
end
it 'can use and array of "sysfs" values' do
expect(resource).to receive(:generate_content?).and_return(true)
resource[:content] = ''
resource[:sysfs] = %w(
/sys/block/sda/queue/scheduler
/sys/block/sdb/queue/scheduler
)
subject.generate_file_content
expect(resource[:content]).to eq <<-eos
block/sda/queue/scheduler = noop
block/sdb/queue/scheduler = noop
eos
end
it 'can use and array of "exclude" values' do
expect(resource).to receive(:generate_content?).and_return(true)
resource[:content] = ''
resource[:sysfs] = %w(
/sys/block/sda/queue/scheduler
/sys/block/sdb/queue/scheduler
/sys/block/sdc/queue/scheduler
)
resource[:exclude] = %w(
/sys/block/sdc/queue/scheduler
)
subject.generate_file_content
expect(resource[:content]).to eq <<-eos
block/sda/queue/scheduler = noop
block/sdb/queue/scheduler = noop
eos
end
it 'will not generate content if the type tells not to' do
expect(resource).to receive(:generate_content?).and_return(false)
resource[:content] = ''
subject.generate_file_content
expect(resource[:content]).to eq ''
end
end
end

View File

@ -0,0 +1,54 @@
require 'spec_helper'
describe Puppet::Type.type(:sysfs_config_value) do
subject do
Puppet::Type.type(:sysfs_config_value).new(
:name => '/etc/sysfs.d/scheduler.conf',
:sysfs => '/sys/block/sd*/queue/scheduler',
:value => 'noop',
)
end
it 'should exist' do
expect(subject).to be_a Puppet::Type
end
[:name, :sysfs, :exclude, :value, :content].each do |param|
it "should have '#{param}' parameter" do
expect { subject[param] }.not_to raise_error
end
end
it 'should permit the new content generation if there is no content and sysfs and value are present' do
expect(subject.generate_content?).to be true
subject[:content] = 'test'
expect(subject.generate_content?).to be false
end
[:sysfs, :exclude].each do |param|
it "should convert '#{param}' from a string to an array" do
subject[param] = '123'
expect(subject[param]).to eq ['123']
end
it "should pass '#{param}' array values as is" do
subject[param] = ['123']
expect(subject[param]).to eq ['123']
end
end
it 'should accept "value" only as a string or a hash' do
expect {
subject[:value] = ['123']
}.to raise_error
end
it 'should not allow to use the resource without either content or sysfs and value' do
expect {
Puppet::Type.type(:sysfs_config_value).new(
:name => '/etc/sysfs/scheduler.conf',
)
}.to raise_error
end
end