diff --git a/lib/puppet/provider/service_config/svccfg.rb b/lib/puppet/provider/service_config/svccfg.rb new file mode 100644 index 0000000..70bb265 --- /dev/null +++ b/lib/puppet/provider/service_config/svccfg.rb @@ -0,0 +1,54 @@ +require 'strscan' +Puppet::Type.type(:service_config).provide(:svccfg) do + desc "Manages smf configuration with svccfg" + + commands :svccfg => '/usr/sbin/svccfg' + + defaultfor :operatingsystem => :solaris + + def ensure + result = [:absent] + svccfg('-s', resource[:fmri], :listprop, resource[:prop]).each_line do |line| + next if /^\s*$/.match(line) # ignore empty lines + next if /^\s*#/.match(line) # ignore comments + name, type, value = line.chomp.split(/\s+/,3) + scanner = StringScanner.new(value) + result = [] + while !scanner.eos? + scanner.skip(/\s+/) + # TODO: This will not work if the value itself contains escaped + # characters such as \" + if token = scanner.scan(/".*?"|\S+/) + token.gsub!(/"(.*)"/, '\1') + result << token + else + raise Puppet::Error, "Unable to parse value #{value}" + end + end + break + end + result + end + + def ensure=(new_value) + new_value = [new_value] unless new_value.is_a? Array + if new_value == [:absent] + svccfg('-s', resource[:fmri], :delprop, resource[:prop]) + else + quoted_values = case resource[:type] + when :astring + new_value.map {|s| "\"#{s}\"" } + else + new_value + end + argument = if quoted_values.size == 1 + quoted_values.first + else + "(#{quoted_values.join(' ')})" + end + + svccfg('-s', resource[:fmri], :setprop, resource[:prop], '=', "#{resource[:type]}:", argument) + end + end + +end diff --git a/lib/puppet/type/service_config.rb b/lib/puppet/type/service_config.rb new file mode 100644 index 0000000..35079e9 --- /dev/null +++ b/lib/puppet/type/service_config.rb @@ -0,0 +1,104 @@ +module Puppet + newtype(:service_config) do + + @doc = "Manages smf configuration on Solaris 11. + + Solaris 11 and OpenSolaris have moved a lot of configuration aspects + from plaintext files to SMF configuration. The `service_config` type + can be used to set these options with puppet. One serive config is + identified by a service identifier (FMRI) and a property name. + + The title of a `service_config` resource can either be the combination + of a fmri and a property name in the format `:` or can + be just the property name. In the latter case you have to provider + the fmri as a resource parameter. + + E.g. to set the nameserver of your DNS client to `10.0.0.1` and + `10.0.0.2` you can either write + + service_config { 'network/dns/client:config/nameserver': + ensure => [ '10.0.0.1', '10.0.0.2' ], + type => net_address, + } + + or you can write + + service_config { 'config/nameserver': + ensure => [ '10.0.0.1', '10.0.0.2' ], + fmri => 'network/dns/client', + type => net_address, + } + + Be aware that in both cases the resource title has to be unique. + + Valid values to `ensure` are either a single value, an array or the + value `absent` when you want to make sure the specified property is + absent." + + def self.title_patterns + [ + # pattern to parse : + [ + /^(.*):(.*)$/, + [ + [:fmri, lambda{|x| x} ], + [:prop, lambda{|x| x} ] + ] + ], + # pattern to parse + [ + /^(.*)$/, + [ + [:prop, lambda{|x| x}] + ] + ] + ] + end + + def name + # I am not sure if puppet relies on the resource having a name. + # In general the name is the value of the namevar of the resource + # Because we use multiple namevars (fmri and prop) this is not going + # to work, so I overwrite the method here. The type may as well work + # without this method but you knows... + "#{self[:fmri]}:#{self[:prop]}" + end + + newparam(:fmri) do + desc "The name of the service you want to configure, e.g. + `svc:/system/keymap:default`" + + isnamevar + end + + newparam(:prop) do + desc "The name of the property you want to configure, e.g. + `keymap/layout`" + + isnamevar + end + + newparam(:type) do + desc "The type of the property. This is important when changing a setting" + + newvalues :astring + newvalues :boolean + newvalues :integer, :count, :time + newvalues :net_address, :net_address_v4, :net_address_v6 + end + + newproperty(:ensure, :array_matching => :all) do + desc "The desired value of the property. You can either specify a + single value, an array, or the special string `absent`, if you want + to remove a property" + + newvalues :absent + newvalues /.*/ + + def insync?(is) + is == @should + end + end + + end +end diff --git a/spec/integration/provider/service_config/svccfg_spec.rb b/spec/integration/provider/service_config/svccfg_spec.rb new file mode 100644 index 0000000..cc4171b --- /dev/null +++ b/spec/integration/provider/service_config/svccfg_spec.rb @@ -0,0 +1,148 @@ +#! /usr/bin/env ruby + +require 'spec_helper' + +describe Puppet::Type.type(:service_config).provider(:svccfg), '(integration)' do + + before :each do + described_class.stubs(:suitable?).returns true + end + + let :fmri do + 'svc:/network/dns/client' + end + + let :prop do + 'config/search' + end + + let :default_options do + { + :title => "#{fmri}:#{prop}", + :fmri => fmri, + :prop => prop, + :type => :astring + } + end + + let :resource_singlevalue do + Puppet::Type.type(:service_config).new(default_options.merge(:ensure => 'example.com')) + end + + let :resource_listone do + Puppet::Type.type(:service_config).new(default_options.merge(:ensure => ['test.com'])) + end + + let :resource_listthree do + Puppet::Type.type(:service_config).new(default_options.merge(:ensure => ['example.com', 'example.de', 'test.com'])) + end + + let :resource_absent do + Puppet::Type.type(:service_config).new(default_options.merge(:ensure => :absent)) + end + + def run_in_catalog(resource) + catalog = Puppet::Resource::Catalog.new + catalog.host_config = false + resource.expects(:err).never + catalog.add_resource resource + catalog.apply + end + + describe "ensure is a single value" do + it "should do nothing if value is in sync" do + resource_singlevalue.provider.expects(:svccfg).with('-s', fmri, :listprop, prop).returns("config/search astring example.com\n") + resource_singlevalue.provider.expects(:svccfg).with('-s', fmri, :setprop, prop, '=', 'astring:', '"example.com"').never + run_in_catalog(resource_singlevalue) + end + + it "should create the property if currently absent" do + resource_singlevalue.provider.expects(:svccfg).with('-s', fmri, :listprop, prop).returns("\n\n") + resource_singlevalue.provider.expects(:svccfg).with('-s', fmri, :setprop, prop, '=', 'astring:', '"example.com"') + run_in_catalog(resource_singlevalue) + end + + it "should replace a single value" do + resource_singlevalue.provider.expects(:svccfg).with('-s', fmri, :listprop, prop).returns("config/search astring wrong.com\n") + resource_singlevalue.provider.expects(:svccfg).with('-s', fmri, :setprop, prop, '=', 'astring:', '"example.com"') + run_in_catalog(resource_singlevalue) + end + + it "should replace a list of values" do + resource_singlevalue.provider.expects(:svccfg).with('-s', fmri, :listprop, prop).returns("config/search astring \"example.com\" \"wrong.com\"\n") + resource_singlevalue.provider.expects(:svccfg).with('-s', fmri, :setprop, prop, '=', 'astring:', '"example.com"') + run_in_catalog(resource_singlevalue) + end + end + + describe "ensure is a list of values with one element" do + it "should do nothing if value is in sync" do + resource_listone.provider.expects(:svccfg).with('-s', fmri, :listprop, prop).returns("config/search astring test.com\n") + resource_listone.provider.expects(:svccfg).with('-s', fmri, :setprop, prop, '=', 'astring:', '"test.com"').never + run_in_catalog(resource_listone) + end + + it "should create the property if currently absent" do + resource_listone.provider.expects(:svccfg).with('-s', fmri, :listprop, prop).returns("\n\n") + resource_listone.provider.expects(:svccfg).with('-s', fmri, :setprop, prop, '=', 'astring:', '"test.com"') + run_in_catalog(resource_listone) + end + + it "should replace a single value" do + resource_listone.provider.expects(:svccfg).with('-s', fmri, :listprop, prop).returns("config/search astring wrong.com\n") + resource_listone.provider.expects(:svccfg).with('-s', fmri, :setprop, prop, '=', 'astring:', '"test.com"') + run_in_catalog(resource_listone) + end + + it "should replace a list of values" do + resource_listone.provider.expects(:svccfg).with('-s', fmri, :listprop, prop).returns("config/search astring \"example.com\" \"wrong.com\"\n") + resource_listone.provider.expects(:svccfg).with('-s', fmri, :setprop, prop, '=', 'astring:', '"test.com"') + run_in_catalog(resource_listone) + end + end + + describe "ensure is a list of values with more than one element" do + it "should do nothing if value is in sync" do + resource_listthree.provider.expects(:svccfg).with('-s', fmri, :listprop, prop).returns("config/search astring \"example.com\" \"example.de\" \"test.com\"\n") + resource_listthree.provider.expects(:svccfg).with('-s', fmri, :setprop, prop, '=', 'astring:', '("example.com" "example.de" "test.com")').never + run_in_catalog(resource_listthree) + end + + it "should create the property if currently absent" do + resource_listthree.provider.expects(:svccfg).with('-s', fmri, :listprop, prop).returns("\n\n") + resource_listthree.provider.expects(:svccfg).with('-s', fmri, :setprop, prop, '=', 'astring:', '("example.com" "example.de" "test.com")') + run_in_catalog(resource_listthree) + end + + it "should replace a single value" do + resource_listthree.provider.expects(:svccfg).with('-s', fmri, :listprop, prop).returns("config/search astring wrong.com\n") + resource_listthree.provider.expects(:svccfg).with('-s', fmri, :setprop, prop, '=', 'astring:', '("example.com" "example.de" "test.com")') + run_in_catalog(resource_listthree) + end + + it "should replace a list of values" do + resource_listthree.provider.expects(:svccfg).with('-s', fmri, :listprop, prop).returns("config/search astring \"example.com\" \"test.com\" \"example.de\"\n") + resource_listthree.provider.expects(:svccfg).with('-s', fmri, :setprop, prop, '=', 'astring:', '("example.com" "example.de" "test.com")') + run_in_catalog(resource_listthree) + end + end + + describe "ensure is absent" do + it "should to nothing if property is already absent" do + resource_absent.provider.expects(:svccfg).with('-s', fmri, :listprop, prop).returns("\n\n") + resource_absent.provider.expects(:svccfg).never + run_in_catalog(resource_absent) + end + it "should remove the property if it has a single value" do + resource_absent.provider.expects(:svccfg).with('-s', fmri, :listprop, prop).returns("config/search astring wrong.com\n") + resource_absent.provider.expects(:svccfg).with('-s', fmri, :delprop, prop) + run_in_catalog(resource_absent) + end + + it "should remove the property if it has a list of values" do + resource_absent.provider.expects(:svccfg).with('-s', fmri, :listprop, prop).returns("config/search astring \"example.com\" \"test.com\" \"example.de\"\n") + resource_absent.provider.expects(:svccfg).with('-s', fmri, :delprop, prop) + run_in_catalog(resource_absent) + end + end +end diff --git a/spec/unit/provider/service_config/svccfg_spec.rb b/spec/unit/provider/service_config/svccfg_spec.rb new file mode 100755 index 0000000..0ad4843 --- /dev/null +++ b/spec/unit/provider/service_config/svccfg_spec.rb @@ -0,0 +1,149 @@ +#!/usr/bin/env ruby + +require 'spec_helper' + +describe Puppet::Type.type(:service_config).provider(:svccfg) do + + let :provider do + described_class.new + end + + let :fmri do + 'svc:/system/keymap:default' + end + + let :prop do + 'keymap/layout' + end + + let :title do + {:title => "#{fmri}:#{prop}", :fmri => fmri, :prop => prop} + end + + describe "#ensure" do + before :each do + Puppet::Type.type(:service_config).new(title.merge(:ensure => 'German', :type => :astring, :provider => provider)) + end + + it "should use svccfg to return the current value" do + provider.expects(:svccfg).with('-s', 'svc:/system/keymap:default', :listprop, 'keymap/layout').returns 'keymap/layout astring German' + provider.ensure + end + + it "should get a string value" do + provider.stubs(:svccfg).returns 'keymap/layout astring German' + provider.ensure.should == ['German'] + end + + it "should get a quoted string value" do + provider.stubs(:svccfg).returns 'config/network astring "nis [NOTFOUND=return] files"' + provider.ensure.should == ['nis [NOTFOUND=return] files'] + end + + it "should get a list of strings" do + provider.stubs(:svccfg).returns 'config/search astring "example.com" "test.com"' + provider.ensure.should == ['example.com', 'test.com'] + end + + it "should get a numeric value" do + provider.stubs(:svccfg).returns 'keymap/kbd_beeper_freq integer 2000' + provider.ensure.should == ['2000'] + end + + it "should get a decimal value" do + provider.stubs(:svccfg).returns 'restarter/start_method_timestamp time 1358352122.695415000' + provider.ensure.should == ['1358352122.695415000'] + end + + it "should get a single address" do + provider.stubs(:svccfg).returns 'config/nameserver net_address 192.168.0.1' + provider.ensure.should == ['192.168.0.1'] + end + + it "should get a list of addresses" do + provider.stubs(:svccfg).returns 'config/nameserver net_address 192.168.0.1 10.0.0.1' + provider.ensure.should == ['192.168.0.1', '10.0.0.1'] + end + + it "should get :absent when property is not present" do + provider.stubs(:svccfg).returns "\n\n" + provider.ensure.should == [:absent] + end + end + + describe "#ensure=" do + + describe "and type is astring" do + before :each do + Puppet::Type.type(:service_config).new(title.merge(:type => :astring, :ensure => 'old value', :provider => provider)) + end + + it "should remove the property" do + provider.expects(:svccfg).with('-s', fmri, :delprop, prop) + provider.ensure = [:absent] + end + + it "should set a single string" do + provider.expects(:svccfg).with('-s', fmri, :setprop, prop, '=', 'astring:', '"foo"') + provider.ensure = ['foo'] + end + + it "should set a single string with spaces" do + provider.expects(:svccfg).with('-s', fmri, :setprop, prop, '=', 'astring:', %q{"foo bar"}) + provider.ensure = ['foo bar'] + end + + it "should set a list of strings" do + provider.expects(:svccfg).with('-s', fmri, :setprop, prop, '=', 'astring:', %q{("foo" "bar")}) + provider.ensure = [ 'foo', 'bar' ] + end + + it "should set a list of strings with spaces" do + provider.expects(:svccfg).with('-s', fmri, :setprop, prop, '=', 'astring:', %q{("foo" "bar baz")}) + provider.ensure = [ 'foo', 'bar baz' ] + end + end + + describe "and type is net_address" do + before :each do + Puppet::Type.type(:service_config).new(title.merge(:type => :net_address, :ensure => '10.0.0.1', :provider => provider)) + end + + it "should remove the property" do + provider.expects(:svccfg).with('-s', fmri, :delprop, prop) + provider.ensure = [:absent] + end + + it "should set a single address" do + provider.expects(:svccfg).with('-s', fmri, :setprop, prop, '=', 'net_address:', %q{127.0.0.1}) + provider.ensure = ['127.0.0.1'] + end + + it "should set a list of addresses" do + provider.expects(:svccfg).with('-s', fmri, :setprop, prop, '=', 'net_address:', %q{(10.0.0.141 10.0.0.142)}) + provider.ensure = [ '10.0.0.141', '10.0.0.142' ] + end + end + + describe "and type is integer" do + before :each do + Puppet::Type.type(:service_config).new(title.merge(:type => :integer, :ensure => '10', :provider => provider)) + end + + it "should remove the property" do + provider.expects(:svccfg).with('-s', fmri, :delprop, prop) + provider.ensure = [:absent] + end + + it "should set a single value" do + provider.expects(:svccfg).with('-s', fmri, :setprop, prop, '=', 'integer:', '500') + provider.ensure = ['500'] + end + + it "should set a list of values" do + provider.expects(:svccfg).with('-s', fmri, :setprop, prop, '=', 'integer:', '(500 600)') + provider.ensure = [ '500', '600' ] + end + end + end +end diff --git a/spec/unit/type/service_config_spec.rb b/spec/unit/type/service_config_spec.rb new file mode 100755 index 0000000..353b1af --- /dev/null +++ b/spec/unit/type/service_config_spec.rb @@ -0,0 +1,89 @@ +#!/usr/bin/env ruby + +require 'spec_helper' + +describe Puppet::Type.type(:service_config) do + + let :providerclass do + described_class.provider(:simple) { mk_resource_methods } + end + + let :fmri do + 'svc:/system/keymap:default' + end + + let :prop do + 'keymap/layout' + end + + let :title do + {:title => "#{fmri}:#{prop}", :fmri => fmri, :prop => prop} + end + + it "should have fmri as a keyattribute" do + described_class.key_attributes.should include :fmri + end + + it "should have prop as a keyattribute" do + described_class.key_attributes.should include :prop + end + + describe "title splitting" do + [ + {:fmri => 'svc:/system/keymap:default', :prop => 'keymap/layout'}, + {:fmri => 'network/dns/client', :prop => 'config/nameserver'}, + {:fmri => 'svc:/network/dns/client', :prop => 'config/value_authorization'} + ].each do |input| + input[:title] = "#{input[:fmri]}:#{input[:prop]}" + it "should correctly split #{input[:title]} into frmi and property" do + regex = described_class.title_patterns[0][0] + regex.match(input[:title]).captures.should == [ input[:fmri], input[:prop] ] + end + end + + it "should work with only the property as a title" do + regex = described_class.title_patterns[1][0] + regex.match('config/value_authorization').captures.should == [ 'config/value_authorization' ] + end + end + + describe "when validating attributes" do + [:fmri, :prop, :type, :provider].each do |param| + it "should have a #{param} parameter" do + described_class.attrtype(param).should == :param + end + end + + [:ensure].each do |property| + it "should have #{property} property" do + described_class.attrtype(property).should == :property + end + end + end + + describe "when validating values" do + describe "for type" do + [:astring, :count, :net_address_v4, :net_address_v6, :net_address, :boolean, :integer, :time].each do |type| + it "should support #{type} as value" do + expect { described_class.new(title.merge(:type => type, :ensure => 'foo')) }.to_not raise_error + end + end + it "should not support different values" do + expect { described_class.new(title.merge(:type => :foo, :ensure => 'foo')) }.to raise_error(Puppet::Error, /Invalid value/) + end + end + describe "for ensure" do + it "should support a single value" do + expect { described_class.new(title.merge(:ensure => 'foo')) }.to_not raise_error + end + + it "should support an array" do + expect { described_class.new(title.merge(:ensure => ['foo', 'bar'])) }.to_not raise_error + end + + it "should support absent" do + expect { described_class.new(title.merge(:ensure => :absent)) }.to_not raise_error + end + end + end +end