From 9c76b6af1200c71e7ae72e0e2f349919a3081738 Mon Sep 17 00:00:00 2001 From: Chris Price Date: Sat, 28 Jul 2012 21:59:54 -0700 Subject: First (basic) working version of ini_setting provider --- lib/puppet/provider/ini_setting/ruby.rb | 19 +++ lib/puppet/type/ini_setting.rb | 26 +++ lib/puppet/util/external_iterator.rb | 24 +++ lib/puppet/util/ini_file.rb | 132 +++++++++++++++ lib/puppet/util/ini_file/section.rb | 34 ++++ spec/spec_helper.rb | 8 +- spec/unit/puppet/provider/ini_setting/ruby_spec.rb | 186 ++++++--------------- spec/unit/puppet/util/ini_file_spec.rb | 47 ++++++ 8 files changed, 341 insertions(+), 135 deletions(-) create mode 100644 lib/puppet/util/external_iterator.rb create mode 100644 lib/puppet/util/ini_file.rb create mode 100644 lib/puppet/util/ini_file/section.rb create mode 100644 spec/unit/puppet/util/ini_file_spec.rb diff --git a/lib/puppet/provider/ini_setting/ruby.rb b/lib/puppet/provider/ini_setting/ruby.rb index e69de29..e44a107 100644 --- a/lib/puppet/provider/ini_setting/ruby.rb +++ b/lib/puppet/provider/ini_setting/ruby.rb @@ -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 diff --git a/lib/puppet/type/ini_setting.rb b/lib/puppet/type/ini_setting.rb index fb7f6e2..9af47c1 100644 --- a/lib/puppet/type/ini_setting.rb +++ b/lib/puppet/type/ini_setting.rb @@ -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 \ No newline at end of file diff --git a/lib/puppet/util/external_iterator.rb b/lib/puppet/util/external_iterator.rb new file mode 100644 index 0000000..67b3375 --- /dev/null +++ b/lib/puppet/util/external_iterator.rb @@ -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 diff --git a/lib/puppet/util/ini_file.rb b/lib/puppet/util/ini_file.rb new file mode 100644 index 0000000..75d8a9f --- /dev/null +++ b/lib/puppet/util/ini_file.rb @@ -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 \ No newline at end of file diff --git a/lib/puppet/util/ini_file/section.rb b/lib/puppet/util/ini_file/section.rb new file mode 100644 index 0000000..39f2959 --- /dev/null +++ b/lib/puppet/util/ini_file/section.rb @@ -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 \ No newline at end of file diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 3ded441..ddbcd6e 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -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' + diff --git a/spec/unit/puppet/provider/ini_setting/ruby_spec.rb b/spec/unit/puppet/provider/ini_setting/ruby_spec.rb index 7eb4c6d..91d3050 100644 --- a/spec/unit/puppet/provider/ini_setting/ruby_spec.rb +++ b/spec/unit/puppet/provider/ini_setting/ruby_spec.rb @@ -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 diff --git a/spec/unit/puppet/util/ini_file_spec.rb b/spec/unit/puppet/util/ini_file_spec.rb new file mode 100644 index 0000000..7e7458a --- /dev/null +++ b/spec/unit/puppet/util/ini_file_spec.rb @@ -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 \ No newline at end of file -- cgit v1.2.3