This new type "port" handles entries in /etc/services. It uses multiple key_attributes (name and protocol), so you are able to add e.g. multiple telnet lines for tcp and udp. Sample usage
port { 'telnet': number => '23', protocol => 'tcp', description => 'Telnet' } Because the type makes use of the title_patterns function this can also be written as port { 'telnet/tcp': number => '23', description => 'Telnet' } This type only supports tcp and udp and might not work on OS X Signed-off-by: Stefan Schulte <stefan.schu...@taunusstein.net> --- Local-branch: feature/next/5660N lib/puppet/type/port.rb | 258 ++++++++++++++++++++++------------------- spec/unit/type/port_spec.rb | 270 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 410 insertions(+), 118 deletions(-) create mode 100644 spec/unit/type/port_spec.rb diff --git a/lib/puppet/type/port.rb b/lib/puppet/type/port.rb index e199885..f895785 100755 --- a/lib/puppet/type/port.rb +++ b/lib/puppet/type/port.rb @@ -1,119 +1,141 @@ -#module Puppet -# newtype(:port) do -# @doc = "Installs and manages port entries. For most systems, these -# entries will just be in /etc/services, but some systems (notably OS X) -# will have different solutions." -# -# ensurable -# -# newproperty(:protocols) do -# desc "The protocols the port uses. Valid values are *udp* and *tcp*. -# Most services have both protocols, but not all. If you want -# both protocols, you must specify that; Puppet replaces the -# current values, it does not merge with them. If you specify -# multiple protocols they must be as an array." -# -# def is=(value) -# case value -# when String -# @is = value.split(/\s+/) -# else -# @is = value -# end -# end -# -# def is -# @is -# end -# -# # We actually want to return the whole array here, not just the first -# # value. -# def should -# if defined?(@should) -# if @should[0] == :absent -# return :absent -# else -# return @should -# end -# else -# return nil -# end -# end -# -# validate do |value| -# valids = ["udp", "tcp", "ddp", :absent] -# unless valids.include? value -# raise Puppet::Error, -# "Protocols can be either 'udp' or 'tcp', not #{value}" -# end -# end -# end -# -# newproperty(:number) do -# desc "The port number." -# end -# -# newproperty(:description) do -# desc "The port description." -# end -# -# newproperty(:port_aliases) do -# desc 'Any aliases the port might have. Multiple values must be -# specified as an array. Note that this property is not the same as -# the "alias" metaparam; use this property to add aliases to a port -# in the services file, and "alias" to aliases for use in your Puppet -# scripts.' -# -# # We actually want to return the whole array here, not just the first -# # value. -# def should -# if defined?(@should) -# if @should[0] == :absent -# return :absent -# else -# return @should -# end -# else -# return nil -# end -# end -# -# validate do |value| -# if value.is_a? String and value =~ /\s/ -# raise Puppet::Error, -# "Aliases cannot have whitespace in them: %s" % -# value.inspect -# end -# end -# -# munge do |value| -# unless value == "absent" or value == :absent -# # Add the :alias metaparam in addition to the property -# @resource.newmetaparam( -# @resource.class.metaparamclass(:alias), value -# ) -# end -# value -# end -# end -# -# newproperty(:target) do -# desc "The file in which to store service information. Only used by -# those providers that write to disk." -# -# defaultto { if @resource.class.defaultprovider.ancestors.include?(Puppet::Provider::ParsedFile) -# @resource.class.defaultprovider.default_target -# else -# nil -# end -# } -# end -# -# newparam(:name) do -# desc "The port name." -# -# isnamevar -# end -# end -#end +require 'puppet/property/ordered_list' +module Puppet + newtype(:port) do + @doc = "Installs and manages port entries. For most systems, these + entries will just be in `/etc/services`, but some systems (notably OS X) + will have different solutions. + + This type uses a composite key of (port) `name` and (port) `number` to + identify a resource. You are able to set both keys with the resource + title if you seperate them with a slash. So instead of specifying protocol + explicitly: + + port { \"telnet\": + protocol => tcp, + number => 23, + } + + you can also specify both name and protocol implicitly through the title: + + port { \"telnet/tcp\": + number => 23, + } + + The second way is the prefered way if you want to specifiy a port that + uses both tcp and udp as a protocol. You need to define two resources + for such a port but the resource title still has to be uniq. + + Example: To make sure you have the telnet port in your `/etc/services` + file you will now write: + + port { \"telnet/tcp\": + number => 23, + } + port { \"telnet/udp\": + number => 23, + } + + Currently only tcp and udp are supported and recognised when setting + the protocol via the title." + + def self.title_patterns + [ + # we have two title_patterns "name" and "name:protocol". We won't use + # one pattern (that will eventually set :protocol to nil) because we + # want to use a default value for :protocol. And that does only work + # if :protocol is not put in the parameter hash while initialising + [ + /^(.*?)\/(tcp|udp)$/, # Set name and protocol + [ + # We don't need a lot of post-parsing + [ :name, lambda{|x| x} ], + [ :protocol, lambda{ |x| x.intern unless x.nil? } ] + ] + ], + [ + /^(.*)$/, + [ + [ :name, lambda{|x| x} ] + ] + ] + ] + end + + ensurable + + newparam(:name) do + desc "The port name." + + validate do |value| + raise Puppet::Error "Port name must not contain whitespace: #{value}" if value =~ /\s/ + end + + isnamevar + end + + newparam(:protocol) do + desc "The protocol the port uses. Valid values are *udp* and *tcp*. + Most services have both protocols, but not all. If you want both + protocols you have to define two resources. Remeber that you cannot + specify two resources with the same title but you can use a title + to set both, name and protocol if you use ':' as a seperator. So + port { \"telnet/tcp\": ... } sets both name and protocol and you don't + have to specify them explicitly." + + newvalues :tcp, :udp + + defaultto :tcp + + isnamevar + end + + + newproperty(:number) do + desc "The port number." + + validate do |value| + raise Puppet::Error, "number has to be numeric, not #{value}" unless value =~ /^[0-9]+$/ + raise Puppet::Error, "number #{value} out of range (0-65535)" unless (0...2**16).include?(Integer(value)) + end + + end + + newproperty(:description) do + desc "The description for the port. The description will appear" + "as a comment in the `/etc/services` file" + end + + newproperty(:port_aliases, :parent => Puppet::Property::OrderedList) do + desc "Any aliases the port might have. Multiple values must be + specified as an array." + + def inclusive? + true + end + + def delimiter + " " + end + + validate do |value| + raise Puppet::Error, "Aliases must not contain whitespace: #{value}" if value =~ /\s/ + end + end + + + newproperty(:target) do + desc "The file in which to store service information. Only used by + those providers that write to disk." + + defaultto do + if @resource.class.defaultprovider.ancestors.include?(Puppet::Provider::ParsedFile) + @resource.class.defaultprovider.default_target + else + nil + end + end + end + + end +end diff --git a/spec/unit/type/port_spec.rb b/spec/unit/type/port_spec.rb new file mode 100644 index 0000000..8386ae5 --- /dev/null +++ b/spec/unit/type/port_spec.rb @@ -0,0 +1,270 @@ +#!/usr/bin/env ruby + +require File.expand_path(File.dirname(__FILE__) + '/../../spec_helper') +require 'puppet/property/ordered_list' + +port = Puppet::Type.type(:port) + +describe port do + before do + @class = port + @provider_class = stub 'provider_class', :name => 'fake', :ancestors => [], :suitable? => true, :supports_parameter? => true + @class.stubs(:defaultprovider).returns @provider_class + @class.stubs(:provider).returns @provider_class + + @provider = stub 'provider', :class => @provider_class, :clean => nil, :exists? => false + @resource = stub 'resource', :resource => nil, :provider => @provider + + @provider.stubs(:port_aliases).returns :absent + + @provider_class.stubs(:new).returns(@provider) + @catalog = Puppet::Resource::Catalog.new + end + + it "should have a title pattern that splits name and protocol" do + regex = @class.title_patterns[0][0] + regex.match("telnet/tcp").captures.should == ['telnet','tcp' ] + regex.match("telnet/udp").captures.should == ['telnet','udp' ] + regex.match("telnet/baz").should == nil + end + + it "should have a second title pattern that will set only name" do + regex = @class.title_patterns[1][0] + regex.match("telnet/tcp").captures.should == ['telnet/tcp' ] + regex.match("telnet/udp").captures.should == ['telnet/udp' ] + regex.match("telnet/baz").captures.should == ['telnet/baz' ] + end + + it "should have two key_attributes" do + @class.key_attributes.size.should == 2 + end + + it "should have :name as a key_attribute" do + @class.key_attributes.should include :name + end + + it "should have :protocol as a key_attribute" do + @class.key_attributes.should include :protocol + end + + describe "when validating attributes" do + + [:name, :provider, :protocol].each do |param| + it "should have a #{param} parameter" do + @class.attrtype(param).should == :param + end + end + + [:ensure, :port_aliases, :description, :number].each do |property| + it "should have #{property} property" do + @class.attrtype(property).should == :property + end + end + + it "should have a list port_aliases" do + @class.attrclass(:port_aliases).ancestors.should include Puppet::Property::OrderedList + end + + end + + describe "when validating values" do + + it "should support present as a value for ensure" do + lambda { @class.new(:name => "whev", :protocol => :tcp, :ensure => :present) }.should_not raise_error + end + + it "should support absent as a value for ensure" do + proc { @class.new(:name => "whev", :protocol => :tcp, :ensure => :absent) }.should_not raise_error + end + + it "should support :tcp as a value for protocol" do + proc { @class.new(:name => "whev", :protocol => :tcp) }.should_not raise_error + end + + it "should support :udp as a value for protocol" do + proc { @class.new(:name => "whev", :protocol => :udp) }.should_not raise_error + end + + it "should not support other protocols than tcp and udp" do + proc { @class.new(:name => "whev", :protocol => :tcpp) }.should raise_error(Puppet::Error) + end + + it "should use tcp as default protocol" do + port_test = @class.new(:name => "whev") + port_test[:protocol].should == :tcp + end + + it "should support valid portnumbers" do + proc { @class.new(:name => "whev", :protocol => :tcp, :number => '0') }.should_not raise_error + proc { @class.new(:name => "whev", :protocol => :tcp, :number => '1') }.should_not raise_error + proc { @class.new(:name => "whev", :protocol => :tcp, :number => "#{2**16-1}") }.should_not raise_error + end + + it "should not support portnumbers that arent numeric" do + proc { @class.new(:name => "whev", :protocol => :tcp, :number => "aa") }.should raise_error(Puppet::Error) + proc { @class.new(:name => "whev", :protocol => :tcp, :number => "22a") }.should raise_error(Puppet::Error) + proc { @class.new(:name => "whev", :protocol => :tcp, :number => "a22") }.should raise_error(Puppet::Error) + end + + it "should not support portnumbers that are out of range" do + proc { @class.new(:name => "whev", :protocol => :tcp, :number => "-1") }.should raise_error(Puppet::Error) + proc { @class.new(:name => "whev", :protocol => :tcp, :number => "#{2**16}") }.should raise_error(Puppet::Error) + end + + it "should support single port_alias" do + proc { @class.new(:name => "foo", :protocol => :tcp, :port_aliases => 'bar') }.should_not raise_error + end + + it "should support multiple port_aliases" do + proc { @class.new(:name => "foo", :protocol => :tcp, :port_aliases => ['bar','bar2']) }.should_not raise_error + end + + it "should not support whitespaces in any port_alias" do + proc { @class.new(:name => "whev", :protocol => :tcp, :port_aliases => ['bar','fo o']) }.should raise_error(Puppet::Error) + end + + it "should not support whitespaces in resourcename" do + proc { @class.new(:name => "foo bar", :protocol => :tcp) }.should raise_error(Puppet::Error) + end + + it "should not allow a resource with no name" do + proc { @class.new(:protocol => :tcp) }.should raise_error(Puppet::Error) + end + + it "should allow a resource with no protocol when the default is tcp" do + proc { @class.new(:name => "foo") }.should_not raise_error(Puppet::Error) + end + + it "should not allow a resource with no protocol when we have no default" do + @class.attrclass(:protocol).stubs(:method_defined?).with(:default).returns(false) + proc { @class.new(:name => "foo") }.should raise_error(Puppet::Error) + end + + it "should extract name and protocol from title if not explicitly set" do + res = @class.new(:title => 'telnet/tcp', :number => '23') + res[:number].should == '23' + res[:name].should == 'telnet' + res[:protocol].should == :tcp + end + + it "should not extract name from title if explicitly set" do + res = @class.new(:title => 'telnet/tcp', :name => 'ssh', :number => '23') + res[:number].should == '23' + res[:name].should == 'ssh' + res[:protocol].should == :tcp + end + + it "should not extract protocol from title if explicitly set" do + res = @class.new(:title => 'telnet/tcp', :protocol => :udp, :number => '23') + res[:number].should == '23' + res[:name].should == 'telnet' + res[:protocol].should == :udp + end + + it "should not extract name and protocol from title when they are explicitly set" do + res = @class.new(:title => 'foo/udp', :name => 'bar', :protocol => :tcp, :number => '23') + res[:number].should == '23' + res[:name].should == 'bar' + res[:protocol].should == :tcp + end + + end + + describe "when syncing" do + + it "should send the first value to the provider for number property" do + number = @class.attrclass(:number).new(:resource => @resource, :should => %w{100 200}) + @provider.expects(:number=).with '100' + number.sync + end + + it "should send the joined array to the provider for port_aliases property" do + port_aliases = @class.attrclass(:port_aliases).new(:resource => @resource, :should => %w{foo bar}) + @provider.expects(:port_aliases=).with 'foo bar' + port_aliases.sync + end + + it "should care about the order of port_aliases" do + port_aliases = @class.attrclass(:port_aliases).new(:resource => @resource, :should => %w{a z b}) + port_aliases.insync?(%w{a z b}).should == true + port_aliases.insync?(%w{a b z}).should == false + port_aliases.insync?(%w{b a z}).should == false + port_aliases.insync?(%w{z a b}).should == false + port_aliases.insync?(%w{z b a}).should == false + port_aliases.insync?(%w{b z a}).should == false + end + + end + + describe "when comparing uniqueness_key of two ports" do + + it "should be equal if name and protocol are the same" do + foo_tcp1 = @class.new(:name => "foo", :protocol => :tcp, :number => '23') + foo_tcp2 = @class.new(:name => "foo", :protocol => :tcp, :number => '23') + foo_tcp1.uniqueness_key.should == ['foo', :tcp ] + foo_tcp2.uniqueness_key.should == ['foo', :tcp ] + foo_tcp1.uniqueness_key.should == foo_tcp2.uniqueness_key + end + + it "should not be equal if protocol differs" do + foo_tcp = @class.new(:name => "foo", :protocol => :tcp, :number => '23') + foo_udp = @class.new(:name => "foo", :protocol => :udp, :number => '23') + foo_tcp.uniqueness_key.should == [ 'foo', :tcp ] + foo_udp.uniqueness_key.should == [ 'foo', :udp ] + foo_tcp.uniqueness_key.should_not == foo_udp.uniqueness_key + end + + it "should not be equal if name differs" do + foo_tcp = @class.new(:name => "foo", :protocol => :tcp, :number => '23') + bar_tcp = @class.new(:name => "bar", :protocol => :tcp, :number => '23') + foo_tcp.uniqueness_key.should == [ 'foo', :tcp ] + bar_tcp.uniqueness_key.should == [ 'bar', :tcp ] + foo_tcp.uniqueness_key.should_not == bar_tcp.uniqueness_key + end + + it "should not be equal if both name and protocol differ" do + foo_tcp = @class.new(:name => "foo", :protocol => :tcp, :number => '23') + bar_udp = @class.new(:name => "bar", :protocol => :udp, :number => '23') + foo_tcp.uniqueness_key.should == [ 'foo', :tcp ] + bar_udp.uniqueness_key.should == [ 'bar', :udp ] + foo_tcp.uniqueness_key.should_not == bar_udp.uniqueness_key + end + + end + + describe "when adding resource to a catalog" do + + it "should not allow two resources with the same name and protocol" do + res1 = @class.new(:name => "telnet", :protocol => :tcp, :number => '23') + res2 = @class.new(:name => "telnet", :protocol => :tcp, :number => '23') + proc { @catalog.add_resource(res1) }.should_not raise_error + proc { @catalog.add_resource(res2) }.should raise_error(Puppet::Resource::Catalog::DuplicateResourceError) + end + + it "should allow two resources with different name and protocol" do + res1 = @class.new(:name => "telnet", :protocol => :tcp, :number => '23') + res2 = @class.new(:name => "git", :protocol => :tcp, :number => '9418') + proc { @catalog.add_resource(res1) }.should_not raise_error + proc { @catalog.add_resource(res2) }.should_not raise_error + end + + it "should allow two resources with same name and different protocol" do + # I would like to have a gentitle method that would not automatically set + # title to resource[:name] but to uniqueness_key.join('/') or + # similar - stschulte + res1 = @class.new(:title => 'telnet/tcp', :name => 'telnet', :protocol => :tcp, :number => '23') + res2 = @class.new(:title => 'telnet/udp', :name => 'telnet', :protocol => :udp, :number => '23') + proc { @catalog.add_resource(res1) }.should_not raise_error + proc { @catalog.add_resource(res2) }.should_not raise_error + end + + it "should allow two resources with the same protocol but different names" do + res1 = @class.new(:title => 'telnet/tcp', :name => 'telnet', :protocol => :tcp, :number => '23') + res2 = @class.new(:title => 'ssh/tcp', :name => 'ssh', :protocol => :tcp, :number => '23') + proc { @catalog.add_resource(res1) }.should_not raise_error + proc { @catalog.add_resource(res2) }.should_not raise_error + end + + end + +end -- 1.7.4.1 -- You received this message because you are subscribed to the Google Groups "Puppet Developers" group. To post to this group, send email to puppet-dev@googlegroups.com. To unsubscribe from this group, send email to puppet-dev+unsubscr...@googlegroups.com. For more options, visit this group at http://groups.google.com/group/puppet-dev?hl=en.