First (basic) working version of ini_setting provider

This commit is contained in:
Chris Price 2012-07-28 21:59:54 -07:00
parent 91273a5a2b
commit 9c76b6af12
8 changed files with 341 additions and 135 deletions

View file

@ -0,0 +1,19 @@
require 'puppet/util/ini_file'
Puppet::Type.type(:ini_setting).provide(:ruby) do
def exists?
ini_file.get_value(resource[:section], resource[:setting]) == resource[:value]
end
def create
ini_file.set_value(resource[:section], resource[:setting], resource[:value])
ini_file.save
@ini_file = nil
end
private
def ini_file
@ini_file ||= Puppet::Util::IniFile.new(resource[:path])
end
end

View file

@ -4,4 +4,30 @@ Puppet::Type.newtype(:ini_setting) do
defaultvalues
defaultto :present
end
newparam(:name, :namevar => true) do
desc 'An arbitrary name used as the identity of the resource.'
end
newparam(:section) do
desc 'The name of the section in the ini file in which the setting should be defined.'
end
newparam(:setting) do
desc 'The name of the setting to be defined.'
end
newparam(:value) do
desc 'The value of the setting to be defined.'
end
newparam(:path) do
desc 'The ini file Puppet will ensure contains the specified setting.'
validate do |value|
unless (Puppet.features.posix? and value =~ /^\//) or (Puppet.features.microsoft_windows? and (value =~ /^.:\// or value =~ /^\/\/[^\/]+\/[^\/]+/))
raise(Puppet::Error, "File paths must be fully qualified, not '#{value}'")
end
end
end
end

View file

@ -0,0 +1,24 @@
module Puppet
module Util
class ExternalIterator
def initialize(coll)
@coll = coll
@cur_index = 0
end
def next
@cur_index = @cur_index + 1
item_at(@cur_index)
end
def peek
item_at(@cur_index + 1)
end
private
def item_at(index)
[@coll[index], index]
end
end
end
end

132
lib/puppet/util/ini_file.rb Normal file
View file

@ -0,0 +1,132 @@
require 'puppet/util/external_iterator'
require 'puppet/util/ini_file/section'
module Puppet
module Util
class IniFile
SECTION_REGEX = /^\s*\[([\w\d\.]+)\]\s*$/
SETTING_REGEX = /^\s*([\w\d\.]+)\s*=\s*([\w\d\.]+)\s*$/
def initialize(path)
@path = path
@section_names = []
@sections_hash = {}
parse_file
end
def section_names
@section_names
end
def get_value(section_name, setting)
if (@sections_hash.has_key?(section_name))
@sections_hash[section_name].get_value(setting)
end
end
def set_value(section_name, setting, value)
unless (@sections_hash.has_key?(section_name))
add_section(Section.new(section_name, nil, nil, nil))
end
section = @sections_hash[section_name]
if (section.has_existing_setting?(setting))
update_line(section, setting, value)
section.update_existing_setting(setting, value)
else
section.set_additional_setting(setting, value)
end
end
def save
File.open(@path, 'w') do |fh|
first_section = @sections_hash[@section_names[0]]
(0..first_section.start_line - 1).each do |line_num|
fh.puts(lines[line_num])
end
@section_names.each do |name|
section = @sections_hash[name]
if (section.start_line.nil?)
fh.puts("\n[#{section.name}]")
else
(section.start_line..section.end_line).each do |line_num|
fh.puts(lines[line_num])
end
end
section.additional_settings.each_pair do |key, value|
fh.puts("#{key} = #{value}")
end
end
end
end
private
def add_section(section)
@sections_hash[section.name] = section
@section_names << section.name
end
def parse_file
line_iter = create_line_iter
line, line_num = line_iter.next
while line
if (match = SECTION_REGEX.match(line))
section = read_section(match[1], line_num, line_iter)
add_section(section)
end
line, line_num = line_iter.next
end
end
def read_section(name, start_line, line_iter)
settings = {}
while true
line, line_num = line_iter.peek
if (line.nil? or match = SECTION_REGEX.match(line))
return Section.new(name, start_line, line_num - 1, settings)
elsif (match = SETTING_REGEX.match(line))
settings[match[1]] = match[2]
end
line_iter.next
end
end
def update_line(section, setting, value)
(section.start_line..section.end_line).each do |line_num|
if (match = SETTING_REGEX.match(lines[line_num]))
if (match[1] == setting)
lines[line_num] = "#{setting} = #{value}"
end
end
end
end
def create_line_iter
ExternalIterator.new(lines)
end
def lines
@lines ||= IniFile.readlines(@path)
end
# This is mostly here because it makes testing easier--we don't have
# to try to stub any methods on File.
def self.readlines(path)
# If this type is ever used with very large files, we should
# write this in a different way, using a temp
# file; for now assuming that this type is only used on
# small-ish config files that can fit into memory without
# too much trouble.
File.readlines(path)
end
end
end
end

View file

@ -0,0 +1,34 @@
module Puppet
module Util
class IniFile
class Section
def initialize(name, start_line, end_line, settings)
@name = name
@start_line = start_line
@end_line = end_line
@existing_settings = settings.nil? ? {} : settings
@additional_settings = {}
end
attr_reader :name, :start_line, :end_line, :additional_settings
def get_value(setting_name)
@existing_settings[setting_name] || @additional_settings[setting_name]
end
def has_existing_setting?(setting_name)
@existing_settings.has_key?(setting_name)
end
def update_existing_setting(setting_name, value)
@existing_settings[setting_name] = value
end
def set_additional_setting(setting_name, value)
@additional_settings[setting_name] = value
end
end
end
end
end

View file

@ -1,4 +1,8 @@
dir = File.expand_path(File.dirname(__FILE__))
$LOAD_PATH.unshift File.join(dir, 'lib')
gem 'rspec', '>=2.0.0'
require 'rspec/expectations'
require 'puppetlabs_spec_helper/puppetlabs_spec_helper'
require 'puppetlabs_spec_helper/puppetlabs_spec/files'

View file

@ -5,7 +5,6 @@ provider_class = Puppet::Type.type(:ini_setting).provider(:ruby)
describe provider_class do
include PuppetlabsSpec::Files
let(:tmpfile) { tmpfilename("ini_setting_test") }
let(:orig_content) {
<<-EOS
@ -38,20 +37,15 @@ baz=bazvalue
context "when ensuring that a setting is present" do
let(:common_params) { {
:title => 'ini_setting_ensure_present_test',
:file => tmpfile,
:path => tmpfile,
:section => 'section2',
} }
it "should add a missing setting to the correct section" do
puts "common params (#{common_params.class}:"
require 'pp'
pp common_params
resource = Puppet::Type::Ini_setting.new(common_params.merge(
:setting => 'yahoo', :value => 'yippee'))
puts "parse title..."
pp resource.parse_title
provider = described_class.new(resource)
provider.exists?.should be_nil
provider.exists?.should == false
provider.create
validate_file(<<-EOS
# This is a comment
@ -72,133 +66,59 @@ yahoo = yippee
end
it "should modify an existing setting with a different value" do
fail
resource = Puppet::Type::Ini_setting.new(common_params.merge(
:setting => 'baz', :value => 'bazvalue2'))
provider = described_class.new(resource)
provider.exists?.should == false
provider.create
validate_file(<<-EOS
# This is a comment
[section1]
; This is also a comment
foo=foovalue
bar = barvalue
[section2]
foo= foovalue2
baz = bazvalue2
#another comment
; yet another comment
EOS
)
end
it "should recognize an existing setting with the specified value and leave it intact" do
fail
it "should recognize an existing setting with the specified value" do
resource = Puppet::Type::Ini_setting.new(common_params.merge(
:setting => 'baz', :value => 'bazvalue'))
provider = described_class.new(resource)
provider.exists?.should == true
end
it "should add a new section if the section does not exist" do
resource = Puppet::Type::Ini_setting.new(common_params.merge(
:section => "section3", :setting => 'huzzah', :value => 'shazaam'))
provider = described_class.new(resource)
provider.exists?.should == false
provider.create
validate_file(<<-EOS
# This is a comment
[section1]
; This is also a comment
foo=foovalue
bar = barvalue
[section2]
foo= foovalue2
baz=bazvalue
#another comment
; yet another comment
[section3]
huzzah = shazaam
EOS
)
end
end
#it "should pass" do
# File.read(@tmpfile).should == orig_content
#end
#context "when adding" do
# before :each do
# #tmp = tmpfilename
# #
# #@resource = Puppet::Type::File_line.new(
# # {:name => 'foo', :path => @tmpfile, :line => 'foo'}
# #)
# #@provider = provider_class.new(@resource)
# end
# it 'should detect if the line exists in the file' do
# File.open(@tmpfile, 'w') do |fh|
# fh.write('foo')
# end
# @provider.exists?.should be_true
# end
# it 'should detect if the line does not exist in the file' do
# File.open(@tmpfile, 'w') do |fh|
# fh.write('foo1')
# end
# @provider.exists?.should be_nil
# end
# it 'should append to an existing file when creating' do
# @provider.create
# File.read(@tmpfile).chomp.should == 'foo'
# end
#end
#
#context "when matching" do
# before :each do
# # TODO: these should be ported over to use the PuppetLabs spec_helper
# # file fixtures once the following pull request has been merged:
# # https://github.com/puppetlabs/puppetlabs-stdlib/pull/73/files
# tmp = Tempfile.new('tmp')
# @tmpfile = tmp.path
# tmp.close!
# @resource = Puppet::Type::File_line.new(
# {
# :name => 'foo',
# :path => @tmpfile,
# :line => 'foo = bar',
# :match => '^foo\s*=.*$',
# }
# )
# @provider = provider_class.new(@resource)
# end
#
# it 'should raise an error if more than one line matches, and should not have modified the file' do
# File.open(@tmpfile, 'w') do |fh|
# fh.write("foo1\nfoo=blah\nfoo2\nfoo=baz")
# end
# @provider.exists?.should be_nil
# expect { @provider.create }.to raise_error(Puppet::Error, /More than one line.*matches/)
# File.read(@tmpfile).should eql("foo1\nfoo=blah\nfoo2\nfoo=baz")
# end
#
# it 'should replace a line that matches' do
# File.open(@tmpfile, 'w') do |fh|
# fh.write("foo1\nfoo=blah\nfoo2")
# end
# @provider.exists?.should be_nil
# @provider.create
# File.read(@tmpfile).chomp.should eql("foo1\nfoo = bar\nfoo2")
# end
# it 'should add a new line if no lines match' do
# File.open(@tmpfile, 'w') do |fh|
# fh.write("foo1\nfoo2")
# end
# @provider.exists?.should be_nil
# @provider.create
# File.read(@tmpfile).should eql("foo1\nfoo2\nfoo = bar\n")
# end
# it 'should do nothing if the exact line already exists' do
# File.open(@tmpfile, 'w') do |fh|
# fh.write("foo1\nfoo = bar\nfoo2")
# end
# @provider.exists?.should be_true
# @provider.create
# File.read(@tmpfile).chomp.should eql("foo1\nfoo = bar\nfoo2")
# end
#end
#
#context "when removing" do
# before :each do
# # TODO: these should be ported over to use the PuppetLabs spec_helper
# # file fixtures once the following pull request has been merged:
# # https://github.com/puppetlabs/puppetlabs-stdlib/pull/73/files
# tmp = Tempfile.new('tmp')
# @tmpfile = tmp.path
# tmp.close!
# @resource = Puppet::Type::File_line.new(
# {:name => 'foo', :path => @tmpfile, :line => 'foo', :ensure => 'absent' }
# )
# @provider = provider_class.new(@resource)
# end
# it 'should remove the line if it exists' do
# File.open(@tmpfile, 'w') do |fh|
# fh.write("foo1\nfoo\nfoo2")
# end
# @provider.destroy
# File.read(@tmpfile).should eql("foo1\nfoo2")
# end
#
# it 'should remove the line without touching the last new line' do
# File.open(@tmpfile, 'w') do |fh|
# fh.write("foo1\nfoo\nfoo2\n")
# end
# @provider.destroy
# File.read(@tmpfile).should eql("foo1\nfoo2\n")
# end
#
# it 'should remove any occurence of the line' do
# File.open(@tmpfile, 'w') do |fh|
# fh.write("foo1\nfoo\nfoo2\nfoo\nfoo")
# end
# @provider.destroy
# File.read(@tmpfile).should eql("foo1\nfoo2\n")
# end
#end
end

View file

@ -0,0 +1,47 @@
require 'spec_helper'
require 'puppet/util/ini_file'
describe Puppet::Util::IniFile do
context "when parsing a file" do
let(:subject) { Puppet::Util::IniFile.new("/my/ini/file/path") }
let(:sample_content) {
template = <<-EOS
# This is a comment
[section1]
; This is also a comment
foo=foovalue
bar = barvalue
[section2]
foo= foovalue2
baz=bazvalue
#another comment
; yet another comment
EOS
template.split("\n")
}
before :each do
described_class.should_receive(:readlines).once.with("/my/ini/file/path") do
sample_content
end
end
it "should parse the correct number of sections" do
subject.section_names.length.should == 2
end
it "should parse the correct section_names" do
subject.section_names.should == ["section1", "section2"]
end
it "should expose settings for sections" do
subject.get_value("section1", "foo").should == "foovalue"
subject.get_value("section1", "bar").should == "barvalue"
subject.get_value("section2", "foo").should == "foovalue2"
subject.get_value("section2", "baz").should == "bazvalue"
end
end
end