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 <[email protected]>
---
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 [email protected].
To unsubscribe from this group, send email to
[email protected].
For more options, visit this group at
http://groups.google.com/group/puppet-dev?hl=en.