From 180583c1d5a24eeb198dc645a84cb186a9c76f73 Mon Sep 17 00:00:00 2001 From: fatmcgav Date: Thu, 24 Jan 2013 16:02:56 +0000 Subject: [PATCH] Add support for managing the oranfstab provided by oracle for Direct NFS support. --- Rakefile | 1 + lib/puppet/provider/oranfstab/oranfstab.rb | 282 +++++++++++++++++++++ lib/puppet/type/oranfstab.rb | 69 +++++ spec/unit/type/oranfstab_spec.rb | 117 +++++++++ 4 files changed, 469 insertions(+) create mode 100644 lib/puppet/provider/oranfstab/oranfstab.rb create mode 100644 lib/puppet/type/oranfstab.rb create mode 100644 spec/unit/type/oranfstab_spec.rb diff --git a/Rakefile b/Rakefile index 14f1c24..dc1a407 100644 --- a/Rakefile +++ b/Rakefile @@ -1,2 +1,3 @@ require 'rubygems' require 'puppetlabs_spec_helper/rake_tasks' + diff --git a/lib/puppet/provider/oranfstab/oranfstab.rb b/lib/puppet/provider/oranfstab/oranfstab.rb new file mode 100644 index 0000000..4eb596a --- /dev/null +++ b/lib/puppet/provider/oranfstab/oranfstab.rb @@ -0,0 +1,282 @@ +require 'puppet/provider/parsedfile' + +oranfstab = case Facter.value(:operatingsystem) +when 'Solaris' + '/var/opt/oracle/oranfstab' +else + '/etc/oranfstab' +end + +Puppet::Type.type(:oranfstab).provide(:oranfstab, + :parent => Puppet::Provider::ParsedFile, + :default_target => oranfstab, + :filetype => :flat + ) do + + text_line :footer, :match => /^\s*#\s+End Instance$/ + + text_line :comment, :match => %r{^\s*#}, + :post_parse => proc { |record| + record[:name] = $1 if record[:line] =~ /^\s*#\s+Instance:\s+(\S{6,8})$/ + } + + text_line :blank, :match => /^\s*$/ + + record_line :nfsserver, :fields => %w{nfsserver}, + :match => /^server:\s*([\w-]+)/ + + record_line :localip, :fields => %w{localip}, + :match => %r{^local:\s*(([01]?\d\d?|2[0-4]\d|25[0-5])\.([01]?\d\d?|2[0-4]\d|25[0-5])\.([01]?\d\d?|2[0-4]\d|25[0-5])\.([01]?\d\d?|2[0-4]\d|25[0-5]))$} + + record_line :remoteip, :fields => %w{remoteip}, + :match => %r{^path:\s*(([01]?\d\d?|2[0-4]\d|25[0-5])\.([01]?\d\d?|2[0-4]\d|25[0-5])\.([01]?\d\d?|2[0-4]\d|25[0-5])\.([01]?\d\d?|2[0-4]\d|25[0-5]))$} + + record_line :nfsmount, :fields => %w{export path}, + :match => %r{^\s*export:\s+([\/\w]+)\s+mount:\s+([\/\w]+)} + + # + ## Local overrides of standard parsedfile defs'. + # + + # Add name and environments as necessary. + def self.to_line(record) + Puppet.debug("Got to self.to_line. Record is: \n") + #ap record + str = "" + str = "# Instance: #{record[:name]}\n" + # Add nfsserver line if present + if record[:nfsserver] and record[:nfsserver] != :absent and record[:nfsserver] != [:absent] + str += "server: " + record[:nfsserver] + "\n" + end + # Add localip and remoteip if present + if record[:localips] and record[:localips] != :absent and record[:localips] != [:absent] + # Itterate arrays + @local = record[:localips] + @remote = record[:remoteips] + # Merge localips and remoteips + @local.zip(@remote).each do |local, remote| + str += "local: #{local}\n" + str += "path: #{remote}\n" + end + end + if record[:mounts] and record[:mounts] != :absent and record[:mounts] != [:absent] + mounts = record[:mounts] + mounts.each do |mount| + Puppet.debug("Export = #{mount["export"]}, Path = #{mount["path"]}\n") + str += "export: #{mount["export"]} mount: #{mount["path"]}\n" + end + end + str += "# End Instance" + Puppet.debug("Returning str: \n") + #ap str + str + end + + + # Return the header placed at the top of each generated file, warning + # users that modifying this file manually is probably a bad idea. + def self.header +%{# HEADER: This file was autogenerated at #{Time.now} by puppet. +# HEADER: While it can still be managed manually, it is definitely not recommended. +# HEADER: Note particularly that the comments starting with '#' should +# HEADER: not be deleted, as doing so could cause duplicate database entries.\n} + end + + # See if we can match the record against an existing cron job. + def self.match(record, resources) + Puppet.debug("Got to match. \n") + Puppet.debug("Record = \n") + #ap record + #Puppet.debug("Resources = \n") + #ap resources + resources.each do |name, resource| + Puppet.debug("Name = \n") + #ap name + + # Match the DB name first + Puppet.debug("record[:name] = #{record[:name]}, resource.value[:name] = #{resource.value(:name)}") + next unless record[:name] == resource.value(:name) + + # Then the normal fields. + matched = true + record_type(record[:record_type]).fields.each do |field| + next if field == :name + if record[field] and ! resource.value(field) + #Puppet.info "Cron is missing %s: %s and %s" % + # [field, record[field].inspect, resource.value(field).inspect] + matched = false + break + end + + if ! record[field] and resource.value(field) + #Puppet.info "Hash is missing %s: %s and %s" % + # [field, resource.value(field).inspect, record[field].inspect] + matched = false + break + end + + # Yay differing definitions of absent. + next if (record[field] == :absent and resource.value(field) == "*") + + # Everything should be in the form of arrays, not the normal text. + next if (record[field] == resource.value(field)) + #Puppet.info "Did not match %s: %s vs %s" % + # [field, resource.value(field).inspect, record[field].inspect] + matched = false + break + + end + Puppet.debug("Matched = #{matched}") + return resource if matched + + end + + Puppet.debug("Didn't match, returning false. \n") + false + + end + + def self.prefetch_hook(records) + Puppet.debug("Got to prefetch_hook. \n") + #Puppet.debug("Records =. \n") + #ap records + + # Create empty placeholders + name = nil + nfsserver = nil + mounts = nil + localips = nil + remoteips = nil + + result = records.each { |record| + Puppet.debug("Processing record: \n") + #ap record + case record[:record_type] + when :comment + Puppet.debug("Got a :comment record. \n") + Puppet.debug("Line = #{record[:line]}. \n") + if record[:name] + name = record[:name] + record[:skip] = true + + # Start collecting data + mounts = [] + localips = [] + remoteips = [] + end + when :blank + Puppet.debug("Got a :blank record. \n") + when :nfsserver + Puppet.debug("Got a :nfsserver record. \n") + nfsserver = record[:nfsserver] + record[:skip] = true + Puppet.debug("nfsserver looks like: \n") + #ap nfsserver + when :nfsmount + Puppet.debug("Got a :nfsmount record. \n") + if mounts + r = {} + r['export'] = record[:export] + r['path'] = record[:path] + mounts << r + r = nil + record[:skip] = true + end + Puppet.debug("mounts array looks like: \n") + #ap mounts + when :localip + Puppet.debug("Got a :localip record. \n") + localips << record[:localip] + record[:skip] = true + #ap localips + when :remoteip + Puppet.debug("Got a :remoteip. \n") + remoteips << record[:remoteip] + record[:skip] = true + #ap remoteips + else + # Set name if #appropriate. + if name + record[:name] = name + name = nil + end + # Add nfsserver value + if nfsserver.nil? or nfsserver.empty? + Puppet.debug("nfsserver is nil or empty. Setting to :absent .\n") + record[:nfsserver] = :absent + else + Puppet.debug("Populating record[:nfsserver] with nfsserver.\n") + record[:nfsserver] = nfsserver + nfsserver = nil + end + # Add localips value + if localips.nil? or localips.empty? + Puppet.debug("localips is nil or empty. Setting to :absent. \n") + record[:localips] = :absent + else + Puppet.debug("Populating record[:localips] with localips. \n") + record[:localips] = localips + localips = nil + end + # Add remoteips value + if remoteips.nil? or remoteips.empty? + Puppet.debug("remoteips is nil or empty. Setting to :absent. \n") + record[:remoteips] = :absent + else + Puppet.debug("Populating record[:remoteips] with remoteips. \n") + record[:remoteips] = remoteips + remoteips = nil + end + # Add mounts value + if mounts.nil? or mounts.empty? + Puppet.debug("mounts is nil or empty. Setting to :absent. \n") + record[:mounts] = :absent + else + Puppet.debug("Populating record[:mounts] with mounts. \n") + record[:mounts] = mounts + mounts = nil + end + end + }.reject { |record| record[:skip] } + + Puppet.debug("Returning result: \n") + #ap result + + result + + end + + def self.to_file(records) + Puppet.debug("Got to to_file. \n") + Puppet.debug("Records = \n") + #ap records + text = super + text + end + + ## Temporary overrides for debugging. + # + def self.match_providers_with_resources(resources) + Puppet.debug("Got to self.match_providers_with_resources... \n") + return unless resources + matchers = resources.dup + @records.each do |record| + # Skip things like comments and blank lines + Puppet.debug("Skip_record = #{skip_record?(record).to_s}. Not actually skipping though... \n") + #next if skip_record?(record) + + if name = record[:name] and resource = resources[name] + Puppet.debug("Record name matches resource name. \n") + resource.provider = new(record) + elsif respond_to?(:match) + if resource = match(record, matchers) + # Remove this resource from circulation so we don't unnecessarily try to match + matchers.delete(resource.title) + record[:name] = resource[:name] + resource.provider = new(record) + end + end + end + end + +end diff --git a/lib/puppet/type/oranfstab.rb b/lib/puppet/type/oranfstab.rb new file mode 100644 index 0000000..527599a --- /dev/null +++ b/lib/puppet/type/oranfstab.rb @@ -0,0 +1,69 @@ +require 'puppet/provider/parsedfile' + +module Puppet + newtype(:oranfstab) do + + @doc = "Define database mount points in /etc/oranfstab." + + newparam(:name) do + desc "The instance's name." + isnamevar + + validate do |value| + raise Puppet::Error, "Name must not contain whitespace: #{value}" if value =~ /\s/ + raise Puppet::Error, "Name must not be empty" if value.empty? + end + end + + ensurable + + newproperty(:nfsserver) do + desc "The NFS Server hostname for this instance." + validate do |value| + raise Puppet::Error, "Nfsserver must contain a valid hostname: #{value}" unless value =~ /^[\w-]+$/ + raise Puppet::Error, "Nfsserver must not be empty" if value.empty? + end + end + + newproperty(:localips, :array_matching => :all) do + desc "The local IP(s) to use." + validate do |value| + raise Puppet::Error, "Localips should not be blank." if value.empty? + raise Puppet::Error, "Localips should contain valid IP address': #{value}" unless value =~ /^([01]?\d\d?|2[0-4]\d|25[0-5])\.([01]?\d\d?|2[0-4]\d|25[0-5])\.([01]?\d\d?|2[0-4]\d|25[0-5])\.([01]?\d\d?|2[0-4]\d|25[0-5])$/ + end + end + + newproperty(:remoteips, :array_matching => :all) do + desc "The remote IP(s) to use." + validate do |value| + raise Puppet::Error, "Remoteips should not be blank." if value.empty? + raise Puppet::Error, "Remoteips should contain valid IP address': #{value}" unless value =~ /^([01]?\d\d?|2[0-4]\d|25[0-5])\.([01]?\d\d?|2[0-4]\d|25[0-5])\.([01]?\d\d?|2[0-4]\d|25[0-5])\.([01]?\d\d?|2[0-4]\d|25[0-5])$/ + end + end + + newproperty(:mounts, :array_matching => :all) do + desc "Array of mounts" + validate do |value| + raise Puppet::Error, "Mounts should be an array of hashes." unless value.is_a?(Hash) + end + + def insync?(is) + if is.is_a?(Array) + # array of hashes doesn't support .sort + return is.sort_by(&:hash) == @should.sort_by(&:hash) + else + return is == @should + end + end + + def should_to_s(newvalue) + if newvalue == :absent + return "absent" + else + newvalue + end + end + end + + end +end diff --git a/spec/unit/type/oranfstab_spec.rb b/spec/unit/type/oranfstab_spec.rb new file mode 100644 index 0000000..b2bb3a4 --- /dev/null +++ b/spec/unit/type/oranfstab_spec.rb @@ -0,0 +1,117 @@ +#!/usr/bin/env ruby + +require 'spec_helper' + +describe Puppet::Type.type(:oranfstab) do + + before do + @provider_class = described_class.provide(:fake) { mk_resource_methods } + @provider_class.stubs(:suitable?).returns true + described_class.stubs(:defaultprovider).returns @provider_class + end + + it "should have :name as its keyattribute" do + described_class.key_attributes.should == [:name] + end + + describe "when validating attributes" do + [:name, :provider].each do |param| + it "should have a #{param} parameter" do + described_class.attrtype(param).should == :param + end + end + + [:ensure, :nfsserver, :localips, :remoteips, :mounts].each do |property| + it "should have a #{property} property" do + described_class.attrtype(property).should == :property + end + end + end + + describe "when validating value" do + + describe "for ensure" do + it "should support present" do + proc { described_class.new(:name => 'foo', :ensure => :present) }.should_not raise_error + end + + it "should support absent" do + proc { described_class.new(:name => 'foo', :ensure => :absent) }.should_not raise_error + end + + it "should not support other values" do + proc { described_class.new(:name => 'foo', :ensure => :foo) }.should raise_error(Puppet::Error, /Invalid value/) + end + end + + describe "for name" do + it "should support a valid name" do + proc { described_class.new(:name => 'TEST01E', :ensure => :present) }.should_not raise_error + proc { described_class.new(:name => 'MY_FANCY_DB', :ensure => :present) }.should_not raise_error + end + + it "should not support whitespace" do + proc { described_class.new(:name => 'TEST 01E', :ensure => :present) }.should raise_error(Puppet::Error, /Name.*whitespace/) + proc { described_class.new(:name => 'TEST01E ', :ensure => :present) }.should raise_error(Puppet::Error, /Name.*whitespace/) + proc { described_class.new(:name => ' TEST01E', :ensure => :present) }.should raise_error(Puppet::Error, /Name.*whitespace/) + proc { described_class.new(:name => "TEST\t01E", :ensure => :present) }.should raise_error(Puppet::Error, /Name.*whitespace/) + end + + it "should not support an empty name" do + proc { described_class.new(:name => '', :ensure => :present) }.should raise_error(Puppet::Error, /Name.*empty/) + end + end + + describe "for nfsserver" do + it "should support a hostname" do + proc { described_class.new(:name => 'TEST01E', :nfsserver => 'test-nactl01', :ensure => :present) }.should_not raise_error + end + it "should not support an invalid hostname" do + proc { described_class.new(:name => 'TEST01E', :nfsserver => 'host#', :ensure => :present) }.should raise_error(Puppet::Error, /Nfsserver must contain a valid hostname:/) + proc { described_class.new(:name => 'TEST01E', :nfsserver => 'invalid host', :ensure => :present) }.should raise_error(Puppet::Error, /Nfsserver must contain a valid hostname:/) + end + it "should not support an empty hostname" do + proc { described_class.new(:name => "TEST01E", :nfsserver => '', :ensure => :present) }.should raise_error(Puppet::Error, /Nfsserver must contain a valid hostname:/) + end + end + + describe "for localips" do + it "should support array" do + proc { described_class.new(:name => 'TEST01E', :localips => ["192.168.1.1"]) }.should_not raise_error + proc { described_class.new(:name => 'TEST01E', :localips => ["192.168.1.1", "192.168.1.2"]) }.should_not raise_error + end + it "should not support invalid IP contents" do + proc { described_class.new(:name => "TEST01E", :localips => ["not an ip"], :ensure => :present) }.should raise_error(Puppet::Error, /Localips should contain valid IP address/) + end + it "should not support blank contents" do + proc { described_class.new(:name => "TEST01E", :localips => [""], :ensure => :present) }.should raise_error(Puppet::Error, /Localips should not be blank/) + proc { described_class.new(:name => "TEST01E", :localips => "", :ensure => :present) }.should raise_error(Puppet::Error, /Localips should not be blank/) + end + end + + describe "for remoteips" do + it "should support array" do + proc { described_class.new(:name => 'TEST01E', :remoteips => ["192.168.1.1"]) }.should_not raise_error + proc { described_class.new(:name => 'TEST01E', :remoteips => ["192.168.1.1", "192.168.1.2"]) }.should_not raise_error + end + it "should not support invalid IP contents" do + proc { described_class.new(:name => "TEST01E", :remoteips => ["not an ip"], :ensure => :present) }.should raise_error(Puppet::Error, /Remoteips should contain valid IP address/) + end + it "should not support blank contents" do + proc { described_class.new(:name => "TEST01E", :remoteips => [""], :ensure => :present) }.should raise_error(Puppet::Error, /Remoteips should not be blank/) + proc { described_class.new(:name => "TEST01E", :remoteips => "", :ensure => :present) }.should raise_error(Puppet::Error, /Remoteips should not be blank/) + end + end + + describe "for mounts" do + it "should support array of hashes" do + proc { described_class.new(:name => 'TEST01E', :mounts => [ {"export" => "/vol/volume/qtree", "path" => "/mountpoint"}]) }.should_not raise_error + end + it "should not support an array" do + proc { described_class.new(:name => "TEST01E", :mounts => ["not a hash"], :ensure => :present) }.should raise_error(Puppet::Error, /Mounts should be an array of hashes./) + end + end + + end +end +