On Tue, Mar 22, 2011 at 3:35 PM, Stefan Schulte <
[email protected]> wrote:
> 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
>
Is "port" the best name for this?
That feels like we're grabbing an awfully generic name for a quite specific
task.
>
> 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.
>
>
--
Nigel Kersten
Product, Puppet Labs
@nigelkersten
--
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.