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.

Reply via email to