diff --git a/README.queueing b/README.queueing index 8c4a18029..83a8e19c0 100644 --- a/README.queueing +++ b/README.queueing @@ -1,126 +1,126 @@ *PUPPET QUEUEING Puppet Queueing is a feature which is designed to take some load off of the PuppetMaster by transferring the task of updating the database to a separate program which is named puppetqd (Puppet Queue Daemon). Currently this is only supported for "Storeconfigs" which is documented at: -http://reductivelabs.com/trac/puppet/wiki/UsingStoredConfiguration +http://projects.puppetlabs.com/projects/1/wiki/Using_Stored_Configuration In the future this feature can be extended to any new puppet data which involves storage in a database. *OPERATION In a nutshell: puppetmasterd -> stomp -> service -> stomp -> puppetqd -> database At the moment the only messaging protocol supported is "stomp". Although others could be implemented, stomp is considered by many as the default queueing mechanism for Ruby and Rails applications. It is distributed as a Ruby gem and is easily installed. (The queueing code inside Puppet has been written so that when other interfaces and protocols are implemented they will be easy to use by changing settings in puppet.conf). The "service" in the diagram above is any queueing service that supports the Stomp API. For details refer to: http://xircles.codehaus.org/projects/stomp Both puppetmasterd and puppetqd subscribe to the same queueing service using the stomp interface. As puppetmasterd posts data to the queue, puppetqd receives it and stores it. The details of how to connect to the service and the name of the queue to use are set in puppet.conf: [main] queue_type = stomp queue_source = stomp://localhost:61613 [puppetmasterd] async_storeconfigs = true Note: since puppetmasterd needs to recover the data being stored at a later time, both puppetmasterd and puppetqd need to work with the same database as defined in the STORECONFIGS setup. *QUEUEING SERVICES As mentioned previously any queueing service that supports the Stomp protocol can be used. Which one you use depends on your needs. We have tested with two of the most popular services - StompServer and ActiveMQ. + StompServer http://rubyforge.org/projects/stompserver/ StompServer is a lightweight queueing service written in Ruby which is suitable for testing or low volume puppet usage. Works well when both puppetmasterd and puppetd are running on the same machine that it's running on but we encountered some problems when using it from multiple machines. Just install the stompserver gem and run 'stompserver'. + Apache ActiveMQ http://activemq.apache.org Considered by many to be the most popular message service in use today, ActiveMQ has hundreds of features for scaling, persistence and so on. Although installation is fairly simple, the configuration can seem quite intimidating, but for our use a one line change to the standard configuration is all that is required and is explained at: http://activemq.apache.org/stomp.html Other customization of the internal workings of ActiveMQ, if any, will depend on your needs and deployment. A quick skimming of the ActiveMQ documentation will give you enough info to decide. Others We have looked at but not tried some other queuing services which are compatible with the Stomp API: + POE Component Message Queue + JBoss Messaging (with 3rd party support for Stomp) *SCALING For StoreConfigs you basically need to have the catalog for a node stored in the database before the next time the node connects and asks for a new catalog. If the puppetd on your nodes is set to check every 30 minutes, then it would seem that there is no problem. However if you have 3000 nodes you have a LOT of catalogs to store and it is possible you will not get a catalog saved in time. Running puppetmaster, your queueing service and puppetqd on the same machine means that they are all competing for the same CPU cycles. Bumping up the power of the server they are running on may be enough to handle even fairly large deployments. However since most queueing services (even StompServer) are designed to deliver messages from a "queue" to whoever asks for the next message you can split things up between machines: puppetmaster1 --\ /-- puppetqd1 -\ puppetmaster2 ----> ActiveMQ ---> puppetqd2 ---> database puppetmaster3 --/ \-- puppetqd33 -/ \- puppetqd4-/ This is, of course a totally contrived example, but it gets the point across. As long as the data gets to the database, it doesn't matter which machines or services it goes through. Although for StoreConfigs absolute reliability is not a requirement as a new catalog will be sent the next time a node connects, some amount of persistence should some process crash may be desirable. Both ActiveMQ and MySQL (and other databases) have these kind of features built in which can be activated as needed. diff --git a/Rakefile b/Rakefile index 38cfec6ce..6cc53fe18 100644 --- a/Rakefile +++ b/Rakefile @@ -1,53 +1,53 @@ # Rakefile for Puppet -*- ruby -*- $LOAD_PATH << File.join(File.dirname(__FILE__), 'tasks') require 'rake' require 'rake/packagetask' require 'rake/gempackagetask' require 'rspec' require "rspec/core/rake_task" module Puppet PUPPETVERSION = File.read('lib/puppet.rb')[/PUPPETVERSION *= *'(.*)'/,1] or fail "Couldn't find PUPPETVERSION" end Dir['tasks/**/*.rake'].each { |t| load t } FILES = FileList[ '[A-Z]*', 'install.rb', 'bin/**/*', 'sbin/**/*', 'lib/**/*', 'conf/**/*', 'man/**/*', 'examples/**/*', 'ext/**/*', 'tasks/**/*', 'test/**/*', 'spec/**/*' ] Rake::PackageTask.new("puppet", Puppet::PUPPETVERSION) do |pkg| pkg.package_dir = 'pkg' pkg.need_tar_gz = true pkg.package_files = FILES.to_a end task :default do sh %{rake -T} end desc "Create the tarball and the gem - use when releasing" task :puppetpackages => [:create_gem, :package] RSpec::Core::RakeTask.new do |t| t.pattern ='spec/{unit,integration}/**/*.rb' - t.fail_on_error = false + t.fail_on_error = true end desc "Run the unit tests" task :unit do sh "cd test; rake" end diff --git a/conf/solaris/smf/svc-puppetd b/conf/solaris/smf/svc-puppetd index b6cf05736..8acc19cfa 100755 --- a/conf/solaris/smf/svc-puppetd +++ b/conf/solaris/smf/svc-puppetd @@ -1,66 +1,64 @@ #!/bin/sh # This is the /etc/init.d file for puppetd # Modified for CSW # # description: puppetd - Puppet Automation Client # . /lib/svc/share/smf_include.sh prefix=/opt/csw exec_prefix=/opt/csw sysconfdir=/opt/csw/etc sbindir=/opt/csw/sbin pidfile=/var/lib/puppet/run/agent.pid case "$1" in start) cd / # Start daemons. printf "Starting Puppet client services:" /opt/csw/sbin/puppetd printf " puppetd" echo "" ;; stop) printf "Stopping Puppet client services:" kill `cat $pidfile` printf " puppetd" echo "" ;; restart) printf "Restarting Puppet client services:" kill -HUP `cat $pidfile` printf " puppetd" echo "" ;; reload) printf "Reloading Puppet client services:" kill -HUP `cat $pidfile` printf " puppetd" echo "" ;; status) if [ -f $pidfile ]; then pid=`cat $pidfile` curpid=`pgrep puppetd` if [ "$pid" -eq "$curpid" ]; then exit 0 else exit 1 fi else exit 1 fi esac exit 0 - -# $Id$ diff --git a/conf/solaris/smf/svc-puppetmasterd b/conf/solaris/smf/svc-puppetmasterd index 80e3d464a..683324dec 100755 --- a/conf/solaris/smf/svc-puppetmasterd +++ b/conf/solaris/smf/svc-puppetmasterd @@ -1,62 +1,60 @@ #!/bin/sh # . /lib/svc/share/smf_include.sh prefix=/opt/csw exec_prefix=/opt/csw sysconfdir=/opt/csw/etc sbindir=/opt/csw/sbin pidfile=/var/lib/puppet/run/master.pid case "$1" in start) cd / # Start daemons. printf "Starting Puppet server services:" /opt/csw/sbin/puppetmasterd printf " puppetmaster" echo "" ;; stop) printf "Stopping Puppet server services:" kill `cat $pidfile` printf " puppetmasterd" echo "" ;; restart) printf "Restarting Puppet server services:" kill -HUP `cat $pidfile` printf " puppetmasterd" echo "" ;; reload) printf "Reloading Puppet server services:" kill -HUP `cat $pidfile` printf " puppetmasterd" echo "" ;; status) if [ -f $pidfile ]; then pid=`cat $pidfile` curpid=`pgrep puppetmasterd` if [ "$pid" -eq "$curpid" ]; then exit 0 else exit 1 fi else exit 1 fi esac exit 0 - -# $Id$ diff --git a/examples/etc/init.d/sleeper b/examples/etc/init.d/sleeper index 63f7e9c2e..c60a5555c 100755 --- a/examples/etc/init.d/sleeper +++ b/examples/etc/init.d/sleeper @@ -1,72 +1,70 @@ #!/bin/bash -# $Id$ - script=$0 path=`echo $script | sed 's/etc..*/bin/'` PATH=$PATH:$path ps=`facter ps` if [ -z "$ps" ]; then ps="ps -ef" fi function start { cd $path ./sleeper } function stop { #if [ -n `which pgrep` ]; then # pid=`pgrep sleeper` #else pid=`$ps | grep -v grep | grep sleeper | grep ruby | awk '{print $2}'` #fi if [ -n "$pid" ]; then kill $pid fi } function restart { stop start } function status { #if [ -n `which pgrep` ]; then # cmd="pgrep sleeper" #else #cmd="$ps | grep -v grep | grep sleeper | grep ruby | awk '{print $2}'" #fi #$cmd $ps | grep -v grep | grep sleeper | grep ruby } case "$1" in start) start ;; stop) stop ;; restart) stop; start ;; status) output=`status` #status exit $? ;; *) echo "Usage: $N {start|stop|restart|force-reload}" >&2 exit 1 ;; esac exit 0 diff --git a/examples/modules/sample-module/README.txt b/examples/modules/sample-module/README.txt index cd35c83a1..233e54b8d 100644 --- a/examples/modules/sample-module/README.txt +++ b/examples/modules/sample-module/README.txt @@ -1,17 +1,17 @@ Jeff McCune 2007-08-14 This small, sample module demonstrates how to extend the puppet language with a new parser function. See: manifests/init.pp lib/puppet/parser/functions/hostname_to_dn.rb templates/sample.erb Note the consistent naming of files for Puppet::Util::Autoload Reference Documents: -http://reductivelabs.com/trac/puppet/wiki/ModuleOrganisation +http://docs.puppetlabs.com/guides/modules.html http://docs.puppetlabs.com/guides/custom_functions.html -http://reductivelabs.com/trac/puppet/wiki/FunctionReference +http://docs.puppetlabs.com/references/latest/function.html diff --git a/ext/puppetstoredconfigclean.rb b/ext/puppetstoredconfigclean.rb index 16f39efa1..dcbefa816 100644 --- a/ext/puppetstoredconfigclean.rb +++ b/ext/puppetstoredconfigclean.rb @@ -1,91 +1,91 @@ #!/usr/bin/env ruby # Script to clean up stored configs for (a) given host(s) # # Credits: -# Script was taken from http://reductivelabs.com/trac/puppet/attachment/wiki/UsingStoredConfiguration/kill_node_in_storedconfigs_db.rb +# Script was taken from http://reductivelabs.com/trac/puppet/attachment/wiki/UsingStoredConfiguration/kill_node_in_storedconfigs_db.rb (link no longer valid), # which haven been initially posted by James Turnbull # duritong adapted and improved the script a bit. require 'getoptlong' config = '/etc/puppet/puppet.conf' def printusage(error_code) puts "Usage: #{$0} [ list of hostnames as stored in hosts table ]" puts "\n Options:" puts "--config " exit(error_code) end opts = GetoptLong.new( [ "--config", "-c", GetoptLong::REQUIRED_ARGUMENT ], [ "--help", "-h", GetoptLong::NO_ARGUMENT ], [ "--usage", "-u", GetoptLong::NO_ARGUMENT ], [ "--version", "-v", GetoptLong::NO_ARGUMENT ] ) begin opts.each do |opt, arg| case opt when "--config" config = arg when "--help" printusage(0) when "--usage" printusage(0) when "--version" puts "#{Puppet.version}" exit end end rescue GetoptLong::InvalidOption => detail $stderr.puts "Try '#{$0} --help'" exit(1) end printusage(1) unless ARGV.size > 0 require 'puppet/rails' Puppet[:config] = config Puppet.parse_config pm_conf = Puppet.settings.instance_variable_get(:@values)[:master] adapter = pm_conf[:dbadapter] args = {:adapter => adapter, :log_level => pm_conf[:rails_loglevel]} case adapter when "sqlite3" args[:dbfile] = pm_conf[:dblocation] when "mysql", "postgresql" args[:host] = pm_conf[:dbserver] unless pm_conf[:dbserver].to_s.empty? args[:username] = pm_conf[:dbuser] unless pm_conf[:dbuser].to_s.empty? args[:password] = pm_conf[:dbpassword] unless pm_conf[:dbpassword].to_s.empty? args[:database] = pm_conf[:dbname] unless pm_conf[:dbname].to_s.empty? args[:port] = pm_conf[:dbport] unless pm_conf[:dbport].to_s.empty? socket = pm_conf[:dbsocket] args[:socket] = socket unless socket.to_s.empty? else raise ArgumentError, "Invalid db adapter #{adapter}" end args[:database] = "puppet" unless not args[:database].to_s.empty? ActiveRecord::Base.establish_connection(args) ARGV.each { |hostname| if @host = Puppet::Rails::Host.find_by_name(hostname.strip) print "Killing #{hostname}..." $stdout.flush @host.destroy puts "done." else puts "Can't find host #{hostname}." end } exit 0 diff --git a/install.rb b/install.rb index 7627a8d11..e8755e07a 100755 --- a/install.rb +++ b/install.rb @@ -1,496 +1,491 @@ #! /usr/bin/env ruby #-- # Copyright 2004 Austin Ziegler # Install utility. Based on the original installation script for rdoc by the # Pragmatic Programmers. # # This program is free software. It may be redistributed and/or modified under # the terms of the GPL version 2 (or later) or the Ruby licence. # # Usage # ----- # In most cases, if you have a typical project layout, you will need to do # absolutely nothing to make this work for you. This layout is: # # bin/ # executable files -- "commands" # lib/ # the source of the library # tests/ # unit tests # # The default behaviour: # 1) Run all unit test files (ending in .rb) found in all directories under # tests/. # 2) Build Rdoc documentation from all files in bin/ (excluding .bat and .cmd), # all .rb files in lib/, ./README, ./ChangeLog, and ./Install. # 3) Build ri documentation from all files in bin/ (excluding .bat and .cmd), # and all .rb files in lib/. This is disabled by default on Microsoft Windows. # 4) Install commands from bin/ into the Ruby bin directory. On Windows, if a # if a corresponding batch file (.bat or .cmd) exists in the bin directory, # it will be copied over as well. Otherwise, a batch file (always .bat) will # be created to run the specified command. # 5) Install all library files ending in .rb from lib/ into Ruby's # site_lib/version directory. # #++ require 'rbconfig' require 'find' require 'fileutils' begin require 'ftools' # apparently on some system ftools doesn't get loaded $haveftools = true rescue LoadError puts "ftools not found. Using FileUtils instead.." $haveftools = false end require 'optparse' require 'ostruct' begin require 'rdoc/rdoc' $haverdoc = true rescue LoadError puts "Missing rdoc; skipping documentation" $haverdoc = false end begin if $haverdoc ronn = %x{which ronn} $haveman = true else $haveman = false end rescue puts "Missing ronn; skipping man page creation" $haveman = false end PREREQS = %w{openssl facter xmlrpc/client xmlrpc/server cgi} MIN_FACTER_VERSION = 1.5 InstallOptions = OpenStruct.new def glob(list) g = list.map { |i| Dir.glob(i) } g.flatten! g.compact! g.reject! { |e| e =~ /\.svn/ } g end # Set these values to what you want installed. configs = glob(%w{conf/auth.conf}) sbins = glob(%w{sbin/*}) bins = glob(%w{bin/*}) rdoc = glob(%w{bin/* sbin/* lib/**/*.rb README README-library CHANGELOG TODO Install}).reject { |e| e=~ /\.(bat|cmd)$/ } ri = glob(%w{bin/*.rb sbin/* lib/**/*.rb}).reject { |e| e=~ /\.(bat|cmd)$/ } man = glob(%w{man/man[0-9]/*}) libs = glob(%w{lib/**/*.rb lib/**/*.py lib/puppet/util/command_line/*}) tests = glob(%w{test/**/*.rb}) def do_configs(configs, target, strip = 'conf/') Dir.mkdir(target) unless File.directory? target configs.each do |cf| ocf = File.join(InstallOptions.config_dir, cf.gsub(/#{strip}/, '')) File.install(cf, ocf, 0644, true) end end def do_bins(bins, target, strip = 's?bin/') Dir.mkdir(target) unless File.directory? target bins.each do |bf| obf = bf.gsub(/#{strip}/, '') install_binfile(bf, obf, target) end end def do_libs(libs, strip = 'lib/') libs.each do |lf| olf = File.join(InstallOptions.site_dir, lf.gsub(/#{strip}/, '')) op = File.dirname(olf) if $haveftools File.makedirs(op, true) File.chmod(0755, op) File.install(lf, olf, 0644, true) else FileUtils.makedirs(op, {:mode => 0755, :verbose => true}) FileUtils.chmod(0755, op) FileUtils.install(lf, olf, {:mode => 0644, :verbose => true}) end end end def do_man(man, strip = 'man/') man.each do |mf| omf = File.join(InstallOptions.man_dir, mf.gsub(/#{strip}/, '')) om = File.dirname(omf) if $haveftools File.makedirs(om, true) File.chmod(0755, om) File.install(mf, omf, 0644, true) else FileUtils.makedirs(om, {:mode => 0755, :verbose => true}) FileUtils.chmod(0755, om) FileUtils.install(mf, omf, {:mode => 0644, :verbose => true}) end gzip = %x{which gzip} gzip.chomp! %x{#{gzip} -f #{omf}} end end # Verify that all of the prereqs are installed def check_prereqs PREREQS.each { |pre| begin require pre if pre == "facter" # to_f isn't quite exact for strings like "1.5.1" but is good # enough for this purpose. facter_version = Facter.version.to_f if facter_version < MIN_FACTER_VERSION puts "Facter version: #{facter_version}; minimum required: #{MIN_FACTER_VERSION}; cannot install" exit -1 end end rescue LoadError puts "Could not load #{pre}; cannot install" exit -1 end } end ## # Prepare the file installation. # def prepare_installation $operatingsystem = Facter["operatingsystem"].value InstallOptions.configs = true # Only try to do docs if we're sure they have rdoc if $haverdoc InstallOptions.rdoc = true InstallOptions.ri = $operatingsystem != "windows" else InstallOptions.rdoc = false InstallOptions.ri = false end if $haveman InstallOptions.man = true if $operatingsystem == "windows" InstallOptions.man = false end else InstallOptions.man = false end InstallOptions.tests = true ARGV.options do |opts| opts.banner = "Usage: #{File.basename($0)} [options]" opts.separator "" opts.on('--[no-]rdoc', 'Prevents the creation of RDoc output.', 'Default on.') do |onrdoc| InstallOptions.rdoc = onrdoc end opts.on('--[no-]ri', 'Prevents the creation of RI output.', 'Default off on mswin32.') do |onri| InstallOptions.ri = onri end opts.on('--[no-]man', 'Prevents the creation of man pages.', 'Default on.') do |onman| InstallOptions.man = onman end opts.on('--[no-]tests', 'Prevents the execution of unit tests.', 'Default on.') do |ontest| InstallOptions.tests = ontest end opts.on('--[no-]configs', 'Prevents the installation of config files', 'Default off.') do |ontest| InstallOptions.configs = ontest end opts.on('--destdir[=OPTIONAL]', 'Installation prefix for all targets', 'Default essentially /') do |destdir| InstallOptions.destdir = destdir end opts.on('--configdir[=OPTIONAL]', 'Installation directory for config files', 'Default /etc/puppet') do |configdir| InstallOptions.configdir = configdir end opts.on('--bindir[=OPTIONAL]', 'Installation directory for binaries', 'overrides Config::CONFIG["bindir"]') do |bindir| InstallOptions.bindir = bindir end opts.on('--sbindir[=OPTIONAL]', 'Installation directory for system binaries', 'overrides Config::CONFIG["sbindir"]') do |sbindir| InstallOptions.sbindir = sbindir end opts.on('--sitelibdir[=OPTIONAL]', 'Installation directory for libraries', 'overrides Config::CONFIG["sitelibdir"]') do |sitelibdir| InstallOptions.sitelibdir = sitelibdir end opts.on('--mandir[=OPTIONAL]', 'Installation directory for man pages', 'overrides Config::CONFIG["mandir"]') do |mandir| InstallOptions.mandir = mandir end opts.on('--quick', 'Performs a quick installation. Only the', 'installation is done.') do |quick| InstallOptions.rdoc = false InstallOptions.ri = false InstallOptions.tests = false InstallOptions.configs = true end opts.on('--full', 'Performs a full installation. All', 'optional installation steps are run.') do |full| InstallOptions.rdoc = true InstallOptions.man = true InstallOptions.ri = true InstallOptions.tests = true InstallOptions.configs = true end opts.separator("") opts.on_tail('--help', "Shows this help text.") do $stderr.puts opts exit end opts.parse! end tmpdirs = [ENV['TMP'], ENV['TEMP'], "/tmp", "/var/tmp", "."] version = [Config::CONFIG["MAJOR"], Config::CONFIG["MINOR"]].join(".") libdir = File.join(Config::CONFIG["libdir"], "ruby", version) # Mac OS X 10.5 and higher declare bindir and sbindir as # /System/Library/Frameworks/Ruby.framework/Versions/1.8/usr/bin # /System/Library/Frameworks/Ruby.framework/Versions/1.8/usr/sbin # which is not generally where people expect executables to be installed # These settings are appropriate defaults for all OS X versions. if RUBY_PLATFORM =~ /^universal-darwin[\d\.]+$/ Config::CONFIG['bindir'] = "/usr/bin" Config::CONFIG['sbindir'] = "/usr/sbin" end if not InstallOptions.configdir.nil? configdir = InstallOptions.configdir else configdir = "/etc/puppet" end if not InstallOptions.bindir.nil? bindir = InstallOptions.bindir else bindir = Config::CONFIG['bindir'] end if not InstallOptions.sbindir.nil? sbindir = InstallOptions.sbindir else sbindir = Config::CONFIG['sbindir'] end if not InstallOptions.sitelibdir.nil? sitelibdir = InstallOptions.sitelibdir else sitelibdir = Config::CONFIG["sitelibdir"] if sitelibdir.nil? sitelibdir = $LOAD_PATH.find { |x| x =~ /site_ruby/ } if sitelibdir.nil? sitelibdir = File.join(libdir, "site_ruby") elsif sitelibdir !~ Regexp.quote(version) sitelibdir = File.join(sitelibdir, version) end end end if not InstallOptions.mandir.nil? mandir = InstallOptions.mandir else mandir = Config::CONFIG['mandir'] end - # To be deprecated once people move over to using --destdir option - if (destdir = ENV['DESTDIR']) - configdir = "#{destdir}#{configdir}" - bindir = "#{destdir}#{bindir}" - sbindir = "#{destdir}#{sbindir}" - mandir = "#{destdir}#{mandir}" - sitelibdir = "#{destdir}#{sitelibdir}" - - FileUtils.makedirs(configdir) if InstallOptions.configs - FileUtils.makedirs(bindir) - FileUtils.makedirs(sbindir) - FileUtils.makedirs(mandir) - FileUtils.makedirs(sitelibdir) # This is the new way forward - elsif (destdir = InstallOptions.destdir) - configdir = "#{destdir}#{configdir}" - bindir = "#{destdir}#{bindir}" - sbindir = "#{destdir}#{sbindir}" - mandir = "#{destdir}#{mandir}" - sitelibdir = "#{destdir}#{sitelibdir}" - - FileUtils.makedirs(configdir) if InstallOptions.configs - FileUtils.makedirs(bindir) - FileUtils.makedirs(sbindir) - FileUtils.makedirs(mandir) - FileUtils.makedirs(sitelibdir) + if not InstallOptions.destdir.nil? + destdir = InstallOptions.destdir + # To be deprecated once people move over to using --destdir option + elsif ENV['DESTDIR'] != nil? + destdir = ENV['DESTDIR'] + warn "DESTDIR is deprecated. Use --destdir instead." + else + destdir = '' end + configdir = "#{destdir}#{configdir}" + bindir = "#{destdir}#{bindir}" + sbindir = "#{destdir}#{sbindir}" + mandir = "#{destdir}#{mandir}" + sitelibdir = "#{destdir}#{sitelibdir}" + + FileUtils.makedirs(configdir) if InstallOptions.configs + FileUtils.makedirs(bindir) + FileUtils.makedirs(sbindir) + FileUtils.makedirs(mandir) + FileUtils.makedirs(sitelibdir) + tmpdirs << bindir InstallOptions.tmp_dirs = tmpdirs.compact InstallOptions.site_dir = sitelibdir InstallOptions.config_dir = configdir InstallOptions.bin_dir = bindir InstallOptions.sbin_dir = sbindir InstallOptions.lib_dir = libdir InstallOptions.man_dir = mandir end ## # Build the rdoc documentation. Also, try to build the RI documentation. # def build_rdoc(files) return unless $haverdoc begin r = RDoc::RDoc.new r.document(["--main", "README", "--title", "Puppet -- Site Configuration Management", "--line-numbers"] + files) rescue RDoc::RDocError => e $stderr.puts e.message rescue Exception => e $stderr.puts "Couldn't build RDoc documentation\n#{e.message}" end end def build_ri(files) return unless $haverdoc begin ri = RDoc::RDoc.new #ri.document(["--ri-site", "--merge"] + files) ri.document(["--ri-site"] + files) rescue RDoc::RDocError => e $stderr.puts e.message rescue Exception => e $stderr.puts "Couldn't build Ri documentation\n#{e.message}" $stderr.puts "Continuing with install..." end end def build_man(bins, sbins) return unless $haveman begin # Locate ronn ronn = %x{which ronn} ronn.chomp! # Create puppet.conf.5 man page %x{bin/puppetdoc --reference configuration > ./man/man5/puppetconf.5.ronn} %x{#{ronn} -r ./man/man5/puppetconf.5.ronn} File.move("./man/man5/puppetconf.5", "./man/man5/puppet.conf.5") File.unlink("./man/man5/puppetconf.5.ronn") # Create binary man pages binary = bins + sbins binary.each do |bin| b = bin.gsub( /(bin|sbin)\//, "") %x{#{bin} --help > ./man/man8/#{b}.8.ronn} %x{#{ronn} -r ./man/man8/#{b}.8.ronn} File.unlink("./man/man8/#{b}.8.ronn") end rescue SystemCallError $stderr.puts "Couldn't build man pages: " + $ERROR_INFO $stderr.puts "Continuing with install..." end end def run_tests(test_list) require 'test/unit/ui/console/testrunner' $LOAD_PATH.unshift "lib" test_list.each do |test| next if File.directory?(test) require test end tests = [] ObjectSpace.each_object { |o| tests << o if o.kind_of?(Class) } tests.delete_if { |o| !o.ancestors.include?(Test::Unit::TestCase) } tests.delete_if { |o| o == Test::Unit::TestCase } tests.each { |test| Test::Unit::UI::Console::TestRunner.run(test) } $LOAD_PATH.shift rescue LoadError puts "Missing testrunner library; skipping tests" end ## # Install file(s) from ./bin to Config::CONFIG['bindir']. Patch it on the way # to insert a #! line; on a Unix install, the command is named as expected # (e.g., bin/rdoc becomes rdoc); the shebang line handles running it. Under # windows, we add an '.rb' extension and let file associations do their stuff. def install_binfile(from, op_file, target) tmp_dir = nil InstallOptions.tmp_dirs.each do |t| if File.directory?(t) and File.writable?(t) tmp_dir = t break end end fail "Cannot find a temporary directory" unless tmp_dir tmp_file = File.join(tmp_dir, '_tmp') ruby = File.join(Config::CONFIG['bindir'], Config::CONFIG['ruby_install_name']) File.open(from) do |ip| File.open(tmp_file, "w") do |op| ruby = File.join(Config::CONFIG['bindir'], Config::CONFIG['ruby_install_name']) op.puts "#!#{ruby}" contents = ip.readlines contents.shift if contents[0] =~ /^#!/ op.write contents.join end end if $operatingsystem == "windows" installed_wrapper = false if File.exists?("#{from}.bat") FileUtils.install("#{from}.bat", File.join(target, "#{op_file}.bat"), :mode => 0755, :verbose => true) installed_wrapper = true end if File.exists?("#{from}.cmd") FileUtils.install("#{from}.cmd", File.join(target, "#{op_file}.cmd"), :mode => 0755, :verbose => true) installed_wrapper = true end if not installed_wrapper tmp_file2 = File.join(tmp_dir, '_tmp_wrapper') cwn = File.join(Config::CONFIG['bindir'], op_file) cwv = CMD_WRAPPER.gsub('', ruby.gsub(%r{/}) { "\\" }).gsub!('', cwn.gsub(%r{/}) { "\\" } ) File.open(tmp_file2, "wb") { |cw| cw.puts cwv } FileUtils.install(tmp_file2, File.join(target, "#{op_file}.bat"), :mode => 0755, :verbose => true) File.unlink(tmp_file2) installed_wrapper = true end end FileUtils.install(tmp_file, File.join(target, op_file), :mode => 0755, :verbose => true) File.unlink(tmp_file) end CMD_WRAPPER = <<-EOS @echo off if "%OS%"=="Windows_NT" goto WinNT -x "" %1 %2 %3 %4 %5 %6 %7 %8 %9 goto done :WinNT -x "" %* goto done :done EOS check_prereqs prepare_installation #run_tests(tests) if InstallOptions.tests #build_rdoc(rdoc) if InstallOptions.rdoc #build_ri(ri) if InstallOptions.ri #build_man(bins, sbins) if InstallOptions.man do_configs(configs, InstallOptions.config_dir) if InstallOptions.configs do_bins(sbins, InstallOptions.sbin_dir) do_bins(bins, InstallOptions.bin_dir) do_libs(libs) do_man(man) unless $operatingsystem == "windows" diff --git a/lib/puppet/application/agent.rb b/lib/puppet/application/agent.rb index 2b75505fd..3749241f8 100644 --- a/lib/puppet/application/agent.rb +++ b/lib/puppet/application/agent.rb @@ -1,270 +1,270 @@ require 'puppet/application' class Puppet::Application::Agent < Puppet::Application should_parse_config run_mode :agent attr_accessor :args, :agent, :daemon, :host def preinit # Do an initial trap, so that cancels don't get a stack trace. - trap(:INT) do + Signal.trap(:INT) do $stderr.puts "Cancelling startup" exit(0) end { :waitforcert => nil, :detailed_exitcodes => false, :verbose => false, :debug => false, :centrallogs => false, :setdest => false, :enable => false, :disable => false, :client => true, :fqdn => nil, :serve => [], :digest => :MD5, :fingerprint => false, }.each do |opt,val| options[opt] = val end @args = {} require 'puppet/daemon' @daemon = Puppet::Daemon.new @daemon.argv = ARGV.dup end option("--centrallogging") option("--disable") option("--enable") option("--debug","-d") option("--fqdn FQDN","-f") option("--test","-t") option("--verbose","-v") option("--fingerprint") option("--digest DIGEST") option("--serve HANDLER", "-s") do |arg| if Puppet::Network::Handler.handler(arg) options[:serve] << arg.to_sym else raise "Could not find handler for #{arg}" end end option("--no-client") do |arg| options[:client] = false end option("--detailed-exitcodes") do |arg| options[:detailed_exitcodes] = true end option("--logdest DEST", "-l DEST") do |arg| begin Puppet::Util::Log.newdestination(arg) options[:setdest] = true rescue => detail puts detail.backtrace if Puppet[:debug] $stderr.puts detail.to_s end end option("--waitforcert WAITFORCERT", "-w") do |arg| options[:waitforcert] = arg.to_i end option("--port PORT","-p") do |arg| @args[:Port] = arg end def run_command return fingerprint if options[:fingerprint] return onetime if Puppet[:onetime] main end def fingerprint unless cert = host.certificate || host.certificate_request $stderr.puts "Fingerprint asked but no certificate nor certificate request have yet been issued" exit(1) return end unless fingerprint = cert.fingerprint(options[:digest]) raise ArgumentError, "Could not get fingerprint for digest '#{options[:digest]}'" end Puppet.notice fingerprint end def onetime unless options[:client] $stderr.puts "onetime is specified but there is no client" exit(43) return end @daemon.set_signal_traps begin report = @agent.run rescue => detail puts detail.backtrace if Puppet[:trace] Puppet.err detail.to_s end if not report exit(1) - elsif not Puppet[:noop] and options[:detailed_exitcodes] then + elsif options[:detailed_exitcodes] then exit(report.exit_status) else exit(0) end end def main Puppet.notice "Starting Puppet client version #{Puppet.version}" @daemon.start end # Enable all of the most common test options. def setup_test Puppet.settings.handlearg("--ignorecache") Puppet.settings.handlearg("--no-usecacheonfailure") Puppet.settings.handlearg("--no-splay") Puppet.settings.handlearg("--show_diff") Puppet.settings.handlearg("--no-daemonize") options[:verbose] = true Puppet[:onetime] = true options[:detailed_exitcodes] = true end # Handle the logging settings. def setup_logs if options[:debug] or options[:verbose] Puppet::Util::Log.newdestination(:console) if options[:debug] Puppet::Util::Log.level = :debug else Puppet::Util::Log.level = :info end end Puppet::Util::Log.newdestination(:syslog) unless options[:setdest] end def enable_disable_client(agent) if options[:enable] agent.enable elsif options[:disable] agent.disable end exit(0) end def setup_listen unless FileTest.exists?(Puppet[:authconfig]) Puppet.err "Will not start without authorization file #{Puppet[:authconfig]}" exit(14) end handlers = nil if options[:serve].empty? handlers = [:Runner] else handlers = options[:serve] end require 'puppet/network/server' # No REST handlers yet. server = Puppet::Network::Server.new(:xmlrpc_handlers => handlers, :port => Puppet[:puppetport]) @daemon.server = server end def setup_host @host = Puppet::SSL::Host.new waitforcert = options[:waitforcert] || (Puppet[:onetime] ? 0 : 120) cert = @host.wait_for_cert(waitforcert) unless options[:fingerprint] end def setup setup_test if options[:test] setup_logs exit(Puppet.settings.print_configs ? 0 : 1) if Puppet.settings.print_configs? # If noop is set, then also enable diffs Puppet[:show_diff] = true if Puppet[:noop] args[:Server] = Puppet[:server] if options[:fqdn] args[:FQDN] = options[:fqdn] Puppet[:certname] = options[:fqdn] end if options[:centrallogs] logdest = args[:Server] logdest += ":" + args[:Port] if args.include?(:Port) Puppet::Util::Log.newdestination(logdest) end Puppet.settings.use :main, :agent, :ssl # Always ignoreimport for agent. It really shouldn't even try to import, # but this is just a temporary band-aid. Puppet[:ignoreimport] = true # We need to specify a ca location for all of the SSL-related i # indirected classes to work; in fingerprint mode we just need # access to the local files and we don't need a ca. Puppet::SSL::Host.ca_location = options[:fingerprint] ? :none : :remote Puppet::Transaction::Report.terminus_class = :rest # Override the default; puppetd needs this, usually. # You can still override this on the command-line with, e.g., :compiler. Puppet[:catalog_terminus] = :rest # Override the default. Puppet[:facts_terminus] = :facter Puppet::Resource::Catalog.cache_class = :yaml # We need tomake the client either way, we just don't start it # if --no-client is set. require 'puppet/agent' require 'puppet/configurer' @agent = Puppet::Agent.new(Puppet::Configurer) enable_disable_client(@agent) if options[:enable] or options[:disable] @daemon.agent = agent if options[:client] # It'd be nice to daemonize later, but we have to daemonize before the # waitforcert happens. @daemon.daemonize if Puppet[:daemonize] setup_host @objects = [] # This has to go after the certs are dealt with. if Puppet[:listen] unless Puppet[:onetime] setup_listen else Puppet.notice "Ignoring --listen on onetime run" end end end end diff --git a/lib/puppet/application/apply.rb b/lib/puppet/application/apply.rb index 33a70ce8a..cc733e1f5 100644 --- a/lib/puppet/application/apply.rb +++ b/lib/puppet/application/apply.rb @@ -1,157 +1,157 @@ require 'puppet/application' class Puppet::Application::Apply < Puppet::Application should_parse_config option("--debug","-d") option("--execute EXECUTE","-e") do |arg| options[:code] = arg end option("--loadclasses","-L") option("--verbose","-v") option("--use-nodes") option("--detailed-exitcodes") option("--apply catalog", "-a catalog") do |arg| options[:catalog] = arg end option("--logdest LOGDEST", "-l") do |arg| begin Puppet::Util::Log.newdestination(arg) options[:logset] = true rescue => detail $stderr.puts detail.to_s end end def run_command if options[:catalog] apply elsif Puppet[:parseonly] parseonly else main end end def apply if options[:catalog] == "-" text = $stdin.read else text = File.read(options[:catalog]) end begin catalog = Puppet::Resource::Catalog.convert_from(Puppet::Resource::Catalog.default_format,text) catalog = Puppet::Resource::Catalog.pson_create(catalog) unless catalog.is_a?(Puppet::Resource::Catalog) rescue => detail raise Puppet::Error, "Could not deserialize catalog from pson: #{detail}" end catalog = catalog.to_ral require 'puppet/configurer' configurer = Puppet::Configurer.new configurer.run :catalog => catalog end def parseonly # Set our code or file to use. if options[:code] or command_line.args.length == 0 Puppet[:code] = options[:code] || STDIN.read else Puppet[:manifest] = command_line.args.shift end begin Puppet::Node::Environment.new(Puppet[:environment]).known_resource_types rescue => detail Puppet.err detail exit 1 end exit 0 end def main # Set our code or file to use. if options[:code] or command_line.args.length == 0 Puppet[:code] = options[:code] || STDIN.read else manifest = command_line.args.shift raise "Could not find file #{manifest}" unless File.exist?(manifest) Puppet.warning("Only one file can be applied per run. Skipping #{command_line.args.join(', ')}") if command_line.args.size > 0 Puppet[:manifest] = manifest end # Collect our facts. unless facts = Puppet::Node::Facts.find(Puppet[:certname]) raise "Could not find facts for #{Puppet[:certname]}" end # Find our Node unless node = Puppet::Node.find(Puppet[:certname]) raise "Could not find node #{Puppet[:certname]}" end # Merge in the facts. node.merge(facts.values) # Allow users to load the classes that puppet agent creates. if options[:loadclasses] file = Puppet[:classfile] if FileTest.exists?(file) unless FileTest.readable?(file) $stderr.puts "#{file} is not readable" exit(63) end node.classes = File.read(file).split(/[\s\n]+/) end end begin # Compile our catalog starttime = Time.now catalog = Puppet::Resource::Catalog.find(node.name, :use_node => node) # Translate it to a RAL catalog catalog = catalog.to_ral catalog.finalize catalog.retrieval_duration = Time.now - starttime require 'puppet/configurer' configurer = Puppet::Configurer.new report = configurer.run(:skip_plugin_download => true, :catalog => catalog) - exit( Puppet[:noop] ? 0 : options[:detailed_exitcodes] ? report.exit_status : 0 ) + exit( options[:detailed_exitcodes] ? report.exit_status : 0 ) rescue => detail puts detail.backtrace if Puppet[:trace] $stderr.puts detail.message exit(1) end end def setup exit(Puppet.settings.print_configs ? 0 : 1) if Puppet.settings.print_configs? # If noop is set, then also enable diffs Puppet[:show_diff] = true if Puppet[:noop] Puppet::Util::Log.newdestination(:console) unless options[:logset] client = nil server = nil - trap(:INT) do + Signal.trap(:INT) do $stderr.puts "Exiting" exit(1) end if options[:debug] Puppet::Util::Log.level = :debug elsif options[:verbose] Puppet::Util::Log.level = :info end end end diff --git a/lib/puppet/application/cert.rb b/lib/puppet/application/cert.rb index 467b0c859..ee59b7e56 100644 --- a/lib/puppet/application/cert.rb +++ b/lib/puppet/application/cert.rb @@ -1,85 +1,101 @@ require 'puppet/application' class Puppet::Application::Cert < Puppet::Application should_parse_config run_mode :master - attr_accessor :cert_mode, :all, :ca, :digest, :signed + attr_accessor :all, :ca, :digest, :signed - def find_mode(opt) - require 'puppet/ssl/certificate_authority' - modes = Puppet::SSL::CertificateAuthority::Interface::INTERFACE_METHODS - tmp = opt.sub("--", '').to_sym - @cert_mode = modes.include?(tmp) ? tmp : nil + def subcommand + @subcommand + end + def subcommand=(name) + # Handle the nasty, legacy mapping of "clean" to "destroy". + sub = name.to_sym + @subcommand = (sub == :clean ? :destroy : sub) end option("--clean", "-c") do - @cert_mode = :destroy + self.subcommand = "destroy" end option("--all", "-a") do @all = true end option("--digest DIGEST") do |arg| @digest = arg end option("--signed", "-s") do @signed = true end option("--debug", "-d") do |arg| Puppet::Util::Log.level = :debug end require 'puppet/ssl/certificate_authority/interface' Puppet::SSL::CertificateAuthority::Interface::INTERFACE_METHODS.reject {|m| m == :destroy }.each do |method| option("--#{method}", "-#{method.to_s[0,1]}") do - find_mode("--#{method}") + self.subcommand = method end end option("--verbose", "-v") do Puppet::Util::Log.level = :info end def main if @all hosts = :all elsif @signed hosts = :signed else hosts = command_line.args.collect { |h| h.downcase } end begin - @ca.apply(:revoke, :to => hosts) if @cert_mode == :destroy - @ca.apply(@cert_mode, :to => hosts, :digest => @digest) + @ca.apply(:revoke, :to => hosts) if subcommand == :destroy + @ca.apply(subcommand, :to => hosts, :digest => @digest) rescue => detail puts detail.backtrace if Puppet[:trace] puts detail.to_s exit(24) end end def setup + require 'puppet/ssl/certificate_authority' exit(Puppet.settings.print_configs ? 0 : 1) if Puppet.settings.print_configs? Puppet::Util::Log.newdestination :console - if [:generate, :destroy].include? @cert_mode + if [:generate, :destroy].include? subcommand Puppet::SSL::Host.ca_location = :local else Puppet::SSL::Host.ca_location = :only end begin @ca = Puppet::SSL::CertificateAuthority.new rescue => detail puts detail.backtrace if Puppet[:trace] puts detail.to_s exit(23) end end + + def parse_options + # handle the bareword subcommand pattern. + result = super + unless self.subcommand then + if sub = self.command_line.args.shift then + self.subcommand = sub + else + help + end + end + result + end end diff --git a/lib/puppet/application/filebucket.rb b/lib/puppet/application/filebucket.rb index 9c3c79bc3..5c91c4f64 100644 --- a/lib/puppet/application/filebucket.rb +++ b/lib/puppet/application/filebucket.rb @@ -1,87 +1,87 @@ require 'puppet/application' class Puppet::Application::Filebucket < Puppet::Application should_not_parse_config option("--bucket BUCKET","-b") option("--debug","-d") option("--local","-l") option("--remote","-r") option("--verbose","-v") attr :args def run_command @args = command_line.args command = args.shift return send(command) if %w{get backup restore}.include? command help end def get md5 = args.shift out = @client.getfile(md5) print out end def backup args.each do |file| unless FileTest.exists?(file) $stderr.puts "#{file}: no such file" next end unless FileTest.readable?(file) $stderr.puts "#{file}: cannot read file" next end md5 = @client.backup(file) puts "#{file}: #{md5}" end end def restore file = args.shift md5 = args.shift @client.restore(file, md5) end def setup Puppet::Log.newdestination(:console) @client = nil @server = nil - trap(:INT) do + Signal.trap(:INT) do $stderr.puts "Cancelling" exit(1) end if options[:debug] Puppet::Log.level = :debug elsif options[:verbose] Puppet::Log.level = :info end # Now parse the config Puppet.parse_config exit(Puppet.settings.print_configs ? 0 : 1) if Puppet.settings.print_configs? require 'puppet/file_bucket/dipper' begin if options[:local] or options[:bucket] path = options[:bucket] || Puppet[:bucketdir] @client = Puppet::FileBucket::Dipper.new(:Path => path) else @client = Puppet::FileBucket::Dipper.new(:Server => Puppet[:server]) end rescue => detail $stderr.puts detail puts detail.backtrace if Puppet[:trace] exit(1) end end end diff --git a/lib/puppet/application/inspect.rb b/lib/puppet/application/inspect.rb index 764c8c4c0..ce32c661c 100644 --- a/lib/puppet/application/inspect.rb +++ b/lib/puppet/application/inspect.rb @@ -1,178 +1,178 @@ require 'puppet' require 'puppet/application' require 'puppet/file_bucket/dipper' class Puppet::Application::Inspect < Puppet::Application should_parse_config run_mode :agent option("--debug","-d") option("--verbose","-v") option("--logdest LOGDEST", "-l") do |arg| begin Puppet::Util::Log.newdestination(arg) options[:logset] = true rescue => detail $stderr.puts detail.to_s end end def help puts <<-HELP ; exit # XXX SYNOPSIS ======== Prepare and submit an inspection report to the puppet master. USAGE ===== puppet inspect DESCRIPTION =========== This command uses the cached catalog from the previous run of 'puppet agent' to determine which attributes of which resources have been marked as auditable with the 'audit' metaparameter. It then examines the current state of the system, writes the state of the specified resource attributes to a report, and submits the report to the puppet master. Puppet inspect does not run as a daemon, and must be run manually or from cron. OPTIONS ======= Any configuration setting which is valid in the configuration file is also a valid long argument, e.g. '--server=master.domain.com'. See the configuration file documentation at http://docs.puppetlabs.com/references/latest/configuration.html for the full list of acceptable settings. AUTHOR ====== Puppet Labs COPYRIGHT ========= Copyright (c) 2011 Puppet Labs, LLC Licensed under the GNU General Public License version 2 HELP end def setup exit(Puppet.settings.print_configs ? 0 : 1) if Puppet.settings.print_configs? raise "Inspect requires reporting to be enabled. Set report=true in puppet.conf to enable reporting." unless Puppet[:report] @report = Puppet::Transaction::Report.new("inspect") Puppet::Util::Log.newdestination(@report) Puppet::Util::Log.newdestination(:console) unless options[:logset] - trap(:INT) do + Signal.trap(:INT) do $stderr.puts "Exiting" exit(1) end if options[:debug] Puppet::Util::Log.level = :debug elsif options[:verbose] Puppet::Util::Log.level = :info end Puppet::Transaction::Report.terminus_class = :rest Puppet::Resource::Catalog.terminus_class = :yaml end def run_command benchmark(:notice, "Finished inspection") do retrieval_starttime = Time.now unless catalog = Puppet::Resource::Catalog.find(Puppet[:certname]) raise "Could not find catalog for #{Puppet[:certname]}" end @report.configuration_version = catalog.version inspect_starttime = Time.now @report.add_times("config_retrieval", inspect_starttime - retrieval_starttime) if Puppet[:archive_files] dipper = Puppet::FileBucket::Dipper.new(:Server => Puppet[:archive_file_server]) end catalog.to_ral.resources.each do |ral_resource| audited_attributes = ral_resource[:audit] next unless audited_attributes status = Puppet::Resource::Status.new(ral_resource) begin audited_resource = ral_resource.to_resource rescue StandardError => detail puts detail.backtrace if Puppet[:trace] ral_resource.err "Could not inspect #{ral_resource}; skipping: #{detail}" audited_attributes.each do |name| event = ral_resource.event( :property => name, :status => "failure", :audited => true, :message => "failed to inspect #{name}" ) status.add_event(event) end else audited_attributes.each do |name| next if audited_resource[name].nil? # Skip :absent properties of :absent resources. Really, it would be nicer if the RAL returned nil for those, but it doesn't. ~JW if name == :ensure or audited_resource[:ensure] != :absent or audited_resource[name] != :absent event = ral_resource.event( :previous_value => audited_resource[name], :property => name, :status => "audit", :audited => true, :message => "inspected value is #{audited_resource[name].inspect}" ) status.add_event(event) end end end if Puppet[:archive_files] and ral_resource.type == :file and audited_attributes.include?(:content) path = ral_resource[:path] if File.readable?(path) begin dipper.backup(path) rescue StandardError => detail Puppet.warning detail end end end @report.add_resource_status(status) end finishtime = Time.now @report.add_times("inspect", finishtime - inspect_starttime) @report.finalize_report begin @report.save rescue => detail puts detail.backtrace if Puppet[:trace] Puppet.err "Could not send report: #{detail}" end end end end diff --git a/lib/puppet/application/kick.rb b/lib/puppet/application/kick.rb index 37aeb1ef2..b3c95e21d 100644 --- a/lib/puppet/application/kick.rb +++ b/lib/puppet/application/kick.rb @@ -1,212 +1,212 @@ require 'puppet/application' class Puppet::Application::Kick < Puppet::Application should_not_parse_config attr_accessor :hosts, :tags, :classes option("--all","-a") option("--foreground","-f") option("--debug","-d") option("--ping","-P") option("--test") option("--host HOST") do |arg| @hosts << arg end option("--tag TAG", "-t") do |arg| @tags << arg end option("--class CLASS", "-c") do |arg| @classes << arg end option("--no-fqdn", "-n") do |arg| options[:fqdn] = false end option("--parallel PARALLEL", "-p") do |arg| begin options[:parallel] = Integer(arg) rescue $stderr.puts "Could not convert #{arg.inspect} to an integer" exit(23) end end def run_command @hosts += command_line.args options[:test] ? test : main end def test puts "Skipping execution in test mode" exit(0) end def main require 'puppet/network/client' Puppet.warning "Failed to load ruby LDAP library. LDAP functionality will not be available" unless Puppet.features.ldap? require 'puppet/util/ldap/connection' todo = @hosts.dup failures = [] # Now do the actual work go = true while go # If we don't have enough children in process and we still have hosts left to # do, then do the next host. if @children.length < options[:parallel] and ! todo.empty? host = todo.shift pid = fork do run_for_host(host) end @children[pid] = host else # Else, see if we can reap a process. begin pid = Process.wait if host = @children[pid] # Remove our host from the list of children, so the parallelization # continues working. @children.delete(pid) failures << host if $CHILD_STATUS.exitstatus != 0 print "#{host} finished with exit code #{$CHILD_STATUS.exitstatus}\n" else $stderr.puts "Could not find host for PID #{pid} with status #{$CHILD_STATUS.exitstatus}" end rescue Errno::ECHILD # There are no children left, so just exit unless there are still # children left to do. next unless todo.empty? if failures.empty? puts "Finished" exit(0) else puts "Failed: #{failures.join(", ")}" exit(3) end end end end end def run_for_host(host) if options[:ping] out = %x{ping -c 1 #{host}} unless $CHILD_STATUS == 0 $stderr.print "Could not contact #{host}\n" next end end require 'puppet/run' Puppet::Run.indirection.terminus_class = :rest port = Puppet[:puppetport] url = ["https://#{host}:#{port}", "production", "run", host].join('/') print "Triggering #{host}\n" begin run_options = { :tags => @tags, :background => ! options[:foreground], :ignoreschedules => options[:ignoreschedules] } run = Puppet::Run.new( run_options ).save( url ) puts "Getting status" result = run.status puts "status is #{result}" rescue => detail puts detail.backtrace if Puppet[:trace] $stderr.puts "Host #{host} failed: #{detail}\n" exit(2) end case result when "success"; exit(0) when "running" $stderr.puts "Host #{host} is already running" exit(3) else $stderr.puts "Host #{host} returned unknown answer '#{result}'" exit(12) end end def initialize(*args) super @hosts = [] @classes = [] @tags = [] end def preinit [:INT, :TERM].each do |signal| - trap(signal) do + Signal.trap(signal) do $stderr.puts "Cancelling" exit(1) end end options[:parallel] = 1 options[:verbose] = true options[:fqdn] = true options[:ignoreschedules] = false options[:foreground] = false end def setup if options[:debug] Puppet::Util::Log.level = :debug else Puppet::Util::Log.level = :info end # Now parse the config Puppet.parse_config if Puppet[:node_terminus] == "ldap" and (options[:all] or @classes) if options[:all] @hosts = Puppet::Node.search("whatever", :fqdn => options[:fqdn]).collect { |node| node.name } puts "all: #{@hosts.join(", ")}" else @hosts = [] @classes.each do |klass| list = Puppet::Node.search("whatever", :fqdn => options[:fqdn], :class => klass).collect { |node| node.name } puts "#{klass}: #{list.join(", ")}" @hosts += list end end elsif ! @classes.empty? $stderr.puts "You must be using LDAP to specify host classes" exit(24) end @children = {} # If we get a signal, then kill all of our children and get out. [:INT, :TERM].each do |signal| - trap(signal) do + Signal.trap(signal) do Puppet.notice "Caught #{signal}; shutting down" @children.each do |pid, host| Process.kill("INT", pid) end waitall exit(1) end end end end diff --git a/lib/puppet/application/master.rb b/lib/puppet/application/master.rb index fde474907..6d1cdef1b 100644 --- a/lib/puppet/application/master.rb +++ b/lib/puppet/application/master.rb @@ -1,153 +1,153 @@ require 'puppet/application' class Puppet::Application::Master < Puppet::Application should_parse_config run_mode :master option("--debug", "-d") option("--verbose", "-v") # internal option, only to be used by ext/rack/config.ru option("--rack") option("--compile host", "-c host") do |arg| options[:node] = arg end option("--logdest DEST", "-l DEST") do |arg| begin Puppet::Util::Log.newdestination(arg) options[:setdest] = true rescue => detail puts detail.backtrace if Puppet[:debug] $stderr.puts detail.to_s end end def preinit - trap(:INT) do + Signal.trap(:INT) do $stderr.puts "Cancelling startup" exit(0) end # Create this first-off, so we have ARGV require 'puppet/daemon' @daemon = Puppet::Daemon.new @daemon.argv = ARGV.dup end def run_command if options[:node] compile elsif Puppet[:parseonly] parseonly else main end end def compile Puppet::Util::Log.newdestination :console raise ArgumentError, "Cannot render compiled catalogs without pson support" unless Puppet.features.pson? begin unless catalog = Puppet::Resource::Catalog.find(options[:node]) raise "Could not compile catalog for #{options[:node]}" end jj catalog.to_resource rescue => detail $stderr.puts detail exit(30) end exit(0) end def parseonly begin Puppet::Node::Environment.new(Puppet[:environment]).known_resource_types rescue => detail Puppet.err detail exit 1 end exit(0) end def main require 'etc' require 'puppet/file_serving/content' require 'puppet/file_serving/metadata' xmlrpc_handlers = [:Status, :FileServer, :Master, :Report, :Filebucket] xmlrpc_handlers << :CA if Puppet[:ca] # Make sure we've got a localhost ssl cert Puppet::SSL::Host.localhost # And now configure our server to *only* hit the CA for data, because that's # all it will have write access to. Puppet::SSL::Host.ca_location = :only if Puppet::SSL::CertificateAuthority.ca? if Puppet.features.root? begin Puppet::Util.chuser rescue => detail puts detail.backtrace if Puppet[:trace] $stderr.puts "Could not change user to #{Puppet[:user]}: #{detail}" exit(39) end end unless options[:rack] require 'puppet/network/server' @daemon.server = Puppet::Network::Server.new(:xmlrpc_handlers => xmlrpc_handlers) @daemon.daemonize if Puppet[:daemonize] else require 'puppet/network/http/rack' @app = Puppet::Network::HTTP::Rack.new(:xmlrpc_handlers => xmlrpc_handlers, :protocols => [:rest, :xmlrpc]) end Puppet.notice "Starting Puppet master version #{Puppet.version}" unless options[:rack] @daemon.start else return @app end end def setup # Handle the logging settings. if options[:debug] or options[:verbose] if options[:debug] Puppet::Util::Log.level = :debug else Puppet::Util::Log.level = :info end unless Puppet[:daemonize] or options[:rack] Puppet::Util::Log.newdestination(:console) options[:setdest] = true end end Puppet::Util::Log.newdestination(:syslog) unless options[:setdest] exit(Puppet.settings.print_configs ? 0 : 1) if Puppet.settings.print_configs? Puppet.settings.use :main, :master, :ssl # Cache our nodes in yaml. Currently not configurable. Puppet::Node.cache_class = :yaml # Configure all of the SSL stuff. if Puppet::SSL::CertificateAuthority.ca? Puppet::SSL::Host.ca_location = :local Puppet.settings.use :ca Puppet::SSL::CertificateAuthority.instance else Puppet::SSL::Host.ca_location = :none end end end diff --git a/lib/puppet/application/queue.rb b/lib/puppet/application/queue.rb index 239f6b2ad..ede47d0a6 100644 --- a/lib/puppet/application/queue.rb +++ b/lib/puppet/application/queue.rb @@ -1,92 +1,92 @@ require 'puppet/application' require 'puppet/util' class Puppet::Application::Queue < Puppet::Application should_parse_config attr_accessor :daemon def preinit require 'puppet/daemon' @daemon = Puppet::Daemon.new @daemon.argv = ARGV.dup Puppet::Util::Log.newdestination(:console) # Do an initial trap, so that cancels don't get a stack trace. # This exits with exit code 1 - trap(:INT) do + Signal.trap(:INT) do $stderr.puts "Caught SIGINT; shutting down" exit(1) end # This is a normal shutdown, so code 0 - trap(:TERM) do + Signal.trap(:TERM) do $stderr.puts "Caught SIGTERM; shutting down" exit(0) end { :verbose => false, :debug => false }.each do |opt,val| options[opt] = val end end option("--debug","-d") option("--verbose","-v") def main require 'puppet/indirector/catalog/queue' # provides Puppet::Indirector::Queue.subscribe Puppet.notice "Starting puppetqd #{Puppet.version}" Puppet::Resource::Catalog::Queue.subscribe do |catalog| # Once you have a Puppet::Resource::Catalog instance, calling save on it should suffice # to put it through to the database via its active_record indirector (which is determined # by the terminus_class = :active_record setting above) Puppet::Util.benchmark(:notice, "Processing queued catalog for #{catalog.name}") do begin catalog.save rescue => detail puts detail.backtrace if Puppet[:trace] Puppet.err "Could not save queued catalog for #{catalog.name}: #{detail}" end end end Thread.list.each { |thread| thread.join } end # Handle the logging settings. def setup_logs if options[:debug] or options[:verbose] Puppet::Util::Log.newdestination(:console) if options[:debug] Puppet::Util::Log.level = :debug else Puppet::Util::Log.level = :info end end end def setup unless Puppet.features.stomp? raise ArgumentError, "Could not load the 'stomp' library, which must be present for queueing to work. You must install the required library." end setup_logs exit(Puppet.settings.print_configs ? 0 : 1) if Puppet.settings.print_configs? require 'puppet/resource/catalog' Puppet::Resource::Catalog.terminus_class = :active_record daemon.daemonize if Puppet[:daemonize] # We want to make sure that we don't have a cache # class set up, because if storeconfigs is enabled, # we'll get a loop of continually caching the catalog # for storage again. Puppet::Resource::Catalog.cache_class = nil end end diff --git a/lib/puppet/configurer/downloader.rb b/lib/puppet/configurer/downloader.rb index 1b587ed4b..b3696201a 100644 --- a/lib/puppet/configurer/downloader.rb +++ b/lib/puppet/configurer/downloader.rb @@ -1,79 +1,80 @@ require 'puppet/configurer' require 'puppet/resource/catalog' class Puppet::Configurer::Downloader attr_reader :name, :path, :source, :ignore # Determine the timeout value to use. def self.timeout timeout = Puppet[:configtimeout] case timeout when String if timeout =~ /^\d+$/ timeout = Integer(timeout) else raise ArgumentError, "Configuration timeout must be an integer" end when Integer # nothing else raise ArgumentError, "Configuration timeout must be an integer" end timeout end # Evaluate our download, returning the list of changed values. def evaluate Puppet.info "Retrieving #{name}" files = [] begin Timeout.timeout(self.class.timeout) do catalog.apply do |trans| trans.changed?.find_all do |resource| yield resource if block_given? files << resource[:path] end end end rescue Puppet::Error, Timeout::Error => detail puts detail.backtrace if Puppet[:debug] Puppet.err "Could not retrieve #{name}: #{detail}" end files end def initialize(name, path, source, ignore = nil) @name, @path, @source, @ignore = name, path, source, ignore end def catalog catalog = Puppet::Resource::Catalog.new + catalog.host_config = false catalog.add_resource(file) catalog end def file args = default_arguments.merge(:path => path, :source => source) args[:ignore] = ignore.split if ignore Puppet::Type.type(:file).new(args) end private def default_arguments { :path => path, :recurse => true, :source => source, :tag => name, :owner => Process.uid, :group => Process.gid, :purge => true, :force => true, :backup => false, :noop => false } end end diff --git a/lib/puppet/configurer/plugin_handler.rb b/lib/puppet/configurer/plugin_handler.rb index cfc6b5a0b..ae088f26f 100644 --- a/lib/puppet/configurer/plugin_handler.rb +++ b/lib/puppet/configurer/plugin_handler.rb @@ -1,26 +1,33 @@ # Break out the code related to plugins. This module is # just included into the agent, but having it here makes it # easier to test. module Puppet::Configurer::PluginHandler def download_plugins? Puppet[:pluginsync] end # Retrieve facts from the central server. def download_plugins return nil unless download_plugins? - Puppet::Configurer::Downloader.new("plugin", Puppet[:plugindest], Puppet[:pluginsource], Puppet[:pluginsignore]).evaluate.each { |file| load_plugin(file) } + plugin_downloader = Puppet::Configurer::Downloader.new( + "plugin", + Puppet[:plugindest], + Puppet[:pluginsource], + Puppet[:pluginsignore] + ) + + plugin_downloader.evaluate.each { |file| load_plugin(file) } end def load_plugin(file) return unless FileTest.exist?(file) return if FileTest.directory?(file) begin Puppet.info "Loading downloaded plugin #{file}" load file rescue Exception => detail Puppet.err "Could not load downloaded file #{file}: #{detail}" end end end diff --git a/lib/puppet/daemon.rb b/lib/puppet/daemon.rb index c76d63a54..22630ffb8 100755 --- a/lib/puppet/daemon.rb +++ b/lib/puppet/daemon.rb @@ -1,130 +1,130 @@ require 'puppet' require 'puppet/util/pidlock' require 'puppet/external/event-loop' require 'puppet/application' # A module that handles operations common to all daemons. This is included # into the Server and Client base classes. class Puppet::Daemon attr_accessor :agent, :server, :argv def daemonname Puppet[:name] end # Put the daemon into the background. def daemonize if pid = fork Process.detach(pid) exit(0) end create_pidfile # Get rid of console logging Puppet::Util::Log.close(:console) Process.setsid Dir.chdir("/") begin $stdin.reopen "/dev/null" $stdout.reopen "/dev/null", "a" $stderr.reopen $stdout Puppet::Util::Log.reopen rescue => detail Puppet.err "Could not start #{Puppet[:name]}: #{detail}" Puppet::Util::secure_open("/tmp/daemonout", "w") { |f| f.puts "Could not start #{Puppet[:name]}: #{detail}" } exit(12) end end # Create a pidfile for our daemon, so we can be stopped and others # don't try to start. def create_pidfile Puppet::Util.synchronize_on(Puppet[:name],Sync::EX) do raise "Could not create PID file: #{pidfile}" unless Puppet::Util::Pidlock.new(pidfile).lock end end # Provide the path to our pidfile. def pidfile Puppet[:pidfile] end def reexec raise Puppet::DevError, "Cannot reexec unless ARGV arguments are set" unless argv command = $0 + " " + argv.join(" ") Puppet.notice "Restarting with '#{command}'" stop(:exit => false) exec(command) end def reload return unless agent if agent.running? Puppet.notice "Not triggering already-running agent" return end agent.run end # Remove the pid file for our daemon. def remove_pidfile Puppet::Util.synchronize_on(Puppet[:name],Sync::EX) do locker = Puppet::Util::Pidlock.new(pidfile) locker.unlock or Puppet.err "Could not remove PID file #{pidfile}" if locker.locked? end end def restart Puppet::Application.restart! reexec unless agent and agent.running? end def reopen_logs Puppet::Util::Log.reopen end # Trap a couple of the main signals. This should probably be handled # in a way that anyone else can register callbacks for traps, but, eh. def set_signal_traps signals = {:INT => :stop, :TERM => :stop } # extended signals not supported under windows signals.update({:HUP => :restart, :USR1 => :reload, :USR2 => :reopen_logs }) unless Puppet.features.microsoft_windows? signals.each do |signal, method| - trap(signal) do + Signal.trap(signal) do Puppet.notice "Caught #{signal}; calling #{method}" send(method) end end end # Stop everything def stop(args = {:exit => true}) Puppet::Application.stop! server.stop if server remove_pidfile Puppet::Util::Log.close_all exit if args[:exit] end def start set_signal_traps create_pidfile raise Puppet::DevError, "Daemons must have an agent, server, or both" unless agent or server server.start if server agent.start if agent EventLoop.current.run end end diff --git a/lib/puppet/defaults.rb b/lib/puppet/defaults.rb index 687ac4eb0..8da104086 100644 --- a/lib/puppet/defaults.rb +++ b/lib/puppet/defaults.rb @@ -1,801 +1,817 @@ # The majority of the system configuration parameters are set in this file. module Puppet setdefaults(:main, :confdir => [Puppet.run_mode.conf_dir, "The main Puppet configuration directory. The default for this parameter is calculated based on the user. If the process is running as root or the user that Puppet is supposed to run as, it defaults to a system directory, but if it's running as any other user, it defaults to being in the user's home directory."], :vardir => [Puppet.run_mode.var_dir, "Where Puppet stores dynamic and growing data. The default for this parameter is calculated specially, like `confdir`_."], :name => [Puppet.application_name.to_s, "The name of the application, if we are running as one. The default is essentially $0 without the path or `.rb`."], :run_mode => [Puppet.run_mode.name.to_s, "The effective 'run mode' of the application: master, agent, or user."] ) setdefaults(:main, :logdir => Puppet.run_mode.logopts) setdefaults(:main, :trace => [false, "Whether to print stack traces on some errors"], :autoflush => { :default => false, :desc => "Whether log files should always flush to disk.", :hook => proc { |value| Log.autoflush = value } }, :syslogfacility => ["daemon", "What syslog facility to use when logging to syslog. Syslog has a fixed list of valid facilities, and you must choose one of those; you cannot just make one up."], :statedir => { :default => "$vardir/state", :mode => 01755, :desc => "The directory where Puppet state is stored. Generally, this directory can be removed without causing harm (although it might result in spurious service restarts)." }, :rundir => { :default => Puppet.run_mode.run_dir, :mode => 01777, :desc => "Where Puppet PID files are kept." }, :genconfig => [false, "Whether to just print a configuration to stdout and exit. Only makes sense when used interactively. Takes into account arguments specified on the CLI."], :genmanifest => [false, "Whether to just print a manifest to stdout and exit. Only makes sense when used interactively. Takes into account arguments specified on the CLI."], :configprint => ["", "Print the value of a specific configuration parameter. If a parameter is provided for this, then the value is printed and puppet exits. Comma-separate multiple values. For a list of all values, specify 'all'. This feature is only available in Puppet versions higher than 0.18.4."], :color => ["ansi", "Whether to use colors when logging to the console. Valid values are `ansi` (equivalent to `true`), `html` (mostly used during testing with TextMate), and `false`, which produces no color."], :mkusers => [false, "Whether to create the necessary user and group that puppet agent will run as."], :manage_internal_file_permissions => [true, "Whether Puppet should manage the owner, group, and mode of files it uses internally" ], :onetime => {:default => false, :desc => "Run the configuration once, rather than as a long-running daemon. This is useful for interactively running puppetd.", :short => 'o' }, :path => {:default => "none", :desc => "The shell search path. Defaults to whatever is inherited from the parent process.", :call_on_define => true, # Call our hook with the default value, so we always get the libdir set. :hook => proc do |value| ENV["PATH"] = "" if ENV["PATH"].nil? ENV["PATH"] = value unless value == "none" paths = ENV["PATH"].split(File::PATH_SEPARATOR) %w{/usr/sbin /sbin}.each do |path| ENV["PATH"] += File::PATH_SEPARATOR + path unless paths.include?(path) end value end }, :libdir => {:default => "$vardir/lib", :desc => "An extra search path for Puppet. This is only useful for those files that Puppet will load on demand, and is only guaranteed to work for those cases. In fact, the autoload mechanism is responsible for making sure this directory is in Ruby's search path", :call_on_define => true, # Call our hook with the default value, so we always get the libdir set. :hook => proc do |value| $LOAD_PATH.delete(@oldlibdir) if defined?(@oldlibdir) and $LOAD_PATH.include?(@oldlibdir) @oldlibdir = value $LOAD_PATH << value end }, :ignoreimport => [false, "A parameter that can be used in commit hooks, since it enables you to parse-check a single file rather than requiring that all files exist."], :authconfig => [ "$confdir/namespaceauth.conf", "The configuration file that defines the rights to the different namespaces and methods. This can be used as a coarse-grained authorization system for both `puppet agent` and `puppet master`." ], :environment => {:default => "production", :desc => "The environment Puppet is running in. For clients (e.g., `puppet agent`) this determines the environment itself, which is used to find modules and much more. For servers (i.e., `puppet master`) this provides the default environment for nodes we know nothing about." }, :diff_args => ["-u", "Which arguments to pass to the diff command when printing differences between files."], :diff => ["diff", "Which diff command to use when printing differences between files."], :show_diff => [false, "Whether to print a contextual diff when files are being replaced. The diff is printed on stdout, so this option is meaningless unless you are running Puppet interactively. This feature currently requires the `diff/lcs` Ruby library."], :daemonize => { :default => true, :desc => "Send the process into the background. This is the default.", :short => "D" }, :maximum_uid => [4294967290, "The maximum allowed UID. Some platforms use negative UIDs but then ship with tools that do not know how to handle signed ints, so the UIDs show up as huge numbers that can then not be fed back into the system. This is a hackish way to fail in a slightly more useful way when that happens."], :node_terminus => ["plain", "Where to find information about nodes."], :catalog_terminus => ["compiler", "Where to get node catalogs. This is useful to change if, for instance, you'd like to pre-compile catalogs and store them in memcached or some other easily-accessed store."], - :facts_terminus => [Puppet.application_name.to_s == "master" ? 'yaml' : 'facter', "The node facts terminus."], + :facts_terminus => { + :default => Puppet.application_name.to_s == "master" ? 'yaml' : 'facter', + :desc => "The node facts terminus.", + :hook => proc do |value| + require 'puppet/node/facts' + if value.to_s == "rest" + Puppet::Node::Facts.cache_class = :yaml + end + end + }, + :inventory_terminus => [ "$facts_terminus", "Should usually be the same as the facts terminus" ], :httplog => { :default => "$logdir/http.log", :owner => "root", :mode => 0640, :desc => "Where the puppet agent web server logs." }, :http_proxy_host => ["none", "The HTTP proxy host to use for outgoing connections. Note: You may need to use a FQDN for the server hostname when using a proxy."], :http_proxy_port => [3128, "The HTTP proxy port to use for outgoing connections"], :filetimeout => [ 15, "The minimum time to wait (in seconds) between checking for updates in configuration files. This timeout determines how quickly Puppet checks whether a file (such as manifests or templates) has changed on disk." ], :queue_type => ["stomp", "Which type of queue to use for asynchronous processing."], :queue_type => ["stomp", "Which type of queue to use for asynchronous processing."], :queue_source => ["stomp://localhost:61613/", "Which type of queue to use for asynchronous processing. If your stomp server requires authentication, you can include it in the URI as long as your stomp client library is at least 1.1.1"], :async_storeconfigs => {:default => false, :desc => "Whether to use a queueing system to provide asynchronous database integration. Requires that `puppetqd` be running and that 'PSON' support for ruby be installed.", :hook => proc do |value| if value # This reconfigures the terminii for Node, Facts, and Catalog Puppet.settings[:storeconfigs] = true # But then we modify the configuration Puppet::Resource::Catalog.cache_class = :queue else raise "Cannot disable asynchronous storeconfigs in a running process" end end }, :thin_storeconfigs => {:default => false, :desc => "Boolean; wether storeconfigs store in the database only the facts and exported resources. If true, then storeconfigs performance will be higher and still allow exported/collected resources, but other usage external to Puppet might not work", :hook => proc do |value| Puppet.settings[:storeconfigs] = true if value end }, :config_version => ["", "How to determine the configuration version. By default, it will be the time that the configuration is parsed, but you can provide a shell script to override how the version is determined. The output of this script will be added to every log message in the reports, allowing you to correlate changes on your hosts to the source version on the server."], :zlib => [true, "Boolean; whether to use the zlib library", ], :prerun_command => ["", "A command to run before every agent run. If this command returns a non-zero return code, the entire Puppet run will fail."], :postrun_command => ["", "A command to run after every agent run. If this command returns a non-zero return code, the entire Puppet run will be considered to have failed, even though it might have performed work during the normal run."], :freeze_main => [false, "Freezes the 'main' class, disallowing any code to be added to it. This essentially means that you can't have any code outside of a node, class, or definition other than in the site manifest."] ) hostname = Facter["hostname"].value domain = Facter["domain"].value if domain and domain != "" fqdn = [hostname, domain].join(".") else fqdn = hostname end Puppet.setdefaults( :main, # We have to downcase the fqdn, because the current ssl stuff (as oppsed to in master) doesn't have good facilities for # manipulating naming. :certname => {:default => fqdn.downcase, :desc => "The name to use when handling certificates. Defaults to the fully qualified domain name.", :call_on_define => true, # Call our hook with the default value, so we're always downcased :hook => proc { |value| raise(ArgumentError, "Certificate names must be lower case; see #1168") unless value == value.downcase }}, :certdnsnames => ['', "The DNS names on the Server certificate as a colon-separated list. If it's anything other than an empty string, it will be used as an alias in the created certificate. By default, only the server gets an alias set up, and only for 'puppet'."], :certdir => { :default => "$ssldir/certs", :owner => "service", :desc => "The certificate directory." }, :ssldir => { :default => "$confdir/ssl", :mode => 0771, :owner => "service", :desc => "Where SSL certificates are kept." }, :publickeydir => { :default => "$ssldir/public_keys", :owner => "service", :desc => "The public key directory." }, :requestdir => { :default => "$ssldir/certificate_requests", :owner => "service", :desc => "Where host certificate requests are stored." }, :privatekeydir => { :default => "$ssldir/private_keys", :mode => 0750, :owner => "service", :desc => "The private key directory." }, :privatedir => { :default => "$ssldir/private", :mode => 0750, :owner => "service", :desc => "Where the client stores private certificate information." }, :passfile => { :default => "$privatedir/password", :mode => 0640, :owner => "service", :desc => "Where puppet agent stores the password for its private key. Generally unused." }, :hostcsr => { :default => "$ssldir/csr_$certname.pem", :mode => 0644, :owner => "service", :desc => "Where individual hosts store and look for their certificate requests." }, :hostcert => { :default => "$certdir/$certname.pem", :mode => 0644, :owner => "service", :desc => "Where individual hosts store and look for their certificates." }, :hostprivkey => { :default => "$privatekeydir/$certname.pem", :mode => 0600, :owner => "service", :desc => "Where individual hosts store and look for their private key." }, :hostpubkey => { :default => "$publickeydir/$certname.pem", :mode => 0644, :owner => "service", :desc => "Where individual hosts store and look for their public key." }, :localcacert => { :default => "$certdir/ca.pem", :mode => 0644, :owner => "service", :desc => "Where each client stores the CA certificate." }, :hostcrl => { :default => "$ssldir/crl.pem", :mode => 0644, :owner => "service", :desc => "Where the host's certificate revocation list can be found. This is distinct from the certificate authority's CRL." }, :certificate_revocation => [true, "Whether certificate revocation should be supported by downloading a Certificate Revocation List (CRL) to all clients. If enabled, CA chaining will almost definitely not work."] ) setdefaults( :ca, :ca_name => ["Puppet CA: $certname", "The name to use the Certificate Authority certificate."], :cadir => { :default => "$ssldir/ca", :owner => "service", :group => "service", :mode => 0770, :desc => "The root directory for the certificate authority." }, :cacert => { :default => "$cadir/ca_crt.pem", :owner => "service", :group => "service", :mode => 0660, :desc => "The CA certificate." }, :cakey => { :default => "$cadir/ca_key.pem", :owner => "service", :group => "service", :mode => 0660, :desc => "The CA private key." }, :capub => { :default => "$cadir/ca_pub.pem", :owner => "service", :group => "service", :desc => "The CA public key." }, :cacrl => { :default => "$cadir/ca_crl.pem", :owner => "service", :group => "service", :mode => 0664, :desc => "The certificate revocation list (CRL) for the CA. Will be used if present but otherwise ignored.", :hook => proc do |value| if value == 'false' Puppet.warning "Setting the :cacrl to 'false' is deprecated; Puppet will just ignore the crl if yours is missing" end end }, :caprivatedir => { :default => "$cadir/private", :owner => "service", :group => "service", :mode => 0770, :desc => "Where the CA stores private certificate information." }, :csrdir => { :default => "$cadir/requests", :owner => "service", :group => "service", :desc => "Where the CA stores certificate requests" }, :signeddir => { :default => "$cadir/signed", :owner => "service", :group => "service", :mode => 0770, :desc => "Where the CA stores signed certificates." }, :capass => { :default => "$caprivatedir/ca.pass", :owner => "service", :group => "service", :mode => 0660, :desc => "Where the CA stores the password for the private key" }, :serial => { :default => "$cadir/serial", :owner => "service", :group => "service", :mode => 0644, :desc => "Where the serial number for certificates is stored." }, :autosign => { :default => "$confdir/autosign.conf", :mode => 0644, :desc => "Whether to enable autosign. Valid values are true (which autosigns any key request, and is a very bad idea), false (which never autosigns any key request), and the path to a file, which uses that configuration file to determine which keys to sign."}, :ca_days => ["", "How long a certificate should be valid. This parameter is deprecated, use ca_ttl instead"], :ca_ttl => ["5y", "The default TTL for new certificates; valid values must be an integer, optionally followed by one of the units 'y' (years of 365 days), 'd' (days), 'h' (hours), or 's' (seconds). The unit defaults to seconds. If this parameter is set, ca_days is ignored. Examples are '3600' (one hour) and '1825d', which is the same as '5y' (5 years) "], :ca_md => ["md5", "The type of hash used in certificates."], :req_bits => [2048, "The bit length of the certificates."], :keylength => [1024, "The bit length of keys."], :cert_inventory => { :default => "$cadir/inventory.txt", :mode => 0644, :owner => "service", :group => "service", :desc => "A Complete listing of all certificates" } ) # Define the config default. setdefaults( Puppet.settings[:name], :config => ["$confdir/puppet.conf", "The configuration file for #{Puppet[:name]}."], :pidfile => ["$rundir/$name.pid", "The pid file"], :bindaddress => ["", "The address a listening server should bind to. Mongrel servers default to 127.0.0.1 and WEBrick defaults to 0.0.0.0."], :servertype => {:default => "webrick", :desc => "The type of server to use. Currently supported options are webrick and mongrel. If you use mongrel, you will need a proxy in front of the process or processes, since Mongrel cannot speak SSL.", :call_on_define => true, # Call our hook with the default value, so we always get the correct bind address set. :hook => proc { |value| value == "webrick" ? Puppet.settings[:bindaddress] = "0.0.0.0" : Puppet.settings[:bindaddress] = "127.0.0.1" if Puppet.settings[:bindaddress] == "" } } ) setdefaults(:master, :user => ["puppet", "The user puppet master should run as."], :group => ["puppet", "The group puppet master should run as."], :manifestdir => ["$confdir/manifests", "Where puppet master looks for its manifests."], :manifest => ["$manifestdir/site.pp", "The entry-point manifest for puppet master."], :code => ["", "Code to parse directly. This is essentially only used by `puppet`, and should only be set if you're writing your own Puppet executable"], :masterlog => { :default => "$logdir/puppetmaster.log", :owner => "service", :group => "service", :mode => 0660, :desc => "Where puppet master logs. This is generally not used, since syslog is the default log destination." }, :masterhttplog => { :default => "$logdir/masterhttp.log", :owner => "service", :group => "service", :mode => 0660, :create => true, :desc => "Where the puppet master web server logs." }, :masterport => [8140, "Which port puppet master listens on."], :parseonly => [false, "Just check the syntax of the manifests."], :node_name => ["cert", "How the puppetmaster determines the client's identity and sets the 'hostname', 'fqdn' and 'domain' facts for use in the manifest, in particular for determining which 'node' statement applies to the client. Possible values are 'cert' (use the subject's CN in the client's certificate) and 'facter' (use the hostname that the client reported in its facts)"], :bucketdir => { :default => "$vardir/bucket", :mode => 0750, :owner => "service", :group => "service", :desc => "Where FileBucket files are stored." }, :rest_authconfig => [ "$confdir/auth.conf", "The configuration file that defines the rights to the different rest indirections. This can be used as a fine-grained authorization system for `puppet master`." ], :ca => [true, "Wether the master should function as a certificate authority."], :modulepath => {:default => "$confdir/modules:/usr/share/puppet/modules", :desc => "The search path for modules as a colon-separated list of directories.", :type => :setting }, # We don't want this to be considered a file, since it's multiple files. :ssl_client_header => ["HTTP_X_CLIENT_DN", "The header containing an authenticated client's SSL DN. Only used with Mongrel. This header must be set by the proxy to the authenticated client's SSL DN (e.g., `/CN=puppet.puppetlabs.com`). See http://projects.puppetlabs.com/projects/puppet/wiki/Using_Mongrel for more information."], :ssl_client_verify_header => ["HTTP_X_CLIENT_VERIFY", "The header containing the status message of the client verification. Only used with Mongrel. This header must be set by the proxy to 'SUCCESS' if the client successfully authenticated, and anything else otherwise. See http://projects.puppetlabs.com/projects/puppet/wiki/Using_Mongrel for more information."], # To make sure this directory is created before we try to use it on the server, we need # it to be in the server section (#1138). :yamldir => {:default => "$vardir/yaml", :owner => "service", :group => "service", :mode => "750", :desc => "The directory in which YAML data is stored, usually in a subdirectory."}, :server_datadir => {:default => "$vardir/server_data", :owner => "service", :group => "service", :mode => "750", :desc => "The directory in which serialized data is stored, usually in a subdirectory."}, :reports => ["store", "The list of reports to generate. All reports are looked for in `puppet/reports/name.rb`, and multiple report names should be comma-separated (whitespace is okay)." ], :reportdir => {:default => "$vardir/reports", :mode => 0750, :owner => "service", :group => "service", :desc => "The directory in which to store reports received from the client. Each client gets a separate subdirectory."}, :reporturl => ["http://localhost:3000/reports", "The URL used by the http reports processor to send reports"], :fileserverconfig => ["$confdir/fileserver.conf", "Where the fileserver configuration is stored."], :strict_hostname_checking => [false, "Whether to only search for the complete hostname as it is in the certificate when searching for node information in the catalogs."] ) setdefaults(:metrics, :rrddir => {:default => "$vardir/rrd", :owner => "service", :group => "service", :desc => "The directory where RRD database files are stored. Directories for each reporting host will be created under this directory." }, :rrdinterval => ["$runinterval", "How often RRD should expect data. This should match how often the hosts report back to the server."] ) setdefaults(:agent, :localconfig => { :default => "$statedir/localconfig", :owner => "root", :mode => 0660, :desc => "Where puppet agent caches the local configuration. An extension indicating the cache format is added automatically."}, :statefile => { :default => "$statedir/state.yaml", :mode => 0660, :desc => "Where puppet agent and puppet master store state associated with the running configuration. In the case of puppet master, this file reflects the state discovered through interacting with clients." }, :clientyamldir => {:default => "$vardir/client_yaml", :mode => "750", :desc => "The directory in which client-side YAML data is stored."}, :client_datadir => {:default => "$vardir/client_data", :mode => "750", :desc => "The directory in which serialized data is stored on the client."}, :classfile => { :default => "$statedir/classes.txt", :owner => "root", :mode => 0644, :desc => "The file in which puppet agent stores a list of the classes associated with the retrieved configuration. Can be loaded in the separate `puppet` executable using the `--loadclasses` option."}, :puppetdlog => { :default => "$logdir/puppetd.log", :owner => "root", :mode => 0640, :desc => "The log file for puppet agent. This is generally not used." }, :server => ["puppet", "The server to which server puppet agent should connect"], :ignoreschedules => [false, "Boolean; whether puppet agent should ignore schedules. This is useful for initial puppet agent runs."], :puppetport => [8139, "Which port puppet agent listens on."], :noop => [false, "Whether puppet agent should be run in noop mode."], :runinterval => [1800, # 30 minutes "How often puppet agent applies the client configuration; in seconds."], :listen => [false, "Whether puppet agent should listen for connections. If this is true, then by default only the `runner` server is started, which allows remote authorized and authenticated nodes to connect and trigger `puppet agent` runs."], :ca_server => ["$server", "The server to use for certificate authority requests. It's a separate server because it cannot and does not need to horizontally scale."], :ca_port => ["$masterport", "The port to use for the certificate authority."], :catalog_format => { :default => "", :desc => "(Deprecated for 'preferred_serialization_format') What format to use to dump the catalog. Only supports 'marshal' and 'yaml'. Only matters on the client, since it asks the server for a specific format.", :hook => proc { |value| if value Puppet.warning "Setting 'catalog_format' is deprecated; use 'preferred_serialization_format' instead." Puppet.settings[:preferred_serialization_format] = value end } }, :preferred_serialization_format => ["pson", "The preferred means of serializing ruby instances for passing over the wire. This won't guarantee that all instances will be serialized using this method, since not all classes can be guaranteed to support this format, but it will be used for all classes that support it."], :puppetdlockfile => [ "$statedir/puppetdlock", "A lock file to temporarily stop puppet agent from doing anything."], :usecacheonfailure => [true, "Whether to use the cached configuration when the remote configuration will not compile. This option is useful for testing new configurations, where you want to fix the broken configuration rather than reverting to a known-good one." ], :use_cached_catalog => [false, "Whether to only use the cached catalog rather than compiling a new catalog on every run. Puppet can be run with this enabled by default and then selectively disabled when a recompile is desired."], :ignorecache => [false, "Ignore cache and always recompile the configuration. This is useful for testing new configurations, where the local cache may in fact be stale even if the timestamps are up to date - if the facts change or if the server changes." ], :downcasefacts => [false, "Whether facts should be made all lowercase when sent to the server."], :dynamicfacts => ["memorysize,memoryfree,swapsize,swapfree", "Facts that are dynamic; these facts will be ignored when deciding whether changed facts should result in a recompile. Multiple facts should be comma-separated."], :splaylimit => ["$runinterval", "The maximum time to delay before runs. Defaults to being the same as the run interval."], :splay => [false, "Whether to sleep for a pseudo-random (but consistent) amount of time before a run."], :clientbucketdir => { :default => "$vardir/clientbucket", :mode => 0750, :desc => "Where FileBucket files are stored locally." }, :configtimeout => [120, "How long the client should wait for the configuration to be retrieved before considering it a failure. This can help reduce flapping if too many clients contact the server at one time." ], :reportserver => { :default => "$server", :call_on_define => false, :desc => "(Deprecated for 'report_server') The server to which to send transaction reports.", :hook => proc do |value| Puppet.settings[:report_server] = value if value end }, :report_server => ["$server", - "The server to which to send transaction reports." + "The server to send transaction reports to." ], :report_port => ["$masterport", "The port to communicate with the report_server." ], + :inventory_server => ["$server", + "The server to send facts to." + ], + :inventory_port => ["$masterport", + "The port to communicate with the inventory_server." + ], :report => [false, "Whether to send reports after every transaction." ], :graph => [false, "Whether to create dot graph files for the different configuration graphs. These dot files can be interpreted by tools like OmniGraffle or dot (which is part of ImageMagick)."], :graphdir => ["$statedir/graphs", "Where to store dot-outputted graphs."], :http_compression => [false, "Allow http compression in REST communication with the master. This setting might improve performance for agent -> master communications over slow WANs. Your puppetmaster needs to support compression (usually by activating some settings in a reverse-proxy in front of the puppetmaster, which rules out webrick). It is harmless to activate this settings if your master doesn't support compression, but if it supports it, this setting might reduce performance on high-speed LANs."] ) setdefaults(:inspect, :archive_files => [false, "During an inspect run, whether to archive files whose contents are audited to a file bucket."], :archive_file_server => ["$server", "During an inspect run, the file bucket server to archive files to if archive_files is set."] ) # Plugin information. setdefaults( :main, :plugindest => ["$libdir", "Where Puppet should store plugins that it pulls down from the central server."], :pluginsource => ["puppet://$server/plugins", "From where to retrieve plugins. The standard Puppet `file` type is used for retrieval, so anything that is a valid file source can be used here."], :pluginsync => [false, "Whether plugins should be synced with the central server."], :pluginsignore => [".svn CVS .git", "What files to ignore when pulling down plugins."] ) # Central fact information. setdefaults( :main, :factpath => {:default => "$vardir/lib/facter:$vardir/facts", :desc => "Where Puppet should look for facts. Multiple directories should be colon-separated, like normal PATH variables.", :call_on_define => true, # Call our hook with the default value, so we always get the value added to facter. :type => :setting, # Don't consider it a file, because it could be multiple colon-separated files :hook => proc { |value| Facter.search(value) if Facter.respond_to?(:search) }}, :factdest => ["$vardir/facts/", "Where Puppet should store facts that it pulls down from the central server."], :factsource => ["puppet://$server/facts/", "From where to retrieve facts. The standard Puppet `file` type is used for retrieval, so anything that is a valid file source can be used here."], :factsync => [false, "Whether facts should be synced with the central server."], :factsignore => [".svn CVS", "What files to ignore when pulling down facts."] ) setdefaults( :tagmail, :tagmap => ["$confdir/tagmail.conf", "The mapping between reporting tags and email addresses."], :sendmail => [which('sendmail') || '', "Where to find the sendmail binary with which to send email."], :reportfrom => ["report@" + [Facter["hostname"].value, Facter["domain"].value].join("."), "The 'from' email address for the reports."], :smtpserver => ["none", "The server through which to send email reports."] ) setdefaults( :rails, :dblocation => { :default => "$statedir/clientconfigs.sqlite3", :mode => 0660, :owner => "service", :group => "service", :desc => "The database cache for client configurations. Used for querying within the language." }, :dbadapter => [ "sqlite3", "The type of database to use." ], :dbmigrate => [ false, "Whether to automatically migrate the database." ], :dbname => [ "puppet", "The name of the database to use." ], :dbserver => [ "localhost", "The database server for caching. Only used when networked databases are used."], :dbport => [ "", "The database password for caching. Only used when networked databases are used."], :dbuser => [ "puppet", "The database user for caching. Only used when networked databases are used."], :dbpassword => [ "puppet", "The database password for caching. Only used when networked databases are used."], :dbconnections => [ '', "The number of database connections for networked databases. Will be ignored unless the value is a positive integer."], :dbsocket => [ "", "The database socket location. Only used when networked databases are used. Will be ignored if the value is an empty string."], :railslog => {:default => "$logdir/rails.log", :mode => 0600, :owner => "service", :group => "service", :desc => "Where Rails-specific logs are sent" }, :rails_loglevel => ["info", "The log level for Rails connections. The value must be a valid log level within Rails. Production environments normally use `info` and other environments normally use `debug`."] ) setdefaults( :couchdb, :couchdb_url => ["http://127.0.0.1:5984/puppet", "The url where the puppet couchdb database will be created"] ) setdefaults( :transaction, :tags => ["", "Tags to use to find resources. If this is set, then only resources tagged with the specified tags will be applied. Values must be comma-separated."], :evaltrace => [false, "Whether each resource should log when it is being evaluated. This allows you to interactively see exactly what is being done."], :summarize => [false, "Whether to print a transaction summary." ] ) setdefaults( :main, :external_nodes => ["none", "An external command that can produce node information. The output must be a YAML dump of a hash, and that hash must have one or both of `classes` and `parameters`, where `classes` is an array and `parameters` is a hash. For unknown nodes, the commands should exit with a non-zero exit code. This command makes it straightforward to store your node mapping information in other data sources like databases."]) setdefaults( :ldap, :ldapnodes => [false, "Whether to search for node configurations in LDAP. See http://projects.puppetlabs.com/projects/puppet/wiki/LDAP_Nodes for more information."], :ldapssl => [false, "Whether SSL should be used when searching for nodes. Defaults to false because SSL usually requires certificates to be set up on the client side."], :ldaptls => [false, "Whether TLS should be used when searching for nodes. Defaults to false because TLS usually requires certificates to be set up on the client side."], :ldapserver => ["ldap", "The LDAP server. Only used if `ldapnodes` is enabled."], :ldapport => [389, "The LDAP port. Only used if `ldapnodes` is enabled."], :ldapstring => ["(&(objectclass=puppetClient)(cn=%s))", "The search string used to find an LDAP node."], :ldapclassattrs => ["puppetclass", "The LDAP attributes to use to define Puppet classes. Values should be comma-separated."], :ldapstackedattrs => ["puppetvar", "The LDAP attributes that should be stacked to arrays by adding the values in all hierarchy elements of the tree. Values should be comma-separated."], :ldapattrs => ["all", "The LDAP attributes to include when querying LDAP for nodes. All returned attributes are set as variables in the top-level scope. Multiple values should be comma-separated. The value 'all' returns all attributes."], :ldapparentattr => ["parentnode", "The attribute to use to define the parent node."], :ldapuser => ["", "The user to use to connect to LDAP. Must be specified as a full DN."], :ldappassword => ["", "The password to use to connect to LDAP."], :ldapbase => ["", "The search base for LDAP searches. It's impossible to provide a meaningful default here, although the LDAP libraries might have one already set. Generally, it should be the 'ou=Hosts' branch under your main directory."] ) setdefaults(:master, :storeconfigs => {:default => false, :desc => "Whether to store each client's configuration. This requires ActiveRecord from Ruby on Rails.", :call_on_define => true, # Call our hook with the default value, so we always get the libdir set. :hook => proc do |value| require 'puppet/node' require 'puppet/node/facts' if value require 'puppet/rails' raise "StoreConfigs not supported without ActiveRecord 2.1 or higher" unless Puppet.features.rails? Puppet::Resource::Catalog.cache_class = :active_record unless Puppet.settings[:async_storeconfigs] Puppet::Node::Facts.cache_class = :active_record Puppet::Node.cache_class = :active_record end end } ) # This doesn't actually work right now. setdefaults( :parser, :lexical => [false, "Whether to use lexical scoping (vs. dynamic)."], :templatedir => ["$vardir/templates", "Where Puppet looks for template files. Can be a list of colon-seperated directories." ] ) end diff --git a/lib/puppet/external/nagios.rb b/lib/puppet/external/nagios.rb index 6b8852ee5..2ed829198 100755 --- a/lib/puppet/external/nagios.rb +++ b/lib/puppet/external/nagios.rb @@ -1,50 +1,48 @@ #!/usr/bin/env ruby -w #-------------------- # A script to retrieve hosts from ldap and create an importable # cfservd file from them -# -# $Id: nagios.rb,v 1.3 2004/06/09 20:32:46 luke Exp $ require 'digest/md5' #require 'ldap' require 'puppet/external/nagios/parser.rb' require 'puppet/external/nagios/base.rb' module Nagios NAGIOSVERSION = '1.1' # yay colors PINK = "" GREEN = "" YELLOW = "" SLATE = "" ORANGE = "" BLUE = "" NOCOLOR = "" RESET = "" def self.version NAGIOSVERSION end class Config def Config.import(config) text = String.new File.open(config) { |file| file.each { |line| text += line } } parser = Nagios::Parser.new parser.parse(text) end def Config.each Nagios::Object.objects.each { |object| yield object } end end end diff --git a/lib/puppet/external/nagios/base.rb b/lib/puppet/external/nagios/base.rb index ac1d25e2e..e98760e01 100755 --- a/lib/puppet/external/nagios/base.rb +++ b/lib/puppet/external/nagios/base.rb @@ -1,474 +1,472 @@ # The base class for all of our Nagios object types. Everything else # is mostly just data. class Nagios::Base class UnknownNagiosType < RuntimeError # When an unknown type is asked for by name. end include Enumerable class << self attr_accessor :parameters, :derivatives, :ocs, :name, :att attr_accessor :ldapbase attr_writer :namevar attr_reader :superior end # Attach one class to another. def self.attach(hash) @attach ||= {} hash.each do |n, v| @attach[n] = v end end # Convert a parameter to camelcase def self.camelcase(param) param.gsub(/_./) do |match| match.sub(/_/,'').capitalize end end # Uncamelcase a parameter. def self.decamelcase(param) param.gsub(/[A-Z]/) do |match| "_#{match.downcase}" end end # Create a new instance of a given class. def self.create(name, args = {}) name = name.intern if name.is_a? String if @types.include?(name) @types[name].new(args) else raise UnknownNagiosType, "Unknown type #{name}" end end # Yield each type in turn. def self.eachtype @types.each do |name, type| yield [name, type] end end # Create a mapping. def self.map(hash) @map ||= {} hash.each do |n, v| @map[n] = v end end # Return a mapping (or nil) for a param def self.mapping(name) name = name.intern if name.is_a? String if defined?(@map) @map[name] else nil end end # Return the namevar for the canonical name. def self.namevar if defined?(@namevar) return @namevar else if parameter?(:name) return :name elsif tmp = (self.name.to_s + "_name").intern and parameter?(tmp) @namevar = tmp return @namevar else raise "Type #{self.name} has no name var" end end end # Create a new type. def self.newtype(name, &block) name = name.intern if name.is_a? String @types ||= {} # Create the class, with the correct name. t = Class.new(self) t.name = name # Everyone gets this. There should probably be a better way, and I # should probably hack the attribute system to look things up based on # this "use" setting, but, eh. t.parameters = [:use] const_set(name.to_s.capitalize,t) # Evaluate the passed block. This should usually define all of the work. t.class_eval(&block) @types[name] = t end # Define both the normal case and camelcase method for a parameter def self.paramattr(name) camel = camelcase(name) param = name [name, camel].each do |method| define_method(method) do @parameters[param] end define_method(method.to_s + "=") do |value| @parameters[param] = value end end end # Is the specified name a valid parameter? def self.parameter?(name) name = name.intern if name.is_a? String @parameters.include?(name) end # Manually set the namevar def self.setnamevar(name) name = name.intern if name.is_a? String @namevar = name end # Set the valid parameters for this class def self.setparameters(*array) @parameters += array end # Set the superior ldap object class. Seems silly to include this # in this class, but, eh. def self.setsuperior(name) @superior = name end # Parameters to suppress in output. def self.suppress(name) @suppress ||= [] @suppress << name end # Whether a given parameter is suppressed. def self.suppress?(name) defined?(@suppress) and @suppress.include?(name) end # Return our name as the string. def self.to_s self.name.to_s end # Return a type by name. def self.type(name) name = name.intern if name.is_a? String @types[name] end # Convenience methods. def [](param) send(param) end # Convenience methods. def []=(param,value) send(param.to_s + "=", value) end # Iterate across all ofour set parameters. def each @parameters.each { |param,value| yield(param,value) } end # Initialize our object, optionally with a list of parameters. def initialize(args = {}) @parameters = {} args.each { |param,value| self[param] = value } if @namevar == :_naginator_name self['_naginator_name'] = self['name'] end end # Handle parameters like attributes. def method_missing(mname, *args) pname = mname.to_s pname.sub!(/=/, '') if self.class.parameter?(pname) if pname =~ /A-Z/ pname = self.class.decamelcase(pname) end self.class.paramattr(pname) # Now access the parameters directly, to make it at least less # likely we'll end up in an infinite recursion. if mname.to_s =~ /=$/ @parameters[pname] = *args else return @parameters[mname] end else super end end # Retrieve our name, through a bit of redirection. def name send(self.class.namevar) end # This is probably a bad idea. def name=(value) unless self.class.namevar.to_s == "name" send(self.class.namevar.to_s + "=", value) end end def namevar (self.type + "_name").intern end def parammap(param) unless defined?(@map) map = { self.namevar => "cn" } map.update(self.class.map) if self.class.map end if map.include?(param) return map[param] else return "nagios-" + param.id2name.gsub(/_/,'-') end end def parent unless defined?(self.class.attached) puts "Duh, you called parent on an unattached class" return end klass,param = self.class.attached unless @parameters.include?(param) puts "Huh, no attachment param" return end klass[@parameters[param]] end # okay, this sucks # how do i get my list of ocs? def to_ldif base = self.class.ldapbase str = self.dn + "\n" ocs = Array.new if self.class.ocs # i'm storing an array, so i have to flatten it and stuff kocs = self.class.ocs ocs.push(*kocs) end ocs.push "top" oc = self.class.to_s oc.sub!(/Nagios/,'nagios') oc.sub!(/::/,'') ocs.push oc ocs.each { |oc| str += "objectclass: #{oc}\n" } @parameters.each { |name,value| next if self.class.suppress.include?(name) ldapname = self.parammap(name) str += ldapname + ": #{value}\n" } str += "\n" end def to_s str = "define #{self.type} {\n" self.each { |param,value| str += %{\t%-30s %s\n} % [ param, if value.is_a? Array value.join(",") else value end ] } str += "}\n" str end # The type of object we are. def type self.class.name end # object types newtype :host do setparameters :host_name, :alias, :display_name, :address, :parents, :hostgroups, :check_command, :initial_state, :max_check_attempts, :check_interval, :retry_interval, :active_checks_enabled, :passive_checks_enabled, :check_period, :obsess_over_host, :check_freshness, :freshness_threshold, :event_handler, :event_handler_enabled, :low_flap_threshold, :high_flap_threshold, :flap_detection_enabled, :flap_detection_options, :failure_prediction_enabled, :process_perf_data, :retain_status_information, :retain_nonstatus_information, :contacts, :contact_groups, :notification_interval, :first_notification_delay, :notification_period, :notification_options, :notifications_enabled, :stalking_options, :notes, :notes_url, :action_url, :icon_image, :icon_image_alt, :vrml_image, :statusmap_image, "2d_coords".intern, "3d_coords".intern, :register, :use setsuperior "person" map :address => "ipHostNumber" end newtype :hostgroup do setparameters :hostgroup_name, :alias, :members, :hostgroup_members, :notes, :notes_url, :action_url, :register, :use end newtype :service do attach :host => :host_name setparameters :host_name, :hostgroup_name, :service_description, :display_name, :servicegroups, :is_volatile, :check_command, :initial_state, :max_check_attempts, :check_interval, :retry_interval, :normal_check_interval, :retry_check_interval, :active_checks_enabled, :passive_checks_enabled, :parallelize_check, :check_period, :obsess_over_service, :check_freshness, :freshness_threshold, :event_handler, :event_handler_enabled, :low_flap_threshold, :high_flap_threshold, :flap_detection_enabled,:flap_detection_options, :process_perf_data, :failure_prediction_enabled, :retain_status_information, :retain_nonstatus_information, :notification_interval, :first_notification_delay, :notification_period, :notification_options, :notifications_enabled, :contacts, :contact_groups, :stalking_options, :notes, :notes_url, :action_url, :icon_image, :icon_image_alt, :register, :use, :_naginator_name suppress :host_name setnamevar :_naginator_name end newtype :servicegroup do setparameters :servicegroup_name, :alias, :members, :servicegroup_members, :notes, :notes_url, :action_url, :register, :use end newtype :contact do setparameters :contact_name, :alias, :contactgroups, :host_notifications_enabled, :service_notifications_enabled, :host_notification_period, :service_notification_period, :host_notification_options, :service_notification_options, :host_notification_commands, :service_notification_commands, :email, :pager, :address1, :address2, :address3, :address4, :address5, :address6, :can_submit_commands, :retain_status_information, :retain_nonstatus_information, :register, :use setsuperior "person" end newtype :contactgroup do setparameters :contactgroup_name, :alias, :members, :contactgroup_members, :register, :use end # TODO - We should support generic time periods here eg "day 1 - 15" newtype :timeperiod do setparameters :timeperiod_name, :alias, :sunday, :monday, :tuesday, :wednesday, :thursday, :friday, :saturday, :exclude, :register, :use end newtype :command do setparameters :command_name, :command_line end newtype :servicedependency do auxiliary = true setparameters :dependent_host_name, :dependent_hostgroup_name, :dependent_service_description, :host_name, :hostgroup_name, :service_description, :inherits_parent, :execution_failure_criteria, :notification_failure_criteria, :dependency_period, :register, :use, :_naginator_name setnamevar :_naginator_name end newtype :serviceescalation do setparameters :host_name, :hostgroup_name, :servicegroup_name, :service_description, :contacts, :contact_groups, :first_notification, :last_notification, :notification_interval, :escalation_period, :escalation_options, :register, :use, :_naginator_name setnamevar :_naginator_name end newtype :hostdependency do auxiliary = true setparameters :dependent_host_name, :dependent_hostgroup_name, :host_name, :hostgroup_name, :inherits_parent, :execution_failure_criteria, :notification_failure_criteria, :dependency_period, :register, :use, :_naginator_name setnamevar :_naginator_name end newtype :hostescalation do setparameters :host_name, :hostgroup_name, :contacts, :contact_groups, :first_notification, :last_notification, :notification_interval, :escalation_period, :escalation_options, :register, :use, :_naginator_name setnamevar :_naginator_name end newtype :hostextinfo do auxiliary = true setparameters :host_name, :notes, :notes_url, :icon_image, :icon_image_alt, :vrml_image, :statusmap_image, "2d_coords".intern, "3d_coords".intern, :register, :use setnamevar :host_name end newtype :serviceextinfo do auxiliary = true setparameters :host_name, :service_description, :notes, :notes_url, :action_url, :icon_image, :icon_image_alt, :register, :use, :_naginator_name setnamevar :_naginator_name end end - -# $Id$ diff --git a/lib/puppet/indirector/facts/inventory_active_record.rb b/lib/puppet/indirector/facts/inventory_active_record.rb new file mode 100644 index 000000000..db4c63f00 --- /dev/null +++ b/lib/puppet/indirector/facts/inventory_active_record.rb @@ -0,0 +1,97 @@ +require 'puppet/rails' +require 'puppet/rails/inventory_node' +require 'puppet/rails/inventory_fact' +require 'puppet/indirector/active_record' + +class Puppet::Node::Facts::InventoryActiveRecord < Puppet::Indirector::ActiveRecord + def find(request) + node = Puppet::Rails::InventoryNode.find_by_name(request.key) + return nil unless node + facts = Puppet::Node::Facts.new(node.name, node.facts_to_hash) + facts.timestamp = node.timestamp + facts + end + + def save(request) + facts = request.instance + node = Puppet::Rails::InventoryNode.find_by_name(request.key) || Puppet::Rails::InventoryNode.create(:name => request.key, :timestamp => facts.timestamp) + node.timestamp = facts.timestamp + + ActiveRecord::Base.transaction do + Puppet::Rails::InventoryFact.delete_all(:node_id => node.id) + # We don't want to save internal values as facts, because those are + # metadata that belong on the node + facts.values.each do |name,value| + next if name.to_s =~ /^_/ + node.facts.build(:name => name, :value => value) + end + node.save + end + end + + def search(request) + return [] unless request.options + matching_nodes = [] + fact_names = [] + fact_filters = Hash.new {|h,k| h[k] = []} + meta_filters = Hash.new {|h,k| h[k] = []} + request.options.each do |key,value| + type, name, operator = key.to_s.split(".") + operator ||= "eq" + if type == "facts" + fact_filters[operator] << [name,value] + elsif type == "meta" and name == "timestamp" + meta_filters[operator] << [name,value] + end + end + + matching_nodes = nodes_matching_fact_filters(fact_filters) + nodes_matching_meta_filters(meta_filters) + + # to_a because [].inject == nil + matching_nodes.inject {|nodes,this_set| nodes & this_set}.to_a.sort + end + + private + + def nodes_matching_fact_filters(fact_filters) + node_sets = [] + fact_filters['eq'].each do |name,value| + node_sets << Puppet::Rails::InventoryNode.has_fact_with_value(name,value).map {|node| node.name} + end + fact_filters['ne'].each do |name,value| + node_sets << Puppet::Rails::InventoryNode.has_fact_without_value(name,value).map {|node| node.name} + end + { + 'gt' => '>', + 'lt' => '<', + 'ge' => '>=', + 'le' => '<=' + }.each do |operator_name,operator| + fact_filters[operator_name].each do |name,value| + facts = Puppet::Rails::InventoryFact.find_by_sql(["SELECT inventory_facts.value, inventory_nodes.name AS node_name + FROM inventory_facts INNER JOIN inventory_nodes + ON inventory_facts.node_id = inventory_nodes.id + WHERE inventory_facts.name = ?", name]) + node_sets << facts.select {|fact| fact.value.to_f.send(operator, value.to_f)}.map {|fact| fact.node_name} + end + end + node_sets + end + + def nodes_matching_meta_filters(meta_filters) + node_sets = [] + { + 'eq' => '=', + 'ne' => '!=', + 'gt' => '>', + 'lt' => '<', + 'ge' => '>=', + 'le' => '<=' + }.each do |operator_name,operator| + meta_filters[operator_name].each do |name,value| + node_sets << Puppet::Rails::InventoryNode.find(:all, :select => "name", :conditions => ["timestamp #{operator} ?", value]).map {|node| node.name} + end + end + node_sets + end +end diff --git a/lib/puppet/indirector/facts/rest.rb b/lib/puppet/indirector/facts/rest.rb index 07491fc77..e2afa14b2 100644 --- a/lib/puppet/indirector/facts/rest.rb +++ b/lib/puppet/indirector/facts/rest.rb @@ -1,6 +1,8 @@ require 'puppet/node/facts' require 'puppet/indirector/rest' class Puppet::Node::Facts::Rest < Puppet::Indirector::REST desc "Find and save facts about nodes over HTTP via REST." + use_server_setting(:inventory_server) + use_port_setting(:inventory_port) end diff --git a/lib/puppet/indirector/facts/yaml.rb b/lib/puppet/indirector/facts/yaml.rb index 89feaf2ab..65bd78354 100644 --- a/lib/puppet/indirector/facts/yaml.rb +++ b/lib/puppet/indirector/facts/yaml.rb @@ -1,7 +1,82 @@ require 'puppet/node/facts' require 'puppet/indirector/yaml' class Puppet::Node::Facts::Yaml < Puppet::Indirector::Yaml desc "Store client facts as flat files, serialized using YAML, or return deserialized facts from disk." + + def search(request) + node_names = [] + Dir.glob(yaml_dir_path).each do |file| + facts = YAML.load_file(file) + node_names << facts.name if node_matches?(facts, request.options) + end + node_names + end + + private + + # Return the path to a given node's file. + def yaml_dir_path + base = Puppet.run_mode.master? ? Puppet[:yamldir] : Puppet[:clientyamldir] + File.join(base, 'facts', '*.yaml') + end + + def node_matches?(facts, options) + options.each do |key, value| + type, name, operator = key.to_s.split(".") + operator ||= 'eq' + + return false unless node_matches_option?(type, name, operator, value, facts) + end + return true + end + + def node_matches_option?(type, name, operator, value, facts) + case type + when "meta" + case name + when "timestamp" + compare_timestamp(operator, facts.timestamp, Time.parse(value)) + end + when "facts" + compare_facts(operator, facts.values[name], value) + end + end + + def compare_facts(operator, value1, value2) + return false unless value1 + + case operator + when "eq" + value1.to_s == value2.to_s + when "le" + value1.to_f <= value2.to_f + when "ge" + value1.to_f >= value2.to_f + when "lt" + value1.to_f < value2.to_f + when "gt" + value1.to_f > value2.to_f + when "ne" + value1.to_s != value2.to_s + end + end + + def compare_timestamp(operator, value1, value2) + case operator + when "eq" + value1 == value2 + when "le" + value1 <= value2 + when "ge" + value1 >= value2 + when "lt" + value1 < value2 + when "gt" + value1 > value2 + when "ne" + value1 != value2 + end + end end diff --git a/lib/puppet/indirector/indirection.rb b/lib/puppet/indirector/indirection.rb index ec147ec69..3d17e6e47 100644 --- a/lib/puppet/indirector/indirection.rb +++ b/lib/puppet/indirector/indirection.rb @@ -1,318 +1,319 @@ require 'puppet/util/docs' require 'puppet/indirector/envelope' require 'puppet/indirector/request' require 'puppet/util/cacher' # The class that connects functional classes with their different collection # back-ends. Each indirection has a set of associated terminus classes, # each of which is a subclass of Puppet::Indirector::Terminus. class Puppet::Indirector::Indirection include Puppet::Util::Cacher include Puppet::Util::Docs @@indirections = [] # Find an indirection by name. This is provided so that Terminus classes # can specifically hook up with the indirections they are associated with. def self.instance(name) @@indirections.find { |i| i.name == name } end # Return a list of all known indirections. Used to generate the # reference. def self.instances @@indirections.collect { |i| i.name } end # Find an indirected model by name. This is provided so that Terminus classes # can specifically hook up with the indirections they are associated with. def self.model(name) return nil unless match = @@indirections.find { |i| i.name == name } match.model end attr_accessor :name, :model # Create and return our cache terminus. def cache raise(Puppet::DevError, "Tried to cache when no cache class was set") unless cache_class terminus(cache_class) end # Should we use a cache? def cache? cache_class ? true : false end attr_reader :cache_class # Define a terminus class to be used for caching. def cache_class=(class_name) validate_terminus_class(class_name) if class_name @cache_class = class_name end # This is only used for testing. def delete @@indirections.delete(self) if @@indirections.include?(self) end # Set the time-to-live for instances created through this indirection. def ttl=(value) raise ArgumentError, "Indirection TTL must be an integer" unless value.is_a?(Fixnum) @ttl = value end # Default to the runinterval for the ttl. def ttl @ttl ||= Puppet[:runinterval].to_i end # Calculate the expiration date for a returned instance. def expiration Time.now + ttl end # Generate the full doc string. def doc text = "" text += scrub(@doc) + "\n\n" if @doc if s = terminus_setting text += "* **Terminus Setting**: #{terminus_setting}" end text end def initialize(model, name, options = {}) @model = model @name = name @cache_class = nil @terminus_class = nil raise(ArgumentError, "Indirection #{@name} is already defined") if @@indirections.find { |i| i.name == @name } @@indirections << self if mod = options[:extend] extend(mod) options.delete(:extend) end # This is currently only used for cache_class and terminus_class. options.each do |name, value| begin send(name.to_s + "=", value) rescue NoMethodError raise ArgumentError, "#{name} is not a valid Indirection parameter" end end end # Set up our request object. def request(*args) Puppet::Indirector::Request.new(self.name, *args) end # Return the singleton terminus for this indirection. def terminus(terminus_name = nil) # Get the name of the terminus. raise Puppet::DevError, "No terminus specified for #{self.name}; cannot redirect" unless terminus_name ||= terminus_class termini[terminus_name] ||= make_terminus(terminus_name) end # This can be used to select the terminus class. attr_accessor :terminus_setting # Determine the terminus class. def terminus_class unless @terminus_class if setting = self.terminus_setting self.terminus_class = Puppet.settings[setting].to_sym else raise Puppet::DevError, "No terminus class nor terminus setting was provided for indirection #{self.name}" end end @terminus_class end def reset_terminus_class @terminus_class = nil end # Specify the terminus class to use. def terminus_class=(klass) validate_terminus_class(klass) @terminus_class = klass end # This is used by terminus_class= and cache=. def validate_terminus_class(terminus_class) raise ArgumentError, "Invalid terminus name #{terminus_class.inspect}" unless terminus_class and terminus_class.to_s != "" unless Puppet::Indirector::Terminus.terminus_class(self.name, terminus_class) raise ArgumentError, "Could not find terminus #{terminus_class} for indirection #{self.name}" end end # Expire a cached object, if one is cached. Note that we don't actually # remove it, we expire it and write it back out to disk. This way people # can still use the expired object if they want. def expire(key, *args) request = request(:expire, key, *args) return nil unless cache? return nil unless instance = cache.find(request(:find, key, *args)) Puppet.info "Expiring the #{self.name} cache of #{instance.name}" # Set an expiration date in the past instance.expiration = Time.now - 60 cache.save(request(:save, instance, *args)) end # Search for an instance in the appropriate terminus, caching the # results if caching is configured.. def find(key, *args) request = request(:find, key, *args) terminus = prepare(request) if result = find_in_cache(request) return result end # Otherwise, return the result from the terminus, caching if appropriate. if ! request.ignore_terminus? and result = terminus.find(request) result.expiration ||= self.expiration if result.respond_to?(:expiration) if cache? and request.use_cache? Puppet.info "Caching #{self.name} for #{request.key}" cache.save request(:save, result, *args) end return terminus.respond_to?(:filter) ? terminus.filter(result) : result end nil end # Search for an instance in the appropriate terminus, and return a # boolean indicating whether the instance was found. def head(key, *args) request = request(:head, key, *args) terminus = prepare(request) # Look in the cache first, then in the terminus. Force the result # to be a boolean. !!(find_in_cache(request) || terminus.head(request)) end def find_in_cache(request) # See if our instance is in the cache and up to date. return nil unless cache? and ! request.ignore_cache? and cached = cache.find(request) if cached.expired? Puppet.info "Not using expired #{self.name} for #{request.key} from cache; expired at #{cached.expiration}" return nil end Puppet.debug "Using cached #{self.name} for #{request.key}" cached rescue => detail puts detail.backtrace if Puppet[:trace] Puppet.err "Cached #{self.name} for #{request.key} failed: #{detail}" nil end # Remove something via the terminus. def destroy(key, *args) request = request(:destroy, key, *args) terminus = prepare(request) result = terminus.destroy(request) if cache? and cached = cache.find(request(:find, key, *args)) # Reuse the existing request, since it's equivalent. cache.destroy(request) end result end # Search for more than one instance. Should always return an array. def search(key, *args) request = request(:search, key, *args) terminus = prepare(request) if result = terminus.search(request) raise Puppet::DevError, "Search results from terminus #{terminus.name} are not an array" unless result.is_a?(Array) result.each do |instance| + next unless instance.respond_to? :expiration instance.expiration ||= self.expiration end return result end end # Save the instance in the appropriate terminus. This method is # normally an instance method on the indirected class. def save(key, instance = nil) request = request(:save, key, instance) terminus = prepare(request) result = terminus.save(request) # If caching is enabled, save our document there cache.save(request) if cache? result end private # Check authorization if there's a hook available; fail if there is one # and it returns false. def check_authorization(request, terminus) # At this point, we're assuming authorization makes no sense without # client information. return unless request.node # This is only to authorize via a terminus-specific authorization hook. return unless terminus.respond_to?(:authorized?) unless terminus.authorized?(request) msg = "Not authorized to call #{request.method} on #{request}" msg += " with #{request.options.inspect}" unless request.options.empty? raise ArgumentError, msg end end # Setup a request, pick the appropriate terminus, check the request's authorization, and return it. def prepare(request) # Pick our terminus. if respond_to?(:select_terminus) unless terminus_name = select_terminus(request) raise ArgumentError, "Could not determine appropriate terminus for #{request}" end else terminus_name = terminus_class end dest_terminus = terminus(terminus_name) check_authorization(request, dest_terminus) dest_terminus end # Create a new terminus instance. def make_terminus(terminus_class) # Load our terminus class. unless klass = Puppet::Indirector::Terminus.terminus_class(self.name, terminus_class) raise ArgumentError, "Could not find terminus #{terminus_class} for indirection #{self.name}" end klass.new end # Cache our terminus instances indefinitely, but make it easy to clean them up. cached_attr(:termini) { Hash.new } end diff --git a/lib/puppet/module.rb b/lib/puppet/module.rb index 8da19c2ce..43266b2b5 100644 --- a/lib/puppet/module.rb +++ b/lib/puppet/module.rb @@ -1,204 +1,204 @@ require 'puppet/util/logging' # Support for modules class Puppet::Module class Error < Puppet::Error; end class MissingModule < Error; end class IncompatibleModule < Error; end class UnsupportedPlatform < Error; end class IncompatiblePlatform < Error; end class MissingMetadata < Error; end class InvalidName < Error; end include Puppet::Util::Logging TEMPLATES = "templates" FILES = "files" MANIFESTS = "manifests" PLUGINS = "plugins" FILETYPES = [MANIFESTS, FILES, TEMPLATES, PLUGINS] # Return an array of paths by splitting the +modulepath+ config # parameter. Only consider paths that are absolute and existing # directories def self.modulepath(environment = nil) Puppet::Node::Environment.new(environment).modulepath end # Find and return the +module+ that +path+ belongs to. If +path+ is # absolute, or if there is no module whose name is the first component # of +path+, return +nil+ def self.find(modname, environment = nil) return nil unless modname Puppet::Node::Environment.new(environment).module(modname) end attr_reader :name, :environment attr_writer :environment attr_accessor :source, :author, :version, :license, :puppetversion, :summary, :description, :project_page def has_metadata? return false unless metadata_file FileTest.exist?(metadata_file) end def initialize(name, environment = nil) @name = name assert_validity if environment.is_a?(Puppet::Node::Environment) @environment = environment else @environment = Puppet::Node::Environment.new(environment) end load_metadata if has_metadata? validate_puppet_version validate_dependencies end FILETYPES.each do |type| # A boolean method to let external callers determine if # we have files of a given type. define_method(type +'?') do return false unless path return false unless FileTest.exist?(subpath(type)) return true end # A method for returning a given file of a given type. # e.g., file = mod.manifest("my/manifest.pp") # # If the file name is nil, then the base directory for the # file type is passed; this is used for fileserving. define_method(type.to_s.sub(/s$/, '')) do |file| return nil unless path # If 'file' is nil then they're asking for the base path. # This is used for things like fileserving. if file full_path = File.join(subpath(type), file) else full_path = subpath(type) end return nil unless FileTest.exist?(full_path) return full_path end end def exist? ! path.nil? end # Find the first 'files' directory. This is used by the XMLRPC fileserver. def file_directory subpath("files") end def license_file return @license_file if defined?(@license_file) return @license_file = nil unless path @license_file = File.join(path, "License") end def load_metadata data = PSON.parse File.read(metadata_file) [:source, :author, :version, :license, :puppetversion].each do |attr| unless value = data[attr.to_s] unless attr == :puppetversion raise MissingMetadata, "No #{attr} module metadata provided for #{self.name}" end end send(attr.to_s + "=", value) end end # Return the list of manifests matching the given glob pattern, # defaulting to 'init.{pp,rb}' for empty modules. def match_manifests(rest) pat = File.join(path, MANIFESTS, rest || 'init') [manifest("init.pp"),manifest("init.rb")].compact + Dir. glob(pat + (File.extname(pat).empty? ? '.{pp,rb}' : '')). reject { |f| FileTest.directory?(f) } end def metadata_file return @metadata_file if defined?(@metadata_file) return @metadata_file = nil unless path @metadata_file = File.join(path, "metadata.json") end # Find this module in the modulepath. def path environment.modulepath.collect { |path| File.join(path, name) }.find { |d| FileTest.exist?(d) } end # Find all plugin directories. This is used by the Plugins fileserving mount. def plugin_directory subpath("plugins") end def requires(name, version = nil) @requires ||= [] @requires << [name, version] end def supports(name, version = nil) @supports ||= [] @supports << [name, version] end def to_s result = "Module #{name}" result += "(#{path})" if path result end def validate_dependencies return unless defined?(@requires) @requires.each do |name, version| unless mod = environment.module(name) raise MissingModule, "Missing module #{name} required by #{self.name}" end if version and mod.version != version raise IncompatibleModule, "Required module #{name} is version #{mod.version} but #{self.name} requires #{version}" end end end def validate_puppet_version return unless puppetversion and puppetversion != Puppet.version raise IncompatibleModule, "Module #{self.name} is only compatible with Puppet version #{puppetversion}, not #{Puppet.version}" end private def subpath(type) return File.join(path, type) unless type.to_s == "plugins" backward_compatible_plugins_dir end def backward_compatible_plugins_dir if dir = File.join(path, "plugins") and FileTest.exist?(dir) - warning "using the deprecated 'plugins' directory for ruby extensions; please move to 'lib'" + Puppet.warning "using the deprecated 'plugins' directory for ruby extensions; please move to 'lib'" return dir else return File.join(path, "lib") end end def assert_validity raise InvalidName, "Invalid module name; module names must be alphanumeric (plus '-'), not '#{name}'" unless name =~ /^[-\w]+$/ end end diff --git a/lib/puppet/network/http/api/v1.rb b/lib/puppet/network/http/api/v1.rb index 8aa1f0ee1..9e51aae36 100644 --- a/lib/puppet/network/http/api/v1.rb +++ b/lib/puppet/network/http/api/v1.rb @@ -1,73 +1,72 @@ require 'puppet/network/http/api' module Puppet::Network::HTTP::API::V1 # How we map http methods and the indirection name in the URI # to an indirection method. METHOD_MAP = { "GET" => { :plural => :search, :singular => :find }, "PUT" => { :singular => :save }, "DELETE" => { :singular => :destroy }, "HEAD" => { :singular => :head } } def uri2indirection(http_method, uri, params) environment, indirection, key = uri.split("/", 4)[1..-1] # the first field is always nil because of the leading slash raise ArgumentError, "The environment must be purely alphanumeric, not '#{environment}'" unless environment =~ /^\w+$/ raise ArgumentError, "The indirection name must be purely alphanumeric, not '#{indirection}'" unless indirection =~ /^\w+$/ method = indirection_method(http_method, indirection) params[:environment] = environment raise ArgumentError, "No request key specified in #{uri}" if key == "" or key.nil? key = URI.unescape(key) Puppet::Indirector::Request.new(indirection, method, key, params) end def indirection2uri(request) indirection = request.method == :search ? pluralize(request.indirection_name.to_s) : request.indirection_name.to_s "/#{request.environment.to_s}/#{indirection}/#{request.escaped_key}#{request.query_string}" end def indirection_method(http_method, indirection) raise ArgumentError, "No support for http method #{http_method}" unless METHOD_MAP[http_method] unless method = METHOD_MAP[http_method][plurality(indirection)] raise ArgumentError, "No support for plural #{http_method} operations" end method end def pluralize(indirection) return(indirection == "status" ? "statuses" : indirection + "s") end def plurality(indirection) # NOTE This specific hook for facts is ridiculous, but it's a *many*-line # fix to not need this, and our goal is to move away from the complication # that leads to the fix being too long. return :singular if indirection == "facts" - - # "status" really is singular return :singular if indirection == "status" + return :plural if indirection == "inventory" - result = (indirection =~ /s$/) ? :plural : :singular + result = (indirection =~ /s$|_search$/) ? :plural : :singular - indirection.sub!(/s$/, '') if result + indirection.sub!(/s$|_search$|es$/, '') result end end diff --git a/lib/puppet/network/http/compression.rb b/lib/puppet/network/http/compression.rb index d9b56f184..c8d001169 100644 --- a/lib/puppet/network/http/compression.rb +++ b/lib/puppet/network/http/compression.rb @@ -1,111 +1,114 @@ require 'puppet/network/http' module Puppet::Network::HTTP::Compression # this module function allows to use the right underlying # methods depending on zlib presence def module return(Puppet.features.zlib? ? Active : None) end module_function :module module Active require 'zlib' require 'stringio' # return an uncompressed body if the response has been # compressed def uncompress_body(response) case response['content-encoding'] when 'gzip' return Zlib::GzipReader.new(StringIO.new(response.body)).read when 'deflate' return Zlib::Inflate.new.inflate(response.body) when nil, 'identity' return response.body else raise Net::HTTPError.new("Unknown content encoding - #{response['content-encoding']}", response) end end def uncompress(response) raise Net::HTTPError.new("No block passed") unless block_given? case response['content-encoding'] when 'gzip','deflate' uncompressor = ZlibAdapter.new when nil, 'identity' uncompressor = IdentityAdapter.new else raise Net::HTTPError.new("Unknown content encoding - #{response['content-encoding']}", response) end yield uncompressor uncompressor.close end def add_accept_encoding(headers={}) headers['accept-encoding'] = 'gzip; q=1.0, deflate; q=1.0; identity' if Puppet.settings[:http_compression] headers end # This adapters knows how to uncompress both 'zlib' stream (the deflate algorithm from Content-Encoding) # and GZip streams. class ZlibAdapter def initialize # Create an inflater that knows to parse GZip streams and zlib streams. # This uses a property of the C Zlib library, documented as follow: # windowBits can also be greater than 15 for optional gzip decoding. Add # 32 to windowBits to enable zlib and gzip decoding with automatic header # detection, or add 16 to decode only the gzip format (the zlib format will # return a Z_DATA_ERROR). If a gzip stream is being decoded, strm->adler is # a crc32 instead of an adler32. @uncompressor = Zlib::Inflate.new(15 + 32) @first = true end def uncompress(chunk) out = @uncompressor.inflate(chunk) @first = false return out rescue Zlib::DataError => z # it can happen that we receive a raw deflate stream # which might make our inflate throw a data error. # in this case, we try with a verbatim (no header) # deflater. @uncompressor = Zlib::Inflate.new - retry if @first + if @first then + @first = false + retry + end raise end def close @uncompressor.finish @uncompressor.close end end end module None def uncompress_body(response) response.body end def add_accept_encoding(headers) headers end def uncompress(response) yield IdentityAdapter.new end end class IdentityAdapter def uncompress(chunk) chunk end def close end end end diff --git a/lib/puppet/network/http/handler.rb b/lib/puppet/network/http/handler.rb index 9e9356b2f..e192613ad 100644 --- a/lib/puppet/network/http/handler.rb +++ b/lib/puppet/network/http/handler.rb @@ -1,244 +1,244 @@ module Puppet::Network::HTTP end require 'puppet/network/http/api/v1' require 'puppet/network/rest_authorization' require 'puppet/network/rights' require 'resolv' module Puppet::Network::HTTP::Handler include Puppet::Network::HTTP::API::V1 include Puppet::Network::RestAuthorization attr_reader :server, :handler # Retrieve the accept header from the http request. def accept_header(request) raise NotImplementedError end # Retrieve the Content-Type header from the http request. def content_type_header(request) raise NotImplementedError end # Which format to use when serializing our response or interpreting the request. # IF the client provided a Content-Type use this, otherwise use the Accept header # and just pick the first value. def format_to_use(request) unless header = accept_header(request) raise ArgumentError, "An Accept header must be provided to pick the right format" end format = nil header.split(/,\s*/).each do |name| next unless format = Puppet::Network::FormatHandler.format(name) next unless format.suitable? return format end raise "No specified acceptable formats (#{header}) are functional on this machine" end def request_format(request) if header = content_type_header(request) header.gsub!(/\s*;.*$/,'') # strip any charset format = Puppet::Network::FormatHandler.mime(header) raise "Client sent a mime-type (#{header}) that doesn't correspond to a format we support" if format.nil? return format.name.to_s if format.suitable? end raise "No Content-Type header was received, it isn't possible to unserialize the request" end def format_to_mime(format) format.is_a?(Puppet::Network::Format) ? format.mime : format end def initialize_for_puppet(server) @server = server end # handle an HTTP request def process(request, response) indirection_request = uri2indirection(http_method(request), path(request), params(request)) check_authorization(indirection_request) send("do_#{indirection_request.method}", indirection_request, request, response) rescue SystemExit,NoMemoryError raise rescue Exception => e return do_exception(response, e) end # Set the response up, with the body and status. def set_response(response, body, status = 200) raise NotImplementedError end # Set the specified format as the content type of the response. def set_content_type(response, format) raise NotImplementedError end def do_exception(response, exception, status=400) if exception.is_a?(Puppet::Network::AuthorizationError) # make sure we return the correct status code # for authorization issues status = 403 if status == 400 end if exception.is_a?(Exception) puts exception.backtrace if Puppet[:trace] Puppet.err(exception) end set_content_type(response, "text/plain") set_response(response, exception.to_s, status) end # Execute our find. def do_find(indirection_request, request, response) unless result = indirection_request.model.find(indirection_request.key, indirection_request.to_hash) Puppet.info("Could not find #{indirection_request.indirection_name} for '#{indirection_request.key}'") return do_exception(response, "Could not find #{indirection_request.indirection_name} #{indirection_request.key}", 404) end # The encoding of the result must include the format to use, # and it needs to be used for both the rendering and as # the content type. format = format_to_use(request) set_content_type(response, format) if result.respond_to?(:render) set_response(response, result.render(format)) else set_response(response, result) end end # Execute our head. def do_head(indirection_request, request, response) unless indirection_request.model.head(indirection_request.key, indirection_request.to_hash) Puppet.info("Could not find #{indirection_request.indirection_name} for '#{indirection_request.key}'") return do_exception(response, "Could not find #{indirection_request.indirection_name} #{indirection_request.key}", 404) end # No need to set a response because no response is expected from a # HEAD request. All we need to do is not die. end # Execute our search. def do_search(indirection_request, request, response) result = indirection_request.model.search(indirection_request.key, indirection_request.to_hash) - if result.nil? or (result.is_a?(Array) and result.empty?) + if result.nil? return do_exception(response, "Could not find instances in #{indirection_request.indirection_name} with '#{indirection_request.key}'", 404) end format = format_to_use(request) set_content_type(response, format) set_response(response, indirection_request.model.render_multiple(format, result)) end # Execute our destroy. def do_destroy(indirection_request, request, response) result = indirection_request.model.destroy(indirection_request.key, indirection_request.to_hash) return_yaml_response(response, result) end # Execute our save. def do_save(indirection_request, request, response) data = body(request).to_s raise ArgumentError, "No data to save" if !data or data.empty? format = request_format(request) obj = indirection_request.model.convert_from(format, data) result = save_object(indirection_request, obj) return_yaml_response(response, result) end # resolve node name from peer's ip address # this is used when the request is unauthenticated def resolve_node(result) begin return Resolv.getname(result[:ip]) rescue => detail Puppet.err "Could not resolve #{result[:ip]}: #{detail}" end result[:ip] end private def return_yaml_response(response, body) set_content_type(response, Puppet::Network::FormatHandler.format("yaml")) set_response(response, body.to_yaml) end # LAK:NOTE This has to be here for testing; it's a stub-point so # we keep infinite recursion from happening. def save_object(ind_request, object) object.save(ind_request.key) end def get?(request) http_method(request) == 'GET' end def put?(request) http_method(request) == 'PUT' end def delete?(request) http_method(request) == 'DELETE' end # methods to be overridden by the including web server class def http_method(request) raise NotImplementedError end def path(request) raise NotImplementedError end def request_key(request) raise NotImplementedError end def body(request) raise NotImplementedError end def params(request) raise NotImplementedError end def decode_params(params) params.inject({}) do |result, ary| param, value = ary next result if param.nil? || param.empty? param = param.to_sym # These shouldn't be allowed to be set by clients # in the query string, for security reasons. next result if param == :node next result if param == :ip value = CGI.unescape(value) if value =~ /^---/ value = YAML.load(value) else value = true if value == "true" value = false if value == "false" value = Integer(value) if value =~ /^\d+$/ value = value.to_f if value =~ /^\d+\.\d+$/ end result[param] = value result end end end diff --git a/lib/puppet/network/rest_authconfig.rb b/lib/puppet/network/rest_authconfig.rb index 7a6147a82..e6067612a 100644 --- a/lib/puppet/network/rest_authconfig.rb +++ b/lib/puppet/network/rest_authconfig.rb @@ -1,88 +1,88 @@ require 'puppet/network/authconfig' module Puppet class Network::RestAuthConfig < Network::AuthConfig extend MonitorMixin attr_accessor :rights DEFAULT_ACL = [ { :acl => "~ ^\/catalog\/([^\/]+)$", :method => :find, :allow => '$1', :authenticated => true }, # this one will allow all file access, and thus delegate # to fileserver.conf { :acl => "/file" }, { :acl => "/certificate_revocation_list/ca", :method => :find, :authenticated => true }, { :acl => "/report", :method => :save, :authenticated => true }, { :acl => "/certificate/ca", :method => :find, :authenticated => false }, { :acl => "/certificate/", :method => :find, :authenticated => false }, { :acl => "/certificate_request", :method => [:find, :save], :authenticated => false }, { :acl => "/status", :method => [:find], :authenticated => true }, ] def self.main synchronize do add_acl = @main.nil? super @main.insert_default_acl if add_acl and !@main.exists? end @main end # check wether this request is allowed in our ACL # raise an Puppet::Network::AuthorizedError if the request # is denied. def allowed?(request) read # we're splitting the request in part because # fail_on_deny could as well be called in the XMLRPC context # with a ClientRequest. if authorization_failure_exception = @rights.is_request_forbidden_and_why?(request) Puppet.warning("Denying access: #{authorization_failure_exception}") raise authorization_failure_exception end end def initialize(file = nil, parsenow = true) super(file || Puppet[:rest_authconfig], parsenow) # if we didn't read a file (ie it doesn't exist) # make sure we can create some default rights @rights ||= Puppet::Network::Rights.new end def parse super() insert_default_acl end # force regular ACLs to be present def insert_default_acl DEFAULT_ACL.each do |acl| unless rights[acl[:acl]] - Puppet.info "Inserting default '#{acl[:acl]}'(#{acl[:authenticated] ? "auth" : "non-auth"}) acl because #{( !exists? ? "#{Puppet[:rest_authconfig]} doesn't exist" : "none where found in '#{@file}'")}" + Puppet.info "Inserting default '#{acl[:acl]}'(#{acl[:authenticated] ? "auth" : "non-auth"}) ACL because #{( !exists? ? "#{Puppet[:rest_authconfig]} doesn't exist" : "none were found in '#{@file}'")}" mk_acl(acl) end end # queue an empty (ie deny all) right for every other path # actually this is not strictly necessary as the rights system # denies not explicitely allowed paths unless rights["/"] rights.newright("/") rights.restrict_authenticated("/", :any) end end def mk_acl(acl) @rights.newright(acl[:acl]) @rights.allow(acl[:acl], acl[:allow] || "*") if method = acl[:method] method = [method] unless method.is_a?(Array) method.each { |m| @rights.restrict_method(acl[:acl], m) } end @rights.restrict_authenticated(acl[:acl], acl[:authenticated]) unless acl[:authenticated].nil? end end end diff --git a/lib/puppet/node/facts.rb b/lib/puppet/node/facts.rb index b77ad22d5..0a96e553b 100755 --- a/lib/puppet/node/facts.rb +++ b/lib/puppet/node/facts.rb @@ -1,70 +1,99 @@ +require 'time' + require 'puppet/node' require 'puppet/indirector' +require 'puppet/util/pson' + # Manage a given node's facts. This either accepts facts and stores them, or # returns facts for a given node. class Puppet::Node::Facts # Set up indirection, so that nodes can be looked for in # the node sources. extend Puppet::Indirector + extend Puppet::Util::Pson # We want to expire any cached nodes if the facts are saved. module NodeExpirer def save(key, instance) Puppet::Node.expire(instance.name) super end end indirects :facts, :terminus_setting => :facts_terminus, :extend => NodeExpirer attr_accessor :name, :values def add_local_facts values["clientcert"] = Puppet.settings[:certname] values["clientversion"] = Puppet.version.to_s values["environment"] ||= Puppet.settings[:environment] end def initialize(name, values = {}) @name = name @values = values add_internal end def downcase_if_necessary return unless Puppet.settings[:downcasefacts] Puppet.warning "DEPRECATION NOTICE: Fact downcasing is deprecated; please disable (20080122)" values.each do |fact, value| values[fact] = value.downcase if value.is_a?(String) end end # Convert all fact values into strings. def stringify values.each do |fact, value| values[fact] = value.to_s end end def ==(other) return false unless self.name == other.name strip_internal == other.send(:strip_internal) end + def timestamp=(time) + self.values[:_timestamp] = time + end + + def timestamp + self.values[:_timestamp] + end + + def self.from_pson(data) + result = new(data['name'], data['values']) + result.timestamp = Time.parse(data['timestamp']) + result.expiration = Time.parse(data['expiration']) + result + end + + def to_pson(*args) + { + 'expiration' => expiration, + 'name' => name, + 'timestamp' => timestamp, + 'values' => strip_internal, + }.to_pson(*args) + end + private # Add internal data to the facts for storage. def add_internal self.values[:_timestamp] = Time.now end # Strip out that internal data. def strip_internal newvals = values.dup newvals.find_all { |name, value| name.to_s =~ /^_/ }.each { |name, value| newvals.delete(name) } newvals end end diff --git a/lib/puppet/parser/ast/collection.rb b/lib/puppet/parser/ast/collection.rb index ef36b7143..565b83195 100644 --- a/lib/puppet/parser/ast/collection.rb +++ b/lib/puppet/parser/ast/collection.rb @@ -1,48 +1,49 @@ require 'puppet' require 'puppet/parser/ast/branch' require 'puppet/parser/collector' # An object that collects stored objects from the central cache and returns # them to the current host, yo. class Puppet::Parser::AST class Collection < AST::Branch attr_accessor :type, :query, :form attr_reader :override associates_doc # We return an object that does a late-binding evaluation. def evaluate(scope) str, code = query && query.safeevaluate(scope) resource_type = scope.find_resource_type(@type) + fail "Resource type #{@type} doesn't exist" unless resource_type newcoll = Puppet::Parser::Collector.new(scope, resource_type.name, str, code, self.form) scope.compiler.add_collection(newcoll) # overrides if any # Evaluate all of the specified params. if @override params = @override.collect { |param| param.safeevaluate(scope) } newcoll.add_override( :parameters => params, - :file => @file, - :line => @line, - :source => scope.source, - :scope => scope + :file => @file, + :line => @line, + :source => scope.source, + :scope => scope ) end newcoll end # Handle our parameter ourselves def override=(override) @override = if override.is_a?(AST::ASTArray) override else AST::ASTArray.new(:line => override.line,:file => override.file,:children => [override]) end end end end diff --git a/lib/puppet/parser/ast/leaf.rb b/lib/puppet/parser/ast/leaf.rb index fcdd219d7..77617e992 100644 --- a/lib/puppet/parser/ast/leaf.rb +++ b/lib/puppet/parser/ast/leaf.rb @@ -1,221 +1,221 @@ class Puppet::Parser::AST # The base class for all of the leaves of the parse trees. These # basically just have types and values. Both of these parameters # are simple values, not AST objects. class Leaf < AST attr_accessor :value, :type # Return our value. def evaluate(scope) @value end def match(value) @value == value end def to_s @value.to_s unless @value.nil? end end # The boolean class. True or false. Converts the string it receives # to a Ruby boolean. class Boolean < AST::Leaf # Use the parent method, but then convert to a real boolean. def initialize(hash) super unless @value == true or @value == false raise Puppet::DevError, "'#{@value}' is not a boolean" end @value end def to_s @value ? "true" : "false" end end # The base string class. class String < AST::Leaf def evaluate(scope) @value.dup end def to_s "\"#{@value}\"" end end # An uninterpreted string. class FlatString < AST::Leaf def evaluate(scope) @value end def to_s "\"#{@value}\"" end end class Concat < AST::Leaf def evaluate(scope) @value.collect { |x| x.evaluate(scope) }.collect{ |x| x == :undef ? '' : x }.join end def to_s - "concat(#{@value.join(',')})" + "#{@value.map { |s| s.to_s.gsub(/^"(.*)"$/, '\1') }.join}" end end # The 'default' option on case statements and selectors. class Default < AST::Leaf; end # Capitalized words; used mostly for type-defaults, but also # get returned by the lexer any other time an unquoted capitalized # word is found. class Type < AST::Leaf; end # Lower-case words. class Name < AST::Leaf; end # double-colon separated class names class ClassName < AST::Leaf; end # undef values; equiv to nil class Undef < AST::Leaf; end # Host names, either fully qualified or just the short name, or even a regex class HostName < AST::Leaf def initialize(hash) super # Note that this is an AST::Regex, not a Regexp @value = @value.to_s.downcase unless @value.is_a?(Regex) if @value =~ /[^-\w.]/ raise Puppet::DevError, "'#{@value}' is not a valid hostname" end end # implementing eql? and hash so that when an HostName is stored # in a hash it has the same hashing properties as the underlying value def eql?(value) value = value.value if value.is_a?(HostName) @value.eql?(value) end def hash @value.hash end def to_s @value.to_s end end # A simple variable. This object is only used during interpolation; # the VarDef class is used for assignment. class Variable < Name # Looks up the value of the object in the scope tree (does # not include syntactical constructs, like '$' and '{}'). def evaluate(scope) parsewrap do if (var = scope.lookupvar(@value, false)) == :undefined var = :undef end var end end def to_s "\$#{value}" end end class HashOrArrayAccess < AST::Leaf attr_accessor :variable, :key def evaluate_container(scope) container = variable.respond_to?(:evaluate) ? variable.safeevaluate(scope) : variable (container.is_a?(Hash) or container.is_a?(Array)) ? container : scope.lookupvar(container) end def evaluate_key(scope) key.respond_to?(:evaluate) ? key.safeevaluate(scope) : key end def array_index_or_key(object, key) if object.is_a?(Array) raise Puppet::ParserError, "#{key} is not an integer, but is used as an index of an array" unless key = Puppet::Parser::Scope.number?(key) end key end def evaluate(scope) object = evaluate_container(scope) accesskey = evaluate_key(scope) raise Puppet::ParseError, "#{variable} is not an hash or array when accessing it with #{accesskey}" unless object.is_a?(Hash) or object.is_a?(Array) object[array_index_or_key(object, accesskey)] end # Assign value to this hashkey or array index def assign(scope, value) object = evaluate_container(scope) accesskey = evaluate_key(scope) if object.is_a?(Hash) and object.include?(accesskey) raise Puppet::ParseError, "Assigning to the hash '#{variable}' with an existing key '#{accesskey}' is forbidden" end # assign to hash or array object[array_index_or_key(object, accesskey)] = value end def to_s "\$#{variable.to_s}[#{key.to_s}]" end end class Regex < AST::Leaf def initialize(hash) super @value = Regexp.new(@value) unless @value.is_a?(Regexp) end # we're returning self here to wrap the regexp and to be used in places # where a string would have been used, without modifying any client code. # For instance, in many places we have the following code snippet: # val = @val.safeevaluate(@scope) # if val.match(otherval) # ... # end # this way, we don't have to modify this test specifically for handling # regexes. def evaluate(scope) self end def evaluate_match(value, scope, options = {}) value = value.is_a?(String) ? value : value.to_s if matched = @value.match(value) scope.ephemeral_from(matched, options[:file], options[:line]) end matched end def match(value) @value.match(value) end def to_s "/#{@value.source}/" end end end diff --git a/lib/puppet/parser/functions/regsubst.rb b/lib/puppet/parser/functions/regsubst.rb index f655db7b3..b6bb5afcf 100644 --- a/lib/puppet/parser/functions/regsubst.rb +++ b/lib/puppet/parser/functions/regsubst.rb @@ -1,100 +1,93 @@ module Puppet::Parser::Functions newfunction( :regsubst, :type => :rvalue, :doc => " - Perform regexp replacement on a string or array of strings. +Perform regexp replacement on a string or array of strings. * *Parameters* (in order): - - _target_ The string or array of strings to operate on. If an array, the replacement will be performed on each of the elements in the array, and the return value will be an array. - - _regexp_ The regular expression matching the target string. If you want it anchored at the start and or end of the string, you must do that with ^ and $ yourself. - - _replacement_ Replacement string. Can contain back references to what was matched using \\0, \\1, and so on. - - _flags_ Optional. String of single letter flags for how the regexp is interpreted: - + * _target_ The string or array of strings to operate on. If an array, the replacement will be performed on each of the elements in the array, and the return value will be an array. + * _regexp_ The regular expression matching the target string. If you want it anchored at the start and or end of the string, you must do that with ^ and $ yourself. + * _replacement_ Replacement string. Can contain backreferences to what was matched using \\0 (whole match), \\1 (first set of parentheses), and so on. + * _flags_ Optional. String of single letter flags for how the regexp is interpreted: - *E* Extended regexps - *I* Ignore case in regexps - *M* Multiline regexps - *G* Global replacement; all occurrences of the regexp in each target string will be replaced. Without this, only the first occurrence will be replaced. - - _lang_ Optional. How to handle multibyte characters. A single-character string with the following values: - + * _encoding_ Optional. How to handle multibyte characters. A single-character string with the following values: - *N* None - *E* EUC - *S* SJIS - *U* UTF-8 * *Examples* Get the third octet from the node's IP address: - $i3 = regsubst($ipaddress,'^([0-9]+)[.]([0-9]+)[.]([0-9]+)[.]([0-9]+)$','\\3') + $i3 = regsubst($ipaddress,'^(\\d+)\\.(\\d+)\\.(\\d+)\\.(\\d+)$','\\3') Put angle brackets around each octet in the node's IP address: $x = regsubst($ipaddress, '([0-9]+)', '<\\1>', 'G') ") \ do |args| unless args.length.between?(3, 5) raise( Puppet::ParseError, "regsubst(): got #{args.length} arguments, expected 3 to 5") end target, regexp, replacement, flags, lang = args reflags = 0 operation = :sub if flags == nil flags = [] elsif flags.respond_to?(:split) flags = flags.split('') else raise( Puppet::ParseError, "regsubst(): bad flags parameter #{flags.class}:`#{flags}'") end flags.each do |f| case f when 'G' then operation = :gsub when 'E' then reflags |= Regexp::EXTENDED when 'I' then reflags |= Regexp::IGNORECASE when 'M' then reflags |= Regexp::MULTILINE else raise(Puppet::ParseError, "regsubst(): bad flag `#{f}'") end end begin re = Regexp.compile(regexp, reflags, lang) rescue RegexpError, TypeError raise( Puppet::ParseError, "regsubst(): Bad regular expression `#{regexp}'") end if target.respond_to?(operation) # String parameter -> string result result = target.send(operation, re, replacement) elsif target.respond_to?(:collect) and target.respond_to?(:all?) and target.all? { |e| e.respond_to?(operation) } # Array parameter -> array result result = target.collect { |e| e.send(operation, re, replacement) } else raise( Puppet::ParseError, "regsubst(): bad target #{target.class}:`#{target}'") end return result end end diff --git a/lib/puppet/parser/functions/split.rb b/lib/puppet/parser/functions/split.rb index 52394095a..ad027865b 100644 --- a/lib/puppet/parser/functions/split.rb +++ b/lib/puppet/parser/functions/split.rb @@ -1,29 +1,29 @@ module Puppet::Parser::Functions newfunction( :split, :type => :rvalue, :doc => "\ Split a string variable into an array using the specified split regexp. - Usage: +*Example:* $string = 'v1.v2:v3.v4' $array_var1 = split($string, ':') $array_var2 = split($string, '[.]') $array_var3 = split($string, '[.:]') -$array_var1 now holds the result ['v1.v2', 'v3.v4'], -while $array_var2 holds ['v1', 'v2:v3', 'v4'], and -$array_var3 holds ['v1', 'v2', 'v3', 'v4']. +`$array_var1` now holds the result `['v1.v2', 'v3.v4']`, +while `$array_var2` holds `['v1', 'v2:v3', 'v4']`, and +`$array_var3` holds `['v1', 'v2', 'v3', 'v4']`. -Note that in the second example, we split on a string that contains -a regexp meta-character (.), and that needs protection. A simple +Note that in the second example, we split on a literal string that contains +a regexp meta-character (.), which must be escaped. A simple way to do that for a single character is to enclose it in square -brackets.") do |args| +brackets; a backslash will also escape a single character.") do |args| raise Puppet::ParseError, ("split(): wrong number of arguments (#{args.length}; must be 2)") if args.length != 2 return args[0].split(Regexp.compile(args[1])) end end diff --git a/lib/puppet/parser/grammar.ra b/lib/puppet/parser/grammar.ra index 7a316d4d7..af0ab182b 100644 --- a/lib/puppet/parser/grammar.ra +++ b/lib/puppet/parser/grammar.ra @@ -1,875 +1,873 @@ # vim: syntax=ruby # the parser class Puppet::Parser::Parser token STRING DQPRE DQMID DQPOST token LBRACK RBRACK LBRACE RBRACE SYMBOL FARROW COMMA TRUE token FALSE EQUALS APPENDS LESSEQUAL NOTEQUAL DOT COLON LLCOLLECT RRCOLLECT token QMARK LPAREN RPAREN ISEQUAL GREATEREQUAL GREATERTHAN LESSTHAN token IF ELSE IMPORT DEFINE ELSIF VARIABLE CLASS INHERITS NODE BOOLEAN token NAME SEMIC CASE DEFAULT AT LCOLLECT RCOLLECT CLASSNAME CLASSREF token NOT OR AND UNDEF PARROW PLUS MINUS TIMES DIV LSHIFT RSHIFT UMINUS token MATCH NOMATCH REGEX IN_EDGE OUT_EDGE IN_EDGE_SUB OUT_EDGE_SUB token IN prechigh right NOT nonassoc UMINUS left IN MATCH NOMATCH left TIMES DIV left MINUS PLUS left LSHIFT RSHIFT left NOTEQUAL ISEQUAL left GREATEREQUAL GREATERTHAN LESSTHAN LESSEQUAL left AND left OR preclow rule program: statements { if val[0] # Make sure we always return an array. if val[0].is_a?(AST::ASTArray) if val[0].children.empty? result = nil else result = val[0] end else result = aryfy(val[0]) end else result = nil end } | nil statements: statement | statements statement { if val[0] and val[1] if val[0].instance_of?(AST::ASTArray) val[0].push(val[1]) result = val[0] else result = ast AST::ASTArray, :children => [val[0],val[1]] end elsif obj = (val[0] || val[1]) result = obj else result = nil end } # The main list of valid statements statement: resource | virtualresource | collection | assignment | casestatement | ifstatement_begin | import | fstatement | definition | hostclass | nodedef | resourceoverride | append | relationship relationship: relationship_side edge relationship_side { result = AST::Relationship.new(val[0], val[2], val[1][:value], ast_context) } | relationship edge relationship_side { result = AST::Relationship.new(val[0], val[2], val[1][:value], ast_context) } relationship_side: resource | resourceref | collection edge: IN_EDGE | OUT_EDGE | IN_EDGE_SUB | OUT_EDGE_SUB fstatement: NAME LPAREN funcvalues RPAREN { args = aryfy(val[2]) result = ast AST::Function, :name => val[0][:value], :line => val[0][:line], :arguments => args, :ftype => :statement } | NAME LPAREN funcvalues COMMA RPAREN { args = aryfy(val[2]) result = ast AST::Function, :name => val[0][:value], :line => val[0][:line], :arguments => args, :ftype => :statement } | NAME LPAREN RPAREN { result = ast AST::Function, :name => val[0][:value], :line => val[0][:line], :arguments => AST::ASTArray.new({}), :ftype => :statement } | NAME funcvalues { args = aryfy(val[1]) result = ast AST::Function, :name => val[0][:value], :line => val[0][:line], :arguments => args, :ftype => :statement } funcvalues: namestring | resourceref | funcvalues COMMA namestring { result = aryfy(val[0], val[2]) result.line = @lexer.line result.file = @lexer.file } | funcvalues COMMA resourceref { unless val[0].is_a?(AST::ASTArray) val[0] = aryfy(val[0]) end val[0].push(val[2]) result = val[0] } # This is *almost* an rvalue, but I couldn't get a full # rvalue to work without scads of shift/reduce conflicts. namestring: name | variable | type | boolean | funcrvalue | selector | quotedtext | hasharrayaccesses | CLASSNAME { result = ast AST::Name, :value => val[0][:value] } resource: classname LBRACE resourceinstances endsemi RBRACE { @lexer.commentpop array = val[2] array = [array] if array.instance_of?(AST::ResourceInstance) result = ast AST::ASTArray # this iterates across each specified resourceinstance array.each { |instance| raise Puppet::Dev, "Got something that isn't an instance" unless instance.instance_of?(AST::ResourceInstance) # now, i need to somehow differentiate between those things with # arrays in their names, and normal things result.push ast( AST::Resource, :type => val[0], :title => instance[0], :parameters => instance[1]) } } | classname LBRACE params endcomma RBRACE { # This is a deprecated syntax. error "All resource specifications require names" } | classref LBRACE params endcomma RBRACE { # a defaults setting for a type @lexer.commentpop result = ast(AST::ResourceDefaults, :type => val[0], :parameters => val[2]) } # Override a value set elsewhere in the configuration. resourceoverride: resourceref LBRACE anyparams endcomma RBRACE { @lexer.commentpop result = ast AST::ResourceOverride, :object => val[0], :parameters => val[2] } # Exported and virtual resources; these don't get sent to the client # unless they get collected elsewhere in the db. virtualresource: at resource { type = val[0] if (type == :exported and ! Puppet[:storeconfigs]) and ! Puppet[:parseonly] Puppet.warning addcontext("You cannot collect without storeconfigs being set") end error "Defaults are not virtualizable" if val[1].is_a? AST::ResourceDefaults method = type.to_s + "=" # Just mark our resources as exported and pass them through. if val[1].instance_of?(AST::ASTArray) val[1].each do |obj| obj.send(method, true) end else val[1].send(method, true) end result = val[1] } at: AT { result = :virtual } | AT AT { result = :exported } # A collection statement. Currently supports no arguments at all, but eventually # will, I assume. collection: classref collectrhand LBRACE anyparams endcomma RBRACE { @lexer.commentpop Puppet.warning addcontext("Collection names must now be capitalized") if val[0] =~ /^[a-z]/ type = val[0].downcase args = {:type => type} if val[1].is_a?(AST::CollExpr) args[:query] = val[1] args[:query].type = type args[:form] = args[:query].form else args[:form] = val[1] end if args[:form] == :exported and ! Puppet[:storeconfigs] and ! Puppet[:parseonly] Puppet.warning addcontext("You cannot collect exported resources without storeconfigs being set; the collection will be ignored") end args[:override] = val[3] result = ast AST::Collection, args } | classref collectrhand { if val[0] =~ /^[a-z]/ Puppet.warning addcontext("Collection names must now be capitalized") end type = val[0].downcase args = {:type => type } if val[1].is_a?(AST::CollExpr) args[:query] = val[1] args[:query].type = type args[:form] = args[:query].form else args[:form] = val[1] end if args[:form] == :exported and ! Puppet[:storeconfigs] and ! Puppet[:parseonly] Puppet.warning addcontext("You cannot collect exported resources without storeconfigs being set; the collection will be ignored") end result = ast AST::Collection, args } collectrhand: LCOLLECT collstatements RCOLLECT { if val[1] result = val[1] result.form = :virtual else result = :virtual end } | LLCOLLECT collstatements RRCOLLECT { if val[1] result = val[1] result.form = :exported else result = :exported end } # A mini-language for handling collection comparisons. This is organized # to avoid the need for precedence indications. collstatements: nil | collstatement | collstatements colljoin collstatement { result = ast AST::CollExpr, :test1 => val[0], :oper => val[1], :test2 => val[2] } collstatement: collexpr | LPAREN collstatements RPAREN { result = val[1] result.parens = true } colljoin: AND { result=val[0][:value] } | OR { result=val[0][:value] } collexpr: colllval ISEQUAL simplervalue { result = ast AST::CollExpr, :test1 => val[0], :oper => val[1][:value], :test2 => val[2] #result = ast AST::CollExpr #result.push *val } | colllval NOTEQUAL simplervalue { result = ast AST::CollExpr, :test1 => val[0], :oper => val[1][:value], :test2 => val[2] #result = ast AST::CollExpr #result.push *val } colllval: variable | name resourceinst: resourcename COLON params endcomma { result = ast AST::ResourceInstance, :children => [val[0],val[2]] } resourceinstances: resourceinst | resourceinstances SEMIC resourceinst { if val[0].instance_of?(AST::ResourceInstance) result = ast AST::ASTArray, :children => [val[0],val[2]] else val[0].push val[2] result = val[0] end } endsemi: # nothing | SEMIC undef: UNDEF { result = ast AST::Undef, :value => :undef } name: NAME { result = ast AST::Name, :value => val[0][:value], :line => val[0][:line] } type: CLASSREF { result = ast AST::Type, :value => val[0][:value], :line => val[0][:line] } resourcename: quotedtext | name | type | selector | variable | array | hasharrayaccesses assignment: VARIABLE EQUALS expression { raise Puppet::ParseError, "Cannot assign to variables in other namespaces" if val[0][:value] =~ /::/ # this is distinct from referencing a variable variable = ast AST::Name, :value => val[0][:value], :line => val[0][:line] result = ast AST::VarDef, :name => variable, :value => val[2], :line => val[0][:line] } | hasharrayaccess EQUALS expression { result = ast AST::VarDef, :name => val[0], :value => val[2] } append: VARIABLE APPENDS expression { variable = ast AST::Name, :value => val[0][:value], :line => val[0][:line] result = ast AST::VarDef, :name => variable, :value => val[2], :append => true, :line => val[0][:line] } params: # nothing { result = ast AST::ASTArray } | param { result = val[0] } | params COMMA param { if val[0].instance_of?(AST::ASTArray) val[0].push(val[2]) result = val[0] else result = ast AST::ASTArray, :children => [val[0],val[2]] end } param: NAME FARROW rvalue { result = ast AST::ResourceParam, :param => val[0][:value], :line => val[0][:line], :value => val[2] } addparam: NAME PARROW rvalue { result = ast AST::ResourceParam, :param => val[0][:value], :line => val[0][:line], :value => val[2], :add => true } anyparam: param | addparam anyparams: # nothing { result = ast AST::ASTArray } | anyparam { result = val[0] } | anyparams COMMA anyparam { if val[0].instance_of?(AST::ASTArray) val[0].push(val[2]) result = val[0] else result = ast AST::ASTArray, :children => [val[0],val[2]] end } rvalues: rvalue | rvalues comma rvalue { if val[0].instance_of?(AST::ASTArray) result = val[0].push(val[2]) else result = ast AST::ASTArray, :children => [val[0],val[2]] end } simplervalue: quotedtext | name | type | boolean | selector | variable rvalue: quotedtext | name | type | boolean | selector | variable | array | hash | hasharrayaccesses | resourceref | funcrvalue | undef # We currently require arguments in these functions. funcrvalue: NAME LPAREN funcvalues RPAREN { args = aryfy(val[2]) result = ast AST::Function, :name => val[0][:value], :line => val[0][:line], :arguments => args, :ftype => :rvalue } | NAME LPAREN RPAREN { result = ast AST::Function, :name => val[0][:value], :line => val[0][:line], :arguments => AST::ASTArray.new({}), :ftype => :rvalue } quotedtext: STRING { result = ast AST::String, :value => val[0][:value], :line => val[0][:line] } | DQPRE dqrval { result = ast AST::Concat, :value => [ast(AST::String,val[0])]+val[1], :line => val[0][:line] } dqrval: expression dqtail { result = [val[0]] + val[1] } dqtail: DQPOST { result = [ast(AST::String,val[0])] } | DQMID dqrval { result = [ast(AST::String,val[0])] + val[1] } boolean: BOOLEAN { result = ast AST::Boolean, :value => val[0][:value], :line => val[0][:line] } resourceref: NAME LBRACK rvalues RBRACK { Puppet.warning addcontext("Deprecation notice: Resource references should now be capitalized") result = ast AST::ResourceReference, :type => val[0][:value], :line => val[0][:line], :title => val[2] } | classref LBRACK rvalues RBRACK { result = ast AST::ResourceReference, :type => val[0], :title => val[2] } ifstatement_begin: IF ifstatement { result = val[1] } ifstatement: expression LBRACE statements RBRACE else { @lexer.commentpop args = { :test => val[0], :statements => val[2] } args[:else] = val[4] if val[4] result = ast AST::IfStatement, args } | expression LBRACE RBRACE else { @lexer.commentpop args = { :test => val[0], :statements => ast(AST::Nop) } args[:else] = val[3] if val[3] result = ast AST::IfStatement, args } else: # nothing | ELSIF ifstatement { result = ast AST::Else, :statements => val[1] } | ELSE LBRACE statements RBRACE { @lexer.commentpop result = ast AST::Else, :statements => val[2] } | ELSE LBRACE RBRACE { @lexer.commentpop result = ast AST::Else, :statements => ast(AST::Nop) } # Unlike yacc/bison, it seems racc # gives tons of shift/reduce warnings # with the following syntax: # # expression: ... # | expression arithop expressio { ... } # # arithop: PLUS | MINUS | DIVIDE | TIMES ... # # So I had to develop the expression by adding one rule # per operator :-( expression: rvalue | expression IN rvalue { result = ast AST::InOperator, :lval => val[0], :rval => val[2] } | expression MATCH regex { result = ast AST::MatchOperator, :operator => val[1][:value], :lval => val[0], :rval => val[2] } | expression NOMATCH regex { result = ast AST::MatchOperator, :operator => val[1][:value], :lval => val[0], :rval => val[2] } | expression PLUS expression { result = ast AST::ArithmeticOperator, :operator => val[1][:value], :lval => val[0], :rval => val[2] } | expression MINUS expression { result = ast AST::ArithmeticOperator, :operator => val[1][:value], :lval => val[0], :rval => val[2] } | expression DIV expression { result = ast AST::ArithmeticOperator, :operator => val[1][:value], :lval => val[0], :rval => val[2] } | expression TIMES expression { result = ast AST::ArithmeticOperator, :operator => val[1][:value], :lval => val[0], :rval => val[2] } | expression LSHIFT expression { result = ast AST::ArithmeticOperator, :operator => val[1][:value], :lval => val[0], :rval => val[2] } | expression RSHIFT expression { result = ast AST::ArithmeticOperator, :operator => val[1][:value], :lval => val[0], :rval => val[2] } | MINUS expression =UMINUS { result = ast AST::Minus, :value => val[1] } | expression NOTEQUAL expression { result = ast AST::ComparisonOperator, :operator => val[1][:value], :lval => val[0], :rval => val[2] } | expression ISEQUAL expression { result = ast AST::ComparisonOperator, :operator => val[1][:value], :lval => val[0], :rval => val[2] } | expression GREATERTHAN expression { result = ast AST::ComparisonOperator, :operator => val[1][:value], :lval => val[0], :rval => val[2] } | expression GREATEREQUAL expression { result = ast AST::ComparisonOperator, :operator => val[1][:value], :lval => val[0], :rval => val[2] } | expression LESSTHAN expression { result = ast AST::ComparisonOperator, :operator => val[1][:value], :lval => val[0], :rval => val[2] } | expression LESSEQUAL expression { result = ast AST::ComparisonOperator, :operator => val[1][:value], :lval => val[0], :rval => val[2] } | NOT expression { result = ast AST::Not, :value => val[1] } | expression AND expression { result = ast AST::BooleanOperator, :operator => val[1][:value], :lval => val[0], :rval => val[2] } | expression OR expression { result = ast AST::BooleanOperator, :operator => val[1][:value], :lval => val[0], :rval => val[2] } | LPAREN expression RPAREN { result = val[1] } casestatement: CASE rvalue LBRACE caseopts RBRACE { @lexer.commentpop options = val[3] options = ast AST::ASTArray, :children => [val[3]] unless options.instance_of?(AST::ASTArray) result = ast AST::CaseStatement, :test => val[1], :options => options } caseopts: caseopt | caseopts caseopt { if val[0].instance_of?(AST::ASTArray) val[0].push val[1] result = val[0] else result = ast AST::ASTArray, :children => [val[0], val[1]] end } caseopt: casevalues COLON LBRACE statements RBRACE { @lexer.commentpop result = ast AST::CaseOpt, :value => val[0], :statements => val[3] } | casevalues COLON LBRACE RBRACE { @lexer.commentpop result = ast( AST::CaseOpt, :value => val[0], :statements => ast(AST::ASTArray) ) } casevalues: selectlhand | casevalues COMMA selectlhand { if val[0].instance_of?(AST::ASTArray) val[0].push(val[2]) result = val[0] else result = ast AST::ASTArray, :children => [val[0],val[2]] end } selector: selectlhand QMARK svalues { result = ast AST::Selector, :param => val[0], :values => val[2] } svalues: selectval | LBRACE sintvalues endcomma RBRACE { @lexer.commentpop result = val[1] } sintvalues: selectval | sintvalues comma selectval { if val[0].instance_of?(AST::ASTArray) val[0].push(val[2]) result = val[0] else result = ast AST::ASTArray, :children => [val[0],val[2]] end } selectval: selectlhand FARROW rvalue { result = ast AST::ResourceParam, :param => val[0], :value => val[2] } selectlhand: name | type | quotedtext | variable | funcrvalue | boolean | undef + | hasharrayaccess | DEFAULT { result = ast AST::Default, :value => val[0][:value], :line => val[0][:line] } | regex # These are only used for importing, and we don't interpolate there. string: STRING { result = [val[0][:value]] } strings: string | strings COMMA string { result = val[0] += val[2] } import: IMPORT strings { val[1].each do |file| import(file) end result = AST::ASTArray.new(:children => []) } # Disable definition inheritance for now. 8/27/06, luke #definition: DEFINE NAME argumentlist parent LBRACE statements RBRACE { definition: DEFINE classname argumentlist LBRACE statements RBRACE { @lexer.commentpop newdefine classname(val[1]), :arguments => val[2], :code => val[4], :line => val[0][:line] @lexer.indefine = false result = nil #} | DEFINE NAME argumentlist parent LBRACE RBRACE { } | DEFINE classname argumentlist LBRACE RBRACE { @lexer.commentpop newdefine classname(val[1]), :arguments => val[2], :line => val[0][:line] @lexer.indefine = false result = nil } #hostclass: CLASS NAME argumentlist parent LBRACE statements RBRACE { hostclass: CLASS classname argumentlist classparent LBRACE statements RBRACE { @lexer.commentpop # Our class gets defined in the parent namespace, not our own. @lexer.namepop newclass classname(val[1]), :arguments => val[2], :parent => val[3], :code => val[5], :line => val[0][:line] result = nil } | CLASS classname argumentlist classparent LBRACE RBRACE { @lexer.commentpop # Our class gets defined in the parent namespace, not our own. @lexer.namepop newclass classname(val[1]), :arguments => val[2], :parent => val[3], :line => val[0][:line] result = nil } nodedef: NODE hostnames nodeparent LBRACE statements RBRACE { @lexer.commentpop newnode val[1], :parent => val[2], :code => val[4], :line => val[0][:line] result = nil } | NODE hostnames nodeparent LBRACE RBRACE { @lexer.commentpop newnode val[1], :parent => val[2], :line => val[0][:line] result = nil } classref: CLASSREF { result = val[0][:value] } classname: NAME { result = val[0][:value] } | CLASSNAME { result = val[0][:value] } | CLASS { result = "class" } # Multiple hostnames, as used for node names. These are all literal # strings, not AST objects. hostnames: nodename | hostnames COMMA nodename { result = val[0] result = [result] unless result.is_a?(Array) result << val[2] } nodename: hostname { result = ast AST::HostName, :value => val[0] } hostname: NAME { result = val[0][:value] } | STRING { result = val[0][:value] } | DEFAULT { result = val[0][:value] } | regex nil: { result = nil } nothing: { result = ast AST::ASTArray, :children => [] } argumentlist: nil | LPAREN nothing RPAREN { result = nil } | LPAREN arguments RPAREN { result = val[1] result = [result] unless result[0].is_a?(Array) } arguments: argument | arguments COMMA argument { result = val[0] result = [result] unless result[0].is_a?(Array) result << val[2] } argument: NAME EQUALS rvalue { Puppet.warning addcontext("Deprecation notice: must now include '$' in prototype") result = [val[0][:value], val[2]] } | NAME { Puppet.warning addcontext("Deprecation notice: must now include '$' in prototype") result = [val[0][:value]] } | VARIABLE EQUALS rvalue { result = [val[0][:value], val[2]] } | VARIABLE { result = [val[0][:value]] } nodeparent: nil | INHERITS hostname { result = val[1] } classparent: nil | INHERITS classnameordefault { result = val[1] } classnameordefault: classname | DEFAULT variable: VARIABLE { result = ast AST::Variable, :value => val[0][:value], :line => val[0][:line] } array: LBRACK rvalues RBRACK { if val[1].instance_of?(AST::ASTArray) result = val[1] else result = ast AST::ASTArray, :children => [val[1]] end } | LBRACK rvalues COMMA RBRACK { if val[1].instance_of?(AST::ASTArray) result = val[1] else result = ast AST::ASTArray, :children => [val[1]] end } | LBRACK RBRACK { result = ast AST::ASTArray } comma: FARROW | COMMA endcomma: # nothing | COMMA { result = nil } regex: REGEX { result = ast AST::Regex, :value => val[0][:value] } hash: LBRACE hashpairs RBRACE { if val[1].instance_of?(AST::ASTHash) result = val[1] else result = ast AST::ASTHash, { :value => val[1] } end } | LBRACE hashpairs COMMA RBRACE { if val[1].instance_of?(AST::ASTHash) result = val[1] else result = ast AST::ASTHash, { :value => val[1] } end } | LBRACE RBRACE { result = ast AST::ASTHash } hashpairs: hashpair | hashpairs COMMA hashpair { if val[0].instance_of?(AST::ASTHash) result = val[0].merge(val[2]) else result = ast AST::ASTHash, :value => val[0] result.merge(val[2]) end } hashpair: key FARROW rvalue { result = ast AST::ASTHash, { :value => { val[0] => val[2] } } } key: NAME { result = val[0][:value] } | quotedtext { result = val[0] } hasharrayaccess: VARIABLE LBRACK rvalue RBRACK { result = ast AST::HashOrArrayAccess, :variable => val[0][:value], :key => val[2] } hasharrayaccesses: hasharrayaccess - | hasharrayaccess LBRACK rvalue RBRACK { + | hasharrayaccesses LBRACK rvalue RBRACK { result = ast AST::HashOrArrayAccess, :variable => val[0], :key => val[2] } end ---- header ---- require 'puppet' require 'puppet/util/loadedfile' require 'puppet/parser/lexer' require 'puppet/parser/ast' module Puppet class ParseError < Puppet::Error; end class ImportError < Racc::ParseError; end class AlreadyImportedError < ImportError; end end ---- inner ---- # It got too annoying having code in a file that needs to be compiled. require 'puppet/parser/parser_support' # Make emacs happy # Local Variables: # mode: ruby # End: - -# $Id$ - diff --git a/lib/puppet/parser/lexer.rb b/lib/puppet/parser/lexer.rb index 31d39ae2f..9a25263f6 100644 --- a/lib/puppet/parser/lexer.rb +++ b/lib/puppet/parser/lexer.rb @@ -1,579 +1,582 @@ # the scanner/lexer require 'strscan' require 'puppet' module Puppet class LexError < RuntimeError; end end module Puppet::Parser; end class Puppet::Parser::Lexer attr_reader :last, :file, :lexing_context, :token_queue attr_accessor :line, :indefine def lex_error msg raise Puppet::LexError.new(msg) end class Token attr_accessor :regex, :name, :string, :skip, :incr_line, :skip_text, :accumulate def initialize(regex, name) if regex.is_a?(String) @name, @string = name, regex @regex = Regexp.new(Regexp.escape(@string)) else @name, @regex = name, regex end end # MQR: Why not just alias? %w{skip accumulate}.each do |method| define_method(method+"?") do self.send(method) end end def to_s if self.string @string else @name.to_s end end def acceptable?(context={}) # By default tokens are aceeptable in any context true end end # Maintain a list of tokens. class TokenList attr_reader :regex_tokens, :string_tokens def [](name) @tokens[name] end # Create a new token. def add_token(name, regex, options = {}, &block) token = Token.new(regex, name) raise(ArgumentError, "Token #{name} already exists") if @tokens.include?(name) @tokens[token.name] = token if token.string @string_tokens << token @tokens_by_string[token.string] = token else @regex_tokens << token end options.each do |name, option| token.send(name.to_s + "=", option) end token.meta_def(:convert, &block) if block_given? token end def initialize @tokens = {} @regex_tokens = [] @string_tokens = [] @tokens_by_string = {} end # Look up a token by its value, rather than name. def lookup(string) @tokens_by_string[string] end # Define more tokens. def add_tokens(hash) hash.each do |regex, name| add_token(name, regex) end end # Sort our tokens by length, so we know once we match, we're done. # This helps us avoid the O(n^2) nature of token matching. def sort_tokens @string_tokens.sort! { |a, b| b.string.length <=> a.string.length } end end TOKENS = TokenList.new TOKENS.add_tokens( '[' => :LBRACK, ']' => :RBRACK, '{' => :LBRACE, '}' => :RBRACE, '(' => :LPAREN, ')' => :RPAREN, '=' => :EQUALS, '+=' => :APPENDS, '==' => :ISEQUAL, '>=' => :GREATEREQUAL, '>' => :GREATERTHAN, '<' => :LESSTHAN, '<=' => :LESSEQUAL, '!=' => :NOTEQUAL, '!' => :NOT, ',' => :COMMA, '.' => :DOT, ':' => :COLON, '@' => :AT, '<<|' => :LLCOLLECT, '->' => :IN_EDGE, '<-' => :OUT_EDGE, '~>' => :IN_EDGE_SUB, '<~' => :OUT_EDGE_SUB, '|>>' => :RRCOLLECT, '<|' => :LCOLLECT, '|>' => :RCOLLECT, ';' => :SEMIC, '?' => :QMARK, '\\' => :BACKSLASH, '=>' => :FARROW, '+>' => :PARROW, '+' => :PLUS, '-' => :MINUS, '/' => :DIV, '*' => :TIMES, '<<' => :LSHIFT, '>>' => :RSHIFT, '=~' => :MATCH, '!~' => :NOMATCH, %r{([a-z][-\w]*)?(::[a-z][-\w]*)+} => :CLASSNAME, # Require '::' in the class name, else we'd compete with NAME %r{((::){0,1}[A-Z][-\w]*)+} => :CLASSREF, "" => :STRING, "" => :DQPRE, "" => :DQMID, "" => :DQPOST, "" => :BOOLEAN ) TOKENS.add_token :NUMBER, %r{\b(?:0[xX][0-9A-Fa-f]+|0?\d+(?:\.\d+)?(?:[eE]-?\d+)?)\b} do |lexer, value| [TOKENS[:NAME], value] end #:stopdoc: # Issue #4161 def (TOKENS[:NUMBER]).acceptable?(context={}) ![:DQPRE,:DQMID].include? context[:after] end #:startdoc: TOKENS.add_token :NAME, %r{[a-z0-9][-\w]*} do |lexer, value| string_token = self # we're looking for keywords here if tmp = KEYWORDS.lookup(value) string_token = tmp if [:TRUE, :FALSE].include?(string_token.name) value = eval(value) string_token = TOKENS[:BOOLEAN] end end [string_token, value] end [:NAME,:CLASSNAME,:CLASSREF].each { |name_token| #:stopdoc: # Issue #4161 def (TOKENS[name_token]).acceptable?(context={}) ![:DQPRE,:DQMID].include? context[:after] end #:startdoc: } TOKENS.add_token :COMMENT, %r{#.*}, :accumulate => true, :skip => true do |lexer,value| value.sub!(/# ?/,'') [self, value] end TOKENS.add_token :MLCOMMENT, %r{/\*(.*?)\*/}m, :accumulate => true, :skip => true do |lexer, value| lexer.line += value.count("\n") value.sub!(/^\/\* ?/,'') value.sub!(/ ?\*\/$/,'') [self,value] end TOKENS.add_token :REGEX, %r{/[^/\n]*/} do |lexer, value| # Make sure we haven't matched an escaped / while value[-2..-2] == '\\' other = lexer.scan_until(%r{/}) value += other end regex = value.sub(%r{\A/}, "").sub(%r{/\Z}, '').gsub("\\/", "/") [self, Regexp.new(regex)] end #:stopdoc: # Issue #4161 def (TOKENS[:REGEX]).acceptable?(context={}) [:NODE,:LBRACE,:RBRACE,:MATCH,:NOMATCH,:COMMA].include? context[:after] end #:startdoc: TOKENS.add_token :RETURN, "\n", :skip => true, :incr_line => true, :skip_text => true TOKENS.add_token :SQUOTE, "'" do |lexer, value| [TOKENS[:STRING], lexer.slurpstring(value,["'"],:ignore_invalid_escapes).first ] end DQ_initial_token_types = {'$' => :DQPRE,'"' => :STRING} DQ_continuation_token_types = {'$' => :DQMID,'"' => :DQPOST} TOKENS.add_token :DQUOTE, /"/ do |lexer, value| lexer.tokenize_interpolated_string(DQ_initial_token_types) end TOKENS.add_token :DQCONT, /\}/ do |lexer, value| lexer.tokenize_interpolated_string(DQ_continuation_token_types) end #:stopdoc: # Issue #4161 def (TOKENS[:DQCONT]).acceptable?(context={}) context[:string_interpolation_depth] > 0 end #:startdoc: TOKENS.add_token :DOLLAR_VAR, %r{\$(\w*::)*\w+} do |lexer, value| [TOKENS[:VARIABLE],value[1..-1]] end TOKENS.add_token :VARIABLE, %r{(\w*::)*\w+} #:stopdoc: # Issue #4161 def (TOKENS[:VARIABLE]).acceptable?(context={}) [:DQPRE,:DQMID].include? context[:after] end #:startdoc: TOKENS.sort_tokens @@pairs = { "{" => "}", "(" => ")", "[" => "]", "<|" => "|>", "<<|" => "|>>" } KEYWORDS = TokenList.new KEYWORDS.add_tokens( "case" => :CASE, "class" => :CLASS, "default" => :DEFAULT, "define" => :DEFINE, "import" => :IMPORT, "if" => :IF, "elsif" => :ELSIF, "else" => :ELSE, "inherits" => :INHERITS, "node" => :NODE, "and" => :AND, "or" => :OR, "undef" => :UNDEF, "false" => :FALSE, "true" => :TRUE, "in" => :IN ) def clear initvars end def expected return nil if @expected.empty? name = @expected[-1] TOKENS.lookup(name) or lex_error "Could not find expected token #{name}" end # scan the whole file # basically just used for testing def fullscan array = [] self.scan { |token, str| # Ignore any definition nesting problems @indefine = false array.push([token,str]) } array end def file=(file) @file = file @line = 1 @scanner = StringScanner.new(File.read(file)) end def shift_token @token_queue.shift end def find_string_token # We know our longest string token is three chars, so try each size in turn # until we either match or run out of chars. This way our worst-case is three # tries, where it is otherwise the number of string token we have. Also, # the lookups are optimized hash lookups, instead of regex scans. # s = @scanner.peek(3) token = TOKENS.lookup(s[0,3]) || TOKENS.lookup(s[0,2]) || TOKENS.lookup(s[0,1]) [ token, token && @scanner.scan(token.regex) ] end # Find the next token that matches a regex. We look for these first. def find_regex_token @regex += 1 best_token = nil best_length = 0 # I tried optimizing based on the first char, but it had # a slightly negative affect and was a good bit more complicated. TOKENS.regex_tokens.each do |token| if length = @scanner.match?(token.regex) and token.acceptable?(lexing_context) # We've found a longer match if length > best_length best_length = length best_token = token end end end return best_token, @scanner.scan(best_token.regex) if best_token end # Find the next token, returning the string and the token. def find_token @find += 1 shift_token || find_regex_token || find_string_token end def indefine? if defined?(@indefine) @indefine else false end end def initialize @find = 0 @regex = 0 initvars end def initvars @line = 1 @previous_token = nil @scanner = nil @file = nil # AAARRGGGG! okay, regexes in ruby are bloody annoying # no one else has "\n" =~ /\s/ @skip = %r{[ \t\r]+} @namestack = [] @token_queue = [] @indefine = false @expected = [] @commentstack = [ ['', @line] ] @lexing_context = { :after => nil, :start_of_line => true, :string_interpolation_depth => 0 } end # Make any necessary changes to the token and/or value. def munge_token(token, value) @line += 1 if token.incr_line skip if token.skip_text return if token.skip and not token.accumulate? token, value = token.convert(self, value) if token.respond_to?(:convert) return unless token if token.accumulate? comment = @commentstack.pop comment[0] << value + "\n" @commentstack.push(comment) end return if token.skip return token, { :value => value, :line => @line } end # Go up one in the namespace. def namepop @namestack.pop end # Collect the current namespace. def namespace @namestack.join("::") end # This value might have :: in it, but we don't care -- it'll be # handled normally when joining, and when popping we want to pop # this full value, however long the namespace is. def namestack(value) @namestack << value end def rest @scanner.rest end # this is the heart of the lexer def scan #Puppet.debug("entering scan") lex_error "Invalid or empty string" unless @scanner # Skip any initial whitespace. skip until token_queue.empty? and @scanner.eos? do yielded = false matched_token, value = find_token # error out if we didn't match anything at all lex_error "Could not match #{@scanner.rest[/^(\S+|\s+|.*)/]}" unless matched_token newline = matched_token.name == :RETURN # this matches a blank line; eat the previously accumulated comments getcomment if lexing_context[:start_of_line] and newline lexing_context[:start_of_line] = newline final_token, token_value = munge_token(matched_token, value) unless final_token skip next end lexing_context[:after] = final_token.name unless newline lexing_context[:string_interpolation_depth] += 1 if final_token.name == :DQPRE lexing_context[:string_interpolation_depth] -= 1 if final_token.name == :DQPOST value = token_value[:value] if match = @@pairs[value] and final_token.name != :DQUOTE and final_token.name != :SQUOTE @expected << match elsif exp = @expected[-1] and exp == value and final_token.name != :DQUOTE and final_token.name != :SQUOTE @expected.pop end - if final_token.name == :LBRACE + if final_token.name == :LBRACE or final_token.name == :LPAREN commentpush end + if final_token.name == :RPAREN + commentpop + end yield [final_token.name, token_value] if @previous_token namestack(value) if @previous_token.name == :CLASS and value != '{' if @previous_token.name == :DEFINE if indefine? msg = "Cannot nest definition #{value} inside #{@indefine}" self.indefine = false raise Puppet::ParseError, msg end @indefine = value end end @previous_token = final_token skip end @scanner = nil # This indicates that we're done parsing. yield [false,false] end # Skip any skipchars in our remaining string. def skip @scanner.skip(@skip) end # Provide some limited access to the scanner, for those # tokens that need it. def scan_until(regex) @scanner.scan_until(regex) end # we've encountered the start of a string... # slurp in the rest of the string and return it def slurpstring(terminators,escapes=%w{ \\ $ ' " n t s }+["\n"],ignore_invalid_escapes=false) # we search for the next quote that isn't preceded by a # backslash; the caret is there to match empty strings str = @scanner.scan_until(/([^\\]|^|[^\\])([\\]{2})*[#{terminators}]/) or lex_error "Unclosed quote after '#{last}' in '#{rest}'" @line += str.count("\n") # literal carriage returns add to the line count. str.gsub!(/\\(.)/m) { ch = $1 if escapes.include? ch case ch when 'n'; "\n" when 't'; "\t" when 's'; " " when "\n": '' else ch end else Puppet.warning "Unrecognised escape sequence '\\#{ch}'#{file && " in file #{file}"}#{line && " at line #{line}"}" unless ignore_invalid_escapes "\\#{ch}" end } [ str[0..-2],str[-1,1] ] end def tokenize_interpolated_string(token_type,preamble='') value,terminator = slurpstring('"$') token_queue << [TOKENS[token_type[terminator]],preamble+value] if terminator != '$' or @scanner.scan(/\{/) token_queue.shift elsif var_name = @scanner.scan(%r{(\w*::)*\w+|[0-9]}) token_queue << [TOKENS[:VARIABLE],var_name] tokenize_interpolated_string(DQ_continuation_token_types) else tokenize_interpolated_string(token_type,token_queue.pop.last + terminator) end end # just parse a string, not a whole file def string=(string) @scanner = StringScanner.new(string) end # returns the content of the currently accumulated content cache def commentpop @commentstack.pop[0] end def getcomment(line = nil) comment = @commentstack.last if line.nil? or comment[1] <= line @commentstack.pop @commentstack.push(['', @line]) return comment[0] end '' end def commentpush @commentstack.push(['', @line]) end end diff --git a/lib/puppet/parser/parser.rb b/lib/puppet/parser/parser.rb index 5be9e5a3f..c2fbf976d 100644 --- a/lib/puppet/parser/parser.rb +++ b/lib/puppet/parser/parser.rb @@ -1,2602 +1,2688 @@ # # DO NOT MODIFY!!!! -# This file is automatically generated by racc 1.4.5 -# from racc grammer file "grammar.ra". +# This file is automatically generated by Racc 1.4.6 +# from Racc grammer file "". # -require 'racc/parser' - +require 'racc/parser.rb' require 'puppet' require 'puppet/util/loadedfile' require 'puppet/parser/lexer' require 'puppet/parser/ast' module Puppet - class ParseError < Puppet::Error; end - class ImportError < Racc::ParseError; end - class AlreadyImportedError < ImportError; end + class ParseError < Puppet::Error; end + class ImportError < Racc::ParseError; end + class AlreadyImportedError < ImportError; end end - module Puppet - module Parser - class Parser < Racc::Parser -module_eval <<'..end grammar.ra modeval..id7145220b1b', 'grammar.ra', 876 +module_eval(<<'...end grammar.ra/module_eval...', 'grammar.ra', 866) # It got too annoying having code in a file that needs to be compiled. require 'puppet/parser/parser_support' # Make emacs happy # Local Variables: # mode: ruby # End: # $Id$ - -..end grammar.ra modeval..id7145220b1b - -##### racc 1.4.5 generates ### - -racc_reduce_table = [ - 0, 0, :racc_error, - 1, 70, :_reduce_1, - 1, 70, :_reduce_none, - 1, 71, :_reduce_none, - 2, 71, :_reduce_4, - 1, 73, :_reduce_none, - 1, 73, :_reduce_none, - 1, 73, :_reduce_none, - 1, 73, :_reduce_none, - 1, 73, :_reduce_none, - 1, 73, :_reduce_none, - 1, 73, :_reduce_none, - 1, 73, :_reduce_none, - 1, 73, :_reduce_none, - 1, 73, :_reduce_none, - 1, 73, :_reduce_none, - 1, 73, :_reduce_none, - 1, 73, :_reduce_none, - 1, 73, :_reduce_none, - 3, 87, :_reduce_19, - 3, 87, :_reduce_20, - 1, 88, :_reduce_none, - 1, 88, :_reduce_none, - 1, 88, :_reduce_none, - 1, 89, :_reduce_none, - 1, 89, :_reduce_none, - 1, 89, :_reduce_none, - 1, 89, :_reduce_none, - 4, 81, :_reduce_28, - 5, 81, :_reduce_29, - 3, 81, :_reduce_30, - 2, 81, :_reduce_31, - 1, 91, :_reduce_none, - 1, 91, :_reduce_none, - 3, 91, :_reduce_34, - 3, 91, :_reduce_35, - 1, 92, :_reduce_none, - 1, 92, :_reduce_none, - 1, 92, :_reduce_none, - 1, 92, :_reduce_none, - 1, 92, :_reduce_none, - 1, 92, :_reduce_none, - 1, 92, :_reduce_none, - 1, 92, :_reduce_none, - 1, 92, :_reduce_44, - 5, 74, :_reduce_45, - 5, 74, :_reduce_46, - 5, 74, :_reduce_47, - 5, 85, :_reduce_48, - 2, 75, :_reduce_49, - 1, 108, :_reduce_50, - 2, 108, :_reduce_51, - 6, 76, :_reduce_52, - 2, 76, :_reduce_53, - 3, 109, :_reduce_54, - 3, 109, :_reduce_55, - 1, 110, :_reduce_none, - 1, 110, :_reduce_none, - 3, 110, :_reduce_58, - 1, 111, :_reduce_none, - 3, 111, :_reduce_60, - 1, 112, :_reduce_61, - 1, 112, :_reduce_62, - 3, 113, :_reduce_63, - 3, 113, :_reduce_64, - 1, 114, :_reduce_none, - 1, 114, :_reduce_none, - 4, 116, :_reduce_67, - 1, 102, :_reduce_none, - 3, 102, :_reduce_69, - 0, 103, :_reduce_none, - 1, 103, :_reduce_none, - 1, 118, :_reduce_72, - 1, 93, :_reduce_73, - 1, 95, :_reduce_74, - 1, 117, :_reduce_none, - 1, 117, :_reduce_none, - 1, 117, :_reduce_none, - 1, 117, :_reduce_none, - 1, 117, :_reduce_none, - 1, 117, :_reduce_none, - 1, 117, :_reduce_none, - 3, 77, :_reduce_82, - 3, 77, :_reduce_83, - 3, 86, :_reduce_84, - 0, 104, :_reduce_85, - 1, 104, :_reduce_86, - 3, 104, :_reduce_87, - 3, 122, :_reduce_88, - 3, 124, :_reduce_89, - 1, 125, :_reduce_none, - 1, 125, :_reduce_none, - 0, 107, :_reduce_92, - 1, 107, :_reduce_93, - 3, 107, :_reduce_94, - 1, 126, :_reduce_none, - 3, 126, :_reduce_96, - 1, 115, :_reduce_none, - 1, 115, :_reduce_none, - 1, 115, :_reduce_none, - 1, 115, :_reduce_none, - 1, 115, :_reduce_none, - 1, 115, :_reduce_none, - 1, 123, :_reduce_none, - 1, 123, :_reduce_none, - 1, 123, :_reduce_none, - 1, 123, :_reduce_none, - 1, 123, :_reduce_none, - 1, 123, :_reduce_none, - 1, 123, :_reduce_none, - 1, 123, :_reduce_none, - 1, 123, :_reduce_none, - 1, 123, :_reduce_none, - 1, 123, :_reduce_none, - 1, 123, :_reduce_none, - 4, 97, :_reduce_115, - 3, 97, :_reduce_116, - 1, 99, :_reduce_117, - 2, 99, :_reduce_118, - 2, 129, :_reduce_119, - 1, 130, :_reduce_120, - 2, 130, :_reduce_121, - 1, 96, :_reduce_122, - 4, 90, :_reduce_123, - 4, 90, :_reduce_124, - 2, 79, :_reduce_125, - 5, 131, :_reduce_126, - 4, 131, :_reduce_127, - 0, 132, :_reduce_none, - 2, 132, :_reduce_129, - 4, 132, :_reduce_130, - 3, 132, :_reduce_131, - 1, 120, :_reduce_none, - 3, 120, :_reduce_133, - 3, 120, :_reduce_134, - 3, 120, :_reduce_135, - 3, 120, :_reduce_136, - 3, 120, :_reduce_137, - 3, 120, :_reduce_138, - 3, 120, :_reduce_139, - 3, 120, :_reduce_140, - 3, 120, :_reduce_141, - 2, 120, :_reduce_142, - 3, 120, :_reduce_143, - 3, 120, :_reduce_144, - 3, 120, :_reduce_145, - 3, 120, :_reduce_146, - 3, 120, :_reduce_147, - 3, 120, :_reduce_148, - 2, 120, :_reduce_149, - 3, 120, :_reduce_150, - 3, 120, :_reduce_151, - 3, 120, :_reduce_152, - 5, 78, :_reduce_153, - 1, 134, :_reduce_none, - 2, 134, :_reduce_155, - 5, 135, :_reduce_156, - 4, 135, :_reduce_157, - 1, 136, :_reduce_none, - 3, 136, :_reduce_159, - 3, 98, :_reduce_160, - 1, 138, :_reduce_none, - 4, 138, :_reduce_162, - 1, 140, :_reduce_none, - 3, 140, :_reduce_164, - 3, 139, :_reduce_165, - 1, 137, :_reduce_none, - 1, 137, :_reduce_none, - 1, 137, :_reduce_none, - 1, 137, :_reduce_none, - 1, 137, :_reduce_none, - 1, 137, :_reduce_none, - 1, 137, :_reduce_none, - 1, 137, :_reduce_173, - 1, 137, :_reduce_none, - 1, 141, :_reduce_175, - 1, 142, :_reduce_none, - 3, 142, :_reduce_177, - 2, 80, :_reduce_178, - 6, 82, :_reduce_179, - 5, 82, :_reduce_180, - 7, 83, :_reduce_181, - 6, 83, :_reduce_182, - 6, 84, :_reduce_183, - 5, 84, :_reduce_184, - 1, 106, :_reduce_185, - 1, 101, :_reduce_186, - 1, 101, :_reduce_187, - 1, 101, :_reduce_188, - 1, 145, :_reduce_none, - 3, 145, :_reduce_190, - 1, 147, :_reduce_191, - 1, 148, :_reduce_192, - 1, 148, :_reduce_193, - 1, 148, :_reduce_194, - 1, 148, :_reduce_none, - 0, 72, :_reduce_196, - 0, 149, :_reduce_197, - 1, 143, :_reduce_none, - 3, 143, :_reduce_199, - 3, 143, :_reduce_200, - 1, 150, :_reduce_none, - 3, 150, :_reduce_202, - 3, 151, :_reduce_203, - 1, 151, :_reduce_204, - 3, 151, :_reduce_205, - 1, 151, :_reduce_206, - 1, 146, :_reduce_none, - 2, 146, :_reduce_208, - 1, 144, :_reduce_none, - 2, 144, :_reduce_210, - 1, 152, :_reduce_none, - 1, 152, :_reduce_none, - 1, 94, :_reduce_213, - 3, 119, :_reduce_214, - 4, 119, :_reduce_215, - 2, 119, :_reduce_216, - 1, 127, :_reduce_none, - 1, 127, :_reduce_none, - 0, 105, :_reduce_none, - 1, 105, :_reduce_220, - 1, 133, :_reduce_221, - 3, 128, :_reduce_222, - 4, 128, :_reduce_223, - 2, 128, :_reduce_224, - 1, 153, :_reduce_none, - 3, 153, :_reduce_226, - 3, 154, :_reduce_227, - 1, 155, :_reduce_228, - 1, 155, :_reduce_229, - 4, 121, :_reduce_230, - 1, 100, :_reduce_none, - 4, 100, :_reduce_232 ] - -racc_reduce_n = 233 - -racc_shift_n = 384 +...end grammar.ra/module_eval... +##### State transition tables begin ### racc_action_table = [ - 256, 257, 228, 82, 54, 72, 75, 181, 251, 48, - 72, 75, 194, 205, 210, 163, 156, 348, 46, 47, - 344, 184, 201, 203, 206, 209, 162, 352, 54, 182, - 351, 169, 54, -168, 72, 75, 241, 242, 102, 305, - 106, 158, 58, 193, 230, 60, 204, 208, 193, 306, + 256, 257, 228, 63, 327, 64, 156, 54, 82, 356, + -166, 245, 176, 205, 210, 254, 37, 357, 65, 244, + 38, -168, 201, 203, 206, 209, 184, 11, 255, 241, + 242, 158, 54, 251, 72, 75, 72, 75, 102, 117, + 106, -170, 62, 194, 230, 58, 204, 208, 60, 306, 213, 196, 197, 198, 200, 202, 97, 207, 211, 72, - 75, 72, 75, 163, 199, 59, 58, 71, 245, 60, - 58, 83, 86, 60, 162, 92, 244, 72, 75, 169, - 78, 100, 352, 269, 89, 351, 63, 94, 64, 59, - 228, 326, 71, 59, 162, 59, 83, 86, 83, 268, - 92, 65, 92, 184, 76, 78, 307, 137, 163, 89, - 162, 89, 72, 75, 83, 268, 241, 242, 92, 162, - 59, 163, 59, 137, 169, 62, 254, 89, 207, 211, - 72, 75, 162, 308, 102, 199, 106, 169, 59, 255, - 213, 196, 197, 198, -166, 162, 309, 207, 211, 83, - 268, 310, 97, 92, 199, 72, 75, 355, 137, 102, - -170, 106, 89, 71, 218, 356, 173, 83, 86, 220, - 313, 92, -171, 59, 72, 75, 78, 100, 37, 218, - 89, 249, 38, 94, 220, 246, 247, 173, 71, 11, - 210, 59, 83, 86, 246, 367, 92, 271, 201, 37, - -167, 78, 37, 38, 270, 89, 38, 71, 246, 247, - 11, 83, 86, 11, 14, 92, 59, 72, 75, 76, - 78, 102, 278, 106, 89, 277, 213, 196, 197, 198, - 200, 202, 275, 207, 211, 59, 246, 274, 152, 97, - 199, 37, 318, 72, 75, 127, 319, 102, -169, 106, - 71, 63, 11, 14, 83, 86, -167, 37, 92, 207, - 211, 127, -169, 78, 100, 97, 199, 89, 11, 14, - 94, -166, 117, 72, 75, -185, 71, 82, 59, 336, - 83, 86, 197, 198, 92, 231, 338, 207, 211, 78, - 100, 181, 48, 89, 199, 74, 94, 240, -168, 72, - 75, 241, 242, 102, 59, 106, 71, 184, 176, 37, - 83, 86, 59, 38, 92, 345, 322, 175, 76, 78, - 11, 97, -172, 89, -171, 72, 75, -170, 59, 102, - 214, 106, 71, 64, 59, 215, 83, 86, 173, 217, - 92, -23, -23, -23, -23, 78, 100, 97, 155, 89, - 72, 75, 94, 122, 102, 152, 106, 82, 71, 223, - 59, 122, 83, 86, 72, 75, 92, -168, 102, 225, - 106, 78, 100, -166, 276, 89, 226, 117, 94, 44, - 45, 41, 42, 71, -169, -167, 59, 83, 86, 72, - 75, 92, 226, 102, 229, 106, 78, 71, 52, -168, - 89, 83, 86, 72, 75, 92, -166, 102, -169, 106, - 78, 59, 197, 198, 89, -167, -171, 207, 211, 365, - 231, 152, 71, 234, 199, 59, 83, 86, 50, 210, - 92, -21, -21, -21, -21, 78, 71, 201, 372, 89, - 83, 86, 49, 374, 92, 72, 75, 228, -220, 78, - 59, 226, 354, 89, 377, 72, 75, 40, 39, 102, - 237, 106, 341, nil, 59, 213, 196, 197, 198, 200, - 202, nil, 207, 211, nil, nil, nil, 97, 162, 199, - nil, nil, 83, 268, nil, nil, 92, nil, 71, nil, - nil, 137, 83, 86, nil, 89, 92, 44, 45, 41, - 42, 78, 100, 72, 75, 89, 59, 102, 94, 106, - 213, 196, 197, 198, 200, 202, 59, 207, 211, nil, - 213, 196, 197, 198, 199, 97, nil, 207, 211, 72, - 75, nil, nil, 102, 199, 106, 71, nil, nil, nil, - 83, 86, nil, nil, 92, nil, nil, nil, nil, 78, - 100, 97, nil, 89, nil, nil, 94, nil, nil, 72, - 75, nil, 71, 102, 59, 106, 83, 86, nil, nil, - 92, nil, nil, nil, nil, 78, 100, nil, nil, 89, - nil, 97, 94, nil, nil, 72, 75, nil, nil, 102, - 59, 106, 71, nil, nil, nil, 83, 86, nil, nil, - 92, nil, nil, nil, nil, 78, 100, 97, nil, 89, - 72, 75, 94, nil, 102, nil, 106, nil, 71, nil, - 59, nil, 83, 86, 72, 75, 92, nil, 102, nil, - nil, 78, 100, nil, nil, 89, nil, nil, 94, nil, - nil, nil, nil, 71, nil, nil, 59, 83, 86, 72, - 75, 92, nil, 102, nil, 106, 78, 71, nil, nil, - 89, 83, 143, nil, nil, 92, nil, nil, nil, nil, - 137, 59, nil, nil, 89, 72, 75, nil, nil, 102, - nil, 106, 71, nil, nil, 59, 83, 86, nil, nil, - 92, nil, nil, nil, nil, 78, nil, 97, nil, 89, - nil, 72, 75, nil, nil, 102, nil, 106, 71, nil, - 59, nil, 83, 86, nil, nil, 92, nil, nil, nil, + 75, 241, 242, 102, 199, 106, 163, 71, 59, 307, + 58, 83, 86, 60, 193, 92, 54, 162, 72, 75, + 78, 100, 169, 163, 89, 72, 75, 94, 308, 102, + 163, 106, 71, 59, 162, 59, 83, 86, 59, 169, + 92, 162, 311, 72, 75, 78, 169, 97, 181, 89, + 353, 71, 228, 352, 58, 83, 269, 60, 71, 92, + 59, 345, 83, 86, 137, 184, 92, -171, 89, 72, + 75, 78, 100, 246, 368, 89, 71, 59, 94, 59, + 83, 86, 309, 173, 92, 314, 59, 163, 76, 78, + 72, 75, -167, 89, 102, 310, 106, 37, 162, 173, + 218, 127, 71, 169, 59, 220, 83, 269, 11, 14, + 92, 63, 97, 152, 37, 137, 72, 75, 127, 89, + 102, 319, 106, 71, 218, 11, 14, 83, 86, 220, + 59, 92, 72, 75, 72, 75, 78, 100, 270, 279, + 89, 349, 278, 94, 353, 207, 211, 352, 320, 71, + -169, 59, 199, 83, 86, 197, 198, 92, 72, 75, + 207, 211, 78, -169, 37, 71, 89, 199, 38, 83, + 269, -167, 193, 92, -166, 11, 14, 59, 137, 72, + 75, 272, 89, 102, 182, 106, 37, 207, 211, -186, + 38, 71, 181, 59, 199, 83, 86, 11, 337, 92, + 231, 97, 339, 76, 78, 72, 75, 37, 89, 82, + 48, 38, 71, 48, 323, 176, 83, 86, 11, 59, + 92, 342, 46, 47, 184, 78, 100, 74, -168, 89, + 72, 75, 94, -172, 102, 346, 106, -173, 71, 175, + 59, 59, 83, 86, 240, -171, 92, -170, 241, 242, + 76, 78, 97, 197, 198, 89, 72, 75, 207, 211, + 102, 214, 106, 71, 64, 199, 59, 83, 86, 276, + 215, 92, 217, 246, 275, 173, 78, 100, 97, 82, + 89, 72, 75, 94, 155, 102, 122, 106, 152, 71, + 223, 59, -168, 83, 86, 249, 277, 92, 176, 246, + 247, 122, 78, 100, 225, -166, 89, 72, 75, 94, + 117, 102, 226, 106, 71, -169, 271, 59, 83, 86, + 246, 247, 92, -21, -21, -21, -21, 78, 226, 97, + -167, 89, 72, 75, 52, -168, 102, -166, 106, -169, + 71, -167, 59, -171, 83, 86, 366, 152, 92, -23, + -23, -23, -23, 78, 100, 228, 226, 89, 72, 75, + 94, 50, 102, 373, 106, 71, 49, 375, 59, 83, + 86, 229, -221, 92, 237, 378, 72, 75, 78, 40, + 97, 39, 89, 355, 44, 45, 41, 42, 231, 234, + nil, 71, nil, 59, nil, 83, 86, nil, nil, 92, + 44, 45, 41, 42, 78, 100, 72, 75, 89, 71, + 102, 94, 106, 83, 269, nil, nil, 92, nil, 59, + nil, nil, 137, nil, nil, nil, 89, nil, 97, nil, + nil, nil, 72, 75, nil, nil, 102, 59, 106, 71, + nil, nil, nil, 83, 86, nil, nil, 92, nil, nil, + nil, nil, 78, 100, 97, nil, 89, nil, nil, 94, + nil, nil, 72, 75, nil, 71, 102, 59, 106, 83, + 86, nil, nil, 92, nil, nil, nil, nil, 78, 100, + nil, nil, 89, nil, 97, 94, nil, nil, 72, 75, + nil, nil, 102, 59, 106, 71, nil, nil, nil, 83, + 86, nil, nil, 92, nil, nil, 72, 75, 78, 100, + 97, nil, 89, 72, 75, 94, nil, 102, nil, 106, + nil, 71, nil, 59, nil, 83, 86, 72, 75, 92, + nil, 102, nil, nil, 78, 100, nil, nil, 89, 71, + nil, 94, nil, 83, 269, nil, 71, 92, nil, 59, + 83, 86, 137, nil, 92, nil, 89, nil, nil, 78, + 71, nil, nil, 89, 83, 143, nil, 59, 92, nil, + nil, nil, nil, 137, 59, 72, 75, 89, nil, 102, + nil, 106, 213, 196, 197, 198, 200, 202, 59, 207, + 211, nil, 213, 196, 197, 198, 199, 97, nil, 207, + 211, 72, 75, nil, nil, 102, 199, 106, 71, nil, + nil, nil, 83, 86, nil, nil, 92, nil, nil, nil, nil, 78, 100, 97, nil, 89, nil, nil, 94, nil, nil, 72, 75, nil, 71, 102, 59, 106, 83, 86, nil, nil, 92, nil, nil, nil, nil, 78, 100, nil, - nil, 89, nil, 97, 94, nil, nil, 72, 75, nil, - nil, 102, 59, 106, 71, nil, nil, nil, 83, 86, - nil, nil, 92, nil, nil, nil, nil, 78, 100, 97, - nil, 89, 72, 75, 94, nil, 102, nil, 106, nil, - 71, nil, 59, nil, 83, 86, nil, nil, 92, nil, - nil, nil, nil, 78, 100, nil, nil, 89, 72, 75, - 94, nil, 102, nil, 106, 71, nil, nil, 59, 83, - 86, nil, nil, 92, nil, nil, nil, nil, 78, nil, - 97, nil, 89, nil, 72, 75, nil, nil, 102, nil, - 106, 71, nil, 59, nil, 83, 86, nil, nil, 92, - nil, nil, 72, 75, 78, 100, 97, nil, 89, 72, - 75, 94, nil, 102, nil, 106, nil, 71, nil, 59, - nil, 83, 86, nil, nil, 92, nil, nil, nil, nil, - 78, 100, nil, nil, 89, 162, nil, 94, nil, 83, - 268, nil, 71, 92, nil, 59, 83, 86, 137, nil, - 92, nil, 89, nil, nil, 78, 72, 75, nil, 89, - 102, nil, 106, 59, nil, nil, nil, nil, nil, nil, - 59, nil, nil, nil, nil, 72, 75, nil, 97, nil, - nil, nil, nil, nil, nil, nil, nil, nil, nil, 71, - nil, nil, nil, 83, 86, nil, nil, 92, 177, nil, - 72, 75, 78, 100, nil, nil, 89, nil, 71, 94, - nil, nil, 83, 86, nil, nil, 92, 59, 72, 75, - 76, 78, 102, 339, 106, 89, nil, nil, nil, nil, - nil, nil, nil, 71, nil, nil, 59, 83, 86, nil, - 97, 92, nil, 72, 75, 76, 78, 102, nil, 106, - 89, 71, nil, 72, 75, 83, 86, nil, nil, 92, - nil, 59, nil, nil, 78, 100, nil, nil, 89, 72, - 75, 94, nil, nil, nil, nil, 71, nil, nil, 59, - 83, 86, nil, nil, 92, nil, 162, nil, nil, 78, - 83, 268, nil, 89, 92, nil, 72, 75, nil, 137, - 102, nil, 162, 89, 59, nil, 83, 268, nil, nil, - 92, nil, 72, 75, 59, 137, 102, nil, 106, 89, - nil, nil, 72, 75, nil, nil, 102, nil, 106, 71, - 59, nil, nil, 83, 268, nil, nil, 92, nil, nil, - nil, nil, 137, nil, 97, 71, 89, nil, nil, 83, - 86, nil, nil, 92, nil, 71, nil, 59, 78, 83, - 86, nil, 89, 92, nil, nil, nil, nil, 78, 100, - 72, 75, 89, 59, 102, 94, 106, 213, 196, 197, - 198, 200, 202, 59, 207, 211, nil, nil, nil, 72, - 75, 199, 97, 102, 189, 106, 72, 75, nil, nil, + nil, 89, nil, 97, 94, nil, nil, nil, nil, nil, + nil, nil, 59, nil, 71, nil, nil, nil, 83, 86, + 72, 75, 92, nil, 102, 189, 106, 78, 100, nil, + nil, 89, nil, nil, 94, nil, nil, nil, nil, 72, + 75, nil, 59, 102, nil, 106, 72, 75, nil, nil, 102, nil, 106, 71, nil, nil, nil, 83, 86, nil, - nil, 92, nil, nil, nil, nil, 78, 100, 72, 75, - 89, nil, 71, 94, nil, nil, 83, 86, nil, 71, + nil, 92, nil, nil, nil, nil, 78, nil, 97, nil, + 89, nil, 71, nil, nil, nil, 83, 86, nil, 71, 92, 59, nil, 83, 86, 78, nil, 92, nil, 89, - nil, nil, 78, 72, 75, nil, 89, 102, nil, 106, - 59, 162, nil, nil, nil, 83, 268, 59, nil, 92, - nil, 72, 75, nil, 137, 102, nil, 106, 89, nil, - nil, nil, nil, nil, nil, nil, 71, nil, nil, 59, - 83, 86, nil, 97, 92, nil, nil, nil, nil, 78, - nil, 72, 75, 89, 71, 102, nil, 106, 83, 86, - nil, nil, 92, nil, 59, nil, nil, 78, 100, nil, - nil, 89, nil, 97, 94, nil, nil, 72, 75, nil, - nil, 102, 59, 106, 71, nil, nil, nil, 83, 86, - nil, nil, 92, nil, nil, nil, nil, 78, 100, 97, - nil, 89, nil, nil, 94, nil, nil, nil, nil, nil, - 71, nil, 59, nil, 83, 86, 212, nil, 92, nil, - nil, nil, nil, 78, 100, 205, 210, 89, nil, nil, - 94, nil, nil, nil, 201, 203, 206, 209, 59, nil, - 205, 210, nil, nil, nil, nil, nil, nil, nil, 201, - 203, 206, 209, nil, nil, nil, nil, nil, 204, 208, - nil, nil, 213, 196, 197, 198, 200, 202, nil, 207, - 211, nil, nil, 204, 208, nil, 199, 213, 196, 197, - 198, 200, 202, nil, 207, 211, 205, 210, nil, nil, - nil, 199, nil, nil, nil, 201, 203, 206, 209, nil, - nil, 205, 210, nil, nil, nil, nil, nil, nil, nil, - 201, 203, 206, 209, nil, nil, nil, nil, nil, 204, - 208, nil, nil, 213, 196, 197, 198, 200, 202, nil, - 207, 211, nil, nil, 204, 208, nil, 199, 213, 196, - 197, 198, 200, 202, nil, 207, 211, 205, 210, nil, - nil, nil, 199, nil, nil, nil, 201, 203, 206, 209, - nil, nil, 205, 210, nil, nil, nil, nil, nil, nil, - 273, 201, 203, 206, 209, nil, nil, nil, nil, nil, - nil, 208, nil, nil, 213, 196, 197, 198, 200, 202, - nil, 207, 211, nil, nil, 204, 208, nil, 199, 213, - 196, 197, 198, 200, 202, nil, 207, 211, 205, 210, - nil, nil, nil, 199, nil, nil, nil, 201, 203, 206, - 209, nil, nil, 26, 210, 33, 1, nil, 7, 12, - nil, 17, 201, 23, nil, 29, nil, 3, nil, nil, - 11, 14, nil, 210, nil, 213, 196, 197, 198, 200, - 202, 201, 207, 211, nil, nil, nil, nil, nil, 199, - 213, 196, 197, 198, 200, 202, nil, 207, 211, nil, - nil, 324, nil, nil, 199, nil, nil, nil, nil, 213, - 196, 197, 198, 200, 202, nil, 207, 211, nil, nil, - 379, nil, 26, 199, 33, 1, nil, 7, 12, nil, + nil, nil, 78, 100, nil, nil, 89, 72, 75, 94, + 59, 102, nil, 106, nil, nil, nil, 59, nil, nil, + nil, nil, nil, nil, nil, nil, 72, 75, nil, 97, + 102, nil, 106, 72, 75, nil, nil, 102, nil, 106, + 71, nil, nil, nil, 83, 86, nil, nil, 92, nil, + nil, nil, nil, 78, 100, 97, nil, 89, nil, 71, + 94, nil, nil, 83, 86, nil, 71, 92, 59, nil, + 83, 86, 78, nil, 92, nil, 89, nil, nil, 78, + 100, nil, nil, 89, 72, 75, 94, 59, 102, nil, + 106, nil, nil, nil, 59, nil, nil, nil, nil, nil, + nil, nil, nil, 72, 75, nil, 97, nil, nil, 72, + 75, nil, nil, nil, nil, nil, nil, 71, nil, nil, + nil, 83, 86, nil, nil, 92, 340, nil, nil, nil, + 78, 100, 177, nil, 89, nil, 71, 94, nil, nil, + 83, 86, 71, nil, 92, 59, 83, 86, 76, 78, + 92, nil, nil, 89, 76, 78, 72, 75, nil, 89, + 102, nil, 106, nil, 59, nil, 213, 196, 197, 198, + 59, nil, nil, 207, 211, 72, 75, nil, 97, 102, + 199, 106, 72, 75, nil, nil, nil, nil, nil, 71, + nil, nil, nil, 83, 86, nil, nil, 92, nil, nil, + 72, 75, 78, 100, nil, nil, 89, nil, 71, 94, + nil, nil, 83, 86, nil, 71, 92, 59, nil, 83, + 269, 78, nil, 92, nil, 89, nil, nil, 137, nil, + nil, nil, 89, 71, nil, nil, 59, 83, 269, nil, + nil, 92, nil, 59, nil, nil, 137, 72, 75, nil, + 89, 102, nil, 106, nil, nil, nil, nil, nil, nil, + nil, 59, nil, nil, nil, nil, 72, 75, nil, 97, + 102, nil, 106, 72, 75, nil, nil, 102, nil, 106, + 71, nil, nil, nil, 83, 86, nil, nil, 92, nil, + nil, nil, nil, 78, 100, 72, 75, 89, nil, 71, + 94, nil, nil, 83, 86, nil, 71, 92, 59, nil, + 83, 86, 78, nil, 92, nil, 89, nil, nil, 78, + 72, 75, nil, 89, 102, nil, 106, 59, 71, nil, + nil, nil, 83, 269, 59, nil, 92, nil, 72, 75, + nil, 137, 102, nil, 106, 89, nil, nil, nil, nil, + nil, nil, nil, 71, nil, nil, 59, 83, 86, nil, + 97, 92, nil, 72, 75, nil, 78, 102, nil, 106, + 89, 71, nil, 72, 75, 83, 86, 102, nil, 92, + nil, 59, nil, nil, 78, 100, nil, nil, 89, nil, + nil, 94, nil, nil, nil, nil, 71, nil, nil, 59, + 83, 86, nil, nil, 92, nil, 71, nil, nil, 78, + 83, 269, nil, 89, 92, nil, 72, 75, nil, 137, + 102, nil, 106, 89, 59, nil, nil, nil, nil, nil, + nil, nil, nil, nil, 59, 72, 75, nil, 97, 102, + nil, 106, 72, 75, nil, nil, 102, nil, 106, 71, + nil, nil, nil, 83, 86, nil, nil, 92, nil, nil, + nil, nil, 78, 100, 97, nil, 89, nil, 71, 94, + nil, nil, 83, 86, nil, 71, 92, 59, nil, 83, + 86, 78, nil, 92, nil, 89, nil, nil, 78, 100, + 212, nil, 89, nil, nil, 94, 59, nil, nil, 205, + 210, nil, nil, 59, nil, nil, nil, nil, 201, 203, + 206, 209, nil, nil, 205, 210, nil, nil, nil, nil, + nil, nil, 274, 201, 203, 206, 209, nil, nil, nil, + nil, nil, 204, 208, nil, nil, 213, 196, 197, 198, + 200, 202, nil, 207, 211, nil, nil, 204, 208, nil, + 199, 213, 196, 197, 198, 200, 202, nil, 207, 211, + 205, 210, nil, nil, nil, 199, nil, nil, nil, 201, + 203, 206, 209, nil, nil, 205, 210, nil, nil, nil, + nil, nil, nil, nil, 201, 203, 206, 209, nil, nil, + nil, nil, nil, nil, 208, nil, nil, 213, 196, 197, + 198, 200, 202, nil, 207, 211, nil, nil, 204, 208, + nil, 199, 213, 196, 197, 198, 200, 202, nil, 207, + 211, 205, 210, nil, nil, nil, 199, nil, nil, nil, + 201, 203, 206, 209, nil, nil, 205, 210, nil, nil, + nil, nil, nil, nil, nil, 201, 203, 206, 209, nil, + nil, nil, nil, nil, 204, 208, nil, nil, 213, 196, + 197, 198, 200, 202, nil, 207, 211, nil, nil, nil, + nil, nil, 199, 213, 196, 197, 198, 200, 202, nil, + 207, 211, 205, 210, nil, nil, nil, 199, nil, nil, + nil, 201, 203, 206, 209, nil, nil, nil, 210, nil, + 213, 196, 197, 198, 200, 202, 201, 207, 211, nil, + nil, nil, nil, nil, 199, 204, 208, 210, nil, 213, + 196, 197, 198, 200, 202, 201, 207, 211, nil, nil, + nil, nil, 210, 199, 213, 196, 197, 198, 200, 202, + 201, 207, 211, nil, nil, nil, nil, nil, 199, nil, + nil, 210, nil, 213, 196, 197, 198, 200, 202, 201, + 207, 211, nil, nil, nil, nil, nil, 199, 213, 196, + 197, 198, 200, 202, nil, 207, 211, nil, nil, 384, + nil, nil, 199, nil, nil, nil, nil, 213, 196, 197, + 198, 200, 202, nil, 207, 211, nil, nil, 297, nil, + 26, 199, 33, 1, nil, 7, 12, nil, 17, nil, + 23, nil, 29, nil, 3, nil, nil, 11, 14, 26, + 305, 33, 1, nil, 7, 12, nil, 17, nil, 23, + nil, 29, nil, 3, nil, nil, 11, 14, nil, 383, + nil, 26, nil, 33, 1, nil, 7, 12, nil, 17, + nil, 23, nil, 29, nil, 3, nil, nil, 11, 14, + 26, 325, 33, 1, nil, 7, 12, nil, 17, nil, + 23, nil, 29, nil, 3, nil, nil, 11, 14, nil, + 382, nil, 26, nil, 33, 1, nil, 7, 12, nil, 17, nil, 23, nil, 29, nil, 3, nil, nil, 11, - 14, 26, 382, 33, 1, nil, 7, 12, nil, 17, + 14, 26, 380, 33, 1, nil, 7, 12, nil, 17, nil, 23, nil, 29, nil, 3, nil, nil, 11, 14, - nil, 296, nil, 26, nil, 33, 1, nil, 7, 12, + nil, 376, nil, 26, nil, 33, 1, nil, 7, 12, nil, 17, nil, 23, nil, 29, nil, 3, nil, nil, - 11, 14, 26, 364, 33, 1, nil, 7, 12, nil, + 11, 14, 26, 350, 33, 1, nil, 7, 12, nil, 17, nil, 23, nil, 29, nil, 3, nil, nil, 11, - 14, nil, 381, nil, 26, nil, 33, 1, nil, 7, + 14, nil, 358, nil, 26, nil, 33, 1, nil, 7, 12, nil, 17, nil, 23, nil, 29, nil, 3, nil, - nil, 11, 14, 26, 383, 33, 1, nil, 7, 12, + nil, 11, 14, 26, 365, 33, 1, nil, 7, 12, nil, 17, nil, 23, nil, 29, nil, 3, nil, nil, - 11, 14, nil, 357, nil, 26, nil, 33, 1, nil, + 11, 14, nil, 364, nil, 26, nil, 33, 1, nil, 7, 12, nil, 17, nil, 23, nil, 29, nil, 3, - nil, nil, 11, 14, 26, 363, 33, 1, nil, 7, + nil, nil, 11, 14, 26, nil, 33, 1, nil, 7, 12, nil, 17, nil, 23, nil, 29, nil, 3, nil, - nil, 11, 14, nil, 375, nil, 26, nil, 33, 1, - nil, 7, 12, nil, 17, nil, 23, nil, 29, nil, - 3, nil, nil, 11, 14, 26, 304, 33, 1, nil, - 7, 12, nil, 17, nil, 23, nil, 29, nil, 3, - nil, nil, 11, 14, nil, 349, nil, 26, nil, 33, - 1, nil, 7, 12, nil, 17, nil, 23, nil, 29, - nil, 3, nil, nil, 11, 14, 26, nil, 33, 1, - nil, 7, 12, nil, 17, nil, 23, nil, 29, nil, - 3, nil, nil, 11, 14, 26, nil, 33, 1, nil, - 7, 12, nil, 17, nil, 23, nil, 29, nil, 3, - nil, nil, 11, 14 ] + nil, 11, 14, 26, nil, 33, 1, nil, 7, 12, + nil, 17, nil, 23, nil, 29, nil, 3, nil, nil, + 11, 14, 26, nil, 33, 1, nil, 7, 12, nil, + 17, nil, 23, nil, 29, nil, 3, nil, nil, 11, + 14 ] racc_action_check = [ - 180, 180, 152, 86, 156, 106, 106, 272, 174, 7, - 277, 277, 106, 180, 180, 65, 55, 277, 7, 7, - 272, 86, 180, 180, 180, 180, 65, 296, 17, 80, - 296, 65, 158, 95, 202, 202, 174, 174, 202, 218, - 202, 55, 156, 106, 152, 156, 180, 180, 277, 219, - 180, 180, 180, 180, 180, 180, 202, 180, 180, 181, - 181, 368, 368, 239, 180, 156, 17, 202, 165, 17, - 158, 202, 202, 158, 239, 202, 165, 182, 182, 239, - 202, 202, 349, 182, 202, 349, 22, 202, 22, 17, - 143, 243, 181, 158, 368, 202, 181, 181, 368, 368, - 181, 22, 368, 143, 181, 181, 220, 368, 163, 181, - 182, 368, 355, 355, 182, 182, 243, 243, 182, 163, - 181, 62, 368, 182, 163, 22, 178, 182, 281, 281, - 351, 351, 62, 221, 351, 281, 351, 62, 182, 178, - 285, 285, 285, 285, 101, 355, 221, 285, 285, 355, - 355, 224, 351, 355, 285, 341, 341, 300, 355, 341, - 91, 341, 355, 351, 308, 300, 226, 351, 351, 308, - 227, 351, 90, 355, 184, 184, 351, 351, 12, 122, - 351, 171, 12, 351, 122, 171, 171, 229, 341, 12, - 286, 351, 341, 341, 343, 343, 341, 184, 286, 1, - 87, 341, 30, 1, 183, 341, 30, 184, 183, 183, - 1, 184, 184, 30, 30, 184, 341, 196, 196, 184, - 184, 196, 195, 196, 184, 195, 286, 286, 286, 286, - 286, 286, 188, 286, 286, 184, 188, 188, 231, 196, - 286, 120, 232, 197, 197, 120, 233, 197, 103, 197, - 196, 85, 120, 120, 196, 196, 105, 43, 196, 280, - 280, 43, 84, 196, 196, 197, 280, 196, 43, 43, - 196, 81, 215, 23, 23, 78, 197, 23, 196, 250, - 197, 197, 279, 279, 197, 252, 253, 279, 279, 197, - 197, 77, 71, 197, 279, 23, 197, 160, 68, 26, - 26, 160, 160, 26, 197, 26, 23, 268, 67, 234, - 23, 23, 211, 234, 23, 274, 234, 66, 23, 23, - 234, 26, 107, 23, 108, 198, 198, 109, 207, 198, - 114, 198, 26, 115, 23, 119, 26, 26, 64, 121, - 26, 35, 35, 35, 35, 26, 26, 198, 52, 26, - 29, 29, 26, 51, 29, 50, 29, 127, 198, 132, - 26, 36, 198, 198, 307, 307, 198, 133, 307, 136, - 307, 198, 198, 138, 192, 198, 139, 33, 198, 34, - 34, 34, 34, 29, 140, 142, 198, 29, 29, 305, - 305, 29, 315, 305, 144, 305, 29, 307, 16, 327, - 29, 307, 307, 199, 199, 307, 328, 199, 330, 199, - 307, 29, 297, 297, 307, 331, 332, 297, 297, 337, - 153, 175, 305, 154, 297, 307, 305, 305, 9, 288, - 305, 28, 28, 28, 28, 305, 199, 288, 352, 305, - 199, 199, 8, 356, 199, 298, 298, 173, 367, 199, - 305, 172, 298, 199, 369, 200, 200, 3, 2, 200, - 157, 200, 263, nil, 199, 288, 288, 288, 288, 288, - 288, nil, 288, 288, nil, nil, nil, 200, 298, 288, - nil, nil, 298, 298, nil, nil, 298, nil, 200, nil, - nil, 298, 200, 200, nil, 298, 200, 4, 4, 4, - 4, 200, 200, 39, 39, 200, 298, 39, 200, 39, - 293, 293, 293, 293, 293, 293, 200, 293, 293, nil, - 283, 283, 283, 283, 293, 39, nil, 283, 283, 201, - 201, nil, nil, 201, 283, 201, 39, nil, nil, nil, - 39, 39, nil, nil, 39, nil, nil, nil, nil, 39, - 39, 201, nil, 39, nil, nil, 39, nil, nil, 46, - 46, nil, 201, 46, 39, 46, 201, 201, nil, nil, - 201, nil, nil, nil, nil, 201, 201, nil, nil, 201, - nil, 46, 201, nil, nil, 47, 47, nil, nil, 47, - 201, 47, 46, nil, nil, nil, 46, 46, nil, nil, - 46, nil, nil, nil, nil, 46, 46, 47, nil, 46, - 48, 48, 46, nil, 48, nil, 48, nil, 47, nil, - 46, nil, 47, 47, 49, 49, 47, nil, 49, nil, - nil, 47, 47, nil, nil, 47, nil, nil, 47, nil, - nil, nil, nil, 48, nil, nil, 47, 48, 48, 176, - 176, 48, nil, 176, nil, 176, 48, 49, nil, nil, - 48, 49, 49, nil, nil, 49, nil, nil, nil, nil, - 49, 48, nil, nil, 49, 203, 203, nil, nil, 203, - nil, 203, 176, nil, nil, 49, 176, 176, nil, nil, - 176, nil, nil, nil, nil, 176, nil, 203, nil, 176, - nil, 204, 204, nil, nil, 204, nil, 204, 203, nil, - 176, nil, 203, 203, nil, nil, 203, nil, nil, nil, - nil, 203, 203, 204, nil, 203, nil, nil, 203, nil, - nil, 205, 205, nil, 204, 205, 203, 205, 204, 204, - nil, nil, 204, nil, nil, nil, nil, 204, 204, nil, - nil, 204, nil, 205, 204, nil, nil, 100, 100, nil, - nil, 100, 204, 100, 205, nil, nil, nil, 205, 205, - nil, nil, 205, nil, nil, nil, nil, 205, 205, 100, - nil, 205, 63, 63, 205, nil, 63, nil, 63, nil, - 100, nil, 205, nil, 100, 100, nil, nil, 100, nil, - nil, nil, nil, 100, 100, nil, nil, 100, 208, 208, - 100, nil, 208, nil, 208, 63, nil, nil, 100, 63, - 63, nil, nil, 63, nil, nil, nil, nil, 63, nil, - 208, nil, 63, nil, 209, 209, nil, nil, 209, nil, - 209, 208, nil, 63, nil, 208, 208, nil, nil, 208, - nil, nil, 269, 269, 208, 208, 209, nil, 208, 276, - 276, 208, nil, 276, nil, 276, nil, 209, nil, 208, - nil, 209, 209, nil, nil, 209, nil, nil, nil, nil, - 209, 209, nil, nil, 209, 269, nil, 209, nil, 269, - 269, nil, 276, 269, nil, 209, 276, 276, 269, nil, - 276, nil, 269, nil, nil, 276, 256, 256, nil, 276, - 256, nil, 256, 269, nil, nil, nil, nil, nil, nil, - 276, nil, nil, nil, nil, 74, 74, nil, 256, nil, - nil, nil, nil, nil, nil, nil, nil, nil, nil, 256, - nil, nil, nil, 256, 256, nil, nil, 256, 74, nil, - 254, 254, 256, 256, nil, nil, 256, nil, 74, 256, - nil, nil, 74, 74, nil, nil, 74, 256, 75, 75, - 74, 74, 75, 254, 75, 74, nil, nil, nil, nil, - nil, nil, nil, 254, nil, nil, 74, 254, 254, nil, - 75, 254, nil, 248, 248, 254, 254, 248, nil, 248, - 254, 75, nil, 245, 245, 75, 75, nil, nil, 75, - nil, 254, nil, nil, 75, 75, nil, nil, 75, 244, - 244, 75, nil, nil, nil, nil, 248, nil, nil, 75, - 248, 248, nil, nil, 248, nil, 245, nil, nil, 248, - 245, 245, nil, 248, 245, nil, 225, 225, nil, 245, - 225, nil, 244, 245, 248, nil, 244, 244, nil, nil, - 244, nil, 82, 82, 245, 244, 82, nil, 82, 244, - nil, nil, 210, 210, nil, nil, 210, nil, 210, 225, - 244, nil, nil, 225, 225, nil, nil, 225, nil, nil, - nil, nil, 225, nil, 210, 82, 225, nil, nil, 82, - 82, nil, nil, 82, nil, 210, nil, 225, 82, 210, - 210, nil, 82, 210, nil, nil, nil, nil, 210, 210, - 213, 213, 210, 82, 213, 210, 213, 284, 284, 284, - 284, 284, 284, 210, 284, 284, nil, nil, nil, 102, - 102, 284, 213, 102, 102, 102, 230, 230, nil, nil, - 230, nil, 230, 213, nil, nil, nil, 213, 213, nil, - nil, 213, nil, nil, nil, nil, 213, 213, 214, 214, - 213, nil, 102, 213, nil, nil, 102, 102, nil, 230, - 102, 213, nil, 230, 230, 102, nil, 230, nil, 102, - nil, nil, 230, 228, 228, nil, 230, 228, nil, 228, - 102, 214, nil, nil, nil, 214, 214, 230, nil, 214, - nil, 94, 94, nil, 214, 94, nil, 94, 214, nil, - nil, nil, nil, nil, nil, nil, 228, nil, nil, 214, - 228, 228, nil, 94, 228, nil, nil, nil, nil, 228, - nil, 97, 97, 228, 94, 97, nil, 97, 94, 94, - nil, nil, 94, nil, 228, nil, nil, 94, 94, nil, - nil, 94, nil, 97, 94, nil, nil, 206, 206, nil, - nil, 206, 94, 206, 97, nil, nil, nil, 97, 97, - nil, nil, 97, nil, nil, nil, nil, 97, 97, 206, - nil, 97, nil, nil, 97, nil, nil, nil, nil, nil, - 206, nil, 97, nil, 206, 206, 111, nil, 206, nil, - nil, nil, nil, 206, 206, 111, 111, 206, nil, nil, - 206, nil, nil, nil, 111, 111, 111, 111, 206, nil, - 124, 124, nil, nil, nil, nil, nil, nil, nil, 124, - 124, 124, 124, nil, nil, nil, nil, nil, 111, 111, - nil, nil, 111, 111, 111, 111, 111, 111, nil, 111, - 111, nil, nil, 124, 124, nil, 111, 124, 124, 124, - 124, 124, 124, nil, 124, 124, 130, 130, nil, nil, - nil, 124, nil, nil, nil, 130, 130, 130, 130, nil, - nil, 131, 131, nil, nil, nil, nil, nil, nil, nil, - 131, 131, 131, 131, nil, nil, nil, nil, nil, 130, - 130, nil, nil, 130, 130, 130, 130, 130, 130, nil, - 130, 130, nil, nil, 131, 131, nil, 130, 131, 131, - 131, 131, 131, 131, nil, 131, 131, 287, 287, nil, - nil, nil, 131, nil, nil, nil, 287, 287, 287, 287, - nil, nil, 186, 186, nil, nil, nil, nil, nil, nil, - 186, 186, 186, 186, 186, nil, nil, nil, nil, nil, - nil, 287, nil, nil, 287, 287, 287, 287, 287, 287, - nil, 287, 287, nil, nil, 186, 186, nil, 287, 186, - 186, 186, 186, 186, 186, nil, 186, 186, 291, 291, - nil, nil, nil, 186, nil, nil, nil, 291, 291, 291, - 291, nil, nil, 19, 292, 19, 19, nil, 19, 19, - nil, 19, 292, 19, nil, 19, nil, 19, nil, nil, - 19, 19, nil, 289, nil, 291, 291, 291, 291, 291, - 291, 289, 291, 291, nil, nil, nil, nil, nil, 291, - 292, 292, 292, 292, 292, 292, nil, 292, 292, nil, - nil, 237, nil, nil, 292, nil, nil, nil, nil, 289, - 289, 289, 289, 289, 289, nil, 289, 289, nil, nil, - 372, nil, 237, 289, 237, 237, nil, 237, 237, nil, + 180, 180, 152, 22, 243, 22, 55, 17, 86, 301, + 81, 165, 96, 180, 180, 178, 12, 301, 22, 165, + 12, 95, 180, 180, 180, 180, 86, 12, 178, 243, + 243, 55, 156, 174, 208, 208, 106, 106, 208, 215, + 208, 91, 22, 106, 152, 17, 180, 180, 17, 218, + 180, 180, 180, 180, 180, 180, 208, 180, 180, 176, + 176, 174, 174, 176, 180, 176, 62, 208, 17, 219, + 156, 208, 208, 156, 106, 208, 158, 62, 369, 369, + 208, 208, 62, 239, 208, 205, 205, 208, 220, 205, + 65, 205, 176, 156, 239, 208, 176, 176, 211, 239, + 176, 65, 224, 181, 181, 176, 65, 205, 273, 176, + 350, 369, 143, 350, 158, 369, 369, 158, 205, 369, + 176, 273, 205, 205, 369, 143, 205, 90, 369, 356, + 356, 205, 205, 344, 344, 205, 181, 158, 205, 369, + 181, 181, 221, 226, 181, 227, 205, 163, 181, 181, + 352, 352, 87, 181, 352, 221, 352, 120, 163, 229, + 309, 120, 356, 163, 181, 309, 356, 356, 120, 120, + 356, 85, 352, 231, 43, 356, 342, 342, 43, 356, + 342, 232, 342, 352, 122, 43, 43, 352, 352, 122, + 356, 352, 182, 182, 278, 278, 352, 352, 182, 195, + 352, 278, 195, 352, 297, 281, 281, 297, 233, 342, + 103, 352, 281, 342, 342, 280, 280, 342, 184, 184, + 280, 280, 342, 84, 30, 182, 342, 280, 30, 182, + 182, 105, 278, 182, 101, 30, 30, 342, 182, 204, + 204, 184, 182, 204, 80, 204, 1, 282, 282, 78, + 1, 184, 77, 182, 282, 184, 184, 1, 250, 184, + 252, 204, 253, 184, 184, 23, 23, 234, 184, 23, + 71, 234, 204, 7, 234, 70, 204, 204, 234, 184, + 204, 264, 7, 7, 269, 204, 204, 23, 68, 204, + 26, 26, 204, 107, 26, 275, 26, 67, 23, 66, + 204, 207, 23, 23, 160, 108, 23, 109, 160, 160, + 23, 23, 26, 298, 298, 23, 196, 196, 298, 298, + 196, 114, 196, 26, 115, 298, 23, 26, 26, 188, + 119, 26, 121, 188, 188, 64, 26, 26, 196, 127, + 26, 29, 29, 26, 52, 29, 51, 29, 50, 196, + 132, 26, 133, 196, 196, 171, 192, 196, 135, 171, + 171, 36, 196, 196, 136, 138, 196, 213, 213, 196, + 33, 213, 139, 213, 29, 140, 183, 196, 29, 29, + 183, 183, 29, 28, 28, 28, 28, 29, 316, 213, + 142, 29, 306, 306, 16, 328, 306, 329, 306, 331, + 213, 332, 29, 333, 213, 213, 338, 175, 213, 35, + 35, 35, 35, 213, 213, 173, 172, 213, 197, 197, + 213, 9, 197, 353, 197, 306, 8, 357, 213, 306, + 306, 144, 368, 306, 157, 370, 299, 299, 306, 3, + 197, 2, 306, 299, 34, 34, 34, 34, 153, 154, + nil, 197, nil, 306, nil, 197, 197, nil, nil, 197, + 4, 4, 4, 4, 197, 197, 39, 39, 197, 299, + 39, 197, 39, 299, 299, nil, nil, 299, nil, 197, + nil, nil, 299, nil, nil, nil, 299, nil, 39, nil, + nil, nil, 206, 206, nil, nil, 206, 299, 206, 39, + nil, nil, nil, 39, 39, nil, nil, 39, nil, nil, + nil, nil, 39, 39, 206, nil, 39, nil, nil, 39, + nil, nil, 46, 46, nil, 206, 46, 39, 46, 206, + 206, nil, nil, 206, nil, nil, nil, nil, 206, 206, + nil, nil, 206, nil, 46, 206, nil, nil, 47, 47, + nil, nil, 47, 206, 47, 46, nil, nil, nil, 46, + 46, nil, nil, 46, nil, nil, 270, 270, 46, 46, + 47, nil, 46, 48, 48, 46, nil, 48, nil, 48, + nil, 47, nil, 46, nil, 47, 47, 49, 49, 47, + nil, 49, nil, nil, 47, 47, nil, nil, 47, 270, + nil, 47, nil, 270, 270, nil, 48, 270, nil, 47, + 48, 48, 270, nil, 48, nil, 270, nil, nil, 48, + 49, nil, nil, 48, 49, 49, nil, 270, 49, nil, + nil, nil, nil, 49, 48, 203, 203, 49, nil, 203, + nil, 203, 294, 294, 294, 294, 294, 294, 49, 294, + 294, nil, 284, 284, 284, 284, 294, 203, nil, 284, + 284, 209, 209, nil, nil, 209, 284, 209, 203, nil, + nil, nil, 203, 203, nil, nil, 203, nil, nil, nil, + nil, 203, 203, 209, nil, 203, nil, nil, 203, nil, + nil, 210, 210, nil, 209, 210, 203, 210, 209, 209, + nil, nil, 209, nil, nil, nil, nil, 209, 209, nil, + nil, 209, nil, 210, 209, nil, nil, nil, nil, nil, + nil, nil, 209, nil, 210, nil, nil, nil, 210, 210, + 102, 102, 210, nil, 102, 102, 102, 210, 210, nil, + nil, 210, nil, nil, 210, nil, nil, nil, nil, 63, + 63, nil, 210, 63, nil, 63, 202, 202, nil, nil, + 202, nil, 202, 102, nil, nil, nil, 102, 102, nil, + nil, 102, nil, nil, nil, nil, 102, nil, 202, nil, + 102, nil, 63, nil, nil, nil, 63, 63, nil, 202, + 63, 102, nil, 202, 202, 63, nil, 202, nil, 63, + nil, nil, 202, 202, nil, nil, 202, 100, 100, 202, + 63, 100, nil, 100, nil, nil, nil, 202, nil, nil, + nil, nil, nil, nil, nil, nil, 277, 277, nil, 100, + 277, nil, 277, 198, 198, nil, nil, 198, nil, 198, + 100, nil, nil, nil, 100, 100, nil, nil, 100, nil, + nil, nil, nil, 100, 100, 198, nil, 100, nil, 277, + 100, nil, nil, 277, 277, nil, 198, 277, 100, nil, + 198, 198, 277, nil, 198, nil, 277, nil, nil, 198, + 198, nil, nil, 198, 256, 256, 198, 277, 256, nil, + 256, nil, nil, nil, 198, nil, nil, nil, nil, nil, + nil, nil, nil, 254, 254, nil, 256, nil, nil, 74, + 74, nil, nil, nil, nil, nil, nil, 256, nil, nil, + nil, 256, 256, nil, nil, 256, 254, nil, nil, nil, + 256, 256, 74, nil, 256, nil, 254, 256, nil, nil, + 254, 254, 74, nil, 254, 256, 74, 74, 254, 254, + 74, nil, nil, 254, 74, 74, 75, 75, nil, 74, + 75, nil, 75, nil, 254, nil, 286, 286, 286, 286, + 74, nil, nil, 286, 286, 248, 248, nil, 75, 248, + 286, 248, 245, 245, nil, nil, nil, nil, nil, 75, + nil, nil, nil, 75, 75, nil, nil, 75, nil, nil, + 244, 244, 75, 75, nil, nil, 75, nil, 248, 75, + nil, nil, 248, 248, nil, 245, 248, 75, nil, 245, + 245, 248, nil, 245, nil, 248, nil, nil, 245, nil, + nil, nil, 245, 244, nil, nil, 248, 244, 244, nil, + nil, 244, nil, 245, nil, nil, 244, 97, 97, nil, + 244, 97, nil, 97, nil, nil, nil, nil, nil, nil, + nil, 244, nil, nil, nil, nil, 82, 82, nil, 97, + 82, nil, 82, 199, 199, nil, nil, 199, nil, 199, + 97, nil, nil, nil, 97, 97, nil, nil, 97, nil, + nil, nil, nil, 97, 97, 214, 214, 97, nil, 82, + 97, nil, nil, 82, 82, nil, 199, 82, 97, nil, + 199, 199, 82, nil, 199, nil, 82, nil, nil, 199, + 230, 230, nil, 199, 230, nil, 230, 82, 214, nil, + nil, nil, 214, 214, 199, nil, 214, nil, 200, 200, + nil, 214, 200, nil, 200, 214, nil, nil, nil, nil, + nil, nil, nil, 230, nil, nil, 214, 230, 230, nil, + 200, 230, nil, 228, 228, nil, 230, 228, nil, 228, + 230, 200, nil, 225, 225, 200, 200, 225, nil, 200, + nil, 230, nil, nil, 200, 200, nil, nil, 200, nil, + nil, 200, nil, nil, nil, nil, 228, nil, nil, 200, + 228, 228, nil, nil, 228, nil, 225, nil, nil, 228, + 225, 225, nil, 228, 225, nil, 201, 201, nil, 225, + 201, nil, 201, 225, 228, nil, nil, nil, nil, nil, + nil, nil, nil, nil, 225, 308, 308, nil, 201, 308, + nil, 308, 94, 94, nil, nil, 94, nil, 94, 201, + nil, nil, nil, 201, 201, nil, nil, 201, nil, nil, + nil, nil, 201, 201, 94, nil, 201, nil, 308, 201, + nil, nil, 308, 308, nil, 94, 308, 201, nil, 94, + 94, 308, nil, 94, nil, 308, nil, nil, 94, 94, + 111, nil, 94, nil, nil, 94, 308, nil, nil, 111, + 111, nil, nil, 94, nil, nil, nil, nil, 111, 111, + 111, 111, nil, nil, 186, 186, nil, nil, nil, nil, + nil, nil, 186, 186, 186, 186, 186, nil, nil, nil, + nil, nil, 111, 111, nil, nil, 111, 111, 111, 111, + 111, 111, nil, 111, 111, nil, nil, 186, 186, nil, + 111, 186, 186, 186, 186, 186, 186, nil, 186, 186, + 288, 288, nil, nil, nil, 186, nil, nil, nil, 288, + 288, 288, 288, nil, nil, 131, 131, nil, nil, nil, + nil, nil, nil, nil, 131, 131, 131, 131, nil, nil, + nil, nil, nil, nil, 288, nil, nil, 288, 288, 288, + 288, 288, 288, nil, 288, 288, nil, nil, 131, 131, + nil, 288, 131, 131, 131, 131, 131, 131, nil, 131, + 131, 130, 130, nil, nil, nil, 131, nil, nil, nil, + 130, 130, 130, 130, nil, nil, 292, 292, nil, nil, + nil, nil, nil, nil, nil, 292, 292, 292, 292, nil, + nil, nil, nil, nil, 130, 130, nil, nil, 130, 130, + 130, 130, 130, 130, nil, 130, 130, nil, nil, nil, + nil, nil, 130, 292, 292, 292, 292, 292, 292, nil, + 292, 292, 124, 124, nil, nil, nil, 292, nil, nil, + nil, 124, 124, 124, 124, nil, nil, nil, 293, nil, + 285, 285, 285, 285, 285, 285, 293, 285, 285, nil, + nil, nil, nil, nil, 285, 124, 124, 289, nil, 124, + 124, 124, 124, 124, 124, 289, 124, 124, nil, nil, + nil, nil, 287, 124, 293, 293, 293, 293, 293, 293, + 287, 293, 293, nil, nil, nil, nil, nil, 293, nil, + nil, 290, nil, 289, 289, 289, 289, 289, 289, 290, + 289, 289, nil, nil, nil, nil, nil, 289, 287, 287, + 287, 287, 287, 287, nil, 287, 287, nil, nil, 381, + nil, nil, 287, nil, nil, nil, nil, 290, 290, 290, + 290, 290, 290, nil, 290, 290, nil, nil, 212, nil, + 381, 290, 381, 381, nil, 381, 381, nil, 381, nil, + 381, nil, 381, nil, 381, nil, nil, 381, 381, 212, + 217, 212, 212, nil, 212, 212, nil, 212, nil, 212, + nil, 212, nil, 212, nil, nil, 212, 212, nil, 379, + nil, 217, nil, 217, 217, nil, 217, 217, nil, 217, + nil, 217, nil, 217, nil, 217, nil, nil, 217, 217, + 379, 237, 379, 379, nil, 379, 379, nil, 379, nil, + 379, nil, 379, nil, 379, nil, nil, 379, 379, nil, + 375, nil, 237, nil, 237, 237, nil, 237, 237, nil, 237, nil, 237, nil, 237, nil, 237, nil, nil, 237, - 237, 372, 378, 372, 372, nil, 372, 372, nil, 372, - nil, 372, nil, 372, nil, 372, nil, nil, 372, 372, - nil, 212, nil, 378, nil, 378, 378, nil, 378, 378, - nil, 378, nil, 378, nil, 378, nil, 378, nil, nil, - 378, 378, 212, 323, 212, 212, nil, 212, 212, nil, - 212, nil, 212, nil, 212, nil, 212, nil, nil, 212, - 212, nil, 374, nil, 323, nil, 323, 323, nil, 323, - 323, nil, 323, nil, 323, nil, 323, nil, 323, nil, - nil, 323, 323, 374, 380, 374, 374, nil, 374, 374, - nil, 374, nil, 374, nil, 374, nil, 374, nil, nil, - 374, 374, nil, 303, nil, 380, nil, 380, 380, nil, - 380, 380, nil, 380, nil, 380, nil, 380, nil, 380, - nil, nil, 380, 380, 303, 319, 303, 303, nil, 303, - 303, nil, 303, nil, 303, nil, 303, nil, 303, nil, - nil, 303, 303, nil, 362, nil, 319, nil, 319, 319, - nil, 319, 319, nil, 319, nil, 319, nil, 319, nil, - 319, nil, nil, 319, 319, 362, 217, 362, 362, nil, - 362, 362, nil, 362, nil, 362, nil, 362, nil, 362, - nil, nil, 362, 362, nil, 295, nil, 217, nil, 217, - 217, nil, 217, 217, nil, 217, nil, 217, nil, 217, - nil, 217, nil, nil, 217, 217, 295, nil, 295, 295, - nil, 295, 295, nil, 295, nil, 295, nil, 295, nil, - 295, nil, nil, 295, 295, 0, nil, 0, 0, nil, - 0, 0, nil, 0, nil, 0, nil, 0, nil, 0, - nil, nil, 0, 0 ] + 237, 375, 373, 375, 375, nil, 375, 375, nil, 375, + nil, 375, nil, 375, nil, 375, nil, nil, 375, 375, + nil, 363, nil, 373, nil, 373, 373, nil, 373, 373, + nil, 373, nil, 373, nil, 373, nil, 373, nil, nil, + 373, 373, 363, 296, 363, 363, nil, 363, 363, nil, + 363, nil, 363, nil, 363, nil, 363, nil, nil, 363, + 363, nil, 304, nil, 296, nil, 296, 296, nil, 296, + 296, nil, 296, nil, 296, nil, 296, nil, 296, nil, + nil, 296, 296, 304, 324, 304, 304, nil, 304, 304, + nil, 304, nil, 304, nil, 304, nil, 304, nil, nil, + 304, 304, nil, 320, nil, 324, nil, 324, 324, nil, + 324, 324, nil, 324, nil, 324, nil, 324, nil, 324, + nil, nil, 324, 324, 320, nil, 320, 320, nil, 320, + 320, nil, 320, nil, 320, nil, 320, nil, 320, nil, + nil, 320, 320, 19, nil, 19, 19, nil, 19, 19, + nil, 19, nil, 19, nil, 19, nil, 19, nil, nil, + 19, 19, 0, nil, 0, 0, nil, 0, 0, nil, + 0, nil, 0, nil, 0, nil, 0, nil, nil, 0, + 0 ] racc_action_pointer = [ - 1795, 163, 443, 413, 433, nil, nil, 3, 434, 420, - nil, nil, 142, nil, nil, nil, 398, 26, nil, 1483, - nil, nil, 80, 271, nil, nil, 297, nil, 367, 348, - 166, nil, nil, 375, 315, 277, 337, nil, nil, 501, - nil, nil, nil, 221, nil, nil, 557, 583, 608, 622, - 315, 329, 348, nil, nil, 4, nil, nil, nil, nil, - nil, nil, 97, 780, 298, -9, 309, 302, 275, nil, - nil, 286, nil, nil, 923, 966, nil, 279, 269, nil, - 6, 248, 1060, nil, 239, 245, -3, 177, nil, nil, - 149, 137, nil, nil, 1209, 10, nil, 1239, nil, nil, - 755, 121, 1137, 225, nil, 233, 3, 299, 301, 304, - nil, 1298, nil, nil, 322, 325, nil, nil, nil, 323, - 205, 331, 144, nil, 1313, nil, nil, 351, nil, nil, - 1359, 1374, 352, 344, nil, nil, 328, nil, 350, 364, - 361, nil, 362, 79, 374, nil, nil, nil, nil, nil, - nil, nil, -9, 408, 386, nil, 2, 452, 30, nil, - 251, nil, nil, 84, nil, 50, nil, nil, nil, nil, - nil, 174, 439, 436, -14, 381, 647, nil, 114, nil, - -4, 57, 75, 197, 172, nil, 1435, nil, 225, nil, - nil, nil, 363, nil, nil, 213, 215, 241, 323, 401, - 453, 527, 32, 673, 699, 729, 1265, 265, 806, 832, - 1070, 249, 1612, 1118, 1166, 270, nil, 1757, 24, 24, - 91, 121, nil, nil, 142, 1044, 126, 161, 1191, 147, - 1144, 198, 233, 238, 273, nil, nil, 1552, nil, 39, - nil, nil, nil, 66, 1017, 1001, nil, nil, 991, nil, - 270, nil, 273, 279, 948, nil, 904, nil, nil, nil, - nil, nil, nil, 451, nil, nil, nil, nil, 283, 850, - nil, nil, -5, nil, 308, nil, 857, 8, nil, 226, - 198, 67, nil, 466, 1073, 86, 172, 1420, 411, 1515, - nil, 1481, 1496, 456, nil, 1776, -4, 356, 443, nil, - 145, nil, nil, 1694, nil, 387, nil, 362, 129, nil, - nil, nil, nil, nil, nil, 380, nil, nil, nil, 1716, - nil, nil, nil, 1634, nil, nil, nil, 376, 383, nil, - 385, 392, 393, nil, nil, nil, nil, 410, nil, nil, - nil, 153, nil, 183, nil, nil, nil, nil, nil, 51, - nil, 128, 430, nil, nil, 110, 435, nil, nil, nil, - nil, nil, 1735, nil, nil, nil, nil, 439, 59, 445, - nil, nil, 1571, nil, 1653, nil, nil, nil, 1593, nil, - 1675, nil, nil, nil ] + 1832, 210, 426, 395, 396, nil, nil, 267, 418, 413, + nil, nil, -20, nil, nil, nil, 394, 5, nil, 1813, + nil, nil, -3, 263, nil, nil, 288, nil, 319, 339, + 188, nil, nil, 368, 380, 345, 337, nil, nil, 464, + nil, nil, nil, 138, nil, nil, 520, 546, 571, 585, + 308, 322, 344, nil, nil, -6, nil, nil, nil, nil, + nil, nil, 42, 747, 295, 66, 291, 274, 265, nil, + 269, 264, nil, nil, 907, 954, nil, 240, 243, nil, + 221, -13, 1064, nil, 200, 165, 2, 129, nil, nil, + 104, 18, nil, nil, 1240, -2, 6, 1045, nil, nil, + 805, 211, 728, 187, nil, 208, 34, 270, 282, 284, + nil, 1282, nil, nil, 313, 316, nil, nil, nil, 318, + 121, 324, 149, nil, 1465, nil, nil, 333, nil, nil, + 1404, 1358, 343, 329, nil, 352, 323, nil, 342, 360, + 352, nil, 367, 101, 411, nil, nil, nil, nil, nil, + nil, nil, -9, 436, 412, nil, 30, 426, 74, nil, + 258, nil, nil, 123, nil, -7, nil, nil, nil, nil, + nil, 348, 404, 404, 11, 367, 57, nil, 3, nil, + -4, 101, 190, 369, 216, nil, 1297, nil, 322, nil, + nil, nil, 345, nil, nil, 190, 314, 416, 831, 1071, + 1136, 1214, 754, 633, 237, 83, 490, 238, 32, 659, + 689, 35, 1589, 365, 1093, 37, nil, 1611, 34, 44, + 73, 130, nil, nil, 93, 1171, 103, 136, 1161, 119, + 1118, 133, 172, 200, 231, nil, nil, 1652, nil, 59, + nil, nil, nil, -21, 998, 980, nil, nil, 973, nil, + 249, nil, 248, 255, 901, nil, 882, nil, nil, nil, + nil, nil, nil, nil, 270, nil, nil, nil, nil, 260, + 564, nil, nil, 96, nil, 288, nil, 824, 192, nil, + 159, 144, 186, nil, 598, 1446, 912, 1514, 1343, 1499, + 1533, nil, 1419, 1480, 588, nil, 1734, 173, 257, 434, + nil, -3, nil, nil, 1753, nil, 390, nil, 1233, 125, + nil, nil, nil, nil, nil, nil, 376, nil, nil, nil, + 1794, nil, nil, nil, 1775, nil, nil, nil, 372, 374, + nil, 376, 378, 380, nil, nil, nil, nil, 397, nil, + nil, nil, 174, nil, 122, nil, nil, nil, nil, nil, + 79, nil, 148, 415, nil, nil, 127, 419, nil, nil, + nil, nil, nil, 1712, nil, nil, nil, nil, 423, 76, + 426, nil, nil, 1693, nil, 1671, nil, nil, nil, 1630, + nil, 1570, nil, nil, nil ] racc_action_default = [ - -196, -233, -233, -50, -233, -8, -9, -233, -233, -22, - -10, -187, -188, -11, -185, -12, -233, -233, -13, -1, - -14, -2, -233, -186, -15, -3, -233, -16, -5, -233, - -233, -17, -6, -233, -18, -7, -196, -188, -186, -233, - -51, -26, -27, -233, -24, -25, -233, -233, -233, -85, - -92, -196, -233, -195, -193, -196, -189, -191, -192, -221, - -194, -4, -196, -233, -85, -196, -53, -231, -42, -174, - -43, -213, -117, -33, -233, -233, -44, -31, -74, -32, - -233, -36, -233, -122, -37, -233, -73, -38, -172, -72, - -39, -40, -173, -41, -233, -103, -111, -233, -132, -112, - -233, -104, -233, -108, -110, -105, -233, -114, -106, -113, - -109, -233, -125, -107, -233, -233, -49, -175, -176, -178, - -233, -233, -197, -198, -83, -19, -22, -186, -21, -23, - -82, -84, -233, -75, -86, -81, -70, -74, -76, -219, - -79, -68, -77, -73, -233, -171, -170, -80, -78, -90, - -91, -93, -233, -219, -196, 384, -233, -233, -233, -207, - -233, -57, -213, -196, -59, -233, -66, -65, -56, -73, - -95, -233, -219, -233, -233, -92, -233, -30, -233, -118, - -233, -233, -233, -233, -233, -142, -233, -149, -233, -216, - -229, -225, -233, -228, -224, -233, -233, -233, -233, -233, - -233, -233, -233, -233, -233, -233, -233, -233, -233, -233, - -233, -233, -233, -233, -233, -233, -20, -233, -206, -233, - -204, -233, -201, -230, -233, -71, -220, -233, -233, -85, - -233, -220, -233, -233, -233, -209, -190, -233, -208, -233, - -54, -62, -61, -233, -233, -233, -217, -218, -233, -124, - -233, -55, -219, -233, -233, -28, -233, -120, -119, -35, - -34, -168, -166, -233, -169, -160, -167, -161, -73, -233, - -123, -116, -233, -152, -218, -214, -233, -233, -222, -137, - -139, -138, -133, -140, -144, -141, -146, -151, -148, -145, - -134, -150, -147, -143, -135, -233, -128, -136, -233, -154, - -233, -158, -177, -233, -180, -233, -199, -233, -233, -200, - -45, -69, -87, -46, -88, -219, -89, -94, -48, -233, - -211, -210, -212, -233, -184, -58, -60, -97, -98, -63, - -102, -99, -100, -101, -64, -96, -47, -233, -232, -29, - -121, -233, -163, -219, -115, -215, -227, -226, -223, -128, - -127, -233, -233, -155, -153, -233, -233, -179, -205, -203, - -202, -67, -233, -182, -183, -52, -165, -218, -233, -233, - -126, -129, -233, -159, -233, -181, -164, -162, -233, -131, - -233, -157, -130, -156 ] + -197, -234, -234, -50, -234, -8, -9, -234, -234, -22, + -10, -188, -189, -11, -186, -12, -234, -234, -13, -1, + -14, -2, -234, -187, -15, -3, -234, -16, -5, -234, + -234, -17, -6, -234, -18, -7, -197, -189, -187, -234, + -51, -26, -27, -234, -24, -25, -234, -234, -234, -85, + -92, -197, -234, -196, -194, -197, -190, -192, -193, -222, + -195, -4, -197, -234, -85, -197, -53, -232, -42, -175, + -43, -214, -117, -33, -234, -234, -44, -31, -74, -32, + -234, -36, -234, -122, -37, -234, -73, -38, -172, -72, + -39, -40, -174, -41, -234, -103, -111, -234, -132, -112, + -234, -104, -234, -108, -110, -105, -234, -114, -106, -113, + -109, -234, -125, -107, -234, -234, -49, -176, -177, -179, + -234, -234, -198, -199, -83, -19, -22, -187, -21, -23, + -82, -84, -234, -75, -86, -81, -70, -74, -76, -220, + -79, -68, -77, -73, -234, -171, -170, -80, -78, -90, + -91, -93, -234, -220, -197, 385, -234, -234, -234, -208, + -234, -57, -214, -197, -59, -234, -66, -65, -56, -73, + -95, -234, -220, -234, -234, -92, -234, -30, -234, -118, + -234, -234, -234, -234, -234, -142, -234, -149, -234, -217, + -230, -226, -234, -229, -225, -234, -234, -234, -234, -234, + -234, -234, -234, -234, -234, -234, -234, -234, -234, -234, + -234, -234, -234, -234, -234, -234, -20, -234, -207, -234, + -205, -234, -202, -231, -234, -71, -221, -234, -234, -85, + -234, -221, -234, -234, -234, -210, -191, -234, -209, -234, + -54, -62, -61, -234, -234, -234, -218, -219, -234, -124, + -234, -55, -220, -234, -234, -28, -234, -120, -119, -35, + -34, -173, -168, -166, -234, -169, -160, -167, -161, -73, + -234, -123, -116, -234, -152, -219, -215, -234, -234, -223, + -137, -139, -138, -133, -140, -144, -141, -146, -151, -148, + -145, -134, -150, -147, -143, -135, -234, -128, -136, -234, + -154, -234, -158, -178, -234, -181, -234, -200, -234, -234, + -201, -45, -69, -87, -46, -88, -220, -89, -94, -48, + -234, -212, -211, -213, -234, -185, -58, -60, -97, -98, + -63, -102, -99, -100, -101, -64, -96, -47, -234, -233, + -29, -121, -234, -163, -220, -115, -216, -228, -227, -224, + -128, -127, -234, -234, -155, -153, -234, -234, -180, -206, + -204, -203, -67, -234, -183, -184, -52, -165, -219, -234, + -234, -126, -129, -234, -159, -234, -182, -164, -162, -234, + -131, -234, -157, -130, -156 ] racc_goto_table = [ - 22, 9, 68, 112, 53, 118, 61, 36, 91, 222, - 19, 267, 70, 93, 2, 227, 139, 191, 51, 22, - 9, 179, 77, 56, 73, 149, 21, 141, 133, 232, - 115, 172, 153, 2, 146, 263, 125, 116, 135, 148, - 147, 299, 129, 22, 126, 260, 160, 350, 250, 174, - 128, 171, 43, 68, 121, 329, 334, 298, 368, 91, - 258, 265, 123, 70, 93, 317, 343, 301, 136, 154, - 183, 119, 224, 178, 233, 73, 55, 123, 157, 66, - 238, 159, 120, 219, 221, 190, 325, 321, 195, 16, - 188, nil, nil, nil, nil, nil, nil, nil, 342, nil, - 370, nil, nil, nil, nil, nil, nil, nil, nil, nil, - nil, nil, nil, 216, nil, nil, nil, nil, 260, 129, - 22, 126, 263, nil, nil, 353, nil, 128, 337, nil, - nil, nil, nil, nil, nil, nil, nil, nil, nil, 81, - nil, nil, nil, 53, nil, 53, nil, 243, nil, nil, - 149, 301, nil, nil, nil, nil, nil, 252, nil, nil, - 68, 261, 236, 68, nil, 138, 91, 146, nil, 91, - 70, 93, nil, 70, 93, nil, nil, nil, 166, nil, - 235, 166, 259, 272, nil, 73, nil, 302, 347, nil, - 81, 361, nil, 261, 290, 360, 315, 376, 294, 146, - nil, 312, 340, 311, 133, nil, 149, nil, 373, nil, - 146, nil, 22, 9, 135, 148, 147, 22, 9, 369, - nil, 263, 295, 327, 327, nil, 2, 303, nil, 146, - 146, 2, nil, 68, 333, 333, nil, 22, 9, 91, - 320, 88, nil, 70, 93, nil, nil, 323, 261, nil, - nil, 2, nil, nil, 146, 259, 190, nil, nil, nil, - nil, nil, nil, nil, nil, nil, nil, 88, nil, nil, - nil, nil, nil, nil, nil, 87, nil, 261, nil, 166, - nil, nil, 61, 146, nil, nil, nil, nil, nil, nil, - 61, nil, 88, nil, nil, 22, 9, 81, 262, nil, - 81, 142, nil, 22, 9, nil, nil, nil, nil, 2, - 61, nil, nil, nil, nil, nil, nil, 2, nil, 22, - 9, nil, nil, 22, 9, nil, 87, nil, 371, 362, - 262, nil, nil, 2, 261, nil, nil, 2, nil, nil, - 146, 138, nil, nil, nil, nil, nil, 261, nil, 61, - nil, nil, nil, 146, nil, 166, nil, nil, nil, nil, - 328, 328, 22, 9, 114, 61, nil, 61, nil, 84, - 81, nil, 22, 9, 22, 9, 2, 90, 22, 9, - 22, 9, 378, 132, 380, 262, 2, nil, 2, nil, - nil, nil, 2, nil, 2, 140, nil, nil, 170, 88, - 88, nil, 88, 145, nil, nil, nil, nil, 167, nil, - nil, 167, nil, nil, 262, nil, nil, 170, nil, nil, - 84, nil, nil, nil, nil, nil, nil, nil, 90, nil, - nil, nil, 88, 87, 266, nil, 87, 170, nil, nil, - nil, nil, nil, 88, nil, nil, nil, nil, nil, nil, - nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, - nil, nil, 88, 88, nil, nil, 266, nil, nil, nil, - nil, 262, 88, nil, nil, nil, nil, 142, nil, nil, - nil, nil, nil, nil, 262, nil, nil, 88, nil, nil, - nil, nil, nil, nil, nil, nil, 331, 331, nil, nil, - nil, nil, nil, nil, nil, nil, 87, nil, nil, 167, - nil, 253, nil, nil, nil, nil, 88, nil, nil, nil, - nil, 266, nil, nil, nil, nil, nil, 84, 264, nil, - 84, nil, nil, nil, 282, 90, 145, nil, 90, nil, - nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, - 266, nil, nil, nil, nil, nil, nil, nil, nil, nil, - 264, nil, nil, 314, nil, 316, nil, 124, 145, nil, - nil, 140, nil, 88, 130, 131, nil, nil, nil, 145, - nil, nil, nil, 335, nil, 167, 88, nil, nil, nil, - 330, 330, nil, nil, nil, nil, nil, nil, 332, 332, - 84, nil, nil, 180, nil, nil, nil, 266, 90, nil, - nil, 346, nil, nil, nil, 264, nil, nil, nil, nil, - 266, nil, 185, 145, nil, 186, nil, nil, 187, nil, - nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, - 358, nil, 359, nil, 264, nil, nil, nil, nil, nil, - nil, nil, 145, nil, nil, nil, nil, nil, nil, nil, + 22, 9, 68, 112, 222, 264, 61, 36, 53, 179, + 268, 141, 70, 19, 2, 77, 191, 118, 51, 22, + 9, 139, 116, 21, 73, 91, 56, 147, 133, 149, + 115, 227, 153, 2, 300, 128, 172, 302, 135, 160, + 125, 129, 174, 22, 126, 232, 351, 43, 171, 299, + 260, 146, 369, 68, 121, 330, 335, 258, 266, 123, + 318, 344, 136, 70, 250, 119, 178, 183, 224, 154, + 233, 55, 157, 66, 123, 73, 91, 120, 159, 238, + 219, 221, 326, 322, 195, 190, 16, 188, nil, nil, + nil, nil, nil, 264, nil, nil, nil, nil, 343, 371, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, - nil, nil, nil, nil, nil, nil, 366, nil, nil, nil, + nil, nil, 128, nil, nil, 88, nil, 216, 129, 354, + 22, 126, 302, 260, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, + 243, 88, nil, nil, 338, nil, nil, 53, nil, 53, + nil, nil, nil, nil, 149, nil, nil, 252, nil, nil, + 68, 262, nil, 68, nil, 236, 88, nil, nil, nil, + 70, nil, nil, 70, nil, nil, 273, 235, nil, 374, + nil, nil, 259, 91, 146, 73, 91, 312, 348, nil, + 341, 361, 264, 262, 87, nil, 261, 377, 291, 303, + nil, 316, 295, 147, 133, 313, nil, nil, 362, nil, + 149, nil, 22, 9, 135, nil, 146, 22, 9, nil, + 142, nil, nil, 328, 328, 296, 2, 146, 261, nil, + 304, 2, nil, 68, nil, nil, 370, 22, 9, nil, + 321, nil, nil, 70, nil, 87, 146, 146, nil, 262, + 324, 2, nil, nil, nil, 259, 91, 190, 261, 261, + nil, 81, nil, nil, nil, nil, nil, nil, nil, nil, + nil, nil, 146, 88, 88, nil, 88, nil, 262, nil, + nil, nil, nil, 61, 261, nil, nil, 138, 84, nil, + nil, 61, nil, nil, nil, nil, 22, 9, nil, nil, + 166, 146, nil, 166, 22, 9, 88, nil, nil, nil, + 2, 61, 81, 261, 140, nil, nil, 88, 2, nil, + 22, 9, nil, nil, 22, 9, nil, 167, nil, 372, + 167, nil, nil, 363, 2, 262, 88, 88, 2, 84, + nil, 90, nil, nil, nil, nil, 88, nil, 262, nil, + 61, nil, 87, 267, nil, 87, nil, nil, 146, nil, + nil, nil, 88, 22, 9, nil, 61, 145, 61, nil, + 261, 146, nil, 22, 9, 22, 9, 2, 114, 22, + 9, 22, 9, 261, 93, 267, 379, 2, 381, 2, + nil, 88, 90, 2, nil, 2, 142, 132, nil, nil, + nil, 166, nil, nil, nil, nil, nil, nil, nil, nil, + 148, nil, 170, nil, nil, 332, 332, nil, nil, 81, + 263, nil, 81, nil, nil, 87, nil, nil, 167, nil, + nil, 170, nil, nil, nil, 93, nil, nil, nil, nil, + nil, 267, nil, nil, nil, nil, 84, 265, 88, 84, + nil, 170, 263, nil, nil, nil, nil, nil, nil, nil, + nil, 88, nil, 138, nil, nil, nil, nil, nil, nil, + 267, nil, nil, nil, nil, nil, nil, 166, 124, 265, + nil, nil, 329, 329, nil, 130, 131, nil, nil, nil, + 140, nil, 81, nil, nil, nil, nil, nil, nil, 90, + 145, nil, 90, nil, 167, nil, nil, nil, 263, 331, + 331, nil, nil, nil, 180, nil, nil, nil, nil, 84, + nil, nil, nil, nil, nil, 253, nil, 267, nil, nil, + nil, nil, 145, 185, nil, 265, 186, 263, nil, 187, + 267, nil, 93, 145, nil, 93, nil, nil, 283, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, - nil, 264, nil, nil, nil, nil, nil, nil, nil, 145, - nil, nil, nil, nil, 264, nil, nil, nil, nil, nil, - nil, nil, 145, nil, 279, 280, 281, nil, 283, 284, - 285, 286, 287, 288, 289, nil, 291, 292, 293, nil, - nil, 297, nil, nil, nil, nil, nil, nil, nil, nil, + nil, nil, 333, 333, 265, nil, nil, nil, nil, nil, + nil, nil, 90, nil, nil, nil, nil, 315, nil, 317, + nil, nil, nil, nil, nil, nil, 148, nil, 145, nil, + nil, nil, nil, nil, 263, nil, nil, 336, nil, nil, + nil, nil, nil, nil, nil, 334, 334, 263, nil, nil, + nil, nil, nil, nil, nil, 93, nil, 145, nil, nil, + nil, 265, nil, nil, nil, nil, 347, nil, nil, nil, + nil, nil, nil, nil, 265, 280, 281, 282, nil, 284, + 285, 286, 287, 288, 289, 290, nil, 292, 293, 294, + nil, nil, 298, nil, nil, 359, nil, 360, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, - nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, - nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, - nil, nil, nil, nil, 180 ] + nil, nil, nil, nil, 145, nil, nil, nil, nil, nil, + nil, nil, nil, nil, nil, nil, nil, 145, nil, nil, + nil, 367, nil, nil, nil, 180 ] racc_goto_check = [ - 37, 21, 30, 62, 64, 72, 4, 32, 28, 82, - 2, 70, 31, 29, 52, 36, 35, 85, 32, 37, - 21, 60, 22, 78, 21, 53, 3, 47, 30, 36, - 37, 35, 38, 52, 28, 68, 19, 5, 31, 29, - 50, 66, 7, 37, 21, 23, 41, 63, 36, 41, - 5, 57, 20, 30, 74, 46, 46, 65, 58, 28, - 61, 69, 3, 31, 29, 56, 71, 68, 33, 74, - 57, 73, 34, 22, 75, 21, 76, 3, 77, 40, - 79, 3, 20, 80, 81, 30, 42, 83, 84, 1, - 57, nil, nil, nil, nil, nil, nil, nil, 70, nil, - 63, nil, nil, nil, nil, nil, nil, nil, nil, nil, - nil, nil, nil, 19, nil, nil, nil, nil, 23, 7, - 37, 21, 68, nil, nil, 66, nil, 5, 36, nil, - nil, nil, nil, nil, nil, nil, nil, nil, nil, 24, - nil, nil, nil, 64, nil, 64, nil, 41, nil, nil, - 53, 68, nil, nil, nil, nil, nil, 38, nil, nil, - 30, 30, 78, 30, nil, 24, 28, 28, nil, 28, - 31, 29, nil, 31, 29, nil, nil, nil, 24, nil, - 3, 24, 21, 22, nil, 21, nil, 72, 85, nil, - 24, 36, nil, 30, 64, 82, 35, 70, 64, 28, - nil, 53, 60, 47, 30, nil, 53, nil, 68, nil, - 28, nil, 37, 21, 31, 29, 50, 37, 21, 36, - nil, 68, 2, 30, 30, nil, 52, 2, nil, 28, - 28, 52, nil, 30, 29, 29, nil, 37, 21, 28, - 32, 49, nil, 31, 29, nil, nil, 2, 30, nil, - nil, 52, nil, nil, 28, 21, 30, nil, nil, nil, - nil, nil, nil, nil, nil, nil, nil, 49, nil, nil, - nil, nil, nil, nil, nil, 26, nil, 30, nil, 24, - nil, nil, 4, 28, nil, nil, nil, nil, nil, nil, - 4, nil, 49, nil, nil, 37, 21, 24, 24, nil, - 24, 26, nil, 37, 21, nil, nil, nil, nil, 52, - 4, nil, nil, nil, nil, nil, nil, 52, nil, 37, - 21, nil, nil, 37, 21, nil, 26, nil, 62, 2, - 24, nil, nil, 52, 30, nil, nil, 52, nil, nil, - 28, 24, nil, nil, nil, nil, nil, 30, nil, 4, - nil, nil, nil, 28, nil, 24, nil, nil, nil, nil, - 24, 24, 37, 21, 54, 4, nil, 4, nil, 25, - 24, nil, 37, 21, 37, 21, 52, 27, 37, 21, - 37, 21, 2, 54, 2, 24, 52, nil, 52, nil, - nil, nil, 52, nil, 52, 25, nil, nil, 54, 49, - 49, nil, 49, 27, nil, nil, nil, nil, 25, nil, - nil, 25, nil, nil, 24, nil, nil, 54, nil, nil, - 25, nil, nil, nil, nil, nil, nil, nil, 27, nil, - nil, nil, 49, 26, 26, nil, 26, 54, nil, nil, - nil, nil, nil, 49, nil, nil, nil, nil, nil, nil, - nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, - nil, nil, 49, 49, nil, nil, 26, nil, nil, nil, - nil, 24, 49, nil, nil, nil, nil, 26, nil, nil, - nil, nil, nil, nil, 24, nil, nil, 49, nil, nil, - nil, nil, nil, nil, nil, nil, 26, 26, nil, nil, - nil, nil, nil, nil, nil, nil, 26, nil, nil, 25, - nil, 54, nil, nil, nil, nil, 49, nil, nil, nil, - nil, 26, nil, nil, nil, nil, nil, 25, 25, nil, - 25, nil, nil, nil, 54, 27, 27, nil, 27, nil, - nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, - 26, nil, nil, nil, nil, nil, nil, nil, nil, nil, - 25, nil, nil, 54, nil, 54, nil, 51, 27, nil, - nil, 25, nil, 49, 51, 51, nil, nil, nil, 27, - nil, nil, nil, 54, nil, 25, 49, nil, nil, nil, - 25, 25, nil, nil, nil, nil, nil, nil, 27, 27, - 25, nil, nil, 51, nil, nil, nil, 26, 27, nil, - nil, 54, nil, nil, nil, 25, nil, nil, nil, nil, - 26, nil, 51, 27, nil, 51, nil, nil, 51, nil, - nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, - 54, nil, 54, nil, 25, nil, nil, nil, nil, nil, - nil, nil, 27, nil, nil, nil, nil, nil, nil, nil, + 37, 21, 30, 62, 82, 68, 4, 32, 64, 60, + 70, 47, 31, 2, 52, 22, 85, 72, 32, 37, + 21, 35, 5, 3, 21, 28, 78, 50, 30, 53, + 37, 36, 38, 52, 66, 5, 35, 68, 31, 41, + 19, 7, 41, 37, 21, 36, 63, 20, 57, 65, + 23, 28, 58, 30, 74, 46, 46, 61, 69, 3, + 56, 71, 33, 31, 36, 73, 22, 57, 34, 74, + 75, 76, 77, 40, 3, 21, 28, 20, 3, 79, + 80, 81, 42, 83, 84, 30, 1, 57, nil, nil, + nil, nil, nil, 68, nil, nil, nil, nil, 70, 63, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, - nil, nil, nil, nil, nil, nil, 54, nil, nil, nil, + nil, nil, 5, nil, nil, 49, nil, 19, 7, 66, + 37, 21, 68, 23, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, + 41, 49, nil, nil, 36, nil, nil, 64, nil, 64, + nil, nil, nil, nil, 53, nil, nil, 38, nil, nil, + 30, 30, nil, 30, nil, 78, 49, nil, nil, nil, + 31, nil, nil, 31, nil, nil, 22, 3, nil, 68, + nil, nil, 21, 28, 28, 21, 28, 47, 85, nil, + 60, 82, 68, 30, 26, nil, 52, 70, 64, 72, + nil, 35, 64, 50, 30, 53, nil, nil, 36, nil, + 53, nil, 37, 21, 31, nil, 28, 37, 21, nil, + 26, nil, nil, 30, 30, 2, 52, 28, 52, nil, + 2, 52, nil, 30, nil, nil, 36, 37, 21, nil, + 32, nil, nil, 31, nil, 26, 28, 28, nil, 30, + 2, 52, nil, nil, nil, 21, 28, 30, 52, 52, + nil, 24, nil, nil, nil, nil, nil, nil, nil, nil, + nil, nil, 28, 49, 49, nil, 49, nil, 30, nil, + nil, nil, nil, 4, 52, nil, nil, 24, 25, nil, + nil, 4, nil, nil, nil, nil, 37, 21, nil, nil, + 24, 28, nil, 24, 37, 21, 49, nil, nil, nil, + 52, 4, 24, 52, 25, nil, nil, 49, 52, nil, + 37, 21, nil, nil, 37, 21, nil, 25, nil, 62, + 25, nil, nil, 2, 52, 30, 49, 49, 52, 25, + nil, 27, nil, nil, nil, nil, 49, nil, 30, nil, + 4, nil, 26, 26, nil, 26, nil, nil, 28, nil, + nil, nil, 49, 37, 21, nil, 4, 27, 4, nil, + 52, 28, nil, 37, 21, 37, 21, 52, 54, 37, + 21, 37, 21, 52, 29, 26, 2, 52, 2, 52, + nil, 49, 27, 52, nil, 52, 26, 54, nil, nil, + nil, 24, nil, nil, nil, nil, nil, nil, nil, nil, + 29, nil, 54, nil, nil, 26, 26, nil, nil, 24, + 24, nil, 24, nil, nil, 26, nil, nil, 25, nil, + nil, 54, nil, nil, nil, 29, nil, nil, nil, nil, + nil, 26, nil, nil, nil, nil, 25, 25, 49, 25, + nil, 54, 24, nil, nil, nil, nil, nil, nil, nil, + nil, 49, nil, 24, nil, nil, nil, nil, nil, nil, + 26, nil, nil, nil, nil, nil, nil, 24, 51, 25, + nil, nil, 24, 24, nil, 51, 51, nil, nil, nil, + 25, nil, 24, nil, nil, nil, nil, nil, nil, 27, + 27, nil, 27, nil, 25, nil, nil, nil, 24, 25, + 25, nil, nil, nil, 51, nil, nil, nil, nil, 25, + nil, nil, nil, nil, nil, 54, nil, 26, nil, nil, + nil, nil, 27, 51, nil, 25, 51, 24, nil, 51, + 26, nil, 29, 27, nil, 29, nil, nil, 54, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, - nil, 25, nil, nil, nil, nil, nil, nil, nil, 27, - nil, nil, nil, nil, 25, nil, nil, nil, nil, nil, - nil, nil, 27, nil, 51, 51, 51, nil, 51, 51, - 51, 51, 51, 51, 51, nil, 51, 51, 51, nil, - nil, 51, nil, nil, nil, nil, nil, nil, nil, nil, + nil, nil, 27, 27, 25, nil, nil, nil, nil, nil, + nil, nil, 27, nil, nil, nil, nil, 54, nil, 54, + nil, nil, nil, nil, nil, nil, 29, nil, 27, nil, + nil, nil, nil, nil, 24, nil, nil, 54, nil, nil, + nil, nil, nil, nil, nil, 29, 29, 24, nil, nil, + nil, nil, nil, nil, nil, 29, nil, 27, nil, nil, + nil, 25, nil, nil, nil, nil, 54, nil, nil, nil, + nil, nil, nil, nil, 25, 51, 51, 51, nil, 51, + 51, 51, 51, 51, 51, 51, nil, 51, 51, 51, + nil, nil, 51, nil, nil, 54, nil, 54, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, - nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, - nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, - nil, nil, nil, nil, 51 ] + nil, nil, nil, nil, 27, nil, nil, nil, nil, nil, + nil, nil, nil, nil, nil, nil, nil, 27, nil, nil, + nil, 54, nil, nil, nil, 51 ] racc_goto_pointer = [ - nil, 89, 10, 26, -13, 7, nil, -1, nil, nil, - nil, nil, nil, nil, nil, nil, nil, nil, nil, -7, - 48, 1, -1, -136, 116, 346, 252, 354, -15, -10, - -21, -11, 6, 19, -64, -33, -124, 0, -18, nil, - 57, -16, -153, nil, nil, nil, -189, -22, nil, 218, - -9, 528, 14, -25, 335, nil, -166, -12, -285, nil, - -54, -120, -23, -249, -13, -157, -173, nil, -147, -121, - -171, -203, -28, 38, 18, -80, 59, 23, 6, -78, - -39, -38, -113, -147, -18, -89, nil ] + nil, 86, 13, 23, -13, -8, nil, -2, nil, nil, + nil, nil, nil, nil, nil, nil, nil, nil, nil, -3, + 43, 1, -8, -131, 238, 265, 171, 318, 2, 361, + -21, -11, 6, 13, -68, -28, -108, 0, -18, nil, + 51, -23, -157, nil, nil, nil, -189, -38, nil, 92, + -22, 439, 14, -21, 349, nil, -171, -15, -292, nil, + -66, -123, -23, -251, -9, -165, -180, nil, -177, -124, + -172, -209, -16, 32, 18, -84, 54, 17, 9, -79, + -42, -41, -118, -151, -22, -90, nil ] racc_goto_default = [ nil, nil, nil, 168, 25, 28, 32, 35, 5, 6, 10, 13, 15, 18, 20, 24, 27, 31, 34, 4, nil, 99, nil, 79, 101, 103, 105, 108, 109, 113, 95, 96, 8, nil, nil, nil, nil, 85, nil, 30, nil, nil, 161, 239, 164, 165, nil, nil, 144, 107, 110, 111, 67, 134, 98, 150, 151, nil, 248, 104, - nil, nil, nil, nil, 69, nil, nil, 300, 80, nil, + nil, nil, nil, nil, 69, nil, nil, 301, 80, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, 57, nil, nil, nil, nil, nil, nil, 192 ] -racc_token_table = { - false => 0, - Object.new => 1, - :STRING => 2, - :DQPRE => 3, - :DQMID => 4, - :DQPOST => 5, - :LBRACK => 6, - :RBRACK => 7, - :LBRACE => 8, - :RBRACE => 9, - :SYMBOL => 10, - :FARROW => 11, - :COMMA => 12, - :TRUE => 13, - :FALSE => 14, - :EQUALS => 15, - :APPENDS => 16, - :LESSEQUAL => 17, - :NOTEQUAL => 18, - :DOT => 19, - :COLON => 20, - :LLCOLLECT => 21, - :RRCOLLECT => 22, - :QMARK => 23, - :LPAREN => 24, - :RPAREN => 25, - :ISEQUAL => 26, - :GREATEREQUAL => 27, - :GREATERTHAN => 28, - :LESSTHAN => 29, - :IF => 30, - :ELSE => 31, - :IMPORT => 32, - :DEFINE => 33, - :ELSIF => 34, - :VARIABLE => 35, - :CLASS => 36, - :INHERITS => 37, - :NODE => 38, - :BOOLEAN => 39, - :NAME => 40, - :SEMIC => 41, - :CASE => 42, - :DEFAULT => 43, - :AT => 44, - :LCOLLECT => 45, - :RCOLLECT => 46, - :CLASSNAME => 47, - :CLASSREF => 48, - :NOT => 49, - :OR => 50, - :AND => 51, - :UNDEF => 52, - :PARROW => 53, - :PLUS => 54, - :MINUS => 55, - :TIMES => 56, - :DIV => 57, - :LSHIFT => 58, - :RSHIFT => 59, - :UMINUS => 60, - :MATCH => 61, - :NOMATCH => 62, - :REGEX => 63, - :IN_EDGE => 64, - :OUT_EDGE => 65, - :IN_EDGE_SUB => 66, - :OUT_EDGE_SUB => 67, - :IN => 68 } +racc_reduce_table = [ + 0, 0, :racc_error, + 1, 70, :_reduce_1, + 1, 70, :_reduce_none, + 1, 71, :_reduce_none, + 2, 71, :_reduce_4, + 1, 73, :_reduce_none, + 1, 73, :_reduce_none, + 1, 73, :_reduce_none, + 1, 73, :_reduce_none, + 1, 73, :_reduce_none, + 1, 73, :_reduce_none, + 1, 73, :_reduce_none, + 1, 73, :_reduce_none, + 1, 73, :_reduce_none, + 1, 73, :_reduce_none, + 1, 73, :_reduce_none, + 1, 73, :_reduce_none, + 1, 73, :_reduce_none, + 1, 73, :_reduce_none, + 3, 87, :_reduce_19, + 3, 87, :_reduce_20, + 1, 88, :_reduce_none, + 1, 88, :_reduce_none, + 1, 88, :_reduce_none, + 1, 89, :_reduce_none, + 1, 89, :_reduce_none, + 1, 89, :_reduce_none, + 1, 89, :_reduce_none, + 4, 81, :_reduce_28, + 5, 81, :_reduce_29, + 3, 81, :_reduce_30, + 2, 81, :_reduce_31, + 1, 91, :_reduce_none, + 1, 91, :_reduce_none, + 3, 91, :_reduce_34, + 3, 91, :_reduce_35, + 1, 92, :_reduce_none, + 1, 92, :_reduce_none, + 1, 92, :_reduce_none, + 1, 92, :_reduce_none, + 1, 92, :_reduce_none, + 1, 92, :_reduce_none, + 1, 92, :_reduce_none, + 1, 92, :_reduce_none, + 1, 92, :_reduce_44, + 5, 74, :_reduce_45, + 5, 74, :_reduce_46, + 5, 74, :_reduce_47, + 5, 85, :_reduce_48, + 2, 75, :_reduce_49, + 1, 108, :_reduce_50, + 2, 108, :_reduce_51, + 6, 76, :_reduce_52, + 2, 76, :_reduce_53, + 3, 109, :_reduce_54, + 3, 109, :_reduce_55, + 1, 110, :_reduce_none, + 1, 110, :_reduce_none, + 3, 110, :_reduce_58, + 1, 111, :_reduce_none, + 3, 111, :_reduce_60, + 1, 112, :_reduce_61, + 1, 112, :_reduce_62, + 3, 113, :_reduce_63, + 3, 113, :_reduce_64, + 1, 114, :_reduce_none, + 1, 114, :_reduce_none, + 4, 116, :_reduce_67, + 1, 102, :_reduce_none, + 3, 102, :_reduce_69, + 0, 103, :_reduce_none, + 1, 103, :_reduce_none, + 1, 118, :_reduce_72, + 1, 93, :_reduce_73, + 1, 95, :_reduce_74, + 1, 117, :_reduce_none, + 1, 117, :_reduce_none, + 1, 117, :_reduce_none, + 1, 117, :_reduce_none, + 1, 117, :_reduce_none, + 1, 117, :_reduce_none, + 1, 117, :_reduce_none, + 3, 77, :_reduce_82, + 3, 77, :_reduce_83, + 3, 86, :_reduce_84, + 0, 104, :_reduce_85, + 1, 104, :_reduce_86, + 3, 104, :_reduce_87, + 3, 122, :_reduce_88, + 3, 124, :_reduce_89, + 1, 125, :_reduce_none, + 1, 125, :_reduce_none, + 0, 107, :_reduce_92, + 1, 107, :_reduce_93, + 3, 107, :_reduce_94, + 1, 126, :_reduce_none, + 3, 126, :_reduce_96, + 1, 115, :_reduce_none, + 1, 115, :_reduce_none, + 1, 115, :_reduce_none, + 1, 115, :_reduce_none, + 1, 115, :_reduce_none, + 1, 115, :_reduce_none, + 1, 123, :_reduce_none, + 1, 123, :_reduce_none, + 1, 123, :_reduce_none, + 1, 123, :_reduce_none, + 1, 123, :_reduce_none, + 1, 123, :_reduce_none, + 1, 123, :_reduce_none, + 1, 123, :_reduce_none, + 1, 123, :_reduce_none, + 1, 123, :_reduce_none, + 1, 123, :_reduce_none, + 1, 123, :_reduce_none, + 4, 97, :_reduce_115, + 3, 97, :_reduce_116, + 1, 99, :_reduce_117, + 2, 99, :_reduce_118, + 2, 129, :_reduce_119, + 1, 130, :_reduce_120, + 2, 130, :_reduce_121, + 1, 96, :_reduce_122, + 4, 90, :_reduce_123, + 4, 90, :_reduce_124, + 2, 79, :_reduce_125, + 5, 131, :_reduce_126, + 4, 131, :_reduce_127, + 0, 132, :_reduce_none, + 2, 132, :_reduce_129, + 4, 132, :_reduce_130, + 3, 132, :_reduce_131, + 1, 120, :_reduce_none, + 3, 120, :_reduce_133, + 3, 120, :_reduce_134, + 3, 120, :_reduce_135, + 3, 120, :_reduce_136, + 3, 120, :_reduce_137, + 3, 120, :_reduce_138, + 3, 120, :_reduce_139, + 3, 120, :_reduce_140, + 3, 120, :_reduce_141, + 2, 120, :_reduce_142, + 3, 120, :_reduce_143, + 3, 120, :_reduce_144, + 3, 120, :_reduce_145, + 3, 120, :_reduce_146, + 3, 120, :_reduce_147, + 3, 120, :_reduce_148, + 2, 120, :_reduce_149, + 3, 120, :_reduce_150, + 3, 120, :_reduce_151, + 3, 120, :_reduce_152, + 5, 78, :_reduce_153, + 1, 134, :_reduce_none, + 2, 134, :_reduce_155, + 5, 135, :_reduce_156, + 4, 135, :_reduce_157, + 1, 136, :_reduce_none, + 3, 136, :_reduce_159, + 3, 98, :_reduce_160, + 1, 138, :_reduce_none, + 4, 138, :_reduce_162, + 1, 140, :_reduce_none, + 3, 140, :_reduce_164, + 3, 139, :_reduce_165, + 1, 137, :_reduce_none, + 1, 137, :_reduce_none, + 1, 137, :_reduce_none, + 1, 137, :_reduce_none, + 1, 137, :_reduce_none, + 1, 137, :_reduce_none, + 1, 137, :_reduce_none, + 1, 137, :_reduce_none, + 1, 137, :_reduce_174, + 1, 137, :_reduce_none, + 1, 141, :_reduce_176, + 1, 142, :_reduce_none, + 3, 142, :_reduce_178, + 2, 80, :_reduce_179, + 6, 82, :_reduce_180, + 5, 82, :_reduce_181, + 7, 83, :_reduce_182, + 6, 83, :_reduce_183, + 6, 84, :_reduce_184, + 5, 84, :_reduce_185, + 1, 106, :_reduce_186, + 1, 101, :_reduce_187, + 1, 101, :_reduce_188, + 1, 101, :_reduce_189, + 1, 145, :_reduce_none, + 3, 145, :_reduce_191, + 1, 147, :_reduce_192, + 1, 148, :_reduce_193, + 1, 148, :_reduce_194, + 1, 148, :_reduce_195, + 1, 148, :_reduce_none, + 0, 72, :_reduce_197, + 0, 149, :_reduce_198, + 1, 143, :_reduce_none, + 3, 143, :_reduce_200, + 3, 143, :_reduce_201, + 1, 150, :_reduce_none, + 3, 150, :_reduce_203, + 3, 151, :_reduce_204, + 1, 151, :_reduce_205, + 3, 151, :_reduce_206, + 1, 151, :_reduce_207, + 1, 146, :_reduce_none, + 2, 146, :_reduce_209, + 1, 144, :_reduce_none, + 2, 144, :_reduce_211, + 1, 152, :_reduce_none, + 1, 152, :_reduce_none, + 1, 94, :_reduce_214, + 3, 119, :_reduce_215, + 4, 119, :_reduce_216, + 2, 119, :_reduce_217, + 1, 127, :_reduce_none, + 1, 127, :_reduce_none, + 0, 105, :_reduce_none, + 1, 105, :_reduce_221, + 1, 133, :_reduce_222, + 3, 128, :_reduce_223, + 4, 128, :_reduce_224, + 2, 128, :_reduce_225, + 1, 153, :_reduce_none, + 3, 153, :_reduce_227, + 3, 154, :_reduce_228, + 1, 155, :_reduce_229, + 1, 155, :_reduce_230, + 4, 121, :_reduce_231, + 1, 100, :_reduce_none, + 4, 100, :_reduce_233 ] + +racc_reduce_n = 234 + +racc_shift_n = 385 -racc_use_result_var = true +racc_token_table = { + false => 0, + :error => 1, + :STRING => 2, + :DQPRE => 3, + :DQMID => 4, + :DQPOST => 5, + :LBRACK => 6, + :RBRACK => 7, + :LBRACE => 8, + :RBRACE => 9, + :SYMBOL => 10, + :FARROW => 11, + :COMMA => 12, + :TRUE => 13, + :FALSE => 14, + :EQUALS => 15, + :APPENDS => 16, + :LESSEQUAL => 17, + :NOTEQUAL => 18, + :DOT => 19, + :COLON => 20, + :LLCOLLECT => 21, + :RRCOLLECT => 22, + :QMARK => 23, + :LPAREN => 24, + :RPAREN => 25, + :ISEQUAL => 26, + :GREATEREQUAL => 27, + :GREATERTHAN => 28, + :LESSTHAN => 29, + :IF => 30, + :ELSE => 31, + :IMPORT => 32, + :DEFINE => 33, + :ELSIF => 34, + :VARIABLE => 35, + :CLASS => 36, + :INHERITS => 37, + :NODE => 38, + :BOOLEAN => 39, + :NAME => 40, + :SEMIC => 41, + :CASE => 42, + :DEFAULT => 43, + :AT => 44, + :LCOLLECT => 45, + :RCOLLECT => 46, + :CLASSNAME => 47, + :CLASSREF => 48, + :NOT => 49, + :OR => 50, + :AND => 51, + :UNDEF => 52, + :PARROW => 53, + :PLUS => 54, + :MINUS => 55, + :TIMES => 56, + :DIV => 57, + :LSHIFT => 58, + :RSHIFT => 59, + :UMINUS => 60, + :MATCH => 61, + :NOMATCH => 62, + :REGEX => 63, + :IN_EDGE => 64, + :OUT_EDGE => 65, + :IN_EDGE_SUB => 66, + :OUT_EDGE_SUB => 67, + :IN => 68 } racc_nt_base = 69 +racc_use_result_var = true + Racc_arg = [ - racc_action_table, - racc_action_check, - racc_action_default, - racc_action_pointer, - racc_goto_table, - racc_goto_check, - racc_goto_default, - racc_goto_pointer, - racc_nt_base, - racc_reduce_table, - racc_token_table, - racc_shift_n, - racc_reduce_n, - racc_use_result_var ] + racc_action_table, + racc_action_check, + racc_action_default, + racc_action_pointer, + racc_goto_table, + racc_goto_check, + racc_goto_default, + racc_goto_pointer, + racc_nt_base, + racc_reduce_table, + racc_token_table, + racc_shift_n, + racc_reduce_n, + racc_use_result_var ] Racc_token_to_s_table = [ -'$end', -'error', -'STRING', -'DQPRE', -'DQMID', -'DQPOST', -'LBRACK', -'RBRACK', -'LBRACE', -'RBRACE', -'SYMBOL', -'FARROW', -'COMMA', -'TRUE', -'FALSE', -'EQUALS', -'APPENDS', -'LESSEQUAL', -'NOTEQUAL', -'DOT', -'COLON', -'LLCOLLECT', -'RRCOLLECT', -'QMARK', -'LPAREN', -'RPAREN', -'ISEQUAL', -'GREATEREQUAL', -'GREATERTHAN', -'LESSTHAN', -'IF', -'ELSE', -'IMPORT', -'DEFINE', -'ELSIF', -'VARIABLE', -'CLASS', -'INHERITS', -'NODE', -'BOOLEAN', -'NAME', -'SEMIC', -'CASE', -'DEFAULT', -'AT', -'LCOLLECT', -'RCOLLECT', -'CLASSNAME', -'CLASSREF', -'NOT', -'OR', -'AND', -'UNDEF', -'PARROW', -'PLUS', -'MINUS', -'TIMES', -'DIV', -'LSHIFT', -'RSHIFT', -'UMINUS', -'MATCH', -'NOMATCH', -'REGEX', -'IN_EDGE', -'OUT_EDGE', -'IN_EDGE_SUB', -'OUT_EDGE_SUB', -'IN', -'$start', -'program', -'statements', -'nil', -'statement', -'resource', -'virtualresource', -'collection', -'assignment', -'casestatement', -'ifstatement_begin', -'import', -'fstatement', -'definition', -'hostclass', -'nodedef', -'resourceoverride', -'append', -'relationship', -'relationship_side', -'edge', -'resourceref', -'funcvalues', -'namestring', -'name', -'variable', -'type', -'boolean', -'funcrvalue', -'selector', -'quotedtext', -'hasharrayaccesses', -'classname', -'resourceinstances', -'endsemi', -'params', -'endcomma', -'classref', -'anyparams', -'at', -'collectrhand', -'collstatements', -'collstatement', -'colljoin', -'collexpr', -'colllval', -'simplervalue', -'resourceinst', -'resourcename', -'undef', -'array', -'expression', -'hasharrayaccess', -'param', -'rvalue', -'addparam', -'anyparam', -'rvalues', -'comma', -'hash', -'dqrval', -'dqtail', -'ifstatement', -'else', -'regex', -'caseopts', -'caseopt', -'casevalues', -'selectlhand', -'svalues', -'selectval', -'sintvalues', -'string', -'strings', -'argumentlist', -'classparent', -'hostnames', -'nodeparent', -'nodename', -'hostname', -'nothing', -'arguments', -'argument', -'classnameordefault', -'hashpairs', -'hashpair', -'key'] + "$end", + "error", + "STRING", + "DQPRE", + "DQMID", + "DQPOST", + "LBRACK", + "RBRACK", + "LBRACE", + "RBRACE", + "SYMBOL", + "FARROW", + "COMMA", + "TRUE", + "FALSE", + "EQUALS", + "APPENDS", + "LESSEQUAL", + "NOTEQUAL", + "DOT", + "COLON", + "LLCOLLECT", + "RRCOLLECT", + "QMARK", + "LPAREN", + "RPAREN", + "ISEQUAL", + "GREATEREQUAL", + "GREATERTHAN", + "LESSTHAN", + "IF", + "ELSE", + "IMPORT", + "DEFINE", + "ELSIF", + "VARIABLE", + "CLASS", + "INHERITS", + "NODE", + "BOOLEAN", + "NAME", + "SEMIC", + "CASE", + "DEFAULT", + "AT", + "LCOLLECT", + "RCOLLECT", + "CLASSNAME", + "CLASSREF", + "NOT", + "OR", + "AND", + "UNDEF", + "PARROW", + "PLUS", + "MINUS", + "TIMES", + "DIV", + "LSHIFT", + "RSHIFT", + "UMINUS", + "MATCH", + "NOMATCH", + "REGEX", + "IN_EDGE", + "OUT_EDGE", + "IN_EDGE_SUB", + "OUT_EDGE_SUB", + "IN", + "$start", + "program", + "statements", + "nil", + "statement", + "resource", + "virtualresource", + "collection", + "assignment", + "casestatement", + "ifstatement_begin", + "import", + "fstatement", + "definition", + "hostclass", + "nodedef", + "resourceoverride", + "append", + "relationship", + "relationship_side", + "edge", + "resourceref", + "funcvalues", + "namestring", + "name", + "variable", + "type", + "boolean", + "funcrvalue", + "selector", + "quotedtext", + "hasharrayaccesses", + "classname", + "resourceinstances", + "endsemi", + "params", + "endcomma", + "classref", + "anyparams", + "at", + "collectrhand", + "collstatements", + "collstatement", + "colljoin", + "collexpr", + "colllval", + "simplervalue", + "resourceinst", + "resourcename", + "undef", + "array", + "expression", + "hasharrayaccess", + "param", + "rvalue", + "addparam", + "anyparam", + "rvalues", + "comma", + "hash", + "dqrval", + "dqtail", + "ifstatement", + "else", + "regex", + "caseopts", + "caseopt", + "casevalues", + "selectlhand", + "svalues", + "selectval", + "sintvalues", + "string", + "strings", + "argumentlist", + "classparent", + "hostnames", + "nodeparent", + "nodename", + "hostname", + "nothing", + "arguments", + "argument", + "classnameordefault", + "hashpairs", + "hashpair", + "key" ] Racc_debug_parser = false -##### racc system variables end ##### - - # reduce 0 omitted - -module_eval <<'.,.,', 'grammar.ra', 46 - def _reduce_1( val, _values, result ) - if val[0] - # Make sure we always return an array. - if val[0].is_a?(AST::ASTArray) - if val[0].children.empty? - result = nil - else - result = val[0] - end - else - result = aryfy(val[0]) - end - else +##### State transition tables end ##### + +# reduce 0 omitted + +module_eval(<<'.,.,', 'grammar.ra', 31) + def _reduce_1(val, _values, result) + if val[0] + # Make sure we always return an array. + if val[0].is_a?(AST::ASTArray) + if val[0].children.empty? result = nil + else + result = val[0] + end + else + result = aryfy(val[0]) end - result + else + result = nil + end + + result end .,., - # reduce 2 omitted +# reduce 2 omitted - # reduce 3 omitted +# reduce 3 omitted -module_eval <<'.,.,', 'grammar.ra', 62 - def _reduce_4( val, _values, result ) - if val[0] and val[1] - if val[0].instance_of?(AST::ASTArray) - val[0].push(val[1]) - result = val[0] - else - result = ast AST::ASTArray, :children => [val[0],val[1]] - end - elsif obj = (val[0] || val[1]) - result = obj - else result = nil +module_eval(<<'.,.,', 'grammar.ra', 50) + def _reduce_4(val, _values, result) + if val[0] and val[1] + if val[0].instance_of?(AST::ASTArray) + val[0].push(val[1]) + result = val[0] + else + result = ast AST::ASTArray, :children => [val[0],val[1]] end - result + elsif obj = (val[0] || val[1]) + result = obj + else result = nil + end + + result end .,., - # reduce 5 omitted +# reduce 5 omitted + +# reduce 6 omitted - # reduce 6 omitted +# reduce 7 omitted - # reduce 7 omitted +# reduce 8 omitted - # reduce 8 omitted +# reduce 9 omitted - # reduce 9 omitted +# reduce 10 omitted - # reduce 10 omitted +# reduce 11 omitted - # reduce 11 omitted +# reduce 12 omitted - # reduce 12 omitted +# reduce 13 omitted - # reduce 13 omitted +# reduce 14 omitted - # reduce 14 omitted +# reduce 15 omitted - # reduce 15 omitted +# reduce 16 omitted - # reduce 16 omitted +# reduce 17 omitted - # reduce 17 omitted +# reduce 18 omitted - # reduce 18 omitted +module_eval(<<'.,.,', 'grammar.ra', 80) + def _reduce_19(val, _values, result) + result = AST::Relationship.new(val[0], val[2], val[1][:value], ast_context) -module_eval <<'.,.,', 'grammar.ra', 82 - def _reduce_19( val, _values, result ) - result = AST::Relationship.new(val[0], val[2], val[1][:value], ast_context) - result + result end .,., -module_eval <<'.,.,', 'grammar.ra', 85 - def _reduce_20( val, _values, result ) - result = AST::Relationship.new(val[0], val[2], val[1][:value], ast_context) - result +module_eval(<<'.,.,', 'grammar.ra', 83) + def _reduce_20(val, _values, result) + result = AST::Relationship.new(val[0], val[2], val[1][:value], ast_context) + + result end .,., - # reduce 21 omitted +# reduce 21 omitted - # reduce 22 omitted +# reduce 22 omitted - # reduce 23 omitted +# reduce 23 omitted - # reduce 24 omitted +# reduce 24 omitted - # reduce 25 omitted +# reduce 25 omitted - # reduce 26 omitted +# reduce 26 omitted - # reduce 27 omitted +# reduce 27 omitted -module_eval <<'.,.,', 'grammar.ra', 98 - def _reduce_28( val, _values, result ) - args = aryfy(val[2]) - result = ast AST::Function, - :name => val[0][:value], - :line => val[0][:line], - :arguments => args, - :ftype => :statement - result +module_eval(<<'.,.,', 'grammar.ra', 91) + def _reduce_28(val, _values, result) + args = aryfy(val[2]) + result = ast AST::Function, + :name => val[0][:value], + :line => val[0][:line], + :arguments => args, + :ftype => :statement + + result end .,., -module_eval <<'.,.,', 'grammar.ra', 106 - def _reduce_29( val, _values, result ) - args = aryfy(val[2]) - result = ast AST::Function, - :name => val[0][:value], - :line => val[0][:line], - :arguments => args, - :ftype => :statement - result +module_eval(<<'.,.,', 'grammar.ra', 99) + def _reduce_29(val, _values, result) + args = aryfy(val[2]) + result = ast AST::Function, + :name => val[0][:value], + :line => val[0][:line], + :arguments => args, + :ftype => :statement + + result end .,., -module_eval <<'.,.,', 'grammar.ra', 112 - def _reduce_30( val, _values, result ) - result = ast AST::Function, - :name => val[0][:value], - :line => val[0][:line], - :arguments => AST::ASTArray.new({}), - :ftype => :statement - result +module_eval(<<'.,.,', 'grammar.ra', 106) + def _reduce_30(val, _values, result) + result = ast AST::Function, + :name => val[0][:value], + :line => val[0][:line], + :arguments => AST::ASTArray.new({}), + :ftype => :statement + + result end .,., -module_eval <<'.,.,', 'grammar.ra', 120 - def _reduce_31( val, _values, result ) - args = aryfy(val[1]) +module_eval(<<'.,.,', 'grammar.ra', 113) + def _reduce_31(val, _values, result) + args = aryfy(val[1]) result = ast AST::Function, - :name => val[0][:value], - :line => val[0][:line], - :arguments => args, - :ftype => :statement - result + :name => val[0][:value], + :line => val[0][:line], + :arguments => args, + :ftype => :statement + + result end .,., - # reduce 32 omitted +# reduce 32 omitted - # reduce 33 omitted +# reduce 33 omitted -module_eval <<'.,.,', 'grammar.ra', 128 - def _reduce_34( val, _values, result ) - result = aryfy(val[0], val[2]) +module_eval(<<'.,.,', 'grammar.ra', 124) + def _reduce_34(val, _values, result) + result = aryfy(val[0], val[2]) result.line = @lexer.line result.file = @lexer.file - result + + result end .,., -module_eval <<'.,.,', 'grammar.ra', 137 - def _reduce_35( val, _values, result ) - unless val[0].is_a?(AST::ASTArray) - val[0] = aryfy(val[0]) - end +module_eval(<<'.,.,', 'grammar.ra', 129) + def _reduce_35(val, _values, result) + unless val[0].is_a?(AST::ASTArray) + val[0] = aryfy(val[0]) + end - val[0].push(val[2]) + val[0].push(val[2]) - result = val[0] - result + result = val[0] + + result end .,., - # reduce 36 omitted +# reduce 36 omitted - # reduce 37 omitted +# reduce 37 omitted - # reduce 38 omitted +# reduce 38 omitted - # reduce 39 omitted +# reduce 39 omitted - # reduce 40 omitted +# reduce 40 omitted - # reduce 41 omitted +# reduce 41 omitted - # reduce 42 omitted +# reduce 42 omitted - # reduce 43 omitted +# reduce 43 omitted -module_eval <<'.,.,', 'grammar.ra', 151 - def _reduce_44( val, _values, result ) - result = ast AST::Name, :value => val[0][:value] - result +module_eval(<<'.,.,', 'grammar.ra', 149) + def _reduce_44(val, _values, result) + result = ast AST::Name, :value => val[0][:value] + + result end .,., -module_eval <<'.,.,', 'grammar.ra', 173 - def _reduce_45( val, _values, result ) - @lexer.commentpop - array = val[2] - if array.instance_of?(AST::ResourceInstance) - array = [array] - end - result = ast AST::ASTArray +module_eval(<<'.,.,', 'grammar.ra', 153) + def _reduce_45(val, _values, result) + @lexer.commentpop + array = val[2] + array = [array] if array.instance_of?(AST::ResourceInstance) + result = ast AST::ASTArray + + # this iterates across each specified resourceinstance + array.each { |instance| + raise Puppet::Dev, "Got something that isn't an instance" unless instance.instance_of?(AST::ResourceInstance) + # now, i need to somehow differentiate between those things with + # arrays in their names, and normal things - # this iterates across each specified resourceinstance - array.each { |instance| - unless instance.instance_of?(AST::ResourceInstance) - raise Puppet::Dev, "Got something that isn't an instance" - end - # now, i need to somehow differentiate between those things with - # arrays in their names, and normal things - result.push ast(AST::Resource, - :type => val[0], - :title => instance[0], - :parameters => instance[1]) - } - result + result.push ast( + AST::Resource, + :type => val[0], + :title => instance[0], + + :parameters => instance[1]) + } + + result end .,., -module_eval <<'.,.,', 'grammar.ra', 176 - def _reduce_46( val, _values, result ) - # This is a deprecated syntax. - error "All resource specifications require names" - result +module_eval(<<'.,.,', 'grammar.ra', 172) + def _reduce_46(val, _values, result) + # This is a deprecated syntax. + error "All resource specifications require names" + + result end .,., -module_eval <<'.,.,', 'grammar.ra', 180 - def _reduce_47( val, _values, result ) - # a defaults setting for a type - @lexer.commentpop - result = ast(AST::ResourceDefaults, :type => val[0], :parameters => val[2]) - result +module_eval(<<'.,.,', 'grammar.ra', 175) + def _reduce_47(val, _values, result) + # a defaults setting for a type + @lexer.commentpop + result = ast(AST::ResourceDefaults, :type => val[0], :parameters => val[2]) + + result end .,., -module_eval <<'.,.,', 'grammar.ra', 186 - def _reduce_48( val, _values, result ) - @lexer.commentpop - result = ast AST::ResourceOverride, :object => val[0], :parameters => val[2] - result +module_eval(<<'.,.,', 'grammar.ra', 182) + def _reduce_48(val, _values, result) + @lexer.commentpop + result = ast AST::ResourceOverride, :object => val[0], :parameters => val[2] + + result end .,., -module_eval <<'.,.,', 'grammar.ra', 213 - def _reduce_49( val, _values, result ) - type = val[0] +module_eval(<<'.,.,', 'grammar.ra', 189) + def _reduce_49(val, _values, result) + type = val[0] - if (type == :exported and ! Puppet[:storeconfigs]) and ! Puppet[:parseonly] - Puppet.warning addcontext("You cannot collect without storeconfigs being set") - end + if (type == :exported and ! Puppet[:storeconfigs]) and ! Puppet[:parseonly] + Puppet.warning addcontext("You cannot collect without storeconfigs being set") + end - if val[1].is_a? AST::ResourceDefaults - error "Defaults are not virtualizable" - end + error "Defaults are not virtualizable" if val[1].is_a? AST::ResourceDefaults - method = type.to_s + "=" + method = type.to_s + "=" - # Just mark our resources as exported and pass them through. - if val[1].instance_of?(AST::ASTArray) - val[1].each do |obj| - obj.send(method, true) - end - else - val[1].send(method, true) + # Just mark our resources as exported and pass them through. + if val[1].instance_of?(AST::ASTArray) + val[1].each do |obj| + obj.send(method, true) end + else + val[1].send(method, true) + end - result = val[1] - result + result = val[1] + + result end .,., -module_eval <<'.,.,', 'grammar.ra', 214 - def _reduce_50( val, _values, result ) - result = :virtual - result +module_eval(<<'.,.,', 'grammar.ra', 211) + def _reduce_50(val, _values, result) + result = :virtual + result end .,., -module_eval <<'.,.,', 'grammar.ra', 215 - def _reduce_51( val, _values, result ) - result = :exported - result +module_eval(<<'.,.,', 'grammar.ra', 212) + def _reduce_51(val, _values, result) + result = :exported + result end .,., -module_eval <<'.,.,', 'grammar.ra', 240 - def _reduce_52( val, _values, result ) - @lexer.commentpop - if val[0] =~ /^[a-z]/ - Puppet.warning addcontext("Collection names must now be capitalized") - end - type = val[0].downcase - args = {:type => type} +module_eval(<<'.,.,', 'grammar.ra', 217) + def _reduce_52(val, _values, result) + @lexer.commentpop + Puppet.warning addcontext("Collection names must now be capitalized") if val[0] =~ /^[a-z]/ + type = val[0].downcase + args = {:type => type} - if val[1].is_a?(AST::CollExpr) - args[:query] = val[1] - args[:query].type = type - args[:form] = args[:query].form - else - args[:form] = val[1] - end - if args[:form] == :exported and ! Puppet[:storeconfigs] and ! Puppet[:parseonly] - Puppet.warning addcontext("You cannot collect exported resources without storeconfigs being set; the collection will be ignored") - end - args[:override] = val[3] - result = ast AST::Collection, args - result + if val[1].is_a?(AST::CollExpr) + args[:query] = val[1] + args[:query].type = type + args[:form] = args[:query].form + else + args[:form] = val[1] + end + if args[:form] == :exported and ! Puppet[:storeconfigs] and ! Puppet[:parseonly] + Puppet.warning addcontext("You cannot collect exported resources without storeconfigs being set; the collection will be ignored") + end + args[:override] = val[3] + result = ast AST::Collection, args + + result end .,., -module_eval <<'.,.,', 'grammar.ra', 259 - def _reduce_53( val, _values, result ) - if val[0] =~ /^[a-z]/ - Puppet.warning addcontext("Collection names must now be capitalized") - end - type = val[0].downcase - args = {:type => type } +module_eval(<<'.,.,', 'grammar.ra', 236) + def _reduce_53(val, _values, result) + if val[0] =~ /^[a-z]/ + Puppet.warning addcontext("Collection names must now be capitalized") + end + type = val[0].downcase + args = {:type => type } - if val[1].is_a?(AST::CollExpr) - args[:query] = val[1] - args[:query].type = type - args[:form] = args[:query].form - else - args[:form] = val[1] - end - if args[:form] == :exported and ! Puppet[:storeconfigs] and ! Puppet[:parseonly] - Puppet.warning addcontext("You cannot collect exported resources without storeconfigs being set; the collection will be ignored") - end - result = ast AST::Collection, args - result + if val[1].is_a?(AST::CollExpr) + args[:query] = val[1] + args[:query].type = type + args[:form] = args[:query].form + else + args[:form] = val[1] + end + if args[:form] == :exported and ! Puppet[:storeconfigs] and ! Puppet[:parseonly] + Puppet.warning addcontext("You cannot collect exported resources without storeconfigs being set; the collection will be ignored") + end + result = ast AST::Collection, args + + result end .,., -module_eval <<'.,.,', 'grammar.ra', 269 - def _reduce_54( val, _values, result ) - if val[1] - result = val[1] - result.form = :virtual - else - result = :virtual - end - result +module_eval(<<'.,.,', 'grammar.ra', 257) + def _reduce_54(val, _values, result) + if val[1] + result = val[1] + result.form = :virtual + else + result = :virtual + end + + result end .,., -module_eval <<'.,.,', 'grammar.ra', 277 - def _reduce_55( val, _values, result ) - if val[1] - result = val[1] - result.form = :exported - else - result = :exported - end - result +module_eval(<<'.,.,', 'grammar.ra', 265) + def _reduce_55(val, _values, result) + if val[1] + result = val[1] + result.form = :exported + else + result = :exported + end + + result end .,., - # reduce 56 omitted +# reduce 56 omitted - # reduce 57 omitted +# reduce 57 omitted -module_eval <<'.,.,', 'grammar.ra', 285 - def _reduce_58( val, _values, result ) - result = ast AST::CollExpr, :test1 => val[0], :oper => val[1], :test2 => val[2] - result +module_eval(<<'.,.,', 'grammar.ra', 278) + def _reduce_58(val, _values, result) + result = ast AST::CollExpr, :test1 => val[0], :oper => val[1], :test2 => val[2] + + result end .,., - # reduce 59 omitted +# reduce 59 omitted -module_eval <<'.,.,', 'grammar.ra', 291 - def _reduce_60( val, _values, result ) - result = val[1] +module_eval(<<'.,.,', 'grammar.ra', 283) + def _reduce_60(val, _values, result) + result = val[1] result.parens = true - result + + result end .,., -module_eval <<'.,.,', 'grammar.ra', 292 - def _reduce_61( val, _values, result ) - result=val[0][:value] - result +module_eval(<<'.,.,', 'grammar.ra', 287) + def _reduce_61(val, _values, result) + result=val[0][:value] + result end .,., -module_eval <<'.,.,', 'grammar.ra', 293 - def _reduce_62( val, _values, result ) - result=val[0][:value] - result +module_eval(<<'.,.,', 'grammar.ra', 288) + def _reduce_62(val, _values, result) + result=val[0][:value] + result end .,., -module_eval <<'.,.,', 'grammar.ra', 300 - def _reduce_63( val, _values, result ) - result = ast AST::CollExpr, :test1 => val[0], :oper => val[1][:value], :test2 => val[2] - #result = ast AST::CollExpr - #result.push *val - result +module_eval(<<'.,.,', 'grammar.ra', 291) + def _reduce_63(val, _values, result) + result = ast AST::CollExpr, :test1 => val[0], :oper => val[1][:value], :test2 => val[2] + #result = ast AST::CollExpr + #result.push *val + + result end .,., -module_eval <<'.,.,', 'grammar.ra', 305 - def _reduce_64( val, _values, result ) - result = ast AST::CollExpr, :test1 => val[0], :oper => val[1][:value], :test2 => val[2] +module_eval(<<'.,.,', 'grammar.ra', 296) + def _reduce_64(val, _values, result) + result = ast AST::CollExpr, :test1 => val[0], :oper => val[1][:value], :test2 => val[2] #result = ast AST::CollExpr #result.push *val - result + + result end .,., - # reduce 65 omitted +# reduce 65 omitted - # reduce 66 omitted +# reduce 66 omitted -module_eval <<'.,.,', 'grammar.ra', 312 - def _reduce_67( val, _values, result ) - result = ast AST::ResourceInstance, :children => [val[0],val[2]] - result +module_eval(<<'.,.,', 'grammar.ra', 305) + def _reduce_67(val, _values, result) + result = ast AST::ResourceInstance, :children => [val[0],val[2]] + + result end .,., - # reduce 68 omitted +# reduce 68 omitted -module_eval <<'.,.,', 'grammar.ra', 322 - def _reduce_69( val, _values, result ) - if val[0].instance_of?(AST::ResourceInstance) - result = ast AST::ASTArray, :children => [val[0],val[2]] - else - val[0].push val[2] - result = val[0] - end - result +module_eval(<<'.,.,', 'grammar.ra', 310) + def _reduce_69(val, _values, result) + if val[0].instance_of?(AST::ResourceInstance) + result = ast AST::ASTArray, :children => [val[0],val[2]] + else + val[0].push val[2] + result = val[0] + end + + result end .,., - # reduce 70 omitted +# reduce 70 omitted + +# reduce 71 omitted - # reduce 71 omitted +module_eval(<<'.,.,', 'grammar.ra', 322) + def _reduce_72(val, _values, result) + result = ast AST::Undef, :value => :undef -module_eval <<'.,.,', 'grammar.ra', 329 - def _reduce_72( val, _values, result ) - result = ast AST::Undef, :value => :undef - result + result end .,., -module_eval <<'.,.,', 'grammar.ra', 333 - def _reduce_73( val, _values, result ) - result = ast AST::Name, :value => val[0][:value], :line => val[0][:line] - result +module_eval(<<'.,.,', 'grammar.ra', 326) + def _reduce_73(val, _values, result) + result = ast AST::Name, :value => val[0][:value], :line => val[0][:line] + + result end .,., -module_eval <<'.,.,', 'grammar.ra', 337 - def _reduce_74( val, _values, result ) - result = ast AST::Type, :value => val[0][:value], :line => val[0][:line] - result +module_eval(<<'.,.,', 'grammar.ra', 330) + def _reduce_74(val, _values, result) + result = ast AST::Type, :value => val[0][:value], :line => val[0][:line] + + result end .,., - # reduce 75 omitted +# reduce 75 omitted - # reduce 76 omitted +# reduce 76 omitted - # reduce 77 omitted +# reduce 77 omitted - # reduce 78 omitted +# reduce 78 omitted - # reduce 79 omitted +# reduce 79 omitted - # reduce 80 omitted +# reduce 80 omitted - # reduce 81 omitted +# reduce 81 omitted -module_eval <<'.,.,', 'grammar.ra', 354 - def _reduce_82( val, _values, result ) - if val[0][:value] =~ /::/ - raise Puppet::ParseError, "Cannot assign to variables in other namespaces" - end - # this is distinct from referencing a variable - variable = ast AST::Name, :value => val[0][:value], :line => val[0][:line] - result = ast AST::VarDef, :name => variable, :value => val[2], :line => val[0][:line] - result +module_eval(<<'.,.,', 'grammar.ra', 342) + def _reduce_82(val, _values, result) + raise Puppet::ParseError, "Cannot assign to variables in other namespaces" if val[0][:value] =~ /::/ + # this is distinct from referencing a variable + variable = ast AST::Name, :value => val[0][:value], :line => val[0][:line] + result = ast AST::VarDef, :name => variable, :value => val[2], :line => val[0][:line] + + result end .,., -module_eval <<'.,.,', 'grammar.ra', 357 - def _reduce_83( val, _values, result ) - result = ast AST::VarDef, :name => val[0], :value => val[2] - result +module_eval(<<'.,.,', 'grammar.ra', 348) + def _reduce_83(val, _values, result) + result = ast AST::VarDef, :name => val[0], :value => val[2] + + result end .,., -module_eval <<'.,.,', 'grammar.ra', 362 - def _reduce_84( val, _values, result ) - variable = ast AST::Name, :value => val[0][:value], :line => val[0][:line] - result = ast AST::VarDef, :name => variable, :value => val[2], :append => true, :line => val[0][:line] - result +module_eval(<<'.,.,', 'grammar.ra', 352) + def _reduce_84(val, _values, result) + variable = ast AST::Name, :value => val[0][:value], :line => val[0][:line] + result = ast AST::VarDef, :name => variable, :value => val[2], :append => true, :line => val[0][:line] + + result end .,., -module_eval <<'.,.,', 'grammar.ra', 367 - def _reduce_85( val, _values, result ) - result = ast AST::ASTArray - result +module_eval(<<'.,.,', 'grammar.ra', 358) + def _reduce_85(val, _values, result) + result = ast AST::ASTArray + + result end .,., -module_eval <<'.,.,', 'grammar.ra', 367 - def _reduce_86( val, _values, result ) - result = val[0] - result +module_eval(<<'.,.,', 'grammar.ra', 360) + def _reduce_86(val, _values, result) + result = val[0] + result end .,., -module_eval <<'.,.,', 'grammar.ra', 376 - def _reduce_87( val, _values, result ) - if val[0].instance_of?(AST::ASTArray) - val[0].push(val[2]) - result = val[0] - else - result = ast AST::ASTArray, :children => [val[0],val[2]] - end - result +module_eval(<<'.,.,', 'grammar.ra', 362) + def _reduce_87(val, _values, result) + if val[0].instance_of?(AST::ASTArray) + val[0].push(val[2]) + result = val[0] + else + result = ast AST::ASTArray, :children => [val[0],val[2]] + end + + result end .,., -module_eval <<'.,.,', 'grammar.ra', 380 - def _reduce_88( val, _values, result ) - result = ast AST::ResourceParam, :param => val[0][:value], :line => val[0][:line], :value => val[2] - result +module_eval(<<'.,.,', 'grammar.ra', 371) + def _reduce_88(val, _values, result) + result = ast AST::ResourceParam, :param => val[0][:value], :line => val[0][:line], :value => val[2] + + result end .,., -module_eval <<'.,.,', 'grammar.ra', 385 - def _reduce_89( val, _values, result ) - result = ast AST::ResourceParam, :param => val[0][:value], :line => val[0][:line], :value => val[2], - :add => true - result +module_eval(<<'.,.,', 'grammar.ra', 375) + def _reduce_89(val, _values, result) + result = ast AST::ResourceParam, :param => val[0][:value], :line => val[0][:line], :value => val[2], + :add => true + + result end .,., - # reduce 90 omitted +# reduce 90 omitted + +# reduce 91 omitted - # reduce 91 omitted +module_eval(<<'.,.,', 'grammar.ra', 384) + def _reduce_92(val, _values, result) + result = ast AST::ASTArray -module_eval <<'.,.,', 'grammar.ra', 393 - def _reduce_92( val, _values, result ) - result = ast AST::ASTArray - result + result end .,., -module_eval <<'.,.,', 'grammar.ra', 393 - def _reduce_93( val, _values, result ) - result = val[0] - result +module_eval(<<'.,.,', 'grammar.ra', 386) + def _reduce_93(val, _values, result) + result = val[0] + result end .,., -module_eval <<'.,.,', 'grammar.ra', 402 - def _reduce_94( val, _values, result ) - if val[0].instance_of?(AST::ASTArray) - val[0].push(val[2]) - result = val[0] - else - result = ast AST::ASTArray, :children => [val[0],val[2]] - end - result +module_eval(<<'.,.,', 'grammar.ra', 388) + def _reduce_94(val, _values, result) + if val[0].instance_of?(AST::ASTArray) + val[0].push(val[2]) + result = val[0] + else + result = ast AST::ASTArray, :children => [val[0],val[2]] + end + + result end .,., - # reduce 95 omitted +# reduce 95 omitted -module_eval <<'.,.,', 'grammar.ra', 411 - def _reduce_96( val, _values, result ) - if val[0].instance_of?(AST::ASTArray) - result = val[0].push(val[2]) - else - result = ast AST::ASTArray, :children => [val[0],val[2]] - end - result +module_eval(<<'.,.,', 'grammar.ra', 398) + def _reduce_96(val, _values, result) + if val[0].instance_of?(AST::ASTArray) + result = val[0].push(val[2]) + else + result = ast AST::ASTArray, :children => [val[0],val[2]] + end + + result end .,., - # reduce 97 omitted +# reduce 97 omitted - # reduce 98 omitted +# reduce 98 omitted - # reduce 99 omitted +# reduce 99 omitted - # reduce 100 omitted +# reduce 100 omitted - # reduce 101 omitted +# reduce 101 omitted - # reduce 102 omitted +# reduce 102 omitted - # reduce 103 omitted +# reduce 103 omitted - # reduce 104 omitted +# reduce 104 omitted - # reduce 105 omitted +# reduce 105 omitted - # reduce 106 omitted +# reduce 106 omitted - # reduce 107 omitted +# reduce 107 omitted - # reduce 108 omitted +# reduce 108 omitted - # reduce 109 omitted +# reduce 109 omitted - # reduce 110 omitted +# reduce 110 omitted - # reduce 111 omitted +# reduce 111 omitted - # reduce 112 omitted +# reduce 112 omitted - # reduce 113 omitted +# reduce 113 omitted - # reduce 114 omitted +# reduce 114 omitted -module_eval <<'.,.,', 'grammar.ra', 440 - def _reduce_115( val, _values, result ) - args = aryfy(val[2]) - result = ast AST::Function, - :name => val[0][:value], :line => val[0][:line], - :arguments => args, - :ftype => :rvalue - result +module_eval(<<'.,.,', 'grammar.ra', 427) + def _reduce_115(val, _values, result) + args = aryfy(val[2]) + result = ast AST::Function, + :name => val[0][:value], :line => val[0][:line], + :arguments => args, + :ftype => :rvalue + + result end .,., -module_eval <<'.,.,', 'grammar.ra', 445 - def _reduce_116( val, _values, result ) - result = ast AST::Function, - :name => val[0][:value], :line => val[0][:line], - :arguments => AST::ASTArray.new({}), - :ftype => :rvalue - result +module_eval(<<'.,.,', 'grammar.ra', 433) + def _reduce_116(val, _values, result) + result = ast AST::Function, + :name => val[0][:value], :line => val[0][:line], + :arguments => AST::ASTArray.new({}), + :ftype => :rvalue + + result end .,., -module_eval <<'.,.,', 'grammar.ra', 446 - def _reduce_117( val, _values, result ) - result = ast AST::String, :value => val[0][:value], :line => val[0][:line] - result +module_eval(<<'.,.,', 'grammar.ra', 439) + def _reduce_117(val, _values, result) + result = ast AST::String, :value => val[0][:value], :line => val[0][:line] + result end .,., -module_eval <<'.,.,', 'grammar.ra', 447 - def _reduce_118( val, _values, result ) - result = ast AST::Concat, :value => [ast(AST::String,val[0])]+val[1], :line => val[0][:line] - result +module_eval(<<'.,.,', 'grammar.ra', 440) + def _reduce_118(val, _values, result) + result = ast AST::Concat, :value => [ast(AST::String,val[0])]+val[1], :line => val[0][:line] + result end .,., -module_eval <<'.,.,', 'grammar.ra', 449 - def _reduce_119( val, _values, result ) - result = [val[0]] + val[1] - result +module_eval(<<'.,.,', 'grammar.ra', 442) + def _reduce_119(val, _values, result) + result = [val[0]] + val[1] + result end .,., -module_eval <<'.,.,', 'grammar.ra', 451 - def _reduce_120( val, _values, result ) - result = [ast(AST::String,val[0])] - result +module_eval(<<'.,.,', 'grammar.ra', 444) + def _reduce_120(val, _values, result) + result = [ast(AST::String,val[0])] + result end .,., -module_eval <<'.,.,', 'grammar.ra', 452 - def _reduce_121( val, _values, result ) - result = [ast(AST::String,val[0])] + val[1] - result +module_eval(<<'.,.,', 'grammar.ra', 445) + def _reduce_121(val, _values, result) + result = [ast(AST::String,val[0])] + val[1] + result end .,., -module_eval <<'.,.,', 'grammar.ra', 457 - def _reduce_122( val, _values, result ) - result = ast AST::Boolean, :value => val[0][:value], :line => val[0][:line] - result +module_eval(<<'.,.,', 'grammar.ra', 448) + def _reduce_122(val, _values, result) + result = ast AST::Boolean, :value => val[0][:value], :line => val[0][:line] + + result end .,., -module_eval <<'.,.,', 'grammar.ra', 462 - def _reduce_123( val, _values, result ) - Puppet.warning addcontext("Deprecation notice: Resource references should now be capitalized") - result = ast AST::ResourceReference, :type => val[0][:value], :line => val[0][:line], :title => val[2] - result +module_eval(<<'.,.,', 'grammar.ra', 452) + def _reduce_123(val, _values, result) + Puppet.warning addcontext("Deprecation notice: Resource references should now be capitalized") + result = ast AST::ResourceReference, :type => val[0][:value], :line => val[0][:line], :title => val[2] + + result end .,., -module_eval <<'.,.,', 'grammar.ra', 464 - def _reduce_124( val, _values, result ) - result = ast AST::ResourceReference, :type => val[0], :title => val[2] - result +module_eval(<<'.,.,', 'grammar.ra', 455) + def _reduce_124(val, _values, result) + result = ast AST::ResourceReference, :type => val[0], :title => val[2] + + result end .,., -module_eval <<'.,.,', 'grammar.ra', 468 - def _reduce_125( val, _values, result ) - result = val[1] - result +module_eval(<<'.,.,', 'grammar.ra', 459) + def _reduce_125(val, _values, result) + result = val[1] + + result end .,., -module_eval <<'.,.,', 'grammar.ra', 482 - def _reduce_126( val, _values, result ) - @lexer.commentpop - args = { - :test => val[0], - :statements => val[2] - } +module_eval(<<'.,.,', 'grammar.ra', 463) + def _reduce_126(val, _values, result) + @lexer.commentpop + args = { + :test => val[0], + :statements => val[2] + } - if val[4] - args[:else] = val[4] - end + args[:else] = val[4] if val[4] + + result = ast AST::IfStatement, args - result = ast AST::IfStatement, args - result + result end .,., -module_eval <<'.,.,', 'grammar.ra', 495 - def _reduce_127( val, _values, result ) - @lexer.commentpop +module_eval(<<'.,.,', 'grammar.ra', 474) + def _reduce_127(val, _values, result) + @lexer.commentpop args = { - :test => val[0], - :statements => ast(AST::Nop) - } + :test => val[0], + :statements => ast(AST::Nop) + } - if val[3] - args[:else] = val[3] - end + args[:else] = val[3] if val[3] + + result = ast AST::IfStatement, args - result = ast AST::IfStatement, args - result + result end .,., - # reduce 128 omitted +# reduce 128 omitted + +module_eval(<<'.,.,', 'grammar.ra', 487) + def _reduce_129(val, _values, result) + result = ast AST::Else, :statements => val[1] -module_eval <<'.,.,', 'grammar.ra', 501 - def _reduce_129( val, _values, result ) - #@lexer.commentpop - result = ast AST::Else, :statements => val[1] - result + result end .,., -module_eval <<'.,.,', 'grammar.ra', 505 - def _reduce_130( val, _values, result ) - @lexer.commentpop +module_eval(<<'.,.,', 'grammar.ra', 490) + def _reduce_130(val, _values, result) + @lexer.commentpop result = ast AST::Else, :statements => val[2] - result + + result end .,., -module_eval <<'.,.,', 'grammar.ra', 509 - def _reduce_131( val, _values, result ) - @lexer.commentpop +module_eval(<<'.,.,', 'grammar.ra', 494) + def _reduce_131(val, _values, result) + @lexer.commentpop result = ast AST::Else, :statements => ast(AST::Nop) - result + + result end .,., - # reduce 132 omitted +# reduce 132 omitted -module_eval <<'.,.,', 'grammar.ra', 526 - def _reduce_133( val, _values, result ) - result = ast AST::InOperator, :lval => val[0], :rval => val[2] - result +module_eval(<<'.,.,', 'grammar.ra', 512) + def _reduce_133(val, _values, result) + result = ast AST::InOperator, :lval => val[0], :rval => val[2] + + result end .,., -module_eval <<'.,.,', 'grammar.ra', 529 - def _reduce_134( val, _values, result ) - result = ast AST::MatchOperator, :operator => val[1][:value], :lval => val[0], :rval => val[2] - result +module_eval(<<'.,.,', 'grammar.ra', 515) + def _reduce_134(val, _values, result) + result = ast AST::MatchOperator, :operator => val[1][:value], :lval => val[0], :rval => val[2] + + result end .,., -module_eval <<'.,.,', 'grammar.ra', 532 - def _reduce_135( val, _values, result ) - result = ast AST::MatchOperator, :operator => val[1][:value], :lval => val[0], :rval => val[2] - result +module_eval(<<'.,.,', 'grammar.ra', 518) + def _reduce_135(val, _values, result) + result = ast AST::MatchOperator, :operator => val[1][:value], :lval => val[0], :rval => val[2] + + result end .,., -module_eval <<'.,.,', 'grammar.ra', 535 - def _reduce_136( val, _values, result ) - result = ast AST::ArithmeticOperator, :operator => val[1][:value], :lval => val[0], :rval => val[2] - result +module_eval(<<'.,.,', 'grammar.ra', 521) + def _reduce_136(val, _values, result) + result = ast AST::ArithmeticOperator, :operator => val[1][:value], :lval => val[0], :rval => val[2] + + result end .,., -module_eval <<'.,.,', 'grammar.ra', 538 - def _reduce_137( val, _values, result ) - result = ast AST::ArithmeticOperator, :operator => val[1][:value], :lval => val[0], :rval => val[2] - result +module_eval(<<'.,.,', 'grammar.ra', 524) + def _reduce_137(val, _values, result) + result = ast AST::ArithmeticOperator, :operator => val[1][:value], :lval => val[0], :rval => val[2] + + result end .,., -module_eval <<'.,.,', 'grammar.ra', 541 - def _reduce_138( val, _values, result ) - result = ast AST::ArithmeticOperator, :operator => val[1][:value], :lval => val[0], :rval => val[2] - result +module_eval(<<'.,.,', 'grammar.ra', 527) + def _reduce_138(val, _values, result) + result = ast AST::ArithmeticOperator, :operator => val[1][:value], :lval => val[0], :rval => val[2] + + result end .,., -module_eval <<'.,.,', 'grammar.ra', 544 - def _reduce_139( val, _values, result ) - result = ast AST::ArithmeticOperator, :operator => val[1][:value], :lval => val[0], :rval => val[2] - result +module_eval(<<'.,.,', 'grammar.ra', 530) + def _reduce_139(val, _values, result) + result = ast AST::ArithmeticOperator, :operator => val[1][:value], :lval => val[0], :rval => val[2] + + result end .,., -module_eval <<'.,.,', 'grammar.ra', 547 - def _reduce_140( val, _values, result ) - result = ast AST::ArithmeticOperator, :operator => val[1][:value], :lval => val[0], :rval => val[2] - result +module_eval(<<'.,.,', 'grammar.ra', 533) + def _reduce_140(val, _values, result) + result = ast AST::ArithmeticOperator, :operator => val[1][:value], :lval => val[0], :rval => val[2] + + result end .,., -module_eval <<'.,.,', 'grammar.ra', 550 - def _reduce_141( val, _values, result ) - result = ast AST::ArithmeticOperator, :operator => val[1][:value], :lval => val[0], :rval => val[2] - result +module_eval(<<'.,.,', 'grammar.ra', 536) + def _reduce_141(val, _values, result) + result = ast AST::ArithmeticOperator, :operator => val[1][:value], :lval => val[0], :rval => val[2] + + result end .,., -module_eval <<'.,.,', 'grammar.ra', 553 - def _reduce_142( val, _values, result ) - result = ast AST::Minus, :value => val[1] - result +module_eval(<<'.,.,', 'grammar.ra', 539) + def _reduce_142(val, _values, result) + result = ast AST::Minus, :value => val[1] + + result end .,., -module_eval <<'.,.,', 'grammar.ra', 556 - def _reduce_143( val, _values, result ) - result = ast AST::ComparisonOperator, :operator => val[1][:value], :lval => val[0], :rval => val[2] - result +module_eval(<<'.,.,', 'grammar.ra', 542) + def _reduce_143(val, _values, result) + result = ast AST::ComparisonOperator, :operator => val[1][:value], :lval => val[0], :rval => val[2] + + result end .,., -module_eval <<'.,.,', 'grammar.ra', 559 - def _reduce_144( val, _values, result ) - result = ast AST::ComparisonOperator, :operator => val[1][:value], :lval => val[0], :rval => val[2] - result +module_eval(<<'.,.,', 'grammar.ra', 545) + def _reduce_144(val, _values, result) + result = ast AST::ComparisonOperator, :operator => val[1][:value], :lval => val[0], :rval => val[2] + + result end .,., -module_eval <<'.,.,', 'grammar.ra', 562 - def _reduce_145( val, _values, result ) - result = ast AST::ComparisonOperator, :operator => val[1][:value], :lval => val[0], :rval => val[2] - result +module_eval(<<'.,.,', 'grammar.ra', 548) + def _reduce_145(val, _values, result) + result = ast AST::ComparisonOperator, :operator => val[1][:value], :lval => val[0], :rval => val[2] + + result end .,., -module_eval <<'.,.,', 'grammar.ra', 565 - def _reduce_146( val, _values, result ) - result = ast AST::ComparisonOperator, :operator => val[1][:value], :lval => val[0], :rval => val[2] - result +module_eval(<<'.,.,', 'grammar.ra', 551) + def _reduce_146(val, _values, result) + result = ast AST::ComparisonOperator, :operator => val[1][:value], :lval => val[0], :rval => val[2] + + result end .,., -module_eval <<'.,.,', 'grammar.ra', 568 - def _reduce_147( val, _values, result ) - result = ast AST::ComparisonOperator, :operator => val[1][:value], :lval => val[0], :rval => val[2] - result +module_eval(<<'.,.,', 'grammar.ra', 554) + def _reduce_147(val, _values, result) + result = ast AST::ComparisonOperator, :operator => val[1][:value], :lval => val[0], :rval => val[2] + + result end .,., -module_eval <<'.,.,', 'grammar.ra', 571 - def _reduce_148( val, _values, result ) - result = ast AST::ComparisonOperator, :operator => val[1][:value], :lval => val[0], :rval => val[2] - result +module_eval(<<'.,.,', 'grammar.ra', 557) + def _reduce_148(val, _values, result) + result = ast AST::ComparisonOperator, :operator => val[1][:value], :lval => val[0], :rval => val[2] + + result end .,., -module_eval <<'.,.,', 'grammar.ra', 574 - def _reduce_149( val, _values, result ) - result = ast AST::Not, :value => val[1] - result +module_eval(<<'.,.,', 'grammar.ra', 560) + def _reduce_149(val, _values, result) + result = ast AST::Not, :value => val[1] + + result end .,., -module_eval <<'.,.,', 'grammar.ra', 577 - def _reduce_150( val, _values, result ) - result = ast AST::BooleanOperator, :operator => val[1][:value], :lval => val[0], :rval => val[2] - result +module_eval(<<'.,.,', 'grammar.ra', 563) + def _reduce_150(val, _values, result) + result = ast AST::BooleanOperator, :operator => val[1][:value], :lval => val[0], :rval => val[2] + + result end .,., -module_eval <<'.,.,', 'grammar.ra', 580 - def _reduce_151( val, _values, result ) - result = ast AST::BooleanOperator, :operator => val[1][:value], :lval => val[0], :rval => val[2] - result +module_eval(<<'.,.,', 'grammar.ra', 566) + def _reduce_151(val, _values, result) + result = ast AST::BooleanOperator, :operator => val[1][:value], :lval => val[0], :rval => val[2] + + result end .,., -module_eval <<'.,.,', 'grammar.ra', 583 - def _reduce_152( val, _values, result ) - result = val[1] - result +module_eval(<<'.,.,', 'grammar.ra', 569) + def _reduce_152(val, _values, result) + result = val[1] + + result end .,., -module_eval <<'.,.,', 'grammar.ra', 592 - def _reduce_153( val, _values, result ) - @lexer.commentpop - options = val[3] - unless options.instance_of?(AST::ASTArray) - options = ast AST::ASTArray, :children => [val[3]] - end - result = ast AST::CaseStatement, :test => val[1], :options => options - result +module_eval(<<'.,.,', 'grammar.ra', 573) + def _reduce_153(val, _values, result) + @lexer.commentpop + options = val[3] + options = ast AST::ASTArray, :children => [val[3]] unless options.instance_of?(AST::ASTArray) + result = ast AST::CaseStatement, :test => val[1], :options => options + + result end .,., - # reduce 154 omitted +# reduce 154 omitted -module_eval <<'.,.,', 'grammar.ra', 602 - def _reduce_155( val, _values, result ) - if val[0].instance_of?(AST::ASTArray) - val[0].push val[1] - result = val[0] - else - result = ast AST::ASTArray, :children => [val[0], val[1]] - end - result +module_eval(<<'.,.,', 'grammar.ra', 581) + def _reduce_155(val, _values, result) + if val[0].instance_of?(AST::ASTArray) + val[0].push val[1] + result = val[0] + else + result = ast AST::ASTArray, :children => [val[0], val[1]] + end + + result end .,., -module_eval <<'.,.,', 'grammar.ra', 607 - def _reduce_156( val, _values, result ) - @lexer.commentpop - result = ast AST::CaseOpt, :value => val[0], :statements => val[3] - result +module_eval(<<'.,.,', 'grammar.ra', 590) + def _reduce_156(val, _values, result) + @lexer.commentpop + result = ast AST::CaseOpt, :value => val[0], :statements => val[3] + + result end .,., -module_eval <<'.,.,', 'grammar.ra', 613 - def _reduce_157( val, _values, result ) - @lexer.commentpop - result = ast(AST::CaseOpt, - :value => val[0], - :statements => ast(AST::ASTArray) - ) - result +module_eval(<<'.,.,', 'grammar.ra', 593) + def _reduce_157(val, _values, result) + @lexer.commentpop + + result = ast( + AST::CaseOpt, + :value => val[0], + + :statements => ast(AST::ASTArray) + ) + + result end .,., - # reduce 158 omitted +# reduce 158 omitted -module_eval <<'.,.,', 'grammar.ra', 623 - def _reduce_159( val, _values, result ) - if val[0].instance_of?(AST::ASTArray) - val[0].push(val[2]) - result = val[0] - else - result = ast AST::ASTArray, :children => [val[0],val[2]] - end - result +module_eval(<<'.,.,', 'grammar.ra', 605) + def _reduce_159(val, _values, result) + if val[0].instance_of?(AST::ASTArray) + val[0].push(val[2]) + result = val[0] + else + result = ast AST::ASTArray, :children => [val[0],val[2]] + end + + result end .,., -module_eval <<'.,.,', 'grammar.ra', 627 - def _reduce_160( val, _values, result ) - result = ast AST::Selector, :param => val[0], :values => val[2] - result +module_eval(<<'.,.,', 'grammar.ra', 614) + def _reduce_160(val, _values, result) + result = ast AST::Selector, :param => val[0], :values => val[2] + + result end .,., - # reduce 161 omitted +# reduce 161 omitted -module_eval <<'.,.,', 'grammar.ra', 633 - def _reduce_162( val, _values, result ) - @lexer.commentpop +module_eval(<<'.,.,', 'grammar.ra', 619) + def _reduce_162(val, _values, result) + @lexer.commentpop result = val[1] - result + + result end .,., - # reduce 163 omitted +# reduce 163 omitted -module_eval <<'.,.,', 'grammar.ra', 643 - def _reduce_164( val, _values, result ) - if val[0].instance_of?(AST::ASTArray) - val[0].push(val[2]) - result = val[0] - else - result = ast AST::ASTArray, :children => [val[0],val[2]] - end - result +module_eval(<<'.,.,', 'grammar.ra', 625) + def _reduce_164(val, _values, result) + if val[0].instance_of?(AST::ASTArray) + val[0].push(val[2]) + result = val[0] + else + result = ast AST::ASTArray, :children => [val[0],val[2]] + end + + result end .,., -module_eval <<'.,.,', 'grammar.ra', 647 - def _reduce_165( val, _values, result ) - result = ast AST::ResourceParam, :param => val[0], :value => val[2] - result +module_eval(<<'.,.,', 'grammar.ra', 634) + def _reduce_165(val, _values, result) + result = ast AST::ResourceParam, :param => val[0], :value => val[2] + + result end .,., - # reduce 166 omitted +# reduce 166 omitted + +# reduce 167 omitted - # reduce 167 omitted +# reduce 168 omitted - # reduce 168 omitted +# reduce 169 omitted - # reduce 169 omitted +# reduce 170 omitted - # reduce 170 omitted +# reduce 171 omitted - # reduce 171 omitted +# reduce 172 omitted - # reduce 172 omitted +# reduce 173 omitted -module_eval <<'.,.,', 'grammar.ra', 658 - def _reduce_173( val, _values, result ) - result = ast AST::Default, :value => val[0][:value], :line => val[0][:line] - result +module_eval(<<'.,.,', 'grammar.ra', 646) + def _reduce_174(val, _values, result) + result = ast AST::Default, :value => val[0][:value], :line => val[0][:line] + + result end .,., - # reduce 174 omitted +# reduce 175 omitted -module_eval <<'.,.,', 'grammar.ra', 661 - def _reduce_175( val, _values, result ) - result = [val[0][:value]] - result +module_eval(<<'.,.,', 'grammar.ra', 651) + def _reduce_176(val, _values, result) + result = [val[0][:value]] + result end .,., - # reduce 176 omitted +# reduce 177 omitted -module_eval <<'.,.,', 'grammar.ra', 663 - def _reduce_177( val, _values, result ) - result = val[0] += val[2] - result +module_eval(<<'.,.,', 'grammar.ra', 653) + def _reduce_178(val, _values, result) + result = val[0] += val[2] + result end .,., -module_eval <<'.,.,', 'grammar.ra', 672 - def _reduce_178( val, _values, result ) - val[1].each do |file| - import(file) - end +module_eval(<<'.,.,', 'grammar.ra', 656) + def _reduce_179(val, _values, result) + val[1].each do |file| + import(file) + end + + result = AST::ASTArray.new(:children => []) - result = AST::ASTArray.new(:children => []) - result + result end .,., -module_eval <<'.,.,', 'grammar.ra', 683 - def _reduce_179( val, _values, result ) - @lexer.commentpop - newdefine classname(val[1]), :arguments => val[2], :code => val[4], :line => val[0][:line] - @lexer.indefine = false - result = nil +module_eval(<<'.,.,', 'grammar.ra', 666) + def _reduce_180(val, _values, result) + @lexer.commentpop + newdefine classname(val[1]), :arguments => val[2], :code => val[4], :line => val[0][:line] + @lexer.indefine = false + result = nil #} | DEFINE NAME argumentlist parent LBRACE RBRACE { - result + + result end .,., -module_eval <<'.,.,', 'grammar.ra', 688 - def _reduce_180( val, _values, result ) - @lexer.commentpop - newdefine classname(val[1]), :arguments => val[2], :line => val[0][:line] - @lexer.indefine = false - result = nil - result +module_eval(<<'.,.,', 'grammar.ra', 673) + def _reduce_181(val, _values, result) + @lexer.commentpop + newdefine classname(val[1]), :arguments => val[2], :line => val[0][:line] + @lexer.indefine = false + result = nil + + result end .,., -module_eval <<'.,.,', 'grammar.ra', 697 - def _reduce_181( val, _values, result ) - @lexer.commentpop - # Our class gets defined in the parent namespace, not our own. - @lexer.namepop - newclass classname(val[1]), :arguments => val[2], :parent => val[3], :code => val[5], :line => val[0][:line] - result = nil - result +module_eval(<<'.,.,', 'grammar.ra', 681) + def _reduce_182(val, _values, result) + @lexer.commentpop + # Our class gets defined in the parent namespace, not our own. + @lexer.namepop + newclass classname(val[1]), :arguments => val[2], :parent => val[3], :code => val[5], :line => val[0][:line] + result = nil + + result end .,., -module_eval <<'.,.,', 'grammar.ra', 703 - def _reduce_182( val, _values, result ) - @lexer.commentpop - # Our class gets defined in the parent namespace, not our own. - @lexer.namepop - newclass classname(val[1]), :arguments => val[2], :parent => val[3], :line => val[0][:line] - result = nil - result +module_eval(<<'.,.,', 'grammar.ra', 687) + def _reduce_183(val, _values, result) + @lexer.commentpop + # Our class gets defined in the parent namespace, not our own. + @lexer.namepop + newclass classname(val[1]), :arguments => val[2], :parent => val[3], :line => val[0][:line] + result = nil + + result end .,., -module_eval <<'.,.,', 'grammar.ra', 709 - def _reduce_183( val, _values, result ) - @lexer.commentpop - newnode val[1], :parent => val[2], :code => val[4], :line => val[0][:line] - result = nil - result +module_eval(<<'.,.,', 'grammar.ra', 695) + def _reduce_184(val, _values, result) + @lexer.commentpop + newnode val[1], :parent => val[2], :code => val[4], :line => val[0][:line] + result = nil + + result end .,., -module_eval <<'.,.,', 'grammar.ra', 713 - def _reduce_184( val, _values, result ) - @lexer.commentpop - newnode val[1], :parent => val[2], :line => val[0][:line] - result = nil - result +module_eval(<<'.,.,', 'grammar.ra', 699) + def _reduce_185(val, _values, result) + @lexer.commentpop + newnode val[1], :parent => val[2], :line => val[0][:line] + result = nil + + result end .,., -module_eval <<'.,.,', 'grammar.ra', 714 - def _reduce_185( val, _values, result ) - result = val[0][:value] - result +module_eval(<<'.,.,', 'grammar.ra', 704) + def _reduce_186(val, _values, result) + result = val[0][:value] + result end .,., -module_eval <<'.,.,', 'grammar.ra', 716 - def _reduce_186( val, _values, result ) - result = val[0][:value] - result +module_eval(<<'.,.,', 'grammar.ra', 706) + def _reduce_187(val, _values, result) + result = val[0][:value] + result end .,., -module_eval <<'.,.,', 'grammar.ra', 717 - def _reduce_187( val, _values, result ) - result = val[0][:value] - result +module_eval(<<'.,.,', 'grammar.ra', 707) + def _reduce_188(val, _values, result) + result = val[0][:value] + result end .,., -module_eval <<'.,.,', 'grammar.ra', 718 - def _reduce_188( val, _values, result ) - result = "class" - result +module_eval(<<'.,.,', 'grammar.ra', 708) + def _reduce_189(val, _values, result) + result = "class" + result end .,., - # reduce 189 omitted +# reduce 190 omitted -module_eval <<'.,.,', 'grammar.ra', 728 - def _reduce_190( val, _values, result ) - result = val[0] +module_eval(<<'.,.,', 'grammar.ra', 714) + def _reduce_191(val, _values, result) + result = val[0] result = [result] unless result.is_a?(Array) result << val[2] - result + + result end .,., -module_eval <<'.,.,', 'grammar.ra', 732 - def _reduce_191( val, _values, result ) - result = ast AST::HostName, :value => val[0] - result +module_eval(<<'.,.,', 'grammar.ra', 720) + def _reduce_192(val, _values, result) + result = ast AST::HostName, :value => val[0] + + result end .,., -module_eval <<'.,.,', 'grammar.ra', 733 - def _reduce_192( val, _values, result ) - result = val[0][:value] - result +module_eval(<<'.,.,', 'grammar.ra', 723) + def _reduce_193(val, _values, result) + result = val[0][:value] + result end .,., -module_eval <<'.,.,', 'grammar.ra', 734 - def _reduce_193( val, _values, result ) - result = val[0][:value] - result +module_eval(<<'.,.,', 'grammar.ra', 724) + def _reduce_194(val, _values, result) + result = val[0][:value] + result end .,., -module_eval <<'.,.,', 'grammar.ra', 735 - def _reduce_194( val, _values, result ) - result = val[0][:value] - result +module_eval(<<'.,.,', 'grammar.ra', 725) + def _reduce_195(val, _values, result) + result = val[0][:value] + result end .,., - # reduce 195 omitted +# reduce 196 omitted -module_eval <<'.,.,', 'grammar.ra', 741 - def _reduce_196( val, _values, result ) - result = nil - result +module_eval(<<'.,.,', 'grammar.ra', 729) + def _reduce_197(val, _values, result) + result = nil + + result end .,., -module_eval <<'.,.,', 'grammar.ra', 745 - def _reduce_197( val, _values, result ) - result = ast AST::ASTArray, :children => [] - result +module_eval(<<'.,.,', 'grammar.ra', 733) + def _reduce_198(val, _values, result) + result = ast AST::ASTArray, :children => [] + + result end .,., - # reduce 198 omitted +# reduce 199 omitted -module_eval <<'.,.,', 'grammar.ra', 750 - def _reduce_199( val, _values, result ) - result = nil - result +module_eval(<<'.,.,', 'grammar.ra', 738) + def _reduce_200(val, _values, result) + result = nil + + result end .,., -module_eval <<'.,.,', 'grammar.ra', 754 - def _reduce_200( val, _values, result ) - result = val[1] +module_eval(<<'.,.,', 'grammar.ra', 741) + def _reduce_201(val, _values, result) + result = val[1] result = [result] unless result[0].is_a?(Array) - result + + result end .,., - # reduce 201 omitted +# reduce 202 omitted -module_eval <<'.,.,', 'grammar.ra', 761 - def _reduce_202( val, _values, result ) - result = val[0] +module_eval(<<'.,.,', 'grammar.ra', 747) + def _reduce_203(val, _values, result) + result = val[0] result = [result] unless result[0].is_a?(Array) result << val[2] - result + + result end .,., -module_eval <<'.,.,', 'grammar.ra', 766 - def _reduce_203( val, _values, result ) - Puppet.warning addcontext("Deprecation notice: must now include '$' in prototype") - result = [val[0][:value], val[2]] - result +module_eval(<<'.,.,', 'grammar.ra', 753) + def _reduce_204(val, _values, result) + Puppet.warning addcontext("Deprecation notice: must now include '$' in prototype") + result = [val[0][:value], val[2]] + + result end .,., -module_eval <<'.,.,', 'grammar.ra', 770 - def _reduce_204( val, _values, result ) - Puppet.warning addcontext("Deprecation notice: must now include '$' in prototype") +module_eval(<<'.,.,', 'grammar.ra', 757) + def _reduce_205(val, _values, result) + Puppet.warning addcontext("Deprecation notice: must now include '$' in prototype") result = [val[0][:value]] - result + + result end .,., -module_eval <<'.,.,', 'grammar.ra', 772 - def _reduce_205( val, _values, result ) - result = [val[0][:value], val[2]] - result +module_eval(<<'.,.,', 'grammar.ra', 760) + def _reduce_206(val, _values, result) + result = [val[0][:value], val[2]] + + result end .,., -module_eval <<'.,.,', 'grammar.ra', 774 - def _reduce_206( val, _values, result ) - result = [val[0][:value]] - result +module_eval(<<'.,.,', 'grammar.ra', 762) + def _reduce_207(val, _values, result) + result = [val[0][:value]] + + result end .,., - # reduce 207 omitted +# reduce 208 omitted -module_eval <<'.,.,', 'grammar.ra', 779 - def _reduce_208( val, _values, result ) - result = val[1] - result +module_eval(<<'.,.,', 'grammar.ra', 767) + def _reduce_209(val, _values, result) + result = val[1] + + result end .,., - # reduce 209 omitted +# reduce 210 omitted -module_eval <<'.,.,', 'grammar.ra', 784 - def _reduce_210( val, _values, result ) - result = val[1] - result +module_eval(<<'.,.,', 'grammar.ra', 772) + def _reduce_211(val, _values, result) + result = val[1] + + result end .,., - # reduce 211 omitted +# reduce 212 omitted - # reduce 212 omitted +# reduce 213 omitted -module_eval <<'.,.,', 'grammar.ra', 790 - def _reduce_213( val, _values, result ) - result = ast AST::Variable, :value => val[0][:value], :line => val[0][:line] - result +module_eval(<<'.,.,', 'grammar.ra', 778) + def _reduce_214(val, _values, result) + result = ast AST::Variable, :value => val[0][:value], :line => val[0][:line] + + result end .,., -module_eval <<'.,.,', 'grammar.ra', 798 - def _reduce_214( val, _values, result ) - if val[1].instance_of?(AST::ASTArray) - result = val[1] - else - result = ast AST::ASTArray, :children => [val[1]] - end - result +module_eval(<<'.,.,', 'grammar.ra', 782) + def _reduce_215(val, _values, result) + if val[1].instance_of?(AST::ASTArray) + result = val[1] + else + result = ast AST::ASTArray, :children => [val[1]] + end + + result end .,., -module_eval <<'.,.,', 'grammar.ra', 805 - def _reduce_215( val, _values, result ) - if val[1].instance_of?(AST::ASTArray) - result = val[1] - else - result = ast AST::ASTArray, :children => [val[1]] - end - result +module_eval(<<'.,.,', 'grammar.ra', 789) + def _reduce_216(val, _values, result) + if val[1].instance_of?(AST::ASTArray) + result = val[1] + else + result = ast AST::ASTArray, :children => [val[1]] + end + + result end .,., -module_eval <<'.,.,', 'grammar.ra', 807 - def _reduce_216( val, _values, result ) - result = ast AST::ASTArray - result +module_eval(<<'.,.,', 'grammar.ra', 795) + def _reduce_217(val, _values, result) + result = ast AST::ASTArray + + result end .,., - # reduce 217 omitted +# reduce 218 omitted - # reduce 218 omitted +# reduce 219 omitted - # reduce 219 omitted +# reduce 220 omitted -module_eval <<'.,.,', 'grammar.ra', 812 - def _reduce_220( val, _values, result ) - result = nil - result +module_eval(<<'.,.,', 'grammar.ra', 802) + def _reduce_221(val, _values, result) + result = nil + result end .,., -module_eval <<'.,.,', 'grammar.ra', 817 - def _reduce_221( val, _values, result ) - result = ast AST::Regex, :value => val[0][:value] - result +module_eval(<<'.,.,', 'grammar.ra', 805) + def _reduce_222(val, _values, result) + result = ast AST::Regex, :value => val[0][:value] + + result end .,., -module_eval <<'.,.,', 'grammar.ra', 825 - def _reduce_222( val, _values, result ) - if val[1].instance_of?(AST::ASTHash) - result = val[1] - else - result = ast AST::ASTHash, { :value => val[1] } - end - result +module_eval(<<'.,.,', 'grammar.ra', 809) + def _reduce_223(val, _values, result) + if val[1].instance_of?(AST::ASTHash) + result = val[1] + else + result = ast AST::ASTHash, { :value => val[1] } end -.,., -module_eval <<'.,.,', 'grammar.ra', 832 - def _reduce_223( val, _values, result ) - if val[1].instance_of?(AST::ASTHash) - result = val[1] - else - result = ast AST::ASTHash, { :value => val[1] } - end - result + result end .,., -module_eval <<'.,.,', 'grammar.ra', 834 - def _reduce_224( val, _values, result ) - result = ast AST::ASTHash - result +module_eval(<<'.,.,', 'grammar.ra', 816) + def _reduce_224(val, _values, result) + if val[1].instance_of?(AST::ASTHash) + result = val[1] + else + result = ast AST::ASTHash, { :value => val[1] } + end + + result end .,., - # reduce 225 omitted +module_eval(<<'.,.,', 'grammar.ra', 822) + def _reduce_225(val, _values, result) + result = ast AST::ASTHash -module_eval <<'.,.,', 'grammar.ra', 844 - def _reduce_226( val, _values, result ) - if val[0].instance_of?(AST::ASTHash) - result = val[0].merge(val[2]) - else - result = ast AST::ASTHash, :value => val[0] - result.merge(val[2]) - end - result + result end .,., -module_eval <<'.,.,', 'grammar.ra', 848 - def _reduce_227( val, _values, result ) - result = ast AST::ASTHash, { :value => { val[0] => val[2] } } - result +# reduce 226 omitted + +module_eval(<<'.,.,', 'grammar.ra', 827) + def _reduce_227(val, _values, result) + if val[0].instance_of?(AST::ASTHash) + result = val[0].merge(val[2]) + else + result = ast AST::ASTHash, :value => val[0] + result.merge(val[2]) + end + + result end .,., -module_eval <<'.,.,', 'grammar.ra', 849 - def _reduce_228( val, _values, result ) - result = val[0][:value] - result +module_eval(<<'.,.,', 'grammar.ra', 836) + def _reduce_228(val, _values, result) + result = ast AST::ASTHash, { :value => { val[0] => val[2] } } + + result end .,., -module_eval <<'.,.,', 'grammar.ra', 850 - def _reduce_229( val, _values, result ) - result = val[0] - result +module_eval(<<'.,.,', 'grammar.ra', 839) + def _reduce_229(val, _values, result) + result = val[0][:value] + result end .,., -module_eval <<'.,.,', 'grammar.ra', 855 - def _reduce_230( val, _values, result ) - result = ast AST::HashOrArrayAccess, :variable => val[0][:value], :key => val[2] - result +module_eval(<<'.,.,', 'grammar.ra', 840) + def _reduce_230(val, _values, result) + result = val[0] + result end .,., - # reduce 231 omitted +module_eval(<<'.,.,', 'grammar.ra', 843) + def _reduce_231(val, _values, result) + result = ast AST::HashOrArrayAccess, :variable => val[0][:value], :key => val[2] -module_eval <<'.,.,', 'grammar.ra', 860 - def _reduce_232( val, _values, result ) - result = ast AST::HashOrArrayAccess, :variable => val[0], :key => val[2] - result + result end .,., - def _reduce_none( val, _values, result ) - result - end +# reduce 232 omitted - end # class Parser +module_eval(<<'.,.,', 'grammar.ra', 848) + def _reduce_233(val, _values, result) + result = ast AST::HashOrArrayAccess, :variable => val[0], :key => val[2] - end # module Parser + result + end +.,., + +def _reduce_none(val, _values, result) + val[0] +end -end # module Puppet + end # class Parser + end # module Parser + end # module Puppet diff --git a/lib/puppet/parser/parser_support.rb b/lib/puppet/parser/parser_support.rb index 7bbebb124..7a0aa2601 100644 --- a/lib/puppet/parser/parser_support.rb +++ b/lib/puppet/parser/parser_support.rb @@ -1,237 +1,237 @@ # I pulled this into a separate file, because I got # tired of rebuilding the parser.rb file all the time. class Puppet::Parser::Parser require 'puppet/parser/functions' require 'puppet/parser/files' require 'puppet/resource/type_collection' require 'puppet/resource/type_collection_helper' require 'puppet/resource/type' require 'monitor' AST = Puppet::Parser::AST include Puppet::Resource::TypeCollectionHelper attr_reader :version, :environment attr_accessor :files attr_accessor :lexer # Add context to a message; useful for error messages and such. def addcontext(message, obj = nil) obj ||= @lexer message += " on line #{obj.line}" if file = obj.file message += " in file #{file}" end message end # Create an AST array out of all of the args def aryfy(*args) if args[0].instance_of?(AST::ASTArray) result = args.shift args.each { |arg| result.push arg } else result = ast AST::ASTArray, :children => args end result end # Create an AST object, and automatically add the file and line information if # available. def ast(klass, hash = {}) klass.new ast_context(klass.use_docs, hash[:line]).merge(hash) end def ast_context(include_docs = false, ast_line = nil) result = { :line => ast_line || lexer.line, :file => lexer.file } result[:doc] = lexer.getcomment(result[:line]) if include_docs result end # The fully qualifed name, with the full namespace. def classname(name) [@lexer.namespace, name].join("::").sub(/^::/, '') end def clear initvars end # Raise a Parse error. def error(message) if brace = @lexer.expected message += "; expected '%s'" end except = Puppet::ParseError.new(message) except.line = @lexer.line except.file = @lexer.file if @lexer.file raise except end def file @lexer.file end def file=(file) unless FileTest.exist?(file) unless file =~ /\.pp$/ file = file + ".pp" end raise Puppet::Error, "Could not find file #{file}" unless FileTest.exist?(file) end raise Puppet::AlreadyImportedError, "Import loop detected" if known_resource_types.watching_file?(file) watch_file(file) @lexer.file = file end [:hostclass, :definition, :node, :nodes?].each do |method| define_method(method) do |*args| known_resource_types.send(method, *args) end end def find_hostclass(namespace, name) known_resource_types.find_or_load(namespace, name, :hostclass) end def find_definition(namespace, name) known_resource_types.find_or_load(namespace, name, :definition) end def import(file) known_resource_types.loader.import(file, @lexer.file) end def initialize(env) # The environment is needed to know how to find the resource type collection. @environment = env.is_a?(String) ? Puppet::Node::Environment.new(env) : env initvars end # Initialize or reset all of our variables. def initvars @lexer = Puppet::Parser::Lexer.new end # Split an fq name into a namespace and name def namesplit(fullname) ary = fullname.split("::") n = ary.pop || "" ns = ary.join("::") return ns, n end # Create a new class, or merge with an existing class. def newclass(name, options = {}) - known_resource_types.add Puppet::Resource::Type.new(:hostclass, name, ast_context(true).merge(options)) + known_resource_types.add Puppet::Resource::Type.new(:hostclass, name, ast_context(true, options[:line]).merge(options)) end # Create a new definition. def newdefine(name, options = {}) - known_resource_types.add Puppet::Resource::Type.new(:definition, name, ast_context(true).merge(options)) + known_resource_types.add Puppet::Resource::Type.new(:definition, name, ast_context(true, options[:line]).merge(options)) end # Create a new node. Nodes are special, because they're stored in a global # table, not according to namespaces. def newnode(names, options = {}) names = [names] unless names.instance_of?(Array) - context = ast_context(true) + context = ast_context(true, options[:line]) names.collect do |name| known_resource_types.add(Puppet::Resource::Type.new(:node, name, context.merge(options))) end end def on_error(token,value,stack) if token == 0 # denotes end of file value = 'end of file' else value = "'#{value[:value]}'" end error = "Syntax error at #{value}" if brace = @lexer.expected error += "; expected '#{brace}'" end except = Puppet::ParseError.new(error) except.line = @lexer.line except.file = @lexer.file if @lexer.file raise except end # how should I do error handling here? def parse(string = nil) return parse_ruby_file if self.file =~ /\.rb$/ self.string = string if string begin @yydebug = false main = yyparse(@lexer,:scan) rescue Racc::ParseError => except error = Puppet::ParseError.new(except) error.line = @lexer.line error.file = @lexer.file error.set_backtrace except.backtrace raise error rescue Puppet::ParseError => except except.line ||= @lexer.line except.file ||= @lexer.file raise except rescue Puppet::Error => except # and this is a framework error except.line ||= @lexer.line except.file ||= @lexer.file raise except rescue Puppet::DevError => except except.line ||= @lexer.line except.file ||= @lexer.file raise except rescue => except error = Puppet::DevError.new(except.message) error.line = @lexer.line error.file = @lexer.file error.set_backtrace except.backtrace raise error end if main # Store the results as the top-level class. newclass("", :code => main) end return known_resource_types ensure @lexer.clear end def parse_ruby_file # Execute the contents of the file inside its own "main" object so # that it can call methods in the resource type API. Puppet::DSL::ResourceTypeAPI.new.instance_eval(File.read(self.file)) end def string=(string) @lexer.string = string end def version known_resource_types.version end # Add a new file to be checked when we're checking to see if we should be # reparsed. This is basically only used by the TemplateWrapper to let the # parser know about templates that should be parsed. def watch_file(filename) known_resource_types.watch_file(filename) end end diff --git a/lib/puppet/provider/computer/computer.rb b/lib/puppet/provider/computer/computer.rb index a6be6bdfe..dd055beb3 100644 --- a/lib/puppet/provider/computer/computer.rb +++ b/lib/puppet/provider/computer/computer.rb @@ -1,22 +1,20 @@ require 'puppet/provider/nameservice/directoryservice' Puppet::Type.type(:computer).provide :directoryservice, :parent => Puppet::Provider::NameService::DirectoryService do desc "Computer object management using DirectoryService on OS X. Note that these are distinctly different kinds of objects to 'hosts', as they require a MAC address and can have all sorts of policy attached to them. This provider only manages Computer objects in the local directory service domain, not in remote directories. If you wish to manage /etc/hosts on Mac OS X, then simply use the host - type as per other platforms. - - " + type as per other platforms." confine :operatingsystem => :darwin defaultfor :operatingsystem => :darwin # hurray for abstraction. The nameservice directoryservice provider can # handle everything we need. super. end diff --git a/lib/puppet/provider/mount.rb b/lib/puppet/provider/mount.rb index 354ddb16d..65296eed2 100644 --- a/lib/puppet/provider/mount.rb +++ b/lib/puppet/provider/mount.rb @@ -1,53 +1,49 @@ # Created by Luke Kanies on 2006-11-12. # Copyright (c) 2006. All rights reserved. require 'puppet' # A module just to store the mount/unmount methods. Individual providers # still need to add the mount commands manually. module Puppet::Provider::Mount # This only works when the mount point is synced to the fstab. def mount # Manually pass the mount options in, since some OSes *cough*OS X*cough* don't # read from /etc/fstab but still want to use this type. args = [] args << "-o" << self.options if self.options and self.options != :absent args << resource[:name] - flush if respond_to?(:flush) mountcmd(*args) + case get(:ensure) + when :absent; set(:ensure => :ghost) + when :unmounted; set(:ensure => :mounted) + end end def remount info "Remounting" if resource[:remounts] == :true mountcmd "-o", "remount", resource[:name] else unmount mount end end # This only works when the mount point is synced to the fstab. def unmount - umount resource[:name] + umount(resource[:name]) + + # Update property hash for future queries (e.g. refresh is called) + case get(:ensure) + when :mounted; set(:ensure => :unmounted) + when :ghost; set(:ensure => :absent) + end end # Is the mount currently mounted? def mounted? - platform = Facter.value("operatingsystem") - name = resource[:name] - mounts = mountcmd.split("\n").find do |line| - case platform - when "Darwin" - line =~ / on #{name} / or line =~ %r{ on /private/var/automount#{name}} - when "Solaris", "HP-UX" - line =~ /^#{name} on / - when "AIX" - line.split(/\s+/)[2] == name - else - line =~ / on #{name} / - end - end + [:mounted, :ghost].include?(get(:ensure)) end end diff --git a/lib/puppet/provider/mount/parsed.rb b/lib/puppet/provider/mount/parsed.rb index 82d1628bd..11c5e21a9 100755 --- a/lib/puppet/provider/mount/parsed.rb +++ b/lib/puppet/provider/mount/parsed.rb @@ -1,47 +1,106 @@ require 'puppet/provider/parsedfile' require 'puppet/provider/mount' fstab = nil case Facter.value(:operatingsystem) when "Solaris"; fstab = "/etc/vfstab" else fstab = "/etc/fstab" end - - Puppet::Type.type(:mount).provide( - :parsed, +Puppet::Type.type(:mount).provide( + :parsed, :parent => Puppet::Provider::ParsedFile, :default_target => fstab, - :filetype => :flat ) do include Puppet::Provider::Mount - #confine :exists => fstab commands :mountcmd => "mount", :umount => "umount" - @platform = Facter["operatingsystem"].value - case @platform + case Facter["operatingsystem"] when "Solaris" @fields = [:device, :blockdevice, :name, :fstype, :pass, :atboot, :options] else @fields = [:device, :name, :fstype, :options, :dump, :pass] @fielddefaults = [ nil ] * 4 + [ "0", "2" ] end text_line :comment, :match => /^\s*#/ text_line :blank, :match => /^\s*$/ optional_fields = @fields - [:device, :name, :blockdevice] mandatory_fields = @fields - optional_fields # fstab will ignore lines that have fewer than the mandatory number of columns, # so we should, too. field_pattern = '(\s*(?>\S+))' text_line :incomplete, :match => /^(?!#{field_pattern}{#{mandatory_fields.length}})/ record_line self.name, :fields => @fields, :separator => /\s+/, :joiner => "\t", :optional => optional_fields -end + # Every entry in fstab is :unmounted until we can prove different + def self.prefetch_hook(target_records) + target_records.collect do |record| + record[:ensure] = :unmounted if record[:record_type] == :parsed + record + end + end + + def self.prefetch(resources = nil) + # Get providers for all resources the user defined and that match + # a record in /etc/fstab. + super + # We need to do two things now: + # - Update ensure from :unmounted to :mounted if the resource is mounted + # - Check for mounted devices that are not in fstab and + # set ensure to :ghost (if the user wants to add an entry + # to fstab we need to know if the device was mounted before) + mountinstances.each do |hash| + if mount = resources[hash[:name]] + case mount.provider.get(:ensure) + when :absent # Mount not in fstab + mount.provider.set(:ensure => :ghost) + when :unmounted # Mount in fstab + mount.provider.set(:ensure => :mounted) + end + end + end + end + def self.mountinstances + # XXX: Will not work for mount points that have spaces in path (does fstab support this anyways?) + regex = case Facter.value(:operatingsystem) + when "Darwin" + / on (?:\/private\/var\/automount)?(\S*)/ + when "Solaris", "HP-UX" + /^(\S*) on / + when "AIX" + /^(?:\S*\s+\S+\s+)(\S+)/ + else + / on (\S*)/ + end + instances = [] + mount_output = mountcmd.split("\n") + if mount_output.length >= 2 and mount_output[1] =~ /^[- \t]*$/ + # On some OSes (e.g. AIX) mount output begins with a header line + # followed by a line consisting of dashes and whitespace. + # Discard these two lines. + mount_output[0..1] = [] + end + mount_output.each do |line| + if match = regex.match(line) and name = match.captures.first + instances << {:name => name, :mounted => :yes} # Only :name is important here + else + raise Puppet::Error, "Could not understand line #{line} from mount output" + end + end + instances + end + + def flush + needs_mount = @property_hash.delete(:needs_mount) + super + mount if needs_mount + end +end diff --git a/lib/puppet/provider/parsedfile.rb b/lib/puppet/provider/parsedfile.rb index ffd36e59f..75a215f4b 100755 --- a/lib/puppet/provider/parsedfile.rb +++ b/lib/puppet/provider/parsedfile.rb @@ -1,372 +1,374 @@ require 'puppet' require 'puppet/util/filetype' require 'puppet/util/fileparsing' # This provider can be used as the parent class for a provider that # parses and generates files. Its content must be loaded via the # 'prefetch' method, and the file will be written when 'flush' is called # on the provider instance. At this point, the file is written once # for every provider instance. # # Once the provider prefetches the data, it's the resource's job to copy # that data over to the @is variables. class Puppet::Provider::ParsedFile < Puppet::Provider extend Puppet::Util::FileParsing class << self attr_accessor :default_target, :target end attr_accessor :property_hash def self.clean(hash) newhash = hash.dup [:record_type, :on_disk].each do |p| newhash.delete(p) if newhash.include?(p) end newhash end def self.clear @target_objects.clear @records.clear end def self.filetype @filetype ||= Puppet::Util::FileType.filetype(:flat) end def self.filetype=(type) if type.is_a?(Class) @filetype = type elsif klass = Puppet::Util::FileType.filetype(type) @filetype = klass else raise ArgumentError, "Invalid filetype #{type}" end end # Flush all of the targets for which there are modified records. The only # reason we pass a record here is so that we can add it to the stack if # necessary -- it's passed from the instance calling 'flush'. def self.flush(record) # Make sure this record is on the list to be flushed. unless record[:on_disk] record[:on_disk] = true @records << record # If we've just added the record, then make sure our # target will get flushed. modified(record[:target] || default_target) end return unless defined?(@modified) and ! @modified.empty? flushed = [] @modified.sort { |a,b| a.to_s <=> b.to_s }.uniq.each do |target| Puppet.debug "Flushing #{@resource_type.name} provider target #{target}" flush_target(target) flushed << target end @modified.reject! { |t| flushed.include?(t) } end # Make sure our file is backed up, but only back it up once per transaction. # We cheat and rely on the fact that @records is created on each prefetch. def self.backup_target(target) return nil unless target_object(target).respond_to?(:backup) @backup_stats ||= {} return nil if @backup_stats[target] == @records.object_id target_object(target).backup @backup_stats[target] = @records.object_id end # Flush all of the records relating to a specific target. def self.flush_target(target) backup_target(target) records = target_records(target).reject { |r| r[:ensure] == :absent } target_object(target).write(to_file(records)) 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} # HEADER: by puppet. While it can still be managed manually, it # HEADER: is definitely not recommended.\n} end # Add another type var. def self.initvars @records = [] @target_objects = {} @target = nil # Default to flat files @filetype ||= Puppet::Util::FileType.filetype(:flat) super end # Return a list of all of the records we can find. def self.instances targets.collect do |target| prefetch_target(target) end.flatten.reject { |r| skip_record?(r) }.collect do |record| new(record) end end # Override the default method with a lot more functionality. def self.mk_resource_methods [resource_type.validproperties, resource_type.parameters].flatten.each do |attr| attr = symbolize(attr) define_method(attr) do # if @property_hash.empty? # # Note that this swaps the provider out from under us. # prefetch # if @resource.provider == self # return @property_hash[attr] # else # return @resource.provider.send(attr) # end # end # If it's not a valid field for this record type (which can happen # when different platforms support different fields), then just # return the should value, so the resource shuts up. if @property_hash[attr] or self.class.valid_attr?(self.class.name, attr) @property_hash[attr] || :absent else if defined?(@resource) @resource.should(attr) else nil end end end define_method(attr.to_s + "=") do |val| mark_target_modified @property_hash[attr] = val end end end # Always make the resource methods. def self.resource_type=(resource) super mk_resource_methods end # Mark a target as modified so we know to flush it. This only gets # used within the attr= methods. def self.modified(target) @modified ||= [] @modified << target unless @modified.include?(target) end # Retrieve all of the data from disk. There are three ways to know # which files to retrieve: We might have a list of file objects already # set up, there might be instances of our associated resource and they # will have a path parameter set, and we will have a default path # set. We need to turn those three locations into a list of files, # prefetch each one, and make sure they're associated with each appropriate # resource instance. def self.prefetch(resources = nil) # Reset the record list. @records = prefetch_all_targets(resources) match_providers_with_resources(resources) end def self.match_providers_with_resources(resources) return unless resources matchers = resources.dup @records.each do |record| # Skip things like comments and blank lines next if skip_record?(record) if name = record[:name] and resource = resources[name] 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 def self.prefetch_all_targets(resources) records = [] targets(resources).each do |target| records += prefetch_target(target) end records end # Prefetch an individual target. def self.prefetch_target(target) target_records = retrieve(target).each do |r| r[:on_disk] = true r[:target] = target r[:ensure] = :present end target_records = prefetch_hook(target_records) if respond_to?(:prefetch_hook) raise Puppet::DevError, "Prefetching #{target} for provider #{self.name} returned nil" unless target_records target_records end # Is there an existing record with this name? def self.record?(name) return nil unless @records @records.find { |r| r[:name] == name } end # Retrieve the text for the file. Returns nil in the unlikely # event that it doesn't exist. def self.retrieve(path) # XXX We need to be doing something special here in case of failure. text = target_object(path).read if text.nil? or text == "" # there is no file return [] else # Set the target, for logging. old = @target begin @target = path return self.parse(text) rescue Puppet::Error => detail detail.file = @target raise detail ensure @target = old end end end # Should we skip the record? Basically, we skip text records. # This is only here so subclasses can override it. def self.skip_record?(record) record_type(record[:record_type]).text? end # Initialize the object if necessary. def self.target_object(target) @target_objects[target] ||= filetype.new(target) @target_objects[target] end # Find all of the records for a given target def self.target_records(target) @records.find_all { |r| r[:target] == target } end # Find a list of all of the targets that we should be reading. This is # used to figure out what targets we need to prefetch. def self.targets(resources = nil) targets = [] # First get the default target raise Puppet::DevError, "Parsed Providers must define a default target" unless self.default_target targets << self.default_target # Then get each of the file objects targets += @target_objects.keys # Lastly, check the file from any resource instances if resources resources.each do |name, resource| if value = resource.should(:target) targets << value end end end targets.uniq.compact end def self.to_file(records) text = super header + text end def create @resource.class.validproperties.each do |property| if value = @resource.should(property) @property_hash[property] = value end end mark_target_modified (@resource.class.name.to_s + "_created").intern end def destroy # We use the method here so it marks the target as modified. self.ensure = :absent (@resource.class.name.to_s + "_deleted").intern end def exists? !(@property_hash[:ensure] == :absent or @property_hash[:ensure].nil?) end # Write our data to disk. def flush # Make sure we've got a target and name set. # If the target isn't set, then this is our first modification, so # mark it for flushing. unless @property_hash[:target] @property_hash[:target] = @resource.should(:target) || self.class.default_target self.class.modified(@property_hash[:target]) end - @property_hash[:name] ||= @resource.name + @resource.class.key_attributes.each do |attr| + @property_hash[attr] ||= @resource[attr] + end self.class.flush(@property_hash) #@property_hash = {} end def initialize(record) super # The 'record' could be a resource or a record, depending on how the provider # is initialized. If we got an empty property hash (probably because the resource # is just being initialized), then we want to set up some defualts. @property_hash = self.class.record?(resource[:name]) || {:record_type => self.class.name, :ensure => :absent} if @property_hash.empty? end # Retrieve the current state from disk. def prefetch raise Puppet::DevError, "Somehow got told to prefetch with no resource set" unless @resource self.class.prefetch(@resource[:name] => @resource) end def record_type @property_hash[:record_type] end private # Mark both the resource and provider target as modified. def mark_target_modified if defined?(@resource) and restarget = @resource.should(:target) and restarget != @property_hash[:target] self.class.modified(restarget) end self.class.modified(@property_hash[:target]) if @property_hash[:target] != :absent and @property_hash[:target] end end diff --git a/lib/puppet/provider/service/daemontools.rb b/lib/puppet/provider/service/daemontools.rb index 65abf7728..bbb962a71 100644 --- a/lib/puppet/provider/service/daemontools.rb +++ b/lib/puppet/provider/service/daemontools.rb @@ -1,194 +1,194 @@ # Daemontools service management # # author Brice Figureau Puppet::Type.type(:service).provide :daemontools, :parent => :base do desc "Daemontools service management. This provider manages daemons running supervised by D.J.Bernstein daemontools. It tries to detect the service directory, with by order of preference: * /service * /etc/service * /var/lib/svscan The daemon directory should be placed in a directory that can be by default in: * /var/lib/service * /etc or this can be overriden in the service resource parameters:: - service { - \"myservice\": - provider => \"daemontools\", path => \"/path/to/daemons\"; - } + service { \"myservice\": + provider => \"daemontools\", + path => \"/path/to/daemons\", + } This provider supports out of the box: * start/stop (mapped to enable/disable) * enable/disable * restart * status - If a service has ensure => \"running\", it will link /path/to/daemon to + If a service has `ensure => \"running\"`, it will link /path/to/daemon to /path/to/service, which will automatically enable the service. - If a service has ensure => \"stopped\", it will only down the service, not + If a service has `ensure => \"stopped\"`, it will only down the service, not remove the /path/to/service link. " commands :svc => "/usr/bin/svc", :svstat => "/usr/bin/svstat" class << self attr_writer :defpath # Determine the daemon path. def defpath(dummy_argument=:work_arround_for_ruby_GC_bug) unless @defpath ["/var/lib/service", "/etc"].each do |path| if FileTest.exist?(path) @defpath = path break end end raise "Could not find the daemon directory (tested [/var/lib/service,/etc])" unless @defpath end @defpath end end attr_writer :servicedir # returns all providers for all existing services in @defpath # ie enabled or not def self.instances path = self.defpath unless FileTest.directory?(path) Puppet.notice "Service path #{path} does not exist" next end # reject entries that aren't either a directory # or don't contain a run file Dir.entries(path).reject { |e| fullpath = File.join(path, e) e =~ /^\./ or ! FileTest.directory?(fullpath) or ! FileTest.exist?(File.join(fullpath,"run")) }.collect do |name| new(:name => name, :path => path) end end # returns the daemon dir on this node def self.daemondir self.defpath end # find the service dir on this node def servicedir unless @servicedir ["/service", "/etc/service","/var/lib/svscan"].each do |path| if FileTest.exist?(path) @servicedir = path break end end raise "Could not find service directory" unless @servicedir end @servicedir end # returns the full path of this service when enabled # (ie in the service directory) def service File.join(self.servicedir, resource[:name]) end # returns the full path to the current daemon directory # note that this path can be overriden in the resource # definition def daemon File.join(resource[:path], resource[:name]) end def status begin output = svstat self.service if output =~ /:\s+up \(/ return :running end rescue Puppet::ExecutionFailure => detail raise Puppet::Error.new( "Could not get status for service #{resource.ref}: #{detail}" ) end :stopped end def setupservice if resource[:manifest] Puppet.notice "Configuring #{resource[:name]}" command = [ resource[:manifest], resource[:name] ] #texecute("setupservice", command) rv = system("#{command}") end rescue Puppet::ExecutionFailure => detail raise Puppet::Error.new( "Cannot config #{self.service} to enable it: #{detail}" ) end def enabled? case self.status when :running # obviously if the daemon is running then it is enabled return :true else # the service is enabled if it is linked return FileTest.symlink?(self.service) ? :true : :false end end def enable if ! FileTest.directory?(self.daemon) Puppet.notice "No daemon dir, calling setupservice for #{resource[:name]}" self.setupservice end if self.daemon if ! FileTest.symlink?(self.service) Puppet.notice "Enabling #{self.service}: linking #{self.daemon} -> #{self.service}" File.symlink(self.daemon, self.service) end end rescue Puppet::ExecutionFailure => detail raise Puppet::Error.new( "No daemon directory found for #{self.service}") end def disable begin if ! FileTest.directory?(self.daemon) Puppet.notice "No daemon dir, calling setupservice for #{resource[:name]}" self.setupservice end if self.daemon if FileTest.symlink?(self.service) Puppet.notice "Disabling #{self.service}: removing link #{self.daemon} -> #{self.service}" File.unlink(self.service) end end rescue Puppet::ExecutionFailure => detail raise Puppet::Error.new( "No daemon directory found for #{self.service}") end self.stop end def restart svc "-t", self.service end def start enable unless enabled? == :true svc "-u", self.service end def stop svc "-d", self.service end end diff --git a/lib/puppet/provider/service/gentoo.rb b/lib/puppet/provider/service/gentoo.rb index 382c74267..20f5d77e6 100644 --- a/lib/puppet/provider/service/gentoo.rb +++ b/lib/puppet/provider/service/gentoo.rb @@ -1,52 +1,50 @@ # Manage gentoo services. Start/stop is the same as InitSvc, but enable/disable # is special. Puppet::Type.type(:service).provide :gentoo, :parent => :init do desc "Gentoo's form of `init`-style service management. Uses `rc-update` for service enabling and disabling. " commands :update => "/sbin/rc-update" confine :operatingsystem => :gentoo defaultfor :operatingsystem => :gentoo def self.defpath superclass.defpath end def disable output = update :del, @resource[:name], :default rescue Puppet::ExecutionFailure raise Puppet::Error, "Could not disable #{self.name}: #{output}" end def enabled? begin output = update :show rescue Puppet::ExecutionFailure return :false end line = output.split(/\n/).find { |l| l.include?(@resource[:name]) } return :false unless line # If it's enabled then it will print output showing service | runlevel if output =~ /^\s*#{@resource[:name]}\s*\|\s*(boot|default)/ return :true else return :false end end def enable output = update :add, @resource[:name], :default rescue Puppet::ExecutionFailure raise Puppet::Error, "Could not enable #{self.name}: #{output}" end end - -# $Id $ diff --git a/lib/puppet/provider/service/launchd.rb b/lib/puppet/provider/service/launchd.rb index 1632edabf..07c549a8b 100644 --- a/lib/puppet/provider/service/launchd.rb +++ b/lib/puppet/provider/service/launchd.rb @@ -1,263 +1,266 @@ require 'facter/util/plist' Puppet::Type.type(:service).provide :launchd, :parent => :base do desc "launchd service management framework. - This provider manages launchd jobs, the default service framework for - Mac OS X, that has also been open sourced by Apple for possible use on - other platforms. + This provider manages jobs with launchd, which is the default service framework for + Mac OS X and is potentially available for use on other platforms. See: + * http://developer.apple.com/macosx/launchd.html * http://launchd.macosforge.org/ This provider reads plists out of the following directories: + * /System/Library/LaunchDaemons * /System/Library/LaunchAgents * /Library/LaunchDaemons * /Library/LaunchAgents - and builds up a list of services based upon each plists \"Label\" entry. + ...and builds up a list of services based upon each plist's \"Label\" entry. This provider supports: + * ensure => running/stopped, * enable => true/false * status * restart Here is how the Puppet states correspond to launchd states: - * stopped => job unloaded - * started => job loaded - * enabled => 'Disable' removed from job plist file - * disabled => 'Disable' added to job plist file + + * stopped --- job unloaded + * started --- job loaded + * enabled --- 'Disable' removed from job plist file + * disabled --- 'Disable' added to job plist file Note that this allows you to do something launchctl can't do, which is to be in a state of \"stopped/enabled\ or \"running/disabled\". " commands :launchctl => "/bin/launchctl" commands :sw_vers => "/usr/bin/sw_vers" commands :plutil => "/usr/bin/plutil" defaultfor :operatingsystem => :darwin confine :operatingsystem => :darwin has_feature :enableable Launchd_Paths = ["/Library/LaunchAgents", "/Library/LaunchDaemons", "/System/Library/LaunchAgents", "/System/Library/LaunchDaemons",] Launchd_Overrides = "/var/db/launchd.db/com.apple.launchd/overrides.plist" # Read a plist, whether its format is XML or in Apple's "binary1" # format. def self.read_plist(path) Plist::parse_xml(plutil('-convert', 'xml1', '-o', '/dev/stdout', path)) end # returns a label => path map for either all jobs, or just a single # job if the label is specified def self.jobsearch(label=nil) label_to_path_map = {} Launchd_Paths.each do |path| if FileTest.exists?(path) Dir.entries(path).each do |f| next if f =~ /^\..*$/ next if FileTest.directory?(f) fullpath = File.join(path, f) if FileTest.file?(fullpath) and job = read_plist(fullpath) and job.has_key?("Label") if job["Label"] == label return { label => fullpath } else label_to_path_map[job["Label"]] = fullpath end end end end end # if we didn't find the job above and we should have, error. raise Puppet::Error.new("Unable to find launchd plist for job: #{label}") if label # if returning all jobs label_to_path_map end def self.instances jobs = self.jobsearch jobs.keys.collect do |job| new(:name => job, :provider => :launchd, :path => jobs[job]) end end def self.get_macosx_version_major return @macosx_version_major if defined?(@macosx_version_major) begin # Make sure we've loaded all of the facts Facter.loadfacts if Facter.value(:macosx_productversion_major) product_version_major = Facter.value(:macosx_productversion_major) else # TODO: remove this code chunk once we require Facter 1.5.5 or higher. Puppet.warning("DEPRECATION WARNING: Future versions of the launchd provider will require Facter 1.5.5 or newer.") product_version = Facter.value(:macosx_productversion) fail("Could not determine OS X version from Facter") if product_version.nil? product_version_major = product_version.scan(/(\d+)\.(\d+)./).join(".") end fail("#{product_version_major} is not supported by the launchd provider") if %w{10.0 10.1 10.2 10.3}.include?(product_version_major) @macosx_version_major = product_version_major return @macosx_version_major rescue Puppet::ExecutionFailure => detail fail("Could not determine OS X version: #{detail}") end end # finds the path for a given label and returns the path and parsed plist # as an array of [path, plist]. Note plist is really a Hash here. def plist_from_label(label) job = self.class.jobsearch(label) job_path = job[label] if FileTest.file?(job_path) job_plist = self.class.read_plist(job_path) else raise Puppet::Error.new("Unable to parse launchd plist at path: #{job_path}") end [job_path, job_plist] end def status # launchctl list exits zero if the job is loaded # and non-zero if it isn't. Simple way to check... but is only # available on OS X 10.5 unfortunately, so we grab the whole list # and check if our resource is included. The output formats differ # between 10.4 and 10.5, thus the necessity for splitting begin output = launchctl :list raise Puppet::Error.new("launchctl list failed to return any data.") if output.nil? output.split("\n").each do |j| return :running if j.split(/\s/).last == resource[:name] end return :stopped rescue Puppet::ExecutionFailure raise Puppet::Error.new("Unable to determine status of #{resource[:name]}") end end # start the service. To get to a state of running/enabled, we need to # conditionally enable at load, then disable by modifying the plist file # directly. def start job_path, job_plist = plist_from_label(resource[:name]) did_enable_job = false cmds = [] cmds << :launchctl << :load if self.enabled? == :false # launchctl won't load disabled jobs cmds << "-w" did_enable_job = true end cmds << job_path begin execute(cmds) rescue Puppet::ExecutionFailure raise Puppet::Error.new("Unable to start service: #{resource[:name]} at path: #{job_path}") end # As load -w clears the Disabled flag, we need to add it in after self.disable if did_enable_job and resource[:enable] == :false end def stop job_path, job_plist = plist_from_label(resource[:name]) did_disable_job = false cmds = [] cmds << :launchctl << :unload if self.enabled? == :true # keepalive jobs can't be stopped without disabling cmds << "-w" did_disable_job = true end cmds << job_path begin execute(cmds) rescue Puppet::ExecutionFailure raise Puppet::Error.new("Unable to stop service: #{resource[:name]} at path: #{job_path}") end # As unload -w sets the Disabled flag, we need to add it in after self.enable if did_disable_job and resource[:enable] == :true end # launchd jobs are enabled by default. They are only disabled if the key # "Disabled" is set to true, but it can also be set to false to enable it. # In 10.6, the Disabled key in the job plist is consulted, but only if there # is no entry in the global overrides plist. # We need to draw a distinction between undefined, true and false for both # locations where the Disabled flag can be defined. def enabled? job_plist_disabled = nil overrides_disabled = nil job_path, job_plist = plist_from_label(resource[:name]) job_plist_disabled = job_plist["Disabled"] if job_plist.has_key?("Disabled") if self.class.get_macosx_version_major == "10.6": if FileTest.file?(Launchd_Overrides) and overrides = self.class.read_plist(Launchd_Overrides) if overrides.has_key?(resource[:name]) overrides_disabled = overrides[resource[:name]]["Disabled"] if overrides[resource[:name]].has_key?("Disabled") end end end if overrides_disabled.nil? if job_plist_disabled.nil? or job_plist_disabled == false return :true end elsif overrides_disabled == false return :true end :false end # enable and disable are a bit hacky. We write out the plist with the appropriate value # rather than dealing with launchctl as it is unable to change the Disabled flag # without actually loading/unloading the job. # In 10.6 we need to write out a disabled key to the global overrides plist, in earlier # versions this is stored in the job plist itself. def enable if self.class.get_macosx_version_major == "10.6" overrides = self.class.read_plist(Launchd_Overrides) overrides[resource[:name]] = { "Disabled" => false } Plist::Emit.save_plist(overrides, Launchd_Overrides) else job_path, job_plist = plist_from_label(resource[:name]) if self.enabled? == :false job_plist.delete("Disabled") Plist::Emit.save_plist(job_plist, job_path) end end end def disable if self.class.get_macosx_version_major == "10.6" overrides = self.class.read_plist(Launchd_Overrides) overrides[resource[:name]] = { "Disabled" => true } Plist::Emit.save_plist(overrides, Launchd_Overrides) else job_path, job_plist = plist_from_label(resource[:name]) job_plist["Disabled"] = true Plist::Emit.save_plist(job_plist, job_path) end end end diff --git a/lib/puppet/provider/service/runit.rb b/lib/puppet/provider/service/runit.rb index 0315b9597..736e3db71 100644 --- a/lib/puppet/provider/service/runit.rb +++ b/lib/puppet/provider/service/runit.rb @@ -1,103 +1,103 @@ # Daemontools service management # # author Brice Figureau Puppet::Type.type(:service).provide :runit, :parent => :daemontools do desc "Runit service management. This provider manages daemons running supervised by Runit. It tries to detect the service directory, with by order of preference: * /service * /var/service * /etc/service The daemon directory should be placed in a directory that can be by default in: * /etc/sv or this can be overriden in the service resource parameters:: - service { - \"myservice\": - provider => \"runit\", path => \"/path/to/daemons\"; - } + service { \"myservice\": + provider => \"runit\", + path => \"/path/to/daemons\", + } This provider supports out of the box: * start/stop * enable/disable * restart * status " commands :sv => "/usr/bin/sv" class << self # this is necessary to autodetect a valid resource # default path, since there is no standard for such directory. def defpath(dummy_argument=:work_arround_for_ruby_GC_bug) unless @defpath ["/etc/sv", "/var/lib/service"].each do |path| if FileTest.exist?(path) @defpath = path break end end raise "Could not find the daemon directory (tested [/var/lib/service,/etc])" unless @defpath end @defpath end end # find the service dir on this node def servicedir unless @servicedir ["/service", "/etc/service","/var/service"].each do |path| if FileTest.exist?(path) @servicedir = path break end end raise "Could not find service directory" unless @servicedir end @servicedir end def status begin output = sv "status", self.daemon return :running if output =~ /^run: / rescue Puppet::ExecutionFailure => detail unless detail.message =~ /(warning: |runsv not running$)/ raise Puppet::Error.new( "Could not get status for service #{resource.ref}: #{detail}" ) end end :stopped end def stop sv "stop", self.service end def start enable unless enabled? == :true sv "start", self.service end def restart sv "restart", self.service end # disable by removing the symlink so that runit # doesn't restart our service behind our back # note that runit doesn't need to perform a stop # before a disable def disable # unlink the daemon symlink to disable it File.unlink(self.service) if FileTest.symlink?(self.service) end end diff --git a/lib/puppet/provider/user/useradd.rb b/lib/puppet/provider/user/useradd.rb index ba406cc63..b87971738 100644 --- a/lib/puppet/provider/user/useradd.rb +++ b/lib/puppet/provider/user/useradd.rb @@ -1,109 +1,114 @@ require 'puppet/provider/nameservice/objectadd' Puppet::Type.type(:user).provide :useradd, :parent => Puppet::Provider::NameService::ObjectAdd do desc "User management via `useradd` and its ilk. Note that you will need to install the `Shadow Password` Ruby library often known as ruby-libshadow to manage user passwords." commands :add => "useradd", :delete => "userdel", :modify => "usermod", :password => "chage" options :home, :flag => "-d", :method => :dir options :comment, :method => :gecos options :groups, :flag => "-G" options :password_min_age, :flag => "-m" options :password_max_age, :flag => "-M" verify :gid, "GID must be an integer" do |value| value.is_a? Integer end verify :groups, "Groups must be comma-separated" do |value| value !~ /\s/ end - has_features :manages_homedir, :allows_duplicates, :manages_expiry + has_features :manages_homedir, :allows_duplicates, :manages_expiry, :system_users has_features :manages_passwords, :manages_password_age if Puppet.features.libshadow? def check_allow_dup @resource.allowdupe? ? ["-o"] : [] end def check_manage_home cmd = [] if @resource.managehome? cmd << "-m" elsif %w{Fedora RedHat CentOS OEL OVS}.include?(Facter.value("operatingsystem")) cmd << "-M" end cmd end def check_manage_expiry cmd = [] if @resource[:expiry] cmd << "-e #{@resource[:expiry]}" end cmd end + def check_system_users + @resource.system? ? ["-r"] : [] + end + def add_properties cmd = [] Puppet::Type.type(:user).validproperties.each do |property| next if property == :ensure next if property.to_s =~ /password_.+_age/ # the value needs to be quoted, mostly because -c might # have spaces in it if value = @resource.should(property) and value != "" cmd << flag(property) << value end end cmd end def addcmd cmd = [command(:add)] cmd += add_properties cmd += check_allow_dup cmd += check_manage_home cmd += check_manage_expiry + cmd += check_system_users cmd << @resource[:name] end def passcmd age_limits = [:password_min_age, :password_max_age].select { |property| @resource.should(property) } if age_limits.empty? nil else [command(:password),age_limits.collect { |property| [flag(property), @resource.should(property)]}, @resource[:name]].flatten end end def password_min_age if Puppet.features.libshadow? if ent = Shadow::Passwd.getspnam(@resource.name) return ent.sp_min end end :absent end def password_max_age if Puppet.features.libshadow? if ent = Shadow::Passwd.getspnam(@resource.name) return ent.sp_max end end :absent end # Retrieve the password using the Shadow Password library def password if Puppet.features.libshadow? if ent = Shadow::Passwd.getspnam(@resource.name) return ent.sp_pwdp end end :absent end end diff --git a/lib/puppet/rails/database/004_add_inventory_service_tables.rb b/lib/puppet/rails/database/004_add_inventory_service_tables.rb new file mode 100644 index 000000000..6e6b28c0c --- /dev/null +++ b/lib/puppet/rails/database/004_add_inventory_service_tables.rb @@ -0,0 +1,36 @@ +class AddInventoryServiceTables < ActiveRecord::Migration + def self.up + unless ActiveRecord::Base.connection.tables.include?("inventory_nodes") + create_table :inventory_nodes do |t| + t.column :name, :string, :null => false + t.column :timestamp, :datetime, :null => false + t.column :updated_at, :datetime + t.column :created_at, :datetime + end + + add_index :inventory_nodes, :name, :unique => true + end + + unless ActiveRecord::Base.connection.tables.include?("inventory_facts") + create_table :inventory_facts, :id => false do |t| + t.column :node_id, :integer, :null => false + t.column :name, :string, :null => false + t.column :value, :text, :null => false + end + + add_index :inventory_facts, [:node_id, :name], :unique => true + end + end + + def self.down + unless ActiveRecord::Base.connection.tables.include?("inventory_nodes") + remove_index :inventory_nodes, :name + drop_table :inventory_nodes + end + + if ActiveRecord::Base.connection.tables.include?("inventory_facts") + remove_index :inventory_facts, [:node_id, :name] + drop_table :inventory_facts + end + end +end diff --git a/lib/puppet/rails/database/schema.rb b/lib/puppet/rails/database/schema.rb index 8b389d773..7b75f4216 100644 --- a/lib/puppet/rails/database/schema.rb +++ b/lib/puppet/rails/database/schema.rb @@ -1,114 +1,131 @@ class Puppet::Rails::Schema def self.init oldout = nil Puppet::Util.benchmark(Puppet, :notice, "Initialized database") do # We want to rewrite stdout, so we don't get migration messages. oldout = $stdout $stdout = File.open("/dev/null", "w") ActiveRecord::Schema.define do create_table :resources do |t| t.column :title, :text, :null => false t.column :restype, :string, :null => false t.column :host_id, :integer t.column :source_file_id, :integer t.column :exported, :boolean t.column :line, :integer t.column :updated_at, :datetime t.column :created_at, :datetime end add_index :resources, :host_id, :integer => true add_index :resources, :source_file_id, :integer => true # Thanks, mysql! MySQL requires a length on indexes in text fields. # So, we provide them for mysql and handle everything else specially. # Oracle doesn't index on CLOB fields, so we skip it if Puppet[:dbadapter] == "mysql" execute "CREATE INDEX typentitle ON resources (restype,title(50));" elsif Puppet[:dbadapter] != "oracle_enhanced" add_index :resources, [:title, :restype] end create_table :source_files do |t| t.column :filename, :string t.column :path, :string t.column :updated_at, :datetime t.column :created_at, :datetime end add_index :source_files, :filename create_table :resource_tags do |t| t.column :resource_id, :integer t.column :puppet_tag_id, :integer t.column :updated_at, :datetime t.column :created_at, :datetime end add_index :resource_tags, :resource_id, :integer => true add_index :resource_tags, :puppet_tag_id, :integer => true create_table :puppet_tags do |t| t.column :name, :string t.column :updated_at, :datetime t.column :created_at, :datetime end # Oracle automatically creates a primary key index add_index :puppet_tags, :id, :integer => true if Puppet[:dbadapter] != "oracle_enhanced" create_table :hosts do |t| t.column :name, :string, :null => false t.column :ip, :string t.column :environment, :text t.column :last_compile, :datetime t.column :last_freshcheck, :datetime t.column :last_report, :datetime #Use updated_at to automatically add timestamp on save. t.column :updated_at, :datetime t.column :source_file_id, :integer t.column :created_at, :datetime end add_index :hosts, :source_file_id, :integer => true add_index :hosts, :name create_table :fact_names do |t| t.column :name, :string, :null => false t.column :updated_at, :datetime t.column :created_at, :datetime end add_index :fact_names, :name create_table :fact_values do |t| t.column :value, :text, :null => false t.column :fact_name_id, :integer, :null => false t.column :host_id, :integer, :null => false t.column :updated_at, :datetime t.column :created_at, :datetime end add_index :fact_values, :fact_name_id, :integer => true add_index :fact_values, :host_id, :integer => true create_table :param_values do |t| t.column :value, :text, :null => false t.column :param_name_id, :integer, :null => false t.column :line, :integer t.column :resource_id, :integer t.column :updated_at, :datetime t.column :created_at, :datetime end add_index :param_values, :param_name_id, :integer => true add_index :param_values, :resource_id, :integer => true create_table :param_names do |t| t.column :name, :string, :null => false t.column :updated_at, :datetime t.column :created_at, :datetime end add_index :param_names, :name + + create_table :inventory_nodes do |t| + t.column :name, :string, :null => false + t.column :timestamp, :datetime, :null => false + t.column :updated_at, :datetime + t.column :created_at, :datetime + end + + add_index :inventory_nodes, :name, :unique => true + + create_table :inventory_facts, :id => false do |t| + t.column :node_id, :integer, :null => false + t.column :name, :string, :null => false + t.column :value, :text, :null => false + end + + add_index :inventory_facts, [:node_id, :name], :unique => true end end ensure $stdout.close $stdout = oldout if oldout oldout = nil end end diff --git a/lib/puppet/rails/fact_name.rb b/lib/puppet/rails/fact_name.rb index fb40ec48f..4273399e5 100644 --- a/lib/puppet/rails/fact_name.rb +++ b/lib/puppet/rails/fact_name.rb @@ -1,7 +1,5 @@ require 'puppet/rails/fact_value' class Puppet::Rails::FactName < ActiveRecord::Base has_many :fact_values, :dependent => :destroy end - -# $Id: fact_name.rb 1952 2006-12-19 05:47:57Z luke $ diff --git a/lib/puppet/rails/fact_value.rb b/lib/puppet/rails/fact_value.rb index 45a88b2dc..9fd81ae1c 100644 --- a/lib/puppet/rails/fact_value.rb +++ b/lib/puppet/rails/fact_value.rb @@ -1,10 +1,8 @@ class Puppet::Rails::FactValue < ActiveRecord::Base belongs_to :fact_name belongs_to :host def to_label "#{self.fact_name.name}" end end - -# $Id: fact_value.rb 1952 2006-12-19 05:47:57Z luke $ diff --git a/lib/puppet/rails/inventory_fact.rb b/lib/puppet/rails/inventory_fact.rb new file mode 100644 index 000000000..aa6334eef --- /dev/null +++ b/lib/puppet/rails/inventory_fact.rb @@ -0,0 +1,5 @@ +require 'puppet/rails/inventory_node' + +class Puppet::Rails::InventoryFact < ::ActiveRecord::Base + belongs_to :node, :class_name => "Puppet::Rails::InventoryNode" +end diff --git a/lib/puppet/rails/inventory_node.rb b/lib/puppet/rails/inventory_node.rb new file mode 100644 index 000000000..52f8621a4 --- /dev/null +++ b/lib/puppet/rails/inventory_node.rb @@ -0,0 +1,25 @@ +require 'puppet/rails/inventory_fact' + +class Puppet::Rails::InventoryNode < ::ActiveRecord::Base + has_many :facts, :class_name => "Puppet::Rails::InventoryFact", :foreign_key => :node_id, :dependent => :delete_all + + named_scope :has_fact_with_value, lambda { |name,value| + { + :conditions => ["inventory_facts.name = ? AND inventory_facts.value = ?", name, value], + :joins => :facts + } + } + + named_scope :has_fact_without_value, lambda { |name,value| + { + :conditions => ["inventory_facts.name = ? AND inventory_facts.value != ?", name, value], + :joins => :facts + } + } + + def facts_to_hash + facts.inject({}) do |fact_hash,fact| + fact_hash.merge(fact.name => fact.value) + end + end +end diff --git a/lib/puppet/reference/configuration.rb b/lib/puppet/reference/configuration.rb index c8ff145ba..6581427ff 100644 --- a/lib/puppet/reference/configuration.rb +++ b/lib/puppet/reference/configuration.rb @@ -1,144 +1,144 @@ config = Puppet::Util::Reference.newreference(:configuration, :depth => 1, :doc => "A reference for all configuration parameters") do docs = {} Puppet.settings.each do |name, object| docs[name] = object end str = "" docs.sort { |a, b| a[0].to_s <=> b[0].to_s }.each do |name, object| # Make each name an anchor header = name.to_s str += h(header, 3) # Print the doc string itself begin str += object.desc.gsub(/\n/, " ") rescue => detail puts detail.backtrace puts detail end str += "\n\n" # Now print the data about the item. str += "" val = object.default if name.to_s == "vardir" val = "/var/lib/puppet" elsif name.to_s == "confdir" val = "/etc/puppet" end # Leave out the section information; it was apparently confusing people. #str += "- **Section**: #{object.section}\n" unless val == "" str += "- *Default*: #{val}\n" end str += "\n" end return str end config.header = " ## Specifying Configuration Parameters ### On The Command-Line Every Puppet executable (with the exception of `puppetdoc`) accepts all of the parameters below, but not all of the arguments make sense for every executable. I have tried to be as thorough as possible in the descriptions of the arguments, so it should be obvious whether an argument is appropriate or not. These parameters can be supplied to the executables either as command-line options or in the configuration file. For instance, the command-line invocation below would set the configuration directory to `/private/puppet`: $ puppet agent --confdir=/private/puppet Note that boolean options are turned on and off with a slightly different syntax on the command line: $ puppet agent --storeconfigs $ puppet agent --no-storeconfigs The invocations above will enable and disable, respectively, the storage of the client configuration. ### Configuration Files As mentioned above, the configuration parameters can also be stored in a configuration file, located in the configuration directory. As root, the default configuration directory is `/etc/puppet`, and as a regular user, the default configuration directory is `~user/.puppet`. As of 0.23.0, all executables look for `puppet.conf` in their configuration directory (although they previously looked for separate files). For example, `puppet.conf` is located at `/etc/puppet/puppet.conf` as `root` and `~user/.puppet/puppet.conf` as a regular user by default. All executables will set any parameters set within the `[main]` section, and each executable will also use one of the `[master]`, `[agent]`. #### File Format The file follows INI-style formatting. Here is an example of a very simple `puppet.conf` file: [main] confdir = /private/puppet storeconfigs = true Note that boolean parameters must be explicitly specified as `true` or `false` as seen above. If you need to change file or directory parameters (e.g., reset the mode or owner), do so within curly braces on the same line: [main] vardir = /new/vardir {owner = root, mode = 644} If you're starting out with a fresh configuration, you may wish to let the executable generate a template configuration file for you by invoking the executable in question with the `--genconfig` command. The executable will print a template configuration to standard output, which can be redirected to a file like so: $ puppet agent --genconfig > /etc/puppet/puppet.conf Note that this invocation will replace the contents of any pre-existing `puppet.conf` file, so make a backup of your present config if it contains valuable information. Like the `--genconfig` argument, the executables also accept a `--genmanifest` argument, which will generate a manifest that can be used to manage all of Puppet's directories and files and prints it to standard output. This can likewise be redirected to a file: $ puppet agent --genmanifest > /etc/puppet/manifests/site.pp Puppet can also create user and group accounts for itself (one `puppet` group and one `puppet` user) if it is invoked as `root` with the `--mkusers` argument: - $ puppet agent --mkusers + $ puppet master --mkusers ## Signals The `puppet agent` and `puppet master` executables catch some signals for special handling. Both daemons catch (`SIGHUP`), which forces the server to restart tself. Predictably, interrupt and terminate (`SIGINT` and `SIGTERM`) will shut down the server, whether it be an instance of `puppet agent` or `puppet master`. Sending the `SIGUSR1` signal to an instance of `puppet agent` will cause it to immediately begin a new configuration transaction with the server. This signal has no effect on `puppet master`. ## Configuration Parameter Reference Below is a list of all documented parameters. Not all of them are valid with all Puppet executables, but the executables will ignore any inappropriate values. " diff --git a/lib/puppet/reference/metaparameter.rb b/lib/puppet/reference/metaparameter.rb index c16a1d33a..3c4c08701 100644 --- a/lib/puppet/reference/metaparameter.rb +++ b/lib/puppet/reference/metaparameter.rb @@ -1,41 +1,41 @@ metaparameter = Puppet::Util::Reference.newreference :metaparameter, :doc => "All Puppet metaparameters and all their details" do types = {} Puppet::Type.loadall Puppet::Type.eachtype { |type| next if type.name == :puppet next if type.name == :component types[type.name] = type } str = %{ # Metaparameters Metaparameters are parameters that work with any resource type; they are part of the Puppet framework itself rather than being part of the implementation of any given instance. Thus, any defined metaparameter can be used with any instance in your manifest, including defined components. ## Available Metaparameters } begin params = [] Puppet::Type.eachmetaparam { |param| params << param } params.sort { |a,b| a.to_s <=> b.to_s }.each { |param| - str += paramwrap(param.to_s, scrub(Puppet::Type.metaparamdoc(param)), :level => 4) + str += paramwrap(param.to_s, scrub(Puppet::Type.metaparamdoc(param)), :level => 3) } rescue => detail puts detail.backtrace puts "incorrect metaparams: #{detail}" exit(1) end str end diff --git a/lib/puppet/reports/store.rb b/lib/puppet/reports/store.rb index 99a9fc177..625a263b3 100644 --- a/lib/puppet/reports/store.rb +++ b/lib/puppet/reports/store.rb @@ -1,42 +1,45 @@ require 'puppet' Puppet::Reports.register_report(:store) do desc "Store the yaml report on disk. Each host sends its report as a YAML dump and this just stores the file on disk, in the `reportdir` directory. These files collect quickly -- one every half hour -- so it is a good idea to perform some maintenance on them if you use this report (it's the only default report)." def process # We don't want any tracking back in the fs. Unlikely, but there # you go. client = self.host.gsub("..",".") dir = File.join(Puppet[:reportdir], client) - Dir.mkdir(dir, 0750) unless FileTest.exists?(dir) + if ! FileTest.exists?(dir) + FileUtils.mkdir_p(dir) + FileUtils.chmod_R(0750, dir) + end # Now store the report. now = Time.now.gmtime name = %w{year month day hour min}.collect do |method| # Make sure we're at least two digits everywhere "%02d" % now.send(method).to_s end.join("") + ".yaml" file = File.join(dir, name) begin File.open(file, "w", 0640) do |f| f.print to_yaml end rescue => detail puts detail.backtrace if Puppet[:trace] Puppet.warning "Could not write report for #{client} at #{file}: #{detail}" end # Only testing cares about the return value file end end diff --git a/lib/puppet/resource.rb b/lib/puppet/resource.rb index e832804f5..214516908 100644 --- a/lib/puppet/resource.rb +++ b/lib/puppet/resource.rb @@ -1,425 +1,439 @@ require 'puppet' require 'puppet/util/tagging' require 'puppet/util/pson' # The simplest resource class. Eventually it will function as the # base class for all resource-like behaviour. class Puppet::Resource + # This stub class is only needed for serialization compatibility with 0.25.x. + # Specifically, it exists to provide a compatibility API when using YAML + # serialized objects loaded from StoreConfigs. + Reference = Puppet::Resource + include Puppet::Util::Tagging require 'puppet/resource/type_collection_helper' include Puppet::Resource::TypeCollectionHelper extend Puppet::Util::Pson include Enumerable attr_accessor :file, :line, :catalog, :exported, :virtual, :validate_parameters, :strict attr_reader :type, :title require 'puppet/indirector' extend Puppet::Indirector indirects :resource, :terminus_class => :ral ATTRIBUTES = [:file, :line, :exported] def self.from_pson(pson) raise ArgumentError, "No resource type provided in pson data" unless type = pson['type'] raise ArgumentError, "No resource title provided in pson data" unless title = pson['title'] resource = new(type, title) if params = pson['parameters'] params.each { |param, value| resource[param] = value } end if tags = pson['tags'] tags.each { |tag| resource.tag(tag) } end ATTRIBUTES.each do |a| if value = pson[a.to_s] resource.send(a.to_s + "=", value) end end resource.exported ||= false resource end def inspect "#{@type}[#{@title}]#{to_hash.inspect}" end def to_pson_data_hash data = ([:type, :title, :tags] + ATTRIBUTES).inject({}) do |hash, param| next hash unless value = self.send(param) hash[param.to_s] = value hash end data["exported"] ||= false params = self.to_hash.inject({}) do |hash, ary| param, value = ary # Don't duplicate the title as the namevar next hash if param == namevar and value == title hash[param] = Puppet::Resource.value_to_pson_data(value) hash end data["parameters"] = params unless params.empty? data end def self.value_to_pson_data(value) if value.is_a? Array value.map{|v| value_to_pson_data(v) } elsif value.is_a? Puppet::Resource value.to_s else value end end def yaml_property_munge(x) case x when Hash - x.inject({}) { |h,kv| + x.inject({}) { |h,kv| k,v = kv h[k] = self.class.value_to_pson_data(v) h } else self.class.value_to_pson_data(x) end end def to_pson(*args) to_pson_data_hash.to_pson(*args) end # Proxy these methods to the parameters hash. It's likely they'll # be overridden at some point, but this works for now. %w{has_key? keys length delete empty? <<}.each do |method| define_method(method) do |*args| - @parameters.send(method, *args) + parameters.send(method, *args) end end # Set a given parameter. Converts all passed names # to lower-case symbols. def []=(param, value) validate_parameter(param) if validate_parameters - @parameters[parameter_name(param)] = value + parameters[parameter_name(param)] = value end # Return a given parameter's value. Converts all passed names # to lower-case symbols. def [](param) - @parameters[parameter_name(param)] + parameters[parameter_name(param)] end def ==(other) return false unless other.respond_to?(:title) and self.type == other.type and self.title == other.title return false unless to_hash == other.to_hash true end # Compatibility method. def builtin? builtin_type? end # Is this a builtin resource type? def builtin_type? resource_type.is_a?(Class) end # Iterate over each param/value pair, as required for Enumerable. def each - @parameters.each { |p,v| yield p, v } + parameters.each { |p,v| yield p, v } end def include?(parameter) - super || @parameters.keys.include?( parameter_name(parameter) ) + super || parameters.keys.include?( parameter_name(parameter) ) end # These two methods are extracted into a Helper # module, but file load order prevents me # from including them in the class, and I had weird # behaviour (i.e., sometimes it didn't work) when # I directly extended each resource with the helper. def environment Puppet::Node::Environment.new(@environment) end def environment=(env) if env.is_a?(String) or env.is_a?(Symbol) @environment = env else @environment = env.name end end %w{exported virtual strict}.each do |m| define_method(m+"?") do self.send(m) end end - # This stub class is only needed for serialization compatibility with 0.25.x - class Reference - attr_accessor :type,:title - def initialize(type,title) - @type,@title = type,title - end - end - # Create our resource. def initialize(type, title = nil, attributes = {}) @parameters = {} # Set things like strictness first. attributes.each do |attr, value| next if attr == :parameters send(attr.to_s + "=", value) end @type, @title = extract_type_and_title(type, title) @type = munge_type_name(@type) if @type == "Class" @title = :main if @title == "" @title = munge_type_name(@title) end if params = attributes[:parameters] extract_parameters(params) end tag(self.type) tag(self.title) if valid_tag?(self.title) - @reference = Reference.new(@type,@title) # for serialization compatibility with 0.25.x + @reference = self # for serialization compatibility with 0.25.x if strict? and ! resource_type if @type == 'Class' raise ArgumentError, "Could not find declared class #{title}" else raise ArgumentError, "Invalid resource type #{type}" end end end def ref to_s end # Find our resource. def resolve return(catalog ? catalog.resource(to_s) : nil) end def resource_type case type when "Class"; known_resource_types.hostclass(title == :main ? "" : title) when "Node"; known_resource_types.node(title) else Puppet::Type.type(type.to_s.downcase.to_sym) || known_resource_types.definition(type) end end # Produce a simple hash of our parameters. def to_hash - parse_title.merge @parameters + parse_title.merge parameters end def to_s "#{type}[#{title}]" end def uniqueness_key # Temporary kludge to deal with inconsistant use patters h = self.to_hash h[namevar] ||= h[:name] h[:name] ||= h[namevar] h.values_at(*key_attributes.sort_by { |k| k.to_s }) end def key_attributes return(resource_type.respond_to? :key_attributes) ? resource_type.key_attributes : [:name] end # Convert our resource to Puppet code. def to_manifest - "%s { '%s':\n%s\n}" % [self.type.to_s.downcase, self.title, - @parameters.collect { |p, v| - if v.is_a? Array - " #{p} => [\'#{v.join("','")}\']" - else - " #{p} => \'#{v}\'" - end - }.join(",\n") - ] + # Collect list of attributes to align => and move ensure first + attr = parameters.keys + attr_max = attr.inject(0) { |max,k| k.to_s.length > max ? k.to_s.length : max } + + attr.sort! + if attr.first != :ensure && attr.include?(:ensure) + attr.delete(:ensure) + attr.unshift(:ensure) + end + + attributes = attr.collect { |k| + v = parameters[k] + if v.is_a? Array + " %-#{attr_max}s => %s,\n" % [ k, "[\'#{v.join("', '")}\']" ] + else + " %-#{attr_max}s => %s,\n" % [ k, "\'#{v}\'" ] + end + } + + "%s { '%s':\n%s}" % [self.type.to_s.downcase, self.title, attributes] end def to_ref ref end # Convert our resource to a RAL resource instance. Creates component # instances for resource types that don't exist. def to_ral if typeklass = Puppet::Type.type(self.type) return typeklass.new(self) else return Puppet::Type::Component.new(self) end end # Translate our object to a backward-compatible transportable object. def to_trans if builtin_type? and type.downcase.to_s != "stage" result = to_transobject else result = to_transbucket end result.file = self.file result.line = self.line result end def to_trans_ref [type.to_s, title.to_s] end # Create an old-style TransObject instance, for builtin resource types. def to_transobject # Now convert to a transobject result = Puppet::TransObject.new(title, type) to_hash.each do |p, v| if v.is_a?(Puppet::Resource) v = v.to_trans_ref elsif v.is_a?(Array) v = v.collect { |av| av = av.to_trans_ref if av.is_a?(Puppet::Resource) av } end # If the value is an array with only one value, then # convert it to a single value. This is largely so that # the database interaction doesn't have to worry about # whether it returns an array or a string. result[p.to_s] = if v.is_a?(Array) and v.length == 1 v[0] else v end end result.tags = self.tags result end def name # this is potential namespace conflict # between the notion of an "indirector name" # and a "resource name" [ type, title ].join('/') end def to_resource self end def valid_parameter?(name) resource_type.valid_parameter?(name) end def validate_parameter(name) raise ArgumentError, "Invalid parameter #{name}" unless valid_parameter?(name) end private # Produce a canonical method name. def parameter_name(param) param = param.to_s.downcase.to_sym if param == :name and n = namevar param = namevar end param end # The namevar for our resource type. If the type doesn't exist, # always use :name. def namevar if builtin_type? and t = resource_type and t.key_attributes.length == 1 t.key_attributes.first else :name end end # Create an old-style TransBucket instance, for non-builtin resource types. def to_transbucket bucket = Puppet::TransBucket.new([]) bucket.type = self.type bucket.name = self.title # TransBuckets don't support parameters, which is why they're being deprecated. bucket end def extract_parameters(params) params.each do |param, value| validate_parameter(param) if strict? self[param] = value end end def extract_type_and_title(argtype, argtitle) if (argtitle || argtype) =~ /^([^\[\]]+)\[(.+)\]$/m then [ $1, $2 ] elsif argtitle then [ argtype, argtitle ] elsif argtype.is_a?(Puppet::Type) then [ argtype.class.name, argtype.title ] elsif argtype.is_a?(Hash) then raise ArgumentError, "Puppet::Resource.new does not take a hash as the first argument. "+ "Did you mean (#{(argtype[:type] || argtype["type"]).inspect}, #{(argtype[:title] || argtype["title"]).inspect }) ?" else raise ArgumentError, "No title provided and #{argtype.inspect} is not a valid resource reference" end end def munge_type_name(value) return :main if value == :main return "Class" if value == "" or value.nil? or value.to_s.downcase == "component" value.to_s.split("::").collect { |s| s.capitalize }.join("::") end def parse_title h = {} type = resource_type if type.respond_to? :title_patterns type.title_patterns.each { |regexp, symbols_and_lambdas| if captures = regexp.match(title.to_s) symbols_and_lambdas.zip(captures[1..-1]).each { |symbol_and_lambda,capture| sym, lam = symbol_and_lambda #self[sym] = lam.call(capture) h[sym] = lam.call(capture) } return h end } else return { :name => title.to_s } end end + + def parameters + # @parameters could have been loaded from YAML, causing it to be nil (by + # bypassing initialize). + @parameters ||= {} + end end diff --git a/lib/puppet/type.rb b/lib/puppet/type.rb index f70a3ec0b..d24cc8554 100644 --- a/lib/puppet/type.rb +++ b/lib/puppet/type.rb @@ -1,1910 +1,1910 @@ require 'puppet' require 'puppet/util/log' require 'puppet/util/metric' require 'puppet/property' require 'puppet/parameter' require 'puppet/util' require 'puppet/util/autoload' require 'puppet/metatype/manager' require 'puppet/util/errors' require 'puppet/util/log_paths' require 'puppet/util/logging' require 'puppet/util/cacher' require 'puppet/file_collection/lookup' require 'puppet/util/tagging' # see the bottom of the file for the rest of the inclusions module Puppet class Type include Puppet::Util include Puppet::Util::Errors include Puppet::Util::LogPaths include Puppet::Util::Logging include Puppet::Util::Cacher include Puppet::FileCollection::Lookup include Puppet::Util::Tagging ############################### # Code related to resource type attributes. class << self include Puppet::Util::ClassGen include Puppet::Util::Warnings attr_reader :properties end def self.states warnonce "The states method is deprecated; use properties" properties end # All parameters, in the appropriate order. The key_attributes come first, then # the provider, then the properties, and finally the params and metaparams # in the order they were specified in the files. def self.allattrs key_attributes | (parameters & [:provider]) | properties.collect { |property| property.name } | parameters | metaparams end # Retrieve an attribute alias, if there is one. def self.attr_alias(param) @attr_aliases[symbolize(param)] end # Create an alias to an existing attribute. This will cause the aliased # attribute to be valid when setting and retrieving values on the instance. def self.set_attr_alias(hash) hash.each do |new, old| @attr_aliases[symbolize(new)] = symbolize(old) end end # Find the class associated with any given attribute. def self.attrclass(name) @attrclasses ||= {} # We cache the value, since this method gets called such a huge number # of times (as in, hundreds of thousands in a given run). unless @attrclasses.include?(name) @attrclasses[name] = case self.attrtype(name) when :property; @validproperties[name] when :meta; @@metaparamhash[name] when :param; @paramhash[name] end end @attrclasses[name] end # What type of parameter are we dealing with? Cache the results, because # this method gets called so many times. def self.attrtype(attr) @attrtypes ||= {} unless @attrtypes.include?(attr) @attrtypes[attr] = case when @validproperties.include?(attr); :property when @paramhash.include?(attr); :param when @@metaparamhash.include?(attr); :meta end end @attrtypes[attr] end def self.eachmetaparam @@metaparams.each { |p| yield p.name } end # Create the 'ensure' class. This is a separate method so other types # can easily call it and create their own 'ensure' values. def self.ensurable(&block) if block_given? self.newproperty(:ensure, :parent => Puppet::Property::Ensure, &block) else self.newproperty(:ensure, :parent => Puppet::Property::Ensure) do self.defaultvalues end end end # Should we add the 'ensure' property to this class? def self.ensurable? # If the class has all three of these methods defined, then it's # ensurable. ens = [:exists?, :create, :destroy].inject { |set, method| set &&= self.public_method_defined?(method) } ens end # Deal with any options passed into parameters. def self.handle_param_options(name, options) # If it's a boolean parameter, create a method to test the value easily if options[:boolean] define_method(name.to_s + "?") do val = self[name] if val == :true or val == true return true end end end end # Is the parameter in question a meta-parameter? def self.metaparam?(param) @@metaparamhash.include?(symbolize(param)) end # Find the metaparameter class associated with a given metaparameter name. def self.metaparamclass(name) @@metaparamhash[symbolize(name)] end def self.metaparams @@metaparams.collect { |param| param.name } end def self.metaparamdoc(metaparam) @@metaparamhash[metaparam].doc end # Create a new metaparam. Requires a block and a name, stores it in the # @parameters array, and does some basic checking on it. def self.newmetaparam(name, options = {}, &block) @@metaparams ||= [] @@metaparamhash ||= {} name = symbolize(name) param = genclass( name, :parent => options[:parent] || Puppet::Parameter, :prefix => "MetaParam", :hash => @@metaparamhash, :array => @@metaparams, :attributes => options[:attributes], &block ) # Grr. param.required_features = options[:required_features] if options[:required_features] handle_param_options(name, options) param.metaparam = true param end def self.key_attribute_parameters @key_attribute_parameters ||= ( params = @parameters.find_all { |param| param.isnamevar? or param.name == :name } ) end def self.key_attributes key_attribute_parameters.collect { |p| p.name } end def self.title_patterns case key_attributes.length when 0; [] when 1; identity = lambda {|x| x} [ [ /(.*)/m, [ [key_attributes.first, identity ] ] ] ] else raise Puppet::DevError,"you must specify title patterns when there are two or more key attributes" end end def uniqueness_key - to_resource.uniqueness_key + self.class.key_attributes.sort_by { |attribute_name| attribute_name.to_s }.map{ |attribute_name| self[attribute_name] } end # Create a new parameter. Requires a block and a name, stores it in the # @parameters array, and does some basic checking on it. def self.newparam(name, options = {}, &block) options[:attributes] ||= {} param = genclass( name, :parent => options[:parent] || Puppet::Parameter, :attributes => options[:attributes], :block => block, :prefix => "Parameter", :array => @parameters, :hash => @paramhash ) handle_param_options(name, options) # Grr. param.required_features = options[:required_features] if options[:required_features] param.isnamevar if options[:namevar] param end def self.newstate(name, options = {}, &block) Puppet.warning "newstate() has been deprecrated; use newproperty(#{name})" newproperty(name, options, &block) end # Create a new property. The first parameter must be the name of the property; # this is how users will refer to the property when creating new instances. # The second parameter is a hash of options; the options are: # * :parent: The parent class for the property. Defaults to Puppet::Property. # * :retrieve: The method to call on the provider or @parent object (if # the provider is not set) to retrieve the current value. def self.newproperty(name, options = {}, &block) name = symbolize(name) # This is here for types that might still have the old method of defining # a parent class. unless options.is_a? Hash raise Puppet::DevError, "Options must be a hash, not #{options.inspect}" end raise Puppet::DevError, "Class #{self.name} already has a property named #{name}" if @validproperties.include?(name) if parent = options[:parent] options.delete(:parent) else parent = Puppet::Property end # We have to create our own, new block here because we want to define # an initial :retrieve method, if told to, and then eval the passed # block if available. prop = genclass(name, :parent => parent, :hash => @validproperties, :attributes => options) do # If they've passed a retrieve method, then override the retrieve # method on the class. if options[:retrieve] define_method(:retrieve) do provider.send(options[:retrieve]) end end class_eval(&block) if block end # If it's the 'ensure' property, always put it first. if name == :ensure @properties.unshift prop else @properties << prop end prop end def self.paramdoc(param) @paramhash[param].doc end # Return the parameter names def self.parameters return [] unless defined?(@parameters) @parameters.collect { |klass| klass.name } end # Find the parameter class associated with a given parameter name. def self.paramclass(name) @paramhash[name] end # Return the property class associated with a name def self.propertybyname(name) @validproperties[name] end def self.validattr?(name) name = symbolize(name) return true if name == :name @validattrs ||= {} unless @validattrs.include?(name) @validattrs[name] = !!(self.validproperty?(name) or self.validparameter?(name) or self.metaparam?(name)) end @validattrs[name] end # does the name reflect a valid property? def self.validproperty?(name) name = symbolize(name) @validproperties.include?(name) && @validproperties[name] end # Return the list of validproperties def self.validproperties return {} unless defined?(@parameters) @validproperties.keys end # does the name reflect a valid parameter? def self.validparameter?(name) raise Puppet::DevError, "Class #{self} has not defined parameters" unless defined?(@parameters) !!(@paramhash.include?(name) or @@metaparamhash.include?(name)) end # This is a forward-compatibility method - it's the validity interface we'll use in Puppet::Resource. def self.valid_parameter?(name) validattr?(name) end # Return either the attribute alias or the attribute. def attr_alias(name) name = symbolize(name) if synonym = self.class.attr_alias(name) return synonym else return name end end # Are we deleting this resource? def deleting? obj = @parameters[:ensure] and obj.should == :absent end # Create a new property if it is valid but doesn't exist # Returns: true if a new parameter was added, false otherwise def add_property_parameter(prop_name) if self.class.validproperty?(prop_name) && !@parameters[prop_name] self.newattr(prop_name) return true end false end # # The name_var is the key_attribute in the case that there is only one. # def name_var key_attributes = self.class.key_attributes (key_attributes.length == 1) && key_attributes.first end # abstract accessing parameters and properties, and normalize # access to always be symbols, not strings # This returns a value, not an object. It returns the 'is' # value, but you can also specifically return 'is' and 'should' # values using 'object.is(:property)' or 'object.should(:property)'. def [](name) name = attr_alias(name) fail("Invalid parameter #{name}(#{name.inspect})") unless self.class.validattr?(name) - if name == :name - name = name_var + if name == :name && nv = name_var + name = nv end if obj = @parameters[name] # Note that if this is a property, then the value is the "should" value, # not the current value. obj.value else return nil end end # Abstract setting parameters and properties, and normalize # access to always be symbols, not strings. This sets the 'should' # value on properties, and otherwise just sets the appropriate parameter. def []=(name,value) name = attr_alias(name) fail("Invalid parameter #{name}") unless self.class.validattr?(name) - if name == :name - name = name_var + if name == :name && nv = name_var + name = nv end raise Puppet::Error.new("Got nil value for #{name}") if value.nil? property = self.newattr(name) if property begin # make sure the parameter doesn't have any errors property.value = value rescue => detail error = Puppet::Error.new("Parameter #{name} failed: #{detail}") error.set_backtrace(detail.backtrace) raise error end end nil end # remove a property from the object; useful in testing or in cleanup # when an error has been encountered def delete(attr) attr = symbolize(attr) if @parameters.has_key?(attr) @parameters.delete(attr) else raise Puppet::DevError.new("Undefined attribute '#{attr}' in #{self}") end end # iterate across the existing properties def eachproperty # properties is a private method properties.each { |property| yield property } end # Create a transaction event. Called by Transaction or by # a property. def event(options = {}) Puppet::Transaction::Event.new({:resource => self, :file => file, :line => line, :tags => tags}.merge(options)) end # Let the catalog determine whether a given cached value is # still valid or has expired. def expirer catalog end # retrieve the 'should' value for a specified property def should(name) name = attr_alias(name) (prop = @parameters[name] and prop.is_a?(Puppet::Property)) ? prop.should : nil end # Create the actual attribute instance. Requires either the attribute # name or class as the first argument, then an optional hash of # attributes to set during initialization. def newattr(name) if name.is_a?(Class) klass = name name = klass.name end unless klass = self.class.attrclass(name) raise Puppet::Error, "Resource type #{self.class.name} does not support parameter #{name}" end if provider and ! provider.class.supports_parameter?(klass) missing = klass.required_features.find_all { |f| ! provider.class.feature?(f) } info "Provider %s does not support features %s; not managing attribute %s" % [provider.class.name, missing.join(", "), name] return nil end return @parameters[name] if @parameters.include?(name) @parameters[name] = klass.new(:resource => self) end # return the value of a parameter def parameter(name) @parameters[name.to_sym] end def parameters @parameters.dup end # Is the named property defined? def propertydefined?(name) name = name.intern unless name.is_a? Symbol @parameters.include?(name) end # Return an actual property instance by name; to return the value, use 'resource[param]' # LAK:NOTE(20081028) Since the 'parameter' method is now a superset of this method, # this one should probably go away at some point. def property(name) (obj = @parameters[symbolize(name)] and obj.is_a?(Puppet::Property)) ? obj : nil end # For any parameters or properties that have defaults and have not yet been # set, set them now. This method can be handed a list of attributes, # and if so it will only set defaults for those attributes. def set_default(attr) return unless klass = self.class.attrclass(attr) return unless klass.method_defined?(:default) return if @parameters.include?(klass.name) return unless parameter = newattr(klass.name) if value = parameter.default and ! value.nil? parameter.value = value else @parameters.delete(parameter.name) end end # Convert our object to a hash. This just includes properties. def to_hash rethash = {} @parameters.each do |name, obj| rethash[name] = obj.value end rethash end def type self.class.name end # Return a specific value for an attribute. def value(name) name = attr_alias(name) (obj = @parameters[name] and obj.respond_to?(:value)) ? obj.value : nil end def version return 0 unless catalog catalog.version end # Return all of the property objects, in the order specified in the # class. def properties self.class.properties.collect { |prop| @parameters[prop.name] }.compact end # Is this type's name isomorphic with the object? That is, if the # name conflicts, does it necessarily mean that the objects conflict? # Defaults to true. def self.isomorphic? if defined?(@isomorphic) return @isomorphic else return true end end def isomorphic? self.class.isomorphic? end # is the instance a managed instance? A 'yes' here means that # the instance was created from the language, vs. being created # in order resolve other questions, such as finding a package # in a list def managed? # Once an object is managed, it always stays managed; but an object # that is listed as unmanaged might become managed later in the process, # so we have to check that every time if @managed return @managed else @managed = false properties.each { |property| s = property.should if s and ! property.class.unmanaged @managed = true break end } return @managed end end ############################### # Code related to the container behaviour. # this is a retarded hack method to get around the difference between # component children and file children def self.depthfirst? @depthfirst end def depthfirst? self.class.depthfirst? end # Remove an object. The argument determines whether the object's # subscriptions get eliminated, too. def remove(rmdeps = true) # This is hackish (mmm, cut and paste), but it works for now, and it's # better than warnings. @parameters.each do |name, obj| obj.remove end @parameters.clear @parent = nil # Remove the reference to the provider. if self.provider @provider.clear @provider = nil end end ############################### # Code related to evaluating the resources. # Flush the provider, if it supports it. This is called by the # transaction. def flush self.provider.flush if self.provider and self.provider.respond_to?(:flush) end # if all contained objects are in sync, then we're in sync # FIXME I don't think this is used on the type instances any more, # it's really only used for testing def insync?(is) insync = true if property = @parameters[:ensure] unless is.include? property raise Puppet::DevError, "The is value is not in the is array for '#{property.name}'" end ensureis = is[property] if property.safe_insync?(ensureis) and property.should == :absent return true end end properties.each { |property| unless is.include? property raise Puppet::DevError, "The is value is not in the is array for '#{property.name}'" end propis = is[property] unless property.safe_insync?(propis) property.debug("Not in sync: #{propis.inspect} vs #{property.should.inspect}") insync = false #else # property.debug("In sync") end } #self.debug("#{self} sync status is #{insync}") insync end # retrieve the current value of all contained properties def retrieve fail "Provider #{provider.class.name} is not functional on this host" if self.provider.is_a?(Puppet::Provider) and ! provider.class.suitable? result = Puppet::Resource.new(type, title) # Provide the name, so we know we'll always refer to a real thing result[:name] = self[:name] unless self[:name] == title if ensure_prop = property(:ensure) or (self.class.validattr?(:ensure) and ensure_prop = newattr(:ensure)) result[:ensure] = ensure_state = ensure_prop.retrieve else ensure_state = nil end properties.each do |property| next if property.name == :ensure if ensure_state == :absent result[property] = :absent else result[property] = property.retrieve end end result end def retrieve_resource resource = retrieve resource = Resource.new(type, title, :parameters => resource) if resource.is_a? Hash resource end # Get a hash of the current properties. Returns a hash with # the actual property instance as the key and the current value # as the, um, value. def currentpropvalues # It's important to use the 'properties' method here, as it follows the order # in which they're defined in the class. It also guarantees that 'ensure' # is the first property, which is important for skipping 'retrieve' on # all the properties if the resource is absent. ensure_state = false return properties.inject({}) do | prophash, property| if property.name == :ensure ensure_state = property.retrieve prophash[property] = ensure_state else if ensure_state == :absent prophash[property] = :absent else prophash[property] = property.retrieve end end prophash end end # Are we running in noop mode? def noop? # If we're not a host_config, we're almost certainly part of # Settings, and we want to ignore 'noop' return false if catalog and ! catalog.host_config? if defined?(@noop) @noop else Puppet[:noop] end end def noop noop? end ############################### # Code related to managing resource instances. require 'puppet/transportable' # retrieve a named instance of the current type def self.[](name) raise "Global resource access is deprecated" @objects[name] || @aliases[name] end # add an instance by name to the class list of instances def self.[]=(name,object) raise "Global resource storage is deprecated" newobj = nil if object.is_a?(Puppet::Type) newobj = object else raise Puppet::DevError, "must pass a Puppet::Type object" end if exobj = @objects[name] and self.isomorphic? msg = "Object '#{newobj.class.name}[#{name}]' already exists" msg += ("in file #{object.file} at line #{object.line}") if exobj.file and exobj.line msg += ("and cannot be redefined in file #{object.file} at line #{object.line}") if object.file and object.line error = Puppet::Error.new(msg) raise error else #Puppet.info("adding %s of type %s to class list" % # [name,object.class]) @objects[name] = newobj end end # Create an alias. We keep these in a separate hash so that we don't encounter # the objects multiple times when iterating over them. def self.alias(name, obj) raise "Global resource aliasing is deprecated" if @objects.include?(name) unless @objects[name] == obj raise Puppet::Error.new( "Cannot create alias #{name}: object already exists" ) end end if @aliases.include?(name) unless @aliases[name] == obj raise Puppet::Error.new( "Object #{@aliases[name].name} already has alias #{name}" ) end end @aliases[name] = obj end # remove all of the instances of a single type def self.clear raise "Global resource removal is deprecated" if defined?(@objects) @objects.each do |name, obj| obj.remove(true) end @objects.clear end @aliases.clear if defined?(@aliases) end # Force users to call this, so that we can merge objects if # necessary. def self.create(args) # LAK:DEP Deprecation notice added 12/17/2008 Puppet.warning "Puppet::Type.create is deprecated; use Puppet::Type.new" new(args) end # remove a specified object def self.delete(resource) raise "Global resource removal is deprecated" return unless defined?(@objects) @objects.delete(resource.title) if @objects.include?(resource.title) @aliases.delete(resource.title) if @aliases.include?(resource.title) if @aliases.has_value?(resource) names = [] @aliases.each do |name, otherres| if otherres == resource names << name end end names.each { |name| @aliases.delete(name) } end end # iterate across each of the type's instances def self.each raise "Global resource iteration is deprecated" return unless defined?(@objects) @objects.each { |name,instance| yield instance } end # does the type have an object with the given name? def self.has_key?(name) raise "Global resource access is deprecated" @objects.has_key?(name) end # Retrieve all known instances. Either requires providers or must be overridden. def self.instances raise Puppet::DevError, "#{self.name} has no providers and has not overridden 'instances'" if provider_hash.empty? # Put the default provider first, then the rest of the suitable providers. provider_instances = {} providers_by_source.collect do |provider| provider.instances.collect do |instance| # We always want to use the "first" provider instance we find, unless the resource # is already managed and has a different provider set if other = provider_instances[instance.name] Puppet.warning "%s %s found in both %s and %s; skipping the %s version" % [self.name.to_s.capitalize, instance.name, other.class.name, instance.class.name, instance.class.name] next end provider_instances[instance.name] = instance new(:name => instance.name, :provider => instance, :audit => :all) end end.flatten.compact end # Return a list of one suitable provider per source, with the default provider first. def self.providers_by_source # Put the default provider first, then the rest of the suitable providers. sources = [] [defaultprovider, suitableprovider].flatten.uniq.collect do |provider| next if sources.include?(provider.source) sources << provider.source provider end.compact end # Convert a simple hash into a Resource instance. def self.hash2resource(hash) hash = hash.inject({}) { |result, ary| result[ary[0].to_sym] = ary[1]; result } title = hash.delete(:title) title ||= hash[:name] title ||= hash[key_attributes.first] if key_attributes.length == 1 raise Puppet::Error, "Title or name must be provided" unless title # Now create our resource. resource = Puppet::Resource.new(self.name, title) [:catalog].each do |attribute| if value = hash[attribute] hash.delete(attribute) resource.send(attribute.to_s + "=", value) end end hash.each do |param, value| resource[param] = value end resource end # Create the path for logging and such. def pathbuilder if p = parent [p.pathbuilder, self.ref].flatten else [self.ref] end end ############################### # Add all of the meta parameters. newmetaparam(:noop) do desc "Boolean flag indicating whether work should actually be done." newvalues(:true, :false) munge do |value| case value when true, :true, "true"; @resource.noop = true when false, :false, "false"; @resource.noop = false end end end newmetaparam(:schedule) do desc "On what schedule the object should be managed. You must create a schedule object, and then reference the name of that object to use that for your schedule: schedule { daily: period => daily, range => \"2-4\" } exec { \"/usr/bin/apt-get update\": schedule => daily } The creation of the schedule object does not need to appear in the configuration before objects that use it." end newmetaparam(:audit) do desc "Marks a subset of this resource's unmanaged attributes for auditing. Accepts an attribute name or a list of attribute names. Auditing a resource attribute has two effects: First, whenever a catalog is applied with puppet apply or puppet agent, Puppet will check whether that attribute of the resource has been modified, comparing its current value to the previous run; any change will be logged alongside any actions performed by Puppet while applying the catalog. Secondly, marking a resource attribute for auditing will include that attribute in inspection reports generated by puppet inspect; see the puppet inspect documentation for more details. Managed attributes for a resource can also be audited, but note that changes made by Puppet will be logged as additional modifications. (I.e. if a user manually edits a file whose contents are audited and managed, puppet agent's next two runs will both log an audit notice: the first run will log the user's edit and then revert the file to the desired state, and the second run will log the edit made by Puppet.)" validate do |list| list = Array(list).collect {|p| p.to_sym} unless list == [:all] list.each do |param| next if @resource.class.validattr?(param) fail "Cannot audit #{param}: not a valid attribute for #{resource}" end end end munge do |args| properties_to_audit(args).each do |param| next unless resource.class.validproperty?(param) resource.newattr(param) end end def all_properties resource.class.properties.find_all do |property| resource.provider.nil? or resource.provider.class.supports_parameter?(property) end.collect do |property| property.name end end def properties_to_audit(list) if !list.kind_of?(Array) && list.to_sym == :all list = all_properties else list = Array(list).collect { |p| p.to_sym } end end end newmetaparam(:check) do desc "Audit specified attributes of resources over time, and report if any have changed. This parameter has been deprecated in favor of 'audit'." munge do |args| resource.warning "'check' attribute is deprecated; use 'audit' instead" resource[:audit] = args end end newmetaparam(:loglevel) do desc "Sets the level that information will be logged. The log levels have the biggest impact when logs are sent to syslog (which is currently the default)." defaultto :notice newvalues(*Puppet::Util::Log.levels) newvalues(:verbose) munge do |loglevel| val = super(loglevel) if val == :verbose val = :info end val end end newmetaparam(:alias) do desc "Creates an alias for the object. Puppet uses this internally when you provide a symbolic name: file { sshdconfig: path => $operatingsystem ? { solaris => \"/usr/local/etc/ssh/sshd_config\", default => \"/etc/ssh/sshd_config\" }, source => \"...\" } service { sshd: subscribe => File[sshdconfig] } When you use this feature, the parser sets `sshdconfig` as the name, and the library sets that as an alias for the file so the dependency lookup for `sshd` works. You can use this parameter yourself, but note that only the library can use these aliases; for instance, the following code will not work: file { \"/etc/ssh/sshd_config\": owner => root, group => root, alias => sshdconfig } file { sshdconfig: mode => 644 } There's no way here for the Puppet parser to know that these two stanzas should be affecting the same file. See the [Language Tutorial](http://docs.puppetlabs.com/guides/language_tutorial.html) for more information. " munge do |aliases| aliases = [aliases] unless aliases.is_a?(Array) raise(ArgumentError, "Cannot add aliases without a catalog") unless @resource.catalog aliases.each do |other| if obj = @resource.catalog.resource(@resource.class.name, other) unless obj.object_id == @resource.object_id self.fail("#{@resource.title} can not create alias #{other}: object already exists") end next end # Newschool, add it to the catalog. @resource.catalog.alias(@resource, other) end end end newmetaparam(:tag) do desc "Add the specified tags to the associated resource. While all resources are automatically tagged with as much information as possible (e.g., each class and definition containing the resource), it can be useful to add your own tags to a given resource. Tags are currently useful for things like applying a subset of a host's configuration: puppet agent --test --tags mytag This way, when you're testing a configuration you can run just the portion you're testing." munge do |tags| tags = [tags] unless tags.is_a? Array tags.each do |tag| @resource.tag(tag) end end end class RelationshipMetaparam < Puppet::Parameter class << self attr_accessor :direction, :events, :callback, :subclasses end @subclasses = [] def self.inherited(sub) @subclasses << sub end def munge(references) references = [references] unless references.is_a?(Array) references.collect do |ref| if ref.is_a?(Puppet::Resource) ref else Puppet::Resource.new(ref) end end end def validate_relationship @value.each do |ref| unless @resource.catalog.resource(ref.to_s) description = self.class.direction == :in ? "dependency" : "dependent" fail "Could not find #{description} #{ref} for #{resource.ref}" end end end # Create edges from each of our relationships. :in # relationships are specified by the event-receivers, and :out # relationships are specified by the event generator. This # way 'source' and 'target' are consistent terms in both edges # and events -- that is, an event targets edges whose source matches # the event's source. The direction of the relationship determines # which resource is applied first and which resource is considered # to be the event generator. def to_edges @value.collect do |reference| reference.catalog = resource.catalog # Either of the two retrieval attempts could have returned # nil. unless related_resource = reference.resolve self.fail "Could not retrieve dependency '#{reference}' of #{@resource.ref}" end # Are we requiring them, or vice versa? See the method docs # for futher info on this. if self.class.direction == :in source = related_resource target = @resource else source = @resource target = related_resource end if method = self.class.callback subargs = { :event => self.class.events, :callback => method } self.debug("subscribes to #{related_resource.ref}") else # If there's no callback, there's no point in even adding # a label. subargs = nil self.debug("requires #{related_resource.ref}") end rel = Puppet::Relationship.new(source, target, subargs) end end end def self.relationship_params RelationshipMetaparam.subclasses end # Note that the order in which the relationships params is defined # matters. The labelled params (notify and subcribe) must be later, # so that if both params are used, those ones win. It's a hackish # solution, but it works. newmetaparam(:require, :parent => RelationshipMetaparam, :attributes => {:direction => :in, :events => :NONE}) do desc "One or more objects that this object depends on. This is used purely for guaranteeing that changes to required objects happen before the dependent object. For instance: # Create the destination directory before you copy things down file { \"/usr/local/scripts\": ensure => directory } file { \"/usr/local/scripts/myscript\": source => \"puppet://server/module/myscript\", mode => 755, require => File[\"/usr/local/scripts\"] } Multiple dependencies can be specified by providing a comma-seperated list of resources, enclosed in square brackets: require => [ File[\"/usr/local\"], File[\"/usr/local/scripts\"] ] Note that Puppet will autorequire everything that it can, and there are hooks in place so that it's easy for resources to add new ways to autorequire objects, so if you think Puppet could be smarter here, let us know. In fact, the above code was redundant -- Puppet will autorequire any parent directories that are being managed; it will automatically realize that the parent directory should be created before the script is pulled down. Currently, exec resources will autorequire their CWD (if it is specified) plus any fully qualified paths that appear in the command. For instance, if you had an `exec` command that ran the `myscript` mentioned above, the above code that pulls the file down would be automatically listed as a requirement to the `exec` code, so that you would always be running againts the most recent version. " end newmetaparam(:subscribe, :parent => RelationshipMetaparam, :attributes => {:direction => :in, :events => :ALL_EVENTS, :callback => :refresh}) do desc "One or more objects that this object depends on. Changes in the subscribed to objects result in the dependent objects being refreshed (e.g., a service will get restarted). For instance: class nagios { file { \"/etc/nagios/nagios.conf\": source => \"puppet://server/module/nagios.conf\", alias => nagconf # just to make things easier for me } service { nagios: ensure => running, subscribe => File[nagconf] } } Currently the `exec`, `mount` and `service` type support refreshing. " end newmetaparam(:before, :parent => RelationshipMetaparam, :attributes => {:direction => :out, :events => :NONE}) do desc %{This parameter is the opposite of **require** -- it guarantees that the specified object is applied later than the specifying object: file { "/var/nagios/configuration": source => "...", recurse => true, before => Exec["nagios-rebuid"] } exec { "nagios-rebuild": command => "/usr/bin/make", cwd => "/var/nagios/configuration" } This will make sure all of the files are up to date before the make command is run.} end newmetaparam(:notify, :parent => RelationshipMetaparam, :attributes => {:direction => :out, :events => :ALL_EVENTS, :callback => :refresh}) do desc %{This parameter is the opposite of **subscribe** -- it sends events to the specified object: file { "/etc/sshd_config": source => "....", notify => Service[sshd] } service { sshd: ensure => running } This will restart the sshd service if the sshd config file changes.} end newmetaparam(:stage) do desc %{Which run stage a given resource should reside in. This just creates a dependency on or from the named milestone. For instance, saying that this is in the 'bootstrap' stage creates a dependency on the 'bootstrap' milestone. By default, all classes get directly added to the 'main' stage. You can create new stages as resources: stage { [pre, post]: } To order stages, use standard relationships: stage { pre: before => Stage[main] } Or use the new relationship syntax: Stage[pre] -> Stage[main] -> Stage[post] Then use the new class parameters to specify a stage: class { foo: stage => pre } Stages can only be set on classes, not individual resources. This will fail: file { '/foo': stage => pre, ensure => file } } end ############################### # All of the provider plumbing for the resource types. require 'puppet/provider' require 'puppet/util/provider_features' # Add the feature handling module. extend Puppet::Util::ProviderFeatures attr_reader :provider # the Type class attribute accessors class << self attr_accessor :providerloader attr_writer :defaultprovider end # Find the default provider. def self.defaultprovider unless @defaultprovider suitable = suitableprovider # Find which providers are a default for this system. defaults = suitable.find_all { |provider| provider.default? } # If we don't have any default we use suitable providers defaults = suitable if defaults.empty? max = defaults.collect { |provider| provider.specificity }.max defaults = defaults.find_all { |provider| provider.specificity == max } retval = nil if defaults.length > 1 Puppet.warning( "Found multiple default providers for #{self.name}: #{defaults.collect { |i| i.name.to_s }.join(", ")}; using #{defaults[0].name}" ) retval = defaults.shift elsif defaults.length == 1 retval = defaults.shift else raise Puppet::DevError, "Could not find a default provider for #{self.name}" end @defaultprovider = retval end @defaultprovider end def self.provider_hash_by_type(type) @provider_hashes ||= {} @provider_hashes[type] ||= {} end def self.provider_hash Puppet::Type.provider_hash_by_type(self.name) end # Retrieve a provider by name. def self.provider(name) name = Puppet::Util.symbolize(name) # If we don't have it yet, try loading it. @providerloader.load(name) unless provider_hash.has_key?(name) provider_hash[name] end # Just list all of the providers. def self.providers provider_hash.keys end def self.validprovider?(name) name = Puppet::Util.symbolize(name) (provider_hash.has_key?(name) && provider_hash[name].suitable?) end # Create a new provider of a type. This method must be called # directly on the type that it's implementing. def self.provide(name, options = {}, &block) name = Puppet::Util.symbolize(name) if obj = provider_hash[name] Puppet.debug "Reloading #{name} #{self.name} provider" unprovide(name) end parent = if pname = options[:parent] options.delete(:parent) if pname.is_a? Class pname else if provider = self.provider(pname) provider else raise Puppet::DevError, "Could not find parent provider #{pname} of #{name}" end end else Puppet::Provider end options[:resource_type] ||= self self.providify provider = genclass( name, :parent => parent, :hash => provider_hash, :prefix => "Provider", :block => block, :include => feature_module, :extend => feature_module, :attributes => options ) provider end # Make sure we have a :provider parameter defined. Only gets called if there # are providers. def self.providify return if @paramhash.has_key? :provider newparam(:provider) do desc "The specific backend for #{self.name.to_s} to use. You will seldom need to specify this -- Puppet will usually discover the appropriate provider for your platform." # This is so we can refer back to the type to get a list of # providers for documentation. class << self attr_accessor :parenttype end # We need to add documentation for each provider. def self.doc @doc + " Available providers are:\n\n" + parenttype.providers.sort { |a,b| a.to_s <=> b.to_s }.collect { |i| "* **#{i}**: #{parenttype().provider(i).doc}" }.join("\n") end defaultto { @resource.class.defaultprovider.name } validate do |provider_class| provider_class = provider_class[0] if provider_class.is_a? Array provider_class = provider_class.class.name if provider_class.is_a?(Puppet::Provider) unless provider = @resource.class.provider(provider_class) raise ArgumentError, "Invalid #{@resource.class.name} provider '#{provider_class}'" end end munge do |provider| provider = provider[0] if provider.is_a? Array provider = provider.intern if provider.is_a? String @resource.provider = provider if provider.is_a?(Puppet::Provider) provider.class.name else provider end end end.parenttype = self end def self.unprovide(name) if provider_hash.has_key? name rmclass( name, :hash => provider_hash, :prefix => "Provider" ) if @defaultprovider and @defaultprovider.name == name @defaultprovider = nil end end end # Return an array of all of the suitable providers. def self.suitableprovider providerloader.loadall if provider_hash.empty? provider_hash.find_all { |name, provider| provider.suitable? }.collect { |name, provider| provider }.reject { |p| p.name == :fake } # For testing end def provider=(name) if name.is_a?(Puppet::Provider) @provider = name @provider.resource = self elsif klass = self.class.provider(name) @provider = klass.new(self) else raise ArgumentError, "Could not find #{name} provider of #{self.class.name}" end end ############################### # All of the relationship code. # Specify a block for generating a list of objects to autorequire. This # makes it so that you don't have to manually specify things that you clearly # require. def self.autorequire(name, &block) @autorequires ||= {} @autorequires[name] = block end # Yield each of those autorequires in turn, yo. def self.eachautorequire @autorequires ||= {} @autorequires.each { |type, block| yield(type, block) } end # Figure out of there are any objects we can automatically add as # dependencies. def autorequire(rel_catalog = nil) rel_catalog ||= catalog raise(Puppet::DevError, "You cannot add relationships without a catalog") unless rel_catalog reqs = [] self.class.eachautorequire { |type, block| # Ignore any types we can't find, although that would be a bit odd. next unless typeobj = Puppet::Type.type(type) # Retrieve the list of names from the block. next unless list = self.instance_eval(&block) list = [list] unless list.is_a?(Array) # Collect the current prereqs list.each { |dep| obj = nil # Support them passing objects directly, to save some effort. unless dep.is_a? Puppet::Type # Skip autorequires that we aren't managing unless dep = rel_catalog.resource(type, dep) next end end reqs << Puppet::Relationship.new(dep, self) } } reqs end # Build the dependencies associated with an individual object. def builddepends # Handle the requires self.class.relationship_params.collect do |klass| if param = @parameters[klass.name] param.to_edges end end.flatten.reject { |r| r.nil? } end # Define the initial list of tags. def tags=(list) tag(self.class.name) tag(*list) end # Types (which map to resources in the languages) are entirely composed of # attribute value pairs. Generally, Puppet calls any of these things an # 'attribute', but these attributes always take one of three specific # forms: parameters, metaparams, or properties. # In naming methods, I have tried to consistently name the method so # that it is clear whether it operates on all attributes (thus has 'attr' in # the method name, or whether it operates on a specific type of attributes. attr_writer :title attr_writer :noop include Enumerable # class methods dealing with Type management public # the Type class attribute accessors class << self attr_reader :name attr_accessor :self_refresh include Enumerable, Puppet::Util::ClassGen include Puppet::MetaType::Manager include Puppet::Util include Puppet::Util::Logging end # all of the variables that must be initialized for each subclass def self.initvars # all of the instances of this class @objects = Hash.new @aliases = Hash.new @defaults = {} @parameters ||= [] @validproperties = {} @properties = [] @parameters = [] @paramhash = {} @attr_aliases = {} @paramdoc = Hash.new { |hash,key| key = key.intern if key.is_a?(String) if hash.include?(key) hash[key] else "Param Documentation for #{key} not found" end } @doc ||= "" end def self.to_s if defined?(@name) "Puppet::Type::#{@name.to_s.capitalize}" else super end end # Create a block to validate that our object is set up entirely. This will # be run before the object is operated on. def self.validate(&block) define_method(:validate, &block) #@validate = block end # The catalog that this resource is stored in. attr_accessor :catalog # is the resource exported attr_accessor :exported # is the resource virtual (it should not :-)) attr_accessor :virtual # create a log at specified level def log(msg) Puppet::Util::Log.create( :level => @parameters[:loglevel].value, :message => msg, :source => self ) end # instance methods related to instance intrinsics # e.g., initialize and name public attr_reader :original_parameters # initialize the type instance def initialize(resource) raise Puppet::DevError, "Got TransObject instead of Resource or hash" if resource.is_a?(Puppet::TransObject) resource = self.class.hash2resource(resource) unless resource.is_a?(Puppet::Resource) # The list of parameter/property instances. @parameters = {} # Set the title first, so any failures print correctly. if resource.type.to_s.downcase.to_sym == self.class.name self.title = resource.title else # This should only ever happen for components self.title = resource.ref end [:file, :line, :catalog, :exported, :virtual].each do |getter| setter = getter.to_s + "=" if val = resource.send(getter) self.send(setter, val) end end @tags = resource.tags @original_parameters = resource.to_hash set_name(@original_parameters) set_default(:provider) set_parameters(@original_parameters) self.validate if self.respond_to?(:validate) end private # Set our resource's name. def set_name(hash) self[name_var] = hash.delete(name_var) if name_var end # Set all of the parameters from a hash, in the appropriate order. def set_parameters(hash) # Use the order provided by allattrs, but add in any # extra attributes from the resource so we get failures # on invalid attributes. no_values = [] (self.class.allattrs + hash.keys).uniq.each do |attr| begin # Set any defaults immediately. This is mostly done so # that the default provider is available for any other # property validation. if hash.has_key?(attr) self[attr] = hash[attr] else no_values << attr end rescue ArgumentError, Puppet::Error, TypeError raise rescue => detail error = Puppet::DevError.new( "Could not set #{attr} on #{self.class.name}: #{detail}") error.set_backtrace(detail.backtrace) raise error end end no_values.each do |attr| set_default(attr) end end public # Set up all of our autorequires. def finish # Make sure all of our relationships are valid. Again, must be done # when the entire catalog is instantiated. self.class.relationship_params.collect do |klass| if param = @parameters[klass.name] param.validate_relationship end end.flatten.reject { |r| r.nil? } end # For now, leave the 'name' method functioning like it used to. Once 'title' # works everywhere, I'll switch it. def name self[:name] end # Look up our parent in the catalog, if we have one. def parent return nil unless catalog unless defined?(@parent) if parents = catalog.adjacent(self, :direction => :in) # We should never have more than one parent, so let's just ignore # it if we happen to. @parent = parents.shift else @parent = nil end end @parent end # Return the "type[name]" style reference. def ref "#{self.class.name.to_s.capitalize}[#{self.title}]" end def self_refresh? self.class.self_refresh end # Mark that we're purging. def purging @purging = true end # Is this resource being purged? Used by transactions to forbid # deletion when there are dependencies. def purging? if defined?(@purging) @purging else false end end # Retrieve the title of an object. If no title was set separately, # then use the object's name. def title unless @title if self.class.validparameter?(name_var) @title = self[:name] elsif self.class.validproperty?(name_var) @title = self.should(name_var) else self.devfail "Could not find namevar #{name_var} for #{self.class.name}" end end @title end # convert to a string def to_s self.ref end # Convert to a transportable object def to_trans(ret = true) trans = TransObject.new(self.title, self.class.name) values = retrieve_resource values.each do |name, value| name = name.name if name.respond_to? :name trans[name] = value end @parameters.each do |name, param| # Avoid adding each instance name twice next if param.class.isnamevar? and param.value == self.title # We've already got property values next if param.is_a?(Puppet::Property) trans[name] = param.value end trans.tags = self.tags # FIXME I'm currently ignoring 'parent' and 'path' trans end def to_resource # this 'type instance' versus 'resource' distinction seems artificial # I'd like to see it collapsed someday ~JW self.to_trans.to_resource end def virtual?; !!@virtual; end def exported?; !!@exported; end end end require 'puppet/provider' # Always load these types. require 'puppet/type/component' diff --git a/lib/puppet/type/augeas.rb b/lib/puppet/type/augeas.rb index d29bda648..a8fb1f15f 100644 --- a/lib/puppet/type/augeas.rb +++ b/lib/puppet/type/augeas.rb @@ -1,182 +1,182 @@ #-- # Copyright (C) 2008 Red Hat Inc. # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public # License as published by the Free Software Foundation; either # version 2 of the License, or (at your option) any later version. # # This library is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # General Public License for more details. # # You should have received a copy of the GNU General Public # License along with this library; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # # Author: Bryan Kearney Puppet::Type.newtype(:augeas) do include Puppet::Util feature :parse_commands, "Parse the command string" feature :need_to_run?, "If the command should run" feature :execute_changes, "Actually make the changes" @doc = "Apply the changes (single or array of changes) to the filesystem via the augeas tool. Requires: - augeas to be installed (http://www.augeas.net) - ruby-augeas bindings Sample usage with a string: augeas{\"test1\" : context => \"/files/etc/sysconfig/firstboot\", changes => \"set RUN_FIRSTBOOT YES\", onlyif => \"match other_value size > 0\", } Sample usage with an array and custom lenses: augeas{\"jboss_conf\": context => \"/files\", changes => [ \"set /etc/jbossas/jbossas.conf/JBOSS_IP $ipaddress\", \"set /etc/jbossas/jbossas.conf/JAVA_HOME /usr\" ], load_path => \"$/usr/share/jbossas/lenses\", } " newparam (:name) do desc "The name of this task. Used for uniqueness" isnamevar end newparam (:context) do desc "Optional context path. This value is prepended to the paths of all changes if the path is relative. If INCL is set, defaults to '/files' + INCL, otherwise the empty string" defaultto "" munge do |value| if value.empty? and resource[:incl] "/files" + resource[:incl] else value end end end newparam (:onlyif) do desc "Optional augeas command and comparisons to control the execution of this type. Supported onlyif syntax: get [AUGEAS_PATH] [COMPARATOR] [STRING] match [MATCH_PATH] size [COMPARATOR] [INT] match [MATCH_PATH] include [STRING] match [MATCH_PATH] not_include [STRING] match [MATCH_PATH] == [AN_ARRAY] match [MATCH_PATH] != [AN_ARRAY] where: AUGEAS_PATH is a valid path scoped by the context MATCH_PATH is a valid match synatx scoped by the context COMPARATOR is in the set [> >= != == <= <] STRING is a string INT is a number AN_ARRAY is in the form ['a string', 'another']" defaultto "" end newparam(:changes) do desc "The changes which should be applied to the filesystem. This can be either a string which contains a command or an array of commands. Commands supported are: - set [PATH] [VALUE] Sets the value VALUE at loction PATH - rm [PATH] Removes the node at location PATH - remove [PATH] Synonym for rm - clear [PATH] Keeps the node at PATH, but removes the value. + set [PATH] [VALUE] Sets the value VALUE at loction PATH + rm [PATH] Removes the node at location PATH + remove [PATH] Synonym for rm + clear [PATH] Keeps the node at PATH, but removes the value. ins [LABEL] [WHERE] [PATH] Inserts an empty node LABEL either [WHERE={before|after}] PATH. insert [LABEL] [WHERE] [PATH] Synonym for ins If the parameter 'context' is set that value is prepended to PATH" end newparam(:root) do desc "A file system path; all files loaded by Augeas are loaded underneath ROOT" defaultto "/" end newparam(:load_path) do desc "Optional colon separated list of directories; these directories are searched for schema definitions" defaultto "" end newparam(:force) do desc "Optional command to force the augeas type to execute even if it thinks changes will not be made. This does not overide the only setting. If onlyif is set, then the foce setting will not override that result" defaultto false end newparam(:type_check) do desc "Set to true if augeas should perform typechecking. Optional, defaults to false" newvalues(:true, :false) defaultto :false end newparam(:lens) do desc "Use a specific lens, e.g. `Hosts.lns`. When this parameter is set, you must also set the incl parameter to indicate which file to load. Only that file will be loaded, which greatly speeds up execution of the type" end newparam(:incl) do desc "Load only a specific file, e.g. `/etc/hosts`. When this parameter is set, you must also set the lens parameter to indicate which lens to use." end validate do has_lens = !self[:lens].nil? has_incl = !self[:incl].nil? self.fail "You must specify both the lens and incl parameters, or neither" if has_lens != has_incl end # This is the acutal meat of the code. It forces # augeas to be run and fails or not based on the augeas return # code. newproperty(:returns) do |property| include Puppet::Util desc "The expected return code from the augeas command. Should not be set" defaultto 0 # Make output a bit prettier def change_to_s(currentvalue, newvalue) "executed successfully" end # if the onlyif resource is provided, then the value is parsed. # a return value of 0 will stop exection because it matches the # default value. def retrieve if @resource.provider.need_to_run?() :need_to_run else 0 end end # Actually execute the command. def sync @resource.provider.execute_changes end end end diff --git a/lib/puppet/type/computer.rb b/lib/puppet/type/computer.rb index 89a0692bf..7a2c52d53 100644 --- a/lib/puppet/type/computer.rb +++ b/lib/puppet/type/computer.rb @@ -1,62 +1,66 @@ Puppet::Type.newtype(:computer) do @doc = "Computer object management using DirectoryService on OS X. Note that these are distinctly different kinds of objects to 'hosts', as they require a MAC address and can have all sorts of policy attached to them. This provider only manages Computer objects in the local directory service domain, not in remote directories. If you wish to manage `/etc/hosts` file on Mac OS X, then simply use the host type as per other platforms. This type primarily exists to create localhost Computer objects that MCX - policy can then be attached to." + policy can then be attached to. + + **Autorequires:** If Puppet is managing the plist file representing a + Computer object (located at `/var/db/dslocal/nodes/Default/computers/{name}.plist`), + the Computer resource will autorequire it." # ensurable # We autorequire the computer object in case it is being managed at the # file level by Puppet. autorequire(:file) do if self[:name] "/var/db/dslocal/nodes/Default/computers/#{self[:name]}.plist" else nil end end newproperty(:ensure, :parent => Puppet::Property::Ensure) do desc "Control the existences of this computer record. Set this attribute to `present` to ensure the computer record exists. Set it to `absent` to delete any computer records with this name" newvalue(:present) do provider.create end newvalue(:absent) do provider.delete end end newparam(:name) do desc "The authoritative 'short' name of the computer record." isnamevar end newparam(:realname) do desc "The 'long' name of the computer record." end newproperty(:en_address) do desc "The MAC address of the primary network interface. Must match en0." end newproperty(:ip_address) do desc "The IP Address of the Computer object." end end diff --git a/lib/puppet/type/exec.rb b/lib/puppet/type/exec.rb index daa49e223..5ed2b104c 100755 --- a/lib/puppet/type/exec.rb +++ b/lib/puppet/type/exec.rb @@ -1,649 +1,651 @@ module Puppet newtype(:exec) do include Puppet::Util::Execution require 'timeout' @doc = "Executes external commands. It is critical that all commands executed using this mechanism can be run multiple times without harm, i.e., they are *idempotent*. One useful way to create idempotent commands is to use the checks like `creates` to avoid running the command unless some condition is met. Note that you can restrict an `exec` to only run when it receives events by using the `refreshonly` parameter; this is a useful way to have your configuration respond to events with arbitrary commands. Note also that if an `exec` receives an event from another resource, it will get executed again (or execute the command specified in `refresh`, if there is one). There is a strong tendency to use `exec` to do whatever work Puppet can't already do; while this is obviously acceptable (and unavoidable) in the short term, it is highly recommended to migrate work from `exec` to native Puppet types as quickly as possible. If you find that you are doing a lot of work with `exec`, please at least notify us at Puppet Labs what you are doing, and hopefully we can work with - you to get a native resource type for the work you are doing." + you to get a native resource type for the work you are doing. + + **Autorequires:** If Puppet is managing an exec's cwd or the executable file used in an exec's command, the exec resource will autorequire those files. If Puppet is managing the user that an exec should run as, the exec resource will autorequire that user." require 'open3' # Create a new check mechanism. It's basically just a parameter that # provides one extra 'check' method. def self.newcheck(name, &block) @checks ||= {} check = newparam(name, &block) @checks[name] = check end def self.checks @checks.keys end newproperty(:returns, :array_matching => :all, :event => :executed_command) do |property| include Puppet::Util::Execution munge do |value| value.to_s end def event_name :executed_command end defaultto "0" attr_reader :output desc "The expected return code(s). An error will be returned if the executed command returns something else. Defaults to 0. Can be specified as an array of acceptable return codes or a single value." # Make output a bit prettier def change_to_s(currentvalue, newvalue) "executed successfully" end # First verify that all of our checks pass. def retrieve # Default to somethinng if @resource.check return :notrun else return self.should end end # Actually execute the command. def sync olddir = nil # We need a dir to change to, even if it's just the cwd dir = self.resource[:cwd] || Dir.pwd event = :executed_command tries = self.resource[:tries] try_sleep = self.resource[:try_sleep] begin tries.times do |try| # Only add debug messages for tries > 1 to reduce log spam. debug("Exec try #{try+1}/#{tries}") if tries > 1 @output, @status = @resource.run(self.resource[:command]) break if self.should.include?(@status.exitstatus.to_s) if try_sleep > 0 and tries > 1 debug("Sleeping for #{try_sleep} seconds between tries") sleep try_sleep end end rescue Timeout::Error self.fail "Command exceeded timeout" % value.inspect end if log = @resource[:logoutput] case log when :true log = @resource[:loglevel] when :on_failure unless self.should.include?(@status.exitstatus.to_s) log = @resource[:loglevel] else log = :false end end unless log == :false @output.split(/\n/).each { |line| self.send(log, line) } end end unless self.should.include?(@status.exitstatus.to_s) self.fail("#{self.resource[:command]} returned #{@status.exitstatus} instead of one of [#{self.should.join(",")}]") end event end end newparam(:command) do isnamevar desc "The actual command to execute. Must either be fully qualified or a search path for the command must be provided. If the command succeeds, any output produced will be logged at the instance's normal log level (usually `notice`), but if the command fails (meaning its return code does not match the specified code) then any output is logged at the `err` log level." end newparam(:path) do desc "The search path used for command execution. Commands must be fully qualified if no path is specified. Paths can be specified as an array or as a colon-separated list." # Support both arrays and colon-separated fields. def value=(*values) @value = values.flatten.collect { |val| if val =~ /;/ # recognize semi-colon separated paths val.split(";") elsif val =~ /^\w:[^:]*$/ # heuristic to avoid splitting a driveletter away val else val.split(":") end }.flatten end end newparam(:user) do desc "The user to run the command as. Note that if you use this then any error output is not currently captured. This is because of a bug within Ruby. If you are using Puppet to create this user, the exec will automatically require the user, as long as it is specified by name." # Most validation is handled by the SUIDManager class. validate do |user| self.fail "Only root can execute commands as other users" unless Puppet.features.root? end end newparam(:group) do desc "The group to run the command as. This seems to work quite haphazardly on different platforms -- it is a platform issue not a Ruby or Puppet one, since the same variety exists when running commnands as different users in the shell." # Validation is handled by the SUIDManager class. end newparam(:cwd) do desc "The directory from which to run the command. If this directory does not exist, the command will fail." validate do |dir| unless dir =~ /^#{File::SEPARATOR}/ self.fail("CWD must be a fully qualified path") end end munge do |dir| dir = dir[0] if dir.is_a?(Array) dir end end newparam(:logoutput) do desc "Whether to log output. Defaults to logging output at the loglevel for the `exec` resource. Use *on_failure* to only log the output when the command reports an error. Values are **true**, *false*, *on_failure*, and any legal log level." newvalues(:true, :false, :on_failure) end newparam(:refresh) do desc "How to refresh this command. By default, the exec is just called again when it receives an event from another resource, but this parameter allows you to define a different command for refreshing." validate do |command| @resource.validatecmd(command) end end newparam(:env) do desc "This parameter is deprecated. Use 'environment' instead." munge do |value| warning "'env' is deprecated on exec; use 'environment' instead." resource[:environment] = value end end newparam(:environment) do desc "Any additional environment variables you want to set for a command. Note that if you use this to set PATH, it will override the `path` attribute. Multiple environment variables should be specified as an array." validate do |values| values = [values] unless values.is_a? Array values.each do |value| unless value =~ /\w+=/ raise ArgumentError, "Invalid environment setting '#{value}'" end end end end newparam(:timeout) do desc "The maximum time the command should take. If the command takes longer than the timeout, the command is considered to have failed and will be stopped. Use any negative number to disable the timeout. The time is specified in seconds." munge do |value| value = value.shift if value.is_a?(Array) if value.is_a?(String) unless value =~ /^[-\d.]+$/ raise ArgumentError, "The timeout must be a number." end Float(value) else value end end defaultto 300 end newparam(:tries) do desc "The number of times execution of the command should be tried. Defaults to '1'. This many attempts will be made to execute the command until an acceptable return code is returned. Note that the timeout paramater applies to each try rather than to the complete set of tries." munge do |value| if value.is_a?(String) unless value =~ /^[\d]+$/ raise ArgumentError, "Tries must be an integer" end value = Integer(value) end raise ArgumentError, "Tries must be an integer >= 1" if value < 1 value end defaultto 1 end newparam(:try_sleep) do desc "The time to sleep in seconds between 'tries'." munge do |value| if value.is_a?(String) unless value =~ /^[-\d.]+$/ raise ArgumentError, "try_sleep must be a number" end value = Float(value) end raise ArgumentError, "try_sleep cannot be a negative number" if value < 0 value end defaultto 0 end newcheck(:refreshonly) do desc "The command should only be run as a refresh mechanism for when a dependent object is changed. It only makes sense to use this option when this command depends on some other object; it is useful for triggering an action: # Pull down the main aliases file file { \"/etc/aliases\": source => \"puppet://server/module/aliases\" } # Rebuild the database, but only when the file changes exec { newaliases: path => [\"/usr/bin\", \"/usr/sbin\"], subscribe => File[\"/etc/aliases\"], refreshonly => true } Note that only `subscribe` and `notify` can trigger actions, not `require`, so it only makes sense to use `refreshonly` with `subscribe` or `notify`." newvalues(:true, :false) # We always fail this test, because we're only supposed to run # on refresh. def check(value) # We have to invert the values. if value == :true false else true end end end newcheck(:creates) do desc "A file that this command creates. If this parameter is provided, then the command will only be run if the specified file does not exist: exec { \"tar xf /my/tar/file.tar\": cwd => \"/var/tmp\", creates => \"/var/tmp/myfile\", path => [\"/usr/bin\", \"/usr/sbin\"] } " # FIXME if they try to set this and fail, then we should probably # fail the entire exec, right? validate do |files| files = [files] unless files.is_a? Array files.each do |file| self.fail("'creates' must be set to a fully qualified path") unless file unless file =~ %r{^#{File::SEPARATOR}} self.fail "'creates' files must be fully qualified." end end end # If the file exists, return false (i.e., don't run the command), # else return true def check(value) ! FileTest.exists?(value) end end newcheck(:unless) do desc "If this parameter is set, then this `exec` will run unless the command returns 0. For example: exec { \"/bin/echo root >> /usr/lib/cron/cron.allow\": path => \"/usr/bin:/usr/sbin:/bin\", unless => \"grep root /usr/lib/cron/cron.allow 2>/dev/null\" } This would add `root` to the cron.allow file (on Solaris) unless `grep` determines it's already there. Note that this command follows the same rules as the main command, which is to say that it must be fully qualified if the path is not set. " validate do |cmds| cmds = [cmds] unless cmds.is_a? Array cmds.each do |cmd| @resource.validatecmd(cmd) end end # Return true if the command does not return 0. def check(value) begin output, status = @resource.run(value, true) rescue Timeout::Error err "Check #{value.inspect} exceeded timeout" return false end status.exitstatus != 0 end end newcheck(:onlyif) do desc "If this parameter is set, then this `exec` will only run if the command returns 0. For example: exec { \"logrotate\": path => \"/usr/bin:/usr/sbin:/bin\", onlyif => \"test `du /var/log/messages | cut -f1` -gt 100000\" } This would run `logrotate` only if that test returned true. Note that this command follows the same rules as the main command, which is to say that it must be fully qualified if the path is not set. Also note that onlyif can take an array as its value, e.g.: onlyif => [\"test -f /tmp/file1\", \"test -f /tmp/file2\"] This will only run the exec if /all/ conditions in the array return true. " validate do |cmds| cmds = [cmds] unless cmds.is_a? Array cmds.each do |cmd| @resource.validatecmd(cmd) end end # Return true if the command returns 0. def check(value) begin output, status = @resource.run(value, true) rescue Timeout::Error err "Check #{value.inspect} exceeded timeout" return false end status.exitstatus == 0 end end # Exec names are not isomorphic with the objects. @isomorphic = false validate do validatecmd(self[:command]) end # FIXME exec should autorequire any exec that 'creates' our cwd autorequire(:file) do reqs = [] # Stick the cwd in there if we have it reqs << self[:cwd] if self[:cwd] self[:command].scan(/^(#{File::SEPARATOR}\S+)/) { |str| reqs << str } self[:command].scan(/^"([^"]+)"/) { |str| reqs << str } [:onlyif, :unless].each { |param| next unless tmp = self[param] tmp = [tmp] unless tmp.is_a? Array tmp.each do |line| # And search the command line for files, adding any we # find. This will also catch the command itself if it's # fully qualified. It might not be a bad idea to add # unqualified files, but, well, that's a bit more annoying # to do. reqs += line.scan(%r{(#{File::SEPARATOR}\S+)}) end } # For some reason, the += isn't causing a flattening reqs.flatten! reqs end autorequire(:user) do # Autorequire users if they are specified by name if user = self[:user] and user !~ /^\d+$/ user end end def self.instances [] end # Verify that we pass all of the checks. The argument determines whether # we skip the :refreshonly check, which is necessary because we now check # within refresh def check(refreshing = false) self.class.checks.each { |check| next if refreshing and check == :refreshonly if @parameters.include?(check) val = @parameters[check].value val = [val] unless val.is_a? Array val.each do |value| return false unless @parameters[check].check(value) end end } true end # Verify that we have the executable def checkexe(cmd) exe = extractexe(cmd) if self[:path] if Puppet.features.posix? and !File.exists?(exe) withenv :PATH => self[:path].join(File::PATH_SEPARATOR) do exe = which(exe) || raise(ArgumentError,"Could not find command '#{exe}'") end elsif Puppet.features.microsoft_windows? and !File.exists?(exe) self[:path].each do |path| [".exe", ".ps1", ".bat", ".com", ""].each do |extension| file = File.join(path, exe+extension) return if File.exists?(file) end end end end raise ArgumentError, "Could not find executable '#{exe}'" unless FileTest.exists?(exe) unless FileTest.executable?(exe) raise ArgumentError, "'#{exe}' is not executable" end end def output if self.property(:returns).nil? return nil else return self.property(:returns).output end end # Run the command, or optionally run a separately-specified command. def refresh if self.check(true) if cmd = self[:refresh] self.run(cmd) else self.property(:returns).sync end end end # Run a command. def run(command, check = false) output = nil status = nil dir = nil checkexe(command) if dir = self[:cwd] unless File.directory?(dir) if check dir = nil else self.fail "Working directory '#{dir}' does not exist" end end end dir ||= Dir.pwd if check debug "Executing check '#{command}'" else debug "Executing '#{command}'" end begin # Do our chdir Dir.chdir(dir) do environment = {} environment[:PATH] = self[:path].join(":") if self[:path] if envlist = self[:environment] envlist = [envlist] unless envlist.is_a? Array envlist.each do |setting| if setting =~ /^(\w+)=((.|\n)+)$/ name = $1 value = $2 if environment.include? name warning( "Overriding environment setting '#{name}' with '#{value}'" ) end environment[name] = value else warning "Cannot understand environment setting #{setting.inspect}" end end end withenv environment do Timeout::timeout(self[:timeout]) do output, status = Puppet::Util::SUIDManager.run_and_capture( [command], self[:user], self[:group] ) end # The shell returns 127 if the command is missing. if status.exitstatus == 127 raise ArgumentError, output end end end rescue Errno::ENOENT => detail self.fail detail.to_s end return output, status end def validatecmd(cmd) exe = extractexe(cmd) # if we're not fully qualified, require a path self.fail "'#{cmd}' is not qualified and no path was specified. Please qualify the command or specify a path." if File.expand_path(exe) != exe and self[:path].nil? end def extractexe(cmd) # easy case: command was quoted if cmd =~ /^"([^"]+)"/ $1 else cmd.split(/ /)[0] end end end end diff --git a/lib/puppet/type/file.rb b/lib/puppet/type/file.rb index cbb51bbed..e1a4ecbb9 100644 --- a/lib/puppet/type/file.rb +++ b/lib/puppet/type/file.rb @@ -1,791 +1,793 @@ require 'digest/md5' require 'cgi' require 'etc' require 'uri' require 'fileutils' require 'puppet/network/handler' require 'puppet/util/diff' require 'puppet/util/checksums' require 'puppet/network/client' require 'puppet/util/backups' Puppet::Type.newtype(:file) do include Puppet::Util::MethodHelper include Puppet::Util::Checksums include Puppet::Util::Backups @doc = "Manages local files, including setting ownership and permissions, creation of both files and directories, and retrieving entire files from remote servers. As Puppet matures, it expected that the `file` resource will be used less and less to manage content, and instead native resources will be used to do so. If you find that you are often copying files in from a central location, rather than using native resources, please contact Puppet Labs and we can hopefully work with you to develop a - native resource to support what you are doing." + native resource to support what you are doing. + + **Autorequires:** If Puppet is managing the user or group that owns a file, the file resource will autorequire them. If Puppet is managing any parent directories of a file, the file resource will autorequire them." def self.title_patterns [ [ /^(.*?)\/*\Z/m, [ [ :path, lambda{|x| x} ] ] ] ] end newparam(:path) do desc "The path to the file to manage. Must be fully qualified." isnamevar validate do |value| # accept various path syntaxes: lone slash, posix, win32, unc unless (Puppet.features.posix? and value =~ /^\//) or (Puppet.features.microsoft_windows? and (value =~ /^.:\// or value =~ /^\/\/[^\/]+\/[^\/]+/)) fail Puppet::Error, "File paths must be fully qualified, not '#{value}'" end end # convert the current path in an index into the collection and the last # path name. The aim is to use less storage for all common paths in a hierarchy munge do |value| path, name = File.split(value.gsub(/\/+/,'/')) { :index => Puppet::FileCollection.collection.index(path), :name => name } end # and the reverse unmunge do |value| basedir = Puppet::FileCollection.collection.path(value[:index]) # a lone slash as :name indicates a root dir on windows if value[:name] == '/' basedir else File.join( basedir, value[:name] ) end end end newparam(:backup) do desc "Whether files should be backed up before being replaced. The preferred method of backing files up is via a `filebucket`, which stores files by their MD5 sums and allows easy retrieval without littering directories with backups. You can specify a local filebucket or a network-accessible server-based filebucket by setting `backup => bucket-name`. Alternatively, if you specify any value that begins with a `.` (e.g., `.puppet-bak`), then Puppet will use copy the file in the same directory with that value as the extension of the backup. Setting `backup => false` disables all backups of the file in question. Puppet automatically creates a local filebucket named `puppet` and defaults to backing up there. To use a server-based filebucket, you must specify one in your configuration filebucket { main: server => puppet } The `puppet master` daemon creates a filebucket by default, so you can usually back up to your main server with this configuration. Once you've described the bucket in your configuration, you can use it in any file file { \"/my/file\": source => \"/path/in/nfs/or/something\", backup => main } This will back the file up to the central server. At this point, the benefits of using a filebucket are that you do not have backup files lying around on each of your machines, a given version of a file is only backed up once, and you can restore any given file manually, no matter how old. Eventually, transactional support will be able to automatically restore filebucketed files. " defaultto "puppet" munge do |value| # I don't really know how this is happening. value = value.shift if value.is_a?(Array) case value when false, "false", :false false when true, "true", ".puppet-bak", :true ".puppet-bak" when String value else self.fail "Invalid backup type #{value.inspect}" end end end newparam(:recurse) do desc "Whether and how deeply to do recursive management." newvalues(:true, :false, :inf, :remote, /^[0-9]+$/) # Replace the validation so that we allow numbers in # addition to string representations of them. validate { |arg| } munge do |value| newval = super(value) case newval when :true, :inf; true when :false; false when :remote; :remote when Integer, Fixnum, Bignum self.warning "Setting recursion depth with the recurse parameter is now deprecated, please use recurselimit" # recurse == 0 means no recursion return false if value == 0 resource[:recurselimit] = value true when /^\d+$/ self.warning "Setting recursion depth with the recurse parameter is now deprecated, please use recurselimit" value = Integer(value) # recurse == 0 means no recursion return false if value == 0 resource[:recurselimit] = value true else self.fail "Invalid recurse value #{value.inspect}" end end end newparam(:recurselimit) do desc "How deeply to do recursive management." newvalues(/^[0-9]+$/) munge do |value| newval = super(value) case newval when Integer, Fixnum, Bignum; value when /^\d+$/; Integer(value) else self.fail "Invalid recurselimit value #{value.inspect}" end end end newparam(:replace, :boolean => true) do desc "Whether or not to replace a file that is sourced but exists. This is useful for using file sources purely for initialization." newvalues(:true, :false) aliasvalue(:yes, :true) aliasvalue(:no, :false) defaultto :true end newparam(:force, :boolean => true) do desc "Force the file operation. Currently only used when replacing directories with links." newvalues(:true, :false) defaultto false end newparam(:ignore) do desc "A parameter which omits action on files matching specified patterns during recursion. Uses Ruby's builtin globbing engine, so shell metacharacters are fully supported, e.g. `[a-z]*`. Matches that would descend into the directory structure are ignored, e.g., `*/*`." validate do |value| unless value.is_a?(Array) or value.is_a?(String) or value == false self.devfail "Ignore must be a string or an Array" end end end newparam(:links) do desc "How to handle links during file actions. During file copying, `follow` will copy the target file instead of the link, `manage` will copy the link itself, and `ignore` will just pass it by. When not copying, `manage` and `ignore` behave equivalently (because you cannot really ignore links entirely during local recursion), and `follow` will manage the file to which the link points." newvalues(:follow, :manage) defaultto :manage end newparam(:purge, :boolean => true) do desc "Whether unmanaged files should be purged. If you have a filebucket configured the purged files will be uploaded, but if you do not, this will destroy data. Only use this option for generated files unless you really know what you are doing. This option only makes sense when recursively managing directories. Note that when using `purge` with `source`, Puppet will purge any files that are not on the remote system." defaultto :false newvalues(:true, :false) end newparam(:sourceselect) do desc "Whether to copy all valid sources, or just the first one. This parameter is only used in recursive copies; by default, the first valid source is the only one used as a recursive source, but if this parameter is set to `all`, then all valid sources will have all of their contents copied to the local host, and for sources that have the same file, the source earlier in the list will be used." defaultto :first newvalues(:first, :all) end # Autorequire any parent directories. autorequire(:file) do basedir = File.dirname(self[:path]) if basedir != self[:path] basedir else nil end end # Autorequire the owner and group of the file. {:user => :owner, :group => :group}.each do |type, property| autorequire(type) do if @parameters.include?(property) # The user/group property automatically converts to IDs next unless should = @parameters[property].shouldorig val = should[0] if val.is_a?(Integer) or val =~ /^\d+$/ nil else val end end end end CREATORS = [:content, :source, :target] SOURCE_ONLY_CHECKSUMS = [:none, :ctime, :mtime] validate do creator_count = 0 CREATORS.each do |param| creator_count += 1 if self.should(param) end creator_count += 1 if @parameters.include?(:source) self.fail "You cannot specify more than one of #{CREATORS.collect { |p| p.to_s}.join(", ")}" if creator_count > 1 self.fail "You cannot specify a remote recursion without a source" if !self[:source] and self[:recurse] == :remote self.fail "You cannot specify source when using checksum 'none'" if self[:checksum] == :none && !self[:source].nil? SOURCE_ONLY_CHECKSUMS.each do |checksum_type| self.fail "You cannot specify content when using checksum '#{checksum_type}'" if self[:checksum] == checksum_type && !self[:content].nil? end self.warning "Possible error: recurselimit is set but not recurse, no recursion will happen" if !self[:recurse] and self[:recurselimit] end def self.[](path) return nil unless path super(path.gsub(/\/+/, '/').sub(/\/$/, '')) end def self.instances(base = '/') return self.new(:name => base, :recurse => true, :recurselimit => 1, :audit => :all).recurse_local.values end @depthfirst = false # Determine the user to write files as. def asuser if self.should(:owner) and ! self.should(:owner).is_a?(Symbol) writeable = Puppet::Util::SUIDManager.asuser(self.should(:owner)) { FileTest.writable?(File.dirname(self[:path])) } # If the parent directory is writeable, then we execute # as the user in question. Otherwise we'll rely on # the 'owner' property to do things. asuser = self.should(:owner) if writeable end asuser end def bucket return @bucket if @bucket backup = self[:backup] return nil unless backup return nil if backup =~ /^\./ unless catalog or backup == "puppet" fail "Can not find filebucket for backups without a catalog" end unless catalog and filebucket = catalog.resource(:filebucket, backup) or backup == "puppet" fail "Could not find filebucket #{backup} specified in backup" end return default_bucket unless filebucket @bucket = filebucket.bucket @bucket end def default_bucket Puppet::Type.type(:filebucket).mkdefaultbucket.bucket end # Does the file currently exist? Just checks for whether # we have a stat def exist? stat ? true : false end # We have to do some extra finishing, to retrieve our bucket if # there is one. def finish # Look up our bucket, if there is one bucket super end # Create any children via recursion or whatever. def eval_generate return [] unless self.recurse? recurse #recurse.reject do |resource| # catalog.resource(:file, resource[:path]) #end.each do |child| # catalog.add_resource child # catalog.relationship_graph.add_edge self, child #end end def flush # We want to make sure we retrieve metadata anew on each transaction. @parameters.each do |name, param| param.flush if param.respond_to?(:flush) end @stat = nil end def initialize(hash) # Used for caching clients @clients = {} super # If they've specified a source, we get our 'should' values # from it. unless self[:ensure] if self[:target] self[:ensure] = :symlink elsif self[:content] self[:ensure] = :file end end @stat = nil end # Configure discovered resources to be purged. def mark_children_for_purging(children) children.each do |name, child| next if child[:source] child[:ensure] = :absent end end # Create a new file or directory object as a child to the current # object. def newchild(path) full_path = File.join(self[:path], path) # Add some new values to our original arguments -- these are the ones # set at initialization. We specifically want to exclude any param # values set by the :source property or any default values. # LAK:NOTE This is kind of silly, because the whole point here is that # the values set at initialization should live as long as the resource # but values set by default or by :source should only live for the transaction # or so. Unfortunately, we don't have a straightforward way to manage # the different lifetimes of this data, so we kludge it like this. # The right-side hash wins in the merge. options = @original_parameters.merge(:path => full_path).reject { |param, value| value.nil? } # These should never be passed to our children. [:parent, :ensure, :recurse, :recurselimit, :target, :alias, :source].each do |param| options.delete(param) if options.include?(param) end self.class.new(options) end # Files handle paths specially, because they just lengthen their # path names, rather than including the full parent's title each # time. def pathbuilder # We specifically need to call the method here, so it looks # up our parent in the catalog graph. if parent = parent() # We only need to behave specially when our parent is also # a file if parent.is_a?(self.class) # Remove the parent file name list = parent.pathbuilder list.pop # remove the parent's path info return list << self.ref else return super end else return [self.ref] end end # Should we be purging? def purge? @parameters.include?(:purge) and (self[:purge] == :true or self[:purge] == "true") end # Recursively generate a list of file resources, which will # be used to copy remote files, manage local files, and/or make links # to map to another directory. def recurse children = {} children = recurse_local if self[:recurse] != :remote if self[:target] recurse_link(children) elsif self[:source] recurse_remote(children) end # If we're purging resources, then delete any resource that isn't on the # remote system. mark_children_for_purging(children) if self.purge? result = children.values.sort { |a, b| a[:path] <=> b[:path] } remove_less_specific_files(result) end # This is to fix bug #2296, where two files recurse over the same # set of files. It's a rare case, and when it does happen you're # not likely to have many actual conflicts, which is good, because # this is a pretty inefficient implementation. def remove_less_specific_files(files) mypath = self[:path].split(File::Separator) other_paths = catalog.vertices. select { |r| r.is_a?(self.class) and r[:path] != self[:path] }. collect { |r| r[:path].split(File::Separator) }. select { |p| p[0,mypath.length] == mypath } return files if other_paths.empty? files.reject { |file| path = file[:path].split(File::Separator) other_paths.any? { |p| path[0,p.length] == p } } end # A simple method for determining whether we should be recursing. def recurse? return false unless @parameters.include?(:recurse) val = @parameters[:recurse].value !!(val and (val == true or val == :remote)) end # Recurse the target of the link. def recurse_link(children) perform_recursion(self[:target]).each do |meta| if meta.relative_path == "." self[:ensure] = :directory next end children[meta.relative_path] ||= newchild(meta.relative_path) if meta.ftype == "directory" children[meta.relative_path][:ensure] = :directory else children[meta.relative_path][:ensure] = :link children[meta.relative_path][:target] = meta.full_path end end children end # Recurse the file itself, returning a Metadata instance for every found file. def recurse_local result = perform_recursion(self[:path]) return {} unless result result.inject({}) do |hash, meta| next hash if meta.relative_path == "." hash[meta.relative_path] = newchild(meta.relative_path) hash end end # Recurse against our remote file. def recurse_remote(children) sourceselect = self[:sourceselect] total = self[:source].collect do |source| next unless result = perform_recursion(source) return if top = result.find { |r| r.relative_path == "." } and top.ftype != "directory" result.each { |data| data.source = "#{source}/#{data.relative_path}" } break result if result and ! result.empty? and sourceselect == :first result end.flatten # This only happens if we have sourceselect == :all unless sourceselect == :first found = [] total.reject! do |data| result = found.include?(data.relative_path) found << data.relative_path unless found.include?(data.relative_path) result end end total.each do |meta| if meta.relative_path == "." parameter(:source).metadata = meta next end children[meta.relative_path] ||= newchild(meta.relative_path) children[meta.relative_path][:source] = meta.source children[meta.relative_path][:checksum] = :md5 if meta.ftype == "file" children[meta.relative_path].parameter(:source).metadata = meta end children end def perform_recursion(path) Puppet::FileServing::Metadata.search( path, :links => self[:links], :recurse => (self[:recurse] == :remote ? true : self[:recurse]), :recurselimit => self[:recurselimit], :ignore => self[:ignore], :checksum_type => (self[:source] || self[:content]) ? self[:checksum] : :none ) end # Remove any existing data. This is only used when dealing with # links or directories. def remove_existing(should) return unless s = stat self.fail "Could not back up; will not replace" unless perform_backup unless should.to_s == "link" return if s.ftype.to_s == should.to_s end case s.ftype when "directory" if self[:force] == :true debug "Removing existing directory for replacement with #{should}" FileUtils.rmtree(self[:path]) else notice "Not removing directory; use 'force' to override" end when "link", "file" debug "Removing existing #{s.ftype} for replacement with #{should}" File.unlink(self[:path]) else self.fail "Could not back up files of type #{s.ftype}" end expire end def retrieve if source = parameter(:source) source.copy_source_values end super end # Set the checksum, from another property. There are multiple # properties that modify the contents of a file, and they need the # ability to make sure that the checksum value is in sync. def setchecksum(sum = nil) if @parameters.include? :checksum if sum @parameters[:checksum].checksum = sum else # If they didn't pass in a sum, then tell checksum to # figure it out. currentvalue = @parameters[:checksum].retrieve @parameters[:checksum].checksum = currentvalue end end end # Should this thing be a normal file? This is a relatively complex # way of determining whether we're trying to create a normal file, # and it's here so that the logic isn't visible in the content property. def should_be_file? return true if self[:ensure] == :file # I.e., it's set to something like "directory" return false if e = self[:ensure] and e != :present # The user doesn't really care, apparently if self[:ensure] == :present return true unless s = stat return(s.ftype == "file" ? true : false) end # If we've gotten here, then :ensure isn't set return true if self[:content] return true if stat and stat.ftype == "file" false end # Stat our file. Depending on the value of the 'links' attribute, we # use either 'stat' or 'lstat', and we expect the properties to use the # resulting stat object accordingly (mostly by testing the 'ftype' # value). cached_attr(:stat) do method = :stat # Files are the only types that support links if (self.class.name == :file and self[:links] != :follow) or self.class.name == :tidy method = :lstat end path = self[:path] begin File.send(method, self[:path]) rescue Errno::ENOENT => error return nil rescue Errno::EACCES => error warning "Could not stat; permission denied" return nil end end # We have to hack this just a little bit, because otherwise we'll get # an error when the target and the contents are created as properties on # the far side. def to_trans(retrieve = true) obj = super obj.delete(:target) if obj[:target] == :notlink obj end # Write out the file. Requires the property name for logging. # Write will be done by the content property, along with checksum computation def write(property) remove_existing(:file) use_temporary_file = write_temporary_file? if use_temporary_file path = "#{self[:path]}.puppettmp_#{rand(10000)}" path = "#{self[:path]}.puppettmp_#{rand(10000)}" while File.exists?(path) or File.symlink?(path) else path = self[:path] end mode = self.should(:mode) # might be nil umask = mode ? 000 : 022 mode_int = mode ? mode.to_i(8) : nil content_checksum = Puppet::Util.withumask(umask) { File.open(path, 'w', mode_int ) { |f| write_content(f) } } # And put our new file in place if use_temporary_file # This is only not true when our file is empty. begin fail_if_checksum_is_wrong(path, content_checksum) if validate_checksum? File.rename(path, self[:path]) rescue => detail fail "Could not rename temporary file #{path} to #{self[:path]}: #{detail}" ensure # Make sure the created file gets removed File.unlink(path) if FileTest.exists?(path) end end # make sure all of the modes are actually correct property_fix end private # Should we validate the checksum of the file we're writing? def validate_checksum? self[:checksum] !~ /time/ end # Make sure the file we wrote out is what we think it is. def fail_if_checksum_is_wrong(path, content_checksum) newsum = parameter(:checksum).sum_file(path) return if [:absent, nil, content_checksum].include?(newsum) self.fail "File written to disk did not match checksum; discarding changes (#{content_checksum} vs #{newsum})" end # write the current content. Note that if there is no content property # simply opening the file with 'w' as done in write is enough to truncate # or write an empty length file. def write_content(file) (content = property(:content)) && content.write(file) end private def write_temporary_file? # unfortunately we don't know the source file size before fetching it # so let's assume the file won't be empty (c = property(:content) and c.length) || (s = @parameters[:source] and 1) end # There are some cases where all of the work does not get done on # file creation/modification, so we have to do some extra checking. def property_fix properties.each do |thing| next unless [:mode, :owner, :group, :seluser, :selrole, :seltype, :selrange].include?(thing.name) # Make sure we get a new stat objct expire currentvalue = thing.retrieve thing.sync unless thing.safe_insync?(currentvalue) end end end # We put all of the properties in separate files, because there are so many # of them. The order these are loaded is important, because it determines # the order they are in the property lit. require 'puppet/type/file/checksum' require 'puppet/type/file/content' # can create the file require 'puppet/type/file/source' # can create the file require 'puppet/type/file/target' # creates a different type of file require 'puppet/type/file/ensure' # can create the file require 'puppet/type/file/owner' require 'puppet/type/file/group' require 'puppet/type/file/mode' require 'puppet/type/file/type' require 'puppet/type/file/selcontext' # SELinux file context require 'puppet/type/file/ctime' require 'puppet/type/file/mtime' diff --git a/lib/puppet/type/file/content.rb b/lib/puppet/type/file/content.rb index 6aa173f6c..827183213 100755 --- a/lib/puppet/type/file/content.rb +++ b/lib/puppet/type/file/content.rb @@ -1,223 +1,221 @@ require 'net/http' require 'uri' require 'tempfile' require 'puppet/util/checksums' require 'puppet/network/http/api/v1' require 'puppet/network/http/compression' module Puppet Puppet::Type.type(:file).newproperty(:content) do include Puppet::Util::Diff include Puppet::Util::Checksums include Puppet::Network::HTTP::API::V1 include Puppet::Network::HTTP::Compression.module attr_reader :actual_content desc "Specify the contents of a file as a string. Newlines, tabs, and spaces can be specified using the escaped syntax (e.g., \\n for a newline). The primary purpose of this parameter is to provide a - kind of limited templating:: - - define resolve(nameserver1, nameserver2, domain, search) { - $str = \"search $search - domain $domain - nameserver $nameserver1 - nameserver $nameserver2 - \" - - file { \"/etc/resolv.conf\": - content => $str + kind of limited templating: + + define resolve(nameserver1, nameserver2, domain, search) { + $str = \"search $search + domain $domain + nameserver $nameserver1 + nameserver $nameserver2 + \" + + file { \"/etc/resolv.conf\": + content => $str + } } - } - This attribute is especially useful when used with - `PuppetTemplating templating`:trac:." + This attribute is especially useful when used with templating." # Store a checksum as the value, rather than the actual content. # Simplifies everything. munge do |value| if value == :absent value elsif checksum?(value) # XXX This is potentially dangerous because it means users can't write a file whose # entire contents are a plain checksum value else @actual_content = value resource.parameter(:checksum).sum(value) end end # Checksums need to invert how changes are printed. def change_to_s(currentvalue, newvalue) # Our "new" checksum value is provided by the source. if source = resource.parameter(:source) and tmp = source.checksum newvalue = tmp end if currentvalue == :absent return "defined content as '#{newvalue}'" elsif newvalue == :absent return "undefined content from '#{currentvalue}'" else return "content changed '#{currentvalue}' to '#{newvalue}'" end end def checksum_type if source = resource.parameter(:source) result = source.checksum else checksum = resource.parameter(:checksum) result = resource[:checksum] end if result =~ /^\{(\w+)\}.+/ return $1.to_sym else return result end end def length (actual_content and actual_content.length) || 0 end def content self.should end # Override this method to provide diffs if asked for. # Also, fix #872: when content is used, and replace is true, the file # should be insync when it exists def insync?(is) if resource.should_be_file? return false if is == :absent else return true end return true if ! @resource.replace? result = super if ! result and Puppet[:show_diff] write_temporarily do |path| print diff(@resource[:path], path) end end result end def retrieve return :absent unless stat = @resource.stat ftype = stat.ftype # Don't even try to manage the content on directories or links return nil if ["directory","link"].include?(ftype) begin resource.parameter(:checksum).sum_file(resource[:path]) rescue => detail raise Puppet::Error, "Could not read #{ftype} #{@resource.title}: #{detail}" end end # Make sure we're also managing the checksum property. def should=(value) @resource.newattr(:checksum) unless @resource.parameter(:checksum) super end # Just write our content out to disk. def sync return_event = @resource.stat ? :file_changed : :file_created # We're safe not testing for the 'source' if there's no 'should' # because we wouldn't have gotten this far if there weren't at least # one valid value somewhere. @resource.write(:content) return_event end def write_temporarily tempfile = Tempfile.new("puppet-file") tempfile.open write(tempfile) tempfile.close yield tempfile.path tempfile.delete end def write(file) resource.parameter(:checksum).sum_stream { |sum| each_chunk_from(actual_content || resource.parameter(:source)) { |chunk| sum << chunk file.print chunk } } end def self.standalone? Puppet.settings[:name] == "apply" end # the content is munged so if it's a checksum source_or_content is nil # unless the checksum indirectly comes from source def each_chunk_from(source_or_content) if source_or_content.is_a?(String) yield source_or_content elsif content_is_really_a_checksum? && source_or_content.nil? yield read_file_from_filebucket elsif source_or_content.nil? yield '' elsif self.class.standalone? yield source_or_content.content elsif source_or_content.local? chunk_file_from_disk(source_or_content) { |chunk| yield chunk } else chunk_file_from_source(source_or_content) { |chunk| yield chunk } end end private def content_is_really_a_checksum? checksum?(should) end def chunk_file_from_disk(source_or_content) File.open(source_or_content.full_path, "r") do |src| while chunk = src.read(8192) yield chunk end end end def chunk_file_from_source(source_or_content) request = Puppet::Indirector::Request.new(:file_content, :find, source_or_content.full_path.sub(/^\//,'')) connection = Puppet::Network::HttpPool.http_instance(source_or_content.server, source_or_content.port) connection.request_get(indirection2uri(request), add_accept_encoding({"Accept" => "raw"})) do |response| case response.code - when "404"; nil when /^2/; uncompress(response) { |uncompressor| response.read_body { |chunk| yield uncompressor.uncompress(chunk) } } else # Raise the http error if we didn't get a 'success' of some kind. message = "Error #{response.code} on SERVER: #{(response.body||'').empty? ? response.message : uncompress_body(response)}" raise Net::HTTPError.new(message, response) end end end def read_file_from_filebucket raise "Could not get filebucket from file" unless dipper = resource.bucket sum = should.sub(/\{\w+\}/, '') dipper.getfile(sum) rescue => detail fail "Could not retrieve content for #{should} from filebucket: #{detail}" end end end diff --git a/lib/puppet/type/file/ensure.rb b/lib/puppet/type/file/ensure.rb index 4a68551ee..99652ecc6 100755 --- a/lib/puppet/type/file/ensure.rb +++ b/lib/puppet/type/file/ensure.rb @@ -1,170 +1,164 @@ module Puppet Puppet::Type.type(:file).ensurable do require 'etc' desc "Whether to create files that don't currently exist. Possible values are *absent*, *present*, *file*, and *directory*. Specifying `present` will match any form of file existence, and if the file is missing will create an empty file. Specifying `absent` will delete the file (and directory if recurse => true). - Anything other than those values will be considered to be a symlink. - For instance, the following text creates a link: + Anything other than those values will create a symlink. In the interest of readability and clarity, you should use `ensure => link` and explicitly specify a + target; however, if a `target` attribute isn't provided, the value of the `ensure` + attribute will be used as the symlink target: - # Useful on solaris + # (Useful on Solaris) + # Less maintainable: file { \"/etc/inetd.conf\": - ensure => \"/etc/inet/inetd.conf\" + ensure => \"/etc/inet/inetd.conf\", } - You can make relative links: - - # Useful on solaris + # More maintainable: file { \"/etc/inetd.conf\": - ensure => \"inet/inetd.conf\" + ensure => link, + target => \"/etc/inet/inetd.conf\", } - - If you need to make a relative link to a file named the same - as one of the valid values, you must prefix it with `./` or - something similar. - - You can also make recursive symlinks, which will create a - directory structure that maps to the target directory, - with directories corresponding to each directory - and links corresponding to each file." + + These two declarations are equivalent." # Most 'ensure' properties have a default, but with files we, um, don't. nodefault newvalue(:absent) do File.unlink(@resource[:path]) end aliasvalue(:false, :absent) newvalue(:file, :event => :file_created) do # Make sure we're not managing the content some other way if property = @resource.property(:content) property.sync else @resource.write(:ensure) mode = @resource.should(:mode) end end #aliasvalue(:present, :file) newvalue(:present, :event => :file_created) do # Make a file if they want something, but this will match almost # anything. set_file end newvalue(:directory, :event => :directory_created) do mode = @resource.should(:mode) parent = File.dirname(@resource[:path]) unless FileTest.exists? parent raise Puppet::Error, "Cannot create #{@resource[:path]}; parent directory #{parent} does not exist" end if mode Puppet::Util.withumask(000) do Dir.mkdir(@resource[:path], mode.to_i(8)) end else Dir.mkdir(@resource[:path]) end @resource.send(:property_fix) return :directory_created end newvalue(:link, :event => :link_created) do fail "Cannot create a symlink without a target" unless property = resource.property(:target) property.retrieve property.mklink end # Symlinks. newvalue(/./) do # This code never gets executed. We need the regex to support # specifying it, but the work is done in the 'symlink' code block. end munge do |value| value = super(value) value,resource[:target] = :link,value unless value.is_a? Symbol resource[:links] = :manage if value == :link and resource[:links] != :follow value end def change_to_s(currentvalue, newvalue) return super unless newvalue.to_s == "file" return super unless property = @resource.property(:content) # We know that content is out of sync if we're here, because # it's essentially equivalent to 'ensure' in the transaction. if source = @resource.parameter(:source) should = source.checksum else should = property.should end if should == :absent is = property.retrieve else is = :absent end property.change_to_s(is, should) end # Check that we can actually create anything def check basedir = File.dirname(@resource[:path]) if ! FileTest.exists?(basedir) raise Puppet::Error, "Can not create #{@resource.title}; parent directory does not exist" elsif ! FileTest.directory?(basedir) raise Puppet::Error, "Can not create #{@resource.title}; #{dirname} is not a directory" end end # We have to treat :present specially, because it works with any # type of file. def insync?(currentvalue) unless currentvalue == :absent or resource.replace? return true end if self.should == :present return !(currentvalue.nil? or currentvalue == :absent) else return super(currentvalue) end end def retrieve if stat = @resource.stat(false) return stat.ftype.intern else if self.should == :false return :false else return :absent end end end def sync @resource.remove_existing(self.should) if self.should == :absent return :file_removed end event = super event end end end diff --git a/lib/puppet/type/file/selcontext.rb b/lib/puppet/type/file/selcontext.rb index a33c6a000..ea385eec0 100644 --- a/lib/puppet/type/file/selcontext.rb +++ b/lib/puppet/type/file/selcontext.rb @@ -1,103 +1,119 @@ # Manage SELinux context of files. # # This code actually manages three pieces of data in the context. # # [root@delenn files]# ls -dZ / # drwxr-xr-x root root system_u:object_r:root_t / # # The context of '/' here is 'system_u:object_r:root_t'. This is # three seperate fields: # # system_u is the user context # object_r is the role context # root_t is the type context # # All three of these fields are returned in a single string by the # output of the stat command, but set individually with the chcon # command. This allows the user to specify a subset of the three # values while leaving the others alone. # # See http://www.nsa.gov/selinux/ for complete docs on SELinux. module Puppet require 'puppet/util/selinux' class SELFileContext < Puppet::Property include Puppet::Util::SELinux def retrieve return :absent unless @resource.stat(false) context = self.get_selinux_current_context(@resource[:path]) parse_selinux_context(name, context) end def retrieve_default_context(property) + if @resource[:selinux_ignore_defaults] == :true + return nil + end + unless context = self.get_selinux_default_context(@resource[:path]) return nil end + property_default = self.parse_selinux_context(property, context) self.debug "Found #{property} default '#{property_default}' for #{@resource[:path]}" if not property_default.nil? property_default end def insync?(value) if not selinux_support? debug("SELinux bindings not found. Ignoring parameter.") return true end super end def sync self.set_selinux_context(@resource[:path], @should, name) :file_changed end end + Puppet::Type.type(:file).newparam(:selinux_ignore_defaults) do + desc "If this is set then Puppet will not ask SELinux (via matchpathcon) to + supply defaults for the SELinux attributes (seluser, selrole, + seltype, and selrange). In general, you should leave this set at its + default and only set it to true when you need Puppet to not try to fix + SELinux labels automatically." + newvalues(:true, :false) + + defaultto :false + end + Puppet::Type.type(:file).newproperty(:seluser, :parent => Puppet::SELFileContext) do desc "What the SELinux user component of the context of the file should be. Any valid SELinux user component is accepted. For example `user_u`. If not specified it defaults to the value returned by matchpathcon for the file, if any exists. Only valid on systems with SELinux support enabled." @event = :file_changed defaultto { self.retrieve_default_context(:seluser) } end Puppet::Type.type(:file).newproperty(:selrole, :parent => Puppet::SELFileContext) do desc "What the SELinux role component of the context of the file should be. Any valid SELinux role component is accepted. For example `role_r`. If not specified it defaults to the value returned by matchpathcon for the file, if any exists. Only valid on systems with SELinux support enabled." @event = :file_changed defaultto { self.retrieve_default_context(:selrole) } end Puppet::Type.type(:file).newproperty(:seltype, :parent => Puppet::SELFileContext) do desc "What the SELinux type component of the context of the file should be. Any valid SELinux type component is accepted. For example `tmp_t`. If not specified it defaults to the value returned by matchpathcon for the file, if any exists. Only valid on systems with SELinux support enabled." @event = :file_changed defaultto { self.retrieve_default_context(:seltype) } end Puppet::Type.type(:file).newproperty(:selrange, :parent => Puppet::SELFileContext) do desc "What the SELinux range component of the context of the file should be. Any valid SELinux range component is accepted. For example `s0` or `SystemHigh`. If not specified it defaults to the value returned by matchpathcon for the file, if any exists. Only valid on systems with SELinux support enabled and that have support for MCS (Multi-Category Security)." @event = :file_changed defaultto { self.retrieve_default_context(:selrange) } end end diff --git a/lib/puppet/type/file/target.rb b/lib/puppet/type/file/target.rb index b9fe9213b..7d391e672 100644 --- a/lib/puppet/type/file/target.rb +++ b/lib/puppet/type/file/target.rb @@ -1,74 +1,87 @@ module Puppet Puppet::Type.type(:file).newproperty(:target) do desc "The target for creating a link. Currently, symlinks are the - only type supported." + only type supported. + + You can make relative links: + + # (Useful on Solaris) + file { \"/etc/inetd.conf\": + ensure => link, + target => \"inet/inetd.conf\", + } + + You can also make recursive symlinks, which will create a + directory structure that maps to the target directory, + with directories corresponding to each directory + and links corresponding to each file." newvalue(:notlink) do # We do nothing if the value is absent return :nochange end # Anything else, basically newvalue(/./) do @resource[:ensure] = :link if ! @resource.should(:ensure) # Only call mklink if ensure didn't call us in the first place. currentensure = @resource.property(:ensure).retrieve mklink if @resource.property(:ensure).safe_insync?(currentensure) end # Create our link. def mklink raise Puppet::Error, "Cannot symlink on Microsoft Windows" if Puppet.features.microsoft_windows? target = self.should # Clean up any existing objects. The argument is just for logging, # it doesn't determine what's removed. @resource.remove_existing(target) raise Puppet::Error, "Could not remove existing file" if FileTest.exists?(@resource[:path]) Dir.chdir(File.dirname(@resource[:path])) do Puppet::Util::SUIDManager.asuser(@resource.asuser) do mode = @resource.should(:mode) if mode Puppet::Util.withumask(000) do File.symlink(target, @resource[:path]) end else File.symlink(target, @resource[:path]) end end @resource.send(:property_fix) :link_created end end def insync?(currentvalue) if [:nochange, :notlink].include?(self.should) or @resource.recurse? return true elsif ! @resource.replace? and File.exists?(@resource[:path]) return true else return super(currentvalue) end end def retrieve if stat = @resource.stat if stat.ftype == "link" return File.readlink(@resource[:path]) else return :notlink end else return :absent end end end end diff --git a/lib/puppet/type/k5login.rb b/lib/puppet/type/k5login.rb index a343e9e5c..eac142ff7 100644 --- a/lib/puppet/type/k5login.rb +++ b/lib/puppet/type/k5login.rb @@ -1,87 +1,85 @@ -# $Id: k5login.rb 2468 2007-08-07 23:30:20Z digant $ -# # Plug-in type for handling k5login files Puppet::Type.newtype(:k5login) do @doc = "Manage the `.k5login` file for a user. Specify the full path to the `.k5login` file as the name and an array of principals as the property principals." ensurable # Principals that should exist in the file newproperty(:principals, :array_matching => :all) do desc "The principals present in the `.k5login` file." end # The path/name of the k5login file newparam(:path) do isnamevar desc "The path to the file to manage. Must be fully qualified." validate do |value| unless value =~ /^#{File::SEPARATOR}/ raise Puppet::Error, "File paths must be fully qualified" end end end # To manage the mode of the file newproperty(:mode) do desc "Manage the k5login file's mode" defaultto { "644" } end provide(:k5login) do desc "The k5login provider is the only provider for the k5login type." # Does this file exist? def exists? File.exists?(@resource[:name]) end # create the file def create write(@resource.should(:principals)) should_mode = @resource.should(:mode) unless self.mode == should_mode self.mode = should_mode end end # remove the file def destroy File.unlink(@resource[:name]) end # Return the principals def principals(dummy_argument=:work_arround_for_ruby_GC_bug) if File.exists?(@resource[:name]) File.readlines(@resource[:name]).collect { |line| line.chomp } else :absent end end # Write the principals out to the k5login file def principals=(value) write(value) end # Return the mode as an octal string, not as an integer def mode "%o" % (File.stat(@resource[:name]).mode & 007777) end # Set the file mode, converting from a string to an integer. def mode=(value) File.chmod(Integer("0#{value}"), @resource[:name]) end private def write(value) File.open(@resource[:name], "w") { |f| f.puts value.join("\n") } end end end diff --git a/lib/puppet/type/macauthorization.rb b/lib/puppet/type/macauthorization.rb index ef6fbb6c1..e89aa7c89 100644 --- a/lib/puppet/type/macauthorization.rb +++ b/lib/puppet/type/macauthorization.rb @@ -1,163 +1,166 @@ Puppet::Type.newtype(:macauthorization) do @doc = "Manage the Mac OS X authorization database. - See the [Apple developer site](http://developer.apple.com/documentation/Security/Conceptual/Security_Overview/Security_Services/chapter_4_section_5.html) for more information." + See the [Apple developer site](http://developer.apple.com/documentation/Security/Conceptual/Security_Overview/Security_Services/chapter_4_section_5.html) for more information. + + **Autorequires:** If Puppet is managing the `/etc/authorization` file, each + macauthorization resource will autorequire it." ensurable autorequire(:file) do ["/etc/authorization"] end def munge_boolean(value) case value when true, "true", :true :true when false, "false", :false :false else fail("munge_boolean only takes booleans") end end def munge_integer(value) Integer(value) rescue ArgumentError fail("munge_integer only takes integers") end newparam(:name) do desc "The name of the right or rule to be managed. Corresponds to 'key' in Authorization Services. The key is the name of a rule. A key uses the same naming conventions as a right. The Security Server uses a rule’s key to match the rule with a right. Wildcard keys end with a ‘.’. The generic rule has an empty key value. Any rights that do not match a specific rule use the generic rule." isnamevar end newproperty(:auth_type) do desc "type - can be a 'right' or a 'rule'. 'comment' has not yet been implemented." newvalue(:right) newvalue(:rule) # newvalue(:comment) # not yet implemented. end newproperty(:allow_root, :boolean => true) do desc "Corresponds to 'allow-root' in the authorization store, renamed due to hyphens being problematic. Specifies whether a right should be allowed automatically if the requesting process is running with uid == 0. AuthorizationServices defaults this attribute to false if not specified" newvalue(:true) newvalue(:false) munge do |value| @resource.munge_boolean(value) end end newproperty(:authenticate_user, :boolean => true) do desc "Corresponds to 'authenticate-user' in the authorization store, renamed due to hyphens being problematic." newvalue(:true) newvalue(:false) munge do |value| @resource.munge_boolean(value) end end newproperty(:auth_class) do desc "Corresponds to 'class' in the authorization store, renamed due to 'class' being a reserved word." newvalue(:user) newvalue(:'evaluate-mechanisms') newvalue(:allow) newvalue(:deny) newvalue(:rule) end newproperty(:comment) do desc "The 'comment' attribute for authorization resources." end newproperty(:group) do desc "The user must authenticate as a member of this group. This attribute can be set to any one group." end newproperty(:k_of_n) do desc "k-of-n describes how large a subset of rule mechanisms must succeed for successful authentication. If there are 'n' mechanisms, then 'k' (the integer value of this parameter) mechanisms must succeed. The most common setting for this parameter is '1'. If k-of-n is not set, then 'n-of-n' mechanisms must succeed." munge do |value| @resource.munge_integer(value) end end newproperty(:mechanisms, :array_matching => :all) do desc "an array of suitable mechanisms." end newproperty(:rule, :array_matching => :all) do desc "The rule(s) that this right refers to." end newproperty(:session_owner, :boolean => true) do desc "Corresponds to 'session-owner' in the authorization store, renamed due to hyphens being problematic. Whether the session owner automatically matches this rule or right." newvalue(:true) newvalue(:false) munge do |value| @resource.munge_boolean(value) end end newproperty(:shared, :boolean => true) do desc "If this is set to true, then the Security Server marks the credentials used to gain this right as shared. The Security Server may use any shared credentials to authorize this right. For maximum security, set sharing to false so credentials stored by the Security Server for one application may not be used by another application." newvalue(:true) newvalue(:false) munge do |value| @resource.munge_boolean(value) end end newproperty(:timeout) do desc "The credential used by this rule expires in the specified number of seconds. For maximum security where the user must authenticate every time, set the timeout to 0. For minimum security, remove the timeout attribute so the user authenticates only once per session." munge do |value| @resource.munge_integer(value) end end newproperty(:tries) do desc "The number of tries allowed." munge do |value| @resource.munge_integer(value) end end end diff --git a/lib/puppet/type/mcx.rb b/lib/puppet/type/mcx.rb index 4f0a6c3c5..07c9348dd 100644 --- a/lib/puppet/type/mcx.rb +++ b/lib/puppet/type/mcx.rb @@ -1,115 +1,118 @@ #-- # Copyright (C) 2008 Jeffrey J McCune. # This program and entire repository is free software; you can # redistribute it and/or modify it under the terms of the GNU # General Public License as published by the Free Software # Foundation; either version 2 of the License, or any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA # Author: Jeff McCune Puppet::Type.newtype(:mcx) do @doc = "MCX object management using DirectoryService on OS X. The default provider of this type merely manages the XML plist as reported by the dscl -mcxexport command. This is similar to the content property of the file type in Puppet. The recommended method of using this type is to use Work Group Manager to manage users and groups on the local computer, record the resulting -puppet manifest using the command 'ralsh mcx' then deploying this +puppet manifest using the command `puppet resource mcx`, then deploy it to other machines. + +**Autorequires:** If Puppet is managing the user, group, or computer that these +MCX settings refer to, the MCX resource will autorequire that user, group, or computer. " feature :manages_content, \ "The provider can manage MCXSettings as a string.", :methods => [:content, :content=] ensurable do desc "Create or remove the MCX setting." newvalue(:present) do provider.create end newvalue(:absent) do provider.destroy end end newparam(:name) do desc "The name of the resource being managed. The default naming convention follows Directory Service paths: /Computers/localhost /Groups/admin /Users/localadmin The `ds_type` and `ds_name` type parameters are not necessary if the default naming convention is followed." isnamevar end newparam(:ds_type) do desc "The DirectoryService type this MCX setting attaches to." newvalues(:user, :group, :computer, :computerlist) end newparam(:ds_name) do desc "The name to attach the MCX Setting to. e.g. 'localhost' when ds_type => computer. This setting is not required, as it may be parsed so long as the resource name is parseable. e.g. /Groups/admin where 'group' is the dstype." end newproperty(:content, :required_features => :manages_content) do desc "The XML Plist. The value of MCXSettings in DirectoryService. This is the standard output from the system command: dscl localhost -mcxexport /Local/Default//ds_name Note that `ds_type` is capitalized and plural in the dscl command." end # JJM Yes, this is not DRY at all. Because of the code blocks # autorequire must be done this way. I think. def setup_autorequire(type) # value returns a Symbol name = value(:name) ds_type = value(:ds_type) ds_name = value(:ds_name) if ds_type == type rval = [ ds_name.to_s ] else rval = [ ] end rval end autorequire(:user) do setup_autorequire(:user) end autorequire(:group) do setup_autorequire(:group) end autorequire(:computer) do setup_autorequire(:computer) end end diff --git a/lib/puppet/type/mount.rb b/lib/puppet/type/mount.rb index da9a70bdf..5b8c5ca58 100755 --- a/lib/puppet/type/mount.rb +++ b/lib/puppet/type/mount.rb @@ -1,224 +1,240 @@ module Puppet # We want the mount to refresh when it changes. newtype(:mount, :self_refresh => true) do @doc = "Manages mounted filesystems, including putting mount information into the mount table. The actual behavior depends on the value of the 'ensure' parameter. Note that if a `mount` receives an event from another resource, it will try to remount the filesystems if `ensure` is set to `mounted`." feature :refreshable, "The provider can remount the filesystem.", :methods => [:remount] # Use the normal parent class, because we actually want to # call code when sync is called. newproperty(:ensure) do desc "Control what to do with this mount. Set this attribute to `umounted` to make sure the filesystem is in the filesystem table but not mounted (if the filesystem is currently mounted, it will be unmounted). Set it to `absent` to unmount (if necessary) and remove the filesystem from the fstab. Set to `mounted` to add it to the fstab and mount it. Set to `present` to add to fstab but not change mount/unmount status" + # IS -> SHOULD In Sync Action + # ghost -> present NO create + # absent -> present NO create + # (mounted -> present YES) + # (unmounted -> present YES) newvalue(:defined) do provider.create return :mount_created end aliasvalue :present, :defined + # IS -> SHOULD In Sync Action + # ghost -> unmounted NO create, unmount + # absent -> unmounted NO create + # mounted -> unmounted NO unmount newvalue(:unmounted) do - if provider.mounted? - syncothers + case self.retrieve + when :ghost # (not in fstab but mounted) + provider.create + @resource.flush provider.unmount return :mount_unmounted - else + when nil, :absent # (not in fstab and not mounted) provider.create return :mount_created + when :mounted # (in fstab and mounted) + provider.unmount + syncothers # I guess it's more likely that the mount was originally mounted with + # the wrong attributes so I sync AFTER the umount + return :mount_unmounted + else + raise Puppet::Error, "Unexpected change from #{current_value} to unmounted}" end end + # IS -> SHOULD In Sync Action + # ghost -> absent NO unmount + # mounted -> absent NO provider.destroy AND unmount + # unmounted -> absent NO provider.destroy newvalue(:absent, :event => :mount_deleted) do + current_value = self.retrieve provider.unmount if provider.mounted? - - provider.destroy + provider.destroy unless current_value == :ghost end + # IS -> SHOULD In Sync Action + # ghost -> mounted NO provider.create + # absent -> mounted NO provider.create AND mount + # unmounted -> mounted NO mount newvalue(:mounted, :event => :mount_mounted) do # Create the mount point if it does not already exist. current_value = self.retrieve - provider.create if current_value.nil? or current_value == :absent + currently_mounted = provider.mounted? + provider.create if [nil, :absent, :ghost].include?(current_value) syncothers # The fs can be already mounted if it was absent but mounted - provider.mount unless provider.mounted? + provider.property_hash[:needs_mount] = true unless currently_mounted end + # insync: mounted -> present + # unmounted -> present def insync?(is) - if should == :defined and is != :absent + if should == :defined and [:mounted,:unmounted].include?(is) true else super end end - def retrieve - # We need to special case :mounted; if we're absent, we still - # want - curval = super() - if curval == :absent - return :absent - elsif provider.mounted? - return :mounted - else - return :unmounted - end - end - def syncothers # We have to flush any changes to disk. currentvalues = @resource.retrieve_resource # Determine if there are any out-of-sync properties. oos = @resource.send(:properties).find_all do |prop| unless currentvalues.include?(prop) raise Puppet::DevError, "Parent has property %s but it doesn't appear in the current values", [prop.name] end if prop.name == :ensure false else ! prop.safe_insync?(currentvalues[prop]) end end.each { |prop| prop.sync }.length @resource.flush if oos > 0 end end newproperty(:device) do desc "The device providing the mount. This can be whatever device is supporting by the mount, including network devices or devices specified by UUID rather than device path, depending on the operating system." end # Solaris specifies two devices, not just one. newproperty(:blockdevice) do desc "The device to fsck. This is property is only valid on Solaris, and in most cases will default to the correct value." # Default to the device but with "dsk" replaced with "rdsk". defaultto do if Facter["operatingsystem"].value == "Solaris" device = @resource.value(:device) if device =~ %r{/dsk/} device.sub(%r{/dsk/}, "/rdsk/") else nil end else nil end end end newproperty(:fstype) do desc "The mount type. Valid values depend on the operating system. This is a required option." end newproperty(:options) do desc "Mount options for the mounts, as they would appear in the fstab." end newproperty(:pass) do desc "The pass in which the mount is checked." defaultto { 0 if @resource.managed? } end newproperty(:atboot) do desc "Whether to mount the mount at boot. Not all platforms support this." end newproperty(:dump) do desc "Whether to dump the mount. Not all platform support this. Valid values are `1` or `0`. or `2` on FreeBSD, Default is `0`." if Facter["operatingsystem"].value == "FreeBSD" newvalue(%r{(0|1|2)}) else newvalue(%r{(0|1)}) end newvalue(%r{(0|1)}) defaultto { 0 if @resource.managed? } end newproperty(:target) do desc "The file in which to store the mount table. 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 mount path for the mount." isnamevar end newparam(:path) do desc "The deprecated name for the mount point. Please use `name` now." def value=(value) warning "'path' is deprecated for mounts. Please use 'name'." @resource[:name] = value super end end newparam(:remounts) do desc "Whether the mount can be remounted `mount -o remount`. If this is false, then the filesystem will be unmounted and remounted manually, which is prone to failure." newvalues(:true, :false) defaultto do case Facter.value(:operatingsystem) when "FreeBSD", "Darwin", "AIX" false else true end end end def refresh # Only remount if we're supposed to be mounted. provider.remount if self.should(:fstype) != "swap" and provider.mounted? end def value(name) name = symbolize(name) ret = nil if property = @parameters[name] return property.value end end end end diff --git a/lib/puppet/type/package.rb b/lib/puppet/type/package.rb index d73d90dff..1222a5319 100644 --- a/lib/puppet/type/package.rb +++ b/lib/puppet/type/package.rb @@ -1,319 +1,323 @@ # Define the different packaging systems. Each package system is implemented # in a module, which then gets used to individually extend each package object. # This allows packages to exist on the same machine using different packaging # systems. module Puppet newtype(:package) do @doc = "Manage packages. There is a basic dichotomy in package support right now: Some package types (e.g., yum and apt) can retrieve their own package files, while others (e.g., rpm and sun) cannot. For those package formats that cannot retrieve their own files, you can use the `source` parameter to point to the correct file. Puppet will automatically guess the packaging format that you are using based on the platform you are on, but you can override it using the `provider` parameter; each provider defines what it requires in order to function, and you must meet those requirements - to use a given provider." + to use a given provider. + + **Autorequires:** If Puppet is managing the files specified as a package's + `adminfile`, `responsefile`, or `source`, the package resource will autorequire + those files." feature :installable, "The provider can install packages.", :methods => [:install] feature :uninstallable, "The provider can uninstall packages.", :methods => [:uninstall] feature :upgradeable, "The provider can upgrade to the latest version of a package. This feature is used by specifying `latest` as the desired value for the package.", :methods => [:update, :latest] feature :purgeable, "The provider can purge packages. This generally means that all traces of the package are removed, including existing configuration files. This feature is thus destructive and should be used with the utmost care.", :methods => [:purge] feature :versionable, "The provider is capable of interrogating the package database for installed version(s), and can select which out of a set of available versions of a package to install if asked." feature :holdable, "The provider is capable of placing packages on hold such that they are not automatically upgraded as a result of other package dependencies unless explicit action is taken by a user or another package. Held is considered a superset of installed.", :methods => [:hold] ensurable do desc "What state the package should be in. *latest* only makes sense for those packaging formats that can retrieve new packages on their own and will throw an error on those that cannot. For those packaging systems that allow you to specify package versions, specify them here. Similarly, *purged* is only useful for packaging systems that support the notion of managing configuration files separately from 'normal' system files." attr_accessor :latest newvalue(:present, :event => :package_installed) do provider.install end newvalue(:absent, :event => :package_removed) do provider.uninstall end newvalue(:purged, :event => :package_purged, :required_features => :purgeable) do provider.purge end newvalue(:held, :event => :package_held, :required_features => :holdable) do provider.hold end # Alias the 'present' value. aliasvalue(:installed, :present) newvalue(:latest, :required_features => :upgradeable) do # Because yum always exits with a 0 exit code, there's a retrieve # in the "install" method. So, check the current state now, # to compare against later. current = self.retrieve begin provider.update rescue => detail self.fail "Could not update: #{detail}" end if current == :absent :package_installed else :package_changed end end newvalue(/./, :required_features => :versionable) do begin provider.install rescue => detail self.fail "Could not update: #{detail}" end if self.retrieve == :absent :package_installed else :package_changed end end defaultto :installed # Override the parent method, because we've got all kinds of # funky definitions of 'in sync'. def insync?(is) @latest ||= nil @lateststamp ||= (Time.now.to_i - 1000) # Iterate across all of the should values, and see how they # turn out. @should.each { |should| case should when :present return true unless [:absent, :purged, :held].include?(is) when :latest # Short-circuit packages that are not present return false if is == :absent or is == :purged # Don't run 'latest' more than about every 5 minutes if @latest and ((Time.now.to_i - @lateststamp) / 60) < 5 #self.debug "Skipping latest check" else begin @latest = provider.latest @lateststamp = Time.now.to_i rescue => detail error = Puppet::Error.new("Could not get latest version: #{detail}") error.set_backtrace(detail.backtrace) raise error end end case is when @latest return true when :present # This will only happen on retarded packaging systems # that can't query versions. return true else self.debug "#{@resource.name} #{is.inspect} is installed, latest is #{@latest.inspect}" end when :absent return true if is == :absent or is == :purged when :purged return true if is == :purged when is return true end } false end # This retrieves the current state. LAK: I think this method is unused. def retrieve provider.properties[:ensure] end # Provide a bit more information when logging upgrades. def should_to_s(newvalue = @should) if @latest @latest.to_s else super(newvalue) end end end newparam(:name) do desc "The package name. This is the name that the packaging system uses internally, which is sometimes (especially on Solaris) a name that is basically useless to humans. If you want to abstract package installation, then you can use aliases to provide a common name to packages: # In the 'openssl' class $ssl = $operatingsystem ? { solaris => SMCossl, default => openssl } # It is not an error to set an alias to the same value as the # object name. package { $ssl: ensure => installed, alias => openssl } . etc. . $ssh = $operatingsystem ? { solaris => SMCossh, default => openssh } # Use the alias to specify a dependency, rather than # having another selector to figure it out again. package { $ssh: ensure => installed, alias => openssh, require => Package[openssl] } " isnamevar end newparam(:source) do desc "Where to find the actual package. This must be a local file (or on a network file system) or a URL that your specific packaging type understands; Puppet will not retrieve files for you." end newparam(:instance) do desc "A read-only parameter set by the package." end newparam(:status) do desc "A read-only parameter set by the package." end newparam(:type) do desc "Deprecated form of `provider`." munge do |value| warning "'type' is deprecated; use 'provider' instead" @resource[:provider] = value @resource[:provider] end end newparam(:adminfile) do desc "A file containing package defaults for installing packages. This is currently only used on Solaris. The value will be validated according to system rules, which in the case of Solaris means that it should either be a fully qualified path or it should be in `/var/sadm/install/admin`." end newparam(:responsefile) do desc "A file containing any necessary answers to questions asked by the package. This is currently used on Solaris and Debian. The value will be validated according to system rules, but it should generally be a fully qualified path." end newparam(:configfiles) do desc "Whether configfiles should be kept or replaced. Most packages types do not support this parameter." defaultto :keep newvalues(:keep, :replace) end newparam(:category) do desc "A read-only parameter set by the package." end newparam(:platform) do desc "A read-only parameter set by the package." end newparam(:root) do desc "A read-only parameter set by the package." end newparam(:vendor) do desc "A read-only parameter set by the package." end newparam(:description) do desc "A read-only parameter set by the package." end newparam(:allowcdrom) do desc "Tells apt to allow cdrom sources in the sources.list file. Normally apt will bail if you try this." newvalues(:true, :false) end newparam(:flavor) do desc "Newer versions of OpenBSD support 'flavors', which are further specifications for which type of package you want." end autorequire(:file) do autos = [] [:responsefile, :adminfile].each { |param| if val = self[param] autos << val end } if source = self[:source] if source =~ /^#{File::SEPARATOR}/ autos << source end end autos end # This only exists for testing. def clear if obj = @parameters[:ensure] obj.latest = nil end end # The 'query' method returns a hash of info if the package # exists and returns nil if it does not. def exists? @provider.get(:ensure) != :absent end end end diff --git a/lib/puppet/type/schedule.rb b/lib/puppet/type/schedule.rb index 82f17e533..5fb008f6f 100755 --- a/lib/puppet/type/schedule.rb +++ b/lib/puppet/type/schedule.rb @@ -1,349 +1,349 @@ module Puppet newtype(:schedule) do @doc = "Defined schedules for Puppet. The important thing to understand about how schedules are currently implemented in Puppet is that they can only be used to stop a resource from being applied, they never guarantee that it is applied. Every time Puppet applies its configuration, it will collect the list of resources whose schedule does not eliminate them from running right then, but there is currently no system in place to guarantee that a given resource runs at a given time. If you specify a very restrictive schedule and Puppet happens to run at a time within that schedule, then the resources will get applied; otherwise, that work may never get done. Thus, it behooves you to use wider scheduling (e.g., over a couple of hours) combined with periods and repetitions. For instance, if you wanted to restrict certain resources to only running once, between the hours of two and 4 AM, then you would use this schedule: - schedule { maint: - range => \"2 - 4\", - period => daily, - repeat => 1 - } + schedule { maint: + range => \"2 - 4\", + period => daily, + repeat => 1 + } With this schedule, the first time that Puppet runs between 2 and 4 AM, all resources with this schedule will get applied, but they won't get applied again between 2 and 4 because they will have already run once that day, and they won't get applied outside that schedule because they will be outside the scheduled range. Puppet automatically creates a schedule for each valid period with the same name as that period (e.g., hourly and daily). Additionally, a schedule named *puppet* is created and used as the default, with the following attributes: - schedule { puppet: - period => hourly, - repeat => 2 - } + schedule { puppet: + period => hourly, + repeat => 2 + } This will cause resources to be applied every 30 minutes by default. " newparam(:name) do desc "The name of the schedule. This name is used to retrieve the schedule when assigning it to an object: - schedule { daily: - period => daily, - range => \"2 - 4\", - } - - exec { \"/usr/bin/apt-get update\": - schedule => daily - } + schedule { daily: + period => daily, + range => \"2 - 4\", + } + + exec { \"/usr/bin/apt-get update\": + schedule => daily + } " isnamevar end newparam(:range) do desc "The earliest and latest that a resource can be applied. This is always a range within a 24 hour period, and hours must be specified in numbers between 0 and 23, inclusive. Minutes and seconds can be provided, using the normal colon as a separator. For instance: - schedule { maintenance: - range => \"1:30 - 4:30\" - } + schedule { maintenance: + range => \"1:30 - 4:30\" + } This is mostly useful for restricting certain resources to being applied in maintenance windows or during off-peak hours." # This is lame; properties all use arrays as values, but parameters don't. # That's going to hurt eventually. validate do |values| values = [values] unless values.is_a?(Array) values.each { |value| unless value.is_a?(String) and value =~ /\d+(:\d+){0,2}\s*-\s*\d+(:\d+){0,2}/ self.fail "Invalid range value '#{value}'" end } end munge do |values| values = [values] unless values.is_a?(Array) ret = [] values.each { |value| range = [] # Split each range value into a hour, minute, second triad value.split(/\s*-\s*/).each { |val| # Add the values as an array. range << val.split(":").collect { |n| n.to_i } } self.fail "Invalid range #{value}" if range.length != 2 # Make sure the hours are valid [range[0][0], range[1][0]].each do |n| raise ArgumentError, "Invalid hour '#{n}'" if n < 0 or n > 23 end [range[0][1], range[1][1]].each do |n| raise ArgumentError, "Invalid minute '#{n}'" if n and (n < 0 or n > 59) end if range[0][0] > range[1][0] self.fail(("Invalid range #{value}; ") + "ranges cannot span days." ) end ret << range } # Now our array of arrays ret end def match?(previous, now) # The lowest-level array is of the hour, minute, second triad # then it's an array of two of those, to present the limits # then it's array of those ranges @value = [@value] unless @value[0][0].is_a?(Array) @value.each do |value| limits = value.collect do |range| ary = [now.year, now.month, now.day, range[0]] if range[1] ary << range[1] else ary << now.min end if range[2] ary << range[2] else ary << now.sec end time = Time.local(*ary) unless time.hour == range[0] self.devfail( "Incorrectly converted time: #{time}: #{time.hour} vs #{range[0]}" ) end time end unless limits[0] < limits[1] self.info( "Assuming upper limit should be that time the next day" ) ary = limits[1].to_a ary[3] += 1 limits[1] = Time.local(*ary) #self.devfail("Lower limit is above higher limit: %s" % # limits.inspect #) end #self.info limits.inspect #self.notice now return now.between?(*limits) end # Else, return false, since our current time isn't between # any valid times false end end newparam(:periodmatch) do desc "Whether periods should be matched by number (e.g., the two times are in the same hour) or by distance (e.g., the two times are 60 minutes apart)." newvalues(:number, :distance) defaultto :distance end newparam(:period) do desc "The period of repetition for a resource. Choose from among a fixed list of *hourly*, *daily*, *weekly*, and *monthly*. The default is for a resource to get applied every time that Puppet runs, whatever that period is. Note that the period defines how often a given resource will get applied but not when; if you would like to restrict the hours that a given resource can be applied (e.g., only at night during a maintenance window) then use the `range` attribute. If the provided periods are not sufficient, you can provide a value to the *repeat* attribute, which will cause Puppet to schedule the affected resources evenly in the period the specified number of times. Take this schedule: schedule { veryoften: period => hourly, repeat => 6 } This can cause Puppet to apply that resource up to every 10 minutes. At the moment, Puppet cannot guarantee that level of repetition; that is, it can run up to every 10 minutes, but internal factors might prevent it from actually running that often (e.g., long-running Puppet runs will squash conflictingly scheduled runs). See the `periodmatch` attribute for tuning whether to match times by their distance apart or by their specific value." newvalues(:hourly, :daily, :weekly, :monthly, :never) @@scale = { :hourly => 3600, :daily => 86400, :weekly => 604800, :monthly => 2592000 } @@methods = { :hourly => :hour, :daily => :day, :monthly => :month, :weekly => proc do |prev, now| # Run the resource if the previous day was after this weekday (e.g., prev is wed, current is tue) # or if it's been more than a week since we ran prev.wday > now.wday or (now - prev) > (24 * 3600 * 7) end } def match?(previous, now) return false if value == :never value = self.value case @resource[:periodmatch] when :number method = @@methods[value] if method.is_a?(Proc) return method.call(previous, now) else # We negate it, because if they're equal we don't run return now.send(method) != previous.send(method) end when :distance scale = @@scale[value] # If the number of seconds between the two times is greater # than the unit of time, we match. We divide the scale # by the repeat, so that we'll repeat that often within # the scale. diff = (now.to_i - previous.to_i) comparison = (scale / @resource[:repeat]) return (now.to_i - previous.to_i) >= (scale / @resource[:repeat]) end end end newparam(:repeat) do desc "How often the application gets repeated in a given period. Defaults to 1. Must be an integer." defaultto 1 validate do |value| unless value.is_a?(Integer) or value =~ /^\d+$/ raise Puppet::Error, "Repeat must be a number" end # This implicitly assumes that 'periodmatch' is distance -- that # is, if there's no value, we assume it's a valid value. return unless @resource[:periodmatch] if value != 1 and @resource[:periodmatch] != :distance raise Puppet::Error, "Repeat must be 1 unless periodmatch is 'distance', not '#{@resource[:periodmatch]}'" end end munge do |value| value = Integer(value) unless value.is_a?(Integer) value end def match?(previous, now) true end end def self.instances [] end def self.mkdefaultschedules result = [] Puppet.debug "Creating default schedules" result << self.new( :name => "puppet", :period => :hourly, :repeat => "2" ) # And then one for every period @parameters.find { |p| p.name == :period }.value_collection.values.each { |value| result << self.new( :name => value.to_s, :period => value ) } result end def match?(previous = nil, now = nil) # If we've got a value, then convert it to a Time instance previous &&= Time.at(previous) now ||= Time.now # Pull them in order self.class.allattrs.each { |param| if @parameters.include?(param) and @parameters[param].respond_to?(:match?) return false unless @parameters[param].match?(previous, now) end } # If we haven't returned false, then return true; in other words, # any provided schedules need to all match true end end end diff --git a/lib/puppet/type/selmodule.rb b/lib/puppet/type/selmodule.rb index 60be8a855..e76c18cc0 100644 --- a/lib/puppet/type/selmodule.rb +++ b/lib/puppet/type/selmodule.rb @@ -1,53 +1,55 @@ # # Simple module for manageing SELinux policy modules # Puppet::Type.newtype(:selmodule) do @doc = "Manages loading and unloading of SELinux policy modules on the system. Requires SELinux support. See man semodule(8) - for more information on SELinux policy modules." + for more information on SELinux policy modules. + + **Autorequires:** If Puppet is managing the file containing this SELinux policy module (which is either explicitly specified in the `selmodulepath` attribute or will be found at {`selmoduledir`}/{`name`}.pp), the selmodule resource will autorequire that file." ensurable newparam(:name) do desc "The name of the SELinux policy to be managed. You should not include the customary trailing .pp extension." isnamevar end newparam(:selmoduledir) do desc "The directory to look for the compiled pp module file in. Currently defaults to `/usr/share/selinux/targeted`. If selmodulepath is not specified the module will be looked for in this directory in a in a file called NAME.pp, where NAME is the value of the name parameter." defaultto "/usr/share/selinux/targeted" end newparam(:selmodulepath) do desc "The full path to the compiled .pp policy module. You only need to use this if the module file is not in the directory pointed at by selmoduledir." end newproperty(:syncversion) do desc "If set to `true`, the policy will be reloaded if the version found in the on-disk file differs from the loaded version. If set to `false` (the default) the the only check that will be made is if the policy is loaded at all or not." newvalue(:true) newvalue(:false) end autorequire(:file) do if self[:selmodulepath] [self[:selmodulepath]] else ["#{self[:selmoduledir]}/#{self[:name]}.pp"] end end end diff --git a/lib/puppet/type/ssh_authorized_key.rb b/lib/puppet/type/ssh_authorized_key.rb index e3320140e..8338e2d64 100644 --- a/lib/puppet/type/ssh_authorized_key.rb +++ b/lib/puppet/type/ssh_authorized_key.rb @@ -1,98 +1,102 @@ module Puppet newtype(:ssh_authorized_key) do @doc = "Manages SSH authorized keys. Currently only type 2 keys are - supported." + supported. + + **Autorequires:** If Puppet is managing the user account in which this + SSH key should be installed, the `ssh_authorized_key` resource will autorequire + that user." ensurable newparam(:name) do desc "The SSH key comment. This attribute is currently used as a system-wide primary key and therefore has to be unique." isnamevar end newproperty(:type) do desc "The encryption type used: ssh-dss or ssh-rsa." newvalue("ssh-dss") newvalue("ssh-rsa") aliasvalue(:dsa, "ssh-dss") aliasvalue(:rsa, "ssh-rsa") end newproperty(:key) do desc "The key itself; generally a long string of hex digits." end newproperty(:user) do desc "The user account in which the SSH key should be installed. The resource will automatically depend on this user." end newproperty(:target) do desc "The absolute filename in which to store the SSH key. This property is optional and should only be used in cases where keys are stored in a non-standard location (i.e.` not in `~user/.ssh/authorized_keys`)." defaultto :absent def should return super if defined?(@should) and @should[0] != :absent return nil unless user = resource[:user] begin return File.expand_path("~#{user}/.ssh/authorized_keys") rescue Puppet.debug "The required user is not yet present on the system" return nil end end def insync?(is) is == should end end newproperty(:options, :array_matching => :all) do desc "Key options, see sshd(8) for possible values. Multiple values should be specified as an array." defaultto do :absent end def is_to_s(value) if value == :absent or value.include?(:absent) super else value.join(",") end end def should_to_s(value) if value == :absent or value.include?(:absent) super else value.join(",") end end end autorequire(:user) do should(:user) if should(:user) end validate do # Go ahead if target attribute is defined return if @parameters[:target].shouldorig[0] != :absent # Go ahead if user attribute is defined return if @parameters.include?(:user) # If neither target nor user is defined, this is an error raise Puppet::Error, "Attribute 'user' or 'target' is mandatory" end end end diff --git a/lib/puppet/type/user.rb b/lib/puppet/type/user.rb index e7389a0d1..f74e4266f 100755 --- a/lib/puppet/type/user.rb +++ b/lib/puppet/type/user.rb @@ -1,436 +1,449 @@ require 'etc' require 'facter' require 'puppet/property/list' require 'puppet/property/ordered_list' require 'puppet/property/keyvalue' module Puppet newtype(:user) do @doc = "Manage users. This type is mostly built to manage system users, so it is lacking some features useful for managing normal users. This resource type uses the prescribed native tools for creating groups and generally uses POSIX APIs for retrieving information - about them. It does not directly modify `/etc/passwd` or anything." + about them. It does not directly modify `/etc/passwd` or anything. + + **Autorequires:** If Puppet is managing the user's primary group (as provided in the `gid` attribute), the user resource will autorequire that group. If Puppet is managing any role accounts corresponding to the user's roles, the user resource will autorequire those role accounts." feature :allows_duplicates, "The provider supports duplicate users with the same UID." feature :manages_homedir, "The provider can create and remove home directories." feature :manages_passwords, "The provider can modify user passwords, by accepting a password hash." feature :manages_password_age, "The provider can set age requirements and restrictions for passwords." feature :manages_solaris_rbac, "The provider can manage roles and normal users" feature :manages_expiry, "The provider can manage the expiry date for a user." + feature :system_users, + "The provider allows you to create system users with lower UIDs." + newproperty(:ensure, :parent => Puppet::Property::Ensure) do newvalue(:present, :event => :user_created) do provider.create end newvalue(:absent, :event => :user_removed) do provider.delete end newvalue(:role, :event => :role_created, :required_features => :manages_solaris_rbac) do provider.create_role end desc "The basic state that the object should be in." # If they're talking about the thing at all, they generally want to # say it should exist. defaultto do if @resource.managed? :present else nil end end def retrieve if provider.exists? if provider.respond_to?(:is_role?) and provider.is_role? return :role else return :present end else return :absent end end end newproperty(:home) do desc "The home directory of the user. The directory must be created separately and is not currently checked for existence." end newproperty(:uid) do desc "The user ID. Must be specified numerically. For new users being created, if no user ID is specified then one will be chosen automatically, which will likely result in the same user having different IDs on different systems, which is not recommended. This is especially noteworthy if you use Puppet to manage the same user on both Darwin and other platforms, since Puppet does the ID generation for you on Darwin, but the tools do so on other platforms." munge do |value| case value when String if value =~ /^[-0-9]+$/ value = Integer(value) end end return value end end newproperty(:gid) do desc "The user's primary group. Can be specified numerically or by name." munge do |value| if value.is_a?(String) and value =~ /^[-0-9]+$/ Integer(value) else value end end def insync?(is) # We know the 'is' is a number, so we need to convert the 'should' to a number, # too. @should.each do |value| return true if number = Puppet::Util.gid(value) and is == number end false end def sync found = false @should.each do |value| if number = Puppet::Util.gid(value) provider.gid = number found = true break end end fail "Could not find group(s) #{@should.join(",")}" unless found # Use the default event. end end newproperty(:comment) do desc "A description of the user. Generally is a user's full name." end newproperty(:shell) do desc "The user's login shell. The shell must exist and be executable." end newproperty(:password, :required_features => :manages_passwords) do desc "The user's password, in whatever encrypted format the local machine requires. Be sure to enclose any value that includes a dollar sign ($) in single quotes (\')." validate do |value| raise ArgumentError, "Passwords cannot include ':'" if value.is_a?(String) and value.include?(":") end def change_to_s(currentvalue, newvalue) if currentvalue == :absent return "created password" else return "changed password" end end end newproperty(:password_min_age, :required_features => :manages_password_age) do desc "The minimum amount of time in days a password must be used before it may be changed" munge do |value| case value when String Integer(value) else value end end validate do |value| if value.to_s !~ /^-?\d+$/ raise ArgumentError, "Password minimum age must be provided as a number" end end end newproperty(:password_max_age, :required_features => :manages_password_age) do desc "The maximum amount of time in days a password may be used before it must be changed" munge do |value| case value when String Integer(value) else value end end validate do |value| if value.to_s !~ /^-?\d+$/ raise ArgumentError, "Password maximum age must be provided as a number" end end end newproperty(:groups, :parent => Puppet::Property::List) do desc "The groups of which the user is a member. The primary group should not be listed. Multiple groups should be specified as an array." validate do |value| if value =~ /^\d+$/ raise ArgumentError, "Group names must be provided, not numbers" end raise ArgumentError, "Group names must be provided as an array, not a comma-separated list" if value.include?(",") end end newparam(:name) do desc "User name. While limitations are determined for each operating system, it is generally a good idea to keep to the degenerate 8 characters, beginning with a letter." isnamevar end newparam(:membership) do desc "Whether specified groups should be treated as the only groups of which the user is a member or whether they should merely be treated as the minimum membership list." newvalues(:inclusive, :minimum) defaultto :minimum end + newparam(:system, :boolean => true) do + desc "Whether the user is a system user with lower UID." + + newvalues(:true, :false) + + defaultto false + end + newparam(:allowdupe, :boolean => true) do desc "Whether to allow duplicate UIDs." newvalues(:true, :false) defaultto false end newparam(:managehome, :boolean => true) do desc "Whether to manage the home directory when managing the user." newvalues(:true, :false) defaultto false validate do |val| if val.to_s == "true" raise ArgumentError, "User provider #{provider.class.name} can not manage home directories" unless provider.class.manages_homedir? end end end newproperty(:expiry, :required_features => :manages_expiry) do desc "The expiry date for this user. Must be provided in a zero padded YYYY-MM-DD format - e.g 2010-02-19." validate do |value| if value !~ /^\d{4}-\d{2}-\d{2}$/ raise ArgumentError, "Expiry dates must be YYYY-MM-DD" end end end # Autorequire the group, if it's around autorequire(:group) do autos = [] if obj = @parameters[:gid] and groups = obj.shouldorig groups = groups.collect { |group| if group =~ /^\d+$/ Integer(group) else group end } groups.each { |group| case group when Integer if resource = catalog.resources.find { |r| r.is_a?(Puppet::Type.type(:group)) and r.should(:gid) == group } autos << resource end else autos << group end } end if obj = @parameters[:groups] and groups = obj.should autos += groups.split(",") end autos end # Provide an external hook. Yay breaking out of APIs. def exists? provider.exists? end def retrieve absent = false properties.inject({}) { |prophash, property| current_value = :absent if absent prophash[property] = :absent else current_value = property.retrieve prophash[property] = current_value end if property.name == :ensure and current_value == :absent absent = true end prophash } end newproperty(:roles, :parent => Puppet::Property::List, :required_features => :manages_solaris_rbac) do desc "The roles the user has. Multiple roles should be specified as an array." def membership :role_membership end validate do |value| if value =~ /^\d+$/ raise ArgumentError, "Role names must be provided, not numbers" end raise ArgumentError, "Role names must be provided as an array, not a comma-separated list" if value.include?(",") end end #autorequire the roles that the user has autorequire(:user) do reqs = [] if roles_property = @parameters[:roles] and roles = roles_property.should reqs += roles.split(',') end reqs end newparam(:role_membership) do desc "Whether specified roles should be treated as the only roles of which the user is a member or whether they should merely be treated as the minimum membership list." newvalues(:inclusive, :minimum) defaultto :minimum end newproperty(:auths, :parent => Puppet::Property::List, :required_features => :manages_solaris_rbac) do desc "The auths the user has. Multiple auths should be specified as an array." def membership :auth_membership end validate do |value| if value =~ /^\d+$/ raise ArgumentError, "Auth names must be provided, not numbers" end raise ArgumentError, "Auth names must be provided as an array, not a comma-separated list" if value.include?(",") end end newparam(:auth_membership) do desc "Whether specified auths should be treated as the only auths of which the user is a member or whether they should merely be treated as the minimum membership list." newvalues(:inclusive, :minimum) defaultto :minimum end newproperty(:profiles, :parent => Puppet::Property::OrderedList, :required_features => :manages_solaris_rbac) do desc "The profiles the user has. Multiple profiles should be specified as an array." def membership :profile_membership end validate do |value| if value =~ /^\d+$/ raise ArgumentError, "Profile names must be provided, not numbers" end raise ArgumentError, "Profile names must be provided as an array, not a comma-separated list" if value.include?(",") end end newparam(:profile_membership) do desc "Whether specified roles should be treated as the only roles of which the user is a member or whether they should merely be treated as the minimum membership list." newvalues(:inclusive, :minimum) defaultto :minimum end newproperty(:keys, :parent => Puppet::Property::KeyValue, :required_features => :manages_solaris_rbac) do desc "Specify user attributes in an array of keyvalue pairs" def membership :key_membership end validate do |value| raise ArgumentError, "key value pairs must be seperated by an =" unless value.include?("=") end end newparam(:key_membership) do desc "Whether specified key value pairs should be treated as the only attributes of the user or whether they should merely be treated as the minimum list." newvalues(:inclusive, :minimum) defaultto :minimum end newproperty(:project, :required_features => :manages_solaris_rbac) do desc "The name of the project associated with a user" end end end diff --git a/lib/puppet/type/zfs.rb b/lib/puppet/type/zfs.rb index 1757931f8..6f04bddd8 100755 --- a/lib/puppet/type/zfs.rb +++ b/lib/puppet/type/zfs.rb @@ -1,50 +1,52 @@ module Puppet newtype(:zfs) do - @doc = "Manage zfs. Create destroy and set properties on zfs instances." + @doc = "Manage zfs. Create destroy and set properties on zfs instances. + +**Autorequires:** If Puppet is managing the zpool at the root of this zfs instance, the zfs resource will autorequire it. If Puppet is managing any parent zfs instances, the zfs resource will autorequire them." ensurable newparam(:name) do desc "The full name for this filesystem. (including the zpool)" end newproperty(:mountpoint) do desc "The mountpoint property." end newproperty(:compression) do desc "The compression property." end newproperty(:copies) do desc "The copies property." end newproperty(:quota) do desc "The quota property." end newproperty(:reservation) do desc "The reservation property." end newproperty(:sharenfs) do desc "The sharenfs property." end newproperty(:snapdir) do desc "The snapdir property." end autorequire(:zpool) do #strip the zpool off the zfs name and autorequire it [@parameters[:name].value.split('/')[0]] end autorequire(:zfs) do #slice and dice, we want all the zfs before this one names = @parameters[:name].value.split('/') names.slice(1..-2).inject([]) { |a,v| a << "#{a.last}/#{v}" }.collect { |fs| names[0] + fs } end end end diff --git a/lib/puppet/type/zone.rb b/lib/puppet/type/zone.rb index 408d6f5dd..471619c98 100644 --- a/lib/puppet/type/zone.rb +++ b/lib/puppet/type/zone.rb @@ -1,444 +1,446 @@ Puppet::Type.newtype(:zone) do - @doc = "Solaris zones." + @doc = "Solaris zones. + +**Autorequires:** If Puppet is managing the directory specified as the root of the zone's filesystem (with the `path` attribute), the zone resource will autorequire that directory." # These properties modify the zone configuration, and they need to provide # the text separately from syncing it, so all config statements can be rolled # into a single creation statement. class ZoneConfigProperty < Puppet::Property # Perform the config operation. def sync provider.setconfig self.configtext end end # Those properties that can have multiple instances. class ZoneMultiConfigProperty < ZoneConfigProperty def configtext list = @should current_value = self.retrieve unless current_value.is_a? Symbol if current_value.is_a? Array list += current_value else list << current_value if current_value end end # Some hackery so we can test whether current_value is an array or a symbol if current_value.is_a? Array tmpis = current_value else if current_value tmpis = [current_value] else tmpis = [] end end rms = [] adds = [] # Collect the modifications to make list.sort.uniq.collect do |obj| # Skip objectories that are configured and should be next if tmpis.include?(obj) and @should.include?(obj) if tmpis.include?(obj) rms << obj else adds << obj end end # And then perform all of the removals before any of the adds. (rms.collect { |o| rm(o) } + adds.collect { |o| add(o) }).join("\n") end # We want all specified directories to be included. def insync?(current_value) if current_value.is_a? Array and @should.is_a? Array current_value.sort == @should.sort else current_value == @should end end end ensurable do desc "The running state of the zone. The valid states directly reflect the states that `zoneadm` provides. The states are linear, in that a zone must be `configured` then `installed`, and only then can be `running`. Note also that `halt` is currently used to stop zones." @states = {} @parametervalues = [] def self.alias_state(values) @state_aliases ||= {} values.each do |nick, name| @state_aliases[nick] = name end end def self.newvalue(name, hash) @parametervalues = [] if @parametervalues.is_a? Hash @parametervalues << name @states[name] = hash hash[:name] = name end def self.state_name(name) if other = @state_aliases[name] other else name end end newvalue :absent, :down => :destroy newvalue :configured, :up => :configure, :down => :uninstall newvalue :installed, :up => :install, :down => :stop newvalue :running, :up => :start alias_state :incomplete => :installed, :ready => :installed, :shutting_down => :running defaultto :running def self.state_index(value) @parametervalues.index(state_name(value)) end # Return all of the states between two listed values, exclusive # of the first item. def self.state_sequence(first, second) findex = sindex = nil unless findex = @parametervalues.index(state_name(first)) raise ArgumentError, "'#{first}' is not a valid zone state" end unless sindex = @parametervalues.index(state_name(second)) raise ArgumentError, "'#{first}' is not a valid zone state" end list = nil # Apparently ranges are unidirectional, so we have to reverse # the range op twice. if findex > sindex list = @parametervalues[sindex..findex].collect do |name| @states[name] end.reverse else list = @parametervalues[findex..sindex].collect do |name| @states[name] end end # The first result is the current state, so don't return it. list[1..-1] end def retrieve provider.properties[:ensure] end def sync method = nil if up? direction = :up else direction = :down end # We need to get the state we're currently in and just call # everything between it and us. self.class.state_sequence(self.retrieve, self.should).each do |state| if method = state[direction] warned = false while provider.processing? unless warned info "Waiting for zone to finish processing" warned = true end sleep 1 end provider.send(method) else raise Puppet::DevError, "Cannot move #{direction} from #{st[:name]}" end end ("zone_#{self.should}").intern end # Are we moving up the property tree? def up? current_value = self.retrieve self.class.state_index(current_value) < self.class.state_index(self.should) end end newparam(:name) do desc "The name of the zone." isnamevar end newparam(:id) do desc "The numerical ID of the zone. This number is autogenerated and cannot be changed." end newparam(:clone) do desc "Instead of installing the zone, clone it from another zone. If the zone root resides on a zfs file system, a snapshot will be used to create the clone, is it redisides on ufs, a copy of the zone will be used. The zone you clone from must not be running." end newproperty(:ip, :parent => ZoneMultiConfigProperty) do require 'ipaddr' desc "The IP address of the zone. IP addresses must be specified with the interface, separated by a colon, e.g.: bge0:192.168.0.1. For multiple interfaces, specify them in an array." # Add an interface. def add(str) interface, ip, defrouter = ipsplit(str) cmd = "add net\n" cmd += "set physical=#{interface}\n" if interface cmd += "set address=#{ip}\n" if ip cmd += "set defrouter=#{defrouter}\n" if defrouter #if @resource[:iptype] == :shared cmd += "end\n" end # Convert a string into the component interface, address and defrouter def ipsplit(str) interface, address, defrouter = str.split(':') return interface, address, defrouter end # Remove an interface. def rm(str) interface, ip, defrouter = ipsplit(str) # Reality seems to disagree with the documentation here; the docs # specify that braces are required, but they're apparently only # required if you're specifying multiple values. if ip "remove net address=#{ip}" elsif interface "remove net interface=#{interface}" else raise ArgumentError, "can not remove network based on default router" end end end newproperty(:iptype, :parent => ZoneConfigProperty) do desc "The IP stack type of the zone. Can either be 'shared' or 'exclusive'." defaultto :shared newvalue :shared newvalue :exclusive def configtext "set ip-type=#{self.should}" end end newproperty(:autoboot, :parent => ZoneConfigProperty) do desc "Whether the zone should automatically boot." defaultto true newvalue(:true) {} newvalue(:false) {} def configtext "set autoboot=#{self.should}" end end newproperty(:pool, :parent => ZoneConfigProperty) do desc "The resource pool for this zone." def configtext "set pool=#{self.should}" end end newproperty(:shares, :parent => ZoneConfigProperty) do desc "Number of FSS CPU shares allocated to the zone." def configtext "add rctl\nset name=zone.cpu-shares\nadd value (priv=privileged,limit=#{self.should},action=none)\nend" end end newproperty(:inherit, :parent => ZoneMultiConfigProperty) do desc "The list of directories that the zone inherits from the global zone. All directories must be fully qualified." validate do |value| unless value =~ /^\// raise ArgumentError, "Inherited filesystems must be fully qualified" end end # Add a directory to our list of inherited directories. def add(dir) "add inherit-pkg-dir\nset dir=#{dir}\nend" end def rm(dir) # Reality seems to disagree with the documentation here; the docs # specify that braces are required, but they're apparently only # required if you're specifying multiple values. "remove inherit-pkg-dir dir=#{dir}" end def should @should end end # Specify the sysidcfg file. This is pretty hackish, because it's # only used to boot the zone the very first time. newparam(:sysidcfg) do desc %{The text to go into the sysidcfg file when the zone is first booted. The best way is to use a template: # $templatedir/sysidcfg system_locale=en_US timezone=GMT terminal=xterms security_policy=NONE root_password=<%= password %> timeserver=localhost name_service=DNS {domain_name=<%= domain %> name_server=<%= nameserver %>} network_interface=primary {hostname=<%= realhostname %> ip_address=<%= ip %> netmask=<%= netmask %> protocol_ipv6=no default_route=<%= defaultroute %>} nfs4_domain=dynamic And then call that: zone { myzone: ip => "bge0:192.168.0.23", sysidcfg => template(sysidcfg), path => "/opt/zones/myzone", realhostname => "fully.qualified.domain.name" } The sysidcfg only matters on the first booting of the zone, so Puppet only checks for it at that time.} end newparam(:path) do desc "The root of the zone's filesystem. Must be a fully qualified file name. If you include '%s' in the path, then it will be replaced with the zone's name. At this point, you cannot use Puppet to move a zone." validate do |value| unless value =~ /^\// raise ArgumentError, "The zone base must be fully qualified" end end munge do |value| if value =~ /%s/ value % @resource[:name] else value end end end newparam(:create_args) do desc "Arguments to the zonecfg create command. This can be used to create branded zones." end newparam(:install_args) do desc "Arguments to the zoneadm install command. This can be used to create branded zones." end newparam(:realhostname) do desc "The actual hostname of the zone." end # If Puppet is also managing the base dir or its parent dir, list them # both as prerequisites. autorequire(:file) do if @parameters.include? :path [@parameters[:path].value, File.dirname(@parameters[:path].value)] else nil end end def validate_ip(ip, name) IPAddr.new(ip) if ip rescue ArgumentError self.fail "'#{ip}' is an invalid #{name}" end validate do value = self[:ip] interface, address, defrouter = value.split(':') if self[:iptype] == :shared if (interface && address && defrouter.nil?) || (interface && address && defrouter) validate_ip(address, "IP address") validate_ip(defrouter, "default router") else self.fail "ip must contain interface name and ip address separated by a \":\"" end else self.fail "only interface may be specified when using exclusive IP stack: #{value}" unless interface && address.nil? && defrouter.nil? end self.fail "zone path is required" unless self[:path] end def retrieve provider.flush if hash = provider.properties and hash[:ensure] != :absent result = setstatus(hash) result else # Return all properties as absent. return properties.inject({}) do | prophash, property| prophash[property] = :absent prophash end end end # Take the results of a listing and set everything appropriately. def setstatus(hash) prophash = {} hash.each do |param, value| next if param == :name case self.class.attrtype(param) when :property # Only try to provide values for the properties we're managing if prop = self.property(param) prophash[prop] = value end else self[param] = value end end prophash end end diff --git a/lib/puppet/type/zpool.rb b/lib/puppet/type/zpool.rb index df06522e8..40ee8f286 100755 --- a/lib/puppet/type/zpool.rb +++ b/lib/puppet/type/zpool.rb @@ -1,88 +1,90 @@ module Puppet class Property class VDev < Property def flatten_and_sort(array) array.collect { |a| a.split(' ') }.flatten.sort end def insync?(is) return @should == [:absent] if is == :absent flatten_and_sort(is) == flatten_and_sort(@should) end end class MultiVDev < VDev def insync?(is) return @should == [:absent] if is == :absent return false unless is.length == @should.length is.each_with_index { |list, i| return false unless flatten_and_sort(list) == flatten_and_sort(@should[i]) } #if we made it this far we are in sync true end end end newtype(:zpool) do @doc = "Manage zpools. Create and delete zpools. The provider WILL NOT SYNC, only report differences. Supports vdevs with mirrors, raidz, logs and spares." ensurable newproperty(:disk, :array_matching => :all, :parent => Puppet::Property::VDev) do desc "The disk(s) for this pool. Can be an array or space separated string" end newproperty(:mirror, :array_matching => :all, :parent => Puppet::Property::MultiVDev) do - desc "List of all the devices to mirror for this pool. Each mirror should be a space separated string: + desc "List of all the devices to mirror for this pool. Each mirror should be a + space separated string: - mirror => [\"disk1 disk2\", \"disk3 disk4\"] + mirror => [\"disk1 disk2\", \"disk3 disk4\"], " validate do |value| raise ArgumentError, "mirror names must be provided as string separated, not a comma-separated list" if value.include?(",") end end newproperty(:raidz, :array_matching => :all, :parent => Puppet::Property::MultiVDev) do - desc "List of all the devices to raid for this pool. Should be an array of space separated strings: - - raidz => [\"disk1 disk2\", \"disk3 disk4\"] + desc "List of all the devices to raid for this pool. Should be an array of + space separated strings: + + raidz => [\"disk1 disk2\", \"disk3 disk4\"], " validate do |value| raise ArgumentError, "raid names must be provided as string separated, not a comma-separated list" if value.include?(",") end end newproperty(:spare, :array_matching => :all, :parent => Puppet::Property::VDev) do desc "Spare disk(s) for this pool." end newproperty(:log, :array_matching => :all, :parent => Puppet::Property::VDev) do desc "Log disks for this pool. (doesn't support mirroring yet)" end newparam(:pool) do desc "The name for this pool." isnamevar end newparam(:raid_parity) do desc "Determines parity when using raidz property." end validate do has_should = [:disk, :mirror, :raidz].select { |prop| self.should(prop) } self.fail "You cannot specify #{has_should.join(" and ")} on this type (only one)" if has_should.length > 1 end end end diff --git a/lib/puppet/util.rb b/lib/puppet/util.rb index 850d147e2..d06f44808 100644 --- a/lib/puppet/util.rb +++ b/lib/puppet/util.rb @@ -1,464 +1,469 @@ # A module to collect utility functions. require 'puppet/util/monkey_patches' require 'sync' require 'puppet/external/lock' require 'monitor' +require 'puppet/util/execution_stub' module Puppet # A command failed to execute. require 'puppet/error' class ExecutionFailure < Puppet::Error end module Util require 'benchmark' # These are all for backward compatibility -- these are methods that used # to be in Puppet::Util but have been moved into external modules. require 'puppet/util/posix' extend Puppet::Util::POSIX @@sync_objects = {}.extend MonitorMixin def self.activerecord_version if (defined?(::ActiveRecord) and defined?(::ActiveRecord::VERSION) and defined?(::ActiveRecord::VERSION::MAJOR) and defined?(::ActiveRecord::VERSION::MINOR)) ([::ActiveRecord::VERSION::MAJOR, ::ActiveRecord::VERSION::MINOR].join('.').to_f) else 0 end end def self.synchronize_on(x,type) sync_object,users = 0,1 begin @@sync_objects.synchronize { (@@sync_objects[x] ||= [Sync.new,0])[users] += 1 } @@sync_objects[x][sync_object].synchronize(type) { yield } ensure @@sync_objects.synchronize { @@sync_objects.delete(x) unless (@@sync_objects[x][users] -= 1) > 0 } end end # Change the process to a different user def self.chuser if group = Puppet[:group] group = self.gid(group) raise Puppet::Error, "No such group #{Puppet[:group]}" unless group unless Puppet::Util::SUIDManager.gid == group begin Puppet::Util::SUIDManager.egid = group Puppet::Util::SUIDManager.gid = group rescue => detail Puppet.warning "could not change to group #{group.inspect}: #{detail}" $stderr.puts "could not change to group #{group.inspect}" # Don't exit on failed group changes, since it's # not fatal #exit(74) end end end if user = Puppet[:user] user = self.uid(user) raise Puppet::Error, "No such user #{Puppet[:user]}" unless user unless Puppet::Util::SUIDManager.uid == user begin Puppet::Util::SUIDManager.initgroups(user) Puppet::Util::SUIDManager.uid = user Puppet::Util::SUIDManager.euid = user rescue => detail $stderr.puts "Could not change to user #{user}: #{detail}" exit(74) end end end end # Create instance methods for each of the log levels. This allows # the messages to be a little richer. Most classes will be calling this # method. def self.logmethods(klass, useself = true) Puppet::Util::Log.eachlevel { |level| klass.send(:define_method, level, proc { |args| args = args.join(" ") if args.is_a?(Array) if useself Puppet::Util::Log.create( :level => level, :source => self, :message => args ) else Puppet::Util::Log.create( :level => level, :message => args ) end }) } end # Proxy a bunch of methods to another object. def self.classproxy(klass, objmethod, *methods) classobj = class << klass; self; end methods.each do |method| classobj.send(:define_method, method) do |*args| obj = self.send(objmethod) obj.send(method, *args) end end end # Proxy a bunch of methods to another object. def self.proxy(klass, objmethod, *methods) methods.each do |method| klass.send(:define_method, method) do |*args| obj = self.send(objmethod) obj.send(method, *args) end end end # XXX this should all be done using puppet objects, not using # normal mkdir def self.recmkdir(dir,mode = 0755) if FileTest.exist?(dir) return false else tmp = dir.sub(/^\//,'') path = [File::SEPARATOR] tmp.split(File::SEPARATOR).each { |dir| path.push dir if ! FileTest.exist?(File.join(path)) Dir.mkdir(File.join(path), mode) elsif FileTest.directory?(File.join(path)) next else FileTest.exist?(File.join(path)) raise "Cannot create #{dir}: basedir #{File.join(path)} is a file" end } return true end end # Execute a given chunk of code with a new umask. def self.withumask(mask) cur = File.umask(mask) begin yield ensure File.umask(cur) end end def benchmark(*args) msg = args.pop level = args.pop object = nil if args.empty? if respond_to?(level) object = self else object = Puppet end else object = args.pop end raise Puppet::DevError, "Failed to provide level to :benchmark" unless level unless level == :none or object.respond_to? level raise Puppet::DevError, "Benchmarked object does not respond to #{level}" end # Only benchmark if our log level is high enough if level != :none and Puppet::Util::Log.sendlevel?(level) result = nil seconds = Benchmark.realtime { yield } object.send(level, msg + (" in %0.2f seconds" % seconds)) return seconds else yield end end def which(bin) if bin =~ /^\// return bin if FileTest.file? bin and FileTest.executable? bin else ENV['PATH'].split(File::PATH_SEPARATOR).each do |dir| dest=File.join(dir, bin) return dest if FileTest.file? dest and FileTest.executable? dest end end nil end module_function :which # Execute the provided command in a pipe, yielding the pipe object. def execpipe(command, failonfail = true) if respond_to? :debug debug "Executing '#{command}'" else Puppet.debug "Executing '#{command}'" end output = open("| #{command} 2>&1") do |pipe| yield pipe end if failonfail unless $CHILD_STATUS == 0 raise ExecutionFailure, output end end output end def execfail(command, exception) output = execute(command) return output rescue ExecutionFailure raise exception, output end # Execute the desired command, and return the status and output. # def execute(command, failonfail = true, uid = nil, gid = nil) # :combine sets whether or not to combine stdout/stderr in the output # :stdinfile sets a file that can be used for stdin. Passing a string # for stdin is not currently supported. def execute(command, arguments = {:failonfail => true, :combine => true}) if command.is_a?(Array) command = command.flatten.collect { |i| i.to_s } str = command.join(" ") else # We require an array here so we know where we're incorrectly # using a string instead of an array. Once everything is # switched to an array, we might relax this requirement. raise ArgumentError, "Must pass an array to execute()" end if respond_to? :debug debug "Executing '#{str}'" else Puppet.debug "Executing '#{str}'" end arguments[:uid] = Puppet::Util::SUIDManager.convert_xid(:uid, arguments[:uid]) if arguments[:uid] arguments[:gid] = Puppet::Util::SUIDManager.convert_xid(:gid, arguments[:gid]) if arguments[:gid] + if execution_stub = Puppet::Util::ExecutionStub.current_value + return execution_stub.call(command, arguments) + end + @@os ||= Facter.value(:operatingsystem) output = nil child_pid, child_status = nil # There are problems with read blocking with badly behaved children # read.partialread doesn't seem to capture either stdout or stderr # We hack around this using a temporary file # The idea here is to avoid IO#read whenever possible. output_file="/dev/null" error_file="/dev/null" if ! arguments[:squelch] require "tempfile" output_file = Tempfile.new("puppet") error_file=output_file if arguments[:combine] end if Puppet.features.posix? oldverb = $VERBOSE $VERBOSE = nil child_pid = Kernel.fork $VERBOSE = oldverb if child_pid # Parent process executes this child_status = (Process.waitpid2(child_pid)[1]).to_i >> 8 else # Child process executes this Process.setsid begin if arguments[:stdinfile] $stdin.reopen(arguments[:stdinfile]) else $stdin.reopen("/dev/null") end $stdout.reopen(output_file) $stderr.reopen(error_file) 3.upto(256){|fd| IO::new(fd).close rescue nil} if arguments[:gid] Process.egid = arguments[:gid] Process.gid = arguments[:gid] unless @@os == "Darwin" end if arguments[:uid] Process.euid = arguments[:uid] Process.uid = arguments[:uid] unless @@os == "Darwin" end ENV['LANG'] = ENV['LC_ALL'] = ENV['LC_MESSAGES'] = ENV['LANGUAGE'] = 'C' if command.is_a?(Array) Kernel.exec(*command) else Kernel.exec(command) end rescue => detail puts detail.to_s exit!(1) end end elsif Puppet.features.microsoft_windows? command = command.collect {|part| '"' + part.gsub(/"/, '\\"') + '"'}.join(" ") if command.is_a?(Array) Puppet.debug "Creating process '#{command}'" processinfo = Process.create( :command_line => command ) child_status = (Process.waitpid2(child_pid)[1]).to_i >> 8 end # read output in if required if ! arguments[:squelch] # Make sure the file's actually there. This is # basically a race condition, and is probably a horrible # way to handle it, but, well, oh well. unless FileTest.exists?(output_file.path) Puppet.warning "sleeping" sleep 0.5 unless FileTest.exists?(output_file.path) Puppet.warning "sleeping 2" sleep 1 unless FileTest.exists?(output_file.path) Puppet.warning "Could not get output" output = "" end end end unless output # We have to explicitly open here, so that it reopens # after the child writes. output = output_file.open.read # The 'true' causes the file to get unlinked right away. output_file.close(true) end end if arguments[:failonfail] unless child_status == 0 raise ExecutionFailure, "Execution of '#{str}' returned #{child_status}: #{output}" end end output end module_function :execute # Create an exclusive lock. def threadlock(resource, type = Sync::EX) Puppet::Util.synchronize_on(resource,type) { yield } end # Because some modules provide their own version of this method. alias util_execute execute module_function :benchmark def memory unless defined?(@pmap) @pmap = which('pmap') end if @pmap %x{#{@pmap} #{Process.pid}| grep total}.chomp.sub(/^\s*total\s+/, '').sub(/K$/, '').to_i else 0 end end def symbolize(value) if value.respond_to? :intern value.intern else value end end def symbolizehash(hash) newhash = {} hash.each do |name, val| if name.is_a? String newhash[name.intern] = val else newhash[name] = val end end end def symbolizehash!(hash) hash.each do |name, val| if name.is_a? String hash[name.intern] = val hash.delete(name) end end hash end module_function :symbolize, :symbolizehash, :symbolizehash! # Just benchmark, with no logging. def thinmark seconds = Benchmark.realtime { yield } seconds end module_function :memory, :thinmark def secure_open(file,must_be_w,&block) raise Puppet::DevError,"secure_open only works with mode 'w'" unless must_be_w == 'w' raise Puppet::DevError,"secure_open only requires a block" unless block_given? Puppet.warning "#{file} was a symlink to #{File.readlink(file)}" if File.symlink?(file) if File.exists?(file) or File.symlink?(file) wait = File.symlink?(file) ? 5.0 : 0.1 File.delete(file) sleep wait # give it a chance to reappear, just in case someone is actively trying something. end begin File.open(file,File::CREAT|File::EXCL|File::TRUNC|File::WRONLY,&block) rescue Errno::EEXIST desc = File.symlink?(file) ? "symlink to #{File.readlink(file)}" : File.stat(file).ftype puts "Warning: #{file} was apparently created by another process (as" puts "a #{desc}) as soon as it was deleted by this process. Someone may be trying" puts "to do something objectionable (such as tricking you into overwriting system" puts "files if you are running as root)." raise end end module_function :secure_open end end require 'puppet/util/errors' require 'puppet/util/methodhelper' require 'puppet/util/metaid' require 'puppet/util/classgen' require 'puppet/util/docs' require 'puppet/util/execution' require 'puppet/util/logging' require 'puppet/util/package' require 'puppet/util/warnings' diff --git a/lib/puppet/util/command_line.rb b/lib/puppet/util/command_line.rb index 4aff0a8cb..7f74d266a 100644 --- a/lib/puppet/util/command_line.rb +++ b/lib/puppet/util/command_line.rb @@ -1,99 +1,99 @@ module Puppet module Util class CommandLine LegacyName = Hash.new{|h,k| k}.update( { 'agent' => 'puppetd', 'cert' => 'puppetca', 'doc' => 'puppetdoc', 'filebucket' => 'filebucket', 'apply' => 'puppet', 'describe' => 'pi', 'queue' => 'puppetqd', 'resource' => 'ralsh', 'kick' => 'puppetrun', 'master' => 'puppetmasterd', }) def initialize( zero = $0, argv = ARGV, stdin = STDIN ) @zero = zero @argv = argv.dup @stdin = stdin @subcommand_name, @args = subcommand_and_args( @zero, @argv, @stdin ) end attr :subcommand_name attr :args def appdir File.join('puppet', 'application') end def available_subcommands absolute_appdirs = $LOAD_PATH.collect do |x| File.join(x,'puppet','application') end.select{ |x| File.directory?(x) } absolute_appdirs.inject([]) do |commands, dir| commands + Dir[File.join(dir, '*.rb')].map{|fn| File.basename(fn, '.rb')} end.uniq end def usage_message usage = "Usage: puppet command " available = "Available commands are: #{available_subcommands.sort.join(', ')}" [usage, available].join("\n") end def require_application(application) require File.join(appdir, application) end def execute if subcommand_name.nil? puts usage_message elsif available_subcommands.include?(subcommand_name) #subcommand require_application subcommand_name Puppet::Application.find(subcommand_name).new(self).run else abort "Error: Unknown command #{subcommand_name}.\n#{usage_message}" unless execute_external_subcommand end end def execute_external_subcommand external_command = "puppet-#{subcommand_name}" require 'puppet/util' path_to_subcommand = Puppet::Util.which( external_command ) return false unless path_to_subcommand system( path_to_subcommand, *args ) true end def legacy_executable_name LegacyName[ subcommand_name ] end private def subcommand_and_args( zero, argv, stdin ) zero = File.basename(zero, '.rb') if zero == 'puppet' case argv.first when nil; [ stdin.tty? ? nil : "apply", argv] # ttys get usage info - when "--help"; [nil, argv] # help should give you usage, not the help for `puppet apply` + when "--help", "-h"; [nil, argv] # help should give you usage, not the help for `puppet apply` when /^-|\.pp$|\.rb$/; ["apply", argv] else [ argv.first, argv[1..-1] ] end else [ zero, argv ] end end end end end diff --git a/lib/puppet/util/execution.rb b/lib/puppet/util/execution.rb index dd820f856..69f4f2c15 100644 --- a/lib/puppet/util/execution.rb +++ b/lib/puppet/util/execution.rb @@ -1,21 +1,20 @@ module Puppet::Util::Execution module_function # Run some code with a specific environment. Resets the environment back to # what it was at the end of the code. def withenv(hash) - oldvals = {} + saved = ENV.to_hash hash.each do |name, val| - name = name.to_s - oldvals[name] = ENV[name] - ENV[name] = val + ENV[name.to_s] = val end yield ensure - oldvals.each do |name, val| + ENV.clear + saved.each do |name, val| ENV[name] = val end end end diff --git a/lib/puppet/util/execution_stub.rb b/lib/puppet/util/execution_stub.rb new file mode 100644 index 000000000..af74e0f72 --- /dev/null +++ b/lib/puppet/util/execution_stub.rb @@ -0,0 +1,26 @@ +module Puppet::Util + class ExecutionStub + class << self + # Set a stub block that Puppet::Util.execute() should invoke instead + # of actually executing commands on the target machine. Intended + # for spec testing. + # + # The arguments passed to the block are |command, options|, where + # command is an array of strings and options is an options hash. + def set(&block) + @value = block + end + + # Uninstall any execution stub, so that calls to + # Puppet::Util.execute() behave normally again. + def reset + @value = nil + end + + # Retrieve the current execution stub, or nil if there is no stub. + def current_value + @value + end + end + end +end diff --git a/lib/puppet/util/monkey_patches.rb b/lib/puppet/util/monkey_patches.rb index 6b5af8350..16384855a 100644 --- a/lib/puppet/util/monkey_patches.rb +++ b/lib/puppet/util/monkey_patches.rb @@ -1,50 +1,71 @@ unless defined? JRUBY_VERSION Process.maxgroups = 1024 end module RDoc def self.caller(skip=nil) in_gem_wrapper = false Kernel.caller.reject { |call| in_gem_wrapper ||= call =~ /#{Regexp.escape $0}:\d+:in `load'/ } end end require "yaml" require "puppet/util/zaml.rb" class Symbol def to_zaml(z) z.emit("!ruby/sym ") to_s.to_zaml(z) end + def <=> (other) + self.to_s <=> other.to_s + end end [Object, Exception, Integer, Struct, Date, Time, Range, Regexp, Hash, Array, Float, String, FalseClass, TrueClass, Symbol, NilClass, Class].each { |cls| cls.class_eval do def to_yaml(ignored=nil) ZAML.dump(self) end end } def YAML.dump(*args) ZAML.dump(*args) end # # Workaround for bug in MRI 1.8.7, see # http://redmine.ruby-lang.org/issues/show/2708 # for details # if RUBY_VERSION == '1.8.7' class NilClass def closed? true end end end +class Object + # The following code allows callers to make assertions that are only + # checked when the environment variable PUPPET_ENABLE_ASSERTIONS is + # set to a non-empty string. For example: + # + # assert_that { condition } + # assert_that(message) { condition } + if ENV["PUPPET_ENABLE_ASSERTIONS"].to_s != '' + def assert_that(message = nil) + unless yield + raise Exception.new("Assertion failure: #{message}") + end + end + else + def assert_that(message = nil) + end + end +end diff --git a/lib/puppet/util/rdoc/code_objects.rb b/lib/puppet/util/rdoc/code_objects.rb index 3854fbc01..3c789a0c5 100644 --- a/lib/puppet/util/rdoc/code_objects.rb +++ b/lib/puppet/util/rdoc/code_objects.rb @@ -1,241 +1,280 @@ require 'rdoc/code_objects' module RDoc # This modules contains various class that are used to hold information # about the various Puppet language structures we found while parsing. # # Those will be mapped to their html counterparts which are defined in # PuppetGenerator. # PuppetTopLevel is a top level (usually a .pp/.rb file) class PuppetTopLevel < TopLevel attr_accessor :module_name, :global # will contain all plugins @@all_plugins = {} # contains all cutoms facts @@all_facts = {} def initialize(toplevel) super(toplevel.file_relative_name) end def self.all_plugins @@all_plugins.values end def self.all_facts @@all_facts.values end end # PuppetModule holds a Puppet Module # This is mapped to an HTMLPuppetModule # it leverage the RDoc (ruby) module infrastructure class PuppetModule < NormalModule attr_accessor :facts, :plugins def initialize(name,superclass=nil) @facts = [] @plugins = [] super(name,superclass) end def initialize_classes_and_modules super @nodes = {} end def add_plugin(plugin) add_to(@plugins, plugin) end def add_fact(fact) add_to(@facts, fact) end def add_node(name,superclass) cls = @nodes[name] unless cls cls = PuppetNode.new(name, superclass) @nodes[name] = cls if !@done_documenting cls.parent = self cls.section = @current_section end cls end def each_fact @facts.each {|c| yield c} end def each_plugin @plugins.each {|c| yield c} end def each_node @nodes.each {|c| yield c} end def nodes @nodes.values end end # PuppetClass holds a puppet class # It is mapped to a HTMLPuppetClass for display # It leverages RDoc (ruby) Class class PuppetClass < ClassModule attr_accessor :resource_list, :requires, :childs, :realizes def initialize(name, superclass) super(name,superclass) @resource_list = [] @requires = [] @realizes = [] @childs = [] end def add_resource(resource) add_to(@resource_list, resource) end def is_module? false end def superclass=(superclass) @superclass = superclass end # we're (ab)using the RDoc require system here. # we're adding a required Puppet class, overriding # the RDoc add_require method which sees ruby required files. def add_require(required) add_to(@requires, required) end def add_realize(realized) add_to(@realizes, realized) end def add_child(child) @childs << child end + + # Look up the given symbol. RDoc only looks for class1::class2.method + # or class1::class2#method. Since our definitions are mapped to RDoc methods + # but are written class1::class2::define we need to perform the lookup by + # ourselves. + def find_symbol(symbol, method=nil) + result = super + if not result and symbol =~ /::/ + modules = symbol.split(/::/) + unless modules.empty? + module_name = modules.shift + result = find_module_named(module_name) + if result + last_name = "" + previous = nil + modules.each do |module_name| + previous = result + last_name = module_name + result = result.find_module_named(module_name) + break unless result + end + unless result + result = previous + method = last_name + end + end + end + if result && method + if !result.respond_to?(:find_local_symbol) + p result.name + p method + fail + end + result = result.find_local_symbol(method) + end + end + result + end + end # PuppetNode holds a puppet node # It is mapped to a HTMLPuppetNode for display # A node is just a variation of a class class PuppetNode < PuppetClass def initialize(name, superclass) super(name,superclass) end def is_module? false end end # Plugin holds a native puppet plugin (function,type...) # It is mapped to a HTMLPuppetPlugin for display class Plugin < Context attr_accessor :name, :type def initialize(name, type) super() @name = name @type = type @comment = "" end def <=>(other) @name <=> other.name end def full_name @name end def http_url(prefix) path = full_name.split("::") File.join(prefix, *path) + ".html" end def is_fact? false end def to_s res = self.class.name + ": #{@name} (#{@type})\n" res << @comment.to_s res end end # Fact holds a custom fact # It is mapped to a HTMLPuppetPlugin for display class Fact < Context attr_accessor :name, :confine def initialize(name, confine) super() @name = name @confine = confine @comment = "" end def <=>(other) @name <=> other.name end def is_fact? true end def full_name @name end def to_s res = self.class.name + ": #{@name}\n" res << @comment.to_s res end end # PuppetResource holds a puppet resource # It is mapped to a HTMLPuppetResource for display # A resource is defined by its "normal" form Type[title] class PuppetResource < CodeObject attr_accessor :type, :title, :params def initialize(type, title, comment, params) super() @type = type @title = title @comment = comment @params = params end def <=>(other) full_name <=> other.full_name end def full_name @type + "[#{@title}]" end def name full_name end def to_s res = @type + "[#{@title}]\n" res << @comment.to_s res end end end diff --git a/lib/puppet/util/rdoc/generators/puppet_generator.rb b/lib/puppet/util/rdoc/generators/puppet_generator.rb index e6bbb2e1e..249c9a8ba 100644 --- a/lib/puppet/util/rdoc/generators/puppet_generator.rb +++ b/lib/puppet/util/rdoc/generators/puppet_generator.rb @@ -1,893 +1,911 @@ require 'rdoc/generators/html_generator' require 'puppet/util/rdoc/code_objects' require 'digest/md5' module Generators # This module holds all the classes needed to generate the HTML documentation # of a bunch of puppet manifests. # # It works by traversing all the code objects defined by the Puppet RDoc::Parser # and produces HTML counterparts objects that in turns are used by RDoc template engine # to produce the final HTML. # # It is also responsible of creating the whole directory hierarchy, and various index # files. # # It is to be noted that the whole system is built on top of ruby RDoc. As such there # is an implicit mapping of puppet entities to ruby entitites: # # Puppet => Ruby # ------------------------ # Module Module # Class Class # Definition Method # Resource # Node # Plugin # Fact MODULE_DIR = "modules" NODE_DIR = "nodes" PLUGIN_DIR = "plugins" + # We're monkey patching RDoc markup to allow + # lowercase class1::class2::class3 crossref hyperlinking + module MarkUp + alias :old_markup :markup + + def new_markup(str, remove_para=false) + first = @markup.nil? + res = old_markup(str, remove_para) + if first and not @markup.nil? + @markup.add_special(/\b([a-z]\w+(::\w+)*)/,:CROSSREF) + # we need to call it again, since we added a rule + res = old_markup(str, remove_para) + end + res + end + alias :markup :new_markup + end + # This is a specialized HTMLGenerator tailored to Puppet manifests class PuppetGenerator < HTMLGenerator def PuppetGenerator.for(options) AllReferences::reset HtmlMethod::reset if options.all_one_file PuppetGeneratorInOne.new(options) else PuppetGenerator.new(options) end end def initialize(options) #:not-new: @options = options load_html_template end # loads our own html template file def load_html_template require 'puppet/util/rdoc/generators/template/puppet/puppet' extend RDoc::Page rescue LoadError $stderr.puts "Could not find Puppet template '#{template}'" exit 99 end def gen_method_index # we don't generate an all define index # as the presentation is per module/per class end # This is the central method, it generates the whole structures # along with all the indices. def generate_html super gen_into(@nodes) gen_into(@plugins) end ## # Generate: # the list of modules # the list of classes and definitions of a specific module # the list of all classes # the list of nodes # the list of resources def build_indices @allfiles = [] @nodes = [] @plugins = [] # contains all the seen modules @modules = {} @allclasses = {} # remove unknown toplevels # it can happen that RDoc triggers a different parser for some files (ie .c, .cc or .h) # in this case RDoc generates a RDoc::TopLevel which we do not support in this generator # So let's make sure we don't generate html for those. @toplevels = @toplevels.select { |tl| tl.is_a? RDoc::PuppetTopLevel } # build the modules, classes and per modules classes and define list @toplevels.each do |toplevel| next unless toplevel.document_self file = HtmlFile.new(toplevel, @options, FILE_DIR) classes = [] methods = [] modules = [] nodes = [] # find all classes of this toplevel # store modules if we find one toplevel.each_classmodule do |k| generate_class_list(classes, modules, k, toplevel, CLASS_DIR) end # find all defines belonging to this toplevel HtmlMethod.all_methods.each do |m| # find parent module, check this method is not already # defined. if m.context.parent.toplevel === toplevel methods << m end end classes.each do |k| @allclasses[k.index_name] = k if !@allclasses.has_key?(k.index_name) end # generate nodes and plugins found classes.each do |k| if k.context.is_module? k.context.each_node do |name,node| nodes << HTMLPuppetNode.new(node, toplevel, NODE_DIR, @options) @nodes << nodes.last end k.context.each_plugin do |plugin| @plugins << HTMLPuppetPlugin.new(plugin, toplevel, PLUGIN_DIR, @options) end k.context.each_fact do |fact| @plugins << HTMLPuppetPlugin.new(fact, toplevel, PLUGIN_DIR, @options) end end end @files << file @allfiles << { "file" => file, "modules" => modules, "classes" => classes, "methods" => methods, "nodes" => nodes } end # scan all classes to create the childs references @allclasses.values.each do |klass| if superklass = klass.context.superclass if superklass = AllReferences[superklass] and (superklass.is_a?(HTMLPuppetClass) or superklass.is_a?(HTMLPuppetNode)) superklass.context.add_child(klass.context) end end end @classes = @allclasses.values end # produce a class/module list of HTMLPuppetModule/HTMLPuppetClass # based on the code object traversal. def generate_class_list(classes, modules, from, html_file, class_dir) if from.is_module? and !@modules.has_key?(from.name) k = HTMLPuppetModule.new(from, html_file, class_dir, @options) classes << k @modules[from.name] = k modules << @modules[from.name] elsif from.is_module? modules << @modules[from.name] elsif !from.is_module? k = HTMLPuppetClass.new(from, html_file, class_dir, @options) classes << k end from.each_classmodule do |mod| generate_class_list(classes, modules, mod, html_file, class_dir) end end # generate all the subdirectories, modules, classes and files def gen_sub_directories super File.makedirs(MODULE_DIR) File.makedirs(NODE_DIR) File.makedirs(PLUGIN_DIR) rescue $stderr.puts $ERROR_INFO.message exit 1 end # generate the index of modules def gen_file_index gen_top_index(@modules.values, 'All Modules', RDoc::Page::TOP_INDEX, "fr_modules_index.html") end # generate a top index def gen_top_index(collection, title, template, filename) template = TemplatePage.new(RDoc::Page::FR_INDEX_BODY, template) res = [] collection.sort.each do |f| if f.document_self res << { "classlist" => CGI.escapeHTML("#{MODULE_DIR}/fr_#{f.index_name}.html"), "module" => CGI.escapeHTML("#{CLASS_DIR}/#{f.index_name}.html"),"name" => CGI.escapeHTML(f.index_name) } end end values = { "entries" => res, 'list_title' => CGI.escapeHTML(title), 'index_url' => main_url, 'charset' => @options.charset, 'style_url' => style_url('', @options.css), } File.open(filename, "w") do |f| template.write_html_on(f, values) end end # generate the all classes index file and the combo index def gen_class_index gen_an_index(@classes, 'All Classes', RDoc::Page::CLASS_INDEX, "fr_class_index.html") @allfiles.each do |file| unless file['file'].context.file_relative_name =~ /\.rb$/ gen_composite_index( file, RDoc::Page::COMBO_INDEX, "#{MODULE_DIR}/fr_#{file["file"].context.module_name}.html") end end end def gen_composite_index(collection, template, filename)\ return if FileTest.exists?(filename) template = TemplatePage.new(RDoc::Page::FR_INDEX_BODY, template) res1 = [] collection['classes'].sort.each do |f| if f.document_self res1 << { "href" => "../"+CGI.escapeHTML(f.path), "name" => CGI.escapeHTML(f.index_name) } unless f.context.is_module? end end res2 = [] collection['methods'].sort.each do |f| res2 << { "href" => "../#{f.path}", "name" => f.index_name.sub(/\(.*\)$/,'') } if f.document_self end module_name = [] res3 = [] res4 = [] collection['modules'].sort.each do |f| module_name << { "href" => "../"+CGI.escapeHTML(f.path), "name" => CGI.escapeHTML(f.index_name) } unless f.facts.nil? f.facts.each do |fact| res3 << {"href" => "../"+CGI.escapeHTML(AllReferences["PLUGIN(#{fact.name})"].path), "name" => CGI.escapeHTML(fact.name)} end end unless f.plugins.nil? f.plugins.each do |plugin| res4 << {"href" => "../"+CGI.escapeHTML(AllReferences["PLUGIN(#{plugin.name})"].path), "name" => CGI.escapeHTML(plugin.name)} end end end res5 = [] collection['nodes'].sort.each do |f| res5 << { "href" => "../"+CGI.escapeHTML(f.path), "name" => CGI.escapeHTML(f.name) } if f.document_self end values = { "module" => module_name, "classes" => res1, 'classes_title' => CGI.escapeHTML("Classes"), 'defines_title' => CGI.escapeHTML("Defines"), 'facts_title' => CGI.escapeHTML("Custom Facts"), 'plugins_title' => CGI.escapeHTML("Plugins"), 'nodes_title' => CGI.escapeHTML("Nodes"), 'index_url' => main_url, 'charset' => @options.charset, 'style_url' => style_url('', @options.css), } values["defines"] = res2 if res2.size>0 values["facts"] = res3 if res3.size>0 values["plugins"] = res4 if res4.size>0 values["nodes"] = res5 if res5.size>0 File.open(filename, "w") do |f| template.write_html_on(f, values) end end # returns the initial_page url def main_url main_page = @options.main_page ref = nil if main_page ref = AllReferences[main_page] if ref ref = ref.path else $stderr.puts "Could not find main page #{main_page}" end end unless ref for file in @files if file.document_self and file.context.global ref = CGI.escapeHTML("#{CLASS_DIR}/#{file.context.module_name}.html") break end end end unless ref for file in @files if file.document_self and !file.context.global ref = CGI.escapeHTML("#{CLASS_DIR}/#{file.context.module_name}.html") break end end end unless ref $stderr.puts "Couldn't find anything to document" $stderr.puts "Perhaps you've used :stopdoc: in all classes" exit(1) end ref end end # This module is used to generate a referenced full name list of ContextUser module ReferencedListBuilder def build_referenced_list(list) res = [] list.each do |i| ref = AllReferences[i.name] || @context.find_symbol(i.name) ref = ref.viewer if ref and ref.respond_to?(:viewer) name = i.respond_to?(:full_name) ? i.full_name : i.name h_name = CGI.escapeHTML(name) if ref and ref.document_self path = url(ref.path) res << { "name" => h_name, "aref" => path } else res << { "name" => h_name } end end res end end # This module is used to hold/generate a list of puppet resources # this is used in HTMLPuppetClass and HTMLPuppetNode module ResourceContainer def collect_resources list = @context.resource_list @resources = list.collect {|m| HTMLPuppetResource.new(m, self, @options) } end def build_resource_summary_list(path_prefix='') collect_resources unless @resources resources = @resources.sort res = [] resources.each do |r| res << { "name" => CGI.escapeHTML(r.name), "aref" => CGI.escape(path_prefix)+"\#"+CGI.escape(r.aref) } end res end def build_resource_detail_list(section) outer = [] resources = @resources.sort resources.each do |r| row = {} if r.section == section and r.document_self row["name"] = CGI.escapeHTML(r.name) desc = r.description.strip row["m_desc"] = desc unless desc.empty? row["aref"] = r.aref row["params"] = r.params outer << row end end outer end end class HTMLPuppetClass < HtmlClass include ResourceContainer, ReferencedListBuilder def value_hash super rl = build_resource_summary_list @values["resources"] = rl unless rl.empty? @context.sections.each do |section| secdata = @values["sections"].select { |secdata| secdata["secsequence"] == section.sequence } if secdata.size == 1 secdata = secdata[0] rdl = build_resource_detail_list(section) secdata["resource_list"] = rdl unless rdl.empty? end end rl = build_require_list(@context) @values["requires"] = rl unless rl.empty? rl = build_realize_list(@context) @values["realizes"] = rl unless rl.empty? cl = build_child_list(@context) @values["childs"] = cl unless cl.empty? @values end def build_require_list(context) build_referenced_list(context.requires) end def build_realize_list(context) build_referenced_list(context.realizes) end def build_child_list(context) build_referenced_list(context.childs) end end class HTMLPuppetNode < ContextUser include ResourceContainer, ReferencedListBuilder attr_reader :path def initialize(context, html_file, prefix, options) super(context, options) @html_file = html_file @is_module = context.is_module? @values = {} context.viewer = self if options.all_one_file @path = context.full_name else @path = http_url(context.full_name, prefix) end AllReferences.add("NODE(#{@context.full_name})", self) end def name @context.name end # return the relative file name to store this class in, # which is also its url def http_url(full_name, prefix) path = full_name.dup path.gsub!(/<<\s*(\w*)/) { "from-#$1" } if path['<<'] File.join(prefix, path.split("::").collect { |p| Digest::MD5.hexdigest(p) }) + ".html" end def parent_name @context.parent.full_name end def index_name name end def write_on(f) value_hash template = TemplatePage.new( RDoc::Page::BODYINC, RDoc::Page::NODE_PAGE, RDoc::Page::METHOD_LIST) template.write_html_on(f, @values) end def value_hash class_attribute_values add_table_of_sections @values["charset"] = @options.charset @values["style_url"] = style_url(path, @options.css) d = markup(@context.comment) @values["description"] = d unless d.empty? ml = build_method_summary_list @values["methods"] = ml unless ml.empty? rl = build_resource_summary_list @values["resources"] = rl unless rl.empty? il = build_include_list(@context) @values["includes"] = il unless il.empty? rl = build_require_list(@context) @values["requires"] = rl unless rl.empty? rl = build_realize_list(@context) @values["realizes"] = rl unless rl.empty? cl = build_child_list(@context) @values["childs"] = cl unless cl.empty? @values["sections"] = @context.sections.map do |section| secdata = { "sectitle" => section.title, "secsequence" => section.sequence, "seccomment" => markup(section.comment) } al = build_alias_summary_list(section) secdata["aliases"] = al unless al.empty? co = build_constants_summary_list(section) secdata["constants"] = co unless co.empty? al = build_attribute_list(section) secdata["attributes"] = al unless al.empty? cl = build_class_list(0, @context, section) secdata["classlist"] = cl unless cl.empty? mdl = build_method_detail_list(section) secdata["method_list"] = mdl unless mdl.empty? rdl = build_resource_detail_list(section) secdata["resource_list"] = rdl unless rdl.empty? secdata end @values end def build_attribute_list(section) atts = @context.attributes.sort res = [] atts.each do |att| next unless att.section == section if att.visibility == :public || att.visibility == :protected || @options.show_all entry = { "name" => CGI.escapeHTML(att.name), "rw" => att.rw, "a_desc" => markup(att.comment, true) } unless att.visibility == :public || att.visibility == :protected entry["rw"] << "-" end res << entry end end res end def class_attribute_values h_name = CGI.escapeHTML(name) @values["classmod"] = "Node" @values["title"] = CGI.escapeHTML("#{@values['classmod']}: #{h_name}") c = @context c = c.parent while c and !c.diagram @values["diagram"] = diagram_reference(c.diagram) if c && c.diagram @values["full_name"] = h_name parent_class = @context.superclass if parent_class @values["parent"] = CGI.escapeHTML(parent_class) if parent_name lookup = parent_name + "::#{parent_class}" else lookup = parent_class end lookup = "NODE(#{lookup})" parent_url = AllReferences[lookup] || AllReferences[parent_class] @values["par_url"] = aref_to(parent_url.path) if parent_url and parent_url.document_self end files = [] @context.in_files.each do |f| res = {} full_path = CGI.escapeHTML(f.file_absolute_name) res["full_path"] = full_path res["full_path_url"] = aref_to(f.viewer.path) if f.document_self res["cvsurl"] = cvs_url( @options.webcvs, full_path ) if @options.webcvs files << res end @values['infiles'] = files end def build_require_list(context) build_referenced_list(context.requires) end def build_realize_list(context) build_referenced_list(context.realizes) end def build_child_list(context) build_referenced_list(context.childs) end def <=>(other) self.name <=> other.name end end class HTMLPuppetModule < HtmlClass def initialize(context, html_file, prefix, options) super(context, html_file, prefix, options) end def value_hash @values = super fl = build_facts_summary_list @values["facts"] = fl unless fl.empty? pl = build_plugins_summary_list @values["plugins"] = pl unless pl.empty? nl = build_nodes_list(0, @context) @values["nodelist"] = nl unless nl.empty? @values end def build_nodes_list(level, context) res = "" prefix = "  ::" * level; context.nodes.sort.each do |node| if node.document_self res << prefix << "Node " << href(url(node.viewer.path), "link", node.full_name) << "
\n" end end res end def build_facts_summary_list potentially_referenced_list(context.facts) {|fn| ["PLUGIN(#{fn})"] } end def build_plugins_summary_list potentially_referenced_list(context.plugins) {|fn| ["PLUGIN(#{fn})"] } end def facts @context.facts end def plugins @context.plugins end end class HTMLPuppetPlugin < ContextUser attr_reader :path def initialize(context, html_file, prefix, options) super(context, options) @html_file = html_file @is_module = false @values = {} context.viewer = self if options.all_one_file @path = context.full_name else @path = http_url(context.full_name, prefix) end AllReferences.add("PLUGIN(#{@context.full_name})", self) end def name @context.name end # return the relative file name to store this class in, # which is also its url def http_url(full_name, prefix) path = full_name.dup path.gsub!(/<<\s*(\w*)/) { "from-#$1" } if path['<<'] File.join(prefix, path.split("::")) + ".html" end def parent_name @context.parent.full_name end def index_name name end def write_on(f) value_hash template = TemplatePage.new( RDoc::Page::BODYINC, RDoc::Page::PLUGIN_PAGE, RDoc::Page::PLUGIN_LIST) template.write_html_on(f, @values) end def value_hash attribute_values add_table_of_sections @values["charset"] = @options.charset @values["style_url"] = style_url(path, @options.css) d = markup(@context.comment) @values["description"] = d unless d.empty? if context.is_fact? unless context.confine.empty? res = {} res["type"] = context.confine[:type] res["value"] = context.confine[:value] @values["confine"] = [res] end else @values["type"] = context.type end @values["sections"] = @context.sections.map do |section| secdata = { "sectitle" => section.title, "secsequence" => section.sequence, "seccomment" => markup(section.comment) } secdata end @values end def attribute_values h_name = CGI.escapeHTML(name) if @context.is_fact? @values["classmod"] = "Fact" else @values["classmod"] = "Plugin" end @values["title"] = "#{@values['classmod']}: #{h_name}" c = @context @values["full_name"] = h_name files = [] @context.in_files.each do |f| res = {} full_path = CGI.escapeHTML(f.file_absolute_name) res["full_path"] = full_path res["full_path_url"] = aref_to(f.viewer.path) if f.document_self res["cvsurl"] = cvs_url( @options.webcvs, full_path ) if @options.webcvs files << res end @values['infiles'] = files end def <=>(other) self.name <=> other.name end end class HTMLPuppetResource include MarkUp attr_reader :context @@seq = "R000000" def initialize(context, html_class, options) @context = context @html_class = html_class @options = options @@seq = @@seq.succ @seq = @@seq context.viewer = self AllReferences.add(name, self) end def as_href(from_path) if @options.all_one_file "##{path}" else HTMLGenerator.gen_url(from_path, path) end end def name @context.name end def section @context.section end def index_name "#{@context.name}" end def params @context.params end def parent_name if @context.parent.parent @context.parent.parent.full_name else nil end end def aref @seq end def path if @options.all_one_file aref else @html_class.path + "##{aref}" end end def description markup(@context.comment) end def <=>(other) @context <=> other.context end def document_self @context.document_self end def find_symbol(symbol, method=nil) res = @context.parent.find_symbol(symbol, method) res &&= res.viewer end end class PuppetGeneratorInOne < HTMLGeneratorInOne def gen_method_index gen_an_index(HtmlMethod.all_methods, 'Defines') end end end diff --git a/lib/puppet/util/rdoc/parser.rb b/lib/puppet/util/rdoc/parser.rb index f9becede1..ea7439ad7 100644 --- a/lib/puppet/util/rdoc/parser.rb +++ b/lib/puppet/util/rdoc/parser.rb @@ -1,480 +1,482 @@ # Puppet "parser" for the rdoc system # The parser uses puppet parser and traverse the AST to instruct RDoc about # our current structures. It also parses ruby files that could contain # either custom facts or puppet plugins (functions, types...) # rdoc mandatory includes require "rdoc/code_objects" require "puppet/util/rdoc/code_objects" require "rdoc/tokenstream" require "rdoc/markup/simple_markup/preprocess" require "rdoc/parsers/parserfactory" module RDoc class Parser extend ParserFactory SITE = "__site__" attr_accessor :ast, :input_file_name, :top_level # parser registration into RDoc parse_files_matching(/\.(rb|pp)$/) # called with the top level file def initialize(top_level, file_name, content, options, stats) @options = options @stats = stats @input_file_name = file_name @top_level = PuppetTopLevel.new(top_level) @progress = $stderr unless options.quiet end # main entry point def scan env = Puppet::Node::Environment.new unless env.known_resource_types.watching_file?(@input_file_name) Puppet.info "rdoc: scanning #{@input_file_name}" if @input_file_name =~ /\.pp$/ @parser = Puppet::Parser::Parser.new(env) @parser.file = @input_file_name @ast = @parser.parse end - scan_top_level(@top_level) + else + @ast = env.known_resource_types end + scan_top_level(@top_level) @top_level end # Due to a bug in RDoc, we need to roll our own find_module_named # The issue is that RDoc tries harder by asking the parent for a class/module # of the name. But by doing so, it can mistakenly use a module of same name # but from which we are not descendant. def find_object_named(container, name) return container if container.name == name container.each_classmodule do |m| return m if m.name == name end nil end # walk down the namespace and lookup/create container as needed def get_class_or_module(container, name) # class ::A -> A is in the top level if name =~ /^::/ container = @top_level end names = name.split('::') final_name = names.pop names.each do |name| prev_container = container container = find_object_named(container, name) container ||= prev_container.add_class(PuppetClass, name, nil) end [container, final_name] end # split_module tries to find if +path+ belongs to the module path # if it does, it returns the module name, otherwise if we are sure # it is part of the global manifest path, "__site__" is returned. # And finally if this path couldn't be mapped anywhere, nil is returned. def split_module(path) # find a module fullpath = File.expand_path(path) Puppet.debug "rdoc: testing #{fullpath}" if fullpath =~ /(.*)\/([^\/]+)\/(?:manifests|plugins|lib)\/.+\.(pp|rb)$/ modpath = $1 name = $2 Puppet.debug "rdoc: module #{name} into #{modpath} ?" Puppet::Module.modulepath.each do |mp| if File.identical?(modpath,mp) Puppet.debug "rdoc: found module #{name}" return name end end end if fullpath =~ /\.(pp|rb)$/ # there can be paths we don't want to scan under modules # imagine a ruby or manifest that would be distributed as part as a module # but we don't want those to be hosted under Puppet::Module.modulepath.each do |mp| # check that fullpath is a descendant of mp dirname = fullpath while (dirname = File.dirname(dirname)) != '/' return nil if File.identical?(dirname,mp) end end end # we are under a global manifests Puppet.debug "rdoc: global manifests" SITE end # create documentation for the top level +container+ def scan_top_level(container) # use the module README as documentation for the module comment = "" readme = File.join(File.dirname(File.dirname(@input_file_name)), "README") comment = File.open(readme,"r") { |f| f.read } if FileTest.readable?(readme) look_for_directives_in(container, comment) unless comment.empty? # infer module name from directory name = split_module(@input_file_name) if name.nil? # skip .pp files that are not in manifests directories as we can't guarantee they're part # of a module or the global configuration. container.document_self = false return end Puppet.debug "rdoc: scanning for #{name}" container.module_name = name container.global=true if name == SITE @stats.num_modules += 1 container, name = get_class_or_module(container,name) mod = container.add_module(PuppetModule, name) mod.record_location(@top_level) mod.comment = comment if @input_file_name =~ /\.pp$/ parse_elements(mod) elsif @input_file_name =~ /\.rb$/ parse_plugins(mod) end end # create documentation for include statements we can find in +code+ # and associate it with +container+ def scan_for_include_or_require(container, code) code = [code] unless code.is_a?(Array) code.each do |stmt| scan_for_include_or_require(container,stmt.children) if stmt.is_a?(Puppet::Parser::AST::ASTArray) if stmt.is_a?(Puppet::Parser::AST::Function) and ['include','require'].include?(stmt.name) stmt.arguments.each do |included| - Puppet.debug "found #{stmt.name}: #{included.value}" - container.send("add_#{stmt.name}",Include.new(included.value, stmt.doc)) + Puppet.debug "found #{stmt.name}: #{included}" + container.send("add_#{stmt.name}",Include.new(included.to_s, stmt.doc)) end end end end # create documentation for realize statements we can find in +code+ # and associate it with +container+ def scan_for_realize(container, code) code = [code] unless code.is_a?(Array) code.each do |stmt| scan_for_realize(container,stmt.children) if stmt.is_a?(Puppet::Parser::AST::ASTArray) if stmt.is_a?(Puppet::Parser::AST::Function) and stmt.name == 'realize' stmt.arguments.each do |realized| Puppet.debug "found #{stmt.name}: #{realized}" container.add_realize(Include.new(realized.to_s, stmt.doc)) end end end end # create documentation for global variables assignements we can find in +code+ # and associate it with +container+ def scan_for_vardef(container, code) code = [code] unless code.is_a?(Array) code.each do |stmt| scan_for_vardef(container,stmt.children) if stmt.is_a?(Puppet::Parser::AST::ASTArray) if stmt.is_a?(Puppet::Parser::AST::VarDef) Puppet.debug "rdoc: found constant: #{stmt.name} = #{stmt.value}" container.add_constant(Constant.new(stmt.name.to_s, stmt.value.to_s, stmt.doc)) end end end # create documentation for resources we can find in +code+ # and associate it with +container+ def scan_for_resource(container, code) code = [code] unless code.is_a?(Array) code.each do |stmt| scan_for_resource(container,stmt.children) if stmt.is_a?(Puppet::Parser::AST::ASTArray) if stmt.is_a?(Puppet::Parser::AST::Resource) and !stmt.type.nil? begin type = stmt.type.split("::").collect { |s| s.capitalize }.join("::") title = stmt.title.is_a?(Puppet::Parser::AST::ASTArray) ? stmt.title.to_s.gsub(/\[(.*)\]/,'\1') : stmt.title.to_s Puppet.debug "rdoc: found resource: #{type}[#{title}]" param = [] stmt.parameters.children.each do |p| res = {} res["name"] = p.param res["value"] = "#{p.value.to_s}" unless p.value.nil? param << res end container.add_resource(PuppetResource.new(type, title, stmt.doc, param)) rescue => detail raise Puppet::ParseError, "impossible to parse resource in #{stmt.file} at line #{stmt.line}: #{detail}" end end end end def resource_stmt_to_ref(stmt) type = stmt.type.split("::").collect { |s| s.capitalize }.join("::") title = stmt.title.is_a?(Puppet::Parser::AST::ASTArray) ? stmt.title.to_s.gsub(/\[(.*)\]/,'\1') : stmt.title.to_s param = stmt.params.children.collect do |p| {"name" => p.param, "value" => p.value.to_s} end PuppetResource.new(type, title, stmt.doc, param) end # create documentation for a class named +name+ def document_class(name, klass, container) Puppet.debug "rdoc: found new class #{name}" container, name = get_class_or_module(container, name) superclass = klass.parent superclass = "" if superclass.nil? or superclass.empty? @stats.num_classes += 1 comment = klass.doc look_for_directives_in(container, comment) unless comment.empty? cls = container.add_class(PuppetClass, name, superclass) # it is possible we already encountered this class, while parsing some namespaces # from other classes of other files. But at that time we couldn't know this class superclass # so, now we know it and force it. cls.superclass = superclass cls.record_location(@top_level) # scan class code for include code = klass.code.children if klass.code.is_a?(Puppet::Parser::AST::ASTArray) code ||= klass.code unless code.nil? scan_for_include_or_require(cls, code) scan_for_realize(cls, code) scan_for_resource(cls, code) if Puppet.settings[:document_all] end cls.comment = comment rescue => detail raise Puppet::ParseError, "impossible to parse class '#{name}' in #{klass.file} at line #{klass.line}: #{detail}" end # create documentation for a node def document_node(name, node, container) Puppet.debug "rdoc: found new node #{name}" superclass = node.parent superclass = "" if superclass.nil? or superclass.empty? comment = node.doc look_for_directives_in(container, comment) unless comment.empty? n = container.add_node(name, superclass) n.record_location(@top_level) code = node.code.children if node.code.is_a?(Puppet::Parser::AST::ASTArray) code ||= node.code unless code.nil? scan_for_include_or_require(n, code) scan_for_realize(n, code) scan_for_vardef(n, code) scan_for_resource(n, code) if Puppet.settings[:document_all] end n.comment = comment rescue => detail raise Puppet::ParseError, "impossible to parse node '#{name}' in #{node.file} at line #{node.line}: #{detail}" end # create documentation for a define def document_define(name, define, container) Puppet.debug "rdoc: found new definition #{name}" # find superclas if any @stats.num_methods += 1 # find the parent # split define name by :: to find the complete module hierarchy container, name = get_class_or_module(container,name) # build up declaration declaration = "" define.arguments.each do |arg,value| declaration << "\$#{arg}" unless value.nil? declaration << " => " case value when Puppet::Parser::AST::Leaf declaration << "'#{value.value}'" when Puppet::Parser::AST::ASTArray declaration << "[#{value.children.collect { |v| "'#{v}'" }.join(", ")}]" else declaration << "#{value.to_s}" end end declaration << ", " end declaration.chop!.chop! if declaration.size > 1 # register method into the container meth = AnyMethod.new(declaration, name) meth.comment = define.doc container.add_method(meth) look_for_directives_in(container, meth.comment) unless meth.comment.empty? meth.params = "( #{declaration} )" meth.visibility = :public meth.document_self = true meth.singleton = false rescue => detail raise Puppet::ParseError, "impossible to parse definition '#{name}' in #{define.file} at line #{define.line}: #{detail}" end # Traverse the AST tree and produce code-objects node # that contains the documentation def parse_elements(container) Puppet.debug "rdoc: scanning manifest" @ast.hostclasses.values.sort { |a,b| a.name <=> b.name }.each do |klass| name = klass.name if klass.file == @input_file_name unless name.empty? document_class(name,klass,container) else # on main class document vardefs code = klass.code.children if klass.code.is_a?(Puppet::Parser::AST::ASTArray) code ||= klass.code scan_for_vardef(container, code) unless code.nil? end end end @ast.definitions.each do |name, define| if define.file == @input_file_name document_define(name,define,container) end end @ast.nodes.each do |name, node| if node.file == @input_file_name document_node(name.to_s,node,container) end end end # create documentation for plugins def parse_plugins(container) Puppet.debug "rdoc: scanning plugin or fact" if @input_file_name =~ /\/facter\/[^\/]+\.rb$/ parse_fact(container) else parse_puppet_plugin(container) end end # this is a poor man custom fact parser :-) def parse_fact(container) comments = "" current_fact = nil File.open(@input_file_name) do |of| of.each do |line| # fetch comments if line =~ /^[ \t]*# ?(.*)$/ comments += $1 + "\n" elsif line =~ /^[ \t]*Facter.add\(['"](.*?)['"]\)/ current_fact = Fact.new($1,{}) look_for_directives_in(container, comments) unless comments.empty? current_fact.comment = comments container.add_fact(current_fact) current_fact.record_location(@top_level) comments = "" Puppet.debug "rdoc: found custom fact #{current_fact.name}" elsif line =~ /^[ \t]*confine[ \t]*:(.*?)[ \t]*=>[ \t]*(.*)$/ current_fact.confine = { :type => $1, :value => $2 } unless current_fact.nil? else # unknown line type comments ="" end end end end # this is a poor man puppet plugin parser :-) # it doesn't extract doc nor desc :-( def parse_puppet_plugin(container) comments = "" current_plugin = nil File.open(@input_file_name) do |of| of.each do |line| # fetch comments if line =~ /^[ \t]*# ?(.*)$/ comments += $1 + "\n" elsif line =~ /^[ \t]*newfunction[ \t]*\([ \t]*:(.*?)[ \t]*,[ \t]*:type[ \t]*=>[ \t]*(:rvalue|:lvalue)\)/ current_plugin = Plugin.new($1, "function") container.add_plugin(current_plugin) look_for_directives_in(container, comments) unless comments.empty? current_plugin.comment = comments current_plugin.record_location(@top_level) comments = "" Puppet.debug "rdoc: found new function plugins #{current_plugin.name}" elsif line =~ /^[ \t]*Puppet::Type.newtype[ \t]*\([ \t]*:(.*?)\)/ current_plugin = Plugin.new($1, "type") container.add_plugin(current_plugin) look_for_directives_in(container, comments) unless comments.empty? current_plugin.comment = comments current_plugin.record_location(@top_level) comments = "" Puppet.debug "rdoc: found new type plugins #{current_plugin.name}" elsif line =~ /module Puppet::Parser::Functions/ # skip else # unknown line type comments ="" end end end end # look_for_directives_in scans the current +comment+ for RDoc directives def look_for_directives_in(context, comment) preprocess = SM::PreProcess.new(@input_file_name, @options.rdoc_include) preprocess.handle(comment) do |directive, param| case directive when "stopdoc" context.stop_doc "" when "startdoc" context.start_doc context.force_documentation = true "" when "enddoc" #context.done_documenting = true #"" throw :enddoc when "main" options = Options.instance options.main_page = param "" when "title" options = Options.instance options.title = param "" when "section" context.set_current_section(param, comment) comment.replace("") # 1.8 doesn't support #clear break else warn "Unrecognized directive '#{directive}'" break end end remove_private_comments(comment) end def remove_private_comments(comment) comment.gsub!(/^#--.*?^#\+\+/m, '') comment.sub!(/^#--.*/m, '') end end end diff --git a/lib/puppet/util/settings.rb b/lib/puppet/util/settings.rb index 626ed20eb..f243b8691 100644 --- a/lib/puppet/util/settings.rb +++ b/lib/puppet/util/settings.rb @@ -1,932 +1,932 @@ require 'puppet' require 'sync' require 'getoptlong' require 'puppet/external/event-loop' require 'puppet/util/cacher' require 'puppet/util/loadedfile' # The class for handling configuration files. class Puppet::Util::Settings include Enumerable include Puppet::Util::Cacher require 'puppet/util/settings/setting' require 'puppet/util/settings/file_setting' require 'puppet/util/settings/boolean_setting' attr_accessor :file attr_reader :timer ReadOnly = [:run_mode, :name] # Retrieve a config value def [](param) value(param) end # Set a config value. This doesn't set the defaults, it sets the value itself. def []=(param, value) set_value(param, value, :memory) end # Generate the list of valid arguments, in a format that GetoptLong can # understand, and add them to the passed option list. def addargs(options) # Add all of the config parameters as valid options. self.each { |name, setting| setting.getopt_args.each { |args| options << args } } options end # Generate the list of valid arguments, in a format that OptionParser can # understand, and add them to the passed option list. def optparse_addargs(options) # Add all of the config parameters as valid options. self.each { |name, setting| options << setting.optparse_args } options end # Is our parameter a boolean parameter? def boolean?(param) param = param.to_sym !!(@config.include?(param) and @config[param].kind_of? BooleanSetting) end # Remove all set values, potentially skipping cli values. def clear(exceptcli = false) @sync.synchronize do unsafe_clear(exceptcli) end end # Remove all set values, potentially skipping cli values. def unsafe_clear(exceptcli = false) @values.each do |name, values| @values.delete(name) unless exceptcli and name == :cli end # Don't clear the 'used' in this case, since it's a config file reparse, # and we want to retain this info. @used = [] unless exceptcli @cache.clear end # This is mostly just used for testing. def clearused @cache.clear @used = [] end # Do variable interpolation on the value. def convert(value, environment = nil) return value unless value return value unless value.is_a? String newval = value.gsub(/\$(\w+)|\$\{(\w+)\}/) do |value| varname = $2 || $1 if varname == "environment" and environment environment - elsif pval = self.value(varname) + elsif pval = self.value(varname, environment) pval else raise Puppet::DevError, "Could not find value for #{value}" end end newval end # Return a value's description. def description(name) if obj = @config[name.to_sym] obj.desc else nil end end def each @config.each { |name, object| yield name, object } end # Iterate over each section name. def eachsection yielded = [] @config.each do |name, object| section = object.section unless yielded.include? section yield section yielded << section end end end # Return an object by name. def setting(param) param = param.to_sym @config[param] end # Handle a command-line argument. def handlearg(opt, value = nil) @cache.clear value &&= munge_value(value) str = opt.sub(/^--/,'') bool = true newstr = str.sub(/^no-/, '') if newstr != str str = newstr bool = false end str = str.intern if @config[str].is_a?(Puppet::Util::Settings::BooleanSetting) if value == "" or value.nil? value = bool end end set_value(str, value, :cli) end def include?(name) name = name.intern if name.is_a? String @config.include?(name) end # check to see if a short name is already defined def shortinclude?(short) short = short.intern if name.is_a? String @shortnames.include?(short) end # Create a new collection of config settings. def initialize @config = {} @shortnames = {} @created = [] @searchpath = nil # Mutex-like thing to protect @values @sync = Sync.new # Keep track of set values. @values = Hash.new { |hash, key| hash[key] = {} } # And keep a per-environment cache @cache = Hash.new { |hash, key| hash[key] = {} } # The list of sections we've used. @used = [] end # NOTE: ACS ahh the util classes. . .sigh # as part of a fix for 1183, I pulled the logic for the following 5 methods out of the executables and puppet.rb # They probably deserve their own class, but I don't want to do that until I can refactor environments # its a little better than where they were # Prints the contents of a config file with the available config settings, or it # prints a single value of a config setting. def print_config_options env = value(:environment) val = value(:configprint) if val == "all" hash = {} each do |name, obj| val = value(name,env) val = val.inspect if val == "" hash[name] = val end hash.sort { |a,b| a[0].to_s <=> b[0].to_s }.each do |name, val| puts "#{name} = #{val}" end else val.split(/\s*,\s*/).sort.each do |v| if include?(v) #if there is only one value, just print it for back compatibility if v == val puts value(val,env) break end puts "#{v} = #{value(v,env)}" else puts "invalid parameter: #{v}" return false end end end true end def generate_config puts to_config true end def generate_manifest puts to_manifest true end def print_configs return print_config_options if value(:configprint) != "" return generate_config if value(:genconfig) generate_manifest if value(:genmanifest) end def print_configs? (value(:configprint) != "" || value(:genconfig) || value(:genmanifest)) && true end # Return a given object's file metadata. def metadata(param) if obj = @config[param.to_sym] and obj.is_a?(FileSetting) return [:owner, :group, :mode].inject({}) do |meta, p| if v = obj.send(p) meta[p] = v end meta end else nil end end # Make a directory with the appropriate user, group, and mode def mkdir(default) obj = get_config_file_default(default) Puppet::Util::SUIDManager.asuser(obj.owner, obj.group) do mode = obj.mode || 0750 Dir.mkdir(obj.value, mode) end end # Figure out the section name for the run_mode. def run_mode Puppet.run_mode.name end # Return all of the parameters associated with a given section. def params(section = nil) if section section = section.intern if section.is_a? String @config.find_all { |name, obj| obj.section == section }.collect { |name, obj| name } else @config.keys end end # Parse the configuration file. Just provides # thread safety. def parse raise "No :config setting defined; cannot parse unknown config file" unless self[:config] @sync.synchronize do unsafe_parse(self[:config]) end # Create a timer so that this file will get checked automatically # and reparsed if necessary. set_filetimeout_timer end # Unsafely parse the file -- this isn't thread-safe and causes plenty of problems if used directly. def unsafe_parse(file) return unless FileTest.exist?(file) begin data = parse_file(file) rescue => details puts details.backtrace if Puppet[:trace] Puppet.err "Could not parse #{file}: #{details}" return end unsafe_clear(true) metas = {} data.each do |area, values| metas[area] = values.delete(:_meta) values.each do |key,value| set_value(key, value, area, :dont_trigger_handles => true, :ignore_bad_settings => true ) end end # Determine our environment, if we have one. if @config[:environment] env = self.value(:environment).to_sym else env = "none" end # Call any hooks we should be calling. settings_with_hooks.each do |setting| each_source(env) do |source| if value = @values[source][setting.name] # We still have to use value to retrieve the value, since # we want the fully interpolated value, not $vardir/lib or whatever. # This results in extra work, but so few of the settings # will have associated hooks that it ends up being less work this # way overall. setting.handle(self.value(setting.name, env)) break end end end # We have to do it in the reverse of the search path, # because multiple sections could set the same value # and I'm too lazy to only set the metadata once. searchpath.reverse.each do |source| source = run_mode if source == :run_mode source = @name if (@name && source == :name) if meta = metas[source] set_metadata(meta) end end end # Create a new setting. The value is passed in because it's used to determine # what kind of setting we're creating, but the value itself might be either # a default or a value, so we can't actually assign it. def newsetting(hash) klass = nil hash[:section] = hash[:section].to_sym if hash[:section] if type = hash[:type] unless klass = {:setting => Setting, :file => FileSetting, :boolean => BooleanSetting}[type] raise ArgumentError, "Invalid setting type '#{type}'" end hash.delete(:type) else case hash[:default] when true, false, "true", "false" klass = BooleanSetting when /^\$\w+\//, /^\//, /^\w:\// klass = FileSetting when String, Integer, Float # nothing klass = Setting else raise ArgumentError, "Invalid value '#{hash[:default].inspect}' for #{hash[:name]}" end end hash[:settings] = self setting = klass.new(hash) setting end # This has to be private, because it doesn't add the settings to @config private :newsetting # Iterate across all of the objects in a given section. def persection(section) section = section.to_sym self.each { |name, obj| if obj.section == section yield obj end } end # Cache this in an easily clearable way, since we were # having trouble cleaning it up after tests. cached_attr(:file) do if path = self[:config] and FileTest.exist?(path) Puppet::Util::LoadedFile.new(path) end end # Reparse our config file, if necessary. def reparse if file and file.changed? Puppet.notice "Reparsing #{file.file}" parse reuse end end def reuse return unless defined?(@used) @sync.synchronize do # yay, thread-safe new = @used @used = [] self.use(*new) end end # The order in which to search for values. def searchpath(environment = nil) if environment [:cli, :memory, environment, :run_mode, :main, :mutable_defaults] else [:cli, :memory, :run_mode, :main, :mutable_defaults] end end # Get a list of objects per section def sectionlist sectionlist = [] self.each { |name, obj| section = obj.section || "puppet" sections[section] ||= [] sectionlist << section unless sectionlist.include?(section) sections[section] << obj } return sectionlist, sections end def service_user_available? return @service_user_available if defined?(@service_user_available) return @service_user_available = false unless user_name = self[:user] user = Puppet::Type.type(:user).new :name => self[:user], :audit => :ensure @service_user_available = user.exists? end def legacy_to_mode(type, param) if not defined?(@app_names) require 'puppet/util/command_line' command_line = Puppet::Util::CommandLine.new @app_names = Puppet::Util::CommandLine::LegacyName.inject({}) do |hash, pair| app, legacy = pair command_line.require_application app hash[legacy.to_sym] = Puppet::Application.find(app).run_mode.name hash end end if new_type = @app_names[type] Puppet.warning "You have configuration parameter $#{param} specified in [#{type}], which is a deprecated section. I'm assuming you meant [#{new_type}]" return new_type end type end def set_value(param, value, type, options = {}) param = param.to_sym unless setting = @config[param] if options[:ignore_bad_settings] return else raise ArgumentError, "Attempt to assign a value to unknown configuration parameter #{param.inspect}" end end value = setting.munge(value) if setting.respond_to?(:munge) setting.handle(value) if setting.respond_to?(:handle) and not options[:dont_trigger_handles] if ReadOnly.include? param and type != :mutable_defaults raise ArgumentError, "You're attempting to set configuration parameter $#{param}, which is read-only." end type = legacy_to_mode(type, param) @sync.synchronize do # yay, thread-safe @values[type][param] = value @cache.clear clearused # Clear the list of environments, because they cache, at least, the module path. # We *could* preferentially just clear them if the modulepath is changed, # but we don't really know if, say, the vardir is changed and the modulepath # is defined relative to it. We need the defined?(stuff) because of loading # order issues. Puppet::Node::Environment.clear if defined?(Puppet::Node) and defined?(Puppet::Node::Environment) end value end # Set a bunch of defaults in a given section. The sections are actually pretty # pointless, but they help break things up a bit, anyway. def setdefaults(section, defs) section = section.to_sym call = [] defs.each { |name, hash| if hash.is_a? Array unless hash.length == 2 raise ArgumentError, "Defaults specified as an array must contain only the default value and the decription" end tmp = hash hash = {} [:default, :desc].zip(tmp).each { |p,v| hash[p] = v } end name = name.to_sym hash[:name] = name hash[:section] = section raise ArgumentError, "Parameter #{name} is already defined" if @config.include?(name) tryconfig = newsetting(hash) if short = tryconfig.short if other = @shortnames[short] raise ArgumentError, "Parameter #{other.name} is already using short name '#{short}'" end @shortnames[short] = tryconfig end @config[name] = tryconfig # Collect the settings that need to have their hooks called immediately. # We have to collect them so that we can be sure we're fully initialized before # the hook is called. call << tryconfig if tryconfig.call_on_define } call.each { |setting| setting.handle(self.value(setting.name)) } end # Create a timer to check whether the file should be reparsed. def set_filetimeout_timer return unless timeout = self[:filetimeout] and timeout = Integer(timeout) and timeout > 0 timer = EventLoop::Timer.new(:interval => timeout, :tolerance => 1, :start? => true) { self.reparse } end # Convert the settings we manage into a catalog full of resources that model those settings. def to_catalog(*sections) sections = nil if sections.empty? catalog = Puppet::Resource::Catalog.new("Settings") @config.values.find_all { |value| value.is_a?(FileSetting) }.each do |file| next unless (sections.nil? or sections.include?(file.section)) next unless resource = file.to_resource next if catalog.resource(resource.ref) catalog.add_resource(resource) end add_user_resources(catalog, sections) catalog end # Convert our list of config settings into a configuration file. def to_config str = %{The configuration file for #{Puppet[:name]}. Note that this file is likely to have unused configuration parameters in it; any parameter that's valid anywhere in Puppet can be in any config file, even if it's not used. Every section can specify three special parameters: owner, group, and mode. These parameters affect the required permissions of any files specified after their specification. Puppet will sometimes use these parameters to check its own configured state, so they can be used to make Puppet a bit more self-managing. Generated on #{Time.now}. }.gsub(/^/, "# ") # Add a section heading that matches our name. if @config.include?(:run_mode) str += "[#{self[:run_mode]}]\n" end eachsection do |section| persection(section) do |obj| str += obj.to_config + "\n" unless ReadOnly.include? obj.name or obj.name == :genconfig end end return str end # Convert to a parseable manifest def to_manifest catalog = to_catalog catalog.resource_refs.collect do |ref| catalog.resource(ref).to_manifest end.join("\n\n") end # Create the necessary objects to use a section. This is idempotent; # you can 'use' a section as many times as you want. def use(*sections) sections = sections.collect { |s| s.to_sym } @sync.synchronize do # yay, thread-safe sections = sections.reject { |s| @used.include?(s) } return if sections.empty? begin catalog = to_catalog(*sections).to_ral rescue => detail puts detail.backtrace if Puppet[:trace] Puppet.err "Could not create resources for managing Puppet's files and directories in sections #{sections.inspect}: #{detail}" # We need some way to get rid of any resources created during the catalog creation # but not cleaned up. return end catalog.host_config = false catalog.apply do |transaction| if transaction.any_failed? report = transaction.report failures = report.logs.find_all { |log| log.level == :err } raise "Got #{failures.length} failure(s) while initializing: #{failures.collect { |l| l.to_s }.join("; ")}" end end sections.each { |s| @used << s } @used.uniq! end end def valid?(param) param = param.to_sym @config.has_key?(param) end def uninterpolated_value(param, environment = nil) param = param.to_sym environment &&= environment.to_sym # See if we can find it within our searchable list of values val = catch :foundval do each_source(environment) do |source| # Look for the value. We have to test the hash for whether # it exists, because the value might be false. @sync.synchronize do throw :foundval, @values[source][param] if @values[source].include?(param) end end throw :foundval, nil end # If we didn't get a value, use the default val = @config[param].default if val.nil? val end # Find the correct value using our search path. Optionally accept an environment # in which to search before the other configuration sections. def value(param, environment = nil) param = param.to_sym environment &&= environment.to_sym # Short circuit to nil for undefined parameters. return nil unless @config.include?(param) # Yay, recursion. #self.reparse unless [:config, :filetimeout].include?(param) # Check the cache first. It needs to be a per-environment # cache so that we don't spread values from one env # to another. if cached = @cache[environment||"none"][param] return cached end val = uninterpolated_value(param, environment) if param == :code # if we interpolate code, all hell breaks loose. return val end # Convert it if necessary val = convert(val, environment) # And cache it @cache[environment||"none"][param] = val val end # Open a file with the appropriate user, group, and mode def write(default, *args, &bloc) obj = get_config_file_default(default) writesub(default, value(obj.name), *args, &bloc) end # Open a non-default file under a default dir with the appropriate user, # group, and mode def writesub(default, file, *args, &bloc) obj = get_config_file_default(default) chown = nil if Puppet.features.root? chown = [obj.owner, obj.group] else chown = [nil, nil] end Puppet::Util::SUIDManager.asuser(*chown) do mode = obj.mode || 0640 args << "w" if args.empty? args << mode # Update the umask to make non-executable files Puppet::Util.withumask(File.umask ^ 0111) do File.open(file, *args) do |file| yield file end end end end def readwritelock(default, *args, &bloc) file = value(get_config_file_default(default).name) tmpfile = file + ".tmp" sync = Sync.new raise Puppet::DevError, "Cannot create #{file}; directory #{File.dirname(file)} does not exist" unless FileTest.directory?(File.dirname(tmpfile)) sync.synchronize(Sync::EX) do File.open(file, ::File::CREAT|::File::RDWR, 0600) do |rf| rf.lock_exclusive do if File.exist?(tmpfile) raise Puppet::Error, ".tmp file already exists for #{file}; Aborting locked write. Check the .tmp file and delete if appropriate" end # If there's a failure, remove our tmpfile begin writesub(default, tmpfile, *args, &bloc) rescue File.unlink(tmpfile) if FileTest.exist?(tmpfile) raise end begin File.rename(tmpfile, file) rescue => detail Puppet.err "Could not rename #{file} to #{tmpfile}: #{detail}" File.unlink(tmpfile) if FileTest.exist?(tmpfile) end end end end end private def get_config_file_default(default) obj = nil unless obj = @config[default] raise ArgumentError, "Unknown default #{default}" end raise ArgumentError, "Default #{default} is not a file" unless obj.is_a? FileSetting obj end # Create the transportable objects for users and groups. def add_user_resources(catalog, sections) return unless Puppet.features.root? return unless self[:mkusers] @config.each do |name, setting| next unless setting.respond_to?(:owner) next unless sections.nil? or sections.include?(setting.section) if user = setting.owner and user != "root" and catalog.resource(:user, user).nil? resource = Puppet::Resource.new(:user, user, :parameters => {:ensure => :present}) resource[:gid] = self[:group] if self[:group] catalog.add_resource resource end if group = setting.group and ! %w{root wheel}.include?(group) and catalog.resource(:group, group).nil? catalog.add_resource Puppet::Resource.new(:group, group, :parameters => {:ensure => :present}) end end end # Yield each search source in turn. def each_source(environment) searchpath(environment).each do |source| # Modify the source as necessary. source = self.run_mode if source == :run_mode yield source end end # Return all settings that have associated hooks; this is so # we can call them after parsing the configuration file. def settings_with_hooks @config.values.find_all { |setting| setting.respond_to?(:handle) } end # Extract extra setting information for files. def extract_fileinfo(string) result = {} value = string.sub(/\{\s*([^}]+)\s*\}/) do params = $1 params.split(/\s*,\s*/).each do |str| if str =~ /^\s*(\w+)\s*=\s*([\w\d]+)\s*$/ param, value = $1.intern, $2 result[param] = value raise ArgumentError, "Invalid file option '#{param}'" unless [:owner, :mode, :group].include?(param) if param == :mode and value !~ /^\d+$/ raise ArgumentError, "File modes must be numbers" end else raise ArgumentError, "Could not parse '#{string}'" end end '' end result[:value] = value.sub(/\s*$/, '') result end # Convert arguments into booleans, integers, or whatever. def munge_value(value) # Handle different data types correctly return case value when /^false$/i; false when /^true$/i; true when /^\d+$/i; Integer(value) when true; true when false; false else value.gsub(/^["']|["']$/,'').sub(/\s+$/, '') end end # This method just turns a file in to a hash of hashes. def parse_file(file) text = read_file(file) result = Hash.new { |names, name| names[name] = {} } count = 0 # Default to 'main' for the section. section = :main result[section][:_meta] = {} text.split(/\n/).each { |line| count += 1 case line when /^\s*\[(\w+)\]\s*$/ section = $1.intern # Section names # Add a meta section result[section][:_meta] ||= {} when /^\s*#/; next # Skip comments when /^\s*$/; next # Skip blanks when /^\s*(\w+)\s*=\s*(.*?)\s*$/ # settings var = $1.intern # We don't want to munge modes, because they're specified in octal, so we'll # just leave them as a String, since Puppet handles that case correctly. if var == :mode value = $2 else value = munge_value($2) end # Check to see if this is a file argument and it has extra options begin if value.is_a?(String) and options = extract_fileinfo(value) value = options[:value] options.delete(:value) result[section][:_meta][var] = options end result[section][var] = value rescue Puppet::Error => detail detail.file = file detail.line = line raise end else error = Puppet::Error.new("Could not match line #{line}") error.file = file error.line = line raise error end } result end # Read the file in. def read_file(file) begin return File.read(file) rescue Errno::ENOENT raise ArgumentError, "No such file #{file}" rescue Errno::EACCES raise ArgumentError, "Permission denied to file #{file}" end end # Set file metadata. def set_metadata(meta) meta.each do |var, values| values.each do |param, value| @config[var].send(param.to_s + "=", value) end end end end diff --git a/spec/integration/parser/parser_spec.rb b/spec/integration/parser/parser_spec.rb index 7b85bcacb..20d87c228 100755 --- a/spec/integration/parser/parser_spec.rb +++ b/spec/integration/parser/parser_spec.rb @@ -1,113 +1,120 @@ #!/usr/bin/env ruby require File.dirname(__FILE__) + '/../../spec_helper' describe Puppet::Parser::Parser do module ParseMatcher class ParseAs def initialize(klass) @parser = Puppet::Parser::Parser.new "development" @class = klass end def result_instance @result.hostclass("").code[0] end def matches?(string) @string = string @result = @parser.parse(string) result_instance.instance_of?(@class) end def description "parse as a #{@class}" end def failure_message " expected #{@string} to parse as #{@class} but was #{result_instance.class}" end def negative_failure_message " expected #{@string} not to parse as #{@class}" end end def parse_as(klass) ParseAs.new(klass) end class ParseWith def initialize(block) @parser = Puppet::Parser::Parser.new "development" @block = block end def result_instance @result.hostclass("").code[0] end def matches?(string) @string = string @result = @parser.parse(string) @block.call(result_instance) end def description "parse with the block evaluating to true" end def failure_message " expected #{@string} to parse with a true result in the block" end def negative_failure_message " expected #{@string} not to parse with a true result in the block" end end def parse_with(&block) ParseWith.new(block) end end include ParseMatcher before :each do @resource_type_collection = Puppet::Resource::TypeCollection.new("env") @parser = Puppet::Parser::Parser.new "development" end describe "when parsing comments before statement" do it "should associate the documentation to the statement AST node" do ast = @parser.parse(""" # comment class test {} """) ast.hostclass("test").doc.should == "comment\n" end end describe "when parsing" do it "should be able to parse normal left to right relationships" do "Notify[foo] -> Notify[bar]".should parse_as(Puppet::Parser::AST::Relationship) end it "should be able to parse right to left relationships" do "Notify[foo] <- Notify[bar]".should parse_as(Puppet::Parser::AST::Relationship) end it "should be able to parse normal left to right subscriptions" do "Notify[foo] ~> Notify[bar]".should parse_as(Puppet::Parser::AST::Relationship) end it "should be able to parse right to left subscriptions" do "Notify[foo] <~ Notify[bar]".should parse_as(Puppet::Parser::AST::Relationship) end it "should correctly set the arrow type of a relationship" do "Notify[foo] <~ Notify[bar]".should parse_with { |rel| rel.arrow == "<~" } end + + it "should be able to parse deep hash access" do + %q{ + $hash = { 'a' => { 'b' => { 'c' => 'it works' } } } + $out = $hash['a']['b']['c'] + }.should parse_with { |v| v.value.is_a?(Puppet::Parser::AST::ASTHash) } + end end end diff --git a/spec/integration/provider/mount_spec.rb b/spec/integration/provider/mount_spec.rb new file mode 100644 index 000000000..d6f25fe1d --- /dev/null +++ b/spec/integration/provider/mount_spec.rb @@ -0,0 +1,151 @@ +require File.dirname(__FILE__) + '/../../spec_helper' + +require 'puppet/file_bucket/dipper' + +describe "mount provider (integration)" do + include PuppetSpec::Files + + def create_fake_fstab(initially_contains_entry) + File.open(@fake_fstab, 'w') do |f| + if initially_contains_entry + f.puts("/dev/disk1s1\t/Volumes/foo_disk\tmsdos\tlocal\t0\t0") + end + end + end + + before :each do + @fake_fstab = tmpfile('fstab') + @current_options = "local" + @current_device = "/dev/disk1s1" + Puppet::Type.type(:mount).defaultprovider.stubs(:default_target).returns(@fake_fstab) + Facter.stubs(:value).with(:operatingsystem).returns('Darwin') + Puppet::Util::ExecutionStub.set do |command, options| + case command[0] + when %r{/s?bin/mount} + if command.length == 1 + if @mounted + "#{@current_device} on /Volumes/foo_disk (msdos, #{@current_options})\n" + else + '' + end + else + command.length.should == 4 + command[1].should == '-o' + command[3].should == '/Volumes/foo_disk' + @mounted.should == false # verify that we don't try to call "mount" redundantly + @current_options = command[2] + @current_device = check_fstab(true) + @mounted = true + '' + end + when %r{/s?bin/umount} + command.length.should == 2 + command[1].should == '/Volumes/foo_disk' + @mounted.should == true # "umount" doesn't work when device not mounted (see #6632) + @mounted = false + '' + else + fail "Unexpected command #{command.inspect} executed" + end + end + end + + after :each do + Puppet::Type::Mount::ProviderParsed.clear # Work around bug #6628 + end + + def check_fstab(expected_to_be_present) + # Verify that the fake fstab has the expected data in it + fstab_contents = File.read(@fake_fstab).lines.map(&:chomp).reject { |x| x =~ /^#|^$/ } + if expected_to_be_present + fstab_contents.length().should == 1 + device, rest_of_line = fstab_contents[0].split(/\t/,2) + rest_of_line.should == "/Volumes/foo_disk\tmsdos\t#{@desired_options}\t0\t0" + device + else + fstab_contents.length().should == 0 + nil + end + end + + def run_in_catalog(settings) + resource = Puppet::Type.type(:mount).new(settings.merge(:name => "/Volumes/foo_disk", + :device => "/dev/disk1s1", :fstype => "msdos")) + Puppet::FileBucket::Dipper.any_instance.stubs(:backup) # Don't backup to the filebucket + resource.expects(:err).never + catalog = Puppet::Resource::Catalog.new + catalog.host_config = false # Stop Puppet from doing a bunch of magic + catalog.add_resource resource + catalog.apply + end + + [false, true].each do |initial_state| + describe "When initially #{initial_state ? 'mounted' : 'unmounted'}" do + before :each do + @mounted = initial_state + end + + [false, true].each do |initial_fstab_entry| + describe "When there is #{initial_fstab_entry ? 'an' : 'no'} initial fstab entry" do + before :each do + create_fake_fstab(initial_fstab_entry) + end + + [:defined, :present, :mounted, :unmounted, :absent].each do |ensure_setting| + expected_final_state = case ensure_setting + when :mounted + true + when :unmounted, :absent + false + when :defined, :present + initial_state + else + fail "Unknown ensure_setting #{ensure_setting}" + end + expected_fstab_data = (ensure_setting != :absent) + describe "When setting ensure => #{ensure_setting}" do + ["local", "journaled"].each do |options_setting| + describe "When setting options => #{options_setting}" do + it "should leave the system in the #{expected_final_state ? 'mounted' : 'unmounted'} state, #{expected_fstab_data ? 'with' : 'without'} data in /etc/fstab" do + @desired_options = options_setting + run_in_catalog(:ensure=>ensure_setting, :options => options_setting) + @mounted.should == expected_final_state + if expected_fstab_data + check_fstab(expected_fstab_data).should == "/dev/disk1s1" + else + check_fstab(expected_fstab_data).should == nil + end + if @mounted + if ![:defined, :present].include?(ensure_setting) + @current_options.should == @desired_options + elsif initial_fstab_entry + @current_options.should == @desired_options + else + @current_options.should == 'local' #Workaround for #6645 + end + end + end + end + end + end + end + end + end + end + end + + describe "When the wrong device is mounted" do + it "should remount the correct device" do + pending "Due to bug 6309" + @mounted = true + @current_device = "/dev/disk2s2" + create_fake_fstab(true) + @desired_options = "local" + run_in_catalog(:ensure=>:mounted, :options=>'local') + @current_device.should=="/dev/disk1s1" + @mounted.should==true + @current_options.should=='local' + check_fstab(true).should == "/dev/disk1s1" + end + end +end diff --git a/spec/integration/type/file_spec.rb b/spec/integration/type/file_spec.rb index 4b91e5ef9..31f4adee6 100755 --- a/spec/integration/type/file_spec.rb +++ b/spec/integration/type/file_spec.rb @@ -1,506 +1,509 @@ #!/usr/bin/env ruby require File.dirname(__FILE__) + '/../../spec_helper' require 'puppet_spec/files' describe Puppet::Type.type(:file) do include PuppetSpec::Files before do # stub this to not try to create state.yaml Puppet::Util::Storage.stubs(:store) end it "should not attempt to manage files that do not exist if no means of creating the file is specified" do file = Puppet::Type.type(:file).new :path => "/my/file", :mode => "755" catalog = Puppet::Resource::Catalog.new catalog.add_resource file file.parameter(:mode).expects(:retrieve).never transaction = Puppet::Transaction.new(catalog) transaction.resource_harness.evaluate(file).should_not be_failed end describe "when writing files" do it "should backup files to a filebucket when one is configured" do bucket = Puppet::Type.type(:filebucket).new :path => tmpfile("filebucket"), :name => "mybucket" file = Puppet::Type.type(:file).new :path => tmpfile("bucket_backs"), :backup => "mybucket", :content => "foo" catalog = Puppet::Resource::Catalog.new catalog.add_resource file, bucket File.open(file[:path], "w") { |f| f.puts "bar" } md5 = Digest::MD5.hexdigest(File.read(file[:path])) catalog.apply bucket.bucket.getfile(md5).should == "bar\n" end it "should backup files in the local directory when a backup string is provided" do file = Puppet::Type.type(:file).new :path => tmpfile("bucket_backs"), :backup => ".bak", :content => "foo" catalog = Puppet::Resource::Catalog.new catalog.add_resource file File.open(file[:path], "w") { |f| f.puts "bar" } catalog.apply backup = file[:path] + ".bak" FileTest.should be_exist(backup) File.read(backup).should == "bar\n" end it "should fail if no backup can be performed" do dir = tmpfile("backups") Dir.mkdir(dir) path = File.join(dir, "testfile") file = Puppet::Type.type(:file).new :path => path, :backup => ".bak", :content => "foo" catalog = Puppet::Resource::Catalog.new catalog.add_resource file File.open(file[:path], "w") { |f| f.puts "bar" } # Create a directory where the backup should be so that writing to it fails Dir.mkdir(File.join(dir, "testfile.bak")) Puppet::Util::Log.stubs(:newmessage) catalog.apply File.read(file[:path]).should == "bar\n" end it "should not backup symlinks" do link = tmpfile("link") dest1 = tmpfile("dest1") dest2 = tmpfile("dest2") bucket = Puppet::Type.type(:filebucket).new :path => tmpfile("filebucket"), :name => "mybucket" file = Puppet::Type.type(:file).new :path => link, :target => dest2, :ensure => :link, :backup => "mybucket" catalog = Puppet::Resource::Catalog.new catalog.add_resource file, bucket File.open(dest1, "w") { |f| f.puts "whatever" } File.symlink(dest1, link) md5 = Digest::MD5.hexdigest(File.read(file[:path])) catalog.apply File.readlink(link).should == dest2 Find.find(bucket[:path]) { |f| File.file?(f) }.should be_nil end it "should backup directories to the local filesystem by copying the whole directory" do file = Puppet::Type.type(:file).new :path => tmpfile("bucket_backs"), :backup => ".bak", :content => "foo", :force => true catalog = Puppet::Resource::Catalog.new catalog.add_resource file Dir.mkdir(file[:path]) otherfile = File.join(file[:path], "foo") File.open(otherfile, "w") { |f| f.print "yay" } catalog.apply backup = file[:path] + ".bak" FileTest.should be_directory(backup) File.read(File.join(backup, "foo")).should == "yay" end it "should backup directories to filebuckets by backing up each file separately" do bucket = Puppet::Type.type(:filebucket).new :path => tmpfile("filebucket"), :name => "mybucket" file = Puppet::Type.type(:file).new :path => tmpfile("bucket_backs"), :backup => "mybucket", :content => "foo", :force => true catalog = Puppet::Resource::Catalog.new catalog.add_resource file, bucket Dir.mkdir(file[:path]) foofile = File.join(file[:path], "foo") barfile = File.join(file[:path], "bar") File.open(foofile, "w") { |f| f.print "fooyay" } File.open(barfile, "w") { |f| f.print "baryay" } foomd5 = Digest::MD5.hexdigest(File.read(foofile)) barmd5 = Digest::MD5.hexdigest(File.read(barfile)) catalog.apply bucket.bucket.getfile(foomd5).should == "fooyay" bucket.bucket.getfile(barmd5).should == "baryay" end it "should propagate failures encountered when renaming the temporary file" do file = Puppet::Type.type(:file).new :path => tmpfile("fail_rename"), :content => "foo" file.stubs(:remove_existing) # because it tries to make a backup catalog = Puppet::Resource::Catalog.new catalog.add_resource file File.open(file[:path], "w") { |f| f.print "bar" } File.expects(:rename).raises ArgumentError lambda { file.write(:content) }.should raise_error(Puppet::Error) File.read(file[:path]).should == "bar" end end describe "when recursing" do def build_path(dir) Dir.mkdir(dir) File.chmod(0750, dir) @dirs = [dir] @files = [] %w{one two}.each do |subdir| fdir = File.join(dir, subdir) Dir.mkdir(fdir) File.chmod(0750, fdir) @dirs << fdir %w{three}.each do |file| ffile = File.join(fdir, file) @files << ffile File.open(ffile, "w") { |f| f.puts "test #{file}" } File.chmod(0640, ffile) end end end it "should be able to recurse over a nonexistent file" do @path = tmpfile("file_integration_tests") - @file = Puppet::Type::File.new(:name => @path, :mode => 0644, :recurse => true, :backup => false) + @file = Puppet::Type::File.new( + :name => @path, + :mode => 0644, + :recurse => true, + :backup => false + ) @catalog = Puppet::Resource::Catalog.new @catalog.add_resource @file lambda { @file.eval_generate }.should_not raise_error end it "should be able to recursively set properties on existing files" do @path = tmpfile("file_integration_tests") build_path(@path) - @file = Puppet::Type::File.new(:name => @path, :mode => 0644, :recurse => true, :backup => false) + @file = Puppet::Type::File.new( + :name => @path, + :mode => 0644, + :recurse => true, + :backup => false + ) @catalog = Puppet::Resource::Catalog.new @catalog.add_resource @file @catalog.apply @dirs.each do |path| (File.stat(path).mode & 007777).should == 0755 end @files.each do |path| (File.stat(path).mode & 007777).should == 0644 end end it "should be able to recursively make links to other files" do source = tmpfile("file_link_integration_source") build_path(source) dest = tmpfile("file_link_integration_dest") @file = Puppet::Type::File.new(:name => dest, :target => source, :recurse => true, :ensure => :link, :backup => false) @catalog = Puppet::Resource::Catalog.new @catalog.add_resource @file @catalog.apply @dirs.each do |path| link_path = path.sub(source, dest) File.lstat(link_path).should be_directory end @files.each do |path| link_path = path.sub(source, dest) File.lstat(link_path).ftype.should == "link" end end it "should be able to recursively copy files" do source = tmpfile("file_source_integration_source") build_path(source) dest = tmpfile("file_source_integration_dest") @file = Puppet::Type::File.new(:name => dest, :source => source, :recurse => true, :backup => false) @catalog = Puppet::Resource::Catalog.new @catalog.add_resource @file @catalog.apply @dirs.each do |path| newpath = path.sub(source, dest) File.lstat(newpath).should be_directory end @files.each do |path| newpath = path.sub(source, dest) File.lstat(newpath).ftype.should == "file" end end it "should not recursively manage files managed by a more specific explicit file" do dir = tmpfile("recursion_vs_explicit_1") subdir = File.join(dir, "subdir") file = File.join(subdir, "file") FileUtils.mkdir_p(subdir) File.open(file, "w") { |f| f.puts "" } base = Puppet::Type::File.new(:name => dir, :recurse => true, :backup => false, :mode => "755") sub = Puppet::Type::File.new(:name => subdir, :recurse => true, :backup => false, :mode => "644") @catalog = Puppet::Resource::Catalog.new @catalog.add_resource base @catalog.add_resource sub @catalog.apply (File.stat(file).mode & 007777).should == 0644 end it "should recursively manage files even if there is an explicit file whose name is a prefix of the managed file" do dir = tmpfile("recursion_vs_explicit_2") managed = File.join(dir, "file") generated = File.join(dir, "file_with_a_name_starting_with_the_word_file") FileUtils.mkdir_p(dir) File.open(managed, "w") { |f| f.puts "" } File.open(generated, "w") { |f| f.puts "" } @catalog = Puppet::Resource::Catalog.new @catalog.add_resource Puppet::Type::File.new(:name => dir, :recurse => true, :backup => false, :mode => "755") @catalog.add_resource Puppet::Type::File.new(:name => managed, :recurse => true, :backup => false, :mode => "644") @catalog.apply (File.stat(generated).mode & 007777).should == 0755 end end describe "when generating resources" do before do @source = tmpfile("generating_in_catalog_source") @dest = tmpfile("generating_in_catalog_dest") Dir.mkdir(@source) s1 = File.join(@source, "one") s2 = File.join(@source, "two") File.open(s1, "w") { |f| f.puts "uno" } File.open(s2, "w") { |f| f.puts "dos" } @file = Puppet::Type::File.new(:name => @dest, :source => @source, :recurse => true, :backup => false) @catalog = Puppet::Resource::Catalog.new @catalog.add_resource @file end it "should add each generated resource to the catalog" do @catalog.apply do |trans| @catalog.resource(:file, File.join(@dest, "one")).should be_instance_of(@file.class) @catalog.resource(:file, File.join(@dest, "two")).should be_instance_of(@file.class) end end it "should have an edge to each resource in the relationship graph" do @catalog.apply do |trans| one = @catalog.resource(:file, File.join(@dest, "one")) @catalog.relationship_graph.should be_edge(@file, one) two = @catalog.resource(:file, File.join(@dest, "two")) @catalog.relationship_graph.should be_edge(@file, two) end end end describe "when copying files" do # Ticket #285. it "should be able to copy files with pound signs in their names" do source = tmpfile("filewith#signs") dest = tmpfile("destwith#signs") File.open(source, "w") { |f| f.print "foo" } file = Puppet::Type::File.new(:name => dest, :source => source) catalog = Puppet::Resource::Catalog.new catalog.add_resource file catalog.apply File.read(dest).should == "foo" end it "should be able to copy files with spaces in their names" do source = tmpfile("filewith spaces") dest = tmpfile("destwith spaces") File.open(source, "w") { |f| f.print "foo" } File.chmod(0755, source) file = Puppet::Type::File.new(:path => dest, :source => source) catalog = Puppet::Resource::Catalog.new catalog.add_resource file catalog.apply File.read(dest).should == "foo" (File.stat(dest).mode & 007777).should == 0755 end it "should be able to copy individual files even if recurse has been specified" do source = tmpfile("source") dest = tmpfile("dest") File.open(source, "w") { |f| f.print "foo" } file = Puppet::Type::File.new(:name => dest, :source => source, :recurse => true) catalog = Puppet::Resource::Catalog.new catalog.add_resource file catalog.apply File.read(dest).should == "foo" end end it "should be able to create files when 'content' is specified but 'ensure' is not" do dest = tmpfile("files_with_content") - file = Puppet::Type.type(:file).new( - - :name => dest, - + file = Puppet::Type.type(:file).new( + :name => dest, :content => "this is some content, yo" ) catalog = Puppet::Resource::Catalog.new catalog.add_resource file catalog.apply File.read(dest).should == "this is some content, yo" end it "should create files with content if both 'content' and 'ensure' are set" do dest = tmpfile("files_with_content") - file = Puppet::Type.type(:file).new( - - :name => dest, - :ensure => "file", - + file = Puppet::Type.type(:file).new( + :name => dest, + :ensure => "file", :content => "this is some content, yo" ) catalog = Puppet::Resource::Catalog.new catalog.add_resource file catalog.apply File.read(dest).should == "this is some content, yo" end it "should delete files with sources but that are set for deletion" do dest = tmpfile("dest_source_with_ensure") source = tmpfile("source_source_with_ensure") File.open(source, "w") { |f| f.puts "yay" } File.open(dest, "w") { |f| f.puts "boo" } - file = Puppet::Type.type(:file).new( - - :name => dest, + file = Puppet::Type.type(:file).new( + :name => dest, :ensure => :absent, :source => source, - :backup => false ) catalog = Puppet::Resource::Catalog.new catalog.add_resource file catalog.apply File.should_not be_exist(dest) end describe "when purging files" do before do @sourcedir = tmpfile("purge_source") @destdir = tmpfile("purge_dest") Dir.mkdir(@sourcedir) Dir.mkdir(@destdir) @sourcefile = File.join(@sourcedir, "sourcefile") @copiedfile = File.join(@destdir, "sourcefile") @localfile = File.join(@destdir, "localfile") @purgee = File.join(@destdir, "to_be_purged") File.open(@localfile, "w") { |f| f.puts "rahtest" } File.open(@sourcefile, "w") { |f| f.puts "funtest" } # this file should get removed File.open(@purgee, "w") { |f| f.puts "footest" } - @lfobj = Puppet::Type.newfile( - - :title => "localfile", - :path => @localfile, + @lfobj = Puppet::Type.newfile( + :title => "localfile", + :path => @localfile, :content => "rahtest\n", - :ensure => :file, - - :backup => false + :ensure => :file, + :backup => false ) - @destobj = Puppet::Type.newfile( - :title => "destdir", :path => @destdir, - :source => @sourcedir, - :backup => false, - :purge => true, - - :recurse => true) + @destobj = Puppet::Type.newfile( + :title => "destdir", + :path => @destdir, + :source => @sourcedir, + :backup => false, + :purge => true, + :recurse => true + ) @catalog = Puppet::Resource::Catalog.new @catalog.add_resource @lfobj, @destobj end it "should still copy remote files" do @catalog.apply FileTest.should be_exist(@copiedfile) end it "should not purge managed, local files" do @catalog.apply FileTest.should be_exist(@localfile) end it "should purge files that are neither remote nor otherwise managed" do @catalog.apply FileTest.should_not be_exist(@purgee) end end end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index ed4e826c9..ae4edb2d9 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -1,81 +1,83 @@ unless defined?(SPEC_HELPER_IS_LOADED) SPEC_HELPER_IS_LOADED = 1 dir = File.expand_path(File.dirname(__FILE__)) $LOAD_PATH.unshift("#{dir}/") $LOAD_PATH.unshift("#{dir}/lib") # a spec-specific test lib dir $LOAD_PATH.unshift("#{dir}/../lib") $LOAD_PATH.unshift("#{dir}/../test/lib") # Don't want puppet getting the command line arguments for rake or autotest ARGV.clear require 'puppet' require 'mocha' gem 'rspec', '>=2.0.0' # So everyone else doesn't have to include this base constant. module PuppetSpec FIXTURE_DIR = File.join(dir = File.expand_path(File.dirname(__FILE__)), "fixtures") unless defined?(FIXTURE_DIR) end module PuppetTest end require 'lib/puppet_spec/files' require 'monkey_patches/alias_should_to_must' require 'monkey_patches/publicize_methods' RSpec.configure do |config| config.mock_with :mocha config.after :each do Puppet.settings.clear Puppet::Node::Environment.clear Puppet::Util::Storage.clear + Puppet::Util::ExecutionStub.reset if defined?($tmpfiles) $tmpfiles.each do |file| file = File.expand_path(file) if Puppet.features.posix? and file !~ /^\/tmp/ and file !~ /^\/var\/folders/ puts "Not deleting tmpfile #{file} outside of /tmp or /var/folders" next elsif Puppet.features.microsoft_windows? tempdir = File.expand_path(File.join(Dir::LOCAL_APPDATA, "Temp")) if file !~ /^#{tempdir}/ puts "Not deleting tmpfile #{file} outside of #{tempdir}" next end end if FileTest.exist?(file) system("chmod -R 755 '#{file}'") system("rm -rf '#{file}'") end end $tmpfiles.clear end @logs.clear Puppet::Util::Log.close_all end config.before :each do # these globals are set by Application $puppet_application_mode = nil $puppet_application_name = nil + Signal.stubs(:trap) # Set the confdir and vardir to gibberish so that tests # have to be correctly mocked. Puppet[:confdir] = "/dev/null" Puppet[:vardir] = "/dev/null" # Avoid opening ports to the outside world Puppet.settings[:bindaddress] = "127.0.0.1" @logs = [] Puppet::Util::Log.newdestination(@logs) end end end diff --git a/spec/unit/application/agent_spec.rb b/spec/unit/application/agent_spec.rb index ff504eedf..8f498d4ba 100755 --- a/spec/unit/application/agent_spec.rb +++ b/spec/unit/application/agent_spec.rb @@ -1,589 +1,585 @@ #!/usr/bin/env ruby require File.dirname(__FILE__) + '/../../spec_helper' require 'puppet/agent' require 'puppet/application/agent' require 'puppet/network/server' require 'puppet/network/handler' require 'puppet/daemon' describe Puppet::Application::Agent do before :each do @puppetd = Puppet::Application[:agent] @puppetd.stubs(:puts) @daemon = stub_everything 'daemon' Puppet::Daemon.stubs(:new).returns(@daemon) @agent = stub_everything 'agent' Puppet::Agent.stubs(:new).returns(@agent) @puppetd.preinit Puppet::Util::Log.stubs(:newdestination) Puppet::Util::Log.stubs(:level=) Puppet::Node.stubs(:terminus_class=) Puppet::Node.stubs(:cache_class=) Puppet::Node::Facts.stubs(:terminus_class=) end it "should operate in agent run_mode" do @puppetd.class.run_mode.name.should == :agent end it "should ask Puppet::Application to parse Puppet configuration file" do @puppetd.should_parse_config?.should be_true end it "should declare a main command" do @puppetd.should respond_to(:main) end it "should declare a onetime command" do @puppetd.should respond_to(:onetime) end it "should declare a fingerprint command" do @puppetd.should respond_to(:fingerprint) end it "should declare a preinit block" do @puppetd.should respond_to(:preinit) end describe "in preinit" do - before :each do - @puppetd.stubs(:trap) - end - it "should catch INT" do - @puppetd.expects(:trap).with { |arg,block| arg == :INT } + Signal.expects(:trap).with { |arg,block| arg == :INT } @puppetd.preinit end it "should init client to true" do @puppetd.preinit @puppetd.options[:client].should be_true end it "should init fqdn to nil" do @puppetd.preinit @puppetd.options[:fqdn].should be_nil end it "should init serve to []" do @puppetd.preinit @puppetd.options[:serve].should == [] end it "should use MD5 as default digest algorithm" do @puppetd.preinit @puppetd.options[:digest].should == :MD5 end it "should not fingerprint by default" do @puppetd.preinit @puppetd.options[:fingerprint].should be_false end end describe "when handling options" do before do @puppetd.command_line.stubs(:args).returns([]) end [:centrallogging, :disable, :enable, :debug, :fqdn, :test, :verbose, :digest].each do |option| it "should declare handle_#{option} method" do @puppetd.should respond_to("handle_#{option}".to_sym) end it "should store argument value when calling handle_#{option}" do @puppetd.options.expects(:[]=).with(option, 'arg') @puppetd.send("handle_#{option}".to_sym, 'arg') end end it "should set an existing handler on server" do Puppet::Network::Handler.stubs(:handler).with("handler").returns(true) @puppetd.handle_serve("handler") @puppetd.options[:serve].should == [ :handler ] end it "should set client to false with --no-client" do @puppetd.handle_no_client(nil) @puppetd.options[:client].should be_false end it "should set waitforcert to 0 with --onetime and if --waitforcert wasn't given" do Puppet[:onetime] = true Puppet::SSL::Host.any_instance.expects(:wait_for_cert).with(0) @puppetd.setup_host end it "should use supplied waitforcert when --onetime is specified" do Puppet[:onetime] = true @puppetd.handle_waitforcert(60) Puppet::SSL::Host.any_instance.expects(:wait_for_cert).with(60) @puppetd.setup_host end it "should use a default value for waitforcert when --onetime and --waitforcert are not specified" do Puppet::SSL::Host.any_instance.expects(:wait_for_cert).with(120) @puppetd.setup_host end it "should set the log destination with --logdest" do @puppetd.options.stubs(:[]=).with { |opt,val| opt == :setdest } Puppet::Log.expects(:newdestination).with("console") @puppetd.handle_logdest("console") end it "should put the setdest options to true" do @puppetd.options.expects(:[]=).with(:setdest,true) @puppetd.handle_logdest("console") end it "should parse the log destination from the command line" do @puppetd.command_line.stubs(:args).returns(%w{--logdest /my/file}) Puppet::Util::Log.expects(:newdestination).with("/my/file") @puppetd.parse_options end it "should store the waitforcert options with --waitforcert" do @puppetd.options.expects(:[]=).with(:waitforcert,42) @puppetd.handle_waitforcert("42") end it "should set args[:Port] with --port" do @puppetd.handle_port("42") @puppetd.args[:Port].should == "42" end end describe "during setup" do before :each do @puppetd.options.stubs(:[]) Puppet.stubs(:info) FileTest.stubs(:exists?).returns(true) Puppet.stubs(:[]) Puppet.stubs(:[]=) Puppet.stubs(:[]).with(:libdir).returns("/dev/null/lib") Puppet.settings.stubs(:print_config?) Puppet.settings.stubs(:print_config) Puppet::SSL::Host.stubs(:ca_location=) Puppet::Transaction::Report.stubs(:terminus_class=) Puppet::Resource::Catalog.stubs(:terminus_class=) Puppet::Resource::Catalog.stubs(:cache_class=) Puppet::Node::Facts.stubs(:terminus_class=) @host = stub_everything 'host' Puppet::SSL::Host.stubs(:new).returns(@host) Puppet.stubs(:settraps) end describe "with --test" do before :each do Puppet.settings.stubs(:handlearg) @puppetd.options.stubs(:[]=) end it "should call setup_test" do @puppetd.options.stubs(:[]).with(:test).returns(true) @puppetd.expects(:setup_test) @puppetd.setup end it "should set options[:verbose] to true" do @puppetd.options.expects(:[]=).with(:verbose,true) @puppetd.setup_test end it "should set options[:onetime] to true" do Puppet.expects(:[]=).with(:onetime,true) @puppetd.setup_test end it "should set options[:detailed_exitcodes] to true" do @puppetd.options.expects(:[]=).with(:detailed_exitcodes,true) @puppetd.setup_test end end it "should call setup_logs" do @puppetd.expects(:setup_logs) @puppetd.setup end describe "when setting up logs" do before :each do Puppet::Util::Log.stubs(:newdestination) end it "should set log level to debug if --debug was passed" do @puppetd.options.stubs(:[]).with(:debug).returns(true) Puppet::Util::Log.expects(:level=).with(:debug) @puppetd.setup_logs end it "should set log level to info if --verbose was passed" do @puppetd.options.stubs(:[]).with(:verbose).returns(true) Puppet::Util::Log.expects(:level=).with(:info) @puppetd.setup_logs end [:verbose, :debug].each do |level| it "should set console as the log destination with level #{level}" do @puppetd.options.stubs(:[]).with(level).returns(true) Puppet::Util::Log.expects(:newdestination).with(:console) @puppetd.setup_logs end end it "should set syslog as the log destination if no --logdest" do @puppetd.options.stubs(:[]).with(:setdest).returns(false) Puppet::Util::Log.expects(:newdestination).with(:syslog) @puppetd.setup_logs end end it "should print puppet config if asked to in Puppet config" do @puppetd.stubs(:exit) Puppet.settings.stubs(:print_configs?).returns(true) Puppet.settings.expects(:print_configs) @puppetd.setup end it "should exit after printing puppet config if asked to in Puppet config" do Puppet.settings.stubs(:print_configs?).returns(true) lambda { @puppetd.setup }.should raise_error(SystemExit) end it "should set a central log destination with --centrallogs" do @puppetd.options.stubs(:[]).with(:centrallogs).returns(true) Puppet.stubs(:[]).with(:server).returns("puppet.reductivelabs.com") Puppet::Util::Log.stubs(:newdestination).with(:syslog) Puppet::Util::Log.expects(:newdestination).with("puppet.reductivelabs.com") @puppetd.setup end it "should use :main, :puppetd, and :ssl" do Puppet.settings.expects(:use).with(:main, :agent, :ssl) @puppetd.setup end it "should install a remote ca location" do Puppet::SSL::Host.expects(:ca_location=).with(:remote) @puppetd.setup end it "should install a none ca location in fingerprint mode" do @puppetd.options.stubs(:[]).with(:fingerprint).returns(true) Puppet::SSL::Host.expects(:ca_location=).with(:none) @puppetd.setup end it "should tell the report handler to use REST" do Puppet::Transaction::Report.expects(:terminus_class=).with(:rest) @puppetd.setup end it "should change the catalog_terminus setting to 'rest'" do Puppet.expects(:[]=).with(:catalog_terminus, :rest) @puppetd.setup end it "should tell the catalog handler to use cache" do Puppet::Resource::Catalog.expects(:cache_class=).with(:yaml) @puppetd.setup end it "should change the facts_terminus setting to 'facter'" do Puppet.expects(:[]=).with(:facts_terminus, :facter) @puppetd.setup end it "should create an agent" do Puppet::Agent.stubs(:new).with(Puppet::Configurer) @puppetd.setup end [:enable, :disable].each do |action| it "should delegate to enable_disable_client if we #{action} the agent" do @puppetd.options.stubs(:[]).with(action).returns(true) @puppetd.expects(:enable_disable_client).with(@agent) @puppetd.setup end end describe "when enabling or disabling agent" do [:enable, :disable].each do |action| it "should call client.#{action}" do @puppetd.stubs(:exit) @puppetd.options.stubs(:[]).with(action).returns(true) @agent.expects(action) @puppetd.enable_disable_client(@agent) end end it "should finally exit" do lambda { @puppetd.enable_disable_client(@agent) }.should raise_error(SystemExit) end end it "should inform the daemon about our agent if :client is set to 'true'" do @puppetd.options.expects(:[]).with(:client).returns true @daemon.expects(:agent=).with(@agent) @puppetd.setup end it "should not inform the daemon about our agent if :client is set to 'false'" do @puppetd.options[:client] = false @daemon.expects(:agent=).never @puppetd.setup end it "should daemonize if needed" do Puppet.stubs(:[]).with(:daemonize).returns(true) @daemon.expects(:daemonize) @puppetd.setup end it "should wait for a certificate" do @puppetd.options.stubs(:[]).with(:waitforcert).returns(123) @host.expects(:wait_for_cert).with(123) @puppetd.setup end it "should not wait for a certificate in fingerprint mode" do @puppetd.options.stubs(:[]).with(:fingerprint).returns(true) @puppetd.options.stubs(:[]).with(:waitforcert).returns(123) @host.expects(:wait_for_cert).never @puppetd.setup end it "should setup listen if told to and not onetime" do Puppet.stubs(:[]).with(:listen).returns(true) @puppetd.options.stubs(:[]).with(:onetime).returns(false) @puppetd.expects(:setup_listen) @puppetd.setup end describe "when setting up listen" do before :each do Puppet.stubs(:[]).with(:authconfig).returns('auth') FileTest.stubs(:exists?).with('auth').returns(true) File.stubs(:exist?).returns(true) @puppetd.options.stubs(:[]).with(:serve).returns([]) @puppetd.stubs(:exit) @server = stub_everything 'server' Puppet::Network::Server.stubs(:new).returns(@server) end it "should exit if no authorization file" do Puppet.stubs(:err) FileTest.stubs(:exists?).with('auth').returns(false) @puppetd.expects(:exit) @puppetd.setup_listen end it "should create a server to listen on at least the Runner handler" do Puppet::Network::Server.expects(:new).with { |args| args[:xmlrpc_handlers] == [:Runner] } @puppetd.setup_listen end it "should create a server to listen for specific handlers" do @puppetd.options.stubs(:[]).with(:serve).returns([:handler]) Puppet::Network::Server.expects(:new).with { |args| args[:xmlrpc_handlers] == [:handler] } @puppetd.setup_listen end it "should use puppet default port" do Puppet.stubs(:[]).with(:puppetport).returns(:port) Puppet::Network::Server.expects(:new).with { |args| args[:port] == :port } @puppetd.setup_listen end end end describe "when running" do before :each do @puppetd.agent = @agent @puppetd.daemon = @daemon @puppetd.options.stubs(:[]).with(:fingerprint).returns(false) end it "should dispatch to fingerprint if --fingerprint is used" do @puppetd.options.stubs(:[]).with(:fingerprint).returns(true) @puppetd.stubs(:fingerprint) @puppetd.run_command end it "should dispatch to onetime if --onetime is used" do @puppetd.options.stubs(:[]).with(:onetime).returns(true) @puppetd.stubs(:onetime) @puppetd.run_command end it "should dispatch to main if --onetime and --fingerprint are not used" do @puppetd.options.stubs(:[]).with(:onetime).returns(false) @puppetd.stubs(:main) @puppetd.run_command end describe "with --onetime" do before :each do @agent.stubs(:run).returns(:report) @puppetd.options.stubs(:[]).with(:client).returns(:client) @puppetd.options.stubs(:[]).with(:detailed_exitcodes).returns(false) @puppetd.stubs(:exit).with(0) Puppet.stubs(:newservice) end it "should exit if no defined --client" do $stderr.stubs(:puts) @puppetd.options.stubs(:[]).with(:client).returns(nil) @puppetd.expects(:exit).with(43) @puppetd.onetime end it "should setup traps" do @daemon.expects(:set_signal_traps) @puppetd.onetime end it "should let the agent run" do @agent.expects(:run).returns(:report) @puppetd.onetime end it "should finish by exiting with 0 error code" do @puppetd.expects(:exit).with(0) @puppetd.onetime end describe "and --detailed-exitcodes" do before :each do @puppetd.options.stubs(:[]).with(:detailed_exitcodes).returns(true) end it "should exit with report's computed exit status" do Puppet.stubs(:[]).with(:noop).returns(false) report = stub 'report', :exit_status => 666 @agent.stubs(:run).returns(report) @puppetd.expects(:exit).with(666) @puppetd.onetime end - it "should always exit with 0 if --noop" do + it "should exit with the report's computer exit status, even if --noop is set." do Puppet.stubs(:[]).with(:noop).returns(true) report = stub 'report', :exit_status => 666 @agent.stubs(:run).returns(report) - @puppetd.expects(:exit).with(0) + @puppetd.expects(:exit).with(666) @puppetd.onetime end end end describe "with --fingerprint" do before :each do @cert = stub_everything 'cert' @puppetd.options.stubs(:[]).with(:fingerprint).returns(true) @puppetd.options.stubs(:[]).with(:digest).returns(:MD5) @host = stub_everything 'host' @puppetd.stubs(:host).returns(@host) end it "should fingerprint the certificate if it exists" do @host.expects(:certificate).returns(@cert) @cert.expects(:fingerprint).with(:MD5).returns "fingerprint" @puppetd.fingerprint end it "should fingerprint the certificate request if no certificate have been signed" do @host.expects(:certificate).returns(nil) @host.expects(:certificate_request).returns(@cert) @cert.expects(:fingerprint).with(:MD5).returns "fingerprint" @puppetd.fingerprint end it "should display the fingerprint" do @host.stubs(:certificate).returns(@cert) @cert.stubs(:fingerprint).with(:MD5).returns("DIGEST") Puppet.expects(:notice).with("DIGEST") @puppetd.fingerprint end end describe "without --onetime and --fingerprint" do before :each do Puppet.stubs(:notice) @puppetd.options.stubs(:[]).with(:client) end it "should start our daemon" do @daemon.expects(:start) @puppetd.main end end end end diff --git a/spec/unit/application/apply_spec.rb b/spec/unit/application/apply_spec.rb index 4e1744206..d4f39abe0 100755 --- a/spec/unit/application/apply_spec.rb +++ b/spec/unit/application/apply_spec.rb @@ -1,405 +1,420 @@ #!/usr/bin/env ruby require File.dirname(__FILE__) + '/../../spec_helper' require 'puppet/application/apply' require 'puppet/file_bucket/dipper' require 'puppet/configurer' describe Puppet::Application::Apply do before :each do @apply = Puppet::Application[:apply] Puppet::Util::Log.stubs(:newdestination) Puppet::Util::Log.stubs(:level=) end [:debug,:loadclasses,:verbose,:use_nodes,:detailed_exitcodes].each do |option| it "should declare handle_#{option} method" do @apply.should respond_to("handle_#{option}".to_sym) end it "should store argument value when calling handle_#{option}" do @apply.options.expects(:[]=).with(option, 'arg') @apply.send("handle_#{option}".to_sym, 'arg') end end it "should set the code to the provided code when :execute is used" do @apply.options.expects(:[]=).with(:code, 'arg') @apply.send("handle_execute".to_sym, 'arg') end it "should ask Puppet::Application to parse Puppet configuration file" do @apply.should_parse_config?.should be_true end describe "when applying options" do it "should set the log destination with --logdest" do Puppet::Log.expects(:newdestination).with("console") @apply.handle_logdest("console") end it "should put the logset options to true" do @apply.options.expects(:[]=).with(:logset,true) @apply.handle_logdest("console") end end describe "during setup" do before :each do Puppet::Log.stubs(:newdestination) - Puppet.stubs(:trap) Puppet::Log.stubs(:level=) Puppet.stubs(:parse_config) Puppet::FileBucket::Dipper.stubs(:new) STDIN.stubs(:read) @apply.options.stubs(:[]).with(any_parameters) end it "should set show_diff on --noop" do Puppet.stubs(:[]=) Puppet.stubs(:[]).with(:config) Puppet.stubs(:[]).with(:noop).returns(true) Puppet.expects(:[]=).with(:show_diff, true) @apply.setup end it "should set console as the log destination if logdest option wasn't provided" do Puppet::Log.expects(:newdestination).with(:console) @apply.setup end it "should set INT trap" do - @apply.expects(:trap).with(:INT) + Signal.expects(:trap).with(:INT) @apply.setup end it "should set log level to debug if --debug was passed" do @apply.options.stubs(:[]).with(:debug).returns(true) Puppet::Log.expects(:level=).with(:debug) @apply.setup end it "should set log level to info if --verbose was passed" do @apply.options.stubs(:[]).with(:verbose).returns(true) Puppet::Log.expects(:level=).with(:info) @apply.setup end it "should print puppet config if asked to in Puppet config" do @apply.stubs(:exit) Puppet.settings.stubs(:print_configs?).returns(true) Puppet.settings.expects(:print_configs) @apply.setup end it "should exit after printing puppet config if asked to in Puppet config" do Puppet.settings.stubs(:print_configs?).returns(true) lambda { @apply.setup }.should raise_error(SystemExit) end end describe "when executing" do it "should dispatch to parseonly if parseonly is set" do @apply.stubs(:options).returns({}) Puppet.stubs(:[]).with(:parseonly).returns(true) @apply.expects(:parseonly) @apply.run_command end it "should dispatch to 'apply' if it was called with 'apply'" do @apply.options[:catalog] = "foo" @apply.expects(:apply) @apply.run_command end it "should dispatch to main if parseonly is not set" do @apply.stubs(:options).returns({}) Puppet.stubs(:[]).with(:parseonly).returns(false) @apply.expects(:main) @apply.run_command end describe "the parseonly command" do before :each do Puppet.stubs(:[]).with(:environment) Puppet.stubs(:[]).with(:manifest).returns("site.pp") Puppet.stubs(:err) @apply.stubs(:exit) @apply.options.stubs(:[]).with(:code).returns "some code" @collection = stub_everything Puppet::Resource::TypeCollection.stubs(:new).returns(@collection) end it "should use a Puppet Resource Type Collection to parse the file" do @collection.expects(:perform_initial_import) @apply.parseonly end it "should exit with exit code 0 if no error" do @apply.expects(:exit).with(0) @apply.parseonly end it "should exit with exit code 1 if error" do @collection.stubs(:perform_initial_import).raises(Puppet::ParseError) @apply.expects(:exit).with(1) @apply.parseonly end + + it "should exit with exit code 1 if error, even if --noop is set" do + Puppet[:noop] = true + @collection.stubs(:perform_initial_import).raises(Puppet::ParseError) + @apply.expects(:exit).with(1) + @apply.parseonly + end end describe "the main command" do before :each do Puppet.stubs(:[]) Puppet.settings.stubs(:use) Puppet.stubs(:[]).with(:prerun_command).returns "" Puppet.stubs(:[]).with(:postrun_command).returns "" Puppet.stubs(:[]).with(:trace).returns(true) @apply.options.stubs(:[]) @facts = stub_everything 'facts' Puppet::Node::Facts.stubs(:find).returns(@facts) @node = stub_everything 'node' Puppet::Node.stubs(:find).returns(@node) @catalog = stub_everything 'catalog' @catalog.stubs(:to_ral).returns(@catalog) Puppet::Resource::Catalog.stubs(:find).returns(@catalog) STDIN.stubs(:read) @transaction = stub_everything 'transaction' @catalog.stubs(:apply).returns(@transaction) @apply.stubs(:exit) Puppet::Util::Storage.stubs(:load) Puppet::Configurer.any_instance.stubs(:save_last_run_summary) # to prevent it from trying to write files end it "should set the code to run from --code" do @apply.options.stubs(:[]).with(:code).returns("code to run") Puppet.expects(:[]=).with(:code,"code to run") @apply.main end it "should set the code to run from STDIN if no arguments" do @apply.command_line.stubs(:args).returns([]) STDIN.stubs(:read).returns("code to run") Puppet.expects(:[]=).with(:code,"code to run") @apply.main end it "should set the manifest if a file is passed on command line and the file exists" do File.stubs(:exist?).with('site.pp').returns true @apply.command_line.stubs(:args).returns(['site.pp']) Puppet.expects(:[]=).with(:manifest,"site.pp") @apply.main end it "should raise an error if a file is passed on command line and the file does not exist" do File.stubs(:exist?).with('noexist.pp').returns false @apply.command_line.stubs(:args).returns(['noexist.pp']) lambda { @apply.main }.should raise_error(RuntimeError, 'Could not find file noexist.pp') end it "should set the manifest to the first file and warn other files will be skipped" do File.stubs(:exist?).with('starwarsIV').returns true File.expects(:exist?).with('starwarsI').never @apply.command_line.stubs(:args).returns(['starwarsIV', 'starwarsI', 'starwarsII']) Puppet.expects(:[]=).with(:manifest,"starwarsIV") Puppet.expects(:warning).with('Only one file can be applied per run. Skipping starwarsI, starwarsII') @apply.main end it "should collect the node facts" do Puppet::Node::Facts.expects(:find).returns(@facts) @apply.main end it "should raise an error if we can't find the node" do Puppet::Node::Facts.expects(:find).returns(nil) lambda { @apply.main }.should raise_error end it "should look for the node" do Puppet::Node.expects(:find).returns(@node) @apply.main end it "should raise an error if we can't find the node" do Puppet::Node.expects(:find).returns(nil) lambda { @apply.main }.should raise_error end it "should merge in our node the loaded facts" do @facts.stubs(:values).returns("values") @node.expects(:merge).with("values") @apply.main end it "should load custom classes if loadclasses" do @apply.options.stubs(:[]).with(:loadclasses).returns(true) Puppet.stubs(:[]).with(:classfile).returns("/etc/puppet/classes.txt") FileTest.stubs(:exists?).with("/etc/puppet/classes.txt").returns(true) FileTest.stubs(:readable?).with("/etc/puppet/classes.txt").returns(true) File.stubs(:read).with("/etc/puppet/classes.txt").returns("class") @node.expects(:classes=) @apply.main end it "should compile the catalog" do Puppet::Resource::Catalog.expects(:find).returns(@catalog) @apply.main end it "should transform the catalog to ral" do @catalog.expects(:to_ral).returns(@catalog) @apply.main end it "should finalize the catalog" do @catalog.expects(:finalize) @apply.main end it "should call the prerun and postrun commands on a Configurer instance" do Puppet::Configurer.any_instance.expects(:execute_prerun_command) Puppet::Configurer.any_instance.expects(:execute_postrun_command) @apply.main end it "should apply the catalog" do @catalog.expects(:apply).returns(stub_everything('transaction')) @apply.main end describe "with detailed_exitcodes" do it "should exit with report's computed exit status" do Puppet.stubs(:[]).with(:noop).returns(false) @apply.options.stubs(:[]).with(:detailed_exitcodes).returns(true) Puppet::Transaction::Report.any_instance.stubs(:exit_status).returns(666) @apply.expects(:exit).with(666) @apply.main end + it "should exit with report's computed exit status, even if --noop is set" do + Puppet.stubs(:[]).with(:noop).returns(true) + @apply.options.stubs(:[]).with(:detailed_exitcodes).returns(true) + Puppet::Transaction::Report.any_instance.stubs(:exit_status).returns(666) + @apply.expects(:exit).with(666) + + @apply.main + end + it "should always exit with 0 if option is disabled" do Puppet.stubs(:[]).with(:noop).returns(false) @apply.options.stubs(:[]).with(:detailed_exitcodes).returns(false) report = stub 'report', :exit_status => 666 @transaction.stubs(:report).returns(report) @apply.expects(:exit).with(0) @apply.main end it "should always exit with 0 if --noop" do Puppet.stubs(:[]).with(:noop).returns(true) @apply.options.stubs(:[]).with(:detailed_exitcodes).returns(true) report = stub 'report', :exit_status => 666 @transaction.stubs(:report).returns(report) @apply.expects(:exit).with(0) @apply.main end end end describe "the 'apply' command" do it "should read the catalog in from disk if a file name is provided" do @apply.options[:catalog] = "/my/catalog.pson" File.expects(:read).with("/my/catalog.pson").returns "something" Puppet::Resource::Catalog.stubs(:convert_from).with(:pson,'something').returns Puppet::Resource::Catalog.new @apply.apply end it "should read the catalog in from stdin if '-' is provided" do @apply.options[:catalog] = "-" $stdin.expects(:read).returns "something" Puppet::Resource::Catalog.stubs(:convert_from).with(:pson,'something').returns Puppet::Resource::Catalog.new @apply.apply end it "should deserialize the catalog from the default format" do @apply.options[:catalog] = "/my/catalog.pson" File.stubs(:read).with("/my/catalog.pson").returns "something" Puppet::Resource::Catalog.stubs(:default_format).returns :rot13_piglatin Puppet::Resource::Catalog.stubs(:convert_from).with(:rot13_piglatin,'something').returns Puppet::Resource::Catalog.new @apply.apply end it "should fail helpfully if deserializing fails" do @apply.options[:catalog] = "/my/catalog.pson" File.stubs(:read).with("/my/catalog.pson").returns "something syntacically invalid" lambda { @apply.apply }.should raise_error(Puppet::Error) end it "should convert plain data structures into a catalog if deserialization does not do so" do @apply.options[:catalog] = "/my/catalog.pson" File.stubs(:read).with("/my/catalog.pson").returns "something" Puppet::Resource::Catalog.stubs(:convert_from).with(:pson,"something").returns({:foo => "bar"}) Puppet::Resource::Catalog.expects(:pson_create).with({:foo => "bar"}).returns(Puppet::Resource::Catalog.new) @apply.apply end it "should convert the catalog to a RAL catalog and use a Configurer instance to apply it" do @apply.options[:catalog] = "/my/catalog.pson" File.stubs(:read).with("/my/catalog.pson").returns "something" catalog = Puppet::Resource::Catalog.new Puppet::Resource::Catalog.stubs(:convert_from).with(:pson,'something').returns catalog catalog.expects(:to_ral).returns "mycatalog" configurer = stub 'configurer' Puppet::Configurer.expects(:new).returns configurer configurer.expects(:run).with(:catalog => "mycatalog") @apply.apply end end end end diff --git a/spec/unit/application/cert_spec.rb b/spec/unit/application/cert_spec.rb index 4663fc938..b3257916b 100755 --- a/spec/unit/application/cert_spec.rb +++ b/spec/unit/application/cert_spec.rb @@ -1,194 +1,234 @@ #!/usr/bin/env ruby require File.dirname(__FILE__) + '/../../spec_helper' require 'puppet/application/cert' describe Puppet::Application::Cert do before :each do @cert_app = Puppet::Application[:cert] Puppet::Util::Log.stubs(:newdestination) Puppet::Util::Log.stubs(:level=) end it "should operate in master run_mode" do @cert_app.class.run_mode.name.should equal(:master) end it "should ask Puppet::Application to parse Puppet configuration file" do @cert_app.should_parse_config?.should be_true end it "should declare a main command" do @cert_app.should respond_to(:main) end Puppet::SSL::CertificateAuthority::Interface::INTERFACE_METHODS.reject{ |m| m == :destroy }.each do |method| it "should declare option --#{method}" do @cert_app.should respond_to("handle_#{method}".to_sym) end end it "should set log level to info with the --verbose option" do Puppet::Log.expects(:level=).with(:info) @cert_app.handle_verbose(0) end it "should set log level to debug with the --debug option" do Puppet::Log.expects(:level=).with(:debug) @cert_app.handle_debug(0) end it "should set the fingerprint digest with the --digest option" do @cert_app.handle_digest(:digest) @cert_app.digest.should == :digest end it "should set cert_mode to :destroy for --clean" do @cert_app.handle_clean(0) - @cert_app.cert_mode.should == :destroy + @cert_app.subcommand.should == :destroy end it "should set all to true for --all" do @cert_app.handle_all(0) @cert_app.all.should be_true end it "should set signed to true for --signed" do @cert_app.handle_signed(0) @cert_app.signed.should be_true end Puppet::SSL::CertificateAuthority::Interface::INTERFACE_METHODS.reject { |m| m == :destroy }.each do |method| it "should set cert_mode to #{method} with option --#{method}" do @cert_app.send("handle_#{method}".to_sym, nil) - @cert_app.cert_mode.should == method + @cert_app.subcommand.should == method end end describe "during setup" do before :each do Puppet::Log.stubs(:newdestination) Puppet::SSL::Host.stubs(:ca_location=) Puppet::SSL::CertificateAuthority.stubs(:new) end it "should set console as the log destination" do Puppet::Log.expects(:newdestination).with(:console) @cert_app.setup end it "should print puppet config if asked to in Puppet config" do @cert_app.stubs(:exit) Puppet.settings.stubs(:print_configs?).returns(true) Puppet.settings.expects(:print_configs) @cert_app.setup end it "should exit after printing puppet config if asked to in Puppet config" do Puppet.settings.stubs(:print_configs?).returns(true) lambda { @cert_app.setup }.should raise_error(SystemExit) end it "should set the CA location to 'only'" do Puppet::SSL::Host.expects(:ca_location=).with(:only) @cert_app.setup end it "should create a new certificate authority" do Puppet::SSL::CertificateAuthority.expects(:new) @cert_app.setup end it "should set the ca_location to :local if the cert_mode is generate" do - @cert_app.find_mode('--generate') + @cert_app.subcommand = 'generate' Puppet::SSL::Host.expects(:ca_location=).with(:local) @cert_app.setup end it "should set the ca_location to :local if the cert_mode is destroy" do - @cert_app.find_mode('--destroy') + @cert_app.subcommand = 'destroy' Puppet::SSL::Host.expects(:ca_location=).with(:local) @cert_app.setup end it "should set the ca_location to :only if the cert_mode is print" do - @cert_app.find_mode('--print') + @cert_app.subcommand = 'print' Puppet::SSL::Host.expects(:ca_location=).with(:only) @cert_app.setup end end describe "when running" do before :each do @cert_app.all = false @ca = stub_everything 'ca' @cert_app.ca = @ca @cert_app.command_line.stubs(:args).returns([]) end it "should delegate to the CertificateAuthority" do @ca.expects(:apply) @cert_app.main end it "should delegate with :all if option --all was given" do @cert_app.handle_all(0) @ca.expects(:apply).with { |cert_mode,to| to[:to] == :all } @cert_app.main end it "should delegate to ca.apply with the hosts given on command line" do @cert_app.command_line.stubs(:args).returns(["host"]) @ca.expects(:apply).with { |cert_mode,to| to[:to] == ["host"]} @cert_app.main end it "should send the currently set digest" do @cert_app.command_line.stubs(:args).returns(["host"]) @cert_app.handle_digest(:digest) @ca.expects(:apply).with { |cert_mode,to| to[:digest] == :digest} @cert_app.main end - it "should delegate to ca.apply with current set cert_mode" do - @cert_app.cert_mode = "currentmode" + it "should revoke cert if cert_mode is clean" do + @cert_app.subcommand = :destroy @cert_app.command_line.stubs(:args).returns(["host"]) - @ca.expects(:apply).with { |cert_mode,to| cert_mode == "currentmode" } + @ca.expects(:apply).with { |cert_mode,to| cert_mode == :revoke } + @ca.expects(:apply).with { |cert_mode,to| cert_mode == :destroy } @cert_app.main end + end - it "should revoke cert if cert_mode is clean" do - @cert_app.cert_mode = :destroy - @cert_app.command_line.stubs(:args).returns(["host"]) + describe "when identifying subcommands" do + before :each do + @cert_app.all = false + @ca = stub_everything 'ca' + @cert_app.ca = @ca + end - @ca.expects(:apply).with { |cert_mode,to| cert_mode == :revoke } - @ca.expects(:apply).with { |cert_mode,to| cert_mode == :destroy } + it "should not fail when no command is given" do + # Make the help method silent for testing; this is a bit nasty, but we + # can't identify a cleaner method. Help welcome. --daniel 2011-02-22 + Puppet.features.stubs(:usage?).returns(false) + @cert_app.stubs(:puts) - @cert_app.main + @cert_app.command_line.stubs(:args).returns([]) + expect { @cert_app.parse_options }.should raise_error SystemExit end + %w{list revoke generate sign print verify fingerprint}.each do |cmd| + short = cmd[0,1] + [cmd, "--#{cmd}", "-#{short}"].each do |option| + # In our command line '-v' was eaten by 'verbose', so we can't consume + # it here; this is a special case from our otherwise standard + # processing. --daniel 2011-02-22 + next if option == "-v" + + it "should recognise '#{option}'" do + args = [option, "fun.example.com"] + + @cert_app.command_line.stubs(:args).returns(args) + @cert_app.parse_options + @cert_app.subcommand.should == cmd.to_sym + + args.should == ["fun.example.com"] + end + end + end + + %w{clean --clean -c}.each do |ugly| + it "should recognise the '#{ugly}' option as destroy" do + args = [ugly, "fun.example.com"] + + @cert_app.command_line.stubs(:args).returns(args) + @cert_app.parse_options + @cert_app.subcommand.should == :destroy + + args.should == ["fun.example.com"] + end + end end end diff --git a/spec/unit/application/filebucket_spec.rb b/spec/unit/application/filebucket_spec.rb index e6272f179..95135c7eb 100644 --- a/spec/unit/application/filebucket_spec.rb +++ b/spec/unit/application/filebucket_spec.rb @@ -1,224 +1,224 @@ #!/usr/bin/env ruby require File.dirname(__FILE__) + '/../../spec_helper' require 'puppet/application/filebucket' require 'puppet/file_bucket/dipper' describe Puppet::Application::Filebucket do before :each do @filebucket = Puppet::Application[:filebucket] end it "should ask Puppet::Application to not parse Puppet configuration file" do @filebucket.should_parse_config?.should be_false end it "should declare a get command" do @filebucket.should respond_to(:get) end it "should declare a backup command" do @filebucket.should respond_to(:backup) end it "should declare a restore command" do @filebucket.should respond_to(:restore) end [:bucket, :debug, :local, :remote, :verbose].each do |option| it "should declare handle_#{option} method" do @filebucket.should respond_to("handle_#{option}".to_sym) end it "should store argument value when calling handle_#{option}" do @filebucket.options.expects(:[]=).with("#{option}".to_sym, 'arg') @filebucket.send("handle_#{option}".to_sym, 'arg') end end describe "during setup" do before :each do Puppet::Log.stubs(:newdestination) Puppet.stubs(:settraps) Puppet::Log.stubs(:level=) Puppet.stubs(:parse_config) Puppet::FileBucket::Dipper.stubs(:new) @filebucket.options.stubs(:[]).with(any_parameters) end it "should set console as the log destination" do Puppet::Log.expects(:newdestination).with(:console) @filebucket.setup end it "should trap INT" do - @filebucket.expects(:trap).with(:INT) + Signal.expects(:trap).with(:INT) @filebucket.setup end it "should set log level to debug if --debug was passed" do @filebucket.options.stubs(:[]).with(:debug).returns(true) Puppet::Log.expects(:level=).with(:debug) @filebucket.setup end it "should set log level to info if --verbose was passed" do @filebucket.options.stubs(:[]).with(:verbose).returns(true) Puppet::Log.expects(:level=).with(:info) @filebucket.setup end it "should Parse puppet config" do Puppet.expects(:parse_config) @filebucket.setup end it "should print puppet config if asked to in Puppet config" do @filebucket.stubs(:exit) Puppet.settings.stubs(:print_configs?).returns(true) Puppet.settings.expects(:print_configs) @filebucket.setup end it "should exit after printing puppet config if asked to in Puppet config" do Puppet.settings.stubs(:print_configs?).returns(true) lambda { @filebucket.setup }.should raise_error(SystemExit) end describe "with local bucket" do before :each do @filebucket.options.stubs(:[]).with(:local).returns(true) end it "should create a client with the default bucket if none passed" do Puppet.stubs(:[]).with(:bucketdir).returns("path") Puppet::FileBucket::Dipper.expects(:new).with { |h| h[:Path] == "path" } @filebucket.setup end it "should create a local Dipper with the given bucket" do @filebucket.options.stubs(:[]).with(:bucket).returns("path") Puppet::FileBucket::Dipper.expects(:new).with { |h| h[:Path] == "path" } @filebucket.setup end end describe "with remote bucket" do it "should create a remote Client to the configured server" do Puppet.stubs(:[]).with(:server).returns("puppet.reductivelabs.com") Puppet::FileBucket::Dipper.expects(:new).with { |h| h[:Server] == "puppet.reductivelabs.com" } @filebucket.setup end end end describe "when running" do before :each do Puppet::Log.stubs(:newdestination) Puppet.stubs(:settraps) Puppet::Log.stubs(:level=) Puppet.stubs(:parse_config) Puppet::FileBucket::Dipper.stubs(:new) @filebucket.options.stubs(:[]).with(any_parameters) @client = stub 'client' Puppet::FileBucket::Dipper.stubs(:new).returns(@client) @filebucket.setup end it "should use the first non-option parameter as the dispatch" do @filebucket.command_line.stubs(:args).returns(['get']) @filebucket.expects(:get) @filebucket.run_command end describe "the command get" do before :each do @filebucket.stubs(:print) @filebucket.stubs(:args).returns([]) end it "should call the client getfile method" do @client.expects(:getfile) @filebucket.get end it "should call the client getfile method with the given md5" do md5="DEADBEEF" @filebucket.stubs(:args).returns([md5]) @client.expects(:getfile).with(md5) @filebucket.get end it "should print the file content" do @client.stubs(:getfile).returns("content") @filebucket.expects(:print).returns("content") @filebucket.get end end describe "the command backup" do it "should call the client backup method for each given parameter" do @filebucket.stubs(:puts) FileTest.stubs(:exists?).returns(true) FileTest.stubs(:readable?).returns(true) @filebucket.stubs(:args).returns(["file1", "file2"]) @client.expects(:backup).with("file1") @client.expects(:backup).with("file2") @filebucket.backup end end describe "the command restore" do it "should call the client getfile method with the given md5" do md5="DEADBEEF" file="testfile" @filebucket.stubs(:args).returns([file, md5]) @client.expects(:restore).with(file,md5) @filebucket.restore end end end end diff --git a/spec/unit/application/queue_spec.rb b/spec/unit/application/queue_spec.rb index bd0d53ab1..f8ebbd0b4 100755 --- a/spec/unit/application/queue_spec.rb +++ b/spec/unit/application/queue_spec.rb @@ -1,187 +1,183 @@ #!/usr/bin/env ruby require File.dirname(__FILE__) + '/../../spec_helper' require 'puppet/application/queue' require 'puppet/indirector/catalog/queue' describe Puppet::Application::Queue do before :each do @queue = Puppet::Application[:queue] @queue.stubs(:puts) @daemon = stub_everything 'daemon', :daemonize => nil Puppet::Util::Log.stubs(:newdestination) Puppet::Util::Log.stubs(:level=) Puppet::Resource::Catalog.stubs(:terminus_class=) end it "should ask Puppet::Application to parse Puppet configuration file" do @queue.should_parse_config?.should be_true end it "should declare a main command" do @queue.should respond_to(:main) end it "should declare a preinit block" do @queue.should respond_to(:preinit) end describe "in preinit" do - before :each do - @queue.stubs(:trap) - end - it "should catch INT" do - @queue.expects(:trap).with { |arg,block| arg == :INT } + Signal.expects(:trap).with { |arg,block| arg == :INT } @queue.preinit end it "should init :verbose to false" do @queue.preinit @queue.options[:verbose].should be_false end it "should init :debug to false" do @queue.preinit @queue.options[:debug].should be_false end it "should create a Daemon instance and copy ARGV to it" do ARGV.expects(:dup).returns "eh" daemon = mock("daemon") daemon.expects(:argv=).with("eh") Puppet::Daemon.expects(:new).returns daemon @queue.preinit end end describe "when handling options" do [:debug, :verbose].each do |option| it "should declare handle_#{option} method" do @queue.should respond_to("handle_#{option}".to_sym) end it "should store argument value when calling handle_#{option}" do @queue.options.expects(:[]=).with(option, 'arg') @queue.send("handle_#{option}".to_sym, 'arg') end end end describe "during setup" do before :each do @queue.options.stubs(:[]) @queue.daemon.stubs(:daemonize) Puppet.stubs(:info) Puppet.features.stubs(:stomp?).returns true Puppet::Resource::Catalog.stubs(:terminus_class=) Puppet.stubs(:settraps) Puppet.settings.stubs(:print_config?) Puppet.settings.stubs(:print_config) end it "should fail if the stomp feature is missing" do Puppet.features.expects(:stomp?).returns false lambda { @queue.setup }.should raise_error(ArgumentError) end it "should print puppet config if asked to in Puppet config" do @queue.stubs(:exit) Puppet.settings.stubs(:print_configs?).returns(true) Puppet.settings.expects(:print_configs) @queue.setup end it "should exit after printing puppet config if asked to in Puppet config" do Puppet.settings.stubs(:print_configs?).returns(true) lambda { @queue.setup }.should raise_error(SystemExit) end it "should call setup_logs" do @queue.expects(:setup_logs) @queue.setup end describe "when setting up logs" do before :each do Puppet::Util::Log.stubs(:newdestination) end it "should set log level to debug if --debug was passed" do @queue.options.stubs(:[]).with(:debug).returns(true) Puppet::Util::Log.expects(:level=).with(:debug) @queue.setup_logs end it "should set log level to info if --verbose was passed" do @queue.options.stubs(:[]).with(:verbose).returns(true) Puppet::Util::Log.expects(:level=).with(:info) @queue.setup_logs end [:verbose, :debug].each do |level| it "should set console as the log destination with level #{level}" do @queue.options.stubs(:[]).with(level).returns(true) Puppet::Util::Log.expects(:newdestination).with(:console) @queue.setup_logs end end end it "should configure the Catalog class to use ActiveRecord" do Puppet::Resource::Catalog.expects(:terminus_class=).with(:active_record) @queue.setup end it "should daemonize if needed" do Puppet.expects(:[]).with(:daemonize).returns(true) @queue.daemon.expects(:daemonize) @queue.setup end end describe "when running" do before :each do @queue.stubs(:sleep_forever) Puppet::Resource::Catalog::Queue.stubs(:subscribe) Thread.list.each { |t| t.stubs(:join) } end it "should subscribe to the queue" do Puppet::Resource::Catalog::Queue.expects(:subscribe) @queue.main end it "should log and save each catalog passed by the queue" do catalog = mock 'catalog', :name => 'eh' catalog.expects(:save) Puppet::Resource::Catalog::Queue.expects(:subscribe).yields(catalog) Puppet.expects(:notice).times(2) @queue.main end it "should join all of the running threads" do Thread.list.each { |t| t.expects(:join) } @queue.main end end end diff --git a/spec/unit/configurer/downloader_spec.rb b/spec/unit/configurer/downloader_spec.rb index c57f39fb5..4080263e7 100755 --- a/spec/unit/configurer/downloader_spec.rb +++ b/spec/unit/configurer/downloader_spec.rb @@ -1,188 +1,200 @@ #!/usr/bin/env ruby require File.dirname(__FILE__) + '/../../spec_helper' require 'puppet/configurer/downloader' describe Puppet::Configurer::Downloader do + require 'puppet_spec/files' + include PuppetSpec::Files it "should require a name" do lambda { Puppet::Configurer::Downloader.new }.should raise_error(ArgumentError) end it "should require a path and a source at initialization" do lambda { Puppet::Configurer::Downloader.new("name") }.should raise_error(ArgumentError) end it "should set the name, path and source appropriately" do dler = Puppet::Configurer::Downloader.new("facts", "path", "source") dler.name.should == "facts" dler.path.should == "path" dler.source.should == "source" end it "should be able to provide a timeout value" do Puppet::Configurer::Downloader.should respond_to(:timeout) end it "should use the configtimeout, converted to an integer, as its timeout" do Puppet.settings.expects(:value).with(:configtimeout).returns "50" Puppet::Configurer::Downloader.timeout.should == 50 end describe "when creating the file that does the downloading" do before do @dler = Puppet::Configurer::Downloader.new("foo", "path", "source") end it "should create a file instance with the right path and source" do Puppet::Type.type(:file).expects(:new).with { |opts| opts[:path] == "path" and opts[:source] == "source" } @dler.file end it "should tag the file with the downloader name" do Puppet::Type.type(:file).expects(:new).with { |opts| opts[:tag] == "foo" } @dler.file end it "should always recurse" do Puppet::Type.type(:file).expects(:new).with { |opts| opts[:recurse] == true } @dler.file end it "should always purge" do Puppet::Type.type(:file).expects(:new).with { |opts| opts[:purge] == true } @dler.file end it "should never be in noop" do Puppet::Type.type(:file).expects(:new).with { |opts| opts[:noop] == false } @dler.file end it "should always set the owner to the current UID" do Process.expects(:uid).returns 51 Puppet::Type.type(:file).expects(:new).with { |opts| opts[:owner] == 51 } @dler.file end it "should always set the group to the current GID" do Process.expects(:gid).returns 61 Puppet::Type.type(:file).expects(:new).with { |opts| opts[:group] == 61 } @dler.file end it "should always force the download" do Puppet::Type.type(:file).expects(:new).with { |opts| opts[:force] == true } @dler.file end it "should never back up when downloading" do Puppet::Type.type(:file).expects(:new).with { |opts| opts[:backup] == false } @dler.file end it "should support providing an 'ignore' parameter" do Puppet::Type.type(:file).expects(:new).with { |opts| opts[:ignore] == [".svn"] } @dler = Puppet::Configurer::Downloader.new("foo", "path", "source", ".svn") @dler.file end it "should split the 'ignore' parameter on whitespace" do Puppet::Type.type(:file).expects(:new).with { |opts| opts[:ignore] == %w{.svn CVS} } @dler = Puppet::Configurer::Downloader.new("foo", "path", "source", ".svn CVS") @dler.file end end describe "when creating the catalog to do the downloading" do before do - @dler = Puppet::Configurer::Downloader.new("foo", "path", "source") + @dler = Puppet::Configurer::Downloader.new("foo", "/download/path", "source") end it "should create a catalog and add the file to it" do - file = mock 'file' - catalog = mock 'catalog' - - @dler.expects(:file).returns file - - Puppet::Resource::Catalog.expects(:new).returns catalog - catalog.expects(:add_resource).with(file) + catalog = @dler.catalog + catalog.resources.size.should == 1 + catalog.resources.first.class.should == Puppet::Type::File + catalog.resources.first.name.should == "/download/path" + end - @dler.catalog.should equal(catalog) + it "should specify that it is not managing a host catalog" do + @dler.catalog.host_config.should == false end + end describe "when downloading" do before do - @dler = Puppet::Configurer::Downloader.new("foo", "path", "source") + @dl_name = tmpfile("downloadpath") + source_name = tmpfile("source") + File.open(source_name, 'w') {|f| f.write('hola mundo') } + @dler = Puppet::Configurer::Downloader.new("foo", @dl_name, source_name) + end + + it "should not skip downloaded resources when filtering on tags" do + Puppet[:tags] = 'maytag' + @dler.evaluate + + File.exists?(@dl_name).should be_true end it "should log that it is downloading" do Puppet.expects(:info) Timeout.stubs(:timeout) @dler.evaluate end it "should set a timeout for the download" do Puppet::Configurer::Downloader.expects(:timeout).returns 50 Timeout.expects(:timeout).with(50) @dler.evaluate end it "should apply the catalog within the timeout block" do catalog = mock 'catalog' @dler.expects(:catalog).returns(catalog) Timeout.expects(:timeout).yields catalog.expects(:apply) @dler.evaluate end it "should return all changed file paths" do trans = mock 'transaction' catalog = mock 'catalog' @dler.expects(:catalog).returns(catalog) catalog.expects(:apply).yields(trans) Timeout.expects(:timeout).yields resource = mock 'resource' resource.expects(:[]).with(:path).returns "/changed/file" trans.expects(:changed?).returns([resource]) @dler.evaluate.should == %w{/changed/file} end it "should yield the resources if a block is given" do trans = mock 'transaction' catalog = mock 'catalog' @dler.expects(:catalog).returns(catalog) catalog.expects(:apply).yields(trans) Timeout.expects(:timeout).yields resource = mock 'resource' resource.expects(:[]).with(:path).returns "/changed/file" trans.expects(:changed?).returns([resource]) yielded = nil @dler.evaluate { |r| yielded = r } yielded.should == resource end it "should catch and log exceptions" do Puppet.expects(:err) Timeout.stubs(:timeout).raises(Puppet::Error, "testing") lambda { @dler.evaluate }.should_not raise_error end end end diff --git a/spec/unit/daemon_spec.rb b/spec/unit/daemon_spec.rb index e24db7881..39592b7c9 100755 --- a/spec/unit/daemon_spec.rb +++ b/spec/unit/daemon_spec.rb @@ -1,300 +1,296 @@ #!/usr/bin/env ruby require File.dirname(__FILE__) + '/../spec_helper' require 'puppet/daemon' def without_warnings flag = $VERBOSE $VERBOSE = nil yield $VERBOSE = flag end describe Puppet::Daemon do before do @daemon = Puppet::Daemon.new end it "should be able to manage an agent" do @daemon.should respond_to(:agent) end it "should be able to manage a network server" do @daemon.should respond_to(:server) end it "should reopen the Log logs when told to reopen logs" do Puppet::Util::Log.expects(:reopen) @daemon.reopen_logs end describe "when setting signal traps" do - before do - @daemon.stubs(:trap) - end - {:INT => :stop, :TERM => :stop, :HUP => :restart, :USR1 => :reload, :USR2 => :reopen_logs}.each do |signal, method| it "should log and call #{method} when it receives #{signal}" do - @daemon.expects(:trap).with(signal).yields + Signal.expects(:trap).with(signal).yields Puppet.expects(:notice) @daemon.expects(method) @daemon.set_signal_traps end end end describe "when starting" do before do @daemon.stubs(:create_pidfile) @daemon.stubs(:set_signal_traps) EventLoop.current.stubs(:run) end it "should fail if it has neither agent nor server" do lambda { @daemon.start }.should raise_error(Puppet::DevError) end it "should create its pidfile" do @daemon.stubs(:agent).returns stub('agent', :start => nil) @daemon.expects(:create_pidfile) @daemon.start end it "should start the agent if the agent is configured" do agent = mock 'agent' agent.expects(:start) @daemon.stubs(:agent).returns agent @daemon.start end it "should start its server if one is configured" do server = mock 'server' server.expects(:start) @daemon.stubs(:server).returns server @daemon.start end it "should let the current EventLoop run" do @daemon.stubs(:agent).returns stub('agent', :start => nil) EventLoop.current.expects(:run) @daemon.start end end describe "when stopping" do before do @daemon.stubs(:remove_pidfile) @daemon.stubs(:exit) Puppet::Util::Log.stubs(:close_all) # to make the global safe to mock, set it to a subclass of itself, # then restore it in an after pass without_warnings { Puppet::Application = Class.new(Puppet::Application) } end after do # restore from the superclass so we lose the stub garbage without_warnings { Puppet::Application = Puppet::Application.superclass } end it "should stop its server if one is configured" do server = mock 'server' server.expects(:stop) @daemon.stubs(:server).returns server @daemon.stop end it 'should request a stop from Puppet::Application' do Puppet::Application.expects(:stop!) @daemon.stop end it "should remove its pidfile" do @daemon.expects(:remove_pidfile) @daemon.stop end it "should close all logs" do Puppet::Util::Log.expects(:close_all) @daemon.stop end it "should exit unless called with ':exit => false'" do @daemon.expects(:exit) @daemon.stop end it "should not exit if called with ':exit => false'" do @daemon.expects(:exit).never @daemon.stop :exit => false end end describe "when creating its pidfile" do it "should use an exclusive mutex" do Puppet.settings.expects(:value).with(:name).returns "me" Puppet::Util.expects(:synchronize_on).with("me",Sync::EX) @daemon.create_pidfile end it "should lock the pidfile using the Pidlock class" do pidfile = mock 'pidfile' Puppet.settings.stubs(:value).with(:name).returns "eh" Puppet.settings.expects(:value).with(:pidfile).returns "/my/file" Puppet::Util::Pidlock.expects(:new).with("/my/file").returns pidfile pidfile.expects(:lock).returns true @daemon.create_pidfile end it "should fail if it cannot lock" do pidfile = mock 'pidfile' Puppet.settings.stubs(:value).with(:name).returns "eh" Puppet.settings.stubs(:value).with(:pidfile).returns "/my/file" Puppet::Util::Pidlock.expects(:new).with("/my/file").returns pidfile pidfile.expects(:lock).returns false lambda { @daemon.create_pidfile }.should raise_error end end describe "when removing its pidfile" do it "should use an exclusive mutex" do Puppet.settings.expects(:value).with(:name).returns "me" Puppet::Util.expects(:synchronize_on).with("me",Sync::EX) @daemon.remove_pidfile end it "should do nothing if the pidfile is not present" do pidfile = mock 'pidfile', :locked? => false Puppet::Util::Pidlock.expects(:new).with("/my/file").returns pidfile Puppet.settings.stubs(:value).with(:name).returns "eh" Puppet.settings.stubs(:value).with(:pidfile).returns "/my/file" pidfile.expects(:unlock).never @daemon.remove_pidfile end it "should unlock the pidfile using the Pidlock class" do pidfile = mock 'pidfile', :locked? => true Puppet::Util::Pidlock.expects(:new).with("/my/file").returns pidfile pidfile.expects(:unlock).returns true Puppet.settings.stubs(:value).with(:name).returns "eh" Puppet.settings.stubs(:value).with(:pidfile).returns "/my/file" @daemon.remove_pidfile end it "should warn if it cannot remove the pidfile" do pidfile = mock 'pidfile', :locked? => true Puppet::Util::Pidlock.expects(:new).with("/my/file").returns pidfile pidfile.expects(:unlock).returns false Puppet.settings.stubs(:value).with(:name).returns "eh" Puppet.settings.stubs(:value).with(:pidfile).returns "/my/file" Puppet.expects :err @daemon.remove_pidfile end end describe "when reloading" do it "should do nothing if no agent is configured" do @daemon.reload end it "should do nothing if the agent is running" do agent = mock 'agent' agent.expects(:running?).returns true @daemon.stubs(:agent).returns agent @daemon.reload end it "should run the agent if one is available and it is not running" do agent = mock 'agent' agent.expects(:running?).returns false agent.expects :run @daemon.stubs(:agent).returns agent @daemon.reload end end describe "when restarting" do before do without_warnings { Puppet::Application = Class.new(Puppet::Application) } end after do without_warnings { Puppet::Application = Puppet::Application.superclass } end it 'should set Puppet::Application.restart!' do Puppet::Application.expects(:restart!) @daemon.stubs(:reexec) @daemon.restart end it "should reexec itself if no agent is available" do @daemon.expects(:reexec) @daemon.restart end it "should reexec itself if the agent is not running" do agent = mock 'agent' agent.expects(:running?).returns false @daemon.stubs(:agent).returns agent @daemon.expects(:reexec) @daemon.restart end end describe "when reexecing it self" do before do @daemon.stubs(:exec) @daemon.stubs(:stop) end it "should fail if no argv values are available" do @daemon.expects(:argv).returns nil lambda { @daemon.reexec }.should raise_error(Puppet::DevError) end it "should shut down without exiting" do @daemon.argv = %w{foo} @daemon.expects(:stop).with(:exit => false) @daemon.reexec end it "should call 'exec' with the original executable and arguments" do @daemon.argv = %w{foo} @daemon.expects(:exec).with($0 + " foo") @daemon.reexec end end end diff --git a/spec/unit/indirector/facts/inventory_active_record_spec.rb b/spec/unit/indirector/facts/inventory_active_record_spec.rb new file mode 100644 index 000000000..9558abde2 --- /dev/null +++ b/spec/unit/indirector/facts/inventory_active_record_spec.rb @@ -0,0 +1,163 @@ +#!/usr/bin/env ruby + +require File.dirname(__FILE__) + '/../../../spec_helper' +require 'sqlite3' rescue nil +require 'tempfile' +require 'puppet/rails' + +describe "Puppet::Node::Facts::InventoryActiveRecord", :if => (Puppet.features.rails? and defined? SQLite3) do + let(:terminus) { Puppet::Node::Facts::InventoryActiveRecord.new } + + before :all do + require 'puppet/indirector/facts/inventory_active_record' + @dbfile = Tempfile.new("testdb") + @dbfile.close + end + + after :all do + Puppet::Node::Facts.indirection.reset_terminus_class + @dbfile.unlink + end + + before :each do + Puppet::Node::Facts.terminus_class = :inventory_active_record + Puppet[:dbadapter] = 'sqlite3' + Puppet[:dblocation] = @dbfile.path + Puppet[:railslog] = "/dev/null" + Puppet::Rails.init + end + + after :each do + Puppet::Rails.teardown + end + + describe "#save" do + it "should use an existing node if possible" do + node = Puppet::Rails::InventoryNode.new(:name => "foo", :timestamp => Time.now) + node.save + Puppet::Node::Facts.new("foo", "uptime_days" => "60", "kernel" => "Darwin").save + + Puppet::Rails::InventoryNode.count.should == 1 + Puppet::Rails::InventoryNode.first.should == node + end + + it "should create a new node if one can't be found" do + # This test isn't valid if there are nodes to begin with + Puppet::Rails::InventoryNode.count.should == 0 + + Puppet::Node::Facts.new("foo", "uptime_days" => "60", "kernel" => "Darwin").save + + Puppet::Rails::InventoryNode.count.should == 1 + Puppet::Rails::InventoryNode.first.name.should == "foo" + end + + it "should save the facts" do + Puppet::Node::Facts.new("foo", "uptime_days" => "60", "kernel" => "Darwin").save + + Puppet::Rails::InventoryFact.all.map{|f| [f.name,f.value]}.should =~ [["uptime_days","60"],["kernel","Darwin"]] + end + + it "should remove the previous facts for an existing node" do + Puppet::Node::Facts.new("foo", "uptime_days" => "30", "kernel" => "Darwin").save + bar_facts = Puppet::Node::Facts.new("bar", "uptime_days" => "35", "kernel" => "Linux") + foo_facts = Puppet::Node::Facts.new("foo", "uptime_days" => "60", "is_virtual" => "false") + bar_facts.save + foo_facts.save + + Puppet::Node::Facts.find("bar").should == bar_facts + Puppet::Node::Facts.find("foo").should == foo_facts + Puppet::Rails::InventoryFact.all.map{|f| [f.name,f.value]}.should_not include(["uptime_days", "30"], ["kernel", "Darwin"]) + end + end + + describe "#find" do + before do + @foo_facts = Puppet::Node::Facts.new("foo", "uptime_days" => "60", "kernel" => "Darwin") + @bar_facts = Puppet::Node::Facts.new("bar", "uptime_days" => "30", "kernel" => "Linux") + @foo_facts.save + @bar_facts.save + end + + it "should identify facts by node name" do + Puppet::Node::Facts.find("foo").should == @foo_facts + end + + it "should return nil if no node instance can be found" do + Puppet::Node::Facts.find("non-existent node").should == nil + end + end + + describe "#search" do + def search_request(conditions) + Puppet::Indirector::Request.new(:facts, :search, nil, conditions) + end + + before :each do + @now = Time.now + @foo = Puppet::Node::Facts.new("foo", "fact1" => "value1", "fact2" => "value2", "uptime_days" => "30") + @bar = Puppet::Node::Facts.new("bar", "fact1" => "value1", "uptime_days" => "60") + @baz = Puppet::Node::Facts.new("baz", "fact1" => "value2", "fact2" => "value1", "uptime_days" => "90") + @bat = Puppet::Node::Facts.new("bat") + @foo.timestamp = @now - 3600*1 + @bar.timestamp = @now - 3600*3 + @baz.timestamp = @now - 3600*5 + @bat.timestamp = @now - 3600*7 + @foo.save + @bar.save + @baz.save + @bat.save + end + + it "should return node names that match 'equal' constraints" do + request = search_request('facts.fact1.eq' => 'value1', + 'facts.fact2.eq' => 'value2') + terminus.search(request).should == ["foo"] + end + + it "should return node names that match 'not equal' constraints" do + request = search_request('facts.fact1.ne' => 'value2') + terminus.search(request).should == ["bar","foo"] + end + + it "should return node names that match strict inequality constraints" do + request = search_request('facts.uptime_days.gt' => '20', + 'facts.uptime_days.lt' => '70') + terminus.search(request).should == ["bar","foo"] + end + + it "should return node names that match non-strict inequality constraints" do + request = search_request('facts.uptime_days.ge' => '30', + 'facts.uptime_days.le' => '60') + terminus.search(request).should == ["bar","foo"] + end + + it "should return node names whose facts are within a given timeframe" do + request = search_request('meta.timestamp.ge' => @now - 3600*5, + 'meta.timestamp.le' => @now - 3600*1) + terminus.search(request).should == ["bar","baz","foo"] + end + + it "should return node names whose facts are from a specific time" do + request = search_request('meta.timestamp.eq' => @now - 3600*3) + terminus.search(request).should == ["bar"] + end + + it "should return node names whose facts are not from a specific time" do + request = search_request('meta.timestamp.ne' => @now - 3600*1) + terminus.search(request).should == ["bar","bat","baz"] + end + + it "should perform strict searches on nodes by timestamp" do + request = search_request('meta.timestamp.gt' => @now - 3600*5, + 'meta.timestamp.lt' => @now - 3600*1) + terminus.search(request).should == ["bar"] + end + + it "should search nodes based on both facts and timestamp values" do + request = search_request('facts.uptime_days.gt' => '45', + 'meta.timestamp.lt' => @now - 3600*4) + terminus.search(request).should == ["baz"] + end + end +end + diff --git a/spec/unit/indirector/facts/yaml_spec.rb b/spec/unit/indirector/facts/yaml_spec.rb index e7bac3471..c625c8ee3 100755 --- a/spec/unit/indirector/facts/yaml_spec.rb +++ b/spec/unit/indirector/facts/yaml_spec.rb @@ -1,26 +1,240 @@ #!/usr/bin/env ruby require File.dirname(__FILE__) + '/../../../spec_helper' require 'puppet/node/facts' require 'puppet/indirector/facts/yaml' describe Puppet::Node::Facts::Yaml do it "should be a subclass of the Yaml terminus" do Puppet::Node::Facts::Yaml.superclass.should equal(Puppet::Indirector::Yaml) end - it "should have documentation" do Puppet::Node::Facts::Yaml.doc.should_not be_nil + Puppet::Node::Facts::Yaml.doc.should_not be_empty end it "should be registered with the facts indirection" do indirection = Puppet::Indirector::Indirection.instance(:facts) Puppet::Node::Facts::Yaml.indirection.should equal(indirection) end - it "should have its name set to :facts" do + it "should have its name set to :yaml" do Puppet::Node::Facts::Yaml.name.should == :yaml end + + describe "#search" do + def assert_search_matches(matching, nonmatching, query) + request = Puppet::Indirector::Request.new(:inventory, :search, nil, query) + + Dir.stubs(:glob).returns(matching.keys + nonmatching.keys) + [matching, nonmatching].each do |examples| + examples.each do |key, value| + YAML.stubs(:load_file).with(key).returns value + end + end + Puppet::Node::Facts::Yaml.new.search(request).should =~ matching.values.map {|facts| facts.name} + end + + it "should return node names that match the search query options" do + assert_search_matches({ + '/path/to/matching.yaml' => Puppet::Node::Facts.new("matchingnode", "architecture" => "i386", 'processor_count' => '4'), + '/path/to/matching1.yaml' => Puppet::Node::Facts.new("matchingnode1", "architecture" => "i386", 'processor_count' => '4', 'randomfact' => 'foo') + }, + { + "/path/to/nonmatching.yaml" => Puppet::Node::Facts.new("nonmatchingnode", "architecture" => "powerpc", 'processor_count' => '4'), + "/path/to/nonmatching1.yaml" => Puppet::Node::Facts.new("nonmatchingnode1", "architecture" => "powerpc", 'processor_count' => '5'), + "/path/to/nonmatching2.yaml" => Puppet::Node::Facts.new("nonmatchingnode2", "architecture" => "i386", 'processor_count' => '5'), + "/path/to/nonmatching3.yaml" => Puppet::Node::Facts.new("nonmatchingnode3", 'processor_count' => '4'), + }, + {'facts.architecture' => 'i386', 'facts.processor_count' => '4'} + ) + end + + it "should return empty array when no nodes match the search query options" do + assert_search_matches({}, { + "/path/to/nonmatching.yaml" => Puppet::Node::Facts.new("nonmatchingnode", "architecture" => "powerpc", 'processor_count' => '10'), + "/path/to/nonmatching1.yaml" => Puppet::Node::Facts.new("nonmatchingnode1", "architecture" => "powerpc", 'processor_count' => '5'), + "/path/to/nonmatching2.yaml" => Puppet::Node::Facts.new("nonmatchingnode2", "architecture" => "i386", 'processor_count' => '5'), + "/path/to/nonmatching3.yaml" => Puppet::Node::Facts.new("nonmatchingnode3", 'processor_count' => '4'), + }, + {'facts.processor_count.lt' => '4', 'facts.processor_count.gt' => '4'} + ) + end + + + it "should return node names that match the search query options with the greater than operator" do + assert_search_matches({ + '/path/to/matching.yaml' => Puppet::Node::Facts.new("matchingnode", "architecture" => "i386", 'processor_count' => '5'), + '/path/to/matching1.yaml' => Puppet::Node::Facts.new("matchingnode1", "architecture" => "powerpc", 'processor_count' => '10', 'randomfact' => 'foo') + }, + { + "/path/to/nonmatching.yaml" => Puppet::Node::Facts.new("nonmatchingnode", "architecture" => "powerpc", 'processor_count' => '4'), + "/path/to/nonmatching2.yaml" => Puppet::Node::Facts.new("nonmatchingnode2", "architecture" => "i386", 'processor_count' => '3'), + "/path/to/nonmatching3.yaml" => Puppet::Node::Facts.new("nonmatchingnode3" ), + }, + {'facts.processor_count.gt' => '4'} + ) + end + + it "should return node names that match the search query options with the less than operator" do + assert_search_matches({ + '/path/to/matching.yaml' => Puppet::Node::Facts.new("matchingnode", "architecture" => "i386", 'processor_count' => '5'), + '/path/to/matching1.yaml' => Puppet::Node::Facts.new("matchingnode1", "architecture" => "powerpc", 'processor_count' => '30', 'randomfact' => 'foo') + }, + { + "/path/to/nonmatching.yaml" => Puppet::Node::Facts.new("nonmatchingnode", "architecture" => "powerpc", 'processor_count' => '50' ), + "/path/to/nonmatching2.yaml" => Puppet::Node::Facts.new("nonmatchingnode2", "architecture" => "i386", 'processor_count' => '100'), + "/path/to/nonmatching3.yaml" => Puppet::Node::Facts.new("nonmatchingnode3" ), + }, + {'facts.processor_count.lt' => '50'} + ) + end + + it "should return node names that match the search query options with the less than or equal to operator" do + assert_search_matches({ + '/path/to/matching.yaml' => Puppet::Node::Facts.new("matchingnode", "architecture" => "i386", 'processor_count' => '5'), + '/path/to/matching1.yaml' => Puppet::Node::Facts.new("matchingnode1", "architecture" => "powerpc", 'processor_count' => '50', 'randomfact' => 'foo') + }, + { + "/path/to/nonmatching.yaml" => Puppet::Node::Facts.new("nonmatchingnode", "architecture" => "powerpc", 'processor_count' => '100' ), + "/path/to/nonmatching2.yaml" => Puppet::Node::Facts.new("nonmatchingnode2", "architecture" => "i386", 'processor_count' => '5000'), + "/path/to/nonmatching3.yaml" => Puppet::Node::Facts.new("nonmatchingnode3" ), + }, + {'facts.processor_count.le' => '50'} + ) + end + + it "should return node names that match the search query options with the greater than or equal to operator" do + assert_search_matches({ + '/path/to/matching.yaml' => Puppet::Node::Facts.new("matchingnode", "architecture" => "i386", 'processor_count' => '100'), + '/path/to/matching1.yaml' => Puppet::Node::Facts.new("matchingnode1", "architecture" => "powerpc", 'processor_count' => '50', 'randomfact' => 'foo') + }, + { + "/path/to/nonmatching.yaml" => Puppet::Node::Facts.new("nonmatchingnode", "architecture" => "powerpc", 'processor_count' => '40'), + "/path/to/nonmatching2.yaml" => Puppet::Node::Facts.new("nonmatchingnode2", "architecture" => "i386", 'processor_count' => '9' ), + "/path/to/nonmatching3.yaml" => Puppet::Node::Facts.new("nonmatchingnode3" ), + }, + {'facts.processor_count.ge' => '50'} + ) + end + + it "should return node names that match the search query options with the not equal operator" do + assert_search_matches({ + '/path/to/matching.yaml' => Puppet::Node::Facts.new("matchingnode", "architecture" => 'arm' ), + '/path/to/matching1.yaml' => Puppet::Node::Facts.new("matchingnode1", "architecture" => 'powerpc', 'randomfact' => 'foo') + }, + { + "/path/to/nonmatching.yaml" => Puppet::Node::Facts.new("nonmatchingnode", "architecture" => "i386" ), + "/path/to/nonmatching2.yaml" => Puppet::Node::Facts.new("nonmatchingnode2", "architecture" => "i386", 'processor_count' => '9' ), + "/path/to/nonmatching3.yaml" => Puppet::Node::Facts.new("nonmatchingnode3" ), + }, + {'facts.architecture.ne' => 'i386'} + ) + end + + def apply_timestamp(facts, timestamp) + facts.timestamp = timestamp + facts + end + + it "should be able to query based on meta.timestamp.gt" do + assert_search_matches({ + '/path/to/2010-11-01.yaml' => apply_timestamp(Puppet::Node::Facts.new("2010-11-01", {}), Time.parse("2010-11-01")), + '/path/to/2010-11-10.yaml' => apply_timestamp(Puppet::Node::Facts.new("2010-11-10", {}), Time.parse("2010-11-10")), + }, + { + '/path/to/2010-10-01.yaml' => apply_timestamp(Puppet::Node::Facts.new("2010-10-01", {}), Time.parse("2010-10-01")), + '/path/to/2010-10-10.yaml' => apply_timestamp(Puppet::Node::Facts.new("2010-10-10", {}), Time.parse("2010-10-10")), + '/path/to/2010-10-15.yaml' => apply_timestamp(Puppet::Node::Facts.new("2010-10-15", {}), Time.parse("2010-10-15")), + }, + {'meta.timestamp.gt' => '2010-10-15'} + ) + end + + it "should be able to query based on meta.timestamp.le" do + assert_search_matches({ + '/path/to/2010-10-01.yaml' => apply_timestamp(Puppet::Node::Facts.new("2010-10-01", {}), Time.parse("2010-10-01")), + '/path/to/2010-10-10.yaml' => apply_timestamp(Puppet::Node::Facts.new("2010-10-10", {}), Time.parse("2010-10-10")), + '/path/to/2010-10-15.yaml' => apply_timestamp(Puppet::Node::Facts.new("2010-10-15", {}), Time.parse("2010-10-15")), + }, + { + '/path/to/2010-11-01.yaml' => apply_timestamp(Puppet::Node::Facts.new("2010-11-01", {}), Time.parse("2010-11-01")), + '/path/to/2010-11-10.yaml' => apply_timestamp(Puppet::Node::Facts.new("2010-11-10", {}), Time.parse("2010-11-10")), + }, + {'meta.timestamp.le' => '2010-10-15'} + ) + end + + it "should be able to query based on meta.timestamp.lt" do + assert_search_matches({ + '/path/to/2010-10-01.yaml' => apply_timestamp(Puppet::Node::Facts.new("2010-10-01", {}), Time.parse("2010-10-01")), + '/path/to/2010-10-10.yaml' => apply_timestamp(Puppet::Node::Facts.new("2010-10-10", {}), Time.parse("2010-10-10")), + }, + { + '/path/to/2010-11-01.yaml' => apply_timestamp(Puppet::Node::Facts.new("2010-11-01", {}), Time.parse("2010-11-01")), + '/path/to/2010-11-10.yaml' => apply_timestamp(Puppet::Node::Facts.new("2010-11-10", {}), Time.parse("2010-11-10")), + '/path/to/2010-10-15.yaml' => apply_timestamp(Puppet::Node::Facts.new("2010-10-15", {}), Time.parse("2010-10-15")), + }, + {'meta.timestamp.lt' => '2010-10-15'} + ) + end + + it "should be able to query based on meta.timestamp.ge" do + assert_search_matches({ + '/path/to/2010-11-01.yaml' => apply_timestamp(Puppet::Node::Facts.new("2010-11-01", {}), Time.parse("2010-11-01")), + '/path/to/2010-11-10.yaml' => apply_timestamp(Puppet::Node::Facts.new("2010-11-10", {}), Time.parse("2010-11-10")), + '/path/to/2010-10-15.yaml' => apply_timestamp(Puppet::Node::Facts.new("2010-10-15", {}), Time.parse("2010-10-15")), + }, + { + '/path/to/2010-10-01.yaml' => apply_timestamp(Puppet::Node::Facts.new("2010-10-01", {}), Time.parse("2010-10-01")), + '/path/to/2010-10-10.yaml' => apply_timestamp(Puppet::Node::Facts.new("2010-10-10", {}), Time.parse("2010-10-10")), + }, + {'meta.timestamp.ge' => '2010-10-15'} + ) + end + + it "should be able to query based on meta.timestamp.eq" do + assert_search_matches({ + '/path/to/2010-10-15.yaml' => apply_timestamp(Puppet::Node::Facts.new("2010-10-15", {}), Time.parse("2010-10-15")), + }, + { + '/path/to/2010-11-01.yaml' => apply_timestamp(Puppet::Node::Facts.new("2010-11-01", {}), Time.parse("2010-11-01")), + '/path/to/2010-11-10.yaml' => apply_timestamp(Puppet::Node::Facts.new("2010-11-10", {}), Time.parse("2010-11-10")), + '/path/to/2010-10-01.yaml' => apply_timestamp(Puppet::Node::Facts.new("2010-10-01", {}), Time.parse("2010-10-01")), + '/path/to/2010-10-10.yaml' => apply_timestamp(Puppet::Node::Facts.new("2010-10-10", {}), Time.parse("2010-10-10")), + }, + {'meta.timestamp.eq' => '2010-10-15'} + ) + end + + it "should be able to query based on meta.timestamp" do + assert_search_matches({ + '/path/to/2010-10-15.yaml' => apply_timestamp(Puppet::Node::Facts.new("2010-10-15", {}), Time.parse("2010-10-15")), + }, + { + '/path/to/2010-11-01.yaml' => apply_timestamp(Puppet::Node::Facts.new("2010-11-01", {}), Time.parse("2010-11-01")), + '/path/to/2010-11-10.yaml' => apply_timestamp(Puppet::Node::Facts.new("2010-11-10", {}), Time.parse("2010-11-10")), + '/path/to/2010-10-01.yaml' => apply_timestamp(Puppet::Node::Facts.new("2010-10-01", {}), Time.parse("2010-10-01")), + '/path/to/2010-10-10.yaml' => apply_timestamp(Puppet::Node::Facts.new("2010-10-10", {}), Time.parse("2010-10-10")), + }, + {'meta.timestamp' => '2010-10-15'} + ) + end + + it "should be able to query based on meta.timestamp.ne" do + assert_search_matches({ + '/path/to/2010-11-01.yaml' => apply_timestamp(Puppet::Node::Facts.new("2010-11-01", {}), Time.parse("2010-11-01")), + '/path/to/2010-11-10.yaml' => apply_timestamp(Puppet::Node::Facts.new("2010-11-10", {}), Time.parse("2010-11-10")), + '/path/to/2010-10-01.yaml' => apply_timestamp(Puppet::Node::Facts.new("2010-10-01", {}), Time.parse("2010-10-01")), + '/path/to/2010-10-10.yaml' => apply_timestamp(Puppet::Node::Facts.new("2010-10-10", {}), Time.parse("2010-10-10")), + }, + { + '/path/to/2010-10-15.yaml' => apply_timestamp(Puppet::Node::Facts.new("2010-10-15", {}), Time.parse("2010-10-15")), + }, + {'meta.timestamp.ne' => '2010-10-15'} + ) + end + end end diff --git a/spec/unit/indirector/queue_spec.rb b/spec/unit/indirector/queue_spec.rb index 00463ee0f..bbe00c75f 100755 --- a/spec/unit/indirector/queue_spec.rb +++ b/spec/unit/indirector/queue_spec.rb @@ -1,121 +1,124 @@ #!/usr/bin/env ruby require File.dirname(__FILE__) + '/../../spec_helper' require 'puppet/indirector/queue' class Puppet::Indirector::Queue::TestClient end class FooExampleData attr_accessor :name def self.pson_create(pson) new(pson['data'].to_sym) end def initialize(name = nil) @name = name if name end def render(format = :pson) to_pson end def to_pson(*args) {:type => self.class.to_s, :data => name}.to_pson(*args) end end describe Puppet::Indirector::Queue, :if => Puppet.features.pson? do before :each do @model = mock 'model' @indirection = stub 'indirection', :name => :my_queue, :register_terminus_type => nil, :model => @model Puppet::Indirector::Indirection.stubs(:instance).with(:my_queue).returns(@indirection) @store_class = Class.new(Puppet::Indirector::Queue) do def self.to_s 'MyQueue::MyType' end end @store = @store_class.new @subject_class = FooExampleData @subject = @subject_class.new @subject.name = :me Puppet.settings.stubs(:value).returns("bogus setting data") Puppet.settings.stubs(:value).with(:queue_type).returns(:test_client) Puppet::Util::Queue.stubs(:queue_type_to_class).with(:test_client).returns(Puppet::Indirector::Queue::TestClient) @request = stub 'request', :key => :me, :instance => @subject end it "should require PSON" do Puppet.features.expects(:pson?).returns false lambda { @store_class.new }.should raise_error(ArgumentError) end it 'should use the correct client type and queue' do @store.queue.should == :my_queue @store.client.should be_an_instance_of(Puppet::Indirector::Queue::TestClient) end describe "when saving" do it 'should render the instance using pson' do @subject.expects(:render).with(:pson) @store.client.stubs(:send_message) @store.save(@request) end it "should send the rendered message to the appropriate queue on the client" do @subject.expects(:render).returns "mypson" @store.client.expects(:send_message).with(:my_queue, "mypson") @store.save(@request) end it "should catch any exceptions raised" do @store.client.expects(:send_message).raises ArgumentError lambda { @store.save(@request) }.should raise_error(Puppet::Error) end end describe "when subscribing to the queue" do before do @store_class.stubs(:model).returns @model end it "should use the model's Format support to intern the message from pson" do @model.expects(:convert_from).with(:pson, "mymessage") @store_class.client.expects(:subscribe).yields("mymessage") @store_class.subscribe {|o| o } end it "should yield each interned received message" do @model.stubs(:convert_from).returns "something" @subject_two = @subject_class.new @subject_two.name = :too @store_class.client.expects(:subscribe).with(:my_queue).multiple_yields(@subject, @subject_two) received = [] @store_class.subscribe do |obj| received.push(obj) end received.should == %w{something something} end it "should log but not propagate errors" do @store_class.client.expects(:subscribe).yields("foo") @store_class.expects(:intern).raises ArgumentError Puppet.expects(:err) + + @store_class.expects(:puts) + @store_class.subscribe {|o| o } end end end diff --git a/spec/unit/module_spec.rb b/spec/unit/module_spec.rb index 37dad7e25..0b4873f5f 100755 --- a/spec/unit/module_spec.rb +++ b/spec/unit/module_spec.rb @@ -1,545 +1,545 @@ #!/usr/bin/env ruby require File.dirname(__FILE__) + '/../spec_helper' describe Puppet::Module do before do # This is necessary because of the extra checks we have for the deprecated # 'plugins' directory FileTest.stubs(:exist?).returns false end it "should have a class method that returns a named module from a given environment" do env = mock 'module' env.expects(:module).with("mymod").returns "yep" Puppet::Node::Environment.expects(:new).with("myenv").returns env Puppet::Module.find("mymod", "myenv").should == "yep" end it "should return nil if asked for a named module that doesn't exist" do env = mock 'module' env.expects(:module).with("mymod").returns nil Puppet::Node::Environment.expects(:new).with("myenv").returns env Puppet::Module.find("mymod", "myenv").should be_nil end it "should support a 'version' attribute" do mod = Puppet::Module.new("mymod") mod.version = 1.09 mod.version.should == 1.09 end it "should support a 'source' attribute" do mod = Puppet::Module.new("mymod") mod.source = "http://foo/bar" mod.source.should == "http://foo/bar" end it "should support a 'project_page' attribute" do mod = Puppet::Module.new("mymod") mod.project_page = "http://foo/bar" mod.project_page.should == "http://foo/bar" end it "should support an 'author' attribute" do mod = Puppet::Module.new("mymod") mod.author = "Luke Kanies " mod.author.should == "Luke Kanies " end it "should support a 'license' attribute" do mod = Puppet::Module.new("mymod") mod.license = "GPL2" mod.license.should == "GPL2" end it "should support a 'summary' attribute" do mod = Puppet::Module.new("mymod") mod.summary = "GPL2" mod.summary.should == "GPL2" end it "should support a 'description' attribute" do mod = Puppet::Module.new("mymod") mod.description = "GPL2" mod.description.should == "GPL2" end it "should support specifying a compatible puppet version" do mod = Puppet::Module.new("mymod") mod.puppetversion = "0.25" mod.puppetversion.should == "0.25" end it "should validate that the puppet version is compatible" do mod = Puppet::Module.new("mymod") mod.puppetversion = "0.25" Puppet.expects(:version).returns "0.25" mod.validate_puppet_version end it "should fail if the specified puppet version is not compatible" do mod = Puppet::Module.new("mymod") mod.puppetversion = "0.25" Puppet.stubs(:version).returns "0.24" lambda { mod.validate_puppet_version }.should raise_error(Puppet::Module::IncompatibleModule) end describe "when specifying required modules" do it "should support specifying a required module" do mod = Puppet::Module.new("mymod") mod.requires "foobar" end it "should support specifying multiple required modules" do mod = Puppet::Module.new("mymod") mod.requires "foobar" mod.requires "baz" end it "should support specifying a required module and version" do mod = Puppet::Module.new("mymod") mod.requires "foobar", 1.0 end it "should fail when required modules are missing" do mod = Puppet::Module.new("mymod") mod.requires "foobar" mod.environment.expects(:module).with("foobar").returns nil lambda { mod.validate_dependencies }.should raise_error(Puppet::Module::MissingModule) end it "should fail when required modules are present but of the wrong version" do mod = Puppet::Module.new("mymod") mod.requires "foobar", 1.0 foobar = Puppet::Module.new("foobar") foobar.version = 2.0 mod.environment.expects(:module).with("foobar").returns foobar lambda { mod.validate_dependencies }.should raise_error(Puppet::Module::IncompatibleModule) end it "should have valid dependencies when no dependencies have been specified" do mod = Puppet::Module.new("mymod") lambda { mod.validate_dependencies }.should_not raise_error end it "should fail when some dependencies are present but others aren't" do mod = Puppet::Module.new("mymod") mod.requires "foobar" mod.requires "baz" mod.environment.expects(:module).with("foobar").returns Puppet::Module.new("foobar") mod.environment.expects(:module).with("baz").returns nil lambda { mod.validate_dependencies }.should raise_error(Puppet::Module::MissingModule) end it "should have valid dependencies when all dependencies are met" do mod = Puppet::Module.new("mymod") mod.requires "foobar", 1.0 mod.requires "baz" foobar = Puppet::Module.new("foobar") foobar.version = 1.0 baz = Puppet::Module.new("baz") mod.environment.expects(:module).with("foobar").returns foobar mod.environment.expects(:module).with("baz").returns baz lambda { mod.validate_dependencies }.should_not raise_error end it "should validate its dependendencies on initialization" do Puppet::Module.any_instance.expects(:validate_dependencies) Puppet::Module.new("mymod") end end describe "when managing supported platforms" do it "should support specifying a supported platform" do mod = Puppet::Module.new("mymod") mod.supports "solaris" end it "should support specifying a supported platform and version" do mod = Puppet::Module.new("mymod") mod.supports "solaris", 1.0 end it "should fail when not running on a supported platform" do pending "Not sure how to send client platform to the module" mod = Puppet::Module.new("mymod") Facter.expects(:value).with("operatingsystem").returns "Solaris" mod.supports "hpux" lambda { mod.validate_supported_platform }.should raise_error(Puppet::Module::UnsupportedPlatform) end it "should fail when supported platforms are present but of the wrong version" do pending "Not sure how to send client platform to the module" mod = Puppet::Module.new("mymod") Facter.expects(:value).with("operatingsystem").returns "Solaris" Facter.expects(:value).with("operatingsystemrelease").returns 2.0 mod.supports "Solaris", 1.0 lambda { mod.validate_supported_platform }.should raise_error(Puppet::Module::IncompatiblePlatform) end it "should be considered supported when no supported platforms have been specified" do pending "Not sure how to send client platform to the module" mod = Puppet::Module.new("mymod") lambda { mod.validate_supported_platform }.should_not raise_error end it "should be considered supported when running on a supported platform" do pending "Not sure how to send client platform to the module" mod = Puppet::Module.new("mymod") Facter.expects(:value).with("operatingsystem").returns "Solaris" Facter.expects(:value).with("operatingsystemrelease").returns 2.0 mod.supports "Solaris", 1.0 lambda { mod.validate_supported_platform }.should raise_error(Puppet::Module::IncompatiblePlatform) end it "should be considered supported when running on any of multiple supported platforms" do pending "Not sure how to send client platform to the module" end it "should validate its platform support on initialization" do pending "Not sure how to send client platform to the module" end end it "should return nil if asked for a module whose name is 'nil'" do Puppet::Module.find(nil, "myenv").should be_nil end it "should provide support for logging" do Puppet::Module.ancestors.should be_include(Puppet::Util::Logging) end it "should be able to be converted to a string" do Puppet::Module.new("foo").to_s.should == "Module foo" end it "should add the path to its string form if the module is found" do mod = Puppet::Module.new("foo") mod.stubs(:path).returns "/a" mod.to_s.should == "Module foo(/a)" end it "should fail if its name is not alphanumeric" do lambda { Puppet::Module.new(".something") }.should raise_error(Puppet::Module::InvalidName) end it "should require a name at initialization" do lambda { Puppet::Module.new }.should raise_error(ArgumentError) end it "should convert an environment name into an Environment instance" do Puppet::Module.new("foo", "prod").environment.should be_instance_of(Puppet::Node::Environment) end it "should accept an environment at initialization" do Puppet::Module.new("foo", :prod).environment.name.should == :prod end it "should use the default environment if none is provided" do env = Puppet::Node::Environment.new Puppet::Module.new("foo").environment.should equal(env) end it "should use any provided Environment instance" do env = Puppet::Node::Environment.new Puppet::Module.new("foo", env).environment.should equal(env) end it "should return the path to the first found instance in its environment's module paths as its path" do mod = Puppet::Module.new("foo") env = mock 'environment' mod.stubs(:environment).returns env env.expects(:modulepath).returns %w{/a /b /c} FileTest.expects(:exist?).with("/a/foo").returns false FileTest.expects(:exist?).with("/b/foo").returns true FileTest.expects(:exist?).with("/c/foo").never mod.path.should == "/b/foo" end it "should be considered existent if it exists in at least one module path" do mod = Puppet::Module.new("foo") mod.expects(:path).returns "/a/foo" mod.should be_exist end it "should be considered nonexistent if it does not exist in any of the module paths" do mod = Puppet::Module.new("foo") mod.expects(:path).returns nil mod.should_not be_exist end [:plugins, :templates, :files, :manifests].each do |filetype| dirname = filetype == :plugins ? "lib" : filetype.to_s it "should be able to return individual #{filetype}" do mod = Puppet::Module.new("foo") mod.stubs(:path).returns "/a/foo" path = File.join("/a/foo", dirname, "my/file") FileTest.expects(:exist?).with(path).returns true mod.send(filetype.to_s.sub(/s$/, ''), "my/file").should == path end it "should consider #{filetype} to be present if their base directory exists" do mod = Puppet::Module.new("foo") mod.stubs(:path).returns "/a/foo" path = File.join("/a/foo", dirname) FileTest.expects(:exist?).with(path).returns true mod.send(filetype.to_s + "?").should be_true end it "should consider #{filetype} to be absent if their base directory does not exist" do mod = Puppet::Module.new("foo") mod.stubs(:path).returns "/a/foo" path = File.join("/a/foo", dirname) FileTest.expects(:exist?).with(path).returns false mod.send(filetype.to_s + "?").should be_false end it "should consider #{filetype} to be absent if the module base directory does not exist" do mod = Puppet::Module.new("foo") mod.stubs(:path).returns nil mod.send(filetype.to_s + "?").should be_false end it "should return nil if asked to return individual #{filetype} that don't exist" do mod = Puppet::Module.new("foo") mod.stubs(:path).returns "/a/foo" path = File.join("/a/foo", dirname, "my/file") FileTest.expects(:exist?).with(path).returns false mod.send(filetype.to_s.sub(/s$/, ''), "my/file").should be_nil end it "should return nil when asked for individual #{filetype} if the module does not exist" do mod = Puppet::Module.new("foo") mod.stubs(:path).returns nil mod.send(filetype.to_s.sub(/s$/, ''), "my/file").should be_nil end it "should return the base directory if asked for a nil path" do mod = Puppet::Module.new("foo") mod.stubs(:path).returns "/a/foo" base = File.join("/a/foo", dirname) FileTest.expects(:exist?).with(base).returns true mod.send(filetype.to_s.sub(/s$/, ''), nil).should == base end end %w{plugins files}.each do |filetype| short = filetype.sub(/s$/, '') dirname = filetype == "plugins" ? "lib" : filetype.to_s it "should be able to return the #{short} directory" do Puppet::Module.new("foo").should respond_to(short + "_directory") end it "should return the path to the #{short} directory" do mod = Puppet::Module.new("foo") mod.stubs(:path).returns "/a/foo" mod.send(short + "_directory").should == "/a/foo/#{dirname}" end end it "should throw a warning if plugins are in a 'plugins' directory rather than a 'lib' directory" do mod = Puppet::Module.new("foo") mod.stubs(:path).returns "/a/foo" FileTest.expects(:exist?).with("/a/foo/plugins").returns true - mod.expects(:warning) - mod.plugin_directory.should == "/a/foo/plugins" + @logs.first.message.should == "using the deprecated 'plugins' directory for ruby extensions; please move to 'lib'" + @logs.first.level.should == :warning end it "should default to 'lib' for the plugins directory" do mod = Puppet::Module.new("foo") mod.stubs(:path).returns "/a/foo" mod.plugin_directory.should == "/a/foo/lib" end end describe Puppet::Module, " when building its search path" do it "should use the current environment's search path if no environment is specified" do env = mock 'env' env.expects(:modulepath).returns "eh" Puppet::Node::Environment.expects(:new).with(nil).returns env Puppet::Module.modulepath.should == "eh" end it "should use the specified environment's search path if an environment is specified" do env = mock 'env' env.expects(:modulepath).returns "eh" Puppet::Node::Environment.expects(:new).with("foo").returns env Puppet::Module.modulepath("foo").should == "eh" end end describe Puppet::Module, "when finding matching manifests" do before do @mod = Puppet::Module.new("mymod") @mod.stubs(:path).returns "/a" @pq_glob_with_extension = "yay/*.xx" @fq_glob_with_extension = "/a/manifests/#{@pq_glob_with_extension}" end it "should return all manifests matching the glob pattern" do Dir.expects(:glob).with(@fq_glob_with_extension).returns(%w{foo bar}) FileTest.stubs(:directory?).returns false @mod.match_manifests(@pq_glob_with_extension).should == %w{foo bar} end it "should not return directories" do Dir.expects(:glob).with(@fq_glob_with_extension).returns(%w{foo bar}) FileTest.expects(:directory?).with("foo").returns false FileTest.expects(:directory?).with("bar").returns true @mod.match_manifests(@pq_glob_with_extension).should == %w{foo} end it "should default to the 'init' file if no glob pattern is specified" do Dir.expects(:glob).with("/a/manifests/init.{pp,rb}").returns(%w{/a/manifests/init.pp}) @mod.match_manifests(nil).should == %w{/a/manifests/init.pp} end it "should return all manifests matching the glob pattern in all existing paths" do Dir.expects(:glob).with(@fq_glob_with_extension).returns(%w{a b}) @mod.match_manifests(@pq_glob_with_extension).should == %w{a b} end it "should match the glob pattern plus '.{pp,rb}' if no extention is specified" do Dir.expects(:glob).with("/a/manifests/yay/foo.{pp,rb}").returns(%w{yay}) @mod.match_manifests("yay/foo").should == %w{yay} end it "should return an empty array if no manifests matched" do Dir.expects(:glob).with(@fq_glob_with_extension).returns([]) @mod.match_manifests(@pq_glob_with_extension).should == [] end end describe Puppet::Module do before do Puppet::Module.any_instance.stubs(:path).returns "/my/mod/path" @module = Puppet::Module.new("foo") end it "should use 'License' in its current path as its metadata file" do @module.license_file.should == "/my/mod/path/License" end it "should return nil as its license file when the module has no path" do Puppet::Module.any_instance.stubs(:path).returns nil Puppet::Module.new("foo").license_file.should be_nil end it "should cache the license file" do Puppet::Module.any_instance.expects(:path).once.returns nil mod = Puppet::Module.new("foo") mod.license_file.should == mod.license_file end it "should use 'metadata.json' in its current path as its metadata file" do @module.metadata_file.should == "/my/mod/path/metadata.json" end it "should return nil as its metadata file when the module has no path" do Puppet::Module.any_instance.stubs(:path).returns nil Puppet::Module.new("foo").metadata_file.should be_nil end it "should cache the metadata file" do Puppet::Module.any_instance.expects(:path).once.returns nil mod = Puppet::Module.new("foo") mod.metadata_file.should == mod.metadata_file end it "should know if it has a metadata file" do FileTest.expects(:exist?).with(@module.metadata_file).returns true @module.should be_has_metadata end it "should know if it is missing a metadata file" do FileTest.expects(:exist?).with(@module.metadata_file).returns false @module.should_not be_has_metadata end it "should be able to parse its metadata file" do @module.should respond_to(:load_metadata) end it "should parse its metadata file on initialization if it is present" do Puppet::Module.any_instance.expects(:has_metadata?).returns true Puppet::Module.any_instance.expects(:load_metadata) Puppet::Module.new("yay") end describe "when loading the medatada file", :if => Puppet.features.json? do before do @data = { :license => "GPL2", :author => "luke", :version => "1.0", :source => "http://foo/", :puppetversion => "0.25" } @text = @data.to_json @module = Puppet::Module.new("foo") @module.stubs(:metadata_file).returns "/my/file" File.stubs(:read).with("/my/file").returns @text end %w{source author version license}.each do |attr| it "should set #{attr} if present in the metadata file" do @module.load_metadata @module.send(attr).should == @data[attr.to_sym] end it "should fail if #{attr} is not present in the metadata file" do @data.delete(attr.to_sym) @text = @data.to_json File.stubs(:read).with("/my/file").returns @text lambda { @module.load_metadata }.should raise_error(Puppet::Module::MissingMetadata) end end it "should set puppetversion if present in the metadata file" do @module.load_metadata @module.puppetversion.should == @data[:puppetversion] end it "should fail if the discovered name is different than the metadata name" end end diff --git a/spec/unit/network/handler/fileserver_spec.rb b/spec/unit/network/handler/fileserver_spec.rb index b37d4f551..9d34e9cdd 100644 --- a/spec/unit/network/handler/fileserver_spec.rb +++ b/spec/unit/network/handler/fileserver_spec.rb @@ -1,170 +1,170 @@ #!/usr/bin/env ruby require File.dirname(__FILE__) + '/../../../spec_helper' require 'puppet/network/handler/fileserver' describe Puppet::Network::Handler::FileServer do include PuppetSpec::Files def create_file(filename) File.open(filename, "w") { |f| f.puts filename} end def create_nested_file dirname = File.join(@basedir, "nested_dir") Dir.mkdir(dirname) file = File.join(dirname, "nested_dir_file") create_file(file) end before do @basedir = tmpdir("test_network_handler") @file = File.join(@basedir, "aFile") @link = File.join(@basedir, "aLink") create_file(@file) @mount = Puppet::Network::Handler::FileServer::Mount.new("some_path", @basedir) end it "should list a single directory" do @mount.list("/", false, false).should == [["/", "directory"]] end it "should list a file within a directory when given the file path" do @mount.list("/aFile", false, "false").should == [["/", "file"]] end it "should list a file within a directory when given the file path with recursion" do @mount.list("/aFile", true, "false").should == [["/", "file"]] end it "should return nil for a non-existent path" do @mount.list("/no_such_file", false, false).should be(nil) end it "should list a symbolic link as a file when given the link path" do File.symlink(@file, @link) @mount.list("/aLink", false, false).should == [["/", "file"]] end it "should return nil for a dangling symbolic link when given the link path" do File.symlink("/some/where", @link) @mount.list("/aLink", false, false).should be(nil) end it "should list directory contents of a flat directory structure when asked to recurse" do list = @mount.list("/", true, false) list.should include(["/aFile", "file"]) list.should include(["/", "directory"]) list.should have(2).items end it "should list the contents of a nested directory" do create_nested_file list = @mount.list("/", true, false) list.sort.should == [ ["/aFile", "file"], ["/", "directory"] , ["/nested_dir", "directory"], ["/nested_dir/nested_dir_file", "file"]].sort end it "should list the contents of a directory ignoring files that match" do create_nested_file list = @mount.list("/", true, "*File") list.sort.should == [ ["/", "directory"] , ["/nested_dir", "directory"], ["/nested_dir/nested_dir_file", "file"]].sort end it "should list the contents of a directory ignoring directories that match" do create_nested_file list = @mount.list("/", true, "*nested_dir") list.sort.should == [ ["/aFile", "file"], ["/", "directory"] ].sort end it "should list the contents of a directory ignoring all ignore patterns that match" do create_nested_file list = @mount.list("/", true, ["*File" , "*nested_dir"]) list.should == [ ["/", "directory"] ] end it "should list the directory when recursing to a depth of zero" do create_nested_file list = @mount.list("/", 0, false) list.should == [["/", "directory"]] end it "should list the base directory and files and nested directory to a depth of one" do create_nested_file list = @mount.list("/", 1, false) list.sort.should == [ ["/aFile", "file"], ["/nested_dir", "directory"], ["/", "directory"] ].sort end it "should list the base directory and files and nested directory to a depth of two" do create_nested_file list = @mount.list("/", 2, false) list.sort.should == [ ["/aFile", "file"], ["/", "directory"] , ["/nested_dir", "directory"], ["/nested_dir/nested_dir_file", "file"]].sort end it "should list the base directory and files and nested directory to a depth greater than the directory structure" do create_nested_file list = @mount.list("/", 42, false) list.sort.should == [ ["/aFile", "file"], ["/", "directory"] , ["/nested_dir", "directory"], ["/nested_dir/nested_dir_file", "file"]].sort end it "should list a valid symbolic link as a file when recursing base dir" do File.symlink(@file, @link) list = @mount.list("/", true, false) list.sort.should == [ ["/", "directory"], ["/aFile", "file"], ["/aLink", "file"] ].sort end it "should not error when a dangling symlink is present" do File.symlink("/some/where", @link) lambda { @mount.list("/", true, false) }.should_not raise_error end it "should return the directory contents of valid entries when a dangling symlink is present" do File.symlink("/some/where", @link) list = @mount.list("/", true, false) list.sort.should == [ ["/aFile", "file"], ["/", "directory"] ].sort end describe Puppet::Network::Handler::FileServer::PluginMount do PLUGINS = Puppet::Network::Handler::FileServer::PLUGINS # create a module plugin hierarchy def create_plugin(mod, plugin) dirname = File.join(@basedir, mod) Dir.mkdir(dirname) plugins = File.join(dirname, PLUGINS) Dir.mkdir(plugins) facter = File.join(plugins, plugin) Dir.mkdir(facter) create_file(File.join(facter,"fact.rb")) end before :each do @modules = ["one","two"] @modules.each { |m| create_plugin(m, "facter") } Puppet::Node::Environment.new.stubs(:modulepath).returns @basedir @mount = Puppet::Network::Handler::FileServer::PluginMount.new(PLUGINS) @mount.allow("*") end it "should list a file within a directory when given the file path with recursion" do @mount.list("facter/fact.rb", true, "false").should == [["/", "file"], ["/", "file"]] end it "should return a merged view of all plugins for all modules" do list = @mount.list("facter",true,false) list.should == [["/", "directory"], ["/fact.rb", "file"], ["/", "directory"], ["/fact.rb", "file"]] end it "should not fail for inexistant plugins type" do - lambda { @mount.list("puppet/parser",true,false) }.should_not raise_error + @mount.list("puppet/parser",true,false) end end after do FileUtils.rm_rf(@basedir) end end diff --git a/spec/unit/network/http/api/v1_spec.rb b/spec/unit/network/http/api/v1_spec.rb index 23a291cf3..9a8780c62 100644 --- a/spec/unit/network/http/api/v1_spec.rb +++ b/spec/unit/network/http/api/v1_spec.rb @@ -1,126 +1,154 @@ #!/usr/bin/env ruby Dir.chdir(File.dirname(__FILE__)) { (s = lambda { |f| File.exist?(f) ? require(f) : Dir.chdir("..") { s.call(f) } }).call("spec/spec_helper.rb") } require 'puppet/network/http/api/v1' class V1RestApiTester include Puppet::Network::HTTP::API::V1 end describe Puppet::Network::HTTP::API::V1 do before do @tester = V1RestApiTester.new end it "should be able to convert a URI into a request" do @tester.should respond_to(:uri2indirection) end it "should be able to convert a request into a URI" do @tester.should respond_to(:indirection2uri) end describe "when converting a URI into a request" do before do @tester.stubs(:handler).returns "foo" end it "should require the http method, the URI, and the query parameters" do # Not a terribly useful test, but an important statement for the spec lambda { @tester.uri2indirection("/foo") }.should raise_error(ArgumentError) end it "should use the first field of the URI as the environment" do @tester.uri2indirection("GET", "/env/foo/bar", {}).environment.should == Puppet::Node::Environment.new("env") end it "should fail if the environment is not alphanumeric" do lambda { @tester.uri2indirection("GET", "/env ness/foo/bar", {}) }.should raise_error(ArgumentError) end it "should use the environment from the URI even if one is specified in the parameters" do @tester.uri2indirection("GET", "/env/foo/bar", {:environment => "otherenv"}).environment.should == Puppet::Node::Environment.new("env") end it "should use the second field of the URI as the indirection name" do @tester.uri2indirection("GET", "/env/foo/bar", {}).indirection_name.should == :foo end it "should fail if the indirection name is not alphanumeric" do lambda { @tester.uri2indirection("GET", "/env/foo ness/bar", {}) }.should raise_error(ArgumentError) end it "should use the remainder of the URI as the indirection key" do @tester.uri2indirection("GET", "/env/foo/bar", {}).key.should == "bar" end it "should support the indirection key being a /-separated file path" do @tester.uri2indirection("GET", "/env/foo/bee/baz/bomb", {}).key.should == "bee/baz/bomb" end it "should fail if no indirection key is specified" do lambda { @tester.uri2indirection("GET", "/env/foo/", {}) }.should raise_error(ArgumentError) lambda { @tester.uri2indirection("GET", "/env/foo", {}) }.should raise_error(ArgumentError) end it "should choose 'find' as the indirection method if the http method is a GET and the indirection name is singular" do @tester.uri2indirection("GET", "/env/foo/bar", {}).method.should == :find end it "should choose 'head' as the indirection method if the http method is a HEAD and the indirection name is singular" do @tester.uri2indirection("HEAD", "/env/foo/bar", {}).method.should == :head end it "should choose 'search' as the indirection method if the http method is a GET and the indirection name is plural" do @tester.uri2indirection("GET", "/env/foos/bar", {}).method.should == :search end + it "should choose 'find' as the indirection method if the http method is a GET and the indirection name is facts" do + @tester.uri2indirection("GET", "/env/facts/bar", {}).method.should == :find + end + + it "should choose 'save' as the indirection method if the http method is a PUT and the indirection name is facts" do + @tester.uri2indirection("PUT", "/env/facts/bar", {}).method.should == :save + end + + it "should choose 'search' as the indirection method if the http method is a GET and the indirection name is inventory" do + @tester.uri2indirection("GET", "/env/inventory/search", {}).method.should == :search + end + + it "should choose 'search' as the indirection method if the http method is a GET and the indirection name is facts_search" do + @tester.uri2indirection("GET", "/env/facts_search/bar", {}).method.should == :search + end + + it "should change indirection name to 'facts' if the http method is a GET and the indirection name is facts_search" do + @tester.uri2indirection("GET", "/env/facts_search/bar", {}).indirection_name.should == :facts + end + + it "should not change indirection name from 'facts' if the http method is a GET and the indirection name is facts" do + @tester.uri2indirection("GET", "/env/facts/bar", {}).indirection_name.should == :facts + end + + it "should change indirection name to 'status' if the http method is a GET and the indirection name is statuses" do + @tester.uri2indirection("GET", "/env/statuses/bar", {}).indirection_name.should == :status + end + it "should choose 'delete' as the indirection method if the http method is a DELETE and the indirection name is singular" do @tester.uri2indirection("DELETE", "/env/foo/bar", {}).method.should == :destroy end it "should choose 'save' as the indirection method if the http method is a PUT and the indirection name is singular" do @tester.uri2indirection("PUT", "/env/foo/bar", {}).method.should == :save end it "should fail if an indirection method cannot be picked" do lambda { @tester.uri2indirection("UPDATE", "/env/foo/bar", {}) }.should raise_error(ArgumentError) end it "should URI unescape the indirection key" do escaped = URI.escape("foo bar") @tester.uri2indirection("GET", "/env/foo/#{escaped}", {}).key.should == "foo bar" end end describe "when converting a request into a URI" do before do @request = Puppet::Indirector::Request.new(:foo, :find, "with spaces", :foo => :bar, :environment => "myenv") end it "should use the environment as the first field of the URI" do @tester.indirection2uri(@request).split("/")[1].should == "myenv" end it "should use the indirection as the second field of the URI" do @tester.indirection2uri(@request).split("/")[2].should == "foo" end it "should pluralize the indirection name if the method is 'search'" do @request.stubs(:method).returns :search @tester.indirection2uri(@request).split("/")[2].should == "foos" end it "should use the escaped key as the remainder of the URI" do escaped = URI.escape("with spaces") @tester.indirection2uri(@request).split("/")[3].sub(/\?.+/, '').should == escaped end it "should add the query string to the URI" do @request.expects(:query_string).returns "?query" @tester.indirection2uri(@request).should =~ /\?query$/ end end end diff --git a/spec/unit/network/http/compression_spec.rb b/spec/unit/network/http/compression_spec.rb old mode 100644 new mode 100755 index c5bbbb064..3828ec59c --- a/spec/unit/network/http/compression_spec.rb +++ b/spec/unit/network/http/compression_spec.rb @@ -1,197 +1,197 @@ #!/usr/bin/env ruby require File.dirname(__FILE__) + '/../../../spec_helper' describe "http compression" do describe "when zlib is not available" do before(:each) do Puppet.features.stubs(:zlib?).returns false require 'puppet/network/http/compression' class HttpUncompressor include Puppet::Network::HTTP::Compression::None end @uncompressor = HttpUncompressor.new end it "should have a module function that returns the None underlying module" do Puppet::Network::HTTP::Compression.module.should == Puppet::Network::HTTP::Compression::None end it "should not add any Accept-Encoding header" do @uncompressor.add_accept_encoding({}).should == {} end it "should not tamper the body" do response = stub 'response', :body => "data" @uncompressor.uncompress_body(response).should == "data" end it "should yield an identity uncompressor" do response = stub 'response' @uncompressor.uncompress(response) { |u| u.should be_instance_of(Puppet::Network::HTTP::Compression::IdentityAdapter) } end end describe "when zlib is available", :if => Puppet.features.zlib? do before(:each) do Puppet.features.stubs(:zlib?).returns true require 'puppet/network/http/compression' class HttpUncompressor include Puppet::Network::HTTP::Compression::Active end @uncompressor = HttpUncompressor.new end it "should have a module function that returns the Active underlying module" do Puppet::Network::HTTP::Compression.module.should == Puppet::Network::HTTP::Compression::Active end it "should add an Accept-Encoding header when http compression is available" do Puppet.settings.expects(:[]).with(:http_compression).returns(true) headers = @uncompressor.add_accept_encoding({}) headers.should have_key('accept-encoding') headers['accept-encoding'].should =~ /gzip/ headers['accept-encoding'].should =~ /deflate/ headers['accept-encoding'].should =~ /identity/ end it "should not add Accept-Encoding header if http compression is not available" do Puppet.settings.stubs(:[]).with(:http_compression).returns(false) @uncompressor.add_accept_encoding({}).should == {} end describe "when uncompressing response body" do before do @response = stub 'response' @response.stubs(:[]).with('content-encoding') @response.stubs(:body).returns("mydata") end it "should return untransformed response body with no content-encoding" do @uncompressor.uncompress_body(@response).should == "mydata" end it "should return untransformed response body with 'identity' content-encoding" do @response.stubs(:[]).with('content-encoding').returns('identity') @uncompressor.uncompress_body(@response).should == "mydata" end it "should use a Zlib inflater with 'deflate' content-encoding" do @response.stubs(:[]).with('content-encoding').returns('deflate') inflater = stub 'inflater' Zlib::Inflate.expects(:new).returns(inflater) inflater.expects(:inflate).with("mydata").returns "uncompresseddata" @uncompressor.uncompress_body(@response).should == "uncompresseddata" end it "should use a GzipReader with 'gzip' content-encoding" do @response.stubs(:[]).with('content-encoding').returns('gzip') io = stub 'io' StringIO.expects(:new).with("mydata").returns io reader = stub 'gzip reader' Zlib::GzipReader.expects(:new).with(io).returns(reader) reader.expects(:read).returns "uncompresseddata" @uncompressor.uncompress_body(@response).should == "uncompresseddata" end end describe "when uncompressing by chunk" do before do @response = stub 'response' @response.stubs(:[]).with('content-encoding') @inflater = stub_everything 'inflater' Zlib::Inflate.stubs(:new).returns(@inflater) end it "should yield an identity uncompressor with no content-encoding" do @uncompressor.uncompress(@response) { |u| u.should be_instance_of(Puppet::Network::HTTP::Compression::IdentityAdapter) } end it "should yield an identity uncompressor with 'identity' content-encoding" do @response.stubs(:[]).with('content-encoding').returns 'identity' @uncompressor.uncompress(@response) { |u| u.should be_instance_of(Puppet::Network::HTTP::Compression::IdentityAdapter) } end %w{gzip deflate}.each do |c| it "should yield a Zlib uncompressor with '#{c}' content-encoding" do @response.stubs(:[]).with('content-encoding').returns c @uncompressor.uncompress(@response) { |u| u.should be_instance_of(Puppet::Network::HTTP::Compression::Active::ZlibAdapter) } end end it "should close the underlying adapter" do adapter = stub_everything 'adapter' Puppet::Network::HTTP::Compression::IdentityAdapter.expects(:new).returns(adapter) adapter.expects(:close) @uncompressor.uncompress(@response) { |u| } end end describe "zlib adapter" do before do @inflater = stub_everything 'inflater' Zlib::Inflate.stubs(:new).returns(@inflater) @adapter = Puppet::Network::HTTP::Compression::Active::ZlibAdapter.new end it "should initialize the underlying inflater with gzip/zlib header parsing" do Zlib::Inflate.expects(:new).with(15+32) Puppet::Network::HTTP::Compression::Active::ZlibAdapter.new end it "should inflate the given chunk" do @inflater.expects(:inflate).with("chunk") @adapter.uncompress("chunk") end it "should return the inflated chunk" do @inflater.stubs(:inflate).with("chunk").returns("uncompressed") @adapter.uncompress("chunk").should == "uncompressed" end it "should try a 'regular' inflater on Zlib::DataError" do @inflater.expects(:inflate).raises(Zlib::DataError.new("not a zlib stream")) inflater = stub_everything 'inflater2' inflater.expects(:inflate).with("chunk").returns("uncompressed") Zlib::Inflate.expects(:new).with.returns(inflater) @adapter.uncompress("chunk") end it "should raise the error the second time" do - @inflater.expects(:inflate).raises(Zlib::DataError.new("not a zlib stream")) + @inflater.stubs(:inflate).raises(Zlib::DataError.new("not a zlib stream")) Zlib::Inflate.expects(:new).with.returns(@inflater) lambda { @adapter.uncompress("chunk") }.should raise_error end it "should finish the stream on close" do @inflater.expects(:finish) @adapter.close end it "should close the stream on close" do @inflater.expects(:close) @adapter.close end end end end diff --git a/spec/unit/network/http/handler_spec.rb b/spec/unit/network/http/handler_spec.rb index 8464ae68e..68c7b9aa3 100755 --- a/spec/unit/network/http/handler_spec.rb +++ b/spec/unit/network/http/handler_spec.rb @@ -1,494 +1,497 @@ #!/usr/bin/env ruby require File.dirname(__FILE__) + '/../../../spec_helper' require 'puppet/network/http/handler' require 'puppet/network/rest_authorization' class HttpHandled include Puppet::Network::HTTP::Handler end describe Puppet::Network::HTTP::Handler do before do @handler = HttpHandled.new end it "should include the v1 REST API" do Puppet::Network::HTTP::Handler.ancestors.should be_include(Puppet::Network::HTTP::API::V1) end it "should include the Rest Authorization system" do Puppet::Network::HTTP::Handler.ancestors.should be_include(Puppet::Network::RestAuthorization) end it "should have a method for initializing" do @handler.should respond_to(:initialize_for_puppet) end describe "when initializing" do it "should fail when no server type has been provided" do lambda { @handler.initialize_for_puppet }.should raise_error(ArgumentError) end it "should set server type" do @handler.initialize_for_puppet("foo") @handler.server.should == "foo" end end it "should be able to process requests" do @handler.should respond_to(:process) end describe "when processing a request" do before do @request = stub('http request') @request.stubs(:[]).returns "foo" @response = stub('http response') @model_class = stub('indirected model class') @result = stub 'result', :render => "mytext" @handler.stubs(:check_authorization) stub_server_interface end # Stub out the interface we require our including classes to # implement. def stub_server_interface @handler.stubs(:accept_header ).returns "format_one,format_two" @handler.stubs(:content_type_header).returns "text/yaml" @handler.stubs(:set_content_type ).returns "my_result" @handler.stubs(:set_response ).returns "my_result" @handler.stubs(:path ).returns "/my_handler/my_result" @handler.stubs(:http_method ).returns("GET") @handler.stubs(:params ).returns({}) @handler.stubs(:content_type ).returns("text/plain") end it "should create an indirection request from the path, parameters, and http method" do @handler.expects(:path).with(@request).returns "mypath" @handler.expects(:http_method).with(@request).returns "mymethod" @handler.expects(:params).with(@request).returns "myparams" @handler.expects(:uri2indirection).with("mymethod", "mypath", "myparams").returns stub("request", :method => :find) @handler.stubs(:do_find) @handler.process(@request, @response) end it "should call the 'do' method associated with the indirection method" do request = stub 'request' @handler.expects(:uri2indirection).returns request request.expects(:method).returns "mymethod" @handler.expects(:do_mymethod).with(request, @request, @response) @handler.process(@request, @response) end it "should delegate authorization to the RestAuthorization layer" do request = stub 'request' @handler.expects(:uri2indirection).returns request request.expects(:method).returns "mymethod" @handler.expects(:do_mymethod).with(request, @request, @response) @handler.expects(:check_authorization).with(request) @handler.process(@request, @response) end it "should return 403 if the request is not authorized" do request = stub 'request' @handler.expects(:uri2indirection).returns request @handler.expects(:do_mymethod).never @handler.expects(:check_authorization).with(request).raises(Puppet::Network::AuthorizationError.new("forbindden")) @handler.expects(:set_response).with { |response, body, status| status == 403 } @handler.process(@request, @response) end it "should serialize a controller exception when an exception is thrown while finding the model instance" do @handler.expects(:uri2indirection).returns stub("request", :method => :find) @handler.expects(:do_find).raises(ArgumentError, "The exception") @handler.expects(:set_response).with { |response, body, status| body == "The exception" and status == 400 } @handler.process(@request, @response) end it "should set the format to text/plain when serializing an exception" do @handler.expects(:set_content_type).with(@response, "text/plain") @handler.do_exception(@response, "A test", 404) end it "should raise an error if the request is formatted in an unknown format" do @handler.stubs(:content_type_header).returns "unknown format" lambda { @handler.request_format(@request) }.should raise_error end it "should still find the correct format if content type contains charset information" do @handler.stubs(:content_type_header).returns "text/plain; charset=UTF-8" @handler.request_format(@request).should == "s" end describe "when finding a model instance" do before do @irequest = stub 'indirection_request', :method => :find, :indirection_name => "my_handler", :to_hash => {}, :key => "my_result", :model => @model_class @model_class.stubs(:find).returns @result @format = stub 'format', :suitable? => true, :mime => "text/format", :name => "format" Puppet::Network::FormatHandler.stubs(:format).returns @format @oneformat = stub 'one', :suitable? => true, :mime => "text/one", :name => "one" Puppet::Network::FormatHandler.stubs(:format).with("one").returns @oneformat end it "should use the indirection request to find the model class" do @irequest.expects(:model).returns @model_class @handler.do_find(@irequest, @request, @response) end it "should use the escaped request key" do @model_class.expects(:find).with do |key, args| key == "my_result" end.returns @result @handler.do_find(@irequest, @request, @response) end it "should use a common method for determining the request parameters" do @irequest.stubs(:to_hash).returns(:foo => :baz, :bar => :xyzzy) @model_class.expects(:find).with do |key, args| args[:foo] == :baz and args[:bar] == :xyzzy end.returns @result @handler.do_find(@irequest, @request, @response) end it "should set the content type to the first format specified in the accept header" do @handler.expects(:accept_header).with(@request).returns "one,two" @handler.expects(:set_content_type).with(@response, @oneformat) @handler.do_find(@irequest, @request, @response) end it "should fail if no accept header is provided" do @handler.expects(:accept_header).with(@request).returns nil lambda { @handler.do_find(@irequest, @request, @response) }.should raise_error(ArgumentError) end it "should fail if the accept header does not contain a valid format" do @handler.expects(:accept_header).with(@request).returns "" lambda { @handler.do_find(@irequest, @request, @response) }.should raise_error(RuntimeError) end it "should not use an unsuitable format" do @handler.expects(:accept_header).with(@request).returns "foo,bar" foo = mock 'foo', :suitable? => false bar = mock 'bar', :suitable? => true Puppet::Network::FormatHandler.expects(:format).with("foo").returns foo Puppet::Network::FormatHandler.expects(:format).with("bar").returns bar @handler.expects(:set_content_type).with(@response, bar) # the suitable one @handler.do_find(@irequest, @request, @response) end it "should render the result using the first format specified in the accept header" do @handler.expects(:accept_header).with(@request).returns "one,two" @result.expects(:render).with(@oneformat) @handler.do_find(@irequest, @request, @response) end it "should pass the result through without rendering it if the result is a string" do @model_class.stubs(:find).returns "foo" @handler.expects(:set_response).with(@response, "foo") @handler.do_find(@irequest, @request, @response) end it "should use the default status when a model find call succeeds" do @handler.expects(:set_response).with { |response, body, status| status.nil? } @handler.do_find(@irequest, @request, @response) end it "should return a serialized object when a model find call succeeds" do @model_instance = stub('model instance') @model_instance.expects(:render).returns "my_rendered_object" @handler.expects(:set_response).with { |response, body, status| body == "my_rendered_object" } @model_class.stubs(:find).returns(@model_instance) @handler.do_find(@irequest, @request, @response) end it "should return a 404 when no model instance can be found" do @model_class.stubs(:name).returns "my name" @handler.expects(:set_response).with { |response, body, status| status == 404 } @model_class.stubs(:find).returns(nil) @handler.do_find(@irequest, @request, @response) end it "should write a log message when no model instance can be found" do @model_class.stubs(:name).returns "my name" @model_class.stubs(:find).returns(nil) Puppet.expects(:info).with("Could not find my_handler for 'my_result'") @handler.do_find(@irequest, @request, @response) end it "should serialize the result in with the appropriate format" do @model_instance = stub('model instance') @handler.expects(:format_to_use).returns(@oneformat) @model_instance.expects(:render).with(@oneformat).returns "my_rendered_object" @model_class.stubs(:find).returns(@model_instance) @handler.do_find(@irequest, @request, @response) end end describe "when performing head operation" do before do @irequest = stub 'indirection_request', :method => :head, :indirection_name => "my_handler", :to_hash => {}, :key => "my_result", :model => @model_class @model_class.stubs(:head).returns true end it "should use the indirection request to find the model class" do @irequest.expects(:model).returns @model_class @handler.do_head(@irequest, @request, @response) end it "should use the escaped request key" do @model_class.expects(:head).with do |key, args| key == "my_result" end.returns true @handler.do_head(@irequest, @request, @response) end it "should not generate a response when a model head call succeeds" do @handler.expects(:set_response).never @handler.do_head(@irequest, @request, @response) end it "should return a 404 when the model head call returns false" do @model_class.stubs(:name).returns "my name" @handler.expects(:set_response).with { |response, body, status| status == 404 } @model_class.stubs(:head).returns(false) @handler.do_head(@irequest, @request, @response) end end describe "when searching for model instances" do before do @irequest = stub 'indirection_request', :method => :find, :indirection_name => "my_handler", :to_hash => {}, :key => "key", :model => @model_class @result1 = mock 'result1' @result2 = mock 'results' @result = [@result1, @result2] @model_class.stubs(:render_multiple).returns "my rendered instances" @model_class.stubs(:search).returns(@result) @format = stub 'format', :suitable? => true, :mime => "text/format", :name => "format" Puppet::Network::FormatHandler.stubs(:format).returns @format @oneformat = stub 'one', :suitable? => true, :mime => "text/one", :name => "one" Puppet::Network::FormatHandler.stubs(:format).with("one").returns @oneformat end it "should use the indirection request to find the model" do @irequest.expects(:model).returns @model_class @handler.do_search(@irequest, @request, @response) end it "should use a common method for determining the request parameters" do @irequest.stubs(:to_hash).returns(:foo => :baz, :bar => :xyzzy) @model_class.expects(:search).with do |key, args| args[:foo] == :baz and args[:bar] == :xyzzy end.returns @result @handler.do_search(@irequest, @request, @response) end it "should use the default status when a model search call succeeds" do @model_class.stubs(:search).returns(@result) @handler.do_search(@irequest, @request, @response) end it "should set the content type to the first format returned by the accept header" do @handler.expects(:accept_header).with(@request).returns "one,two" @handler.expects(:set_content_type).with(@response, @oneformat) @handler.do_search(@irequest, @request, @response) end it "should return a list of serialized objects when a model search call succeeds" do @handler.expects(:accept_header).with(@request).returns "one,two" @model_class.stubs(:search).returns(@result) @model_class.expects(:render_multiple).with(@oneformat, @result).returns "my rendered instances" @handler.expects(:set_response).with { |response, data| data == "my rendered instances" } @handler.do_search(@irequest, @request, @response) end - it "should return a 404 when searching returns an empty array" do - @model_class.stubs(:name).returns "my name" - @handler.expects(:set_response).with { |response, body, status| status == 404 } + it "should return [] when searching returns an empty array" do + @handler.expects(:accept_header).with(@request).returns "one,two" @model_class.stubs(:search).returns([]) + @model_class.expects(:render_multiple).with(@oneformat, []).returns "[]" + + + @handler.expects(:set_response).with { |response, data| data == "[]" } @handler.do_search(@irequest, @request, @response) end it "should return a 404 when searching returns nil" do @model_class.stubs(:name).returns "my name" @handler.expects(:set_response).with { |response, body, status| status == 404 } - @model_class.stubs(:search).returns([]) + @model_class.stubs(:search).returns(nil) @handler.do_search(@irequest, @request, @response) end end describe "when destroying a model instance" do before do @irequest = stub 'indirection_request', :method => :destroy, :indirection_name => "my_handler", :to_hash => {}, :key => "key", :model => @model_class @result = stub 'result', :render => "the result" @model_class.stubs(:destroy).returns @result end it "should use the indirection request to find the model" do @irequest.expects(:model).returns @model_class @handler.do_destroy(@irequest, @request, @response) end it "should use the escaped request key to destroy the instance in the model" do @irequest.expects(:key).returns "foo bar" @model_class.expects(:destroy).with do |key, args| key == "foo bar" end @handler.do_destroy(@irequest, @request, @response) end it "should use a common method for determining the request parameters" do @irequest.stubs(:to_hash).returns(:foo => :baz, :bar => :xyzzy) @model_class.expects(:destroy).with do |key, args| args[:foo] == :baz and args[:bar] == :xyzzy end @handler.do_destroy(@irequest, @request, @response) end it "should use the default status code a model destroy call succeeds" do @handler.expects(:set_response).with { |response, body, status| status.nil? } @handler.do_destroy(@irequest, @request, @response) end it "should return a yaml-encoded result when a model destroy call succeeds" do @result = stub 'result', :to_yaml => "the result" @model_class.expects(:destroy).returns(@result) @handler.expects(:set_response).with { |response, body, status| body == "the result" } @handler.do_destroy(@irequest, @request, @response) end end describe "when saving a model instance" do before do @irequest = stub 'indirection_request', :method => :save, :indirection_name => "my_handler", :to_hash => {}, :key => "key", :model => @model_class @handler.stubs(:body).returns('my stuff') @handler.stubs(:content_type_header).returns("text/yaml") @result = stub 'result', :render => "the result" @model_instance = stub('indirected model instance', :save => true) @model_class.stubs(:convert_from).returns(@model_instance) @format = stub 'format', :suitable? => true, :name => "format", :mime => "text/format" Puppet::Network::FormatHandler.stubs(:format).returns @format @yamlformat = stub 'yaml', :suitable? => true, :name => "yaml", :mime => "text/yaml" Puppet::Network::FormatHandler.stubs(:format).with("yaml").returns @yamlformat end it "should use the indirection request to find the model" do @irequest.expects(:model).returns @model_class @handler.do_save(@irequest, @request, @response) end it "should use the 'body' hook to retrieve the body of the request" do @handler.expects(:body).returns "my body" @model_class.expects(:convert_from).with { |format, body| body == "my body" }.returns @model_instance @handler.do_save(@irequest, @request, @response) end it "should fail to save model if data is not specified" do @handler.stubs(:body).returns('') lambda { @handler.do_save(@irequest, @request, @response) }.should raise_error(ArgumentError) end it "should use a common method for determining the request parameters" do @model_instance.expects(:save).with('key').once @handler.do_save(@irequest, @request, @response) end it "should use the default status when a model save call succeeds" do @handler.expects(:set_response).with { |response, body, status| status.nil? } @handler.do_save(@irequest, @request, @response) end it "should return the yaml-serialized result when a model save call succeeds" do @model_instance.stubs(:save).returns(@model_instance) @model_instance.expects(:to_yaml).returns('foo') @handler.do_save(@irequest, @request, @response) end it "should set the content to yaml" do @handler.expects(:set_content_type).with(@response, @yamlformat) @handler.do_save(@irequest, @request, @response) end it "should use the content-type header to know the body format" do @handler.expects(:content_type_header).returns("text/format") Puppet::Network::FormatHandler.stubs(:mime).with("text/format").returns @format @model_class.expects(:convert_from).with { |format, body| format == "format" }.returns @model_instance @handler.do_save(@irequest, @request, @response) end end end describe "when resolving node" do it "should use a look-up from the ip address" do Resolv.expects(:getname).with("1.2.3.4").returns("host.domain.com") @handler.resolve_node(:ip => "1.2.3.4") end it "should return the look-up result" do Resolv.stubs(:getname).with("1.2.3.4").returns("host.domain.com") @handler.resolve_node(:ip => "1.2.3.4").should == "host.domain.com" end it "should return the ip address if resolving fails" do Resolv.stubs(:getname).with("1.2.3.4").raises(RuntimeError, "no such host") @handler.resolve_node(:ip => "1.2.3.4").should == "1.2.3.4" end end end diff --git a/spec/unit/node/facts_spec.rb b/spec/unit/node/facts_spec.rb index 394db7913..cb2aa3dc7 100755 --- a/spec/unit/node/facts_spec.rb +++ b/spec/unit/node/facts_spec.rb @@ -1,113 +1,137 @@ #!/usr/bin/env ruby require File.dirname(__FILE__) + '/../../spec_helper' require 'puppet/node/facts' describe Puppet::Node::Facts, "when indirecting" do before do @facts = Puppet::Node::Facts.new("me") end it "should be able to convert all fact values to strings" do @facts.values["one"] = 1 @facts.stringify @facts.values["one"].should == "1" end it "should add the node's certificate name as the 'clientcert' fact when adding local facts" do @facts.add_local_facts @facts.values["clientcert"].should == Puppet.settings[:certname] end it "should add the Puppet version as a 'clientversion' fact when adding local facts" do @facts.add_local_facts @facts.values["clientversion"].should == Puppet.version.to_s end it "should add the current environment as a fact if one is not set when adding local facts" do @facts.add_local_facts @facts.values["environment"].should == Puppet[:environment] end it "should not replace any existing environment fact when adding local facts" do @facts.values["environment"] = "foo" @facts.add_local_facts @facts.values["environment"].should == "foo" end it "should be able to downcase fact values" do Puppet.settings.stubs(:value).returns "eh" Puppet.settings.expects(:value).with(:downcasefacts).returns true @facts.values["one"] = "Two" @facts.downcase_if_necessary @facts.values["one"].should == "two" end it "should only try to downcase strings" do Puppet.settings.stubs(:value).returns "eh" Puppet.settings.expects(:value).with(:downcasefacts).returns true @facts.values["now"] = Time.now @facts.downcase_if_necessary @facts.values["now"].should be_instance_of(Time) end it "should not downcase facts if not configured to do so" do Puppet.settings.stubs(:value).returns "eh" Puppet.settings.expects(:value).with(:downcasefacts).returns false @facts.values["one"] = "Two" @facts.downcase_if_necessary @facts.values["one"].should == "Two" end describe "when indirecting" do before do @indirection = stub 'indirection', :request => mock('request'), :name => :facts # We have to clear the cache so that the facts ask for our indirection stub, # instead of anything that might be cached. Puppet::Util::Cacher.expire @facts = Puppet::Node::Facts.new("me", "one" => "two") end it "should redirect to the specified fact store for retrieval" do Puppet::Node::Facts.stubs(:indirection).returns(@indirection) @indirection.expects(:find) Puppet::Node::Facts.find(:my_facts) end it "should redirect to the specified fact store for storage" do Puppet::Node::Facts.stubs(:indirection).returns(@indirection) @indirection.expects(:save) @facts.save end describe "when the Puppet application is 'master'" do it "should default to the 'yaml' terminus" do pending "Cannot test the behavior of defaults in defaults.rb" # Puppet::Node::Facts.indirection.terminus_class.should == :yaml end end describe "when the Puppet application is not 'master'" do it "should default to the 'facter' terminus" do pending "Cannot test the behavior of defaults in defaults.rb" # Puppet::Node::Facts.indirection.terminus_class.should == :facter end end end describe "when storing and retrieving" do it "should add metadata to the facts" do facts = Puppet::Node::Facts.new("me", "one" => "two", "three" => "four") facts.values[:_timestamp].should be_instance_of(Time) end + + describe "using pson" do + before :each do + @timestamp = Time.parse("Thu Oct 28 11:16:31 -0700 2010") + @expiration = Time.parse("Thu Oct 28 11:21:31 -0700 2010") + end + + it "should accept properly formatted pson" do + pson = %Q({"name": "foo", "expiration": "#{@expiration}", "timestamp": "#{@timestamp}", "values": {"a": "1", "b": "2", "c": "3"}}) + format = Puppet::Network::FormatHandler.format('pson') + facts = format.intern(Puppet::Node::Facts,pson) + facts.name.should == 'foo' + facts.expiration.should == @expiration + facts.values.should == {'a' => '1', 'b' => '2', 'c' => '3', :_timestamp => @timestamp} + end + + it "should generate properly formatted pson" do + Time.stubs(:now).returns(@timestamp) + facts = Puppet::Node::Facts.new("foo", {'a' => 1, 'b' => 2, 'c' => 3}) + facts.expiration = @expiration + pson = PSON.parse(facts.to_pson) + pson.should == {"name"=>"foo", "timestamp"=>@timestamp.to_s, "expiration"=>@expiration.to_s, "values"=>{"a"=>1, "b"=>2, "c"=>3}} + end + end end end diff --git a/spec/unit/parser/ast/collection_spec.rb b/spec/unit/parser/ast/collection_spec.rb index 392a2c0f0..cc33075b7 100755 --- a/spec/unit/parser/ast/collection_spec.rb +++ b/spec/unit/parser/ast/collection_spec.rb @@ -1,67 +1,71 @@ #!/usr/bin/env ruby require File.dirname(__FILE__) + '/../../../spec_helper' describe Puppet::Parser::AST::Collection do before :each do - @scope = stub_everything 'scope' - @mytype = stub_everything('mytype') - @scope.stubs(:find_resource_type).returns @mytype - @compiler = stub_everything 'compile' - @scope.stubs(:compiler).returns(@compiler) + @mytype = Puppet::Resource::Type.new(:definition, "mytype") + @environment = Puppet::Node::Environment.new + @environment.known_resource_types.add @mytype + + @compiler = Puppet::Parser::Compiler.new(Puppet::Node.new("foonode", :environment => @environment)) + @scope = Puppet::Parser::Scope.new(:compiler => @compiler) @overrides = stub_everything 'overrides' @overrides.stubs(:is_a?).with(Puppet::Parser::AST).returns(true) - end it "should evaluate its query" do query = mock 'query' collection = Puppet::Parser::AST::Collection.new :query => query, :form => :virtual + collection.type = 'mytype' query.expects(:safeevaluate).with(@scope) collection.evaluate(@scope) end it "should instantiate a Collector for this type" do collection = Puppet::Parser::AST::Collection.new :form => :virtual, :type => "test" - @test_type = stub 'type', :name => 'test' - @scope.expects(:find_resource_type).with('test').returns @test_type + @test_type = Puppet::Resource::Type.new(:definition, "test") + @environment.known_resource_types.add @test_type Puppet::Parser::Collector.expects(:new).with(@scope, "test", nil, nil, :virtual) collection.evaluate(@scope) end it "should tell the compiler about this collector" do - collection = Puppet::Parser::AST::Collection.new :form => :virtual, :type => "test" + collection = Puppet::Parser::AST::Collection.new :form => :virtual, :type => "mytype" Puppet::Parser::Collector.stubs(:new).returns("whatever") @compiler.expects(:add_collection).with("whatever") collection.evaluate(@scope) end it "should evaluate overriden paramaters" do collector = stub_everything 'collector' - collection = Puppet::Parser::AST::Collection.new :form => :virtual, :type => "test", :override => @overrides + collection = Puppet::Parser::AST::Collection.new :form => :virtual, :type => "mytype", :override => @overrides Puppet::Parser::Collector.stubs(:new).returns(collector) @overrides.expects(:safeevaluate).with(@scope) collection.evaluate(@scope) end it "should tell the collector about overrides" do collector = mock 'collector' - collection = Puppet::Parser::AST::Collection.new :form => :virtual, :type => "test", :override => @overrides + collection = Puppet::Parser::AST::Collection.new :form => :virtual, :type => "mytype", :override => @overrides Puppet::Parser::Collector.stubs(:new).returns(collector) collector.expects(:add_override) collection.evaluate(@scope) end - + it "should fail when evaluating undefined resource types" do + collection = Puppet::Parser::AST::Collection.new :form => :virtual, :type => "bogus" + lambda { collection.evaluate(@scope) }.should raise_error "Resource type bogus doesn't exist" + end end diff --git a/spec/unit/parser/lexer_spec.rb b/spec/unit/parser/lexer_spec.rb index 860326973..4ef242cf5 100755 --- a/spec/unit/parser/lexer_spec.rb +++ b/spec/unit/parser/lexer_spec.rb @@ -1,665 +1,681 @@ #!/usr/bin/env ruby require File.dirname(__FILE__) + '/../../spec_helper' require 'puppet/parser/lexer' # This is a special matcher to match easily lexer output RSpec::Matchers.define :be_like do |*expected| match do |actual| expected.zip(actual).all? { |e,a| !e or a[0] == e or (e.is_a? Array and a[0] == e[0] and (a[1] == e[1] or (a[1].is_a?(Hash) and a[1][:value] == e[1]))) } end end __ = nil describe Puppet::Parser::Lexer do describe "when reading strings" do before { @lexer = Puppet::Parser::Lexer.new } it "should increment the line count for every carriage return in the string" do @lexer.line = 10 @lexer.string = "this\nis\natest'" @lexer.slurpstring("'") @lexer.line.should == 12 end it "should not increment the line count for escapes in the string" do @lexer.line = 10 @lexer.string = "this\\nis\\natest'" @lexer.slurpstring("'") @lexer.line.should == 10 end it "should not think the terminator is escaped, when preceeded by an even number of backslashes" do @lexer.line = 10 @lexer.string = "here\nis\nthe\nstring\\\\'with\nextra\njunk" @lexer.slurpstring("'") @lexer.line.should == 13 end end end describe Puppet::Parser::Lexer::Token do before do @token = Puppet::Parser::Lexer::Token.new(%r{something}, :NAME) end [:regex, :name, :string, :skip, :incr_line, :skip_text, :accumulate].each do |param| it "should have a #{param.to_s} reader" do @token.should be_respond_to(param) end it "should have a #{param.to_s} writer" do @token.should be_respond_to(param.to_s + "=") end end end describe Puppet::Parser::Lexer::Token, "when initializing" do it "should create a regex if the first argument is a string" do Puppet::Parser::Lexer::Token.new("something", :NAME).regex.should == %r{something} end it "should set the string if the first argument is one" do Puppet::Parser::Lexer::Token.new("something", :NAME).string.should == "something" end it "should set the regex if the first argument is one" do Puppet::Parser::Lexer::Token.new(%r{something}, :NAME).regex.should == %r{something} end end describe Puppet::Parser::Lexer::TokenList do before do @list = Puppet::Parser::Lexer::TokenList.new end it "should have a method for retrieving tokens by the name" do token = @list.add_token :name, "whatever" @list[:name].should equal(token) end it "should have a method for retrieving string tokens by the string" do token = @list.add_token :name, "whatever" @list.lookup("whatever").should equal(token) end it "should add tokens to the list when directed" do token = @list.add_token :name, "whatever" @list[:name].should equal(token) end it "should have a method for adding multiple tokens at once" do @list.add_tokens "whatever" => :name, "foo" => :bar @list[:name].should_not be_nil @list[:bar].should_not be_nil end it "should fail to add tokens sharing a name with an existing token" do @list.add_token :name, "whatever" lambda { @list.add_token :name, "whatever" }.should raise_error(ArgumentError) end it "should set provided options on tokens being added" do token = @list.add_token :name, "whatever", :skip_text => true token.skip_text.should == true end it "should define any provided blocks as a :convert method" do token = @list.add_token(:name, "whatever") do "foo" end token.convert.should == "foo" end it "should store all string tokens in the :string_tokens list" do one = @list.add_token(:name, "1") @list.string_tokens.should be_include(one) end it "should store all regex tokens in the :regex_tokens list" do one = @list.add_token(:name, %r{one}) @list.regex_tokens.should be_include(one) end it "should not store string tokens in the :regex_tokens list" do one = @list.add_token(:name, "1") @list.regex_tokens.should_not be_include(one) end it "should not store regex tokens in the :string_tokens list" do one = @list.add_token(:name, %r{one}) @list.string_tokens.should_not be_include(one) end it "should sort the string tokens inversely by length when asked" do one = @list.add_token(:name, "1") two = @list.add_token(:other, "12") @list.sort_tokens @list.string_tokens.should == [two, one] end end describe Puppet::Parser::Lexer::TOKENS do before do @lexer = Puppet::Parser::Lexer.new end { :LBRACK => '[', :RBRACK => ']', :LBRACE => '{', :RBRACE => '}', :LPAREN => '(', :RPAREN => ')', :EQUALS => '=', :ISEQUAL => '==', :GREATEREQUAL => '>=', :GREATERTHAN => '>', :LESSTHAN => '<', :LESSEQUAL => '<=', :NOTEQUAL => '!=', :NOT => '!', :COMMA => ',', :DOT => '.', :COLON => ':', :AT => '@', :LLCOLLECT => '<<|', :RRCOLLECT => '|>>', :LCOLLECT => '<|', :RCOLLECT => '|>', :SEMIC => ';', :QMARK => '?', :BACKSLASH => '\\', :FARROW => '=>', :PARROW => '+>', :APPENDS => '+=', :PLUS => '+', :MINUS => '-', :DIV => '/', :TIMES => '*', :LSHIFT => '<<', :RSHIFT => '>>', :MATCH => '=~', :NOMATCH => '!~', :IN_EDGE => '->', :OUT_EDGE => '<-', :IN_EDGE_SUB => '~>', :OUT_EDGE_SUB => '<~', }.each do |name, string| it "should have a token named #{name.to_s}" do Puppet::Parser::Lexer::TOKENS[name].should_not be_nil end it "should match '#{string}' for the token #{name.to_s}" do Puppet::Parser::Lexer::TOKENS[name].string.should == string end end { "case" => :CASE, "class" => :CLASS, "default" => :DEFAULT, "define" => :DEFINE, "import" => :IMPORT, "if" => :IF, "elsif" => :ELSIF, "else" => :ELSE, "inherits" => :INHERITS, "node" => :NODE, "and" => :AND, "or" => :OR, "undef" => :UNDEF, "false" => :FALSE, "true" => :TRUE, "in" => :IN, }.each do |string, name| it "should have a keyword named #{name.to_s}" do Puppet::Parser::Lexer::KEYWORDS[name].should_not be_nil end it "should have the keyword for #{name.to_s} set to #{string}" do Puppet::Parser::Lexer::KEYWORDS[name].string.should == string end end # These tokens' strings don't matter, just that the tokens exist. [:STRING, :DQPRE, :DQMID, :DQPOST, :BOOLEAN, :NAME, :NUMBER, :COMMENT, :MLCOMMENT, :RETURN, :SQUOTE, :DQUOTE, :VARIABLE].each do |name| it "should have a token named #{name.to_s}" do Puppet::Parser::Lexer::TOKENS[name].should_not be_nil end end end describe Puppet::Parser::Lexer::TOKENS[:CLASSNAME] do before { @token = Puppet::Parser::Lexer::TOKENS[:CLASSNAME] } it "should match against lower-case alpha-numeric terms separated by double colons" do @token.regex.should =~ "one::two" end it "should match against many lower-case alpha-numeric terms separated by double colons" do @token.regex.should =~ "one::two::three::four::five" end it "should match against lower-case alpha-numeric terms prefixed by double colons" do @token.regex.should =~ "::one" end end describe Puppet::Parser::Lexer::TOKENS[:CLASSREF] do before { @token = Puppet::Parser::Lexer::TOKENS[:CLASSREF] } it "should match against single upper-case alpha-numeric terms" do @token.regex.should =~ "One" end it "should match against upper-case alpha-numeric terms separated by double colons" do @token.regex.should =~ "One::Two" end it "should match against many upper-case alpha-numeric terms separated by double colons" do @token.regex.should =~ "One::Two::Three::Four::Five" end it "should match against upper-case alpha-numeric terms prefixed by double colons" do @token.regex.should =~ "::One" end end describe Puppet::Parser::Lexer::TOKENS[:NAME] do before { @token = Puppet::Parser::Lexer::TOKENS[:NAME] } it "should match against lower-case alpha-numeric terms" do @token.regex.should =~ "one-two" end it "should return itself and the value if the matched term is not a keyword" do Puppet::Parser::Lexer::KEYWORDS.expects(:lookup).returns(nil) @token.convert(stub("lexer"), "myval").should == [Puppet::Parser::Lexer::TOKENS[:NAME], "myval"] end it "should return the keyword token and the value if the matched term is a keyword" do keyword = stub 'keyword', :name => :testing Puppet::Parser::Lexer::KEYWORDS.expects(:lookup).returns(keyword) @token.convert(stub("lexer"), "myval").should == [keyword, "myval"] end it "should return the BOOLEAN token and 'true' if the matched term is the string 'true'" do keyword = stub 'keyword', :name => :TRUE Puppet::Parser::Lexer::KEYWORDS.expects(:lookup).returns(keyword) @token.convert(stub('lexer'), "true").should == [Puppet::Parser::Lexer::TOKENS[:BOOLEAN], true] end it "should return the BOOLEAN token and 'false' if the matched term is the string 'false'" do keyword = stub 'keyword', :name => :FALSE Puppet::Parser::Lexer::KEYWORDS.expects(:lookup).returns(keyword) @token.convert(stub('lexer'), "false").should == [Puppet::Parser::Lexer::TOKENS[:BOOLEAN], false] end end describe Puppet::Parser::Lexer::TOKENS[:NUMBER] do before do @token = Puppet::Parser::Lexer::TOKENS[:NUMBER] @regex = @token.regex end it "should match against numeric terms" do @regex.should =~ "2982383139" end it "should match against float terms" do @regex.should =~ "29823.235" end it "should match against hexadecimal terms" do @regex.should =~ "0xBEEF0023" end it "should match against float with exponent terms" do @regex.should =~ "10e23" end it "should match against float terms with negative exponents" do @regex.should =~ "10e-23" end it "should match against float terms with fractional parts and exponent" do @regex.should =~ "1.234e23" end it "should return the NAME token and the value" do @token.convert(stub("lexer"), "myval").should == [Puppet::Parser::Lexer::TOKENS[:NAME], "myval"] end end describe Puppet::Parser::Lexer::TOKENS[:COMMENT] do before { @token = Puppet::Parser::Lexer::TOKENS[:COMMENT] } it "should match against lines starting with '#'" do @token.regex.should =~ "# this is a comment" end it "should be marked to get skipped" do @token.skip?.should be_true end it "should be marked to accumulate" do @token.accumulate?.should be_true end it "'s block should return the comment without the #" do @token.convert(@lexer,"# this is a comment")[1].should == "this is a comment" end end describe Puppet::Parser::Lexer::TOKENS[:MLCOMMENT] do before do @token = Puppet::Parser::Lexer::TOKENS[:MLCOMMENT] @lexer = stub 'lexer', :line => 0 end it "should match against lines enclosed with '/*' and '*/'" do @token.regex.should =~ "/* this is a comment */" end it "should match multiple lines enclosed with '/*' and '*/'" do @token.regex.should =~ """/* this is a comment */""" end it "should increase the lexer current line number by the amount of lines spanned by the comment" do @lexer.expects(:line=).with(2) @token.convert(@lexer, "1\n2\n3") end it "should not greedily match comments" do match = @token.regex.match("/* first */ word /* second */") match[1].should == " first " end it "should be marked to accumulate" do @token.accumulate?.should be_true end it "'s block should return the comment without the comment marks" do @lexer.stubs(:line=).with(0) @token.convert(@lexer,"/* this is a comment */")[1].should == "this is a comment" end end describe Puppet::Parser::Lexer::TOKENS[:RETURN] do before { @token = Puppet::Parser::Lexer::TOKENS[:RETURN] } it "should match against carriage returns" do @token.regex.should =~ "\n" end it "should be marked to initiate text skipping" do @token.skip_text.should be_true end it "should be marked to increment the line" do @token.incr_line.should be_true end end def tokens_scanned_from(s) lexer = Puppet::Parser::Lexer.new lexer.string = s lexer.fullscan[0..-2] end describe Puppet::Parser::Lexer,"when lexing strings" do { %q{'single quoted string')} => [[:STRING,'single quoted string']], %q{"double quoted string"} => [[:STRING,'double quoted string']], %q{'single quoted string with an escaped "\\'"'} => [[:STRING,'single quoted string with an escaped "\'"']], %q{'single quoted string with an escaped "\$"'} => [[:STRING,'single quoted string with an escaped "\$"']], %q{'single quoted string with an escaped "\."'} => [[:STRING,'single quoted string with an escaped "\."']], %q{'single quoted string with an escaped "\n"'} => [[:STRING,'single quoted string with an escaped "\n"']], %q{'single quoted string with an escaped "\\\\"'} => [[:STRING,'single quoted string with an escaped "\\\\"']], %q{"string with an escaped '\\"'"} => [[:STRING,"string with an escaped '\"'"]], %q{"string with an escaped '\\$'"} => [[:STRING,"string with an escaped '$'"]], %Q{"string with a line ending with a backslash: \\\nfoo"} => [[:STRING,"string with a line ending with a backslash: foo"]], %q{"string with $v (but no braces)"} => [[:DQPRE,"string with "],[:VARIABLE,'v'],[:DQPOST,' (but no braces)']], %q["string with ${v} in braces"] => [[:DQPRE,"string with "],[:VARIABLE,'v'],[:DQPOST,' in braces']], %q["string with ${qualified::var} in braces"] => [[:DQPRE,"string with "],[:VARIABLE,'qualified::var'],[:DQPOST,' in braces']], %q{"string with $v and $v (but no braces)"} => [[:DQPRE,"string with "],[:VARIABLE,"v"],[:DQMID," and "],[:VARIABLE,"v"],[:DQPOST," (but no braces)"]], %q["string with ${v} and ${v} in braces"] => [[:DQPRE,"string with "],[:VARIABLE,"v"],[:DQMID," and "],[:VARIABLE,"v"],[:DQPOST," in braces"]], %q["string with ${'a nested single quoted string'} inside it."] => [[:DQPRE,"string with "],[:STRING,'a nested single quoted string'],[:DQPOST,' inside it.']], %q["string with ${['an array ',$v2]} in it."] => [[:DQPRE,"string with "],:LBRACK,[:STRING,"an array "],:COMMA,[:VARIABLE,"v2"],:RBRACK,[:DQPOST," in it."]], %q{a simple "scanner" test} => [[:NAME,"a"],[:NAME,"simple"], [:STRING,"scanner"],[:NAME,"test"]], %q{a simple 'single quote scanner' test} => [[:NAME,"a"],[:NAME,"simple"], [:STRING,"single quote scanner"],[:NAME,"test"]], %q{a harder 'a $b \c"'} => [[:NAME,"a"],[:NAME,"harder"], [:STRING,'a $b \c"']], %q{a harder "scanner test"} => [[:NAME,"a"],[:NAME,"harder"], [:STRING,"scanner test"]], %q{a hardest "scanner \"test\""} => [[:NAME,"a"],[:NAME,"hardest"],[:STRING,'scanner "test"']], %Q{a hardestest "scanner \\"test\\"\n"} => [[:NAME,"a"],[:NAME,"hardestest"],[:STRING,%Q{scanner "test"\n}]], %q{function("call")} => [[:NAME,"function"],[:LPAREN,"("],[:STRING,'call'],[:RPAREN,")"]], %q["string with ${(3+5)/4} nested math."] => [[:DQPRE,"string with "],:LPAREN,[:NAME,"3"],:PLUS,[:NAME,"5"],:RPAREN,:DIV,[:NAME,"4"],[:DQPOST," nested math."]], %q["$$$$"] => [[:STRING,"$$$$"]], %q["$variable"] => [[:DQPRE,""],[:VARIABLE,"variable"],[:DQPOST,""]], %q["$var$other"] => [[:DQPRE,""],[:VARIABLE,"var"],[:DQMID,""],[:VARIABLE,"other"],[:DQPOST,""]], %q["foo$bar$"] => [[:DQPRE,"foo"],[:VARIABLE,"bar"],[:DQPOST,"$"]], %q["foo$$bar"] => [[:DQPRE,"foo$"],[:VARIABLE,"bar"],[:DQPOST,""]], %q[""] => [[:STRING,""]], }.each { |src,expected_result| it "should handle #{src} correctly" do tokens_scanned_from(src).should be_like(*expected_result) end } end describe Puppet::Parser::Lexer::TOKENS[:DOLLAR_VAR] do before { @token = Puppet::Parser::Lexer::TOKENS[:DOLLAR_VAR] } it "should match against alpha words prefixed with '$'" do @token.regex.should =~ '$this_var' end it "should return the VARIABLE token and the variable name stripped of the '$'" do @token.convert(stub("lexer"), "$myval").should == [Puppet::Parser::Lexer::TOKENS[:VARIABLE], "myval"] end end describe Puppet::Parser::Lexer::TOKENS[:REGEX] do before { @token = Puppet::Parser::Lexer::TOKENS[:REGEX] } it "should match against any expression enclosed in //" do @token.regex.should =~ '/this is a regex/' end it 'should not match if there is \n in the regex' do @token.regex.should_not =~ "/this is \n a regex/" end describe "when scanning" do it "should not consider escaped slashes to be the end of a regex" do tokens_scanned_from("$x =~ /this \\/ foo/").should be_like(__,__,[:REGEX,%r{this / foo}]) end it "should not lex chained division as a regex" do tokens_scanned_from("$x = $a/$b/$c").collect { |name, data| name }.should_not be_include( :REGEX ) end it "should accept a regular expression after NODE" do tokens_scanned_from("node /www.*\.mysite\.org/").should be_like(__,[:REGEX,Regexp.new("www.*\.mysite\.org")]) end it "should accept regular expressions in a CASE" do s = %q{case $variable { "something": {$othervar = 4096 / 2} /regex/: {notice("this notably sucks")} } } tokens_scanned_from(s).should be_like( :CASE,:VARIABLE,:LBRACE,:STRING,:COLON,:LBRACE,:VARIABLE,:EQUALS,:NAME,:DIV,:NAME,:RBRACE,[:REGEX,/regex/],:COLON,:LBRACE,:NAME,:LPAREN,:STRING,:RPAREN,:RBRACE,:RBRACE ) end end it "should return the REGEX token and a Regexp" do @token.convert(stub("lexer"), "/myregex/").should == [Puppet::Parser::Lexer::TOKENS[:REGEX], Regexp.new(/myregex/)] end end describe Puppet::Parser::Lexer, "when lexing comments" do before { @lexer = Puppet::Parser::Lexer.new } it "should accumulate token in munge_token" do token = stub 'token', :skip => true, :accumulate? => true, :incr_line => nil, :skip_text => false token.stubs(:convert).with(@lexer, "# this is a comment").returns([token, " this is a comment"]) @lexer.munge_token(token, "# this is a comment") @lexer.munge_token(token, "# this is a comment") @lexer.getcomment.should == " this is a comment\n this is a comment\n" end it "should add a new comment stack level on LBRACE" do @lexer.string = "{" @lexer.expects(:commentpush) @lexer.fullscan end + it "should add a new comment stack level on LPAREN" do + @lexer.string = "(" + + @lexer.expects(:commentpush) + + @lexer.fullscan + end + + it "should pop the current comment on RPAREN" do + @lexer.string = ")" + + @lexer.expects(:commentpop) + + @lexer.fullscan + end + it "should return the current comments on getcomment" do @lexer.string = "# comment" @lexer.fullscan @lexer.getcomment.should == "comment\n" end it "should discard the previous comments on blank line" do @lexer.string = "# 1\n\n# 2" @lexer.fullscan @lexer.getcomment.should == "2\n" end it "should skip whitespace before lexing the next token after a non-token" do tokens_scanned_from("/* 1\n\n */ \ntest").should be_like([:NAME, "test"]) end it "should not return comments seen after the current line" do @lexer.string = "# 1\n\n# 2" @lexer.fullscan @lexer.getcomment(1).should == "" end it "should return a comment seen before the current line" do @lexer.string = "# 1\n# 2" @lexer.fullscan @lexer.getcomment(2).should == "1\n2\n" end end # FIXME: We need to rewrite all of these tests, but I just don't want to take the time right now. describe "Puppet::Parser::Lexer in the old tests" do before { @lexer = Puppet::Parser::Lexer.new } it "should do simple lexing" do { %q{\\} => [[:BACKSLASH,"\\"]], %q{simplest scanner test} => [[:NAME,"simplest"],[:NAME,"scanner"],[:NAME,"test"]], %Q{returned scanner test\n} => [[:NAME,"returned"],[:NAME,"scanner"],[:NAME,"test"]] }.each { |source,expected| tokens_scanned_from(source).should be_like(*expected) } end it "should fail usefully" do lambda { tokens_scanned_from('^') }.should raise_error(RuntimeError) end it "should fail if the string is not set" do lambda { @lexer.fullscan }.should raise_error(Puppet::LexError) end it "should correctly identify keywords" do tokens_scanned_from("case").should be_like([:CASE, "case"]) end it "should correctly parse class references" do %w{Many Different Words A Word}.each { |t| tokens_scanned_from(t).should be_like([:CLASSREF,t])} end # #774 it "should correctly parse namespaced class refernces token" do %w{Foo ::Foo Foo::Bar ::Foo::Bar}.each { |t| tokens_scanned_from(t).should be_like([:CLASSREF, t]) } end it "should correctly parse names" do %w{this is a bunch of names}.each { |t| tokens_scanned_from(t).should be_like([:NAME,t]) } end it "should correctly parse names with numerals" do %w{1name name1 11names names11}.each { |t| tokens_scanned_from(t).should be_like([:NAME,t]) } end it "should correctly parse empty strings" do lambda { tokens_scanned_from('$var = ""') }.should_not raise_error end it "should correctly parse virtual resources" do tokens_scanned_from("@type {").should be_like([:AT, "@"], [:NAME, "type"], [:LBRACE, "{"]) end it "should correctly deal with namespaces" do @lexer.string = %{class myclass} @lexer.fullscan @lexer.namespace.should == "myclass" @lexer.namepop @lexer.namespace.should == "" @lexer.string = "class base { class sub { class more" @lexer.fullscan @lexer.namespace.should == "base::sub::more" @lexer.namepop @lexer.namespace.should == "base::sub" end it "should not put class instantiation on the namespace" do @lexer.string = "class base { class sub { class { mode" @lexer.fullscan @lexer.namespace.should == "base::sub" end it "should correctly handle fully qualified names" do @lexer.string = "class base { class sub::more {" @lexer.fullscan @lexer.namespace.should == "base::sub::more" @lexer.namepop @lexer.namespace.should == "base" end it "should correctly lex variables" do ["$variable", "$::variable", "$qualified::variable", "$further::qualified::variable"].each do |string| tokens_scanned_from(string).should be_like([:VARIABLE,string.sub(/^\$/,'')]) end end end require File.dirname(__FILE__) + '/../../../test/lib/puppettest' require File.dirname(__FILE__) + '/../../../test/lib/puppettest/support/utils' describe "Puppet::Parser::Lexer in the old tests when lexing example files" do extend PuppetTest::Support::Utils textfiles do |file| it "should correctly lex #{file}" do lexer = Puppet::Parser::Lexer.new lexer.file = file lambda { lexer.fullscan }.should_not raise_error end end end diff --git a/spec/unit/parser/parser_spec.rb b/spec/unit/parser/parser_spec.rb index 07e2d220b..2ed279fd9 100755 --- a/spec/unit/parser/parser_spec.rb +++ b/spec/unit/parser/parser_spec.rb @@ -1,410 +1,443 @@ #!/usr/bin/env ruby require File.dirname(__FILE__) + '/../../spec_helper' describe Puppet::Parser do ast = Puppet::Parser::AST before :each do @known_resource_types = Puppet::Resource::TypeCollection.new("development") @parser = Puppet::Parser::Parser.new "development" @parser.stubs(:known_resource_types).returns @known_resource_types @true_ast = Puppet::Parser::AST::Boolean.new :value => true end it "should require an environment at initialization" do lambda { Puppet::Parser::Parser.new }.should raise_error(ArgumentError) end it "should set the environment" do env = Puppet::Node::Environment.new Puppet::Parser::Parser.new(env).environment.should == env end it "should convert the environment into an environment instance if a string is provided" do env = Puppet::Node::Environment.new("testing") Puppet::Parser::Parser.new("testing").environment.should == env end it "should be able to look up the environment-specific resource type collection" do rtc = Puppet::Node::Environment.new("development").known_resource_types parser = Puppet::Parser::Parser.new "development" parser.known_resource_types.should equal(rtc) end it "should delegate importing to the known resource type loader" do parser = Puppet::Parser::Parser.new "development" parser.known_resource_types.loader.expects(:import).with("newfile", "current_file") parser.lexer.expects(:file).returns "current_file" parser.import("newfile") end describe "when parsing files" do before do FileTest.stubs(:exist?).returns true File.stubs(:read).returns "" @parser.stubs(:watch_file) end it "should treat files ending in 'rb' as ruby files" do @parser.expects(:parse_ruby_file) @parser.file = "/my/file.rb" @parser.parse end end describe "when parsing append operator" do it "should not raise syntax errors" do lambda { @parser.parse("$var += something") }.should_not raise_error end it "shouldraise syntax error on incomplete syntax " do lambda { @parser.parse("$var += ") }.should raise_error end it "should call ast::VarDef with append=true" do ast::VarDef.expects(:new).with { |h| h[:append] == true } @parser.parse("$var += 2") end it "should work with arrays too" do ast::VarDef.expects(:new).with { |h| h[:append] == true } @parser.parse("$var += ['test']") end end + describe "when parsing selector" do + it "should support hash access on the left hand side" do + lambda { @parser.parse("$h = { 'a' => 'b' } $a = $h['a'] ? { 'b' => 'd', default => undef }") }.should_not raise_error + end + end + describe "when parsing 'if'" do it "not, it should create the correct ast objects" do ast::Not.expects(:new).with { |h| h[:value].is_a?(ast::Boolean) } @parser.parse("if ! true { $var = 1 }") end it "boolean operation, it should create the correct ast objects" do ast::BooleanOperator.expects(:new).with { |h| h[:rval].is_a?(ast::Boolean) and h[:lval].is_a?(ast::Boolean) and h[:operator]=="or" } @parser.parse("if true or true { $var = 1 }") end it "comparison operation, it should create the correct ast objects" do ast::ComparisonOperator.expects(:new).with { |h| h[:lval].is_a?(ast::Name) and h[:rval].is_a?(ast::Name) and h[:operator]=="<" } @parser.parse("if 1 < 2 { $var = 1 }") end end describe "when parsing if complex expressions" do it "should create a correct ast tree" do aststub = stub_everything 'ast' ast::ComparisonOperator.expects(:new).with { |h| h[:rval].is_a?(ast::Name) and h[:lval].is_a?(ast::Name) and h[:operator]==">" }.returns(aststub) ast::ComparisonOperator.expects(:new).with { |h| h[:rval].is_a?(ast::Name) and h[:lval].is_a?(ast::Name) and h[:operator]=="==" }.returns(aststub) ast::BooleanOperator.expects(:new).with { |h| h[:rval]==aststub and h[:lval]==aststub and h[:operator]=="and" } @parser.parse("if (1 > 2) and (1 == 2) { $var = 1 }") end it "should raise an error on incorrect expression" do lambda { @parser.parse("if (1 > 2 > ) or (1 == 2) { $var = 1 }") }.should raise_error end end describe "when parsing resource references" do it "should not raise syntax errors" do lambda { @parser.parse('exec { test: param => File["a"] }') }.should_not raise_error end it "should not raise syntax errors with multiple references" do lambda { @parser.parse('exec { test: param => File["a","b"] }') }.should_not raise_error end it "should create an ast::ResourceReference" do ast::Resource.stubs(:new) ast::ResourceReference.expects(:new).with { |arg| arg[:line]==1 and arg[:type]=="File" and arg[:title].is_a?(ast::ASTArray) } @parser.parse('exec { test: command => File["a","b"] }') end end describe "when parsing resource overrides" do it "should not raise syntax errors" do lambda { @parser.parse('Resource["title"] { param => value }') }.should_not raise_error end it "should not raise syntax errors with multiple overrides" do lambda { @parser.parse('Resource["title1","title2"] { param => value }') }.should_not raise_error end it "should create an ast::ResourceOverride" do ast::ResourceOverride.expects(:new).with { |arg| arg[:line]==1 and arg[:object].is_a?(ast::ResourceReference) and arg[:parameters].is_a?(ast::ResourceParam) } @parser.parse('Resource["title1","title2"] { param => value }') end end describe "when parsing if statements" do it "should not raise errors with empty if" do lambda { @parser.parse("if true { }") }.should_not raise_error end it "should not raise errors with empty else" do lambda { @parser.parse("if false { notice('if') } else { }") }.should_not raise_error end it "should not raise errors with empty if and else" do lambda { @parser.parse("if false { } else { }") }.should_not raise_error end it "should create a nop node for empty branch" do ast::Nop.expects(:new) @parser.parse("if true { }") end it "should create a nop node for empty else branch" do ast::Nop.expects(:new) @parser.parse("if true { notice('test') } else { }") end it "should build a chain of 'ifs' if there's an 'elsif'" do ast = @parser.parse(<<-PP) if true { notice('test') } elsif true {} else { } PP end end describe "when parsing function calls" do it "should not raise errors with no arguments" do lambda { @parser.parse("tag()") }.should_not raise_error end it "should not raise errors with rvalue function with no args" do lambda { @parser.parse("$a = template()") }.should_not raise_error end it "should not raise errors with arguments" do lambda { @parser.parse("notice(1)") }.should_not raise_error end it "should not raise errors with multiple arguments" do lambda { @parser.parse("notice(1,2)") }.should_not raise_error end it "should not raise errors with multiple arguments and a trailing comma" do lambda { @parser.parse("notice(1,2,)") }.should_not raise_error end end describe "when parsing arrays with trailing comma" do it "should not raise errors with a trailing comma" do lambda { @parser.parse("$a = [1,2,]") }.should_not raise_error end end describe "when providing AST context" do before do @lexer = stub 'lexer', :line => 50, :file => "/foo/bar", :getcomment => "whev" @parser.stubs(:lexer).returns @lexer end it "should include the lexer's line" do @parser.ast_context[:line].should == 50 end it "should include the lexer's file" do @parser.ast_context[:file].should == "/foo/bar" end it "should include the docs if directed to do so" do @parser.ast_context(true)[:doc].should == "whev" end it "should not include the docs when told not to" do @parser.ast_context(false)[:doc].should be_nil end it "should not include the docs by default" do @parser.ast_context[:doc].should be_nil end end describe "when building ast nodes" do before do @lexer = stub 'lexer', :line => 50, :file => "/foo/bar", :getcomment => "whev" @parser.stubs(:lexer).returns @lexer - @class = stub 'class', :use_docs => false + @class = Puppet::Resource::Type.new(:hostclass, "myclass", :use_docs => false) end it "should return a new instance of the provided class created with the provided options" do @class.expects(:new).with { |opts| opts[:foo] == "bar" } @parser.ast(@class, :foo => "bar") end it "should merge the ast context into the provided options" do @class.expects(:new).with { |opts| opts[:file] == "/foo" } @parser.expects(:ast_context).returns :file => "/foo" @parser.ast(@class, :foo => "bar") end it "should prefer provided options over AST context" do @class.expects(:new).with { |opts| opts[:file] == "/bar" } @lexer.expects(:file).returns "/foo" @parser.ast(@class, :file => "/bar") end it "should include docs when the AST class uses them" do @class.expects(:use_docs).returns true @class.stubs(:new) - @parser.expects(:ast_context).with{ |a| a[0] == true }.returns({}) + @parser.expects(:ast_context).with{ |docs, line| docs == true }.returns({}) @parser.ast(@class, :file => "/bar") end it "should get docs from lexer using the correct AST line number" do @class.expects(:use_docs).returns true @class.stubs(:new).with{ |a| a[:doc] == "doc" } @lexer.expects(:getcomment).with(12).returns "doc" @parser.ast(@class, :file => "/bar", :line => 12) end end describe "when creating a node" do before :each do @lexer = stub 'lexer' @lexer.stubs(:getcomment) @parser.stubs(:lexer).returns(@lexer) @node = stub_everything 'node' @parser.stubs(:ast_context).returns({}) @parser.stubs(:node).returns(nil) @nodename = stub 'nodename', :is_a? => false, :value => "foo" @nodename.stubs(:is_a?).with(Puppet::Parser::AST::HostName).returns(true) end it "should return an array of nodes" do @parser.newnode(@nodename).should be_instance_of(Array) end + + it "should initialize the ast context with the correct line number" do + @parser.expects(:ast_context).with { |a,b| b == 123 }.returns({}) + @parser.newnode(@nodename, { :line => 123 }) + end + end + + %w{class define}.each do |entity| + describe "when creating a #{entity}" do + before :each do + @parser.stubs(:ast_context).returns({}) + + @name = stub "#{entity}name", :is_a? => false, :value => "foo" + end + + it "should create and add the correct resource type" do + instance = stub 'instance' + Puppet::Resource::Type.expects(:new).returns(instance) + @parser.known_resource_types.expects(:add).with(instance) + @parser.send("new#{entity}", @name) + end + + it "should initialize the ast context with the correct line number" do + @parser.expects(:ast_context).with { |a,b| b == 123 }.returns({}) + @parser.send("new#{entity}", @name, { :line => 123 }) + end + end end describe "when retrieving a specific node" do it "should delegate to the known_resource_types node" do @known_resource_types.expects(:node).with("node") @parser.node("node") end end describe "when retrieving a specific class" do it "should delegate to the loaded code" do @known_resource_types.expects(:hostclass).with("class") @parser.hostclass("class") end end describe "when retrieving a specific definitions" do it "should delegate to the loaded code" do @known_resource_types.expects(:definition).with("define") @parser.definition("define") end end describe "when determining the configuration version" do it "should determine it from the resource type collection" do @parser.known_resource_types.expects(:version).returns "foo" @parser.version.should == "foo" end end describe "when looking up definitions" do it "should use the known resource types to check for them by name" do @parser.known_resource_types.stubs(:find_or_load).with("namespace","name",:definition).returns(:this_value) @parser.find_definition("namespace","name").should == :this_value end end describe "when looking up hostclasses" do it "should use the known resource types to check for them by name" do @parser.known_resource_types.stubs(:find_or_load).with("namespace","name",:hostclass).returns(:this_value) @parser.find_hostclass("namespace","name").should == :this_value end end describe "when parsing classes" do before :each do @krt = Puppet::Resource::TypeCollection.new("development") @parser = Puppet::Parser::Parser.new "development" @parser.stubs(:known_resource_types).returns @krt end it "should create new classes" do @parser.parse("class foobar {}") @krt.hostclass("foobar").should be_instance_of(Puppet::Resource::Type) end it "should correctly set the parent class when one is provided" do @parser.parse("class foobar inherits yayness {}") @krt.hostclass("foobar").parent.should == "yayness" end it "should correctly set the parent class for multiple classes at a time" do @parser.parse("class foobar inherits yayness {}\nclass boo inherits bar {}") @krt.hostclass("foobar").parent.should == "yayness" @krt.hostclass("boo").parent.should == "bar" end it "should define the code when some is provided" do @parser.parse("class foobar { $var = val }") @krt.hostclass("foobar").code.should_not be_nil end it "should define parameters when provided" do @parser.parse("class foobar($biz,$baz) {}") @krt.hostclass("foobar").arguments.should == {"biz" => nil, "baz" => nil} end end describe "when parsing resources" do before :each do @krt = Puppet::Resource::TypeCollection.new("development") @parser = Puppet::Parser::Parser.new "development" @parser.stubs(:known_resource_types).returns @krt end it "should be able to parse class resources" do @krt.add(Puppet::Resource::Type.new(:hostclass, "foobar", :arguments => {"biz" => nil})) lambda { @parser.parse("class { foobar: biz => stuff }") }.should_not raise_error end it "should correctly mark exported resources as exported" do @parser.parse("@@file { '/file': }") @krt.hostclass("").code[0].exported.should be_true end it "should correctly mark virtual resources as virtual" do @parser.parse("@file { '/file': }") @krt.hostclass("").code[0].virtual.should be_true end end end diff --git a/spec/unit/provider/mount/parsed_spec.rb b/spec/unit/provider/mount/parsed_spec.rb index 5a1c986b1..cf29bd358 100755 --- a/spec/unit/provider/mount/parsed_spec.rb +++ b/spec/unit/provider/mount/parsed_spec.rb @@ -1,192 +1,277 @@ #!/usr/bin/env ruby # # Created by Luke Kanies on 2007-9-12. # Copyright (c) 2006. All rights reserved. require File.dirname(__FILE__) + '/../../../spec_helper' +require 'puppet_spec/files' require 'puppettest/support/utils' require 'puppettest/fileparsing' module ParsedMountTesting include PuppetTest::Support::Utils include PuppetTest::FileParsing + include PuppetSpec::Files def fake_fstab - os = Facter['operatingsystem'] + os = Facter.value(:operatingsystem) if os == "Solaris" name = "solaris.fstab" elsif os == "FreeBSD" name = "freebsd.fstab" else # Catchall for other fstabs name = "linux.fstab" end - oldpath = @provider_class.default_target fakefile(File::join("data/types/mount", name)) end - def mkmountargs - mount = nil - - if defined?(@pcount) - @pcount += 1 + def fake_mountoutput + os = Facter.value(:operatingsystem) + if os == "Darwin" + name = "darwin.mount" + elsif os == "HP-UX" + name = "hpux.mount" + elsif os == "Solaris" + name = "solaris.mount" + elsif os == "AIX" + name = "aix.mount" else - @pcount = 1 - end - args = { - :name => "/fspuppet#{@pcount}", - :device => "/dev/dsk#{@pcount}", - } - - @provider_class.fields(:parsed).each do |field| - args[field] = "fake#{field}#{@pcount}" unless args.include? field + # Catchall for other fstabs + name = "linux.mount" end - - args + fakefile(File::join("data/providers/mount/parsed", name)) end - def mkmount - hash = mkmountargs - #hash[:provider] = @provider_class.name - - fakeresource = stub :type => :mount, :name => hash[:name] - fakeresource.stubs(:[]).with(:name).returns(hash[:name]) - fakeresource.stubs(:should).with(:target).returns(nil) - - mount = @provider_class.new(fakeresource) - hash[:record_type] = :parsed - hash[:ensure] = :present - mount.property_hash = hash - - mount - end - - # Here we just create a fake host type that answers to all of the methods - # but does not modify our actual system. - def mkfaketype - @provider.stubs(:filetype).returns(Puppet::Util::FileType.filetype(:ram)) - end end provider_class = Puppet::Type.type(:mount).provider(:parsed) describe provider_class do + before :each do @mount_class = Puppet::Type.type(:mount) - @provider_class = @mount_class.provider(:parsed) + @provider = @mount_class.provider(:parsed) + end + + # LAK:FIXME I can't mock Facter because this test happens at parse-time. + it "should default to /etc/vfstab on Solaris" do + pending "This test only works on Solaris" unless Facter.value(:operatingsystem) == 'Solaris' + Puppet::Type.type(:mount).provider(:parsed).default_target.should == '/etc/vfstab' + end + + it "should default to /etc/fstab on anything else" do + pending "This test does not work on Solaris" if Facter.value(:operatingsystem) == 'Solaris' + Puppet::Type.type(:mount).provider(:parsed).default_target.should == '/etc/fstab' end + describe "when parsing a line" do - describe provider_class do - include ParsedMountTesting + it "should not crash on incomplete lines in fstab" do + parse = @provider.parse <<-FSTAB +/dev/incomplete +/dev/device name +FSTAB + lambda{ @provider.to_line(parse[0]) }.should_not raise_error + end - it "should be able to parse all of the example mount tabs" do - tab = fake_fstab - @provider = @provider_class - # LAK:FIXME Again, a relatively bad test, but I don't know how to rspec-ify this. - # I suppose this is more of an integration test? I dunno. - fakedataparse(tab) do - # Now just make we've got some mounts we know will be there - hashes = @provider_class.target_records(tab).find_all { |i| i.is_a? Hash } - (hashes.length > 0).should be_true - root = hashes.find { |i| i[:name] == "/" } + describe "on Solaris", :if => Facter.value(:operatingsystem) == 'Solaris' do - proc { @provider_class.to_file(hashes) }.should_not raise_error + before :each do + @example_line = "/dev/dsk/c0d0s0 /dev/rdsk/c0d0s0 \t\t / \t ufs 1 no\t-" end - end - # LAK:FIXME I can't mock Facter because this test happens at parse-time. - it "should default to /etc/vfstab on Solaris and /etc/fstab everywhere else" do - should = case Facter.value(:operatingsystem) - when "Solaris"; "/etc/vfstab" - else - "/etc/fstab" - end - Puppet::Type.type(:mount).provider(:parsed).default_target.should == should - end + it "should extract device from the first field" do + @provider.parse_line(@example_line)[:device].should == '/dev/dsk/c0d0s0' + end - it "should not crash on incomplete lines in fstab" do - parse = @provider_class.parse <<-FSTAB -/dev/incomplete -/dev/device name - FSTAB + it "should extract blockdevice from second field" do + @provider.parse_line(@example_line)[:blockdevice].should == "/dev/rdsk/c0d0s0" + end - lambda{ @provider_class.to_line(parse[0]) }.should_not raise_error - end - end + it "should extract name from third field" do + @provider.parse_line(@example_line)[:name].should == "/" + end - describe provider_class, " when mounting an absent filesystem" do - include ParsedMountTesting + it "should extract fstype from fourth field" do + @provider.parse_line(@example_line)[:fstype].should == "ufs" + end - # #730 - Make sure 'flush' is called when a mount is moving from absent to mounted - it "should flush the fstab to disk" do - mount = mkmount + it "should extract pass from fifth field" do + @provider.parse_line(@example_line)[:pass].should == "1" + end - # Mark the mount as absent - mount.property_hash[:ensure] = :absent + it "should extract atboot from sixth field" do + @provider.parse_line(@example_line)[:atboot].should == "no" + end - mount.stubs(:mountcmd) # just so we don't actually try to mount anything + it "should extract options from seventh field" do + @provider.parse_line(@example_line)[:options].should == "-" + end - mount.expects(:flush) - mount.mount end - end - describe provider_class, " when modifying the filesystem tab" do - include ParsedMountTesting - before do - Puppet.settings.stubs(:use) - # Never write to disk, only to RAM. - #@provider_class.stubs(:filetype).returns(Puppet::Util::FileType.filetype(:ram)) - @provider_class.stubs(:target_object).returns(Puppet::Util::FileType.filetype(:ram).new("eh")) - @provider_class.clear + describe "on other platforms than Solaris", :if => Facter.value(:operatingsystem) != 'Solaris' do - @mount = mkmount - @target = @provider_class.default_target - end + before :each do + @example_line = "/dev/vg00/lv01\t/spare \t \t ext3 defaults\t1 2" + end - it "should write the mount to disk when :flush is called" do - old_text = @provider_class.target_object(@provider_class.default_target).read + it "should extract device from the first field" do + @provider.parse_line(@example_line)[:device].should == '/dev/vg00/lv01' + end - @mount.flush + it "should extract name from second field" do + @provider.parse_line(@example_line)[:name].should == "/spare" + end + + it "should extract fstype from third field" do + @provider.parse_line(@example_line)[:fstype].should == "ext3" + end + + it "should extract options from fourth field" do + @provider.parse_line(@example_line)[:options].should == "defaults" + end + + it "should extract dump from fifth field" do + @provider.parse_line(@example_line)[:dump].should == "1" + end + + it "should extract options from sixth field" do + @provider.parse_line(@example_line)[:pass].should == "2" + end - text = @provider_class.target_object(@provider_class.default_target).read - text.should == old_text + @mount.class.to_line(@mount.property_hash) + "\n" end + end - describe provider_class, " when parsing information about the root filesystem", :if => Facter["operatingsystem"].value != "Darwin" do + describe "mountinstances" do include ParsedMountTesting - before do - @mount = @mount_class.new :name => "/" - @provider = @mount.provider + it "should get name from mountoutput found on Solaris" do + Facter.stubs(:value).with(:operatingsystem).returns 'Solaris' + @provider.stubs(:mountcmd).returns(File.read(fake_mountoutput)) + mounts = @provider.mountinstances + mounts.size.should == 6 + mounts[0].should == { :name => '/', :mounted => :yes } + mounts[1].should == { :name => '/proc', :mounted => :yes } + mounts[2].should == { :name => '/etc/mnttab', :mounted => :yes } + mounts[3].should == { :name => '/tmp', :mounted => :yes } + mounts[4].should == { :name => '/export/home', :mounted => :yes } + mounts[5].should == { :name => '/ghost', :mounted => :yes } end - it "should have a filesystem tab" do - FileTest.should be_exist(@provider_class.default_target) + it "should get name from mountoutput found on HP-UX" do + Facter.stubs(:value).with(:operatingsystem).returns 'HP-UX' + @provider.stubs(:mountcmd).returns(File.read(fake_mountoutput)) + mounts = @provider.mountinstances + mounts.size.should == 17 + mounts[0].should == { :name => '/', :mounted => :yes } + mounts[1].should == { :name => '/devices', :mounted => :yes } + mounts[2].should == { :name => '/dev', :mounted => :yes } + mounts[3].should == { :name => '/system/contract', :mounted => :yes } + mounts[4].should == { :name => '/proc', :mounted => :yes } + mounts[5].should == { :name => '/etc/mnttab', :mounted => :yes } + mounts[6].should == { :name => '/etc/svc/volatile', :mounted => :yes } + mounts[7].should == { :name => '/system/object', :mounted => :yes } + mounts[8].should == { :name => '/etc/dfs/sharetab', :mounted => :yes } + mounts[9].should == { :name => '/lib/libc.so.1', :mounted => :yes } + mounts[10].should == { :name => '/dev/fd', :mounted => :yes } + mounts[11].should == { :name => '/tmp', :mounted => :yes } + mounts[12].should == { :name => '/var/run', :mounted => :yes } + mounts[13].should == { :name => '/export', :mounted => :yes } + mounts[14].should == { :name => '/export/home', :mounted => :yes } + mounts[15].should == { :name => '/rpool', :mounted => :yes } + mounts[16].should == { :name => '/ghost', :mounted => :yes } end - it "should find the root filesystem" do - @provider_class.prefetch("/" => @mount) - @mount.provider.property_hash[:ensure].should == :present + it "should get name from mountoutput found on Darwin" do + Facter.stubs(:value).with(:operatingsystem).returns 'Darwin' + @provider.stubs(:mountcmd).returns(File.read(fake_mountoutput)) + mounts = @provider.mountinstances + mounts.size.should == 6 + mounts[0].should == { :name => '/', :mounted => :yes } + mounts[1].should == { :name => '/dev', :mounted => :yes } + mounts[2].should == { :name => '/net', :mounted => :yes } + mounts[3].should == { :name => '/home', :mounted => :yes } + mounts[4].should == { :name => '/usr', :mounted => :yes } + mounts[5].should == { :name => '/ghost', :mounted => :yes } end - it "should determine that the root fs is mounted" do - @provider_class.prefetch("/" => @mount) - @mount.provider.should be_mounted + it "should get name from mountoutput found on Linux" do + Facter.stubs(:value).with(:operatingsystem).returns 'Gentoo' + @provider.stubs(:mountcmd).returns(File.read(fake_mountoutput)) + mounts = @provider.mountinstances + mounts[0].should == { :name => '/', :mounted => :yes } + mounts[1].should == { :name => '/lib64/rc/init.d', :mounted => :yes } + mounts[2].should == { :name => '/sys', :mounted => :yes } + mounts[3].should == { :name => '/usr/portage', :mounted => :yes } + mounts[4].should == { :name => '/ghost', :mounted => :yes } end + + it "should get name from mountoutput found on AIX" do + Facter.stubs(:value).with(:operatingsystem).returns 'AIX' + @provider.stubs(:mountcmd).returns(File.read(fake_mountoutput)) + mounts = @provider.mountinstances + mounts[0].should == { :name => '/', :mounted => :yes } + mounts[1].should == { :name => '/tmp', :mounted => :yes } + mounts[2].should == { :name => '/home', :mounted => :yes } + mounts[3].should == { :name => '/usr', :mounted => :yes } + mounts[4].should == { :name => '/usr/code', :mounted => :yes } + end + + it "should raise an error if a line is not understandable" do + @provider.stubs(:mountcmd).returns("bazinga!") + lambda { @provider.mountinstances }.should raise_error Puppet::Error + end + end - describe provider_class, " when mounting and unmounting" do + describe "when prefetching" do include ParsedMountTesting - it "should call the 'mount' command to mount the filesystem" + before :each do + # Note: we have to stub default_target before creating resources + # because it is used by Puppet::Type::Mount.new to populate the + # :target property. + @provider.stubs(:default_target).returns fake_fstab + + @res_ghost = Puppet::Type::Mount.new(:name => '/ghost') # in no fake fstab + @res_mounted = Puppet::Type::Mount.new(:name => '/') # in every fake fstab + @res_unmounted = Puppet::Type::Mount.new(:name => '/boot') # in every fake fstab + @res_absent = Puppet::Type::Mount.new(:name => '/absent') # in no fake fstab + + # Simulate transaction.rb:prefetch + @resource_hash = {} + [@res_ghost, @res_mounted, @res_unmounted, @res_absent].each do |resource| + @resource_hash[resource.name] = resource + end + + @provider.stubs(:mountcmd).returns File.read(fake_mountoutput) + end + + it "should set :ensure to :unmounted if found in fstab but not mounted" do + @provider.prefetch(@resource_hash) + @res_unmounted.provider.get(:ensure).should == :unmounted + end + + it "should set :ensure to :mounted if found in fstab and mounted" do + @provider.prefetch(@resource_hash) + @res_ghost.provider.get(:ensure).should == :ghost + end + + it "should set :ensure to :ghost if not found in fstab but mounted" do + @provider.prefetch(@resource_hash) + @res_mounted.provider.get(:ensure).should == :mounted + end - it "should call the 'unmount' command to unmount the filesystem" + it "should set :ensure to :absent if not found in fstab and not mounted" do + @provider.prefetch(@resource_hash) + @res_absent.provider.get(:ensure).should == :absent + end - it "should specify the filesystem when remounting a filesystem" end + end diff --git a/spec/unit/provider/mount_spec.rb b/spec/unit/provider/mount_spec.rb index f567a4a40..3fc8a8664 100755 --- a/spec/unit/provider/mount_spec.rb +++ b/spec/unit/provider/mount_spec.rb @@ -1,145 +1,146 @@ #!/usr/bin/env ruby require File.dirname(__FILE__) + '/../../spec_helper' require 'puppet/provider/mount' describe Puppet::Provider::Mount do before :each do @mounter = Object.new @mounter.extend(Puppet::Provider::Mount) @name = "/" @resource = stub 'resource' @resource.stubs(:[]).with(:name).returns(@name) @mounter.stubs(:resource).returns(@resource) end describe Puppet::Provider::Mount, " when mounting" do - it "should use the 'mountcmd' method to mount" do - @mounter.stubs(:options).returns(nil) - @mounter.expects(:mountcmd) - - @mounter.mount + before :each do + @mounter.stubs(:get).with(:ensure).returns(:mounted) end - it "should flush before mounting if a flush method exists" do - @mounter.meta_def(:flush) { } - @mounter.expects(:flush) - @mounter.stubs(:mountcmd) + it "should use the 'mountcmd' method to mount" do @mounter.stubs(:options).returns(nil) + @mounter.expects(:mountcmd) @mounter.mount end it "should add the options following '-o' if they exist and are not set to :absent" do @mounter.stubs(:options).returns("ro") @mounter.expects(:mountcmd).with { |*ary| ary[0] == "-o" and ary[1] == "ro" } @mounter.mount end it "should specify the filesystem name to the mount command" do @mounter.stubs(:options).returns(nil) @mounter.expects(:mountcmd).with { |*ary| ary[-1] == @name } @mounter.mount end + + it "should update the :ensure state to :mounted if it was :unmounted before" do + @mounter.expects(:mountcmd) + @mounter.stubs(:options).returns(nil) + @mounter.expects(:get).with(:ensure).returns(:unmounted) + @mounter.expects(:set).with(:ensure => :mounted) + @mounter.mount + end + + it "should update the :ensure state to :ghost if it was :absent before" do + @mounter.expects(:mountcmd) + @mounter.stubs(:options).returns(nil) + @mounter.expects(:get).with(:ensure).returns(:absent) + @mounter.expects(:set).with(:ensure => :ghost) + @mounter.mount + end + end describe Puppet::Provider::Mount, " when remounting" do it "should use '-o remount' if the resource specifies it supports remounting" do @mounter.stubs(:info) @resource.stubs(:[]).with(:remounts).returns(:true) @mounter.expects(:mountcmd).with("-o", "remount", @name) @mounter.remount end it "should unmount and mount if the resource does not specify it supports remounting" do @mounter.stubs(:info) @resource.stubs(:[]).with(:remounts).returns(false) @mounter.expects(:unmount) @mounter.expects(:mount) @mounter.remount end it "should log that it is remounting" do @resource.stubs(:[]).with(:remounts).returns(:true) @mounter.stubs(:mountcmd) @mounter.expects(:info).with("Remounting") @mounter.remount end end describe Puppet::Provider::Mount, " when unmounting" do + before :each do + @mounter.stubs(:get).with(:ensure).returns(:unmounted) + end + it "should call the :umount command with the resource name" do @mounter.expects(:umount).with(@name) @mounter.unmount end - end - - describe Puppet::Provider::Mount, " when determining if it is mounted" do - - it "should parse the results of running the mount command with no arguments" do - Facter.stubs(:value).returns("whatever") - @mounter.expects(:mountcmd).returns("") - @mounter.mounted? + it "should update the :ensure state to :absent if it was :ghost before" do + @mounter.expects(:umount).with(@name).returns true + @mounter.expects(:get).with(:ensure).returns(:ghost) + @mounter.expects(:set).with(:ensure => :absent) + @mounter.unmount end - it "should match ' on /private/var/automount' if the operating system is Darwin" do - Facter.stubs(:value).with("operatingsystem").returns("Darwin") - @mounter.expects(:mountcmd).returns("/dev/whatever on /private/var/automount/\ndevfs on /dev") - - @mounter.should be_mounted + it "should update the :ensure state to :unmounted if it was :mounted before" do + @mounter.expects(:umount).with(@name).returns true + @mounter.expects(:get).with(:ensure).returns(:mounted) + @mounter.expects(:set).with(:ensure => :unmounted) + @mounter.unmount end - it "should match ' on ' if the operating system is Darwin" do - Facter.stubs(:value).with("operatingsystem").returns("Darwin") - @mounter.expects(:mountcmd).returns("/dev/disk03 on / (local, journaled)\ndevfs on /dev") - - @mounter.should be_mounted - end + end - it "should match '^ on' if the operating system is Solaris" do - Facter.stubs(:value).with("operatingsystem").returns("Solaris") - @mounter.expects(:mountcmd).returns("/ on /dev/dsk/whatever\n/var on /dev/dsk/other") + describe Puppet::Provider::Mount, " when determining if it is mounted" do - @mounter.should be_mounted + it "should query the property_hash" do + @mounter.expects(:get).with(:ensure).returns(:mounted) + @mounter.mounted? end - it "should match '^ on' if the operating system is HP-UX" do - Facter.stubs(:value).with("operatingsystem").returns("HP-UX") - @mounter.expects(:mountcmd).returns("/ on /dev/dsk/whatever\n/var on /dev/dsk/other") - - @mounter.should be_mounted + it "should return true if prefetched value is :mounted" do + @mounter.stubs(:get).with(:ensure).returns(:mounted) + @mounter.mounted? == true end - it "should match mounted devices if the operating system is AIX" do - Facter.stubs(:value).with("operatingsystem").returns("AIX") - mount_data = File.read(File.join(File.dirname(__FILE__), '..', '..', 'fixtures', 'unit', 'provider', 'mount', 'mount-output.aix.txt')) - @mounter.expects(:mountcmd).returns(mount_data) - - @mounter.should be_mounted + it "should return true if prefetched value is :ghost" do + @mounter.stubs(:get).with(:ensure).returns(:ghost) + @mounter.mounted? == true end - it "should match ' on ' if the operating system is not Darwin, Solaris, or HP-UX" do - Facter.stubs(:value).with("operatingsystem").returns("Debian") - @mounter.expects(:mountcmd).returns("/dev/dsk/whatever on / and stuff\n/dev/other/disk on /var and stuff") - - @mounter.should be_mounted + it "should return false if prefetched value is :absent" do + @mounter.stubs(:get).with(:ensure).returns(:absent) + @mounter.mounted? == false end - it "should not be considered mounted if it did not match the mount output" do - Facter.stubs(:value).with("operatingsystem").returns("Debian") - @mounter.expects(:mountcmd).returns("/dev/dsk/whatever on /something/else and stuff\n/dev/other/disk on /var and stuff") - - @mounter.should_not be_mounted + it "should return false if prefetched value is :unmounted" do + @mounter.stubs(:get).with(:ensure).returns(:unmounted) + @mounter.mounted? == false end + end + end diff --git a/spec/unit/provider/ssh_authorized_key/parsed_spec.rb b/spec/unit/provider/ssh_authorized_key/parsed_spec.rb index 11e9233e0..2e5be165a 100755 --- a/spec/unit/provider/ssh_authorized_key/parsed_spec.rb +++ b/spec/unit/provider/ssh_authorized_key/parsed_spec.rb @@ -1,229 +1,230 @@ #!/usr/bin/env ruby require File.dirname(__FILE__) + '/../../../spec_helper' require 'puppet_spec/files' require 'puppettest/support/utils' require 'puppettest/fileparsing' require 'puppettest/fakes' provider_class = Puppet::Type.type(:ssh_authorized_key).provider(:parsed) describe provider_class do include PuppetSpec::Files extend PuppetTest::Support::Utils include PuppetTest include PuppetTest::FileParsing before :each do @sshauthkey_class = Puppet::Type.type(:ssh_authorized_key) @provider = @sshauthkey_class.provider(:parsed) @keyfile = tmpfile('authorized_keys') @provider.any_instance.stubs(:target).returns @keyfile @user = 'random_bob' Puppet::Util.stubs(:uid).with(@user).returns 12345 end after :each do @provider.initvars end def mkkey(args) fakeresource = fakeresource(:ssh_authorized_key, args[:name]) fakeresource.stubs(:should).with(:user).returns @user fakeresource.stubs(:should).with(:target).returns @keyfile key = @provider.new(fakeresource) args.each do |p,v| key.send(p.to_s + "=", v) end key end def genkey(key) @provider.stubs(:filetype).returns(Puppet::Util::FileType::FileTypeRam) File.stubs(:chown) File.stubs(:chmod) Puppet::Util::SUIDManager.stubs(:asuser).yields key.flush @provider.target_object(@keyfile).read end fakedata("data/providers/ssh_authorized_key/parsed").each { |file| it "should be able to parse example data in #{file}" do fakedataparse(file) end } it "should be able to generate a basic authorized_keys file" do key = mkkey( { :name => "Just Testing", :key => "AAAAfsfddsjldjgksdflgkjsfdlgkj", :type => "ssh-dss", :ensure => :present, :options => [:absent] }) genkey(key).should == "ssh-dss AAAAfsfddsjldjgksdflgkjsfdlgkj Just Testing\n" end it "should be able to generate a authorized_keys file with options" do key = mkkey( { :name => "root@localhost", :key => "AAAAfsfddsjldjgksdflgkjsfdlgkj", :type => "ssh-rsa", :ensure => :present, :options => ['from="192.168.1.1"', "no-pty", "no-X11-forwarding"] }) genkey(key).should == "from=\"192.168.1.1\",no-pty,no-X11-forwarding ssh-rsa AAAAfsfddsjldjgksdflgkjsfdlgkj root@localhost\n" end it "should be able to parse options containing commas via its parse_options method" do options = %w{from="host1.reductlivelabs.com,host.reductivelabs.com" command="/usr/local/bin/run" ssh-pty} optionstr = options.join(", ") @provider.parse_options(optionstr).should == options end it "should use '' as name for entries that lack a comment" do line = "ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAut8aOSxenjOqF527dlsdHWV4MNoAsX14l9M297+SQXaQ5Z3BedIxZaoQthkDALlV/25A1COELrg9J2MqJNQc8Xe9XQOIkBQWWinUlD/BXwoOTWEy8C8zSZPHZ3getMMNhGTBO+q/O+qiJx3y5cA4MTbw2zSxukfWC87qWwcZ64UUlegIM056vPsdZWFclS9hsROVEa57YUMrehQ1EGxT4Z5j6zIopufGFiAPjZigq/vqgcAqhAKP6yu4/gwO6S9tatBeEjZ8fafvj1pmvvIplZeMr96gHE7xS3pEEQqnB3nd4RY7AF6j9kFixnsytAUO7STPh/M3pLiVQBN89TvWPQ==" @provider.parse(line)[0][:name].should == "" end end describe provider_class do before :each do @resource = stub("resource", :name => "foo") @resource.stubs(:[]).returns "foo" + @resource.class.stubs(:key_attributes).returns( [:name] ) @provider = provider_class.new(@resource) provider_class.stubs(:filetype).returns(Puppet::Util::FileType::FileTypeRam) Puppet::Util::SUIDManager.stubs(:asuser).yields end describe "when flushing" do before :each do # Stub file and directory operations Dir.stubs(:mkdir) File.stubs(:chmod) File.stubs(:chown) end describe "and both a user and a target have been specified" do before :each do Puppet::Util.stubs(:uid).with("random_bob").returns 12345 @resource.stubs(:should).with(:user).returns "random_bob" target = "/tmp/.ssh_dir/place_to_put_authorized_keys" @resource.stubs(:should).with(:target).returns target end it "should create the directory" do File.stubs(:exist?).with("/tmp/.ssh_dir").returns false Dir.expects(:mkdir).with("/tmp/.ssh_dir", 0700) @provider.flush end it "should chown the directory to the user" do uid = Puppet::Util.uid("random_bob") File.expects(:chown).with(uid, nil, "/tmp/.ssh_dir") @provider.flush end it "should chown the key file to the user" do uid = Puppet::Util.uid("random_bob") File.expects(:chown).with(uid, nil, "/tmp/.ssh_dir/place_to_put_authorized_keys") @provider.flush end it "should chmod the key file to 0600" do File.expects(:chmod).with(0600, "/tmp/.ssh_dir/place_to_put_authorized_keys") @provider.flush end end describe "and a user has been specified with no target" do before :each do @resource.stubs(:should).with(:user).returns "nobody" @resource.stubs(:should).with(:target).returns nil # # I'd like to use random_bob here and something like # # File.stubs(:expand_path).with("~random_bob/.ssh").returns "/users/r/random_bob/.ssh" # # but mocha objects strenuously to stubbing File.expand_path # so I'm left with using nobody. @dir = File.expand_path("~nobody/.ssh") end it "should create the directory if it doesn't exist" do File.stubs(:exist?).with(@dir).returns false Dir.expects(:mkdir).with(@dir,0700) @provider.flush end it "should not create or chown the directory if it already exist" do File.stubs(:exist?).with(@dir).returns false Dir.expects(:mkdir).never @provider.flush end it "should chown the directory to the user if it creates it" do File.stubs(:exist?).with(@dir).returns false Dir.stubs(:mkdir).with(@dir,0700) uid = Puppet::Util.uid("nobody") File.expects(:chown).with(uid, nil, @dir) @provider.flush end it "should not create or chown the directory if it already exist" do File.stubs(:exist?).with(@dir).returns false Dir.expects(:mkdir).never File.expects(:chown).never @provider.flush end it "should chown the key file to the user" do uid = Puppet::Util.uid("nobody") File.expects(:chown).with(uid, nil, File.expand_path("~nobody/.ssh/authorized_keys")) @provider.flush end it "should chmod the key file to 0600" do File.expects(:chmod).with(0600, File.expand_path("~nobody/.ssh/authorized_keys")) @provider.flush end end describe "and a target has been specified with no user" do before :each do @resource.stubs(:should).with(:user).returns nil @resource.stubs(:should).with(:target).returns("/tmp/.ssh_dir/place_to_put_authorized_keys") end it "should raise an error" do proc { @provider.flush }.should raise_error end end describe "and a invalid user has been specified with no target" do before :each do @resource.stubs(:should).with(:user).returns "thisusershouldnotexist" @resource.stubs(:should).with(:target).returns nil end it "should catch an exception and raise a Puppet error" do lambda { @provider.flush }.should raise_error(Puppet::Error) end end end end diff --git a/spec/unit/provider/user/user_role_add_spec.rb b/spec/unit/provider/user/user_role_add_spec.rb index 9cf649267..f73942389 100644 --- a/spec/unit/provider/user/user_role_add_spec.rb +++ b/spec/unit/provider/user/user_role_add_spec.rb @@ -1,266 +1,267 @@ #!/usr/bin/env ruby require File.dirname(__FILE__) + '/../../../spec_helper' provider_class = Puppet::Type.type(:user).provider(:user_role_add) describe provider_class do before do @resource = stub("resource", :name => "myuser", :managehome? => nil) @resource.stubs(:should).returns "fakeval" @resource.stubs(:[]).returns "fakeval" @resource.stubs(:allowdupe?).returns false @provider = provider_class.new(@resource) end describe "when calling command" do before do klass = stub("provider") klass.stubs(:command).with(:foo).returns("userfoo") klass.stubs(:command).with(:role_foo).returns("rolefoo") @provider.stubs(:class).returns(klass) end it "should use the command if not a role and ensure!=role" do @provider.stubs(:is_role?).returns(false) @provider.stubs(:exists?).returns(false) @resource.stubs(:[]).with(:ensure).returns(:present) @provider.command(:foo).should == "userfoo" end it "should use the role command when a role" do @provider.stubs(:is_role?).returns(true) @provider.command(:foo).should == "rolefoo" end it "should use the role command when !exists and ensure=role" do @provider.stubs(:is_role?).returns(false) @provider.stubs(:exists?).returns(false) @resource.stubs(:[]).with(:ensure).returns(:role) @provider.command(:foo).should == "rolefoo" end end describe "when calling transition" do it "should return the type set to whatever is passed in" do @provider.expects(:command).with(:modify).returns("foomod") @provider.transition("bar").include?("type=bar") end end describe "when calling create" do before do @provider.stubs(:password=) end it "should use the add command when the user is not a role" do @provider.stubs(:is_role?).returns(false) @provider.expects(:addcmd).returns("useradd") @provider.expects(:run).at_least_once @provider.create end it "should use transition(normal) when the user is a role" do @provider.stubs(:is_role?).returns(true) @provider.expects(:transition).with("normal") @provider.expects(:run) @provider.create end it "should set password age rules" do @resource = Puppet::Type.type(:user).new :name => "myuser", :password_min_age => 5, :password_max_age => 10, :provider => :user_role_add @provider = provider_class.new(@resource) @provider.stubs(:user_attributes) @provider.stubs(:execute) @provider.expects(:execute).with { |cmd, *args| args == ["-n", 5, "-x", 10, "myuser"] } @provider.create end end describe "when calling destroy" do it "should use the delete command if the user exists and is not a role" do @provider.stubs(:exists?).returns(true) @provider.stubs(:is_role?).returns(false) @provider.expects(:deletecmd) @provider.expects(:run) @provider.destroy end it "should use the delete command if the user is a role" do @provider.stubs(:exists?).returns(true) @provider.stubs(:is_role?).returns(true) @provider.expects(:deletecmd) @provider.expects(:run) @provider.destroy end end describe "when calling create_role" do it "should use the transition(role) if the user exists" do @provider.stubs(:exists?).returns(true) @provider.stubs(:is_role?).returns(false) @provider.expects(:transition).with("role") @provider.expects(:run) @provider.create_role end it "should use the add command when role doesn't exists" do @provider.stubs(:exists?).returns(false) @provider.expects(:addcmd) @provider.expects(:run) @provider.create_role end end describe "when allow duplicate is enabled" do before do @resource.expects(:allowdupe?).returns true + @resource.stubs(:system?) @provider.stubs(:is_role?).returns(false) @provider.stubs(:execute) @provider.expects(:execute).with { |args| args.include?("-o") } end it "should add -o when the user is being created" do @provider.stubs(:password=) @provider.create end it "should add -o when the uid is being modified" do @provider.uid = 150 end end [:roles, :auths, :profiles].each do |val| describe "when getting #{val}" do it "should get the user_attributes" do @provider.expects(:user_attributes) @provider.send(val) end it "should get the #{val} attribute" do attributes = mock("attributes") attributes.expects(:[]).with(val) @provider.stubs(:user_attributes).returns(attributes) @provider.send(val) end end end describe "when getting the keys" do it "should get the user_attributes" do @provider.expects(:user_attributes) @provider.keys end it "should call removed_managed_attributes" do @provider.stubs(:user_attributes).returns({ :type => "normal", :foo => "something" }) @provider.expects(:remove_managed_attributes) @provider.keys end it "should removed managed attribute (type, auths, roles, etc)" do @provider.stubs(:user_attributes).returns({ :type => "normal", :foo => "something" }) @provider.keys.should == { :foo => "something" } end end describe "when adding properties" do it "should call build_keys_cmd" do @resource.stubs(:should).returns "" @resource.expects(:should).with(:keys).returns({ :foo => "bar" }) @provider.expects(:build_keys_cmd).returns([]) @provider.add_properties end it "should add the elements of the keys hash to an array" do @resource.stubs(:should).returns "" @resource.expects(:should).with(:keys).returns({ :foo => "bar"}) @provider.add_properties.must == ["-K", "foo=bar"] end end describe "when calling build_keys_cmd" do it "should build cmd array with keypairs seperated by -K ending with user" do @provider.build_keys_cmd({"foo" => "bar", "baz" => "boo"}).should.eql? ["-K", "foo=bar", "-K", "baz=boo"] end end describe "when setting the keys" do before do @provider.stubs(:is_role?).returns(false) end it "should run a command" do @provider.expects(:run) @provider.keys=({}) end it "should build the command" do @resource.stubs(:[]).with(:name).returns("someuser") @provider.stubs(:command).returns("usermod") @provider.expects(:build_keys_cmd).returns(["-K", "foo=bar"]) @provider.expects(:run).with(["usermod", "-K", "foo=bar", "someuser"], "modify attribute key pairs") @provider.keys=({}) end end describe "when getting the hashed password" do before do @array = mock "array" end it "should readlines of /etc/shadow" do File.expects(:readlines).with("/etc/shadow").returns([]) @provider.password end it "should reject anything that doesn't start with alpha numerics" do @array.expects(:reject).returns([]) File.stubs(:readlines).with("/etc/shadow").returns(@array) @provider.password end it "should collect splitting on ':'" do @array.stubs(:reject).returns(@array) @array.expects(:collect).returns([]) File.stubs(:readlines).with("/etc/shadow").returns(@array) @provider.password end it "should find the matching user" do @resource.stubs(:[]).with(:name).returns("username") @array.stubs(:reject).returns(@array) @array.stubs(:collect).returns([["username", "hashedpassword"], ["someoneelse", "theirpassword"]]) File.stubs(:readlines).with("/etc/shadow").returns(@array) @provider.password.must == "hashedpassword" end it "should get the right password" do @resource.stubs(:[]).with(:name).returns("username") File.stubs(:readlines).with("/etc/shadow").returns(["#comment", " nonsense", " ", "username:hashedpassword:stuff:foo:bar:::", "other:pword:yay:::"]) @provider.password.must == "hashedpassword" end end describe "when setting the password" do #how can you mock these blocks up? it "should open /etc/shadow for reading and /etc/shadow_tmp for writing" do File.expects(:open).with("/etc/shadow", "r") File.stubs(:rename) @provider.password=("hashedpassword") end it "should rename the /etc/shadow_tmp to /etc/shadow" do File.stubs(:open).with("/etc/shadow", "r") File.expects(:rename).with("/etc/shadow_tmp", "/etc/shadow") @provider.password=("hashedpassword") end end describe "#shadow_entry" do it "should return the line for the right user" do File.stubs(:readlines).returns(["someuser:!:10:5:20:7:1::\n", "fakeval:*:20:10:30:7:2::\n", "testuser:*:30:15:40:7:3::\n"]) @provider.shadow_entry.should == ["fakeval", "*", "20", "10", "30", "7", "2"] end end end diff --git a/spec/unit/provider/user/useradd_spec.rb b/spec/unit/provider/user/useradd_spec.rb index 9ebba596c..fd75c43f3 100755 --- a/spec/unit/provider/user/useradd_spec.rb +++ b/spec/unit/provider/user/useradd_spec.rb @@ -1,176 +1,216 @@ #!/usr/bin/env ruby require File.dirname(__FILE__) + '/../../../spec_helper' provider_class = Puppet::Type.type(:user).provider(:useradd) describe provider_class do before do @resource = stub("resource", :name => "myuser", :managehome? => nil) @resource.stubs(:should).returns "fakeval" @resource.stubs(:[]).returns "fakeval" @provider = provider_class.new(@resource) end # #1360 it "should add -o when allowdupe is enabled and the user is being created" do @resource.expects(:allowdupe?).returns true + @resource.expects(:system?).returns true @provider.stubs(:execute) @provider.expects(:execute).with { |args| args.include?("-o") } @provider.create end it "should add -o when allowdupe is enabled and the uid is being modified" do @resource.expects(:allowdupe?).returns true @provider.expects(:execute).with { |args| args.include?("-o") } @provider.uid = 150 end + it "should add -r when system is enabled" do + @resource.expects(:allowdupe?).returns true + @resource.expects(:system?).returns true + @provider.stubs(:execute) + @provider.expects(:execute).with { |args| args.include?("-r") } + @provider.create + end + it "should set password age rules" do provider_class.has_feature :manages_password_age @resource = Puppet::Type.type(:user).new :name => "myuser", :password_min_age => 5, :password_max_age => 10, :provider => :useradd @provider = provider_class.new(@resource) @provider.stubs(:execute) @provider.expects(:execute).with { |cmd, *args| args == ["-m", 5, "-M", 10, "myuser"] } @provider.create end describe "when checking to add allow dup" do it "should check allow dup" do @resource.expects(:allowdupe?) @provider.check_allow_dup end it "should return an array with a flag if dup is allowed" do @resource.stubs(:allowdupe?).returns true @provider.check_allow_dup.must == ["-o"] end it "should return an empty array if no dup is allowed" do @resource.stubs(:allowdupe?).returns false @provider.check_allow_dup.must == [] end end + describe "when checking to add system users" do + it "should check system users" do + @resource.expects(:system?) + @provider.check_system_users + end + + it "should return an array with a flag if it's a system user" do + @resource.stubs(:system?).returns true + @provider.check_system_users.must == ["-r"] + end + + it "should return an empty array if it's not a system user" do + @resource.stubs(:system?).returns false + @provider.check_system_users.must == [] + end + end + describe "when checking manage home" do it "should check manage home" do @resource.expects(:managehome?) @provider.check_manage_home end it "should return an array with -m flag if home is managed" do @resource.stubs(:managehome?).returns true @provider.check_manage_home.must == ["-m"] end it "should return an array with -M if home is not managed and on Redhat" do Facter.stubs(:value).with("operatingsystem").returns("RedHat") @resource.stubs(:managehome?).returns false @provider.check_manage_home.must == ["-M"] end it "should return an empty array if home is not managed and not on Redhat" do Facter.stubs(:value).with("operatingsystem").returns("some OS") @resource.stubs(:managehome?).returns false @provider.check_manage_home.must == [] end end describe "when adding properties" do it "should get the valid properties" it "should not add the ensure property" it "should add the flag and value to an array" it "should return and array of flags and values" end describe "when calling addcmd" do before do @resource.stubs(:allowdupe?).returns true @resource.stubs(:managehome?).returns true + @resource.stubs(:system?).returns true end it "should call command with :add" do @provider.expects(:command).with(:add) @provider.addcmd end it "should add properties" do @provider.expects(:add_properties).returns([]) @provider.addcmd end it "should check and add if dup allowed" do @provider.expects(:check_allow_dup).returns([]) @provider.addcmd end it "should check and add if home is managed" do @provider.expects(:check_manage_home).returns([]) @provider.addcmd end it "should add the resource :name" do @resource.expects(:[]).with(:name) @provider.addcmd end + it "should return an array with -r if system? is true" do + resource = Puppet::Type.type(:user).new( :name => "bob", :system => true) + + provider_class.new( resource ).addcmd.should include("-r") + end + + it "should return an array without -r if system? is false" do + resource = Puppet::Type.type(:user).new( :name => "bob", :system => false) + + provider_class.new( resource ).addcmd.should_not include("-r") + end + it "should return an array with full command" do @provider.stubs(:command).with(:add).returns("useradd") @provider.stubs(:add_properties).returns(["-G", "somegroup"]) @resource.stubs(:[]).with(:name).returns("someuser") @resource.stubs(:[]).with(:expiry).returns("somedate") - @provider.addcmd.must == ["useradd", "-G", "somegroup", "-o", "-m", '-e somedate', "someuser"] + @provider.addcmd.must == ["useradd", "-G", "somegroup", "-o", "-m", '-e somedate', "-r", "someuser"] end - it "should return an array without -e if expery is undefined full command" do + it "should return an array without -e if expiry is undefined full command" do @provider.stubs(:command).with(:add).returns("useradd") @provider.stubs(:add_properties).returns(["-G", "somegroup"]) @resource.stubs(:[]).with(:name).returns("someuser") @resource.stubs(:[]).with(:expiry).returns nil - @provider.addcmd.must == ["useradd", "-G", "somegroup", "-o", "-m", "someuser"] + @provider.addcmd.must == ["useradd", "-G", "somegroup", "-o", "-m", "-r", "someuser"] end end describe "when calling passcmd" do before do @resource.stubs(:allowdupe?).returns true @resource.stubs(:managehome?).returns true + @resource.stubs(:system?).returns true end it "should call command with :pass" do @provider.expects(:command).with(:password) @provider.passcmd end it "should return nil if neither min nor max is set" do @resource.stubs(:should).with(:password_min_age).returns nil @resource.stubs(:should).with(:password_max_age).returns nil @provider.passcmd.must == nil end it "should return a chage command array with -m and the user name if password_min_age is set" do @provider.stubs(:command).with(:password).returns("chage") @resource.stubs(:[]).with(:name).returns("someuser") @resource.stubs(:should).with(:password_min_age).returns 123 @resource.stubs(:should).with(:password_max_age).returns nil @provider.passcmd.must == ['chage','-m',123,'someuser'] end it "should return a chage command array with -M if password_max_age is set" do @provider.stubs(:command).with(:password).returns("chage") @resource.stubs(:[]).with(:name).returns("someuser") @resource.stubs(:should).with(:password_min_age).returns nil @resource.stubs(:should).with(:password_max_age).returns 999 @provider.passcmd.must == ['chage','-M',999,'someuser'] end it "should return a chage command array with -M -m if both password_min_age and password_max_age are set" do @provider.stubs(:command).with(:password).returns("chage") @resource.stubs(:[]).with(:name).returns("someuser") @resource.stubs(:should).with(:password_min_age).returns 123 @resource.stubs(:should).with(:password_max_age).returns 999 @provider.passcmd.must == ['chage','-m',123,'-M',999,'someuser'] end end end diff --git a/spec/unit/reports/store_spec.rb b/spec/unit/reports/store_spec.rb index 1acb5badd..9d9042386 100644 --- a/spec/unit/reports/store_spec.rb +++ b/spec/unit/reports/store_spec.rb @@ -1,31 +1,31 @@ #!/usr/bin/env ruby Dir.chdir(File.dirname(__FILE__)) { (s = lambda { |f| File.exist?(f) ? require(f) : Dir.chdir("..") { s.call(f) } }).call("spec/spec_helper.rb") } require 'puppet/reports' require 'time' processor = Puppet::Reports.report(:store) describe processor do describe "#process" do include PuppetSpec::Files before :each do - Puppet[:reportdir] = tmpdir('reports') + Puppet[:reportdir] = tmpdir('reports') << '/reports' @report = YAML.load_file(File.join(PuppetSpec::FIXTURE_DIR, 'yaml/report2.6.x.yaml')).extend processor end it "should create a report directory for the client if one doesn't exist" do @report.process File.should be_directory(File.join(Puppet[:reportdir], @report.host)) end it "should write the report to the file in YAML" do Time.stubs(:now).returns(Time.parse("2011-01-06 12:00:00 UTC")) @report.process File.read(File.join(Puppet[:reportdir], @report.host, "201101061200.yaml")).should == @report.to_yaml end end end diff --git a/spec/unit/resource_spec.rb b/spec/unit/resource_spec.rb index ff31b2492..345ccd06e 100755 --- a/spec/unit/resource_spec.rb +++ b/spec/unit/resource_spec.rb @@ -1,786 +1,812 @@ #!/usr/bin/env ruby require File.dirname(__FILE__) + '/../spec_helper' require 'puppet/resource' describe Puppet::Resource do before do @basepath = Puppet.features.posix? ? "/somepath" : "C:/somepath" end [:catalog, :file, :line].each do |attr| it "should have an #{attr} attribute" do resource = Puppet::Resource.new("file", "/my/file") resource.should respond_to(attr) resource.should respond_to(attr.to_s + "=") end end it "should have a :title attribute" do Puppet::Resource.new(:user, "foo").title.should == "foo" end it "should require the type and title" do lambda { Puppet::Resource.new }.should raise_error(ArgumentError) end it "should canonize types to capitalized strings" do Puppet::Resource.new(:user, "foo").type.should == "User" end it "should canonize qualified types so all strings are capitalized" do Puppet::Resource.new("foo::bar", "foo").type.should == "Foo::Bar" end it "should tag itself with its type" do Puppet::Resource.new("file", "/f").should be_tagged("file") end it "should tag itself with its title if the title is a valid tag" do Puppet::Resource.new("user", "bar").should be_tagged("bar") end it "should not tag itself with its title if the title is a not valid tag" do Puppet::Resource.new("file", "/bar").should_not be_tagged("/bar") end it "should allow setting of attributes" do Puppet::Resource.new("file", "/bar", :file => "/foo").file.should == "/foo" Puppet::Resource.new("file", "/bar", :exported => true).should be_exported end it "should set its type to 'Class' and its title to the passed title if the passed type is :component and the title has no square brackets in it" do ref = Puppet::Resource.new(:component, "foo") ref.type.should == "Class" ref.title.should == "Foo" end it "should interpret the title as a reference and assign appropriately if the type is :component and the title contains square brackets" do ref = Puppet::Resource.new(:component, "foo::bar[yay]") ref.type.should == "Foo::Bar" ref.title.should == "yay" end it "should set the type to 'Class' if it is nil and the title contains no square brackets" do ref = Puppet::Resource.new(nil, "yay") ref.type.should == "Class" ref.title.should == "Yay" end it "should interpret the title as a reference and assign appropriately if the type is nil and the title contains square brackets" do ref = Puppet::Resource.new(nil, "foo::bar[yay]") ref.type.should == "Foo::Bar" ref.title.should == "yay" end it "should interpret the title as a reference and assign appropriately if the type is nil and the title contains nested square brackets" do ref = Puppet::Resource.new(nil, "foo::bar[baz[yay]]") ref.type.should == "Foo::Bar" ref.title.should =="baz[yay]" end it "should interpret the type as a reference and assign appropriately if the title is nil and the type contains square brackets" do ref = Puppet::Resource.new("foo::bar[baz]") ref.type.should == "Foo::Bar" ref.title.should =="baz" end it "should be able to extract its information from a Puppet::Type instance" do ral = Puppet::Type.type(:file).new :path => @basepath+"/foo" ref = Puppet::Resource.new(ral) ref.type.should == "File" ref.title.should == @basepath+"/foo" end it "should fail if the title is nil and the type is not a valid resource reference string" do lambda { Puppet::Resource.new("foo") }.should raise_error(ArgumentError) end it 'should fail if strict is set and type does not exist' do - lambda { Puppet::Resource.new('foo', 'title', {:strict=>true}) }.should raise_error(ArgumentError, 'Invalid resource type foo') + lambda { Puppet::Resource.new('foo', 'title', {:strict=>true}) }.should raise_error(ArgumentError, 'Invalid resource type foo') end it 'should fail if strict is set and class does not exist' do - lambda { Puppet::Resource.new('Class', 'foo', {:strict=>true}) }.should raise_error(ArgumentError, 'Could not find declared class foo') + lambda { Puppet::Resource.new('Class', 'foo', {:strict=>true}) }.should raise_error(ArgumentError, 'Could not find declared class foo') end it "should fail if the title is a hash and the type is not a valid resource reference string" do lambda { Puppet::Resource.new({:type => "foo", :title => "bar"}) }.should raise_error(ArgumentError, 'Puppet::Resource.new does not take a hash as the first argument. Did you mean ("foo", "bar") ?' ) end it "should be able to produce a backward-compatible reference array" do Puppet::Resource.new("foobar", "/f").to_trans_ref.should == %w{Foobar /f} end it "should be taggable" do Puppet::Resource.ancestors.should be_include(Puppet::Util::Tagging) end it "should have an 'exported' attribute" do resource = Puppet::Resource.new("file", "/f") resource.exported = true resource.exported.should == true resource.should be_exported end it "should support an environment attribute" do Puppet::Resource.new("file", "/my/file", :environment => :foo).environment.name.should == :foo end describe "and munging its type and title" do describe "when modeling a builtin resource" do it "should be able to find the resource type" do Puppet::Resource.new("file", "/my/file").resource_type.should equal(Puppet::Type.type(:file)) end it "should set its type to the capitalized type name" do Puppet::Resource.new("file", "/my/file").type.should == "File" end end describe "when modeling a defined resource" do describe "that exists" do before do @type = Puppet::Resource::Type.new(:definition, "foo::bar") Puppet::Node::Environment.new.known_resource_types.add @type end it "should set its type to the capitalized type name" do Puppet::Resource.new("foo::bar", "/my/file").type.should == "Foo::Bar" end it "should be able to find the resource type" do Puppet::Resource.new("foo::bar", "/my/file").resource_type.should equal(@type) end it "should set its title to the provided title" do Puppet::Resource.new("foo::bar", "/my/file").title.should == "/my/file" end end describe "that does not exist" do it "should set its resource type to the capitalized resource type name" do Puppet::Resource.new("foo::bar", "/my/file").type.should == "Foo::Bar" end end end describe "when modeling a node" do # Life's easier with nodes, because they can't be qualified. it "should set its type to 'Node' and its title to the provided title" do node = Puppet::Resource.new("node", "foo") node.type.should == "Node" node.title.should == "foo" end end describe "when modeling a class" do it "should set its type to 'Class'" do Puppet::Resource.new("class", "foo").type.should == "Class" end describe "that exists" do before do @type = Puppet::Resource::Type.new(:hostclass, "foo::bar") Puppet::Node::Environment.new.known_resource_types.add @type end it "should set its title to the capitalized, fully qualified resource type" do Puppet::Resource.new("class", "foo::bar").title.should == "Foo::Bar" end it "should be able to find the resource type" do Puppet::Resource.new("class", "foo::bar").resource_type.should equal(@type) end end describe "that does not exist" do it "should set its type to 'Class' and its title to the capitalized provided name" do klass = Puppet::Resource.new("class", "foo::bar") klass.type.should == "Class" klass.title.should == "Foo::Bar" end end describe "and its name is set to the empty string" do it "should set its title to :main" do Puppet::Resource.new("class", "").title.should == :main end describe "and a class exists whose name is the empty string" do # this was a bit tough to track down it "should set its title to :main" do @type = Puppet::Resource::Type.new(:hostclass, "") Puppet::Node::Environment.new.known_resource_types.add @type Puppet::Resource.new("class", "").title.should == :main end end end describe "and its name is set to :main" do it "should set its title to :main" do Puppet::Resource.new("class", :main).title.should == :main end describe "and a class exists whose name is the empty string" do # this was a bit tough to track down it "should set its title to :main" do @type = Puppet::Resource::Type.new(:hostclass, "") Puppet::Node::Environment.new.known_resource_types.add @type Puppet::Resource.new("class", :main).title.should == :main end end end end end it "should return nil when looking up resource types that don't exist" do Puppet::Resource.new("foobar", "bar").resource_type.should be_nil end it "should not fail when an invalid parameter is used and strict mode is disabled" do type = Puppet::Resource::Type.new(:definition, "foobar") Puppet::Node::Environment.new.known_resource_types.add type resource = Puppet::Resource.new("foobar", "/my/file") resource[:yay] = true end it "should be considered equivalent to another resource if their type and title match and no parameters are set" do Puppet::Resource.new("file", "/f").should == Puppet::Resource.new("file", "/f") end it "should be considered equivalent to another resource if their type, title, and parameters are equal" do Puppet::Resource.new("file", "/f", :parameters => {:foo => "bar"}).should == Puppet::Resource.new("file", "/f", :parameters => {:foo => "bar"}) end it "should not be considered equivalent to another resource if their type and title match but parameters are different" do Puppet::Resource.new("file", "/f", :parameters => {:fee => "baz"}).should_not == Puppet::Resource.new("file", "/f", :parameters => {:foo => "bar"}) end it "should not be considered equivalent to a non-resource" do Puppet::Resource.new("file", "/f").should_not == "foo" end it "should not be considered equivalent to another resource if their types do not match" do Puppet::Resource.new("file", "/f").should_not == Puppet::Resource.new("exec", "/f") end it "should not be considered equivalent to another resource if their titles do not match" do Puppet::Resource.new("file", "/foo").should_not == Puppet::Resource.new("file", "/f") end describe "when referring to a resource with name canonicalization" do it "should canonicalize its own name" do res = Puppet::Resource.new("file", "/path/") res.uniqueness_key.should == ["/path"] res.ref.should == "File[/path/]" end end describe "when running in strict mode" do it "should be strict" do Puppet::Resource.new("file", "/path", :strict => true).should be_strict end it "should fail if invalid parameters are used" do lambda { Puppet::Resource.new("file", "/path", :strict => true, :parameters => {:nosuchparam => "bar"}) }.should raise_error end it "should fail if the resource type cannot be resolved" do lambda { Puppet::Resource.new("nosuchtype", "/path", :strict => true) }.should raise_error end end describe "when managing parameters" do before do @resource = Puppet::Resource.new("file", "/my/file") end it "should correctly detect when provided parameters are not valid for builtin types" do Puppet::Resource.new("file", "/my/file").should_not be_valid_parameter("foobar") end it "should correctly detect when provided parameters are valid for builtin types" do Puppet::Resource.new("file", "/my/file").should be_valid_parameter("mode") end it "should correctly detect when provided parameters are not valid for defined resource types" do type = Puppet::Resource::Type.new(:definition, "foobar") Puppet::Node::Environment.new.known_resource_types.add type Puppet::Resource.new("foobar", "/my/file").should_not be_valid_parameter("myparam") end it "should correctly detect when provided parameters are valid for defined resource types" do type = Puppet::Resource::Type.new(:definition, "foobar", :arguments => {"myparam" => nil}) Puppet::Node::Environment.new.known_resource_types.add type Puppet::Resource.new("foobar", "/my/file").should be_valid_parameter("myparam") end it "should allow setting and retrieving of parameters" do @resource[:foo] = "bar" @resource[:foo].should == "bar" end it "should allow setting of parameters at initialization" do Puppet::Resource.new("file", "/my/file", :parameters => {:foo => "bar"})[:foo].should == "bar" end it "should canonicalize retrieved parameter names to treat symbols and strings equivalently" do @resource[:foo] = "bar" @resource["foo"].should == "bar" end it "should canonicalize set parameter names to treat symbols and strings equivalently" do @resource["foo"] = "bar" @resource[:foo].should == "bar" end it "should set the namevar when asked to set the name" do resource = Puppet::Resource.new("user", "bob") Puppet::Type.type(:user).stubs(:key_attributes).returns [:myvar] resource[:name] = "bob" resource[:myvar].should == "bob" end it "should return the namevar when asked to return the name" do resource = Puppet::Resource.new("user", "bob") Puppet::Type.type(:user).stubs(:key_attributes).returns [:myvar] resource[:myvar] = "test" resource[:name].should == "test" end it "should be able to set the name for non-builtin types" do resource = Puppet::Resource.new(:foo, "bar") resource[:name] = "eh" lambda { resource[:name] = "eh" }.should_not raise_error end it "should be able to return the name for non-builtin types" do resource = Puppet::Resource.new(:foo, "bar") resource[:name] = "eh" resource[:name].should == "eh" end it "should be able to iterate over parameters" do @resource[:foo] = "bar" @resource[:fee] = "bare" params = {} @resource.each do |key, value| params[key] = value end params.should == {:foo => "bar", :fee => "bare"} end it "should include Enumerable" do @resource.class.ancestors.should be_include(Enumerable) end it "should have a method for testing whether a parameter is included" do @resource[:foo] = "bar" @resource.should be_has_key(:foo) @resource.should_not be_has_key(:eh) end it "should have a method for providing the list of parameters" do @resource[:foo] = "bar" @resource[:bar] = "foo" keys = @resource.keys keys.should be_include(:foo) keys.should be_include(:bar) end it "should have a method for providing the number of parameters" do @resource[:foo] = "bar" @resource.length.should == 1 end it "should have a method for deleting parameters" do @resource[:foo] = "bar" @resource.delete(:foo) @resource[:foo].should be_nil end it "should have a method for testing whether the parameter list is empty" do @resource.should be_empty @resource[:foo] = "bar" @resource.should_not be_empty end it "should be able to produce a hash of all existing parameters" do @resource[:foo] = "bar" @resource[:fee] = "yay" hash = @resource.to_hash hash[:foo].should == "bar" hash[:fee].should == "yay" end it "should not provide direct access to the internal parameters hash when producing a hash" do hash = @resource.to_hash hash[:foo] = "bar" @resource[:foo].should be_nil end it "should use the title as the namevar to the hash if no namevar is present" do resource = Puppet::Resource.new("user", "bob") Puppet::Type.type(:user).stubs(:key_attributes).returns [:myvar] resource.to_hash[:myvar].should == "bob" end it "should set :name to the title if :name is not present for non-builtin types" do krt = Puppet::Resource::TypeCollection.new("myenv") krt.add Puppet::Resource::Type.new(:definition, :foo) resource = Puppet::Resource.new :foo, "bar" resource.stubs(:known_resource_types).returns krt resource.to_hash[:name].should == "bar" end end describe "when serializing" do before do @resource = Puppet::Resource.new("file", "/my/file") @resource["one"] = "test" @resource["two"] = "other" end it "should be able to be dumped to yaml" do proc { YAML.dump(@resource) }.should_not raise_error end it "should produce an equivalent yaml object" do text = YAML.dump(@resource) newresource = YAML.load(text) newresource.title.should == @resource.title newresource.type.should == @resource.type %w{one two}.each do |param| newresource[param].should == @resource[param] end end end + describe "when loading 0.25.x storedconfigs YAML" do + before :each do + @old_storedconfig_yaml = %q{--- !ruby/object:Puppet::Resource::Reference +builtin_type: +title: /tmp/bar +type: File +} + end + + it "should deserialize a Puppet::Resource::Reference without exceptions" do + lambda { YAML.load(@old_storedconfig_yaml) }.should_not raise_error + end + + it "should deserialize as a Puppet::Resource::Reference as a Puppet::Resource" do + YAML.load(@old_storedconfig_yaml).class.should == Puppet::Resource + end + + it "should to_hash properly" do + YAML.load(@old_storedconfig_yaml).to_hash.should == { :path => "/tmp/bar" } + end + end + describe "when converting to a RAL resource" do it "should use the resource type's :new method to create the resource if the resource is of a builtin type" do resource = Puppet::Resource.new("file", @basepath+"/my/file") result = resource.to_ral result.should be_instance_of(Puppet::Type.type(:file)) result[:path].should == @basepath+"/my/file" end it "should convert to a component instance if the resource type is not of a builtin type" do resource = Puppet::Resource.new("foobar", "somename") result = resource.to_ral result.should be_instance_of(Puppet::Type.type(:component)) result.title.should == "Foobar[somename]" end end it "should be able to convert itself to Puppet code" do Puppet::Resource.new("one::two", "/my/file").should respond_to(:to_manifest) end describe "when converting to puppet code" do before do - @resource = Puppet::Resource.new("one::two", "/my/file", :parameters => {:noop => true, :foo => %w{one two}}) - end - - it "should print the type and title" do - @resource.to_manifest.should be_include("one::two { '/my/file':\n") - end - - it "should print each parameter, with the value single-quoted" do - @resource.to_manifest.should be_include(" noop => 'true'") + @resource = Puppet::Resource.new("one::two", "/my/file", + :parameters => { + :noop => true, + :foo => %w{one two}, + :ensure => 'present', + } + ) end - it "should print array values appropriately" do - @resource.to_manifest.should be_include(" foo => ['one','two']") + it "should align, sort and add trailing commas to attributes with ensure first" do + @resource.to_manifest.should == <<-HEREDOC.gsub(/^\s{8}/, '').gsub(/\n$/, '') + one::two { '/my/file': + ensure => 'present', + foo => ['one', 'two'], + noop => 'true', + } + HEREDOC end end it "should be able to convert itself to a TransObject instance" do Puppet::Resource.new("one::two", "/my/file").should respond_to(:to_trans) end describe "when converting to a TransObject" do describe "and the resource is not an instance of a builtin type" do before do @resource = Puppet::Resource.new("foo", "bar") end it "should return a simple TransBucket if it is not an instance of a builtin type" do bucket = @resource.to_trans bucket.should be_instance_of(Puppet::TransBucket) bucket.type.should == @resource.type bucket.name.should == @resource.title end it "should return a simple TransBucket if it is a stage" do @resource = Puppet::Resource.new("stage", "bar") bucket = @resource.to_trans bucket.should be_instance_of(Puppet::TransBucket) bucket.type.should == @resource.type bucket.name.should == @resource.title end it "should copy over the resource's file" do @resource.file = "/foo/bar" @resource.to_trans.file.should == "/foo/bar" end it "should copy over the resource's line" do @resource.line = 50 @resource.to_trans.line.should == 50 end end describe "and the resource is an instance of a builtin type" do before do @resource = Puppet::Resource.new("file", "bar") end it "should return a TransObject if it is an instance of a builtin resource type" do trans = @resource.to_trans trans.should be_instance_of(Puppet::TransObject) trans.type.should == "file" trans.name.should == @resource.title end it "should copy over the resource's file" do @resource.file = "/foo/bar" @resource.to_trans.file.should == "/foo/bar" end it "should copy over the resource's line" do @resource.line = 50 @resource.to_trans.line.should == 50 end # Only TransObjects support tags, annoyingly it "should copy over the resource's tags" do @resource.tag "foo" @resource.to_trans.tags.should == @resource.tags end it "should copy the resource's parameters into the transobject and convert the parameter name to a string" do @resource[:foo] = "bar" @resource.to_trans["foo"].should == "bar" end it "should be able to copy arrays of values" do @resource[:foo] = %w{yay fee} @resource.to_trans["foo"].should == %w{yay fee} end it "should reduce single-value arrays to just a value" do @resource[:foo] = %w{yay} @resource.to_trans["foo"].should == "yay" end it "should convert resource references into the backward-compatible form" do @resource[:foo] = Puppet::Resource.new(:file, "/f") @resource.to_trans["foo"].should == %w{File /f} end it "should convert resource references into the backward-compatible form even when within arrays" do @resource[:foo] = ["a", Puppet::Resource.new(:file, "/f")] @resource.to_trans["foo"].should == ["a", %w{File /f}] end end end describe "when converting to pson", :if => Puppet.features.pson? do def pson_output_should @resource.class.expects(:pson_create).with { |hash| yield hash } end it "should include the pson util module" do Puppet::Resource.singleton_class.ancestors.should be_include(Puppet::Util::Pson) end # LAK:NOTE For all of these tests, we convert back to the resource so we can # trap the actual data structure then. it "should set its type to the provided type" do Puppet::Resource.from_pson(PSON.parse(Puppet::Resource.new("File", "/foo").to_pson)).type.should == "File" end it "should set its title to the provided title" do Puppet::Resource.from_pson(PSON.parse(Puppet::Resource.new("File", "/foo").to_pson)).title.should == "/foo" end it "should include all tags from the resource" do resource = Puppet::Resource.new("File", "/foo") resource.tag("yay") Puppet::Resource.from_pson(PSON.parse(resource.to_pson)).tags.should == resource.tags end it "should include the file if one is set" do resource = Puppet::Resource.new("File", "/foo") resource.file = "/my/file" Puppet::Resource.from_pson(PSON.parse(resource.to_pson)).file.should == "/my/file" end it "should include the line if one is set" do resource = Puppet::Resource.new("File", "/foo") resource.line = 50 Puppet::Resource.from_pson(PSON.parse(resource.to_pson)).line.should == 50 end it "should include the 'exported' value if one is set" do resource = Puppet::Resource.new("File", "/foo") resource.exported = true Puppet::Resource.from_pson(PSON.parse(resource.to_pson)).exported.should be_true end it "should set 'exported' to false if no value is set" do resource = Puppet::Resource.new("File", "/foo") Puppet::Resource.from_pson(PSON.parse(resource.to_pson)).exported.should be_false end it "should set all of its parameters as the 'parameters' entry" do resource = Puppet::Resource.new("File", "/foo") resource[:foo] = %w{bar eh} resource[:fee] = %w{baz} result = Puppet::Resource.from_pson(PSON.parse(resource.to_pson)) result["foo"].should == %w{bar eh} result["fee"].should == %w{baz} end it "should serialize relationships as reference strings" do resource = Puppet::Resource.new("File", "/foo") resource[:requires] = Puppet::Resource.new("File", "/bar") result = Puppet::Resource.from_pson(PSON.parse(resource.to_pson)) result[:requires].should == "File[/bar]" end it "should serialize multiple relationships as arrays of reference strings" do resource = Puppet::Resource.new("File", "/foo") resource[:requires] = [Puppet::Resource.new("File", "/bar"), Puppet::Resource.new("File", "/baz")] result = Puppet::Resource.from_pson(PSON.parse(resource.to_pson)) result[:requires].should == [ "File[/bar]", "File[/baz]" ] end end describe "when converting from pson", :if => Puppet.features.pson? do def pson_result_should Puppet::Resource.expects(:new).with { |hash| yield hash } end before do @data = { 'type' => "file", 'title' => @basepath+"/yay", } end it "should set its type to the provided type" do Puppet::Resource.from_pson(@data).type.should == "File" end it "should set its title to the provided title" do Puppet::Resource.from_pson(@data).title.should == @basepath+"/yay" end it "should tag the resource with any provided tags" do @data['tags'] = %w{foo bar} resource = Puppet::Resource.from_pson(@data) resource.tags.should be_include("foo") resource.tags.should be_include("bar") end it "should set its file to the provided file" do @data['file'] = "/foo/bar" Puppet::Resource.from_pson(@data).file.should == "/foo/bar" end it "should set its line to the provided line" do @data['line'] = 50 Puppet::Resource.from_pson(@data).line.should == 50 end it "should 'exported' to true if set in the pson data" do @data['exported'] = true Puppet::Resource.from_pson(@data).exported.should be_true end it "should 'exported' to false if not set in the pson data" do Puppet::Resource.from_pson(@data).exported.should be_false end it "should fail if no title is provided" do @data.delete('title') lambda { Puppet::Resource.from_pson(@data) }.should raise_error(ArgumentError) end it "should fail if no type is provided" do @data.delete('type') lambda { Puppet::Resource.from_pson(@data) }.should raise_error(ArgumentError) end it "should set each of the provided parameters" do @data['parameters'] = {'foo' => %w{one two}, 'fee' => %w{three four}} resource = Puppet::Resource.from_pson(@data) resource['foo'].should == %w{one two} resource['fee'].should == %w{three four} end it "should convert single-value array parameters to normal values" do @data['parameters'] = {'foo' => %w{one}} resource = Puppet::Resource.from_pson(@data) resource['foo'].should == %w{one} end end describe "it should implement to_resource" do resource = Puppet::Resource.new("file", "/my/file") resource.to_resource.should == resource end describe "because it is an indirector model" do it "should include Puppet::Indirector" do Puppet::Resource.should be_is_a(Puppet::Indirector) end it "should have a default terminus" do Puppet::Resource.indirection.terminus_class.should == :ral end it "should have a name" do Puppet::Resource.new("file", "/my/file").name.should == "File//my/file" end end describe "when resolving resources with a catalog" do it "should resolve all resources using the catalog" do catalog = mock 'catalog' resource = Puppet::Resource.new("foo::bar", "yay") resource.catalog = catalog catalog.expects(:resource).with("Foo::Bar[yay]").returns(:myresource) resource.resolve.should == :myresource end end describe "when generating the uniqueness key" do it "should include all of the key_attributes in alphabetical order by attribute name" do Puppet::Type.type(:file).stubs(:key_attributes).returns [:myvar, :owner, :path] Puppet::Type.type(:file).stubs(:title_patterns).returns( [ [ /(.*)/, [ [:path, lambda{|x| x} ] ] ] ] ) res = Puppet::Resource.new("file", "/my/file", :parameters => {:owner => 'root', :content => 'hello'}) res.uniqueness_key.should == [ nil, 'root', '/my/file'] end end end diff --git a/spec/unit/type/file/content_spec.rb b/spec/unit/type/file/content_spec.rb index 5ee26cc52..bd2b2adaf 100755 --- a/spec/unit/type/file/content_spec.rb +++ b/spec/unit/type/file/content_spec.rb @@ -1,535 +1,436 @@ #!/usr/bin/env ruby Dir.chdir(File.dirname(__FILE__)) { (s = lambda { |f| File.exist?(f) ? require(f) : Dir.chdir("..") { s.call(f) } }).call("spec/spec_helper.rb") } content = Puppet::Type.type(:file).attrclass(:content) describe content do + include PuppetSpec::Files before do - @resource = Puppet::Type.type(:file).new :path => "/foo/bar" + @filename = tmpfile('testfile') + @resource = Puppet::Type.type(:file).new :path => @filename + File.open(@filename, 'w') {|f| f.write "initial file content"} content.stubs(:standalone?).returns(false) end - it "should be a subclass of Property" do - content.superclass.must == Puppet::Property - end - describe "when determining the checksum type" do it "should use the type specified in the source checksum if a source is set" do @resource[:source] = "/foo" @resource.parameter(:source).expects(:checksum).returns "{md5lite}eh" @content = content.new(:resource => @resource) @content.checksum_type.should == :md5lite end it "should use the type specified by the checksum parameter if no source is set" do @resource[:checksum] = :md5lite @content = content.new(:resource => @resource) @content.checksum_type.should == :md5lite end end describe "when determining the actual content to write" do it "should use the set content if available" do @content = content.new(:resource => @resource) @content.should = "ehness" @content.actual_content.should == "ehness" end it "should not use the content from the source if the source is set" do source = mock 'source' @resource.expects(:parameter).never.with(:source).returns source @content = content.new(:resource => @resource) @content.actual_content.should be_nil end end describe "when setting the desired content" do it "should make the actual content available via an attribute" do @content = content.new(:resource => @resource) @content.stubs(:checksum_type).returns "md5" @content.should = "this is some content" @content.actual_content.should == "this is some content" end it "should store the checksum as the desired content" do @content = content.new(:resource => @resource) digest = Digest::MD5.hexdigest("this is some content") @content.stubs(:checksum_type).returns "md5" @content.should = "this is some content" @content.should.must == "{md5}#{digest}" end it "should not checksum 'absent'" do @content = content.new(:resource => @resource) @content.should = :absent @content.should.must == :absent end it "should accept a checksum as the desired content" do @content = content.new(:resource => @resource) digest = Digest::MD5.hexdigest("this is some content") string = "{md5}#{digest}" @content.should = string @content.should.must == string end end describe "when retrieving the current content" do it "should return :absent if the file does not exist" do @content = content.new(:resource => @resource) @resource.expects(:stat).returns nil @content.retrieve.should == :absent end it "should not manage content on directories" do @content = content.new(:resource => @resource) stat = mock 'stat', :ftype => "directory" @resource.expects(:stat).returns stat @content.retrieve.should be_nil end it "should not manage content on links" do @content = content.new(:resource => @resource) stat = mock 'stat', :ftype => "link" @resource.expects(:stat).returns stat @content.retrieve.should be_nil end it "should always return the checksum as a string" do @content = content.new(:resource => @resource) @resource[:checksum] = :mtime stat = mock 'stat', :ftype => "file" @resource.expects(:stat).returns stat time = Time.now @resource.parameter(:checksum).expects(:mtime_file).with(@resource[:path]).returns time @content.retrieve.should == "{mtime}#{time}" end it "should return the checksum of the file if it exists and is a normal file" do @content = content.new(:resource => @resource) stat = mock 'stat', :ftype => "file" @resource.expects(:stat).returns stat @resource.parameter(:checksum).expects(:md5_file).with(@resource[:path]).returns "mysum" @content.retrieve.should == "{md5}mysum" end end describe "when testing whether the content is in sync" do before do @resource[:ensure] = :file @content = content.new(:resource => @resource) end it "should return true if the resource shouldn't be a regular file" do @resource.expects(:should_be_file?).returns false @content.should = "foo" @content.must be_safe_insync("whatever") end it "should return false if the current content is :absent" do @content.should = "foo" @content.should_not be_safe_insync(:absent) end it "should return false if the file should be a file but is not present" do @resource.expects(:should_be_file?).returns true @content.should = "foo" @content.should_not be_safe_insync(:absent) end describe "and the file exists" do before do @resource.stubs(:stat).returns mock("stat") end it "should return false if the current contents are different from the desired content" do @content.should = "some content" @content.should_not be_safe_insync("other content") end it "should return true if the sum for the current contents is the same as the sum for the desired content" do @content.should = "some content" @content.must be_safe_insync("{md5}" + Digest::MD5.hexdigest("some content")) end describe "and Puppet[:show_diff] is set" do before do Puppet[:show_diff] = true end it "should display a diff if the current contents are different from the desired content" do @content.should = "some content" @content.expects(:diff).returns("my diff").once @content.expects(:print).with("my diff").once @content.safe_insync?("other content") end it "should not display a diff if the sum for the current contents is the same as the sum for the desired content" do @content.should = "some content" @content.expects(:diff).never @content.safe_insync?("{md5}" + Digest::MD5.hexdigest("some content")) end end end describe "and :replace is false" do before do @resource.stubs(:replace?).returns false end it "should be insync if the file exists and the content is different" do @resource.stubs(:stat).returns mock('stat') @content.must be_safe_insync("whatever") end it "should be insync if the file exists and the content is right" do @resource.stubs(:stat).returns mock('stat') @content.must be_safe_insync("something") end it "should not be insync if the file does not exist" do @content.should = "foo" @content.should_not be_safe_insync(:absent) end end end describe "when changing the content" do before do @content = content.new(:resource => @resource) @content.should = "some content" @resource.stubs(:[]).with(:path).returns "/boo" @resource.stubs(:stat).returns "eh" end it "should use the file's :write method to write the content" do @resource.expects(:write).with(:content) @content.sync end it "should return :file_changed if the file already existed" do @resource.expects(:stat).returns "something" @resource.stubs(:write) @content.sync.should == :file_changed end it "should return :file_created if the file did not exist" do @resource.expects(:stat).returns nil @resource.stubs(:write) @content.sync.should == :file_created end end describe "when writing" do before do @content = content.new(:resource => @resource) - @fh = stub_everything end it "should attempt to read from the filebucket if no actual content nor source exists" do + @fh = File.open(@filename, 'w') @content.should = "{md5}foo" @content.resource.bucket.class.any_instance.stubs(:getfile).returns "foo" @content.write(@fh) end describe "from actual content" do before(:each) do @content.stubs(:actual_content).returns("this is content") end it "should write to the given file handle" do @fh.expects(:print).with("this is content") @content.write(@fh) end it "should return the current checksum value" do @resource.parameter(:checksum).expects(:sum_stream).returns "checksum" @content.write(@fh).should == "checksum" end end describe "from a file bucket" do it "should fail if a file bucket cannot be retrieved" do @content.should = "{md5}foo" @content.resource.expects(:bucket).returns nil lambda { @content.write(@fh) }.should raise_error(Puppet::Error) end it "should fail if the file bucket cannot find any content" do @content.should = "{md5}foo" bucket = stub 'bucket' @content.resource.expects(:bucket).returns bucket bucket.expects(:getfile).with("foo").raises "foobar" lambda { @content.write(@fh) }.should raise_error(Puppet::Error) end it "should write the returned content to the file" do @content.should = "{md5}foo" bucket = stub 'bucket' @content.resource.expects(:bucket).returns bucket bucket.expects(:getfile).with("foo").returns "mycontent" @fh.expects(:print).with("mycontent") @content.write(@fh) end end describe "from local source" do before(:each) do - @content.stubs(:actual_content).returns(nil) - @source = stub_everything 'source', :local? => true, :full_path => "/path/to/source" - @resource.stubs(:parameter).with(:source).returns @source - - @sum = stub_everything 'sum' - @resource.stubs(:parameter).with(:checksum).returns(@sum) - - @digest = stub_everything 'digest' - @sum.stubs(:sum_stream).yields(@digest) - - @file = stub_everything 'file' - File.stubs(:open).yields(@file) - @file.stubs(:read).with(8192).returns("chunk1").then.returns("chunk2").then.returns(nil) - end - - it "should open the local file" do - File.expects(:open).with("/path/to/source", "r") - @content.write(@fh) - end + @resource = Puppet::Type.type(:file).new :path => @filename, :backup => false + @sourcename = tmpfile('source') + @source_content = "source file content"*10000 + @sourcefile = File.open(@sourcename, 'w') {|f| f.write @source_content} - it "should read the local file by chunks" do - @file.expects(:read).with(8192).returns("chunk1").then.returns(nil) - @content.write(@fh) + @content = @resource.newattr(:content) + @source = @resource.newattr(:source) + @source.stubs(:metadata).returns stub_everything('metadata', :source => @sourcename, :ftype => 'file') end - it "should write each chunk to the file" do - @fh.expects(:print).with("chunk1").then.with("chunk2") - @content.write(@fh) - end - - it "should pass each chunk to the current sum stream" do - @digest.expects(:<<).with("chunk1").then.with("chunk2") - @content.write(@fh) + it "should copy content from the source to the file" do + @resource.write(@source) + File.read(@filename).should == @source_content end it "should return the checksum computed" do - @sum.stubs(:sum_stream).yields(@digest).returns("checksum") - @content.write(@fh).should == "checksum" + File.open(@filename, 'w') do |file| + @content.write(file).should == "{md5}#{Digest::MD5.hexdigest(@source_content)}" + end end end describe "from remote source" do before(:each) do - @response = stub_everything 'mock response', :code => "404" + @resource = Puppet::Type.type(:file).new :path => @filename, :backup => false + @response = stub_everything 'response', :code => "200" + @source_content = "source file content"*10000 + @response.stubs(:read_body).multiple_yields(*(["source file content"]*10000)) + @conn = stub_everything 'connection' @conn.stubs(:request_get).yields(@response) Puppet::Network::HttpPool.stubs(:http_instance).returns @conn - @content.stubs(:actual_content).returns(nil) - @source = stub_everything 'source', :local? => false, :full_path => "/path/to/source", :server => "server", :port => 1234 - @resource.stubs(:parameter).with(:source).returns @source - - @sum = stub_everything 'sum' - @resource.stubs(:parameter).with(:checksum).returns(@sum) - - @digest = stub_everything 'digest' - @sum.stubs(:sum_stream).yields(@digest) - end - - it "should open a network connection to source server and port" do - Puppet::Network::HttpPool.expects(:http_instance).with("server", 1234).returns @conn - @content.write(@fh) - end - - it "should send the correct indirection uri" do - @conn.expects(:request_get).with { |uri,headers| uri == "/production/file_content/path/to/source" }.yields(@response) - @content.write(@fh) + @content = @resource.newattr(:content) + @sourcename = "puppet:///test/foo" + @source = @resource.newattr(:source) + @source.stubs(:metadata).returns stub_everything('metadata', :source => @sourcename, :ftype => 'file') end - it "should return nil if source is not found" do - @response.expects(:code).returns("404") - @content.write(@fh).should == nil + it "should write the contents to the file" do + @resource.write(@source) + File.read(@filename).should == @source_content end it "should not write anything if source is not found" do - @response.expects(:code).returns("404") - @fh.expects(:print).never - @content.write(@fh).should == nil + @response.stubs(:code).returns("404") + lambda {@resource.write(@source)}.should raise_error(Net::HTTPError) { |e| e.message =~ /404/ } + File.read(@filename).should == "initial file content" end it "should raise an HTTP error in case of server error" do - @response.expects(:code).returns("500") - lambda { @content.write(@fh) }.should raise_error - end - - it "should write content by chunks" do - @response.expects(:code).returns("200") - @response.expects(:read_body).multiple_yields("chunk1","chunk2") - @fh.expects(:print).with("chunk1").then.with("chunk2") - @content.write(@fh) - end - - it "should pass each chunk to the current sum stream" do - @response.expects(:code).returns("200") - @response.expects(:read_body).multiple_yields("chunk1","chunk2") - @digest.expects(:<<).with("chunk1").then.with("chunk2") - @content.write(@fh) + @response.stubs(:code).returns("500") + lambda { @content.write(@fh) }.should raise_error { |e| e.message.include? @source_content } end it "should return the checksum computed" do - @response.expects(:code).returns("200") - @response.expects(:read_body).multiple_yields("chunk1","chunk2") - @sum.expects(:sum_stream).yields(@digest).returns("checksum") - @content.write(@fh).should == "checksum" - end - - it "should get the current accept encoding header value" do - @content.expects(:add_accept_encoding) - @content.write(@fh) - end - - it "should uncompress body on error" do - @response.expects(:code).returns("500") - @response.expects(:body).returns("compressed body") - @content.expects(:uncompress_body).with(@response).returns("uncompressed") - lambda { @content.write(@fh) }.should raise_error { |e| e.message =~ /uncompressed/ } - end - - it "should uncompress chunk by chunk" do - uncompressor = stub_everything 'uncompressor' - @content.expects(:uncompress).with(@response).yields(uncompressor) - @response.expects(:code).returns("200") - @response.expects(:read_body).multiple_yields("chunk1","chunk2") - - uncompressor.expects(:uncompress).with("chunk1").then.with("chunk2") - @content.write(@fh) - end - - it "should write uncompressed chunks to the file" do - uncompressor = stub_everything 'uncompressor' - @content.expects(:uncompress).with(@response).yields(uncompressor) - @response.expects(:code).returns("200") - @response.expects(:read_body).multiple_yields("chunk1","chunk2") - - uncompressor.expects(:uncompress).with("chunk1").returns("uncompressed1") - uncompressor.expects(:uncompress).with("chunk2").returns("uncompressed2") - - @fh.expects(:print).with("uncompressed1") - @fh.expects(:print).with("uncompressed2") - - @content.write(@fh) - end - - it "should pass each uncompressed chunk to the current sum stream" do - uncompressor = stub_everything 'uncompressor' - @content.expects(:uncompress).with(@response).yields(uncompressor) - @response.expects(:code).returns("200") - @response.expects(:read_body).multiple_yields("chunk1","chunk2") - - uncompressor.expects(:uncompress).with("chunk1").returns("uncompressed1") - uncompressor.expects(:uncompress).with("chunk2").returns("uncompressed2") - - @digest.expects(:<<).with("uncompressed1").then.with("uncompressed2") - @content.write(@fh) + File.open(@filename, 'w') do |file| + @content.write(file).should == "{md5}#{Digest::MD5.hexdigest(@source_content)}" + end end end - describe "from a filebucket" do - end - # These are testing the implementation rather than the desired behaviour; while that bites, there are a whole # pile of other methods in the File type that depend on intimate details of this implementation and vice-versa. # If these blow up, you are gonna have to review the callers to make sure they don't explode! --daniel 2011-02-01 describe "each_chunk_from should work" do before do @content = content.new(:resource => @resource) end it "when content is a string" do @content.each_chunk_from('i_am_a_string') { |chunk| chunk.should == 'i_am_a_string' } end # The following manifest is a case where source and content.should are both set # file { "/tmp/mydir" : # source => '/tmp/sourcedir', # recurse => true, # } it "when content checksum comes from source" do source_param = Puppet::Type.type(:file).attrclass(:source) source = source_param.new(:resource => @resource) @content.should = "{md5}123abcd" @content.expects(:chunk_file_from_source).returns('from_source') @content.each_chunk_from(source) { |chunk| chunk.should == 'from_source' } end it "when no content, source, but ensure present" do @resource[:ensure] = :present @content.each_chunk_from(nil) { |chunk| chunk.should == '' } end # you might do this if you were just auditing it "when no content, source, but ensure file" do @resource[:ensure] = :file @content.each_chunk_from(nil) { |chunk| chunk.should == '' } end it "when source_or_content is nil and content not a checksum" do @content.each_chunk_from(nil) { |chunk| chunk.should == '' } end # the content is munged so that if it's a checksum nil gets passed in it "when content is a checksum it should try to read from filebucket" do @content.should = "{md5}123abcd" @content.expects(:read_file_from_filebucket).once.returns('im_a_filebucket') @content.each_chunk_from(nil) { |chunk| chunk.should == 'im_a_filebucket' } end it "when running as puppet apply" do @content.class.expects(:standalone?).returns true source_or_content = stubs('source_or_content') source_or_content.expects(:content).once.returns :whoo @content.each_chunk_from(source_or_content) { |chunk| chunk.should == :whoo } end it "when running from source with a local file" do source_or_content = stubs('source_or_content') source_or_content.expects(:local?).returns true @content.expects(:chunk_file_from_disk).with(source_or_content).once.yields 'woot' @content.each_chunk_from(source_or_content) { |chunk| chunk.should == 'woot' } end it "when running from source with a remote file" do source_or_content = stubs('source_or_content') source_or_content.expects(:local?).returns false @content.expects(:chunk_file_from_source).with(source_or_content).once.yields 'woot' @content.each_chunk_from(source_or_content) { |chunk| chunk.should == 'woot' } end end end end diff --git a/spec/unit/type/file/selinux_spec.rb b/spec/unit/type/file/selinux_spec.rb index 043471dec..a2444acd9 100644 --- a/spec/unit/type/file/selinux_spec.rb +++ b/spec/unit/type/file/selinux_spec.rb @@ -1,83 +1,88 @@ #!/usr/bin/env ruby Dir.chdir(File.dirname(__FILE__)) { (s = lambda { |f| File.exist?(f) ? require(f) : Dir.chdir("..") { s.call(f) } }).call("spec/spec_helper.rb") } [:seluser, :selrole, :seltype, :selrange].each do |param| property = Puppet::Type.type(:file).attrclass(param) describe property do before do @resource = Puppet::Type.type(:file).new :path => "/my/file" @sel = property.new :resource => @resource end it "retrieve on #{param} should return :absent if the file isn't statable" do @resource.expects(:stat).returns nil @sel.retrieve.should == :absent end it "should retrieve nil for #{param} if there is no SELinux support" do stat = stub 'stat', :ftype => "foo" @resource.expects(:stat).returns stat @sel.expects(:get_selinux_current_context).with("/my/file").returns nil @sel.retrieve.should be_nil end it "should retrieve #{param} if a SELinux context is found with a range" do stat = stub 'stat', :ftype => "foo" @resource.expects(:stat).returns stat @sel.expects(:get_selinux_current_context).with("/my/file").returns "user_u:role_r:type_t:s0" expectedresult = case param when :seluser; "user_u" when :selrole; "role_r" when :seltype; "type_t" when :selrange; "s0" end @sel.retrieve.should == expectedresult end it "should retrieve #{param} if a SELinux context is found without a range" do stat = stub 'stat', :ftype => "foo" @resource.expects(:stat).returns stat @sel.expects(:get_selinux_current_context).with("/my/file").returns "user_u:role_r:type_t" expectedresult = case param when :seluser; "user_u" when :selrole; "role_r" when :seltype; "type_t" when :selrange; nil end @sel.retrieve.should == expectedresult end it "should handle no default gracefully" do @sel.expects(:get_selinux_default_context).with("/my/file").returns nil @sel.default.must be_nil end it "should be able to detect matchpathcon defaults" do @sel.stubs(:debug) @sel.expects(:get_selinux_default_context).with("/my/file").returns "user_u:role_r:type_t:s0" expectedresult = case param when :seluser; "user_u" when :selrole; "role_r" when :seltype; "type_t" when :selrange; "s0" end @sel.default.must == expectedresult end + it "should return nil for defaults if selinux_ignore_defaults is true" do + @resource[:selinux_ignore_defaults] = :true + @sel.default.must be_nil + end + it "should be able to set a new context" do stat = stub 'stat', :ftype => "foo" @sel.should = %w{newone} @sel.expects(:set_selinux_context).with("/my/file", ["newone"], param) @sel.sync end it "should do nothing for safe_insync? if no SELinux support" do @sel.should = %{newcontext} @sel.expects(:selinux_support?).returns false @sel.safe_insync?("oldcontext").should == true end end end diff --git a/spec/unit/type/mount_spec.rb b/spec/unit/type/mount_spec.rb index 0d74042e3..fdb67f7d5 100755 --- a/spec/unit/type/mount_spec.rb +++ b/spec/unit/type/mount_spec.rb @@ -1,261 +1,306 @@ #!/usr/bin/env ruby require File.dirname(__FILE__) + '/../../spec_helper' describe Puppet::Type.type(:mount) do it "should have a :refreshable feature that requires the :remount method" do Puppet::Type.type(:mount).provider_feature(:refreshable).methods.should == [:remount] end it "should have no default value for :ensure" do mount = Puppet::Type.type(:mount).new(:name => "yay") mount.should(:ensure).should be_nil end + + it "should have :name as the only keyattribut" do + Puppet::Type.type(:mount).key_attributes.should == [:name] + end end describe Puppet::Type.type(:mount), "when validating attributes" do - [:name, :remounts].each do |param| + [:name, :remounts, :provider].each do |param| it "should have a #{param} parameter" do Puppet::Type.type(:mount).attrtype(param).should == :param end end [:ensure, :device, :blockdevice, :fstype, :options, :pass, :dump, :atboot, :target].each do |param| it "should have a #{param} property" do Puppet::Type.type(:mount).attrtype(param).should == :property end end end describe Puppet::Type.type(:mount)::Ensure, "when validating values" do before do @provider = stub 'provider', :class => Puppet::Type.type(:mount).defaultprovider, :clear => nil Puppet::Type.type(:mount).defaultprovider.expects(:new).returns(@provider) end it "should alias :present to :defined as a value to :ensure" do mount = Puppet::Type.type(:mount).new(:name => "yay", :ensure => :present) mount.should(:ensure).should == :defined end + it "should support :present as a value to :ensure" do + Puppet::Type.type(:mount).new(:name => "yay", :ensure => :present) + end + + it "should support :defined as a value to :ensure" do + Puppet::Type.type(:mount).new(:name => "yay", :ensure => :defined) + end + it "should support :unmounted as a value to :ensure" do - mount = Puppet::Type.type(:mount).new(:name => "yay", :ensure => :unmounted) - mount.should(:ensure).should == :unmounted + Puppet::Type.type(:mount).new(:name => "yay", :ensure => :unmounted) end it "should support :absent as a value to :ensure" do Puppet::Type.type(:mount).new(:name => "yay", :ensure => :absent) end it "should support :mounted as a value to :ensure" do Puppet::Type.type(:mount).new(:name => "yay", :ensure => :mounted) end end describe Puppet::Type.type(:mount)::Ensure do before :each do - @provider = stub 'provider', :class => Puppet::Type.type(:mount).defaultprovider, :clear => nil, :satisfies? => true, :name => :mock + provider_properties = {} + @provider = stub 'provider', :class => Puppet::Type.type(:mount).defaultprovider, :clear => nil, :satisfies? => true, :name => :mock, :property_hash => provider_properties Puppet::Type.type(:mount).defaultprovider.stubs(:new).returns(@provider) @mount = Puppet::Type.type(:mount).new(:name => "yay", :check => :ensure) @ensure = @mount.property(:ensure) end def mount_stub(params) Puppet::Type.type(:mount).validproperties.each do |prop| unless params[prop] params[prop] = :absent @mount[prop] = :absent end end params.each do |param, value| @provider.stubs(param).returns(value) end end - describe Puppet::Type.type(:mount)::Ensure, "when retrieving its current state" do - - it "should return the provider's value if it is :absent" do - @provider.expects(:ensure).returns(:absent) - @ensure.retrieve.should == :absent - end - - it "should return :mounted if the provider indicates it is mounted and the value is not :absent" do - @provider.expects(:ensure).returns(:present) - @provider.expects(:mounted?).returns(true) - @ensure.retrieve.should == :mounted - end - - it "should return :unmounted if the provider indicates it is not mounted and the value is not :absent" do - @provider.expects(:ensure).returns(:present) - @provider.expects(:mounted?).returns(false) - @ensure.retrieve.should == :unmounted - end - end - describe Puppet::Type.type(:mount)::Ensure, "when changing the host" do - it "should destroy itself if it should be absent" do - @provider.stubs(:mounted?).returns(false) - @provider.expects(:destroy) - @ensure.should = :absent - @ensure.sync - end - - it "should unmount itself before destroying if it is mounted and should be absent" do - @provider.expects(:mounted?).returns(true) - @provider.expects(:unmount) - @provider.expects(:destroy) - @ensure.should = :absent - @ensure.sync - end - - it "should create itself if it is absent and should be defined" do - @provider.stubs(:ensure).returns(:absent) - @provider.stubs(:mounted?).returns(true) - - @provider.stubs(:mounted?).returns(false) - @provider.expects(:create) - @ensure.should = :defined - @ensure.sync - end - - it "should not unmount itself if it is mounted and should be defined" do - @provider.stubs(:ensure).returns(:mounted) - @provider.stubs(:mounted?).returns(true) - - @provider.stubs(:create) - @provider.expects(:mount).never - @provider.expects(:unmount).never - @ensure.should = :defined - @ensure.sync - end - - it "should not mount itself if it is unmounted and should be defined" do - @provider.stubs(:ensure).returns(:unmounted) - @provider.stubs(:mounted?).returns(false) - - @ensure.stubs(:syncothers) - @provider.stubs(:create) + def test_ensure_change(options) + @provider.stubs(:get).with(:ensure).returns options[:from] + @provider.stubs(:ensure).returns options[:from] + @provider.stubs(:mounted?).returns([:mounted,:ghost].include? options[:from]) + @provider.expects(:create).times(options[:create] || 0) + @provider.expects(:destroy).times(options[:destroy] || 0) @provider.expects(:mount).never - @provider.expects(:unmount).never - @ensure.should = :present - @ensure.sync - end - - it "should unmount itself if it is mounted and should be unmounted" do - @provider.stubs(:ensure).returns(:present) - @provider.stubs(:mounted?).returns(true) - + @provider.expects(:unmount).times(options[:unmount] || 0) @ensure.stubs(:syncothers) - @provider.expects(:unmount) - @ensure.should = :unmounted + @ensure.should = options[:to] @ensure.sync - end + (!!@provider.property_hash[:needs_mount]).should == (!!options[:mount]) + end + + it "should create itself when changing from :ghost to :present" do + test_ensure_change(:from => :ghost, :to => :present, :create => 1) + end + + it "should create itself when changing from :absent to :present" do + test_ensure_change(:from => :absent, :to => :present, :create => 1) + end - it "should create and mount itself if it does not exist and should be mounted" do - @provider.stubs(:ensure).returns(:absent) - @provider.stubs(:mounted?).returns(false) - @provider.expects(:create) - @ensure.stubs(:syncothers) - @provider.expects(:mount) - @ensure.should = :mounted - @ensure.sync - end + it "should create itself and unmount when changing from :ghost to :unmounted" do + test_ensure_change(:from => :ghost, :to => :unmounted, :create => 1, :unmount => 1) + end + + it "should unmount resource when changing from :mounted to :unmounted" do + test_ensure_change(:from => :mounted, :to => :unmounted, :unmount => 1) + end + + it "should create itself when changing from :absent to :unmounted" do + test_ensure_change(:from => :absent, :to => :unmounted, :create => 1) + end + + it "should unmount resource when changing from :ghost to :absent" do + test_ensure_change(:from => :ghost, :to => :absent, :unmount => 1) + end + + it "should unmount and destroy itself when changing from :mounted to :absent" do + test_ensure_change(:from => :mounted, :to => :absent, :destroy => 1, :unmount => 1) + end + + it "should destroy itself when changing from :unmounted to :absent" do + test_ensure_change(:from => :unmounted, :to => :absent, :destroy => 1) + end + + it "should create itself when changing from :ghost to :mounted" do + test_ensure_change(:from => :ghost, :to => :mounted, :create => 1) + end + + it "should create itself and mount when changing from :absent to :mounted" do + test_ensure_change(:from => :absent, :to => :mounted, :create => 1, :mount => 1) + end + + it "should mount resource when changing from :unmounted to :mounted" do + test_ensure_change(:from => :unmounted, :to => :mounted, :mount => 1) + end + + + it "should be in sync if it is :absent and should be :absent" do + @ensure.should = :absent + @ensure.safe_insync?(:absent).should == true + end + + it "should be out of sync if it is :absent and should be :defined" do + @ensure.should = :defined + @ensure.safe_insync?(:absent).should == false + end + + it "should be out of sync if it is :absent and should be :mounted" do + @ensure.should = :mounted + @ensure.safe_insync?(:absent).should == false + end + + it "should be out of sync if it is :absent and should be :unmounted" do + @ensure.should = :unmounted + @ensure.safe_insync?(:absent).should == false + end + + it "should be out of sync if it is :mounted and should be :absent" do + @ensure.should = :absent + @ensure.safe_insync?(:mounted).should == false + end + + it "should be in sync if it is :mounted and should be :defined" do + @ensure.should = :defined + @ensure.safe_insync?(:mounted).should == true + end + + it "should be in sync if it is :mounted and should be :mounted" do + @ensure.should = :mounted + @ensure.safe_insync?(:mounted).should == true + end + + it "should be out in sync if it is :mounted and should be :unmounted" do + @ensure.should = :unmounted + @ensure.safe_insync?(:mounted).should == false + end + + + it "should be out of sync if it is :unmounted and should be :absent" do + @ensure.should = :absent + @ensure.safe_insync?(:unmounted).should == false + end + + it "should be in sync if it is :unmounted and should be :defined" do + @ensure.should = :defined + @ensure.safe_insync?(:unmounted).should == true + end + + it "should be out of sync if it is :unmounted and should be :mounted" do + @ensure.should = :mounted + @ensure.safe_insync?(:unmounted).should == false + end + + it "should be in sync if it is :unmounted and should be :unmounted" do + @ensure.should = :unmounted + @ensure.safe_insync?(:unmounted).should == true + end - it "should mount itself if it is present and should be mounted" do - @provider.stubs(:ensure).returns(:present) - @provider.stubs(:mounted?).returns(false) - @ensure.stubs(:syncothers) - @provider.expects(:mount) - @ensure.should = :mounted - @ensure.sync - end - it "should create but not mount itself if it is absent and mounted and should be mounted" do - @provider.stubs(:ensure).returns(:absent) - @provider.stubs(:mounted?).returns(true) - @ensure.stubs(:syncothers) - @provider.expects(:create) - @ensure.should = :mounted - @ensure.sync - end - - it "should be insync if it is mounted and should be defined" do - @ensure.should = :defined - @ensure.safe_insync?(:mounted).should == true - end - - it "should be insync if it is unmounted and should be defined" do - @ensure.should = :defined - @ensure.safe_insync?(:unmounted).should == true - end + it "should be out of sync if it is :ghost and should be :absent" do + @ensure.should = :absent + @ensure.safe_insync?(:ghost).should == false + end - it "should be insync if it is mounted and should be present" do - @ensure.should = :present - @ensure.safe_insync?(:mounted).should == true - end + it "should be out of sync if it is :ghost and should be :defined" do + @ensure.should = :defined + @ensure.safe_insync?(:ghost).should == false + end + + it "should be out of sync if it is :ghost and should be :mounted" do + @ensure.should = :mounted + @ensure.safe_insync?(:ghost).should == false + end + + it "should be out of sync if it is :ghost and should be :unmounted" do + @ensure.should = :unmounted + @ensure.safe_insync?(:ghost).should == false + end - it "should be insync if it is unmounted and should be present" do - @ensure.should = :present - @ensure.safe_insync?(:unmounted).should == true - end - end + end describe Puppet::Type.type(:mount), "when responding to events" do it "should remount if it is currently mounted" do @provider.expects(:mounted?).returns(true) @provider.expects(:remount) @mount.refresh end it "should not remount if it is not currently mounted" do @provider.expects(:mounted?).returns(false) @provider.expects(:remount).never @mount.refresh end it "should not remount swap filesystems" do @mount[:fstype] = "swap" @provider.expects(:remount).never @mount.refresh end end end describe Puppet::Type.type(:mount), "when modifying an existing mount entry" do before do @provider = stub 'provider', :class => Puppet::Type.type(:mount).defaultprovider, :clear => nil, :satisfies? => true, :name => :mock, :remount => nil Puppet::Type.type(:mount).defaultprovider.stubs(:new).returns(@provider) @mount = Puppet::Type.type(:mount).new(:name => "yay", :ensure => :mounted) {:device => "/foo/bar", :blockdevice => "/other/bar", :target => "/what/ever", :fstype => 'eh', :options => "", :pass => 0, :dump => 0, :atboot => 0, :ensure => :mounted}.each do |param, value| @mount.provider.stubs(param).returns value @mount[param] = value end @mount.provider.stubs(:mounted?).returns true # stub this to not try to create state.yaml Puppet::Util::Storage.stubs(:store) @catalog = Puppet::Resource::Catalog.new @catalog.add_resource @mount end it "should use the provider to change the dump value" do @mount.provider.expects(:dump).returns 0 @mount.provider.expects(:dump=).with(1) @mount[:dump] = 1 @catalog.apply end + + it "should umount before flushing changes to disk" do + syncorder = sequence('syncorder') + @mount.provider.expects(:options).returns 'soft' + @mount.provider.expects(:ensure).returns :mounted + + @mount.provider.expects(:unmount).in_sequence(syncorder) + @mount.provider.expects(:options=).in_sequence(syncorder).with 'hard' + @mount.expects(:flush).in_sequence(syncorder) # Call inside syncothers + @mount.expects(:flush).in_sequence(syncorder) # I guess transaction or anything calls flush again + + @mount[:ensure] = :unmounted + @mount[:options] = 'hard' + + @catalog.apply + end + end diff --git a/spec/unit/type/user_spec.rb b/spec/unit/type/user_spec.rb index 297134446..5a84af443 100755 --- a/spec/unit/type/user_spec.rb +++ b/spec/unit/type/user_spec.rb @@ -1,332 +1,336 @@ #!/usr/bin/env ruby require File.dirname(__FILE__) + '/../../spec_helper' user = Puppet::Type.type(:user) describe user do before do ENV["PATH"] += File::PATH_SEPARATOR + "/usr/sbin" unless ENV["PATH"].split(File::PATH_SEPARATOR).include?("/usr/sbin") @provider = stub 'provider' @resource = stub 'resource', :resource => nil, :provider => @provider, :line => nil, :file => nil end it "should have a default provider inheriting from Puppet::Provider" do user.defaultprovider.ancestors.should be_include(Puppet::Provider) end it "should be able to create a instance" do user.new(:name => "foo").should_not be_nil end it "should have an allows_duplicates feature" do user.provider_feature(:allows_duplicates).should_not be_nil end it "should have an manages_homedir feature" do user.provider_feature(:manages_homedir).should_not be_nil end it "should have an manages_passwords feature" do user.provider_feature(:manages_passwords).should_not be_nil end it "should have a manages_solaris_rbac feature" do user.provider_feature(:manages_solaris_rbac).should_not be_nil end it "should have a manages_expiry feature" do user.provider_feature(:manages_expiry).should_not be_nil end it "should have a manages_password_age feature" do user.provider_feature(:manages_password_age).should_not be_nil end + it "should have a system_users feature" do + user.provider_feature(:system_users).should_not be_nil + end + describe "instances" do it "should have a valid provider" do user.new(:name => "foo").provider.class.ancestors.should be_include(Puppet::Provider) end it "should delegate existence questions to its provider" do instance = user.new(:name => "foo") instance.provider.expects(:exists?).returns "eh" instance.exists?.should == "eh" end end properties = [:ensure, :uid, :gid, :home, :comment, :shell, :password, :password_min_age, :password_max_age, :groups, :roles, :auths, :profiles, :project, :keys, :expiry] properties.each do |property| it "should have a #{property} property" do user.attrclass(property).ancestors.should be_include(Puppet::Property) end it "should have documentation for its #{property} property" do user.attrclass(property).doc.should be_instance_of(String) end end list_properties = [:groups, :roles, :auths] list_properties.each do |property| it "should have a list '#{property}'" do user.attrclass(property).ancestors.should be_include(Puppet::Property::List) end end it "should have an ordered list 'profiles'" do user.attrclass(:profiles).ancestors.should be_include(Puppet::Property::OrderedList) end it "should have key values 'keys'" do user.attrclass(:keys).ancestors.should be_include(Puppet::Property::KeyValue) end describe "when retrieving all current values" do before do @user = user.new(:name => "foo", :uid => 10) end it "should return a hash containing values for all set properties" do @user[:gid] = 10 @user.property(:ensure).expects(:retrieve).returns :present @user.property(:uid).expects(:retrieve).returns 15 @user.property(:gid).expects(:retrieve).returns 15 values = @user.retrieve [@user.property(:uid), @user.property(:gid)].each { |property| values.should be_include(property) } end it "should set all values to :absent if the user is absent" do @user.property(:ensure).expects(:retrieve).returns :absent @user.property(:uid).expects(:retrieve).never @user.retrieve[@user.property(:uid)].should == :absent end it "should include the result of retrieving each property's current value if the user is present" do @user.property(:ensure).expects(:retrieve).returns :present @user.property(:uid).expects(:retrieve).returns 15 @user.retrieve[@user.property(:uid)].should == 15 end end describe "when managing the ensure property" do before do @ensure = user.attrclass(:ensure).new(:resource => @resource) end it "should support a :present value" do lambda { @ensure.should = :present }.should_not raise_error end it "should support an :absent value" do lambda { @ensure.should = :absent }.should_not raise_error end it "should call :create on the provider when asked to sync to the :present state" do @provider.expects(:create) @ensure.should = :present @ensure.sync end it "should call :delete on the provider when asked to sync to the :absent state" do @provider.expects(:delete) @ensure.should = :absent @ensure.sync end describe "and determining the current state" do it "should return :present when the provider indicates the user exists" do @provider.expects(:exists?).returns true @ensure.retrieve.should == :present end it "should return :absent when the provider indicates the user does not exist" do @provider.expects(:exists?).returns false @ensure.retrieve.should == :absent end end end describe "when managing the uid property" do it "should convert number-looking strings into actual numbers" do uid = user.attrclass(:uid).new(:resource => @resource) uid.should = "50" uid.should.must == 50 end it "should support UIDs as numbers" do uid = user.attrclass(:uid).new(:resource => @resource) uid.should = 50 uid.should.must == 50 end it "should :absent as a value" do uid = user.attrclass(:uid).new(:resource => @resource) uid.should = :absent uid.should.must == :absent end end describe "when managing the gid" do it "should :absent as a value" do gid = user.attrclass(:gid).new(:resource => @resource) gid.should = :absent gid.should.must == :absent end it "should convert number-looking strings into actual numbers" do gid = user.attrclass(:gid).new(:resource => @resource) gid.should = "50" gid.should.must == 50 end it "should support GIDs specified as integers" do gid = user.attrclass(:gid).new(:resource => @resource) gid.should = 50 gid.should.must == 50 end it "should support groups specified by name" do gid = user.attrclass(:gid).new(:resource => @resource) gid.should = "foo" gid.should.must == "foo" end describe "when testing whether in sync" do before do @gid = user.attrclass(:gid).new(:resource => @resource, :should => %w{foo bar}) end it "should return true if no 'should' values are set" do @gid = user.attrclass(:gid).new(:resource => @resource) @gid.must be_safe_insync(500) end it "should return true if any of the specified groups are equal to the current integer" do Puppet::Util.expects(:gid).with("foo").returns 300 Puppet::Util.expects(:gid).with("bar").returns 500 @gid.must be_safe_insync(500) end it "should return false if none of the specified groups are equal to the current integer" do Puppet::Util.expects(:gid).with("foo").returns 300 Puppet::Util.expects(:gid).with("bar").returns 500 @gid.should_not be_safe_insync(700) end end describe "when syncing" do before do @gid = user.attrclass(:gid).new(:resource => @resource, :should => %w{foo bar}) end it "should use the first found, specified group as the desired value and send it to the provider" do Puppet::Util.expects(:gid).with("foo").returns nil Puppet::Util.expects(:gid).with("bar").returns 500 @provider.expects(:gid=).with 500 @gid.sync end end end describe "when managing expiry" do before do @expiry = user.attrclass(:expiry).new(:resource => @resource) end it "should fail if given an invalid date" do lambda { @expiry.should = "200-20-20" }.should raise_error(Puppet::Error) end end describe "when managing minimum password age" do before do @age = user.attrclass(:password_min_age).new(:resource => @resource) end it "should accept a negative minimum age" do expect { @age.should = -1 }.should_not raise_error end it "should fail with an empty minimum age" do expect { @age.should = '' }.should raise_error(Puppet::Error) end end describe "when managing maximum password age" do before do @age = user.attrclass(:password_max_age).new(:resource => @resource) end it "should accept a negative maximum age" do expect { @age.should = -1 }.should_not raise_error end it "should fail with an empty maximum age" do expect { @age.should = '' }.should raise_error(Puppet::Error) end end describe "when managing passwords" do before do @password = user.attrclass(:password).new(:resource => @resource, :should => "mypass") end it "should not include the password in the change log when adding the password" do @password.change_to_s(:absent, "mypass").should_not be_include("mypass") end it "should not include the password in the change log when changing the password" do @password.change_to_s("other", "mypass").should_not be_include("mypass") end it "should fail if a ':' is included in the password" do lambda { @password.should = "some:thing" }.should raise_error(Puppet::Error) end it "should allow the value to be set to :absent" do lambda { @password.should = :absent }.should_not raise_error end end describe "when manages_solaris_rbac is enabled" do before do @provider.stubs(:satisfies?).returns(false) @provider.expects(:satisfies?).with([:manages_solaris_rbac]).returns(true) end it "should support a :role value for ensure" do @ensure = user.attrclass(:ensure).new(:resource => @resource) lambda { @ensure.should = :role }.should_not raise_error end end describe "when user has roles" do before do # To test this feature, we have to support it. user.new(:name => "foo").provider.class.stubs(:feature?).returns(true) end it "should autorequire roles" do testuser = Puppet::Type.type(:user).new(:name => "testuser") testuser[:roles] = "testrole" testrole = Puppet::Type.type(:user).new(:name => "testrole") config = Puppet::Resource::Catalog.new :testing do |conf| [testuser, testrole].each { |resource| conf.add_resource resource } end Puppet::Type::User::ProviderDirectoryservice.stubs(:get_macosx_version_major).returns "10.5" rel = testuser.autorequire[0] rel.source.ref.should == testrole.ref rel.target.ref.should == testuser.ref end end end diff --git a/spec/unit/type_spec.rb b/spec/unit/type_spec.rb index b7a08977e..6d9d0b234 100755 --- a/spec/unit/type_spec.rb +++ b/spec/unit/type_spec.rb @@ -1,573 +1,584 @@ #!/usr/bin/env ruby require File.dirname(__FILE__) + '/../spec_helper' describe Puppet::Type do it "should include the Cacher module" do Puppet::Type.ancestors.should be_include(Puppet::Util::Cacher) end it "should consider a parameter to be valid if it is a valid parameter" do Puppet::Type.type(:mount).should be_valid_parameter(:path) end it "should consider a parameter to be valid if it is a valid property" do Puppet::Type.type(:mount).should be_valid_parameter(:fstype) end it "should consider a parameter to be valid if it is a valid metaparam" do Puppet::Type.type(:mount).should be_valid_parameter(:noop) end it "should use its catalog as its expirer" do catalog = Puppet::Resource::Catalog.new resource = Puppet::Type.type(:mount).new(:name => "foo", :fstype => "bar", :pass => 1, :ensure => :present) resource.catalog = catalog resource.expirer.should equal(catalog) end it "should do nothing when asked to expire when it has no catalog" do resource = Puppet::Type.type(:mount).new(:name => "foo", :fstype => "bar", :pass => 1, :ensure => :present) lambda { resource.expire }.should_not raise_error end it "should be able to retrieve a property by name" do resource = Puppet::Type.type(:mount).new(:name => "foo", :fstype => "bar", :pass => 1, :ensure => :present) resource.property(:fstype).must be_instance_of(Puppet::Type.type(:mount).attrclass(:fstype)) end it "should be able to retrieve a parameter by name" do resource = Puppet::Type.type(:mount).new(:name => "foo", :fstype => "bar", :pass => 1, :ensure => :present) resource.parameter(:name).must be_instance_of(Puppet::Type.type(:mount).attrclass(:name)) end it "should be able to retrieve a property by name using the :parameter method" do resource = Puppet::Type.type(:mount).new(:name => "foo", :fstype => "bar", :pass => 1, :ensure => :present) resource.parameter(:fstype).must be_instance_of(Puppet::Type.type(:mount).attrclass(:fstype)) end it "should be able to retrieve all set properties" do resource = Puppet::Type.type(:mount).new(:name => "foo", :fstype => "bar", :pass => 1, :ensure => :present) props = resource.properties props.should_not be_include(nil) [:fstype, :ensure, :pass].each do |name| props.should be_include(resource.parameter(name)) end end it "should have a method for setting default values for resources" do Puppet::Type.type(:mount).new(:name => "foo").should respond_to(:set_default) end it "should do nothing for attributes that have no defaults and no specified value" do Puppet::Type.type(:mount).new(:name => "foo").parameter(:noop).should be_nil end it "should have a method for adding tags" do Puppet::Type.type(:mount).new(:name => "foo").should respond_to(:tags) end it "should use the tagging module" do Puppet::Type.type(:mount).ancestors.should be_include(Puppet::Util::Tagging) end it "should delegate to the tagging module when tags are added" do resource = Puppet::Type.type(:mount).new(:name => "foo") resource.stubs(:tag).with(:mount) resource.expects(:tag).with(:tag1, :tag2) resource.tags = [:tag1,:tag2] end it "should add the current type as tag" do resource = Puppet::Type.type(:mount).new(:name => "foo") resource.stubs(:tag) resource.expects(:tag).with(:mount) resource.tags = [:tag1,:tag2] end it "should have a method to know if the resource is exported" do Puppet::Type.type(:mount).new(:name => "foo").should respond_to(:exported?) end it "should have a method to know if the resource is virtual" do Puppet::Type.type(:mount).new(:name => "foo").should respond_to(:virtual?) end it "should consider its version to be its catalog version" do resource = Puppet::Type.type(:mount).new(:name => "foo") catalog = Puppet::Resource::Catalog.new catalog.version = 50 catalog.add_resource resource resource.version.should == 50 end it "should consider its version to be zero if it has no catalog" do Puppet::Type.type(:mount).new(:name => "foo").version.should == 0 end it "should provide source_descriptors" do resource = Puppet::Type.type(:mount).new(:name => "foo") catalog = Puppet::Resource::Catalog.new catalog.version = 50 catalog.add_resource resource resource.source_descriptors.should == {:tags=>["mount", "foo"], :path=>"/Mount[foo]"} end it "should consider its type to be the name of its class" do Puppet::Type.type(:mount).new(:name => "foo").type.should == :mount end it "should use any provided noop value" do Puppet::Type.type(:mount).new(:name => "foo", :noop => true).must be_noop end it "should use the global noop value if none is provided" do Puppet[:noop] = true Puppet::Type.type(:mount).new(:name => "foo").must be_noop end it "should not be noop if in a non-host_config catalog" do resource = Puppet::Type.type(:mount).new(:name => "foo") catalog = Puppet::Resource::Catalog.new catalog.add_resource resource resource.should_not be_noop end describe "when creating an event" do before do @resource = Puppet::Type.type(:mount).new :name => "foo" end it "should have the resource's reference as the resource" do @resource.event.resource.should == "Mount[foo]" end it "should have the resource's log level as the default log level" do @resource[:loglevel] = :warning @resource.event.default_log_level.should == :warning end {:file => "/my/file", :line => 50, :tags => %{foo bar}}.each do |attr, value| it "should set the #{attr}" do @resource.stubs(attr).returns value @resource.event.send(attr).should == value end end it "should allow specification of event attributes" do @resource.event(:status => "noop").status.should == "noop" end end describe "when choosing a default provider" do it "should choose the provider with the highest specificity" do # Make a fake type type = Puppet::Type.newtype(:defaultprovidertest) do newparam(:name) do end end basic = type.provide(:basic) {} greater = type.provide(:greater) {} basic.stubs(:specificity).returns 1 greater.stubs(:specificity).returns 2 type.defaultprovider.should equal(greater) end end describe "when initializing" do describe "and passed a TransObject" do it "should fail" do trans = Puppet::TransObject.new("/foo", :mount) lambda { Puppet::Type.type(:mount).new(trans) }.should raise_error(Puppet::DevError) end end describe "and passed a Puppet::Resource instance" do it "should set its title to the title of the resource if the resource type is equal to the current type" do resource = Puppet::Resource.new(:mount, "/foo", :parameters => {:name => "/other"}) Puppet::Type.type(:mount).new(resource).title.should == "/foo" end it "should set its title to the resource reference if the resource type is not equal to the current type" do resource = Puppet::Resource.new(:user, "foo") Puppet::Type.type(:mount).new(resource).title.should == "User[foo]" end [:line, :file, :catalog, :exported, :virtual].each do |param| it "should copy '#{param}' from the resource if present" do resource = Puppet::Resource.new(:mount, "/foo") resource.send(param.to_s + "=", "foo") resource.send(param.to_s + "=", "foo") Puppet::Type.type(:mount).new(resource).send(param).should == "foo" end end it "should copy any tags from the resource" do resource = Puppet::Resource.new(:mount, "/foo") resource.tag "one", "two" tags = Puppet::Type.type(:mount).new(resource).tags tags.should be_include("one") tags.should be_include("two") end it "should copy the resource's parameters as its own" do resource = Puppet::Resource.new(:mount, "/foo", :parameters => {:atboot => true, :fstype => "boo"}) params = Puppet::Type.type(:mount).new(resource).to_hash params[:fstype].should == "boo" params[:atboot].should == true end end describe "and passed a Hash" do it "should extract the title from the hash" do Puppet::Type.type(:mount).new(:title => "/yay").title.should == "/yay" end it "should work when hash keys are provided as strings" do Puppet::Type.type(:mount).new("title" => "/yay").title.should == "/yay" end it "should work when hash keys are provided as symbols" do Puppet::Type.type(:mount).new(:title => "/yay").title.should == "/yay" end it "should use the name from the hash as the title if no explicit title is provided" do Puppet::Type.type(:mount).new(:name => "/yay").title.should == "/yay" end it "should use the Resource Type's namevar to determine how to find the name in the hash" do Puppet::Type.type(:file).new(:path => "/yay").title.should == "/yay" end [:catalog].each do |param| it "should extract '#{param}' from the hash if present" do Puppet::Type.type(:mount).new(:name => "/yay", param => "foo").send(param).should == "foo" end end it "should use any remaining hash keys as its parameters" do resource = Puppet::Type.type(:mount).new(:title => "/foo", :catalog => "foo", :atboot => true, :fstype => "boo") resource[:fstype].must == "boo" resource[:atboot].must == true end end it "should fail if any invalid attributes have been provided" do lambda { Puppet::Type.type(:mount).new(:title => "/foo", :nosuchattr => "whatever") }.should raise_error(Puppet::Error) end it "should set its name to the resource's title if the resource does not have a :name or namevar parameter set" do resource = Puppet::Resource.new(:mount, "/foo") Puppet::Type.type(:mount).new(resource).name.should == "/foo" end it "should fail if no title, name, or namevar are provided" do lambda { Puppet::Type.type(:file).new(:atboot => true) }.should raise_error(Puppet::Error) end it "should set the attributes in the order returned by the class's :allattrs method" do Puppet::Type.type(:mount).stubs(:allattrs).returns([:name, :atboot, :noop]) resource = Puppet::Resource.new(:mount, "/foo", :parameters => {:name => "myname", :atboot => "myboot", :noop => "whatever"}) set = [] Puppet::Type.type(:mount).any_instance.stubs(:newattr).with do |param, hash| set << param true end.returns(stub_everything("a property")) Puppet::Type.type(:mount).new(resource) set[-1].should == :noop set[-2].should == :atboot end it "should always set the name and then default provider before anything else" do Puppet::Type.type(:mount).stubs(:allattrs).returns([:provider, :name, :atboot]) resource = Puppet::Resource.new(:mount, "/foo", :parameters => {:name => "myname", :atboot => "myboot"}) set = [] Puppet::Type.type(:mount).any_instance.stubs(:newattr).with do |param, hash| set << param true end.returns(stub_everything("a property")) Puppet::Type.type(:mount).new(resource) set[0].should == :name set[1].should == :provider end # This one is really hard to test :/ it "should each default immediately if no value is provided" do defaults = [] Puppet::Type.type(:package).any_instance.stubs(:set_default).with { |value| defaults << value; true } Puppet::Type.type(:package).new :name => "whatever" defaults[0].should == :provider end it "should retain a copy of the originally provided parameters" do Puppet::Type.type(:mount).new(:name => "foo", :atboot => true, :noop => false).original_parameters.should == {:atboot => true, :noop => false} end it "should delete the name via the namevar from the originally provided parameters" do Puppet::Type.type(:file).new(:name => "/foo").original_parameters[:path].should be_nil end end it "should have a class method for converting a hash into a Puppet::Resource instance" do Puppet::Type.type(:mount).must respond_to(:hash2resource) end describe "when converting a hash to a Puppet::Resource instance" do before do @type = Puppet::Type.type(:mount) end it "should treat a :title key as the title of the resource" do @type.hash2resource(:name => "/foo", :title => "foo").title.should == "foo" end it "should use the name from the hash as the title if no explicit title is provided" do @type.hash2resource(:name => "foo").title.should == "foo" end it "should use the Resource Type's namevar to determine how to find the name in the hash" do @type.stubs(:key_attributes).returns([ :myname ]) @type.hash2resource(:myname => "foo").title.should == "foo" end [:catalog].each do |attr| it "should use any provided #{attr}" do @type.hash2resource(:name => "foo", attr => "eh").send(attr).should == "eh" end end it "should set all provided parameters on the resource" do @type.hash2resource(:name => "foo", :fstype => "boo", :boot => "fee").to_hash.should == {:name => "foo", :fstype => "boo", :boot => "fee"} end it "should not set the title as a parameter on the resource" do @type.hash2resource(:name => "foo", :title => "eh")[:title].should be_nil end it "should not set the catalog as a parameter on the resource" do @type.hash2resource(:name => "foo", :catalog => "eh")[:catalog].should be_nil end it "should treat hash keys equivalently whether provided as strings or symbols" do resource = @type.hash2resource("name" => "foo", "title" => "eh", "fstype" => "boo") resource.title.should == "eh" resource[:name].should == "foo" resource[:fstype].should == "boo" end end describe "when retrieving current property values" do before do @resource = Puppet::Type.type(:mount).new(:name => "foo", :fstype => "bar", :pass => 1, :ensure => :present) @resource.property(:ensure).stubs(:retrieve).returns :absent end it "should fail if its provider is unsuitable" do @resource = Puppet::Type.type(:mount).new(:name => "foo", :fstype => "bar", :pass => 1, :ensure => :present) @resource.provider.class.expects(:suitable?).returns false lambda { @resource.retrieve_resource }.should raise_error(Puppet::Error) end it "should return a Puppet::Resource instance with its type and title set appropriately" do result = @resource.retrieve_resource result.should be_instance_of(Puppet::Resource) result.type.should == "Mount" result.title.should == "foo" end it "should set the name of the returned resource if its own name and title differ" do @resource[:name] = "my name" @resource.title = "other name" @resource.retrieve_resource[:name].should == "my name" end it "should provide a value for all set properties" do values = @resource.retrieve_resource [:ensure, :fstype, :pass].each { |property| values[property].should_not be_nil } end it "should provide a value for 'ensure' even if no desired value is provided" do @resource = Puppet::Type.type(:file).new(:path => "/my/file/that/can't/exist") end it "should not call retrieve on non-ensure properties if the resource is absent and should consider the property absent" do @resource.property(:ensure).expects(:retrieve).returns :absent @resource.property(:fstype).expects(:retrieve).never @resource.retrieve_resource[:fstype].should == :absent end it "should include the result of retrieving each property's current value if the resource is present" do @resource.property(:ensure).expects(:retrieve).returns :present @resource.property(:fstype).expects(:retrieve).returns 15 @resource.retrieve_resource[:fstype] == 15 end end describe ".title_patterns" do describe "when there's one namevar" do before do @type_class = Puppet::Type.type(:notify) @type_class.stubs(:key_attributes).returns([:one]) end it "should have a default pattern for when there's one namevar" do patterns = @type_class.title_patterns patterns.length.should == 1 patterns[0].length.should == 2 end - + it "should have a regexp that captures the entire string" do patterns = @type_class.title_patterns string = "abc\n\tdef" patterns[0][0] =~ string $1.should == "abc\n\tdef" end end end describe "when in a catalog" do before do @catalog = Puppet::Resource::Catalog.new @container = Puppet::Type.type(:component).new(:name => "container") @one = Puppet::Type.type(:file).new(:path => "/file/one") @two = Puppet::Type.type(:file).new(:path => "/file/two") @catalog.add_resource @container @catalog.add_resource @one @catalog.add_resource @two @catalog.add_edge @container, @one @catalog.add_edge @container, @two end it "should have no parent if there is no in edge" do @container.parent.should be_nil end it "should set its parent to its in edge" do @one.parent.ref.should == @container.ref end after do @catalog.clear(true) end end it "should have a 'stage' metaparam" do Puppet::Type.metaparamclass(:stage).should be_instance_of(Class) end end describe Puppet::Type::RelationshipMetaparam do it "should be a subclass of Puppet::Parameter" do Puppet::Type::RelationshipMetaparam.superclass.should equal(Puppet::Parameter) end it "should be able to produce a list of subclasses" do Puppet::Type::RelationshipMetaparam.should respond_to(:subclasses) end describe "when munging relationships" do before do @resource = Puppet::Type.type(:mount).new :name => "/foo" @metaparam = Puppet::Type.metaparamclass(:require).new :resource => @resource end it "should accept Puppet::Resource instances" do ref = Puppet::Resource.new(:file, "/foo") @metaparam.munge(ref)[0].should equal(ref) end it "should turn any string into a Puppet::Resource" do @metaparam.munge("File[/ref]")[0].should be_instance_of(Puppet::Resource) end end it "should be able to validate relationships" do Puppet::Type.metaparamclass(:require).new(:resource => mock("resource")).should respond_to(:validate_relationship) end it "should fail if any specified resource is not found in the catalog" do catalog = mock 'catalog' resource = stub 'resource', :catalog => catalog, :ref => "resource" param = Puppet::Type.metaparamclass(:require).new(:resource => resource, :value => %w{Foo[bar] Class[test]}) catalog.expects(:resource).with("Foo[bar]").returns "something" catalog.expects(:resource).with("Class[Test]").returns nil param.expects(:fail).with { |string| string.include?("Class[Test]") } param.validate_relationship end end describe Puppet::Type.metaparamclass(:check) do it "should warn and create an instance of ':audit'" do file = Puppet::Type.type(:file).new :path => "/foo" file.expects(:warning) file[:check] = :mode file[:audit].should == [:mode] end end describe Puppet::Type.metaparamclass(:audit) do before do @resource = Puppet::Type.type(:file).new :path => "/foo" end it "should default to being nil" do @resource[:audit].should be_nil end it "should specify all possible properties when asked to audit all properties" do @resource[:audit] = :all list = @resource.class.properties.collect { |p| p.name } @resource[:audit].should == list end it "should accept the string 'all' to specify auditing all possible properties" do @resource[:audit] = 'all' list = @resource.class.properties.collect { |p| p.name } @resource[:audit].should == list end it "should fail if asked to audit an invalid property" do lambda { @resource[:audit] = :foobar }.should raise_error(Puppet::Error) end it "should create an attribute instance for each auditable property" do @resource[:audit] = :mode @resource.parameter(:mode).should_not be_nil end it "should accept properties specified as a string" do @resource[:audit] = "mode" @resource.parameter(:mode).should_not be_nil end it "should not create attribute instances for parameters, only properties" do @resource[:audit] = :noop @resource.parameter(:noop).should be_nil end + + describe "when generating the uniqueness key" do + it "should include all of the key_attributes in alphabetical order by attribute name" do + Puppet::Type.type(:file).stubs(:key_attributes).returns [:path, :mode, :owner] + Puppet::Type.type(:file).stubs(:title_patterns).returns( + [ [ /(.*)/, [ [:path, lambda{|x| x} ] ] ] ] + ) + res = Puppet::Type.type(:file).new( :title => '/my/file', :path => '/my/file', :owner => 'root', :content => 'hello' ) + res.uniqueness_key.should == [ nil, 'root', '/my/file'] + end + end end diff --git a/spec/unit/util/execution_spec.rb b/spec/unit/util/execution_spec.rb new file mode 100644 index 000000000..312dd3b8e --- /dev/null +++ b/spec/unit/util/execution_spec.rb @@ -0,0 +1,49 @@ +#!/usr/bin/env ruby + +require File.dirname(__FILE__) + '/../../spec_helper' + +describe Puppet::Util::Execution do + include Puppet::Util::Execution + describe "#withenv" do + before :each do + @original_path = ENV["PATH"] + @new_env = {:PATH => "/some/bogus/path"} + end + + it "should change environment variables within the block then reset environment variables to their original values" do + withenv @new_env do + ENV["PATH"].should == "/some/bogus/path" + end + ENV["PATH"].should == @original_path + end + + it "should reset environment variables to their original values even if the block fails" do + begin + withenv @new_env do + ENV["PATH"].should == "/some/bogus/path" + raise "This is a failure" + end + rescue + end + ENV["PATH"].should == @original_path + end + + it "should reset environment variables even when they are set twice" do + # Setting Path & Environment parameters in Exec type can cause weirdness + @new_env["PATH"] = "/someother/bogus/path" + withenv @new_env do + # When assigning duplicate keys, can't guarantee order of evaluation + ENV["PATH"].should =~ /\/some.*\/bogus\/path/ + end + ENV["PATH"].should == @original_path + end + + it "should remove any new environment variables after the block ends" do + @new_env[:FOO] = "bar" + withenv @new_env do + ENV["FOO"].should == "bar" + end + ENV["FOO"].should == nil + end + end +end diff --git a/spec/unit/util/execution_stub_spec.rb b/spec/unit/util/execution_stub_spec.rb new file mode 100644 index 000000000..14cf9c67a --- /dev/null +++ b/spec/unit/util/execution_stub_spec.rb @@ -0,0 +1,35 @@ +#!/usr/bin/env ruby + +require File.dirname(__FILE__) + '/../../spec_helper' + +describe Puppet::Util::ExecutionStub do + it "should use the provided stub code when 'set' is called" do + Puppet::Util::ExecutionStub.set do |command, options| + command.should == ['/bin/foo', 'bar'] + "stub output" + end + Puppet::Util::ExecutionStub.current_value.should_not == nil + Puppet::Util.execute(['/bin/foo', 'bar']).should == "stub output" + end + + it "should automatically restore normal execution at the conclusion of each spec test" do + # Note: this test relies on the previous test creating a stub. + Puppet::Util::ExecutionStub.current_value.should == nil + end + + it "should restore normal execution after 'reset' is called" do + true_command = Puppet::Util.which('true') # Note: "true" exists at different paths in different OSes + stub_call_count = 0 + Puppet::Util::ExecutionStub.set do |command, options| + command.should == [true_command] + stub_call_count += 1 + 'stub called' + end + Puppet::Util.execute([true_command]).should == 'stub called' + stub_call_count.should == 1 + Puppet::Util::ExecutionStub.reset + Puppet::Util::ExecutionStub.current_value.should == nil + Puppet::Util.execute([true_command]).should == '' + stub_call_count.should == 1 + end +end diff --git a/spec/unit/util/rdoc/parser_spec.rb b/spec/unit/util/rdoc/parser_spec.rb index 28c33c295..b4453ae86 100755 --- a/spec/unit/util/rdoc/parser_spec.rb +++ b/spec/unit/util/rdoc/parser_spec.rb @@ -1,544 +1,556 @@ #!/usr/bin/env ruby Dir.chdir(File.dirname(__FILE__)) { (s = lambda { |f| File.exist?(f) ? require(f) : Dir.chdir("..") { s.call(f) } }).call("spec/spec_helper.rb") } require 'puppet/resource/type_collection' require 'puppet/util/rdoc/parser' require 'puppet/util/rdoc/code_objects' require 'rdoc/options' require 'rdoc/rdoc' describe RDoc::Parser do before :each do File.stubs(:stat).with("init.pp") @top_level = stub_everything 'toplevel', :file_relative_name => "init.pp" @parser = RDoc::Parser.new(@top_level, "module/manifests/init.pp", nil, Options.instance, RDoc::Stats.new) end describe "when scanning files" do it "should parse puppet files with the puppet parser" do @parser.stubs(:scan_top_level) parser = stub 'parser' Puppet::Parser::Parser.stubs(:new).returns(parser) parser.expects(:parse) parser.expects(:file=).with("module/manifests/init.pp") @parser.scan end it "should scan the ast for Puppet files" do parser = stub_everything 'parser' Puppet::Parser::Parser.stubs(:new).returns(parser) @parser.expects(:scan_top_level) @parser.scan end it "should return a PuppetTopLevel to RDoc" do parser = stub_everything 'parser' Puppet::Parser::Parser.stubs(:new).returns(parser) @parser.expects(:scan_top_level) @parser.scan.should be_a(RDoc::PuppetTopLevel) end + + it "should scan the top level even if the file has already parsed" do + known_type = stub 'known_types' + env = stub 'env' + Puppet::Node::Environment.stubs(:new).returns(env) + env.stubs(:known_resource_types).returns(known_type) + known_type.expects(:watching_file?).with("module/manifests/init.pp").returns(true) + + @parser.expects(:scan_top_level) + + @parser.scan + end end describe "when scanning top level entities" do before :each do @resource_type_collection = stub_everything 'resource_type_collection' @parser.ast = @resource_type_collection @parser.stubs(:split_module).returns("module") @topcontainer = stub_everything 'topcontainer' @container = stub_everything 'container' @module = stub_everything 'module' @container.stubs(:add_module).returns(@module) @parser.stubs(:get_class_or_module).returns([@container, "module"]) end it "should read any present README as module documentation" do FileTest.stubs(:readable?).returns(true) File.stubs(:open).returns("readme") @parser.stubs(:parse_elements) @module.expects(:comment=).with("readme") @parser.scan_top_level(@topcontainer) end it "should tell the container its module name" do @parser.stubs(:parse_elements) @topcontainer.expects(:module_name=).with("module") @parser.scan_top_level(@topcontainer) end it "should not document our toplevel if it isn't a valid module" do @parser.stubs(:split_module).returns(nil) @topcontainer.expects(:document_self=).with(false) @parser.expects(:parse_elements).never @parser.scan_top_level(@topcontainer) end it "should set the module as global if we parse the global manifests (ie __site__ module)" do @parser.stubs(:split_module).returns(RDoc::Parser::SITE) @parser.stubs(:parse_elements) @topcontainer.expects(:global=).with(true) @parser.scan_top_level(@topcontainer) end it "should attach this module container to the toplevel container" do @parser.stubs(:parse_elements) @container.expects(:add_module).with(RDoc::PuppetModule, "module").returns(@module) @parser.scan_top_level(@topcontainer) end it "should defer ast parsing to parse_elements for this module" do @parser.expects(:parse_elements).with(@module) @parser.scan_top_level(@topcontainer) end it "should defer plugins parsing to parse_plugins for this module" do @parser.input_file_name = "module/lib/puppet/parser/function.rb" @parser.expects(:parse_plugins).with(@module) @parser.scan_top_level(@topcontainer) end end describe "when finding modules from filepath" do before :each do Puppet::Module.stubs(:modulepath).returns("/path/to/modules") end it "should return the module name for modulized puppet manifests" do File.stubs(:expand_path).returns("/path/to/module/manifests/init.pp") File.stubs(:identical?).with("/path/to", "/path/to/modules").returns(true) @parser.split_module("/path/to/modules/mymodule/manifests/init.pp").should == "module" end it "should return for manifests not under module path" do File.stubs(:expand_path).returns("/path/to/manifests/init.pp") File.stubs(:identical?).returns(false) @parser.split_module("/path/to/manifests/init.pp").should == RDoc::Parser::SITE end end describe "when parsing AST elements" do before :each do @klass = stub_everything 'klass', :file => "module/manifests/init.pp", :name => "myclass", :type => :hostclass @definition = stub_everything 'definition', :file => "module/manifests/init.pp", :type => :definition, :name => "mydef" @node = stub_everything 'node', :file => "module/manifests/init.pp", :type => :node, :name => "mynode" @resource_type_collection = Puppet::Resource::TypeCollection.new("env") @parser.ast = @resource_type_collection @container = stub_everything 'container' end it "should document classes in the parsed file" do @resource_type_collection.add_hostclass(@klass) @parser.expects(:document_class).with("myclass", @klass, @container) @parser.parse_elements(@container) end it "should not document class parsed in an other file" do @klass.stubs(:file).returns("/not/same/path/file.pp") @resource_type_collection.add_hostclass(@klass) @parser.expects(:document_class).with("myclass", @klass, @container).never @parser.parse_elements(@container) end it "should document vardefs for the main class" do @klass.stubs(:name).returns :main @resource_type_collection.add_hostclass(@klass) code = stub 'code', :is_a? => false @klass.stubs(:name).returns("") @klass.stubs(:code).returns(code) @parser.expects(:scan_for_vardef).with(@container, code) @parser.parse_elements(@container) end it "should document definitions in the parsed file" do @resource_type_collection.add_definition(@definition) @parser.expects(:document_define).with("mydef", @definition, @container) @parser.parse_elements(@container) end it "should not document definitions parsed in an other file" do @definition.stubs(:file).returns("/not/same/path/file.pp") @resource_type_collection.add_definition(@definition) @parser.expects(:document_define).with("mydef", @definition, @container).never @parser.parse_elements(@container) end it "should document nodes in the parsed file" do @resource_type_collection.add_node(@node) @parser.expects(:document_node).with("mynode", @node, @container) @parser.parse_elements(@container) end it "should not document node parsed in an other file" do @node.stubs(:file).returns("/not/same/path/file.pp") @resource_type_collection.add_node(@node) @parser.expects(:document_node).with("mynode", @node, @container).never @parser.parse_elements(@container) end end describe "when documenting definition" do before(:each) do @define = stub_everything 'define', :arguments => [], :doc => "mydoc", :file => "file", :line => 42 @class = stub_everything 'class' @parser.stubs(:get_class_or_module).returns([@class, "mydef"]) end it "should register a RDoc method to the current container" do @class.expects(:add_method).with { |m| m.name == "mydef"} @parser.document_define("mydef", @define, @class) end it "should attach the documentation to this method" do @class.expects(:add_method).with { |m| m.comment = "mydoc" } @parser.document_define("mydef", @define, @class) end it "should produce a better error message on unhandled exception" do @class.expects(:add_method).raises(ArgumentError) lambda { @parser.document_define("mydef", @define, @class) }.should raise_error(Puppet::ParseError, /in file at line 42/) end it "should convert all definition parameter to string" do arg = stub 'arg' val = stub 'val' @define.stubs(:arguments).returns({arg => val}) arg.expects(:to_s).returns("arg") val.expects(:to_s).returns("val") @parser.document_define("mydef", @define, @class) end end describe "when documenting nodes" do before :each do @code = stub_everything 'code' @node = stub_everything 'node', :doc => "mydoc", :parent => "parent", :code => @code, :file => "file", :line => 42 @rdoc_node = stub_everything 'rdocnode' @class = stub_everything 'class' @class.stubs(:add_node).returns(@rdoc_node) end it "should add a node to the current container" do @class.expects(:add_node).with("mynode", "parent").returns(@rdoc_node) @parser.document_node("mynode", @node, @class) end it "should associate the node documentation to the rdoc node" do @rdoc_node.expects(:comment=).with("mydoc") @parser.document_node("mynode", @node, @class) end it "should scan for include and require" do @parser.expects(:scan_for_include_or_require).with(@rdoc_node, @code) @parser.document_node("mynode", @node, @class) end it "should scan for variable definition" do @parser.expects(:scan_for_vardef).with(@rdoc_node, @code) @parser.document_node("mynode", @node, @class) end it "should scan for resources if needed" do Puppet.settings.stubs(:[]).with(:document_all).returns(true) @parser.expects(:scan_for_resource).with(@rdoc_node, @code) @parser.document_node("mynode", @node, @class) end it "should produce a better error message on unhandled exception" do @class.stubs(:add_node).raises(ArgumentError) lambda { @parser.document_node("mynode", @node, @class) }.should raise_error(Puppet::ParseError, /in file at line 42/) end end describe "when documenting classes" do before :each do @code = stub_everything 'code' @class = stub_everything 'class', :doc => "mydoc", :parent => "parent", :code => @code, :file => "file", :line => 42 @rdoc_class = stub_everything 'rdoc-class' @module = stub_everything 'class' @module.stubs(:add_class).returns(@rdoc_class) @parser.stubs(:get_class_or_module).returns([@module, "myclass"]) end it "should add a class to the current container" do @module.expects(:add_class).with(RDoc::PuppetClass, "myclass", "parent").returns(@rdoc_class) @parser.document_class("mynode", @class, @module) end it "should set the superclass" do @rdoc_class.expects(:superclass=).with("parent") @parser.document_class("mynode", @class, @module) end it "should associate the node documentation to the rdoc class" do @rdoc_class.expects(:comment=).with("mydoc") @parser.document_class("mynode", @class, @module) end it "should scan for include and require" do @parser.expects(:scan_for_include_or_require).with(@rdoc_class, @code) @parser.document_class("mynode", @class, @module) end it "should scan for resources if needed" do Puppet.settings.stubs(:[]).with(:document_all).returns(true) @parser.expects(:scan_for_resource).with(@rdoc_class, @code) @parser.document_class("mynode", @class, @module) end it "should produce a better error message on unhandled exception" do @module.stubs(:add_class).raises(ArgumentError) lambda { @parser.document_class("mynode", @class, @module) }.should raise_error(Puppet::ParseError, /in file at line 42/) end end describe "when scanning for includes and requires" do def create_stmt(name) - stmt_value = stub "#{name}_value", :value => "myclass" + stmt_value = stub "#{name}_value", :to_s => "myclass" Puppet::Parser::AST::Function.new( :name => name, :arguments => [stmt_value], :doc => 'mydoc' ) end before(:each) do @class = stub_everything 'class' @code = stub_everything 'code' @code.stubs(:is_a?).with(Puppet::Parser::AST::ASTArray).returns(true) end it "should also scan mono-instruction code" do @class.expects(:add_include).with { |i| i.is_a?(RDoc::Include) and i.name == "myclass" and i.comment == "mydoc" } - @parser.scan_for_include_or_require(@class,create_stmt("include")) + @parser.scan_for_include_or_require(@class, create_stmt("include")) end it "should register recursively includes to the current container" do @code.stubs(:children).returns([ create_stmt("include") ]) - @class.expects(:add_include).with { |i| i.is_a?(RDoc::Include) and i.name == "myclass" and i.comment == "mydoc" } + @class.expects(:add_include)#.with { |i| i.is_a?(RDoc::Include) and i.name == "myclass" and i.comment == "mydoc" } @parser.scan_for_include_or_require(@class, [@code]) end it "should register requires to the current container" do @code.stubs(:children).returns([ create_stmt("require") ]) @class.expects(:add_require).with { |i| i.is_a?(RDoc::Include) and i.name == "myclass" and i.comment == "mydoc" } @parser.scan_for_include_or_require(@class, [@code]) end end describe "when scanning for realized virtual resources" do def create_stmt stmt_value = stub "resource_ref", :to_s => "File[\"/tmp/a\"]" Puppet::Parser::AST::Function.new( :name => 'realize', :arguments => [stmt_value], :doc => 'mydoc' ) end before(:each) do @class = stub_everything 'class' @code = stub_everything 'code' @code.stubs(:is_a?).with(Puppet::Parser::AST::ASTArray).returns(true) end it "should also scan mono-instruction code" do @class.expects(:add_realize).with { |i| i.is_a?(RDoc::Include) and i.name == "File[\"/tmp/a\"]" and i.comment == "mydoc" } @parser.scan_for_realize(@class,create_stmt) end it "should register recursively includes to the current container" do @code.stubs(:children).returns([ create_stmt ]) @class.expects(:add_realize).with { |i| i.is_a?(RDoc::Include) and i.name == "File[\"/tmp/a\"]" and i.comment == "mydoc" } @parser.scan_for_realize(@class, [@code]) end end describe "when scanning for variable definition" do before :each do @class = stub_everything 'class' @stmt = stub_everything 'stmt', :name => "myvar", :value => "myvalue", :doc => "mydoc" @stmt.stubs(:is_a?).with(Puppet::Parser::AST::ASTArray).returns(false) @stmt.stubs(:is_a?).with(Puppet::Parser::AST::VarDef).returns(true) @code = stub_everything 'code' @code.stubs(:is_a?).with(Puppet::Parser::AST::ASTArray).returns(true) end it "should recursively register variables to the current container" do @code.stubs(:children).returns([ @stmt ]) @class.expects(:add_constant).with { |i| i.is_a?(RDoc::Constant) and i.name == "myvar" and i.comment == "mydoc" } @parser.scan_for_vardef(@class, [ @code ]) end it "should also scan mono-instruction code" do @class.expects(:add_constant).with { |i| i.is_a?(RDoc::Constant) and i.name == "myvar" and i.comment == "mydoc" } @parser.scan_for_vardef(@class, @stmt) end end describe "when scanning for resources" do before :each do @class = stub_everything 'class' @stmt = Puppet::Parser::AST::Resource.new( :type => "File", :title => "myfile", :doc => 'mydoc', :parameters => Puppet::Parser::AST::ASTArray.new(:children => []) ) @code = stub_everything 'code' @code.stubs(:is_a?).with(Puppet::Parser::AST::ASTArray).returns(true) end it "should register a PuppetResource to the current container" do @code.stubs(:children).returns([ @stmt ]) @class.expects(:add_resource).with { |i| i.is_a?(RDoc::PuppetResource) and i.title == "myfile" and i.comment == "mydoc" } @parser.scan_for_resource(@class, [ @code ]) end it "should also scan mono-instruction code" do @class.expects(:add_resource).with { |i| i.is_a?(RDoc::PuppetResource) and i.title == "myfile" and i.comment == "mydoc" } @parser.scan_for_resource(@class, @stmt) end end describe "when parsing plugins" do before :each do @container = stub 'container' end it "should delegate parsing custom facts to parse_facts" do @parser = RDoc::Parser.new(@top_level, "module/manifests/lib/puppet/facter/test.rb", nil, Options.instance, RDoc::Stats.new) @parser.expects(:parse_fact).with(@container) @parser.parse_plugins(@container) end it "should delegate parsing plugins to parse_plugins" do @parser = RDoc::Parser.new(@top_level, "module/manifests/lib/puppet/functions/test.rb", nil, Options.instance, RDoc::Stats.new) @parser.expects(:parse_puppet_plugin).with(@container) @parser.parse_plugins(@container) end end describe "when parsing plugins" do before :each do @container = stub_everything 'container' end it "should add custom functions to the container" do File.stubs(:open).yields("# documentation module Puppet::Parser::Functions newfunction(:myfunc, :type => :rvalue) do |args| File.dirname(args[0]) end end".split("\n")) @container.expects(:add_plugin).with do |plugin| plugin.comment == "documentation\n" #and plugin.name == "myfunc" end @parser.parse_puppet_plugin(@container) end it "should add custom types to the container" do File.stubs(:open).yields("# documentation Puppet::Type.newtype(:mytype) do end".split("\n")) @container.expects(:add_plugin).with do |plugin| plugin.comment == "documentation\n" #and plugin.name == "mytype" end @parser.parse_puppet_plugin(@container) end end describe "when parsing facts" do before :each do @container = stub_everything 'container' File.stubs(:open).yields(["# documentation", "Facter.add('myfact') do", "confine :kernel => :linux", "end"]) end it "should add facts to the container" do @container.expects(:add_fact).with do |fact| fact.comment == "documentation\n" and fact.name == "myfact" end @parser.parse_fact(@container) end it "should add confine to the parsed facts" do ourfact = nil @container.expects(:add_fact).with do |fact| ourfact = fact true end @parser.parse_fact(@container) ourfact.confine.should == { :type => "kernel", :value => ":linux" } end end end diff --git a/spec/unit/util/settings_spec.rb b/spec/unit/util/settings_spec.rb index 7bca44b76..07b712c08 100755 --- a/spec/unit/util/settings_spec.rb +++ b/spec/unit/util/settings_spec.rb @@ -1,1099 +1,1108 @@ #!/usr/bin/env ruby require File.dirname(__FILE__) + '/../../spec_helper' describe Puppet::Util::Settings do describe "when specifying defaults" do before do @settings = Puppet::Util::Settings.new end it "should start with no defined parameters" do @settings.params.length.should == 0 end it "should allow specification of default values associated with a section as an array" do @settings.setdefaults(:section, :myvalue => ["defaultval", "my description"]) end it "should not allow duplicate parameter specifications" do @settings.setdefaults(:section, :myvalue => ["a", "b"]) lambda { @settings.setdefaults(:section, :myvalue => ["c", "d"]) }.should raise_error(ArgumentError) end it "should allow specification of default values associated with a section as a hash" do @settings.setdefaults(:section, :myvalue => {:default => "defaultval", :desc => "my description"}) end it "should consider defined parameters to be valid" do @settings.setdefaults(:section, :myvalue => ["defaultval", "my description"]) @settings.valid?(:myvalue).should be_true end it "should require a description when defaults are specified with an array" do lambda { @settings.setdefaults(:section, :myvalue => ["a value"]) }.should raise_error(ArgumentError) end it "should require a description when defaults are specified with a hash" do lambda { @settings.setdefaults(:section, :myvalue => {:default => "a value"}) }.should raise_error(ArgumentError) end it "should raise an error if we can't guess the type" do lambda { @settings.setdefaults(:section, :myvalue => {:default => Object.new, :desc => "An impossible object"}) }.should raise_error(ArgumentError) end it "should support specifying owner, group, and mode when specifying files" do @settings.setdefaults(:section, :myvalue => {:default => "/some/file", :owner => "service", :mode => "boo", :group => "service", :desc => "whatever"}) end it "should support specifying a short name" do @settings.setdefaults(:section, :myvalue => {:default => "w", :desc => "b", :short => "m"}) end it "should support specifying the setting type" do @settings.setdefaults(:section, :myvalue => {:default => "/w", :desc => "b", :type => :setting}) @settings.setting(:myvalue).should be_instance_of(Puppet::Util::Settings::Setting) end it "should fail if an invalid setting type is specified" do lambda { @settings.setdefaults(:section, :myvalue => {:default => "w", :desc => "b", :type => :foo}) }.should raise_error(ArgumentError) end it "should fail when short names conflict" do @settings.setdefaults(:section, :myvalue => {:default => "w", :desc => "b", :short => "m"}) lambda { @settings.setdefaults(:section, :myvalue => {:default => "w", :desc => "b", :short => "m"}) }.should raise_error(ArgumentError) end end describe "when setting values" do before do @settings = Puppet::Util::Settings.new @settings.setdefaults :main, :myval => ["val", "desc"] @settings.setdefaults :main, :bool => [true, "desc"] end it "should provide a method for setting values from other objects" do @settings[:myval] = "something else" @settings[:myval].should == "something else" end it "should support a getopt-specific mechanism for setting values" do @settings.handlearg("--myval", "newval") @settings[:myval].should == "newval" end it "should support a getopt-specific mechanism for turning booleans off" do @settings[:bool] = true @settings.handlearg("--no-bool", "") @settings[:bool].should == false end it "should support a getopt-specific mechanism for turning booleans on" do # Turn it off first @settings[:bool] = false @settings.handlearg("--bool", "") @settings[:bool].should == true end it "should consider a cli setting with no argument to be a boolean" do # Turn it off first @settings[:bool] = false @settings.handlearg("--bool") @settings[:bool].should == true end it "should consider a cli setting with an empty string as an argument to be a boolean, if the setting itself is a boolean" do # Turn it off first @settings[:bool] = false @settings.handlearg("--bool", "") @settings[:bool].should == true end it "should consider a cli setting with an empty string as an argument to be an empty argument, if the setting itself is not a boolean" do @settings[:myval] = "bob" @settings.handlearg("--myval", "") @settings[:myval].should == "" end it "should consider a cli setting with a boolean as an argument to be a boolean" do # Turn it off first @settings[:bool] = false @settings.handlearg("--bool", "true") @settings[:bool].should == true end it "should not consider a cli setting of a non boolean with a boolean as an argument to be a boolean" do # Turn it off first @settings[:myval] = "bob" @settings.handlearg("--no-myval", "") @settings[:myval].should == "" end it "should clear the cache when setting getopt-specific values" do @settings.setdefaults :mysection, :one => ["whah", "yay"], :two => ["$one yay", "bah"] @settings[:two].should == "whah yay" @settings.handlearg("--one", "else") @settings[:two].should == "else yay" end it "should not clear other values when setting getopt-specific values" do @settings[:myval] = "yay" @settings.handlearg("--no-bool", "") @settings[:myval].should == "yay" end it "should clear the list of used sections" do @settings.expects(:clearused) @settings[:myval] = "yay" end it "should call passed blocks when values are set" do values = [] @settings.setdefaults(:section, :hooker => {:default => "yay", :desc => "boo", :hook => lambda { |v| values << v }}) values.should == [] @settings[:hooker] = "something" values.should == %w{something} end it "should call passed blocks when values are set via the command line" do values = [] @settings.setdefaults(:section, :hooker => {:default => "yay", :desc => "boo", :hook => lambda { |v| values << v }}) values.should == [] @settings.handlearg("--hooker", "yay") values.should == %w{yay} end it "should provide an option to call passed blocks during definition" do values = [] @settings.setdefaults(:section, :hooker => {:default => "yay", :desc => "boo", :call_on_define => true, :hook => lambda { |v| values << v }}) values.should == %w{yay} end it "should pass the fully interpolated value to the hook when called on definition" do values = [] @settings.setdefaults(:section, :one => ["test", "a"]) @settings.setdefaults(:section, :hooker => {:default => "$one/yay", :desc => "boo", :call_on_define => true, :hook => lambda { |v| values << v }}) values.should == %w{test/yay} end it "should munge values using the setting-specific methods" do @settings[:bool] = "false" @settings[:bool].should == false end it "should prefer cli values to values set in Ruby code" do @settings.handlearg("--myval", "cliarg") @settings[:myval] = "memarg" @settings[:myval].should == "cliarg" end it "should clear the list of environments" do Puppet::Node::Environment.expects(:clear).at_least(1) @settings[:myval] = "memarg" end it "should raise an error if we try to set 'name'" do lambda{ @settings[:name] = "foo" }.should raise_error(ArgumentError) end it "should raise an error if we try to set 'run_mode'" do lambda{ @settings[:run_mode] = "foo" }.should raise_error(ArgumentError) end it "should warn and use [master] if we ask for [puppetmasterd]" do Puppet.expects(:warning) @settings.set_value(:myval, "foo", :puppetmasterd) @settings.stubs(:run_mode).returns(:master) @settings.value(:myval).should == "foo" end it "should warn and use [agent] if we ask for [puppetd]" do Puppet.expects(:warning) @settings.set_value(:myval, "foo", :puppetd) @settings.stubs(:run_mode).returns(:agent) @settings.value(:myval).should == "foo" end end describe "when returning values" do before do @settings = Puppet::Util::Settings.new @settings.setdefaults :section, :config => ["/my/file", "eh"], :one => ["ONE", "a"], :two => ["$one TWO", "b"], :three => ["$one $two THREE", "c"], :four => ["$two $three FOUR", "d"] FileTest.stubs(:exist?).returns true end it "should provide a mechanism for returning set values" do @settings[:one] = "other" @settings[:one].should == "other" end it "should interpolate default values for other parameters into returned parameter values" do @settings[:one].should == "ONE" @settings[:two].should == "ONE TWO" @settings[:three].should == "ONE ONE TWO THREE" end it "should interpolate default values that themselves need to be interpolated" do @settings[:four].should == "ONE TWO ONE ONE TWO THREE FOUR" end it "should provide a method for returning uninterpolated values" do @settings[:two] = "$one tw0" @settings.uninterpolated_value(:two).should == "$one tw0" @settings.uninterpolated_value(:four).should == "$two $three FOUR" end it "should interpolate set values for other parameters into returned parameter values" do @settings[:one] = "on3" @settings[:two] = "$one tw0" @settings[:three] = "$one $two thr33" @settings[:four] = "$one $two $three f0ur" @settings[:one].should == "on3" @settings[:two].should == "on3 tw0" @settings[:three].should == "on3 on3 tw0 thr33" @settings[:four].should == "on3 on3 tw0 on3 on3 tw0 thr33 f0ur" end it "should not cache interpolated values such that stale information is returned" do @settings[:two].should == "ONE TWO" @settings[:one] = "one" @settings[:two].should == "one TWO" end it "should not cache values such that information from one environment is returned for another environment" do text = "[env1]\none = oneval\n[env2]\none = twoval\n" @settings.stubs(:read_file).returns(text) @settings.parse @settings.value(:one, "env1").should == "oneval" @settings.value(:one, "env2").should == "twoval" end it "should have a run_mode that defaults to user" do @settings.run_mode.should == :user end end describe "when choosing which value to return" do before do @settings = Puppet::Util::Settings.new @settings.setdefaults :section, :config => ["/my/file", "a"], - :one => ["ONE", "a"] + :one => ["ONE", "a"], + :two => ["TWO", "b"] FileTest.stubs(:exist?).returns true Puppet.stubs(:run_mode).returns stub('run_mode', :name => :mymode) end it "should return default values if no values have been set" do @settings[:one].should == "ONE" end it "should return values set on the cli before values set in the configuration file" do text = "[main]\none = fileval\n" @settings.stubs(:read_file).returns(text) @settings.handlearg("--one", "clival") @settings.parse @settings[:one].should == "clival" end it "should return values set on the cli before values set in Ruby" do @settings[:one] = "rubyval" @settings.handlearg("--one", "clival") @settings[:one].should == "clival" end it "should return values set in the mode-specific section before values set in the main section" do text = "[main]\none = mainval\n[mymode]\none = modeval\n" @settings.stubs(:read_file).returns(text) @settings.parse @settings[:one].should == "modeval" end it "should not return values outside of its search path" do text = "[other]\none = oval\n" file = "/some/file" @settings.stubs(:read_file).returns(text) @settings.parse @settings[:one].should == "ONE" end it "should return values in a specified environment" do text = "[env]\none = envval\n" @settings.stubs(:read_file).returns(text) @settings.parse @settings.value(:one, "env").should == "envval" end - it "should interpolate found values using the current environment" do + it 'should use the current environment for $environment' do @settings.setdefaults :main, :myval => ["$environment/foo", "mydocs"] @settings.value(:myval, "myenv").should == "myenv/foo" end + it "should interpolate found values using the current environment" do + text = "[main]\none = mainval\n[myname]\none = nameval\ntwo = $one/two\n" + @settings.stubs(:read_file).returns(text) + @settings.parse + + @settings.value(:two, "myname").should == "nameval/two" + end + it "should return values in a specified environment before values in the main or name sections" do text = "[env]\none = envval\n[main]\none = mainval\n[myname]\none = nameval\n" @settings.stubs(:read_file).returns(text) @settings.parse @settings.value(:one, "env").should == "envval" end end describe "when parsing its configuration" do before do @settings = Puppet::Util::Settings.new @settings.stubs(:service_user_available?).returns true @file = "/some/file" @settings.setdefaults :section, :user => ["suser", "doc"], :group => ["sgroup", "doc"] @settings.setdefaults :section, :config => ["/some/file", "eh"], :one => ["ONE", "a"], :two => ["$one TWO", "b"], :three => ["$one $two THREE", "c"] FileTest.stubs(:exist?).returns true end it "should not ignore the report setting" do @settings.setdefaults :section, :report => ["false", "a"] myfile = stub "myfile" @settings[:config] = myfile text = <<-CONF [puppetd] report=true CONF @settings.expects(:read_file).returns(text) @settings.parse @settings[:report].should be_true end it "should use its current ':config' value for the file to parse" do myfile = Puppet.features.posix? ? "/my/file" : "C:/myfile" # do not stub expand_path here, as this leads to a stack overflow, when mocha tries to use it @settings[:config] = myfile File.expects(:read).with(myfile).returns "[main]" @settings.parse end it "should fail if no configuration setting is defined" do @settings = Puppet::Util::Settings.new lambda { @settings.parse }.should raise_error(RuntimeError) end it "should not try to parse non-existent files" do FileTest.expects(:exist?).with("/some/file").returns false File.expects(:read).with("/some/file").never @settings.parse end it "should set a timer that triggers reparsing, even if the file does not exist" do FileTest.expects(:exist?).returns false @settings.expects(:set_filetimeout_timer) @settings.parse end it "should return values set in the configuration file" do text = "[main] one = fileval " @settings.expects(:read_file).returns(text) @settings.parse @settings[:one].should == "fileval" end #484 - this should probably be in the regression area it "should not throw an exception on unknown parameters" do text = "[main]\nnosuchparam = mval\n" @settings.expects(:read_file).returns(text) lambda { @settings.parse }.should_not raise_error end it "should convert booleans in the configuration file into Ruby booleans" do text = "[main] one = true two = false " @settings.expects(:read_file).returns(text) @settings.parse @settings[:one].should == true @settings[:two].should == false end it "should convert integers in the configuration file into Ruby Integers" do text = "[main] one = 65 " @settings.expects(:read_file).returns(text) @settings.parse @settings[:one].should == 65 end it "should support specifying all metadata (owner, group, mode) in the configuration file" do @settings.setdefaults :section, :myfile => ["/myfile", "a"] text = "[main] myfile = /other/file {owner = service, group = service, mode = 644} " @settings.expects(:read_file).returns(text) @settings.parse @settings[:myfile].should == "/other/file" @settings.metadata(:myfile).should == {:owner => "suser", :group => "sgroup", :mode => "644"} end it "should support specifying a single piece of metadata (owner, group, or mode) in the configuration file" do @settings.setdefaults :section, :myfile => ["/myfile", "a"] text = "[main] myfile = /other/file {owner = service} " file = "/some/file" @settings.expects(:read_file).returns(text) @settings.parse @settings[:myfile].should == "/other/file" @settings.metadata(:myfile).should == {:owner => "suser"} end it "should call hooks associated with values set in the configuration file" do values = [] @settings.setdefaults :section, :mysetting => {:default => "defval", :desc => "a", :hook => proc { |v| values << v }} text = "[main] mysetting = setval " @settings.expects(:read_file).returns(text) @settings.parse values.should == ["setval"] end it "should not call the same hook for values set multiple times in the configuration file" do values = [] @settings.setdefaults :section, :mysetting => {:default => "defval", :desc => "a", :hook => proc { |v| values << v }} text = "[user] mysetting = setval [main] mysetting = other " @settings.expects(:read_file).returns(text) @settings.parse values.should == ["setval"] end it "should pass the environment-specific value to the hook when one is available" do values = [] @settings.setdefaults :section, :mysetting => {:default => "defval", :desc => "a", :hook => proc { |v| values << v }} @settings.setdefaults :section, :environment => ["yay", "a"] @settings.setdefaults :section, :environments => ["yay,foo", "a"] text = "[main] mysetting = setval [yay] mysetting = other " @settings.expects(:read_file).returns(text) @settings.parse values.should == ["other"] end it "should pass the interpolated value to the hook when one is available" do values = [] @settings.setdefaults :section, :base => {:default => "yay", :desc => "a", :hook => proc { |v| values << v }} @settings.setdefaults :section, :mysetting => {:default => "defval", :desc => "a", :hook => proc { |v| values << v }} text = "[main] mysetting = $base/setval " @settings.expects(:read_file).returns(text) @settings.parse values.should == ["yay/setval"] end it "should allow empty values" do @settings.setdefaults :section, :myarg => ["myfile", "a"] text = "[main] myarg = " @settings.stubs(:read_file).returns(text) @settings.parse @settings[:myarg].should == "" end describe "and when reading a non-positive filetimeout value from the config file" do before do @settings.setdefaults :foo, :filetimeout => [5, "eh"] somefile = "/some/file" text = "[main] filetimeout = -1 " File.expects(:read).with(somefile).returns(text) File.expects(:expand_path).with(somefile).returns somefile @settings[:config] = somefile end it "should not set a timer" do EventLoop::Timer.expects(:new).never @settings.parse end end end describe "when reparsing its configuration" do before do @settings = Puppet::Util::Settings.new @settings.setdefaults :section, :config => ["/test/file", "a"], :one => ["ONE", "a"], :two => ["$one TWO", "b"], :three => ["$one $two THREE", "c"] FileTest.stubs(:exist?).returns true end it "should use a LoadedFile instance to determine if the file has changed" do file = mock 'file' Puppet::Util::LoadedFile.expects(:new).with("/test/file").returns file file.expects(:changed?) @settings.stubs(:parse) @settings.reparse end it "should not create the LoadedFile instance and should not parse if the file does not exist" do FileTest.expects(:exist?).with("/test/file").returns false Puppet::Util::LoadedFile.expects(:new).never @settings.expects(:parse).never @settings.reparse end it "should not reparse if the file has not changed" do file = mock 'file' Puppet::Util::LoadedFile.expects(:new).with("/test/file").returns file file.expects(:changed?).returns false @settings.expects(:parse).never @settings.reparse end it "should reparse if the file has changed" do file = stub 'file', :file => "/test/file" Puppet::Util::LoadedFile.expects(:new).with("/test/file").returns file file.expects(:changed?).returns true @settings.expects(:parse) @settings.reparse end it "should use a cached LoadedFile instance" do first = mock 'first' second = mock 'second' Puppet::Util::LoadedFile.expects(:new).times(2).with("/test/file").returns(first).then.returns(second) @settings.file.should equal(first) Puppet::Util::Cacher.expire @settings.file.should equal(second) end it "should replace in-memory values with on-file values" do # Init the value text = "[main]\none = disk-init\n" file = mock 'file' file.stubs(:changed?).returns(true) file.stubs(:file).returns("/test/file") @settings[:one] = "init" @settings.file = file # Now replace the value text = "[main]\none = disk-replace\n" # This is kinda ridiculous - the reason it parses twice is that # it goes to parse again when we ask for the value, because the # mock always says it should get reparsed. @settings.stubs(:read_file).returns(text) @settings.reparse @settings[:one].should == "disk-replace" end it "should retain parameters set by cli when configuration files are reparsed" do @settings.handlearg("--one", "clival") text = "[main]\none = on-disk\n" @settings.stubs(:read_file).returns(text) @settings.parse @settings[:one].should == "clival" end it "should remove in-memory values that are no longer set in the file" do # Init the value text = "[main]\none = disk-init\n" @settings.expects(:read_file).returns(text) @settings.parse @settings[:one].should == "disk-init" # Now replace the value text = "[main]\ntwo = disk-replace\n" @settings.expects(:read_file).returns(text) @settings.parse #@settings.reparse # The originally-overridden value should be replaced with the default @settings[:one].should == "ONE" # and we should now have the new value in memory @settings[:two].should == "disk-replace" end it "should retain in-memory values if the file has a syntax error" do # Init the value text = "[main]\none = initial-value\n" @settings.expects(:read_file).returns(text) @settings.parse @settings[:one].should == "initial-value" # Now replace the value with something bogus text = "[main]\nkenny = killed-by-what-follows\n1 is 2, blah blah florp\n" @settings.expects(:read_file).returns(text) @settings.parse # The originally-overridden value should not be replaced with the default @settings[:one].should == "initial-value" # and we should not have the new value in memory @settings[:kenny].should be_nil end end it "should provide a method for creating a catalog of resources from its configuration" do Puppet::Util::Settings.new.should respond_to(:to_catalog) end describe "when creating a catalog" do before do @settings = Puppet::Util::Settings.new @settings.stubs(:service_user_available?).returns true @prefix = Puppet.features.posix? ? "" : "C:" end it "should add all file resources to the catalog if no sections have been specified" do @settings.setdefaults :main, :maindir => [@prefix+"/maindir", "a"], :seconddir => [@prefix+"/seconddir", "a"] @settings.setdefaults :other, :otherdir => [@prefix+"/otherdir", "a"] catalog = @settings.to_catalog [@prefix+"/maindir", @prefix+"/seconddir", @prefix+"/otherdir"].each do |path| catalog.resource(:file, path).should be_instance_of(Puppet::Resource) end end it "should add only files in the specified sections if section names are provided" do @settings.setdefaults :main, :maindir => [@prefix+"/maindir", "a"] @settings.setdefaults :other, :otherdir => [@prefix+"/otherdir", "a"] catalog = @settings.to_catalog(:main) catalog.resource(:file, @prefix+"/otherdir").should be_nil catalog.resource(:file, @prefix+"/maindir").should be_instance_of(Puppet::Resource) end it "should not try to add the same file twice" do @settings.setdefaults :main, :maindir => [@prefix+"/maindir", "a"] @settings.setdefaults :other, :otherdir => [@prefix+"/maindir", "a"] lambda { @settings.to_catalog }.should_not raise_error end it "should ignore files whose :to_resource method returns nil" do @settings.setdefaults :main, :maindir => [@prefix+"/maindir", "a"] @settings.setting(:maindir).expects(:to_resource).returns nil Puppet::Resource::Catalog.any_instance.expects(:add_resource).never @settings.to_catalog end describe "when adding users and groups to the catalog" do before do Puppet.features.stubs(:root?).returns true @settings.setdefaults :foo, :mkusers => [true, "e"], :user => ["suser", "doc"], :group => ["sgroup", "doc"] @settings.setdefaults :other, :otherdir => {:default => "/otherdir", :desc => "a", :owner => "service", :group => "service"} @catalog = @settings.to_catalog end it "should add each specified user and group to the catalog if :mkusers is a valid setting, is enabled, and we're running as root" do @catalog.resource(:user, "suser").should be_instance_of(Puppet::Resource) @catalog.resource(:group, "sgroup").should be_instance_of(Puppet::Resource) end it "should only add users and groups to the catalog from specified sections" do @settings.setdefaults :yay, :yaydir => {:default => "/yaydir", :desc => "a", :owner => "service", :group => "service"} catalog = @settings.to_catalog(:other) catalog.resource(:user, "jane").should be_nil catalog.resource(:group, "billy").should be_nil end it "should not add users or groups to the catalog if :mkusers not running as root" do Puppet.features.stubs(:root?).returns false catalog = @settings.to_catalog catalog.resource(:user, "suser").should be_nil catalog.resource(:group, "sgroup").should be_nil end it "should not add users or groups to the catalog if :mkusers is not a valid setting" do Puppet.features.stubs(:root?).returns true settings = Puppet::Util::Settings.new settings.setdefaults :other, :otherdir => {:default => "/otherdir", :desc => "a", :owner => "service", :group => "service"} catalog = settings.to_catalog catalog.resource(:user, "suser").should be_nil catalog.resource(:group, "sgroup").should be_nil end it "should not add users or groups to the catalog if :mkusers is a valid setting but is disabled" do @settings[:mkusers] = false catalog = @settings.to_catalog catalog.resource(:user, "suser").should be_nil catalog.resource(:group, "sgroup").should be_nil end it "should not try to add users or groups to the catalog twice" do @settings.setdefaults :yay, :yaydir => {:default => "/yaydir", :desc => "a", :owner => "service", :group => "service"} # This would fail if users/groups were added twice lambda { @settings.to_catalog }.should_not raise_error end it "should set :ensure to :present on each created user and group" do @catalog.resource(:user, "suser")[:ensure].should == :present @catalog.resource(:group, "sgroup")[:ensure].should == :present end it "should set each created user's :gid to the service group" do @settings.to_catalog.resource(:user, "suser")[:gid].should == "sgroup" end it "should not attempt to manage the root user" do Puppet.features.stubs(:root?).returns true @settings.setdefaults :foo, :foodir => {:default => "/foodir", :desc => "a", :owner => "root", :group => "service"} @settings.to_catalog.resource(:user, "root").should be_nil end end end it "should be able to be converted to a manifest" do Puppet::Util::Settings.new.should respond_to(:to_manifest) end describe "when being converted to a manifest" do it "should produce a string with the code for each resource joined by two carriage returns" do @settings = Puppet::Util::Settings.new @settings.setdefaults :main, :maindir => ["/maindir", "a"], :seconddir => ["/seconddir", "a"] main = stub 'main_resource', :ref => "File[/maindir]" main.expects(:to_manifest).returns "maindir" second = stub 'second_resource', :ref => "File[/seconddir]" second.expects(:to_manifest).returns "seconddir" @settings.setting(:maindir).expects(:to_resource).returns main @settings.setting(:seconddir).expects(:to_resource).returns second @settings.to_manifest.split("\n\n").sort.should == %w{maindir seconddir} end end describe "when using sections of the configuration to manage the local host" do before do @settings = Puppet::Util::Settings.new @settings.stubs(:service_user_available?).returns true @settings.setdefaults :main, :noop => [false, ""] @settings.setdefaults :main, :maindir => ["/maindir", "a"], :seconddir => ["/seconddir", "a"] @settings.setdefaults :main, :user => ["suser", "doc"], :group => ["sgroup", "doc"] @settings.setdefaults :other, :otherdir => {:default => "/otherdir", :desc => "a", :owner => "service", :group => "service", :mode => 0755} @settings.setdefaults :third, :thirddir => ["/thirddir", "b"] @settings.setdefaults :files, :myfile => {:default => "/myfile", :desc => "a", :mode => 0755} end it "should provide a method that writes files with the correct modes" do @settings.should respond_to(:write) end it "should provide a method that creates directories with the correct modes" do Puppet::Util::SUIDManager.expects(:asuser).with("suser", "sgroup").yields Dir.expects(:mkdir).with("/otherdir", 0755) @settings.mkdir(:otherdir) end it "should create a catalog with the specified sections" do @settings.expects(:to_catalog).with(:main, :other).returns Puppet::Resource::Catalog.new("foo") @settings.use(:main, :other) end it "should canonicalize the sections" do @settings.expects(:to_catalog).with(:main, :other).returns Puppet::Resource::Catalog.new("foo") @settings.use("main", "other") end it "should ignore sections that have already been used" do @settings.expects(:to_catalog).with(:main).returns Puppet::Resource::Catalog.new("foo") @settings.use(:main) @settings.expects(:to_catalog).with(:other).returns Puppet::Resource::Catalog.new("foo") @settings.use(:main, :other) end it "should ignore tags and schedules when creating files and directories" it "should be able to provide all of its parameters in a format compatible with GetOpt::Long" do pending "Not converted from test/unit yet" end it "should convert the created catalog to a RAL catalog" do @catalog = Puppet::Resource::Catalog.new("foo") @settings.expects(:to_catalog).with(:main).returns @catalog @catalog.expects(:to_ral).returns @catalog @settings.use(:main) end it "should specify that it is not managing a host catalog" do catalog = Puppet::Resource::Catalog.new("foo") catalog.expects(:apply) @settings.expects(:to_catalog).returns catalog catalog.stubs(:to_ral).returns catalog catalog.expects(:host_config=).with false @settings.use(:main) end it "should support a method for re-using all currently used sections" do @settings.expects(:to_catalog).with(:main, :third).times(2).returns Puppet::Resource::Catalog.new("foo") @settings.use(:main, :third) @settings.reuse end it "should fail with an appropriate message if any resources fail" do @catalog = Puppet::Resource::Catalog.new("foo") @catalog.stubs(:to_ral).returns @catalog @settings.expects(:to_catalog).returns @catalog @trans = mock("transaction") @catalog.expects(:apply).yields(@trans) @trans.expects(:any_failed?).returns(true) report = mock 'report' @trans.expects(:report).returns report log = mock 'log', :to_s => "My failure", :level => :err report.expects(:logs).returns [log] @settings.expects(:raise).with { |msg| msg.include?("My failure") } @settings.use(:whatever) end end describe "when dealing with printing configs" do before do @settings = Puppet::Util::Settings.new #these are the magic default values @settings.stubs(:value).with(:configprint).returns("") @settings.stubs(:value).with(:genconfig).returns(false) @settings.stubs(:value).with(:genmanifest).returns(false) @settings.stubs(:value).with(:environment).returns(nil) end describe "when checking print_config?" do it "should return false when the :configprint, :genconfig and :genmanifest are not set" do @settings.print_configs?.should be_false end it "should return true when :configprint has a value" do @settings.stubs(:value).with(:configprint).returns("something") @settings.print_configs?.should be_true end it "should return true when :genconfig has a value" do @settings.stubs(:value).with(:genconfig).returns(true) @settings.print_configs?.should be_true end it "should return true when :genmanifest has a value" do @settings.stubs(:value).with(:genmanifest).returns(true) @settings.print_configs?.should be_true end end describe "when printing configs" do describe "when :configprint has a value" do it "should call print_config_options" do @settings.stubs(:value).with(:configprint).returns("something") @settings.expects(:print_config_options) @settings.print_configs end it "should get the value of the option using the environment" do @settings.stubs(:value).with(:configprint).returns("something") @settings.stubs(:include?).with("something").returns(true) @settings.expects(:value).with(:environment).returns("env") @settings.expects(:value).with("something", "env").returns("foo") @settings.stubs(:puts).with("foo") @settings.print_configs end it "should print the value of the option" do @settings.stubs(:value).with(:configprint).returns("something") @settings.stubs(:include?).with("something").returns(true) @settings.stubs(:value).with("something", nil).returns("foo") @settings.expects(:puts).with("foo") @settings.print_configs end it "should print the value pairs if there are multiple options" do @settings.stubs(:value).with(:configprint).returns("bar,baz") @settings.stubs(:include?).with("bar").returns(true) @settings.stubs(:include?).with("baz").returns(true) @settings.stubs(:value).with("bar", nil).returns("foo") @settings.stubs(:value).with("baz", nil).returns("fud") @settings.expects(:puts).with("bar = foo") @settings.expects(:puts).with("baz = fud") @settings.print_configs end it "should print a whole bunch of stuff if :configprint = all" it "should return true after printing" do @settings.stubs(:value).with(:configprint).returns("something") @settings.stubs(:include?).with("something").returns(true) @settings.stubs(:value).with("something", nil).returns("foo") @settings.stubs(:puts).with("foo") @settings.print_configs.should be_true end it "should return false if a config param is not found" do @settings.stubs :puts @settings.stubs(:value).with(:configprint).returns("something") @settings.stubs(:include?).with("something").returns(false) @settings.print_configs.should be_false end end describe "when genconfig is true" do before do @settings.stubs :puts end it "should call to_config" do @settings.stubs(:value).with(:genconfig).returns(true) @settings.expects(:to_config) @settings.print_configs end it "should return true from print_configs" do @settings.stubs(:value).with(:genconfig).returns(true) @settings.stubs(:to_config) @settings.print_configs.should be_true end end describe "when genmanifest is true" do before do @settings.stubs :puts end it "should call to_config" do @settings.stubs(:value).with(:genmanifest).returns(true) @settings.expects(:to_manifest) @settings.print_configs end it "should return true from print_configs" do @settings.stubs(:value).with(:genmanifest).returns(true) @settings.stubs(:to_manifest) @settings.print_configs.should be_true end end end end describe "when setting a timer to trigger configuration file reparsing" do before do @settings = Puppet::Util::Settings.new @settings.setdefaults :foo, :filetimeout => [5, "eh"] end it "should do nothing if no filetimeout setting is available" do @settings.expects(:value).with(:filetimeout).returns nil EventLoop::Timer.expects(:new).never @settings.set_filetimeout_timer end it "should always convert the timer interval to an integer" do @settings.expects(:value).with(:filetimeout).returns "10" EventLoop::Timer.expects(:new).with(:interval => 10, :start? => true, :tolerance => 1) @settings.set_filetimeout_timer end it "should do nothing if the filetimeout setting is not greater than 0" do @settings.expects(:value).with(:filetimeout).returns -2 EventLoop::Timer.expects(:new).never @settings.set_filetimeout_timer end it "should create a timer with its interval set to the filetimeout, start? set to true, and a tolerance of 1" do @settings.expects(:value).with(:filetimeout).returns 5 EventLoop::Timer.expects(:new).with(:interval => 5, :start? => true, :tolerance => 1) @settings.set_filetimeout_timer end it "should reparse when the timer goes off" do EventLoop::Timer.expects(:new).with(:interval => 5, :start? => true, :tolerance => 1).yields @settings.expects(:reparse) @settings.set_filetimeout_timer end end describe "when determining if the service user is available" do it "should return false if there is no user setting" do Puppet::Util::Settings.new.should_not be_service_user_available end it "should return false if the user provider says the user is missing" do settings = Puppet::Util::Settings.new settings.setdefaults :main, :user => ["foo", "doc"] user = mock 'user' user.expects(:exists?).returns false Puppet::Type.type(:user).expects(:new).with { |args| args[:name] == "foo" }.returns user settings.should_not be_service_user_available end it "should return true if the user provider says the user is present" do settings = Puppet::Util::Settings.new settings.setdefaults :main, :user => ["foo", "doc"] user = mock 'user' user.expects(:exists?).returns true Puppet::Type.type(:user).expects(:new).with { |args| args[:name] == "foo" }.returns user settings.should be_service_user_available end it "should cache the result" end end diff --git a/tasks/rake/git_workflow.rake b/tasks/rake/git_workflow.rake index b2f96c603..980d2fbce 100644 --- a/tasks/rake/git_workflow.rake +++ b/tasks/rake/git_workflow.rake @@ -1,121 +1,121 @@ # This set of tasks helps automate the workflow as described on -# http://reductivelabs.com/trac/puppet/wiki/Development/DevelopmentLifecycle +# http://projects.puppetlabs.com/projects/puppet/wiki/Development_Lifecycle def find_start(start) # This is a case statement, as we might want to map certain # git tags to starting points that are not currently in git. case start when nil?: when @next_release: return "master" else return start end end desc "Set up git for working with Puppet" task :git_setup do # This should be changed as new versions get released @next_release = '0.26.x' @remote = {} default_remote = {} default_remote[:url] = 'git://github.com/reductivelabs/puppet' default_remote[:name] = 'origin' @remote[:name] = %x{git config puppet.defaultremote}.chomp @remote[:name] = @remote[:name].empty? ? default_remote[:name] : @remote[:name] @remote[:url] = default_remote[:url] if @remote[:name] == default_remote[:name] default_fetch = '+refs/heads/*:refs/remotes/puppet/*' @remote[:fetch] = %x{git config puppet.#{@remote[:name]}.fetch}.chomp @remote[:fetch] = @remote[:fetch].empty? ? default_fetch : @remote[:fetch] end desc "Start work on a feature" task :start_feature, [:feature,:remote,:branch] => :git_setup do |t, args| args.with_defaults(:remote => @remote[:name]) args.with_defaults(:branch => @next_release) start_at = find_start(args.branch) branch = "feature/#{start_at}/#{args.feature}" sh "git checkout -b #{branch} #{start_at}" do |ok, res| if ! ok raise < :git_setup do |t, args| args.with_defaults(:remote => @remote[:name]) args.with_defaults(:branch => @next_release) start_at = find_start(args.branch) branch = "tickets/#{start_at}/#{args.ticket}" sh "git checkout -b #{branch} #{start_at}" do |ok, res| unless ok raise < 0 raise "Patches already exist matching '00*.patch'; clean up first" end unless %x{git status} =~ /On branch (.+)/ raise "Could not get branch from 'git status'" end branch = $1 unless branch =~ %r{^([^\/]+)/([^\/]+)/([^\/]+)$} raise "Branch name does not follow // model; cannot autodetect parent branch" end type, parent, name = $1, $2, $3 # Create all of the patches sh "git format-patch -C -M -s -n --subject-prefix='PATCH/puppet' #{parent}..HEAD" # And then mail them out. # If we've got more than one patch, add --compose if Dir.glob("00*.patch").length > 1 compose = "--compose" else compose = "" end # Now send the mail. sh "git send-email #{compose} --no-signed-off-by-cc --suppress-from --to puppet-dev@googlegroups.com 00*.patch" # Finally, clean up the patches sh "rm 00*.patch" end diff --git a/test/Rakefile b/test/Rakefile index 47be213d9..7cde050f6 100644 --- a/test/Rakefile +++ b/test/Rakefile @@ -1,98 +1,96 @@ # let Emacs know it's -*- ruby -*- begin require 'rake/testtask' rescue LoadError $stderr.puts "You must have 'rake' installed to use this file" exit(1) end require 'find' include Find include FileTest $exclusions = %W(lib data) $test_library_paths = %W(lib ../lib) $: << File.join(Dir.getwd, "lib") require 'rake/puppet_testtask' filemap = Hash.new { |hash, key| hash[key] = [] } allfiles = [] # First collect the entire file list. find(".") do |f| # Get rid of the leading ./ f = f.sub(/^\.\//, '') file = File.basename(f) dir = File.dirname(f) # Prune . directories and excluded dirs if (file =~ /^\./ and f != ".") or $exclusions.include?(File.basename(file)) prune next end next if f == "." next if dir == "." # If we're a ruby script, then add it to the list of files for that dir if file =~ /\.rb$/ allfiles << f # Add it to all of the parent dirs, not just our own parts = File.split(dir) if parts[0] == "." parts.shift end parts.each_with_index { |part, i| path = File.join(parts[0..i]) filemap[path] << f } end end desc "Run the full test suite" Rake::PuppetTestTask.new :test do |t| t.libs += $test_library_paths # Add every file as a test file to run t.test_files = allfiles t.verbose = true end task :default => :test # Now create a task for every directory filemap.each do |dir, files| ns = dir.gsub "/", ":" # First create a separate task for each file in the namespace. namespace ns do files.each do |file| Rake::PuppetTestTask.new File.basename(file, '.rb').to_sym do |t| t.libs += $test_library_paths + ['..'] t.libs << '..' t.test_files = [ file ] t.verbose = true end end end # Then create a task that matches the directory itself. Rake::PuppetTestTask.new dir do |t| t.libs += $test_library_paths if ENV["TESTFILES"] t.test_files = ENV["TESTFILES"].split(/\s+/) else t.test_files = files.sort end t.verbose = true end # And alias it with a slash on the end task(dir + "/" => dir) end - -# $Id$ diff --git a/test/data/providers/mount/parsed/aix.mount b/test/data/providers/mount/parsed/aix.mount new file mode 100644 index 000000000..380dbc5ae --- /dev/null +++ b/test/data/providers/mount/parsed/aix.mount @@ -0,0 +1,7 @@ +node mounted mounted over vfs date options +---- ------- ------------ --- ------------ ------------------- + /dev/hd0 / jfs Dec 17 08:04 rw, log =/dev/hd8 + /dev/hd3 /tmp jfs Dec 17 08:04 rw, log =/dev/hd8 + /dev/hd1 /home jfs Dec 17 08:06 rw, log =/dev/hd8 + /dev/hd2 /usr jfs Dec 17 08:06 rw, log =/dev/hd8 +sue /home/local/src /usr/code nfs Dec 17 08:06 ro, log =/dev/hd8 diff --git a/test/data/providers/mount/parsed/darwin.mount b/test/data/providers/mount/parsed/darwin.mount new file mode 100644 index 000000000..1bdfcf89a --- /dev/null +++ b/test/data/providers/mount/parsed/darwin.mount @@ -0,0 +1,6 @@ +/dev/disk0s2 on / (hfs, local, journaled) +devfs on /dev (devfs, local, nobrowse) +map -hosts on /net (autofs, nosuid, automounted, nobrowse) +map auto_home on /home (autofs, automounted, nobrowse) +/dev/disk0s3 on /usr (hfs, local, journaled) +/dev/fake on /ghost (hfs, local, journaled) diff --git a/test/data/providers/mount/parsed/hpux.mount b/test/data/providers/mount/parsed/hpux.mount new file mode 100644 index 000000000..d414fa47a --- /dev/null +++ b/test/data/providers/mount/parsed/hpux.mount @@ -0,0 +1,17 @@ +/ on rpool/ROOT/opensolaris read/write/setuid/devices/dev=2d90002 on Wed Dec 31 16:00:00 1969 +/devices on /devices read/write/setuid/devices/dev=4a00000 on Thu Feb 17 14:34:02 2011 +/dev on /dev read/write/setuid/devices/dev=4a40000 on Thu Feb 17 14:34:02 2011 +/system/contract on ctfs read/write/setuid/devices/dev=4ac0001 on Thu Feb 17 14:34:02 2011 +/proc on proc read/write/setuid/devices/dev=4b00000 on Thu Feb 17 14:34:02 2011 +/etc/mnttab on mnttab read/write/setuid/devices/dev=4b40001 on Thu Feb 17 14:34:02 2011 +/etc/svc/volatile on swap read/write/setuid/devices/xattr/dev=4b80001 on Thu Feb 17 14:34:02 2011 +/system/object on objfs read/write/setuid/devices/dev=4bc0001 on Thu Feb 17 14:34:02 2011 +/etc/dfs/sharetab on sharefs read/write/setuid/devices/dev=4c00001 on Thu Feb 17 14:34:02 2011 +/lib/libc.so.1 on /usr/lib/libc/libc_hwcap1.so.1 read/write/setuid/devices/dev=2d90002 on Thu Feb 17 14:34:14 2011 +/dev/fd on fd read/write/setuid/devices/dev=4d00001 on Thu Feb 17 14:34:18 2011 +/tmp on swap read/write/setuid/devices/xattr/dev=4b80002 on Thu Feb 17 14:34:19 2011 +/var/run on swap read/write/setuid/devices/xattr/dev=4b80003 on Thu Feb 17 14:34:19 2011 +/export on rpool/export read/write/setuid/devices/nonbmand/exec/xattr/atime/dev=2d90006 on Thu Feb 17 14:37:48 2011 +/export/home on rpool/export/home read/write/setuid/devices/nonbmand/exec/xattr/atime/dev=2d90007 on Thu Feb 17 14:37:48 2011 +/rpool on rpool read/write/setuid/devices/nonbmand/exec/xattr/atime/dev=2d90009 on Thu Feb 17 14:37:48 2011 +/ghost on /dev/fake read/write/setuid/devices/nonbmand/exec/xattr/atime/dev=2d90009 on Thu Feb 17 14:37:48 2011 diff --git a/test/data/providers/mount/parsed/linux.mount b/test/data/providers/mount/parsed/linux.mount new file mode 100644 index 000000000..75dd71fd4 --- /dev/null +++ b/test/data/providers/mount/parsed/linux.mount @@ -0,0 +1,5 @@ +/dev/root on / type jfs (rw,noatime) +rc-svcdir on /lib64/rc/init.d type tmpfs (rw,nosuid,nodev,noexec,relatime,size=1024k,mode=755) +sysfs on /sys type sysfs (rw,nosuid,nodev,noexec,relatime) +/dev/sda9 on /usr/portage type jfs (rw) +/dev/fake on /ghost type jfs (rw) diff --git a/test/data/providers/mount/parsed/solaris.mount b/test/data/providers/mount/parsed/solaris.mount new file mode 100644 index 000000000..26fabc575 --- /dev/null +++ b/test/data/providers/mount/parsed/solaris.mount @@ -0,0 +1,6 @@ +/ on /dev/dsk/c0t0d0s0 read/write/setuid/intr/largefiles/xattr/onerror=panic/dev=2200000 on Mon Mar 18 08:48:45 2002 +/proc on /proc read/write/setuid/dev=4300000 on Mon Mar 18 08:48:44 2002 +/etc/mnttab on mnttab read/write/setuid/dev=43c0000 on Mon Mar 18 08:48:44 2002 +/tmp on swap read/write/setuid/xattr/dev=2 on Mon Mar 18 08:48:52 2002 +/export/home on /dev/dsk/c0t0d0s7 read/write/setuid/intr/largefiles/xattr/onerror=panic/dev=2200007 on Mon Mar 18 +/ghost on /dev/dsk/c0t1d0s7 read/write/setuid/intr/largefiles/xattr/onerror=panic/dev=2200007 on Mon Mar 18 diff --git a/test/data/types/mount/linux.fstab b/test/data/types/mount/linux.fstab index b1debff9c..c94ec7fa8 100644 --- a/test/data/types/mount/linux.fstab +++ b/test/data/types/mount/linux.fstab @@ -1,11 +1,12 @@ # A sample fstab, typical for a Fedora system /dev/vg00/lv00 / ext3 defaults 1 1 LABEL=/boot /boot ext3 defaults 1 2 devpts /dev/pts devpts gid=5,mode=620 0 tmpfs /dev/shm tmpfs defaults 0 LABEL=/home /home ext3 defaults 1 2 /home /homes auto bind 0 2 proc /proc proc defaults 0 0 /dev/vg00/lv01 /spare ext3 defaults 1 2 sysfs /sys sysfs defaults 0 0 LABEL=SWAP-hda6 swap swap defaults 0 0 +/dev/sda1 /usr xfs noatime 0 0 diff --git a/test/data/types/mount/solaris.fstab b/test/data/types/mount/solaris.fstab index 54afc898c..348b9d50b 100644 --- a/test/data/types/mount/solaris.fstab +++ b/test/data/types/mount/solaris.fstab @@ -1,11 +1,12 @@ #device device mount FS fsck mount mount #to mount to fsck point type pass at boot options # fd - /dev/fd fd - no - /proc - /proc proc - no - /dev/dsk/c0d0s0 /dev/rdsk/c0d0s0 / ufs 1 no - /dev/dsk/c0d0p0:boot - /boot pcfs - no - /devices - /devices devfs - no - ctfs - /system/contract ctfs - no - objfs - /system/object objfs - no - #swap - /tmp tmpfs - yes - +/dev/dsk/c0d0s2 /dev/rdsk/c0d0s2 /usr ufs 1 no -