diff --git a/CHANGELOG b/CHANGELOG index 27aa0fa5e..2e9fd1b34 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,777 +1,779 @@ 0.23.2 (misspiggy) + Fixed CA race condition. (#693) + Added shortname support to config.rb and refactored addargs Fixed the problem in cron jobs where environment settings tended to multiple. (#749) Collection of resources now correctly only collects exported resources again. This was broken in 0.23.0. (#731) 'gen_config' now generates a configuration with all parameters under a heading that matches the process name, rather than keeping section headings. Refactored how the parser and interpreter relate, so parsing is now effectively an atomic process (thus fixing #314 and #729). This makes the interpreter less prone to error and less prone to show the error to the clients. Note that this means that if a configuration fails to parse, then the previous, parseable configuration will be used instead, so the client will not know that the configuration failed to parse. Added support for managing interfaces, thanks to work by Paul Rose. Fixed #652, thanks to a patch by emerose; --fqdn again works with puppetd. Added an extra check to the Mongrel support so that Apache can be used with optional cert checking, instead of mandatory, thus allowing Mongrel to function as the CA. This is thanks to work done by Marcin Owsiany. 0.23.1 (beaker) You can now specify relationships to classes, which work exactly like relationships to defined types: require => Class[myclass] This works with qualified classes, too. You can now do simple queries in a collection of exported resources. You still cannot do multi-condition queries, though. (#703) puppetca now exits with a non-zero code if it cannot find any host certificates to clean. (Patch by Dean Wilson.) Fully-qualified resources can now have defaults. (#589) Resource references can now be fully-qualified names, meaning you can list definitions with a namespace as dependencies. (#468) Files modified using a FileType instance, as ParsedFile does, will now automatically get backed up to the filebucket named "puppet". Added a 'maillist' type for managing mailing lists. Added a 'mailalias' type for managing mail aliases. Added patch by Valentin Vidic that adds the '+>' syntax to resources, so parameter values can be added to. The configuration client now pulls libraries down to $libdir, and all autoloading is done from there with full support for any reloadable file, such as types and providers. (#621) Note that this is not backward compatible -- if you're using pluginsync right now, you'll need to disable it on your clients until you can upgrade them. The Rails log level can now be set via (shockingly!) the 'rails_loglevel' parameter (#710). Note that this isn't exactly the feature asked for, but I could not find a way to directly copy ActiveRecord's concept of an environment. External node sources can now return undefined classes (#687). Puppet clients now have http proxy support (#701). The parser now throws an error when a resource reference is created for an unknown type. Also, resource references look up defined types and translate their type accordingly. (#706) Hostnames can now be double quoted. Adding module autoloading (#596) -- you can now 'include' classes from modules without ever needing to specifically load them. Class names and node names now conflict (#620). 0.23.0 Modified the fileserver to cache file information, so that each file isn't being read on every connection. Also, added londo's patch from #678 to avoid reading entire files into memory. Fixed environment handling in the crontab provider (#669). Added patch by trombik in #572, supporting old-style freebsd init scripts with '.sh' endings. Added fink package provider (#642), as provided by 'do'. Marked the dpkg package provider as versionable (#647). Applied patches by trombik to fix FreeBSD ports (#624 and #628). Fixed the CA server so that it refuses to send back a certificate whose public key doesn't match the CSR. Instead, it tells the user to run 'puppetca --clean'. Invalid certificates are no longer written to disk (#578). Added a package provider (appdmg) able to install .app packages on .dmg files on OS X (#641). Applied the patch from #667 to hopefully kill the client hanging problems (permanently, this time). Fixed functions so that they accept most other rvalues as valid values (#548). COMPATIBILITY ALERT: Significantly reworked external node support, in a way that's NOT backward-compatible: Only ONE node source can be used -- you can use LDAP, code, or an external node program, but not more than one. LDAP node support has two changes: First, the "ldapattrs" attribute is now used for setting the attributes to retrieve from the server (in addition to required attriutes), and second, all retrieved attributes are set as variables in the top scope. This means you can set attributes on your LDAP nodes and they will automatically appear as variables in your configurations. External node support has been completely rewritten. These programs must now generate a YAML dump of a hash, with "classes" and "parameters" keys. The classes should be an array, and the parameters should be a hash. The external node program has no support for parent nodes -- the script must handle that on its own. Reworked the database schema used to store configurations with the storeconfigs option. Replaced the obsolete RRD ruby library with the maintained RubyRRDtool library (which requires rrdtool2) (#659). The Portage package provider now calls eix-update automatically when eix's database is absent or out of sync (#666). Mounts now correctly handle existing fstabs with no pass or dump values (#550). Mounts now default to 0 for pass and dump (#112). Added urpmi support (#592). Finishing up the type => provider interface work. Basically, package providers now return lists of provider instances. In the proces, I rewrote the interface between package types and providers, and also enabled prefetching on all packages. This should significantly speed up most package operations. Hopefully fixing the file descriptor/open port problems, with patches from Valentin Vidic. Significantly reworked the type => provider interface with respect to listing existing provider instances. The class method on both class heirarchies has been renamed to 'instances', to start. Providers are now expected to return provider instances, instead of creating resources, and the resource's 'instances' method is expected to find the matching resource, if any, and set the resource's provider appropriately. This *significantly* reduces the reliance on effectively global state (resource references in the resource classes). This global state will go away soon. Along with this change, the 'prefetch' class method on providers now accepts the list of resources for prefetching. This again reduces reliance on global state, and makes the execution path much easier to follow. Fixed #532 -- reparsing config files now longer throws an exception. Added some warnings and logs to the service type so users will be encouraged to specify either "ensure" or "enabled" and added debugging to indicate why restarting is skipped when it is. Changed the location of the classes.txt to the state directory. Added better error reporting on unmatched brackets. Moved puppetd and puppetmasterd to sbin in svn and fixed install.rb to copy them into sbin on the local system appropriately. (#323) Added a splay option (#501). It's disabled when running under --test in puppetd. The value is random but cached. It defaults to the runinterval but can be tuned with --splaylimit Changing the notify type so that it always uses the loglevel. Fixing #568 - nodes can inherit from quoted node names. Tags (and thus definitions and classes) can now be a single character. (#566) Added an 'undef' keyword (#629), which will evaluate to "" within strings but when used as a resource parameter value will cause that parameter to be evaluated as undefined. Changed the topological sort algorithm (#507) so it will always fail on cycles. Added a 'dynamicfacts' configuration option; any facts in that comma-separated list will be ignored when comparing facts to see if they have changed and thus whether a recompile is necessary. Renamed some poorly named internal variables: @models in providers are now either @resource or @resource_type (#605). @children is no longer used except by components (#606). @parent is now @resource within parameters (#607). The old variables are still set for backward compatibility. Significantly reworking configuration parsing. Executables all now look for 'puppet.conf' (#206), although they will parse the old-style configuration files if they are present, although they throw a deprecation warning. Also, file parameters (owner, mode, group) are now set on the same line as the parameter, in brackets. (#422) Added transaction summaries (available with the --summarize option), useful for getting a quick idea of what happened in a transaction. Currently only useful on the client or with the puppet interpreter. Changed the interal workings for retrieve and removed the :is attribute from Property. The retrieve methods now return the current value of the property for the system. Removed acts_as_taggable from the rails models. 0.22.4 Execs now autorequire the user they run as, as long as the user is specified by name. (#430) Files on the local machine but not on the remote server during a source copy are now purged if purge => true. (#594) Providers can now specify that some commands are optional (#585). Also, the 'command' method returns nil on missing commands, rather than throwing an error, so the presence of commands be tested. The 'useradd' provider for Users can now manage passwords. No other providers can, at this point. Parameters can now declare a dependency on specific features, and parameters that require missing features will not be instantiated. This is most useful for properties. FileParsing classes can now use instance_eval to add many methods at once to a record type. Modules no longer return directories in the list of found manifests (#588). The crontab provider now defaults to root when there is no USER set in the environment. Puppetd once again correctly responds to HUP. Added a syntax for referring to variables defined in other classes (e.g., $puppet::server). STDIN, STDOUT, STDERR are now redirected to /dev/null in service providers descending from base. Certificates are now valid starting one day before they are created, to help handle small amounts of clock skew. Files are no longer considered out of sync if some properties are out of sync but they have no properties that can create the file. 0.22.3 Fixed backward compatibility for logs and metrics from older clients. Fixed the location of the authconfig parameters so there aren't loading order issues. Enabling attribute validation on the providers that subclass 'nameservice', so we can verify that an integer is passed to UID and GID. Added a stand-alone filebucket client, named 'filebucket'. Fixed the new nested paths for filebuckets; the entire md5 sum was not being stored. Fixing #553; -M is no longer added when home directories are being managed on Red Hat. 0.22.2 (grover) Users can now manage their home directories, using the managehome parameter, partially using patches provided by Tim Stoop and Matt Palmer. (#432) Added 'ralsh' (formerly x2puppet) to the svn tree. When possible it should be added to the packages. The 'notify' type now defaults to its message being the same as its name. Reopening $stdin to read from /dev/null during execution, in hopes that init scripts will stop hanging. Changed the 'servername' fact set on the server to use the server's fqdn, instead of the short-name. Changing the location of the configuration cache. It now defaults to being in the state directory, rather than in the configuration directory. All parameter instances are stored in a single @parameters instance variable hash within resource type instances. We used to use separate hashes for each parameter type. Added the concept of provider features. Eventually these should be able to express the full range of provider functionality, but for now they can test a provider to see what methods it has set and determine what features it provides as a result. These features are integrated into the doc generation system so that you get feature documentation automatically. Switched apt/aptitide to using "apt-cache policy" instead of "apt-cache showpkg" for determining the latest available version. (#487) FileBuckets now use a deeply nested structure for storing files, so you do not end up with hundreds or thousands of files in the same directory. (#447) Facts are now cached in the state file, and when they change the configuration is always recompiled. (#519) Added 'ignoreimport' setting for use in commit hooks. This causes the parser to ignore import statements so a single file can be parse-checked. (#544) Import statements can now specify multiple comma-separated arguments. Definitions now support both 'name' and 'title', just like any other resource type. (#539) Added a generate() command, which sets values to the result of an external command. (#541) Added a file() command to read in files with no interpolation. The first found file has its content returned. puppetd now exits if no cert is present in onetime mode. (#533) The client configuration cache can be safely removed and the client will correctly realize the client is not in sync. Resources can now be freely deleted, thus fixing many problems introduced when deletion of required resources was forbidden when purging was introduced. Only resources being purged will not be deleted. Facts and plugins now download even in noop mode (#540). Resources in noop mode now log when they would have responded to an event (#542). Refactored cron support entirely. Cron now uses providers, and there is a single 'crontab' provider that handles user crontabs. While this refactor does not include providers for /etc/crontab or cron.d, it should now be straightforward to write those providers. Changed the parameter sorting so that the provider parameter comes right after name, so the provider is available when the other parameters and properties are being created. Redid some of the internals of the ParsedFile provider base class. It now passes a FileRecord around instead of a hash. Fixing a bug related to link recursion that caused link directories to always be considered out of sync. The bind address for puppetmasterd can now be specified with --bindaddress. Added (probably experimental) mongrel support. At this point you're still responsible for starting each individual process, and you have to set up a proxy in front of it. Redesigned the 'network' tree to support multiple web servers, including refactoring most of the structural code so it's much clearer and more reusable now. Set up the CA client to default to ca_server and ca_port, so you can easily run a separate CA. Supporting hosts with no domain name, thanks to a patch from Dennis Jacobfeuerborn. Added an 'ignorecache' option to tell puppetd to force a recompile, thanks to a patch by Chris McEniry. Made up2date the default for RHEL < 4 and yum the default for the rest. The yum provider now supports versions. Case statements correctly match when multiple values are provided, thanks to a patch by David Schmitt. Functions can now be called with no arguments. String escapes parse correctly in all cases now, thanks to a patch by cstorey. Subclasses again search parent classes for defaults. You can now purge apt and dpkg packages. When doing file recursion, 'ensure' only affects the top-level directory. States have been renamed to Properties. 0.22.1 (kermit) -- Mostly a bugfix release Compile times now persist between restarts of puppetd. Timeouts have been added to many parts of Puppet, reducing the likelihood if it hanging forever on broken scripts or servers. All of the documentation and recipes have been moved to the wiki by Peter Abrahamsen and Ben Kite has moved the FAQ to the wiki. Explicit relationships now override automatic relationships, allowing you to manually specify deletion order when removing resources. Resources with dependencies can now be deleted as long as all of their dependencies are also being deleted. Namespaces for both classes and definitions now work much more consistently. You should now be able to specify a class or definition with a namespace everywhere you would normally expect to be able to specify one without. Downcasing of facts can be selectively disabled. Cyclic dependency graphs are now checked for and forbidden. The netinfo mounts provider was commented out, because it really doesn't work at all. Stupid NetInfo stores mount information with the device as the key, which doesn't work with my current NetInfo code. Otherwise, lots and lots of bugfixes. Check the tickets associated with the 'kermit' milestone. 0.22.0 Integrated the GRATR graph library into Puppet, for handling resource relationships. Lots of bug-fixes (see bugs tickets associated with the 'minor' milestone). Added new 'resources' metatype, which currently only includes the ability to purge unmanaged resources. Added better ability to generate new resource objects during transactions (using 'generate' and 'eval_generate' methods). Rewrote all Rails support with a much better database design. Export/collect now works, although the database is incompatible with previous versions. Removed downcasing of facts and made most of the language case-insensitive. Added support for printing the graphs built during transactions. Reworked how paths are built for logging. Switched all providers to directly executing commands instead of going through a subshell, which removes the need to quote or escape arguments. 0.20.1 Mostly a bug-fix release, with the most important fix being the multiple-definition error. Completely rewrote the ParsedFile system; each provider is now much shorter and much more maintainable. However, fundamental problems were found with the 'port' type, so it was disabled. Also, added a NetInfo provider for 'host' and an experimental NetInfo provider for 'mount'. Made the RRDGraph report *much* better and added reference generation for reports and functions. 0.20.0 Significantly refactored the parser. Resource overrides now consistently work anywhere in a class hierarchy. The language was also modified somewhat. The previous export/collect syntax is now used for handling virtual objects, and export/collect (which is still experimental) now uses double sigils (@@ and <<| |>>). Resource references (e.g., File["/etc/passwd"]) now have to be capitalized, in fitting in with capitalizing type operations. As usual, lots of other smaller fixes, but most of the work was in the language. 0.19.3 Fixing a bug in server/master.rb that causes the hostname not to be available in locally-executed manifests. 0.19.2 Fixing a few smaller bugs, notably in the reports system. Refreshed objects now generate an event, which can result in further refreshes of other objects. 0.19.1 Fixing two critical bugs: User management works again and cron jobs are no longer added to all user accounts. 0.19.0 Added provider support. Added support for %h, %H, and %d expansion in fileserver.conf. Added Certificate Revocation support. Made dynamic loading pervasive -- nearly every aspect of Puppet will now automatically load new instances (e.g., types, providers, and reports). Added support for automatic distribution of facts and plugins (custom types). 0.18.4 Another bug-fix release. The most import bug fixed is that cronjobs again work even with initially empty crontabs. 0.18.3 Mostly a bug-fix release; fixed small bugs in the functionality added in 0.18.2. 0.18.2 Added templating support. Added reporting. Added gem and blastwave packaging support. 0.18.1 Added signal handlers for HUP, so both client and server deal correctly with it. Added signal handler for USR1, which triggers a run on the client. As usual, fixed many bugs. Significant fixes to puppetrun -- it should behave much more correctly now. Added "fail" function which throws a syntax error if it's encountered. Added plugin downloading from the central server to the client. It must be enabled with --pluginsync. Added support for FreeBSD's special "@daily" cron schedules. Correctly handling spaces in file sources. Moved documentation into svn tree. 0.18.0 Added support for a "default" node. When multiple nodes are specified, they must now be comma-separated (this introduces a language incompatibility). Failed dependencies cause dependent objects within the same transaction not to run. Many updates to puppetrun Many bug fixes Function names are no longer reserved words. Links can now replace files. 0.17.2 Added "puppetrun" application and associated runner server and client classes. Fixed cron support so it better supports valid values and environment settings. 0.17.1 Fixing a bug requiring rails on all Debian boxes Fixing a couple of other small bugs 0.17.0 Adding ActiveRecord integration on the server Adding export/collect functionality Fixing many bugs 0.16.5 Fixing a critical bug in importing classes from other files Fixing nodename handling to actually allow dashes 0.16.4 Fixing a critical bug in puppetd when acquiring a certificate for the first time 0.16.3 Some significant bug fixes Modified puppetd so that it can now function as an agent independent of a puppetmasterd process, e.g., using the PuppetShow web application. 0.16.2 Modified some of the AST classes so that class names, definition names, and node names are all set within the code being evaluated, so 'tagged(name)' returns true while evaluating 'name', for instance. Added '--clean' argument to puppetca to remove all traces of a given client. 0.16.1 Added 'tagged' and 'defined' functions. Moved all functions to a general framework that makes it very easy to add new functions. 0.16.0 Added 'tag' keyword/function. Added FreeBSD Ports support Added 'pelement' server for sending or receiving Puppet objects, although none of the executables use it yet. 0.15.3 Fixed many bugs in :exec, including adding support for arrays of checks Added autoloading for types and service variants (e.g., you can now just create a new type in the appropriate location and use it in Puppet, without modifying the core Puppet libs). 0.15.2 Added darwinport, Apple .pkg, and freebsd package types Added 'mount type Host facts are now set at the top scope (Bug #103) Added -e (inline exection) flag to 'puppet' executable Many small bug fixes 0.15.1 Fixed 'yum' installs so that they successfully upgrade packages. Fixed puppetmasterd.conf file so group settings take. 0.15.0 Upped the minor release because the File server is incompatible with 0.14, because it now handles links. The 'symlink' type is deprecated (but still present), in favor of using files with the 'target' parameter. Unset variables no longer throw an error, they just return an empty string You can now specify tags to restrict which objects run during a given run. You can also specify to skip running against the cached copy when there's a failure, which is useful for testing new configurations. RPMs and Sun packages can now install, as long as they specify a package location, and they'll automatically upgrade if you point them to a new file with an upgrade. Multiple bug fixes. 0.14.1 Fixed a couple of small logging bugs Fixed a bug with handling group ownership of links 0.14.0 Added some ability to selectively manage symlinks when doing file management Many bug fixes Variables can now be used as the test values in case statements and selectors Bumping a minor release number because 0.13.4 introduced a protocol incompatibility and should have had a minor rev bump 0.13.6 Many, many small bug fixes FreeBSD user/group support has been added The configuration system has been rewritten so that daemons can now generate and repair the files and directories they need. (Fixed bug #68.) Fixed the element override issues; now only subclasses can override values. 0.13.5 Fixed packages so types can be specified Added 'enable' state to services, although it does not work everywhere yet 0.13.4 A few important bug fixes, mostly in the parser. 0.13.3 Changed transactions to be one-stage instead of two Changed all types to use self[:name] instead of self.name, to support the symbolic naming implemented in 0.13.1 0.13.2 Changed package[answerfile] to package[adminfile], and added package[responsefile] Fixed a bunch of internal functions to behave more consistently and usefully 0.13.1 Fixed RPM spec files to create puppet user and group (lutter) Fixed crontab reading and writing (luke) Added symbolic naming in the language (luke) 0.13.0 Added support for configuration files. Even more bug fixes, including the infamous 'frozen object' bug, which was a problem with 'waitforcert'. David Lutterkort got RPM into good shape. 0.12.0 Added Scheduling, and many bug fixes, of course. 0.11.2 Fixed bugs related to specifying arrays of requirements Fixed a key bug in retrieving checksums Fixed lots of usability bugs Added 'fail' methods that automatically add file and line info when possible, and converted many errors to use that method 0.11.1 Fixed bug with recursive copying with 'ignore' set. Added OpenBSD package support. 0.11.0 Added 'ensure' state to many elements. Modified puppetdoc to correctly handle indentation and such. Significantly rewrote much of the builtin documentation to take advantage of the new features in puppetdoc, including many examples. 0.10.2 Added SMF support Added autorequire functionality, with specific support for exec and file Exec elements autorequire any mentioned files, including the scripts, along with their CWDs. Files autorequire any parent directories. Added 'alias' metaparam. Fixed dependencies so they don't depend on file order. 0.10.1 Added Solaris package support and changed puppetmasterd to run as a non-root user. 0.10.0 Significant refactoring of how types, states, and parameters work, including breaking out parameters into a separate class. This refactoring did not introduce much new functionality, but made extension of Puppet significantly easier Also, fixed the bug with 'waitforcert' in puppetd. 0.9.4 Small fix to wrap the StatusServer class in the checks for required classes. 0.9.3 Fixed some significant bugs in cron job management. 0.9.2 Second Public Beta 0.9.0 First Public Beta diff --git a/Rakefile b/Rakefile index d560b85b9..6b0f6a827 100644 --- a/Rakefile +++ b/Rakefile @@ -1,141 +1,139 @@ # Rakefile for Puppet -*- ruby -*- $: << File.expand_path(File.join(File.dirname(__FILE__), 'lib')) begin require 'rake/reductive' rescue LoadError $stderr.puts "You must have the Reductive build library in your RUBYLIB." exit(14) end TESTHOSTS = %w{rh3a fedora1 centos1 freebsd1 culain} project = Rake::RedLabProject.new("puppet") do |p| p.summary = "System Automation and Configuration Management Software" p.description = "Puppet is a declarative language for expressing system configuration, a client and server for distributing it, and a library for realizing the configuration." p.filelist = [ 'install.rb', '[A-Z]*', 'lib/puppet.rb', 'lib/puppet/**/*.rb', 'test/**/*.rb', 'bin/**/*', 'ext/**/*', 'examples/**/*', 'conf/**/*' ] p.filelist.exclude("bin/pi") p.add_dependency('facter', '1.1.0') #p.epmhosts = %w{culain} #p.sunpkghost = "sol10b" #p.rpmhost = "fedora1" end if project.has?(:gem) # Make our gem task. This actually just fills out the spec. project.mkgemtask do |task| task.require_path = 'lib' # Use these for libraries. task.bindir = "bin" # Use these for applications. task.executables = ["puppet", "puppetd", "puppetmasterd", "puppetdoc", "puppetca", "puppetrun", "ralsh"] task.default_executable = "puppet" task.autorequire = 'puppet' #### Documentation and testing. task.has_rdoc = true #s.extra_rdoc_files = rd.rdoc_files.reject { |fn| fn =~ /\.rb$/ }.to_a task.rdoc_options << '--title' << 'Puppet - Configuration Management' << '--main' << 'README' << '--line-numbers' task.test_file = "test/Rakefile" end end if project.has?(:epm) project.mkepmtask do |task| task.bins = FileList.new("bin/puppet", "bin/puppetca") task.sbins = FileList.new("bin/puppetmasterd", "bin/puppetd") task.rubylibs = FileList.new('lib/**/*') end end rule(/_is_runnable$/) do |t| available = false executable = t.name.sub(/_is_runnable$/, '') ENV['PATH'].split(':').each do |elem| available = true if File.executable? File.join(elem, executable) end unless available puts "You do not have #{executable} available in your path" exit 1 end end file "debian" => :bzr_is_runnable do system("bzr get http://www.hezmatt.org/~mpalmer/bzr/puppet.debian.svn debian") || exit(1) end task :check_build_deps => 'dpkg-checkbuilddeps_is_runnable' do system("dpkg-checkbuilddeps") || exit(1) end task :debian_packages => [ "debian", :check_build_deps, :fakeroot_is_runnable ] do system("fakeroot debian/rules clean") || exit(1) system("fakeroot debian/rules binary") || exit(1) end def dailyfile(package) "#{downdir}/#{package}/#{package}-daily-#{stamp}.tgz" end def daily(package) edir = "/tmp/daily-export" Dir.mkdir edir Dir.chdir(edir) do sh %{svn export http://reductivelabs.com/svn/#{package}/trunk #{package} >/dev/null} sh %{tar cf - #{package} | gzip -c > #{dailyfile(package)}} end FileUtils.rm_rf(edir) end def downdir ENV['DOWNLOAD_DIR'] || "/opt/rl/docroots/reductivelabs.com/htdocs/downloads" end def stamp [Time.now.year, Time.now.month, Time.now.day].collect { |i| i.to_s}.join end pdaily = dailyfile("puppet") fdaily = dailyfile("facter") file pdaily do daily("puppet") end file fdaily do daily("facter") end task :daily => [pdaily, fdaily] task :dailyclean do Dir.glob("#{downdir}/*/*daily*.tgz").each do |file| puts "Removing %s" % file File.unlink(file) end end - -# $Id$ diff --git a/bin/puppet b/bin/puppet index 241bac785..36f0fcd62 100755 --- a/bin/puppet +++ b/bin/puppet @@ -1,200 +1,209 @@ #!/usr/bin/env ruby # # = Synopsis # # Run a stand-alone +puppet+ script. # # = Usage # # puppet [-h|--help] [-V|--version] [-d|--debug] [-v|--verbose] # [-l|--logdest ] # # = Description # # This is the standalone puppet execution script; use it to execute # individual scripts that you write. If you need to execute site-wide # scripts, use +puppetd+ and +puppetmasterd+. # # = Options # # Note that any configuration parameter that's valid in the configuration file # is also a valid long argument. For example, 'ssldir' is a valid configuration # parameter, so you can specify '--ssldir ' as an argument. # # See the configuration file documentation at # http://reductivelabs.com/projects/puppet/reference/configref.html for # the full list of acceptable parameters. A commented list of all # configuration options can also be generated by running puppet with # '--genconfig'. # # debug:: # Enable full debugging. # # help:: # Print this help message # # loadclasses:: # Load any stored classes. +puppetd+ caches configured classes (usually at # /etc/puppet/classes.txt), and setting this option causes all of those classes # to be set in your +puppet+ manifest. # # logdest:: # Where to send messages. Choose between syslog, the console, and a log file. # Defaults to sending messages to the console. # # verbose:: # Print extra information. # # = Example # # puppet -l /tmp/script.log script.pp # # = Author # # Luke Kanies # # = Copyright # # Copyright (c) 2005 Reductive Labs, LLC # Licensed under the GNU Public License require 'puppet' require 'puppet/network/handler' require 'puppet/network/client' require 'getoptlong' options = [ [ "--debug", "-d", GetoptLong::NO_ARGUMENT ], [ "--help", "-h", GetoptLong::NO_ARGUMENT ], [ "--logdest", "-l", GetoptLong::REQUIRED_ARGUMENT ], [ "--execute", "-e", GetoptLong::REQUIRED_ARGUMENT ], [ "--loadclasses", "-L", GetoptLong::NO_ARGUMENT ], [ "--verbose", "-v", GetoptLong::NO_ARGUMENT ], [ "--use-nodes", GetoptLong::NO_ARGUMENT ], [ "--version", "-V", GetoptLong::NO_ARGUMENT ] ] # Add all of the config parameters as valid options. Puppet.config.addargs(options) result = GetoptLong.new(*options) debug = false verbose = false noop = false logfile = false loadclasses = false logset = false code = nil master = { :Local => true } begin result.each { |opt,arg| case opt when "--version" puts "%s" % Puppet.version exit when "--help" if Puppet.features.usage? RDoc::usage && exit else puts "No help available unless you have RDoc::usage installed" exit end when "--use-nodes" master[:UseNodes] = true when "--verbose" verbose = true when "--debug" debug = true when "--execute" code = arg when "--loadclasses" loadclasses = true when "--logdest" begin Puppet::Util::Log.newdestination(arg) logset = true rescue => detail $stderr.puts detail.to_s end else Puppet.config.handlearg(opt, arg) end } rescue GetoptLong::InvalidOption => detail $stderr.puts "Try '#{$0} --help'" exit(1) end Puppet.parse_config +# If noop is set, then also enable diffs +if Puppet[:noop] + Puppet[:show_diff] = true +end + unless logset Puppet::Util::Log.newdestination(:console) end client = nil server = nil Puppet.settraps if debug Puppet::Util::Log.level = :debug elsif verbose Puppet::Util::Log.level = :info end # Now parse the config if Puppet[:config] and File.exists? Puppet[:config] Puppet.config.parse(Puppet[:config]) end Puppet.genconfig Puppet.genmanifest if code master[:Code] = code else if ARGV.length > 0 master[:Manifest] = ARGV.shift else master[:Code] = STDIN.read end end # Allow users to load the classes that puppetd creates. if loadclasses file = Puppet[:classfile] if FileTest.exists?(file) unless FileTest.readable?(file) $stderr.puts "%s is not readable" % file exit(63) end master[:Classes] = File.read(file).split(/[\s\n]+/) end end begin server = Puppet::Network::Handler.master.new(master) client = Puppet::Network::Client.master.new( :Master => server, :Cache => false ) if Puppet[:parseonly] exit(0) end client.getconfig client.apply rescue => detail - $stderr.puts detail + if detail.is_a?(XMLRPC::FaultException) + $stderr.puts detail.message + else + $stderr.puts detail + end if Puppet[:trace] puts detail.backtrace end exit(1) end diff --git a/bin/puppetd b/bin/puppetd index 848a05b20..a03ed8f10 100755 --- a/bin/puppetd +++ b/bin/puppetd @@ -1,450 +1,456 @@ #!/usr/bin/env ruby # == Synopsis # # Retrieve the client configuration from the central puppet server and apply # it to the local host. # # Currently must be run out periodically, using cron or something similar. # # = Usage # # puppetd [-D|--daemonize] [-d|--debug] [--disable] [--enable] # [-h|--help] [--fqdn ] [-l|--logdest syslog||console] # [-o|--onetime] [--serve ] [-t|--test] # [-V|--version] [-v|--verbose] [-w|--waitforcert ] # # = Description # # This is the main puppet client. Its job is to retrieve the local machine's # configuration from a remote server and apply it. In order to successfully # communicate with the remote server, the client must have a certificate signed # by a certificate authority that the server trusts; the recommended method # for this, at the moment, is to run a certificate authority as part of the # puppet server (which is the default). The client will connect and request # a signed certificate, and will continue connecting until it receives one. # # Once the client has a signed certificate, it will retrieve its configuration # and apply it. # # = Usage Notes # # +puppetd+ does its best to find a compromise between interactive use and # daemon use. Run with no arguments and no configuration, it will go into the # backgroun, attempt to get a signed certificate, and retrieve and apply its # configuration every 30 minutes. # # Some flags are meant specifically for interactive use -- in particular, # +test+ and +tags+ are useful. +test+ enables verbose logging, causes # the daemon to stay in the foreground, exits if the server's configuration is # invalid (this happens if, for instance, you've left a syntax error on the # server), and exits after running the configuration once (rather than hanging # around as a long-running process). # # +tags+ allows you to specify what portions of a configuration you want to apply. # Puppet elements are tagged with all of the class or definition names that # contain them, and you can use the +tags+ flag to specify one of these names, # causing only configuration elements contained within that class or definition # to be applied. This is very useful when you are testing new configurations -- # for instance, if you are just starting to manage +ntpd+, you would put all of # the new elements into an +ntpd+ class, and call puppet with +--tags ntpd+, # which would only apply that small portion of the configuration during your # testing, rather than applying the whole thing. # # = Options # # Note that any configuration parameter that's valid in the configuration file # is also a valid long argument. For example, 'server' is a valid configuration # parameter, so you can specify '--server ' as an argument. # # See the configuration file documentation at # http://reductivelabs.com/projects/puppet/reference/configref.html for # the full list of acceptable parameters. A commented list of all # configuration options can also be generated by running puppetd with # '--genconfig'. # # daemonize:: # Send the process into the background. This is the default unless # +verbose+ or +debug+ is enabled. # # debug:: # Enable full debugging. # # disable:: # Disable working on the local system. This puts a lock file in place, # causing +puppetd+ not to work on the system until the lock file is removed. # This is useful if you are testing a configuration and do not want the central # configuration to override the local state until everything is tested and # committed. # # +puppetd+ uses the same lock file while it is running, so no more than one # +puppetd+ process is working at a time. # # +puppetd+ exits after executing this. # # enable:: # Enable working on the local system. This removes any lock file, causing # +puppetd+ to start managing the local system again (although it will continue # to use its normal scheduling, so it might not start for another half hour). # # +puppetd+ exits after executing this. # # fqdn:: # Set the fully-qualified domain name of the client. This is only used for # certificate purposes, but can be used to override the discovered hostname. # If you need to use this flag, it is generally an indication of a setup problem. # # help:: # Print this help message # # logdest:: # Where to send messages. Choose between syslog, the console, and a log file. # Defaults to sending messages to syslog, or the console if debugging or # verbosity is enabled. # # no-client:: # Do not create a config client. This will cause the daemon to run # without ever checking for its configuration automatically, and only # makes sense when used in conjunction with --listen. # # onetime:: # Run the configuration once, rather than as a long-running daemon. This is # useful for interactively running puppetd. # # serve:: # Start another type of server. By default, +puppetd+ will start # a service handler that allows authenticated and authorized remote nodes to # trigger the configuration to be pulled down and applied. You can specify # any handler here that does not require configuration, e.g., filebucket, ca, # or resource. The handlers are in +lib/puppet/network/handler+, and the names # must match exactly, both in the call to +serve+ and in +namespaceauth.conf+. # # test:: # Enable the most common options used for testing. These are +onetime+, # +verbose+, +ignorecache, and +no-usecacheonfailure+. # # verbose:: # Turn on verbose reporting. # # version:: # Print the puppet version number and exit. # # waitforcert:: # This option only matters for daemons that do not yet have certificates # and it is enabled by default, with a value of 120 (seconds). This causes # +puppetd+ to connect to the server every 2 minutes and ask it to sign a # certificate request. This is useful for the initial setup of a puppet # client. You can turn off waiting for certificates by specifying a time # of 0. # # = Example # # puppetd --server puppet.domain.com # # = Author # # Luke Kanies # # = Copyright # # Copyright (c) 2005, 2006 Reductive Labs, LLC # Licensed under the GNU Public License # Do an initial trap, so that cancels don't get a stack trace. trap(:INT) do $stderr.puts "Cancelling startup" exit(0) end require 'puppet' require 'puppet/network/client' require 'getoptlong' options = [ [ "--centrallogging", GetoptLong::NO_ARGUMENT ], [ "--daemonize", "-D", GetoptLong::NO_ARGUMENT ], [ "--disable", GetoptLong::NO_ARGUMENT ], [ "--debug", "-d", GetoptLong::NO_ARGUMENT ], [ "--enable", GetoptLong::NO_ARGUMENT ], [ "--fqdn", "-f", GetoptLong::REQUIRED_ARGUMENT ], [ "--help", "-h", GetoptLong::NO_ARGUMENT ], [ "--logdest", "-l", GetoptLong::REQUIRED_ARGUMENT ], [ "--onetime", "-o", GetoptLong::NO_ARGUMENT ], [ "--test", "-t", GetoptLong::NO_ARGUMENT ], [ "--serve", "-s", GetoptLong::REQUIRED_ARGUMENT ], [ "--no-client", GetoptLong::NO_ARGUMENT ], [ "--verbose", "-v", GetoptLong::NO_ARGUMENT ], [ "--version", "-V", GetoptLong::NO_ARGUMENT ], [ "--waitforcert", "-w", GetoptLong::REQUIRED_ARGUMENT ] ] # Add all of the config parameters as valid options. Puppet.config.addargs(options) result = GetoptLong.new(*options) args = {} options = { :waitforcert => 120, # Default to checking for certs every 5 minutes :onetime => false, :centrallogs => false, :setdest => false, :enable => false, :disable => false, :client => true, :fqdn => nil, :serve => {} } begin explicit_waitforcert = false result.each { |opt,arg| case opt # First check to see if the argument is a valid configuration parameter; # if so, set it. when "--daemonize" options[:daemonize] = true when "--disable" options[:disable] = true when "--serve" if Puppet::Network::Handler.handler(arg) options[:serve][arg.to_sym] = {} else raise "Could not find handler for %s" % arg end when "--enable" options[:enable] = true when "--test" # Enable all of the most common test options. Puppet.config.handlearg("--ignorecache") Puppet.config.handlearg("--no-usecacheonfailure") Puppet.config.handlearg("--no-splay") + Puppet.config.handlearg("--show_diff") options[:onetime] = true options[:waitforcert] = 0 unless Puppet::Util::Log.level == :debug Puppet::Util::Log.level = :info end Puppet::Util::Log.newdestination(:console) when "--centrallogging" options[:centrallogs] = true when "--help" if Puppet.features.usage? RDoc::usage && exit else puts "No help available unless you have RDoc::usage installed" exit end when "--version" puts "%s" % Puppet.version exit when "--verbose" Puppet::Util::Log.level = :info Puppet::Util::Log.newdestination(:console) when "--debug" Puppet::Util::Log.level = :debug Puppet::Util::Log.newdestination(:console) when "--fqdn" options[:fqdn] = arg when "--no-client" options[:client] = false when "--onetime" options[:onetime] = true options[:waitforcert] = 0 unless explicit_waitforcert when "--port" args[:Port] = arg when "--logdest" begin Puppet::Util::Log.newdestination(arg) options[:setdest] = true rescue => detail $stderr.puts detail.to_s end when "--waitforcert" options[:waitforcert] = arg.to_i explicit_waitforcert = true else Puppet.config.handlearg(opt, arg) end } rescue GetoptLong::InvalidOption => detail $stderr.puts detail $stderr.puts "Try '#{$0} --help'" exit(1) end # Now parse the config Puppet.parse_config Puppet.genconfig Puppet.genmanifest +# If noop is set, then also enable diffs +if Puppet[:noop] + Puppet[:show_diff] = true +end + # Default to daemonizing, but if verbose or debug is specified, # default to staying in the foreground. unless options.include?(:daemonize) if Puppet::Util::Log.level == :debug or Puppet::Util::Log.level == :info options[:daemonize] = false else options[:daemonize] = true end end unless options[:setdest] Puppet::Util::Log.newdestination(:syslog) end args[:Server] = Puppet[:server] if options[:fqdn] args[:FQDN] = options[:fqdn] Puppet[:certname] = options[:fqdn] end if options[:centrallogs] logdest = args[:Server] if args.include?(:Port) logdest += ":" + args[:Port] end Puppet::Util::Log.newdestination(logdest) end # We need tomake the client either way, we just don't start it # if --no-client is set. client = Puppet::Network::Client.master.new(args) if options[:enable] client.enable elsif options[:disable] client.disable end if options[:enable] or options[:disable] exit(0) end server = nil # It'd be nice to daemonize later, but we have to daemonize before the # waitforcert happens. if options[:daemonize] client.daemonize end unless client.read_cert # If we don't already have the certificate, then create a client to # request one. Use the special ca stuff, don't use the normal server and port. caclient = Puppet::Network::Client.ca.new() if options[:waitforcert] > 0 begin while ! caclient.request_cert do Puppet.notice "Did not receive certificate" sleep options[:waitforcert] end rescue => detail Puppet.err "Could not request certificate: %s" % detail.to_s exit(23) end else unless caclient.request_cert Puppet.notice "No certificates; exiting" exit(1) end end # Now read the new cert in. if client.read_cert Puppet.notice "Got signed certificate" else Puppet.err "Could not read certificates after retrieving them" exit(34) end end objects = [] # This has to go after the certs are dealt with. if Puppet[:listen] and ! options[:onetime] unless FileTest.exists?(Puppet[:authconfig]) Puppet.err "Will not start without authorization file %s" % Puppet[:authconfig] exit(14) end # FIXME: we should really figure out how to distribute the CRL # to clients. In the meantime, we just disable CRL checking if # the CRL file doesn't exist unless File::exist?(Puppet[:cacrl]) Puppet[:cacrl] = 'none' end handlers = nil if options[:serve].empty? handlers = {:Runner => {}} else handlers = options[:serve] end handlers.each do |name, hash| Puppet.info "Starting handler for %s" % name end args[:Handlers] = handlers args[:Port] = Puppet[:puppetport] require 'puppet/network/server/webrick' begin server = Puppet::Network::Server::WEBrick.new(args) rescue => detail $stderr.puts detail puts detail.backtrace exit(1) end objects << server elsif options[:onetime] and Puppet[:listen] Puppet.notice "Ignoring --listen on onetime run" end if options[:client] objects << client end # Set traps for INT and TERM Puppet.settraps # If --onetime is specified, we don't run 'start', which means we don't # create a pidfile. if options[:onetime] unless options[:client] $stderr.puts "onetime is specified but there is no client" exit(43) end # Add the service, so the traps work correctly. Puppet.newservice(client) begin client.run rescue => detail if Puppet[:trace] puts detail.backtrace end Puppet.err detail.to_s end exit(0) else if server Puppet.newservice(server) end if options[:client] Puppet.notice "Starting Puppet client version %s" % [Puppet.version] Puppet.newservice(client) end Puppet.settraps Puppet.start end # $Id$ diff --git a/conf/redhat/puppet.spec b/conf/redhat/puppet.spec index 038991776..98a85e5e3 100644 --- a/conf/redhat/puppet.spec +++ b/conf/redhat/puppet.spec @@ -1,286 +1,289 @@ %{!?ruby_sitelibdir: %define ruby_sitelibdir %(ruby -rrbconfig -e 'puts Config::CONFIG["sitelibdir"]')} %define pbuild %{_builddir}/%{name}-%{version} %define confdir conf/redhat %define has_ruby_abi 0%{?fedora:%fedora} >= 5 || 0%{?rhel:%rhel} >= 5 %define has_ruby_noarch %has_ruby_abi Summary: A network tool for managing many disparate systems Name: puppet Version: 0.23.2 Release: 1%{?dist} License: GPL Group: System Environment/Base URL: http://puppet.reductivelabs.com/ Source: http://reductivelabs.com/downloads/puppet/%{name}-%{version}.tgz Requires: ruby >= 1.8.1 %if %has_ruby_abi Requires: ruby(abi) = 1.8 %endif Requires: facter >= 1.1.4 BuildRoot: %{_tmppath}/%{name}-%{version}-%{release}-root-%(%{__id_u} -n) %if %has_ruby_noarch BuildArchitectures: noarch %endif BuildRequires: ruby >= 1.8.1 %description Puppet lets you centrally manage every important aspect of your system using a cross-platform specification language that manages all the separate elements normally aggregated in different files, like users, cron jobs, and hosts, along with obviously discrete elements like packages, services, and files. %package server Group: System Environment/Base Summary: Server for the puppet system management tool Requires: puppet = %{version}-%{release} %description server Provides the central puppet server daemon which provides manifests to clients. The server can also function as a certificate authority and file server. %prep %setup -q %build for f in bin/* ; do sed -i -e '1c#!/usr/bin/ruby' $f done %install rm -rf %{buildroot} install -d -m0755 %{buildroot}%{_sbindir} install -d -m0755 %{buildroot}%{_bindir} install -d -m0755 %{buildroot}%{ruby_sitelibdir} install -d -m0755 %{buildroot}%{_sysconfdir}/puppet/manifests install -d -m0755 %{buildroot}%{_docdir}/%{name}-%{version} install -d -m0755 %{buildroot}%{_localstatedir}/lib/puppet install -d -m0755 %{buildroot}%{_localstatedir}/run/puppet install -d -m0755 %{buildroot}%{_localstatedir}/log/puppet install -Dp -m0755 %{pbuild}/bin/* %{buildroot}%{_sbindir} mv %{buildroot}%{_sbindir}/puppet %{buildroot}%{_bindir}/puppet mv %{buildroot}%{_sbindir}/ralsh %{buildroot}%{_bindir}/ralsh mv %{buildroot}%{_sbindir}/filebucket %{buildroot}%{_bindir}/filebucket mv %{buildroot}%{_sbindir}/puppetrun %{buildroot}%{_bindir}/puppetrun install -Dp -m0644 %{pbuild}/lib/puppet.rb %{buildroot}%{ruby_sitelibdir}/puppet.rb cp -a %{pbuild}/lib/puppet %{buildroot}%{ruby_sitelibdir} find %{buildroot}%{ruby_sitelibdir} -type f -perm +ugo+x -print0 | xargs -0 -r chmod a-x install -Dp -m0644 %{confdir}/client.sysconfig %{buildroot}%{_sysconfdir}/sysconfig/puppet install -Dp -m0755 %{confdir}/client.init %{buildroot}%{_initrddir}/puppet install -Dp -m0644 %{confdir}/server.sysconfig %{buildroot}%{_sysconfdir}/sysconfig/puppetmaster install -Dp -m0755 %{confdir}/server.init %{buildroot}%{_initrddir}/puppetmaster install -Dp -m0644 %{confdir}/fileserver.conf %{buildroot}%{_sysconfdir}/puppet/fileserver.conf install -Dp -m0644 %{confdir}/puppet.conf %{buildroot}%{_sysconfdir}/puppet/puppet.conf install -Dp -m0644 %{confdir}/logrotate %{buildroot}%{_sysconfdir}/logrotate.d/puppet # We need something for these ghosted files, otherwise rpmbuild # will complain loudly. They won't be included in the binary packages touch %{buildroot}%{_sysconfdir}/puppet/puppetmasterd.conf touch %{buildroot}%{_sysconfdir}/puppet/puppetca.conf touch %{buildroot}%{_sysconfdir}/puppet/puppetd.conf %files %defattr(-, root, root, 0755) %{_bindir}/puppet %{_bindir}/ralsh %{_bindir}/filebucket %{_sbindir}/puppetd %{ruby_sitelibdir}/* %{_initrddir}/puppet %dir %{_sysconfdir}/puppet %config(noreplace) %{_sysconfdir}/sysconfig/puppet %config(noreplace) %{_sysconfdir}/puppet/puppet.conf %ghost %config(noreplace,missingok) %{_sysconfdir}/puppet/puppetd.conf %doc CHANGELOG COPYING LICENSE README examples %exclude %{_sbindir}/puppetdoc %config(noreplace) %{_sysconfdir}/logrotate.d/puppet # These need to be owned by puppet so the server can # write to them %attr(-, puppet, puppet) %{_localstatedir}/run/puppet %attr(-, puppet, puppet) %{_localstatedir}/log/puppet %attr(-, puppet, puppet) %{_localstatedir}/lib/puppet %files server %defattr(-, root, root, 0755) %{_sbindir}/puppetmasterd %{_bindir}/puppetrun %{_initrddir}/puppetmaster %config(noreplace) %{_sysconfdir}/puppet/fileserver.conf %dir %{_sysconfdir}/puppet/manifests %config(noreplace) %{_sysconfdir}/sysconfig/puppetmaster %ghost %config(noreplace,missingok) %{_sysconfdir}/puppet/puppetca.conf %ghost %config(noreplace,missingok) %{_sysconfdir}/puppet/puppetmasterd.conf %{_sbindir}/puppetca %pre /usr/sbin/groupadd -r puppet 2>/dev/null || : /usr/sbin/useradd -g puppet -c "Puppet" \ -s /sbin/nologin -r -d /var/lib/puppet puppet 2> /dev/null || : if [ $1 -gt 1 ] ; then /usr/sbin/usermod -d /var/lib/puppet puppet || : fi %post /sbin/chkconfig --add puppet exit 0 %post server /sbin/chkconfig --add puppetmaster %preun if [ "$1" = 0 ] ; then /sbin/service puppet stop > /dev/null 2>&1 /sbin/chkconfig --del puppet fi %preun server if [ "$1" = 0 ] ; then /sbin/service puppetmaster stop > /dev/null 2>&1 /sbin/chkconfig --del puppetmaster fi %postun server if [ "$1" -ge 1 ]; then /sbin/service puppetmaster condrestart > /dev/null 2>&1 fi %clean rm -rf %{buildroot} %changelog +* Wed Aug 22 2007 David Lutterkort - 0.23.2-1 +- New version + * Thu Jul 26 2007 David Lutterkort - 0.23.1-1 - Remove old config files * Wed Jun 20 2007 David Lutterkort - 0.23.0-1 - Install one puppet.conf instead of old config files, keep old configs around to ease update - Use plain shell commands in install instead of macros * Wed May 2 2007 David Lutterkort - 0.22.4-1 - New version * Thu Mar 29 2007 David Lutterkort - 0.22.3-1 - Claim ownership of _sysconfdir/puppet (bz 233908) * Mon Mar 19 2007 David Lutterkort - 0.22.2-1 - Set puppet's homedir to /var/lib/puppet, not /var/puppet - Remove no-lockdir patch, not needed anymore * Mon Feb 12 2007 David Lutterkort - 0.22.1-2 - Fix bogus config parameter in puppetd.conf * Sat Feb 3 2007 David Lutterkort - 0.22.1-1 - New version * Fri Jan 5 2007 David Lutterkort - 0.22.0-1 - New version * Mon Nov 20 2006 David Lutterkort - 0.20.1-2 - Make require ruby(abi) and buildarch: noarch conditional for fedora 5 or later to allow building on older fedora releases * Mon Nov 13 2006 David Lutterkort - 0.20.1-1 - New version * Mon Oct 23 2006 David Lutterkort - 0.20.0-1 - New version * Tue Sep 26 2006 David Lutterkort - 0.19.3-1 - New version * Mon Sep 18 2006 David Lutterkort - 0.19.1-1 - New version * Thu Sep 7 2006 David Lutterkort - 0.19.0-1 - New version * Tue Aug 1 2006 David Lutterkort - 0.18.4-2 - Use /usr/bin/ruby directly instead of /usr/bin/env ruby in executables. Otherwise, initscripts break since pidof can't find the right process * Tue Aug 1 2006 David Lutterkort - 0.18.4-1 - New version * Fri Jul 14 2006 David Lutterkort - 0.18.3-1 - New version * Wed Jul 5 2006 David Lutterkort - 0.18.2-1 - New version * Wed Jun 28 2006 David Lutterkort - 0.18.1-1 - Removed lsb-config.patch and yumrepo.patch since they are upstream now * Mon Jun 19 2006 David Lutterkort - 0.18.0-1 - Patch config for LSB compliance (lsb-config.patch) - Changed config moves /var/puppet to /var/lib/puppet, /etc/puppet/ssl to /var/lib/puppet, /etc/puppet/clases.txt to /var/lib/puppet/classes.txt, /etc/puppet/localconfig.yaml to /var/lib/puppet/localconfig.yaml * Fri May 19 2006 David Lutterkort - 0.17.2-1 - Added /usr/bin/puppetrun to server subpackage - Backported patch for yumrepo type (yumrepo.patch) * Wed May 3 2006 David Lutterkort - 0.16.4-1 - Rebuilt * Fri Apr 21 2006 David Lutterkort - 0.16.0-1 - Fix default file permissions in server subpackage - Run puppetmaster as user puppet - rebuilt for 0.16.0 * Mon Apr 17 2006 David Lutterkort - 0.15.3-2 - Don't create empty log files in post-install scriptlet * Fri Apr 7 2006 David Lutterkort - 0.15.3-1 - Rebuilt for new version * Wed Mar 22 2006 David Lutterkort - 0.15.1-1 - Patch0: Run puppetmaster as root; running as puppet is not ready for primetime * Mon Mar 13 2006 David Lutterkort - 0.15.0-1 - Commented out noarch; requires fix for bz184199 * Mon Mar 6 2006 David Lutterkort - 0.14.0-1 - Added BuildRequires for ruby * Wed Mar 1 2006 David Lutterkort - 0.13.5-1 - Removed use of fedora-usermgmt. It is not required for Fedora Extras and makes it unnecessarily hard to use this rpm outside of Fedora. Just allocate the puppet uid/gid dynamically * Sun Feb 19 2006 David Lutterkort - 0.13.0-4 - Use fedora-usermgmt to create puppet user/group. Use uid/gid 24. Fixed problem with listing fileserver.conf and puppetmaster.conf twice * Wed Feb 8 2006 David Lutterkort - 0.13.0-3 - Fix puppetd.conf * Wed Feb 8 2006 David Lutterkort - 0.13.0-2 - Changes to run puppetmaster as user puppet * Mon Feb 6 2006 David Lutterkort - 0.13.0-1 - Don't mark initscripts as config files * Mon Feb 6 2006 David Lutterkort - 0.12.0-2 - Fix BuildRoot. Add dist to release * Tue Jan 17 2006 David Lutterkort - 0.11.0-1 - Rebuild * Thu Jan 12 2006 David Lutterkort - 0.10.2-1 - Updated for 0.10.2 Fixed minor kink in how Source is given * Wed Jan 11 2006 David Lutterkort - 0.10.1-3 - Added basic fileserver.conf * Wed Jan 11 2006 David Lutterkort - 0.10.1-1 - Updated. Moved installation of library files to sitelibdir. Pulled initscripts into separate files. Folded tools rpm into server * Thu Nov 24 2005 Duane Griffin - Added init scripts for the client * Wed Nov 23 2005 Duane Griffin - First packaging diff --git a/documentation/Rakefile b/documentation/Rakefile deleted file mode 100644 index 6bfe4d239..000000000 --- a/documentation/Rakefile +++ /dev/null @@ -1,58 +0,0 @@ -# vim: syntax=ruby - -require 'bluecloth' - -htmlfiles = [] - -CLEAN = [] - -FileList['**/*.page'].each do |src| - name = src.sub(".page", ".html") - htmlfiles << name - CLEAN << name - file name => [src, "Rakefile"] do - File.open(name, "w") do |f| - text = File.read(src).sub(/\A^---[^-]+^---$/, '') - f.puts BlueCloth.new( text ).to_html - end - end -end - -task :clean do - CLEAN.each do |file| - if FileTest.directory?(file) - sh %{rm -rf #{file}} - elsif FileTest.exists?(file) - File.unlink(file) - end - end -end - -task :html => htmlfiles - -task :default => :html - -docs = %w{configref typedocs reports functions} - -docs.each do |doc| - task doc do - docs = %x{puppetdoc --mode #{doc}} - - header = "reference/%s.header" % doc - if FileTest.exists?(header) - headertext = File.read(header) - else - headertext = "" - end - - file = "reference/%s.page" % doc - - puts "Creating %s" % file - File.open(file, "w") do |f| - f.puts headertext - f.puts docs - end - end -end - -task :docs => docs diff --git a/documentation/faq.page b/documentation/faq.page deleted file mode 100644 index 83a83dca6..000000000 --- a/documentation/faq.page +++ /dev/null @@ -1,258 +0,0 @@ ---- -inMenu: true -title: FAQ -orderInfo: 2 ---- -What is Puppet? ----------------- -Puppet is an open-source next-generation server automation tool. It is -composed of a *declarative* language for expressing system configuration, a -*client* and *server* for distributing it, and a *library* for realizing the -configuration. - -The primary design goal of Puppet is that it have an expressive enough -language backed by a powerful enough library that you can write your own -server automation applications in just a few lines of code. With Puppet, you -can express the configuration of your entire network in one program capable of -realizing the configuration. The fact that Puppet has open source combined -with how easily it can be extended means that you can add whatever -functionality you think is missing and then contribute it back to the main -project if you desire. - -You can learn more about Puppet by reading its [Documentation][]. - -What license is Puppet released under? --------------------------------------- -Puppet is open source and is released under the [GNU Public License][]. - -Why does Puppet exist? ----------------------- -Luke Kanies, who founded Reductive Labs, has been doing server automation for -years, and Puppet is the result of his frustration with existing tools. After -significant effort spent trying to enhance cfengine, plus a stint at a -commercial server automation vendor, Luke concluded that the only way to get a -great automation tool was to develop one. - -Puppet is actually the result of years of design and prototyping (called Blink -during its prototype phases), but only in 2005 was a commercial company -(Reductive Labs) built to be fully dedicated to its creation. If Puppet is -not the most powerful and most flexible server automation platform available, -then its goals are not being met. - -Why does Puppet have its own language? --------------------------------------- -This actually is a frequently asked question, and people most often ask why I -did not choose to use something like XML or YAML as the configuration -format; otherwise people ask why I didn't just choose to just use Ruby as the -input language. - -The input format for Puppet is not XML or YAML because these are data formats -developed to be easy for computers to handle. They do not do conditionals -(although, yes, they support data structures that could be considered -conditionals), but mostly, they're just horrible human interfaces. While some -people are comfortable reading and writing them, there's a reason why we use -web browsers instead of just reading the HTML directly. Also, using XML or -YAML would limit the ability to make sure the interface is declarative -- one -process might treat an XML configuration differently from another. - -As to just using Ruby as the input format, that unnecessarily ties Puppet to -Ruby, which is undesirable, and Ruby provides a bit too much functionality. -For more detail, see the [thread][] on the puppet-dev list. - - -How does Puppet compare to cfengine? ------------------------------------- -Puppet could be said to be the next-generation cfengine. The overall design -is heavily influenced by cfengine, but the language is more powerful than -cfengine's and the library is more flexible. In addition, Puppet's client and -server use standard protocols like XMLRPC and are easy to enhance with new -functionality, so they are well-positioned to become the platform for the -network applications of the future, while cfengine's client and server rely -entirely on cfengine-specific protocols and are quite difficult to enhance. - -See [How Puppet Compares to Cfengine][] for more information. - -How does Puppet compare to available commercial products? ---------------------------------------------------------- -The primary commercial vendors are BladeLogic and OpsWare. While they both -have useful product lines, Puppet surpasses them by reframing the entire -server automation problem -- while the commercial vendors are writing GUI -applications for you, Reductive Labs is providing a development platform with -all the features of a great language, like library development, code sharing, -and the ability to version control your configurations. - -Trying to express a complex network configuration entirely through a GUI is an -exercise in frustration that no one should suffer, but expressing the -abstraction necessary to share those GUI configurations goes beyond -frustrating. - -Of course, another great difference between Puppet and the commercial products -is that Puppet is open sourced under the [GNU Public License][]. You can -[download][] the product, try it out, peruse the source, and make whatever -modifications you want. You have to have more than 100 servers just to get a -demo from the commercial vendors, but Puppet is available for testing by any -company that needs to reduce its server administration costs. - -Who would find Puppet useful? ------------------------------ -Any organization that would like to reduce the cost of maintaining its -computers could benefit from using Puppet. However, because the return on -investment is linked to multiple factors, like current administrative -overhead, diversity among existing computers, and cost of downtime, it can be -difficult for organizations to determine whether they should invest in any -configuration management tools, much less Puppet. Reductive Labs can always -be contacted directly at info at reductivelabs.com to help answer this question. - -Generally, however, an organization should be using server automation if any -of the following are true: - -* It has high server administration costs -* It pays a high price for downtime, either because of contracts or - opportunity cost -* It has many servers that are essentially either identical or nearly - identical -* Flexibility and agility in server configuration are essential - -Can Puppet manage workstations? -------------------------------- -Yes, Puppet can manage any type of machine. We have found that most -organizations are more concerned with server management than workstation -management, and frankly, the term 'server' is slightly more aesthetically -appealing than 'computer', but Puppet would be ideal for organizations with a -large number of workstations. - -Does Puppet run on Windows? ---------------------------- -The short answer is 'not yet'. It will eventually, but Reductive Labs does -not yet have the development bandwidth to make this work. - -What size organizations should use Puppet? ------------------------------------------- -There is no minimum or maximum organization size that can benefit from Puppet, -but there are sizes that are more likely to benefit. Organizations with fewer -than 10-20 servers are unlikely to consider maintaining those servers to be a -real problem, and thus they can avoid investment in tools even though those -tools could likely provide savings. - -There is no real upper limit to who could benefit from using Puppet. -Obviously as the server count increases the investment must increase somewhat, -but with Puppet that increase is not linear. - -My servers are all unique; can Puppet still help? -------------------------------------------------- -All servers are at least somewhat unique -- with different host names and -different IP addresses -- but very few servers are entirely unique, since -nearly every one runs a relatively standard operating system. Servers are -also often very similar to other servers within a single organization -- all -Solaris servers might have similar security settings, or all web servers might -have roughly equivalent configurations -- even if they're very different from -servers in other organizations. Finally, servers are often needlessly unique, -in that they have been built and managed manually with no attempt at retaining -appropriate consistency. - -Puppet can help both on the side of consistency and uniqueness. Puppet -can be used to express the consistency that should exist, even if -that consistency spans arbritrary sets of servers based on any type of data -like operating system, data center, or physical location. Puppet can also be -used to handle uniqueness, either by allowing special provision of what makes -a given host unique or through specifying exceptions to otherwise standard -classes. - -Who is Reductive Labs? ----------------------- -Reductive Labs is a small, private company focused on reframing the server -automation problem. Our primary focus is Puppet, but Reductive Labs also -provides automation consulting, training, and custom development. For more -information email info at reductivelabs.com. - -The [Projects][] page lists our active projects. - -How Do I Install Puppet? ------------------------- - -The [Installation Guide][] documents the fastest way to start using Puppet. - -What is a Manifest? -------------------- -Because the word *script* implies a procedural one-step-after-another program, -the word does not apply well to Puppet programs. Thus, we use the word -*manifest* to describe declarative Puppet programs. Speaking of applying, -Puppet *applies* a manifest to a server or a network, rather than *executing* -it. - -How Do I Write Manifests? -------------------------- -The best way is to download Puppet and just start writing. There are multiple -sets of examples, including the [examples][] used in [unit testing][], -and the [reference][]will obviously be useful. - -How Do I Run Manifests? ------------------------ -Once you have Puppet installed according the the [Installation Guide][], -just run the ``puppet`` executable against your example: - - puppet -v example.pp - -How do I contribute? --------------------- -First join the [Mailing List][] -- there is currently only a development list, -but as the community grows a user list will be created. You can also join the -IRC channel ``#puppet`` on irc.freenode.net, where Puppet's developers will be -hanging out most days (and nights). - -The most valuable contribution you can make, though, is to use Puppet and -submit your feedback, either directly on IRC or through the mailing list, or -via the [bug database][]. We're always looking for great ideas to incorporate -into Puppet. - -When is the Next Release? -------------------------- -There are regular feature and release updates on the [Mailing List][], -and you can always find the latest release in the [download][] directory. - -I keep getting "certificates were not trusted". What's wrong? --------------------------------------------------------------- -Historically this has usually been a problem with the client machine having -such a different date setting that the certificate is not yet valid. - -You can figure the problem out by manually verifying the certificate with -openssl: - - sudo openssl verify -CAfile /etc/puppet/ssl/certs/ca.pem /etc/puppet/ssl/certs/myhostname.domain.com.pem - -[Mailing List]: http://mail.madstop.com/mailman/listinfo/puppet-dev -[Projects]: /projects/ -[Documentation]: documentation/index.html -[Installation Guide]: documentation/installation.html -[How Puppet Compares to Cfengine]: documentation/notcfengine.html -[GNU Public License]: http://www.gnu.org/copyleft/gpl.html -[examples]: /trac/puppet/browser/trunk/examples/code/ -[unit testing]: http://www.pragmaticprogrammer.com/starter_kit/ut/ -[bug database]: /trac/puppet/report -[reference]: documentation/typedocs.html -[download]: /downloads -[thread]: http://mail.madstop.com/pipermail/puppet-dev/2006-April/000393.html - -I'm getting IPv6 errors; what's wrong? --------------------------------------- -This can apparently happen if Ruby is not compiled with IPv6 support; see the -[mail thread](http://mail.madstop.com/pipermail/puppet-dev/2006-August/001410.html) -for more details. The only known solution is to make sure you're running a -version of Ruby compiled with IPv6 support. - -I'm getting ``tlsv1 alert unknown ca`` errors; what's wrong? ------------------------------------------------------------- -This problem is caused by ``puppetmasterd`` not being able to read its -ca certificate. This problem might occur up to 0.18.4 but has been -fixed in 0.19.0. You can probably fix it for versions before 0.19.0 by -chgrping /etc/puppet/ssl to the puppet group, but ``puppetd`` might -chgrp it back. Having ``puppetmasterd`` start as the root group should -fix the problem permanently until you can upgrade. - -How do all of these variables, like ``operatingsystem``, get set? ------------------------------------------------------------------ -The variables are all set by [Facter](/projects/facter). You can get -a full listing of the available variables and their values by running -``facter`` by itself in a shell. - -*$Id$* diff --git a/documentation/index.page b/documentation/index.page deleted file mode 100644 index 940951258..000000000 --- a/documentation/index.page +++ /dev/null @@ -1,71 +0,0 @@ ---- -inMenu: false -directoryName: Puppet ---- - -Puppet lets you centrally manage every important aspect of your system using a -cross-platform specification language that manages all the separate elements -normally aggregated in different files, like users, cron jobs, and hosts, -along with obviously discrete elements like packages, services, and files. - -Puppet's simple declarative specification language provides powerful classing -abilities for drawing out the similarities between hosts while allowing them -to be as specific as necessary, and it handles dependency and prerequisite -relationships between objects clearly and explicitly. Puppet is written -entirely in [Ruby](http://www.ruby-lang.org/). - -Many general questions about Puppet and Reductive are answered in the -[FAQ](faq.html), such as "How to get started quickly", "How to contribute", -and "What is Puppet's License? (GPL)") - -You can also often get good support on ``#puppet`` on irc.freenode.net; -Puppet's primary author, Luke Kanies, is usually online there. - -## Relevant Links - -* [Documentation](/trac/puppet/wiki/DocumentationStart) - - Available documentation on puppet. Including an Introduction, and Language - & Type Library References. - -* [Puppet Recipe Manager](http://prmweb.hezmatt.org/) - - A CPAN-like site for sharing and downloading Puppet recipes. - -* [Cookbook](/trac/puppet/tags/puppet%2Crecipe) - - All of the cookbook recipes on the Puppet wiki. - -* [Downloads](/downloads/) - - Puppet source code, Packages (RPMs, debs, etc.), and Ruby GEM packages. - -* [Source Code](/svn/puppet/) - - Puppet Subversion Repository - -* [Bug Tracker](https://reductivelabs.com/trac/puppet) - - Bug tickets, feature enhancements, and source browsing - -* [Configuration Management Blog](http://madstop.com) - - A blog Luke Kanies is maintaining about the development process of Puppet. - -## Mailing Lists - -* [Puppet User](http://mail.madstop.com/mailman/listinfo/puppet-users) - - The Puppet users mailing list, for any and all Puppet discussion. - -* [Puppet Developer](http://mail.madstop.com/mailman/listinfo/puppet-dev) - - The Puppet-dev mailing list, for all public discussions related to the - development of puppet. All emails generated by the bug tracker are also - sent to this list. - -* [Puppet Commits](http://mail.madstop.com/mailman/listinfo/puppet-commit) - - A read-only list that gets a copy of all subversion commits. - -*$Id$* diff --git a/documentation/reference/configref.header b/documentation/reference/configref.header deleted file mode 100644 index 854eb1d99..000000000 --- a/documentation/reference/configref.header +++ /dev/null @@ -1,94 +0,0 @@ ---- -inMenu: true -title: Configuration Reference -orderInfo: 40 ---- -# Puppet Configuration Reference - -## Specifying Configuration Parameters - -Every Puppet executable (with the exception of ``puppetdoc``) accepts all of -the arguments below, but not all of the arguments make sense for every executable. -Each argument has a section listed with it in parentheses; often, that section -will map to an executable (e.g., ``puppetd``), in which case it probably only -makes sense for that one executable. If ``puppet`` is listed as the section, -it is most likely an option that is valid for everyone. - -This will not always be the case. 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 arguments can be supplied to the executables either as command-line -arugments or in the configuration file for the appropriate executable. For -instance, the command-line invocation below would set the configuration directory -to /private/puppet - - $ puppetd --confdir=/private/puppet - -Note that boolean options are turned on and off with a slightly different syntax -on the command line: - - $ puppetd --storeconfigs - - $ puppetd --no-storeconfigs - -The invocations above will enable and disable, respectively, the storage of -the client configuration. - -As mentioned above, the configuration parameters can also be stored in a -configuration file located in the configuration directory (`/etc/puppet` -by default). The file is named for the executable it is intended for, for -example `/etc/puppetd.conf` is the configuration file for `puppetd`. - -The file, which follows INI-style formatting, should contain a bracketed -heading named for the executable, followed by pairs of parameters with their -values. Here is an example of a very simple `puppetd.conf` file: - - [puppetd] - confdir = /private/puppet - storeconfigs = true - -Note that boolean parameters must be explicitly specified as `true` or -`false` as seen above. - -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: - - $ puppetd --genconfig > /etc/puppet/puppetd.conf - -Note that this invocation will "clobber" (throw away) the contents of any -pre-existing `puppetd.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: - - $ puppetd --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: - - $ puppetd --mkusers - -## Signals - -The `puppetd` and `puppetmasterd` executables catch some signals for special -handling. Both daemons catch (`SIGHUP`), which forces the server to restart -tself. Predictably, interrupt and terminate (`SIGINT` and `SIGHUP`) will shut -down the server, whether it be an instance of `puppetd` or `puppetmasterd`. - -Sending the `SIGUSR1` signal to an instance of `puppetd` will cause it to -immediately begin a new configuration transaction with the server. This -signal has no effect on `puppetmasterd`. - - -## Configuration Parameter Reference - -Below is a list of all documented parameters. Any default values are in ``block type`` at the end of the description. - - diff --git a/documentation/reference/configref.page b/documentation/reference/configref.page deleted file mode 100644 index d3081fe0c..000000000 --- a/documentation/reference/configref.page +++ /dev/null @@ -1,576 +0,0 @@ ---- -inMenu: true -title: Configuration Reference -orderInfo: 40 ---- -# Puppet Configuration Reference - -## Specifying Configuration Parameters - -Every Puppet executable (with the exception of ``puppetdoc``) accepts all of -the arguments below, but not all of the arguments make sense for every executable. -Each argument has a section listed with it in parentheses; often, that section -will map to an executable (e.g., ``puppetd``), in which case it probably only -makes sense for that one executable. If ``puppet`` is listed as the section, -it is most likely an option that is valid for everyone. - -This will not always be the case. 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 arguments can be supplied to the executables either as command-line -arugments or in the configuration file for the appropriate executable. For -instance, the command-line invocation below would set the configuration directory -to /private/puppet - - $ puppetd --confdir=/private/puppet - -Note that boolean options are turned on and off with a slightly different syntax -on the command line: - - $ puppetd --storeconfigs - - $ puppetd --no-storeconfigs - -The invocations above will enable and disable, respectively, the storage of -the client configuration. - -As mentioned above, the configuration parameters can also be stored in a -configuration file located in the configuration directory (`/etc/puppet` -by default). The file is named for the executable it is intended for, for -example `/etc/puppetd.conf` is the configuration file for `puppetd`. - -The file, which follows INI-style formatting, should contain a bracketed -heading named for the executable, followed by pairs of parameters with their -values. Here is an example of a very simple `puppetd.conf` file: - - [puppetd] - confdir = /private/puppet - storeconfigs = true - -Note that boolean parameters must be explicitly specified as `true` or -`false` as seen above. - -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: - - $ puppetd --genconfig > /etc/puppet/puppetd.conf - -Note that this invocation will "clobber" (throw away) the contents of any -pre-existing `puppetd.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: - - $ puppetd --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: - - $ puppetd --mkusers - -## Signals - -The `puppetd` and `puppetmasterd` executables catch some signals for special -handling. Both daemons catch (`SIGHUP`), which forces the server to restart -tself. Predictably, interrupt and terminate (`SIGINT` and `SIGHUP`) will shut -down the server, whether it be an instance of `puppetd` or `puppetmasterd`. - -Sending the `SIGUSR1` signal to an instance of `puppetd` will cause it to -immediately begin a new configuration transaction with the server. This -signal has no effect on `puppetmasterd`. - - -## Configuration Parameter Reference - -Below is a list of all documented parameters. Any default values are in ``block type`` at the end of the description. - - -#### authconfig (puppet) - -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 ``puppetd`` and ``puppetmasterd``. ``/etc/puppet/namespaceauth.conf`` - -#### autoflush (puppet) - -Whether log files should always flush to disk. - -#### autosign (ca) - -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. ``/etc/puppet/autosign.conf`` - -#### bucketdir (puppetmasterd) - -Where FileBucket files are stored. ``/var/puppet/bucket`` - -#### ca_days (ca) - -How long a certificate should be valid. This parameter is deprecated, use ca_ttl instead - -#### ca_md (ca) - -The type of hash used in certificates. ``md5`` - -#### ca_ttl (ca) - -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) ``5y`` - -#### cacert (ca) - -The CA certificate. ``/etc/puppet/ssl/ca/ca_crt.pem`` - -#### cacrl (ca) - -The certificate revocation list (CRL) for the CA. Set this to 'none' if you do not want to use a CRL. ``/etc/puppet/ssl/ca/ca_crl.pem`` - -#### cadir (ca) - -The root directory for the certificate authority. ``/etc/puppet/ssl/ca`` - -#### cakey (ca) - -The CA private key. ``/etc/puppet/ssl/ca/ca_key.pem`` - -#### capass (ca) - -Where the CA stores the password for the private key ``/etc/puppet/ssl/ca/private/ca.pass`` - -#### caprivatedir (ca) - -Where the CA stores private certificate information. ``/etc/puppet/ssl/ca/private`` - -#### capub (ca) - -The CA public key. ``/etc/puppet/ssl/ca/ca_pub.pem`` - -#### casesensitive (puppet) - -Whether matching in case statements and selectors should be case-sensitive. Case insensitivity is handled by downcasing all values before comparison. - -#### cert_inventory (ca) - -A Complete listing of all certificates ``/etc/puppet/ssl/ca/inventory.txt`` - -#### certdir (certificates) - -The certificate directory. ``/etc/puppet/ssl/certs`` - -#### classfile (puppetd) - -The file in which puppetd stores a list of the classes associated with the retrieved configuratiion. Can be loaded in the separate ``puppet`` executable using the ``--loadclasses`` option. ``/etc/puppet/classes.txt`` - -#### clientbucketdir (filebucket) - -Where FileBucket files are stored locally. ``/var/puppet/clientbucket`` - -#### color (puppet) - -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. ``ansi`` - -#### confdir (puppet) - -The main Puppet configuration directory. ``/etc/puppet`` - -#### config (puppetdoc) - -The configuration file for puppetdoc. ``/etc/puppet/puppetdoc.conf`` - -#### configprint (puppet) - -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. - -#### configtimeout (puppetd) - -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. ``30`` - -#### csrdir (ca) - -Where the CA stores certificate requests ``/etc/puppet/ssl/ca/requests`` - -#### dbadapter (puppetmaster) - -The type of database to use. ``sqlite3`` - -#### dblocation (puppetmaster) - -The database cache for client configurations. Used for querying within the language. ``/var/puppet/state/clientconfigs.sqlite3`` - -#### dbmigrate (puppetmaster) - -Whether to automatically migrate the database. - -#### dbname (puppetmaster) - -The name of the database to use. ``puppet`` - -#### dbpassword (puppetmaster) - -The database password for Client caching. Only used when networked databases are used. ``puppet`` - -#### dbserver (puppetmaster) - -The database server for Client caching. Only used when networked databases are used. ``localhost`` - -#### dbuser (puppetmaster) - -The database user for Client caching. Only used when networked databases are used. ``puppet`` - -#### downcasefacts (puppetd) - -Whether facts should be made all lowercase when sent to the server. - -#### evaltrace (transaction) - -Whether each resource should log when it is being evaluated. This allows you to interactively see exactly what is being done. - -#### external_nodes (puppet) - -An external command that can produce node information. The first line of output must be either the parent node or blank, and if there is a second line of output it should be a list of whitespace-separated classes to include on that node. This command makes it straightforward to store your node mapping information in other data sources like databases. For unknown nodes, the commands should exit with an exit code of 1. ``none`` - -#### factdest (puppet) - -Where Puppet should store facts that it pulls down from the central server. ``/var/puppet/facts`` - -#### factpath (puppet) - -Where Puppet should look for facts. Multiple directories should be colon-separated, like normal PATH variables. ``/var/puppet/facts`` - -#### factsignore (puppet) - -What files to ignore when pulling down facts. ``.svn CVS`` - -#### factsource (puppet) - -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. ``puppet://puppet/facts`` - -#### factsync (puppet) - -Whether facts should be synced with the central server. - -#### fileserverconfig (fileserver) - -Where the fileserver configuration is stored. ``/etc/puppet/fileserver.conf`` - -#### filetimeout (puppet) - -The minimum time to wait between checking for updates in configuration files. ``15`` - -#### genconfig (puppet) - -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 (puppet) - -Whether to just print a manifest to stdout and exit. Only makes sense when used interactively. Takes into account arguments specified on the CLI. - -#### graph (puppet) - -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 (puppet) - -Where to store dot-outputted graphs. ``/var/puppet/state/graphs`` - -#### group (puppetmasterd) - -The group puppetmasterd should run as. ``puppet`` - -#### hostcert (certificates) - -Where individual hosts store and look for their certificates. ``/etc/puppet/ssl/certs/culain.madstop.com.pem`` - -#### hostprivkey (certificates) - -Where individual hosts store and look for their private key. ``/etc/puppet/ssl/private_keys/culain.madstop.com.pem`` - -#### hostpubkey (certificates) - -Where individual hosts store and look for their public key. ``/etc/puppet/ssl/public_keys/culain.madstop.com.pem`` - -#### httplog (puppetd) - -Where the puppetd web server logs. ``/var/puppet/log/http.log`` - -#### ignoreschedules (puppetd) - -Boolean; whether puppetd should ignore schedules. This is useful for initial puppetd runs. - -#### keylength (ca) - -The bit length of keys. ``1024`` - -#### ldapattrs (ldap) - -The LDAP attributes to use to define Puppet classes. Values should be comma-separated. ``puppetclass`` - -#### ldapbase (ldap) - -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. - -#### ldapnodes (ldap) - -Whether to search for node configurations in LDAP. - -#### ldapparentattr (ldap) - -The attribute to use to define the parent node. ``parentnode`` - -#### ldappassword (ldap) - -The password to use to connect to LDAP. - -#### ldapport (ldap) - -The LDAP port. Only used if ``ldapnodes`` is enabled. ``389`` - -#### ldapserver (ldap) - -The LDAP server. Only used if ``ldapnodes`` is enabled. ``ldap`` - -#### ldapssl (ldap) - -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. - -#### ldapstring (ldap) - -The search string used to find an LDAP node. ``(&(objectclass=puppetClient)(cn=%s))`` - -#### ldaptls (ldap) - -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. - -#### ldapuser (ldap) - -The user to use to connect to LDAP. Must be specified as a full DN. - -#### lexical (puppet) - -Whether to use lexical scoping (vs. dynamic). - -#### listen (puppetd) - -Whether puppetd 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 ``puppetd`` runs. - -#### localcacert (certificates) - -Where each client stores the CA certificate. ``/etc/puppet/ssl/certs/ca.pem`` - -#### localconfig (puppetd) - -Where puppetd caches the local configuration. An extension indicating the cache format is added automatically. ``/etc/puppet/localconfig`` - -#### logdir (puppet) - -The Puppet log directory. ``/var/puppet/log`` - -#### manifest (puppetmasterd) - -The entry-point manifest for puppetmasterd. ``/etc/puppet/manifests/site.pp`` - -#### manifestdir (puppetmasterd) - -Where puppetmasterd looks for its manifests. ``/etc/puppet/manifests`` - -#### masterhttplog (puppetmasterd) - -Where the puppetmasterd web server logs. ``/var/puppet/log/masterhttp.log`` - -#### masterlog (puppetmasterd) - -Where puppetmasterd logs. This is generally not used, since syslog is the default log destination. ``/var/puppet/log/puppetmaster.log`` - -#### masterport (puppetmasterd) - -Which port puppetmasterd listens on. ``8140`` - -#### mkusers (puppet) - -Whether to create the necessary user and group that puppetd will run as. - -#### node_name (puppetmasterd) - -How the puppetmaster determines the client's identity and sets the 'hostname' fact 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) ``cert`` - -#### noop (puppetd) - -Whether puppetd should be run in noop mode. - -#### paramcheck (ast) - -Whether to validate parameters during parsing. ``true`` - -#### parseonly (puppetmasterd) - -Just check the syntax of the manifests. - -#### passfile (certificates) - -Where puppetd stores the password for its private key. Generally unused. ``/etc/puppet/ssl/private/password`` - -#### path (puppet) - -The shell search path. Defaults to whatever is inherited from the parent process. ``none`` - -#### plugindest (puppet) - -Where Puppet should store plugins that it pulls down from the central server. ``/var/puppet/plugins`` - -#### pluginpath (puppet) - -Where Puppet should look for plugins. Multiple directories should be colon-separated, like normal PATH variables. ``/var/puppet/plugins`` - -#### pluginsignore (puppet) - -What files to ignore when pulling down plugins. ``.svn CVS`` - -#### pluginsource (puppet) - -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. ``puppet://puppet/plugins`` - -#### pluginsync (puppet) - -Whether plugins should be synced with the central server. - -#### privatedir (certificates) - -Where the client stores private certificate information. ``/etc/puppet/ssl/private`` - -#### privatekeydir (certificates) - -The private key directory. ``/etc/puppet/ssl/private_keys`` - -#### publickeydir (certificates) - -The public key directory. ``/etc/puppet/ssl/public_keys`` - -#### puppetdlockfile (puppetd) - -A lock file to temporarily stop puppetd from doing anything. ``/var/puppet/state/puppetdlock`` - -#### puppetdlog (puppetd) - -The log file for puppetd. This is generally not used. ``/var/puppet/log/puppetd.log`` - -#### puppetport (puppetd) - -Which port puppetd listens on. ``8139`` - -#### railslog (puppetmaster) - -Where Rails-specific logs are sent ``/var/puppet/log/rails.log`` - -#### report (puppetd) - -Whether to send reports after every transaction. - -#### reportdir (reporting) - -The directory in which to store reports received from the client. Each client gets a separate subdirectory. ``/var/puppet/reports`` - -#### reports (reporting) - -The list of reports to generate. All reports are looked for in puppet/reports/.rb, and multiple report names should be comma-separated (whitespace is okay). ``store`` - -#### reportserver (puppetd) - -The server to which to send transaction reports. ``puppet`` - -#### req_bits (ca) - -The bit length of the certificates. ``2048`` - -#### rrddir (metrics) - -The directory where RRD database files are stored. Directories for each reporting host will be created under this directory. ``/var/puppet/rrd`` - -#### rrdgraph (metrics) - -Whether RRD information should be graphed. - -#### rrdinterval (metrics) - -How often RRD should expect data. This should match how often the hosts report back to the server. ``1800`` - -#### rundir (puppet) - -Where Puppet PID files are kept. ``/var/run/puppet`` - -#### runinterval (puppetd) - -How often puppetd applies the client configuration; in seconds ``1800`` - -#### serial (ca) - -Where the serial number for certificates is stored. ``/etc/puppet/ssl/ca/serial`` - -#### server (puppetd) - -The server to which server puppetd should connect ``puppet`` - -#### setpidfile (puppet) - -Whether to store a PID file for the daemon. ``true`` - -#### signeddir (ca) - -Where the CA stores signed certificates. ``/etc/puppet/ssl/ca/signed`` - -#### ssldir (puppet) - -Where SSL certificates are kept. ``/etc/puppet/ssl`` - -#### statedir (puppet) - -The directory where Puppet state is stored. Generally, this directory can be removed without causing harm (although it might result in spurious service restarts). ``/var/puppet/state`` - -#### statefile (puppet) - -Where puppetd and puppetmasterd store state associated with the running configuration. In the case of puppetmasterd, this file reflects the state discovered through interacting with clients. ``/var/puppet/state/state.yaml`` - -#### storeconfigs (puppetmaster) - -Whether to store each client's configuration. This requires ActiveRecord from Ruby on Rails. - -#### syslogfacility (puppet) - -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. ``daemon`` - -#### tags (transaction) - -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. - -#### templatedir (puppet) - -Where Puppet looks for template files. ``/var/puppet/templates`` - -#### trace (puppet) - -Whether to print stack traces on some errors - -#### typecheck (ast) - -Whether to validate types during parsing. ``true`` - -#### usecacheonfailure (puppetd) - -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. ``true`` - -#### user (puppetmasterd) - -The user puppetmasterd should run as. ``puppet`` - -#### vardir (puppet) - -Where Puppet stores dynamic and growing data. ``/var/puppet`` - - - ----------------- - - -*This page autogenerated on Fri Jan 26 16:40:43 CST 2007* diff --git a/documentation/reference/functions.header b/documentation/reference/functions.header deleted file mode 100644 index 9c58cd36a..000000000 --- a/documentation/reference/functions.header +++ /dev/null @@ -1,15 +0,0 @@ ---- -inMenu: true -title: Function Reference -orderInfo: 40 ---- - -There are two types of functions in Puppet: Statements and rvalues. -Statements stand on their own and do not return arguments; they are used for -performing stand-alone work like importing. Rvalues return values and can -only be used in a statement requiring a value, such as an assignment or a case -statement. - -Here are the functions available in Puppet: - - diff --git a/documentation/reference/functions.page b/documentation/reference/functions.page deleted file mode 100644 index ad2c03f5a..000000000 --- a/documentation/reference/functions.page +++ /dev/null @@ -1,51 +0,0 @@ ---- -inMenu: true -title: Function Reference -orderInfo: 40 ---- - -There are two types of functions in Puppet: Statements and rvalues. -Statements stand on their own and do not return arguments; they are used for -performing stand-alone work like importing. Rvalues return values and can -only be used in a statement requiring a value, such as an assignment or a case -statement. - -Here are the functions available in Puppet: - - -* **alert** (*statement*): Log a message on the server at level alert. - -* **crit** (*statement*): Log a message on the server at level crit. - -* **debug** (*statement*): Log a message on the server at level debug. - -* **defined** (*rvalue*): Determine whether a given type is defined, either as a native type or a defined type, or whether a resource has been specified. If you are checking with a resource is defined, use the normal resource reference syntax, e.g., ``File['/etc/passwd']``. - -* **emerg** (*statement*): Log a message on the server at level emerg. - -* **err** (*statement*): Log a message on the server at level err. - -* **fail** (*statement*): Fail with a parse error. - -* **include** (*statement*): Evaluate one or more classes. - -* **info** (*statement*): Log a message on the server at level info. - -* **notice** (*statement*): Log a message on the server at level notice. - -* **realize** (*statement*): Make a virtual object real. This is useful when you want to know the name of the virtual object and don't want to bother with a full collection. It is slightly faster than a collection, and, of course, is a bit shorter. You must pass the object using a reference; e.g.: ``realize User[luke]``. - -* **tag** (*statement*): Add the specified tags to the containing class or definition. All contained objects will then acquire that tag, also. - -* **tagged** (*rvalue*): A boolean function that tells you whether the current container is tagged with the specified tags. The tags are ANDed, so thta all of the specified tags must be included for the function to return true. - -* **template** (*rvalue*): Evaluate a template and return its value. See [the templating docs](/trac/puppet/wiki/PuppetTemplating) for more information. Note that if multiple templates are specified, their output is all concatenated and returned as the output of the function. - -* **warning** (*statement*): Log a message on the server at level warning. - - - ----------------- - - -*This page autogenerated on Fri Jan 26 16:40:49 CST 2007* diff --git a/documentation/reference/index.page b/documentation/reference/index.page deleted file mode 100644 index 809b854d3..000000000 --- a/documentation/reference/index.page +++ /dev/null @@ -1,18 +0,0 @@ ---- -inMenu: false -directoryName: Reference -title: Reference -orderInfo: 4 -subtreeLevel: 6 ---- - -Reference -========= -* [Configuration Parameter Reference](configref.html) -* [Function Reference](functions.html) -* [Reports Reference](reports.html) -* [Type Reference](typedocs.html) - - - -*$Id: index.page 1843 2006-11-09 20:47:30Z luke $* diff --git a/documentation/reference/reports.header b/documentation/reference/reports.header deleted file mode 100644 index f919a1aca..000000000 --- a/documentation/reference/reports.header +++ /dev/null @@ -1,19 +0,0 @@ ---- -inMenu: true -title: Reports Reference -orderInfo: 40 ---- - -Puppet clients can report back to the server after each -transaction. This transaction report is sent as a YAML dump and includes every -log message that was generated during the transaction along with as many metrics -as Puppet knows how to collect. - -Currently, clients default to not sending in reports; you can enable reporting -by setting the ``report`` parameter to true. - -To use a report, set the ``reports`` parameter on the server; multiple -reports must be comma-separated. - -Puppet provides multiple report handlers that will process client reports: - diff --git a/documentation/reference/reports.page b/documentation/reference/reports.page deleted file mode 100644 index 0319589ae..000000000 --- a/documentation/reference/reports.page +++ /dev/null @@ -1,82 +0,0 @@ ---- -inMenu: true -title: Reports Reference -orderInfo: 40 ---- - -Puppet clients can report back to the server after each -transaction. This transaction report is sent as a YAML dump and includes every -log message that was generated during the transaction along with as many metrics -as Puppet knows how to collect. - -Currently, clients default to not sending in reports; you can enable reporting -by setting the ``report`` parameter to true. - -To use a report, set the ``reports`` parameter on the server; multiple -reports must be comma-separated. - -Puppet provides multiple report handlers that will process client reports: - -## log - -Send all received logs to the local log destinations. - -## rrdgraph - -Graph all available data about hosts using the RRD library. You -must have the RRD binary library installed to use this report, which -you can get from [Tobias Oetiker's site](http://people.ee.ethz.ch/~oetiker/webtools/rrdtool/pub/contrib/). - -This report will create, manage, and graph RRD database files for each -of the metrics generated during transactions, and it will create a -few simple html files to display the reporting host's graphs. At this -point, it will not create a common index file to display links to -all hosts. - -All RRD files and graphs get created in the ``rrddir`` directory. If -you want to serve these publicly, you should be able to just alias that -directory in a web server. - -## store - -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). - -## tagmail - -This report sends specific log messages to specific email addresses -based on the tags in the log messages. See the -[tag documentation](/trac/puppet/wiki/UsingTags) for more information -on tags. - -To use this report, you must create a ``tagmail.conf`` (in the location -specified by ``tagmap``). This is a simple file that maps tags to -email addresses: Any log messages in the report that match the specified -tags will be sent to the specified email addresses. - -Tags must be comma-separated, and they can be negated so that messages -only match when they do not have that tag. The tags are separated from -the email addresses by a colon, and the email addresses should also -be comma-separated. - -Lastly, there is an ``all`` tag that will always match all log messages. - -Here is an example tagmail.conf: - - all: me@domain.com - webserver, !mailserver: httpadmins@domain.com - -This will send all messages to ``me@domain.com``, and all messages from -webservers that are not also from mailservers to ``httpadmins@domain.com``. - - - - ----------------- - - -*This page autogenerated on Fri Jan 26 16:40:48 CST 2007* diff --git a/documentation/reference/typedocs.header b/documentation/reference/typedocs.header deleted file mode 100644 index 69fabd07c..000000000 --- a/documentation/reference/typedocs.header +++ /dev/null @@ -1,7 +0,0 @@ ---- -inMenu: true -title: Type Reference -orderInfo: 40 ---- -# Type Reference - diff --git a/documentation/reference/typedocs.page b/documentation/reference/typedocs.page deleted file mode 100644 index 3fff7ff70..000000000 --- a/documentation/reference/typedocs.page +++ /dev/null @@ -1,1775 +0,0 @@ ---- -inMenu: true -title: Type Reference -orderInfo: 40 ---- -# Type Reference - -## Table of Contents -1. Meta-Parameters -1. Cron -1. Exec -1. File -1. Filebucket -1. Group -1. Host -1. Mount -1. Notify -1. Package -1. Resources -1. Schedule -1. Service -1. Sshkey -1. Tidy -1. User -1. Yumrepo -1. Zone - -

Meta-Parameters

- -Metaparameters are parameters that work with any element; 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. - - -#### alias -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][] for more information. - -[language tutorial]: languagetutorial.html - - -#### before -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. - -#### check -States which should have their values retrieved -but which should not actually be modified. This is currently used -internally, but will eventually be used for querying, so that you -could specify that you wanted to check the install state of all -packages, and then query the Puppet client daemon to get reports -on all packages. - -#### loglevel -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). Valid values are ``debug``, ``info``, ``notice``, ``warning``, ``err``, ``alert``, ``emerg``, ``crit``, ``verbose``. - -#### noop -Boolean flag indicating whether work should actually -be done. Valid values are ``true``, ``false``. - -#### notify -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. - -#### require -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"] - } - -Note that Puppet will autorequire everything that it can, and -there are hooks in place so that it's easy for elements 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 elements 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. - -#### schedule -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. - -#### subscribe -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: - running => true, - subscribe => file[nagconf] - } - } - -#### tag -Add the specified tags to the associated element. While all elements -are automatically tagged with as much information as possible -(e.g., each class and component containing the element), it can -be useful to add your own tags to a given element. - -Tags are currently useful for things like applying a subset of a -host's configuration: - - puppetd --test --tag mytag - -This way, when you're testing a configuration you can run just the -portion you're testing. - - -## Types - -- *namevar* is the parameter used to uniquely identify a type instance. - This is the parameter that gets assigned when a string is provided before - the colon in a type declaration. In general, only developers will need to - worry about which parameter is the ``namevar``. - - In the following code: - - file { "/etc/passwd": - owner => root, - group => root, - mode => 644 - } - - "/etc/passwd" is considered the name of the file object (used for things like - dependency handling), and because ``path`` is the namevar for ``file``, that - string is assigned to the ``path`` parameter. - -- *parameters* determine the specific configuration of the instance. They either - directly modify the system (internally, these are called states) or they affect - how the instance behaves (e.g., adding a search path for ``exec`` instances - or determining recursion on ``file`` instances). - -When required binaries are specified for providers, fully qualifed paths -indicate that the binary must exist at that specific path and unqualified -binaries indicate that Puppet will search for the binary using the shell -path. - - - - ----------------- - - -

cron

-Installs and manages cron jobs. All fields except the command -and the user are optional, although specifying no periodic -fields would result in the command being executed every -minute. While the name of the cron job is not part of the actual -job, it is used by Puppet to store and retrieve it. - -If you specify a cron job that matches an existing job in every way -except name, then the jobs will be considered equivalent and the -new name will be permanently associated with that job. Once this -association is made and synced to disk, you can then manage the job -normally (e.g., change the schedule of the job). - -Example: - - cron { logrotate: - command => "/usr/sbin/logrotate", - user => root, - hour => 2, - minute => 0 - } - - - -### Cron Parameters -#### command -The command to execute in the cron job. The environment -provided to the command varies by local system rules, and it is -best to always provide a fully qualified command. The user's -profile is not sourced when the command is run, so if the -user's environment is desired it should be sourced manually. - -All cron parameters support ``absent`` as a value; this will -remove any existing values for that field. - -#### ensure -The basic state that the object should be in. Valid values are ``absent``, ``present``. - -#### environment -Any environment settings associated with this cron job. They -will be stored between the header and the job in the crontab. There -can be no guarantees that other, earlier settings will not also -affect a given cron job. - -Also, Puppet cannot automatically determine whether an existing, -unmanaged environment setting is associated with a given cron -job. If you already have cron jobs with environment settings, -then Puppet will keep those settings in the same place in the file, -but will not associate them with a specific job. - -Settings should be specified exactly as they should appear in -the crontab, e.g., 'PATH=/bin:/usr/bin:/usr/sbin'. Multiple -settings should be specified as an array. - -#### hour -The hour at which to run the cron job. Optional; -if specified, must be between 0 and 23, inclusive. - -#### minute -The minute at which to run the cron job. -Optional; if specified, must be between 0 and 59, inclusive. - -#### month -The month of the year. Optional; if specified -must be between 1 and 12 or the month name (e.g., December). - -#### monthday -The day of the month on which to run the -command. Optional; if specified, must be between 1 and 31. - -#### name (*namevar*) -The symbolic name of the cron job. This name -is used for human reference only and is generated automatically -for cron jobs found on the system. This generally won't -matter, as Puppet will do its best to match existing cron jobs -against specified jobs (and Puppet adds a comment to cron jobs it -adds), but it is at least possible that converting from -unmanaged jobs to managed jobs might require manual -intervention. - -The names can only have alphanumeric characters plus the '-' -character. - -#### special -Special schedules only supported on FreeBSD. - -#### user -The user to run the command as. This user must -be allowed to run cron jobs, which is not currently checked by -Puppet. - -The user defaults to whomever Puppet is running as. - -#### weekday -The weekday on which to run the command. -Optional; if specified, must be between 0 and 6, inclusive, with -0 being Sunday, or must be the name of the day (e.g., Tuesday). - - - - ----------------- - - -

exec

-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 *creates* parameter. - -It is worth noting that ``exec`` is special, in that it is not -currently considered an error to have multiple ``exec`` instances -with the same name. This was done purely because it had to be this -way in order to get certain functionality, but it complicates things. -In particular, you will not be able to use ``exec`` instances that -share their commands with other instances as a dependency, since -Puppet has no way of knowing which instance you mean. - -For example: - - # defined in the production class - exec { "make": - cwd => "/prod/build/dir", - path => "/usr/bin:/usr/sbin:/bin" - } - - . etc. . - - # defined in the test class - exec { "make": - cwd => "/test/build/dir", - path => "/usr/bin:/usr/sbin:/bin" - } - -Any other type would throw an error, complaining that you had -the same instance being managed in multiple places, but these are -obviously different images, so ``exec`` had to be treated specially. - -It is recommended to avoid duplicate names whenever possible. - -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 real Puppet element types as quickly as possible. If you find that -you are doing a lot of work with ``exec``, please at least notify -us at Reductive Labs what you are doing, and hopefully we can work with -you to get a native element type for the work you are doing. In general, -it is a Puppet bug if you need ``exec`` to do your work. - - -### Exec Parameters -#### command (*namevar*) -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. - -#### creates -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"] - } - - -#### cwd -The directory from which to run the command. If -this directory does not exist, the command will fail. - -#### env -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. - -#### group -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. - -#### logoutput -Whether to log output. Defaults to logging output at the -loglevel for the ``exec`` element. Values are **true**, *false*, -and any legal log level. Valid values are ``true``, ``false``, ``debug``, ``info``, ``notice``, ``warning``, ``err``, ``alert``, ``emerg``, ``crit``. - -#### onlyif -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. - -#### path -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. - -#### refreshonly -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`` can trigger actions, not ``require``, -so it only makes sense to use ``refreshonly`` with ``subscribe``. Valid values are ``true``, ``false``. - -#### returns -The expected return code. An error will be returned if the -executed command returns something else. Defaults to 0. - -#### timeout -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. - -#### unless -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. - -#### user -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. - - - - ----------------- - - -

file

-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`` element will be used less and less to -manage content, and instead native elements will be used to do so. - -If you find that you are often copying files in from a central -location, rather than using native elements, please contact -Reductive Labs and we can hopefully work with you to develop a -native element to support what you are doing. - - -### File Parameters -#### backup -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. 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. - -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 ``puppetmasterd`` 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. - -#### checksum -How to check whether a file has changed. This state is used internally -for file copying, but it can also be used to monitor files somewhat -like Tripwire without managing the file contents in any way. You can -specify that a file's checksum should be monitored and then subscribe to -the file from another object and receive events to signify -checksum changes, for instance. Valid values are ``time``, ``md5lite``, ``nosum``, ``timestamp``, ``md5``, ``mtime``. Values can also match ``(?-mix:^\{md5|md5lite|timestamp|mtime|time\})``. - -#### content -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 - } - } - - This attribute is especially useful when used with - [templating](/trac/puppet/wiki/PuppetTemplating). - -#### ensure -Whether to create files that don't currently exist. -Possible values are *absent*, *present* (equivalent to ``exists`` in -most file tests -- will match any form of file existence, and if the -file is missing will create an empty file), *file*, and -*directory*. Specifying ``absent`` will delete the file, although -currently this will not recursively delete directories. - -Anything other than those values will be considered to be a symlink. -For instance, the following text creates a link: - - # Useful on solaris - file { "/etc/inetd.conf": - ensure => "/etc/inet/inetd.conf" - } - -You can make relative links: - - # Useful on solaris - file { "/etc/inetd.conf": - ensure => "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. Valid values are ``link``, ``present``, ``absent`` (also called ``false``), ``directory``, ``file``. Values can also match ``(?-mix:.)``. - -#### force -Force the file operation. Currently only used when replacing -directories with links. Valid values are ``true``, ``false``. - -#### group -Which group should own the file. Argument can be either group -name or group ID. - -#### ignore -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., ``*/*``. - -#### linkmaker -An internal parameter used by the *symlink* -type to do recursive link creation. - -#### links -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. Valid values are ``follow``, ``manage``, ``ignore``. - -#### mode -Mode the file should be. Currently relatively limited: -you must specify the exact mode the file should be. - -#### owner -To whom the file should belong. Argument can be user name or -user ID. - -#### path (*namevar*) -The path to the file to manage. Must be fully qualified. - -#### purge -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. Valid values are ``true``, ``false``. - -#### recurse -Whether and how deeply to do recursive -management. Valid values are ``true``, ``false``, ``inf``. Values can also match ``(?-mix:^[0-9]+$)``. - -#### replace -Whether or not to replace a file that is -sourced but exists. This is useful for using file sources -purely for initialization. Valid values are ``true`` (also called ``yes``), ``false`` (also called ``no``). - -#### source -Copy a file over the current file. Uses ``checksum`` to -determine when a file should be copied. Valid values are either -fully qualified paths to files, or URIs. Currently supported URI -types are *puppet* and *file*. - -This is one of the primary mechanisms for getting content into -applications that Puppet does not directly support and is very -useful for those configuration files that don't change much across -sytems. For instance: - - class sendmail { - file { "/etc/mail/sendmail.cf": - source => "puppet://server/module/sendmail.cf" - } - } - -See the [fileserver docs][] for information on how to configure -and use file services within Puppet. - -If you specify multiple file sources for a file, then the first -source that exists will be used. This allows you to specify -what amount to search paths for files: - - file { "/path/to/my/file": - source => [ - "/nfs/files/file.$host", - "/nfs/files/file.$operatingsystem", - "/nfs/files/file" - ] - } - -This will use the first found file as the source. - -You cannot currently copy links using this mechanism; set ``links`` -to ``follow`` if any remote sources are links. - -[fileserver docs]: ../installing/fsconfigref.html - - -#### sourceselect -Whether to copy all valid sources, or just the first one. Valid values are ``first``, ``all``. - -#### target -The target for creating a link. Currently, symlinks are the -only type supported. Valid values are ``notlink``. Values can also match ``(?-mix:.)``. - -#### type -A read-only state to check the file type. - - - - ----------------- - - -

filebucket

-A repository for backing up files. If no filebucket is -defined, then files will be backed up in their current directory, -but the filebucket can be either a host- or site-global repository -for backing up. It stores files and returns the MD5 sum, which -can later be used to retrieve the file if restoration becomes -necessary. A filebucket does not do any work itself; instead, -it can be specified as the value of *backup* in a **file** object. - -Currently, filebuckets are only useful for manual retrieval of -accidentally removed files (e.g., you look in the log for the md5 -sum and retrieve the file with that sum from the filebucket), but -when transactions are fully supported filebuckets will be used to -undo transactions. - -You will normally want to define a single filebucket for your -whole network and then use that as the default backup location: - - # Define the bucket - filebucket { main: server => puppet } - - # Specify it as the default target - File { backup => main } - -Puppetmaster servers create a filebucket by default, so this will -work in a default configuration. - - - -### Filebucket Parameters -#### name (*namevar*) -The name of the filebucket. - -#### path -The path to the local filebucket. If this is -not specified, then the bucket is remote and *server* must be -specified. - -#### port -The port on which the remote server is listening. -Defaults to the normal Puppet port, 8140. - -#### server -The server providing the filebucket. If this is -not specified, then the bucket is local and *path* must be -specified. - - - - ----------------- - - -

group

-Manage groups. This type can only create groups. Group -membership must be managed on individual users. This element 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/group or anything. - -For most platforms, the tools used are ``groupadd`` and its ilk; -for Mac OS X, NetInfo is used. This is currently unconfigurable, -but if you desperately need it to be so, please contact us. - - -### Group Parameters -#### allowdupe -Whether to allow duplicate GIDs. This option does not work on -FreeBSD (contract to the ``pw`` man page). Valid values are ``true``, ``false``. - -#### ensure -The basic state that the object should be in. Valid values are ``absent``, ``present``. - -#### gid -The group ID. Must be specified numerically. If not -specified, a number will be picked, which can result in ID -differences across systems and thus is not recommended. The -GID is picked according to local system standards. - -#### name (*namevar*) -The group name. While naming limitations vary by -system, it is advisable to keep the name to the degenerate -limitations, which is a maximum of 8 characters beginning with -a letter. - -#### provider -The specific backend for provider to use. You will -seldom need to specify this -- Puppet will usually discover the -appropriate provider for your platform. Available providers are: - -* **groupadd**: Group management via ``groupadd`` and its ilk. The default - for most platforms Required binaries: ``groupadd``, ``groupmod``, ``groupdel``. -* **netinfo**: Group management using NetInfo. Default for ``operatingsystem`` == ``darwin``. Required binaries: ``nireport``, ``niutil``. -* **pw**: Group management via ``pw``. Only works on FreeBSD. Default for ``operatingsystem`` == ``freebsd``. Required binaries: ``/usr/sbin/pw``. - - - - ----------------- - - -

host

-Installs and manages host entries. For most systems, these -entries will just be in /etc/hosts, but some systems (notably OS X) -will have different solutions. - - -### Host Parameters -#### alias -Any alias the host might have. Multiple values must be -specified as an array. Note that this state has the same name -as one of the metaparams; using this state to set aliases will -make those aliases available in your Puppet scripts and also on -disk. - -#### ensure -The basic state that the object should be in. Valid values are ``absent``, ``present``. - -#### ip -The host's IP address, IPv4 or IPv6. - -#### name (*namevar*) -The host name. - -#### provider -The specific backend for provider to use. You will -seldom need to specify this -- Puppet will usually discover the -appropriate provider for your platform. Available providers are: - -* **netinfo**: Host management in NetInfo. This provider is highly experimental and is known - not to work currently. Default for ``operatingsystem`` == ``darwin``. Required binaries: ``nireport``, ``mount``, ``niutil``, ``df``, ``umount``. -* **parsed**: - -#### target -The file in which to store service information. Only used by -those providers that write to disk (i.e., not NetInfo). - - - - ----------------- - - -

mount

-Manages mounted mounts, including putting mount -information into the mount table. The actual behavior depends -on the value of the 'ensure' parameter. - - -### Mount Parameters -#### atboot -Whether to mount the mount at boot. Not all platforms -support this. - -#### blockdevice -The the device to fsck. This is state is only valid -on Solaris, and in most cases will default to the correct -value. - -#### device -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. - -#### dump -Whether to dump the mount. Not all platforms -support this. - -#### ensure -Control what to do with this mount. If the value is -``present``, the mount is entered into the mount table, -but not mounted, if it is ``absent``, the entry is removed -from the mount table and the filesystem is unmounted if -currently mounted, if it is ``mounted``, the filesystem -is entered into the mount table and mounted. Valid values are ``absent``, ``present`` (also called ``unmounted``), ``mounted``. - -#### fstype -The mount type. Valid values depend on the -operating system. - -#### name (*namevar*) -The mount path for the mount. - -#### options -Mount options for the mounts, as they would -appear in the fstab. - -#### pass -The pass in which the mount is checked. - -#### path -The deprecated name for the mount point. Please use ``name`` now. - -#### provider -The specific backend for provider to use. You will -seldom need to specify this -- Puppet will usually discover the -appropriate provider for your platform. Available providers are: - -* **netinfo**: Mount management in NetInfo. This provider is highly experimental and is known - not to work currently. Default for ``operatingsystem`` == ``darwin``. Required binaries: ``nireport``, ``mount``, ``niutil``, ``df``, ``umount``. -* **parsed**: Required binaries: ``mount``, ``df``, ``umount``. - -#### target -The file in which to store the mount table. Only used by -those providers that write to disk (i.e., not NetInfo). - - - - ----------------- - - -

notify

-Sends an arbitrary message to the puppetd run-time log. - - -### Notify Parameters -#### message -The message to be sent to the log. - -#### name (*namevar*) -An arbitrary tag for your own reference; the name of the message. - -#### withpath -Whether to not to show the full object path. Sends the -message at the current loglevel. Valid values are ``true``, ``false``. - - - - ----------------- - - -

package

-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 ``type`` parameter; obviously, if you specify that you -want to use ``rpm`` then the ``rpm`` tools must be available. - - -### Package Parameters -#### adminfile -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. - -#### allowcdrom -Tells apt to allow cdrom sources in the sources.list file. -Normally apt will bail if you try this. Valid values are ``true``, ``false``. - -#### category -A read-only parameter set by the package. - -#### configfiles -Whether configfiles should be kept or replaced. Most packages -types do not support this parameter. Valid values are ``keep``, ``replace``. - -#### description -A read-only parameter set by the package. - -#### ensure -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. Valid values are ``absent``, ``present`` (also called ``installed``), ``latest``. Values can also match ``(?-mix:.)``. - -#### instance -A read-only parameter set by the package. - -#### name (*namevar*) -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 = $operationgsystem ? { - 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 = $operationgsystem ? { - 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] - } - - -#### platform -A read-only parameter set by the package. - -#### provider -The specific backend for provider to use. You will -seldom need to specify this -- Puppet will usually discover the -appropriate provider for your platform. Available providers are: - -* **apple**: Package management based on OS X's builtin packaging system. This is - essentially the simplest and least functional package system in existence -- - it only supports installation; no deletion or upgrades. Default for ``operatingsystem`` == ``darwin``. Required binaries: ``/usr/sbin/installer``. -* **apt**: Package management via ``apt-get``. Default for ``operatingsystem`` == ``debian``. Required binaries: ``/usr/bin/apt-get``, ``/usr/bin/apt-cache``, ``/usr/bin/debconf-set-selections``. -* **aptitude**: Package management via ``aptitude``. Required binaries: ``/usr/bin/apt-cache``, ``/usr/bin/aptitude``. -* **blastwave**: Package management using Blastwave.org's ``pkg-get`` command on Solaris. Required binaries: ``pkg-get``. -* **darwinport**: Package management using DarwinPorts on OS X. Required binaries: ``/opt/local/bin/port``. -* **dpkg**: Package management via ``dpkg``. Because this only uses ``dpkg`` - and not ``apt``, you must specify the source of any packages you want - to manage. Required binaries: ``/usr/bin/dpkg``, ``/usr/bin/dpkg-query``. -* **freebsd**: The specific form of package management on FreeBSD. This is an - extremely quirky packaging system, in that it freely mixes between - ports and packages. Apparently all of the tools are written in Ruby, - so there are plans to rewrite this support to directly use those - libraries. Required binaries: ``/usr/sbin/pkg_info``, ``/usr/sbin/pkg_add``, ``/usr/sbin/pkg_delete``. -* **gem**: Ruby Gem support. By default uses remote gems, but you can specify - the path to a local gem via ``source``. Required binaries: ``gem``. -* **openbsd**: OpenBSD's form of ``pkg_add`` support. Default for ``operatingsystem`` == ``openbsd``. Required binaries: ``pkg_info``, ``pkg_add``, ``pkg_delete``. -* **pkgdmg**: Package management based on Apple's Installer.app and DiskUtility.app Required binaries: ``/usr/sbin/installer``, ``/usr/bin/hdiutil``, ``/usr/bin/curl``. -* **portage**: Provides packaging support for Gentoo's portage system. Default for ``operatingsystem`` == ``gentoo``. Required binaries: ``/usr/bin/emerge``, ``/usr/bin/eix``. -* **ports**: Support for FreeBSD's ports. Again, this still mixes packages - and ports. Default for ``operatingsystem`` == ``freebsd``. Required binaries: ``/usr/local/sbin/portupgrade``, ``/usr/local/sbin/portversion``, ``/usr/local/sbin/pkg_deinstall``, ``/usr/sbin/pkg_info``. -* **rpm**: RPM packaging support; should work anywhere with a working ``rpm`` - binary. Required binaries: ``rpm``. -* **sun**: Sun's packaging system. Requires that you specify the source for - the packages you're managing. Default for ``operatingsystem`` == ``solaris``. Required binaries: ``/usr/bin/pkginfo``, ``/usr/sbin/pkgrm``, ``/usr/sbin/pkgadd``. -* **sunfreeware**: Package management using sunfreeware.com's ``pkg-get`` command on Solaris. - At this point, support is exactly the same as ``blastwave`` support and - has not actually been tested. Required binaries: ``pkg-get``. -* **up2date**: Support for Red Hat's proprietary ``up2date`` package update - mechanism. Default for ``operatingsystem`` == ``redhat``. Required binaries: ``/usr/sbin/up2date-nox``. -* **yum**: Support via ``yum``. Default for ``operatingsystem`` == ``fedoracentos``. Required binaries: ``yum``, ``rpm``. - -#### responsefile -A file containing any necessary answers to questions asked by -the package. This is currently only used on Solaris. The -value will be validated according to system rules, but it should -generally be a fully qualified path. - -#### root -A read-only parameter set by the package. - -#### source -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. - -#### status -A read-only parameter set by the package. - -#### type -Deprecated form of ``provider``. - -#### vendor -A read-only parameter set by the package. - - - - ----------------- - - -

resources

-This is a metatype that can manage other resource types. Any -metaparams specified here will be passed on to any generated resources, -so you can purge umanaged resources but set ``noop`` to true so the -purging is only logged and does not actually happen. - - -### Resources Parameters -#### name (*namevar*) -The name of the type to be managed. - -#### purge -Purge unmanaged resources. This will delete any resource -that is not specified in your configuration -and is not required by any specified resources. Valid values are ``true``, ``false``. - -#### unless_system_user -This keeps system users from being purged. By default, it -does not purge users whose UIDs are less than or equal to 500, but you can specify -a different UID as the inclusive limit. Valid values are ``true``, ``false``. Values can also match ``(?-mix:^\d+$)``. - - - - ----------------- - - -

schedule

-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 an element from being applied, they never -guarantee that it is applied. - -Every time Puppet applies its configuration, it will collect the -list of elements whose schedule does not eliminate them from -running right then, but there is currently no system in place to -guarantee that a given element 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 elements 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 elements 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 - } - -With this schedule, the first time that Puppet runs between 2 and 4 AM, -all elements 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 - } - -This will cause elements to be applied every 30 minutes by default. - - - -### Schedule Parameters -#### name (*namevar*) -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 - } - - -#### period -The period of repetition for an element. Choose from among -a fixed list of *hourly*, *daily*, *weekly*, and *monthly*. -The default is for an element to get applied every time that -Puppet runs, whatever that period is. - -Note that the period defines how often a given element will get -applied but not when; if you would like to restrict the hours -that a given element 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 elements 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 element 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. Valid values are ``hourly``, ``daily``, ``weekly``, ``monthly``. - -#### periodmatch -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). *number*/**distance** Valid values are ``number``, ``distance``. - -#### range -The earliest and latest that an element 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" - } - -This is mostly useful for restricting certain elements to being -applied in maintenance windows or during off-peak hours. - -#### repeat -How often the application gets repeated in a given period. -Defaults to 1. Must be an integer. - - - - ----------------- - - -

service

-Manage running services. Service support unfortunately varies -widely by platform -- some platforms have very little if any -concept of a running service, and some have a very codified and -powerful concept. Puppet's service support will generally be able -to make up for any inherent shortcomings (e.g., if there is no -'status' command, then Puppet will look in the process table for a -command matching the service name), but the more information you -can provide the better behaviour you will get. Or, you can just -use a platform that has very good service support. - - -### Service Parameters -#### binary -The path to the daemon. This is only used for -systems that do not support init scripts. This binary will be -used to start the service if no ``start`` parameter is -provided. - -#### enable -Whether a service should be enabled to start at boot. -This state behaves quite differently depending on the platform; -wherever possible, it relies on local tools to enable or disable -a given service. *true*/*false*/*runlevels* Valid values are ``true``, ``false``. - -#### ensure -Whether a service should be running. **true**/*false* Valid values are ``running`` (also called ``true``), ``stopped`` (also called ``false``). - -#### hasrestart -Specify that an init script has a ``restart`` option. Otherwise, -the init script's ``stop`` and ``start`` methods are used. Valid values are ``true``, ``false``. - -#### hasstatus -Declare the the service's init script has a -functional status command. Based on testing, it was found -that a large number of init scripts on different platforms do -not support any kind of status command; thus, you must specify -manually whether the service you are running has such a -command (or you can specify a specific command using the -``status`` parameter). - -If you do not specify anything, then the service name will be -looked for in the process table. - -#### name (*namevar*) -The name of the service to run. This name -is used to find the service in whatever service subsystem it -is in. - -#### path -The search path for finding init scripts. - -#### pattern -The pattern to search for in the process table. -This is used for stopping services on platforms that do not -support init scripts, and is also used for determining service -status on those service whose init scripts do not include a status -command. - -If this is left unspecified and is needed to check the status -of a service, then the service name will be used instead. - -The pattern can be a simple string or any legal Ruby pattern. - -#### provider -The specific backend for provider to use. You will -seldom need to specify this -- Puppet will usually discover the -appropriate provider for your platform. Available providers are: - -* **base**: The simplest form of service support. You have to specify - enough about your service for this to work; the minimum you can specify - is a binary for starting the process, and this same binary will be searched - for in the process table to stop the service. It is preferable to - specify start, stop, and status commands, akin to how you would do - so using ``init``. Required binaries: ``kill``. -* **debian**: Debian's form of ``init``-style management. The only difference - is that this supports service enabling and disabling via ``update-rc.d``. Default for ``operatingsystem`` == ``debian``. Required binaries: ``/usr/sbin/update-rc.d``. -* **gentoo**: Gentoo's form of ``init``-style service - management; uses ``rc-update`` for service enabling and disabling. Default for ``operatingsystem`` == ``gentoo``. Required binaries: ``/sbin/rc-update``. -* **init**: Standard init service management. This provider assumes that the - init script has not ``status`` command, because so few scripts do, - so you need to either provide a status command or specify via ``hasstatus`` - that one already exists in the init script. -* **redhat**: Red Hat's (and probably many others) form of ``init``-style service - management; uses ``chkconfig`` for service enabling and disabling. Default for ``operatingsystem`` == ``redhatfedorasuse``. Required binaries: ``/sbin/chkconfig``. -* **smf**: Support for Sun's new Service Management Framework. Starting a service - is effectively equivalent to enabling it, so there is only support - for starting and stopping services, which also enables and disables them, - respectively. Default for ``operatingsystem`` == ``solaris``. Required binaries: ``/usr/bin/svcs``, ``/usr/sbin/svcadm``. - -#### restart -Specify a *restart* command manually. If left -unspecified, the service will be stopped and then started. - -#### running -A place-holder parameter that wraps ``ensure``, because -``running`` is deprecated. You should use ``ensure`` instead -of this, but using this will still work, albeit with a -warning. - -#### start -Specify a *start* command manually. Most service subsystems -support a ``start`` command, so this will not need to be -specified. - -#### status -Specify a *status* command manually. If left -unspecified, the status method will be determined -automatically, usually by looking for the service in the -process table. - -#### stop -Specify a *stop* command manually. - -#### type -Deprecated form of ``provder``. - - - - ----------------- - - -

sshkey

-Installs and manages ssh host keys. At this point, this type -only knows how to install keys into /etc/ssh/ssh_known_hosts, and -it cannot manage user authorized keys yet. - - -### Sshkey Parameters -#### alias -Any alias the host might have. Multiple values must be -specified as an array. Note that this state has the same name -as one of the metaparams; using this state to set aliases will -make those aliases available in your Puppet scripts. - -#### ensure -The basic state that the object should be in. Valid values are ``absent``, ``present``. - -#### key -The key itself; generally a long string of hex digits. - -#### name (*namevar*) -The host name. - -#### provider -The specific backend for provider to use. You will -seldom need to specify this -- Puppet will usually discover the -appropriate provider for your platform. Available providers are: - -* **parsed**: - -#### target -The file in which to store the mount table. Only used by -those providers that write to disk (i.e., not NetInfo). - -#### type -The encryption type used. Probably ssh-dss or ssh-rsa. Valid values are ``ssh-dss`` (also called ``dsa``), ``ssh-rsa`` (also called ``rsa``). - - - - ----------------- - - -

tidy

-Remove unwanted files based on specific criteria. Multiple -criteria are OR'd together, so a file that is too large but is not -old enough will still get tidied. - - -### Tidy Parameters -#### age -Tidy files whose age is equal to or greater than -the specified time. You can choose seconds, minutes, -hours, days, or weeks by specifying the first letter of any -of those words (e.g., '1w'). - -#### backup -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. 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. - -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 ``puppetmasterd`` 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. - -#### path (*namevar*) -The path to the file or directory to manage. Must be fully -qualified. - -#### recurse -If target is a directory, recursively descend -into the directory looking for files to tidy. - -#### rmdirs -Tidy directories in addition to files; that is, remove -directories whose age is older than the specified criteria. -This will only remove empty directories, so all contained -files must also be tidied before a directory gets removed. - -#### size -Tidy files whose size is equal to or greater than -the specified size. Unqualified values are in kilobytes, but -*b*, *k*, and *m* can be appended to specify *bytes*, *kilobytes*, -and *megabytes*, respectively. Only the first character is -significant, so the full word can also be used. - -#### type -Set the mechanism for determining age. Valid values are ``atime``, ``mtime``, ``ctime``. - - - - ----------------- - - -

user

-Manage users. Currently can create and modify users, but -cannot delete them. Theoretically all of the parameters are -optional, but if no parameters are specified the comment will -be set to the user name in order to make the internals work out -correctly. - -This element 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. - -For most platforms, the tools used are ``useradd`` and its ilk; -for Mac OS X, NetInfo is used. This is currently unconfigurable, -but if you desperately need it to be so, please contact us. - - -### User Parameters -#### allowdupe -Whether to allow duplicate UIDs. Valid values are ``true``, ``false``. - -#### comment -A description of the user. Generally is a user's full name. - -#### ensure -The basic state that the object should be in. Valid values are ``absent``, ``present``. - -#### gid -The user's primary group. Can be specified numerically or -by name. - -#### groups -The groups of which the user is a member. The primary -group should not be listed. Multiple groups should be -specified as an array. - -#### home -The home directory of the user. The directory must be created -separately and is not currently checked for existence. - -#### membership -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. Valid values are ``inclusive``, ``minimum``. - -#### name (*namevar*) -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. - -#### provider -The specific backend for provider to use. You will -seldom need to specify this -- Puppet will usually discover the -appropriate provider for your platform. Available providers are: - -* **netinfo**: User management in NetInfo. Default for ``operatingsystem`` == ``darwin``. Required binaries: ``nireport``, ``niutil``. -* **pw**: User management via ``pw`` on FreeBSD. Default for ``operatingsystem`` == ``freebsd``. Required binaries: ``pw``. -* **useradd**: User management via ``useradd`` and its ilk. Required binaries: ``useradd``, ``usermod``, ``userdel``. - -#### shell -The user's login shell. The shell must exist and be -executable. - -#### uid -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. - - - - ----------------- - - -

yumrepo

-The client-side description of a yum repository. Repository -configurations are found by parsing /etc/yum.conf and -the files indicated by reposdir in that file (see yum.conf(5) -for details) - -Most parameters are identical to the ones documented -in yum.conf(5) - -Continuation lines that yum supports for example for the -baseurl are not supported. No attempt is made to access -files included with the **include** directive - - -### Yumrepo Parameters -#### baseurl -The URL for this repository. -Set this to 'absent' to remove it from the file completely Valid values are ``absent``. Values can also match ``(?-mix:.*)``. - -#### descr -A human readable description of the repository. -Set this to 'absent' to remove it from the file completely Valid values are ``absent``. Values can also match ``(?-mix:.*)``. - -#### enabled -Whether this repository is enabled or disabled. Possible -values are '0', and '1'. -Set this to 'absent' to remove it from the file completely Valid values are ``absent``. Values can also match ``(?-mix:(0|1))``. - -#### enablegroups -Determines whether yum will allow the use of -package groups for this repository. Possible -values are '0', and '1'. -Set this to 'absent' to remove it from the file completely Valid values are ``absent``. Values can also match ``(?-mix:(0|1))``. - -#### exclude -List of shell globs. Matching packages will never be -considered in updates or installs for this repo. -Set this to 'absent' to remove it from the file completely Valid values are ``absent``. Values can also match ``(?-mix:.*)``. - -#### failovermethod -Either 'roundrobin' or 'priority'. -Set this to 'absent' to remove it from the file completely Valid values are ``absent``. Values can also match ``(?-mix:roundrobin|priority)``. - -#### gpgcheck -Whether to check the GPG signature on packages installed -from this repository. Possible values are '0', and '1'. - -Set this to 'absent' to remove it from the file completely Valid values are ``absent``. Values can also match ``(?-mix:(0|1))``. - -#### gpgkey -The URL for the GPG key with which packages from this -repository are signed. -Set this to 'absent' to remove it from the file completely Valid values are ``absent``. Values can also match ``(?-mix:.*)``. - -#### include -A URL from which to include the config. -Set this to 'absent' to remove it from the file completely Valid values are ``absent``. Values can also match ``(?-mix:.*)``. - -#### includepkgs -List of shell globs. If this is set, only packages -matching one of the globs will be considered for -update or install. -Set this to 'absent' to remove it from the file completely Valid values are ``absent``. Values can also match ``(?-mix:.*)``. - -#### keepalive -Either '1' or '0'. This tells yum whether or not HTTP/1.1 -keepalive should be used with this repository. -Set this to 'absent' to remove it from the file completely Valid values are ``absent``. Values can also match ``(?-mix:(0|1))``. - -#### metadata_expire -Number of seconds after which the metadata will expire. -Set this to 'absent' to remove it from the file completely Valid values are ``absent``. Values can also match ``(?-mix:[0-9]+)``. - -#### mirrorlist -The URL that holds the list of mirrors for this repository. -Set this to 'absent' to remove it from the file completely Valid values are ``absent``. Values can also match ``(?-mix:.*)``. - -#### name (*namevar*) -The name of the repository. - -#### timeout -Number of seconds to wait for a connection before timing -out. -Set this to 'absent' to remove it from the file completely Valid values are ``absent``. Values can also match ``(?-mix:[0-9]+)``. - - - - ----------------- - - -

zone

-Solaris zones. - - -### Zone Parameters -#### autoboot -Whether the zone should automatically boot. Valid values are ``true``, ``false``. - -#### ensure -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. Valid values are ``absent``, ``configured``, ``installed``, ``running``. - -#### id -The numerical ID of the zone. This number is autogenerated -and cannot be changed. - -#### inherit -The list of directories that the zone inherits from the global -zone. All directories must be fully qualified. - -#### ip -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. - -#### name (*namevar*) -The name of the zone. - -#### path -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. - -#### pool -The resource pool for this zone. - -#### provider -The specific backend for provider to use. You will -seldom need to specify this -- Puppet will usually discover the -appropriate provider for your platform. Available providers are: - -* **solaris**: Provider for Solaris Zones. Default for ``operatingsystem`` == ``solaris``. Required binaries: ``/usr/sbin/zoneadm``, ``/usr/sbin/zonecfg``. - -#### realhostname -The actual hostname of the zone. - -#### shares -Number of FSS CPU shares allocated to the zone. - -#### sysidcfg -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. - - - - - ----------------- - - -*This page autogenerated on Fri Jan 26 16:40:46 CST 2007* diff --git a/lib/puppet.rb b/lib/puppet.rb index b11f81d49..b9a09bb49 100644 --- a/lib/puppet.rb +++ b/lib/puppet.rb @@ -1,431 +1,431 @@ # Try to load rubygems. Hey rubygems, I hate you. begin require 'rubygems' rescue LoadError end # see the bottom of the file for further inclusions require 'singleton' require 'facter' require 'puppet/error' require 'puppet/external/event-loop' require 'puppet/util' require 'puppet/util/log' require 'puppet/util/autoload' require 'puppet/util/config' require 'puppet/util/feature' require 'puppet/util/suidmanager' #------------------------------------------------------------ # the top-level module # # all this really does is dictate how the whole system behaves, through # preferences for things like debugging # # it's also a place to find top-level commands like 'debug' module Puppet PUPPETVERSION = '0.23.2' def Puppet.version return PUPPETVERSION end class << self # So we can monitor signals and such. include SignalObserver include Puppet::Util # To keep a copy of arguments. Set within Config#addargs, because I'm # lazy. attr_accessor :args attr_reader :features attr_writer :name end # the hash that determines how our system behaves @@config = Puppet::Util::Config.new # The services running in this process. @services ||= [] # define helper messages for each of the message levels Puppet::Util::Log.eachlevel { |level| define_method(level,proc { |args| if args.is_a?(Array) args = args.join(" ") end Puppet::Util::Log.create( :level => level, :message => args ) }) module_function level } # I keep wanting to use Puppet.error # XXX this isn't actually working right now alias :error :err # The feature collection @features = Puppet::Util::Feature.new('puppet/feature') # Load the base features. require 'puppet/feature/base' # Store a new default value. def self.setdefaults(section, hash) @@config.setdefaults(section, hash) end # configuration parameter access and stuff def self.[](param) case param when :debug: if Puppet::Util::Log.level == :debug return true else return false end else return @@config[param] end end # configuration parameter access and stuff def self.[]=(param,value) @@config[param] = value end def self.clear @@config.clear end def self.debug=(value) if value Puppet::Util::Log.level=(:debug) else Puppet::Util::Log.level=(:notice) end end def self.config @@config end # Load all of the configuration parameters. - require 'puppet/configuration' + require 'puppet/defaults' def self.genconfig if Puppet[:configprint] != "" val = Puppet[:configprint] if val == "all" hash = {} Puppet.config.each do |name, obj| val = obj.value case val when true, false, "": val = val.inspect end hash[name] = val end hash.sort { |a,b| a[0].to_s <=> b[0].to_s }.each do |name, val| puts "%s = %s" % [name, val] end elsif val =~ /,/ val.split(/\s*,\s*/).sort.each do |v| if Puppet.config.include?(v) puts "%s = %s" % [v, Puppet[v]] else puts "invalid parameter: %s" % v exit(1) end end else val.split(/\s*,\s*/).sort.each do |v| if Puppet.config.include?(v) puts Puppet[val] else puts "invalid parameter: %s" % v exit(1) end end end exit(0) end if Puppet[:genconfig] puts Puppet.config.to_config exit(0) end end def self.genmanifest if Puppet[:genmanifest] puts Puppet.config.to_manifest exit(0) end end # Run all threads to their ends def self.join defined? @threads and @threads.each do |t| t.join end end # Create a new service that we're supposed to run def self.newservice(service) @services ||= [] @services << service end def self.newthread(&block) @threads ||= [] @threads << Thread.new do yield end end def self.newtimer(hash, &block) timer = nil threadlock(:timers) do @timers ||= [] timer = EventLoop::Timer.new(hash) @timers << timer if block_given? observe_signal(timer, :alarm, &block) end end # In case they need it for something else. timer end # Parse the config file for this process. def self.parse_config(oldconfig = nil) # First look for the old configuration file. oldconfig ||= File.join(Puppet[:confdir], Puppet[:name].to_s + ".conf") if FileTest.exists?(oldconfig) and Puppet[:name] != "puppet" Puppet.warning "Individual config files are deprecated; remove %s and use puppet.conf" % oldconfig Puppet.config.old_parse(oldconfig) return end # Now check for the normal config. if Puppet[:config] and File.exists? Puppet[:config] Puppet.debug "Parsing %s" % Puppet[:config] Puppet.config.parse(Puppet[:config]) end end # Relaunch the executable. def self.restart command = $0 + " " + self.args.join(" ") Puppet.notice "Restarting with '%s'" % command Puppet.shutdown(false) Puppet::Util::Log.reopen exec(command) 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 self.settraps [:INT, :TERM].each do |signal| trap(signal) do Puppet.notice "Caught #{signal}; shutting down" Puppet.debug "Signal caught here:" caller.each { |l| Puppet.debug l } Puppet.shutdown end end # Handle restarting. trap(:HUP) do if client = @services.find { |s| s.is_a? Puppet::Network::Client::Master } and client.running? client.restart else Puppet.restart end end # Provide a hook for running clients where appropriate trap(:USR1) do done = 0 Puppet.notice "Caught USR1; triggering client run" @services.find_all { |s| s.is_a? Puppet::Network::Client }.each do |client| if client.respond_to? :running? if client.running? Puppet.info "Ignoring running %s" % client.class else done += 1 begin client.runnow rescue => detail Puppet.err "Could not run client: %s" % detail end end else Puppet.info "Ignoring %s; cannot test whether it is running" % client.class end end unless done > 0 Puppet.notice "No clients were run" end end trap(:USR2) do Puppet::Util::Log.reopen end end # Shutdown our server process, meaning stop all services and all threads. # Optionally, exit. def self.shutdown(leave = true) Puppet.notice "Shutting down" # Unmonitor our timers defined? @timers and @timers.each do |timer| EventLoop.current.ignore_timer timer end # This seems to exit the process, although I can't find where it does # so. Leaving it out doesn't seem to hurt anything. #if EventLoop.current.running? # EventLoop.current.quit #end # Stop our services defined? @services and @services.each do |svc| next unless svc.respond_to?(:shutdown) begin timeout(20) do svc.shutdown end rescue TimeoutError Puppet.err "%s could not shut down within 20 seconds" % svc.class end end # And wait for them all to die, giving a decent amount of time defined? @threads and @threads.each do |thr| begin timeout(20) do thr.join end rescue TimeoutError # Just ignore this, since we can't intelligently provide a warning end end if leave exit(0) end end # Start all of our services and optionally our event loop, which blocks, # waiting for someone, somewhere, to generate events of some kind. def self.start(block = true) # Starting everything in its own thread, fwiw defined? @services and @services.dup.each do |svc| newthread do begin svc.start rescue => detail if Puppet[:trace] puts detail.backtrace end @services.delete svc Puppet.err "Could not start %s: %s" % [svc.class, detail] end end end # We need to give the services a chance to register their timers before # we try to start monitoring them. sleep 0.5 unless @services.length > 0 Puppet.notice "No remaining services; exiting" exit(1) end if defined? @timers and ! @timers.empty? @timers.each do |timer| EventLoop.current.monitor_timer timer end end if block EventLoop.current.run end end # Create the timer that our different objects (uh, mostly the client) # check. def self.timer unless defined? @timer #Puppet.info "Interval is %s" % Puppet[:runinterval] #@timer = EventLoop::Timer.new(:interval => Puppet[:runinterval]) @timer = EventLoop::Timer.new( :interval => Puppet[:runinterval], :tolerance => 1, :start? => true ) EventLoop.current.monitor_timer @timer end @timer 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)) begin Dir.mkdir(File.join(path), mode) rescue Errno::EACCES => detail Puppet.err detail.to_s return false rescue => detail Puppet.err "Could not create %s: %s" % [path, detail.to_s] return false end elsif FileTest.directory?(File.join(path)) next else FileTest.exist?(File.join(path)) raise Puppet::Error, "Cannot create %s: basedir %s is a file" % [dir, File.join(path)] end } return true end end # Create a new type. Just proxy to the Type class. def self.newtype(name, options = {}, &block) Puppet::Type.newtype(name, options, &block) end # Retrieve a type by name. Just proxy to the Type class. def self.type(name) Puppet::Type.type(name) end end require 'puppet/type' require 'puppet/module' require 'puppet/util/storage' require 'puppet/parser/interpreter' if Puppet[:storeconfigs] require 'puppet/rails' end # $Id$ diff --git a/lib/puppet/configuration.rb b/lib/puppet/defaults.rb similarity index 98% rename from lib/puppet/configuration.rb rename to lib/puppet/defaults.rb index a8f6e0c35..78364e786 100644 --- a/lib/puppet/configuration.rb +++ b/lib/puppet/defaults.rb @@ -1,649 +1,654 @@ # The majority of the system configuration parameters are set in this file. module Puppet # If we're running the standalone puppet process as a non-root user, # use basedirs that are in the user's home directory. conf = nil var = nil name = $0.gsub(/.+#{File::SEPARATOR}/,'').sub(/\.rb$/, '') if name != "puppetmasterd" and Puppet::Util::SUIDManager.uid != 0 conf = File.expand_path("~/.puppet") var = File.expand_path("~/.puppet/var") else # Else, use system-wide directories. conf = "/etc/puppet" var = "/var/puppet" end self.setdefaults(:main, :confdir => [conf, "The main Puppet configuration directory. The default for this parameter is calculated based on the user. If the process is runnig as root or the user that ``puppetmasterd`` 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 ``~``."], :vardir => [var, "Where Puppet stores dynamic and growing data. The default for this parameter is calculated specially, like `confdir`_."], :name => [name, "The name of the service, if we are running as one. The default is essentially $0 without the path or ``.rb``."] ) if name == "puppetmasterd" logopts = {:default => "$vardir/log", :mode => 0750, :owner => "$user", :group => "$group", :desc => "The Puppet log directory." } else logopts = ["$vardir/log", "The Puppet log directory."] end setdefaults(:main, :logdir => logopts) # This name hackery is necessary so that the rundir is set reasonably during # unit tests. if Process.uid == 0 and %w{puppetd puppetmasterd}.include?(self.name) rundir = "/var/run/puppet" else rundir = "$vardir/run" end self.setdefaults(:main, :trace => [false, "Whether to print stack traces on some errors"], :autoflush => [false, "Whether log files should always flush to disk."], :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 => 01777, :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)." }, :statefile => { :default => "$statedir/state.yaml", :mode => 0660, :desc => "Where puppetd and puppetmasterd store state associated with the running configuration. In the case of puppetmasterd, this file reflects the state discovered through interacting with clients." }, :ssldir => { :default => "$confdir/ssl", :mode => 0771, :owner => "root", :desc => "Where SSL certificates are kept." }, :rundir => { :default => rundir, :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 puppetd will run as."], :path => {:default => "none", :desc => "The shell search path. Defaults to whatever is inherited from the parent process.", :hook => proc do |value| ENV["PATH"] = value unless value == "none" 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", :hook => proc do |value| if defined? @oldlibdir and $:.include?(@oldlibdir) $:.delete(@oldlibdir) end @oldlibdir = value $: << 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 ``puppetd`` and ``puppetmasterd``." ], :environment => ["", "The environment Puppet is running in. For clients (e.g., ``puppetd``) this determines the environment itself, which is used to find modules and much more. For servers (i.e., ``puppetmasterd``) this provides the default environment for nodes we - know nothing about."] + know nothing about."], + :diff_args => ["", "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."] ) hostname = Facter["hostname"].value domain = Facter["domain"].value if domain and domain != "" fqdn = [hostname, domain].join(".") else fqdn = hostname end Puppet.setdefaults(:ssl, :certname => [fqdn, "The name to use when handling certificates. Defaults to the fully qualified domain name."], :certdir => ["$ssldir/certs", "The certificate directory."], :publickeydir => ["$ssldir/public_keys", "The public key directory."], :privatekeydir => { :default => "$ssldir/private_keys", :mode => 0750, :desc => "The private key directory." }, :privatedir => { :default => "$ssldir/private", :mode => 0750, :desc => "Where the client stores private certificate information." }, :passfile => { :default => "$privatedir/password", :mode => 0640, :desc => "Where puppetd stores the password for its private key. Generally unused." }, :hostcsr => { :default => "$ssldir/csr_$certname.pem", :mode => 0644, :desc => "Where individual hosts store and look for their certificates." }, :hostcert => { :default => "$certdir/$certname.pem", :mode => 0644, :desc => "Where individual hosts store and look for their certificates." }, :hostprivkey => { :default => "$privatekeydir/$certname.pem", :mode => 0600, :desc => "Where individual hosts store and look for their private key." }, :hostpubkey => { :default => "$publickeydir/$certname.pem", :mode => 0644, :desc => "Where individual hosts store and look for their public key." }, :localcacert => { :default => "$certdir/ca.pem", :mode => 0644, :desc => "Where each client stores the CA certificate." } ) setdefaults(:ca, :cadir => { :default => "$ssldir/ca", :owner => "$user", :group => "$group", :mode => 0770, :desc => "The root directory for the certificate authority." }, :cacert => { :default => "$cadir/ca_crt.pem", :owner => "$user", :group => "$group", :mode => 0660, :desc => "The CA certificate." }, :cakey => { :default => "$cadir/ca_key.pem", :owner => "$user", :group => "$group", :mode => 0660, :desc => "The CA private key." }, :capub => { :default => "$cadir/ca_pub.pem", :owner => "$user", :group => "$group", :desc => "The CA public key." }, :cacrl => { :default => "$cadir/ca_crl.pem", :owner => "$user", :group => "$group", :mode => 0664, :desc => "The certificate revocation list (CRL) for the CA. Set this to 'none' if you do not want to use a CRL." }, :caprivatedir => { :default => "$cadir/private", :owner => "$user", :group => "$group", :mode => 0770, :desc => "Where the CA stores private certificate information." }, :csrdir => { :default => "$cadir/requests", :owner => "$user", :group => "$group", :desc => "Where the CA stores certificate requests" }, :signeddir => { :default => "$cadir/signed", :owner => "$user", :group => "$group", :mode => 0770, :desc => "Where the CA stores signed certificates." }, :capass => { :default => "$caprivatedir/ca.pass", :owner => "$user", :group => "$group", :mode => 0660, :desc => "Where the CA stores the password for the private key" }, :serial => { :default => "$cadir/serial", :owner => "$user", :group => "$group", :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 => "$user", :group => "$group", :desc => "A Complete listing of all certificates" } ) # Define the config default. self.setdefaults(self.config[:name], :config => ["$confdir/puppet.conf", "The configuration file for #{Puppet[:name]}."], :pidfile => ["", "The pid file"], :bindaddress => ["", "The address to bind to. Mongrel servers default to 127.0.0.1 and WEBrick defaults to 0.0.0.0."], :servertype => ["webrick", "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."] ) self.setdefaults(:puppetmasterd, :user => ["puppet", "The user puppetmasterd should run as."], :group => ["puppet", "The group puppetmasterd should run as."], :manifestdir => ["$confdir/manifests", "Where puppetmasterd looks for its manifests."], :manifest => ["$manifestdir/site.pp", "The entry-point manifest for puppetmasterd."], :masterlog => { :default => "$logdir/puppetmaster.log", :owner => "$user", :group => "$group", :mode => 0660, :desc => "Where puppetmasterd logs. This is generally not used, since syslog is the default log destination." }, :masterhttplog => { :default => "$logdir/masterhttp.log", :owner => "$user", :group => "$group", :mode => 0660, :create => true, :desc => "Where the puppetmasterd web server logs." }, :masterport => [8140, "Which port puppetmasterd 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' fact 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 => "$user", :group => "$group", :desc => "Where FileBucket files are stored." }, :ca => [true, "Wether the master should function as a certificate authority."], :modulepath => [ "$confdir/modules:/usr/share/puppet/modules", "The search path for modules as a colon-separated list of directories." ], :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.reductivelabs.com``). See the `UsingMongrel`:trac: wiki page 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 the `UsingMongrel`:trac: wiki page for more information."] ) self.setdefaults(:puppetd, :localconfig => { :default => "$statedir/localconfig", :owner => "root", :mode => 0660, :desc => "Where puppetd caches the local configuration. An extension indicating the cache format is added automatically."}, :classfile => { :default => "$statedir/classes.txt", :owner => "root", :mode => 0644, :desc => "The file in which puppetd stores a list of the classes associated with the retrieved configuratiion. 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 puppetd. This is generally not used." }, :httplog => { :default => "$logdir/http.log", :owner => "root", :mode => 0640, :desc => "Where the puppetd 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"], :server => ["puppet", "The server to which server puppetd should connect"], :ignoreschedules => [false, "Boolean; whether puppetd should ignore schedules. This is useful for initial puppetd runs."], :puppetport => [8139, "Which port puppetd listens on."], :noop => [false, "Whether puppetd should be run in noop mode."], :runinterval => [1800, # 30 minutes "How often puppetd applies the client configuration; in seconds."], :listen => [false, "Whether puppetd 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 ``puppetd`` 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."] ) self.setdefaults(:filebucket, :clientbucketdir => { :default => "$vardir/clientbucket", :mode => 0750, :desc => "Where FileBucket files are stored locally." } ) self.setdefaults(:fileserver, :fileserverconfig => ["$confdir/fileserver.conf", "Where the fileserver configuration is stored."] ) self.setdefaults(:reporting, :reports => ["store", "The list of reports to generate. All reports are looked for in puppet/reports/.rb, and multiple report names should be comma-separated (whitespace is okay)." ], :reportdir => {:default => "$vardir/reports", :mode => 0750, :owner => "$user", :group => "$group", :desc => "The directory in which to store reports received from the client. Each client gets a separate subdirectory."} ) self.setdefaults(:puppetd, :puppetdlockfile => [ "$statedir/puppetdlock", "A lock file to temporarily stop puppetd 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." ], :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."] ) self.setdefaults(:puppetd, :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 => ["$server", "The server to which to send transaction reports." ], :report => [false, "Whether to send reports after every transaction." ] ) # Plugin information. self.setdefaults(:main, :pluginpath => ["$vardir/plugins", "Where Puppet should look for plugins. Multiple directories should be colon-separated, like normal PATH variables. As of 0.23.1, this option is deprecated; download your custom libraries to the $libdir instead."], :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", "What files to ignore when pulling down plugins."] ) # Central fact information. self.setdefaults(:main, :factpath => ["$vardir/facts", "Where Puppet should look for facts. Multiple directories should be colon-separated, like normal PATH variables."], :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."] ) self.setdefaults(:tagmail, :tagmap => ["$confdir/tagmail.conf", "The mapping between reporting tags and email addresses."], :sendmail => [%x{which sendmail 2>/dev/null}.chomp, "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."] ) self.setdefaults(:facts, :factstore => ["yaml", "The backend store to use for client facts."] ) self.setdefaults(:yamlfacts, :yamlfactdir => ["$vardir/facts", "The directory in which client facts are stored when the yaml fact store is used."] ) self.setdefaults(:rails, :dblocation => { :default => "$statedir/clientconfigs.sqlite3", :mode => 0660, :owner => "$user", :group => "$group", :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 Client caching. Only used when networked databases are used."], :dbuser => [ "puppet", "The database user for Client caching. Only used when networked databases are used."], :dbpassword => [ "puppet", "The database password for Client caching. Only used when networked databases are used."], :railslog => {:default => "$logdir/rails.log", :mode => 0600, :owner => "$user", :group => "$group", :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(:graphing, :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."] ) 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(:parser, :typecheck => [true, "Whether to validate types during parsing."], :paramcheck => [true, "Whether to validate parameters during parsing."], :node_source => ["none", "Where to look for node configuration information. The default node source, ``none``, just returns a node with its facts filled in, which is required for normal functionality. See the `NodeSourceReference`:trac: for more information."] ) setdefaults(:main, :casesensitive => [false, "Whether matching in case statements and selectors should be case-sensitive. Case insensitivity is handled by downcasing all values before comparison."], :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 `LdapNodes`:trac: 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."], :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(:puppetmasterd, :storeconfigs => [false, "Whether to store each client's configuration. This requires ActiveRecord from Ruby on Rails."] ) # 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." ] ) setdefaults(:main, :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." ] ) setdefaults(:metrics, :rrddir => {:default => "$vardir/rrd", :owner => "$user", :group => "$group", :desc => "The directory where RRD database files are stored. Directories for each reporting host will be created under this directory." }, :rrdgraph => [false, "Whether RRD information should be graphed."], :rrdinterval => ["$runinterval", "How often RRD should expect data. This should match how often the hosts report back to the server."] ) end # $Id$ diff --git a/lib/puppet/dsl.rb b/lib/puppet/dsl.rb index 9c652f082..793578bca 100644 --- a/lib/puppet/dsl.rb +++ b/lib/puppet/dsl.rb @@ -1,371 +1,381 @@ # Just quick mess-around to see what a DSL would look like. # # This is what the executable could look like: ##!/usr/bin/ruby # #require 'puppet' #require 'puppet/dsl' # #Puppet::DSL.import(ARGV[0]) # #bucket = Puppet::TransBucket.new #bucket.type = "top" #bucket.keyword = "class" # #Puppet::DSL.find_all do |name, sub| # sub.included #end.each do |name, sub| # bucket.push sub.export #end # #puts bucket.to_manifest # # And here's what an example config could look like: # ##!/usr/bin/ruby # # # require 'puppet' # require 'puppet/dsl' # # include Puppet::DSL # init() # # aspect :webserver do # file "/tmp/testone", :content => "yaytest" # # exec "testing", :command => "/bin/echo this is a test" # end # # aspect :other, :inherits => :webserver do # file "/tmp/testone", :mode => "755" # end # # acquire :other # # apply module Puppet # Provide the actual commands for acting like a language. module DSL def aspect(name, options = {}, &block) Puppet::Aspect.new(name, options, &block) end def acquire(*names) names.each do |name| if aspect = Puppet::Aspect[name] unless aspect.evaluated? aspect.evaluate end else raise "Could not find aspect %s" % name end end end def apply bucket = export() objects = bucket.to_type master = Puppet::Network::Client.master.new :Master => "whatever" master.objects = objects master.apply end def export objects = Puppet::Aspect.collect do |name, aspect| if aspect.evaluated? aspect.export end end.reject { |a| a.nil? }.flatten.collect do |obj| obj.to_trans end bucket = Puppet::TransBucket.new(objects) bucket.name = "top" return bucket end def init unless Process.uid == 0 Puppet[:confdir] = File.expand_path("~/.puppet") Puppet[:vardir] = File.expand_path("~/.puppet/var") end Puppet[:user] = Process.uid Puppet[:group] = Process.gid Puppet::Util::Log.newdestination(:console) Puppet::Util::Log.level = :info end private end class Aspect Resource = Puppet::Parser::Resource include Puppet::Util include Puppet::DSL extend Puppet::Util extend Enumerable attr_accessor :parent, :name, :evaluated @aspects = {} - # For now, just do some hackery so resources work - @@interp = Puppet::Parser::Interpreter.new :Code => "" - @@scope = Puppet::Parser::Scope.new(:interp => @@interp) - @@objects = Hash.new do |hash, key| hash[key] = {} end # Create an instance method for every type Puppet::Type.loadall Puppet::Type.eachtype do |type| define_method(type.name) do |*args| newresource(type, *args) end end def self.[]=(name, aspect) name = symbolize(name) @aspects[name] = aspect end def self.[](name) name = symbolize(name) # Make sure there's always a main. This can get deleted in testing. if name == :main and ! @aspects[name] new(:main) {} end @aspects[name] end def self.clear @aspects.clear @@objects.clear end def self.delete(name) name = symbolize(name) if @aspects.has_key?(name) @aspects.delete(name) end end def self.each @aspects.each do |name, a| yield name, a end end def child_of?(aspect) unless aspect.is_a?(self.class) obj = self.class[aspect] unless obj raise "Could not find aspect %s" % aspect end aspect = obj end if self.parent if self.parent == aspect return true elsif self.parent.child_of?(aspect) return true else return false end else return false end end def evaluate if self.parent and ! self.parent.evaluated? self.parent.evaluate end unless evaluated? if defined? @block instance_eval(&@block) end @evaluated = true end end def evaluated? if self.evaluated true else false end end def export @resources.dup end def initialize(name, options = {}, &block) name = symbolize(name) @name = name if block @block = block end if pname = options[:inherits] if pname.is_a?(self.class) @parent = pname elsif parent = self.class[pname] @parent = parent else raise "Could not find parent aspect %s" % pname end end @resources = [] self.class[name] = self end def newresource(type, name, params = {}) if self.is_a?(Puppet::Aspect) source = self else source = Puppet::Aspect[:main] end unless obj = @@objects[type][name] obj = Resource.new :title => name, :type => type.name, - :source => source, :scope => @@scope + :source => source, :scope => scope @@objects[type][name] = obj @resources << obj end params.each do |name, value| param = Resource::Param.new( :name => name, :value => value, :source => source ) - obj.set(param) + obj.send(:set_parameter, param) end obj end + def scope + unless defined?(@scope) + @interp = Puppet::Parser::Interpreter.new :Code => "" + # Load the class, so the node object class is available. + require 'puppet/network/handler/node' + @node = Puppet::Node.new(Facter.value(:hostname)) + @node.parameters = Facter.to_hash + @interp = Puppet::Parser::Interpreter.new :Code => "" + @compile = Puppet::Parser::Compile.new(@node, @interp.send(:parser, Puppet[:environment])) + @scope = @compile.topscope + end + @scope + end + def type self.name end end end @aspects = {} class Puppet::DisabledDSL @@subs = {} @name = :DSLClass class << self include Enumerable attr_accessor :included, :name, :objects def each @@subs.each do |name, sub| yield name, sub end end def export bucket = nil if superclass() != Puppet::DSL bucket = superclass.export else bucket = Puppet::TransBucket.new bucket.keyword = "class" bucket.type = self.name end @objects.each do |type, ary| ary.each do |name, obj| if pobj = bucket.find { |sobj| obj.name == sobj.name && obj.type == sobj.type } obj.each do |param, value| pobj[param] = value end else bucket.push obj end end end return bucket end def include(name) if ary = @@subs.find { |n, s| n == name } ary[1].included = true else raise "Could not find class %s" % name end end def inherited(sub) name = sub.to_s.downcase.gsub(/.+::/, '').intern @@subs[name] = sub sub.name = name sub.initvars sub end def initvars #if superclass() == Puppet::DSL @objects = {} #else # @objects = superclass.objects #end end def import(file) text = File.read(file) # If they don't specify a parent class, then specify one # for them. text.gsub!(/^class \S+\s*$/) do |match| "#{match} < Puppet::DSL" end eval(text, binding) end def method_missing(method, *args) if klass = Puppet::Type.type(method) method = method.intern if method.is_a? String @objects[method] ||= {} names = args.shift hash = args.shift names = [names] unless names.is_a? Array names.each do |name| unless obj = @objects[method][name] obj = Puppet::TransObject.new(name, method) @objects[method][name] = obj end hash.each do |param, value| if obj[param] raise "Cannot override %s in %s[%s]" % [param, method, name] else obj[param] = value end end end else raise "No type %s" % method end end end end # $Id$ diff --git a/lib/puppet/error.rb b/lib/puppet/error.rb index 10ccf47be..55fbf2e2f 100644 --- a/lib/puppet/error.rb +++ b/lib/puppet/error.rb @@ -1,54 +1,52 @@ module Puppet # :nodoc: # The base class for all Puppet errors. We want to make it easy to add # line and file information. This probably isn't necessary for all # errors, but... class Error < RuntimeError attr_accessor :line, :file def backtrace if defined? @backtrace return @backtrace else return super end end def initialize(message, line = nil, file = nil) @message = message @line = line if line @file = file if file end def to_s str = nil if self.file and self.line str = "%s at %s:%s" % [@message.to_s, @file, @line] elsif self.line str = "%s at line %s" % [@message.to_s, @line] elsif self.file str = "%s in %s" % [@message.to_s, self.file] else str = @message.to_s end return str end end # An error class for when I don't know what happened. Automatically # prints a stack trace when in debug mode. class DevError < Puppet::Error # XXX This is probably the wrong way to do this, but... def set_backtrace(trace) if Puppet[:trace] puts trace end super end end end - -# $Id$ diff --git a/lib/puppet/fact_stores/yaml.rb b/lib/puppet/fact_stores/yaml.rb index b4b0c0e7c..a4b12a2e5 100644 --- a/lib/puppet/fact_stores/yaml.rb +++ b/lib/puppet/fact_stores/yaml.rb @@ -1,42 +1,40 @@ Puppet::Util::FactStore.newstore(:yaml) do desc "Store client facts as flat files, serialized using YAML." # Get a client's facts. def get(node) file = path(node) return nil unless FileTest.exists?(file) begin facts = YAML::load(File.read(file)) rescue => detail Puppet.err "Could not load facts for %s: %s" % [node, detail] end facts end def initialize Puppet.config.use(:yamlfacts) end # Store the facts to disk. def set(node, facts) File.open(path(node), "w", 0600) do |f| begin f.print YAML::dump(facts) rescue => detail Puppet.err "Could not write facts for %s: %s" % [node, detail] end end nil end private # Return the path to a given node's file. def path(node) File.join(Puppet[:yamlfactdir], node + ".yaml") end end - -# $Id$ diff --git a/lib/puppet/feature/base.rb b/lib/puppet/feature/base.rb index 97200cb39..6368d0931 100644 --- a/lib/puppet/feature/base.rb +++ b/lib/puppet/feature/base.rb @@ -1,23 +1,24 @@ # Created by Luke Kanies on 2006-04-30. # Copyright (c) 2006. All rights reserved. require 'puppet/util/feature' # Add the simple features, all in one file. # We've got LDAP available. Puppet.features.add(:ldap, :libs => ["ldap"]) # We have the Rdoc::Usage library. Puppet.features.add(:usage, :libs => %w{rdoc/ri/ri_paths rdoc/usage}) # We have libshadow, useful for managing passwords. Puppet.features.add(:libshadow, :libs => ["shadow"]) # We're running as root. Puppet.features.add(:root) { require 'puppet/util/suidmanager'; Puppet::Util::SUIDManager.uid == 0 } # We've got mongrel available Puppet.features.add(:mongrel, :libs => %w{rubygems mongrel puppet/network/server/mongrel}) -# $Id$ +# We have lcs diff +Puppet.features.add :diff, :libs => %w{diff/lcs diff/lcs/hunk} diff --git a/lib/puppet/indirector.rb b/lib/puppet/indirector.rb new file mode 100644 index 000000000..0ba538355 --- /dev/null +++ b/lib/puppet/indirector.rb @@ -0,0 +1,76 @@ +# Manage indirections to termini. They are organized in terms of indirections - +# - e.g., configuration, node, file, certificate -- and each indirection has one +# or more terminus types defined. The indirection must have its preferred terminus +# configured via a 'default' in the form of '_terminus'; e.g., +# 'node_terminus = ldap'. +module Puppet::Indirector + # This manages reading in all of our files for us and then retrieving + # loaded instances. We still have to define the 'newX' method, but this + # does all of the rest -- loading, storing, and retrieving by name. + require 'puppet/util/instance_loader' + include Puppet::Util::InstanceLoader + + # Define a new indirection terminus. This method is used by the individual + # termini in their separate files. Again, the autoloader takes care of + # actually loading these files. + def register_terminus(name, options = {}, &block) + genclass(name, :hash => instance_hash(indirection.name), :attributes => options, :block => block) + end + + # Retrieve a terminus class by indirection and name. + def terminus(name) + loaded_instance(name) + end + + # Declare that the including class indirects its methods to + # this terminus. The terminus name must be the name of a Puppet + # default, not the value -- if it's the value, then it gets + # evaluated at parse time, which is before the user has had a chance + # to override it. + def indirects(indirection, options) + @indirection = indirection + @indirect_terminus = options[:to] + + # Set up autoloading of the appropriate termini. + autoload "puppet/indirector/%s" % indirection + end + + # Define methods for each of the HTTP methods. These just point to the + # termini, with consistent error-handling. Each method is called with + # the first argument being the indirection type and the rest of the + # arguments passed directly on to the indirection terminus. There is + # currently no attempt to standardize around what the rest of the arguments + # should allow or include or whatever. + # There is also no attempt to pre-validate that a given indirection supports + # the method in question. We should probably require that indirections + # declare supported methods, and then verify that termini implement all of + # those methods. + [:get, :post, :put, :delete].each do |method_name| + define_method(method_name) do |*args| + begin + terminus.send(method_name, *args) + rescue NoMethodError + raise ArgumentError, "Indirection category %s does not respond to REST method %s" % [indirection, method_name] + end + end + end + + private + + # Create a new terminus instance. + def make_terminus(indirection) + # Load our terminus class. + unless klass = self.class.terminus(indirection, indirection.default) + raise ArgumentError, "Could not find terminus %s for indirection %s" % [indirection.default, indirection] + end + return klass.new + end + + # Return the singleton terminus for this indirection. + def terminus + unless terminus = @termini[indirection.name] + terminus = @termini[indirection.name] = make_terminus(indirection) + end + terminus + end +end diff --git a/lib/puppet/metatype/evaluation.rb b/lib/puppet/metatype/evaluation.rb index 4b8d293c4..b358fefad 100644 --- a/lib/puppet/metatype/evaluation.rb +++ b/lib/puppet/metatype/evaluation.rb @@ -1,156 +1,165 @@ class Puppet::Type # This method is responsible for collecting property changes we always # descend into the children before we evaluate our current properties. # This returns any changes resulting from testing, thus 'collect' rather # than 'each'. def evaluate #Puppet.err "Evaluating %s" % self.path.join(":") unless defined? @evalcount self.err "No evalcount defined on '%s' of type '%s'" % [self.title,self.class] @evalcount = 0 end @evalcount += 1 if p = self.provider and p.respond_to?(:prefetch) p.prefetch end # this only operates on properties, not properties + children # it's important that we call retrieve() on the type instance, # not directly on the property, because it allows the type to override # the method, like pfile does currentvalues = self.retrieve changes = propertychanges(currentvalues).flatten # now record how many changes we've resulted in if changes.length > 0 self.debug "%s change(s)" % [changes.length] end - self.cache(:checked, Time.now) + + # If we're in noop mode, we don't want to store the checked time, + # because it will result in the resource not getting scheduled if + # someone were to run the configuration in non-noop mode. + # We're going to go ahead and record that we checked if there were + # no changes, since it's unlikely it will affect the scheduling. + noop = noop? + if ! noop or (noop && changes.length == 0) + self.cache(:checked, Time.now) + end return changes.flatten end # Flush the provider, if it supports it. This is called by the # transaction. def flush if self.provider and self.provider.respond_to?(:flush) self.provider.flush end 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 '%s'" % [property.name] end ensureis = is[property] if property.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 '%s'" % [property.name] end propis = is[property] unless property.insync?(propis) property.debug("Not in sync: %s vs %s" % [propis.inspect, property.should.inspect]) insync = false #else # property.debug("In sync") end } #self.debug("%s sync status is %s" % [self,insync]) return insync end # retrieve the current value of all contained properties def retrieve return currentpropvalues end # get a hash of the current properties. def currentpropvalues(override_value = nil) # it's important to use the method here, as it follows the order # in which they're defined in the object return properties().inject({}) { | prophash, property| prophash[property] = override_value.nil? ? property.retrieve : override_value prophash } end # Are we running in noop mode? def noop? @noop || Puppet[:noop] end def noop noop? end # Retrieve the changes associated with all of the properties. def propertychanges(currentvalues) # If we are changing the existence of the object, then none of # the other properties matter. changes = [] ensureparam = @parameters[:ensure] if @parameters.include?(:ensure) && !currentvalues.include?(ensureparam) raise Puppet::DevError, "Parameter ensure defined but missing from current values" end if @parameters.include?(:ensure) and ! ensureparam.insync?(currentvalues[ensureparam]) # self.info "ensuring %s from %s" % # [@parameters[:ensure].should, @parameters[:ensure].is] changes << Puppet::PropertyChange.new(ensureparam, currentvalues[ensureparam]) # Else, if the 'ensure' property is correctly absent, then do # nothing elsif @parameters.include?(:ensure) and currentvalues[ensureparam] == :absent # self.info "Object is correctly absent" return [] else # if @parameters.include?(:ensure) # self.info "ensure: Is: %s, Should: %s" % # [@parameters[:ensure].is, @parameters[:ensure].should] # else # self.info "no ensure property" # end changes = properties().find_all { |property| unless currentvalues.include?(property) raise Puppet::DevError, "Property %s does not have a current value", [property.name] end ! property.insync?(currentvalues[property]) }.collect { |property| Puppet::PropertyChange.new(property, currentvalues[property]) } end if Puppet[:debug] and changes.length > 0 self.debug("Changing " + changes.collect { |ch| ch.property.name }.join(",") ) end changes end end # $Id$ diff --git a/lib/puppet/metatype/metaparams.rb b/lib/puppet/metatype/metaparams.rb index 9825efc61..92f12e6cb 100644 --- a/lib/puppet/metatype/metaparams.rb +++ b/lib/puppet/metatype/metaparams.rb @@ -1,417 +1,417 @@ require 'puppet' require 'puppet/type' class Puppet::Type # Add all of the meta parameters. #newmetaparam(:onerror) do # desc "How to handle errors -- roll back innermost # transaction, roll back entire transaction, ignore, etc. Currently # non-functional." #end 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(:check) do desc "Propertys which should have their values retrieved but which should not actually be modified. This is currently used internally, but will eventually be used for querying, so that you could specify that you wanted to check the install state of all packages, and then query the Puppet client daemon to get reports on all packages." munge do |args| # If they've specified all, collect all known properties if args == :all args = @resource.class.properties.find_all do |property| # Only get properties supported by our provider if @resource.provider @resource.provider.class.supports_parameter?(property) else true end end.collect do |property| property.name end end unless args.is_a?(Array) args = [args] end unless defined? @resource self.devfail "No parent for %s, %s?" % [self.class, self.name] end args.each { |property| unless property.is_a?(Symbol) property = property.intern end next if @resource.propertydefined?(property) unless propertyklass = @resource.class.validproperty?(property) if @resource.class.validattr?(property) next else raise Puppet::Error, "%s is not a valid attribute for %s" % [property, self.class.name] end end next unless propertyklass.checkable? @resource.newattr(property) } end end # We've got four relationship metaparameters, so this method is used # to reduce code duplication between them. def store_relationship(param, values) # We need to support values passed in as an array or as a # resource reference. result = [] # 'values' could be an array or a reference. If it's an array, # it could be an array of references or an array of arrays. if values.is_a?(Puppet::Type) result << [values.class.name, values.title] else unless values.is_a?(Array) devfail "Relationships must be resource references" end if values[0].is_a?(String) or values[0].is_a?(Symbol) # we're a type/title array reference values[0] = symbolize(values[0]) result << values else # we're an array of stuff values.each do |value| if value.is_a?(Puppet::Type) result << [value.class.name, value.title] elsif value.is_a?(Array) value[0] = symbolize(value[0]) result << value else devfail "Invalid relationship %s" % value.inspect end end end end if existing = self[param] result = existing + result end result 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 `LanguageTutorial language tutorial`:trac: for more information. " munge do |aliases| unless aliases.is_a?(Array) aliases = [aliases] end @resource.info "Adding aliases %s" % aliases.collect { |a| a.inspect }.join(", ") aliases.each do |other| if obj = @resource.class[other] unless obj == @resource self.fail( "%s can not create alias %s: object already exists" % [@resource.title, other] ) end next end @resource.class.alias(other, @resource) 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:: puppetd --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(rels) @resource.store_relationship(self.class.name, rels) 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 |value| # we just have a name and a type, and we need to convert it # to an object... tname, name = value object = nil if type = Puppet::Type.type(tname) object = type[name] else # try to treat it as a component object = Puppet::Type::Component["#{tname}[#{name}]"] end # Either of the two retrieval attempts could have returned # nil. unless object - self.fail "Could not retrieve dependency '%s[%s]'" % - [tname.to_s.capitalize, name] + self.fail "Could not retrieve dependency '%s[%s]' of %s" % + [tname.to_s.capitalize, @resource.ref, name] end # Are we requiring them, or vice versa? See the method docs # for futher info on this. if self.class.direction == :in source = object target = @resource else source = @resource target = object end if method = self.class.callback subargs = { :event => self.class.events, :callback => method } self.debug("subscribes to %s" % [object.ref]) else # If there's no callback, there's no point in even adding # a label. subargs = nil self.debug("requires %s" % [object.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: running => true, 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 end # Puppet::Type # $Id$ diff --git a/lib/puppet/metatype/providers.rb b/lib/puppet/metatype/providers.rb index 8210dd382..c302d9928 100644 --- a/lib/puppet/metatype/providers.rb +++ b/lib/puppet/metatype/providers.rb @@ -1,253 +1,254 @@ require 'puppet/provider' require 'puppet/util/provider_features' class Puppet::Type # 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 defined? @defaultprovider and @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.defaultnum }.max defaults = defaults.find_all { |provider| provider.defaultnum == max } retval = nil if defaults.length > 1 Puppet.warning( "Found multiple default providers for %s: %s; using %s" % [self.name, defaults.collect { |i| i.name.to_s }.join(", "), defaults[0].name] ) retval = defaults.shift elsif defaults.length == 1 retval = defaults.shift else raise Puppet::DevError, "Could not find a default provider for %s" % self.name end @defaultprovider = retval end return @defaultprovider end # Convert a hash, as provided by, um, a provider, into an instance of self. def self.hash2obj(hash) obj = nil namevar = self.namevar unless hash.include?(namevar) and hash[namevar] raise Puppet::DevError, "Hash was not passed with namevar" end # if the obj already exists with that name... if obj = self[hash[namevar]] # We're assuming here that objects with the same name # are the same object, which *should* be the case, assuming # we've set up our naming stuff correctly everywhere. # Mark found objects as present hash.each { |param, value| if property = obj.property(param) elsif val = obj[param] obj[param] = val else # There is a value on disk, but it should go away obj[param] = :absent end } else # create a new obj, since no existing one seems to # match obj = self.create(namevar => hash[namevar]) # We can't just pass the hash in at object creation time, # because it sets the should value, not the is value. hash.delete(namevar) hash.each { |param, value| obj[param] = value unless obj.add_property_parameter(param) } end return obj 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. unless @providers.has_key?(name) @providerloader.load(name) end return @providers[name] end # Just list all of the providers. def self.providers @providers.keys end def self.validprovider?(name) name = Puppet::Util.symbolize(name) return (@providers.has_key?(name) && @providers[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 = @providers[name] Puppet.debug "Reloading %s %s provider" % [name, self.name] 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 %s of %s" % [pname, name] end end else Puppet::Provider end options[:resource_type] ||= self self.providify provider = genclass(name, :parent => parent, :hash => @providers, :prefix => "Provider", :block => block, :include => feature_module, :extend => feature_module, :attributes => options ) return 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| "* **%s**: %s" % [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 if provider_class.is_a?(Puppet::Provider) provider_class = provider_class.class.name end if provider = @resource.class.provider(provider_class) unless provider.suitable? raise ArgumentError, "Provider '%s' is not functional on this platform" % [provider_class] end else raise ArgumentError, "Invalid %s provider '%s'" % [@resource.class.name, provider_class] end end munge do |provider| provider = provider[0] if provider.is_a? Array if provider.is_a? String provider = provider.intern end @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 @providers.has_key? name rmclass(name, :hash => @providers, :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 + if @providers.empty? + providerloader.loadall + end @providers.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 %s provider of %s" % [name, self.class.name] end end end - -# $Id$ diff --git a/lib/puppet/module.rb b/lib/puppet/module.rb index 87f849d17..924958bbe 100644 --- a/lib/puppet/module.rb +++ b/lib/puppet/module.rb @@ -1,115 +1,127 @@ # Support for modules class Puppet::Module TEMPLATES = "templates" FILES = "files" MANIFESTS = "manifests" # Return an array of paths by splitting the +modulepath+ config # parameter. Only consider paths that are absolute and existing # directories - def self.modulepath - dirs = Puppet[:modulepath].split(":") + def self.modulepath(environment = nil) + dirs = Puppet.config.value(:modulepath, environment).split(":") if ENV["PUPPETLIB"] dirs = ENV["PUPPETLIB"].split(":") + dirs + else end dirs.select do |p| p =~ /^#{File::SEPARATOR}/ && File::directory?(p) end 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(path) - if path =~ %r/^#{File::SEPARATOR}/ + def self.find(modname, environment = nil) + if modname =~ %r/^#{File::SEPARATOR}/ return nil end - modname, rest = path.split(File::SEPARATOR, 2) - return nil if modname.nil? || modname.empty? - - modpath = modulepath.collect { |p| + modpath = modulepath(environment).collect { |p| File::join(p, modname) }.find { |f| File::directory?(f) } return nil unless modpath return self.new(modname, modpath) end # Instance methods # Find the concrete file denoted by +file+. If +file+ is absolute, # return it directly. Otherwise try to find it as a template in a # module. If that fails, return it relative to the +templatedir+ config # param. # In all cases, an absolute path is returned, which does not # necessarily refer to an existing file - def self.find_template(file) - if file =~ /^#{File::SEPARATOR}/ - return file + def self.find_template(template, environment = nil) + if template =~ /^#{File::SEPARATOR}/ + return template end - mod = find(file) + path, file = split_path(template) + + # Because templates don't have an assumed template name, like manifests do, + # we treat templates with no name as being templates in the main template + # directory. + if file.nil? + mod = nil + else + mod = find(path, environment) + end if mod return mod.template(file) else - return File.join(Puppet[:templatedir], file) + return File.join(Puppet.config.value(:templatedir, environment), template) end end # Return a list of manifests (as absolute filenames) that match +pat+ # with the current directory set to +cwd+. If the first component of # +pat+ does not contain any wildcards and is an existing module, return # a list of manifests in that module matching the rest of +pat+ # Otherwise, try to find manifests matching +pat+ relative to +cwd+ - def self.find_manifests(pat, cwd = nil) - cwd ||= Dir.getwd - mod = find(pat) + def self.find_manifests(start, options = {}) + cwd = options[:cwd] || Dir.getwd + path, pat = split_path(start) + mod = find(path, options[:environment]) if mod return mod.manifests(pat) else - abspat = File::expand_path(pat, cwd) + abspat = File::expand_path(start, cwd) files = Dir.glob(abspat).reject { |f| FileTest.directory?(f) } if files.size == 0 files = Dir.glob(abspat + ".pp").reject { |f| FileTest.directory?(f) } end return files end end + # Split the path into the module and the rest of the path. + def self.split_path(path) + if path =~ %r/^#{File::SEPARATOR}/ + return nil + end + + modname, rest = path.split(File::SEPARATOR, 2) + return nil if modname.nil? || modname.empty? + return modname, rest + end + attr_reader :name, :path def initialize(name, path) @name = name @path = path end - def strip(file) - n, rest = file.split(File::SEPARATOR, 2) - rest = nil if rest && rest.empty? - return rest - end - def template(file) - return File::join(path, TEMPLATES, strip(file)) + return File::join(path, TEMPLATES, file) end def files return File::join(path, FILES) end - def manifests(pat) - rest = strip(pat) + # Return the list of manifests matching the given glob pattern, + # defaulting to 'init.pp' for empty modules. + def manifests(rest) rest ||= "init.pp" p = File::join(path, MANIFESTS, rest) files = Dir.glob(p) if files.size == 0 files = Dir.glob(p + ".pp") end return files end private :initialize end - -# $Id$ diff --git a/lib/puppet/network/client/master.rb b/lib/puppet/network/client/master.rb index 41fd0f7f2..c6d7cd75d 100644 --- a/lib/puppet/network/client/master.rb +++ b/lib/puppet/network/client/master.rb @@ -1,695 +1,696 @@ # The client for interacting with the puppetmaster config server. require 'sync' require 'timeout' class Puppet::Network::Client::Master < Puppet::Network::Client unless defined? @@sync @@sync = Sync.new end attr_accessor :objects attr_reader :compile_time class << self # Puppetd should only have one instance running, and we need a way # to retrieve it. attr_accessor :instance include Puppet::Util end def self.facts # Retrieve the facts from the central server. if Puppet[:factsync] self.getfacts() end down = Puppet[:downcasefacts] facts = {} Facter.each { |name,fact| if down facts[name] = fact.to_s.downcase else facts[name] = fact.to_s end } # Add our client version to the list of facts, so people can use it # in their manifests facts["clientversion"] = Puppet.version.to_s + # And add our environment as a fact. + facts["environment"] = Puppet[:environment] + facts end # Return the list of dynamic facts as an array of symbols def self.dynamic_facts Puppet.config[:dynamicfacts].split(/\s*,\s*/).collect { |fact| fact.downcase } end # This method actually applies the configuration. def apply(tags = nil, ignoreschedules = false) unless defined? @objects raise Puppet::Error, "Cannot apply; objects not defined" end transaction = @objects.evaluate if tags transaction.tags = tags end if ignoreschedules transaction.ignoreschedules = true end transaction.addtimes :config_retrieval => @configtime begin transaction.evaluate rescue Puppet::Error => detail Puppet.err "Could not apply complete configuration: %s" % detail rescue => detail Puppet.err "Got an uncaught exception of type %s: %s" % [detail.class, detail] if Puppet[:trace] puts detail.backtrace end ensure Puppet::Util::Storage.store end if Puppet[:report] or Puppet[:summarize] report(transaction) end return transaction ensure if defined? transaction and transaction transaction.cleanup end end # Cache the config def cache(text) Puppet.info "Caching configuration at %s" % self.cachefile confdir = ::File.dirname(Puppet[:localconfig]) ::File.open(self.cachefile + ".tmp", "w", 0660) { |f| f.print text } ::File.rename(self.cachefile + ".tmp", self.cachefile) end def cachefile unless defined? @cachefile @cachefile = Puppet[:localconfig] + ".yaml" end @cachefile end def clear @objects.remove(true) if @objects Puppet::Type.allclear mkdefault_objects @objects = nil end # Initialize and load storage def dostorage begin Puppet::Util::Storage.load @compile_time ||= Puppet::Util::Storage.cache(:configuration)[:compile_time] rescue => detail if Puppet[:trace] puts detail.backtrace end Puppet.err "Corrupt state file %s: %s" % [Puppet[:statefile], detail] begin ::File.unlink(Puppet[:statefile]) retry rescue => detail raise Puppet::Error.new("Cannot remove %s: %s" % [Puppet[:statefile], detail]) end end end # Check whether our configuration is up to date def fresh?(facts) if Puppet[:ignorecache] Puppet.notice "Ignoring cache" return false end unless self.compile_time Puppet.debug "No cached compile time" return false end if facts_changed?(facts) Puppet.info "Facts have changed; recompiling" unless local? return false end - # We're willing to give a 2 second drift newcompile = @driver.freshness + # We're willing to give a 2 second drift if newcompile - @compile_time.to_i < 1 return true else - Puppet.debug "Server compile time is %s vs %s" % [newcompile, @compile_time] + Puppet.debug "Server compile time is %s vs %s" % [newcompile, @compile_time.to_i] return false end end # Let the daemon run again, freely in the filesystem. Frolick, little # daemon! def enable lockfile.unlock(:anonymous => true) end # Stop the daemon from making any configuration runs. def disable lockfile.lock(:anonymous => true) end # Retrieve the config from a remote server. If this fails, then # use the cached copy. def getconfig dostorage() facts = nil Puppet::Util.benchmark(:debug, "Retrieved facts") do facts = self.class.facts end if self.objects or FileTest.exists?(self.cachefile) if self.fresh?(facts) Puppet.info "Config is up to date" if self.objects return end if oldtext = self.retrievecache begin @objects = YAML.load(oldtext).to_type rescue => detail Puppet.warning "Could not load cached configuration: %s" % detail end return end end end Puppet.debug("getting config") # Retrieve the plugins. if Puppet[:pluginsync] getplugins() end unless facts.length > 0 raise Puppet::Network::ClientError.new( "Could not retrieve any facts" ) end unless objects = get_actual_config(facts) @objects = nil return end unless objects.is_a?(Puppet::TransBucket) raise NetworkClientError, "Invalid returned objects of type %s" % objects.class end self.setclasses(objects.classes) # Clear all existing objects, so we can recreate our stack. if self.objects clear() end # Now convert the objects to real Puppet objects @objects = objects.to_type if @objects.nil? raise Puppet::Error, "Configuration could not be processed" end # and perform any necessary final actions before we evaluate. @objects.finalize return @objects end # A simple proxy method, so it's easy to test. def getplugins self.class.getplugins end # Just so we can specify that we are "the" instance. def initialize(*args) Puppet.config.use(:main, :ssl, :puppetd) super # This might be nil @configtime = 0 self.class.instance = self @running = false mkdefault_objects end # Make the default objects necessary for function. def mkdefault_objects # First create the default scheduling objects Puppet::Type.type(:schedule).mkdefaultschedules # And filebuckets Puppet::Type.type(:filebucket).mkdefaultbucket end # Mark that we should restart. The Puppet module checks whether we're running, # so this only gets called if we're in the middle of a run. def restart # If we're currently running, then just mark for later Puppet.notice "Received signal to restart; waiting until run is complete" @restart = true end # Should we restart? def restart? if defined? @restart @restart else false end end # Retrieve the cached config def retrievecache if FileTest.exists?(self.cachefile) return ::File.read(self.cachefile) else return nil end end # The code that actually runs the configuration. def run(tags = nil, ignoreschedules = false) got_lock = false splay Puppet::Util.sync(:puppetrun).synchronize(Sync::EX) do if !lockfile.lock Puppet.notice "Lock file %s exists; skipping configuration run" % lockfile.lockfile else got_lock = true begin @configtime = thinmark do self.getconfig end rescue => detail Puppet.err "Could not retrieve configuration: %s" % detail end if defined? @objects and @objects unless @local Puppet.notice "Starting configuration run" end benchmark(:notice, "Finished configuration run") do self.apply(tags, ignoreschedules) end end end lockfile.unlock # Did we get HUPped during the run? If so, then restart now that we're # done with the run. if self.restart? Process.kill(:HUP, $$) end end ensure # Just make sure we remove the lock file if we set it. lockfile.unlock if got_lock and lockfile.locked? clear() end def running? lockfile.locked? end # Store the classes in the classfile, but only if we're not local. def setclasses(ary) if @local return end unless ary and ary.length > 0 Puppet.info "No classes to store" return end begin ::File.open(Puppet[:classfile], "w") { |f| f.puts ary.join("\n") } rescue => detail Puppet.err "Could not create class file %s: %s" % [Puppet[:classfile], detail] end end private # Download files from the remote server, returning a list of all # changed files. def self.download(args) objects = Puppet::Type.type(:component).create( :name => "#{args[:name]}_collector" ) hash = { :path => args[:dest], :recurse => true, :source => args[:source], :tag => "#{args[:name]}s", :owner => Process.uid, :group => Process.gid, :purge => true, :backup => false } if args[:ignore] hash[:ignore] = args[:ignore].split(/\s+/) end objects.push Puppet::Type.type(:file).create(hash) Puppet.info "Retrieving #{args[:name]}s" noop = Puppet[:noop] Puppet[:noop] = false begin trans = objects.evaluate trans.ignoretags = true Timeout::timeout(self.timeout) do trans.evaluate end rescue Puppet::Error, Timeout::Error => detail if Puppet[:debug] puts detail.backtrace end Puppet.err "Could not retrieve #{args[:name]}s: %s" % detail end # Now source all of the changed objects, but only source those # that are top-level. files = [] trans.changed?.find_all do |object| yield object if block_given? files << object[:path] end trans.cleanup # Now clean up after ourselves objects.remove files ensure # I can't imagine why this is necessary, but apparently at last one person has had problems with noop # being nil here. if noop.nil? Puppet[:noop] = false else Puppet[:noop] = noop end end # Retrieve facts from the central server. def self.getfacts # Clear all existing definitions. Facter.clear # Download the new facts path = Puppet[:factpath].split(":") files = [] download(:dest => Puppet[:factdest], :source => Puppet[:factsource], :ignore => Puppet[:factsignore], :name => "fact") do |object| next unless path.include?(::File.dirname(object[:path])) files << object[:path] end ensure # Reload everything. if Facter.respond_to? :loadfacts Facter.loadfacts elsif Facter.respond_to? :load Facter.load else raise Puppet::Error, "You must upgrade your version of Facter to use centralized facts" end # This loads all existing facts and any new ones. We have to remove and # reload because there's no way to unload specific facts. loadfacts() end # Retrieve the plugins from the central server. We only have to load the # changed plugins, because Puppet::Type loads plugins on demand. def self.getplugins download(:dest => Puppet[:plugindest], :source => Puppet[:pluginsource], :ignore => Puppet[:pluginsignore], :name => "plugin") do |object| next if FileTest.directory?(object[:path]) path = object[:path].sub(Puppet[:plugindest], '').sub(/^\/+/, '') unless Puppet::Util::Autoload.loaded?(path) next end begin Puppet.info "Reloading downloaded file %s" % path load object[:path] rescue => detail Puppet.warning "Could not reload downloaded file %s: %s" % [object[:path], detail] end end end def self.loaddir(dir, type) return unless FileTest.directory?(dir) Dir.entries(dir).find_all { |e| e =~ /\.rb$/ }.each do |file| fqfile = ::File.join(dir, file) begin Puppet.info "Loading #{type} %s" % ::File.basename(file.sub(".rb",'')) Timeout::timeout(self.timeout) do load fqfile end rescue => detail Puppet.warning "Could not load #{type} %s: %s" % [fqfile, detail] end end end def self.loadfacts Puppet[:factpath].split(":").each do |dir| loaddir(dir, "fact") end end 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 return timeout end # Send off the transaction report. def report(transaction) begin report = transaction.generate_report() rescue => detail Puppet.err "Could not generate report: %s" % detail return end if Puppet[:rrdgraph] == true report.graph() end if Puppet[:summarize] puts report.summary end if Puppet[:report] begin reportclient().report(report) rescue => detail Puppet.err "Reporting failed: %s" % detail end end end def reportclient unless defined? @reportclient @reportclient = Puppet::Network::Client.report.new( :Server => Puppet[:reportserver] ) end @reportclient end loadfacts() # Have the facts changed since we last compiled? def facts_changed?(facts) oldfacts = (Puppet::Util::Storage.cache(:configuration)[:facts] || {}).dup newfacts = facts.dup self.class.dynamic_facts.each do |fact| [oldfacts, newfacts].each do |facthash| facthash.delete(fact) if facthash.include?(fact) end end if oldfacts == newfacts return false else # unless oldfacts # puts "no old facts" # return true # end # newfacts.keys.each do |k| # unless newfacts[k] == oldfacts[k] # puts "%s: %s vs %s" % [k, newfacts[k], oldfacts[k]] # end # end return true end end # Actually retrieve the configuration, either from the server or from a # local master. def get_actual_config(facts) if @local return get_local_config(facts) else begin Timeout::timeout(self.class.timeout) do return get_remote_config(facts) end rescue Timeout::Error Puppet.err "Configuration retrieval timed out" return nil end end end # Retrieve a configuration from a local master. def get_local_config(facts) # If we're local, we don't have to do any of the conversion # stuff. objects = @driver.getconfig(facts, "yaml") @compile_time = Time.now if objects == "" raise Puppet::Error, "Could not retrieve configuration" end return objects end # Retrieve a config from a remote master. def get_remote_config(facts) textobjects = "" textfacts = CGI.escape(YAML.dump(facts)) benchmark(:debug, "Retrieved configuration") do # error handling for this is done in the network client begin textobjects = @driver.getconfig(textfacts, "yaml") begin textobjects = CGI.unescape(textobjects) rescue => detail raise Puppet::Error, "Could not CGI.unescape configuration" end rescue => detail Puppet.err "Could not retrieve configuration: %s" % detail unless Puppet[:usecacheonfailure] @objects = nil Puppet.warning "Not using cache on failed configuration" return end end end fromcache = false if textobjects == "" unless textobjects = self.retrievecache raise Puppet::Error.new( "Cannot connect to server and there is no cached configuration" ) end Puppet.warning "Could not get config; using cached copy" fromcache = true else @compile_time = Time.now Puppet::Util::Storage.cache(:configuration)[:facts] = facts Puppet::Util::Storage.cache(:configuration)[:compile_time] = @compile_time end begin objects = YAML.load(textobjects) rescue => detail raise Puppet::Error, "Could not understand configuration: %s" % detail.to_s end if @cache and ! fromcache self.cache(textobjects) end return objects end def lockfile unless defined?(@lockfile) @lockfile = Puppet::Util::Pidlock.new(Puppet[:puppetdlockfile]) end @lockfile end # Sleep when splay is enabled; else just return. def splay return unless Puppet[:splay] limit = Integer(Puppet[:splaylimit]) # Pick a splay time and then cache it. unless time = Puppet::Util::Storage.cache(:configuration)[:splay_time] time = rand(limit) Puppet::Util::Storage.cache(:configuration)[:splay_time] = time end Puppet.info "Sleeping for %s seconds (splay is enabled)" % time sleep(time) end end - -# $Id$ diff --git a/lib/puppet/network/handler.rb b/lib/puppet/network/handler.rb index 33343e4fe..c2fbfcba5 100644 --- a/lib/puppet/network/handler.rb +++ b/lib/puppet/network/handler.rb @@ -1,50 +1,54 @@ require 'puppet/util/docs' require 'puppet/util/subclass_loader' module Puppet::Network # The base class for the different handlers. The handlers are each responsible # for separate xmlrpc namespaces. class Handler extend Puppet::Util::Docs # This is so that the handlers can subclass just 'Handler', rather # then having to specify the full class path. Handler = self - attr_accessor :server + attr_accessor :server, :local extend Puppet::Util::SubclassLoader extend Puppet::Util handle_subclasses :handler, "puppet/network/handler" # Return the xmlrpc interface. def self.interface if defined? @interface return @interface else raise Puppet::DevError, "Handler %s has no defined interface" % self end end # Set/Determine whether we're a client- or server-side handler. def self.side(side = nil) if side side = side.intern if side.is_a?(String) unless [:client, :server].include?(side) raise ArgumentError, "Invalid side registration '%s' for %s" % [side, self.name] end @side = side else @side ||= :server return @side end end # Create an empty init method with the same signature. def initialize(hash = {}) end + + def local? + self.local + end end end # $Id$ diff --git a/lib/puppet/network/handler/configuration.rb b/lib/puppet/network/handler/configuration.rb index 7f81879ba..2c72d3d2b 100644 --- a/lib/puppet/network/handler/configuration.rb +++ b/lib/puppet/network/handler/configuration.rb @@ -1,226 +1,220 @@ require 'openssl' require 'puppet' require 'puppet/parser/interpreter' require 'puppet/sslcertificates' require 'xmlrpc/server' require 'yaml' class Puppet::Network::Handler class Configuration < Handler desc "Puppet's configuration compilation interface. Passed a node name - or other key, retrieves information about the node and returns a - compiled configuration." + or other key, retrieves information about the node (using the ``node_source``) + and returns a compiled configuration." include Puppet::Util - attr_accessor :ast, :local - attr_reader :ca + attr_accessor :local @interface = XMLRPC::Service::Interface.new("configuration") { |iface| iface.add_method("string configuration(string)") iface.add_method("string version()") } - # FIXME At some point, this should be autodocumenting. - def addfacts(facts) - # Add our server version to the fact list - facts["serverversion"] = Puppet.version.to_s - - # And then add the server name and IP - {"servername" => "fqdn", - "serverip" => "ipaddress" - }.each do |var, fact| - if obj = Facter[fact] - facts[var] = obj.value - else - Puppet.warning "Could not retrieve fact %s" % fact - end + # Compile a node's configuration. + def configuration(key, client = nil, clientip = nil) + # If we want to use the cert name as our key + if Puppet[:node_name] == 'cert' and client + key = client end - if facts["servername"].nil? - host = Facter.value(:hostname) - if domain = Facter.value(:domain) - facts["servername"] = [host, domain].join(".") - else - facts["servername"] = host - end + # Note that this is reasonable, because either their node source should actually + # know about the node, or they should be using the ``none`` node source, which + # will always return data. + unless node = node_handler.details(key) + raise Puppet::Error, "Could not find node '%s'" % key end + + # Add any external data to the node. + add_node_data(node) + + configuration = compile(node) + + return translate(configuration) end - # Manipulate the client name as appropriate. - def clientname(name, ip, facts) - # Always use the hostname from Facter. - client = facts["hostname"] - clientip = facts["ipaddress"] - if Puppet[:node_name] == 'cert' - if name - client = name - end - if ip - clientip = ip - end + def initialize(options = {}) + if options[:Local] + @local = options[:Local] + else + @local = false end - return client, clientip + # Just store the options, rather than creating the interpreter + # immediately. Mostly, this is so we can create the interpreter + # on-demand, which is easier for testing. + @options = options + + set_server_facts end - # Tell a client whether there's a fresh config for it - def freshness(client = nil, clientip = nil) - if Puppet.features.rails? and Puppet[:storeconfigs] - Puppet::Rails.connect + # Are we running locally, or are our clients networked? + def local? + self.local + end - host = Puppet::Rails::Host.find_or_create_by_name(client) - host.last_freshcheck = Time.now - if clientip and (! host.ip or host.ip == "" or host.ip == "NULL") - host.ip = clientip - end - host.save - end - if defined? @interpreter - return @interpreter.parsedate + # Return the configuration version. + def version(client = nil, clientip = nil) + if client and node = node_handler.details(client) + update_node_check(node) + return interpreter.configuration_version(node) else - return 0 + # Just return something that will always result in a recompile, because + # this is local. + return (Time.now + 1000).to_i end end - def initialize(hash = {}) - args = {} + private - # Allow specification of a code snippet or of a file - if code = hash[:Code] - args[:Code] = code - else - args[:Manifest] = hash[:Manifest] || Puppet[:manifest] + # Add any extra data necessary to the node. + def add_node_data(node) + # Merge in our server-side facts, so they can be used during compilation. + node.fact_merge(@server_facts) + + # Add any specified classes to the node's class list. + if classes = @options[:Classes] + classes.each do |klass| + node.classes << klass + end end + end - if hash[:Local] - @local = hash[:Local] + # Compile the actual configuration. + def compile(node) + # Pick the benchmark level. + if local? + level = :none else - @local = false + level = :notice end - args[:Local] = @local + # Ask the interpreter to compile the configuration. + str = "Compiled configuration for %s" % node.name + if node.environment + str += " in environment %s" % node.environment + end + config = nil + benchmark(level, "Compiled configuration for %s" % node.name) do + begin + config = interpreter.compile(node) + rescue Puppet::Error => detail + if Puppet[:trace] + puts detail.backtrace + end + unless local? + Puppet.err detail.to_s + end + raise XMLRPC::FaultException.new( + 1, detail.to_s + ) + end + end - if hash.include?(:CA) and hash[:CA] - @ca = Puppet::SSLCertificates::CA.new() - else - @ca = nil + return config + end + + # Create our interpreter object. + def create_interpreter(options) + args = {} + + # Allow specification of a code snippet or of a file + if code = options[:Code] + args[:Code] = code + elsif options[:Manifest] + args[:Manifest] = options[:Manifest] end - Puppet.debug("Creating interpreter") + args[:Local] = local? - if hash.include?(:UseNodes) - args[:UseNodes] = hash[:UseNodes] + if options.include?(:UseNodes) + args[:UseNodes] = options[:UseNodes] elsif @local args[:UseNodes] = false end # This is only used by the cfengine module, or if --loadclasses was # specified in +puppet+. - if hash.include?(:Classes) - args[:Classes] = hash[:Classes] + if options.include?(:Classes) + args[:Classes] = options[:Classes] end - @interpreter = Puppet::Parser::Interpreter.new(args) + return Puppet::Parser::Interpreter.new(args) end - def getconfig(facts, format = "marshal", client = nil, clientip = nil) - if @local - # we don't need to do anything, since we should already - # have raw objects - Puppet.debug "Our client is local" - else - Puppet.debug "Our client is remote" - - # XXX this should definitely be done in the protocol, somehow - case format - when "marshal": - Puppet.warning "You should upgrade your client. 'Marshal' will not be supported much longer." - begin - facts = Marshal::load(CGI.unescape(facts)) - rescue => detail - raise XMLRPC::FaultException.new( - 1, "Could not rebuild facts" - ) - end - when "yaml": - begin - facts = YAML.load(CGI.unescape(facts)) - rescue => detail - raise XMLRPC::FaultException.new( - 1, "Could not rebuild facts" - ) - end - else - raise XMLRPC::FaultException.new( - 1, "Unavailable config format %s" % format - ) - end + # Create/return our interpreter. + def interpreter + unless defined?(@interpreter) and @interpreter + @interpreter = create_interpreter(@options) end + @interpreter + end - client, clientip = clientname(client, clientip, facts) + # Create a node handler instance for looking up our nodes. + def node_handler + unless defined?(@node_handler) + @node_handler = Puppet::Network::Handler.handler(:node).create + end + @node_handler + end - # Add any server-side facts to our server. - addfacts(facts) + # Initialize our server fact hash; we add these to each client, and they + # won't change while we're running, so it's safe to cache the values. + def set_server_facts + @server_facts = {} - retobjects = nil + # Add our server version to the fact list + @server_facts["serverversion"] = Puppet.version.to_s - # This is hackish, but there's no "silence" option for benchmarks - # right now - if @local - #begin - retobjects = @interpreter.run(client, facts) - #rescue Puppet::Error => detail - # Puppet.err detail - # raise XMLRPC::FaultException.new( - # 1, detail.to_s - # ) - #rescue => detail - # Puppet.err detail.to_s - # return "" - #end - else - benchmark(:notice, "Compiled configuration for %s" % client) do - begin - retobjects = @interpreter.run(client, facts) - rescue Puppet::Error => detail - Puppet.err detail - raise XMLRPC::FaultException.new( - 1, detail.to_s - ) - rescue => detail - Puppet.err detail.to_s - return "" - end + # And then add the server name and IP + {"servername" => "fqdn", + "serverip" => "ipaddress" + }.each do |var, fact| + if value = Facter.value(fact) + @server_facts[var] = value + else + Puppet.warning "Could not retrieve fact %s" % fact end end - if @local - return retobjects - else - str = nil - case format - when "marshal": - str = Marshal::dump(retobjects) - when "yaml": - str = retobjects.to_yaml(:UseBlock => true) + if @server_facts["servername"].nil? + host = Facter.value(:hostname) + if domain = Facter.value(:domain) + @server_facts["servername"] = [host, domain].join(".") else - raise XMLRPC::FaultException.new( - 1, "Unavailable config format %s" % format - ) + @server_facts["servername"] = host end - return CGI.escape(str) end end - def local? - if defined? @local and @local - return true + # Translate our configuration appropriately for sending back to a client. + def translate(config) + if local? + config else - return false + CGI.escape(config.to_yaml(:UseBlock => true)) + end + end + + # Mark that the node has checked in. FIXME this needs to be moved into + # the Node class, or somewhere that's got abstract backends. + def update_node_check(node) + if Puppet.features.rails? and Puppet[:storeconfigs] + Puppet::Rails.connect + + host = Puppet::Rails::Host.find_or_create_by_name(node.name) + host.last_freshcheck = Time.now + host.save end end end end - -# $Id$ diff --git a/lib/puppet/network/handler/facts.rb b/lib/puppet/network/handler/facts.rb index e0b93f942..4767e8be4 100755 --- a/lib/puppet/network/handler/facts.rb +++ b/lib/puppet/network/handler/facts.rb @@ -1,70 +1,68 @@ require 'yaml' require 'puppet/util/fact_store' class Puppet::Network::Handler # Receive logs from remote hosts. class Facts < Handler desc "An interface for storing and retrieving client facts. Currently only used internally by Puppet." @interface = XMLRPC::Service::Interface.new("facts") { |iface| iface.add_method("void set(string, string)") iface.add_method("string get(string)") iface.add_method("integer store_date(string)") } def initialize(hash = {}) super backend = Puppet[:factstore] unless klass = Puppet::Util::FactStore.store(backend) raise Puppet::Error, "Could not find fact store %s" % backend end @backend = klass.new end # Get the facts from our back end. def get(node) if facts = @backend.get(node) return strip_internal(facts) else return nil end end # Set the facts in the backend. def set(node, facts) @backend.set(node, add_internal(facts)) nil end # Retrieve a client's storage date. def store_date(node) if facts = get(node) facts[:_puppet_timestamp].to_i else nil end end private # Add internal data to the facts for storage. def add_internal(facts) facts = facts.dup facts[:_puppet_timestamp] = Time.now facts end # Strip out that internal data. def strip_internal(facts) facts = facts.dup facts.find_all { |name, value| name.to_s =~ /^_puppet_/ }.each { |name, value| facts.delete(name) } facts end end end - -# $Id$ diff --git a/lib/puppet/network/handler/fileserver.rb b/lib/puppet/network/handler/fileserver.rb index fdf515d6e..a429412d2 100755 --- a/lib/puppet/network/handler/fileserver.rb +++ b/lib/puppet/network/handler/fileserver.rb @@ -1,660 +1,684 @@ require 'puppet' require 'puppet/network/authstore' require 'webrick/httpstatus' require 'cgi' require 'delegate' require 'sync' class Puppet::Network::Handler AuthStoreError = Puppet::AuthStoreError class FileServerError < Puppet::Error; end class FileServer < Handler desc "The interface to Puppet's fileserving abilities." attr_accessor :local CHECKPARAMS = [:mode, :type, :owner, :group, :checksum] # Special filserver module for puppet's module system MODULES = "modules" @interface = XMLRPC::Service::Interface.new("fileserver") { |iface| iface.add_method("string describe(string, string)") iface.add_method("string list(string, string, boolean, array)") iface.add_method("string retrieve(string, string)") } def self.params CHECKPARAMS.dup end # Describe a given file. This returns all of the manageable aspects # of that file. def describe(url, links = :ignore, client = nil, clientip = nil) links = links.intern if links.is_a? String if links == :manage raise Puppet::Network::Handler::FileServerError, "Cannot currently copy links" end mount, path = convert(url, client, clientip) if client mount.debug "Describing %s for %s" % [url, client] end obj = nil unless obj = mount.getfileobject(path, links) return "" end currentvalues = mount.check(obj) desc = [] CHECKPARAMS.each { |check| if value = currentvalues[check] desc << value else if check == "checksum" and currentvalues[:type] == "file" mount.notice "File %s does not have data for %s" % [obj.name, check] end desc << nil end } return desc.join("\t") end # Create a new fileserving module. def initialize(hash = {}) @mounts = {} @files = {} if hash[:Local] @local = hash[:Local] else @local = false end if hash[:Config] == false @noreadconfig = true else @config = Puppet::Util::LoadedFile.new( hash[:Config] || Puppet[:fileserverconfig] ) @noreadconfig = false end if hash.include?(:Mount) @passedconfig = true unless hash[:Mount].is_a?(Hash) raise Puppet::DevError, "Invalid mount hash %s" % hash[:Mount].inspect end hash[:Mount].each { |dir, name| if FileTest.exists?(dir) self.mount(dir, name) end } self.mount(nil, MODULES) else @passedconfig = false readconfig(false) # don't check the file the first time. end end # List a specific directory's contents. def list(url, links = :ignore, recurse = false, ignore = false, client = nil, clientip = nil) mount, path = convert(url, client, clientip) if client mount.debug "Listing %s for %s" % [url, client] end obj = nil unless FileTest.exists?(path) return "" end # We pass two paths here, but reclist internally changes one # of the arguments when called internally. desc = reclist(mount, path, path, recurse, ignore) if desc.length == 0 mount.notice "Got no information on //%s/%s" % [mount, path] return "" end desc.collect { |sub| sub.join("\t") }.join("\n") end def local? self.local end # Mount a new directory with a name. def mount(path, name) if @mounts.include?(name) if @mounts[name] != path raise FileServerError, "%s is already mounted at %s" % [@mounts[name].path, name] else # it's already mounted; no problem return end end # Let the mounts do their own error-checking. @mounts[name] = Mount.new(name, path) @mounts[name].info "Mounted %s" % path return @mounts[name] end # Retrieve a file from the local disk and pass it to the remote # client. def retrieve(url, links = :ignore, client = nil, clientip = nil) links = links.intern if links.is_a? String mount, path = convert(url, client, clientip) if client mount.info "Sending %s to %s" % [url, client] end unless FileTest.exists?(path) return "" end links = links.intern if links.is_a? String if links == :ignore and FileTest.symlink?(path) return "" end str = nil if links == :manage raise Puppet::Error, "Cannot copy links yet." else str = File.read(path) end if @local return str else return CGI.escape(str) end end def umount(name) @mounts.delete(name) if @mounts.include? name end private def authcheck(file, mount, client, clientip) # If we're local, don't bother passing in information. if local? client = nil clientip = nil end unless mount.allowed?(client, clientip) mount.warning "%s cannot access %s" % [client, file] raise Puppet::AuthorizationError, "Cannot access %s" % mount end end def convert(url, client, clientip) readconfig url = URI.unescape(url) mount, stub = splitpath(url, client) authcheck(url, mount, client, clientip) path = nil unless path = mount.subdir(stub, client) mount.notice "Could not find subdirectory %s" % "//%s/%s" % [mount, stub] return "" end return mount, path end # Deal with ignore parameters. def handleignore(children, path, ignore) ignore.each { |ignore| Dir.glob(File.join(path,ignore), File::FNM_DOTMATCH) { |match| children.delete(File.basename(match)) } } return children end + # Return the mount for the Puppet modules; allows file copying from + # the modules. + def modules_mount(module_name, client) + # Find our environment, if we have one. + if node = node_handler.details(client || Facter.value("hostname")) + env = node.environment + else + env = nil + end + + # And use the environment to look up the module. + mod = Puppet::Module::find(module_name, env) + if mod + return @mounts[MODULES].copy(mod.name, mod.files) + else + return nil + end + end + + # Create a node handler instance for looking up our nodes. + def node_handler + unless defined?(@node_handler) + @node_handler = Puppet::Network::Handler.handler(:node).create + end + @node_handler + end + # Read the configuration file. def readconfig(check = true) return if @noreadconfig if check and ! @config.changed? return end newmounts = {} begin File.open(@config.file) { |f| mount = nil count = 1 f.each { |line| case line when /^\s*#/: next # skip comments when /^\s*$/: next # skip blank lines when /\[([-\w]+)\]/: name = $1 if newmounts.include?(name) raise FileServerError, "%s is already mounted at %s" % [newmounts[name], name], count, @config.file end mount = Mount.new(name) newmounts[name] = mount when /^\s*(\w+)\s+(.+)$/: var = $1 value = $2 case var when "path": if mount.name == MODULES Puppet.warning "The '#{MODULES}' module can not have a path. Ignoring attempt to set it" else begin mount.path = value rescue FileServerError => detail Puppet.err "Removing mount %s: %s" % [mount.name, detail] newmounts.delete(mount.name) end end when "allow": value.split(/\s*,\s*/).each { |val| begin mount.info "allowing %s access" % val mount.allow(val) rescue AuthStoreError => detail raise FileServerError.new(detail.to_s, count, @config.file) end } when "deny": value.split(/\s*,\s*/).each { |val| begin mount.info "denying %s access" % val mount.deny(val) rescue AuthStoreError => detail raise FileServerError.new(detail.to_s, count, @config.file) end } else raise FileServerError.new("Invalid argument '%s'" % var, count, @config.file) end else raise FileServerError.new("Invalid line '%s'" % line.chomp, count, @config.file) end count += 1 } } rescue Errno::EACCES => detail Puppet.err "FileServer error: Cannot read %s; cannot serve" % @config #raise Puppet::Error, "Cannot read %s" % @config rescue Errno::ENOENT => detail Puppet.err "FileServer error: '%s' does not exist; cannot serve" % @config #raise Puppet::Error, "%s does not exit" % @config #rescue FileServerError => detail # Puppet.err "FileServer error: %s" % detail end unless newmounts[MODULES] mount = Mount.new(MODULES) mount.allow("*") newmounts[MODULES] = mount end # Verify each of the mounts are valid. # We let the check raise an error, so that it can raise an error # pointing to the specific problem. newmounts.each { |name, mount| unless mount.valid? raise FileServerError, "No path specified for mount %s" % name end } @mounts = newmounts end # Recursively list the directory. FIXME This should be using # puppet objects, not directly listing. def reclist(mount, root, path, recurse, ignore) # Take out the root of the path. name = path.sub(root, '') if name == "" name = "/" end if name == path raise FileServerError, "Could not match %s in %s" % [root, path] end desc = [name] ftype = File.stat(path).ftype desc << ftype if recurse.is_a?(Integer) recurse -= 1 end ary = [desc] if recurse == true or (recurse.is_a?(Integer) and recurse > -1) if ftype == "directory" children = Dir.entries(path) if ignore children = handleignore(children, path, ignore) end children.each { |child| next if child =~ /^\.\.?$/ reclist(mount, root, File.join(path, child), recurse, ignore).each { |cobj| ary << cobj } } end end return ary.reject { |c| c.nil? } end # Split the path into the separate mount point and path. def splitpath(dir, client) # the dir is based on one of the mounts # so first retrieve the mount path mount = nil path = nil if dir =~ %r{/([-\w]+)/?} - tmp = $1 - path = dir.sub(%r{/#{tmp}/?}, '') + # Strip off the mount name. + mount_name, path = dir.sub(%r{^/}, '').split(File::Separator, 2) - mod = Puppet::Module::find(tmp) - if mod - mount = @mounts[MODULES].copy(mod.name, mod.files) - else - unless mount = @mounts[tmp] - raise FileServerError, "Fileserver module '%s' not mounted" % tmp + unless mount = modules_mount(mount_name, client) + unless mount = @mounts[mount_name] + raise FileServerError, "Fileserver module '%s' not mounted" % mount_name end end else raise FileServerError, "Fileserver error: Invalid path '%s'" % dir end if path == "" path = nil - else + elsif path # Remove any double slashes that might have occurred path = URI.unescape(path.gsub(/\/\//, "/")) end return mount, path end def to_s "fileserver" end # A simple class for wrapping mount points. Instances of this class # don't know about the enclosing object; they're mainly just used for # authorization. class Mount < Puppet::Network::AuthStore attr_reader :name @@syncs = {} @@files = {} Puppet::Util.logmethods(self, true) def getfileobject(dir, links) unless FileTest.exists?(dir) self.notice "File source %s does not exist" % dir return nil end return fileobj(dir, links) end # Run 'retrieve' on a file. This gets the actual parameters, so # we can pass them to the client. def check(obj) # Retrieval is enough here, because we don't want to cache # any information in the state file, and we don't want to generate # any state changes or anything. We don't even need to sync # the checksum, because we're always going to hit the disk # directly. # We're now caching file data, using the LoadedFile to check the # disk no more frequently than the :filetimeout. path = obj[:path] sync = sync(path) unless data = @@files[path] data = {} sync.synchronize(Sync::EX) do @@files[path] = data data[:loaded_obj] = Puppet::Util::LoadedFile.new(path) data[:values] = properties(obj) return data[:values] end end changed = nil sync.synchronize(Sync::SH) do changed = data[:loaded_obj].changed? end if changed sync.synchronize(Sync::EX) do data[:values] = properties(obj) return data[:values] end else sync.synchronize(Sync::SH) do return data[:values] end end end # Create a map for a specific client. def clientmap(client) { "h" => client.sub(/\..*$/, ""), "H" => client, "d" => client.sub(/[^.]+\./, "") # domain name } end # Replace % patterns as appropriate. def expand(path, client = nil) # This map should probably be moved into a method. map = nil if client map = clientmap(client) else Puppet.notice "No client; expanding '%s' with local host" % path # Else, use the local information map = localmap() end path.gsub(/%(.)/) do |v| key = $1 if key == "%" "%" else map[key] || v end end end # Do we have any patterns in our path, yo? def expandable? if defined? @expandable @expandable else false end end # Create out object. It must have a name. def initialize(name, path = nil) unless name =~ %r{^[-\w]+$} raise FileServerError, "Invalid name format '%s'" % name end @name = name if path self.path = path else @path = nil end super() end def fileobj(path, links) obj = nil if obj = Puppet.type(:file)[path] # This can only happen in local fileserving, but it's an # important one. It'd be nice if we didn't just set # the check params every time, but I'm not sure it's worth # the effort. obj[:check] = CHECKPARAMS else obj = Puppet.type(:file).create( :name => path, :check => CHECKPARAMS ) end if links == :manage links = :follow end # This, ah, might be completely redundant unless obj[:links] == links obj[:links] = links end return obj end # Cache this manufactured map, since if it's used it's likely # to get used a lot. def localmap unless defined? @@localmap @@localmap = { "h" => Facter.value("hostname"), "H" => [Facter.value("hostname"), Facter.value("domain")].join("."), "d" => Facter.value("domain") } end @@localmap end # Return the path as appropriate, expanding as necessary. def path(client = nil) if expandable? return expand(@path, client) else return @path end end # Set the path. def path=(path) # FIXME: For now, just don't validate paths with replacement # patterns in them. if path =~ /%./ # Mark that we're expandable. @expandable = true else unless FileTest.exists?(path) raise FileServerError, "%s does not exist" % path end unless FileTest.directory?(path) raise FileServerError, "%s is not a directory" % path end unless FileTest.readable?(path) raise FileServerError, "%s is not readable" % path end @expandable = false end @path = path end # Return the current values for the object. def properties(obj) obj.retrieve.inject({}) { |props, ary| props[ary[0].name] = ary[1]; props } end # Retrieve a specific directory relative to a mount point. # If they pass in a client, then expand as necessary. def subdir(dir = nil, client = nil) basedir = self.path(client) dirname = if dir File.join(basedir, dir.split("/").join(File::SEPARATOR)) else basedir end dirname end def sync(path) @@syncs[path] ||= Sync.new @@syncs[path] end def to_s "mount[%s]" % @name end # Verify our configuration is valid. This should really check to # make sure at least someone will be allowed, but, eh. def valid? if name == MODULES return @path.nil? else return ! @path.nil? end end # Return a new mount with the same properties as +self+, except # with a different name and path. def copy(name, path) result = self.clone result.path = path result.instance_variable_set(:@name, name) return result end end end end # $Id$ diff --git a/lib/puppet/network/handler/master.rb b/lib/puppet/network/handler/master.rb index e889c1ba8..c8db277ba 100644 --- a/lib/puppet/network/handler/master.rb +++ b/lib/puppet/network/handler/master.rb @@ -1,226 +1,153 @@ require 'openssl' require 'puppet' require 'puppet/parser/interpreter' require 'puppet/sslcertificates' require 'xmlrpc/server' require 'yaml' class Puppet::Network::Handler class MasterError < Puppet::Error; end class Master < Handler desc "Puppet's configuration interface. Used for all interactions related to generating client configurations." include Puppet::Util - attr_accessor :ast, :local + attr_accessor :ast attr_reader :ca @interface = XMLRPC::Service::Interface.new("puppetmaster") { |iface| iface.add_method("string getconfig(string)") iface.add_method("int freshness()") } - # FIXME At some point, this should be autodocumenting. - def addfacts(facts) - # Add our server version to the fact list - facts["serverversion"] = Puppet.version.to_s - - # And then add the server name and IP - {"servername" => "fqdn", - "serverip" => "ipaddress" - }.each do |var, fact| - if obj = Facter[fact] - facts[var] = obj.value - else - Puppet.warning "Could not retrieve fact %s" % fact - end - end - - if facts["servername"].nil? - host = Facter.value(:hostname) - if domain = Facter.value(:domain) - facts["servername"] = [host, domain].join(".") - else - facts["servername"] = host - end - end - end - - # Manipulate the client name as appropriate. - def clientname(name, ip, facts) - # Always use the hostname from Facter. - client = facts["hostname"] - clientip = facts["ipaddress"] - if Puppet[:node_name] == 'cert' - if name - client = name - end - if ip - clientip = ip - end - end - - return client, clientip - end - # Tell a client whether there's a fresh config for it def freshness(client = nil, clientip = nil) - if Puppet.features.rails? and Puppet[:storeconfigs] - Puppet::Rails.connect - - host = Puppet::Rails::Host.find_or_create_by_name(client) - host.last_freshcheck = Time.now - if clientip and (! host.ip or host.ip == "" or host.ip == "NULL") - host.ip = clientip - end - host.save - end - if defined? @interpreter - return @interpreter.parsedate - else - return 0 - end + client ||= Facter.value("hostname") + config_handler.version(client, clientip) end def initialize(hash = {}) args = {} # Allow specification of a code snippet or of a file if code = hash[:Code] args[:Code] = code - else - args[:Manifest] = hash[:Manifest] || Puppet[:manifest] + elsif man = hash[:Manifest] + args[:Manifest] = man end if hash[:Local] @local = hash[:Local] else @local = false end - args[:Local] = @local + args[:Local] = true if hash.include?(:CA) and hash[:CA] @ca = Puppet::SSLCertificates::CA.new() else @ca = nil end Puppet.debug("Creating interpreter") if hash.include?(:UseNodes) args[:UseNodes] = hash[:UseNodes] elsif @local args[:UseNodes] = false end # This is only used by the cfengine module, or if --loadclasses was # specified in +puppet+. if hash.include?(:Classes) args[:Classes] = hash[:Classes] end - @interpreter = Puppet::Parser::Interpreter.new(args) + @config_handler = Puppet::Network::Handler.handler(:configuration).new(args) end + # Call our various handlers; this handler is getting deprecated. def getconfig(facts, format = "marshal", client = nil, clientip = nil) - if @local - # we don't need to do anything, since we should already - # have raw objects - Puppet.debug "Our client is local" - else - Puppet.debug "Our client is remote" + facts = decode_facts(facts) + client, clientip = clientname(client, clientip, facts) - # XXX this should definitely be done in the protocol, somehow - case format - when "marshal": - Puppet.warning "You should upgrade your client. 'Marshal' will not be supported much longer." - begin - facts = Marshal::load(CGI.unescape(facts)) - rescue => detail - raise XMLRPC::FaultException.new( - 1, "Could not rebuild facts" - ) - end - when "yaml": - begin - facts = YAML.load(CGI.unescape(facts)) - rescue => detail - raise XMLRPC::FaultException.new( - 1, "Could not rebuild facts" - ) - end - else - raise XMLRPC::FaultException.new( - 1, "Unavailable config format %s" % format - ) - end - end + # Pass the facts to the fact handler + fact_handler.set(client, facts) - client, clientip = clientname(client, clientip, facts) + # And get the configuration from the config handler + begin + config = config_handler.configuration(client) + rescue => detail + puts detail.backtrace + raise + end - # Add any server-side facts to our server. - addfacts(facts) + return translate(config.extract) + end - retobjects = nil + private - # This is hackish, but there's no "silence" option for benchmarks - # right now - if @local - #begin - retobjects = @interpreter.run(client, facts) - #rescue Puppet::Error => detail - # Puppet.err detail - # raise XMLRPC::FaultException.new( - # 1, detail.to_s - # ) - #rescue => detail - # Puppet.err detail.to_s - # return "" - #end - else - benchmark(:notice, "Compiled configuration for %s" % client) do - begin - retobjects = @interpreter.run(client, facts) - rescue Puppet::Error => detail - Puppet.err detail - raise XMLRPC::FaultException.new( - 1, detail.to_s - ) - rescue => detail - Puppet.err detail.to_s - return "" - end + # Manipulate the client name as appropriate. + def clientname(name, ip, facts) + # Always use the hostname from Facter. + client = facts["hostname"] + clientip = facts["ipaddress"] + if Puppet[:node_name] == 'cert' + if name + client = name + end + if ip + clientip = ip end end + return client, clientip + end + + def config_handler + unless defined? @config_handler + @config_handler = Puppet::Network::Handler.handler(:config).new :local => local? + end + @config_handler + end + + # + def decode_facts(facts) if @local - return retobjects + # we don't need to do anything, since we should already + # have raw objects + Puppet.debug "Our client is local" else - str = nil - case format - when "marshal": - str = Marshal::dump(retobjects) - when "yaml": - str = retobjects.to_yaml(:UseBlock => true) - else + Puppet.debug "Our client is remote" + + begin + facts = YAML.load(CGI.unescape(facts)) + rescue => detail raise XMLRPC::FaultException.new( - 1, "Unavailable config format %s" % format + 1, "Could not rebuild facts" ) end - return CGI.escape(str) end + + return facts end - def local? - if defined? @local and @local - return true + def fact_handler + unless defined? @fact_handler + @fact_handler = Puppet::Network::Handler.handler(:facts).new :local => local? + end + @fact_handler + end + + # Translate our configuration appropriately for sending back to a client. + def translate(config) + if local? + config else - return false + CGI.escape(config.to_yaml(:UseBlock => true)) end end end end - -# $Id$ diff --git a/lib/puppet/network/handler/node.rb b/lib/puppet/network/handler/node.rb index 898db7c22..bf2424b18 100644 --- a/lib/puppet/network/handler/node.rb +++ b/lib/puppet/network/handler/node.rb @@ -1,240 +1,242 @@ # Created by Luke A. Kanies on 2007-08-13. # Copyright (c) 2007. All rights reserved. require 'puppet/util' +require 'puppet/node' require 'puppet/util/classgen' require 'puppet/util/instance_loader' # Look up a node, along with all the details about it. class Puppet::Network::Handler::Node < Puppet::Network::Handler - # A simplistic class for managing the node information itself. - class SimpleNode - attr_accessor :name, :classes, :parameters, :environment, :source, :ipaddress - - def initialize(name, options = {}) - @name = name - - if classes = options[:classes] - if classes.is_a?(String) - @classes = [classes] - else - @classes = classes - end - else - @classes = [] - end - - @parameters = options[:parameters] || {} - - unless @environment = options[:environment] - if env = Puppet[:environment] and env != "" - @environment = env - end - end - end - - # Merge the node facts with parameters from the node source. - # This is only called if the node source has 'fact_merge' set to true. - def fact_merge(facts) - facts.each do |name, value| - @parameters[name] = value unless @parameters.include?(name) - end - end - end - desc "Retrieve information about nodes." - extend Puppet::Util::ClassGen - extend Puppet::Util::InstanceLoader - - # A simple base module we can use for modifying how our node sources work. - module SourceBase - include Puppet::Util::Docs + # Create a singleton node handler + def self.create + unless @handler + @handler = new + end + @handler end - @interface = XMLRPC::Service::Interface.new("nodes") { |iface| - iface.add_method("string details(key)") - iface.add_method("string parameters(key)") - iface.add_method("string environment(key)") - iface.add_method("string classes(key)") - } - - # Set up autoloading and retrieving of reports. - autoload :node_source, 'puppet/node_source' - - attr_reader :source - # Add a new node source. def self.newnode_source(name, options = {}, &block) name = symbolize(name) fact_merge = options[:fact_merge] mod = genmodule(name, :extend => SourceBase, :hash => instance_hash(:node_source), :block => block) mod.send(:define_method, :fact_merge?) do fact_merge end mod end # Collect the docs for all of our node sources. def self.node_source_docs docs = "" # Use this method so they all get loaded instance_loader(:node_source).loadall loaded_instances(:node_source).sort { |a,b| a.to_s <=> b.to_s }.each do |name| mod = self.node_source(name) docs += "%s\n%s\n" % [name, "-" * name.to_s.length] docs += Puppet::Util::Docs.scrub(mod.doc) + "\n\n" end docs end # List each of the node sources. def self.node_sources instance_loader(:node_source).loadall loaded_instances(:node_source) end # Remove a defined node source; basically only used for testing. def self.rm_node_source(name) rmclass(name, :hash => instance_hash(:node_source)) end + extend Puppet::Util::ClassGen + extend Puppet::Util::InstanceLoader + + # A simple base module we can use for modifying how our node sources work. + module SourceBase + include Puppet::Util::Docs + end + + @interface = XMLRPC::Service::Interface.new("nodes") { |iface| + iface.add_method("string details(key)") + iface.add_method("string parameters(key)") + iface.add_method("string environment(key)") + iface.add_method("string classes(key)") + } + + # Set up autoloading and retrieving of reports. + autoload :node_source, 'puppet/node_source' + + attr_reader :source + # Return a given node's classes. def classes(key) if node = details(key) node.classes else nil end end # Return an entire node configuration. This uses the 'nodesearch' method # defined in the node_source to look for the node. def details(key, client = nil, clientip = nil) + return nil unless key + if node = cached?(key) + return node + end facts = node_facts(key) node = nil names = node_names(key, facts) names.each do |name| name = name.to_s if name.is_a?(Symbol) if node = nodesearch(name) Puppet.info "Found %s in %s" % [name, @source] break end end # If they made it this far, we haven't found anything, so look for a # default node. unless node or names.include?("default") if node = nodesearch("default") Puppet.notice "Using default node for %s" % key end end if node node.source = @source + node.names = names # Merge the facts into the parameters. if fact_merge? node.fact_merge(facts) end + + cache(node) + return node else return nil end end # Return a given node's environment. def environment(key, client = nil, clientip = nil) if node = details(key) node.environment else nil end end # Create our node lookup tool. def initialize(hash = {}) @source = hash[:Source] || Puppet[:node_source] unless mod = self.class.node_source(@source) raise ArgumentError, "Unknown node source '%s'" % @source end extend(mod) super + + # We cache node info for speed + @node_cache = {} end # Try to retrieve a given node's parameters. def parameters(key, client = nil, clientip = nil) if node = details(key) node.parameters else nil end end private + # Store the node to make things a bit faster. + def cache(node) + @node_cache[node.name] = node + end + + # If the node is cached, return it. + def cached?(name) + # Don't use cache when the filetimeout is set to 0 + return false if [0, "0"].include?(Puppet[:filetimeout]) + + if node = @node_cache[name] and Time.now - node.time < Puppet[:filetimeout] + return node + else + return false + end + end + # Create/cache a fact handler. def fact_handler unless defined?(@fact_handler) @fact_handler = Puppet::Network::Handler.handler(:facts).new end @fact_handler end # Short-hand for creating a new node, so the node sources don't need to # specify the constant. def newnode(options) - SimpleNode.new(options) + Puppet::Node.new(options) end # Look up the node facts from our fact handler. def node_facts(key) if facts = fact_handler.get(key) facts else {} end end # Calculate the list of node names we should use for looking # up our node. def node_names(key, facts = nil) facts ||= node_facts(key) names = [] if hostname = facts["hostname"] unless hostname == key names << hostname end else hostname = key end if fqdn = facts["fqdn"] hostname = fqdn names << fqdn end # Make sure both the fqdn and the short name of the # host can be used in the manifest if hostname =~ /\./ names << hostname.sub(/\..+/,'') elsif domain = facts['domain'] names << hostname + "." + domain end # Sort the names inversely by name length. names.sort! { |a,b| b.length <=> a.length } # And make sure the key is first, since that's the most # likely usage. ([key] + names).uniq end end diff --git a/lib/puppet/network/handler/resource.rb b/lib/puppet/network/handler/resource.rb index ca492bd81..ac29dce53 100755 --- a/lib/puppet/network/handler/resource.rb +++ b/lib/puppet/network/handler/resource.rb @@ -1,202 +1,202 @@ require 'puppet' require 'puppet/network/handler' # Serve Puppet elements. Useful for querying, copying, and, um, other stuff. class Puppet::Network::Handler class Resource < Handler desc "An interface for interacting with client-based resources that can be used for querying or managing remote machines without using Puppet's central server tools. The ``describe`` and ``list`` methods return TransBuckets containing TransObject instances (``describe`` returns a single TransBucket), and the ``apply`` method accepts a TransBucket of TransObjects and applies them locally. " attr_accessor :local @interface = XMLRPC::Service::Interface.new("resource") { |iface| iface.add_method("string apply(string, string)") iface.add_method("string describe(string, string, array, array)") iface.add_method("string list(string, array, string)") } side :client # Apply a TransBucket as a transaction. def apply(bucket, format = "yaml", client = nil, clientip = nil) - unless @local + unless local? begin case format when "yaml": bucket = YAML::load(Base64.decode64(bucket)) else raise Puppet::Error, "Unsupported format '%s'" % format end rescue => detail raise Puppet::Error, "Could not load YAML TransBucket: %s" % detail end end component = bucket.to_type # Create a client, but specify the remote machine as the server # because the class requires it, even though it's unused - client = Puppet::Network::Client.client(:Master).new(:Server => client||"localhost") + client = Puppet::Network::Client.client(:Master).new(:Master => client||"localhost") # Set the objects client.objects = component # And then apply the configuration. This way we're reusing all # the code in there. It should probably just be separated out, though. transaction = client.apply # And then clean up component.remove # It'd be nice to return some kind of report, but... at this point # we have no such facility. return "success" end # Describe a given object. This returns the 'is' values for every property # available on the object type. def describe(type, name, retrieve = nil, ignore = [], format = "yaml", client = nil, clientip = nil) Puppet.info "Describing %s[%s]" % [type.to_s.capitalize, name] @local = true unless client typeklass = nil unless typeklass = Puppet.type(type) raise Puppet::Error, "Puppet type %s is unsupported" % type end obj = nil retrieve ||= :all ignore ||= [] if obj = typeklass[name] obj[:check] = retrieve else begin obj = typeklass.create(:name => name, :check => retrieve) rescue Puppet::Error => detail raise Puppet::Error, "%s[%s] could not be created: %s" % [type, name, detail] end end unless obj raise XMLRPC::FaultException.new( 1, "Could not create %s[%s]" % [type, name] ) end trans = obj.to_trans # Now get rid of any attributes they specifically don't want ignore.each do |st| if trans.include? st trans.delete(st) end end # And get rid of any attributes that are nil trans.each do |attr, value| if value.nil? trans.delete(attr) end end unless @local case format when "yaml": trans = Base64.encode64(YAML::dump(trans)) else raise XMLRPC::FaultException.new( 1, "Unavailable config format %s" % format ) end end return trans end # Create a new fileserving module. def initialize(hash = {}) if hash[:Local] @local = hash[:Local] else @local = false end end # List all of the elements of a given type. def list(type, ignore = [], base = nil, format = "yaml", client = nil, clientip = nil) @local = true unless client typeklass = nil unless typeklass = Puppet.type(type) raise Puppet::Error, "Puppet type %s is unsupported" % type end # They can pass in false ignore ||= [] ignore = [ignore] unless ignore.is_a? Array bucket = Puppet::TransBucket.new bucket.type = typeklass.name typeklass.instances.each do |obj| next if ignore.include? obj.name #object = Puppet::TransObject.new(obj.name, typeklass.name) bucket << obj.to_trans end unless @local case format when "yaml": begin bucket = Base64.encode64(YAML::dump(bucket)) rescue => detail Puppet.err detail raise XMLRPC::FaultException.new( 1, detail.to_s ) end else raise XMLRPC::FaultException.new( 1, "Unavailable config format %s" % format ) end end return bucket end private def authcheck(file, mount, client, clientip) unless mount.allowed?(client, clientip) mount.warning "%s cannot access %s" % [client, file] raise Puppet::AuthorizationError, "Cannot access %s" % mount end end # Deal with ignore parameters. def handleignore(children, path, ignore) ignore.each { |ignore| Dir.glob(File.join(path,ignore), File::FNM_DOTMATCH) { |match| children.delete(File.basename(match)) } } return children end def to_s "resource" end end end # $Id$ diff --git a/lib/puppet/node.rb b/lib/puppet/node.rb new file mode 100644 index 000000000..2d3ac712e --- /dev/null +++ b/lib/puppet/node.rb @@ -0,0 +1,61 @@ +# A simplistic class for managing the node information itself. +class Puppet::Node + attr_accessor :name, :classes, :parameters, :source, :ipaddress, :names + attr_reader :time + attr_writer :environment + + # Do not return environments tha are empty string, and use + # explicitly set environments, then facts, then a central env + # value. + def environment + unless @environment and @environment != "" + if env = parameters["environment"] and env != "" + @environment = env + elsif env = Puppet[:environment] and env != "" + @environment = env + else + @environment = nil + end + end + @environment + end + + def initialize(name, options = {}) + @name = name + + # Provide a default value. + if names = options[:names] + if names.is_a?(String) + @names = [names] + else + @names = names + end + else + @names = [name] + end + + if classes = options[:classes] + if classes.is_a?(String) + @classes = [classes] + else + @classes = classes + end + else + @classes = [] + end + + @parameters = options[:parameters] || {} + + @environment = options[:environment] + + @time = Time.now + end + + # Merge the node facts with parameters from the node source. + # This is only called if the node source has 'fact_merge' set to true. + def fact_merge(facts) + facts.each do |name, value| + @parameters[name] = value unless @parameters.include?(name) + end + end +end diff --git a/lib/puppet/node/configuration.rb b/lib/puppet/node/configuration.rb new file mode 100644 index 000000000..4f93fdbe5 --- /dev/null +++ b/lib/puppet/node/configuration.rb @@ -0,0 +1,123 @@ +require 'puppet/external/gratr/digraph' + +# This class models a node configuration. It is the thing +# meant to be passed from server to client, and it contains all +# of the information in the configuration, including the resources +# and the relationships between them. +class Puppet::Node::Configuration < GRATR::Digraph + attr_accessor :name, :version + attr_reader :extraction_format + + # Add classes to our class list. + def add_class(*classes) + classes.each do |klass| + @classes << klass + end + + # Add the class names as tags, too. + tag(*classes) + end + + def classes + @classes.dup + end + + # Make sure we support the requested extraction format. + def extraction_format=(value) + unless respond_to?("extract_to_%s" % value) + raise ArgumentError, "Invalid extraction format %s" % value + end + @extraction_format = value + end + + # Turn our configuration graph into whatever the client is expecting. + def extract + send("extract_to_%s" % extraction_format) + end + + # Create the traditional TransBuckets and TransObjects from our configuration + # graph. This will hopefully be deprecated soon. + def extract_to_transportable + top = nil + current = nil + buckets = {} + + unless main = vertices.find { |res| res.type == "class" and res.title == :main } + raise Puppet::DevError, "Could not find 'main' class; cannot generate configuration" + end + + # Create a proc for examining edges, which we'll use to build our tree + # of TransBuckets and TransObjects. + bucket = nil + edges = proc do |edge| + # The sources are always non-builtins. + source, target = edge.source, edge.target + unless tmp = buckets[source.to_s] + if tmp = buckets[source.to_s] = source.to_trans + bucket = tmp + else + # This is because virtual resources return nil. If a virtual + # container resource contains realized resources, we still need to get + # to them. So, we keep a reference to the last valid bucket + # we returned and use that if the container resource is virtual. + end + end + bucket = tmp || bucket + if child = target.to_trans + unless bucket + raise "No bucket created for %s" % source + end + bucket.push child + + # It's important that we keep a reference to any TransBuckets we've created, so + # we don't create multiple buckets for children. + unless target.builtin? + buckets[target.to_s] = child + end + end + end + dfs(:start => main, :examine_edge => edges) + + unless main + raise Puppet::DevError, "Could not find 'main' class; cannot generate configuration" + end + + # Retrive the bucket for the top-level scope and set the appropriate metadata. + unless result = buckets[main.to_s] + raise Puppet::DevError, "Did not evaluate top scope" + end + + result.classes = classes + + # Clear the cache to encourage the GC + buckets.clear + return result + end + + def initialize(name) + super() + @name = name + @extraction_format ||= :transportable + @tags = [] + @classes = [] + end + + # Add a tag. + def tag(*names) + names.each do |name| + name = name.to_s + @tags << name unless @tags.include?(name) + if name.include?("::") + name.split("::").each do |sub| + @tags << sub unless @tags.include?(sub) + end + end + end + nil + end + + # Return the list of tags. + def tags + @tags.dup + end +end diff --git a/lib/puppet/node_source/ldap.rb b/lib/puppet/node_source/ldap.rb index 9332fcb40..7b60a3c62 100644 --- a/lib/puppet/node_source/ldap.rb +++ b/lib/puppet/node_source/ldap.rb @@ -1,118 +1,138 @@ Puppet::Network::Handler::Node.newnode_source(:ldap, :fact_merge => true) do desc "Search in LDAP for node configuration information." # Find the ldap node, return the class list and parent node specially, # and everything else in a parameter hash. def ldapsearch(node) - unless defined? @ldap and @ldap - setup_ldap() - unless @ldap - Puppet.info "Skipping ldap source; no ldap connection" - return nil - end - end - filter = Puppet[:ldapstring] classattrs = Puppet[:ldapclassattrs].split("\s*,\s*") if Puppet[:ldapattrs] == "all" # A nil value here causes all attributes to be returned. search_attrs = nil else search_attrs = classattrs + Puppet[:ldapattrs].split("\s*,\s*") end pattr = nil if pattr = Puppet[:ldapparentattr] if pattr == "" pattr = nil else search_attrs << pattr unless search_attrs.nil? end end if filter =~ /%s/ filter = filter.gsub(/%s/, node) end parent = nil classes = [] parameters = nil found = false count = 0 begin # We're always doing a sub here; oh well. - @ldap.search(Puppet[:ldapbase], 2, filter, search_attrs) do |entry| + ldap.search(Puppet[:ldapbase], 2, filter, search_attrs) do |entry| found = true if pattr if values = entry.vals(pattr) if values.length > 1 raise Puppet::Error, "Node %s has more than one parent: %s" % [node, values.inspect] end unless values.empty? parent = values.shift end end end classattrs.each { |attr| if values = entry.vals(attr) values.each do |v| classes << v end end } parameters = entry.to_hash.inject({}) do |hash, ary| if ary[1].length == 1 hash[ary[0]] = ary[1].shift else hash[ary[0]] = ary[1] end hash end end rescue => detail if count == 0 # Try reconnecting to ldap @ldap = nil - setup_ldap() retry else raise Puppet::Error, "LDAP Search failed: %s" % detail end end classes.flatten! if classes.empty? classes = nil end if parent or classes or parameters return parent, classes, parameters else return nil end end # Look for our node in ldap. def nodesearch(node) unless ary = ldapsearch(node) return nil end parent, classes, parameters = ary while parent parent, tmpclasses, tmpparams = ldapsearch(parent) classes += tmpclasses if tmpclasses tmpparams.each do |param, value| # Specifically test for whether it's set, so false values are handled # correctly. parameters[param] = value unless parameters.include?(param) end end return newnode(node, :classes => classes, :source => "ldap", :parameters => parameters) end + + private + + # Create an ldap connection. + def ldap + unless defined? @ldap and @ldap + unless Puppet.features.ldap? + raise Puppet::Error, "Could not set up LDAP Connection: Missing ruby/ldap libraries" + end + begin + if Puppet[:ldapssl] + @ldap = LDAP::SSLConn.new(Puppet[:ldapserver], Puppet[:ldapport]) + elsif Puppet[:ldaptls] + @ldap = LDAP::SSLConn.new( + Puppet[:ldapserver], Puppet[:ldapport], true + ) + else + @ldap = LDAP::Conn.new(Puppet[:ldapserver], Puppet[:ldapport]) + end + @ldap.set_option(LDAP::LDAP_OPT_PROTOCOL_VERSION, 3) + @ldap.set_option(LDAP::LDAP_OPT_REFERRALS, LDAP::LDAP_OPT_ON) + @ldap.simple_bind(Puppet[:ldapuser], Puppet[:ldappassword]) + rescue => detail + raise Puppet::Error, "Could not connect to LDAP: %s" % detail + end + end + + return @ldap + end end diff --git a/lib/puppet/parser/ast/astarray.rb b/lib/puppet/parser/ast/astarray.rb index a0bd5bf89..9a2dc286c 100644 --- a/lib/puppet/parser/ast/astarray.rb +++ b/lib/puppet/parser/ast/astarray.rb @@ -1,99 +1,75 @@ require 'puppet/parser/ast/branch' class Puppet::Parser::AST # The basic container class. This object behaves almost identically # to a normal array except at initialization time. Note that its name # is 'AST::ASTArray', rather than plain 'AST::Array'; I had too many # bugs when it was just 'AST::Array', because things like # 'object.is_a?(Array)' never behaved as I expected. class ASTArray < Branch include Enumerable # Return a child by index. Probably never used. def [](index) @children[index] end # Evaluate our children. def evaluate(hash) scope = hash[:scope] rets = nil # We basically always operate declaratively, and when we # do we need to evaluate the settor-like statements first. This # is basically variable and type-default declarations. - if scope.declarative? - # This is such a stupid hack. I've no real idea how to make a - # "real" declarative language, so I hack it so it looks like - # one, yay. - settors = [] - others = [] + # This is such a stupid hack. I've no real idea how to make a + # "real" declarative language, so I hack it so it looks like + # one, yay. + settors = [] + others = [] - # Make a new array, so we don't have to deal with the details of - # flattening and such - items = [] - - # First clean out any AST::ASTArrays - @children.each { |child| - if child.instance_of?(AST::ASTArray) - child.each do |ac| - if ac.class.settor? - settors << ac - else - others << ac - end - end - else - if child.class.settor? - settors << child + # Make a new array, so we don't have to deal with the details of + # flattening and such + items = [] + + # First clean out any AST::ASTArrays + @children.each { |child| + if child.instance_of?(AST::ASTArray) + child.each do |ac| + if ac.class.settor? + settors << ac else - others << child + others << ac end end - } - rets = [settors, others].flatten.collect { |child| - child.safeevaluate(:scope => scope) - } - return rets.reject { |o| o.nil? } - else - # If we're not declarative, just do everything in order. - return @children.collect { |item| - item.safeevaluate(:scope => scope) - }.reject { |o| o.nil? } - end + else + if child.class.settor? + settors << child + else + others << child + end + end + } + rets = [settors, others].flatten.collect { |child| + child.safeevaluate(:scope => scope) + } + return rets.reject { |o| o.nil? } end def push(*ary) ary.each { |child| #Puppet.debug "adding %s(%s) of type %s to %s" % # [child, child.object_id, child.class.to_s.sub(/.+::/,''), # self.object_id] @children.push(child) } return self end - - # Convert to a string. Only used for printing the parse tree. - def to_s - return "[" + @children.collect { |child| - child.to_s - }.join(", ") + "]" - end - - # Print the parse tree. - def tree(indent = 0) - #puts((AST.indent * indent) + self.pin) - self.collect { |child| - child.tree(indent) - }.join("\n" + (AST.midline * (indent+1)) + "\n") - end end # A simple container class, containing the parameters for an object. # Used for abstracting the grammar declarations. Basically unnecessary # except that I kept finding bugs because I had too many arrays that # meant completely different things. - class ResourceInst < ASTArray; end + class ResourceInstance < ASTArray; end end - -# $Id$ diff --git a/lib/puppet/parser/ast/branch.rb b/lib/puppet/parser/ast/branch.rb index 5a4d323f5..dcd09baa3 100644 --- a/lib/puppet/parser/ast/branch.rb +++ b/lib/puppet/parser/ast/branch.rb @@ -1,49 +1,39 @@ class Puppet::Parser::AST # The parent class of all AST objects that contain other AST objects. # Everything but the really simple objects descend from this. It is # important to note that Branch objects contain other AST objects only -- # if you want to contain values, use a descendent of the AST::Leaf class. class Branch < AST include Enumerable attr_accessor :pin, :children # Yield each contained AST node in turn. Used mostly by 'evaluate'. # This definition means that I don't have to override 'evaluate' # every time, but each child of Branch will likely need to override # this method. def each @children.each { |child| yield child } end # Initialize our object. Largely relies on the method from the base # class, but also does some verification. def initialize(arghash) super(arghash) # Create the hash, if it was not set at initialization time. unless defined? @children @children = [] end # Verify that we only got valid AST nodes. @children.each { |child| unless child.is_a?(AST) raise Puppet::DevError, "child %s is a %s instead of ast" % [child, child.class] end } end - - # Pretty-print the parse tree. - def tree(indent = 0) - return ((@@indline * indent) + - self.typewrap(self.pin)) + "\n" + self.collect { |child| - child.tree(indent + 1) - }.join("\n") - end end end - -# $Id$ diff --git a/lib/puppet/parser/ast/caseopt.rb b/lib/puppet/parser/ast/caseopt.rb index 11483d082..d1d9d0e9c 100644 --- a/lib/puppet/parser/ast/caseopt.rb +++ b/lib/puppet/parser/ast/caseopt.rb @@ -1,71 +1,60 @@ require 'puppet/parser/ast/branch' class Puppet::Parser::AST # Each individual option in a case statement. class CaseOpt < AST::Branch attr_accessor :value, :statements # CaseOpt is a bit special -- we just want the value first, # so that CaseStatement can compare, and then it will selectively # decide whether to fully evaluate this option def each [@value,@statements].each { |child| yield child } end # Are we the default option? def default? # Cache the @default value. if defined? @default return @default end if @value.is_a?(AST::ASTArray) @value.each { |subval| if subval.is_a?(AST::Default) @default = true break end } else if @value.is_a?(AST::Default) @default = true end end unless defined? @default @default = false end return @default end # You can specify a list of values; return each in turn. def eachvalue(scope) if @value.is_a?(AST::ASTArray) @value.each { |subval| yield subval.evaluate(:scope => scope) } else yield @value.evaluate(:scope => scope) end end # Evaluate the actual statements; this only gets called if # our option matched. def evaluate(hash) return @statements.safeevaluate(hash) end - - def tree(indent = 0) - rettree = [ - @value.tree(indent + 1), - ((@@indline * indent) + self.typewrap(self.pin)), - @statements.tree(indent + 1) - ] - return rettree.flatten.join("\n") - end end end - -# $Id$ diff --git a/lib/puppet/parser/ast/casestatement.rb b/lib/puppet/parser/ast/casestatement.rb index c56e33bc9..3c6f9c7e2 100644 --- a/lib/puppet/parser/ast/casestatement.rb +++ b/lib/puppet/parser/ast/casestatement.rb @@ -1,70 +1,58 @@ require 'puppet/parser/ast/branch' class Puppet::Parser::AST # The basic logical structure in Puppet. Supports a list of # tests and statement arrays. class CaseStatement < AST::Branch attr_accessor :test, :options, :default # Short-curcuit evaluation. Return the value of the statements for # the first option that matches. def evaluate(hash) scope = hash[:scope] value = @test.safeevaluate(:scope => scope) sensitive = Puppet[:casesensitive] value = value.downcase if ! sensitive and value.respond_to?(:downcase) retvalue = nil found = false # Iterate across the options looking for a match. default = nil @options.each { |option| option.eachvalue(scope) { |opval| opval = opval.downcase if ! sensitive and opval.respond_to?(:downcase) if opval == value found = true break end } if found # we found a matching option retvalue = option.safeevaluate(:scope => scope) break end if option.default? default = option end } # Unless we found something, look for the default. unless found if default retvalue = default.safeevaluate(:scope => scope) else Puppet.debug "No true answers and no default" retvalue = nil end end return retvalue end - def tree(indent = 0) - rettree = [ - @test.tree(indent + 1), - ((@@indline * indent) + self.typewrap(self.pin)), - @options.tree(indent + 1) - ] - - return rettree.flatten.join("\n") - end - def each [@test,@options].each { |child| yield child } end end end - -# $Id$ diff --git a/lib/puppet/parser/ast/collection.rb b/lib/puppet/parser/ast/collection.rb index c817b5c5e..e05977a47 100644 --- a/lib/puppet/parser/ast/collection.rb +++ b/lib/puppet/parser/ast/collection.rb @@ -1,30 +1,28 @@ 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 # We return an object that does a late-binding evaluation. def evaluate(hash) scope = hash[:scope] if self.query str, code = self.query.safeevaluate :scope => scope else str = code = nil end newcoll = Puppet::Parser::Collector.new(scope, @type, str, code, self.form) - scope.newcollection(newcoll) + scope.compile.add_collection(newcoll) newcoll end end end - -# $Id$ diff --git a/lib/puppet/parser/ast/collexpr.rb b/lib/puppet/parser/ast/collexpr.rb index 5fb11c709..4a96d9c61 100644 --- a/lib/puppet/parser/ast/collexpr.rb +++ b/lib/puppet/parser/ast/collexpr.rb @@ -1,81 +1,79 @@ 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 CollExpr < AST::Branch attr_accessor :test1, :test2, :oper, :form, :type, :parens # We return an object that does a late-binding evaluation. def evaluate(hash) scope = hash[:scope] # Make sure our contained expressions have all the info they need. [@test1, @test2].each do |t| if t.is_a?(self.class) t.form ||= self.form t.type ||= self.type end end # The code is only used for virtual lookups str1, code1 = @test1.safeevaluate :scope => scope str2, code2 = @test2.safeevaluate :scope => scope # First build up the virtual code. # If we're a conjunction operator, then we're calling code. I did # some speed comparisons, and it's at least twice as fast doing these # case statements as doing an eval here. code = proc do |resource| case @oper when "and": code1.call(resource) and code2.call(resource) when "or": code1.call(resource) or code2.call(resource) when "==": resource[str1] == str2 when "!=": resource[str1] != str2 end end # Now build up the rails conditions code if self.parens and self.form == :exported Puppet.warning "Parentheses are ignored in Rails searches" end case @oper when "and", "or": if form == :exported raise Puppet::ParseError, "Puppet does not currently support collecting exported resources with more than one condition" end oper = @oper.upcase when "==": oper = "=" else oper = @oper end if oper == "=" or oper == "!=" # Add the rails association info where necessary if str1 == "title" str = "title #{oper} '#{str2}'" else str = "param_values.value #{oper} '#{str2}' and " + "param_names.name = '#{str1}'" end else str = "(%s) %s (%s)" % [str1, oper, str2] end return str, code end def initialize(hash = {}) super unless %w{== != and or}.include?(@oper) raise ArgumentError, "Invalid operator %s" % @oper end end end end - -# $Id$ diff --git a/lib/puppet/parser/ast/component.rb b/lib/puppet/parser/ast/component.rb index 65f310212..1d7fe9cdd 100644 --- a/lib/puppet/parser/ast/component.rb +++ b/lib/puppet/parser/ast/component.rb @@ -1,226 +1,224 @@ require 'puppet/parser/ast/branch' class Puppet::Parser::AST # Evaluate the stored parse tree for a given component. This will # receive the arguments passed to the component and also the type and # name of the component. class Component < AST::Branch include Puppet::Util include Puppet::Util::Warnings include Puppet::Util::MethodHelper class << self attr_accessor :name end # The class name @name = :definition attr_accessor :classname, :arguments, :code, :scope, :keyword attr_accessor :exported, :namespace, :parser, :virtual # These are retrieved when looking up the superclass attr_accessor :name attr_reader :parentclass def child_of?(klass) false end - def evaluate(hash) + def evaluate_resource(hash) origscope = hash[:scope] title = hash[:title] args = symbolize_options(hash[:arguments] || {}) name = args[:name] || title exported = hash[:exported] virtual = hash[:virtual] pscope = origscope scope = subscope(pscope, title) if virtual or origscope.virtual? scope.virtual = true end if exported or origscope.exported? scope.exported = true end # Additionally, add a tag for whatever kind of class # we are if @classname != "" and ! @classname.nil? @classname.split(/::/).each { |tag| scope.tag(tag) } end [name, title].each do |str| unless str.nil? or str =~ /[^\w]/ or str == "" scope.tag(str) end end # define all of the arguments in our local scope if self.arguments # Verify that all required arguments are either present or # have been provided with defaults. self.arguments.each { |arg, default| arg = symbolize(arg) unless args.include?(arg) if defined? default and ! default.nil? default = default.safeevaluate :scope => scope args[arg] = default #Puppet.debug "Got default %s for %s in %s" % # [default.inspect, arg.inspect, @name.inspect] else parsefail "Must pass %s to %s of type %s" % [arg,title,@classname] end end } end # Set each of the provided arguments as variables in the # component's scope. args.each { |arg,value| unless validattr?(arg) parsefail "%s does not accept attribute %s" % [@classname, arg] end exceptwrap do scope.setvar(arg.to_s,args[arg]) end } unless args.include? :title scope.setvar("title",title) end unless args.include? :name scope.setvar("name",name) end if self.code return self.code.safeevaluate(:scope => scope) else return nil end end def initialize(hash = {}) @arguments = nil @parentclass = nil super # Convert the arguments to a hash for ease of later use. if @arguments unless @arguments.is_a? Array @arguments = [@arguments] end oldargs = @arguments @arguments = {} oldargs.each do |arg, val| @arguments[arg] = val end else @arguments = {} end # Deal with metaparams in the argument list. @arguments.each do |arg, defvalue| next unless Puppet::Type.metaparamclass(arg) if defvalue warnonce "%s is a metaparam; this value will inherit to all contained elements" % arg else raise Puppet::ParseError, "%s is a metaparameter; please choose another name" % name end end end def find_parentclass @parser.findclass(namespace, parentclass) end # Set our parent class, with a little check to avoid some potential # weirdness. def parentclass=(name) if name == self.classname parsefail "Parent classes must have dissimilar names" end @parentclass = name end # Hunt down our class object. def parentobj if @parentclass # Cache our result, since it should never change. unless defined?(@parentobj) unless tmp = find_parentclass parsefail "Could not find %s %s" % [self.class.name, @parentclass] end if tmp == self parsefail "Parent classes must have dissimilar names" end @parentobj = tmp end @parentobj else nil end end # Create a new subscope in which to evaluate our code. def subscope(scope, name = nil) args = { :type => self.classname, :keyword => self.keyword, :namespace => self.namespace } args[:name] = name if name scope = scope.newscope(args) scope.source = self return scope end def to_s classname end # Check whether a given argument is valid. Searches up through # any parent classes that might exist. def validattr?(param) param = param.to_s if @arguments.include?(param) # It's a valid arg for us return true elsif param == "name" return true # elsif defined? @parentclass and @parentclass # # Else, check any existing parent # if parent = @scope.lookuptype(@parentclass) and parent != [] # return parent.validarg?(param) # elsif builtin = Puppet::Type.type(@parentclass) # return builtin.validattr?(param) # else # raise Puppet::Error, "Could not find parent class %s" % # @parentclass # end elsif Puppet::Type.metaparam?(param) return true else # Or just return false return false end end end end - -# $Id$ diff --git a/lib/puppet/parser/ast/component.rb b/lib/puppet/parser/ast/definition.rb similarity index 79% copy from lib/puppet/parser/ast/component.rb copy to lib/puppet/parser/ast/definition.rb index 65f310212..1cdaa6431 100644 --- a/lib/puppet/parser/ast/component.rb +++ b/lib/puppet/parser/ast/definition.rb @@ -1,226 +1,214 @@ require 'puppet/parser/ast/branch' class Puppet::Parser::AST # Evaluate the stored parse tree for a given component. This will # receive the arguments passed to the component and also the type and # name of the component. - class Component < AST::Branch + class Definition < AST::Branch include Puppet::Util include Puppet::Util::Warnings include Puppet::Util::MethodHelper class << self attr_accessor :name end # The class name @name = :definition attr_accessor :classname, :arguments, :code, :scope, :keyword attr_accessor :exported, :namespace, :parser, :virtual # These are retrieved when looking up the superclass attr_accessor :name attr_reader :parentclass def child_of?(klass) false end - def evaluate(hash) - origscope = hash[:scope] - title = hash[:title] - args = symbolize_options(hash[:arguments] || {}) + def evaluate(options) + origscope = options[:scope] + resource = options[:resource] - name = args[:name] || title - - exported = hash[:exported] - virtual = hash[:virtual] - - pscope = origscope - scope = subscope(pscope, title) - - if virtual or origscope.virtual? - scope.virtual = true - end - - if exported or origscope.exported? - scope.exported = true - end + # Create a new scope. + scope = subscope(origscope, resource) # Additionally, add a tag for whatever kind of class # we are if @classname != "" and ! @classname.nil? - @classname.split(/::/).each { |tag| scope.tag(tag) } + @classname.split(/::/).each { |tag| scope.resource.tag(tag) } end - [name, title].each do |str| + [resource.name, resource.title].each do |str| unless str.nil? or str =~ /[^\w]/ or str == "" - scope.tag(str) + scope.resource.tag(str) end end - # define all of the arguments in our local scope - if self.arguments - # Verify that all required arguments are either present or - # have been provided with defaults. - self.arguments.each { |arg, default| - arg = symbolize(arg) - unless args.include?(arg) - if defined? default and ! default.nil? - default = default.safeevaluate :scope => scope - args[arg] = default - #Puppet.debug "Got default %s for %s in %s" % - # [default.inspect, arg.inspect, @name.inspect] - else - parsefail "Must pass %s to %s of type %s" % - [arg,title,@classname] - end - end - } - end - - # Set each of the provided arguments as variables in the - # component's scope. - args.each { |arg,value| - unless validattr?(arg) - parsefail "%s does not accept attribute %s" % [@classname, arg] - end - - exceptwrap do - scope.setvar(arg.to_s,args[arg]) - end - } - - unless args.include? :title - scope.setvar("title",title) - end - - unless args.include? :name - scope.setvar("name",name) - end + set_resource_parameters(scope, resource) if self.code return self.code.safeevaluate(:scope => scope) else return nil end end def initialize(hash = {}) @arguments = nil @parentclass = nil super # Convert the arguments to a hash for ease of later use. if @arguments unless @arguments.is_a? Array @arguments = [@arguments] end oldargs = @arguments @arguments = {} oldargs.each do |arg, val| @arguments[arg] = val end else @arguments = {} end # Deal with metaparams in the argument list. @arguments.each do |arg, defvalue| next unless Puppet::Type.metaparamclass(arg) if defvalue - warnonce "%s is a metaparam; this value will inherit to all contained elements" % arg + warnonce "%s is a metaparam; this value will inherit to all contained resources" % arg else raise Puppet::ParseError, "%s is a metaparameter; please choose another name" % name end end end def find_parentclass @parser.findclass(namespace, parentclass) end # Set our parent class, with a little check to avoid some potential # weirdness. def parentclass=(name) if name == self.classname parsefail "Parent classes must have dissimilar names" end @parentclass = name end # Hunt down our class object. def parentobj if @parentclass # Cache our result, since it should never change. unless defined?(@parentobj) unless tmp = find_parentclass parsefail "Could not find %s %s" % [self.class.name, @parentclass] end if tmp == self parsefail "Parent classes must have dissimilar names" end @parentobj = tmp end @parentobj else nil end end # Create a new subscope in which to evaluate our code. - def subscope(scope, name = nil) + def subscope(scope, resource) args = { - :type => self.classname, + :resource => resource, :keyword => self.keyword, - :namespace => self.namespace + :namespace => self.namespace, + :source => self } - args[:name] = name if name + oldscope = scope scope = scope.newscope(args) scope.source = self return scope end def to_s classname end # Check whether a given argument is valid. Searches up through # any parent classes that might exist. def validattr?(param) param = param.to_s if @arguments.include?(param) # It's a valid arg for us return true elsif param == "name" return true # elsif defined? @parentclass and @parentclass # # Else, check any existing parent # if parent = @scope.lookuptype(@parentclass) and parent != [] # return parent.validarg?(param) # elsif builtin = Puppet::Type.type(@parentclass) # return builtin.validattr?(param) # else # raise Puppet::Error, "Could not find parent class %s" % # @parentclass # end elsif Puppet::Type.metaparam?(param) return true else # Or just return false return false end end + + private + + # Set any arguments passed by the resource as variables in the scope. + def set_resource_parameters(scope, resource) + args = symbolize_options(resource.to_hash || {}) + + # Verify that all required arguments are either present or + # have been provided with defaults. + if self.arguments + self.arguments.each { |arg, default| + arg = symbolize(arg) + unless args.include?(arg) + if defined? default and ! default.nil? + default = default.safeevaluate :scope => scope + args[arg] = default + #Puppet.debug "Got default %s for %s in %s" % + # [default.inspect, arg.inspect, @name.inspect] + else + parsefail "Must pass %s to %s of type %s" % + [arg, resource.title, @classname] + end + end + } + end + + # Set each of the provided arguments as variables in the + # definition's scope. + args.each { |arg,value| + unless validattr?(arg) + parsefail "%s does not accept attribute %s" % [@classname, arg] + end + + exceptwrap do + scope.setvar(arg.to_s, args[arg]) + end + } + + scope.setvar("title", resource.title) unless args.include? :title + scope.setvar("name", resource.name) unless args.include? :name + end end end - -# $Id$ diff --git a/lib/puppet/parser/ast/else.rb b/lib/puppet/parser/ast/else.rb index ff2f233b7..e76051372 100644 --- a/lib/puppet/parser/ast/else.rb +++ b/lib/puppet/parser/ast/else.rb @@ -1,30 +1,20 @@ require 'puppet/parser/ast/branch' class Puppet::Parser::AST # A separate ElseIf statement; can function as an 'else' if there's no # test. class Else < AST::Branch attr_accessor :statements def each yield @statements end # Evaluate the actual statements; this only gets called if # our test was true matched. def evaluate(hash) scope = hash[:scope] return @statements.safeevaluate(:scope => scope) end - - def tree(indent = 0) - rettree = [ - ((@@indline * indent) + self.typewrap(self.pin)), - @statements.tree(indent + 1) - ] - return rettree.flatten.join("\n") - end end end - -# $Id$ diff --git a/lib/puppet/parser/ast/function.rb b/lib/puppet/parser/ast/function.rb index 052b8a8b1..0cd1fff62 100644 --- a/lib/puppet/parser/ast/function.rb +++ b/lib/puppet/parser/ast/function.rb @@ -1,57 +1,55 @@ require 'puppet/parser/ast/branch' class Puppet::Parser::AST # An AST object to call a function. class Function < AST::Branch attr_accessor :name, :arguments @settor = true def evaluate(hash) # We don't need to evaluate the name, because it's plaintext # Just evaluate the arguments scope = hash[:scope] args = @arguments.safeevaluate(:scope => scope) #exceptwrap :message => "Failed to execute %s" % @name, # :type => Puppet::ParseError do return scope.send("function_" + @name, args) #end end def initialize(hash) @ftype = hash[:ftype] || :rvalue hash.delete(:ftype) if hash.include? :ftype super(hash) # Make sure it's a defined function unless @fname = Puppet::Parser::Functions.function(@name) raise Puppet::ParseError, "Unknown function %s" % @name end # Now check that it's been used correctly case @ftype when :rvalue: unless Puppet::Parser::Functions.rvalue?(@name) raise Puppet::ParseError, "Function '%s' does not return a value" % @name end when :statement: if Puppet::Parser::Functions.rvalue?(@name) raise Puppet::ParseError, "Function '%s' must be the value of a statement" % @name end else raise Puppet::DevError, "Invalid function type %s" % @ftype.inspect end # Lastly, check the arity end end end - -# $Id$ diff --git a/lib/puppet/parser/ast/hostclass.rb b/lib/puppet/parser/ast/hostclass.rb index d1ce370da..9c039bb49 100644 --- a/lib/puppet/parser/ast/hostclass.rb +++ b/lib/puppet/parser/ast/hostclass.rb @@ -1,81 +1,80 @@ -require 'puppet/parser/ast/component' +require 'puppet/parser/ast/definition' class Puppet::Parser::AST - # The code associated with a class. This is different from components + # The code associated with a class. This is different from definitions # in that each class is a singleton -- only one will exist for a given # node. - class HostClass < AST::Component + class HostClass < AST::Definition @name = :class # Are we a child of the passed class? Do a recursive search up our # parentage tree to figure it out. def child_of?(klass) return false unless self.parentclass if klass == self.parentobj return true else return self.parentobj.child_of?(klass) end end # Evaluate the code associated with this class. - def evaluate(hash) - scope = hash[:scope] - args = hash[:arguments] - - # Verify that we haven't already been evaluated, and if we have been evaluated, - # make sure that we match the class. - if existing_scope = scope.class_scope(self) - raise "Fix this portion of the code -- check that the scopes match classes" - #if existing_scope.source.object_id == self.object_id + def evaluate(options) + scope = options[:scope] + raise(ArgumentError, "Classes require resources") unless options[:resource] + # Verify that we haven't already been evaluated. This is + # what provides the singleton aspect. + if existing_scope = scope.compile.class_scope(self) Puppet.debug "%s class already evaluated" % @type return nil end + scope.compile.configuration.tag(self.classname) + pnames = nil if pklass = self.parentobj - pklass.safeevaluate :scope => scope + pklass.safeevaluate :scope => scope, :resource => options[:resource] scope = parent_scope(scope, pklass) pnames = scope.namespaces end - unless hash[:nosubscope] - scope = subscope(scope) + # Don't create a subscope for the top-level class, since it already + # has its own scope. + unless options[:resource].title == :main + scope = subscope(scope, options[:resource]) end if pnames pnames.each do |ns| scope.add_namespace(ns) end end # Set the class before we do anything else, so that it's set # during the evaluation and can be inspected. - scope.setclass(self) + scope.compile.class_set(self.classname, scope) # Now evaluate our code, yo. if self.code return self.code.evaluate(:scope => scope) else return nil end end - def initialize(hash) + def initialize(options) @parentclass = nil super end def parent_scope(scope, klass) - if s = scope.class_scope(klass) + if s = scope.compile.class_scope(klass) return s else - raise Puppet::DevError, "Could not find scope for %s" % klass.fqname + raise Puppet::DevError, "Could not find scope for %s" % klass.classname end end end end - -# $Id$ diff --git a/lib/puppet/parser/ast/ifstatement.rb b/lib/puppet/parser/ast/ifstatement.rb index 300f68dab..66a07b01f 100644 --- a/lib/puppet/parser/ast/ifstatement.rb +++ b/lib/puppet/parser/ast/ifstatement.rb @@ -1,43 +1,30 @@ require 'puppet/parser/ast/branch' class Puppet::Parser::AST # A basic 'if/elsif/else' statement. class IfStatement < AST::Branch attr_accessor :test, :else, :statements def each [@test,@else,@statements].each { |child| yield child } end # Short-curcuit evaluation. If we're true, evaluate our statements, # else if there's an 'else' setting, evaluate it. # the first option that matches. def evaluate(hash) scope = hash[:scope] value = @test.safeevaluate(:scope => scope) if Puppet::Parser::Scope.true?(value) return @statements.safeevaluate(:scope => scope) else if defined? @else return @else.safeevaluate(:scope => scope) else return nil end end end - - def tree(indent = 0) - rettree = [ - @test.tree(indent + 1), - ((@@indline * indent) + self.typewrap(self.pin)), - @statements.tree(indent + 1), - @else.tree(indent + 1) - ] - - return rettree.flatten.join("\n") - end end end - -# $Id$ diff --git a/lib/puppet/parser/ast/leaf.rb b/lib/puppet/parser/ast/leaf.rb index c2fd0939d..225253061 100644 --- a/lib/puppet/parser/ast/leaf.rb +++ b/lib/puppet/parser/ast/leaf.rb @@ -1,98 +1,90 @@ 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(hash) return @value end - # Print the value in parse tree context. - def tree(indent = 0) - return ((@@indent * indent) + self.typewrap(self.value)) - end - def to_s return @value 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, "'%s' is not a boolean" % @value end @value end end # The base string class. class String < AST::Leaf # Interpolate the string looking for variables, and then return # the result. def evaluate(hash) return hash[:scope].strinterp(@value, @file, @line) end end # An uninterpreted string. class FlatString < AST::Leaf def evaluate(hash) return @value 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 class HostName < AST::Leaf def initialize(hash) super unless @value =~ %r{^[0-9a-zA-Z\-]+(\.[0-9a-zA-Z\-]+)*$} raise Puppet::DevError, "'%s' is not a valid hostname" % @value end 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(hash) parsewrap do return hash[:scope].lookupvar(@value) end end end - end - -# $Id$ diff --git a/lib/puppet/parser/ast/node.rb b/lib/puppet/parser/ast/node.rb index b9052168a..a296e43ba 100644 --- a/lib/puppet/parser/ast/node.rb +++ b/lib/puppet/parser/ast/node.rb @@ -1,64 +1,67 @@ require 'puppet/parser/ast/hostclass' class Puppet::Parser::AST # The specific code associated with a host. Nodes are annoyingly unlike # other objects. That's just the way it is, at least for now. class Node < AST::HostClass @name = :node attr_accessor :name - #def evaluate(scope, facts = {}) - def evaluate(hash) - scope = hash[:scope] + def evaluate(options) + scope = options[:scope] - #pscope = if ! Puppet[:lexical] or hash[:asparent] + #pscope = if ! Puppet[:lexical] or options[:asparent] # @scope #else # origscope #end # We don't have to worry about the declarativeness of node parentage, # because the entry point is always a single node definition. if parent = self.parentobj - scope = parent.safeevaluate :scope => scope + scope = parent.safeevaluate :scope => scope, :resource => options[:resource] end scope = scope.newscope( - :type => self.name, + :resource => options[:resource], :keyword => @keyword, :source => self, :namespace => "" # nodes are always in "" ) # Mark our node name as a class, too, but strip it of the domain # name. Make the mark before we evaluate the code, so that it is # marked within the code itself. - scope.setclass(self) + scope.compile.class_set(self.classname, scope) # And then evaluate our code if we have any if self.code @code.safeevaluate(:scope => scope) end return scope end - def initialize(hash) + def initialize(options) @parentclass = nil super # Do some validation on the node name if @name =~ /[^-\w.]/ raise Puppet::ParseError, "Invalid node name %s" % @name end end + # Make sure node scopes are marked as such. + def subscope(*args) + scope = super + scope.nodescope = true + end + private # Search for the object matching our parent class. def find_parentclass @parser.findnode(parentclass) end end end - -# $Id$ diff --git a/lib/puppet/parser/ast/resource.rb b/lib/puppet/parser/ast/resource.rb new file mode 100644 index 000000000..c53ab0a68 --- /dev/null +++ b/lib/puppet/parser/ast/resource.rb @@ -0,0 +1,75 @@ +require 'puppet/parser/ast/resource_reference' + +# Any normal puppet resource declaration. Can point to a definition or a +# builtin type. +class Puppet::Parser::AST +class Resource < AST::ResourceReference + attr_accessor :title, :type, :exported, :virtual + attr_reader :params + + # Does not actually return an object; instead sets an object + # in the current scope. + def evaluate(options) + scope = options[:scope] + + # Evaluate all of the specified params. + paramobjects = @params.collect { |param| + param.safeevaluate(:scope => scope) + } + + objtitles = @title.safeevaluate(:scope => scope) + + # it's easier to always use an array, even for only one name + unless objtitles.is_a?(Array) + objtitles = [objtitles] + end + + objtype = qualified_type(scope) + + # This is where our implicit iteration takes place; if someone + # passed an array as the name, then we act just like the called us + # many times. + objtitles.collect { |objtitle| + exceptwrap :type => Puppet::ParseError do + exp = self.exported || scope.resource.exported? + # We want virtual to be true if exported is true. We can't + # just set :virtual => self.virtual in the initialization, + # because sometimes the :virtual attribute is set *after* + # :exported, in which case it clobbers :exported if :exported + # is true. Argh, this was a very tough one to track down. + virt = self.virtual || scope.resource.virtual? || exp + obj = Puppet::Parser::Resource.new( + :type => objtype, + :title => objtitle, + :params => paramobjects, + :file => self.file, + :line => self.line, + :exported => exp, + :virtual => virt, + :source => scope.source, + :scope => scope + ) + + # And then store the resource in the compile. + # At some point, we need to switch all of this to return + # objects instead of storing them like this. + scope.compile.store_resource(scope, obj) + obj + end + }.reject { |obj| obj.nil? } + end + + # Set the parameters for our object. + def params=(params) + if params.is_a?(AST::ASTArray) + @params = params + else + @params = AST::ASTArray.new( + :line => params.line, + :file => params.file, + :children => [params] + ) + end + end +end +end diff --git a/lib/puppet/parser/ast/resourcedefaults.rb b/lib/puppet/parser/ast/resource_defaults.rb similarity index 60% rename from lib/puppet/parser/ast/resourcedefaults.rb rename to lib/puppet/parser/ast/resource_defaults.rb index df16b1b59..44ec146b0 100644 --- a/lib/puppet/parser/ast/resourcedefaults.rb +++ b/lib/puppet/parser/ast/resource_defaults.rb @@ -1,40 +1,21 @@ require 'puppet/parser/ast/branch' class Puppet::Parser::AST # A statement syntactically similar to an ResourceDef, but uses a # capitalized object type and cannot have a name. class ResourceDefaults < AST::Branch attr_accessor :type, :params - def each - [@type,@params].each { |child| yield child } - end - # As opposed to ResourceDef, this stores each default for the given # object type. def evaluate(hash) scope = hash[:scope] type = @type.downcase params = @params.safeevaluate(:scope => scope) parsewrap do scope.setdefaults(type, params) end end - - def tree(indent = 0) - return [ - @type.tree(indent + 1), - ((@@indline * 4 * indent) + self.typewrap(self.pin)), - @params.tree(indent + 1) - ].join("\n") - end - - def to_s - return "%s { %s }" % [@type,@params] - end end - end - -# $Id$ diff --git a/lib/puppet/parser/ast/resourceoverride.rb b/lib/puppet/parser/ast/resource_override.rb similarity index 93% rename from lib/puppet/parser/ast/resourceoverride.rb rename to lib/puppet/parser/ast/resource_override.rb index 26d69ae97..46c930902 100644 --- a/lib/puppet/parser/ast/resourceoverride.rb +++ b/lib/puppet/parser/ast/resource_override.rb @@ -1,62 +1,60 @@ -require 'puppet/parser/ast/resourcedef' +require 'puppet/parser/ast/resource' class Puppet::Parser::AST # Set a parameter on a resource specification created somewhere else in the # configuration. The object is responsible for verifying that this is allowed. - class ResourceOverride < ResourceDef + class ResourceOverride < Resource attr_accessor :object attr_reader :params # Iterate across all of our children. def each [@object,@params].flatten.each { |param| #Puppet.debug("yielding param %s" % param) yield param } end # Does not actually return an object; instead sets an object # in the current scope. def evaluate(hash) scope = hash[:scope] # Get our object reference. object = @object.safeevaluate(:scope => scope) hash = {} # Evaluate all of the specified params. params = @params.collect { |param| param.safeevaluate(:scope => scope) } # Now we just create a normal resource, but we call a very different # method on the scope. obj = Puppet::Parser::Resource.new( :type => object.type, :title => object.title, :params => params, :file => @file, :line => @line, :source => scope.source, :scope => scope ) # Now we tell the scope that it's an override, and it behaves as # necessary. - scope.setoverride(obj) + scope.compile.store_override(obj) obj end # Create our ResourceDef. Handles type checking for us. def initialize(hash) @checked = false super #self.typecheck(@type.value) end end end - -# $Id$ diff --git a/lib/puppet/parser/ast/resource_reference.rb b/lib/puppet/parser/ast/resource_reference.rb new file mode 100644 index 000000000..b8edff8f1 --- /dev/null +++ b/lib/puppet/parser/ast/resource_reference.rb @@ -0,0 +1,66 @@ +require 'puppet/parser/ast/branch' + +class Puppet::Parser::AST + # A reference to an object. Only valid as an rvalue. + class ResourceReference < AST::Branch + attr_accessor :title, :type + # Is the type a builtin type? + def builtintype?(type) + if typeklass = Puppet::Type.type(type) + return typeklass + else + return false + end + end + + def each + [@type,@title].flatten.each { |param| + #Puppet.debug("yielding param %s" % param) + yield param + } + end + + # Evaluate our object, but just return a simple array of the type + # and name. + def evaluate(hash) + scope = hash[:scope] + + title = @title.safeevaluate(:scope => scope) + if @type == "class" + objtype = @type + title = qualified_class(scope, title) + else + objtype = qualified_type(scope) + end + + return Puppet::Parser::Resource::Reference.new( + :type => objtype, :title => title + ) + end + + # Look up a fully qualified class name. + def qualified_class(scope, title) + # Look up the full path to the class + if classobj = scope.findclass(title) + title = classobj.classname + else + raise Puppet::ParseError, "Could not find class %s" % title + end + end + + # Look up a fully-qualified type. This method is + # also used in AST::Resource. + def qualified_type(scope, title = nil) + # We want a lower-case type. For some reason. + objtype = @type.downcase + unless builtintype?(objtype) + if dtype = scope.finddefine(objtype) + objtype = dtype.classname + else + raise Puppet::ParseError, "Could not find resource type %s" % objtype + end + end + return objtype + end + end +end diff --git a/lib/puppet/parser/ast/resourcedef.rb b/lib/puppet/parser/ast/resourcedef.rb deleted file mode 100644 index dfd7c447e..000000000 --- a/lib/puppet/parser/ast/resourcedef.rb +++ /dev/null @@ -1,222 +0,0 @@ -require 'puppet/parser/ast/branch' - -# Any normal puppet object declaration. Can result in a class or a -# component, in addition to builtin types. -class Puppet::Parser::AST -class ResourceDef < AST::Branch - attr_accessor :title, :type, :exported, :virtual - attr_reader :params - - # probably not used at all - def []=(index,obj) - @params[index] = obj - end - - # probably not used at all - def [](index) - return @params[index] - end - - # Iterate across all of our children. - def each - [@type,@title,@params].flatten.each { |param| - #Puppet.debug("yielding param %s" % param) - yield param - } - end - - # Does not actually return an object; instead sets an object - # in the current scope. - def evaluate(hash) - scope = hash[:scope] - @scope = scope - hash = {} - - # Get our type and name. - objtype = @type - - # Disable definition inheritance, for now. 8/27/06, luke - #if objtype == "super" - # objtype = supertype() - # @subtype = true - #else - @subtype = false - #end - - # Evaluate all of the specified params. - paramobjects = @params.collect { |param| - param.safeevaluate(:scope => scope) - } - - # Now collect info from our parent. - parentname = nil - if @subtype - parentname = supersetup(hash) - end - - objtitles = nil - # Determine our name if we have one. - if self.title - objtitles = @title.safeevaluate(:scope => scope) - # it's easier to always use an array, even for only one name - unless objtitles.is_a?(Array) - objtitles = [objtitles] - end - else - if parentname - objtitles = [parentname] - else - # See if they specified the name as a parameter instead of - # as a normal name (i.e., before the colon). - unless object # we're a builtin - if objclass = Puppet::Type.type(objtype) - namevar = objclass.namevar - - tmp = hash["name"] || hash[namevar.to_s] - - if tmp - objtitles = [tmp] - end - else - # This isn't grammatically legal. - raise Puppet::ParseError, "Got a resource with no title" - end - end - end - end - - # This is where our implicit iteration takes place; if someone - # passed an array as the name, then we act just like the called us - # many times. - objtitles.collect { |objtitle| - exceptwrap :type => Puppet::ParseError do - exp = self.exported || scope.exported - # We want virtual to be true if exported is true. We can't - # just set :virtual => self.virtual in the initialization, - # because sometimes the :virtual attribute is set *after* - # :exported, in which case it clobbers :exported if :exported - # is true. Argh, this was a very tough one to track down. - virt = self.virtual || exported - obj = Puppet::Parser::Resource.new( - :type => objtype, - :title => objtitle, - :params => paramobjects, - :file => @file, - :line => @line, - :exported => exp, - :virtual => virt, - :source => scope.source, - :scope => scope - ) - - # And then store the resource in the scope. - # XXX At some point, we need to switch all of this to return - # objects instead of storing them like this. - scope.setresource(obj) - obj - end - }.reject { |obj| obj.nil? } - end - - # Create our ResourceDef. Handles type checking for us. - def initialize(hash) - @checked = false - super - - #self.typecheck(@type.value) - end - - # Set the parameters for our object. - def params=(params) - if params.is_a?(AST::ASTArray) - @params = params - else - @params = AST::ASTArray.new( - :line => params.line, - :file => params.file, - :children => [params] - ) - end - end - - def supercomp - unless defined? @supercomp - if @scope and comp = @scope.inside - @supercomp = comp - else - error = Puppet::ParseError.new( - "'super' is only valid within definitions" - ) - error.line = self.line - error.file = self.file - raise error - end - end - @supercomp - end - - # Take all of the arguments of our parent and add them into our own, - # without overriding anything. - def supersetup(hash) - comp = supercomp() - - # Now check each of the arguments from the parent. - comp.arguments.each do |name, value| - unless hash.has_key? name - hash[name] = value - end - end - - # Return the parent name, so it can be used if appropriate. - return comp.name - end - - # Retrieve our supertype. - def supertype - unless defined? @supertype - if parent = supercomp.parentclass - @supertype = parent - else - error = Puppet::ParseError.new( - "%s does not have a parent class" % comp.type - ) - error.line = self.line - error.file = self.file - raise error - end - end - @supertype - end - - # Print this object out. - def tree(indent = 0) - return [ - @type.tree(indent + 1), - @title.tree(indent + 1), - ((@@indline * indent) + self.typewrap(self.pin)), - @params.collect { |param| - begin - param.tree(indent + 1) - rescue NoMethodError => detail - Puppet.err @params.inspect - error = Puppet::DevError.new( - "failed to tree a %s" % self.class - ) - error.set_backtrace detail.backtrace - raise error - end - }.join("\n") - ].join("\n") - end - - def to_s - return "%s => { %s }" % [@title, - @params.collect { |param| - param.to_s - }.join("\n") - ] - end -end -end - -# $Id$ diff --git a/lib/puppet/parser/ast/resourceparam.rb b/lib/puppet/parser/ast/resourceparam.rb index d87720160..8b1e7b367 100644 --- a/lib/puppet/parser/ast/resourceparam.rb +++ b/lib/puppet/parser/ast/resourceparam.rb @@ -1,38 +1,24 @@ require 'puppet/parser/ast/branch' class Puppet::Parser::AST # The AST object for the parameters inside ResourceDefs and Selectors. class ResourceParam < AST::Branch attr_accessor :value, :param, :add def each [@param,@value].each { |child| yield child } end # Return the parameter and the value. def evaluate(hash) scope = hash[:scope] return Puppet::Parser::Resource::Param.new( :name => @param, :value => @value.safeevaluate(:scope => scope), :source => scope.source, :line => self.line, :file => self.file, :add => self.add ) end - - def tree(indent = 0) - return [ - @param.tree(indent + 1), - ((@@indline * indent) + self.typewrap(self.pin)), - @value.tree(indent + 1) - ].join("\n") - end - - def to_s - return "%s => %s" % [@param,@value] - end end end - -# $Id$ diff --git a/lib/puppet/parser/ast/resourceref.rb b/lib/puppet/parser/ast/resourceref.rb deleted file mode 100644 index e5bb69a46..000000000 --- a/lib/puppet/parser/ast/resourceref.rb +++ /dev/null @@ -1,58 +0,0 @@ -require 'puppet/parser/ast/branch' - -class Puppet::Parser::AST - # A reference to an object. Only valid as an rvalue. - class ResourceRef < AST::Branch - attr_accessor :title, :type - - def each - [@type,@title].flatten.each { |param| - #Puppet.debug("yielding param %s" % param) - yield param - } - end - - # Evaluate our object, but just return a simple array of the type - # and name. - def evaluate(hash) - scope = hash[:scope] - - # We want a lower-case type. - objtype = @type.downcase - title = @title.safeevaluate(:scope => scope) - - if scope.builtintype?(objtype) - # nothing - elsif dtype = scope.finddefine(objtype) - objtype = dtype.classname - elsif objtype == "class" - # Look up the full path to the class - if classobj = scope.findclass(title) - title = classobj.classname - else - raise Puppet::ParseError, "Could not find class %s" % title - end - else - raise Puppet::ParseError, "Could not find resource type %s" % objtype - end - - return Puppet::Parser::Resource::Reference.new( - :type => objtype, :title => title - ) - end - - def tree(indent = 0) - return [ - @type.tree(indent + 1), - @title.tree(indent + 1), - ((@@indline * indent) + self.typewrap(self.pin)) - ].join("\n") - end - - def to_s - return "%s[%s]" % [@type,@title] - end - end -end - -# $Id$ diff --git a/lib/puppet/parser/ast/selector.rb b/lib/puppet/parser/ast/selector.rb index e5eb3b6f5..d363ab7e4 100644 --- a/lib/puppet/parser/ast/selector.rb +++ b/lib/puppet/parser/ast/selector.rb @@ -1,74 +1,64 @@ require 'puppet/parser/ast/branch' class Puppet::Parser::AST # The inline conditional operator. Unlike CaseStatement, which executes # code, we just return a value. class Selector < AST::Branch attr_accessor :param, :values def each [@param,@values].each { |child| yield child } end # Find the value that corresponds with the test. def evaluate(hash) scope = hash[:scope] retvalue = nil found = nil # Get our parameter. paramvalue = @param.safeevaluate(:scope => scope) sensitive = Puppet[:casesensitive] if ! sensitive and paramvalue.respond_to?(:downcase) paramvalue = paramvalue.downcase end default = nil unless @values.instance_of? AST::ASTArray or @values.instance_of? Array @values = [@values] end # Then look for a match in the options. @values.each { |obj| param = obj.param.safeevaluate(:scope => scope) if ! sensitive && param.respond_to?(:downcase) param = param.downcase end if param == paramvalue # we found a matching option retvalue = obj.value.safeevaluate(:scope => scope) found = true break elsif obj.param.is_a?(Default) # Store the default, in case it's necessary. default = obj end } # Unless we found something, look for the default. unless found if default retvalue = default.value.safeevaluate(:scope => scope) else self.fail Puppet::ParseError, "No matching value for selector param '%s'" % paramvalue end end return retvalue end - - def tree(indent = 0) - return [ - @param.tree(indent + 1), - ((@@indline * indent) + self.typewrap(self.pin)), - @values.tree(indent + 1) - ].join("\n") - end end end - -# $Id$ diff --git a/lib/puppet/parser/ast/tag.rb b/lib/puppet/parser/ast/tag.rb index 4a2015cde..e2882d2f0 100644 --- a/lib/puppet/parser/ast/tag.rb +++ b/lib/puppet/parser/ast/tag.rb @@ -1,28 +1,26 @@ require 'puppet/parser/ast/branch' class Puppet::Parser::AST # The code associated with a class. This is different from components # in that each class is a singleton -- only one will exist for a given # node. class Tag < AST::Branch @name = :class attr_accessor :type def evaluate(hash) scope = hash[:scope] types = @type.safeevaluate(:scope => scope) types = [types] unless types.is_a? Array types.each do |type| # Now set our class. We don't have to worry about checking # whether we've been evaluated because we're not evaluating # any code. scope.setclass(self.object_id, type) end end end end - -# $Id$ diff --git a/lib/puppet/parser/ast/vardef.rb b/lib/puppet/parser/ast/vardef.rb index ecc10bd14..1e7f874bc 100644 --- a/lib/puppet/parser/ast/vardef.rb +++ b/lib/puppet/parser/ast/vardef.rb @@ -1,41 +1,27 @@ require 'puppet/parser/ast/branch' class Puppet::Parser::AST # Define a variable. Stores the value in the current scope. class VarDef < AST::Branch attr_accessor :name, :value @settor = true # Look up our name and value, and store them appropriately. The # lexer strips off the syntax stuff like '$'. def evaluate(hash) scope = hash[:scope] name = @name.safeevaluate(:scope => scope) value = @value.safeevaluate(:scope => scope) parsewrap do scope.setvar(name,value, @file, @line) end end def each [@name,@value].each { |child| yield child } end - - def tree(indent = 0) - return [ - @name.tree(indent + 1), - ((@@indline * 4 * indent) + self.typewrap(self.pin)), - @value.tree(indent + 1) - ].join("\n") - end - - def to_s - return "%s => %s" % [@name,@value] - end end end - -# $Id$ diff --git a/lib/puppet/parser/collector.rb b/lib/puppet/parser/collector.rb index 6c49c6d57..3eb37dfa2 100644 --- a/lib/puppet/parser/collector.rb +++ b/lib/puppet/parser/collector.rb @@ -1,185 +1,176 @@ # An object that collects stored objects from the central cache and returns # them to the current host, yo. class Puppet::Parser::Collector attr_accessor :type, :scope, :vquery, :rquery, :form, :resources # Collect exported objects. def collect_exported # First get everything from the export table. Just reuse our # collect_virtual method but tell it to use 'exported? for the test. resources = collect_virtual(true).reject { |r| ! r.virtual? } count = 0 unless @scope.host raise Puppet::DevError, "Cannot collect resources for a nil host" end # We're going to collect objects from rails, but we don't want any # objects from this host. unless ActiveRecord::Base.connected? Puppet::Rails.init end host = Puppet::Rails::Host.find_by_name(@scope.host) args = {:include => {:param_values => :param_name}} args[:conditions] = "(exported = 't' AND restype = '%s')" % [@type] if @equery args[:conditions] += " AND (%s)" % [@equery] end if host args[:conditions] = "host_id != %s AND %s" % [host.id, args[:conditions]] else #Puppet.info "Host %s is uninitialized" % @scope.host end # Now look them up in the rails db. When we support attribute comparison # and such, we'll need to vary the conditions, but this works with no # attributes, anyway. time = Puppet::Util.thinmark do Puppet::Rails::Resource.find(:all, @type, true, args ).each do |obj| if resource = export_resource(obj) count += 1 resources << resource end end end scope.debug("Collected %s %s resource%s in %.2f seconds" % [count, @type, count == 1 ? "" : "s", time]) return resources end def collect_resources unless @resources.is_a?(Array) @resources = [@resources] end method = "collect_#{form.to_s}_resources" send(method) end def collect_exported_resources raise Puppet::ParseError, "realize() is not yet implemented for exported resources" end # Collect resources directly; this is the result of using 'realize', # which specifies resources, rather than using a normal collection. def collect_virtual_resources result = @resources.dup.collect do |ref| if res = @scope.findresource(ref.to_s) @resources.delete(ref) res end end.reject { |r| r.nil? }.each do |res| res.virtual = false end # If there are no more resources to find, delete this from the list # of collections. if @resources.empty? - @scope.collections.delete(self) + @scope.compile.delete_collection(self) end return result end - # Collect just virtual objects, from our local configuration. + # Collect just virtual objects, from our local compile. def collect_virtual(exported = false) if exported method = :exported? else method = :virtual? end - scope.resources.find_all do |resource| + scope.compile.resources.find_all do |resource| resource.type == @type and resource.send(method) and match?(resource) end end # Call the collection method, mark all of the returned objects as non-virtual, # and then delete this object from the list of collections to evaluate. def evaluate if self.resources if objects = collect_resources and ! objects.empty? return objects else return false end else method = "collect_#{@form.to_s}" objects = send(method).each { |obj| obj.virtual = false } if objects.empty? return false else return objects end end - -# if objects and ! objects.empty? -# objects.each { |r| r.virtual = false } -# return objects -# else -# return false -# end end def initialize(scope, type, equery, vquery, form) @scope = scope @type = type @equery = equery @vquery = vquery @form = form @tests = [] end # Does the resource match our tests? We don't yet support tests, # so it's always true at the moment. def match?(resource) if self.vquery return self.vquery.call(resource) else return true end end def export_resource(obj) if existing = @scope.findresource(obj.restype, obj.title) # See if we exported it; if so, just move on if @scope.host == obj.host.name return nil else # Next see if we've already collected this resource if existing.rails_id == obj.id # This is the one we've already collected return nil else raise Puppet::ParseError, "Exported resource %s cannot override local resource" % [obj.ref] end end end begin resource = obj.to_resource(self.scope) # XXX Because the scopes don't expect objects to return values, # we have to manually add our objects to the scope. This is # über-lame. - scope.setresource(resource) + scope.compile.store_resource(scope, resource) rescue => detail if Puppet[:trace] puts detail.backtrace end raise end resource.exported = false return resource end end - -# $Id$ diff --git a/lib/puppet/parser/compile.rb b/lib/puppet/parser/compile.rb new file mode 100644 index 000000000..6aeebeaae --- /dev/null +++ b/lib/puppet/parser/compile.rb @@ -0,0 +1,498 @@ +# Created by Luke A. Kanies on 2007-08-13. +# Copyright (c) 2007. All rights reserved. + +require 'puppet/external/gratr/digraph' +require 'puppet/external/gratr/import' +require 'puppet/external/gratr/dot' + +require 'puppet/node' +require 'puppet/node/configuration' +require 'puppet/util/errors' + +# Maintain a graph of scopes, along with a bunch of data +# about the individual configuration we're compiling. +class Puppet::Parser::Compile + include Puppet::Util + include Puppet::Util::Errors + attr_reader :topscope, :parser, :node, :facts, :collections, :configuration + + attr_writer :ast_nodes + + # Add a collection to the global list. + def add_collection(coll) + @collections << coll + end + + # Do we use nodes found in the code, vs. the external node sources? + def ast_nodes? + defined?(@ast_nodes) and @ast_nodes + end + + # Store the fact that we've evaluated a class, and store a reference to + # the scope in which it was evaluated, so that we can look it up later. + def class_set(name, scope) + if existing = @class_scopes[name] + if existing.nodescope? or scope.nodescope? + raise Puppet::ParseError, "Cannot have classes, nodes, or definitions with the same name" + else + raise Puppet::DevError, "Somehow evaluated the same class twice" + end + end + @class_scopes[name] = scope + @configuration.add_class(name) unless name == "" + end + + # Return the scope associated with a class. This is just here so + # that subclasses can set their parent scopes to be the scope of + # their parent class, and it's also used when looking up qualified + # variables. + def class_scope(klass) + # They might pass in either the class or class name + if klass.respond_to?(:classname) + @class_scopes[klass.classname] + else + @class_scopes[klass] + end + end + + # Return a list of all of the defined classes. + def classlist + return @configuration.classes + end + + # Compile our configuration. This mostly revolves around finding and evaluating classes. + # This is the main entry into our configuration. + def compile + # Set the client's parameters into the top scope. + set_node_parameters() + + evaluate_main() + + evaluate_ast_node() + + evaluate_node_classes() + + evaluate_generators() + + fail_on_unevaluated() + + finish() + + if Puppet[:storeconfigs] + store() + end + + return @configuration + end + + # FIXME There are no tests for this. + def delete_collection(coll) + @collections.delete(coll) if @collections.include?(coll) + end + + # FIXME There are no tests for this. + def delete_resource(resource) + @resource_table.delete(resource.ref) if @resource_table.include?(resource.ref) + end + + # Return the node's environment. + def environment + unless defined? @environment + if node.environment and node.environment != "" + @environment = node.environment + else + @environment = nil + end + end + @environment + end + + # Evaluate all of the classes specified by the node. + def evaluate_node_classes + evaluate_classes(@node.classes, @topscope) + end + + # Evaluate each specified class in turn. If there are any classes we can't + # find, just tag the configuration and move on. This method really just + # creates resource objects that point back to the classes, and then the + # resources are themselves evaluated later in the process. + def evaluate_classes(classes, scope) + unless scope.source + raise Puppet::DevError, "No source for scope passed to evaluate_classes" + end + found = [] + classes.each do |name| + # If we can find the class, then make a resource that will evaluate it. + if klass = scope.findclass(name) + # Create a resource to model this class, and then add it to the list + # of resources. + resource = Puppet::Parser::Resource.new(:type => "class", :title => klass.classname, :scope => scope, :source => scope.source) + store_resource(scope, resource) + @configuration.tag(klass.classname) + found << name + else + Puppet.info "Could not find class %s for %s" % [name, node.name] + @configuration.tag(name) + end + end + found + end + + # Return a resource by either its ref or its type and title. + def findresource(string, name = nil) + if name + string = "%s[%s]" % [string.capitalize, name] + end + + @resource_table[string] + end + + # Set up our compile. We require a parser + # and a node object; the parser is so we can look up classes + # and AST nodes, and the node has all of the client's info, + # like facts and environment. + def initialize(node, parser, options = {}) + @node = node + @parser = parser + + options.each do |param, value| + begin + send(param.to_s + "=", value) + rescue NoMethodError + raise ArgumentError, "Compile objects do not accept %s" % param + end + end + + initvars() + init_main() + end + + # Create a new scope, with either a specified parent scope or + # using the top scope. Adds an edge between the scope and + # its parent to the graph. + def newscope(parent, options = {}) + parent ||= @topscope + options[:compile] = self + options[:parser] ||= self.parser + scope = Puppet::Parser::Scope.new(options) + @scope_graph.add_edge!(parent, scope) + scope + end + + # Find the parent of a given scope. Assumes scopes only ever have + # one in edge, which will always be true. + def parent(scope) + if ary = @scope_graph.adjacent(scope, :direction => :in) and ary.length > 0 + ary[0] + else + nil + end + end + + # Return any overrides for the given resource. + def resource_overrides(resource) + @resource_overrides[resource.ref] + end + + # Return a list of all resources. + def resources + @resource_table.values + end + + # Store a resource override. + def store_override(override) + override.override = true + + # If possible, merge the override in immediately. + if resource = @resource_table[override.ref] + resource.merge(override) + else + # Otherwise, store the override for later; these + # get evaluated in Resource#finish. + @resource_overrides[override.ref] << override + end + end + + # Store a resource in our resource table. + def store_resource(scope, resource) + # This might throw an exception + verify_uniqueness(resource) + + # Store it in the global table. + @resource_table[resource.ref] = resource + + # And in the resource graph. At some point, this might supercede + # the global resource table, but the table is a lot faster + # so it makes sense to maintain for now. + @configuration.add_edge!(scope.resource, resource) + end + + private + + # If ast nodes are enabled, then see if we can find and evaluate one. + def evaluate_ast_node + return unless ast_nodes? + + # Now see if we can find the node. + astnode = nil + @node.names.each do |name| + break if astnode = @parser.nodes[name.to_s.downcase] + end + + unless astnode + astnode = @parser.nodes["default"] + end + unless astnode + raise Puppet::ParseError, "Could not find default node or by name with '%s'" % node.names.join(", ") + end + + # Create a resource to model this node, and then add it to the list + # of resources. + resource = Puppet::Parser::Resource.new(:type => "node", :title => astnode.classname, :scope => topscope, :source => topscope.source) + store_resource(topscope, resource) + @configuration.tag(astnode.classname) + end + + # Evaluate our collections and return true if anything returned an object. + # The 'true' is used to continue a loop, so it's important. + def evaluate_collections + return false if @collections.empty? + + found_something = false + exceptwrap do + @collections.each do |collection| + if collection.evaluate + found_something = true + end + end + end + + return found_something + end + + # Make sure all of our resources have been evaluated into native resources. + # We return true if any resources have, so that we know to continue the + # evaluate_generators loop. + def evaluate_definitions + exceptwrap do + if ary = unevaluated_resources + ary.each do |resource| + resource.evaluate + end + # If we evaluated, let the loop know. + return true + else + return false + end + end + end + + # Iterate over collections and resources until we're sure that the whole + # compile is evaluated. This is necessary because both collections + # and defined resources can generate new resources, which themselves could + # be defined resources. + def evaluate_generators + count = 0 + loop do + done = true + + # Call collections first, then definitions. + done = false if evaluate_collections + done = false if evaluate_definitions + break if done + if count > 1000 + raise Puppet::ParseError, "Somehow looped more than 1000 times while evaluating host configuration" + end + end + end + + # Find and evaluate our main object, if possible. + def evaluate_main + @main = @parser.findclass("", "") || @parser.newclass("") + @topscope.source = @main + @main_resource = Puppet::Parser::Resource.new(:type => "class", :title => :main, :scope => @topscope, :source => @main) + @topscope.resource = @main_resource + + @configuration.add_vertex!(@main_resource) + + @resource_table["Class[main]"] = @main_resource + end + + # Make sure the entire configuration is evaluated. + def fail_on_unevaluated + fail_on_unevaluated_overrides + fail_on_unevaluated_resource_collections + end + + # If there are any resource overrides remaining, then we could + # not find the resource they were supposed to override, so we + # want to throw an exception. + def fail_on_unevaluated_overrides + remaining = [] + @resource_overrides.each do |name, overrides| + remaining += overrides + end + + unless remaining.empty? + fail Puppet::ParseError, + "Could not find object(s) %s" % remaining.collect { |o| + o.ref + }.join(", ") + end + end + + # Make sure we don't have any remaining collections that specifically + # look for resources, because we want to consider those to be + # parse errors. + def fail_on_unevaluated_resource_collections + remaining = [] + @collections.each do |coll| + # We're only interested in the 'resource' collections, + # which result from direct calls of 'realize'. Anything + # else is allowed not to return resources. + # Collect all of them, so we have a useful error. + if r = coll.resources + if r.is_a?(Array) + remaining += r + else + remaining << r + end + end + end + + unless remaining.empty? + raise Puppet::ParseError, "Failed to realize virtual resources %s" % + remaining.join(', ') + end + end + + # Make sure all of our resources and such have done any last work + # necessary. + def finish + @resource_table.each { |name, resource| resource.finish if resource.respond_to?(:finish) } + end + + # Initialize the top-level scope, class, and resource. + def init_main + # Create our initial scope and a resource that will evaluate main. + @topscope = Puppet::Parser::Scope.new(:compile => self, :parser => self.parser) + @scope_graph.add_vertex!(@topscope) + end + + # Set up all of our internal variables. + def initvars + # The table for storing class singletons. This will only actually + # be used by top scopes and node scopes. + @class_scopes = {} + + # The table for all defined resources. + @resource_table = {} + + # The list of objects that will available for export. + @exported_resources = {} + + # The list of overrides. This is used to cache overrides on objects + # that don't exist yet. We store an array of each override. + @resource_overrides = Hash.new do |overs, ref| + overs[ref] = [] + end + + # The list of collections that have been created. This is a global list, + # but they each refer back to the scope that created them. + @collections = [] + + # A list of tags we've generated; most class names. + @tags = [] + + # A graph for maintaining scope relationships. + @scope_graph = GRATR::Digraph.new + @scope_graph.add_vertex!(@topscope) + + # For maintaining the relationship between scopes and their resources. + @configuration = Puppet::Node::Configuration.new(@node.name) + @configuration.version = @parser.version + end + + # Set the node's parameters into the top-scope as variables. + def set_node_parameters + node.parameters.each do |param, value| + @topscope.setvar(param, value) + end + end + + # Store the configuration into the database. + def store + unless Puppet.features.rails? + raise Puppet::Error, + "storeconfigs is enabled but rails is unavailable" + end + + unless ActiveRecord::Base.connected? + Puppet::Rails.connect + end + + # We used to have hooks here for forking and saving, but I don't + # think it's worth retaining at this point. + store_to_active_record(@node, @resource_table.values) + end + + # Do the actual storage. + def store_to_active_record(node, resources) + begin + # We store all of the objects, even the collectable ones + benchmark(:info, "Stored configuration for #{node.name}") do + Puppet::Rails::Host.transaction do + Puppet::Rails::Host.store(node, resources) + end + end + rescue => detail + if Puppet[:trace] + puts detail.backtrace + end + Puppet.err "Could not store configs: %s" % detail.to_s + end + end + + # Return an array of all of the unevaluated resources. These will be definitions, + # which need to get evaluated into native resources. + def unevaluated_resources + ary = @resource_table.find_all do |name, object| + ! object.builtin? and ! object.evaluated? + end.collect { |name, object| object } + + if ary.empty? + return nil + else + return ary + end + end + + # Verify that the given resource isn't defined elsewhere. + def verify_uniqueness(resource) + # Short-curcuit the common case, + unless existing_resource = @resource_table[resource.ref] + return true + end + + if typeclass = Puppet::Type.type(resource.type) and ! typeclass.isomorphic? + Puppet.info "Allowing duplicate %s" % typeclass.name + return true + end + + # Either it's a defined type, which are never + # isomorphic, or it's a non-isomorphic type, so + # we should throw an exception. + msg = "Duplicate definition: %s is already defined" % resource.ref + + if existing_resource.file and existing_resource.line + msg << " in file %s at line %s" % + [existing_resource.file, existing_resource.line] + end + + if resource.line or resource.file + msg << "; cannot redefine" + end + + raise Puppet::ParseError.new(msg) + end +end diff --git a/lib/puppet/parser/configuration.rb b/lib/puppet/parser/configuration.rb deleted file mode 100644 index c7979e51f..000000000 --- a/lib/puppet/parser/configuration.rb +++ /dev/null @@ -1,133 +0,0 @@ -# Created by Luke A. Kanies on 2007-08-13. -# Copyright (c) 2007. All rights reserved. - -require 'puppet/external/gratr/digraph' -require 'puppet/external/gratr/import' -require 'puppet/external/gratr/dot' - -# Maintain a graph of scopes, along with a bunch of data -# about the individual configuration we're compiling. -class Puppet::Parser::Configuration - attr_reader :topscope, :interpreter, :host, :facts - - # Add a collection to the global list. - def add_collection(coll) - @collections << coll - end - - # Store the fact that we've evaluated a class, and store a reference to - # the scope in which it was evaluated, so that we can look it up later. - def class_set(name, scope) - @class_scopes[name] = scope - end - - # Return the scope associated with a class. This is just here so - # that subclasses can set their parent scopes to be the scope of - # their parent class, and it's also used when looking up qualified - # variables. - def class_scope(klass) - # They might pass in either the class or class name - if klass.respond_to?(:classname) - @class_scopes[klass.classname] - else - @class_scopes[klass] - end - end - - # Return a list of all of the defined classes. - def classlist - return @class_scopes.keys.reject { |k| k == "" } - end - - # Should the scopes behave declaratively? - def declarative? - true - end - - # Set up our configuration. We require an interpreter - # and a host name, and we normally are passed facts, too. - def initialize(options) - @interpreter = options[:interpreter] or - raise ArgumentError, "You must pass an interpreter to the configuration" - @facts = options[:facts] || {} - @host = options[:host] or - raise ArgumentError, "You must pass a host name to the configuration" - - # Call the setup methods from the base class. - super() - - initvars() - end - - # Create a new scope, with either a specified parent scope or - # using the top scope. Adds an edge between the scope and - # its parent to the graph. - def newscope(parent = nil) - parent ||= @topscope - scope = Puppet::Parser::Scope.new(:configuration => self) - @graph.add_edge!(parent, scope) - scope - end - - # Find the parent of a given scope. Assumes scopes only ever have - # one in edge, which will always be true. - def parent(scope) - if ary = @graph.adjacent(scope, :direction => :in) and ary.length > 0 - ary[0] - else - nil - end - end - - # Return an array of all of the unevaluated objects - def unevaluated - ary = @definedtable.find_all do |name, object| - ! object.builtin? and ! object.evaluated? - end.collect { |name, object| object } - - if ary.empty? - return nil - else - return ary - end - end - - private - - # Set up all of our internal variables. - def initvars - # The table for storing class singletons. This will only actually - # be used by top scopes and node scopes. - @class_scopes = {} - - # The table for all defined resources. - @resource_table = {} - - # The list of objects that will available for export. - @exported_resources = {} - - # The list of overrides. This is used to cache overrides on objects - # that don't exist yet. We store an array of each override. - @resource_overrides = Hash.new do |overs, ref| - overs[ref] = [] - end - - # The list of collections that have been created. This is a global list, - # but they each refer back to the scope that created them. - @collections = [] - - # Create our initial scope, our scope graph, and add the initial scope to the graph. - @topscope = Puppet::Parser::Scope.new(:configuration => self, :type => "main", :name => "top") - @graph = GRATR::Digraph.new - @graph.add_vertex!(@topscope) - end - - # Return the list of remaining overrides. - def overrides - @resource_overrides.values.flatten - end - - def resources - @resourcetable - end -end diff --git a/lib/puppet/parser/functions.rb b/lib/puppet/parser/functions.rb index 946501154..7ffdb6ccb 100644 --- a/lib/puppet/parser/functions.rb +++ b/lib/puppet/parser/functions.rb @@ -1,306 +1,307 @@ -# Grr require 'puppet/util/autoload' require 'puppet/parser/scope' module Puppet::Parser module Functions # A module for managing parser functions. Each specified function # becomes an instance method on the Scope class. class << self include Puppet::Util end def self.autoloader unless defined? @autoloader @autoloader = Puppet::Util::Autoload.new(self, "puppet/parser/functions", :wrap => false ) end @autoloader end # Create a new function type. def self.newfunction(name, options = {}, &block) @functions ||= {} name = symbolize(name) if @functions.include? name raise Puppet::DevError, "Function %s already defined" % name end # We want to use a separate, hidden module, because we don't want # people to be able to call them directly. unless defined? FCollection eval("module FCollection; end") end ftype = options[:type] || :statement unless ftype == :statement or ftype == :rvalue raise Puppet::DevError, "Invalid statement type %s" % ftype.inspect end fname = "function_" + name.to_s Puppet::Parser::Scope.send(:define_method, fname, &block) # Someday we'll support specifying an arity, but for now, nope #@functions[name] = {:arity => arity, :type => ftype} @functions[name] = {:type => ftype, :name => fname} if options[:doc] @functions[name][:doc] = options[:doc] end end # Determine if a given name is a function def self.function(name) name = symbolize(name) unless @functions.include? name autoloader.load(name) end if @functions.include? name return @functions[name][:name] else return false end end def self.functiondocs autoloader.loadall ret = "" @functions.sort { |a,b| a[0].to_s <=> b[0].to_s }.each do |name, hash| #ret += "%s\n%s\n" % [name, hash[:type]] ret += "%s\n%s\n" % [name, "-" * name.to_s.length] if hash[:doc] ret += hash[:doc].gsub(/\n\s*/, ' ') else ret += "Undocumented.\n" end ret += "\n\n- **Type**: %s\n\n" % hash[:type] end return ret end def self.functions @functions.keys end # Determine if a given function returns a value or not. def self.rvalue?(name) name = symbolize(name) if @functions.include? name case @functions[name][:type] when :statement: return false when :rvalue: return true end else return false end end # Include the specified classes newfunction(:include, :doc => "Evaluate one or more classes.") do |vals| - klasses = evalclasses(*vals) + vals = [vals] unless vals.is_a?(Array) + klasses = compile.evaluate_classes(vals, self) missing = vals.find_all do |klass| ! klasses.include?(klass) end unless missing.empty? # Throw an error if we didn't evaluate all of the classes. str = "Could not find class" if missing.length > 1 str += "es" end str += " " + missing.join(", ") if n = namespaces and ! n.empty? and n != [""] str += " in namespaces %s" % @namespaces.join(", ") end self.fail Puppet::ParseError, str end end # Tag the current scope with each passed name newfunction(:tag, :doc => "Add the specified tags to the containing class or definition. All contained objects will then acquire that tag, also. ") do |vals| - self.tag(*vals) + self.resource.tag(*vals) end # Test whether a given tag is set. This functions as a big OR -- if any of the # specified tags are unset, we return false. newfunction(:tagged, :type => :rvalue, :doc => "A boolean function that tells you whether the current container is tagged with the specified tags. The tags are ANDed, so that all of the specified tags must be included for the function to return true.") do |vals| - classlist = self.classlist + configtags = compile.configuration.tags + resourcetags = resource.tags retval = true vals.each do |val| - unless classlist.include?(val) or self.tags.include?(val) + unless configtags.include?(val) or resourcetags.include?(val) retval = false break end end return retval end # Test whether a given class or definition is defined newfunction(:defined, :type => :rvalue, :doc => "Determine whether a given type is defined, either as a native type or a defined type, or whether a class is defined. This is useful for checking whether a class is defined and only including it if it is. This function can also test whether a resource has been defined, using resource references (e.g., ``if defined(File['/tmp/myfile'] { ... }``). This function is unfortunately dependent on the parse order of the configuration when testing whether a resource is defined.") do |vals| result = false vals.each do |val| case val when String: # For some reason, it doesn't want me to return from here. if Puppet::Type.type(val) or finddefine(val) or findclass(val) result = true break end when Puppet::Parser::Resource::Reference: if findresource(val.to_s) result = true break end else raise ArgumentError, "Invalid argument of type %s to 'defined'" % val.class end end result end newfunction(:fail, :doc => "Fail with a parse error.") do |vals| vals = vals.collect { |s| s.to_s }.join(" ") if vals.is_a? Array raise Puppet::ParseError, vals.to_s end # Runs a newfunction to create a function for each of the log levels Puppet::Util::Log.levels.each do |level| newfunction(level, :doc => "Log a message on the server at level #{level.to_s}.") do |vals| send(level, vals.join(" ")) end end newfunction(:template, :type => :rvalue, :doc => "Evaluate a template and return its value. See `the templating docs`_ for more information. Note that if multiple templates are specified, their output is all concatenated and returned as the output of the function. .. _the templating docs: /trac/puppet/wiki/PuppetTemplating ") do |vals| require 'erb' vals.collect do |file| # Use a wrapper, so the template can't get access to the full # Scope object. debug "Retrieving template %s" % file wrapper = Puppet::Parser::TemplateWrapper.new(self, file) begin wrapper.result() rescue => detail raise Puppet::ParseError, "Failed to parse template %s: %s" % [file, detail] end end.join("") end # This is just syntactic sugar for a collection, although it will generally # be a good bit faster. newfunction(:realize, :doc => "Make a virtual object real. This is useful when you want to know the name of the virtual object and don't want to bother with a full collection. It is slightly faster than a collection, and, of course, is a bit shorter. You must pass the object using a reference; e.g.: ``realize User[luke]``." ) do |vals| coll = Puppet::Parser::Collector.new(self, :nomatter, nil, nil, :virtual) vals = [vals] unless vals.is_a?(Array) coll.resources = vals - newcollection(coll) + compile.add_collection(coll) end newfunction(:search, :doc => "Add another namespace for this class to search. This allows you to create classes with sets of definitions and add those classes to another class's search path.") do |vals| vals.each do |val| add_namespace(val) end end newfunction(:file, :type => :rvalue, :doc => "Return the contents of a file. Multiple files can be passed, and the first file that exists will be read in.") do |vals| ret = nil vals.each do |file| unless file =~ /^#{File::SEPARATOR}/ raise Puppet::ParseError, "Files must be fully qualified" end if FileTest.exists?(file) ret = File.read(file) break end end if ret ret else raise Puppet::ParseError, "Could not find any files from %s" % vals.join(", ") end end newfunction(:generate, :type => :rvalue, :doc => "Calls an external command and returns the results of the command. Any arguments are passed to the external command as arguments. If the generator does not exit with return code of 0, the generator is considered to have failed and a parse error is thrown. Generators can only have file separators, alphanumerics, dashes, and periods in them. This function will attempt to protect you from malicious generator calls (e.g., those with '..' in them), but it can never be entirely safe. No subshell is used to execute generators, so all shell metacharacters are passed directly to the generator.") do |args| unless args[0] =~ /^#{File::SEPARATOR}/ raise Puppet::ParseError, "Generators must be fully qualified" end unless args[0] =~ /^[-#{File::SEPARATOR}\w.]+$/ raise Puppet::ParseError, "Generators can only contain alphanumerics, file separators, and dashes" end if args[0] =~ /\.\./ raise Puppet::ParseError, "Can not use generators with '..' in them." end begin output = Puppet::Util.execute(args) rescue Puppet::ExecutionFailure => detail raise Puppet::ParseError, "Failed to execute generator %s: %s" % [args[0], detail] end output end end end # $Id$ diff --git a/lib/puppet/parser/grammar.ra b/lib/puppet/parser/grammar.ra index c7216186a..26cb42217 100644 --- a/lib/puppet/parser/grammar.ra +++ b/lib/puppet/parser/grammar.ra @@ -1,650 +1,650 @@ # vim: syntax=ruby # the parser class Puppet::Parser::Parser token LBRACK DQTEXT SQTEXT RBRACK LBRACE RBRACE SYMBOL FARROW COMMA TRUE token FALSE EQUALS LESSEQUAL NOTEQUAL DOT COLON TYPE 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 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 | import | fstatement | definition | hostclass | nodedef | resourceoverride fstatement: NAME LPAREN funcvalues RPAREN { args = aryfy(val[2]) result = ast AST::Function, :name => val[0], :arguments => args, :ftype => :statement } | NAME LPAREN RPAREN { result = ast AST::Function, :name => val[0], :arguments => AST::ASTArray.new({}), :ftype => :statement } | NAME funcvalues { args = aryfy(val[1]) result = ast AST::Function, :name => val[0], :arguments => args, :ftype => :statement } funcvalues: namestrings | resourcerefs namestrings: namestring | namestrings COMMA namestring { result = aryfy(val[0], val[2]) result.line = @lexer.line result.file = @lexer.file } # 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 | CLASSNAME { result = ast AST::Name, :value => val[0] } resourcerefs: resourceref | resourcerefs COMMA resourceref { unless val[0].is_a?(AST::ASTArray) val[0] = aryfy(val[0]) end val[0].push(val[2]) result = val[0] } resource: classname LBRACE resourceinstances endsemi RBRACE { array = val[2] - if array.instance_of?(AST::ResourceInst) + if array.instance_of?(AST::ResourceInstance) array = [array] end result = ast AST::ASTArray # this iterates across each specified resourceinstance array.each { |instance| - unless instance.instance_of?(AST::ResourceInst) + 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::ResourceDef, + result.push ast(AST::Resource, :type => val[0], :title => instance[0], :params => 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 result = ast(AST::ResourceDefaults, :type => val[0], :params => val[2]) } # Override a value set elsewhere in the configuration. resourceoverride: resourceref LBRACE anyparams endcomma RBRACE { result = ast AST::ResourceOverride, :object => val[0], :params => 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] error "You cannot collect without storeconfigs being set" end if val[1].is_a? AST::ResourceDefaults error "Defaults are not virtualizable" end 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: collectname 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] error "You cannot collect exported resources without storeconfigs being set" end result = ast AST::Collection, args } collectname: TYPE | NAME 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 | OR collexpr: colllval ISEQUAL simplervalue { result = ast AST::CollExpr, :test1 => val[0], :oper => val[1], :test2 => val[2] #result = ast AST::CollExpr #result.push *val } | colllval NOTEQUAL simplervalue { result = ast AST::CollExpr, :test1 => val[0], :oper => val[1], :test2 => val[2] #result = ast AST::CollExpr #result.push *val } colllval: variable | name resourceinst: resourcename COLON params endcomma { - result = ast AST::ResourceInst, :children => [val[0],val[2]] + result = ast AST::ResourceInstance, :children => [val[0],val[2]] } resourceinstances: resourceinst | resourceinstances SEMIC resourceinst { - if val[0].instance_of?(AST::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] } type: TYPE { result = ast AST::Type, :value => val[0] } resourcename: quotedtext | name | type | selector | variable | array assignment: VARIABLE EQUALS rvalue { if val[0] =~ /::/ 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] result = ast AST::VarDef, :name => variable, :value => val[2] } 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 => val[2] } addparam: NAME PARROW rvalue { result = ast AST::ResourceParam, :param => val[0], :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 | 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], :arguments => args, :ftype => :rvalue } | NAME LPAREN RPAREN { result = ast AST::Function, :name => val[0], :arguments => AST::ASTArray.new({}), :ftype => :rvalue } quotedtext: DQTEXT { result = ast AST::String, :value => val[0] } | SQTEXT { result = ast AST::FlatString, :value => val[0] } boolean: BOOLEAN { result = ast AST::Boolean, :value => val[0] } resourceref: NAME LBRACK rvalue RBRACK { Puppet.warning addcontext("Deprecation notice: Resource references should now be capitalized") - result = ast AST::ResourceRef, :type => val[0], :title => val[2] + result = ast AST::ResourceReference, :type => val[0], :title => val[2] } | classref LBRACK rvalue RBRACK { - result = ast AST::ResourceRef, :type => val[0], :title => val[2] + result = ast AST::ResourceReference, :type => val[0], :title => val[2] } ifstatement: IF iftest LBRACE statements RBRACE else { args = { :test => val[1], :statements => val[3] } if val[5] args[:else] = val[5] end result = ast AST::IfStatement, args } else: # nothing | ELSE LBRACE statements RBRACE { result = ast AST::Else, :statements => val[2] } # Currently we only support a single value, but eventually one assumes # we'll support operators and such. iftest: rvalue casestatement: CASE rvalue LBRACE caseopts RBRACE { 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 } 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 { result = ast AST::CaseOpt, :value => val[0], :statements => val[3] } | casevalues COLON LBRACE RBRACE { 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 { 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 | DEFAULT { result = ast AST::Default, :value => val[0] } # These are only used for importing, and we don't interpolate there. qtexts: quotedtext { result = [val[0].value] } | qtexts COMMA quotedtext { results = val[0] << val[2].value } import: IMPORT qtexts { 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 { newdefine classname(val[1]), :arguments => val[2], :code => val[4] @lexer.indefine = false result = nil #} | DEFINE NAME argumentlist parent LBRACE RBRACE { } | DEFINE classname argumentlist LBRACE RBRACE { newdefine classname(val[1]), :arguments => val[2] @lexer.indefine = false result = nil } #hostclass: CLASS NAME argumentlist parent LBRACE statements RBRACE { hostclass: CLASS classname classparent LBRACE statements RBRACE { # Our class gets defined in the parent namespace, not our own. @lexer.namepop newclass classname(val[1]), :code => val[4], :parent => val[2] result = nil } | CLASS classname classparent LBRACE RBRACE { # Our class gets defined in the parent namespace, not our own. @lexer.namepop newclass classname(val[1]), :parent => val[2] result = nil } nodedef: NODE hostnames nodeparent LBRACE statements RBRACE { newnode val[1], :parent => val[2], :code => val[4] result = nil } | NODE hostnames nodeparent LBRACE RBRACE { newnode val[1], :parent => val[2] result = nil } classref: TYPE | CLASSREF classname: NAME | CLASSNAME # Multiple hostnames, as used for node names. These are all literal # strings, not AST objects. hostnames: hostname | hostnames COMMA hostname { result = val[0] result = [result] unless result.is_a?(Array) result << val[2] } hostname: NAME | SQTEXT | DQTEXT | DEFAULT 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], val[2]] } | NAME { Puppet.warning addcontext("Deprecation notice: must now include '$' in prototype") result = [val[0]] } | VARIABLE EQUALS rvalue { result = [val[0], val[2]] } | VARIABLE { result = [val[0]] } 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] } array: LBRACK rvalues 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 } end ---- header ---- require 'puppet' require 'puppet/util/loadedfile' require 'puppet/parser/lexer' require 'puppet/parser/ast' #require 'puppet/parser/interpreter' module Puppet class ParseError < Puppet::Error; end class ImportError < Racc::ParseError; end class AlreadyImportedError < ImportError; end end Puppet[:typecheck] = true Puppet[:paramcheck] = true ---- 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/interpreter.rb b/lib/puppet/parser/interpreter.rb index 18bf31087..a4ea26572 100644 --- a/lib/puppet/parser/interpreter.rb +++ b/lib/puppet/parser/interpreter.rb @@ -1,550 +1,105 @@ require 'puppet' require 'timeout' require 'puppet/rails' require 'puppet/util/methodhelper' require 'puppet/parser/parser' +require 'puppet/parser/compile' require 'puppet/parser/scope' -# The interpreter's job is to convert from a parsed file to the configuration -# for a given client. It really doesn't do any work on its own, it just collects -# and calls out to other objects. +# The interpreter is a very simple entry-point class that +# manages the existence of the parser (e.g., replacing it +# when files are reparsed). You can feed it a node and +# get the node's configuration back. class Puppet::Parser::Interpreter - class NodeDef - include Puppet::Util::MethodHelper - attr_accessor :name, :classes, :parameters, :source - - def evaluate(options) - begin - parameters.each do |param, value| - # Don't try to override facts with these parameters - options[:scope].setvar(param, value) unless options[:scope].lookupvar(param, false) != :undefined - end - - # Also, set the 'nodename', since it might not be obvious how the node was looked up - options[:scope].setvar("nodename", @name) unless options[:scope].lookupvar(@nodename, false) != :undefined - rescue => detail - raise Puppet::ParseError, "Could not set parameters for %s: %s" % [name, detail] - end - - # Then evaluate the classes. - begin - options[:scope].function_include(classes.find_all { |c| options[:scope].findclass(c) }) - rescue => detail - puts detail.backtrace - raise Puppet::ParseError, "Could not evaluate classes for %s: %s" % [name, detail] - end - end - - def initialize(args) - set_options(args) - - raise Puppet::DevError, "NodeDefs require names" unless self.name - - if self.classes.is_a?(String) - @classes = [@classes] - else - @classes ||= [] - end - @parameters ||= {} - end - - def safeevaluate(args) - evaluate(args) - end - end - include Puppet::Util attr_accessor :usenodes - - class << self - attr_writer :ldap - end - - # just shorten the constant path a bit, using what amounts to an alias - AST = Puppet::Parser::AST + attr_accessor :code, :file include Puppet::Util::Errors - # Create an ldap connection. This is a class method so others can call - # it and use the same variables and such. - def self.ldap - unless defined? @ldap and @ldap - if Puppet[:ldapssl] - @ldap = LDAP::SSLConn.new(Puppet[:ldapserver], Puppet[:ldapport]) - elsif Puppet[:ldaptls] - @ldap = LDAP::SSLConn.new( - Puppet[:ldapserver], Puppet[:ldapport], true - ) - else - @ldap = LDAP::Conn.new(Puppet[:ldapserver], Puppet[:ldapport]) - end - @ldap.set_option(LDAP::LDAP_OPT_PROTOCOL_VERSION, 3) - @ldap.set_option(LDAP::LDAP_OPT_REFERRALS, LDAP::LDAP_OPT_ON) - @ldap.simple_bind(Puppet[:ldapuser], Puppet[:ldappassword]) - end - - return @ldap - end - - # Make sure we don't have any remaining collections that specifically - # look for resources, because we want to consider those to be - # parse errors. - def check_resource_collections(scope) - remaining = [] - scope.collections.each do |coll| - if r = coll.resources - if r.is_a?(Array) - remaining += r - else - remaining << r - end - end - end - unless remaining.empty? - raise Puppet::ParseError, "Failed to find virtual resources %s" % - remaining.join(', ') - end - end - - # Iteratively evaluate all of the objects. This finds all of the objects - # that represent definitions and evaluates the definitions appropriately. - # It also adds defaults and overrides as appropriate. - def evaliterate(scope) - count = 0 - loop do - count += 1 - done = true - # First perform collections, so we can collect defined types. - if coll = scope.collections and ! coll.empty? - exceptwrap do - coll.each do |c| - # Only keep the loop going if we actually successfully - # collected something. - if o = c.evaluate - done = false - end - end - end - end - - # Then evaluate any defined types. - if ary = scope.unevaluated - ary.each do |resource| - resource.evaluate - end - # If we evaluated, then loop through again. - done = false - end - break if done - - if count > 1000 - raise Puppet::ParseError, "Got 1000 class levels, which is unsupported" - end - end - end - - # Evaluate a specific node. - def evalnode(client, scope, facts) - return unless self.usenodes - - unless client - raise Puppet::Error, - "Cannot evaluate nodes with a nil client" - end - names = [client] - - # Make sure both the fqdn and the short name of the - # host can be used in the manifest - if client =~ /\./ - names << client.sub(/\..+/,'') - else - names << "#{client}.#{facts['domain']}" - end - - if names.empty? - raise Puppet::Error, - "Cannot evaluate nodes with a nil client" - end - - # Look up our node object. - if nodeclass = nodesearch(*names) - nodeclass.safeevaluate :scope => scope - else - raise Puppet::Error, "Could not find %s with names %s" % - [client, names.join(", ")] - end - end - - # Evaluate all of the code we can find that's related to our client. - def evaluate(client, facts) - scope = Puppet::Parser::Scope.new(:interp => self) # no parent scope - scope.name = "top" - scope.type = "main" - - scope.host = client || facts["hostname"] || Facter.value(:hostname) - - classes = @classes.dup - - # Okay, first things first. Set our facts. - scope.setfacts(facts) - - # Everyone will always evaluate the top-level class, if there is one. - if klass = findclass("", "") - # Set the source, so objects can tell where they were defined. - scope.source = klass - klass.safeevaluate :scope => scope, :nosubscope => true - end - - # Next evaluate the node. We pass the facts so they can be used - # when building the list of names for which to search. - evalnode(client, scope, facts) - - # If we were passed any classes, evaluate those. - if classes - classes.each do |klass| - if klassobj = findclass("", klass) - klassobj.safeevaluate :scope => scope - end - end - end - - # That was the first pass evaluation. Now iteratively evaluate - # until we've gotten rid of all of everything or thrown an error. - evaliterate(scope) - - # Now make sure we fail if there's anything left to do - failonleftovers(scope) - - # Now finish everything. This recursively calls finish on the - # contained scopes and resources. - scope.finish - - # Store everything. We need to do this before translation, because - # it operates on resources, not transobjects. - if Puppet[:storeconfigs] - args = { - :resources => scope.resources, - :name => scope.host, - :facts => facts - } - unless scope.classlist.empty? - args[:classes] = scope.classlist - end - - storeconfigs(args) - end - - # Now, finally, convert our scope tree + resources into a tree of - # buckets and objects. - objects = scope.translate - - # Add the class list - unless scope.classlist.empty? - objects.classes = scope.classlist - end - - return objects + # Determine the configuration version for a given node's environment. + def configuration_version(node) + parser(node.environment).version end - # Fail if there any overrides left to perform. - def failonleftovers(scope) - overrides = scope.overrides - unless overrides.empty? - fail Puppet::ParseError, - "Could not find object(s) %s" % overrides.collect { |o| - o.ref - }.join(", ") - end - - # Now check that there aren't any extra resource collections. - check_resource_collections(scope) - end - - # Create proxy methods, so the scopes can call the interpreter, since - # they don't have access to the parser. - def findclass(namespace, name) - @parser.findclass(namespace, name) - end - def finddefine(namespace, name) - @parser.finddefine(namespace, name) + # evaluate our whole tree + def compile(node) + return Puppet::Parser::Compile.new(node, parser(node.environment), :ast_nodes => usenodes?).compile end # create our interpreter - def initialize(hash) - if @code = hash[:Code] - @file = nil # to avoid warnings - elsif ! @file = hash[:Manifest] - devfail "You must provide code or a manifest" + def initialize(options = {}) + if @code = options[:Code] + elsif @file = options[:Manifest] end - if hash.include?(:UseNodes) - @usenodes = hash[:UseNodes] + if options.include?(:UseNodes) + @usenodes = options[:UseNodes] else @usenodes = true end - - if Puppet[:ldapnodes] - # Nodes in the file override nodes in ldap. - @nodesource = :ldap - elsif Puppet[:external_nodes] != "none" - @nodesource = :external - else - # By default, we only search for parsed nodes. - @nodesource = :code - end - - @setup = false - - # Set it to either the value or nil. This is currently only used - # by the cfengine module. - @classes = hash[:Classes] || [] - - @local = hash[:Local] || false - - if hash.include?(:ForkSave) - @forksave = hash[:ForkSave] - else - # This is just too dangerous right now. Sorry, it's going - # to have to be slow. - @forksave = false - end - # The class won't always be defined during testing. if Puppet[:storeconfigs] if Puppet.features.rails? Puppet::Rails.init else raise Puppet::Error, "Rails is missing; cannot store configurations" end end - @files = [] - - # Create our parser object - parsefiles - end - - # Pass these methods through to the parser. - [:newclass, :newdefine, :newnode].each do |name| - define_method(name) do |*args| - @parser.send(name, *args) - end - end - - # Add a new file to be checked when we're checking to see if we should be - # reparsed. - def newfile(*files) - files.each do |file| - unless file.is_a? Puppet::Util::LoadedFile - file = Puppet::Util::LoadedFile.new(file) - end - @files << file - end - end - - # Search for our node in the various locations. - def nodesearch(*nodes) - nodes = nodes.collect { |n| n.to_s.downcase } - - method = "nodesearch_%s" % @nodesource - # Do an inverse sort on the length, so the longest match always - # wins - nodes.sort { |a,b| b.length <=> a.length }.each do |node| - node = node.to_s if node.is_a?(Symbol) - if obj = self.send(method, node) - if obj.is_a?(AST::Node) - nsource = obj.file - else - nsource = obj.source - end - Puppet.info "Found %s in %s" % [node, nsource] - return obj - end - end - - # If they made it this far, we haven't found anything, so look for a - # default node. - unless nodes.include?("default") - if defobj = self.nodesearch("default") - Puppet.notice "Using default node for %s" % [nodes[0]] - return defobj - end - end - - return nil + @parsers = {} end - # See if our node was defined in the code. - def nodesearch_code(name) - @parser.nodes[name] - end - - def parsedate - parsefiles() - @parsedate - end - - # evaluate our whole tree - def run(client, facts) - # We have to leave this for after initialization because there - # seems to be a problem keeping ldap open after a fork. - unless @setup - method = "setup_%s" % @nodesource.to_s - if respond_to? method - exceptwrap :type => Puppet::Error, - :message => "Could not set up node source %s" % @nodesource do - self.send(method) - end - end - end - parsefiles() - - # Evaluate all of the appropriate code. - objects = evaluate(client, facts) - - # And return it all. - return objects - end - - # Connect to the LDAP Server - def setup_ldap - self.class.ldap = nil - unless Puppet.features.ldap? - Puppet.notice( - "Could not set up LDAP Connection: Missing ruby/ldap libraries" - ) - @ldap = nil - return - end - - begin - @ldap = self.class.ldap() - rescue => detail - raise Puppet::Error, "Could not connect to LDAP: %s" % detail - end - end - - def scope - return @scope + # Should we parse ast nodes? + def usenodes? + defined?(@usenodes) and @usenodes end private - # Check whether any of our files have changed. - def checkfiles - if @files.find { |f| f.changed? } - @parsedate = Time.now.to_i - end - end - - # Parse the files, generating our parse tree. This automatically - # reparses only if files are updated, so it's safe to call multiple - # times. - def parsefiles - # First check whether there are updates to any non-puppet files - # like templates. If we need to reparse, this will get quashed, - # but it needs to be done first in case there's no reparse - # but there are other file changes. - checkfiles() - - # Check if the parser should reparse. - if @file - if defined? @parser - if stamp = @parser.reparse? - Puppet.notice "Reloading files" - else - return false - end - end - - unless FileTest.exists?(@file) - # If we've already parsed, then we're ok. - if findclass("", "") - return - else - raise Puppet::Error, "Manifest %s must exist" % @file - end - end - end - - # Create a new parser, just to keep things fresh. Don't replace our - # current parser until we know weverything works. - newparser = Puppet::Parser::Parser.new() - if @code - newparser.string = @code - else - newparser.file = @file - end - - # Parsing stores all classes and defines and such in their - # various tables, so we don't worry about the return. + # Create a new parser object and pre-parse the configuration. + def create_parser(environment) begin - if @local - newparser.parse + parser = Puppet::Parser::Parser.new(:environment => environment) + if self.code + parser.string = self.code + elsif self.file + parser.file = self.file else - benchmark(:info, "Parsed manifest") do - newparser.parse - end - end - # We've gotten this far, so it's ok to swap the parsers. - oldparser = @parser - @parser = newparser - if oldparser - oldparser.clear + file = Puppet.config.value(:manifest, environment) + parser.file = file end - - # Mark when we parsed, so we can check freshness - @parsedate = Time.now.to_i + parser.parse + return parser rescue => detail if Puppet[:trace] puts detail.backtrace end - Puppet.err "Could not parse; using old configuration: %s" % detail + msg = "Could not parse" + if environment and environment != "" + msg += " for environment %s" % environment + end + msg += ": %s" % detail + raise Puppet::Error, detail end end - # Store the configs into the database. - def storeconfigs(hash) - unless Puppet.features.rails? - raise Puppet::Error, - "storeconfigs is enabled but rails is unavailable" - end - - unless ActiveRecord::Base.connected? - Puppet::Rails.connect - end - - # Fork the storage, since we don't need the client waiting - # on that. How do I avoid this duplication? - if @forksave - fork { - # We store all of the objects, even the collectable ones - benchmark(:info, "Stored configuration for #{hash[:name]}") do - # Try to batch things a bit, by putting them into - # a transaction - Puppet::Rails::Host.transaction do - Puppet::Rails::Host.store(hash) - end - end - } - else + # Return the parser for a specific environment. + def parser(environment) + if ! @parsers[environment] or @parsers[environment].reparse? + # This will throw an exception if it does not succeed. We only + # want to get rid of the old parser if we successfully create a new + # one. begin - # We store all of the objects, even the collectable ones - benchmark(:info, "Stored configuration for #{hash[:name]}") do - Puppet::Rails::Host.transaction do - Puppet::Rails::Host.store(hash) - end - end - rescue => detail - if Puppet[:trace] - puts detail.backtrace - end - Puppet.err "Could not store configs: %s" % detail.to_s + tmp = create_parser(environment) + @parsers[environment].clear if @parsers[environment] + @parsers[environment] = tmp + rescue + # Nothing, yo. end end + @parsers[environment] end end - -# $Id$ diff --git a/lib/puppet/parser/parser.rb b/lib/puppet/parser/parser.rb index 21a054223..907bddc0a 100644 --- a/lib/puppet/parser/parser.rb +++ b/lib/puppet/parser/parser.rb @@ -1,1748 +1,1748 @@ # # DO NOT MODIFY!!!! # This file is automatically generated by racc 1.4.5 # from racc grammer file "grammar.ra". # require 'racc/parser' require 'puppet' require 'puppet/util/loadedfile' require 'puppet/parser/lexer' require 'puppet/parser/ast' #require 'puppet/parser/interpreter' module Puppet class ParseError < Puppet::Error; end class ImportError < Racc::ParseError; end class AlreadyImportedError < ImportError; end end Puppet[:typecheck] = true Puppet[:paramcheck] = true module Puppet module Parser class Parser < Racc::Parser -module_eval <<'..end grammar.ra modeval..idc5e5087e93', 'grammar.ra', 640 +module_eval <<'..end grammar.ra modeval..id8b4fcf8e20', 'grammar.ra', 640 # 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..idc5e5087e93 +..end grammar.ra modeval..id8b4fcf8e20 ##### racc 1.4.5 generates ### racc_reduce_table = [ 0, 0, :racc_error, 1, 53, :_reduce_1, 1, 53, :_reduce_none, 1, 54, :_reduce_none, 2, 54, :_reduce_4, 1, 56, :_reduce_none, 1, 56, :_reduce_none, 1, 56, :_reduce_none, 1, 56, :_reduce_none, 1, 56, :_reduce_none, 1, 56, :_reduce_none, 1, 56, :_reduce_none, 1, 56, :_reduce_none, 1, 56, :_reduce_none, 1, 56, :_reduce_none, 1, 56, :_reduce_none, 1, 56, :_reduce_none, 4, 64, :_reduce_17, 3, 64, :_reduce_18, 2, 64, :_reduce_19, 1, 69, :_reduce_none, 1, 69, :_reduce_none, 1, 70, :_reduce_none, 3, 70, :_reduce_23, 1, 72, :_reduce_none, 1, 72, :_reduce_none, 1, 72, :_reduce_none, 1, 72, :_reduce_none, 1, 72, :_reduce_none, 1, 72, :_reduce_none, 1, 72, :_reduce_none, 1, 72, :_reduce_31, 1, 71, :_reduce_none, 3, 71, :_reduce_33, 5, 57, :_reduce_34, 5, 57, :_reduce_35, 5, 57, :_reduce_36, 5, 68, :_reduce_37, 2, 58, :_reduce_38, 1, 88, :_reduce_39, 2, 88, :_reduce_40, 2, 59, :_reduce_41, 1, 89, :_reduce_none, 1, 89, :_reduce_none, 3, 90, :_reduce_44, 3, 90, :_reduce_45, 1, 91, :_reduce_none, 1, 91, :_reduce_none, 3, 91, :_reduce_48, 1, 92, :_reduce_none, 3, 92, :_reduce_50, 1, 93, :_reduce_none, 1, 93, :_reduce_none, 3, 94, :_reduce_53, 3, 94, :_reduce_54, 1, 95, :_reduce_none, 1, 95, :_reduce_none, 4, 97, :_reduce_57, 1, 82, :_reduce_none, 3, 82, :_reduce_59, 0, 83, :_reduce_none, 1, 83, :_reduce_none, 1, 99, :_reduce_62, 1, 73, :_reduce_63, 1, 75, :_reduce_64, 1, 98, :_reduce_none, 1, 98, :_reduce_none, 1, 98, :_reduce_none, 1, 98, :_reduce_none, 1, 98, :_reduce_none, 1, 98, :_reduce_none, 3, 60, :_reduce_71, 0, 84, :_reduce_72, 1, 84, :_reduce_73, 3, 84, :_reduce_74, 3, 102, :_reduce_75, 3, 103, :_reduce_76, 1, 104, :_reduce_none, 1, 104, :_reduce_none, 0, 87, :_reduce_79, 1, 87, :_reduce_80, 3, 87, :_reduce_81, 1, 105, :_reduce_none, 3, 105, :_reduce_83, 1, 96, :_reduce_none, 1, 96, :_reduce_none, 1, 96, :_reduce_none, 1, 96, :_reduce_none, 1, 96, :_reduce_none, 1, 96, :_reduce_none, 1, 101, :_reduce_none, 1, 101, :_reduce_none, 1, 101, :_reduce_none, 1, 101, :_reduce_none, 1, 101, :_reduce_none, 1, 101, :_reduce_none, 1, 101, :_reduce_none, 1, 101, :_reduce_none, 1, 101, :_reduce_none, 1, 101, :_reduce_none, 4, 77, :_reduce_100, 3, 77, :_reduce_101, 1, 79, :_reduce_102, 1, 79, :_reduce_103, 1, 76, :_reduce_104, 4, 80, :_reduce_105, 4, 80, :_reduce_106, 6, 62, :_reduce_107, 0, 108, :_reduce_none, 4, 108, :_reduce_109, 1, 107, :_reduce_none, 5, 61, :_reduce_111, 1, 109, :_reduce_none, 2, 109, :_reduce_113, 5, 110, :_reduce_114, 4, 110, :_reduce_115, 1, 111, :_reduce_none, 3, 111, :_reduce_117, 3, 78, :_reduce_118, 1, 113, :_reduce_none, 4, 113, :_reduce_120, 1, 115, :_reduce_none, 3, 115, :_reduce_122, 3, 114, :_reduce_123, 1, 112, :_reduce_none, 1, 112, :_reduce_none, 1, 112, :_reduce_none, 1, 112, :_reduce_none, 1, 112, :_reduce_none, 1, 112, :_reduce_none, 1, 112, :_reduce_none, 1, 112, :_reduce_131, 1, 116, :_reduce_132, 3, 116, :_reduce_133, 2, 63, :_reduce_134, 6, 65, :_reduce_135, 5, 65, :_reduce_136, 6, 66, :_reduce_137, 5, 66, :_reduce_138, 6, 67, :_reduce_139, 5, 67, :_reduce_140, 1, 86, :_reduce_none, 1, 86, :_reduce_none, 1, 81, :_reduce_none, 1, 81, :_reduce_none, 1, 119, :_reduce_none, 3, 119, :_reduce_146, 1, 121, :_reduce_none, 1, 121, :_reduce_none, 1, 121, :_reduce_none, 1, 121, :_reduce_none, 0, 55, :_reduce_151, 0, 122, :_reduce_152, 1, 117, :_reduce_none, 3, 117, :_reduce_154, 3, 117, :_reduce_155, 1, 123, :_reduce_none, 3, 123, :_reduce_157, 3, 124, :_reduce_158, 1, 124, :_reduce_159, 3, 124, :_reduce_160, 1, 124, :_reduce_161, 1, 120, :_reduce_none, 2, 120, :_reduce_163, 1, 118, :_reduce_none, 2, 118, :_reduce_165, 1, 125, :_reduce_none, 1, 125, :_reduce_none, 1, 74, :_reduce_168, 3, 100, :_reduce_169, 2, 100, :_reduce_170, 1, 106, :_reduce_none, 1, 106, :_reduce_none, 0, 85, :_reduce_none, 1, 85, :_reduce_174 ] racc_reduce_n = 175 racc_shift_n = 279 racc_action_table = [ 81, 60, 64, 103, 173, 165, 81, 60, 64, 145, 37, 167, 58, -141, 51, 97, 70, -141, 115, 107, 166, -125, 70, 41, 81, 60, 64, 44, 45, 37, 196, 51, 142, 162, 163, 63, 66, 51, 10, 72, 130, 63, 66, 114, 10, 72, 172, 42, 68, 36, 10, 81, 60, 64, 68, 51, 5, 10, 217, 63, 127, 173, 46, 72, 180, 47, 251, 70, 207, 81, 60, 64, 68, 211, 142, 81, 60, 64, 103, 250, 44, 45, 51, 162, 163, 70, 63, 66, 197, 51, 72, 70, 199, 200, 107, 10, 81, 60, 64, 68, 51, 181, 245, 95, 63, 66, 51, 96, 72, 244, 63, 66, 70, 10, 72, 46, -130, 68, 47, 10, 81, 60, 64, 68, 199, 262, 36, 51, 191, 159, 36, 63, 66, 5, -129, 72, 70, 5, -125, 36, 10, 58, 60, 64, 68, -143, 5, 81, 60, 64, 95, 51, 44, 45, 179, 63, 66, 70, 103, 72, -127, 54, -124, 70, 10, 81, 60, 64, 68, 51, 207, -124, 51, 176, 107, 211, 63, 66, 51, 212, 72, 70, 63, 66, 55, 10, 72, 46, 213, 68, 47, 10, 81, 60, 64, 68, 51, 60, 64, 173, 63, 66, 168, 103, 72, 179, 162, 163, 70, 10, 81, 60, 64, 68, 51, -141, 81, 60, 64, 107, 60, 64, -126, 51, 175, 143, 70, 63, 66, -128, 141, 72, 70, -126, 139, 231, 10, 81, 60, 64, 68, 51, 120, -128, 234, 63, 66, 51, 235, 72, 154, 63, 66, 130, 10, 72, 60, 64, 68, 154, 10, 238, 60, 64, 68, 142, 120, 241, 51, 171, -126, 130, 63, 186, -124, 147, 72, 130, 60, 64, 60, 64, 101, 248, 249, 68, 51, 160, 252, 96, 63, 186, 51, 130, 72, 130, 63, 186, 255, -127, 72, -126, -124, 68, 60, 64, 60, 64, 51, 68, 51, -127, 63, 186, 63, 186, 72, -125, 72, 70, -129, 130, 60, 64, 193, 68, 247, 68, 60, 64, -125, 187, 179, 94, 51, 156, 51, 130, 63, 66, 63, 186, 72, 130, 72, 263, 55, 10, 265, 154, -127, 68, 51, 68, 150, 149, 63, 186, 51, 50, 72, 49, 63, 186, 60, 64, 72, 272, -174, 68, 60, 64, 273, 148, -129, 68, 34, 58, nil, 70, 60, 64, nil, nil, 137, 130, nil, nil, nil, nil, nil, nil, nil, nil, 51, 130, nil, nil, 63, 66, 51, nil, 72, nil, 63, 186, 55, 10, 72, 278, 51, 68, nil, nil, 63, 186, nil, 68, 72, nil, 23, nil, 55, nil, nil, nil, nil, 68, 243, nil, 20, nil, 25, 27, nil, 1, 6, nil, 13, 23, 18, nil, 22, nil, 28, nil, nil, 5, 10, 20, nil, 25, 27, 253, 1, 6, nil, 13, nil, 18, nil, 22, nil, 28, 23, nil, 5, 10, nil, nil, nil, nil, 274, nil, 20, nil, 25, 27, nil, 1, 6, nil, 13, 23, 18, nil, 22, nil, 28, nil, nil, 5, 10, 20, nil, 25, 27, 227, 1, 6, nil, 13, nil, 18, nil, 22, nil, 28, 23, nil, 5, 10, nil, nil, nil, nil, 256, nil, 20, nil, 25, 27, nil, 1, 6, nil, 13, 23, 18, nil, 22, nil, 28, nil, nil, 5, 10, 20, nil, 25, 27, 257, 1, 6, nil, 13, nil, 18, nil, 22, nil, 28, 23, nil, 5, 10, nil, nil, nil, nil, 277, nil, 20, nil, 25, 27, nil, 1, 6, nil, 13, 23, 18, nil, 22, nil, 28, nil, nil, 5, 10, 20, nil, 25, 27, 270, 1, 6, nil, 13, nil, 18, nil, 22, nil, 28, 23, nil, 5, 10, nil, nil, nil, nil, 215, nil, 20, nil, 25, 27, nil, 1, 6, nil, 13, 23, 18, nil, 22, nil, 28, nil, nil, 5, 10, 20, nil, 25, 27, nil, 1, 6, nil, 13, 23, 18, nil, 22, nil, 28, nil, nil, 5, 10, 20, nil, 25, 27, nil, 1, 6, nil, 13, 23, 18, nil, 22, nil, 28, nil, nil, 5, 10, 20, nil, 25, 27, nil, 1, 6, nil, 13, 23, 18, nil, 22, nil, 28, nil, nil, 5, 10, 20, nil, 25, 27, nil, 1, 6, nil, 13, 23, 18, nil, 22, nil, 28, nil, nil, 5, 10, 20, nil, 25, 27, nil, 1, 6, nil, 13, nil, 18, nil, 22, nil, 28, nil, nil, 5, 10 ] racc_action_check = [ 95, 95, 95, 41, 120, 105, 81, 81, 81, 81, 143, 110, 66, 23, 41, 31, 95, 23, 48, 41, 110, 133, 81, 7, 50, 50, 50, 115, 115, 2, 143, 95, 66, 105, 105, 95, 95, 81, 143, 95, 50, 81, 81, 48, 95, 81, 120, 7, 95, 2, 81, 198, 198, 198, 81, 50, 2, 2, 161, 50, 50, 127, 115, 50, 136, 115, 209, 198, 251, 173, 173, 173, 50, 251, 127, 58, 58, 58, 42, 209, 13, 13, 198, 161, 161, 173, 198, 198, 146, 42, 198, 58, 146, 146, 42, 198, 34, 34, 34, 198, 173, 138, 203, 29, 173, 173, 58, 29, 173, 203, 58, 58, 34, 173, 58, 13, 77, 173, 13, 58, 172, 172, 172, 58, 240, 240, 101, 34, 140, 101, 6, 34, 34, 101, 76, 34, 172, 6, 75, 27, 34, 18, 18, 18, 34, 18, 27, 20, 20, 20, 74, 172, 114, 114, 131, 172, 172, 18, 164, 172, 73, 18, 71, 20, 172, 22, 22, 22, 172, 164, 150, 129, 18, 128, 164, 150, 18, 18, 20, 152, 18, 22, 20, 20, 18, 18, 20, 114, 153, 18, 114, 20, 252, 252, 252, 20, 22, 149, 149, 154, 22, 22, 112, 103, 22, 155, 112, 112, 252, 22, 248, 248, 248, 22, 103, 70, 238, 238, 238, 103, 25, 25, 125, 252, 121, 67, 248, 252, 252, 78, 65, 252, 238, 59, 57, 174, 252, 176, 176, 176, 252, 248, 175, 53, 177, 248, 248, 238, 178, 248, 179, 238, 238, 176, 248, 238, 166, 166, 248, 180, 238, 183, 167, 167, 238, 186, 49, 194, 176, 116, 83, 166, 176, 176, 85, 86, 176, 167, 148, 148, 260, 260, 39, 207, 208, 176, 166, 102, 211, 38, 166, 166, 167, 148, 166, 260, 167, 167, 214, 132, 167, 220, 221, 166, 142, 142, 245, 245, 148, 167, 260, 222, 148, 148, 260, 260, 148, 224, 260, 142, 225, 245, 205, 205, 142, 148, 205, 260, 139, 139, 88, 139, 237, 28, 142, 97, 245, 205, 142, 142, 245, 245, 142, 139, 245, 243, 142, 142, 244, 96, 87, 142, 205, 245, 93, 92, 205, 205, 139, 16, 205, 14, 139, 139, 54, 54, 139, 261, 262, 205, 187, 187, 263, 90, 89, 139, 1, 196, nil, 54, 141, 141, nil, nil, 54, 187, nil, nil, nil, nil, nil, nil, nil, nil, 54, 141, nil, nil, 54, 54, 187, nil, 54, nil, 187, 187, 54, 54, 187, 276, 141, 54, nil, nil, 141, 141, nil, 187, 141, nil, 276, nil, 141, nil, nil, nil, nil, 141, 201, nil, 276, nil, 276, 276, nil, 276, 276, nil, 276, 201, 276, nil, 276, nil, 276, nil, nil, 276, 276, 201, nil, 201, 201, 212, 201, 201, nil, 201, nil, 201, nil, 201, nil, 201, 212, nil, 201, 201, nil, nil, nil, nil, 265, nil, 212, nil, 212, 212, nil, 212, 212, nil, 212, 265, 212, nil, 212, nil, 212, nil, nil, 212, 212, 265, nil, 265, 265, 171, 265, 265, nil, 265, nil, 265, nil, 265, nil, 265, 171, nil, 265, 265, nil, nil, nil, nil, 216, nil, 171, nil, 171, 171, nil, 171, 171, nil, 171, 216, 171, nil, 171, nil, 171, nil, nil, 171, 171, 216, nil, 216, 216, 228, 216, 216, nil, 216, nil, 216, nil, 216, nil, 216, 228, nil, 216, 216, nil, nil, nil, nil, 275, nil, 228, nil, 228, 228, nil, 228, 228, nil, 228, 275, 228, nil, 228, nil, 228, nil, nil, 228, 228, 275, nil, 275, 275, 254, 275, 275, nil, 275, nil, 275, nil, 275, nil, 275, 254, nil, 275, 275, nil, nil, nil, nil, 160, nil, 254, nil, 254, 254, nil, 254, 254, nil, 254, 160, 254, nil, 254, nil, 254, nil, nil, 254, 254, 160, nil, 160, 160, nil, 160, 160, nil, 160, 273, 160, nil, 160, nil, 160, nil, nil, 160, 160, 273, nil, 273, 273, nil, 273, 273, nil, 273, 147, 273, nil, 273, nil, 273, nil, nil, 273, 273, 147, nil, 147, 147, nil, 147, 147, nil, 147, 33, 147, nil, 147, nil, 147, nil, nil, 147, 147, 33, nil, 33, 33, nil, 33, 33, nil, 33, 0, 33, nil, 33, nil, 33, nil, nil, 33, 33, 0, nil, 0, 0, nil, 0, 0, nil, 0, nil, 0, nil, 0, nil, 0, nil, nil, 0, 0 ] racc_action_pointer = [ 673, 363, 11, nil, nil, nil, 92, 4, nil, nil, nil, nil, nil, 77, 355, nil, 353, nil, 139, nil, 145, nil, 163, 11, nil, 217, nil, 101, 291, 101, nil, 15, nil, 654, 94, nil, nil, nil, 283, 247, nil, -19, 56, nil, nil, nil, nil, nil, 8, 228, 22, nil, nil, 222, 361, nil, nil, 213, 73, 212, nil, nil, nil, nil, nil, 220, 10, 215, nil, nil, 213, 141, nil, 139, 148, 117, 113, 95, 208, nil, nil, 4, nil, 249, nil, 253, 269, 329, 309, 353, 367, nil, 345, 332, nil, -2, 311, 335, nil, nil, nil, 88, 281, 181, nil, -15, nil, nil, nil, nil, -4, nil, 158, nil, 149, 24, 263, nil, nil, nil, -5, 214, nil, nil, nil, 201, nil, 52, 134, 150, nil, 144, 278, 0, nil, nil, 47, nil, 78, 325, 123, 377, 301, -8, nil, nil, 83, 635, 275, 194, 137, nil, 173, 183, 190, 195, nil, nil, nil, nil, 597, 35, nil, nil, 136, nil, 253, 259, nil, nil, nil, 490, 118, 67, 228, 204, 235, 237, 241, 212, 221, nil, nil, 252, nil, nil, 243, 367, nil, nil, nil, nil, nil, nil, 244, nil, 375, nil, 49, nil, nil, 421, nil, 92, nil, 319, nil, 270, 261, 56, nil, 275, 446, nil, 291, nil, 509, nil, nil, nil, 280, 281, 290, nil, 296, 299, nil, nil, 534, nil, nil, nil, nil, nil, nil, nil, nil, 322, 214, nil, 115, nil, nil, 316, 342, 303, nil, nil, 208, nil, nil, 35, 190, nil, 578, nil, nil, nil, nil, nil, 277, 360, 361, 366, nil, 465, nil, nil, nil, nil, nil, nil, nil, 616, nil, 553, 402, nil, nil ] racc_action_default = [ -151, -175, -175, -14, -2, -144, -175, -175, -15, -3, -142, -16, -5, -175, -175, -6, -175, -7, -43, -8, -175, -9, -175, -42, -10, -175, -11, -175, -39, -175, -12, -175, -13, -1, -175, -38, -143, -141, -175, -151, -41, -151, -151, -145, -149, -148, -147, -150, -151, -79, -72, -168, -130, -28, -175, -31, -29, -175, -175, -30, -102, -32, -19, -104, -103, -20, -63, -21, -62, -22, -64, -24, -131, -25, -175, -26, -27, -99, -98, -96, -94, -175, -110, -90, -97, -91, -175, -95, -92, -93, -175, -132, -134, -151, -40, -175, -72, -175, -4, -71, -164, -175, -175, -151, -46, -175, -47, -63, -49, -56, -175, -55, -175, -162, -175, -175, -175, -77, -78, -80, -175, -173, -128, -70, -68, -65, -73, -63, -60, -66, -64, -173, -69, -67, -58, -129, -175, -18, -175, -175, -175, -175, -175, -175, -82, -170, -175, -175, -175, -175, -152, -153, -175, -175, -175, -173, 279, -166, -165, -167, -175, -175, -52, -51, -175, -45, -175, -175, -44, -163, -146, -175, -175, -175, -175, -174, -61, -175, -175, -174, -72, -17, -126, -175, -118, -119, -63, -175, -124, -127, -125, -105, -23, -101, -175, -33, -175, -169, -175, -171, -172, -175, -112, -175, -116, -175, -133, -161, -175, -175, -156, -159, -175, -106, -175, -138, -175, -50, -48, -88, -84, -85, -89, -53, -86, -87, -54, -140, -175, -76, -75, -37, -81, -59, -34, -35, -74, -173, -175, -121, -173, -100, -83, -108, -175, -175, -113, -111, -175, -154, -155, -175, -175, -136, -175, -36, -137, -139, -57, -123, -175, -175, -172, -175, -107, -175, -117, -160, -157, -158, -135, -122, -120, -175, -115, -175, -175, -114, -109 ] racc_goto_table = [ 29, 33, 38, 131, 185, 98, 117, 53, 123, 119, 174, 43, 134, 31, 210, 62, 39, 105, 112, 202, 178, 218, 198, 223, 226, 4, 146, 40, 86, 264, 205, 121, 77, 29, 77, 177, 184, 93, 128, 122, 240, 92, 152, 53, 214, 102, 77, 48, 183, 155, 116, 138, 239, 192, 208, 209, 59, 204, 35, 158, nil, nil, nil, 91, 100, nil, nil, nil, nil, 76, 77, nil, nil, 113, nil, nil, 246, nil, nil, 161, nil, nil, nil, nil, 82, nil, 90, nil, 125, nil, nil, nil, 59, 77, nil, nil, 183, nil, 99, nil, nil, 135, nil, nil, nil, 76, nil, 77, nil, nil, nil, 157, 169, 170, 204, 268, 260, nil, 151, nil, nil, 75, 140, nil, nil, 271, 258, nil, 122, 261, 53, 53, 117, 237, 123, 232, 236, 122, 233, 194, nil, nil, nil, nil, nil, 144, nil, 29, 201, nil, nil, nil, nil, 133, 266, 122, 122, 75, nil, 153, 29, 216, nil, nil, nil, 122, nil, nil, nil, 183, nil, 29, 228, 98, nil, nil, 122, 182, nil, 59, 59, nil, nil, 71, 77, 77, 182, 206, 98, nil, 135, nil, 76, 76, 122, nil, nil, 73, nil, 135, 98, 29, nil, nil, 220, 220, 109, 109, nil, nil, 77, nil, 29, 254, 125, 129, 29, 225, 225, 71, 111, 111, nil, nil, nil, 182, 98, 135, 29, 132, nil, nil, nil, 73, 122, nil, 229, 230, 135, nil, nil, nil, 190, 182, 75, 75, nil, 98, 98, 122, 77, 190, nil, 56, 29, nil, 135, nil, nil, nil, 77, nil, 242, nil, 77, 29, 275, nil, 109, 224, 224, nil, nil, 29, 276, 29, 29, nil, nil, 133, nil, nil, 111, 182, nil, 124, nil, nil, nil, 56, 190, nil, nil, nil, nil, nil, 135, nil, 182, nil, nil, nil, 259, nil, 188, nil, 71, 71, 190, nil, nil, 135, 267, 188, nil, nil, 269, nil, 189, nil, 73, 73, 61, nil, 84, nil, 84, 189, nil, 109, nil, 221, 221, nil, nil, nil, nil, nil, 84, nil, nil, 129, nil, 111, nil, 222, 222, nil, 190, nil, nil, nil, 188, nil, nil, 132, nil, nil, 61, nil, nil, nil, 84, 190, nil, nil, 189, nil, nil, nil, 188, nil, nil, nil, nil, nil, 56, 56, nil, nil, nil, nil, nil, nil, 189, 84, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, 84, nil, 219, 219, nil, nil, nil, nil, nil, nil, nil, 188, 124, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, 189, 188, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, 189, nil, nil, nil, nil, nil, nil, 61, 195, 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, 84, 84, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, 84, 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, nil, nil, nil, 84, nil, nil, nil, nil, nil, nil, nil, nil, nil, 84, nil, nil, nil, 84 ] racc_goto_check = [ 34, 2, 34, 32, 62, 4, 50, 25, 48, 52, 33, 69, 45, 1, 72, 17, 29, 39, 39, 58, 33, 40, 54, 44, 44, 3, 53, 38, 55, 56, 57, 35, 47, 34, 47, 31, 61, 29, 30, 25, 63, 64, 65, 25, 33, 66, 47, 67, 60, 32, 68, 17, 62, 20, 70, 71, 27, 60, 5, 73, nil, nil, nil, 27, 3, nil, nil, nil, nil, 24, 47, nil, nil, 3, nil, nil, 58, nil, nil, 39, nil, nil, nil, nil, 49, nil, 49, nil, 27, nil, nil, nil, 27, 47, nil, nil, 60, nil, 49, nil, nil, 24, nil, nil, nil, 24, nil, 47, nil, nil, nil, 29, 69, 69, 60, 72, 54, nil, 3, nil, nil, 23, 49, nil, nil, 62, 33, nil, 25, 33, 25, 25, 50, 32, 48, 52, 50, 25, 45, 17, nil, nil, nil, nil, nil, 49, nil, 34, 2, nil, nil, nil, nil, 23, 60, 25, 25, 23, nil, 49, 34, 2, nil, nil, nil, 25, nil, nil, nil, 60, nil, 34, 2, 4, nil, nil, 25, 27, nil, 27, 27, nil, nil, 21, 47, 47, 27, 27, 4, nil, 24, nil, 24, 24, 25, nil, nil, 22, nil, 24, 4, 34, nil, nil, 27, 27, 21, 21, nil, nil, 47, nil, 34, 2, 27, 21, 34, 24, 24, 21, 22, 22, nil, nil, nil, 27, 4, 24, 34, 22, nil, nil, nil, 22, 25, nil, 49, 49, 24, nil, nil, nil, 23, 27, 23, 23, nil, 4, 4, 25, 47, 23, nil, 26, 34, nil, 24, nil, nil, nil, 47, nil, 49, nil, 47, 34, 2, nil, 21, 23, 23, nil, nil, 34, 2, 34, 34, nil, nil, 23, nil, nil, 22, 27, nil, 26, nil, nil, nil, 26, 23, nil, nil, nil, nil, nil, 24, nil, 27, nil, nil, nil, 49, nil, 21, nil, 21, 21, 23, nil, nil, 24, 49, 21, nil, nil, 49, nil, 22, nil, 22, 22, 28, nil, 28, nil, 28, 22, nil, 21, nil, 21, 21, nil, nil, nil, nil, nil, 28, nil, nil, 21, nil, 22, nil, 22, 22, nil, 23, nil, nil, nil, 21, nil, nil, 22, nil, nil, 28, nil, nil, nil, 28, 23, nil, nil, 22, nil, nil, nil, 21, nil, nil, nil, nil, nil, 26, 26, nil, nil, nil, nil, nil, nil, 22, 28, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, 28, nil, 26, 26, nil, nil, nil, nil, nil, nil, nil, 21, 26, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, 22, 21, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, 22, nil, nil, nil, nil, nil, nil, 28, 28, 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, 28, 28, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, 28, 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, nil, nil, nil, 28, nil, nil, nil, nil, nil, nil, nil, nil, nil, 28, nil, nil, nil, 28 ] racc_goto_pointer = [ nil, 13, 1, 25, -28, 56, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, -3, nil, nil, -88, 165, 179, 103, 51, -11, 235, 38, 304, 10, -12, -93, -47, -111, 0, -18, nil, nil, 20, -24, -143, nil, nil, nil, -143, -38, nil, 12, -42, 64, -43, nil, -40, -55, -124, 8, -214, -118, -129, nil, -91, -103, -135, -147, 16, -51, 6, 34, 2, -2, -96, -95, -136, -42 ] racc_goto_default = [ nil, nil, nil, 104, 9, 12, 15, 17, 19, 21, 24, 26, 30, 32, 3, 8, 11, nil, 65, 67, 69, 85, 87, 88, 89, 78, 80, 83, 14, 16, nil, nil, nil, nil, 74, nil, 2, 7, nil, nil, 106, 164, 108, 110, nil, nil, 136, 52, 79, nil, 126, 118, nil, nil, nil, nil, nil, nil, nil, 203, 57, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil ] racc_token_table = { false => 0, Object.new => 1, :LBRACK => 2, :DQTEXT => 3, :SQTEXT => 4, :RBRACK => 5, :LBRACE => 6, :RBRACE => 7, :SYMBOL => 8, :FARROW => 9, :COMMA => 10, :TRUE => 11, :FALSE => 12, :EQUALS => 13, :LESSEQUAL => 14, :NOTEQUAL => 15, :DOT => 16, :COLON => 17, :TYPE => 18, :LLCOLLECT => 19, :RRCOLLECT => 20, :QMARK => 21, :LPAREN => 22, :RPAREN => 23, :ISEQUAL => 24, :GREATEREQUAL => 25, :GREATERTHAN => 26, :LESSTHAN => 27, :IF => 28, :ELSE => 29, :IMPORT => 30, :DEFINE => 31, :ELSIF => 32, :VARIABLE => 33, :CLASS => 34, :INHERITS => 35, :NODE => 36, :BOOLEAN => 37, :NAME => 38, :SEMIC => 39, :CASE => 40, :DEFAULT => 41, :AT => 42, :LCOLLECT => 43, :RCOLLECT => 44, :CLASSNAME => 45, :CLASSREF => 46, :NOT => 47, :OR => 48, :AND => 49, :UNDEF => 50, :PARROW => 51 } racc_use_result_var = true racc_nt_base = 52 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_token_to_s_table = [ '$end', 'error', 'LBRACK', 'DQTEXT', 'SQTEXT', 'RBRACK', 'LBRACE', 'RBRACE', 'SYMBOL', 'FARROW', 'COMMA', 'TRUE', 'FALSE', 'EQUALS', 'LESSEQUAL', 'NOTEQUAL', 'DOT', 'COLON', 'TYPE', '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', '$start', 'program', 'statements', 'nil', 'statement', 'resource', 'virtualresource', 'collection', 'assignment', 'casestatement', 'ifstatement', 'import', 'fstatement', 'definition', 'hostclass', 'nodedef', 'resourceoverride', 'funcvalues', 'namestrings', 'resourcerefs', 'namestring', 'name', 'variable', 'type', 'boolean', 'funcrvalue', 'selector', 'quotedtext', 'resourceref', 'classname', 'resourceinstances', 'endsemi', 'params', 'endcomma', 'classref', 'anyparams', 'at', 'collectname', 'collectrhand', 'collstatements', 'collstatement', 'colljoin', 'collexpr', 'colllval', 'simplervalue', 'resourceinst', 'resourcename', 'undef', 'array', 'rvalue', 'param', 'addparam', 'anyparam', 'rvalues', 'comma', 'iftest', 'else', 'caseopts', 'caseopt', 'casevalues', 'selectlhand', 'svalues', 'selectval', 'sintvalues', 'qtexts', 'argumentlist', 'classparent', 'hostnames', 'nodeparent', 'hostname', 'nothing', 'arguments', 'argument', 'classnameordefault'] Racc_debug_parser = false ##### racc system variables end ##### # reduce 0 omitted module_eval <<'.,.,', 'grammar.ra', 30 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 result = nil end result end .,., # reduce 2 omitted # reduce 3 omitted module_eval <<'.,.,', 'grammar.ra', 46 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 end result end .,., # reduce 5 omitted # reduce 6 omitted # reduce 7 omitted # reduce 8 omitted # reduce 9 omitted # reduce 10 omitted # reduce 11 omitted # reduce 12 omitted # reduce 13 omitted # reduce 14 omitted # reduce 15 omitted # reduce 16 omitted module_eval <<'.,.,', 'grammar.ra', 68 def _reduce_17( val, _values, result ) args = aryfy(val[2]) result = ast AST::Function, :name => val[0], :arguments => args, :ftype => :statement result end .,., module_eval <<'.,.,', 'grammar.ra', 74 def _reduce_18( val, _values, result ) result = ast AST::Function, :name => val[0], :arguments => AST::ASTArray.new({}), :ftype => :statement result end .,., module_eval <<'.,.,', 'grammar.ra', 81 def _reduce_19( val, _values, result ) args = aryfy(val[1]) result = ast AST::Function, :name => val[0], :arguments => args, :ftype => :statement result end .,., # reduce 20 omitted # reduce 21 omitted # reduce 22 omitted module_eval <<'.,.,', 'grammar.ra', 91 def _reduce_23( val, _values, result ) result = aryfy(val[0], val[2]) result.line = @lexer.line result.file = @lexer.file result end .,., # reduce 24 omitted # reduce 25 omitted # reduce 26 omitted # reduce 27 omitted # reduce 28 omitted # reduce 29 omitted # reduce 30 omitted module_eval <<'.,.,', 'grammar.ra', 104 def _reduce_31( val, _values, result ) result = ast AST::Name, :value => val[0] result end .,., # reduce 32 omitted module_eval <<'.,.,', 'grammar.ra', 115 def _reduce_33( val, _values, result ) unless val[0].is_a?(AST::ASTArray) val[0] = aryfy(val[0]) end val[0].push(val[2]) result = val[0] result end .,., module_eval <<'.,.,', 'grammar.ra', 136 def _reduce_34( val, _values, result ) array = val[2] - if array.instance_of?(AST::ResourceInst) + if array.instance_of?(AST::ResourceInstance) array = [array] end result = ast AST::ASTArray # this iterates across each specified resourceinstance array.each { |instance| - unless instance.instance_of?(AST::ResourceInst) + 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::ResourceDef, + result.push ast(AST::Resource, :type => val[0], :title => instance[0], :params => instance[1]) } result end .,., module_eval <<'.,.,', 'grammar.ra', 139 def _reduce_35( val, _values, result ) # This is a deprecated syntax. error "All resource specifications require names" result end .,., module_eval <<'.,.,', 'grammar.ra', 142 def _reduce_36( val, _values, result ) # a defaults setting for a type result = ast(AST::ResourceDefaults, :type => val[0], :params => val[2]) result end .,., module_eval <<'.,.,', 'grammar.ra', 147 def _reduce_37( val, _values, result ) result = ast AST::ResourceOverride, :object => val[0], :params => val[2] result end .,., module_eval <<'.,.,', 'grammar.ra', 174 def _reduce_38( val, _values, result ) type = val[0] if type == :exported and ! Puppet[:storeconfigs] error "You cannot collect without storeconfigs being set" end if val[1].is_a? AST::ResourceDefaults error "Defaults are not virtualizable" end 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] result end .,., module_eval <<'.,.,', 'grammar.ra', 175 def _reduce_39( val, _values, result ) result = :virtual result end .,., module_eval <<'.,.,', 'grammar.ra', 176 def _reduce_40( val, _values, result ) result = :exported result end .,., module_eval <<'.,.,', 'grammar.ra', 199 def _reduce_41( 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] error "You cannot collect exported resources without storeconfigs being set" end result = ast AST::Collection, args result end .,., # reduce 42 omitted # reduce 43 omitted module_eval <<'.,.,', 'grammar.ra', 210 def _reduce_44( val, _values, result ) if val[1] result = val[1] result.form = :virtual else result = :virtual end result end .,., module_eval <<'.,.,', 'grammar.ra', 218 def _reduce_45( val, _values, result ) if val[1] result = val[1] result.form = :exported else result = :exported end result end .,., # reduce 46 omitted # reduce 47 omitted module_eval <<'.,.,', 'grammar.ra', 226 def _reduce_48( val, _values, result ) result = ast AST::CollExpr, :test1 => val[0], :oper => val[1], :test2 => val[2] result end .,., # reduce 49 omitted module_eval <<'.,.,', 'grammar.ra', 232 def _reduce_50( val, _values, result ) result = val[1] result.parens = true result end .,., # reduce 51 omitted # reduce 52 omitted module_eval <<'.,.,', 'grammar.ra', 240 def _reduce_53( val, _values, result ) result = ast AST::CollExpr, :test1 => val[0], :oper => val[1], :test2 => val[2] #result = ast AST::CollExpr #result.push *val result end .,., module_eval <<'.,.,', 'grammar.ra', 245 def _reduce_54( val, _values, result ) result = ast AST::CollExpr, :test1 => val[0], :oper => val[1], :test2 => val[2] #result = ast AST::CollExpr #result.push *val result end .,., # reduce 55 omitted # reduce 56 omitted module_eval <<'.,.,', 'grammar.ra', 252 def _reduce_57( val, _values, result ) - result = ast AST::ResourceInst, :children => [val[0],val[2]] + result = ast AST::ResourceInstance, :children => [val[0],val[2]] result end .,., # reduce 58 omitted module_eval <<'.,.,', 'grammar.ra', 262 def _reduce_59( val, _values, result ) - if val[0].instance_of?(AST::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 result end .,., # reduce 60 omitted # reduce 61 omitted module_eval <<'.,.,', 'grammar.ra', 269 def _reduce_62( val, _values, result ) result = ast AST::Undef, :value => :undef result end .,., module_eval <<'.,.,', 'grammar.ra', 273 def _reduce_63( val, _values, result ) result = ast AST::Name, :value => val[0] result end .,., module_eval <<'.,.,', 'grammar.ra', 277 def _reduce_64( val, _values, result ) result = ast AST::Type, :value => val[0] result end .,., # reduce 65 omitted # reduce 66 omitted # reduce 67 omitted # reduce 68 omitted # reduce 69 omitted # reduce 70 omitted module_eval <<'.,.,', 'grammar.ra', 293 def _reduce_71( val, _values, result ) if val[0] =~ /::/ 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] result = ast AST::VarDef, :name => variable, :value => val[2] result end .,., module_eval <<'.,.,', 'grammar.ra', 298 def _reduce_72( val, _values, result ) result = ast AST::ASTArray result end .,., module_eval <<'.,.,', 'grammar.ra', 298 def _reduce_73( val, _values, result ) result = val[0] result end .,., module_eval <<'.,.,', 'grammar.ra', 307 def _reduce_74( 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', 311 def _reduce_75( val, _values, result ) result = ast AST::ResourceParam, :param => val[0], :value => val[2] result end .,., module_eval <<'.,.,', 'grammar.ra', 316 def _reduce_76( val, _values, result ) result = ast AST::ResourceParam, :param => val[0], :value => val[2], :add => true result end .,., # reduce 77 omitted # reduce 78 omitted module_eval <<'.,.,', 'grammar.ra', 324 def _reduce_79( val, _values, result ) result = ast AST::ASTArray result end .,., module_eval <<'.,.,', 'grammar.ra', 324 def _reduce_80( val, _values, result ) result = val[0] result end .,., module_eval <<'.,.,', 'grammar.ra', 333 def _reduce_81( 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 82 omitted module_eval <<'.,.,', 'grammar.ra', 342 def _reduce_83( 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 84 omitted # reduce 85 omitted # reduce 86 omitted # reduce 87 omitted # reduce 88 omitted # reduce 89 omitted # reduce 90 omitted # reduce 91 omitted # reduce 92 omitted # reduce 93 omitted # reduce 94 omitted # reduce 95 omitted # reduce 96 omitted # reduce 97 omitted # reduce 98 omitted # reduce 99 omitted module_eval <<'.,.,', 'grammar.ra', 369 def _reduce_100( val, _values, result ) args = aryfy(val[2]) result = ast AST::Function, :name => val[0], :arguments => args, :ftype => :rvalue result end .,., module_eval <<'.,.,', 'grammar.ra', 374 def _reduce_101( val, _values, result ) result = ast AST::Function, :name => val[0], :arguments => AST::ASTArray.new({}), :ftype => :rvalue result end .,., module_eval <<'.,.,', 'grammar.ra', 378 def _reduce_102( val, _values, result ) result = ast AST::String, :value => val[0] result end .,., module_eval <<'.,.,', 'grammar.ra', 380 def _reduce_103( val, _values, result ) result = ast AST::FlatString, :value => val[0] result end .,., module_eval <<'.,.,', 'grammar.ra', 384 def _reduce_104( val, _values, result ) result = ast AST::Boolean, :value => val[0] result end .,., module_eval <<'.,.,', 'grammar.ra', 389 def _reduce_105( val, _values, result ) Puppet.warning addcontext("Deprecation notice: Resource references should now be capitalized") - result = ast AST::ResourceRef, :type => val[0], :title => val[2] + result = ast AST::ResourceReference, :type => val[0], :title => val[2] result end .,., module_eval <<'.,.,', 'grammar.ra', 391 def _reduce_106( val, _values, result ) - result = ast AST::ResourceRef, :type => val[0], :title => val[2] + result = ast AST::ResourceReference, :type => val[0], :title => val[2] result end .,., module_eval <<'.,.,', 'grammar.ra', 404 def _reduce_107( val, _values, result ) args = { :test => val[1], :statements => val[3] } if val[5] args[:else] = val[5] end result = ast AST::IfStatement, args result end .,., # reduce 108 omitted module_eval <<'.,.,', 'grammar.ra', 409 def _reduce_109( val, _values, result ) result = ast AST::Else, :statements => val[2] result end .,., # reduce 110 omitted module_eval <<'.,.,', 'grammar.ra', 421 def _reduce_111( val, _values, result ) 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 end .,., # reduce 112 omitted module_eval <<'.,.,', 'grammar.ra', 431 def _reduce_113( 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', 435 def _reduce_114( val, _values, result ) result = ast AST::CaseOpt, :value => val[0], :statements => val[3] result end .,., module_eval <<'.,.,', 'grammar.ra', 440 def _reduce_115( val, _values, result ) result = ast(AST::CaseOpt, :value => val[0], :statements => ast(AST::ASTArray) ) result end .,., # reduce 116 omitted module_eval <<'.,.,', 'grammar.ra', 450 def _reduce_117( 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', 454 def _reduce_118( val, _values, result ) result = ast AST::Selector, :param => val[0], :values => val[2] result end .,., # reduce 119 omitted module_eval <<'.,.,', 'grammar.ra', 456 def _reduce_120( val, _values, result ) result = val[1] result end .,., # reduce 121 omitted module_eval <<'.,.,', 'grammar.ra', 467 def _reduce_122( 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', 471 def _reduce_123( val, _values, result ) result = ast AST::ResourceParam, :param => val[0], :value => val[2] result end .,., # reduce 124 omitted # reduce 125 omitted # reduce 126 omitted # reduce 127 omitted # reduce 128 omitted # reduce 129 omitted # reduce 130 omitted module_eval <<'.,.,', 'grammar.ra', 482 def _reduce_131( val, _values, result ) result = ast AST::Default, :value => val[0] result end .,., module_eval <<'.,.,', 'grammar.ra', 484 def _reduce_132( val, _values, result ) result = [val[0].value] result end .,., module_eval <<'.,.,', 'grammar.ra', 488 def _reduce_133( val, _values, result ) results = val[0] << val[2].value result end .,., module_eval <<'.,.,', 'grammar.ra', 496 def _reduce_134( val, _values, result ) val[1].each do |file| import(file) end result = AST::ASTArray.new(:children => []) result end .,., module_eval <<'.,.,', 'grammar.ra', 506 def _reduce_135( val, _values, result ) newdefine classname(val[1]), :arguments => val[2], :code => val[4] @lexer.indefine = false result = nil #} | DEFINE NAME argumentlist parent LBRACE RBRACE { result end .,., module_eval <<'.,.,', 'grammar.ra', 510 def _reduce_136( val, _values, result ) newdefine classname(val[1]), :arguments => val[2] @lexer.indefine = false result = nil result end .,., module_eval <<'.,.,', 'grammar.ra', 518 def _reduce_137( val, _values, result ) # Our class gets defined in the parent namespace, not our own. @lexer.namepop newclass classname(val[1]), :code => val[4], :parent => val[2] result = nil result end .,., module_eval <<'.,.,', 'grammar.ra', 523 def _reduce_138( val, _values, result ) # Our class gets defined in the parent namespace, not our own. @lexer.namepop newclass classname(val[1]), :parent => val[2] result = nil result end .,., module_eval <<'.,.,', 'grammar.ra', 528 def _reduce_139( val, _values, result ) newnode val[1], :parent => val[2], :code => val[4] result = nil result end .,., module_eval <<'.,.,', 'grammar.ra', 531 def _reduce_140( val, _values, result ) newnode val[1], :parent => val[2] result = nil result end .,., # reduce 141 omitted # reduce 142 omitted # reduce 143 omitted # reduce 144 omitted # reduce 145 omitted module_eval <<'.,.,', 'grammar.ra', 546 def _reduce_146( val, _values, result ) result = val[0] result = [result] unless result.is_a?(Array) result << val[2] result end .,., # reduce 147 omitted # reduce 148 omitted # reduce 149 omitted # reduce 150 omitted module_eval <<'.,.,', 'grammar.ra', 555 def _reduce_151( val, _values, result ) result = nil result end .,., module_eval <<'.,.,', 'grammar.ra', 559 def _reduce_152( val, _values, result ) result = ast AST::ASTArray, :children => [] result end .,., # reduce 153 omitted module_eval <<'.,.,', 'grammar.ra', 564 def _reduce_154( val, _values, result ) result = nil result end .,., module_eval <<'.,.,', 'grammar.ra', 568 def _reduce_155( val, _values, result ) result = val[1] result = [result] unless result[0].is_a?(Array) result end .,., # reduce 156 omitted module_eval <<'.,.,', 'grammar.ra', 575 def _reduce_157( val, _values, result ) result = val[0] result = [result] unless result[0].is_a?(Array) result << val[2] result end .,., module_eval <<'.,.,', 'grammar.ra', 580 def _reduce_158( val, _values, result ) Puppet.warning addcontext("Deprecation notice: must now include '$' in prototype") result = [val[0], val[2]] result end .,., module_eval <<'.,.,', 'grammar.ra', 584 def _reduce_159( val, _values, result ) Puppet.warning addcontext("Deprecation notice: must now include '$' in prototype") result = [val[0]] result end .,., module_eval <<'.,.,', 'grammar.ra', 586 def _reduce_160( val, _values, result ) result = [val[0], val[2]] result end .,., module_eval <<'.,.,', 'grammar.ra', 588 def _reduce_161( val, _values, result ) result = [val[0]] result end .,., # reduce 162 omitted module_eval <<'.,.,', 'grammar.ra', 593 def _reduce_163( val, _values, result ) result = val[1] result end .,., # reduce 164 omitted module_eval <<'.,.,', 'grammar.ra', 598 def _reduce_165( val, _values, result ) result = val[1] result end .,., # reduce 166 omitted # reduce 167 omitted module_eval <<'.,.,', 'grammar.ra', 604 def _reduce_168( val, _values, result ) result = ast AST::Variable, :value => val[0] result end .,., module_eval <<'.,.,', 'grammar.ra', 612 def _reduce_169( 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', 614 def _reduce_170( val, _values, result ) result = ast AST::ASTArray result end .,., # reduce 171 omitted # reduce 172 omitted # reduce 173 omitted module_eval <<'.,.,', 'grammar.ra', 619 def _reduce_174( val, _values, result ) result = nil result end .,., def _reduce_none( val, _values, result ) result end 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 728f75a69..8cf0dcfe8 100644 --- a/lib/puppet/parser/parser_support.rb +++ b/lib/puppet/parser/parser_support.rb @@ -1,447 +1,454 @@ +# 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' ASTSet = Struct.new(:classes, :definitions, :nodes) # Define an accessor method for each table. We hide the existence of # the struct. [:classes, :definitions, :nodes].each do |name| define_method(name) do @astset.send(name) end end AST = Puppet::Parser::AST - attr_reader :file, :interp + attr_reader :version, :environment attr_accessor :files # Add context to a message; useful for error messages and such. def addcontext(message, obj = nil) obj ||= @lexer message += " on line %s" % obj.line if file = obj.file message += " in file %s" % file end return 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 return result end # Create an AST object, and automatically add the file and line information if # available. def ast(klass, hash = nil) hash ||= {} unless hash.include?(:line) hash[:line] = @lexer.line end unless hash.include?(:file) if file = @lexer.file hash[:file] = file end end return klass.new(hash) 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 if @lexer.file except.file = @lexer.file end raise except end + def file + @lexer.file + end + def file=(file) unless FileTest.exists?(file) unless file =~ /\.pp$/ file = file + ".pp" end unless FileTest.exists?(file) raise Puppet::Error, "Could not find file %s" % file end end if @files.detect { |f| f.file == file } raise Puppet::AlreadyImportedError.new("Import loop detected") else @files << Puppet::Util::LoadedFile.new(file) @lexer.file = file end end # Find a class definition, relative to the current namespace. def findclass(namespace, name) fqfind namespace, name, classes end # Find a component definition, relative to the current namespace. def finddefine(namespace, name) fqfind namespace, name, definitions end # This is only used when nodes are looking up the code for their # parent nodes. def findnode(name) fqfind "", name, nodes end # The recursive method used to actually look these objects up. def fqfind(namespace, name, table) namespace = namespace.downcase - name = name.downcase + name = name.to_s.downcase if name =~ /^::/ or namespace == "" classname = name.sub(/^::/, '') unless table[classname] self.load(classname) end return table[classname] end ary = namespace.split("::") while ary.length > 0 newname = (ary + [name]).join("::").sub(/^::/, '') if obj = table[newname] or (self.load(newname) and obj = table[newname]) return obj end # Delete the second to last object, which reduces our namespace by one. ary.pop end # If we've gotten to this point without finding it, see if the name # exists at the top namespace if obj = table[name] or (self.load(name) and obj = table[name]) return obj end return nil end # Import our files. def import(file) if Puppet[:ignoreimport] return AST::ASTArray.new(:children => []) end # use a path relative to the file doing the importing if @lexer.file dir = @lexer.file.sub(%r{[^/]+$},'').sub(/\/$/, '') else dir = "." end if dir == "" dir = "." end result = ast AST::ASTArray # We can't interpolate at this point since we don't have any # scopes set up. Warn the user if they use a variable reference pat = file if pat.index("$") Puppet.warning( "The import of #{pat} contains a variable reference;" + " variables are not interpolated for imports " + "in file #{@lexer.file} at line #{@lexer.line}" ) end - files = Puppet::Module::find_manifests(pat, dir) + files = Puppet::Module::find_manifests(pat, :cwd => dir) if files.size == 0 raise Puppet::ImportError.new("No file(s) found for import " + "of '#{pat}'") end files.collect { |file| - parser = Puppet::Parser::Parser.new(@astset) + parser = Puppet::Parser::Parser.new(:astset => @astset, :environment => @environment) parser.files = self.files Puppet.debug("importing '%s'" % file) unless file =~ /^#{File::SEPARATOR}/ file = File.join(dir, file) end begin parser.file = file rescue Puppet::AlreadyImportedError # This file has already been imported to just move on next end # This will normally add code to the 'main' class. parser.parse } end - def initialize(astset = nil) + def initialize(options = {}) + @astset = options[:astset] || ASTSet.new({}, {}, {}) + @environment = options[:environment] initvars() - if astset - @astset = astset - end end # Initialize or reset all of our variables. def initvars @lexer = Puppet::Parser::Lexer.new() @files = [] @loaded = [] - - # This is where we store our classes and definitions and nodes. - # Clear each hash, just to help the GC a bit. - if defined?(@astset) - [:classes, :definitions, :nodes].each do |name| - @astset.send(name).clear - end - end - @astset = ASTSet.new({}, {}, {}) end # Try to load a class, since we could not find it. def load(classname) return false if classname == "" filename = classname.gsub("::", File::SEPARATOR) loaded = false # First try to load the top-level module mod = filename.scan(/^[\w-]+/).shift unless @loaded.include?(mod) @loaded << mod begin import(mod) Puppet.info "Autoloaded module %s" % mod loaded = true rescue Puppet::ImportError => detail # We couldn't load the module end end unless filename == mod and ! @loaded.include?(mod) @loaded << mod # Then the individual file begin import(filename) Puppet.info "Autoloaded file %s from module %s" % [filename, mod] loaded = true rescue Puppet::ImportError => detail # We couldn't load the file end end return loaded 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 = {}) name = name.downcase if definitions.include?(name) raise Puppet::ParseError, "Cannot redefine class %s as a definition" % name end code = options[:code] parent = options[:parent] # If the class is already defined, then add code to it. if other = @astset.classes[name] # Make sure the parents match if parent and other.parentclass and (parent != other.parentclass) error("Class %s is already defined at %s:%s; cannot redefine" % [name, other.file, other.line]) end # This might be dangerous... if parent and ! other.parentclass other.parentclass = parent end # This might just be an empty, stub class. if code tmp = name if tmp == "" tmp = "main" end Puppet.debug addcontext("Adding code to %s" % tmp) # Else, add our code to it. if other.code and code other.code.children += code.children else other.code ||= code end end else # Define it anew. # Note we're doing something somewhat weird here -- we're setting # the class's namespace to its fully qualified name. This means # anything inside that class starts looking in that namespace first. args = {:namespace => name, :classname => name, :parser => self} args[:code] = code if code args[:parentclass] = parent if parent @astset.classes[name] = ast AST::HostClass, args end return @astset.classes[name] end # Create a new definition. def newdefine(name, options = {}) name = name.downcase if @astset.classes.include?(name) raise Puppet::ParseError, "Cannot redefine class %s as a definition" % name end # Make sure our definition doesn't already exist if other = @astset.definitions[name] error("%s is already defined at %s:%s; cannot redefine" % [name, other.file, other.line]) end ns, whatever = namesplit(name) args = { :namespace => ns, :arguments => options[:arguments], :code => options[:code], :parser => self, :classname => name } [:code, :arguments].each do |param| args[param] = options[param] if options[param] end - @astset.definitions[name] = ast AST::Component, args + @astset.definitions[name] = ast AST::Definition, args 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) names.collect do |name| name = name.to_s.downcase if other = @astset.nodes[name] error("Node %s is already defined at %s:%s; cannot redefine" % [other.name, other.file, other.line]) end name = name.to_s if name.is_a?(Symbol) args = { :name => name, :parser => self } if options[:code] args[:code] = options[:code] end if options[:parent] args[:parentclass] = options[:parent] end @astset.nodes[name] = ast(AST::Node, args) @astset.nodes[name].classname = name @astset.nodes[name] end end def on_error(token,value,stack) #on '%s' at '%s' in\n'%s'" % [token,value,stack] #error = "line %s: parse error after '%s'" % # [@lexer.line,@lexer.last] error = "Syntax error at '%s'" % [value] if brace = @lexer.expected error += "; expected '%s'" % brace end except = Puppet::ParseError.new(error) except.line = @lexer.line if @lexer.file except.file = @lexer.file end raise except end # how should I do error handling here? def parse(string = nil) if string self.string = string end begin 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 + @version = Time.now.to_i return @astset ensure @lexer.clear end # See if any of the files have changed. def reparse? if file = @files.detect { |file| file.changed? } return file.stamp else return false end end def string=(string) @lexer.string = string end -end -# $Id$ + # 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(*files) + files.each do |file| + unless file.is_a? Puppet::Util::LoadedFile + file = Puppet::Util::LoadedFile.new(file) + end + @files << file + end + end +end diff --git a/lib/puppet/parser/resource.rb b/lib/puppet/parser/resource.rb index 18ec15ac0..a8da6b054 100644 --- a/lib/puppet/parser/resource.rb +++ b/lib/puppet/parser/resource.rb @@ -1,405 +1,450 @@ # A resource that we're managing. This handles making sure that only subclasses # can set parameters. class Puppet::Parser::Resource require 'puppet/parser/resource/param' require 'puppet/parser/resource/reference' - ResParam = Struct.new :name, :value, :source, :line, :file include Puppet::Util include Puppet::Util::MethodHelper include Puppet::Util::Errors include Puppet::Util::Logging attr_accessor :source, :line, :file, :scope, :rails_id - attr_accessor :virtual, :override, :params, :translated + attr_accessor :virtual, :override, :translated - attr_reader :exported + attr_reader :exported, :evaluated, :params attr_writer :tags # Proxy a few methods to our @ref object. [:builtin?, :type, :title].each do |method| define_method(method) do @ref.send(method) end end # Set up some boolean test methods - [:exported, :translated, :override].each do |method| + [:exported, :translated, :override, :virtual, :evaluated].each do |method| newmeth = (method.to_s + "?").intern define_method(newmeth) do self.send(method) end end def [](param) param = symbolize(param) if param == :title return self.title end if @params.has_key?(param) @params[param].value else nil end end - # Add default values from our definition. - def adddefaults - defaults = scope.lookupdefaults(self.type) - - defaults.each do |name, param| - unless @params.include?(param.name) - self.debug "Adding default for %s" % param.name - - @params[param.name] = param - end - end - end - - # Add any metaparams defined in our scope. This actually adds any metaparams - # from any parent scope, and there's currently no way to turn that off. - def addmetaparams - Puppet::Type.eachmetaparam do |name| - next if self[name] - if val = scope.lookupvar(name.to_s, false) - unless val == :undefined - set Param.new(:name => name, :value => val, - :source => scope.source) - end - end - end - end - - # Add any overrides for this object. - def addoverrides - overrides = scope.lookupoverrides(self) - - overrides.each do |over| - self.merge(over) - end - - overrides.clear - end - def builtin=(bool) @ref.builtin = bool end # Retrieve the associated definition and evaluate it. def evaluate if klass = @ref.definedtype finish() - scope.deleteresource(self) - return klass.evaluate(:scope => scope, - :type => self.type, - :title => self.title, - :arguments => self.to_hash, - :scope => self.scope, - :virtual => self.virtual, - :exported => self.exported - ) + scope.compile.delete_resource(self) + return klass.evaluate(:scope => scope, :resource => self) elsif builtin? devfail "Cannot evaluate a builtin type" else self.fail "Cannot find definition %s" % self.type end ensure @evaluated = true end + # Mark this resource as both exported and virtual, + # or remove the exported mark. def exported=(value) if value @virtual = true @exported = value else @exported = value end end - def evaluated? - if defined? @evaluated and @evaluated - true - else - false - end - end - # Do any finishing work on this object, called before evaluation or # before storage/translation. def finish - addoverrides() - adddefaults() - addmetaparams() + add_overrides() + add_defaults() + add_metaparams() + validate() end def initialize(options) - options = symbolize_options(options) - - # Collect the options necessary to make the reference. - refopts = [:type, :title].inject({}) do |hash, param| - hash[param] = options[param] - options.delete(param) - hash + # Set all of the options we can. + options.each do |option, value| + if respond_to?(option.to_s + "=") + send(option.to_s + "=", value) + options.delete(option) + end end - @params = {} - tmpparams = nil - if tmpparams = options[:params] - options.delete(:params) + unless self.scope + raise ArgumentError, "Resources require a scope" end + @source ||= scope.source - # Now set the rest of the options. - set_options(options) + options = symbolize_options(options) - @ref = Reference.new(refopts) + # Set up our reference. + if type = options[:type] and title = options[:title] + options.delete(:type) + options.delete(:title) + else + raise ArgumentError, "Resources require a type and title" + end - requiredopts(:scope, :source) + @ref = Reference.new(:type => type, :title => title, :scope => self.scope) - @ref.scope = self.scope + @params = {} - if tmpparams - tmpparams.each do |param| - # We use the method here, because it does type-checking. - set(param) + # Define all of the parameters + if params = options[:params] + options.delete(:params) + params.each do |param| + set_parameter(param) end end + + # Throw an exception if we've got any arguments left to set. + unless options.empty? + raise ArgumentError, "Resources do not accept %s" % options.keys.collect { |k| k.to_s }.join(", ") + end + + @tags = [] + @tags << @ref.type.to_s + @tags << @ref.title.to_s if @ref.title.to_s =~ /^[-\w]+$/ + if scope.resource + @tags += scope.resource.tags + end end - # Merge an override resource in. + # Merge an override resource in. This will throw exceptions if + # any overrides aren't allowed. def merge(resource) + # Test the resource scope, to make sure the resource is even allowed + # to override. + unless self.source.object_id == resource.source.object_id || resource.source.child_of?(self.source) + raise Puppet::ParseError.new("Only subclasses can override parameters", resource.line, resource.file) + end # Some of these might fail, but they'll fail in the way we want. resource.params.each do |name, param| - set(param) + override_parameter(param) end end + # Modify this resource in the Rails database. Poor design, yo. def modify_rails(db_resource) args = rails_args args.each do |param, value| db_resource[param] = value unless db_resource[param] == value end # Handle file specially if (self.file and (!db_resource.file or db_resource.file != self.file)) db_resource.file = self.file end updated_params = @params.inject({}) do |hash, ary| hash[ary[0].to_s] = ary[1] hash end db_resource.ar_hash_merge(db_resource.get_params_hash(db_resource.param_values), updated_params, :create => Proc.new { |name, parameter| parameter.to_rails(db_resource) }, :delete => Proc.new { |values| values.each { |value| db_resource.param_values.delete(value) } }, :modify => Proc.new { |db, mem| mem.modify_rails_values(db) }) updated_tags = tags.inject({}) { |hash, tag| hash[tag] = tag hash } db_resource.ar_hash_merge(db_resource.get_tag_hash(), updated_tags, :create => Proc.new { |name, tag| db_resource.add_resource_tag(name) }, :delete => Proc.new { |tag| db_resource.resource_tags.delete(tag) }, :modify => Proc.new { |db, mem| # nothing here }) end + # Return the resource name, or the title if no name + # was specified. + def name + unless defined? @name + @name = self[:name] || self.title + end + @name + end + # This *significantly* reduces the number of calls to Puppet.[]. def paramcheck? unless defined? @@paramcheck @@paramcheck = Puppet[:paramcheck] end @@paramcheck end - # Verify that all passed parameters are valid. This throws an error if - # there's a problem, so we don't have to worry about the return value. - def paramcheck(param) - param = param.to_s - # Now make sure it's a valid argument to our class. These checks - # are organized in order of commonhood -- most types, it's a valid - # argument and paramcheck is enabled. - if @ref.typeclass.validattr?(param) - true - elsif %w{name title}.include?(param) # always allow these - true - elsif paramcheck? - self.fail Puppet::ParseError, "Invalid parameter '%s' for type '%s'" % - [param.inspect, @ref.type] - end - end - # A temporary occasion, until I get paths in the scopes figured out. def path to_s end # Return the short version of our name. def ref @ref.to_s end - # You have to pass a Resource::Param to this. - def set(param, value = nil, source = nil) - if value and source - param = Puppet::Parser::Resource::Param.new( - :name => param, :value => value, :source => source - ) - elsif ! param.is_a?(Puppet::Parser::Resource::Param) - raise ArgumentError, "Must pass a parameter or all necessary values" - end - # Because definitions are now parse-time, I can paramcheck immediately. - paramcheck(param.name) - - if current = @params[param.name] - # This is where we'd ignore any equivalent values if we wanted to, - # but that would introduce a lot of really bad ordering issues. - if param.source.child_of?(current.source) - if param.add - # Merge with previous value. - param.value = [ current.value, param.value ].flatten - end - - # Replace it, keeping all of its info. - @params[param.name] = param - else - if Puppet[:trace] - puts caller - end - msg = "Parameter '%s' is already set on %s" % - [param.name, self.to_s] - if current.source.to_s != "" - msg += " by %s" % current.source - end - if current.file or current.line - fields = [] - fields << current.file if current.file - fields << current.line.to_s if current.line - msg += " at %s" % fields.join(":") - end - msg += "; cannot redefine" - error = Puppet::ParseError.new(msg) - error.file = param.file if param.file - error.line = param.line if param.line - raise error + # Add a tag to our current list. These tags will be added to all + # of the objects contained in this scope. + def tag(*ary) + ary.each { |tag| + tag = tag.to_s + unless tag =~ /^\w[-\w]*$/ + fail Puppet::ParseError, "Invalid tag %s" % tag.inspect end - else - if self.source == param.source or param.source.child_of?(self.source) - @params[param.name] = param - else - fail Puppet::ParseError, "Only subclasses can set parameters" + unless @tags.include?(tag) + @tags << tag end - end + } end def tags - unless defined? @tags - @tags = scope.tags - @tags << self.type - end - @tags + @tags.dup end def to_hash @params.inject({}) do |hash, ary| param = ary[1] # Skip "undef" values. if param.value != :undef hash[param.name] = param.value end hash end end # Turn our parser resource into a Rails resource. def to_rails(host) args = rails_args db_resource = host.resources.build(args) # Handle file specially db_resource.file = self.file @params.each { |name, param| param.to_rails(db_resource) } tags.each { |tag| db_resource.add_resource_tag(tag) } return db_resource end def to_s self.ref end # Translate our object to a transportable object. def to_trans - unless builtin? - devfail "Tried to translate a non-builtin resource" + return nil if virtual? + + if builtin? + to_transobject + else + to_transbucket end + end - return nil if virtual? + 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. + return bucket + end + def to_transobject # Now convert to a transobject obj = Puppet::TransObject.new(@ref.title, @ref.type) to_hash.each do |p, v| if v.is_a?(Reference) v = v.to_ref elsif v.is_a?(Array) v = v.collect { |av| if av.is_a?(Reference) av = av.to_ref end 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. obj[p.to_s] = if v.is_a?(Array) and v.length == 1 v[0] else v end end obj.file = self.file obj.line = self.line obj.tags = self.tags return obj end - - def virtual? - self.virtual - end private + + # Add default values from our definition. + def add_defaults + scope.lookupdefaults(self.type).each do |name, param| + unless @params.include?(name) + self.debug "Adding default for %s" % name + + @params[name] = param + end + end + end + + # Add any metaparams defined in our scope. This actually adds any metaparams + # from any parent scope, and there's currently no way to turn that off. + def add_metaparams + Puppet::Type.eachmetaparam do |name| + # Skip metaparams that we already have defined. + next if @params[name] + if val = scope.lookupvar(name.to_s, false) + unless val == :undefined + set_parameter(name, val) + end + end + end + end + + # Add any overrides for this object. + def add_overrides + if overrides = scope.compile.resource_overrides(self) + overrides.each do |over| + self.merge(over) + end + + # Remove the overrides, so that the configuration knows there + # are none left. + overrides.clear + end + end + + # Accept a parameter from an override. + def override_parameter(param) + # This can happen if the override is defining a new parameter, rather + # than replacing an existing one. + unless current = @params[param.name] + @params[param.name] = param + return + end + + # The parameter is already set. See if they're allowed to override it. + if param.source.child_of?(current.source) + if param.add + # Merge with previous value. + param.value = [ current.value, param.value ].flatten + end + + # Replace it, keeping all of its info. + @params[param.name] = param + else + if Puppet[:trace] + puts caller + end + msg = "Parameter '%s' is already set on %s" % + [param.name, self.to_s] + if current.source.to_s != "" + msg += " by %s" % current.source + end + if current.file or current.line + fields = [] + fields << current.file if current.file + fields << current.line.to_s if current.line + msg += " at %s" % fields.join(":") + end + msg += "; cannot redefine" + raise Puppet::ParseError.new(msg, param.line, param.file) + end + end + + # Verify that all passed parameters are valid. This throws an error if + # there's a problem, so we don't have to worry about the return value. + def paramcheck(param) + param = param.to_s + # Now make sure it's a valid argument to our class. These checks + # are organized in order of commonhood -- most types, it's a valid + # argument and paramcheck is enabled. + if @ref.typeclass.validattr?(param) + true + elsif %w{name title}.include?(param) # always allow these + true + elsif paramcheck? + self.fail Puppet::ParseError, "Invalid parameter '%s' for type '%s'" % + [param, @ref.type] + end + end + def rails_args return [:type, :title, :line, :exported].inject({}) do |hash, param| # 'type' isn't a valid column name, so we have to use another name. to = (param == :type) ? :restype : param if value = self.send(param) hash[to] = value end hash end end -end -# $Id$ + # Define a parameter in our resource. + def set_parameter(param, value = nil) + if value + param = Puppet::Parser::Resource::Param.new( + :name => param, :value => value, :source => self.source + ) + elsif ! param.is_a?(Puppet::Parser::Resource::Param) + raise ArgumentError, "Must pass a parameter or all necessary values" + end + + # And store it in our parameter hash. + @params[param.name] = param + end + + # Make sure the resource's parameters are all valid for the type. + def validate + @params.each do |name, param| + # Make sure it's a valid parameter. + paramcheck(name) + end + end +end diff --git a/lib/puppet/parser/resource/reference.rb b/lib/puppet/parser/resource/reference.rb index 19d179660..543ee7195 100644 --- a/lib/puppet/parser/resource/reference.rb +++ b/lib/puppet/parser/resource/reference.rb @@ -1,71 +1,88 @@ # A reference to a resource. Mostly just the type and title. class Puppet::Parser::Resource::Reference include Puppet::Util::MethodHelper include Puppet::Util::Errors attr_accessor :type, :title, :builtin, :file, :line, :scope # Are we a builtin type? def builtin? unless defined? @builtin if builtintype() @builtin = true else @builtin = false end end - self.builtin + @builtin end def builtintype if t = Puppet::Type.type(self.type) and t.name != :component t else nil end end - # Return the defined type for our obj. + # Return the defined type for our obj. This can return classes, + # definitions or nodes. def definedtype unless defined? @definedtype - if tmp = @scope.finddefine(self.type) + type = self.type.to_s.downcase + name = self.title + case type + when "class": # look for host classes + if self.title == :main + tmp = @scope.findclass("") + else + tmp = @scope.findclass(self.title) + end + when "node": # look for node definitions + tmp = @scope.parser.nodes[self.title] + else # normal definitions + # We have to swap these variables around so the errors are right. + name = type + type = "type" + tmp = @scope.finddefine(self.type) + end + + if tmp @definedtype = tmp else - fail Puppet::ParseError, "Could not find resource type '%s'" % self.type + fail Puppet::ParseError, "Could not find resource %s '%s'" % [type, name] end end @definedtype end def initialize(hash) set_options(hash) requiredopts(:type, :title) end def to_ref return [type.to_s,title.to_s] end def to_s unless defined? @namestring - @namestring = "%s[%s]" % [type.capitalize, title] + @namestring = "%s[%s]" % [type.split("::").collect { |s| s.capitalize }.join("::"), title] end @namestring end def typeclass unless defined? @typeclass if tmp = builtintype || definedtype @typeclass = tmp else fail Puppet::ParseError, "Could not find type %s" % self.type end end @typeclass end end - -# $Id$ diff --git a/lib/puppet/parser/scope.rb b/lib/puppet/parser/scope.rb index 1fb4f6906..028414cc0 100644 --- a/lib/puppet/parser/scope.rb +++ b/lib/puppet/parser/scope.rb @@ -1,601 +1,332 @@ # The scope class, which handles storing and retrieving variables and types and # such. require 'puppet/parser/parser' require 'puppet/parser/templatewrapper' require 'puppet/transportable' require 'strscan' class Puppet::Parser::Scope require 'puppet/parser/resource' AST = Puppet::Parser::AST Puppet::Util.logmethods(self) include Enumerable include Puppet::Util::Errors - attr_accessor :parent, :level, :interp, :source - attr_accessor :name, :type, :base, :keyword - attr_accessor :top, :translated, :exported, :virtual, :configuration + attr_accessor :parent, :level, :parser, :source, :resource + attr_accessor :base, :keyword, :nodescope + attr_accessor :top, :translated, :compile # Proxy accessors def host - @configuration.host + @compile.node.name end + def interpreter - @configuration.interpreter + @compile.interpreter end # Is the value true? This allows us to control the definition of truth # in one place. def self.true?(value) if value == false or value == "" or value == :undef return false else return true end end # Add to our list of namespaces. def add_namespace(ns) return false if @namespaces.include?(ns) if @namespaces == [""] @namespaces = [ns] else @namespaces << ns end end - # Is the type a builtin type? - def builtintype?(type) - if typeklass = Puppet::Type.type(type) - return typeklass - else - return false - end - end - - # Create a new child scope. - def child=(scope) - @children.push(scope) - - # Copy all of the shared tables over to the child. - @@sharedtables.each do |name| - scope.send(name.to_s + "=", self.send(name)) - end - end - - # Verify that the given object isn't defined elsewhere. - def chkobjectclosure(obj) - if exobj = @definedtable[obj.ref] - typeklass = Puppet::Type.type(obj.type) - if typeklass and ! typeklass.isomorphic? - Puppet.info "Allowing duplicate %s" % type - else - # Either it's a defined type, which are never - # isomorphic, or it's a non-isomorphic type. - msg = "Duplicate definition: %s is already defined" % obj.ref - - if exobj.file and exobj.line - msg << " in file %s at line %s" % - [exobj.file, exobj.line] - end - - if obj.line or obj.file - msg << "; cannot redefine" - end - - raise Puppet::ParseError.new(msg) - end - end - - return true - end - - # Remove a specific child. - def delete(child) - @children.delete(child) - end - - # Remove a resource from the various tables. This is only used when - # a resource maps to a definition and gets evaluated. - def deleteresource(resource) - if @definedtable[resource.ref] - @definedtable.delete(resource.ref) - end - - if @children.include?(resource) - @children.delete(resource) - end - end - # Are we the top scope? def topscope? @level == 1 end - # Yield each child scope in turn - def each - @children.each { |child| - yield child - } - end - - # Evaluate a list of classes. - def evalclasses(*classes) - retval = [] - classes.each do |klass| - if obj = findclass(klass) - obj.safeevaluate :scope => self - retval << klass - end - end - retval - end - - def exported? - self.exported - end - def findclass(name) @namespaces.each do |namespace| - if r = interp.findclass(namespace, name) + if r = parser.findclass(namespace, name) return r end end return nil end def finddefine(name) @namespaces.each do |namespace| - if r = interp.finddefine(namespace, name) + if r = parser.finddefine(namespace, name) return r end end return nil end def findresource(string, name = nil) - if name - string = "%s[%s]" % [string.capitalize, name] - end - - @definedtable[string] - end - - # Recursively complete the whole tree, in preparation for - # translation or storage. - def finish - self.each do |obj| - obj.finish - end + compile.findresource(string, name) end - # Initialize our new scope. Defaults to having no parent and to - # being declarative. + # Initialize our new scope. Defaults to having no parent. def initialize(hash = {}) - @finished = false if hash.include?(:namespace) if n = hash[:namespace] @namespaces = [n] end hash.delete(:namespace) else @namespaces = [""] end hash.each { |name, val| method = name.to_s + "=" if self.respond_to? method self.send(method, val) else raise Puppet::DevError, "Invalid scope argument %s" % name end } @tags = [] - # Our child scopes and objects - @children = [] - # The symbol table for this scope. This is where we store variables. @symtable = {} # All of the defaults set for types. It's a hash of hashes, # with the first key being the type, then the second key being # the parameter. - @defaultstable = Hash.new { |dhash,type| + @defaults = Hash.new { |dhash,type| dhash[type] = {} } end # Collect all of the defaults set at any higher scopes. # This is a different type of lookup because it's additive -- # it collects all of the defaults, with defaults in closer scopes # overriding those in later scopes. def lookupdefaults(type) values = {} # first collect the values from the parents unless parent.nil? parent.lookupdefaults(type).each { |var,value| values[var] = value } end # then override them with any current values # this should probably be done differently - if @defaultstable.include?(type) - @defaultstable[type].each { |var,value| + if @defaults.include?(type) + @defaults[type].each { |var,value| values[var] = value } end #Puppet.debug "Got defaults for %s: %s" % # [type,values.inspect] return values end - # Look up all of the exported objects of a given type. - def lookupexported(type) - @definedtable.find_all do |name, r| - r.type == type and r.exported? - end - end - - def lookupoverrides(obj) - @overridetable[obj.ref] - end - # Look up a defined type. def lookuptype(name) finddefine(name) || findclass(name) end def lookup_qualified_var(name, usestring) parts = name.split(/::/) shortname = parts.pop klassname = parts.join("::") klass = findclass(klassname) unless klass raise Puppet::ParseError, "Could not find class %s" % klassname end - unless kscope = class_scope(klass) + unless kscope = compile.class_scope(klass) raise Puppet::ParseError, "Class %s has not been evaluated so its variables cannot be referenced" % klass.classname end return kscope.lookupvar(shortname, usestring) end private :lookup_qualified_var # Look up a variable. The simplest value search we do. Default to returning # an empty string for missing values, but support returning a constant. def lookupvar(name, usestring = true) # If the variable is qualified, then find the specified scope and look the variable up there instead. if name =~ /::/ return lookup_qualified_var(name, usestring) end # We can't use "if @symtable[name]" here because the value might be false if @symtable.include?(name) if usestring and @symtable[name] == :undef return "" else return @symtable[name] end elsif self.parent return parent.lookupvar(name, usestring) elsif usestring return "" else return :undefined end end def namespaces @namespaces.dup end - # Create a new scope. - def newscope(hash = {}) - hash[:parent] = self - #debug "Creating new scope, level %s" % [self.level + 1] - return Puppet::Parser::Scope.new(hash) + # Create a new scope and set these options. + def newscope(options = {}) + compile.newscope(self, options) end # Is this class for a node? This is used to make sure that # nodes and classes with the same name conflict (#620), which # is required because of how often the names are used throughout # the system, including on the client. def nodescope? - defined?(@nodescope) and @nodescope - end - - # Return the list of remaining overrides. - def overrides - #@overridetable.collect { |name, overs| overs }.flatten - @overridetable.values.flatten + self.nodescope end # We probably shouldn't cache this value... But it's a lot faster # than doing lots of queries. def parent unless defined?(@parent) - @parent = configuration.parent(self) + @parent = compile.parent(self) end @parent end # Return the list of scopes up to the top scope, ordered with our own first. # This is used for looking up variables and defaults. def scope_path if parent [self, parent.scope_path].flatten.compact else [self] end end - def resources - @definedtable.values - end - - # Store the fact that we've evaluated a given class. We use a hash - # that gets inherited from the top scope down, rather than a global - # hash. We store the object ID, not class name, so that we - # can support multiple unrelated classes with the same name. - def setclass(klass) - if klass.is_a?(AST::HostClass) - unless klass.classname - raise Puppet::DevError, "Got a %s with no fully qualified name" % - klass.class - end - @configuration.class_set(klass.classname, self) - else - raise Puppet::DevError, "Invalid class %s" % klass.inspect - end - if klass.is_a?(AST::Node) - @nodescope = true - end - nil - end - - # Set all of our facts in the top-level scope. - def setfacts(facts) - facts.each { |var, value| - self.setvar(var, value) - } - end - - # Add a new object to our object table and the global list, and do any necessary - # checks. - def setresource(obj) - self.chkobjectclosure(obj) - - @children << obj - - # Mark the resource as virtual or exported, as necessary. - if self.exported? - obj.exported = true - elsif self.virtual? - obj.virtual = true - end - - # The global table - @definedtable[obj.ref] = obj - - return obj - end - - # Override a parameter in an existing object. If the object does not yet - # exist, then cache the override in a global table, so it can be flushed - # at the end. - def setoverride(resource) - resource.override = true - if obj = @definedtable[resource.ref] - obj.merge(resource) - else - @overridetable[resource.ref] << resource - end - end - # Set defaults for a type. The typename should already be downcased, # so that the syntax is isolated. We don't do any kind of type-checking # here; instead we let the resource do it when the defaults are used. def setdefaults(type, params) - table = @defaultstable[type] + table = @defaults[type] # if we got a single param, it'll be in its own array params = [params] unless params.is_a?(Array) params.each { |param| #Puppet.debug "Default for %s is %s => %s" % # [type,ary[0].inspect,ary[1].inspect] - if @@declarative - if table.include?(param.name) - self.fail "Default already defined for %s { %s }" % - [type,param.name] - end - else - if table.include?(param.name) - # we should maybe allow this warning to be turned off... - Puppet.warning "Replacing default for %s { %s }" % - [type,param.name] - end + if table.include?(param.name) + raise Puppet::ParseError.new("Default already defined for %s { %s }; cannot redefine" % [type, param.name], param.line, param.file) end table[param.name] = param } end # Set a variable in the current scope. This will override settings # in scopes above, but will not allow variables in the current scope - # to be reassigned if we're declarative (which is the default). + # to be reassigned. def setvar(name,value, file = nil, line = nil) #Puppet.debug "Setting %s to '%s' at level %s" % # [name.inspect,value,self.level] if @symtable.include?(name) - if @@declarative - error = Puppet::ParseError.new("Cannot reassign variable %s" % name) - if file - error.file = file - end - if line - error.line = line - end - raise error - else - Puppet.warning "Reassigning %s to %s" % [name,value] + error = Puppet::ParseError.new("Cannot reassign variable %s" % name) + if file + error.file = file + end + if line + error.line = line end + raise error end @symtable[name] = value end # Return an interpolated string. def strinterp(string, file = nil, line = nil) # Most strings won't have variables in them. ss = StringScanner.new(string) out = "" while not ss.eos? if ss.scan(/^\$\{((\w*::)*\w+)\}|^\$((\w*::)*\w+)/) # If it matches the backslash, then just retun the dollar sign. if ss.matched == '\\$' out << '$' else # look the variable up out << lookupvar(ss[1] || ss[3]).to_s || "" end elsif ss.scan(/^\\(.)/) # Puppet.debug("Got escape: pos:%d; m:%s" % [ss.pos, ss.matched]) case ss[1] when 'n' out << "\n" when 't' out << "\t" when 's' out << " " when '\\' out << '\\' when '$' out << '$' else str = "Unrecognised escape sequence '#{ss.matched}'" if file str += " in file %s" % file end if line str += " at line %s" % line end Puppet.warning str out << ss.matched end elsif ss.scan(/^\$/) out << '$' elsif ss.scan(/^\\\n/) # an escaped carriage return next else tmp = ss.scan(/[^\\$]+/) # Puppet.debug("Got other: pos:%d; m:%s" % [ss.pos, tmp]) unless tmp error = Puppet::ParseError.new("Could not parse string %s" % string.inspect) {:file= => file, :line= => line}.each do |m,v| error.send(m, v) if v end raise error end out << tmp end end return out end - # Add a tag to our current list. These tags will be added to all - # of the objects contained in this scope. - def tag(*ary) - ary.each { |tag| - if tag.nil? or tag == "" - puts caller - Puppet.debug "got told to tag with %s" % tag.inspect - next - end - unless tag =~ /^\w[-\w]*$/ - fail Puppet::ParseError, "Invalid tag %s" % tag.inspect - end - tag = tag.to_s - unless @tags.include?(tag) - #Puppet.info "Tagging scope %s with %s" % [self.object_id, tag] - @tags << tag - end - } - end - # Return the tags associated with this scope. It's basically # just our parents' tags, plus our type. We don't cache this value # because our parent tags might change between calls. def tags - tmp = [] + @tags - unless ! defined? @type or @type.nil? or @type == "" - tmp << @type.to_s - end - if parent - #info "Looking for tags in %s" % parent.type - parent.tags.each { |tag| - if tag.nil? or tag == "" - Puppet.debug "parent returned tag %s" % tag.inspect - next - end - unless tmp.include?(tag) - tmp << tag - end - } - end - return tmp.sort.uniq + resource.tags end # Used mainly for logging def to_s - if self.name - return "%s[%s]" % [@type, @name] - else - return self.type.to_s - end - end - - # Convert all of our objects as necessary. - def translate - ret = @children.collect do |child| - case child - when Puppet::Parser::Resource - child.to_trans - when self.class - child.translate - else - devfail "Got %s for translation" % child.class - end - end.reject { |o| o.nil? } - bucket = Puppet::TransBucket.new ret - - case self.type - when "": bucket.type = "main" - when nil: devfail "A Scope with no type" - else - bucket.type = @type - end - if self.name - bucket.name = self.name - end - return bucket + "Scope(%s)" % @resource.to_s end # Undefine a variable; only used for testing. def unsetvar(var) if @symtable.include?(var) @symtable.delete(var) end end - - def virtual? - self.virtual || self.exported? - end end - -# $Id$ diff --git a/lib/puppet/parser/templatewrapper.rb b/lib/puppet/parser/templatewrapper.rb index 3b8cc3a3a..f863e31aa 100644 --- a/lib/puppet/parser/templatewrapper.rb +++ b/lib/puppet/parser/templatewrapper.rb @@ -1,54 +1,54 @@ # A simple wrapper for templates, so they don't have full access to # the scope objects. class Puppet::Parser::TemplateWrapper attr_accessor :scope, :file include Puppet::Util Puppet::Util.logmethods(self) def initialize(scope, file) @scope = scope - @file = Puppet::Module::find_template(file) + @file = Puppet::Module::find_template(file, @scope.compile.environment) unless FileTest.exists?(@file) raise Puppet::ParseError, "Could not find template %s" % file end - # We'll only ever not have an interpreter in testing, but, eh. - if @scope.interp - @scope.interp.newfile(@file) + # We'll only ever not have a parser in testing, but, eh. + if @scope.parser + @scope.parser.watch_file(@file) end end # Ruby treats variables like methods, so we can cheat here and # trap missing vars like they were missing methods. def method_missing(name, *args) # We have to tell lookupvar to return :undefined to us when # appropriate; otherwise it converts to "". value = @scope.lookupvar(name.to_s, false) if value != :undefined return value else # Just throw an error immediately, instead of searching for # other missingmethod things or whatever. raise Puppet::ParseError, "Could not find value for '%s'" % name end end def result result = nil benchmark(:debug, "Interpolated template #{@file}") do template = ERB.new(File.read(@file), 0, "-") result = template.result(binding) end result end def to_s "template[%s]" % @file end end # $Id$ diff --git a/lib/puppet/propertychange.rb b/lib/puppet/propertychange.rb index 964ff2b31..35bbede1a 100644 --- a/lib/puppet/propertychange.rb +++ b/lib/puppet/propertychange.rb @@ -1,143 +1,141 @@ # the class responsible for actually doing any work # enables no-op and logging/rollback module Puppet # Handle all of the work around performing an actual change, # including calling 'sync' on the properties and producing events. class PropertyChange attr_accessor :is, :should, :type, :path, :property, :transaction, :changed, :proxy # The log file generated when this object was changed. attr_reader :report # Switch the goals of the property, thus running the change in reverse. def backward @property.should = @is @is = @property.retrieve unless defined? @transaction raise Puppet::Error, "PropertyChange '%s' tried to be executed outside of transaction" % self end unless @property.insync?(@is) @property.info "Backing %s" % self return self.go else @property.debug "rollback is already in sync: %s vs. %s" % [@is, @property.should.inspect] return nil end end def changed? self.changed end # Create our event object. def event(name) # default to a simple event type unless name.is_a?(Symbol) @property.warning("Property '%s' returned invalid event '%s'; resetting to default" % [@property.class, name]) event = @property.resource.class.name.id2name + "_changed" end Puppet::Event.new( :event => name, :transaction => @transaction, :source => self.source ) end def initialize(property, currentvalue) unless property.is_a?(Puppet::Property) raise Puppet::DevError, "Got a %s instead of a property" % property.class end @property = property @path = [property.path,"change"].flatten @is = currentvalue @should = property.should @changed = false end # Perform the actual change. This method can go either forward or # backward, and produces an event. def go if skip? if self.noop return [event(:noop)] else return nil end end # The transaction catches any exceptions here. events = @property.sync if events.nil? return nil end if events.is_a?(Array) if events.empty? return nil end else events = [events] end return events.collect { |name| @report = @property.log(@property.change_to_s(@is, @should)) event(name) } end def forward #@property.debug "moving change forward" unless defined? @transaction raise Puppet::Error, "PropertyChange '%s' tried to be executed outside of transaction" % self end return self.go end def noop return @property.noop end def skip? if @property.insync?(@is) @property.info "Already in sync" return true end if @property.noop @property.log "is %s, should be %s (noop)" % [property.is_to_s(@is), property.should_to_s(@should)] #@property.debug "%s is noop" % @property return true end return false end def source self.proxy || @property.resource end def to_s return "change %s.%s(%s)" % [@transaction.object_id, self.object_id, @property.change_to_s(@is, @should)] #return "change %s.%s" % [@transaction.object_id, self.object_id] end end end - -# $Id$ diff --git a/lib/puppet/rails/host.rb b/lib/puppet/rails/host.rb index ca1e10c93..b7ca4c2e4 100644 --- a/lib/puppet/rails/host.rb +++ b/lib/puppet/rails/host.rb @@ -1,161 +1,157 @@ require 'puppet/rails/resource' require 'puppet/rails/fact_name' require 'puppet/rails/source_file' require 'puppet/util/rails/collection_merger' # Puppet::TIME_DEBUG = true class Puppet::Rails::Host < ActiveRecord::Base include Puppet::Util include Puppet::Util::CollectionMerger has_many :fact_values, :dependent => :destroy has_many :fact_names, :through => :fact_values belongs_to :puppet_classes has_many :source_files has_many :resources, :include => :param_values, :dependent => :destroy # If the host already exists, get rid of its objects def self.clean(host) if obj = self.find_by_name(host) obj.rails_objects.clear return obj else return nil end end # Store our host in the database. - def self.store(hash) + def self.store(node, resources) unless name = hash[:name] raise ArgumentError, "You must specify the hostname for storage" end args = {} host = nil transaction do #unless host = find_by_name(name) seconds = Benchmark.realtime { unless host = find_by_name(name) - host = new(:name => name) + host = new(:name => node.name) end } Puppet.notice("Searched for host in %0.2f seconds" % seconds) if defined?(Puppet::TIME_DEBUG) - if ip = hash[:facts]["ipaddress"] + if ip = node.parameters["ipaddress"] host.ip = ip end # Store the facts into the database. - host.setfacts(hash[:facts]) - - unless hash[:resources] - raise ArgumentError, "You must pass resources" - end + host.setfacts node.parameters seconds = Benchmark.realtime { - host.setresources(hash[:resources]) + host.setresources(resources) } Puppet.notice("Handled resources in %0.2f seconds" % seconds) if defined?(Puppet::TIME_DEBUG) host.last_compile = Time.now host.save end return host end # Return the value of a fact. def fact(name) if fv = self.fact_values.find(:all, :include => :fact_name, :conditions => "fact_names.name = '#{name}'") return fv else return nil end end # returns a hash of fact_names.name => [ fact_values ] for this host. def get_facts_hash fact_values = self.fact_values.find(:all, :include => :fact_name) return fact_values.inject({}) do | hash, value | hash[value.fact_name.name] ||= [] hash[value.fact_name.name] << value hash end end def setfacts(facts) facts = facts.dup ar_hash_merge(get_facts_hash(), facts, :create => Proc.new { |name, values| fact_name = Puppet::Rails::FactName.find_or_create_by_name(name) values = [values] unless values.is_a?(Array) values.each do |value| fact_values.build(:value => value, :fact_name => fact_name) end }, :delete => Proc.new { |values| values.each { |value| self.fact_values.delete(value) } }, :modify => Proc.new { |db, mem| mem = [mem].flatten fact_name = db[0].fact_name db_values = db.collect { |fact_value| fact_value.value } (db_values - (db_values & mem)).each do |value| db.find_all { |fact_value| fact_value.value == value }.each { |fact_value| fact_values.delete(fact_value) } end (mem - (db_values & mem)).each do |value| fact_values.build(:value => value, :fact_name => fact_name) end }) end # Set our resources. def setresources(list) existing = nil seconds = Benchmark.realtime { # Preload the parameters with the resource query, but not the tags, since doing so makes the query take about 10x longer. # I've left the other queries in so that it's straightforward to switch between them for testing, if we so desire. #existing = resources.find(:all, :include => [{:param_values => :param_name, :resource_tags => :puppet_tag}, :source_file]).inject({}) do | hash, resource | #existing = resources.find(:all, :include => [{:resource_tags => :puppet_tag}, :source_file]).inject({}) do | hash, resource | existing = resources.find(:all, :include => [{:param_values => :param_name}, :source_file]).inject({}) do | hash, resource | hash[resource.ref] = resource hash end } Puppet.notice("Searched for resources in %0.2f seconds" % seconds) if defined?(Puppet::TIME_DEBUG) compiled = list.inject({}) do |hash, resource| hash[resource.ref] = resource hash end ar_hash_merge(existing, compiled, :create => Proc.new { |ref, resource| resource.to_rails(self) }, :delete => Proc.new { |resource| self.resources.delete(resource) }, :modify => Proc.new { |db, mem| mem.modify_rails(db) }) end def update_connect_time self.last_connect = Time.now save end end # $Id$ diff --git a/lib/puppet/rails/resource.rb b/lib/puppet/rails/resource.rb index 19aeb9205..785c63419 100644 --- a/lib/puppet/rails/resource.rb +++ b/lib/puppet/rails/resource.rb @@ -1,125 +1,123 @@ require 'puppet' require 'puppet/rails/param_name' require 'puppet/rails/puppet_tag' require 'puppet/util/rails/collection_merger' class Puppet::Rails::Resource < ActiveRecord::Base include Puppet::Util::CollectionMerger has_many :param_values, :dependent => :destroy has_many :param_names, :through => :param_values has_many :resource_tags, :dependent => :destroy has_many :puppet_tags, :through => :resource_tags belongs_to :source_file belongs_to :host def add_resource_tag(tag) pt = Puppet::Rails::PuppetTag.find_or_create_by_name(tag, :include => :puppet_tag) resource_tags.create(:puppet_tag => pt) end def file if f = self.source_file return f.filename else return nil end end def file=(file) self.source_file = Puppet::Rails::SourceFile.find_or_create_by_filename(file) end # returns a hash of param_names.name => [param_values] def get_params_hash(values = nil) values ||= param_values.find(:all, :include => :param_name) values.inject({}) do | hash, value | hash[value.param_name.name] ||= [] hash[value.param_name.name] << value hash end end def get_tag_hash(tags = nil) tags ||= resource_tags.find(:all, :include => :puppet_tag) return tags.inject({}) do |hash, tag| # We have to store the tag object, not just the tag name. hash[tag.puppet_tag.name] = tag hash end end def [](param) return super || parameter(param) end def name ref() end def parameter(param) if pn = param_names.find_by_name(param) if pv = param_values.find(:first, :conditions => [ 'param_name_id = ?', pn] ) return pv.value else return nil end end end def parameters result = get_params_hash result.each do |param, value| if value.is_a?(Array) result[param] = value.collect { |v| v.value } else result[param] = value.value end end result end def ref "%s[%s]" % [self[:restype].capitalize, self[:title]] end # Convert our object to a resource. Do not retain whether the object # is exported, though, since that would cause it to get stripped # from the configuration. def to_resource(scope) hash = self.attributes hash["type"] = hash["restype"] hash.delete("restype") # FIXME At some point, we're going to want to retain this information # for logging and auditing. hash.delete("host_id") hash.delete("updated_at") hash.delete("source_file_id") hash.delete("id") hash.each do |p, v| hash.delete(p) if v.nil? end hash[:scope] = scope hash[:source] = scope.source - obj = Puppet::Parser::Resource.new(hash) - + hash[:params] = [] names = [] self.param_names.each do |pname| # We can get the same name multiple times because of how the # db layout works. next if names.include?(pname.name) names << pname.name - obj.set(pname.to_resourceparam(self, scope.source)) + hash[:params] << pname.to_resourceparam(self, scope.source) end + obj = Puppet::Parser::Resource.new(hash) # Store the ID, so we can check if we're re-collecting the same resource. obj.rails_id = self.id return obj end end - -# $Id$ diff --git a/lib/puppet/sslcertificates/ca.rb b/lib/puppet/sslcertificates/ca.rb index 018640d36..5e355f7fe 100644 --- a/lib/puppet/sslcertificates/ca.rb +++ b/lib/puppet/sslcertificates/ca.rb @@ -1,385 +1,417 @@ +require 'sync' + class Puppet::SSLCertificates::CA include Puppet::Util::Warnings Certificate = Puppet::SSLCertificates::Certificate attr_accessor :keyfile, :file, :config, :dir, :cert, :crl def certfile @config[:cacert] end # Remove all traces of a given host. This is kind of hackish, but, eh. def clean(host) host = host.downcase [:csrdir, :signeddir, :publickeydir, :privatekeydir, :certdir].each do |name| dir = Puppet[name] file = File.join(dir, host + ".pem") if FileTest.exists?(file) begin if Puppet[:name] == "puppetca" puts "Removing %s" % file else Puppet.info "Removing %s" % file end File.unlink(file) rescue => detail raise Puppet::Error, "Could not delete %s: %s" % [file, detail] end end end end def host2csrfile(hostname) File.join(Puppet[:csrdir], [hostname.downcase, "pem"].join(".")) end # this stores signed certs in a directory unrelated to # normal client certs def host2certfile(hostname) File.join(Puppet[:signeddir], [hostname.downcase, "pem"].join(".")) end # Turn our hostname into a Name object def thing2name(thing) thing.subject.to_a.find { |ary| ary[0] == "CN" }[1] end def initialize(hash = {}) Puppet.config.use(:main, :ca, :ssl) self.setconfig(hash) if Puppet[:capass] if FileTest.exists?(Puppet[:capass]) #puts "Reading %s" % Puppet[:capass] #system "ls -al %s" % Puppet[:capass] #File.read Puppet[:capass] @config[:password] = self.getpass else # Don't create a password if the cert already exists unless FileTest.exists?(@config[:cacert]) @config[:password] = self.genpass end end end self.getcert init_crl unless FileTest.exists?(@config[:serial]) Puppet.config.write(:serial) do |f| f << "%04X" % 1 end end end # Generate a new password for the CA. def genpass pass = "" 20.times { pass += (rand(74) + 48).chr } begin Puppet.config.write(:capass) { |f| f.print pass } rescue Errno::EACCES => detail raise Puppet::Error, detail.to_s end return pass end # Get the CA password. def getpass if @config[:capass] and File.readable?(@config[:capass]) return File.read(@config[:capass]) else raise Puppet::Error, "Could not read CA passfile %s" % @config[:capass] end end # Get the CA cert. def getcert if FileTest.exists?(@config[:cacert]) @cert = OpenSSL::X509::Certificate.new( File.read(@config[:cacert]) ) else self.mkrootcert end end # Retrieve a client's CSR. def getclientcsr(host) csrfile = host2csrfile(host) unless File.exists?(csrfile) return nil end return OpenSSL::X509::Request.new(File.read(csrfile)) end # Retrieve a client's certificate. def getclientcert(host) certfile = host2certfile(host) unless File.exists?(certfile) return [nil, nil] end return [OpenSSL::X509::Certificate.new(File.read(certfile)), @cert] end # List certificates waiting to be signed. This returns a list of hostnames, not actual # files -- the names can be converted to full paths with host2csrfile. def list return Dir.entries(Puppet[:csrdir]).find_all { |file| file =~ /\.pem$/ }.collect { |file| file.sub(/\.pem$/, '') } end # Create the root certificate. def mkrootcert # Make the root cert's name the FQDN of the host running the CA. name = Facter["hostname"].value if domain = Facter["domain"].value name += "." + domain end cert = Certificate.new( :name => name, :cert => @config[:cacert], :encrypt => @config[:capass], :key => @config[:cakey], :selfsign => true, :ttl => ttl, :type => :ca ) # This creates the cakey file Puppet::Util::SUIDManager.asuser(Puppet[:user], Puppet[:group]) do @cert = cert.mkselfsigned end Puppet.config.write(:cacert) do |f| f.puts @cert.to_pem end Puppet.config.write(:capub) do |f| f.puts @cert.public_key end return cert end def removeclientcsr(host) csrfile = host2csrfile(host) unless File.exists?(csrfile) raise Puppet::Error, "No certificate request for %s" % host end File.unlink(csrfile) end # Revoke the certificate with serial number SERIAL issued by this # CA. The REASON must be one of the OpenSSL::OCSP::REVOKED_* reasons def revoke(serial, reason = OpenSSL::OCSP::REVOKED_STATUS_KEYCOMPROMISE) if @config[:cacrl] == 'none' raise Puppet::Error, "Revocation requires a CRL, but ca_crl is set to 'none'" end time = Time.now revoked = OpenSSL::X509::Revoked.new revoked.serial = serial revoked.time = time enum = OpenSSL::ASN1::Enumerated(reason) ext = OpenSSL::X509::Extension.new("CRLReason", enum) revoked.add_extension(ext) @crl.add_revoked(revoked) store_crl end # Take the Puppet config and store it locally. def setconfig(hash) @config = {} Puppet.config.params("ca").each { |param| param = param.intern if param.is_a? String if hash.include?(param) @config[param] = hash[param] Puppet[param] = hash[param] hash.delete(param) else @config[param] = Puppet[param] end } if hash.include?(:password) @config[:password] = hash[:password] hash.delete(:password) end if hash.length > 0 raise ArgumentError, "Unknown parameters %s" % hash.keys.join(",") end [:cadir, :csrdir, :signeddir].each { |dir| unless @config[dir] raise Puppet::DevError, "%s is undefined" % dir end } end + # Create an exclusive lock for reading and writing, and do the + # writing in a tmp file. + def readwritelock(file, mode = 0600) + tmpfile = file + ".tmp" + sync = Sync.new + unless FileTest.directory?(File.dirname(tmpfile)) + raise Puppet::DevError, "Cannot create %s; directory %s does not exist" % + [file, File.dirname(file)] + end + sync.synchronize(Sync::EX) do + File.open(file, "r+", mode) do |rf| + rf.lock_exclusive do + File.open(tmpfile, "w", mode) do |tf| + yield tf + end + begin + File.rename(tmpfile, file) + rescue => detail + Puppet.err "Could not rename %s to %s: %s" % + [file, tmpfile, detail] + end + end + end + end + end + + # Sign a given certificate request. def sign(csr) unless csr.is_a?(OpenSSL::X509::Request) raise Puppet::Error, "CA#sign only accepts OpenSSL::X509::Request objects, not %s" % csr.class end unless csr.verify(csr.public_key) raise Puppet::Error, "CSR sign verification failed" end - serial = File.read(@config[:serial]).chomp.hex + serial = nil + readwritelock(@config[:serial]) { |f| + serial = File.read(@config[:serial]).chomp.hex + + # increment the serial + f << "%04X" % (serial + 1) + } + newcert = Puppet::SSLCertificates.mkcert( :type => :server, :name => csr.subject, :ttl => ttl, :issuer => @cert, :serial => serial, :publickey => csr.public_key ) - # increment the serial - Puppet.config.write(:serial) do |f| - f << "%04X" % (serial + 1) - end sign_with_key(newcert) self.storeclientcert(newcert) return [newcert, @cert] end # Store the client's CSR for later signing. This is called from # server/ca.rb, and the CSRs are deleted once the certificate is actually # signed. def storeclientcsr(csr) host = thing2name(csr) csrfile = host2csrfile(host) if File.exists?(csrfile) raise Puppet::Error, "Certificate request for %s already exists" % host end Puppet.config.writesub(:csrdir, csrfile) do |f| f.print csr.to_pem end end # Store the certificate that we generate. def storeclientcert(cert) host = thing2name(cert) certfile = host2certfile(host) if File.exists?(certfile) Puppet.notice "Overwriting signed certificate %s for %s" % [certfile, host] end Puppet::SSLCertificates::Inventory::add(cert) Puppet.config.writesub(:signeddir, certfile) do |f| f.print cert.to_pem end end # TTL for new certificates in seconds. If config param :ca_ttl is set, # use that, otherwise use :ca_days for backwards compatibility def ttl days = @config[:ca_days] if days && days.size > 0 warnonce "Parameter ca_ttl is not set. Using depecated ca_days instead." return @config[:ca_days] * 24 * 60 * 60 else ttl = @config[:ca_ttl] if ttl.is_a?(String) unless ttl =~ /^(\d+)(y|d|h|s)$/ raise ArgumentError, "Invalid ca_ttl #{ttl}" end case $2 when 'y' unit = 365 * 24 * 60 * 60 when 'd' unit = 24 * 60 * 60 when 'h' unit = 60 * 60 when 's' unit = 1 else raise ArgumentError, "Invalid unit for ca_ttl #{ttl}" end return $1.to_i * unit else return ttl end end end private def init_crl if FileTest.exists?(@config[:cacrl]) @crl = OpenSSL::X509::CRL.new( File.read(@config[:cacrl]) ) elsif @config[:cacrl] == 'none' @crl = nil else # Create new CRL @crl = OpenSSL::X509::CRL.new @crl.issuer = @cert.subject @crl.version = 1 store_crl @crl end end def store_crl # Increment the crlNumber e = @crl.extensions.find { |e| e.oid == 'crlNumber' } ext = @crl.extensions.reject { |e| e.oid == 'crlNumber' } crlNum = OpenSSL::ASN1::Integer(e ? e.value.to_i + 1 : 0) ext << OpenSSL::X509::Extension.new("crlNumber", crlNum) @crl.extensions = ext # Set last/next update now = Time.now @crl.last_update = now # Keep CRL valid for 5 years @crl.next_update = now + 5 * 365*24*60*60 sign_with_key(@crl) Puppet.config.write(:cacrl) do |f| f.puts @crl.to_pem end end def sign_with_key(signable, digest = OpenSSL::Digest::SHA1.new) cakey = nil if @config[:password] cakey = OpenSSL::PKey::RSA.new( File.read(@config[:cakey]), @config[:password] ) else cakey = OpenSSL::PKey::RSA.new( File.read(@config[:cakey]) ) end unless @cert.check_private_key(cakey) raise Puppet::Error, "CA Certificate is invalid" end signable.sign(cakey, digest) end end # $Id$ diff --git a/lib/puppet/type/component.rb b/lib/puppet/type/component.rb index 6366c7f24..5905d85ab 100644 --- a/lib/puppet/type/component.rb +++ b/lib/puppet/type/component.rb @@ -1,239 +1,241 @@ # the object allowing us to build complex structures # this thing contains everything else, including itself require 'puppet' require 'puppet/type' require 'puppet/transaction' require 'puppet/pgraph' Puppet::Type.newtype(:component) do include Enumerable attr_accessor :children newparam(:name) do desc "The name of the component. Generally optional." isnamevar end newparam(:type) do desc "The type that this component maps to. Generally some kind of class from the language." defaultto "component" end # Remove a child from the component. def delete(child) if @children.include?(child) @children.delete(child) return true else return super end end # Recurse deeply through the tree, but only yield types, not properties. def delve(&block) self.each do |obj| if obj.is_a?(self.class) obj.delve(&block) end end block.call(self) end # Return each child in turn. def each @children.each { |child| yield child } end # flatten all children, sort them, and evaluate them in order # this is only called on one component over the whole system # this also won't work with scheduling, but eh def evaluate self.finalize unless self.finalized? transaction = Puppet::Transaction.new(self) transaction.component = self return transaction end # Do all of the polishing off, mostly doing autorequires and making # dependencies. This will get run once on the top-level component, # and it will do everything necessary. def finalize started = {} finished = {} # First do all of the finish work, which mostly involves self.delve do |object| # Make sure we don't get into loops if started.has_key?(object) debug "Already finished %s" % object.title next else started[object] = true end unless finished.has_key?(object) object.finish finished[object] = true end end @finalized = true end def finalized? if defined? @finalized return @finalized else return false end end # Initialize a new component def initialize(args) @children = [] super(args) # If the title isn't a full resource reference, assume # we're a class and make an alias for that. unless @title.to_s.include?("[") self.class.alias("class[%s]" % @title, self) end end def initvars super @children = [] end def parent=(parent) if self.parentof?(parent) devfail "%s[%s] is already the parent of %s[%s]" % [self.class.name, self.title, parent.class.name, parent.title] end @parent = parent end # Add a hook for testing for recursion. def parentof?(child) if super(child) return true elsif @children.include?(child) debug "child is already in children array" return true else return false end end def push(*childs) unless defined? @children @children = [] end childs.each { |child| # Make sure we don't have any loops here. if parentof?(child) devfail "Already the parent of %s[%s]" % [child.class.name, child.title] end unless child.is_a?(Puppet::Type) self.debug "Got object of type %s" % child.class self.devfail( "Containers can only contain Puppet resources, not %s" % child.class ) end @children.push(child) child.parent = self } end # Component paths are special because they function as containers. def pathbuilder tmp = [] - if defined? @parent and @parent - tmp += [@parent.pathbuilder, self.title] - else - # The top-level name is always main[top], so we don't bother with - # that. - if self.title == "main[top]" - tmp << "" # This empty field results in "//" in the path - else - tmp << self.title + myname = "" + if self.title =~ /^class\[(.+)\]$/ + # 'main' is the top class, so we want to see '//' instead of + # its name. + unless $1 == "main" + myname = $1 end + else + myname = self.title + end + if self.parent + return [@parent.pathbuilder, myname] + else + return [myname] end - - tmp end # Remove an object. The argument determines whether the object's # subscriptions get eliminated, too. def remove(rmdeps = true) # Our children remove themselves from our @children array (else the object # we called this on at the top would not be removed), so we duplicate the # array and iterate over that. If we don't do this, only half of the # objects get removed. @children.dup.each { |child| child.remove(rmdeps) } @children.clear # Get rid of params and provider, too. super @parent = nil end # We have a different way of setting the title def title unless defined? @title if self[:type] == self[:name] # this is the case for classes @title = self[:type] elsif self[:name] =~ /\[.+\]/ # most components already have ref info in the name @title = self[:name] else # else, set it up @title = "%s[%s]" % [self[:type].capitalize, self[:name]] end end return @title end def refresh @children.collect { |child| if child.respond_to?(:refresh) child.refresh child.log "triggering %s" % :refresh end } end # Convert to a graph object with all of the container info. def to_graph graph = Puppet::PGraph.new delver = proc do |obj| obj.each do |child| graph.add_edge!(obj, child) if child.is_a?(self.class) delver.call(child) end end end delver.call(self) return graph end def to_s if self.title =~ /\[/ return self.title else return "component(%s)" % self.title end end end # $Id$ diff --git a/lib/puppet/type/pfile.rb b/lib/puppet/type/pfile.rb index 5eb81c3e6..99b5a7435 100644 --- a/lib/puppet/type/pfile.rb +++ b/lib/puppet/type/pfile.rb @@ -1,1181 +1,1182 @@ require 'digest/md5' require 'cgi' require 'etc' require 'uri' require 'fileutils' require 'puppet/network/handler' +require 'puppet/util/diff' module Puppet newtype(:file) do include Puppet::Util::MethodHelper @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 Reductive Labs and we can hopefully work with you to develop a native resource to support what you are doing." newparam(:path) do desc "The path to the file to manage. Must be fully qualified." isnamevar validate do |value| unless value =~ /^#{File::SEPARATOR}/ raise Puppet::Error, "File paths must be fully qualified" 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 ``puppetmasterd`` 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 do # Make sure the default file bucket exists. obj = Puppet::Type.type(:filebucket)["puppet"] || Puppet::Type.type(:filebucket).create(:name => "puppet") obj.bucket end munge do |value| # I don't really know how this is happening. if value.is_a?(Array) value = value.shift end case value when false, "false", :false: false when true, "true", ".puppet-bak", :true: ".puppet-bak" when /^\./ value when String: # We can't depend on looking this up right now, # we have to do it after all of the objects # have been instantiated. if bucketobj = Puppet::Type.type(:filebucket)[value] @resource.bucket = bucketobj.bucket bucketobj.title else # Set it to the string; finish() turns it into a # filebucket. @resource.bucket = value value end when Puppet::Network::Client.client(:Dipper): @resource.bucket = value value.name else self.fail "Invalid backup type %s" % value.inspect end end end newparam(:recurse) do desc "Whether and how deeply to do recursive management." newvalues(:true, :false, :inf, /^[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 Integer, Fixnum, Bignum: value when /^\d+$/: Integer(value) else raise ArgumentError, "Invalid recurse value %s" % 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., ``*/*``." defaultto false 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, :ignore) # :ignore and :manage behave equivalently on local files, # but don't copy remote links defaultto :ignore 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 attr_accessor :bucket # Autorequire any parent directories. autorequire(:file) do if self[:path] File.dirname(self[:path]) else Puppet.err "no path for %s, somehow; cannot setup autorequires" % self.ref 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] validate do count = 0 CREATORS.each do |param| count += 1 if self.should(param) end if count > 1 self.fail "You cannot specify more than one of %s" % CREATORS.collect { |p| p.to_s}.join(", ") end end def self.[](path) return nil unless path super(path.gsub(/\/+/, '/').sub(/\/$/, '')) end # List files, but only one level deep. def self.instances(base = "/") unless FileTest.directory?(base) return [] end files = [] Dir.entries(base).reject { |e| e == "." or e == ".." }.each do |name| path = File.join(base, name) if obj = self[path] obj[:check] = :all files << obj else files << self.create( :name => path, :check => :all ) end end files end @depthfirst = false def argument?(arg) @arghash.include?(arg) end # 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. if writeable asuser = self.should(:owner) end end return asuser end # We have to do some extra finishing, to retrieve our bucket if # there is one. def finish # Let's cache these values, since there should really only be # a couple of these buckets @@filebuckets ||= {} # Look up our bucket, if there is one if bucket = self.bucket case bucket when String: if obj = @@filebuckets[bucket] # This sets the @value on :backup, too self.bucket = obj elsif bucket == "puppet" obj = Puppet::Network::Client.client(:Dipper).new( :Path => Puppet[:clientbucketdir] ) self.bucket = obj @@filebuckets[bucket] = obj elsif obj = Puppet::Type.type(:filebucket).bucket(bucket) @@filebuckets[bucket] = obj self.bucket = obj else self.fail "Could not find filebucket %s" % bucket end when Puppet::Network::Client.client(:Dipper): # things are hunky-dorey else self.fail "Invalid bucket type %s" % bucket.class end end super end # Create any children via recursion or whatever. def eval_generate recurse() end # Deal with backups. def handlebackup(file = nil) # let the path be specified file ||= self[:path] # if they specifically don't want a backup, then just say # we're good unless FileTest.exists?(file) return true end unless self[:backup] return true end case File.stat(file).ftype when "directory": if self[:recurse] # we don't need to backup directories when recurse is on return true else backup = self.bucket || self[:backup] case backup when Puppet::Network::Client.client(:Dipper): notice "Recursively backing up to filebucket" require 'find' Find.find(self[:path]) do |f| if File.file?(f) sum = backup.backup(f) self.info "Filebucketed %s to %s with sum %s" % [f, backup.name, sum] end end return true when String: newfile = file + backup # Just move it, since it's a directory. if FileTest.exists?(newfile) remove_backup(newfile) end begin bfile = file + backup # Ruby 1.8.1 requires the 'preserve' addition, but # later versions do not appear to require it. FileUtils.cp_r(file, bfile, :preserve => true) return true rescue => detail # since they said they want a backup, let's error out # if we couldn't make one self.fail "Could not back %s up: %s" % [file, detail.message] end else self.err "Invalid backup type %s" % backup.inspect return false end end when "file": backup = self.bucket || self[:backup] case backup when Puppet::Network::Client.client(:Dipper): sum = backup.backup(file) self.info "Filebucketed to %s with sum %s" % [backup.name, sum] return true when String: newfile = file + backup if FileTest.exists?(newfile) remove_backup(newfile) end begin # FIXME Shouldn't this just use a Puppet object with # 'source' specified? bfile = file + backup # Ruby 1.8.1 requires the 'preserve' addition, but # later versions do not appear to require it. FileUtils.cp(file, bfile, :preserve => true) return true rescue => detail # since they said they want a backup, let's error out # if we couldn't make one self.fail "Could not back %s up: %s" % [file, detail.message] end else self.err "Invalid backup type %s" % backup.inspect return false end when "link": return true else self.notice "Cannot backup files of type %s" % File.stat(file).ftype return false end end def handleignore(children) return children unless self[:ignore] self[:ignore].each { |ignore| ignored = [] Dir.glob(File.join(self[:path],ignore), File::FNM_DOTMATCH) { |match| ignored.push(File.basename(match)) } children = children - ignored } return children end def initialize(hash) # Store a copy of the arguments for later. tmphash = hash.to_hash # Used for caching clients @clients = {} super # Get rid of any duplicate slashes, and remove any trailing slashes. @title = @title.gsub(/\/+/, "/").sub(/\/$/, "") # Clean out as many references to any file paths as possible. # This was the source of many, many bugs. @arghash = tmphash @arghash.delete(self.class.namevar) [:source, :parent].each do |param| if @arghash.include?(param) @arghash.delete(param) end end @stat = nil end # Build a recursive map of a link source def linkrecurse(recurse) target = @parameters[:target].should method = :lstat if self[:links] == :follow method = :stat end targetstat = nil unless FileTest.exist?(target) return end # Now stat our target targetstat = File.send(method, target) unless targetstat.ftype == "directory" return end # Now that we know our corresponding target is a directory, # change our type self[:ensure] = :directory unless FileTest.readable? target self.notice "Cannot manage %s: permission denied" % self.name return end children = Dir.entries(target).reject { |d| d =~ /^\.+$/ } # Get rid of ignored children if @parameters.include?(:ignore) children = handleignore(children) end added = [] children.each do |file| Dir.chdir(target) do longname = File.join(target, file) # Files know to create directories when recursion # is enabled and we're making links args = { :recurse => recurse, :ensure => longname } if child = self.newchild(file, true, args) added << child end end end added end # Build up a recursive map of what's around right now def localrecurse(recurse) unless FileTest.exist?(self[:path]) and self.stat.directory? #self.info "%s is not a directory; not recursing" % # self[:path] return end unless FileTest.readable? self[:path] self.notice "Cannot manage %s: permission denied" % self.name return end children = Dir.entries(self[:path]) #Get rid of ignored children if @parameters.include?(:ignore) children = handleignore(children) end added = [] children.each { |file| file = File.basename(file) next if file =~ /^\.\.?$/ # skip . and .. options = {:recurse => recurse} if child = self.newchild(file, true, options) added << child end } added end # Create a new file or directory object as a child to the current # object. def newchild(path, local, hash = {}) # make local copy of arguments args = symbolize_options(@arghash) # There's probably a better way to do this, but we don't want # to pass this info on. if v = args[:ensure] v = symbolize(v) args.delete(:ensure) end if path =~ %r{^#{File::SEPARATOR}} self.devfail( "Must pass relative paths to PFile#newchild()" ) else path = File.join(self[:path], path) end args[:path] = path unless hash.include?(:recurse) if args.include?(:recurse) if args[:recurse].is_a?(Integer) args[:recurse] -= 1 # reduce the level of recursion end end end hash.each { |key,value| args[key] = value } child = nil klass = self.class # The child might already exist because 'localrecurse' runs # before 'sourcerecurse'. I could push the override stuff into # a separate method or something, but the work is the same other # than this last bit, so it doesn't really make sense. if child = klass[path] unless child.parent.object_id == self.object_id self.debug "Not managing more explicit file %s" % path return nil end # This is only necessary for sourcerecurse, because we might have # created the object with different 'should' values than are # set remotely. unless local args.each { |var,value| next if var == :path next if var == :name # behave idempotently unless child.should(var) == value child[var] = value end } end return nil else # create it anew #notice "Creating new file with args %s" % args.inspect args[:parent] = self begin child = klass.implicitcreate(args) # implicit creation can return nil if child.nil? return nil end rescue Puppet::Error => detail self.notice( "Cannot manage: %s" % [detail.message] ) self.debug args.inspect child = nil rescue => detail self.notice( "Cannot manage: %s" % [detail] ) self.debug args.inspect child = nil end end return child end # Files handle paths specially, because they just lengthen their # path names, rather than including the full parent's title each # time. def pathbuilder if defined? @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 # Recurse into the directory. This basically just calls 'localrecurse' # and maybe 'sourcerecurse', returning the collection of generated # files. def recurse # are we at the end of the recursion? unless self.recurse? return end recurse = self[:recurse] # we might have a string, rather than a number if recurse.is_a?(String) if recurse =~ /^[0-9]+$/ recurse = Integer(recurse) else # anything else is infinite recursion recurse = true end end if recurse.is_a?(Integer) recurse -= 1 end children = [] # We want to do link-recursing before normal recursion so that all # of the target stuff gets copied over correctly. if @parameters.include? :target and ret = self.linkrecurse(recurse) children += ret end if ret = self.localrecurse(recurse) children += ret end # These will be files pulled in by the file source sourced = false if @parameters.include?(:source) ret, sourced = self.sourcerecurse(recurse) if ret children += ret end end # The purge check needs to happen after all of the other recursion. if self.purge? children.each do |child| if (sourced and ! sourced.include?(child[:path])) or ! child.managed? child[:ensure] = :absent end end end children end # A simple method for determining whether we should be recursing. def recurse? return false unless @parameters.include?(:recurse) val = @parameters[:recurse].value if val and (val == true or val > 0) return true else return false end end # Remove the old backup. def remove_backup(newfile) if self.class.name == :file and self[:links] != :follow method = :lstat else method = :stat end old = File.send(method, newfile).ftype if old == "directory" raise Puppet::Error, "Will not remove directory backup %s; use a filebucket" % newfile end info "Removing old backup of type %s" % File.send(method, newfile).ftype begin File.unlink(newfile) rescue => detail if Puppet[:trace] puts detail.backtrace end self.err "Could not remove old backup: %s" % detail return false end end # Remove any existing data. This is only used when dealing with # links or directories. def remove_existing(should) return unless s = stat(true) unless handlebackup self.fail "Could not back up; will not replace" end 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 %s" % should FileUtils.rmtree(self[:path]) else notice "Not removing directory; use 'force' to override" end when "link", "file": debug "Removing existing %s for replacement with %s" % [s.ftype, should] File.unlink(self[:path]) else self.fail "Could not back up files of type %s" % s.ftype end end # a wrapper method to make sure the file exists before doing anything def retrieve unless stat = self.stat(true) self.debug "File does not exist" # If the file doesn't exist but we have a source, then call # retrieve on that property propertyvalues = properties().inject({}) { |hash, property| hash[property] = :absent hash } if @parameters.include?(:source) propertyvalues[:source] = @parameters[:source].retrieve end return propertyvalues end return currentpropvalues() end # This recurses against the remote source and makes sure the local # and remote structures match. It's run after 'localrecurse'. This # method only does anything when its corresponding remote entry is # a directory; in that case, this method creates file objects that # correspond to any contained remote files. def sourcerecurse(recurse) # we'll set this manually as necessary if @arghash.include?(:ensure) @arghash.delete(:ensure) end r = false if recurse unless recurse == 0 r = 1 end end ignore = self[:ignore] result = [] found = [] # Keep track of all the files we found in the source, so we can purge # appropriately. sourced = [] @parameters[:source].should.each do |source| sourceobj, path = uri2obj(source) # okay, we've got our source object; now we need to # build up a local file structure to match the remote # one server = sourceobj.server desc = server.list(path, self[:links], r, ignore) if desc == "" next end # Now create a new child for every file returned in the list. result += desc.split("\n").collect { |line| file, type = line.split("\t") next if file == "/" # skip the listing object name = file.sub(/^\//, '') # This makes sure that the first source *always* wins # for conflicting files. next if found.include?(name) # For directories, keep all of the sources, so that # sourceselect still works as planned. if type == "directory" newsource = @parameters[:source].should.collect do |source| source + file end else newsource = source + file end args = {:source => newsource} if type == file args[:recurse] = nil end found << name sourced << File.join(self[:path], name) self.newchild(name, false, args) }.reject {|c| c.nil? } if self[:sourceselect] == :first return [result, sourced] end end return [result, sourced] 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 # 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). def stat(refresh = false) 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] # Just skip them when they don't exist at all. unless FileTest.exists?(path) or FileTest.symlink?(path) @stat = nil return @stat end if @stat.nil? or refresh == true begin @stat = File.send(method, self[:path]) rescue Errno::ENOENT => error @stat = nil rescue Errno::EACCES => error self.warning "Could not stat; permission denied" @stat = nil end end return @stat 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 if obj[:target] == :notlink obj.delete(:target) end obj end def localfileserver unless defined? @@localfileserver args = { :Local => true, :Mount => { "/" => "localhost" }, :Config => false } @@localfileserver = Puppet::Network::Handler.handler(:fileserver).new(args) end @@localfileserver end def uri2obj(source) sourceobj = FileSource.new path = nil unless source devfail "Got a nil source" end if source =~ /^\// source = "file://localhost/%s" % URI.escape(source) sourceobj.mount = "localhost" sourceobj.local = true end begin uri = URI.parse(URI.escape(source)) rescue => detail self.fail "Could not understand source %s: %s" % [source, detail.to_s] end case uri.scheme when "file": sourceobj.server = localfileserver path = "/localhost" + uri.path when "puppet": # FIXME: We should cache clients by uri.host + uri.port # not by the full source path unless @clients.include?(source) host = uri.host host ||= Puppet[:server] unless Puppet[:name] == "puppet" if host.nil? server = localfileserver else args = { :Server => host } if uri.port args[:Port] = uri.port end server = Puppet::Network::Client.file.new(args) end @clients[source] = server end sourceobj.server = @clients[source] tmp = uri.path if tmp =~ %r{^/(\w+)} sourceobj.mount = $1 path = tmp #path = tmp.sub(%r{^/\w+},'') || "/" else self.fail "Invalid source path %s" % tmp end else self.fail "Got other URL type '%s' from %s" % [uri.scheme, source] end return [sourceobj, path.sub(/\/\//, '/')] end # Write out the file. We open the file correctly, with all of the # uid and mode and such, and then yield the file handle for actual # writing. - def write(usetmp = true) + def write(property, usetmp = true) mode = self.should(:mode) remove_existing(:file) # The temporary file path = nil if usetmp path = self[:path] + ".puppettmp" else path = self[:path] end # As the correct user and group write_if_writable(File.dirname(path)) do f = nil # Open our file with the correct modes if mode Puppet::Util.withumask(000) do f = File.open(path, File::CREAT|File::WRONLY|File::TRUNC, mode) end else f = File.open(path, File::CREAT|File::WRONLY|File::TRUNC) end # Yield it yield f f.flush f.close end # And put our new file in place if usetmp begin File.rename(path, self[:path]) rescue => detail self.err "Could not rename tmp %s for replacing: %s" % [self[:path], detail] ensure # Make sure the created file gets removed if FileTest.exists?(path) File.unlink(path) end end end # make sure all of the modes are actually correct property_fix # And then update our checksum, so the next run doesn't find it. # FIXME This is extra work, because it's going to read the whole # file back in again. self.setchecksum end # Run the block as the specified user if the dir is writeable, else # run it as root (or the current user). def write_if_writable(dir) yield # We're getting different behaviors from different versions of ruby, so... # asroot = true # Puppet::Util::SUIDManager.asuser(asuser(), self.should(:group)) do # if FileTest.writable?(dir) # asroot = false # yield # end # end # # if asroot # yield # end end private # Override the parent method, because we don't want to generate changes # when the file is missing and there is no 'ensure' state. def propertychanges(currentvalues) unless self.stat found = false ([:ensure] + CREATORS).each do |prop| if @parameters.include?(prop) found = true break end end unless found return [] end end super 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].include?(thing.name) # Make sure we get a new stat objct self.stat(true) currentvalue = thing.retrieve unless thing.insync?(currentvalue) thing.sync end end end end # Puppet.type(:pfile) # the filesource class can't include the path, because the path # changes for every file instance class FileSource attr_accessor :mount, :root, :server, :local 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/pfile/checksum' require 'puppet/type/pfile/content' # can create the file require 'puppet/type/pfile/source' # can create the file require 'puppet/type/pfile/target' # creates a different type of file require 'puppet/type/pfile/ensure' # can create the file require 'puppet/type/pfile/owner' require 'puppet/type/pfile/group' require 'puppet/type/pfile/mode' require 'puppet/type/pfile/type' end # $Id$ diff --git a/lib/puppet/type/pfile/content.rb b/lib/puppet/type/pfile/content.rb index 7892ed522..11458ef18 100755 --- a/lib/puppet/type/pfile/content.rb +++ b/lib/puppet/type/pfile/content.rb @@ -1,72 +1,81 @@ module Puppet Puppet.type(:file).newproperty(:content) do + include Puppet::Util::Diff + 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 } } This attribute is especially useful when used with `PuppetTemplating templating`:trac:." def change_to_s(currentvalue, newvalue) newvalue = "{md5}" + Digest::MD5.hexdigest(newvalue) if currentvalue == :absent return "created file with contents %s" % newvalue else currentvalue = "{md5}" + Digest::MD5.hexdigest(currentvalue) return "changed file contents from %s to %s" % [currentvalue, newvalue] end end + # Override this method to provide diffs if asked for. + def insync?(is) + result = super + if ! result and Puppet[:show_diff] and File.exists?(@resource[:path]) + string_file_diff(@resource[:path], self.should) + end + return result + end + # We should probably take advantage of existing md5 sums if they're there, # but I really don't feel like dealing with the complexity right now. def retrieve stat = nil unless stat = @resource.stat return :absent end if stat.ftype == "link" and @resource[:links] == :ignore return self.should end # Don't even try to manage the content on directories if stat.ftype == "directory" and @resource[:links] == :ignore @resource.delete(:content) return nil end begin currentvalue = File.read(@resource[:path]) return currentvalue rescue => detail raise Puppet::Error, "Could not read %s: %s" % [@resource.title, detail] end end # Just write our content out to disk. def sync return_event = @resource.stat ? :file_changed : :file_created - @resource.write { |f| f.print self.should } + @resource.write(:content) { |f| f.print self.should } return return_event end end end - -# $Id$ diff --git a/lib/puppet/type/pfile/source.rb b/lib/puppet/type/pfile/source.rb index b4c96b3da..1849d5a61 100755 --- a/lib/puppet/type/pfile/source.rb +++ b/lib/puppet/type/pfile/source.rb @@ -1,280 +1,297 @@ module Puppet # Copy files from a local or remote source. This state *only* does any work # when the remote file is an actual file; in that case, this state copies # the file down. If the remote file is a dir or a link or whatever, then # this state, during retrieval, modifies the appropriate other states # so that things get taken care of appropriately. Puppet.type(:file).newproperty(:source) do + include Puppet::Util::Diff attr_accessor :source, :local desc "Copy a file over the current file. Uses ``checksum`` to determine when a file should be copied. Valid values are either fully qualified paths to files, or URIs. Currently supported URI types are *puppet* and *file*. This is one of the primary mechanisms for getting content into applications that Puppet does not directly support and is very useful for those configuration files that don't change much across sytems. For instance:: class sendmail { file { \"/etc/mail/sendmail.cf\": source => \"puppet://server/module/sendmail.cf\" } } You can also leave out the server name, in which case ``puppetd`` will fill in the name of its configuration server and ``puppet`` will use the local filesystem. This makes it easy to use the same configuration in both local and centralized forms. Currently, only the ``puppet`` scheme is supported for source URL's. Puppet will connect to the file server running on ``server`` to retrieve the contents of the file. If the ``server`` part is empty, the behavior of the command-line interpreter (``puppet``) and the client demon (``puppetd``) differs slightly: ``puppet`` will look such a file up on the module path on the local host, whereas ``puppetd`` will connect to the puppet server that it received the manifest from. See the `FileServingConfiguration fileserver configuration documentation`:trac: for information on how to configure and use file services within Puppet. If you specify multiple file sources for a file, then the first source that exists will be used. This allows you to specify what amount to search paths for files:: file { \"/path/to/my/file\": source => [ \"/nfs/files/file.$host\", \"/nfs/files/file.$operatingsystem\", \"/nfs/files/file\" ] } This will use the first found file as the source. You cannot currently copy links using this mechanism; set ``links`` to ``follow`` if any remote sources are links. " uncheckable validate do |source| unless @resource.uri2obj(source) raise Puppet::Error, "Invalid source %s" % source end end munge do |source| # if source.is_a? Symbol # return source # end # Remove any trailing slashes source.sub(/\/$/, '') end def change_to_s(currentvalue, newvalue) # newvalue = "{md5}" + @stats[:checksum] if @resource.property(:ensure).retrieve == :absent return "creating from source %s with contents %s" % [@source, @stats[:checksum]] else return "replacing from source %s with contents %s" % [@source, @stats[:checksum]] end end def checksum if defined?(@stats) @stats[:checksum] else nil end end # Ask the file server to describe our file. def describe(source) sourceobj, path = @resource.uri2obj(source) server = sourceobj.server begin desc = server.describe(path, @resource[:links]) rescue Puppet::Network::XMLRPCClientError => detail self.err "Could not describe %s: %s" % [path, detail] return nil end args = {} pinparams.zip( desc.split("\t") ).each { |param, value| if value =~ /^[0-9]+$/ value = value.to_i end unless value.nil? args[param] = value end } # we can't manage ownership as root, so don't even try unless Puppet::Util::SUIDManager.uid == 0 args.delete(:owner) end if args.empty? or (args[:type] == "link" and @resource[:links] == :ignore) return nil else return args end end # Have we successfully described the remote source? def described? ! @stats.nil? and ! @stats[:type].nil? #and @is != :notdescribed end # Use the info we get from describe() to check if we're in sync. def insync?(currentvalue) unless described? info "No specified sources exist" return true end if currentvalue == :nocopy return true end # the only thing this actual state can do is copy files around. Therefore, # only pay attention if the remote is a file. unless @stats[:type] == "file" return true end #FIXARB: Inefficient? Needed to call retrieve on parent's ensure and checksum parentensure = @resource.property(:ensure).retrieve if parentensure != :absent and ! @resource.replace? return true end # Now, we just check to see if the checksums are the same parentchecksum = @resource.property(:checksum).retrieve - return (!parentchecksum.nil? and (parentchecksum == @stats[:checksum])) + result = (!parentchecksum.nil? and (parentchecksum == @stats[:checksum])) + + # Diff the contents if they ask it. This is quite annoying -- we need to do this in + # 'insync?' because they might be in noop mode, but we don't want to do the file + # retrieval twice, so we cache the value annoyingly. + if ! result and Puppet[:show_diff] and File.exists?(@resource[:path]) and ! @stats[:_diffed] + @stats[:_remote_content] = get_remote_content + string_file_diff(@resource[:path], @stats[:_remote_content]) + @stats[:_diffed] = true + end + return result end def pinparams Puppet::Network::Handler.handler(:fileserver).params end # This basically calls describe() on our file, and then sets all # of the local states appropriately. If the remote file is a normal # file then we set it to copy; if it's a directory, then we just mark # that the local directory should be created. def retrieve(remote = true) sum = nil @source = nil # This is set to false by the File#retrieve function on the second # retrieve, so that we do not do two describes. if remote # Find the first source that exists. @shouldorig contains # the sources as specified by the user. @should.each { |source| if @stats = self.describe(source) @source = source break end } end if @stats.nil? or @stats[:type].nil? return nil # :notdescribed end case @stats[:type] when "directory", "file": unless @resource.deleting? @resource[:ensure] = @stats[:type] end else self.info @stats.inspect self.err "Cannot use files of type %s as sources" % @stats[:type] return :nocopy end # Take each of the stats and set them as states on the local file # if a value has not already been provided. @stats.each { |stat, value| next if stat == :checksum next if stat == :type # was the stat already specified, or should the value # be inherited from the source? unless @resource.argument?(stat) @resource[stat] = value end } return @stats[:checksum] end def should @should end # Make sure we're also checking the checksum def should=(value) super checks = (pinparams + [:ensure]) checks.delete(:checksum) @resource[:check] = checks unless @resource.property(:checksum) @resource[:checksum] = :md5 end end def sync + contents = @stats[:_remote_content] || get_remote_content() + + exists = File.exists?(@resource[:path]) + + @resource.write(:source) { |f| f.print contents } + + if exists + return :file_changed + else + return :file_created + end + end + + private + def get_remote_content unless @stats[:type] == "file" #if @stats[:type] == "directory" #[@resource.name, @should.inspect] #end raise Puppet::DevError, "Got told to copy non-file %s" % @resource[:path] end sourceobj, path = @resource.uri2obj(@source) begin contents = sourceobj.server.retrieve(path, @resource[:links]) rescue Puppet::Network::XMLRPCClientError => detail self.err "Could not retrieve %s: %s" % [path, detail] return nil end # FIXME It's stupid that this isn't taken care of in the # protocol. unless sourceobj.server.local contents = CGI.unescape(contents) end if contents == "" self.notice "Could not retrieve contents for %s" % @source end - exists = File.exists?(@resource[:path]) - @resource.write { |f| f.print contents } - - if exists - return :file_changed - else - return :file_created - end + return contents end end end - -# $Id$ diff --git a/lib/puppet/util.rb b/lib/puppet/util.rb index d1d14977c..5a10f5344 100644 --- a/lib/puppet/util.rb +++ b/lib/puppet/util.rb @@ -1,464 +1,464 @@ # A module to collect utility functions. require 'sync' require 'puppet/external/lock' module Puppet # A command failed to execute. class ExecutionFailure < Puppet::Error end module Util require 'benchmark' require 'puppet/util/posix' extend Puppet::Util::POSIX # Create a hash to store the different sync objects. @@syncresources = {} # Return the sync object associated with a given resource. def self.sync(resource) @@syncresources[resource] ||= Sync.new return @@syncresources[resource] end # Change the process to a different user def self.chuser if Facter["operatingsystem"].value == "Darwin" $stderr.puts "Ruby on darwin is broken; puppetmaster will not set its UID to 'puppet' and must run as root" return end if group = Puppet[:group] group = self.gid(group) unless group raise Puppet::Error, "No such group %s" % Puppet[:group] end 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 %s: %s" % [group.inspect, detail] $stderr.puts "could not change to group %s" % 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) unless user raise Puppet::Error, "No such user %s" % Puppet[:user] end unless Puppet::Util::SUIDManager.uid == user begin Puppet::Util::SUIDManager.uid = user Puppet::Util::SUIDManager.euid = user rescue $stderr.puts "could not change to user %s" % user exit(74) end end end end # Create a shared lock for reading def self.readlock(file) self.sync(file).synchronize(Sync::SH) do File.open(file) { |f| f.lock_shared { |lf| yield lf } } end end # Create an exclusive lock for writing, and do the writing in a # tmp file. def self.writelock(file, mode = 0600) tmpfile = file + ".tmp" unless FileTest.directory?(File.dirname(tmpfile)) raise Puppet::DevError, "Cannot create %s; directory %s does not exist" % [file, File.dirname(file)] end self.sync(file).synchronize(Sync::EX) do File.open(file, "w", mode) do |rf| rf.lock_exclusive do |lrf| File.open(tmpfile, "w", mode) do |tf| yield tf end begin File.rename(tmpfile, file) rescue => detail Puppet.err "Could not rename %s to %s: %s" % [file, tmpfile, detail] end 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| if args.is_a?(Array) args = args.join(" ") end 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 %s: basedir %s is a file" % [dir, File.join(path)] 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 unless level raise Puppet::DevError, "Failed to provide level to :benchmark" end - unless object.respond_to? level + unless level == :none or object.respond_to? level raise Puppet::DevError, "Benchmarked object does not respond to %s" % 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 binary(bin) if bin =~ /^\// if FileTest.exists? bin return bin else return nil end else ENV['PATH'].split(":").each do |dir| if FileTest.exists? File.join(dir, bin) return File.join(dir, bin) end end return nil end end module_function :binary # Execute the provided command in a pipe, yielding the pipe object. def execpipe(command, failonfail = true) if respond_to? :debug debug "Executing '%s'" % command else Puppet.debug "Executing '%s'" % command end output = open("| #{command} 2>&1") do |pipe| yield pipe end if failonfail unless $? == 0 raise ExecutionFailure, output end end return output end def execfail(command, exception) begin output = execute(command) return output rescue ExecutionFailure raise exception, output end end # Execute the desired command, and return the status and output. # def execute(command, failonfail = true, uid = nil, gid = nil) def execute(command, arguments = {:failonfail => 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 '%s'" % str else Puppet.debug "Executing '%s'" % str end if arguments[:uid] arguments[:uid] = Puppet::Util::SUIDManager.convert_xid(:uid, arguments[:uid]) end if arguments[:gid] arguments[:gid] = Puppet::Util::SUIDManager.convert_xid(:gid, arguments[:gid]) 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" if ! arguments[:squelch] require "tempfile" output_file = Tempfile.new("puppet") end oldverb = $VERBOSE $VERBOSE = false child_pid = Kernel.fork $VERBOSE = oldverb if child_pid # Parent process executes this child_status = Process.waitpid2(child_pid)[1] else # Child process executes this Process.setsid begin $stdin.reopen("/dev/null") $stdout.reopen(output_file) $stderr.reopen(output_file) 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 # begin; rescue end # if child_pid # 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 '%s' returned %s: %s" % [str, child_status, output] end end return output end module_function :execute # Create an exclusive lock. def threadlock(resource, type = Sync::EX) Puppet::Util.sync(resource).synchronize(type) do yield end end # Because some modules provide their own version of this method. alias util_execute execute module_function :benchmark def memory unless defined? @pmap pmap = %x{which pmap 2>/dev/null}.chomp if $? != 0 or pmap =~ /^no/ @pmap = nil else @pmap = pmap end end if @pmap return %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 return hash end module_function :symbolize, :symbolizehash, :symbolizehash! # Just benchmark, with no logging. def thinmark seconds = Benchmark.realtime { yield } return seconds end module_function :memory, :thinmark 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' # $Id$ diff --git a/lib/puppet/util/autoload.rb b/lib/puppet/util/autoload.rb index be9e14671..280961837 100644 --- a/lib/puppet/util/autoload.rb +++ b/lib/puppet/util/autoload.rb @@ -1,154 +1,151 @@ # Autoload paths, either based on names or all at once. class Puppet::Util::Autoload include Puppet::Util @autoloaders = {} - @loaded = {} + @loaded = [] class << self attr_reader :autoloaders private :autoloaders end # Send [], []=, and :clear to the @autloaders hash Puppet::Util.classproxy self, :autoloaders, "[]", "[]=" - # Clear the list of autoloaders and loaded files. - def self.clear - @autoloaders.clear - @loaded.clear - end - # List all loaded files. def self.list_loaded @loaded.sort { |a,b| a[0] <=> b[0] }.collect do |path, hash| "%s: %s" % [path, hash[:file]] end end # Has a given path been loaded? This is used for testing whether a - # changed file should be loaded or just ignored. + # changed file should be loaded or just ignored. This is only + # used in network/client/master, when downloading plugins, to + # see if a given plugin is currently loaded and thus should be + # reloaded. def self.loaded?(path) path = path.to_s.sub(/\.rb$/, '') - @loaded[path] + @loaded.include?(path) end - # Save the fact that a given path has been loaded - def self.loaded(path, file, loader) - @loaded[path] = {:file => file, :autoloader => loader} + # Save the fact that a given path has been loaded. This is so + # we can load downloaded plugins if they've already been loaded + # into memory. + def self.loaded(file) + @loaded << file unless @loaded.include?(file) end attr_accessor :object, :path, :objwarn, :wrap def initialize(obj, path, options = {}) @path = path.to_s if @path !~ /^\w/ raise ArgumentError, "Autoload paths cannot be fully qualified" end @object = obj self.class[obj] = self options.each do |opt, value| opt = opt.intern if opt.is_a? String begin self.send(opt.to_s + "=", value) rescue NoMethodError raise ArgumentError, "%s is not a valid option" % opt end end unless defined? @wrap @wrap = true end end # Load a single plugin by name. We use 'load' here so we can reload a # given plugin. def load(name) path = name.to_s + ".rb" eachdir do |dir| file = File.join(dir, path) next unless FileTest.exists?(file) begin Kernel.load file, @wrap name = symbolize(name) loaded name, file return true rescue LoadError => detail # I have no idea what's going on here, but different versions # of ruby are raising different errors on missing files. unless detail.to_s =~ /^no such file/i warn "Could not autoload %s: %s" % [name, detail] if Puppet[:trace] puts detail.backtrace end end return false end end return false end # Mark the named object as loaded. Note that this supports unqualified # queries, while we store the result as a qualified query in the class. def loaded(name, file) - self.class.loaded(File.join(@path, name.to_s), file, object) + self.class.loaded(File.join(@path, name.to_s)) end # Indicate whether the specfied plugin has been loaded. def loaded?(name) self.class.loaded?(File.join(@path, name.to_s)) end # Load all instances that we can. This uses require, rather than load, # so that already-loaded files don't get reloaded unnecessarily. def loadall # Load every instance of everything we can find. eachdir do |dir| Dir.glob("#{dir}/*.rb").each do |file| # Load here, rather than require, so that facts # can be reloaded. This has some short-comings, I # believe, but it works as long as real classes # aren't used. name = File.basename(file).sub(".rb", '').intern next if loaded?(name) next if $".include?(File.join(@path, name.to_s + ".rb")) filepath = File.join(@path, name.to_s + ".rb") begin Kernel.require file loaded(name, file) rescue => detail if Puppet[:trace] puts detail.backtrace end warn "Could not autoload %s: %s" % [file.inspect, detail] end end end end private # Yield each subdir in turn. def eachdir searchpath.each do |dir| subdir = File.join(dir, @path) yield subdir if FileTest.directory?(subdir) end end # The list of directories to search through for loadable plugins. def searchpath # JJM: Search for optional lib directories in each module bundle. module_lib_dirs = Puppet[:modulepath].split(":").collect do |d| Dir.glob("%s/*/lib" % d).select do |f| FileTest.directory?(f) end end.flatten [module_lib_dirs, Puppet[:libdir], $:].flatten end end - -# $Id$ diff --git a/lib/puppet/util/config.rb b/lib/puppet/util/config.rb index 932314215..9cdb4cfe3 100644 --- a/lib/puppet/util/config.rb +++ b/lib/puppet/util/config.rb @@ -1,1153 +1,1222 @@ require 'puppet' require 'sync' require 'puppet/transportable' require 'getoptlong' # The class for handling configuration files. class Puppet::Util::Config include Enumerable include Puppet::Util @@sync = Sync.new - attr_reader :file, :timer + attr_accessor :file + attr_reader :timer # Retrieve a config value def [](param) - param = symbolize(param) - - # Yay, recursion. - self.reparse() unless param == :filetimeout - - # Cache the returned values; this method was taking close to - # 10% of the compile time. - unless @returned[param] - if @config.include?(param) - if @config[param] - @returned[param] = @config[param].value - end - else - raise ArgumentError, "Undefined configuration parameter '%s'" % param - end - end - - return @returned[param] + value(param) end # Set a config value. This doesn't set the defaults, it sets the value itself. def []=(param, value) @@sync.synchronize do # yay, thread-safe param = symbolize(param) - unless @config.include?(param) - raise Puppet::Error, + unless element = @config[param] + raise ArgumentError, "Attempt to assign a value to unknown configuration parameter %s" % param.inspect end - unless @order.include?(param) - @order << param + if element.respond_to?(:munge) + value = element.munge(value) + end + if element.respond_to?(:handle) + element.handle(value) end - @config[param].value = value - if @returned.include?(param) - @returned.delete(param) + # Reset the name, so it's looked up again. + if param == :name + @name = nil end + @values[:memory][param] = value + @cache.clear end return value end # A simplified equality operator. def ==(other) self.each { |myname, myobj| - unless other[myname] == myobj.value + unless other[myname] == value(myname) return false end } return true 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) # Hackish, but acceptable. Copy the current ARGV for restarting. Puppet.args = ARGV.dup # Add all of the config parameters as valid options. self.each { |name, element| element.getopt_args.each { |args| options << args } } return options end # Turn the config into a transaction and apply it def apply trans = self.to_transportable begin comp = trans.to_type trans = comp.evaluate trans.evaluate comp.remove rescue => detail if Puppet[:trace] puts detail.backtrace end Puppet.err "Could not configure myself: %s" % detail end end # Is our parameter a boolean parameter? def boolean?(param) param = symbolize(param) if @config.include?(param) and @config[param].kind_of? CBoolean return true else return false end end # Remove all set values, potentially skipping cli values. def clear(exceptcli = false) @config.each { |name, obj| unless exceptcli and obj.setbycli obj.clear end } - @returned.clear + @values.each do |name, values| + next if name == :cli and exceptcli + @values.delete(name) + end # Don't clear the 'used' in this case, since it's a config file reparse, # and we want to retain this info. unless exceptcli @used = [] end + + @cache.clear + + @name = nil end # This is mostly just used for testing. def clearused - @returned.clear + @cache.clear @used = [] end - def each - @order.each { |name| - if @config.include?(name) - yield name, @config[name] + # Do variable interpolation on the value. + def convert(value) + return value unless value + return value unless value.is_a? String + newval = value.gsub(/\$(\w+)|\$\{(\w+)\}/) do |value| + varname = $2 || $1 + if pval = self.value(varname) + pval else - raise Puppet::DevError, "%s is in the order but does not exist" % name + raise Puppet::DevError, "Could not find value for %s" % parent end + end + + return newval + end + + # Return a value's description. + def description(name) + if obj = @config[symbolize(name)] + obj.desc + else + nil + end + end + + def each + @config.each { |name, object| + yield name, object } end # Iterate over each section name. def eachsection yielded = [] - @order.each { |name| - if @config.include?(name) - section = @config[name].section - unless yielded.include? section - yield section - yielded << section - end - else - raise Puppet::DevError, "%s is in the order but does not exist" % name + @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 element(param) param = symbolize(param) @config[param] end # Handle a command-line argument. def handlearg(opt, value = nil) + @cache.clear value = munge_value(value) if value str = opt.sub(/^--/,'') bool = true newstr = str.sub(/^no-/, '') if newstr != str str = newstr bool = false end + str = str.intern if self.valid?(str) if self.boolean?(str) - self[str] = bool + @values[:cli][str] = bool else - self[str] = value + @values[:cli][str] = value end - - # Mark that this was set on the cli, so it's not overridden if the - # config gets reread. - @config[str.intern].setbycli = true else raise ArgumentError, "Invalid argument %s" % opt end 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 config object def initialize - @order = [] @config = {} @shortnames = {} @created = [] - @returned = {} + @searchpath = nil + + # 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] = {} } + + # A central concept of a name. + @name = nil + end + + # Return a given object's file metadata. + def metadata(param) + if obj = @config[symbolize(param)] and obj.is_a?(CFile) + 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 = nil unless obj = @config[default] raise ArgumentError, "Unknown default %s" % default end unless obj.is_a? CFile raise ArgumentError, "Default %s is not a file" % default end Puppet::Util::SUIDManager.asuser(obj.owner, obj.group) do mode = obj.mode || 0750 Dir.mkdir(obj.value, mode) end end + # Figure out our name. + def name + unless @name + unless @config[:name] + return nil + end + searchpath.each do |source| + next if source == :name + break if @name = @values[source][:name] + end + unless @name + @name = convert(@config[:name].default).intern + end + end + @name + end + # Return all of the parameters associated with a given section. - def params(section) - section = section.intern if section.is_a? String - @config.find_all { |name, obj| - obj.section == section - }.collect { |name, obj| - name - } + 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. def parse(file) - configmap = parse_file(file) + clear(true) - # We know we want the 'main' section - if main = configmap[:main] - set_parameter_hash(main) + parse_file(file).each do |area, values| + @values[area] = values end - # Otherwise, we only want our named section - if @config.include?(:name) and named = configmap[symbolize(self[:name])] - set_parameter_hash(named) + # We have to do it in the reverse of the search path, + # because multiple sections could set the same value. + searchpath.reverse.each do |source| + if meta = @values[source][:_meta] + set_metadata(meta) + end end end # Parse the configuration file. As of May 2007, this is a backward-compatibility method and # will be deprecated soon. def old_parse(file) text = nil if file.is_a? Puppet::Util::LoadedFile @file = file else @file = Puppet::Util::LoadedFile.new(file) end # Don't create a timer for the old style parsing. # settimer() begin text = File.read(@file.file) rescue Errno::ENOENT raise Puppet::Error, "No such file %s" % file rescue Errno::EACCES raise Puppet::Error, "Permission denied to file %s" % file end @values = Hash.new { |names, name| names[name] = {} } # Get rid of the values set by the file, keeping cli values. self.clear(true) section = "puppet" metas = %w{owner group mode} values = Hash.new { |hash, key| hash[key] = {} } text.split(/\n/).each { |line| case line when /^\[(\w+)\]$/: section = $1 # Section names when /^\s*#/: next # Skip comments when /^\s*$/: next # Skip blanks when /^\s*(\w+)\s*=\s*(.+)$/: # settings var = $1.intern if var == :mode value = $2 else value = munge_value($2) end # Only warn if we don't know what this config var is. This # prevents exceptions later on. unless @config.include?(var) or metas.include?(var.to_s) Puppet.warning "Discarded unknown configuration parameter %s" % var.inspect next # Skip this line. end # Mmm, "special" attributes if metas.include?(var.to_s) unless values.include?(section) values[section] = {} end values[section][var.to_s] = value # If the parameter is valid, then set it. if section == Puppet[:name] and @config.include?(var) - @config[var].value = value + #@config[var].value = value + @values[:main][var] = value end next end # Don't override set parameters, since the file is parsed # after cli arguments are handled. unless @config.include?(var) and @config[var].setbycli Puppet.debug "%s: Setting %s to '%s'" % [section, var, value] - self[var] = value + @values[:main][var] = value end @config[var].section = symbolize(section) metas.each { |meta| if values[section][meta] if @config[var].respond_to?(meta + "=") @config[var].send(meta + "=", values[section][meta]) end end } else raise Puppet::Error, "Could not match line %s" % line end } end # Create a new element. The value is passed in because it's used to determine # what kind of element we're creating, but the value itself might be either # a default or a value, so we can't actually assign it. def newelement(hash) value = hash[:value] || hash[:default] klass = nil if hash[:section] hash[:section] = symbolize(hash[:section]) end case value when true, false, "true", "false": klass = CBoolean when /^\$\w+\//, /^\//: klass = CFile when String, Integer, Float: # nothing klass = CElement else raise Puppet::Error, "Invalid value '%s' for %s" % [value.inspect, hash[:name]] end hash[:parent] = self element = klass.new(hash) - @order << element.name - return element end + # This has to be private, because it doesn't add the elements to @config + private :newelement + # Iterate across all of the objects in a given section. def persection(section) section = symbolize(section) self.each { |name, obj| if obj.section == section yield obj end } end # Reparse our config file, if necessary. def reparse if defined? @file and @file.changed? Puppet.notice "Reparsing %s" % @file.file @@sync.synchronize do parse(@file) end reuse() end end def reuse return unless defined? @used @@sync.synchronize do # yay, thread-safe @used.each do |section| @used.delete(section) self.use(section) end end end + # The order in which to search for values. + def searchpath(environment = nil) + if environment + [:cli, :memory, environment, :name, :main] + else + [:cli, :memory, :name, :main] + end + end + # Get a list of objects per section def sectionlist sectionlist = [] self.each { |name, obj| section = obj.section || "puppet" sections[section] ||= [] unless sectionlist.include?(section) sectionlist << section end sections[section] << obj } return sectionlist, sections end # Convert a single section into transportable objects. def section_to_transportable(section, done = nil, includefiles = true) done ||= Hash.new { |hash, key| hash[key] = {} } objects = [] persection(section) do |obj| - if @config[:mkusers] and @config[:mkusers].value + if @config[:mkusers] and value(:mkusers) [:owner, :group].each do |attr| type = nil if attr == :owner type = :user else type = attr end # If a user and/or group is set, then make sure we're # managing that object if obj.respond_to? attr and name = obj.send(attr) # Skip root or wheel next if %w{root wheel}.include?(name.to_s) # Skip owners and groups we've already done, but tag # them with our section if necessary if done[type].include?(name) tags = done[type][name].tags unless tags.include?(section) done[type][name].tags = tags << section end elsif newobj = Puppet::Type.type(type)[name] unless newobj.property(:ensure) newobj[:ensure] = "present" end newobj.tag(section) if type == :user newobj[:comment] ||= "%s user" % name end else newobj = Puppet::TransObject.new(name, type.to_s) newobj.tags = ["puppet", "configuration", section] newobj[:ensure] = "present" if type == :user newobj[:comment] ||= "%s user" % name end # Set the group appropriately for the user if type == :user newobj[:gid] = Puppet[:group] end done[type][name] = newobj objects << newobj end end end end if obj.respond_to? :to_transportable - next if obj.value =~ /^\/dev/ + next if value(obj.name) =~ /^\/dev/ transobjects = obj.to_transportable transobjects = [transobjects] unless transobjects.is_a? Array transobjects.each do |trans| # transportable could return nil next unless trans unless done[:file].include? trans.name @created << trans.name objects << trans done[:file][trans.name] = trans end end end end bucket = Puppet::TransBucket.new bucket.type = section bucket.push(*objects) bucket.keyword = "class" return bucket 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 = symbolize(section) 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 = symbolize(name) hash[:name] = name hash[:section] = section name = hash[:name] if @config.include?(name) - raise Puppet::Error, "Parameter %s is already defined" % name + raise ArgumentError, "Parameter %s is already defined" % name end tryconfig = newelement(hash) if short = tryconfig.short if other = @shortnames[short] raise ArgumentError, "Parameter %s is already using short name '%s'" % [other.name, short] end @shortnames[short] = tryconfig end @config[name] = tryconfig } end # Create a timer to check whether the file should be reparsed. def settimer if Puppet[:filetimeout] > 0 @timer = Puppet.newtimer( :interval => Puppet[:filetimeout], :tolerance => 1, :start? => true ) do self.reparse() end end end # Convert our list of objects into a component that can be applied. def to_component transport = self.to_transportable return transport.to_type end # Convert our list of objects 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. Note also that the section names are entirely for human-level organizational purposes; they don't provide separate namespaces. All parameters are in a single namespace. Generated on #{Time.now}. }.gsub(/^/, "# ") # Add a section heading that matches our name. if @config.include?(:name) str += "[%s]\n" % self[:name] end eachsection do |section| persection(section) do |obj| str += obj.to_config + "\n" end end return str end # Convert our configuration into a list of transportable objects. def to_transportable done = Hash.new { |hash, key| hash[key] = {} } topbucket = Puppet::TransBucket.new if defined? @file.file and @file.file topbucket.name = @file.file else topbucket.name = "configtop" end topbucket.type = "puppetconfig" topbucket.top = true # Now iterate over each section eachsection do |section| topbucket.push section_to_transportable(section, done) end topbucket end # Convert to a parseable manifest def to_manifest transport = self.to_transportable manifest = transport.to_manifest + "\n" eachsection { |section| manifest += "include #{section}\n" } return manifest 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) @@sync.synchronize do # yay, thread-safe unless defined? @used @used = [] end runners = sections.collect { |s| symbolize(s) }.find_all { |s| ! @used.include? s } return if runners.empty? bucket = Puppet::TransBucket.new bucket.type = "puppetconfig" bucket.top = true # Create a hash to keep track of what we've done so far. @done = Hash.new { |hash, key| hash[key] = {} } runners.each do |section| bucket.push section_to_transportable(section, @done, false) end objects = bucket.to_type objects.finalize tags = nil if Puppet[:tags] tags = Puppet[:tags] Puppet[:tags] = "" end trans = objects.evaluate trans.ignoretags = true trans.configurator = true trans.evaluate if tags Puppet[:tags] = tags end # Remove is a recursive process, so it's sufficient to just call # it on the component. objects.remove(true) objects = nil runners.each { |s| @used << s } end end def valid?(param) param = symbolize(param) @config.has_key?(param) 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 = symbolize(param) + environment = symbolize(environment) if environment + + # Short circuit to nil for undefined parameters. + return nil unless @config.include?(param) + + # Yay, recursion. + self.reparse() unless param == :filetimeout + + # 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 @cache[environment||"none"].include?(param) + return @cache[environment||"none"][param] + end + + # See if we can find it within our searchable list of values + val = nil + searchpath(environment).each do |source| + # Modify the source as necessary. + source = case source + when :name: + self.name + else + source + end + + # Look for the value. We have to test the hash for whether + # it exists, because the value might be false. + if @values[source].include?(param) + val = @values[source][param] + break + end + end + + # If we didn't get a value, use the default + if val.nil? + val = @config[param].default + end + + # Convert it if necessary + val = convert(val) + + # And cache it + @cache[environment||"none"][param] = val + return val + end + # Open a file with the appropriate user, group, and mode def write(default, *args) obj = nil unless obj = @config[default] raise ArgumentError, "Unknown default %s" % default end unless obj.is_a? CFile raise ArgumentError, "Default %s is not a file" % default end chown = nil if Puppet::Util::SUIDManager.uid == 0 chown = [obj.owner, obj.group] else chown = [nil, nil] end Puppet::Util::SUIDManager.asuser(*chown) do mode = obj.mode || 0640 if args.empty? args << "w" end args << mode - File.open(obj.value, *args) do |file| + File.open(value(obj.name), *args) do |file| yield file end end end # Open a non-default file under a default dir with the appropriate user, # group, and mode def writesub(default, file, *args) obj = nil unless obj = @config[default] raise ArgumentError, "Unknown default %s" % default end unless obj.is_a? CFile raise ArgumentError, "Default %s is not a file" % default end chown = nil if Puppet::Util::SUIDManager.uid == 0 chown = [obj.owner, obj.group] else chown = [nil, nil] end Puppet::Util::SUIDManager.asuser(*chown) do mode = obj.mode || 0640 if args.empty? args << "w" end 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 private - # Extra extra setting information for files. + # Extract extra setting information for files. def extract_fileinfo(string) - paramregex = %r{(\w+)\s*=\s*([\w\d]+)} result = {} - string.scan(/\{\s*([^}]+)\s*\}/) do + value = string.sub(/\{\s*([^}]+)\s*\}/) do params = $1 params.split(/\s*,\s*/).each do |str| - if str =~ /^\s*(\w+)\s*=\s*([\w\w]+)\s*$/ + if str =~ /^\s*(\w+)\s*=\s*([\w\d]+)\s*$/ param, value = $1.intern, $2 result[param] = value unless [:owner, :mode, :group].include?(param) - raise Puppet::Error, "Invalid file option '%s'" % param + raise ArgumentError, "Invalid file option '%s'" % param end if param == :mode and value !~ /^\d+$/ - raise Puppet::Error, "File modes must be numbers" + raise ArgumentError, "File modes must be numbers" end else - raise Puppet::Error, "Could not parse '%s'" % string + raise ArgumentError, "Could not parse '%s'" % string end end - - return result + '' end + result[:value] = value.sub(/\s*$/, '') + return result return nil 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) else value.gsub(/^["']|["']$/,'').sub(/\s+$/, '') end end # This is an abstract method that just turns a file in to a hash of hashes. # We mostly need this for backward compatibility -- as of May 2007 we need to # support parsing old files with any section, or new files with just two # valid sections. def parse_file(file) - text = nil - - if file.is_a? Puppet::Util::LoadedFile - @file = file - else - @file = Puppet::Util::LoadedFile.new(file) - end + text = read_file(file) # Create a timer so that this file will get checked automatically # and reparsed if necessary. settimer() - begin - text = File.read(@file.file) - rescue Errno::ENOENT - raise Puppet::Error, "No such file %s" % file - rescue Errno::EACCES - raise Puppet::Error, "Permission denied to file %s" % file - end - 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 /^\[(\w+)\]$/: + when /^\s*\[(\w+)\]$/: 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*(.+)$/: # 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 %s" % line) error.file = file error.line = line raise error end } return result end - # Take all members of a hash and assign their values appropriately. - def set_parameter_hash(params) - params.each do |param, value| - next if param == :_meta - unless @config.include?(param) - Puppet.warning "Discarded unknown configuration parameter %s" % param - next - end - if @config[param].setbycli - Puppet.debug "Ignoring %s set by config file; overridden by cli" % param - else - self[param] = value - end + # Read the file in. + def read_file(file) + if file.is_a? Puppet::Util::LoadedFile + @file = file + else + @file = Puppet::Util::LoadedFile.new(file) end - if meta = params[:_meta] - meta.each do |var, values| - values.each do |param, value| - @config[var].send(param.to_s + "=", value) - end + begin + return File.read(@file.file) + rescue Errno::ENOENT + raise ArgumentError, "No such file %s" % file + rescue Errno::EACCES + raise ArgumentError, "Permission denied to file %s" % 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 # The base element type. class CElement attr_accessor :name, :section, :default, :parent, :setbycli attr_reader :desc, :short # Unset any set value. def clear @value = nil end - # Do variable interpolation on the value. - def convert(value) - return value unless value - return value unless value.is_a? String - newval = value.gsub(/\$(\w+)|\$\{(\w+)\}/) do |value| - varname = $2 || $1 - if pval = @parent[varname] - pval - else - raise Puppet::DevError, "Could not find value for %s" % parent - end - end - - return newval - end - def desc=(value) @desc = value.gsub(/^\s*/, '') end # get the arguments in getopt format def getopt_args if short [["--#{name}", "-#{short}", GetoptLong::REQUIRED_ARGUMENT]] else [["--#{name}", GetoptLong::REQUIRED_ARGUMENT]] end end def hook=(block) meta_def :handle, &block end # Create the new element. Pretty much just sets the name. def initialize(args = {}) if args.include?(:parent) self.parent = args[:parent] args.delete(:parent) end args.each do |param, value| method = param.to_s + "=" unless self.respond_to? method raise ArgumentError, "%s does not accept %s" % [self.class, param] end self.send(method, value) end unless self.desc raise ArgumentError, "You must provide a description for the %s config option" % self.name end end def iscreated @iscreated = true end def iscreated? if defined? @iscreated return @iscreated else return false end end def set? if defined? @value and ! @value.nil? return true else return false end end # short name for the celement def short=(value) if value.to_s.length != 1 raise ArgumentError, "Short names can only be one character." end @short = value.to_s end # Convert the object to a config statement. def to_config str = @desc.gsub(/^/, "# ") + "\n" # Add in a statement about the default. if defined? @default and @default str += "# The default value is '%s'.\n" % @default end - line = "%s = %s" % [@name, self.value] - # If the value has not been overridden, then print it out commented # and unconverted, so it's clear that that's the default and how it # works. - if defined? @value and ! @value.nil? - line = "%s = %s" % [@name, self.value] + value = @parent.value(self.name) + + if value != @default + line = "%s = %s" % [@name, value] else line = "# %s = %s" % [@name, @default] end str += line + "\n" str.gsub(/^/, " ") end # Retrieves the value, or if it's not set, retrieves the default. def value - retval = nil - if defined? @value and ! @value.nil? - retval = @value - elsif defined? @default - retval = @default - else - return nil - end - - if retval.is_a? String - return convert(retval) - else - return retval - end - end - - # Set the value. - def value=(value) - if respond_to?(:validate) - validate(value) - end - - if respond_to?(:munge) - @value = munge(value) - else - @value = value - end - - if respond_to?(:handle) - handle(@value) - end + @parent.value(self.name) end end # A file. class CFile < CElement attr_writer :owner, :group attr_accessor :mode, :create def group if defined? @group - return convert(@group) + return @parent.convert(@group) else return nil end end def owner if defined? @owner - return convert(@owner) + return @parent.convert(@owner) else return nil end end # Set the type appropriately. Yep, a hack. This supports either naming # the variable 'dir', or adding a slash at the end. def munge(value) if value.to_s =~ /\/$/ @type = :directory return value.sub(/\/$/, '') end return value end # Return the appropriate type. def type - value = self.value + value = @parent.value(self.name) if @name.to_s =~ /dir/ return :directory elsif value.to_s =~ /\/$/ return :directory elsif value.is_a? String return :file else return nil end end # Convert the object to a TransObject instance. # FIXME There's no dependency system in place right now; if you use # a section that requires another section, there's nothing done to # correct that for you, at the moment. def to_transportable type = self.type return nil unless type - path = self.value.split(File::SEPARATOR) + path = @parent.value(self.name).split(File::SEPARATOR) path.shift # remove the leading nil objects = [] - obj = Puppet::TransObject.new(self.value, "file") + path = self.value + unless path =~ /^#{File::SEPARATOR}/ + path = File.join(Dir.getwd, path) + end + obj = Puppet::TransObject.new(path, "file") # Only create directories, or files that are specifically marked to # create. if type == :directory or self.create obj[:ensure] = type end [:mode].each { |var| if value = self.send(var) - # Don't both converting the mode, since the file type + # Don't bother converting the mode, since the file type # can handle it any old way. obj[var] = value end } # Only chown or chgrp when root if Puppet::Util::SUIDManager.uid == 0 [:group, :owner].each { |var| if value = self.send(var) obj[var] = value end } end # And set the loglevel to debug for everything obj[:loglevel] = "debug" # We're not actually modifying any files here, and if we allow a # filebucket to get used here we get into an infinite recursion # trying to set the filebucket up. obj[:backup] = false if self.section obj.tags += ["puppet", "configuration", self.section, self.name] end objects << obj objects end # Make sure any provided variables look up to something. def validate(value) return true unless value.is_a? String value.scan(/\$(\w+)/) { |name| name = $1 unless @parent.include?(name) raise ArgumentError, "Configuration parameter '%s' is undefined" % name end } end end # A simple boolean. class CBoolean < CElement # get the arguments in getopt format def getopt_args if short [["--#{name}", "-#{short}", GetoptLong::NO_ARGUMENT], ["--no-#{name}", GetoptLong::NO_ARGUMENT]] else [["--#{name}", GetoptLong::NO_ARGUMENT], ["--no-#{name}", GetoptLong::NO_ARGUMENT]] end end def munge(value) case value when true, "true": return true when false, "false": return false else - raise Puppet::Error, "Invalid value '%s' for %s" % + raise ArgumentError, "Invalid value '%s' for %s" % [value.inspect, @name] end end end end # $Id$ diff --git a/lib/puppet/util/diff.rb b/lib/puppet/util/diff.rb new file mode 100644 index 000000000..e6ff57108 --- /dev/null +++ b/lib/puppet/util/diff.rb @@ -0,0 +1,71 @@ +# Provide a diff between two strings. +module Puppet::Util::Diff + include Puppet::Util + require 'tempfile' + + def diff(old, new) + command = [Puppet[:diff]] + if args = Puppet[:diff_args] and args != "" + command << args + end + command << old << new + execute(command, :failonfail => false) + end + + module_function :diff + + # return diff string of two input strings + # format defaults to unified + # context defaults to 3 lines + def lcs_diff(data_old, data_new, format=:unified, context_lines=3) + unless Puppet.features.diff? + Puppet.warning "Cannot provide diff without the diff/lcs Ruby library" + return "" + end + data_old = data_old.split(/\n/).map! { |e| e.chomp } + data_new = data_new.split(/\n/).map! { |e| e.chomp } + + output = "" + + diffs = Diff::LCS.diff(data_old, data_new) + return output if diffs.empty? + + oldhunk = hunk = nil + file_length_difference = 0 + + diffs.each do |piece| + begin + hunk = Diff::LCS::Hunk.new(data_old, data_new, piece, + context_lines, + file_length_difference) + file_length_difference = hunk.file_length_difference + next unless oldhunk + # Hunks may overlap, which is why we need to be careful when our + # diff includes lines of context. Otherwise, we might print + # redundant lines. + if (context_lines > 0) and hunk.overlaps?(oldhunk) + hunk.unshift(oldhunk) + else + output << oldhunk.diff(format) + end + ensure + oldhunk = hunk + output << "\n" + end + end + + # Handle the last remaining hunk + output << oldhunk.diff(format) << "\n" + end + + def string_file_diff(path, string) + require 'tempfile' + tempfile = Tempfile.new("puppet-diffing") + tempfile.open + tempfile.print string + tempfile.close + print diff(path, tempfile.path) + tempfile.delete + end +end + diff --git a/lib/puppet/util/methodhelper.rb b/lib/puppet/util/methodhelper.rb index 63158ab67..32fca1877 100644 --- a/lib/puppet/util/methodhelper.rb +++ b/lib/puppet/util/methodhelper.rb @@ -1,37 +1,35 @@ # Where we store helper methods related to, um, methods. module Puppet::Util::MethodHelper def requiredopts(*names) names.each do |name| if self.send(name).nil? devfail("%s is a required option for %s" % [name, self.class]) end end end # Iterate over a hash, treating each member as an attribute. def set_options(options) options.each do |param,value| method = param.to_s + "=" begin self.send(method, value) rescue NoMethodError raise ArgumentError, "Invalid parameter %s to object class %s" % [param,self.class.to_s] end end end # Take a hash and convert all of the keys to symbols if possible. def symbolize_options(options) options.inject({}) do |hash, opts| if opts[0].respond_to? :intern hash[opts[0].intern] = opts[1] else hash[opts[0]] = opts[1] end hash end end end - -# $Id$ diff --git a/spec/Rakefile b/spec/Rakefile new file mode 100644 index 000000000..40d107312 --- /dev/null +++ b/spec/Rakefile @@ -0,0 +1,10 @@ +require File.join(File.dirname(__FILE__), "spec_helper.rb") +require 'rake' +require 'spec/rake/spectask' + +desc "Run all spec unit tests" +Spec::Rake::SpecTask.new('unit') do |t| + t.spec_files = FileList['unit/**/*.rb'] +end + +task :default => [:unit] diff --git a/spec/bin/spec b/spec/bin/spec new file mode 100755 index 000000000..a7e6ce0cb --- /dev/null +++ b/spec/bin/spec @@ -0,0 +1,3 @@ +$LOAD_PATH.unshift(File.expand_path(File.dirname(__FILE__) + "/../lib")) +require 'spec' +::Spec::Runner::CommandLine.run(ARGV, STDERR, STDOUT, true, true) diff --git a/test/lib/spec.rb b/spec/lib/spec.rb similarity index 51% rename from test/lib/spec.rb rename to spec/lib/spec.rb index 9a83c1d5e..48c12595c 100644 --- a/test/lib/spec.rb +++ b/spec/lib/spec.rb @@ -1,8 +1,13 @@ -require 'spec/deprecated' +require 'spec/extensions' require 'spec/version' -require 'spec/callback' require 'spec/matchers' require 'spec/expectations' -require 'spec/mocks' -require 'spec/runner' require 'spec/translator' +require 'spec/dsl' +require 'spec/runner' + +class Object + def metaclass + class << self; self; end + end +end diff --git a/spec/lib/spec/dsl.rb b/spec/lib/spec/dsl.rb new file mode 100644 index 000000000..f960eb907 --- /dev/null +++ b/spec/lib/spec/dsl.rb @@ -0,0 +1,11 @@ +require 'spec/dsl/description' +require 'spec/dsl/errors' +require 'spec/dsl/configuration' +require 'spec/dsl/behaviour_callbacks' +require 'spec/dsl/behaviour' +require 'spec/dsl/behaviour_eval' +require 'spec/dsl/composite_proc_builder' +require 'spec/dsl/example' +require 'spec/dsl/example_matcher' +require 'spec/dsl/example_should_raise_handler' +require 'spec/dsl/behaviour_factory' diff --git a/spec/lib/spec/dsl/behaviour.rb b/spec/lib/spec/dsl/behaviour.rb new file mode 100644 index 000000000..5158bb673 --- /dev/null +++ b/spec/lib/spec/dsl/behaviour.rb @@ -0,0 +1,220 @@ +module Spec + module DSL + class EvalModule < Module; end + class Behaviour + extend BehaviourCallbacks + + class << self + def add_shared_behaviour(behaviour) + return if behaviour.equal?(found_behaviour = find_shared_behaviour(behaviour.description)) + return if found_behaviour and File.expand_path(behaviour.description[:spec_path]) == File.expand_path(found_behaviour.description[:spec_path]) + raise ArgumentError.new("Shared Behaviour '#{behaviour.description}' already exists") if found_behaviour + shared_behaviours << behaviour + end + + def find_shared_behaviour(behaviour_description) + shared_behaviours.find { |b| b.description == behaviour_description } + end + + def shared_behaviours + # TODO - this needs to be global, or at least accessible from + # from subclasses of Behaviour in a centralized place. I'm not loving + # this as a solution, but it works for now. + $shared_behaviours ||= [] + end + end + + def initialize(*args, &behaviour_block) + init_description(*args) + init_eval_module + before_eval + eval_behaviour(&behaviour_block) + end + + private + + def init_description(*args) + unless self.class == Behaviour + args << {} unless Hash === args.last + args.last[:behaviour_class] = self.class + end + @description = Description.new(*args) + end + + def init_eval_module + @eval_module = EvalModule.new + @eval_module.extend BehaviourEval::ModuleMethods + @eval_module.include BehaviourEval::InstanceMethods + @eval_module.include described_type if described_type.class == Module + @eval_module.behaviour = self + @eval_module.description = @description + end + + def eval_behaviour(&behaviour_block) + @eval_module.class_eval(&behaviour_block) + end + + protected + + def before_eval + end + + public + + def run(reporter, dry_run=false, reverse=false, timeout=nil) + raise "shared behaviours should never run" if shared? + # TODO - change add_behaviour to add_description ?????? + reporter.add_behaviour(@description) + prepare_execution_context_class + before_all_errors = run_before_all(reporter, dry_run) + + exs = reverse ? examples.reverse : examples + example_execution_context = nil + + if before_all_errors.empty? + exs.each do |example| + example_execution_context = execution_context(example) + example_execution_context.copy_instance_variables_from(@before_and_after_all_context_instance) unless before_all_proc(behaviour_type).nil? + + befores = before_each_proc(behaviour_type) {|e| raise e} + afters = after_each_proc(behaviour_type) + example.run(reporter, befores, afters, dry_run, example_execution_context, timeout) + end + end + + @before_and_after_all_context_instance.copy_instance_variables_from(example_execution_context) unless after_all_proc(behaviour_type).nil? + run_after_all(reporter, dry_run) + end + + def number_of_examples + examples.length + end + + def matches?(specified_examples) + matcher ||= ExampleMatcher.new(description) + + examples.each do |example| + return true if example.matches?(matcher, specified_examples) + end + return false + end + + def shared? + @description[:shared] + end + + def retain_examples_matching!(specified_examples) + return if specified_examples.index(description) + matcher = ExampleMatcher.new(description) + examples.reject! do |example| + !example.matches?(matcher, specified_examples) + end + end + + def methods + my_methods = super + my_methods |= @eval_module.methods + my_methods + end + + # Includes modules in the Behaviour (the describe block). + def include(*args) + @eval_module.include(*args) + end + + def behaviour_type #:nodoc: + @description[:behaviour_type] + end + + # Sets the #number on each Example and returns the next number + def set_sequence_numbers(number, reverse) #:nodoc: + exs = reverse ? examples.reverse : examples + exs.each do |example| + example.number = number + number += 1 + end + number + end + + protected + + # Messages that this class does not understand + # are passed directly to the @eval_module. + def method_missing(sym, *args, &block) + @eval_module.send(sym, *args, &block) + end + + def prepare_execution_context_class + plugin_mock_framework + weave_in_included_modules + define_predicate_matchers #this is in behaviour_eval + execution_context_class + end + + def weave_in_included_modules + mods = [@eval_module] + mods << included_modules.dup + mods << Spec::Runner.configuration.modules_for(behaviour_type) + execution_context_class.class_eval do + # WARNING - the following can be executed in the context of any + # class, and should never pass more than one module to include + # even though we redefine include in this class. This is NOT + # tested anywhere, hence this comment. + mods.flatten.each {|mod| include mod} + end + end + + def execution_context(example) + execution_context_class.new(example) + end + + def run_before_all(reporter, dry_run) + errors = [] + unless dry_run + begin + @before_and_after_all_context_instance = execution_context(nil) + @before_and_after_all_context_instance.instance_eval(&before_all_proc(behaviour_type)) + rescue Exception => e + errors << e + location = "before(:all)" + # The easiest is to report this as an example failure. We don't have an Example + # at this point, so we'll just create a placeholder. + reporter.example_finished(Example.new(location), e, location) if reporter + end + end + errors + end + + def run_after_all(reporter, dry_run) + unless dry_run + begin + @before_and_after_all_context_instance ||= execution_context(nil) + @before_and_after_all_context_instance.instance_eval(&after_all_proc(behaviour_type)) + rescue Exception => e + location = "after(:all)" + reporter.example_finished(Example.new(location), e, location) if reporter + end + end + end + + def plugin_mock_framework + case mock_framework = Spec::Runner.configuration.mock_framework + when Module + include mock_framework + else + require Spec::Runner.configuration.mock_framework + include Spec::Plugins::MockFramework + end + end + + def description + @description.to_s + end + + def described_type + @description.described_type + end + + end + end +end diff --git a/spec/lib/spec/dsl/behaviour_callbacks.rb b/spec/lib/spec/dsl/behaviour_callbacks.rb new file mode 100644 index 000000000..8b69ad9e5 --- /dev/null +++ b/spec/lib/spec/dsl/behaviour_callbacks.rb @@ -0,0 +1,82 @@ +module Spec + module DSL + # See http://rspec.rubyforge.org/documentation/before_and_after.html + module BehaviourCallbacks + def prepend_before(*args, &block) + scope, options = scope_and_options(*args) + add(scope, options, :before, :unshift, &block) + end + def append_before(*args, &block) + scope, options = scope_and_options(*args) + add(scope, options, :before, :<<, &block) + end + alias_method :before, :append_before + + def prepend_after(*args, &block) + scope, options = scope_and_options(*args) + add(scope, options, :after, :unshift, &block) + end + alias_method :after, :prepend_after + def append_after(*args, &block) + scope, options = scope_and_options(*args) + add(scope, options, :after, :<<, &block) + end + + def scope_and_options(*args) + args, options = args_and_options(*args) + scope = (args[0] || :each), options + end + + def add(scope, options, where, how, &block) + scope ||= :each + options ||= {} + behaviour_type = options[:behaviour_type] + case scope + when :each; self.__send__("#{where}_each_parts", behaviour_type).__send__(how, block) + when :all; self.__send__("#{where}_all_parts", behaviour_type).__send__(how, block) + end + end + + def remove_after(scope, &block) + after_each_parts.delete(block) + end + + # Deprecated. Use before(:each) + def setup(&block) + before(:each, &block) + end + + # Deprecated. Use after(:each) + def teardown(&block) + after(:each, &block) + end + + def before_all_parts(behaviour_type=nil) # :nodoc: + @before_all_parts ||= {} + @before_all_parts[behaviour_type] ||= [] + end + + def after_all_parts(behaviour_type=nil) # :nodoc: + @after_all_parts ||= {} + @after_all_parts[behaviour_type] ||= [] + end + + def before_each_parts(behaviour_type=nil) # :nodoc: + @before_each_parts ||= {} + @before_each_parts[behaviour_type] ||= [] + end + + def after_each_parts(behaviour_type=nil) # :nodoc: + @after_each_parts ||= {} + @after_each_parts[behaviour_type] ||= [] + end + + def clear_before_and_after! # :nodoc: + @before_all_parts = nil + @after_all_parts = nil + @before_each_parts = nil + @after_each_parts = nil + end + end + end +end diff --git a/spec/lib/spec/dsl/behaviour_eval.rb b/spec/lib/spec/dsl/behaviour_eval.rb new file mode 100644 index 000000000..9f7b8281e --- /dev/null +++ b/spec/lib/spec/dsl/behaviour_eval.rb @@ -0,0 +1,231 @@ +module Spec + module DSL + module BehaviourEval + module ModuleMethods + include BehaviourCallbacks + + attr_writer :behaviour + attr_accessor :description + + # RSpec runs every example in a new instance of Object, mixing in + # the behaviour necessary to run examples. Because this behaviour gets + # mixed in, it can get mixed in to an instance of any class at all. + # + # This is something that you would hardly ever use, but there is one + # common use case for it - inheriting from Test::Unit::TestCase. RSpec's + # Rails plugin uses this feature to provide access to all of the features + # that are available for Test::Unit within RSpec examples. + def inherit(klass) + raise ArgumentError.new("Shared behaviours cannot inherit from classes") if @behaviour.shared? + @behaviour_superclass = klass + derive_execution_context_class_from_behaviour_superclass + end + + # You can pass this one or many modules. Each module will subsequently + # be included in the each object in which an example is run. Use this + # to provide global helper methods to your examples. + # + # == Example + # + # module HelperMethods + # def helper_method + # ... + # end + # end + # + # describe Thing do + # include HelperMethods + # it "should do stuff" do + # helper_method + # end + # end + def include(*mods) + mods.each do |mod| + included_modules << mod + mod.send :included, self + end + end + + # Use this to pull in examples from shared behaviours. + # See Spec::Runner for information about shared behaviours. + def it_should_behave_like(behaviour_description) + behaviour = @behaviour.class.find_shared_behaviour(behaviour_description) + if behaviour.nil? + raise RuntimeError.new("Shared Behaviour '#{behaviour_description}' can not be found") + end + behaviour.copy_to(self) + end + + def copy_to(eval_module) # :nodoc: + examples.each { |e| eval_module.examples << e; } + before_each_parts.each { |p| eval_module.before_each_parts << p } + after_each_parts.each { |p| eval_module.after_each_parts << p } + before_all_parts.each { |p| eval_module.before_all_parts << p } + after_all_parts.each { |p| eval_module.after_all_parts << p } + included_modules.each { |m| eval_module.included_modules << m } + eval_module.included_modules << self + end + + # :call-seq: + # predicate_matchers[matcher_name] = method_on_object + # predicate_matchers[matcher_name] = [method1_on_object, method2_on_object] + # + # Dynamically generates a custom matcher that will match + # a predicate on your class. RSpec provides a couple of these + # out of the box: + # + # exist (or state expectations) + # File.should exist("path/to/file") + # + # an_instance_of (for mock argument constraints) + # mock.should_receive(:message).with(an_instance_of(String)) + # + # == Examples + # + # class Fish + # def can_swim? + # true + # end + # end + # + # describe Fish do + # predicate_matchers[:swim] = :can_swim? + # it "should swim" do + # Fish.new.should swim + # end + # end + def predicate_matchers + @predicate_matchers ||= {:exist => :exist?, :an_instance_of => :is_a?} + end + + def define_predicate_matchers(hash=nil) # :nodoc: + if hash.nil? + define_predicate_matchers(predicate_matchers) + define_predicate_matchers(Spec::Runner.configuration.predicate_matchers) + else + hash.each_pair do |matcher_method, method_on_object| + define_method matcher_method do |*args| + eval("be_#{method_on_object.to_s.gsub('?','')}(*args)") + end + end + end + end + + # Creates an instance of Spec::DSL::Example and adds + # it to a collection of examples of the current behaviour. + def it(description=:__generate_description, opts={}, &block) + examples << Example.new(description, opts, &block) + end + + # Alias for it. + def specify(description=:__generate_description, opts={}, &block) + it(description, opts, &block) + end + + def methods # :nodoc: + my_methods = super + my_methods |= behaviour_superclass.methods + my_methods + end + + protected + + def method_missing(method_name, *args) + if behaviour_superclass.respond_to?(method_name) + return execution_context_class.send(method_name, *args) + end + super + end + + def before_each_proc(behaviour_type, &error_handler) + parts = [] + parts.push(*Behaviour.before_each_parts(nil)) + parts.push(*Behaviour.before_each_parts(behaviour_type)) unless behaviour_type.nil? + parts.push(*before_each_parts(nil)) + parts.push(*before_each_parts(behaviour_type)) unless behaviour_type.nil? + CompositeProcBuilder.new(parts).proc(&error_handler) + end + + def before_all_proc(behaviour_type, &error_handler) + parts = [] + parts.push(*Behaviour.before_all_parts(nil)) + parts.push(*Behaviour.before_all_parts(behaviour_type)) unless behaviour_type.nil? + parts.push(*before_all_parts(nil)) + parts.push(*before_all_parts(behaviour_type)) unless behaviour_type.nil? + CompositeProcBuilder.new(parts).proc(&error_handler) + end + + def after_all_proc(behaviour_type) + parts = [] + parts.push(*after_all_parts(behaviour_type)) unless behaviour_type.nil? + parts.push(*after_all_parts(nil)) + parts.push(*Behaviour.after_all_parts(behaviour_type)) unless behaviour_type.nil? + parts.push(*Behaviour.after_all_parts(nil)) + CompositeProcBuilder.new(parts).proc + end + + def after_each_proc(behaviour_type) + parts = [] + parts.push(*after_each_parts(behaviour_type)) unless behaviour_type.nil? + parts.push(*after_each_parts(nil)) + parts.push(*Behaviour.after_each_parts(behaviour_type)) unless behaviour_type.nil? + parts.push(*Behaviour.after_each_parts(nil)) + CompositeProcBuilder.new(parts).proc + end + + private + + def execution_context_class + @execution_context_class ||= derive_execution_context_class_from_behaviour_superclass + end + + def derive_execution_context_class_from_behaviour_superclass + @execution_context_class = Class.new(behaviour_superclass) + behaviour_superclass.spec_inherited(self) if behaviour_superclass.respond_to?(:spec_inherited) + @execution_context_class + end + + def behaviour_superclass + @behaviour_superclass ||= Object + end + + protected + def included_modules + @included_modules ||= [::Spec::Matchers] + end + + def examples + @examples ||= [] + end + end + + module InstanceMethods + def initialize(*args, &block) #:nodoc: + # TODO - inheriting from TestUnit::TestCase fails without this + # - let's figure out why and move this somewhere else + end + + def violated(message="") + raise Spec::Expectations::ExpectationNotMetError.new(message) + end + + def inspect + "[RSpec example]" + end + + def pending(message) + if block_given? + begin + yield + rescue Exception => e + raise Spec::DSL::ExamplePendingError.new(message) + end + raise Spec::DSL::PendingFixedError.new("Expected pending '#{message}' to fail. No Error was raised.") + else + raise Spec::DSL::ExamplePendingError.new(message) + end + end + end + end + end +end diff --git a/spec/lib/spec/dsl/behaviour_factory.rb b/spec/lib/spec/dsl/behaviour_factory.rb new file mode 100755 index 000000000..44b60c641 --- /dev/null +++ b/spec/lib/spec/dsl/behaviour_factory.rb @@ -0,0 +1,42 @@ +module Spec + module DSL + class BehaviourFactory + + class << self + + BEHAVIOUR_CLASSES = {:default => Spec::DSL::Behaviour} + + # Registers a behaviour class +klass+ with the symbol + # +behaviour_type+. For example: + # + # Spec::DSL::BehaviourFactory.add_behaviour_class(:farm, Spec::Farm::DSL::FarmBehaviour) + # + # This will cause Kernel#describe from a file living in + # spec/farm to create behaviour instances of type + # Spec::Farm::DSL::FarmBehaviour. + def add_behaviour_class(behaviour_type, klass) + BEHAVIOUR_CLASSES[behaviour_type] = klass + end + + def remove_behaviour_class(behaviour_type) + BEHAVIOUR_CLASSES.delete(behaviour_type) + end + + def create(*args, &block) + opts = Hash === args.last ? args.last : {} + if opts[:shared] + behaviour_type = :default + elsif opts[:behaviour_type] + behaviour_type = opts[:behaviour_type] + elsif opts[:spec_path] =~ /spec(\\|\/)(#{BEHAVIOUR_CLASSES.keys.join('|')})/ + behaviour_type = $2.to_sym + else + behaviour_type = :default + end + return BEHAVIOUR_CLASSES[behaviour_type].new(*args, &block) + end + + end + end + end +end diff --git a/spec/lib/spec/dsl/composite_proc_builder.rb b/spec/lib/spec/dsl/composite_proc_builder.rb new file mode 100644 index 000000000..373f44953 --- /dev/null +++ b/spec/lib/spec/dsl/composite_proc_builder.rb @@ -0,0 +1,33 @@ +module Spec + module DSL + class CompositeProcBuilder < Array + def initialize(callbacks=[]) + push(*callbacks) + end + + def proc(&error_handler) + parts = self + errors = [] + Proc.new do + result = parts.collect do |part| + begin + if part.is_a?(UnboundMethod) + part.bind(self).call + else + instance_eval(&part) + end + rescue Exception => e + if error_handler + error_handler.call(e) + else + errors << e + end + end + end + raise errors.first unless errors.empty? + result + end + end + end + end +end diff --git a/spec/lib/spec/dsl/configuration.rb b/spec/lib/spec/dsl/configuration.rb new file mode 100755 index 000000000..709574ded --- /dev/null +++ b/spec/lib/spec/dsl/configuration.rb @@ -0,0 +1,135 @@ +module Spec + module DSL + class Configuration + + # Chooses what mock framework to use. Example: + # + # Spec::Runner.configure do |config| + # config.mock_with :rspec, :mocha, :flexmock, or :rr + # end + # + # To use any other mock framework, you'll have to provide + # your own adapter. This is simply a module that responds to + # setup_mocks_for_rspec, verify_mocks_for_rspec and teardown_mocks_for_rspec. + # These are your hooks into the lifecycle of a given example. RSpec will + # call setup_mocks_for_rspec before running anything else in each Example. + # After executing the #after methods, RSpec will then call verify_mocks_for_rspec + # and teardown_mocks_for_rspec (this is guaranteed to run even if there are + # failures in verify_mocks_for_rspec). + # + # Once you've defined this module, you can pass that to mock_with: + # + # Spec::Runner.configure do |config| + # config.mock_with MyMockFrameworkAdapter + # end + # + def mock_with(mock_framework) + @mock_framework = case mock_framework + when Symbol + mock_framework_path(mock_framework.to_s) + else + mock_framework + end + end + + def mock_framework # :nodoc: + @mock_framework ||= mock_framework_path("rspec") + end + + # Declares modules to be included in all behaviours (describe blocks). + # + # config.include(My::Bottle, My::Cup) + # + # If you want to restrict the inclusion to a subset of all the behaviours then + # specify this in a Hash as the last argument: + # + # config.include(My::Pony, My::Horse, :behaviour_type => :farm) + # + # Only behaviours that have that type will get the modules included: + # + # describe "Downtown", :behaviour_type => :city do + # # Will *not* get My::Pony and My::Horse included + # end + # + # describe "Old Mac Donald", :behaviour_type => :farm do + # # *Will* get My::Pony and My::Horse included + # end + # + def include(*args) + args << {} unless Hash === args.last + modules, options = args_and_options(*args) + required_behaviour_type = options[:behaviour_type] + required_behaviour_type = required_behaviour_type.to_sym unless required_behaviour_type.nil? + @modules ||= {} + @modules[required_behaviour_type] ||= [] + @modules[required_behaviour_type] += modules + end + + def modules_for(required_behaviour_type) #:nodoc: + @modules ||= {} + modules = @modules[nil] || [] # general ones + modules << @modules[required_behaviour_type.to_sym] unless required_behaviour_type.nil? + modules.uniq.compact + end + + # This is just for cleanup in RSpec's own examples + def exclude(*modules) #:nodoc: + @modules.each do |behaviour_type, mods| + modules.each{|m| mods.delete(m)} + end + end + + # Defines global predicate matchers. Example: + # + # config.predicate_matchers[:swim] = :can_swim? + # + # This makes it possible to say: + # + # person.should swim # passes if person.should_swim? returns true + # + def predicate_matchers + @predicate_matchers ||= {} + end + + # Prepends a global before block to all behaviours. + # See #append_before for filtering semantics. + def prepend_before(*args, &proc) + Behaviour.prepend_before(*args, &proc) + end + # Appends a global before block to all behaviours. + # + # If you want to restrict the block to a subset of all the behaviours then + # specify this in a Hash as the last argument: + # + # config.prepend_before(:all, :behaviour_type => :farm) + # + # or + # + # config.prepend_before(:behaviour_type => :farm) + # + def append_before(*args, &proc) + Behaviour.append_before(*args, &proc) + end + alias_method :before, :append_before + + # Prepends a global after block to all behaviours. + # See #append_before for filtering semantics. + def prepend_after(*args, &proc) + Behaviour.prepend_after(*args, &proc) + end + alias_method :after, :prepend_after + # Appends a global after block to all behaviours. + # See #append_before for filtering semantics. + def append_after(*args, &proc) + Behaviour.append_after(*args, &proc) + end + + private + + def mock_framework_path(framework_name) + File.expand_path(File.join(File.dirname(__FILE__), "..", "..", "..", "plugins", "mock_frameworks", framework_name)) + end + + end + end +end diff --git a/spec/lib/spec/dsl/description.rb b/spec/lib/spec/dsl/description.rb new file mode 100755 index 000000000..fe8c9b0c9 --- /dev/null +++ b/spec/lib/spec/dsl/description.rb @@ -0,0 +1,76 @@ +module Spec + module DSL + class Description + module ClassMethods + def generate_description(*args) + description = args.shift.to_s + unless args.empty? + suffix = args.shift.to_s + description << " " unless suffix =~ /^\s|\.|#/ + description << suffix + end + description + end + end + extend ClassMethods + + attr_reader :description, :described_type + + def initialize(*args) + args, @options = args_and_options(*args) + init_behaviour_type(@options) + init_spec_path(@options) + init_described_type(args) + init_description(*args) + end + + def [](key) + @options[key] + end + + def []=(key, value) + @options[key] = value + end + + def to_s; @description; end + + def ==(value) + case value + when Description + @description == value.description + else + @description == value + end + end + + private + def init_behaviour_type(options) + # NOTE - BE CAREFUL IF CHANGING THIS NEXT LINE: + # this line is as it is to satisfy JRuby - the original version + # read, simply: "if options[:behaviour_class]", which passed against ruby, but failed against jruby + if options[:behaviour_class] && options[:behaviour_class].ancestors.include?(Behaviour) + options[:behaviour_type] = parse_behaviour_type(@options[:behaviour_class]) + end + end + + def init_spec_path(options) + if options.has_key?(:spec_path) + options[:spec_path] = File.expand_path(@options[:spec_path]) + end + end + + def init_description(*args) + @description = self.class.generate_description(*args) + end + + def init_described_type(args) + @described_type = args.first unless args.first.is_a?(String) + end + + def parse_behaviour_type(behaviour_class) + behaviour_class.to_s.split("::").reverse[0].gsub!('Behaviour', '').downcase.to_sym + end + + end + end +end diff --git a/spec/lib/spec/dsl/errors.rb b/spec/lib/spec/dsl/errors.rb new file mode 100644 index 000000000..ba7046a89 --- /dev/null +++ b/spec/lib/spec/dsl/errors.rb @@ -0,0 +1,9 @@ +module Spec + module DSL + class ExamplePendingError < StandardError + end + + class PendingFixedError < StandardError + end + end +end diff --git a/spec/lib/spec/dsl/example.rb b/spec/lib/spec/dsl/example.rb new file mode 100644 index 000000000..d04073f7e --- /dev/null +++ b/spec/lib/spec/dsl/example.rb @@ -0,0 +1,135 @@ +require 'timeout' + +module Spec + module DSL + class Example + # The global sequence number of this example + attr_accessor :number + + def initialize(description, options={}, &example_block) + @from = caller(0)[3] + @options = options + @example_block = example_block + @description = description + @description_generated_proc = lambda { |desc| @generated_description = desc } + end + + def run(reporter, before_each_block, after_each_block, dry_run, execution_context, timeout=nil) + @dry_run = dry_run + reporter.example_started(self) + return reporter.example_finished(self) if dry_run + + errors = [] + location = nil + Timeout.timeout(timeout) do + before_each_ok = before_example(execution_context, errors, &before_each_block) + example_ok = run_example(execution_context, errors) if before_each_ok + after_each_ok = after_example(execution_context, errors, &after_each_block) + location = failure_location(before_each_ok, example_ok, after_each_ok) + end + + ExampleShouldRaiseHandler.new(@from, @options).handle(errors) + reporter.example_finished(self, errors.first, location, @example_block.nil?) if reporter + end + + def matches?(matcher, specified_examples) + matcher.example_desc = description + matcher.matches?(specified_examples) + end + + def description + @description == :__generate_description ? generated_description : @description + end + + def to_s + description + end + + private + + def generated_description + return @generated_description if @generated_description + if @dry_run + "NO NAME (Because of --dry-run)" + else + if @failed + "NO NAME (Because of Error raised in matcher)" + else + "NO NAME (Because there were no expectations)" + end + end + end + + def before_example(execution_context, errors, &behaviour_before_block) + setup_mocks(execution_context) + Spec::Matchers.description_generated(@description_generated_proc) + + builder = CompositeProcBuilder.new + before_proc = builder.proc(&append_errors(errors)) + execution_context.instance_eval(&before_proc) + + execution_context.instance_eval(&behaviour_before_block) if behaviour_before_block + return errors.empty? + rescue Exception => e + @failed = true + errors << e + return false + end + + def run_example(execution_context, errors) + begin + execution_context.instance_eval(&@example_block) if @example_block + return true + rescue Exception => e + @failed = true + errors << e + return false + end + end + + def after_example(execution_context, errors, &behaviour_after_each) + execution_context.instance_eval(&behaviour_after_each) if behaviour_after_each + + begin + verify_mocks(execution_context) + ensure + teardown_mocks(execution_context) + end + + Spec::Matchers.unregister_description_generated(@description_generated_proc) + + builder = CompositeProcBuilder.new + after_proc = builder.proc(&append_errors(errors)) + execution_context.instance_eval(&after_proc) + + return errors.empty? + rescue Exception => e + @failed = true + errors << e + return false + end + + def setup_mocks(execution_context) + execution_context.setup_mocks_for_rspec if execution_context.respond_to?(:setup_mocks_for_rspec) + end + + def verify_mocks(execution_context) + execution_context.verify_mocks_for_rspec if execution_context.respond_to?(:verify_mocks_for_rspec) + end + + def teardown_mocks(execution_context) + execution_context.teardown_mocks_for_rspec if execution_context.respond_to?(:teardown_mocks_for_rspec) + end + + def append_errors(errors) + proc {|error| errors << error} + end + + def failure_location(before_each_ok, example_ok, after_each_ok) + return 'before(:each)' unless before_each_ok + return description unless example_ok + return 'after(:each)' unless after_each_ok + end + end + end +end diff --git a/spec/lib/spec/dsl/example_matcher.rb b/spec/lib/spec/dsl/example_matcher.rb new file mode 100755 index 000000000..18cc47409 --- /dev/null +++ b/spec/lib/spec/dsl/example_matcher.rb @@ -0,0 +1,40 @@ +module Spec + module DSL + class ExampleMatcher + + attr_writer :example_desc + def initialize(behaviour_desc, example_desc=nil) + @behaviour_desc = behaviour_desc + @example_desc = example_desc + end + + def matches?(specified_examples) + specified_examples.each do |specified_example| + return true if matches_literal_example?(specified_example) || matches_example_not_considering_modules?(specified_example) + end + false + end + + private + def matches_literal_example?(specified_example) + specified_example =~ /(^#{context_regexp} #{example_regexp}$|^#{context_regexp}$|^#{example_regexp}$)/ + end + + def matches_example_not_considering_modules?(specified_example) + specified_example =~ /(^#{context_regexp_not_considering_modules} #{example_regexp}$|^#{context_regexp_not_considering_modules}$|^#{example_regexp}$)/ + end + + def context_regexp + Regexp.escape(@behaviour_desc) + end + + def context_regexp_not_considering_modules + Regexp.escape(@behaviour_desc.split('::').last) + end + + def example_regexp + Regexp.escape(@example_desc) + end + end + end +end diff --git a/test/lib/spec/runner/spec_should_raise_handler.rb b/spec/lib/spec/dsl/example_should_raise_handler.rb similarity index 92% rename from test/lib/spec/runner/spec_should_raise_handler.rb rename to spec/lib/spec/dsl/example_should_raise_handler.rb index c7fa41c4e..942327317 100644 --- a/test/lib/spec/runner/spec_should_raise_handler.rb +++ b/spec/lib/spec/dsl/example_should_raise_handler.rb @@ -1,74 +1,74 @@ module Spec - module Runner - class SpecShouldRaiseHandler + module DSL + class ExampleShouldRaiseHandler def initialize(file_and_line_number, opts) @file_and_line_number = file_and_line_number @options = opts @expected_error_class = determine_error_class(opts) @expected_error_message = determine_error_message(opts) end def determine_error_class(opts) if candidate = opts[:should_raise] if candidate.is_a?(Class) return candidate elsif candidate.is_a?(Array) return candidate[0] else return Exception end end end def determine_error_message(opts) if candidate = opts[:should_raise] if candidate.is_a?(Array) return candidate[1] end end return nil end def build_message(exception=nil) if @expected_error_message.nil? - message = "specify block expected #{@expected_error_class.to_s}" + message = "example block expected #{@expected_error_class.to_s}" else - message = "specify block expected #{@expected_error_class.new(@expected_error_message.to_s).inspect}" + message = "example block expected #{@expected_error_class.new(@expected_error_message.to_s).inspect}" end message << " but raised #{exception.inspect}" if exception message << " but nothing was raised" unless exception message << "\n" message << @file_and_line_number end def error_matches?(error) return false unless error.kind_of?(@expected_error_class) unless @expected_error_message.nil? if @expected_error_message.is_a?(Regexp) return false unless error.message =~ @expected_error_message else return false unless error.message == @expected_error_message end end return true end def handle(errors) if @expected_error_class if errors.empty? errors << Spec::Expectations::ExpectationNotMetError.new(build_message) else error_to_remove = errors.detect do |error| error_matches?(error) end if error_to_remove.nil? errors.insert(0,Spec::Expectations::ExpectationNotMetError.new(build_message(errors[0]))) else errors.delete(error_to_remove) end end end end end end end diff --git a/test/lib/spec/expectations.rb b/spec/lib/spec/expectations.rb similarity index 95% rename from test/lib/spec/expectations.rb rename to spec/lib/spec/expectations.rb index cc58bba15..65ea47425 100644 --- a/test/lib/spec/expectations.rb +++ b/spec/lib/spec/expectations.rb @@ -1,59 +1,56 @@ -require 'spec/deprecated' require 'spec/matchers' -require 'spec/expectations/sugar' require 'spec/expectations/errors' require 'spec/expectations/extensions' -require 'spec/expectations/should' require 'spec/expectations/handler' module Spec # Spec::Expectations lets you set expectations on your objects. # # result.should == 37 # team.should have(11).players_on_the_field # # == How Expectations work. # # Spec::Expectations adds two methods to Object: # # should(matcher=nil) # should_not(matcher=nil) # # Both methods take an optional Expression Matcher (See Spec::Matchers). # # When +should+ receives an Expression Matcher, it calls matches?(self). If # it returns +true+, the spec passes and execution continues. If it returns # +false+, then the spec fails with the message returned by matcher.failure_message. # # Similarly, when +should_not+ receives a matcher, it calls matches?(self). If # it returns +false+, the spec passes and execution continues. If it returns # +true+, then the spec fails with the message returned by matcher.negative_failure_message. # # RSpec ships with a standard set of useful matchers, and writing your own # matchers is quite simple. See Spec::Matchers for details. module Expectations class << self attr_accessor :differ # raises a Spec::Expectations::ExpectationNotMetError with message # # When a differ has been assigned and fail_with is passed # expected and target, passes them # to the differ to append a diff message to the failure message. def fail_with(message, expected=nil, target=nil) # :nodoc: if Array === message && message.length == 3 message, expected, target = message[0], message[1], message[2] end unless (differ.nil? || expected.nil? || target.nil?) if expected.is_a?(String) message << "\nDiff:" << self.differ.diff_as_string(target.to_s, expected) elsif !target.is_a?(Proc) message << "\nDiff:" << self.differ.diff_as_object(target, expected) end end Kernel::raise(Spec::Expectations::ExpectationNotMetError.new(message)) end end end -end \ No newline at end of file +end diff --git a/test/lib/spec/expectations/differs/default.rb b/spec/lib/spec/expectations/differs/default.rb similarity index 98% rename from test/lib/spec/expectations/differs/default.rb rename to spec/lib/spec/expectations/differs/default.rb index e08325728..87e59b3a6 100644 --- a/test/lib/spec/expectations/differs/default.rb +++ b/spec/lib/spec/expectations/differs/default.rb @@ -1,62 +1,61 @@ begin require 'rubygems' require 'diff/lcs' #necessary due to loading bug on some machines - not sure why - DaC require 'diff/lcs/hunk' rescue LoadError ; raise "You must gem install diff-lcs to use diffing" ; end require 'pp' module Spec module Expectations module Differs - # TODO add colour support # TODO add some rdoc class Default def initialize(format=:unified,context_lines=nil,colour=nil) context_lines ||= 3 colour ||= false @format,@context_lines,@colour = format,context_lines,colour end # This is snagged from diff/lcs/ldiff.rb (which is a commandline tool) def diff_as_string(data_old, data_new) data_old = data_old.split(/\n/).map! { |e| e.chomp } data_new = data_new.split(/\n/).map! { |e| e.chomp } output = "" diffs = Diff::LCS.diff(data_old, data_new) return output if diffs.empty? oldhunk = hunk = nil file_length_difference = 0 diffs.each do |piece| begin hunk = Diff::LCS::Hunk.new(data_old, data_new, piece, @context_lines, file_length_difference) file_length_difference = hunk.file_length_difference next unless oldhunk # Hunks may overlap, which is why we need to be careful when our # diff includes lines of context. Otherwise, we might print # redundant lines. if (@context_lines > 0) and hunk.overlaps?(oldhunk) hunk.unshift(oldhunk) else output << oldhunk.diff(@format) end ensure oldhunk = hunk output << "\n" end end #Handle the last remaining hunk output << oldhunk.diff(@format) << "\n" end def diff_as_object(target,expected) diff_as_string(PP.pp(target,""), PP.pp(expected,"")) end end end end end diff --git a/test/lib/spec/expectations/errors.rb b/spec/lib/spec/expectations/errors.rb similarity index 100% rename from test/lib/spec/expectations/errors.rb rename to spec/lib/spec/expectations/errors.rb diff --git a/test/lib/spec/expectations/extensions.rb b/spec/lib/spec/expectations/extensions.rb similarity index 70% rename from test/lib/spec/expectations/extensions.rb rename to spec/lib/spec/expectations/extensions.rb index 0381dc7f3..60c9b9e7d 100644 --- a/test/lib/spec/expectations/extensions.rb +++ b/spec/lib/spec/expectations/extensions.rb @@ -1,3 +1,2 @@ require 'spec/expectations/extensions/object' -require 'spec/expectations/extensions/proc' require 'spec/expectations/extensions/string_and_symbol' diff --git a/spec/lib/spec/expectations/extensions/object.rb b/spec/lib/spec/expectations/extensions/object.rb new file mode 100644 index 000000000..f59af722e --- /dev/null +++ b/spec/lib/spec/expectations/extensions/object.rb @@ -0,0 +1,66 @@ +module Spec + module Expectations + # rspec adds #should and #should_not to every Object (and, + # implicitly, every Class). + module ObjectExpectations + + # :call-seq: + # should(matcher) + # should == expected + # should === expected + # should =~ expected + # + # receiver.should(matcher) + # => Passes if matcher.matches?(receiver) + # + # receiver.should == expected #any value + # => Passes if (receiver == expected) + # + # receiver.should === expected #any value + # => Passes if (receiver === expected) + # + # receiver.should =~ regexp + # => Passes if (receiver =~ regexp) + # + # See Spec::Matchers for more information about matchers + # + # == Warning + # + # NOTE that this does NOT support receiver.should != expected. + # Instead, use receiver.should_not == expected + def should(matcher=nil, &block) + return ExpectationMatcherHandler.handle_matcher(self, matcher, &block) if matcher + Spec::Matchers::PositiveOperatorMatcher.new(self) + end + + # :call-seq: + # should_not(matcher) + # should_not == expected + # should_not === expected + # should_not =~ expected + # + # receiver.should_not(matcher) + # => Passes unless matcher.matches?(receiver) + # + # receiver.should_not == expected + # => Passes unless (receiver == expected) + # + # receiver.should_not === expected + # => Passes unless (receiver === expected) + # + # receiver.should_not =~ regexp + # => Passes unless (receiver =~ regexp) + # + # See Spec::Matchers for more information about matchers + def should_not(matcher=nil, &block) + return NegativeExpectationMatcherHandler.handle_matcher(self, matcher, &block) if matcher + Spec::Matchers::NegativeOperatorMatcher.new(self) + end + + end + end +end + +class Object + include Spec::Expectations::ObjectExpectations +end diff --git a/test/lib/spec/expectations/extensions/string_and_symbol.rb b/spec/lib/spec/expectations/extensions/string_and_symbol.rb similarity index 79% rename from test/lib/spec/expectations/extensions/string_and_symbol.rb rename to spec/lib/spec/expectations/extensions/string_and_symbol.rb index 30f60d4d0..29cfbddfa 100644 --- a/test/lib/spec/expectations/extensions/string_and_symbol.rb +++ b/spec/lib/spec/expectations/extensions/string_and_symbol.rb @@ -1,17 +1,17 @@ module Spec module Expectations module StringHelpers def starts_with?(prefix) - to_s[0..(prefix.length - 1)] == prefix + to_s[0..(prefix.to_s.length - 1)] == prefix.to_s end end end end class String include Spec::Expectations::StringHelpers end class Symbol include Spec::Expectations::StringHelpers -end \ No newline at end of file +end diff --git a/spec/lib/spec/expectations/handler.rb b/spec/lib/spec/expectations/handler.rb new file mode 100644 index 000000000..4caa321e4 --- /dev/null +++ b/spec/lib/spec/expectations/handler.rb @@ -0,0 +1,43 @@ +module Spec + module Expectations + + module MatcherHandlerHelper + def describe(matcher) + matcher.respond_to?(:description) ? matcher.description : "[#{matcher.class.name} does not provide a description]" + end + end + + class ExpectationMatcherHandler + class << self + include MatcherHandlerHelper + def handle_matcher(actual, matcher, &block) + match = matcher.matches?(actual, &block) + ::Spec::Matchers.generated_description = "should #{describe(matcher)}" + Spec::Expectations.fail_with(matcher.failure_message) unless match + end + end + end + + class NegativeExpectationMatcherHandler + class << self + include MatcherHandlerHelper + def handle_matcher(actual, matcher, &block) + unless matcher.respond_to?(:negative_failure_message) + Spec::Expectations.fail_with( +<<-EOF +Matcher does not support should_not. +See Spec::Matchers for more information +about matchers. +EOF +) + end + match = matcher.matches?(actual, &block) + ::Spec::Matchers.generated_description = "should not #{describe(matcher)}" + Spec::Expectations.fail_with(matcher.negative_failure_message) if match + end + end + end + + end +end + diff --git a/spec/lib/spec/extensions.rb b/spec/lib/spec/extensions.rb new file mode 100755 index 000000000..824f03bfb --- /dev/null +++ b/spec/lib/spec/extensions.rb @@ -0,0 +1 @@ +require 'spec/extensions/object' diff --git a/spec/lib/spec/extensions/object.rb b/spec/lib/spec/extensions/object.rb new file mode 100755 index 000000000..6218aa770 --- /dev/null +++ b/spec/lib/spec/extensions/object.rb @@ -0,0 +1,6 @@ +class Object + def args_and_options(*args) + options = Hash === args.last ? args.pop : {} + return args, options + end +end diff --git a/test/lib/spec/matchers.rb b/spec/lib/spec/matchers.rb similarity index 78% rename from test/lib/spec/matchers.rb rename to spec/lib/spec/matchers.rb index 9db24d486..fd208d628 100644 --- a/test/lib/spec/matchers.rb +++ b/spec/lib/spec/matchers.rb @@ -1,160 +1,166 @@ -require 'spec/deprecated' -require 'spec/callback' require 'spec/matchers/be' require 'spec/matchers/be_close' require 'spec/matchers/change' require 'spec/matchers/eql' require 'spec/matchers/equal' require 'spec/matchers/has' require 'spec/matchers/have' require 'spec/matchers/include' require 'spec/matchers/match' require 'spec/matchers/raise_error' require 'spec/matchers/respond_to' require 'spec/matchers/satisfy' require 'spec/matchers/throw_symbol' +require 'spec/matchers/operator_matcher' module Spec # RSpec ships with a number of useful Expression Matchers. An Expression Matcher # is any object that responds to the following methods: # # matches?(actual) # failure_message # negative_failure_message #optional # description #optional # # See Spec::Expectations to learn how to use these as Expectation Matchers. # See Spec::Mocks to learn how to use them as Mock Argument Constraints. # # == Predicates # # In addition to those Expression Matchers that are defined explicitly, RSpec will # create custom Matchers on the fly for any arbitrary predicate, giving your specs # a much more natural language feel. # # A Ruby predicate is a method that ends with a "?" and returns true or false. # Common examples are +empty?+, +nil?+, and +instance_of?+. # # All you need to do is write +should be_+ followed by the predicate without # the question mark, and RSpec will figure it out from there. For example: # # [].should be_empty => [].empty? #passes # [].should_not be_empty => [].empty? #fails # # In addtion to prefixing the predicate matchers with "be_", you can also use "be_a_" # and "be_an_", making your specs read much more naturally: # # "a string".should be_an_instance_of(String) =>"a string".instance_of?(String) #passes # # 3.should be_a_kind_of(Fixnum) => 3.kind_of?(Numeric) #passes # 3.should be_a_kind_of(Numeric) => 3.kind_of?(Numeric) #passes # 3.should be_an_instance_of(Fixnum) => 3.instance_of?(Fixnum) #passes # 3.should_not be_instance_of(Numeric) => 3.instance_of?(Numeric) #fails # # RSpec will also create custom matchers for predicates like +has_key?+. To # use this feature, just state that the object should have_key(:key) and RSpec will # call has_key?(:key) on the target. For example: # # {:a => "A"}.should have_key(:a) => {:a => "A"}.has_key?(:a) #passes # {:a => "A"}.should have_key(:b) => {:a => "A"}.has_key?(:b) #fails # # You can use this feature to invoke any predicate that begins with "has_", whether it is # part of the Ruby libraries (like +Hash#has_key?+) or a method you wrote on your own class. # - # == Custom Expression Matchers + # == Custom Expectation Matchers # - # When you find that none of the stock Expression Matchers provide a natural + # When you find that none of the stock Expectation Matchers provide a natural # feeling expectation, you can very easily write your own. # # For example, imagine that you are writing a game in which players can # be in various zones on a virtual board. To specify that bob should # be in zone 4, you could say: # # bob.current_zone.should eql(Zone.new("4")) # # But you might find it more expressive to say: # # bob.should be_in_zone("4") # # and/or # # bob.should_not be_in_zone("3") # # To do this, you would need to write a class like this: # # class BeInZone # def initialize(expected) # @expected = expected # end - # def matches?(actual) - # @actual = actual - # bob.current_zone.eql?(Zone.new(@expected)) + # def matches?(target) + # @target = target + # @target.current_zone.eql?(Zone.new(@expected)) # end # def failure_message - # "expected #{@actual.inspect} to be in Zone #{@expected}" + # "expected #{@target.inspect} to be in Zone #{@expected}" # end # def negative_failure_message - # "expected #{@actual.inspect} not to be in Zone #{@expected}" + # "expected #{@target.inspect} not to be in Zone #{@expected}" # end # end # # ... and a method like this: # # def be_in_zone(expected) # BeInZone.new(expected) # end # # And then expose the method to your specs. This is normally done # by including the method and the class in a module, which is then # included in your spec: # # module CustomGameMatchers # class BeInZone # ... # end # # def be_in_zone(expected) # ... # end # end # - # context "Player behaviour" do + # describe "Player behaviour" do # include CustomGameMatchers # ... # end + # + # or you can include in globally in a spec_helper.rb file required + # from your spec file(s): + # + # Spec::Runner.configure do |config| + # config.include(CustomGameMatchers) + # end + # module Matchers - - class << self - callback_events :description_generated + module ModuleMethods + def description_generated(callback) + description_generated_callbacks << callback + end + + def unregister_description_generated(callback) + description_generated_callbacks.delete(callback) + end + def generated_description=(name) - notify_callbacks(:description_generated, name) + description_generated_callbacks.each do |callback| + callback.call(name) + end + end + + private + def description_generated_callbacks + @description_generated_callbacks ||= [] end end + extend ModuleMethods def method_missing(sym, *args, &block) # :nodoc: return Matchers::Be.new(sym, *args) if sym.starts_with?("be_") return Matchers::Has.new(sym, *args) if sym.starts_with?("have_") super end - deprecated do - # This supports sugar delegating to Matchers - class Matcher #:nodoc: - include Matchers - - def respond_to?(sym) - if sym.to_s[0..2] == "be_" - return true - else - super - end - end - end - end - class MatcherError < StandardError end end -end \ No newline at end of file +end diff --git a/test/lib/spec/matchers/be.rb b/spec/lib/spec/matchers/be.rb similarity index 76% rename from test/lib/spec/matchers/be.rb rename to spec/lib/spec/matchers/be.rb index 957f23de8..0eb1629a6 100644 --- a/test/lib/spec/matchers/be.rb +++ b/spec/lib/spec/matchers/be.rb @@ -1,161 +1,206 @@ module Spec module Matchers class Be #:nodoc: - def initialize(expected=nil, *args) - @expected = parse_expected(expected) + def initialize(*args) + @expected = parse_expected(args.shift) @args = args @comparison = "" end def matches?(actual) @actual = actual return true if match_or_compare unless handling_predicate? if handling_predicate? begin return @result = actual.__send__(predicate, *@args) rescue => predicate_error # This clause should be empty, but rcov will not report it as covered # unless something (anything) is executed within the clause rcov_error_report = "http://eigenclass.org/hiki.rb?rcov-0.8.0" end # This supports should_exist > target.exists? in the old world. # We should consider deprecating that ability as in the new world # you can't write "should exist" unless you have your own custom matcher. begin return @result = actual.__send__(present_tense_predicate, *@args) rescue raise predicate_error end end return false end def failure_message return "expected #{@comparison}#{expected}, got #{@actual.inspect}" unless handling_predicate? return "expected #{predicate}#{args_to_s} to return true, got #{@result.inspect}" end def negative_failure_message return "expected not #{expected}, got #{@actual.inspect}" unless handling_predicate? return "expected #{predicate}#{args_to_s} to return false, got #{@result.inspect}" end def expected return true if @expected == :true return false if @expected == :false return "nil" if @expected == :nil return @expected.inspect end def match_or_compare return @actual == true if @expected == :true return @actual == false if @expected == :false return @actual.nil? if @expected == :nil return @actual < @expected if @less_than return @actual <= @expected if @less_than_or_equal return @actual >= @expected if @greater_than_or_equal return @actual > @expected if @greater_than + return @actual == @expected if @double_equal + return @actual === @expected if @triple_equal return @actual.equal?(@expected) end + + def ==(expected) + @double_equal = true + @comparison = "== " + @expected = expected + self + end + + def ===(expected) + @triple_equal = true + @comparison = "=== " + @expected = expected + self + end def <(expected) @less_than = true @comparison = "< " @expected = expected self end def <=(expected) @less_than_or_equal = true @comparison = "<= " @expected = expected self end def >=(expected) @greater_than_or_equal = true @comparison = ">= " @expected = expected self end def >(expected) @greater_than = true @comparison = "> " @expected = expected self end def description - "be #{@comparison}#{@expected}" + "#{prefix_to_sentence}#{comparison}#{expected_to_sentence}#{args_to_sentence}" end private def parse_expected(expected) if Symbol === expected - ["be_an_","be_a_","be_"].each do |prefix| - @handling_predicate = true - return "#{expected.to_s.sub(prefix,"")}".to_sym if expected.starts_with?(prefix) + @handling_predicate = true + ["be_an_","be_a_","be_"].each do |@prefix| + return "#{expected.to_s.sub(@prefix,"")}".to_sym if expected.starts_with?(@prefix) end end + @prefix = "be " return expected end + + def handling_predicate? + return false if [:true, :false, :nil].include?(@expected) + return @handling_predicate + end def predicate "#{@expected.to_s}?".to_sym end def present_tense_predicate "#{@expected.to_s}s?".to_sym end def args_to_s return "" if @args.empty? - transformed_args = @args.collect{|a| a.inspect} - return "(#{transformed_args.join(', ')})" + inspected_args = @args.collect{|a| a.inspect} + return "(#{inspected_args.join(', ')})" end - def handling_predicate? - return false if [:true, :false, :nil].include?(@expected) - return @handling_predicate + def comparison + @comparison + end + + def expected_to_sentence + split_words(@expected) + end + + def prefix_to_sentence + split_words(@prefix) end + + def split_words(sym) + sym.to_s.gsub(/_/,' ') + end + + def args_to_sentence + case @args.length + when 0 + "" + when 1 + " #{@args[0]}" + else + " #{@args[0...-1].join(', ')} and #{@args[-1]}" + end + end + end # :call-seq: # should be_true # should be_false # should be_nil # should be_arbitrary_predicate(*args) # should_not be_nil # should_not be_arbitrary_predicate(*args) # # Given true, false, or nil, will pass if actual is # true, false or nil (respectively). # # Predicates are any Ruby method that ends in a "?" and returns true or false. # Given be_ followed by arbitrary_predicate (without the "?"), RSpec will match # convert that into a query against the target object. # # The arbitrary_predicate feature will handle any predicate # prefixed with "be_an_" (e.g. be_an_instance_of), "be_a_" (e.g. be_a_kind_of) # or "be_" (e.g. be_empty), letting you choose the prefix that best suits the predicate. # # == Examples # # target.should be_true # target.should be_false # target.should be_nil # target.should_not be_nil # # collection.should be_empty #passes if target.empty? # "this string".should be_an_intance_of(String) # # target.should_not be_empty #passes unless target.empty? # target.should_not be_old_enough(16) #passes unless target.old_enough?(16) def be(*args) Matchers::Be.new(*args) end end end diff --git a/test/lib/spec/matchers/be_close.rb b/spec/lib/spec/matchers/be_close.rb similarity index 84% rename from test/lib/spec/matchers/be_close.rb rename to spec/lib/spec/matchers/be_close.rb index b09e3fd2f..7763eb97e 100644 --- a/test/lib/spec/matchers/be_close.rb +++ b/spec/lib/spec/matchers/be_close.rb @@ -1,37 +1,37 @@ module Spec module Matchers class BeClose #:nodoc: def initialize(expected, delta) @expected = expected @delta = delta end def matches?(actual) @actual = actual (@actual - @expected).abs < @delta end def failure_message - "expected #{@expected} +/- (<#{@delta}), got #{@actual}" + "expected #{@expected} +/- (< #{@delta}), got #{@actual}" end def description - "be close to #{@expected} (+- #{@delta})" + "be close to #{@expected} (within +- #{@delta})" end end # :call-seq: # should be_close(expected, delta) # should_not be_close(expected, delta) # # Passes if actual == expected +/- delta # # == Example # # result.should be_close(3.0, 0.5) def be_close(expected, delta) Matchers::BeClose.new(expected, delta) end end -end \ No newline at end of file +end diff --git a/test/lib/spec/matchers/change.rb b/spec/lib/spec/matchers/change.rb similarity index 100% rename from test/lib/spec/matchers/change.rb rename to spec/lib/spec/matchers/change.rb diff --git a/test/lib/spec/matchers/eql.rb b/spec/lib/spec/matchers/eql.rb similarity index 99% rename from test/lib/spec/matchers/eql.rb rename to spec/lib/spec/matchers/eql.rb index caca1f7c6..280ca5454 100644 --- a/test/lib/spec/matchers/eql.rb +++ b/spec/lib/spec/matchers/eql.rb @@ -1,43 +1,43 @@ module Spec module Matchers class Eql #:nodoc: def initialize(expected) @expected = expected end def matches?(actual) @actual = actual @actual.eql?(@expected) end def failure_message return "expected #{@expected.inspect}, got #{@actual.inspect} (using .eql?)", @expected, @actual end def negative_failure_message return "expected #{@actual.inspect} not to equal #{@expected.inspect} (using .eql?)", @expected, @actual end def description "eql #{@expected.inspect}" end end # :call-seq: # should eql(expected) # should_not eql(expected) # # Passes if actual and expected are of equal value, but not necessarily the same object. # # See http://www.ruby-doc.org/core/classes/Object.html#M001057 for more information about equality in Ruby. # # == Examples # # 5.should eql(5) # 5.should_not eql(3) def eql(expected) Matchers::Eql.new(expected) end end -end \ No newline at end of file +end diff --git a/test/lib/spec/matchers/equal.rb b/spec/lib/spec/matchers/equal.rb similarity index 99% rename from test/lib/spec/matchers/equal.rb rename to spec/lib/spec/matchers/equal.rb index e987e73cb..4bfc74951 100644 --- a/test/lib/spec/matchers/equal.rb +++ b/spec/lib/spec/matchers/equal.rb @@ -1,43 +1,43 @@ module Spec module Matchers class Equal #:nodoc: def initialize(expected) @expected = expected end def matches?(actual) @actual = actual @actual.equal?(@expected) end def failure_message return "expected #{@expected.inspect}, got #{@actual.inspect} (using .equal?)", @expected, @actual end def negative_failure_message return "expected #{@actual.inspect} not to equal #{@expected.inspect} (using .equal?)", @expected, @actual end def description "equal #{@expected.inspect}" end end # :call-seq: # should equal(expected) # should_not equal(expected) # # Passes if actual and expected are the same object (object identity). # # See http://www.ruby-doc.org/core/classes/Object.html#M001057 for more information about equality in Ruby. # # == Examples # # 5.should equal(5) #Fixnums are equal # "5".should_not equal("5") #Strings that look the same are not the same object def equal(expected) Matchers::Equal.new(expected) end end -end \ No newline at end of file +end diff --git a/test/lib/spec/matchers/has.rb b/spec/lib/spec/matchers/has.rb similarity index 100% rename from test/lib/spec/matchers/has.rb rename to spec/lib/spec/matchers/has.rb diff --git a/test/lib/spec/matchers/have.rb b/spec/lib/spec/matchers/have.rb similarity index 80% rename from test/lib/spec/matchers/have.rb rename to spec/lib/spec/matchers/have.rb index 81f9af3e3..f28b86ad3 100644 --- a/test/lib/spec/matchers/have.rb +++ b/spec/lib/spec/matchers/have.rb @@ -1,140 +1,142 @@ module Spec module Matchers class Have #:nodoc: def initialize(expected, relativity=:exactly) @expected = (expected == :no ? 0 : expected) @relativity = relativity end def relativities @relativities ||= { :exactly => "", :at_least => "at least ", :at_most => "at most " } end def method_missing(sym, *args, &block) @collection_name = sym @args = args @block = block self end def matches?(collection_owner) - if collection_owner.respond_to?(collection_name) - collection = collection_owner.send(collection_name, *@args, &@block) + if collection_owner.respond_to?(@collection_name) + collection = collection_owner.send(@collection_name, *@args, &@block) elsif (collection_owner.respond_to?(:length) || collection_owner.respond_to?(:size)) collection = collection_owner else - collection_owner.send(collection_name, *@args, &@block) + collection_owner.send(@collection_name, *@args, &@block) end - @actual = collection.length if collection.respond_to?(:length) @actual = collection.size if collection.respond_to?(:size) + @actual = collection.length if collection.respond_to?(:length) + raise not_a_collection if @actual.nil? return @actual >= @expected if @relativity == :at_least return @actual <= @expected if @relativity == :at_most return @actual == @expected end + + def not_a_collection + "expected #{@collection_name} to be a collection but it does not respond to #length or #size" + end def failure_message - "expected #{relative_expectation} #{collection_name}, got #{@actual}" + "expected #{relative_expectation} #{@collection_name}, got #{@actual}" end def negative_failure_message if @relativity == :exactly - return "expected target not to have #{@expected} #{collection_name}, got #{@actual}" + return "expected target not to have #{@expected} #{@collection_name}, got #{@actual}" elsif @relativity == :at_most return <<-EOF Isn't life confusing enough? Instead of having to figure out the meaning of this: - should_not have_at_most(#{@expected}).#{collection_name} + should_not have_at_most(#{@expected}).#{@collection_name} We recommend that you use this instead: - should have_at_least(#{@expected + 1}).#{collection_name} + should have_at_least(#{@expected + 1}).#{@collection_name} EOF elsif @relativity == :at_least return <<-EOF Isn't life confusing enough? Instead of having to figure out the meaning of this: - should_not have_at_least(#{@expected}).#{collection_name} + should_not have_at_least(#{@expected}).#{@collection_name} We recommend that you use this instead: - should have_at_most(#{@expected - 1}).#{collection_name} + should have_at_most(#{@expected - 1}).#{@collection_name} EOF end end def description - "have #{relative_expectation} #{collection_name}" + "have #{relative_expectation} #{@collection_name}" end private - def collection_name - @collection_name - end def relative_expectation "#{relativities[@relativity]}#{@expected}" end end # :call-seq: # should have(number).named_collection__or__sugar # should_not have(number).named_collection__or__sugar # # Passes if receiver is a collection with the submitted # number of items OR if the receiver OWNS a collection # with the submitted number of items. # # If the receiver OWNS the collection, you must use the name # of the collection. So if a Team instance has a # collection named #players, you must use that name # to set the expectation. # # If the receiver IS the collection, you can use any name # you like for named_collection. We'd recommend using # either "elements", "members", or "items" as these are all # standard ways of describing the things IN a collection. # # This also works for Strings, letting you set an expectation # about its length # # == Examples # # # Passes if team.players.size == 11 # team.should have(11).players # # # Passes if [1,2,3].length == 3 # [1,2,3].should have(3).items #"items" is pure sugar # # # Passes if "this string".length == 11 # "this string".should have(11).characters #"characters" is pure sugar def have(n) Matchers::Have.new(n) end alias :have_exactly :have # :call-seq: # should have_at_least(number).items # # Exactly like have() with >=. # # == Warning # # +should_not+ +have_at_least+ is not supported def have_at_least(n) Matchers::Have.new(n, :at_least) end # :call-seq: # should have_at_most(number).items # # Exactly like have() with <=. # # == Warning # # +should_not+ +have_at_most+ is not supported def have_at_most(n) Matchers::Have.new(n, :at_most) end end -end \ No newline at end of file +end diff --git a/spec/lib/spec/matchers/include.rb b/spec/lib/spec/matchers/include.rb new file mode 100644 index 000000000..5476f97d8 --- /dev/null +++ b/spec/lib/spec/matchers/include.rb @@ -0,0 +1,70 @@ +module Spec + module Matchers + + class Include #:nodoc: + + def initialize(*expecteds) + @expecteds = expecteds + end + + def matches?(actual) + @actual = actual + @expecteds.each do |expected| + return false unless actual.include?(expected) + end + true + end + + def failure_message + _message + end + + def negative_failure_message + _message("not ") + end + + def description + "include #{_pretty_print(@expecteds)}" + end + + private + def _message(maybe_not="") + "expected #{@actual.inspect} #{maybe_not}to include #{_pretty_print(@expecteds)}" + end + + def _pretty_print(array) + result = "" + array.each_with_index do |item, index| + if index < (array.length - 2) + result << "#{item.inspect}, " + elsif index < (array.length - 1) + result << "#{item.inspect} and " + else + result << "#{item.inspect}" + end + end + result + end + end + + # :call-seq: + # should include(expected) + # should_not include(expected) + # + # Passes if actual includes expected. This works for + # collections and Strings. You can also pass in multiple args + # and it will only pass if all args are found in collection. + # + # == Examples + # + # [1,2,3].should include(3) + # [1,2,3].should include(2,3) #would pass + # [1,2,3].should include(2,3,4) #would fail + # [1,2,3].should_not include(4) + # "spread".should include("read") + # "spread".should_not include("red") + def include(*expected) + Matchers::Include.new(*expected) + end + end +end diff --git a/test/lib/spec/matchers/match.rb b/spec/lib/spec/matchers/match.rb similarity index 100% rename from test/lib/spec/matchers/match.rb rename to spec/lib/spec/matchers/match.rb diff --git a/spec/lib/spec/matchers/operator_matcher.rb b/spec/lib/spec/matchers/operator_matcher.rb new file mode 100755 index 000000000..2d47ea85a --- /dev/null +++ b/spec/lib/spec/matchers/operator_matcher.rb @@ -0,0 +1,72 @@ +module Spec + module Matchers + class BaseOperatorMatcher + + def initialize(target) + @target = target + end + + def ==(expected) + @expected = expected + __delegate_method_missing_to_target("==", expected) + end + + def ===(expected) + @expected = expected + __delegate_method_missing_to_target("===", expected) + end + + def =~(expected) + @expected = expected + __delegate_method_missing_to_target("=~", expected) + end + + def >(expected) + @expected = expected + __delegate_method_missing_to_target(">", expected) + end + + def >=(expected) + @expected = expected + __delegate_method_missing_to_target(">=", expected) + end + + def <(expected) + @expected = expected + __delegate_method_missing_to_target("<", expected) + end + + def <=(expected) + @expected = expected + __delegate_method_missing_to_target("<=", expected) + end + + def fail_with_message(message) + Spec::Expectations.fail_with(message, @expected, @target) + end + + end + + class PositiveOperatorMatcher < BaseOperatorMatcher #:nodoc: + + def __delegate_method_missing_to_target(operator, expected) + ::Spec::Matchers.generated_description = "should #{operator} #{expected.inspect}" + return if @target.send(operator, expected) + return fail_with_message("expected: #{expected.inspect},\n got: #{@target.inspect} (using #{operator})") if ['==','===', '=~'].include?(operator) + return fail_with_message("expected: #{operator} #{expected.inspect},\n got: #{operator.gsub(/./, ' ')} #{@target.inspect}") + end + + end + + class NegativeOperatorMatcher < BaseOperatorMatcher #:nodoc: + + def __delegate_method_missing_to_target(operator, expected) + ::Spec::Matchers.generated_description = "should not #{operator} #{expected.inspect}" + return unless @target.send(operator, expected) + return fail_with_message("expected not: #{operator} #{expected.inspect},\n got: #{operator.gsub(/./, ' ')} #{@target.inspect}") + end + + end + + end +end diff --git a/test/lib/spec/matchers/raise_error.rb b/spec/lib/spec/matchers/raise_error.rb similarity index 90% rename from test/lib/spec/matchers/raise_error.rb rename to spec/lib/spec/matchers/raise_error.rb index 95e82ad5e..b45dcf65c 100644 --- a/test/lib/spec/matchers/raise_error.rb +++ b/spec/lib/spec/matchers/raise_error.rb @@ -1,100 +1,105 @@ module Spec module Matchers class RaiseError #:nodoc: - def initialize(exception=Exception, message=nil) - @expected_error = exception - @expected_message = message + def initialize(error_or_message=Exception, message=nil) + if String === error_or_message + @expected_error = Exception + @expected_message = error_or_message + else + @expected_error = error_or_message + @expected_message = message + end end def matches?(proc) @raised_expected_error = false @raised_other = false begin proc.call rescue @expected_error => @actual_error if @expected_message.nil? @raised_expected_error = true else case @expected_message when Regexp if @expected_message =~ @actual_error.message @raised_expected_error = true else @raised_other = true end else - if @actual_error.message == @expected_message + if @expected_message == @actual_error.message @raised_expected_error = true else @raised_other = true end end end rescue => @actual_error @raised_other = true ensure return @raised_expected_error end end def failure_message return "expected #{expected_error}#{actual_error}" if @raised_other || !@raised_expected_error end def negative_failure_message "expected no #{expected_error}#{actual_error}" end def description "raise #{expected_error}" end private def expected_error case @expected_message when nil @expected_error when Regexp "#{@expected_error} with message matching #{@expected_message.inspect}" else "#{@expected_error} with #{@expected_message.inspect}" end end def actual_error @actual_error.nil? ? " but nothing was raised" : ", got #{@actual_error.inspect}" end end # :call-seq: # should raise_error() # should raise_error(NamedError) # should raise_error(NamedError, String) # should raise_error(NamedError, Regexp) # should_not raise_error() # should_not raise_error(NamedError) # should_not raise_error(NamedError, String) # should_not raise_error(NamedError, Regexp) # # With no args, matches if any error is raised. # With a named error, matches only if that specific error is raised. # With a named error and messsage specified as a String, matches only if both match. # With a named error and messsage specified as a Regexp, matches only if both match. # # == Examples # # lambda { do_something_risky }.should raise_error # lambda { do_something_risky }.should raise_error(PoorRiskDecisionError) # lambda { do_something_risky }.should raise_error(PoorRiskDecisionError, "that was too risky") # lambda { do_something_risky }.should raise_error(PoorRiskDecisionError, /oo ri/) # # lambda { do_something_risky }.should_not raise_error # lambda { do_something_risky }.should_not raise_error(PoorRiskDecisionError) # lambda { do_something_risky }.should_not raise_error(PoorRiskDecisionError, "that was too risky") # lambda { do_something_risky }.should_not raise_error(PoorRiskDecisionError, /oo ri/) def raise_error(error=Exception, message=nil) Matchers::RaiseError.new(error, message) end end end diff --git a/spec/lib/spec/matchers/respond_to.rb b/spec/lib/spec/matchers/respond_to.rb new file mode 100644 index 000000000..3d23422aa --- /dev/null +++ b/spec/lib/spec/matchers/respond_to.rb @@ -0,0 +1,45 @@ +module Spec + module Matchers + + class RespondTo #:nodoc: + def initialize(*names) + @names = names + @names_not_responded_to = [] + end + + def matches?(target) + @names.each do |name| + unless target.respond_to?(name) + @names_not_responded_to << name + end + end + return @names_not_responded_to.empty? + end + + def failure_message + "expected target to respond to #{@names_not_responded_to.collect {|name| name.inspect }.join(', ')}" + end + + def negative_failure_message + "expected target not to respond to #{@names.collect {|name| name.inspect }.join(', ')}" + end + + def description + "respond to ##{@names.to_s}" + end + end + + # :call-seq: + # should respond_to(*names) + # should_not respond_to(*names) + # + # Matches if the target object responds to all of the names + # provided. Names can be Strings or Symbols. + # + # == Examples + # + def respond_to(*names) + Matchers::RespondTo.new(*names) + end + end +end diff --git a/test/lib/spec/matchers/satisfy.rb b/spec/lib/spec/matchers/satisfy.rb similarity index 100% rename from test/lib/spec/matchers/satisfy.rb rename to spec/lib/spec/matchers/satisfy.rb diff --git a/test/lib/spec/matchers/throw_symbol.rb b/spec/lib/spec/matchers/throw_symbol.rb similarity index 90% rename from test/lib/spec/matchers/throw_symbol.rb rename to spec/lib/spec/matchers/throw_symbol.rb index 6732f6fed..6d047bc39 100644 --- a/test/lib/spec/matchers/throw_symbol.rb +++ b/spec/lib/spec/matchers/throw_symbol.rb @@ -1,75 +1,72 @@ module Spec module Matchers class ThrowSymbol #:nodoc: def initialize(expected=nil) @expected = expected end def matches?(proc) begin proc.call rescue NameError => e - @actual = extract_sym_from_name_error(e) + @actual = e.name.to_sym ensure if @expected.nil? return @actual.nil? ? false : true else return @actual == @expected end end end def failure_message if @actual "expected #{expected}, got #{@actual.inspect}" else "expected #{expected} but nothing was thrown" end end def negative_failure_message if @expected "expected #{expected} not to be thrown" else "expected no Symbol, got :#{@actual}" end end def description "throw #{expected}" end private def expected @expected.nil? ? "a Symbol" : @expected.inspect end - def extract_sym_from_name_error(error) - return "#{error.message.split("`").last.split("'").first}".to_sym - end end # :call-seq: # should throw_symbol() # should throw_symbol(:sym) # should_not throw_symbol() # should_not throw_symbol(:sym) # # Given a Symbol argument, matches if a proc throws the specified Symbol. # # Given no argument, matches if a proc throws any Symbol. # # == Examples # # lambda { do_something_risky }.should throw_symbol # lambda { do_something_risky }.should throw_symbol(:that_was_risky) # # lambda { do_something_risky }.should_not throw_symbol # lambda { do_something_risky }.should_not throw_symbol(:that_was_risky) def throw_symbol(sym=nil) Matchers::ThrowSymbol.new(sym) end end end diff --git a/test/lib/spec/mocks.rb b/spec/lib/spec/mocks.rb similarity index 86% rename from test/lib/spec/mocks.rb rename to spec/lib/spec/mocks.rb index d0a5d0299..66cbafb3c 100644 --- a/test/lib/spec/mocks.rb +++ b/spec/lib/spec/mocks.rb @@ -1,232 +1,208 @@ require 'spec/mocks/methods' -require 'spec/mocks/mock_handler' +require 'spec/mocks/argument_constraint_matchers' +require 'spec/mocks/spec_methods' +require 'spec/mocks/proxy' require 'spec/mocks/mock' require 'spec/mocks/argument_expectation' require 'spec/mocks/message_expectation' require 'spec/mocks/order_group' require 'spec/mocks/errors' require 'spec/mocks/error_generator' require 'spec/mocks/extensions/object' +require 'spec/mocks/space' + module Spec # == Mocks and Stubs # # RSpec will create Mock Objects and Stubs for you at runtime, or attach stub/mock behaviour # to any of your real objects (Partial Mock/Stub). Because the underlying implementation # for mocks and stubs is the same, you can intermingle mock and stub # behaviour in either dynamically generated mocks or your pre-existing classes. # There is a semantic difference in how they are created, however, # which can help clarify the role it is playing within a given spec. # # == Mock Objects # # Mocks are objects that allow you to set and verify expectations that they will # receive specific messages during run time. They are very useful for specifying how the subject of # the spec interacts with its collaborators. This approach is widely known as "interaction # testing". # # Mocks are also very powerful as a design tool. As you are # driving the implementation of a given class, Mocks provide an anonymous # collaborator that can change in behaviour as quickly as you can write an expectation in your # spec. This flexibility allows you to design the interface of a collaborator that often # does not yet exist. As the shape of the class being specified becomes more clear, so do the # requirements for its collaborators - often leading to the discovery of new types that are # needed in your system. # # Read Endo-Testing[http://www.mockobjects.com/files/endotesting.pdf] for a much # more in depth description of this process. # # == Stubs # # Stubs are objects that allow you to set "stub" responses to # messages. As Martin Fowler points out on his site, # mocks_arent_stubs[http://www.martinfowler.com/articles/mocksArentStubs.html]. # Paraphrasing Fowler's paraphrasing # of Gerard Meszaros: Stubs provide canned responses to messages they might receive in a test, while # mocks allow you to specify and, subsquently, verify that certain messages should be received during # the execution of a test. # # == Partial Mocks/Stubs # # RSpec also supports partial mocking/stubbing, allowing you to add stub/mock behaviour # to instances of your existing classes. This is generally # something to be avoided, because changes to the class can have ripple effects on # seemingly unrelated specs. When specs fail due to these ripple effects, the fact # that some methods are being mocked can make it difficult to understand why a # failure is occurring. # # That said, partials do allow you to expect and # verify interactions with class methods such as +#find+ and +#create+ # on Ruby on Rails model classes. # # == Further Reading # # There are many different viewpoints about the meaning of mocks and stubs. If you are interested # in learning more, here is some recommended reading: # # * Mock Objects: http://www.mockobjects.com/ # * Endo-Testing: http://www.mockobjects.com/files/endotesting.pdf # * Mock Roles, Not Objects: http://www.mockobjects.com/files/mockrolesnotobjects.pdf # * Test Double Patterns: http://xunitpatterns.com/Test%20Double%20Patterns.html # * Mocks aren't stubs: http://www.martinfowler.com/articles/mocksArentStubs.html # # == Creating a Mock # # You can create a mock in any specification (or setup) using: # # mock(name, options={}) # # The optional +options+ argument is a +Hash+. Currently the only supported # option is +:null_object+. Setting this to true instructs the mock to ignore # any messages it hasn’t been told to expect – and quietly return itself. For example: # # mock("person", :null_object => true) # # == Creating a Stub # # You can create a stub in any specification (or setup) using: # # stub(name, stub_methods_and_values_hash) # # For example, if you wanted to create an object that always returns # "More?!?!?!" to "please_sir_may_i_have_some_more" you would do this: # # stub("Mr Sykes", :please_sir_may_i_have_some_more => "More?!?!?!") # # == Creating a Partial Mock # # You don't really "create" a partial mock, you simply add method stubs and/or # mock expectations to existing classes and objects: # # Factory.should_receive(:find).with(id).and_return(value) # obj.stub!(:to_i).and_return(3) # etc ... # # == Expecting Messages # # my_mock.should_receive(:sym) # my_mock.should_not_receive(:sym) # # == Expecting Arguments # # my_mock.should_receive(:sym).with(*args) # my_mock.should_not_receive(:sym).with(*args) # # == Argument Constraints using Expression Matchers # # Arguments that are passed to #with are compared with actual arguments received # using == by default. In cases in which you want to specify things about the arguments # rather than the arguments themselves, you can use any of the Expression Matchers. # They don't all make syntactic sense (they were primarily designed for use with # Spec::Expectations), but you are free to create your own custom Spec::Matchers. # # Spec::Mocks does provide one additional Matcher method named #ducktype. # # In addition, Spec::Mocks adds some keyword Symbols that you can use to # specify certain kinds of arguments: # - # my_mock.should_receive(:sym).with(:no_args) - # my_mock.should_receive(:sym).with(:any_args) - # my_mock.should_receive(:sym).with(1, :numeric, "b") #2nd argument can any type of Numeric - # my_mock.should_receive(:sym).with(1, :boolean, "b") #2nd argument can true or false - # my_mock.should_receive(:sym).with(1, :string, "b") #2nd argument can be any String + # my_mock.should_receive(:sym).with(no_args()) + # my_mock.should_receive(:sym).with(any_args()) + # my_mock.should_receive(:sym).with(1, an_instance_of(Numeric), "b") #2nd argument can any type of Numeric + # my_mock.should_receive(:sym).with(1, boolean(), "b") #2nd argument can true or false # my_mock.should_receive(:sym).with(1, /abc/, "b") #2nd argument can be any String matching the submitted Regexp - # my_mock.should_receive(:sym).with(1, :anything, "b") #2nd argument can be anything at all + # my_mock.should_receive(:sym).with(1, anything(), "b") #2nd argument can be anything at all # my_mock.should_receive(:sym).with(1, ducktype(:abs, :div), "b") # #2nd argument can be object that responds to #abs and #div # # == Receive Counts # # my_mock.should_receive(:sym).once # my_mock.should_receive(:sym).twice # my_mock.should_receive(:sym).exactly(n).times # my_mock.should_receive(:sym).at_least(:once) # my_mock.should_receive(:sym).at_least(:twice) # my_mock.should_receive(:sym).at_least(n).times # my_mock.should_receive(:sym).at_most(:once) # my_mock.should_receive(:sym).at_most(:twice) # my_mock.should_receive(:sym).at_most(n).times # my_mock.should_receive(:sym).any_number_of_times # # == Ordering # # my_mock.should_receive(:sym).ordered # my_mock.should_receive(:other_sym).ordered # #This will fail if the messages are received out of order # # == Setting Reponses # # Whether you are setting a mock expectation or a simple stub, you can tell the # object precisely how to respond: # # my_mock.should_receive(:sym).and_return(value) # my_mock.should_receive(:sym).exactly(3).times.and_return(value1, value2, value3) # # returns value1 the first time, value2 the second, etc # my_mock.should_receive(:sym).and_return { ... } #returns value returned by the block # my_mock.should_receive(:sym).and_raise(error) # #error can be an instantiated object or a class # #if it is a class, it must be instantiable with no args # my_mock.should_receive(:sym).and_throw(:sym) # my_mock.should_receive(:sym).and_yield([array,of,values,to,yield]) # # Any of these responses can be applied to a stub as well, but stubs do # not support any qualifiers about the message received (i.e. you can't specify arguments # or receive counts): # # my_mock.stub!(:sym).and_return(value) # my_mock.stub!(:sym).and_return(value1, value2, value3) # my_mock.stub!(:sym).and_raise(error) # my_mock.stub!(:sym).and_throw(:sym) # my_mock.stub!(:sym).and_yield([array,of,values,to,yield]) # # == Arbitrary Handling # # Once in a while you'll find that the available expectations don't solve the # particular problem you are trying to solve. Imagine that you expect the message # to come with an Array argument that has a specific length, but you don't care # what is in it. You could do this: # # my_mock.should_receive(:sym) do |arg| # arg.should be_an_istance_of(Array) # arg.length.should == 7 # end # # Note that this would fail if the number of arguments received was different from # the number of block arguments (in this case 1). # # == Combining Expectation Details # # Combining the message name with specific arguments, receive counts and responses # you can get quite a bit of detail in your expectations: # # my_mock.should_receive(:<<).with("illegal value").once.and_raise(ArgumentError) module Mocks - # Shortcut for creating an instance of Spec::Mocks::Mock. - def mock(name, options={}) - Spec::Mocks::Mock.new(name, options) - end - - # Shortcut for creating an instance of Spec::Mocks::Mock with - # predefined method stubs. - # - # == Examples - # - # stub_thing = stub("thing", :a => "A") - # stub_thing.a == "A" => true - # - # stub_person = stub("thing", :name => "Joe", :email => "joe@domain.com") - # stub_person.name => "Joe" - # stub_person.email => "joe@domain.com" - def stub(name, stubs={}) - object_stub = mock(name) - stubs.each { |key, value| object_stub.stub!(key).and_return(value) } - object_stub - end - - # Shortcut for creating an instance of Spec::Mocks::DuckTypeArgConstraint - def duck_type(*args) - return Spec::Mocks::DuckTypeArgConstraint.new(*args) - end - end -end \ No newline at end of file +end diff --git a/spec/lib/spec/mocks/argument_constraint_matchers.rb b/spec/lib/spec/mocks/argument_constraint_matchers.rb new file mode 100644 index 000000000..0e4777082 --- /dev/null +++ b/spec/lib/spec/mocks/argument_constraint_matchers.rb @@ -0,0 +1,27 @@ +module Spec + module Mocks + module ArgumentConstraintMatchers + + # Shortcut for creating an instance of Spec::Mocks::DuckTypeArgConstraint + def duck_type(*args) + DuckTypeArgConstraint.new(*args) + end + + def any_args + AnyArgsConstraint.new + end + + def anything + AnyArgConstraint.new(nil) + end + + def boolean + BooleanArgConstraint.new(nil) + end + + def no_args + NoArgsConstraint.new + end + end + end +end diff --git a/test/lib/spec/mocks/argument_expectation.rb b/spec/lib/spec/mocks/argument_expectation.rb similarity index 62% rename from test/lib/spec/mocks/argument_expectation.rb rename to spec/lib/spec/mocks/argument_expectation.rb index a4870e767..5da069b87 100644 --- a/test/lib/spec/mocks/argument_expectation.rb +++ b/spec/lib/spec/mocks/argument_expectation.rb @@ -1,132 +1,183 @@ module Spec module Mocks class MatcherConstraint def initialize(matcher) @matcher = matcher end def matches?(value) @matcher.matches?(value) end end class LiteralArgConstraint def initialize(literal) @literal_value = literal end def matches?(value) @literal_value == value end end class RegexpArgConstraint def initialize(regexp) @regexp = regexp end def matches?(value) return value =~ @regexp unless value.is_a?(Regexp) value == @regexp end end class AnyArgConstraint def initialize(ignore) end + def ==(other) + true + end + + # TODO - need this? def matches?(value) true end end + class AnyArgsConstraint + def description + "any args" + end + end + + class NoArgsConstraint + def description + "no args" + end + + def ==(args) + args == [] + end + end + class NumericArgConstraint def initialize(ignore) end def matches?(value) value.is_a?(Numeric) end end class BooleanArgConstraint def initialize(ignore) end + def ==(value) + matches?(value) + end + def matches?(value) return true if value.is_a?(TrueClass) return true if value.is_a?(FalseClass) false end end class StringArgConstraint def initialize(ignore) end def matches?(value) value.is_a?(String) end end class DuckTypeArgConstraint - def initialize(*methods_to_respond_do) - @methods_to_respond_do = methods_to_respond_do + def initialize(*methods_to_respond_to) + @methods_to_respond_to = methods_to_respond_to end def matches?(value) - @methods_to_respond_do.all? { |sym| value.respond_to?(sym) } + @methods_to_respond_to.all? { |sym| value.respond_to?(sym) } + end + + def description + "duck_type" end end class ArgumentExpectation attr_reader :args @@constraint_classes = Hash.new { |hash, key| LiteralArgConstraint} @@constraint_classes[:anything] = AnyArgConstraint @@constraint_classes[:numeric] = NumericArgConstraint @@constraint_classes[:boolean] = BooleanArgConstraint @@constraint_classes[:string] = StringArgConstraint def initialize(args) @args = args - if [:any_args] == args then @expected_params = nil - elsif [:no_args] == args then @expected_params = [] + if [:any_args] == args + @expected_params = nil + warn_deprecated(:any_args.inspect, "any_args()") + elsif args.length == 1 && args[0].is_a?(AnyArgsConstraint) then @expected_params = nil + elsif [:no_args] == args + @expected_params = [] + warn_deprecated(:no_args.inspect, "no_args()") + elsif args.length == 1 && args[0].is_a?(NoArgsConstraint) then @expected_params = [] else @expected_params = process_arg_constraints(args) end end def process_arg_constraints(constraints) constraints.collect do |constraint| convert_constraint(constraint) end end + def warn_deprecated(deprecated_method, instead) + STDERR.puts "The #{deprecated_method} constraint is deprecated. Use #{instead} instead." + end + def convert_constraint(constraint) - return @@constraint_classes[constraint].new(constraint) if constraint.is_a?(Symbol) - return constraint if constraint.is_a?(DuckTypeArgConstraint) + if [:anything, :numeric, :boolean, :string].include?(constraint) + case constraint + when :anything + instead = "anything()" + when :boolean + instead = "boolean()" + when :numeric + instead = "an_instance_of(Numeric)" + when :string + instead = "an_instance_of(String)" + end + warn_deprecated(constraint.inspect, instead) + return @@constraint_classes[constraint].new(constraint) + end return MatcherConstraint.new(constraint) if is_matcher?(constraint) return RegexpArgConstraint.new(constraint) if constraint.is_a?(Regexp) return LiteralArgConstraint.new(constraint) end def is_matcher?(obj) return obj.respond_to?(:matches?) && obj.respond_to?(:description) end def check_args(args) return true if @expected_params.nil? return true if @expected_params == args return constraints_match?(args) end def constraints_match?(args) return false if args.length != @expected_params.length @expected_params.each_index { |i| return false unless @expected_params[i].matches?(args[i]) } return true end end end end diff --git a/test/lib/spec/mocks/error_generator.rb b/spec/lib/spec/mocks/error_generator.rb similarity index 91% rename from test/lib/spec/mocks/error_generator.rb rename to spec/lib/spec/mocks/error_generator.rb index 950864a7a..01d8f720d 100644 --- a/test/lib/spec/mocks/error_generator.rb +++ b/spec/lib/spec/mocks/error_generator.rb @@ -1,85 +1,84 @@ module Spec module Mocks class ErrorGenerator attr_writer :opts def initialize(target, name) @target = target @name = name end def opts @opts ||= {} end def raise_unexpected_message_error(sym, *args) __raise "#{intro} received unexpected message :#{sym}#{arg_message(*args)}" end def raise_unexpected_message_args_error(expectation, *args) - #this is either :no_args or an Array - expected_args = (expectation.expected_args == :no_args ? "(no args)" : format_args(*expectation.expected_args)) + expected_args = format_args(*expectation.expected_args) actual_args = args.empty? ? "(no args)" : format_args(*args) __raise "#{intro} expected #{expectation.sym.inspect} with #{expected_args} but received it with #{actual_args}" end def raise_expectation_error(sym, expected_received_count, actual_received_count, *args) __raise "#{intro} expected :#{sym}#{arg_message(*args)} #{count_message(expected_received_count)}, but received it #{count_message(actual_received_count)}" end def raise_out_of_order_error(sym) __raise "#{intro} received :#{sym} out of order" end def raise_block_failed_error(sym, detail) __raise "#{intro} received :#{sym} but passed block failed with: #{detail}" end def raise_missing_block_error(args_to_yield) __raise "#{intro} asked to yield |#{arg_list(*args_to_yield)}| but no block was passed" end def raise_wrong_arity_error(args_to_yield, arity) __raise "#{intro} yielded |#{arg_list(*args_to_yield)}| to block with arity of #{arity}" end private def intro - @name ? "Mock '#{@name}'" : @target.to_s + @name ? "Mock '#{@name}'" : @target.inspect end def __raise(message) message = opts[:message] unless opts[:message].nil? Kernel::raise(Spec::Mocks::MockExpectationError, message) end def arg_message(*args) " with " + format_args(*args) end def format_args(*args) return "(no args)" if args.empty? || args == [:no_args] return "(any args)" if args == [:any_args] "(" + arg_list(*args) + ")" end def arg_list(*args) args.collect do |arg| arg.respond_to?(:description) ? arg.description : arg.inspect end.join(", ") end def count_message(count) return "at least #{pretty_print(count.abs)}" if count < 0 return pretty_print(count) end def pretty_print(count) return "once" if count == 1 return "twice" if count == 2 return "#{count} times" end end end -end \ No newline at end of file +end diff --git a/test/lib/spec/mocks/errors.rb b/spec/lib/spec/mocks/errors.rb similarity index 100% rename from test/lib/spec/mocks/errors.rb rename to spec/lib/spec/mocks/errors.rb diff --git a/test/lib/spec/mocks/extensions/object.rb b/spec/lib/spec/mocks/extensions/object.rb similarity index 100% rename from test/lib/spec/mocks/extensions/object.rb rename to spec/lib/spec/mocks/extensions/object.rb diff --git a/test/lib/spec/mocks/message_expectation.rb b/spec/lib/spec/mocks/message_expectation.rb similarity index 92% rename from test/lib/spec/mocks/message_expectation.rb rename to spec/lib/spec/mocks/message_expectation.rb index 152e65a47..74ade3c58 100644 --- a/test/lib/spec/mocks/message_expectation.rb +++ b/spec/lib/spec/mocks/message_expectation.rb @@ -1,231 +1,242 @@ module Spec module Mocks class BaseExpectation attr_reader :sym def initialize(error_generator, expectation_ordering, expected_from, sym, method_block, expected_received_count=1, opts={}) @error_generator = error_generator @error_generator.opts = opts @expected_from = expected_from @sym = sym @method_block = method_block @return_block = lambda {} @received_count = 0 @expected_received_count = expected_received_count - @args_expectation = ArgumentExpectation.new([:any_args]) + @args_expectation = ArgumentExpectation.new([AnyArgsConstraint.new]) @consecutive = false @exception_to_raise = nil @symbol_to_throw = nil @order_group = expectation_ordering @at_least = nil @at_most = nil @args_to_yield = nil end def expected_args @args_expectation.args end def and_return(*values, &return_block) Kernel::raise AmbiguousReturnError unless @method_block.nil? if values.size == 0 value = nil elsif values.size == 1 value = values[0] else value = values @consecutive = true @expected_received_count = values.size if @expected_received_count != :any && @expected_received_count < values.size end @return_block = block_given? ? return_block : lambda { value } end + # :call-seq: + # and_raise() + # and_raise(Exception) #any exception class + # and_raise(exception) #any exception object + # + # == Warning + # + # When you pass an exception class, the MessageExpectation will + # raise an instance of it, creating it with +new+. If the exception + # class initializer requires any parameters, you must pass in an + # instance and not the class. def and_raise(exception=Exception) @exception_to_raise = exception end def and_throw(symbol) @symbol_to_throw = symbol end def and_yield(*args) @args_to_yield = args end def matches(sym, args) @sym == sym and @args_expectation.check_args(args) end def invoke(args, block) @order_group.handle_order_constraint self begin if @exception_to_raise.class == Class @exception_instance_to_raise = @exception_to_raise.new else @exception_instance_to_raise = @exception_to_raise end Kernel::raise @exception_to_raise unless @exception_to_raise.nil? Kernel::throw @symbol_to_throw unless @symbol_to_throw.nil? if !@method_block.nil? return invoke_method_block(args) elsif !@args_to_yield.nil? return invoke_with_yield(block) elsif @consecutive return invoke_consecutive_return_block(args, block) else return invoke_return_block(args, block) end ensure @received_count += 1 end end protected def invoke_method_block(args) begin @method_block.call(*args) rescue => detail @error_generator.raise_block_failed_error @sym, detail.message end end def invoke_with_yield(block) if block.nil? @error_generator.raise_missing_block_error @args_to_yield end if block.arity > -1 && @args_to_yield.length != block.arity @error_generator.raise_wrong_arity_error @args_to_yield, block.arity end block.call(*@args_to_yield) end def invoke_consecutive_return_block(args, block) args << block unless block.nil? value = @return_block.call(*args) index = [@received_count, value.size-1].min value[index] end def invoke_return_block(args, block) args << block unless block.nil? value = @return_block.call(*args) value end end class MessageExpectation < BaseExpectation def matches_name_but_not_args(sym, args) @sym == sym and not @args_expectation.check_args(args) end def verify_messages_received return if @expected_received_count == :any return if (@at_least) && (@received_count >= @expected_received_count) return if (@at_most) && (@received_count <= @expected_received_count) return if @expected_received_count == @received_count begin @error_generator.raise_expectation_error(@sym, @expected_received_count, @received_count, *@args_expectation.args) rescue => error error.backtrace.insert(0, @expected_from) Kernel::raise error end end def with(*args, &block) @method_block = block if block @args_expectation = ArgumentExpectation.new(args) self end def exactly(n) set_expected_received_count :exactly, n self end def at_least(n) set_expected_received_count :at_least, n self end def at_most(n) set_expected_received_count :at_most, n self end def times(&block) @method_block = block if block self end def any_number_of_times(&block) @method_block = block if block @expected_received_count = :any self end def never @expected_received_count = 0 self end def once(&block) @method_block = block if block @expected_received_count = 1 self end def twice(&block) @method_block = block if block @expected_received_count = 2 self end def ordered(&block) @method_block = block if block @order_group.register(self) @ordered = true self end def negative_expectation_for?(sym) return false end protected def set_expected_received_count(relativity, n) @at_least = (relativity == :at_least) @at_most = (relativity == :at_most) @expected_received_count = 1 if n == :once @expected_received_count = 2 if n == :twice @expected_received_count = n if n.kind_of? Numeric end end class NegativeMessageExpectation < MessageExpectation def initialize(message, expectation_ordering, expected_from, sym, method_block) super(message, expectation_ordering, expected_from, sym, method_block, 0) end def negative_expectation_for?(sym) return @sym == sym end end class MethodStub < BaseExpectation def initialize(message, expectation_ordering, expected_from, sym, method_block) super(message, expectation_ordering, expected_from, sym, method_block, 0) @expected_received_count = :any end end end end diff --git a/spec/lib/spec/mocks/methods.rb b/spec/lib/spec/mocks/methods.rb new file mode 100644 index 000000000..3d898cf31 --- /dev/null +++ b/spec/lib/spec/mocks/methods.rb @@ -0,0 +1,39 @@ +module Spec + module Mocks + module Methods + def should_receive(sym, opts={}, &block) + __mock_proxy.add_message_expectation(opts[:expected_from] || caller(1)[0], sym.to_sym, opts, &block) + end + + def should_not_receive(sym, &block) + __mock_proxy.add_negative_message_expectation(caller(1)[0], sym.to_sym, &block) + end + + def stub!(sym) + __mock_proxy.add_stub(caller(1)[0], sym.to_sym) + end + + def received_message?(sym, *args, &block) #:nodoc: + __mock_proxy.received_message?(sym.to_sym, *args, &block) + end + + def rspec_verify #:nodoc: + __mock_proxy.verify + end + + def rspec_reset #:nodoc: + __mock_proxy.reset + end + + private + + def __mock_proxy + if Mock === self + @mock_proxy ||= Proxy.new(self, @name, @options) + else + @mock_proxy ||= Proxy.new(self, self.class.name) + end + end + end + end +end diff --git a/test/lib/spec/mocks/mock.rb b/spec/lib/spec/mocks/mock.rb similarity index 64% rename from test/lib/spec/mocks/mock.rb rename to spec/lib/spec/mocks/mock.rb index 68de11ff4..aa380e0af 100644 --- a/test/lib/spec/mocks/mock.rb +++ b/spec/lib/spec/mocks/mock.rb @@ -1,26 +1,29 @@ module Spec module Mocks class Mock include Methods # Creates a new mock with a +name+ (that will be used in error messages only) # == Options: # * :null_object - if true, the mock object acts as a forgiving null object allowing any message to be sent to it. def initialize(name, options={}) @name = name @options = options end def method_missing(sym, *args, &block) - __mock_handler.instance_eval {@messages_received << [sym, args, block]} + __mock_proxy.instance_eval {@messages_received << [sym, args, block]} begin - return self if __mock_handler.null_object? + return self if __mock_proxy.null_object? super(sym, *args, &block) rescue NoMethodError - __mock_handler.raise_unexpected_message_error sym, *args + __mock_proxy.raise_unexpected_message_error sym, *args end end + def inspect + "#<#{self.class}:#{sprintf '0x%x', self.object_id} @name=#{@name.inspect}>" + end end end -end \ No newline at end of file +end diff --git a/test/lib/spec/mocks/order_group.rb b/spec/lib/spec/mocks/order_group.rb similarity index 100% rename from test/lib/spec/mocks/order_group.rb rename to spec/lib/spec/mocks/order_group.rb diff --git a/test/lib/spec/mocks/mock_handler.rb b/spec/lib/spec/mocks/proxy.rb similarity index 86% rename from test/lib/spec/mocks/mock_handler.rb rename to spec/lib/spec/mocks/proxy.rb index ef6f97a1c..6c79d1068 100644 --- a/test/lib/spec/mocks/mock_handler.rb +++ b/spec/lib/spec/mocks/proxy.rb @@ -1,166 +1,167 @@ module Spec module Mocks - class MockHandler + class Proxy DEFAULT_OPTIONS = { :null_object => false, - :auto_verify => true } def initialize(target, name, options={}) @target = target @name = name @error_generator = ErrorGenerator.new target, name @expectation_ordering = OrderGroup.new @error_generator @expectations = [] @messages_received = [] @stubs = [] @proxied_methods = [] @options = options ? DEFAULT_OPTIONS.dup.merge(options) : DEFAULT_OPTIONS end def null_object? @options[:null_object] end def add_message_expectation(expected_from, sym, opts={}, &block) - __add expected_from, sym, block + __add sym, block @expectations << MessageExpectation.new(@error_generator, @expectation_ordering, expected_from, sym, block_given? ? block : nil, 1, opts) @expectations.last end def add_negative_message_expectation(expected_from, sym, &block) - __add expected_from, sym, block + __add sym, block @expectations << NegativeMessageExpectation.new(@error_generator, @expectation_ordering, expected_from, sym, block_given? ? block : nil) @expectations.last end def add_stub(expected_from, sym) - __add expected_from, sym, nil + __add sym, nil @stubs.unshift MethodStub.new(@error_generator, @expectation_ordering, expected_from, sym, nil) @stubs.first end def verify #:nodoc: begin verify_expectations ensure reset end end def reset clear_expectations clear_stubs reset_proxied_methods clear_proxied_methods end def received_message?(sym, *args, &block) return true if @messages_received.find {|array| array == [sym, args, block]} return false end def has_negative_expectation?(sym) @expectations.detect {|expectation| expectation.negative_expectation_for?(sym)} end def message_received(sym, *args, &block) if expectation = find_matching_expectation(sym, *args) expectation.invoke(args, block) elsif stub = find_matching_method_stub(sym) stub.invoke([], block) elsif expectation = find_almost_matching_expectation(sym, *args) raise_unexpected_message_args_error(expectation, *args) unless has_negative_expectation?(sym) unless null_object? else @target.send :method_missing, sym, *args, &block end end def raise_unexpected_message_args_error(expectation, *args) @error_generator.raise_unexpected_message_args_error expectation, *args end def raise_unexpected_message_error(sym, *args) @error_generator.raise_unexpected_message_error sym, *args end - private + private - def __add(expected_from, sym, block) - # TODO - this is the only reference in the 'spec/mocks' to the Runner - current_spec = Runner::Specification.current - current_spec.after_teardown {verify} if current_spec && @options[:auto_verify] + def __add(sym, block) + $rspec_mocks.add(@target) unless $rspec_mocks.nil? define_expected_method(sym) end def define_expected_method(sym) if target_responds_to?(sym) && !@proxied_methods.include?(sym) + metaclass.__send__(:alias_method, munge(sym), sym) if metaclass.instance_methods.include?(sym.to_s) @proxied_methods << sym - metaclass.__send__(:alias_method, munge(sym), sym) end - + metaclass_eval(<<-EOF, __FILE__, __LINE__) def #{sym}(*args, &block) - __mock_handler.message_received :#{sym}, *args, &block + __mock_proxy.message_received :#{sym}, *args, &block end EOF end def target_responds_to?(sym) return @target.send(munge(:respond_to?),sym) if @already_proxied_respond_to return @already_proxied_respond_to = true if sym == :respond_to? return @target.respond_to?(sym) end def munge(sym) "proxied_by_rspec__#{sym.to_s}".to_sym end def clear_expectations @expectations.clear end def clear_stubs @stubs.clear end def clear_proxied_methods @proxied_methods.clear end def metaclass_eval(str, filename, lineno) metaclass.class_eval(str, filename, lineno) end def metaclass (class << @target; self; end) end def verify_expectations @expectations.each do |expectation| expectation.verify_messages_received end end def reset_proxied_methods @proxied_methods.each do |sym| - metaclass.__send__(:alias_method, sym, munge(sym)) - metaclass.__send__(:undef_method, munge(sym)) + if metaclass.instance_methods.include?(munge(sym).to_s) + metaclass.__send__(:alias_method, sym, munge(sym)) + metaclass.__send__(:undef_method, munge(sym)) + else + metaclass.__send__(:undef_method, sym) + end end end def find_matching_expectation(sym, *args) @expectations.find {|expectation| expectation.matches(sym, args)} end def find_almost_matching_expectation(sym, *args) @expectations.find {|expectation| expectation.matches_name_but_not_args(sym, args)} end def find_matching_method_stub(sym) @stubs.find {|stub| stub.matches(sym, [])} end end end -end \ No newline at end of file +end diff --git a/spec/lib/spec/mocks/space.rb b/spec/lib/spec/mocks/space.rb new file mode 100644 index 000000000..e04bc5ccb --- /dev/null +++ b/spec/lib/spec/mocks/space.rb @@ -0,0 +1,28 @@ +module Spec + module Mocks + class Space + def add(obj) + mocks << obj unless mocks.include?(obj) + end + + def verify_all + mocks.each do |mock| + mock.rspec_verify + end + end + + def reset_all + mocks.each do |mock| + mock.rspec_reset + end + mocks.clear + end + + private + + def mocks + @mocks ||= [] + end + end + end +end diff --git a/spec/lib/spec/mocks/spec_methods.rb b/spec/lib/spec/mocks/spec_methods.rb new file mode 100644 index 000000000..fd67fd210 --- /dev/null +++ b/spec/lib/spec/mocks/spec_methods.rb @@ -0,0 +1,30 @@ +module Spec + module Mocks + module SpecMethods + include Spec::Mocks::ArgumentConstraintMatchers + + # Shortcut for creating an instance of Spec::Mocks::Mock. + def mock(name, options={}) + Spec::Mocks::Mock.new(name, options) + end + + # Shortcut for creating an instance of Spec::Mocks::Mock with + # predefined method stubs. + # + # == Examples + # + # stub_thing = stub("thing", :a => "A") + # stub_thing.a == "A" => true + # + # stub_person = stub("thing", :name => "Joe", :email => "joe@domain.com") + # stub_person.name => "Joe" + # stub_person.email => "joe@domain.com" + def stub(name, stubs={}) + object_stub = mock(name) + stubs.each { |key, value| object_stub.stub!(key).and_return(value) } + object_stub + end + + end + end +end diff --git a/spec/lib/spec/rake/spectask.rb b/spec/lib/spec/rake/spectask.rb new file mode 100644 index 000000000..f8c6809a9 --- /dev/null +++ b/spec/lib/spec/rake/spectask.rb @@ -0,0 +1,217 @@ +#!/usr/bin/env ruby + +# Define a task library for running RSpec contexts. + +require 'rake' +require 'rake/tasklib' + +module Spec + module Rake + + # A Rake task that runs a set of RSpec contexts. + # + # Example: + # + # Spec::Rake::SpecTask.new do |t| + # t.warning = true + # t.rcov = true + # end + # + # This will create a task that can be run with: + # + # rake spec + # + # If rake is invoked with a "SPEC=filename" command line option, + # then the list of spec files will be overridden to include only the + # filename specified on the command line. This provides an easy way + # to run just one spec. + # + # If rake is invoked with a "SPEC_OPTS=options" command line option, + # then the given options will override the value of the +spec_opts+ + # attribute. + # + # If rake is invoked with a "RCOV_OPTS=options" command line option, + # then the given options will override the value of the +rcov_opts+ + # attribute. + # + # Examples: + # + # rake spec # run specs normally + # rake spec SPEC=just_one_file.rb # run just one spec file. + # rake spec SPEC_OPTS="--diff" # enable diffing + # rake spec RCOV_OPTS="--aggregate myfile.txt" # see rcov --help for details + # + # Each attribute of this task may be a proc. This allows for lazy evaluation, + # which is sometimes handy if you want to defer the evaluation of an attribute value + # until the task is run (as opposed to when it is defined). + class SpecTask < ::Rake::TaskLib + class << self + def attr_accessor(*names) + super(*names) + names.each do |name| + module_eval "def #{name}() evaluate(@#{name}) end" # Allows use of procs + end + end + end + + # Name of spec task. (default is :spec) + attr_accessor :name + + # Array of directories to be added to $LOAD_PATH before running the + # specs. Defaults to [''] + attr_accessor :libs + + # If true, requests that the specs be run with the warning flag set. + # E.g. warning=true implies "ruby -w" used to run the specs. Defaults to false. + attr_accessor :warning + + # Glob pattern to match spec files. (default is 'spec/**/*_spec.rb') + # Setting the SPEC environment variable overrides this. + attr_accessor :pattern + + # Array of commandline options to pass to RSpec. Defaults to []. + # Setting the SPEC_OPTS environment variable overrides this. + attr_accessor :spec_opts + + # Whether or not to use RCov (default is false) + # See http://eigenclass.org/hiki.rb?rcov + attr_accessor :rcov + + # Array of commandline options to pass to RCov. Defaults to ['--exclude', 'lib\/spec,bin\/spec']. + # Ignored if rcov=false + # Setting the RCOV_OPTS environment variable overrides this. + attr_accessor :rcov_opts + + # Directory where the RCov report is written. Defaults to "coverage" + # Ignored if rcov=false + attr_accessor :rcov_dir + + # Array of commandline options to pass to ruby. Defaults to []. + attr_accessor :ruby_opts + + # Whether or not to fail Rake when an error occurs (typically when specs fail). + # Defaults to true. + attr_accessor :fail_on_error + + # A message to print to stderr when there are failures. + attr_accessor :failure_message + + # Where RSpec's output is written. Defaults to STDOUT. + # DEPRECATED. Use --format FORMAT:WHERE in spec_opts. + attr_accessor :out + + # Explicitly define the list of spec files to be included in a + # spec. +spec_files+ is expected to be an array of file names (a + # FileList is acceptable). If both +pattern+ and +spec_files+ are + # used, then the list of spec files is the union of the two. + # Setting the SPEC environment variable overrides this. + attr_accessor :spec_files + + # Defines a new task, using the name +name+. + def initialize(name=:spec) + @name = name + @libs = [File.expand_path(File.dirname(__FILE__) + '/../../../lib')] + @pattern = nil + @spec_files = nil + @spec_opts = [] + @warning = false + @ruby_opts = [] + @fail_on_error = true + @rcov = false + @rcov_opts = ['--exclude', 'lib\/spec,bin\/spec,config\/boot.rb'] + @rcov_dir = "coverage" + + yield self if block_given? + @pattern = 'spec/**/*_spec.rb' if pattern.nil? && spec_files.nil? + define + end + + def define # :nodoc: + spec_script = File.expand_path(File.dirname(__FILE__) + '/../../../bin/spec') + + lib_path = libs.join(File::PATH_SEPARATOR) + actual_name = Hash === name ? name.keys.first : name + unless ::Rake.application.last_comment + desc "Run specs" + (rcov ? " using RCov" : "") + end + task name do + RakeFileUtils.verbose(verbose) do + unless spec_file_list.empty? + # ruby [ruby_opts] -Ilib -S rcov [rcov_opts] bin/spec -- examples [spec_opts] + # or + # ruby [ruby_opts] -Ilib bin/spec examples [spec_opts] + cmd = "ruby " + + rb_opts = ruby_opts.clone + rb_opts << "-I\"#{lib_path}\"" + rb_opts << "-S rcov" if rcov + rb_opts << "-w" if warning + cmd << rb_opts.join(" ") + cmd << " " + cmd << rcov_option_list + cmd << %[ -o "#{rcov_dir}" ] if rcov + cmd << %Q|"#{spec_script}"| + cmd << " " + cmd << "-- " if rcov + cmd << spec_file_list.collect { |fn| %["#{fn}"] }.join(' ') + cmd << " " + cmd << spec_option_list + if out + cmd << " " + cmd << %Q| > "#{out}"| + STDERR.puts "The Spec::Rake::SpecTask#out attribute is DEPRECATED and will be removed in a future version. Use --format FORMAT:WHERE instead." + end + unless system(cmd) + STDERR.puts failure_message if failure_message + raise("Command #{cmd} failed") if fail_on_error + end + end + end + end + + if rcov + desc "Remove rcov products for #{actual_name}" + task paste("clobber_", actual_name) do + rm_r rcov_dir rescue nil + end + + clobber_task = paste("clobber_", actual_name) + task :clobber => [clobber_task] + + task actual_name => clobber_task + end + self + end + + def rcov_option_list # :nodoc: + return "" unless rcov + ENV['RCOV_OPTS'] || rcov_opts.join(" ") || "" + end + + def spec_option_list # :nodoc: + STDERR.puts "RSPECOPTS is DEPRECATED and will be removed in a future version. Use SPEC_OPTS instead." if ENV['RSPECOPTS'] + ENV['SPEC_OPTS'] || ENV['RSPECOPTS'] || spec_opts.join(" ") || "" + end + + def evaluate(o) # :nodoc: + case o + when Proc then o.call + else o + end + end + + def spec_file_list # :nodoc: + if ENV['SPEC'] + FileList[ ENV['SPEC'] ] + else + result = [] + result += spec_files.to_a if spec_files + result += FileList[ pattern ].to_a if pattern + FileList[result] + end + end + + end + end +end + diff --git a/test/lib/spec/rake/verify_rcov.rb b/spec/lib/spec/rake/verify_rcov.rb similarity index 87% rename from test/lib/spec/rake/verify_rcov.rb rename to spec/lib/spec/rake/verify_rcov.rb index a05153e99..9715744e9 100644 --- a/test/lib/spec/rake/verify_rcov.rb +++ b/spec/lib/spec/rake/verify_rcov.rb @@ -1,47 +1,52 @@ module RCov # A task that can verify that the RCov coverage doesn't # drop below a certain threshold. It should be run after # running Spec::Rake::SpecTask. class VerifyTask < Rake::TaskLib # Name of the task. Defaults to :verify_rcov attr_accessor :name # Path to the index.html file generated by RCov, which # is the file containing the total coverage. # Defaults to 'coverage/index.html' attr_accessor :index_html # Whether or not to output details. Defaults to true. attr_accessor :verbose # The threshold value (in percent) for coverage. If the # actual coverage is not equal to this value, the task will raise an # exception. attr_accessor :threshold + # Require the threshold value be met exactly. This is the default. + attr_accessor :require_exact_threshold + def initialize(name=:verify_rcov) @name = name @index_html = 'coverage/index.html' @verbose = true + @require_exact_threshold = true yield self if block_given? raise "Threshold must be set" if @threshold.nil? define end def define desc "Verify that rcov coverage is at least #{threshold}%" task @name do total_coverage = nil + File.open(index_html).each_line do |line| if line =~ /(\d+\.\d+)%<\/tt> <\/td>/ total_coverage = eval($1) break end end puts "Coverage: #{total_coverage}% (threshold: #{threshold}%)" if verbose raise "Coverage must be at least #{threshold}% but was #{total_coverage}%" if total_coverage < threshold - raise "Coverage has increased above the threshold of #{threshold}% to #{total_coverage}%. You should update your threshold value." if total_coverage > threshold + raise "Coverage has increased above the threshold of #{threshold}% to #{total_coverage}%. You should update your threshold value." if (total_coverage > threshold) and require_exact_threshold end end end -end \ No newline at end of file +end diff --git a/spec/lib/spec/runner.rb b/spec/lib/spec/runner.rb new file mode 100644 index 000000000..9d801adc3 --- /dev/null +++ b/spec/lib/spec/runner.rb @@ -0,0 +1,165 @@ +require 'spec/runner/formatter' +require 'spec/runner/behaviour_runner' +require 'spec/runner/options' +require 'spec/runner/option_parser' +require 'spec/runner/command_line' +require 'spec/runner/drb_command_line' +require 'spec/runner/backtrace_tweaker' +require 'spec/runner/reporter' +require 'spec/runner/extensions/object' +require 'spec/runner/extensions/kernel' +require 'spec/runner/spec_parser' + +module Spec + # == Behaviours and Examples + # + # Rather than expressing examples in classes, RSpec uses a custom domain specific language to + # describe Behaviours and Examples of those behaviours. + # + # A Behaviour is the equivalent of a fixture in xUnit-speak. It is a metaphor for the context + # in which you will run your executable example - a set of known objects in a known starting state. + # We begin be describing + # + # describe Account do + # + # before do + # @account = Account.new + # end + # + # it "should have a balance of $0" do + # @account.balance.should == Money.new(0, :dollars) + # end + # + # end + # + # We use the before block to set up the Behaviour (given), and then the #it method to + # hold the example code that expresses the event (when) and the expected outcome (then). + # + # == Helper Methods + # + # A primary goal of RSpec is to keep the examples clear. We therefore prefer + # less indirection than you might see in xUnit examples and in well factored, DRY production code. We feel + # that duplication is OK if removing it makes it harder to understand an example without + # having to look elsewhere to understand its context. + # + # That said, RSpec does support some level of encapsulating common code in helper + # methods that can exist within a context or within an included module. + # + # == Setup and Teardown + # + # You can use before and after within a Behaviour. Both methods take an optional + # scope argument so you can run the block before :each example or before :all examples + # + # describe "..." do + # before :all do + # ... + # end + # + # before :each do + # ... + # end + # + # it "should do something" do + # ... + # end + # + # it "should do something else" do + # ... + # end + # + # after :each do + # ... + # end + # + # after :all do + # ... + # end + # + # end + # + # The before :each block will run before each of the examples, once for each example. Likewise, + # the after :each block will run after each of the examples. + # + # It is also possible to specify a before :all and after :all + # block that will run only once for each behaviour, respectively before the first before :each + # and after the last after :each. The use of these is generally discouraged, because it + # introduces dependencies between the examples. Still, it might prove useful for very expensive operations + # if you know what you are doing. + # + # == Local helper methods + # + # You can include local helper methods by simply expressing them within a context: + # + # describe "..." do + # + # it "..." do + # helper_method + # end + # + # def helper_method + # ... + # end + # + # end + # + # == Included helper methods + # + # You can include helper methods in multiple contexts by expressing them within + # a module, and then including that module in your context: + # + # module AccountExampleHelperMethods + # def helper_method + # ... + # end + # end + # + # describe "A new account" do + # include AccountExampleHelperMethods + # before do + # @account = Account.new + # end + # + # it "should have a balance of $0" do + # helper_method + # @account.balance.should eql(Money.new(0, :dollars)) + # end + # end + # + # == Shared behaviour + # + # You can define a shared behaviour, that may be used on other behaviours + # + # describe "All Editions", :shared => true do + # it "all editions behaviour" ... + # end + # + # describe SmallEdition do + # it_should_behave_like "All Editions" + # + # it "should do small edition stuff" do + # ... + # end + # end + module Runner + class << self + def configuration # :nodoc: + @configuration ||= Spec::DSL::Configuration.new + end + + # Use this to configure various configurable aspects of + # RSpec: + # + # Spec::Runner.configure do |configuration| + # # Configure RSpec here + # end + # + # The yielded configuration object is a + # Spec::DSL::Configuration instance. See its RDoc + # for details about what you can do with it. + # + def configure + yield configuration if @configuration.nil? + end + end + end +end diff --git a/test/lib/spec/runner/backtrace_tweaker.rb b/spec/lib/spec/runner/backtrace_tweaker.rb similarity index 94% rename from test/lib/spec/runner/backtrace_tweaker.rb rename to spec/lib/spec/runner/backtrace_tweaker.rb index 7300b36b8..aacc2c8b8 100644 --- a/test/lib/spec/runner/backtrace_tweaker.rb +++ b/spec/lib/spec/runner/backtrace_tweaker.rb @@ -1,55 +1,57 @@ module Spec module Runner class BacktraceTweaker def clean_up_double_slashes(line) line.gsub!('//','/') end end class NoisyBacktraceTweaker < BacktraceTweaker def tweak_backtrace(error, spec_name) return if error.backtrace.nil? error.backtrace.each do |line| clean_up_double_slashes(line) end end end # Tweaks raised Exceptions to mask noisy (unneeded) parts of the backtrace class QuietBacktraceTweaker < BacktraceTweaker unless defined?(IGNORE_PATTERNS) root_dir = File.expand_path(File.join(__FILE__, '..', '..', '..', '..')) spec_files = Dir["#{root_dir}/lib/spec/*"].map do |path| subpath = path[root_dir.length..-1] /#{subpath}/ end IGNORE_PATTERNS = spec_files + [ /\/lib\/ruby\//, /bin\/spec:/, /bin\/rcov:/, /lib\/rspec_on_rails/, /vendor\/rails/, # TextMate's Ruby and RSpec plugins /Ruby\.tmbundle\/Support\/tmruby.rb:/, /RSpec\.tmbundle\/Support\/lib/, - /temp_textmate\./ + /temp_textmate\./, + /mock_frameworks\/rspec/, + /spec_server/ ] end def tweak_backtrace(error, spec_name) return if error.backtrace.nil? error.backtrace.collect! do |line| clean_up_double_slashes(line) IGNORE_PATTERNS.each do |ignore| if line =~ ignore line = nil break end end line end error.backtrace.compact! end end end -end \ No newline at end of file +end diff --git a/spec/lib/spec/runner/behaviour_runner.rb b/spec/lib/spec/runner/behaviour_runner.rb new file mode 100644 index 000000000..1ac891f3c --- /dev/null +++ b/spec/lib/spec/runner/behaviour_runner.rb @@ -0,0 +1,123 @@ +module Spec + module Runner + class BehaviourRunner + + def initialize(options, arg=nil) + @behaviours = [] + @options = options + end + + def add_behaviour(behaviour) + if !specified_examples.nil? && !specified_examples.empty? + behaviour.retain_examples_matching!(specified_examples) + end + @behaviours << behaviour if behaviour.number_of_examples != 0 && !behaviour.shared? + end + + # Runs all behaviours and returns the number of failures. + def run(paths, exit_when_done) + prepare!(paths) + begin + run_behaviours + rescue Interrupt + ensure + report_end + end + failure_count = report_dump + + heckle if(failure_count == 0 && !@options.heckle_runner.nil?) + + if(exit_when_done) + exit_code = (failure_count == 0) ? 0 : 1 + exit(exit_code) + end + failure_count + end + + def report_end + @options.reporter.end + end + + def report_dump + @options.reporter.dump + end + + def prepare!(paths) + unless paths.nil? # It's nil when running single specs with ruby + paths = find_paths(paths) + sorted_paths = sort_paths(paths) + load_specs(sorted_paths) # This will populate @behaviours via callbacks to add_behaviour + end + @options.reporter.start(number_of_examples) + @behaviours.reverse! if @options.reverse + set_sequence_numbers + end + + def run_behaviours + @behaviours.each do |behaviour| + behaviour.run(@options.reporter, @options.dry_run, @options.reverse, @options.timeout) + end + end + + def number_of_examples + @behaviours.inject(0) {|sum, behaviour| sum + behaviour.number_of_examples} + end + + FILE_SORTERS = { + 'mtime' => lambda {|file_a, file_b| File.mtime(file_b) <=> File.mtime(file_a)} + } + + def sorter(paths) + FILE_SORTERS[@options.loadby] + end + + def sort_paths(paths) + sorter = sorter(paths) + paths = paths.sort(&sorter) unless sorter.nil? + paths + end + + private + + # Sets the #number on each Example + def set_sequence_numbers + number = 0 + @behaviours.each do |behaviour| + number = behaviour.set_sequence_numbers(number, @options.reverse) + end + end + + def find_paths(paths) + result = [] + paths.each do |path| + if File.directory?(path) + result += Dir["#{path}/**/*.rb"] + elsif File.file?(path) + result << path + else + raise "File or directory not found: #{path}" + end + end + result + end + + def load_specs(paths) + paths.each do |path| + load path + end + end + + def specified_examples + @options.examples + end + + def heckle + heckle_runner = @options.heckle_runner + @options.heckle_runner = nil + behaviour_runner = self.class.new(@options) + behaviour_runner.instance_variable_set(:@behaviours, @behaviours) + heckle_runner.heckle_with(behaviour_runner) + end + end + end +end diff --git a/spec/lib/spec/runner/command_line.rb b/spec/lib/spec/runner/command_line.rb new file mode 100644 index 000000000..0d70337e1 --- /dev/null +++ b/spec/lib/spec/runner/command_line.rb @@ -0,0 +1,22 @@ +require 'spec/runner/option_parser' + +module Spec + module Runner + # Facade to run specs without having to fork a new ruby process (using `spec ...`) + class CommandLine + # Runs specs. +argv+ is the commandline args as per the spec commandline API, +err+ + # and +out+ are the streams output will be written to. +exit+ tells whether or + # not a system exit should be called after the specs are run and + # +warn_if_no_files+ tells whether or not a warning (the help message) + # should be printed to +err+ in case no files are specified. + def self.run(argv, err, out, exit=true, warn_if_no_files=true) + old_behaviour_runner = defined?($behaviour_runner) ? $behaviour_runner : nil + $behaviour_runner = OptionParser.new.create_behaviour_runner(argv, err, out, warn_if_no_files) + return if $behaviour_runner.nil? # This is the case if we use --drb + + $behaviour_runner.run(argv, exit) + $behaviour_runner = old_behaviour_runner + end + end + end +end diff --git a/test/lib/spec/runner/drb_command_line.rb b/spec/lib/spec/runner/drb_command_line.rb similarity index 79% rename from test/lib/spec/runner/drb_command_line.rb rename to spec/lib/spec/runner/drb_command_line.rb index d4c7d937d..7e745fb71 100644 --- a/test/lib/spec/runner/drb_command_line.rb +++ b/spec/lib/spec/runner/drb_command_line.rb @@ -1,21 +1,21 @@ require "drb/drb" module Spec module Runner # Facade to run specs by connecting to a DRB server class DrbCommandLine # Runs specs on a DRB server. Note that this API is similar to that of # CommandLine - making it possible for clients to use both interchangeably. def self.run(argv, stderr, stdout, exit=true, warn_if_no_files=true) begin DRb.start_service - rails_spec_server = DRbObject.new_with_uri("druby://localhost:8989") - rails_spec_server.run(argv, stderr, stdout) + spec_server = DRbObject.new_with_uri("druby://localhost:8989") + spec_server.run(argv, stderr, stdout) rescue DRb::DRbConnError stderr.puts "No server is running" exit 1 if exit end end end end -end \ No newline at end of file +end diff --git a/spec/lib/spec/runner/extensions/kernel.rb b/spec/lib/spec/runner/extensions/kernel.rb new file mode 100644 index 000000000..75f2c335e --- /dev/null +++ b/spec/lib/spec/runner/extensions/kernel.rb @@ -0,0 +1,50 @@ +module Kernel + # Creates and registers an instance of a Spec::DSL::Behaviour (or a subclass). + # The instantiated behaviour class depends on the directory of the file + # calling this method. For example, Spec::Rails will use different + # classes for specs living in spec/models, spec/helpers, + # spec/views and spec/controllers. + # + # It is also possible to override autodiscovery of the behaviour class + # with an options Hash as the last argument: + # + # describe "name", :behaviour_type => :something_special do ... + # + # The reason for using different behaviour classes is to have + # different matcher methods available from within the describe + # block. + # + # See Spec::DSL::BehaviourFactory#add_behaviour_class for details about + # how to register special Spec::DSL::Behaviour implementations. + # + def describe(*args, &block) + raise ArgumentError if args.empty? + args << {} unless Hash === args.last + args.last[:spec_path] = caller(0)[1] + register_behaviour(Spec::DSL::BehaviourFactory.create(*args, &block)) + end + alias :context :describe + + def respond_to(*names) + Spec::Matchers::RespondTo.new(*names) + end + +private + + def register_behaviour(behaviour) + if behaviour.shared? + Spec::DSL::Behaviour.add_shared_behaviour(behaviour) + else + behaviour_runner.add_behaviour(behaviour) + end + end + + def behaviour_runner + # TODO: Figure out a better way to get this considered "covered" and keep this statement on multiple lines + unless $behaviour_runner; \ + $behaviour_runner = ::Spec::Runner::OptionParser.new.create_behaviour_runner(ARGV.dup, STDERR, STDOUT, false); \ + at_exit { $behaviour_runner.run(nil, false) }; \ + end + $behaviour_runner + end +end diff --git a/test/lib/spec/runner/extensions/object.rb b/spec/lib/spec/runner/extensions/object.rb similarity index 100% rename from test/lib/spec/runner/extensions/object.rb rename to spec/lib/spec/runner/extensions/object.rb diff --git a/test/lib/spec/runner/formatter.rb b/spec/lib/spec/runner/formatter.rb similarity index 53% rename from test/lib/spec/runner/formatter.rb rename to spec/lib/spec/runner/formatter.rb index f62e81733..17512d958 100644 --- a/test/lib/spec/runner/formatter.rb +++ b/spec/lib/spec/runner/formatter.rb @@ -1,5 +1,9 @@ +require 'spec/runner/formatter/base_formatter' require 'spec/runner/formatter/base_text_formatter' require 'spec/runner/formatter/progress_bar_formatter' require 'spec/runner/formatter/rdoc_formatter' require 'spec/runner/formatter/specdoc_formatter' require 'spec/runner/formatter/html_formatter' +require 'spec/runner/formatter/failing_examples_formatter' +require 'spec/runner/formatter/failing_behaviours_formatter' +require 'spec/runner/formatter/snippet_extractor' diff --git a/spec/lib/spec/runner/formatter/base_formatter.rb b/spec/lib/spec/runner/formatter/base_formatter.rb new file mode 100644 index 000000000..7cc43ef0e --- /dev/null +++ b/spec/lib/spec/runner/formatter/base_formatter.rb @@ -0,0 +1,76 @@ +module Spec + module Runner + module Formatter + # Baseclass for formatters that implements all required methods as no-ops. + class BaseFormatter + def initialize(where) + @where = where + end + + # This method is invoked before any examples are run, right after + # they have all been collected. This can be useful for special + # formatters that need to provide progress on feedback (graphical ones) + # + # This method will only be invoked once, and the next one to be invoked + # is #add_behaviour + def start(example_count) + end + + # This method is invoked at the beginning of the execution of each behaviour. + # +name+ is the name of the behaviour and +first+ is true if it is the + # first behaviour - otherwise it's false. + # + # The next method to be invoked after this is #example_failed or #example_finished + def add_behaviour(name) + end + + # This method is invoked when an +example+ starts. + def example_started(example) + end + + # This method is invoked when an +example+ passes. + def example_passed(example) + end + + # This method is invoked when an +example+ fails, i.e. an exception occurred + # inside it (such as a failed should or other exception). +counter+ is the + # sequence number of the failure (starting at 1) and +failure+ is the associated + # Failure object. + def example_failed(example, counter, failure) + end + + # This method is invoked when an example is not yet implemented (i.e. has not + # been provided a block), or when an ExamplePendingError is raised. + # +name+ is the name of the example. + # +message+ is the message from the ExamplePendingError, if it exists, or the + # default value of "Not Yet Implemented" + def example_pending(behaviour_name, example_name, message) + end + + # This method is invoked after all of the examples have executed. The next method + # to be invoked after this one is #dump_failure (once for each failed example), + def start_dump + end + + # Dumps detailed information about an example failure. + # This method is invoked for each failed example after all examples have run. +counter+ is the sequence number + # of the associated example. +failure+ is a Failure object, which contains detailed + # information about the failure. + def dump_failure(counter, failure) + end + + # This method is invoked after the dumping of examples and failures. + def dump_summary(duration, example_count, failure_count, pending_count) + end + + # This gets invoked after the summary if option is set to do so. + def dump_pending + end + + # This method is invoked at the very end. Allows the formatter to clean up, like closing open streams. + def close + end + end + end + end +end diff --git a/spec/lib/spec/runner/formatter/base_text_formatter.rb b/spec/lib/spec/runner/formatter/base_text_formatter.rb new file mode 100644 index 000000000..c3cf01b76 --- /dev/null +++ b/spec/lib/spec/runner/formatter/base_text_formatter.rb @@ -0,0 +1,130 @@ +module Spec + module Runner + module Formatter + # Baseclass for text-based formatters. Can in fact be used for + # non-text based ones too - just ignore the +output+ constructor + # argument. + class BaseTextFormatter < BaseFormatter + attr_writer :dry_run + + # Creates a new instance that will write to +where+. If +where+ is a + # String, output will be written to the File with that name, otherwise + # +where+ is exected to be an IO (or an object that responds to #puts and #write). + def initialize(where) + super(where) + if where.is_a?(String) + @output = File.open(where, 'w') + elsif where == STDOUT + @output = Kernel + def @output.flush + STDOUT.flush + end + else + @output = where + end + @colour = false + @dry_run = false + @snippet_extractor = SnippetExtractor.new + @pending_examples = [] + end + + def example_pending(behaviour_name, example_name, message) + @pending_examples << ["#{behaviour_name} #{example_name}", message] + end + + def colour=(colour) + @colour = colour + begin ; require 'Win32/Console/ANSI' if @colour && PLATFORM =~ /win32/ ; rescue LoadError ; raise "You must gem install win32console to use colour on Windows" ; end + end + + def dump_failure(counter, failure) + @output.puts + @output.puts "#{counter.to_s})" + @output.puts colourise("#{failure.header}\n#{failure.exception.message}", failure) + @output.puts format_backtrace(failure.exception.backtrace) + @output.flush + end + + def colourise(s, failure) + if(failure.expectation_not_met?) + red(s) + elsif(failure.pending_fixed?) + blue(s) + else + magenta(s) + end + end + + def dump_summary(duration, example_count, failure_count, pending_count) + return if @dry_run + @output.puts + @output.puts "Finished in #{duration} seconds" + @output.puts + + summary = "#{example_count} example#{'s' unless example_count == 1}, #{failure_count} failure#{'s' unless failure_count == 1}" + summary << ", #{pending_count} pending" if pending_count > 0 + + if failure_count == 0 + if pending_count > 0 + @output.puts yellow(summary) + else + @output.puts green(summary) + end + else + @output.puts red(summary) + end + @output.flush + dump_pending + end + + def dump_pending + unless @pending_examples.empty? + @output.puts + @output.puts "Pending:" + @pending_examples.each do |pending_example| + @output.puts "#{pending_example[0]} (#{pending_example[1]})" + end + end + @output.flush + end + + def close + if IO === @output + @output.close + end + end + + def format_backtrace(backtrace) + return "" if backtrace.nil? + backtrace.map { |line| backtrace_line(line) }.join("\n") + end + + protected + + def backtrace_line(line) + line.sub(/\A([^:]+:\d+)$/, '\\1:') + end + + def colour(text, colour_code) + return text unless @colour && output_to_tty? + "#{colour_code}#{text}\e[0m" + end + + def output_to_tty? + begin + @output == Kernel || @output.tty? + rescue NoMethodError + false + end + end + + def green(text); colour(text, "\e[32m"); end + def red(text); colour(text, "\e[31m"); end + def magenta(text); colour(text, "\e[35m"); end + def yellow(text); colour(text, "\e[33m"); end + def blue(text); colour(text, "\e[34m"); end + + end + end + end +end diff --git a/spec/lib/spec/runner/formatter/failing_behaviours_formatter.rb b/spec/lib/spec/runner/formatter/failing_behaviours_formatter.rb new file mode 100644 index 000000000..2b3940fd3 --- /dev/null +++ b/spec/lib/spec/runner/formatter/failing_behaviours_formatter.rb @@ -0,0 +1,29 @@ +module Spec + module Runner + module Formatter + class FailingBehavioursFormatter < BaseTextFormatter + def add_behaviour(behaviour_name) + if behaviour_name =~ /(.*) \(druby.*\)$/ + @behaviour_name = $1 + else + @behaviour_name = behaviour_name + end + end + + def example_failed(example, counter, failure) + unless @behaviour_name.nil? + @output.puts @behaviour_name + @behaviour_name = nil + @output.flush + end + end + + def dump_failure(counter, failure) + end + + def dump_summary(duration, example_count, failure_count, pending_count) + end + end + end + end +end diff --git a/spec/lib/spec/runner/formatter/failing_examples_formatter.rb b/spec/lib/spec/runner/formatter/failing_examples_formatter.rb new file mode 100644 index 000000000..9728deaf0 --- /dev/null +++ b/spec/lib/spec/runner/formatter/failing_examples_formatter.rb @@ -0,0 +1,22 @@ +module Spec + module Runner + module Formatter + class FailingExamplesFormatter < BaseTextFormatter + def add_behaviour(behaviour_name) + @behaviour_name = behaviour_name + end + + def example_failed(example, counter, failure) + @output.puts "#{@behaviour_name} #{example.description}" + @output.flush + end + + def dump_failure(counter, failure) + end + + def dump_summary(duration, example_count, failure_count, pending_count) + end + end + end + end +end diff --git a/spec/lib/spec/runner/formatter/html_formatter.rb b/spec/lib/spec/runner/formatter/html_formatter.rb new file mode 100644 index 000000000..d9c422e55 --- /dev/null +++ b/spec/lib/spec/runner/formatter/html_formatter.rb @@ -0,0 +1,323 @@ +require 'erb' + +module Spec + module Runner + module Formatter + class HtmlFormatter < BaseTextFormatter + include ERB::Util # for the #h method + + def initialize(output) + super + @current_behaviour_number = 0 + @current_example_number = 0 + end + + # The number of the currently running behaviour + def current_behaviour_number + @current_behaviour_number + end + + # The number of the currently running example (a global counter) + def current_example_number + @current_example_number + end + + def start(example_count) + @example_count = example_count + + @output.puts html_header + @output.puts report_header + @output.flush + end + + def add_behaviour(name) + @behaviour_red = false + @behaviour_red = false + @current_behaviour_number += 1 + unless current_behaviour_number == 1 + @output.puts " " + @output.puts "" + end + @output.puts "
" + @output.puts "
" + @output.puts "
#{h(name)}
" + @output.flush + end + + def start_dump + @output.puts "
" + @output.puts "
" + @output.flush + end + + def example_started(example) + @current_example_number = example.number + end + + def example_passed(example) + move_progress + @output.puts "
#{h(example.description)}
" + @output.flush + end + + def example_failed(example, counter, failure) + extra = extra_failure_content(failure) + failure_style = failure.pending_fixed? ? 'pending_fixed' : 'failed' + @output.puts " " unless @header_red + @header_red = true + @output.puts " " unless @behaviour_red + @behaviour_red = true + move_progress + @output.puts "
" + @output.puts " #{h(example.description)}" + @output.puts "
" + @output.puts "
#{h(failure.exception.message)}
" unless failure.exception.nil? + @output.puts "
#{format_backtrace(failure.exception.backtrace)}
" unless failure.exception.nil? + @output.puts extra unless extra == "" + @output.puts "
" + @output.puts "
" + @output.flush + end + + def example_pending(behaviour_name, example_name, message) + @output.puts " " unless @header_red + @output.puts " " unless @behaviour_red + move_progress + @output.puts "
#{h(example_name)}
" + @output.flush + end + + # Override this method if you wish to output extra HTML for a failed spec. For example, you + # could output links to images or other files produced during the specs. + # + def extra_failure_content(failure) + "
#{@snippet_extractor.snippet(failure.exception)}
" + end + + def move_progress + percent_done = @example_count == 0 ? 100.0 : ((current_example_number + 1).to_f / @example_count.to_f * 1000).to_i / 10.0 + @output.puts " " + @output.flush + end + + def dump_failure(counter, failure) + end + + def dump_summary(duration, example_count, failure_count, pending_count) + if @dry_run + totals = "This was a dry-run" + else + totals = "#{example_count} example#{'s' unless example_count == 1}, #{failure_count} failure#{'s' unless failure_count == 1}" + totals << ", #{pending_count} pending" if pending_count > 0 + end + @output.puts "" + @output.puts "" + @output.puts "" + @output.puts "" + @output.puts "" + @output.puts "" + @output.flush + end + + def html_header + <<-EOF + + + + + + RSpec results + + + + + + +EOF + end + + def report_header + <<-EOF +
+ + + +
+

RSpec Results

+ +
+

 

+

 

+
+
+ +
+EOF + end + + def global_scripts + <<-EOF +function moveProgressBar(percentDone) { + document.getElementById("rspec-header").style.width = percentDone +"%"; +} +function makeRed(element_id) { + document.getElementById(element_id).style.background = '#C40D0D'; + document.getElementById(element_id).style.color = '#FFFFFF'; +} + +function makeYellow(element_id) { + if (element_id == "rspec-header" && document.getElementById(element_id).style.background != '#C40D0D') + { + document.getElementById(element_id).style.background = '#FAF834'; + document.getElementById(element_id).style.color = '#000000'; + } + else + { + document.getElementById(element_id).style.background = '#FAF834'; + document.getElementById(element_id).style.color = '#000000'; + } +} +EOF + end + + def global_styles + <<-EOF +#rspec-header { + background: #65C400; color: #fff; +} + +.rspec-report h1 { + margin: 0px 10px 0px 10px; + padding: 10px; + font-family: "Lucida Grande", Helvetica, sans-serif; + font-size: 1.8em; +} + +#summary { + margin: 0; padding: 5px 10px; + font-family: "Lucida Grande", Helvetica, sans-serif; + text-align: right; + position: absolute; + top: 0px; + right: 0px; +} + +#summary p { + margin: 0 0 0 2px; +} + +#summary #totals { + font-size: 1.2em; +} + +.behaviour { + margin: 0 10px 5px; + background: #fff; +} + +dl { + margin: 0; padding: 0 0 5px; + font: normal 11px "Lucida Grande", Helvetica, sans-serif; +} + +dt { + padding: 3px; + background: #65C400; + color: #fff; + font-weight: bold; +} + +dd { + margin: 5px 0 5px 5px; + padding: 3px 3px 3px 18px; +} + +dd.spec.passed { + border-left: 5px solid #65C400; + border-bottom: 1px solid #65C400; + background: #DBFFB4; color: #3D7700; +} + +dd.spec.failed { + border-left: 5px solid #C20000; + border-bottom: 1px solid #C20000; + color: #C20000; background: #FFFBD3; +} + +dd.spec.not_implemented { + border-left: 5px solid #FAF834; + border-bottom: 1px solid #FAF834; + background: #FCFB98; color: #131313; +} + +dd.spec.pending_fixed { + border-left: 5px solid #0000C2; + border-bottom: 1px solid #0000C2; + color: #0000C2; background: #D3FBFF; +} + +.backtrace { + color: #000; + font-size: 12px; +} + +a { + color: #BE5C00; +} + +/* Ruby code, style similar to vibrant ink */ +.ruby { + font-size: 12px; + font-family: monospace; + color: white; + background-color: black; + padding: 0.1em 0 0.2em 0; +} + +.ruby .keyword { color: #FF6600; } +.ruby .constant { color: #339999; } +.ruby .attribute { color: white; } +.ruby .global { color: white; } +.ruby .module { color: white; } +.ruby .class { color: white; } +.ruby .string { color: #66FF00; } +.ruby .ident { color: white; } +.ruby .method { color: #FFCC00; } +.ruby .number { color: white; } +.ruby .char { color: white; } +.ruby .comment { color: #9933CC; } +.ruby .symbol { color: white; } +.ruby .regex { color: #44B4CC; } +.ruby .punct { color: white; } +.ruby .escape { color: white; } +.ruby .interp { color: white; } +.ruby .expr { color: white; } + +.ruby .offending { background-color: gray; } +.ruby .linenum { + width: 75px; + padding: 0.1em 1em 0.2em 0; + color: #000000; + background-color: #FFFBD3; +} +EOF + end + end + end + end +end diff --git a/spec/lib/spec/runner/formatter/progress_bar_formatter.rb b/spec/lib/spec/runner/formatter/progress_bar_formatter.rb new file mode 100644 index 000000000..624f06e7c --- /dev/null +++ b/spec/lib/spec/runner/formatter/progress_bar_formatter.rb @@ -0,0 +1,31 @@ +module Spec + module Runner + module Formatter + class ProgressBarFormatter < BaseTextFormatter + def add_behaviour(name) + end + + def example_failed(example, counter, failure) + @output.print colourise('F', failure) + @output.flush + end + + def example_passed(example) + @output.print green('.') + @output.flush + end + + def example_pending(behaviour_name, example_name, message) + super + @output.print yellow('P') + @output.flush + end + + def start_dump + @output.puts + @output.flush + end + end + end + end +end diff --git a/spec/lib/spec/runner/formatter/rdoc_formatter.rb b/spec/lib/spec/runner/formatter/rdoc_formatter.rb new file mode 100644 index 000000000..0fd22ba6c --- /dev/null +++ b/spec/lib/spec/runner/formatter/rdoc_formatter.rb @@ -0,0 +1,24 @@ +module Spec + module Runner + module Formatter + class RdocFormatter < BaseTextFormatter + def add_behaviour(name) + @output.puts "# #{name}" + end + + def example_passed(example) + @output.puts "# * #{example.description}" + @output.flush + end + + def example_failed(example, counter, failure) + @output.puts "# * #{example.description} [#{counter} - FAILED]" + end + + def example_pending(behaviour_name, example_name, message) + @output.puts "# * #{behaviour_name} #{example_name} [PENDING: #{message}]" + end + end + end + end +end diff --git a/spec/lib/spec/runner/formatter/snippet_extractor.rb b/spec/lib/spec/runner/formatter/snippet_extractor.rb new file mode 100644 index 000000000..41119fe46 --- /dev/null +++ b/spec/lib/spec/runner/formatter/snippet_extractor.rb @@ -0,0 +1,52 @@ +module Spec + module Runner + module Formatter + # This class extracts code snippets by looking at the backtrace of the passed error + class SnippetExtractor #:nodoc: + class NullConverter; def convert(code, pre); code; end; end #:nodoc: + begin; require 'rubygems'; require 'syntax/convertors/html'; @@converter = Syntax::Convertors::HTML.for_syntax "ruby"; rescue LoadError => e; @@converter = NullConverter.new; end + + def snippet(error) + raw_code, line = snippet_for(error.backtrace[0]) + highlighted = @@converter.convert(raw_code, false) + highlighted << "\n# gem install syntax to get syntax highlighting" if @@converter.is_a?(NullConverter) + post_process(highlighted, line) + end + + def snippet_for(error_line) + if error_line =~ /(.*):(\d+)/ + file = $1 + line = $2.to_i + [lines_around(file, line), line] + else + ["# Couldn't get snippet for #{error_line}", 1] + end + end + + def lines_around(file, line) + if File.file?(file) + lines = File.open(file).read.split("\n") + min = [0, line-3].max + max = [line+1, lines.length-1].min + selected_lines = [] + selected_lines.join("\n") + lines[min..max].join("\n") + else + "# Couldn't get snippet for #{file}" + end + end + + def post_process(highlighted, offending_line) + new_lines = [] + highlighted.split("\n").each_with_index do |line, i| + new_line = "#{offending_line+i-2}#{line}" + new_line = "#{new_line}" if i == 2 + new_lines << new_line + end + new_lines.join("\n") + end + + end + end + end +end diff --git a/spec/lib/spec/runner/formatter/specdoc_formatter.rb b/spec/lib/spec/runner/formatter/specdoc_formatter.rb new file mode 100644 index 000000000..ad794b238 --- /dev/null +++ b/spec/lib/spec/runner/formatter/specdoc_formatter.rb @@ -0,0 +1,29 @@ +module Spec + module Runner + module Formatter + class SpecdocFormatter < BaseTextFormatter + def add_behaviour(name) + @output.puts + @output.puts name + @output.flush + end + + def example_failed(example, counter, failure) + @output.puts failure.expectation_not_met? ? red("- #{example.description} (FAILED - #{counter})") : magenta("- #{example.description} (ERROR - #{counter})") + @output.flush + end + + def example_passed(example) + @output.puts green("- #{example.description}") + @output.flush + end + + def example_pending(behaviour_name, example_name, message) + super + @output.puts yellow("- #{example_name} (PENDING: #{message})") + @output.flush + end + end + end + end +end diff --git a/test/lib/spec/runner/heckle_runner.rb b/spec/lib/spec/runner/heckle_runner.rb similarity index 78% rename from test/lib/spec/runner/heckle_runner.rb rename to spec/lib/spec/runner/heckle_runner.rb index fd36389de..b6de4ef73 100644 --- a/test/lib/spec/runner/heckle_runner.rb +++ b/spec/lib/spec/runner/heckle_runner.rb @@ -1,71 +1,72 @@ begin require 'rubygems' require 'heckle' rescue LoadError ; raise "You must gem install heckle to use --heckle" ; end module Spec module Runner # Creates a new Heckler configured to heckle all methods in the classes # whose name matches +filter+ class HeckleRunner def initialize(filter, heckle_class=Heckler) @filter = filter @heckle_class = heckle_class end - # Runs all the contexts held by +context_runner+ once for each of the + # Runs all the contexts held by +behaviour_runner+ once for each of the # methods in the matched classes. - def heckle_with(context_runner) + def heckle_with(behaviour_runner) if @filter =~ /(.*)[#\.](.*)/ heckle_method($1, $2) else heckle_class_or_module(@filter) end end def heckle_method(class_name, method_name) verify_constant(class_name) - heckle = @heckle_class.new(class_name, method_name, context_runner) + heckle = @heckle_class.new(class_name, method_name, behaviour_runner) heckle.validate end def heckle_class_or_module(class_or_module_name) verify_constant(class_or_module_name) pattern = /^#{class_or_module_name}/ classes = [] ObjectSpace.each_object(Class) do |klass| classes << klass if klass.name =~ pattern end classes.each do |klass| klass.instance_methods(false).each do |method_name| - heckle = @heckle_class.new(klass.name, method_name, context_runner) + heckle = @heckle_class.new(klass.name, method_name, behaviour_runner) heckle.validate end end end def verify_constant(name) begin # This is defined in Heckle name.to_class rescue raise "Heckling failed - \"#{name}\" is not a known class or module" end end end #Supports Heckle 1.2 and prior (earlier versions used Heckle::Base) class Heckler < (Heckle.const_defined?(:Base) ? Heckle::Base : Heckle) - def initialize(klass_name, method_name, context_runner) + def initialize(klass_name, method_name, behaviour_runner) super(klass_name, method_name) - @context_runner = context_runner + @behaviour_runner = behaviour_runner end def tests_pass? - failure_count = @context_runner.run(false) + paths = [] # We can pass an empty array of paths - our specs are already loaded. + failure_count = @behaviour_runner.run(paths, false) failure_count == 0 end end end end diff --git a/test/lib/spec/runner/heckle_runner_win.rb b/spec/lib/spec/runner/heckle_runner_unsupported.rb similarity index 96% rename from test/lib/spec/runner/heckle_runner_win.rb rename to spec/lib/spec/runner/heckle_runner_unsupported.rb index 031386599..02aa37953 100644 --- a/test/lib/spec/runner/heckle_runner_win.rb +++ b/spec/lib/spec/runner/heckle_runner_unsupported.rb @@ -1,10 +1,10 @@ -module Spec - module Runner - # Dummy implementation for Windows that just fails (Heckle is not supported on Windows) - class HeckleRunner - def initialize(filter) - raise "Heckle not supported on Windows" - end - end - end -end +module Spec + module Runner + # Dummy implementation for Windows that just fails (Heckle is not supported on Windows) + class HeckleRunner + def initialize(filter) + raise "Heckle not supported on Windows" + end + end + end +end diff --git a/spec/lib/spec/runner/option_parser.rb b/spec/lib/spec/runner/option_parser.rb new file mode 100644 index 000000000..1facb85a8 --- /dev/null +++ b/spec/lib/spec/runner/option_parser.rb @@ -0,0 +1,227 @@ +require 'optparse' +require 'stringio' + +module Spec + module Runner + class OptionParser + BUILT_IN_FORMATTERS = { + 'specdoc' => Formatter::SpecdocFormatter, + 's' => Formatter::SpecdocFormatter, + 'html' => Formatter::HtmlFormatter, + 'h' => Formatter::HtmlFormatter, + 'rdoc' => Formatter::RdocFormatter, + 'r' => Formatter::RdocFormatter, + 'progress' => Formatter::ProgressBarFormatter, + 'p' => Formatter::ProgressBarFormatter, + 'failing_examples' => Formatter::FailingExamplesFormatter, + 'e' => Formatter::FailingExamplesFormatter, + 'failing_behaviours' => Formatter::FailingBehavioursFormatter, + 'b' => Formatter::FailingBehavioursFormatter + } + + COMMAND_LINE = { + :diff => ["-D", "--diff [FORMAT]", "Show diff of objects that are expected to be equal when they are not", + "Builtin formats: unified|u|context|c", + "You can also specify a custom differ class", + "(in which case you should also specify --require)"], + :colour => ["-c", "--colour", "--color", "Show coloured (red/green) output"], + :example => ["-e", "--example [NAME|FILE_NAME]", "Execute example(s) with matching name(s). If the argument is", + "the path to an existing file (typically generated by a previous", + "run using --format failing_examples:file.txt), then the examples", + "on each line of thatfile will be executed. If the file is empty,", + "all examples will be run (as if --example was not specified).", + " ", + "If the argument is not an existing file, then it is treated as", + "an example name directly, causing RSpec to run just the example", + "matching that name"], + :specification => ["-s", "--specification [NAME]", "DEPRECATED - use -e instead", "(This will be removed when autotest works with -e)"], + :line => ["-l", "--line LINE_NUMBER", Integer, "Execute behaviout or specification at given line.", + "(does not work for dynamically generated specs)"], + :format => ["-f", "--format FORMAT[:WHERE]", "Specifies what format to use for output. Specify WHERE to tell", + "the formatter where to write the output. All built-in formats", + "expect WHERE to be a file name, and will write to STDOUT if it's", + "not specified. The --format option may be specified several times", + "if you want several outputs", + " ", + "Builtin formats: ", + "progress|p : Text progress", + "specdoc|s : Behaviour doc as text", + "rdoc|r : Behaviour doc as RDoc", + "html|h : A nice HTML report", + "failing_examples|e : Write all failing examples - input for --example", + "failing_behaviours|b : Write all failing behaviours - input for --example", + " ", + "FORMAT can also be the name of a custom formatter class", + "(in which case you should also specify --require to load it)"], + :require => ["-r", "--require FILE", "Require FILE before running specs", + "Useful for loading custom formatters or other extensions.", + "If this option is used it must come before the others"], + :backtrace => ["-b", "--backtrace", "Output full backtrace"], + :loadby => ["-L", "--loadby STRATEGY", "Specify the strategy by which spec files should be loaded.", + "STRATEGY can currently only be 'mtime' (File modification time)", + "By default, spec files are loaded in alphabetical order if --loadby", + "is not specified."], + :reverse => ["-R", "--reverse", "Run examples in reverse order"], + :timeout => ["-t", "--timeout FLOAT", "Interrupt and fail each example that doesn't complete in the", + "specified time"], + :heckle => ["-H", "--heckle CODE", "If all examples pass, this will mutate the classes and methods", + "identified by CODE little by little and run all the examples again", + "for each mutation. The intent is that for each mutation, at least", + "one example *should* fail, and RSpec will tell you if this is not the", + "case. CODE should be either Some::Module, Some::Class or", + "Some::Fabulous#method}"], + :dry_run => ["-d", "--dry-run", "Invokes formatters without executing the examples."], + :options_file => ["-O", "--options PATH", "Read options from a file"], + :generate_options => ["-G", "--generate-options PATH", "Generate an options file for --options"], + :runner => ["-U", "--runner RUNNER", "Use a custom BehaviourRunner."], + :drb => ["-X", "--drb", "Run examples via DRb. (For example against script/spec_server)"], + :version => ["-v", "--version", "Show version"], + :help => ["-h", "--help", "You're looking at it"] + } + + def initialize + @spec_parser = SpecParser.new + @file_factory = File + end + + def create_behaviour_runner(args, err, out, warn_if_no_files) + options = parse(args, err, out, warn_if_no_files) + # Some exit points in parse (--generate-options, --drb) don't return the options, + # but hand over control. In that case we don't want to continue. + return nil unless options.is_a?(Options) + options.configure + options.behaviour_runner + end + + def parse(args, err, out, warn_if_no_files) + options_file = nil + args_copy = args.dup + options = Options.new(err, out) + + opts = ::OptionParser.new do |opts| + opts.banner = "Usage: spec (FILE|DIRECTORY|GLOB)+ [options]" + opts.separator "" + + def opts.rspec_on(name, &block) + on(*COMMAND_LINE[name], &block) + end + + opts.rspec_on(:diff) {|diff| options.parse_diff(diff)} + + opts.rspec_on(:colour) {options.colour = true} + + opts.rspec_on(:example) {|example| options.parse_example(example)} + + opts.rspec_on(:specification) {|example| options.parse_example(example)} + + opts.rspec_on(:line) {|line_number| options.line_number = line_number.to_i} + + opts.rspec_on(:format) {|format| options.parse_format(format)} + + opts.rspec_on(:require) {|req| options.parse_require(req)} + + opts.rspec_on(:backtrace) {options.backtrace_tweaker = NoisyBacktraceTweaker.new} + + opts.rspec_on(:loadby) {|loadby| options.loadby = loadby} + + opts.rspec_on(:reverse) {options.reverse = true} + + opts.rspec_on(:timeout) {|timeout| options.timeout = timeout.to_f} + + opts.rspec_on(:heckle) {|heckle| options.parse_heckle(heckle)} + + opts.rspec_on(:dry_run) {options.dry_run = true} + + opts.rspec_on(:options_file) do |options_file| + return parse_options_file(options_file, out, err, args_copy, warn_if_no_files) + end + + opts.rspec_on(:generate_options) do |options_file| + options.parse_generate_options(options_file, args_copy, out) + end + + opts.rspec_on(:runner) do |runner| + options.runner_arg = runner + end + + opts.rspec_on(:drb) do + return parse_drb(args_copy, out, err, warn_if_no_files) + end + + opts.rspec_on(:version) {parse_version(out)} + + opts.on_tail(*COMMAND_LINE[:help]) {parse_help(opts, out)} + end + opts.parse!(args) + + if args.empty? && warn_if_no_files + err.puts "No files specified." + err.puts opts + exit(6) if err == $stderr + end + + if options.line_number + set_spec_from_line_number(options, args, err) + end + + if options.formatters.empty? + options.formatters << Formatter::ProgressBarFormatter.new(out) + end + + options + end + + def parse_options_file(options_file, out_stream, error_stream, args_copy, warn_if_no_files) + # Remove the --options option and the argument before writing to file + index = args_copy.index("-O") || args_copy.index("--options") + args_copy.delete_at(index) + args_copy.delete_at(index) + + new_args = args_copy + IO.readlines(options_file).map {|l| l.chomp.split " "}.flatten + return CommandLine.run(new_args, error_stream, out_stream, true, warn_if_no_files) + end + + def parse_drb(args_copy, out_stream, error_stream, warn_if_no_files) + # Remove the --drb option + index = args_copy.index("-X") || args_copy.index("--drb") + args_copy.delete_at(index) + + return DrbCommandLine.run(args_copy, error_stream, out_stream, true, warn_if_no_files) + end + + def parse_version(out_stream) + out_stream.puts ::Spec::VERSION::DESCRIPTION + exit if out_stream == $stdout + end + + def parse_help(opts, out_stream) + out_stream.puts opts + exit if out_stream == $stdout + end + + def set_spec_from_line_number(options, args, err) + if options.examples.empty? + if args.length == 1 + if @file_factory.file?(args[0]) + source = @file_factory.open(args[0]) + example = @spec_parser.spec_name_for(source, options.line_number) + options.parse_example(example) + elsif @file_factory.directory?(args[0]) + err.puts "You must specify one file, not a directory when using the --line option" + exit(1) if err == $stderr + else + err.puts "#{args[0]} does not exist" + exit(2) if err == $stderr + end + else + err.puts "Only one file can be specified when using the --line option: #{args.inspect}" + exit(3) if err == $stderr + end + else + err.puts "You cannot use both --line and --example" + exit(4) if err == $stderr + end + end + end + end +end diff --git a/spec/lib/spec/runner/options.rb b/spec/lib/spec/runner/options.rb new file mode 100644 index 000000000..a940133eb --- /dev/null +++ b/spec/lib/spec/runner/options.rb @@ -0,0 +1,175 @@ +module Spec + module Runner + class Options + BUILT_IN_FORMATTERS = { + 'specdoc' => Formatter::SpecdocFormatter, + 's' => Formatter::SpecdocFormatter, + 'html' => Formatter::HtmlFormatter, + 'h' => Formatter::HtmlFormatter, + 'rdoc' => Formatter::RdocFormatter, + 'r' => Formatter::RdocFormatter, + 'progress' => Formatter::ProgressBarFormatter, + 'p' => Formatter::ProgressBarFormatter, + 'failing_examples' => Formatter::FailingExamplesFormatter, + 'e' => Formatter::FailingExamplesFormatter, + 'failing_behaviours' => Formatter::FailingBehavioursFormatter, + 'b' => Formatter::FailingBehavioursFormatter + } + + attr_accessor( + :backtrace_tweaker, + :colour, + :context_lines, + :diff_format, + :differ_class, + :dry_run, + :examples, + :failure_file, + :formatters, + :generate, + :heckle_runner, + :line_number, + :loadby, + :reporter, + :reverse, + :timeout, + :verbose, + :runner_arg, + :behaviour_runner + ) + + def initialize(err, out) + @err, @out = err, out + @backtrace_tweaker = QuietBacktraceTweaker.new + @examples = [] + @formatters = [] + @colour = false + @dry_run = false + end + + def configure + configure_formatters + create_reporter + configure_differ + create_behaviour_runner + end + + def create_behaviour_runner + return nil if @generate + @behaviour_runner = if @runner_arg + klass_name, arg = split_at_colon(@runner_arg) + runner_type = load_class(klass_name, 'behaviour runner', '--runner') + runner_type.new(self, arg) + else + BehaviourRunner.new(self) + end + end + + def configure_formatters + @formatters.each do |formatter| + formatter.colour = @colour if formatter.respond_to?(:colour=) + formatter.dry_run = @dry_run if formatter.respond_to?(:dry_run=) + end + end + + def create_reporter + @reporter = Reporter.new(@formatters, @backtrace_tweaker) + end + + def configure_differ + if @differ_class + Spec::Expectations.differ = @differ_class.new(@diff_format, @context_lines, @colour) + end + end + + def parse_diff(format) + @context_lines = 3 + case format + when :context, 'context', 'c' + @diff_format = :context + when :unified, 'unified', 'u', '', nil + @diff_format = :unified + end + + if [:context,:unified].include? @diff_format + require 'spec/expectations/differs/default' + @differ_class = Spec::Expectations::Differs::Default + else + @diff_format = :custom + @differ_class = load_class(format, 'differ', '--diff') + end + end + + def parse_example(example) + if(File.file?(example)) + @examples = File.open(example).read.split("\n") + else + @examples = [example] + end + end + + def parse_format(format_arg) + format, where = split_at_colon(format_arg) + # This funky regexp checks whether we have a FILE_NAME or not + if where.nil? + raise "When using several --format options only one of them can be without a file" if @out_used + where = @out + @out_used = true + end + + formatter_type = BUILT_IN_FORMATTERS[format] || load_class(format, 'formatter', '--format') + @formatters << formatter_type.new(where) + end + + def parse_require(req) + req.split(",").each{|file| require file} + end + + def parse_heckle(heckle) + heckle_require = [/mswin/, /java/].detect{|p| p =~ RUBY_PLATFORM} ? 'spec/runner/heckle_runner_unsupported' : 'spec/runner/heckle_runner' + require heckle_require + @heckle_runner = HeckleRunner.new(heckle) + end + + def parse_generate_options(options_file, args_copy, out_stream) + # Remove the --generate-options option and the argument before writing to file + index = args_copy.index("-G") || args_copy.index("--generate-options") + args_copy.delete_at(index) + args_copy.delete_at(index) + File.open(options_file, 'w') do |io| + io.puts args_copy.join("\n") + end + out_stream.puts "\nOptions written to #{options_file}. You can now use these options with:" + out_stream.puts "spec --options #{options_file}" + @generate = true + end + + def split_at_colon(s) + if s =~ /([a-zA-Z_]+(?:::[a-zA-Z_]+)*):?(.*)/ + arg = $2 == "" ? nil : $2 + [$1, arg] + else + raise "Couldn't parse #{s.inspect}" + end + end + + def load_class(name, kind, option) + if name =~ /\A(?:::)?([A-Z]\w*(?:::[A-Z]\w*)*)\z/ + arg = $2 == "" ? nil : $2 + [$1, arg] + else + m = "#{name.inspect} is not a valid class name" + @err.puts m + raise m + end + begin + eval(name, binding, __FILE__, __LINE__) + rescue NameError => e + @err.puts "Couldn't find #{kind} class #{name}" + @err.puts "Make sure the --require option is specified *before* #{option}" + if $_spec_spec ; raise e ; else exit(1) ; end + end + end + end + end +end diff --git a/spec/lib/spec/runner/reporter.rb b/spec/lib/spec/runner/reporter.rb new file mode 100644 index 000000000..b1dc2a27a --- /dev/null +++ b/spec/lib/spec/runner/reporter.rb @@ -0,0 +1,125 @@ +module Spec + module Runner + class Reporter + + def initialize(formatters, backtrace_tweaker) + @formatters = formatters + @backtrace_tweaker = backtrace_tweaker + clear! + end + + def add_behaviour(name) + @formatters.each{|f| f.add_behaviour(name)} + @behaviour_names << name + end + + def example_started(name) + @formatters.each{|f| f.example_started(name)} + end + + def example_finished(name, error=nil, failure_location=nil, not_implemented = false) + @example_names << name + + if not_implemented + example_pending(@behaviour_names.last, name) + elsif error.nil? + example_passed(name) + elsif Spec::DSL::ExamplePendingError === error + example_pending(@behaviour_names.last, name, error.message) + else + example_failed(name, error, failure_location) + end + end + + def start(number_of_examples) + clear! + @start_time = Time.new + @formatters.each{|f| f.start(number_of_examples)} + end + + def end + @end_time = Time.new + end + + # Dumps the summary and returns the total number of failures + def dump + @formatters.each{|f| f.start_dump} + dump_failures + @formatters.each do |f| + f.dump_summary(duration, @example_names.length, @failures.length, @pending_count) + f.close + end + @failures.length + end + + private + + def clear! + @behaviour_names = [] + @failures = [] + @pending_count = 0 + @example_names = [] + @start_time = nil + @end_time = nil + end + + def dump_failures + return if @failures.empty? + @failures.inject(1) do |index, failure| + @formatters.each{|f| f.dump_failure(index, failure)} + index + 1 + end + end + + def duration + return @end_time - @start_time unless (@end_time.nil? or @start_time.nil?) + return "0.0" + end + + def example_passed(name) + @formatters.each{|f| f.example_passed(name)} + end + + def example_failed(name, error, failure_location) + @backtrace_tweaker.tweak_backtrace(error, failure_location) + example_name = "#{@behaviour_names.last} #{name}" + failure = Failure.new(example_name, error) + @failures << failure + @formatters.each{|f| f.example_failed(name, @failures.length, failure)} + end + + def example_pending(behaviour_name, example_name, message="Not Yet Implemented") + @pending_count += 1 + @formatters.each{|f| f.example_pending(behaviour_name, example_name, message)} + end + + class Failure + attr_reader :exception + + def initialize(example_name, exception) + @example_name = example_name + @exception = exception + end + + def header + if expectation_not_met? + "'#{@example_name}' FAILED" + elsif pending_fixed? + "'#{@example_name}' FIXED" + else + "#{@exception.class.name} in '#{@example_name}'" + end + end + + def pending_fixed? + @exception.is_a?(Spec::DSL::PendingFixedError) + end + + def expectation_not_met? + @exception.is_a?(Spec::Expectations::ExpectationNotMetError) + end + + end + end + end +end diff --git a/spec/lib/spec/runner/spec_parser.rb b/spec/lib/spec/runner/spec_parser.rb new file mode 100644 index 000000000..bc9170065 --- /dev/null +++ b/spec/lib/spec/runner/spec_parser.rb @@ -0,0 +1,50 @@ +module Spec + module Runner + # Parses a spec file and finds the nearest example for a given line number. + class SpecParser + def spec_name_for(io, line_number) + source = io.read + behaviour, behaviour_line = behaviour_at_line(source, line_number) + example, example_line = example_at_line(source, line_number) + if behaviour && example && (behaviour_line < example_line) + "#{behaviour} #{example}" + elsif behaviour + behaviour + else + nil + end + end + + protected + + def behaviour_at_line(source, line_number) + find_above(source, line_number, /^\s*(context|describe)\s+(.*)\s+do/) + end + + def example_at_line(source, line_number) + find_above(source, line_number, /^\s*(specify|it)\s+(.*)\s+do/) + end + + # Returns the context/describe or specify/it name and the line number + def find_above(source, line_number, pattern) + lines_above_reversed(source, line_number).each_with_index do |line, n| + return [parse_description($2), line_number-n] if line =~ pattern + end + nil + end + + def lines_above_reversed(source, line_number) + lines = source.split("\n") + lines[0...line_number].reverse + end + + def parse_description(str) + return str[1..-2] if str =~ /^['"].*['"]$/ + if matches = /^(.*)\s*,\s*['"](.*)['"]$/.match(str) + return ::Spec::DSL::Description.generate_description(matches[1], matches[2]) + end + return str + end + end + end +end diff --git a/spec/lib/spec/test_case_adapter.rb b/spec/lib/spec/test_case_adapter.rb new file mode 100755 index 000000000..992e098fd --- /dev/null +++ b/spec/lib/spec/test_case_adapter.rb @@ -0,0 +1,10 @@ +require 'spec/expectations' +require 'spec/matchers' + +module Test + module Unit + class TestCase + include Spec::Matchers + end + end +end diff --git a/spec/lib/spec/translator.rb b/spec/lib/spec/translator.rb new file mode 100644 index 000000000..c1e07eda4 --- /dev/null +++ b/spec/lib/spec/translator.rb @@ -0,0 +1,114 @@ +require 'fileutils' + +module Spec + class Translator + def translate(from, to) + from = File.expand_path(from) + to = File.expand_path(to) + if File.directory?(from) + translate_dir(from, to) + elsif(from =~ /\.rb$/) + translate_file(from, to) + end + end + + def translate_dir(from, to) + FileUtils.mkdir_p(to) unless File.directory?(to) + Dir["#{from}/*"].each do |sub_from| + path = sub_from[from.length+1..-1] + sub_to = File.join(to, path) + translate(sub_from, sub_to) + end + end + + def translate_file(from, to) + translation = "" + File.open(from) do |io| + io.each_line do |line| + translation << translate_line(line) + end + end + File.open(to, "w") do |io| + io.write(translation) + end + end + + def translate_line(line) + # Translate deprecated mock constraints + line.gsub!(/:any_args/, 'any_args') + line.gsub!(/:anything/, 'anything') + line.gsub!(/:boolean/, 'boolean') + line.gsub!(/:no_args/, 'no_args') + line.gsub!(/:numeric/, 'an_instance_of(Numeric)') + line.gsub!(/:string/, 'an_instance_of(String)') + + return line if line =~ /(should_not|should)_receive/ + + line.gsub!(/(^\s*)context([\s*|\(]['|"|A-Z])/, '\1describe\2') + line.gsub!(/(^\s*)specify([\s*|\(]['|"|A-Z])/, '\1it\2') + line.gsub!(/(^\s*)context_setup(\s*[do|\{])/, '\1before(:all)\2') + line.gsub!(/(^\s*)context_teardown(\s*[do|\{])/, '\1after(:all)\2') + line.gsub!(/(^\s*)setup(\s*[do|\{])/, '\1before(:each)\2') + line.gsub!(/(^\s*)teardown(\s*[do|\{])/, '\1after(:each)\2') + + if line =~ /(.*\.)(should_not|should)(?:_be)(?!_)(.*)/m + pre = $1 + should = $2 + post = $3 + be_or_equal = post =~ /(<|>)/ ? "be" : "equal" + + return "#{pre}#{should} #{be_or_equal}#{post}" + end + + if line =~ /(.*\.)(should_not|should)_(?!not)\s*(.*)/m + pre = $1 + should = $2 + post = $3 + + post.gsub!(/^raise/, 'raise_error') + post.gsub!(/^throw/, 'throw_symbol') + + unless standard_matcher?(post) + post = "be_#{post}" + end + + # Add parenthesis + post.gsub!(/^(\w+)\s+([\w|\.|\,|\(.*\)|\'|\"|\:|@| ]+)(\})/, '\1(\2)\3') # inside a block + post.gsub!(/^(redirect_to)\s+(.*)/, '\1(\2)') # redirect_to, which often has http: + post.gsub!(/^(\w+)\s+([\w|\.|\,|\(.*\)|\{.*\}|\'|\"|\:|@| ]+)/, '\1(\2)') + post.gsub!(/(\s+\))/, ')') + post.gsub!(/\)\}/, ') }') + post.gsub!(/^(\w+)\s+(\/.*\/)/, '\1(\2)') #regexps + line = "#{pre}#{should} #{post}" + end + + line + end + + def standard_matcher?(matcher) + patterns = [ + /^be/, + /^be_close/, + /^eql/, + /^equal/, + /^has/, + /^have/, + /^change/, + /^include/, + /^match/, + /^raise_error/, + /^respond_to/, + /^redirect_to/, + /^satisfy/, + /^throw_symbol/, + # Extra ones that we use in spec_helper + /^pass/, + /^fail/, + /^fail_with/, + ] + matched = patterns.detect{ |p| matcher =~ p } + !matched.nil? + end + + end +end diff --git a/spec/lib/spec/version.rb b/spec/lib/spec/version.rb new file mode 100644 index 000000000..5b1db9b37 --- /dev/null +++ b/spec/lib/spec/version.rb @@ -0,0 +1,23 @@ +module Spec + module VERSION + unless defined? MAJOR + MAJOR = 1 + MINOR = 0 + TINY = 8 + RELEASE_CANDIDATE = nil + + # RANDOM_TOKEN: 0.510454315029681 + REV = "$LastChangedRevision: 2338 $".match(/LastChangedRevision: (\d+)/)[1] + + STRING = [MAJOR, MINOR, TINY].join('.') + TAG = "REL_#{[MAJOR, MINOR, TINY, RELEASE_CANDIDATE].compact.join('_')}".upcase.gsub(/\.|-/, '_') + FULL_VERSION = "#{[MAJOR, MINOR, TINY, RELEASE_CANDIDATE].compact.join('.')} (r#{REV})" + + NAME = "RSpec" + URL = "http://rspec.rubyforge.org/" + + DESCRIPTION = "#{NAME}-#{FULL_VERSION} - BDD for Ruby\n#{URL}" + end + end +end + diff --git a/spec/plugins/mock_frameworks/flexmock.rb b/spec/plugins/mock_frameworks/flexmock.rb new file mode 100644 index 000000000..6875a5222 --- /dev/null +++ b/spec/plugins/mock_frameworks/flexmock.rb @@ -0,0 +1,23 @@ +#!/usr/bin/env ruby +# +# Created by Jim Weirich on 2007-04-10. +# Copyright (c) 2007. All rights reserved. + +require 'flexmock/rspec' + +module Spec + module Plugins + module MockFramework + include FlexMock::MockContainer + def setup_mocks_for_rspec + # No setup required + end + def verify_mocks_for_rspec + flexmock_verify + end + def teardown_mocks_for_rspec + flexmock_close + end + end + end +end diff --git a/spec/plugins/mock_frameworks/mocha.rb b/spec/plugins/mock_frameworks/mocha.rb new file mode 100644 index 000000000..69d11636c --- /dev/null +++ b/spec/plugins/mock_frameworks/mocha.rb @@ -0,0 +1,19 @@ +require 'mocha/standalone' +require 'mocha/object' + +module Spec + module Plugins + module MockFramework + include Mocha::Standalone + def setup_mocks_for_rspec + mocha_setup + end + def verify_mocks_for_rspec + mocha_verify + end + def teardown_mocks_for_rspec + mocha_teardown + end + end + end +end diff --git a/spec/plugins/mock_frameworks/rr.rb b/spec/plugins/mock_frameworks/rr.rb new file mode 100644 index 000000000..c019c18a1 --- /dev/null +++ b/spec/plugins/mock_frameworks/rr.rb @@ -0,0 +1,21 @@ +require 'rr' + +patterns = ::Spec::Runner::QuietBacktraceTweaker::IGNORE_PATTERNS +patterns.push(RR::Errors::BACKTRACE_IDENTIFIER) + +module Spec + module Plugins + module MockFramework + include RR::Extensions::InstanceMethods + def setup_mocks_for_rspec + RR::Space.instance.reset + end + def verify_mocks_for_rspec + RR::Space.instance.verify_doubles + end + def teardown_mocks_for_rspec + RR::Space.instance.reset + end + end + end +end diff --git a/spec/plugins/mock_frameworks/rspec.rb b/spec/plugins/mock_frameworks/rspec.rb new file mode 100644 index 000000000..e606c3089 --- /dev/null +++ b/spec/plugins/mock_frameworks/rspec.rb @@ -0,0 +1,18 @@ +require File.expand_path(File.join(File.dirname(__FILE__), "..", "..", "lib", "spec", "mocks")) + +module Spec + module Plugins + module MockFramework + include Spec::Mocks::SpecMethods + def setup_mocks_for_rspec + $rspec_mocks ||= Spec::Mocks::Space.new + end + def verify_mocks_for_rspec + $rspec_mocks.verify_all + end + def teardown_mocks_for_rspec + $rspec_mocks.reset_all + end + end + end +end diff --git a/spec/spec.opts b/spec/spec.opts new file mode 100644 index 000000000..2cac5f260 --- /dev/null +++ b/spec/spec.opts @@ -0,0 +1,5 @@ +--colour +--format +s +--loadby +mtime \ No newline at end of file diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb new file mode 100644 index 000000000..aa56fd93e --- /dev/null +++ b/spec/spec_helper.rb @@ -0,0 +1,13 @@ +dir = File.dirname(__FILE__) +$:.unshift("#{dir}/lib").unshift("#{dir}/../lib") + +# Add the old test dir, so that we can still find mocha and spec +$:.unshift("#{dir}/../test/lib") + +require 'mocha' +require 'spec' +require 'puppettest' + +Spec::Runner.configure do |config| + config.mock_with :mocha +end diff --git a/spec/unit/node/configuration.rb b/spec/unit/node/configuration.rb new file mode 100755 index 000000000..4429fe3a3 --- /dev/null +++ b/spec/unit/node/configuration.rb @@ -0,0 +1,135 @@ +#!/usr/bin/env ruby + +require File.dirname(__FILE__) + '/../../spec_helper' + +describe Puppet::Node::Configuration, " when compiling" do + it "should accept tags" do + config = Puppet::Node::Configuration.new("mynode") + config.tag("one") + config.tags.should == %w{one} + end + + it "should accept multiple tags at once" do + config = Puppet::Node::Configuration.new("mynode") + config.tag("one", "two") + config.tags.should == %w{one two} + end + + it "should convert all tags to strings" do + config = Puppet::Node::Configuration.new("mynode") + config.tag("one", :two) + config.tags.should == %w{one two} + end + + it "should tag with both the qualified name and the split name" do + config = Puppet::Node::Configuration.new("mynode") + config.tag("one::two") + config.tags.include?("one").should be_true + config.tags.include?("one::two").should be_true + end + + it "should accept classes" do + config = Puppet::Node::Configuration.new("mynode") + config.add_class("one") + config.classes.should == %w{one} + config.add_class("two", "three") + config.classes.should == %w{one two three} + end + + it "should tag itself with passed class names" do + config = Puppet::Node::Configuration.new("mynode") + config.add_class("one") + config.tags.should == %w{one} + end +end + +describe Puppet::Node::Configuration, " when extracting" do + it "should return extraction result as the method result" do + config = Puppet::Node::Configuration.new("mynode") + config.expects(:extraction_format).returns(:whatever) + config.expects(:extract_to_whatever).returns(:result) + config.extract.should == :result + end +end + +describe Puppet::Node::Configuration, " when extracting transobjects" do + + def mkscope + @parser = Puppet::Parser::Parser.new :Code => "" + @node = Puppet::Node.new("mynode") + @compile = Puppet::Parser::Compile.new(@node, @parser) + + # XXX This is ridiculous. + @compile.send(:evaluate_main) + @scope = @compile.topscope + end + + def mkresource(type, name) + Puppet::Parser::Resource.new(:type => type, :title => name, :source => @source, :scope => @scope) + end + + # This isn't really a spec-style test, but I don't know how better to do it. + it "should transform the resource graph into a tree of TransBuckets and TransObjects" do + config = Puppet::Node::Configuration.new("mynode") + + @scope = mkscope + @source = mock 'source' + + defined = mkresource("class", :main) + builtin = mkresource("file", "/yay") + + config.add_edge!(defined, builtin) + + bucket = [] + bucket.expects(:classes=).with(config.classes) + defined.stubs(:builtin?).returns(false) + defined.expects(:to_transbucket).returns(bucket) + builtin.expects(:to_transobject).returns(:builtin) + + config.extract_to_transportable.should == [:builtin] + end + + # Now try it with a more complicated graph -- a three tier graph, each tier + it "should transform arbitrarily deep graphs into isomorphic trees" do + config = Puppet::Node::Configuration.new("mynode") + + @scope = mkscope + @scope.stubs(:tags).returns([]) + @source = mock 'source' + + # Create our scopes. + top = mkresource "class", :main + topbucket = [] + topbucket.expects(:classes=).with([]) + top.expects(:to_trans).returns(topbucket) + topres = mkresource "file", "/top" + topres.expects(:to_trans).returns(:topres) + config.add_edge! top, topres + + middle = mkresource "class", "middle" + middle.expects(:to_trans).returns([]) + config.add_edge! top, middle + midres = mkresource "file", "/mid" + midres.expects(:to_trans).returns(:midres) + config.add_edge! middle, midres + + bottom = mkresource "class", "bottom" + bottom.expects(:to_trans).returns([]) + config.add_edge! middle, bottom + botres = mkresource "file", "/bot" + botres.expects(:to_trans).returns(:botres) + config.add_edge! bottom, botres + + toparray = config.extract_to_transportable + + # This is annoying; it should look like: + # [[[:botres], :midres], :topres] + # but we can't guarantee sort order. + toparray.include?(:topres).should be_true + + midarray = toparray.find { |t| t.is_a?(Array) } + midarray.include?(:midres).should be_true + botarray = midarray.find { |t| t.is_a?(Array) } + botarray.include?(:botres).should be_true + end +end diff --git a/spec/unit/other/modules.rb b/spec/unit/other/modules.rb new file mode 100755 index 000000000..0ab37aa9e --- /dev/null +++ b/spec/unit/other/modules.rb @@ -0,0 +1,156 @@ +#!/usr/bin/env ruby + +require File.dirname(__FILE__) + '/../../spec_helper' + +describe Puppet::Module, " when building its search path" do + include PuppetTest + + it "should ignore unqualified paths in the search path" do + Puppet[:modulepath] = "something:/my/something" + File.stubs(:directory?).returns(true) + Puppet::Module.modulepath.should == %w{/my/something} + end + + it "should ignore paths that do not exist" do + Puppet[:modulepath] = "/yes:/no" + File.expects(:directory?).with("/yes").returns(true) + File.expects(:directory?).with("/no").returns(false) + Puppet::Module.modulepath.should == %w{/yes} + end + + it "should prepend PUPPETLIB in search path when set" do + Puppet[:modulepath] = "/my/mod:/other/mod" + ENV["PUPPETLIB"] = "/env/mod:/myenv/mod" + File.stubs(:directory?).returns(true) + Puppet::Module.modulepath.should == %w{/env/mod /myenv/mod /my/mod /other/mod} + end + + it "should use the environment-specific search path when a node environment is provided" do + Puppet.config.expects(:value).with(:modulepath, "myenv").returns("/mone:/mtwo") + File.stubs(:directory?).returns(true) + Puppet::Module.modulepath("myenv").should == %w{/mone /mtwo} + end + + after do + ENV["PUPPETLIB"] = nil + end +end + +describe Puppet::Module, " when searching for modules" do + it "should find modules in the search path" do + path = %w{/dir/path} + Puppet::Module.stubs(:modulepath).returns(path) + File.stubs(:directory?).returns(true) + mod = Puppet::Module.find("mymod") + mod.should be_an_instance_of(Puppet::Module) + mod.path.should == "/dir/path/mymod" + end + + it "should not search for fully qualified modules" do + path = %w{/dir/path} + Puppet::Module.expects(:modulepath).never + File.expects(:directory?).never + Puppet::Module.find("/mymod").should be_nil + end + + it "should search for modules in the order specified in the search path" do + Puppet[:modulepath] = "/one:/two:/three" + Puppet::Module.stubs(:modulepath).returns %w{/one /two /three} + File.expects(:directory?).with("/one/mod").returns(false) + File.expects(:directory?).with("/two/mod").returns(true) + File.expects(:directory?).with("/three/mod").never + mod = Puppet::Module.find("mod") + mod.path.should == "/two/mod" + end + + it "should use a node environment if specified" do + Puppet::Module.expects(:modulepath).with("myenv").returns([]) + Puppet::Module.find("mymod", "myenv") + end +end + +describe Puppet::Module, " when searching for templates" do + it "should return fully-qualified templates directly" do + Puppet::Module.expects(:modulepath).never + Puppet::Module.find_template("/my/template").should == "/my/template" + end + + it "should return the template from the first found module" do + Puppet[:modulepath] = "/one:/two" + File.stubs(:directory?).returns(true) + Puppet::Module.find_template("mymod/mytemplate").should == "/one/mymod/templates/mytemplate" + end + + it "should use the main templatedir if no module is found" do + Puppet.config.expects(:value).with(:templatedir, nil).returns("/my/templates") + Puppet::Module.expects(:find).with("mymod", nil).returns(nil) + Puppet::Module.find_template("mymod/mytemplate").should == "/my/templates/mymod/mytemplate" + end + + it "should return unqualified templates directly in the template dir" do + Puppet.config.expects(:value).with(:templatedir, nil).returns("/my/templates") + Puppet::Module.expects(:find).never + Puppet::Module.find_template("mytemplate").should == "/my/templates/mytemplate" + end + + it "should use the environment templatedir if no module is found and an environment is specified" do + Puppet.config.expects(:value).with(:templatedir, "myenv").returns("/myenv/templates") + Puppet::Module.expects(:find).with("mymod", "myenv").returns(nil) + Puppet::Module.find_template("mymod/mytemplate", "myenv").should == "/myenv/templates/mymod/mytemplate" + end + + it "should use the node environment if specified" do + Puppet.config.expects(:value).with(:modulepath, "myenv").returns("/my/templates") + File.stubs(:directory?).returns(true) + Puppet::Module.find_template("mymod/envtemplate", "myenv").should == "/my/templates/mymod/templates/envtemplate" + end + + after { Puppet.config.clear } +end + +describe Puppet::Module, " when searching for manifests" do + it "should return the manifests from the first found module" do + Puppet[:modulepath] = "/one:/two" + File.stubs(:directory?).returns(true) + Dir.expects(:glob).with("/one/mymod/manifests/init.pp").returns(%w{/one/mymod/manifests/init.pp}) + Puppet::Module.find_manifests("mymod/init.pp").should == ["/one/mymod/manifests/init.pp"] + end + + it "should search the cwd if no module is found" do + Puppet[:modulepath] = "/one:/two" + File.stubs(:find).returns(nil) + cwd = Dir.getwd + Dir.expects(:glob).with("#{cwd}/mymod/init.pp").returns(["#{cwd}/mymod/init.pp"]) + Puppet::Module.find_manifests("mymod/init.pp").should == ["#{cwd}/mymod/init.pp"] + end + + it "should use the node environment if specified" do + Puppet.config.expects(:value).with(:modulepath, "myenv").returns("/env/modules") + File.stubs(:directory?).returns(true) + Dir.expects(:glob).with("/env/modules/mymod/manifests/envmanifest.pp").returns(%w{/env/modules/mymod/manifests/envmanifest.pp}) + Puppet::Module.find_manifests("mymod/envmanifest.pp", :environment => "myenv").should == ["/env/modules/mymod/manifests/envmanifest.pp"] + end + + it "should return all manifests matching the glob pattern" do + Puppet.config.expects(:value).with(:modulepath, nil).returns("/my/modules") + File.stubs(:directory?).returns(true) + Dir.expects(:glob).with("/my/modules/mymod/manifests/yay/*.pp").returns(%w{/one /two}) + Puppet::Module.find_manifests("mymod/yay/*.pp").should == %w{/one /two} + end + + it "should default to the 'init.pp' file in the manifests directory" do + Puppet.config.expects(:value).with(:modulepath, nil).returns("/my/modules") + File.stubs(:directory?).returns(true) + Dir.expects(:glob).with("/my/modules/mymod/manifests/init.pp").returns(%w{my manifest}) + Puppet::Module.find_manifests("mymod").should == %w{my manifest} + end + + after { Puppet.config.clear } +end + +describe Puppet::Module, " when returning files" do + it "should return the path to the module's 'files' directory" do + mod = Puppet::Module.send(:new, "mymod", "/my/mod") + mod.files.should == "/my/mod/files" + end +end diff --git a/spec/unit/other/node.rb b/spec/unit/other/node.rb new file mode 100755 index 000000000..66d5ba9d7 --- /dev/null +++ b/spec/unit/other/node.rb @@ -0,0 +1,101 @@ +#!/usr/bin/env ruby + +require File.dirname(__FILE__) + '/../../spec_helper' + +describe Puppet::Node, " when initializing" do + before do + @node = Puppet::Node.new("testnode") + end + + it "should set the node name" do + @node.name.should == "testnode" + end + + it "should default to an empty parameter hash" do + @node.parameters.should == {} + end + + it "should default to an empty class array" do + @node.classes.should == [] + end + + it "should note its creation time" do + @node.time.should be_instance_of(Time) + end + + it "should accept parameters passed in during initialization" do + params = {"a" => "b"} + @node = Puppet::Node.new("testing", :parameters => params) + @node.parameters.should == params + end + + it "should accept classes passed in during initialization" do + classes = %w{one two} + @node = Puppet::Node.new("testing", :classes => classes) + @node.classes.should == classes + end + + it "should always return classes as an array" do + @node = Puppet::Node.new("testing", :classes => "myclass") + @node.classes.should == ["myclass"] + end + + it "should accept the environment during initialization" do + @node = Puppet::Node.new("testing", :environment => "myenv") + @node.environment.should == "myenv" + end + + it "should accept names passed in" do + @node = Puppet::Node.new("testing", :names => ["myenv"]) + @node.names.should == ["myenv"] + end +end + +describe Puppet::Node, " when returning the environment" do + before do + @node = Puppet::Node.new("testnode") + end + + it "should return the 'environment' fact if present and there is no explicit environment" do + @node.parameters = {"environment" => "myenv"} + @node.environment.should == "myenv" + end + + it "should return the central environment if there is no environment fact nor explicit environment" do + Puppet.config.expects(:[]).with(:environment).returns(:centralenv) + @node.environment.should == :centralenv + end + + it "should not use an explicit environment that is an empty string" do + @node.environment == "" + @node.environment.should be_nil + end + + it "should not use an environment fact that is an empty string" do + @node.parameters = {"environment" => ""} + @node.environment.should be_nil + end + + it "should not use an explicit environment that is an empty string" do + Puppet.config.expects(:[]).with(:environment).returns(nil) + @node.environment.should be_nil + end +end + +describe Puppet::Node, " when merging facts" do + before do + @node = Puppet::Node.new("testnode") + end + + it "should prefer parameters already set on the node over facts from the node" do + @node.parameters = {"one" => "a"} + @node.fact_merge("one" => "c") + @node.parameters["one"].should == "a" + end + + it "should add passed parameters to the parameter list" do + @node.parameters = {"one" => "a"} + @node.fact_merge("two" => "b") + @node.parameters["two"].should == "b" + end +end diff --git a/spec/unit/parser/compile.rb b/spec/unit/parser/compile.rb new file mode 100755 index 000000000..f6b3c9b3f --- /dev/null +++ b/spec/unit/parser/compile.rb @@ -0,0 +1,52 @@ +#!/usr/bin/env ruby + +require File.dirname(__FILE__) + '/../../spec_helper' + +describe Puppet::Parser::Compile, " when compiling" do + before do + @node = stub 'node', :name => 'mynode' + @parser = stub 'parser', :version => "1.0" + @compile = Puppet::Parser::Compile.new(@node, @parser) + end + + def compile_methods + [:set_node_parameters, :evaluate_main, :evaluate_ast_node, :evaluate_node_classes, :evaluate_generators, :fail_on_unevaluated, + :finish, :store, :extract] + end + + # Stub all of the main compile methods except the ones we're specifically interested in. + def compile_stub(*except) + (compile_methods - except).each { |m| @compile.stubs(m) } + end + + it "should set node parameters as variables in the top scope" do + params = {"a" => "b", "c" => "d"} + @node.stubs(:parameters).returns(params) + compile_stub(:set_node_parameters) + @compile.compile + @compile.topscope.lookupvar("a").should == "b" + @compile.topscope.lookupvar("c").should == "d" + end + + it "should evaluate any existing classes named in the node" do + classes = %w{one two three four} + main = stub 'main' + one = stub 'one', :classname => "one" + three = stub 'three', :classname => "three" + @node.stubs(:name).returns("whatever") + @compile.parser.expects(:findclass).with("", "").returns(main) + @compile.parser.expects(:findclass).with("", "one").returns(one) + @compile.parser.expects(:findclass).with("", "two").returns(nil) + @compile.parser.expects(:findclass).with("", "three").returns(three) + @compile.parser.expects(:findclass).with("", "four").returns(nil) + @node.stubs(:classes).returns(classes) + @compile.send :evaluate_main + @compile.send :evaluate_node_classes + + # Now make sure we've created the appropriate resources. + @compile.resources.find { |r| r.to_s == "Class[one]" }.should be_an_instance_of(Puppet::Parser::Resource) + @compile.resources.find { |r| r.to_s == "Class[three]" }.should be_an_instance_of(Puppet::Parser::Resource) + @compile.resources.find { |r| r.to_s == "Class[two]" }.should be_nil + @compile.resources.find { |r| r.to_s == "Class[four]" }.should be_nil + end +end diff --git a/spec/unit/parser/interpreter.rb b/spec/unit/parser/interpreter.rb new file mode 100755 index 000000000..c0f9d54b3 --- /dev/null +++ b/spec/unit/parser/interpreter.rb @@ -0,0 +1,194 @@ +#!/usr/bin/env ruby + +require File.dirname(__FILE__) + '/../../spec_helper' + +describe Puppet::Parser::Interpreter, " when initializing" do + it "should default to neither code nor file" do + interp = Puppet::Parser::Interpreter.new + interp.code.should be_nil + interp.file.should be_nil + end + + it "should set the code to parse" do + interp = Puppet::Parser::Interpreter.new :Code => :code + interp.code.should equal(:code) + end + + it "should set the file to parse" do + interp = Puppet::Parser::Interpreter.new :Manifest => :file + interp.file.should equal(:file) + end + + it "should set code and ignore manifest when both are present" do + interp = Puppet::Parser::Interpreter.new :Code => :code, :Manifest => :file + interp.code.should equal(:code) + interp.file.should be_nil + end + + it "should default to usenodes" do + interp = Puppet::Parser::Interpreter.new + interp.usenodes?.should be_true + end + + it "should allow disabling of usenodes" do + interp = Puppet::Parser::Interpreter.new :UseNodes => false + interp.usenodes?.should be_false + end +end + +describe Puppet::Parser::Interpreter, " when creating parser instances" do + before do + @interp = Puppet::Parser::Interpreter.new + @parser = mock('parser') + end + + it "should create a parser with code passed in at initialization time" do + @interp.code = :some_code + @parser.expects(:string=).with(:some_code) + @parser.expects(:parse) + Puppet::Parser::Parser.expects(:new).with(:environment => :myenv).returns(@parser) + @interp.send(:create_parser, :myenv).object_id.should equal(@parser.object_id) + end + + it "should create a parser with a file passed in at initialization time" do + @interp.file = :a_file + @parser.expects(:file=).with(:a_file) + @parser.expects(:parse) + Puppet::Parser::Parser.expects(:new).with(:environment => :myenv).returns(@parser) + @interp.send(:create_parser, :myenv).should equal(@parser) + end + + it "should create a parser with the main manifest when passed neither code nor file" do + @parser.expects(:parse) + @parser.expects(:file=).with(Puppet[:manifest]) + Puppet::Parser::Parser.expects(:new).with(:environment => :myenv).returns(@parser) + @interp.send(:create_parser, :myenv).should equal(@parser) + end + + it "should return nothing when new parsers fail" do + Puppet::Parser::Parser.expects(:new).with(:environment => :myenv).raises(ArgumentError) + proc { @interp.send(:create_parser, :myenv) }.should raise_error(Puppet::Error) + end + + it "should create parsers with environment-appropriate manifests" do + # Set our per-environment values. We can't just stub :value, because + # it's called by too much of the rest of the code. + text = "[env1]\nmanifest = /t/env1.pp\n[env2]\nmanifest = /t/env2.pp" + file = mock 'file' + file.stubs(:changed?).returns(true) + file.stubs(:file).returns("/whatever") + Puppet.config.stubs(:read_file).with(file).returns(text) + Puppet.config.parse(file) + + parser1 = mock 'parser1' + Puppet::Parser::Parser.expects(:new).with(:environment => :env1).returns(parser1) + parser1.expects(:file=).with("/t/env1.pp") + parser1.expects(:parse) + @interp.send(:create_parser, :env1) + + parser2 = mock 'parser2' + Puppet::Parser::Parser.expects(:new).with(:environment => :env2).returns(parser2) + parser2.expects(:file=).with("/t/env2.pp") + parser2.expects(:parse) + @interp.send(:create_parser, :env2) + end +end + +describe Puppet::Parser::Interpreter, " when managing parser instances" do + before do + @interp = Puppet::Parser::Interpreter.new + @parser = mock('parser') + end + + it "it should an exception when nothing is there and nil is returned" do + @interp.expects(:create_parser).with(:myenv).returns(nil) + @interp.send(:parser, :myenv).should be_nil + end + + it "should create and return a new parser and use the same parser when the parser does not need reparsing" do + @interp.expects(:create_parser).with(:myenv).returns(@parser) + @interp.send(:parser, :myenv).should equal(@parser) + + @parser.expects(:reparse?).returns(false) + @interp.send(:parser, :myenv).should equal(@parser) + end + + it "should create a new parser when reparse is true" do + oldparser = mock('oldparser') + newparser = mock('newparser') + oldparser.expects(:reparse?).returns(true) + oldparser.expects(:clear) + + @interp.expects(:create_parser).with(:myenv).returns(oldparser) + @interp.send(:parser, :myenv).should equal(oldparser) + @interp.expects(:create_parser).with(:myenv).returns(newparser) + @interp.send(:parser, :myenv).should equal(newparser) + end + + it "should keep the old parser if create_parser doesn't return anything." do + # Get the first parser in the hash. + @interp.expects(:create_parser).with(:myenv).returns(@parser) + @interp.send(:parser, :myenv).should equal(@parser) + + # Have it indicate something has changed + @parser.expects(:reparse?).returns(true) + + # But fail to create a new parser + @interp.expects(:create_parser).with(:myenv).returns(nil) + + # And make sure we still get the old valid parser + @interp.send(:parser, :myenv).should equal(@parser) + end + + it "should use different parsers for different environments" do + # get one for the first env + @interp.expects(:create_parser).with(:first_env).returns(@parser) + @interp.send(:parser, :first_env).should equal(@parser) + + other_parser = mock('otherparser') + @interp.expects(:create_parser).with(:second_env).returns(other_parser) + @interp.send(:parser, :second_env).should equal(other_parser) + end +end + +describe Puppet::Parser::Interpreter, " when compiling configurations" do + before do + @interp = Puppet::Parser::Interpreter.new + end + + it "should create a configuration with the node, parser, and whether to use ast nodes" do + node = mock('node') + node.expects(:environment).returns(:myenv) + compile = mock 'compile' + compile.expects(:compile).returns(:config) + parser = mock 'parser' + @interp.expects(:parser).with(:myenv).returns(parser) + @interp.expects(:usenodes?).returns(true) + Puppet::Parser::Compile.expects(:new).with(node, parser, :ast_nodes => true).returns(compile) + @interp.compile(node) + + # Now try it when usenodes is true + @interp = Puppet::Parser::Interpreter.new :UseNodes => false + node.expects(:environment).returns(:myenv) + compile.expects(:compile).returns(:config) + @interp.expects(:parser).with(:myenv).returns(parser) + @interp.expects(:usenodes?).returns(false) + Puppet::Parser::Compile.expects(:new).with(node, parser, :ast_nodes => false).returns(compile) + @interp.compile(node).should equal(:config) + end +end + +describe Puppet::Parser::Interpreter, " when returning configuration version" do + before do + @interp = Puppet::Parser::Interpreter.new + end + + it "should ask the appropriate parser for the configuration version" do + node = mock 'node' + node.expects(:environment).returns(:myenv) + parser = mock 'parser' + parser.expects(:version).returns(:myvers) + @interp.expects(:parser).with(:myenv).returns(parser) + @interp.configuration_version(node).should equal(:myvers) + end +end diff --git a/spec/unit/parser/resource.rb b/spec/unit/parser/resource.rb new file mode 100755 index 000000000..354690711 --- /dev/null +++ b/spec/unit/parser/resource.rb @@ -0,0 +1,39 @@ +#!/usr/bin/env ruby + +require File.dirname(__FILE__) + '/../../spec_helper' + +# LAK: FIXME This is just new tests for resources; I have +# not moved all tests over yet. +describe Puppet::Parser::Resource, " when evaluating" do + before do + @type = Puppet::Parser::Resource + + @parser = Puppet::Parser::Parser.new :Code => "" + @source = @parser.newclass "" + @definition = @parser.newdefine "mydefine" + @class = @parser.newclass "myclass" + @nodedef = @parser.newnode("mynode")[0] + @node = Puppet::Node.new("yaynode") + @compile = Puppet::Parser::Compile.new(@node, @parser) + @scope = @compile.topscope + end + + it "should evaluate the associated AST definition" do + res = @type.new(:type => "mydefine", :title => "whatever", :scope => @scope, :source => @source) + @definition.expects(:evaluate).with(:scope => @scope, :resource => res) + + res.evaluate + end + + it "should evaluate the associated AST class" do + res = @type.new(:type => "class", :title => "myclass", :scope => @scope, :source => @source) + @class.expects(:evaluate).with(:scope => @scope, :resource => res) + res.evaluate + end + + it "should evaluate the associated AST node" do + res = @type.new(:type => "node", :title => "mynode", :scope => @scope, :source => @source) + @nodedef.expects(:evaluate).with(:scope => @scope, :resource => res) + res.evaluate + end +end diff --git a/spec/unit/parser/resource/reference.rb b/spec/unit/parser/resource/reference.rb new file mode 100755 index 000000000..45af3d938 --- /dev/null +++ b/spec/unit/parser/resource/reference.rb @@ -0,0 +1,66 @@ +#!/usr/bin/env ruby + +require File.dirname(__FILE__) + '/../../../spec_helper' + +describe Puppet::Parser::Resource::Reference do + before do + @type = Puppet::Parser::Resource::Reference + end + + it "should require a type" do + proc { @type.new(:title => "yay") }.should raise_error(Puppet::DevError) + end + + it "should require a title" do + proc { @type.new(:type => "file") }.should raise_error(Puppet::DevError) + end + + it "should know when it models a builtin type" do + ref = @type.new(:type => "file", :title => "/tmp/yay") + ref.builtin?.should be_true + ref.builtintype.should equal(Puppet::Type.type(:file)) + end + + it "should return a relationship-style resource reference when asked" do + ref = @type.new(:type => "file", :title => "/tmp/yay") + ref.to_ref.should == ["file", "/tmp/yay"] + end + + it "should return a resource reference string when asked" do + ref = @type.new(:type => "file", :title => "/tmp/yay") + ref.to_s.should == "File[/tmp/yay]" + end +end + +describe Puppet::Parser::Resource::Reference, " when modeling defined types" do + before do + @type = Puppet::Parser::Resource::Reference + + @parser = Puppet::Parser::Parser.new :Code => "" + @definition = @parser.newdefine "mydefine" + @class = @parser.newclass "myclass" + @nodedef = @parser.newnode("mynode")[0] + @node = Puppet::Node.new("yaynode") + + @compile = Puppet::Parser::Compile.new(@node, @parser) + end + + it "should be able to model definitions" do + ref = @type.new(:type => "mydefine", :title => "/tmp/yay", :scope => @compile.topscope) + ref.builtin?.should be_false + ref.definedtype.should equal(@definition) + end + + it "should be able to model classes" do + ref = @type.new(:type => "class", :title => "myclass", :scope => @compile.topscope) + ref.builtin?.should be_false + ref.definedtype.should equal(@class) + end + + it "should be able to model nodes" do + ref = @type.new(:type => "node", :title => "mynode", :scope => @compile.topscope) + ref.builtin?.should be_false + ref.definedtype.object_id.should == @nodedef.object_id + end +end + diff --git a/spec/unit/util/config.rb b/spec/unit/util/config.rb new file mode 100755 index 000000000..348a54893 --- /dev/null +++ b/spec/unit/util/config.rb @@ -0,0 +1,408 @@ +#!/usr/bin/env ruby + +require File.dirname(__FILE__) + '/../../spec_helper' + +describe Puppet::Util::Config, " when specifying defaults" do + before do + @config = Puppet::Util::Config.new + end + + it "should start with no defined parameters" do + @config.params.length.should == 0 + end + + it "should allow specification of default values associated with a section as an array" do + @config.setdefaults(:section, :myvalue => ["defaultval", "my description"]) + end + + it "should not allow duplicate parameter specifications" do + @config.setdefaults(:section, :myvalue => ["a", "b"]) + lambda { @config.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 + @config.setdefaults(:section, :myvalue => {:default => "defaultval", :desc => "my description"}) + end + + it "should consider defined parameters to be valid" do + @config.setdefaults(:section, :myvalue => ["defaultval", "my description"]) + @config.valid?(:myvalue).should be_true + end + + it "should require a description when defaults are specified with an array" do + lambda { @config.setdefaults(:section, :myvalue => ["a value"]) }.should raise_error(ArgumentError) + end + + it "should require a description when defaults are specified with a hash" do + lambda { @config.setdefaults(:section, :myvalue => {:default => "a value"}) }.should raise_error(ArgumentError) + end + + it "should support specifying owner, group, and mode when specifying files" do + @config.setdefaults(:section, :myvalue => {:default => "/some/file", :owner => "blah", :mode => "boo", :group => "yay", :desc => "whatever"}) + end + + it "should support specifying a short name" do + @config.setdefaults(:section, :myvalue => {:default => "w", :desc => "b", :short => "m"}) + end + + it "should fail when short names conflict" do + @config.setdefaults(:section, :myvalue => {:default => "w", :desc => "b", :short => "m"}) + lambda { @config.setdefaults(:section, :myvalue => {:default => "w", :desc => "b", :short => "m"}) }.should raise_error(ArgumentError) + end +end + +describe Puppet::Util::Config, " when setting values" do + before do + @config = Puppet::Util::Config.new + @config.setdefaults :main, :myval => ["val", "desc"] + @config.setdefaults :main, :bool => [true, "desc"] + end + + it "should provide a method for setting values from other objects" do + @config[:myval] = "something else" + @config[:myval].should == "something else" + end + + it "should support a getopt-specific mechanism for setting values" do + @config.handlearg("--myval", "newval") + @config[:myval].should == "newval" + end + + it "should support a getopt-specific mechanism for turning booleans off" do + @config.handlearg("--no-bool") + @config[:bool].should == false + end + + it "should support a getopt-specific mechanism for turning booleans on" do + # Turn it off first + @config[:bool] = false + @config.handlearg("--bool") + @config[:bool].should == true + end + + it "should clear the cache when setting getopt-specific values" do + @config.setdefaults :mysection, :one => ["whah", "yay"], :two => ["$one yay", "bah"] + @config[:two].should == "whah yay" + @config.handlearg("--one", "else") + @config[:two].should == "else yay" + end + + it "should not clear other values when setting getopt-specific values" do + @config[:myval] = "yay" + @config.handlearg("--no-bool") + @config[:myval].should == "yay" + end + + it "should call passed blocks when values are set" do + values = [] + @config.setdefaults(:section, :hooker => {:default => "yay", :desc => "boo", :hook => lambda { |v| values << v }}) + values.should == [] + + @config[:hooker] = "something" + values.should == %w{something} + end + + it "should munge values using the element-specific methods" do + @config[:bool] = "false" + @config[:bool].should == false + end + + it "should prefer cli values to values set in Ruby code" do + @config.handlearg("--myval", "cliarg") + @config[:myval] = "memarg" + @config[:myval].should == "cliarg" + end +end + +describe Puppet::Util::Config, " when returning values" do + before do + @config = Puppet::Util::Config.new + @config.setdefaults :section, :one => ["ONE", "a"], :two => ["$one TWO", "b"], :three => ["$one $two THREE", "c"], :four => ["$two $three FOUR", "d"] + end + + it "should provide a mechanism for returning set values" do + @config[:one] = "other" + @config[:one].should == "other" + end + + it "should interpolate default values for other parameters into returned parameter values" do + @config[:one].should == "ONE" + @config[:two].should == "ONE TWO" + @config[:three].should == "ONE ONE TWO THREE" + end + + it "should interpolate default values that themselves need to be interpolated" do + @config[:four].should == "ONE TWO ONE ONE TWO THREE FOUR" + end + + it "should interpolate set values for other parameters into returned parameter values" do + @config[:one] = "on3" + @config[:two] = "$one tw0" + @config[:three] = "$one $two thr33" + @config[:four] = "$one $two $three f0ur" + @config[:one].should == "on3" + @config[:two].should == "on3 tw0" + @config[:three].should == "on3 on3 tw0 thr33" + @config[:four].should == "on3 on3 tw0 on3 on3 tw0 thr33 f0ur" + end + + it "should not cache interpolated values such that stale information is returned" do + @config[:two].should == "ONE TWO" + @config[:one] = "one" + @config[: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" + file = mock 'file' + file.stubs(:changed?).returns(true) + file.stubs(:file).returns("/whatever") + @config.stubs(:read_file).with(file).returns(text) + @config.parse(file) + + @config.value(:one, "env1").should == "oneval" + @config.value(:one, "env2").should == "twoval" + end + + it "should have a name determined by the 'name' parameter" do + @config.setdefaults(:whatever, :name => ["something", "yayness"]) + @config.name.should == :something + @config[:name] = :other + @config.name.should == :other + end +end + +describe Puppet::Util::Config, " when choosing which value to return" do + before do + @config = Puppet::Util::Config.new + @config.setdefaults :section, + :one => ["ONE", "a"], + :name => ["myname", "w"] + end + + it "should return default values if no values have been set" do + @config[: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" + file = mock 'file' + file.stubs(:changed?).returns(true) + file.stubs(:file).returns("/whatever") + @config.stubs(:parse_file).returns(text) + @config.handlearg("--one", "clival") + @config.parse(file) + + @config[:one].should == "clival" + end + + it "should return values set on the cli before values set in Ruby" do + @config[:one] = "rubyval" + @config.handlearg("--one", "clival") + @config[:one].should == "clival" + end + + it "should return values set in the executable-specific section before values set in the main section" do + text = "[main]\none = mainval\n[myname]\none = nameval\n" + file = mock 'file' + file.stubs(:changed?).returns(true) + file.stubs(:file).returns("/whatever") + @config.stubs(:read_file).with(file).returns(text) + @config.parse(file) + + @config[:one].should == "nameval" + end + + it "should not return values outside of its search path" do + text = "[other]\none = oval\n" + file = "/some/file" + file = mock 'file' + file.stubs(:changed?).returns(true) + file.stubs(:file).returns("/whatever") + @config.stubs(:read_file).with(file).returns(text) + @config.parse(file) + @config[:one].should == "ONE" + end + + it "should return values in a specified environment" do + text = "[env]\none = envval\n" + file = "/some/file" + file = mock 'file' + file.stubs(:changed?).returns(true) + file.stubs(:file).returns("/whatever") + @config.stubs(:read_file).with(file).returns(text) + @config.parse(file) + @config.value(:one, "env").should == "envval" + 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" + file = "/some/file" + file = mock 'file' + file.stubs(:changed?).returns(true) + file.stubs(:file).returns("/whatever") + @config.stubs(:read_file).with(file).returns(text) + @config.parse(file) + @config.value(:one, "env").should == "envval" + end +end + +describe Puppet::Util::Config, " when parsing its configuration" do + before do + @config = Puppet::Util::Config.new + @config.setdefaults :section, :one => ["ONE", "a"], :two => ["$one TWO", "b"], :three => ["$one $two THREE", "c"] + end + + it "should return values set in the configuration file" do + text = "[main] + one = fileval + " + file = "/some/file" + @config.expects(:read_file).with(file).returns(text) + @config.parse(file) + @config[: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" + file = "/some/file" + @config.expects(:read_file).with(file).returns(text) + lambda { @config.parse(file) }.should_not raise_error + end + + it "should support an old parse method when per-executable configuration files still exist" do + # I'm not going to bother testing this method. + @config.should respond_to(:old_parse) + end + + it "should convert booleans in the configuration file into Ruby booleans" do + text = "[main] + one = true + two = false + " + file = "/some/file" + @config.expects(:read_file).with(file).returns(text) + @config.parse(file) + @config[:one].should == true + @config[:two].should == false + end + + it "should convert integers in the configuration file into Ruby Integers" do + text = "[main] + one = 65 + " + file = "/some/file" + @config.expects(:read_file).with(file).returns(text) + @config.parse(file) + @config[:one].should == 65 + end + + it "should support specifying file all metadata (owner, group, mode) in the configuration file" do + @config.setdefaults :section, :myfile => ["/my/file", "a"] + + text = "[main] + myfile = /other/file {owner = luke, group = luke, mode = 644} + " + file = "/some/file" + @config.expects(:read_file).with(file).returns(text) + @config.parse(file) + @config[:myfile].should == "/other/file" + @config.metadata(:myfile).should == {:owner => "luke", :group => "luke", :mode => "644"} + end + + it "should support specifying file a single piece of metadata (owner, group, or mode) in the configuration file" do + @config.setdefaults :section, :myfile => ["/my/file", "a"] + + text = "[main] + myfile = /other/file {owner = luke} + " + file = "/some/file" + @config.expects(:read_file).with(file).returns(text) + @config.parse(file) + @config[:myfile].should == "/other/file" + @config.metadata(:myfile).should == {:owner => "luke"} + end +end + +describe Puppet::Util::Config, " when reparsing its configuration" do + before do + @config = Puppet::Util::Config.new + @config.setdefaults :section, :one => ["ONE", "a"], :two => ["$one TWO", "b"], :three => ["$one $two THREE", "c"] + 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") + @config[:one] = "init" + @config.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. + @config.expects(:read_file).with(file).returns(text).times(2) + @config.reparse + @config[:one].should == "disk-replace" + end + + it "should retain parameters set by cli when configuration files are reparsed" do + @config.handlearg("--one", "clival") + + text = "[main]\none = on-disk\n" + file = mock 'file' + file.stubs(:file).returns("/test/file") + @config.stubs(:read_file).with(file).returns(text) + @config.parse(file) + + @config[: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" + file = mock 'file' + file.stubs(:changed?).returns(true) + file.stubs(:file).returns("/test/file") + @config.expects(:read_file).with(file).returns(text) + @config.parse(file) + @config[:one].should == "disk-init" + + # Now replace the value + text = "[main]\ntwo = disk-replace\n" + @config.expects(:read_file).with(file).returns(text) + @config.parse(file) + #@config.reparse + + # The originally-overridden value should be replaced with the default + @config[:one].should == "ONE" + + # and we should now have the new value in memory + @config[:two].should == "disk-replace" + end +end + +#describe Puppet::Util::Config, " when being used to manage the host machine" do +# it "should provide a method that writes files with the correct modes" +# +# it "should provide a method that creates directories with the correct modes" +# +# it "should provide a method to declare what directories should exist" +# +# it "should provide a method to trigger enforcing of file modes on existing files and directories" +# +# it "should provide a method to convert the file mode enforcement into a Puppet manifest" +# +# it "should provide an option to create needed users and groups" +# +# it "should provide a method to print out the current configuration" +# +# it "should be able to provide all of its parameters in a format compatible with GetOpt::Long" +# +# it "should not attempt to manage files within /dev" +#end diff --git a/test/certmgr/certmgr.rb b/test/certmgr/certmgr.rb index ff0a3b61b..fb1611d7f 100755 --- a/test/certmgr/certmgr.rb +++ b/test/certmgr/certmgr.rb @@ -1,294 +1,300 @@ #!/usr/bin/env ruby $:.unshift("../lib").unshift("../../lib") if __FILE__ =~ /\.rb$/ require 'puppet' require 'puppet/sslcertificates.rb' require 'puppettest' require 'puppettest/certificates' require 'mocha' class TestCertMgr < Test::Unit::TestCase include PuppetTest::Certificates def setup super #@dir = File.join(Puppet[:certdir], "testing") @dir = File.join(@configpath, "certest") system("mkdir -p %s" % @dir) Puppet::Util::SUIDManager.stubs(:asuser).yields end def testCreateSelfSignedCertificate cert = nil name = "testing" newcert = proc { Puppet::SSLCertificates::Certificate.new( :name => name, :selfsign => true ) } assert_nothing_raised { cert = newcert.call() } assert_nothing_raised { cert.mkselfsigned } assert_raise(Puppet::Error) { cert.mkselfsigned } assert_nothing_raised { cert.write } assert(FileTest.exists?(cert.certfile)) assert_nothing_raised { cert.delete } assert_nothing_raised { cert = newcert.call() } assert_nothing_raised { cert.mkselfsigned } assert_nothing_raised { cert.delete } end def disabled_testCreateEncryptedSelfSignedCertificate cert = nil name = "testing" keyfile = mkPassFile assert_nothing_raised { cert = Puppet::SSLCertificates::Certificate.new( :name => name, :selfsign => true, :capass => keyfile ) } assert_nothing_raised { cert.mkselfsigned } assert_nothing_raised { cert.mkhash } assert_raise(Puppet::Error) { cert.mkselfsigned } assert(FileTest.exists?(cert.certfile)) assert(FileTest.exists?(cert.hash)) assert_nothing_raised { cert.delete } assert_nothing_raised { cert.mkselfsigned } assert_nothing_raised { cert.delete } end def testCreateCA ca = nil assert_nothing_raised { ca = Puppet::SSLCertificates::CA.new() } # make the CA again and verify it doesn't fail because everything # still exists assert_nothing_raised { ca = Puppet::SSLCertificates::CA.new() } end def testSignCert ca = mkCA() cert = nil assert_nothing_raised { cert = Puppet::SSLCertificates::Certificate.new( :name => "signedcertest", :property => "TN", :city => "Nashville", :country => "US", :email => "luke@madstop.com", :org => "Reductive", :ou => "Development", :encrypt => mkPassFile() ) } assert_nothing_raised { cert.mkcsr } signedcert = nil cacert = nil assert_nothing_raised { signedcert, cacert = ca.sign(cert.csr) } assert_instance_of(OpenSSL::X509::Certificate, signedcert) assert_instance_of(OpenSSL::X509::Certificate, cacert) assert_nothing_raised { cert.cert = signedcert cert.cacert = cacert cert.write } #system("find %s" % Puppet[:ssldir]) #system("cp -R %s /tmp/ssltesting" % Puppet[:ssldir]) output = nil assert_nothing_raised { output = %x{openssl verify -CAfile #{Puppet[:cacert]} -purpose sslserver #{cert.certfile}} #output = %x{openssl verify -CApath #{Puppet[:certdir]} -purpose sslserver #{cert.certfile}} } assert_equal($?,0) assert_equal(File.join(Puppet[:certdir], "signedcertest.pem: OK\n"), output) end def test_interactiveca ca = nil assert_nothing_raised { ca = Puppet::SSLCertificates::CA.new } # basic initialization hostname = "test.hostname.com" cert = mkcert(hostname) # create the csr csr = nil assert_nothing_raised { csr = cert.mkcsr } assert_nothing_raised { ca.storeclientcsr(csr) } # store it pulledcsr = nil assert_nothing_raised { pulledcsr = ca.getclientcsr(hostname) } assert_equal(csr.to_pem, pulledcsr.to_pem) signedcert = nil assert_nothing_raised { signedcert, cacert = ca.sign(csr) } assert_instance_of(OpenSSL::X509::Certificate, signedcert) newsignedcert = nil assert_nothing_raised { newsignedcert, cacert = ca.getclientcert(hostname) } assert(newsignedcert) assert_equal(signedcert.to_pem, newsignedcert.to_pem) end def test_cafailures ca = mkCA() cert = cacert = nil assert_nothing_raised { cert, cacert = ca.getclientcert("nohost") } assert_nil(cert) end def test_crl ca = mkCA() h1 = mksignedcert(ca, "host1.example.com") h2 = mksignedcert(ca, "host2.example.com") assert(ca.cert.verify(ca.cert.public_key)) assert(h1.verify(ca.cert.public_key)) assert(h2.verify(ca.cert.public_key)) crl = ca.crl assert_not_nil(crl) store = mkStore(ca) assert( store.verify(ca.cert)) assert( store.verify(h1, [ca.cert])) assert( store.verify(h2, [ca.cert])) ca.revoke(h1.serial) + oldcert = File.read(Puppet.config[:cacert]) + oldserial = File.read(Puppet.config[:serial]) + # Recreate the CA from disk ca = mkCA() + newcert = File.read(Puppet.config[:cacert]) + newserial = File.read(Puppet.config[:serial]) + assert_equal(oldcert, newcert, "The certs are not equal after making a new CA.") + assert_equal(oldserial, newserial, "The serials are not equal after making a new CA.") store = mkStore(ca) - assert( store.verify(ca.cert)) - assert(!store.verify(h1, [ca.cert])) - assert( store.verify(h2, [ca.cert])) + assert( store.verify(ca.cert), "Could not verify CA certs after reloading certs.") + assert(!store.verify(h1, [ca.cert]), "Incorrectly verified revoked cert.") + assert( store.verify(h2, [ca.cert]), "Could not verify certs with reloaded CA.") - Puppet.err :yay ca.revoke(h2.serial) assert_equal(1, ca.crl.extensions.size) # Recreate the CA from disk ca = mkCA() store = mkStore(ca) assert( store.verify(ca.cert)) assert(!store.verify(h1, [ca.cert]), "first revoked cert passed") assert(!store.verify(h2, [ca.cert]), "second revoked cert passed") end def test_ttl cert = mksignedcert assert_equal(5 * 365 * 24 * 60 * 60, cert.not_after - cert.not_before) Puppet[:ca_ttl] = 7 * 24 * 60 * 60 cert = mksignedcert assert_equal(7 * 24 * 60 * 60, cert.not_after - cert.not_before) Puppet[:ca_ttl] = "2y" cert = mksignedcert assert_equal(2 * 365 * 24 * 60 * 60, cert.not_after - cert.not_before) Puppet[:ca_ttl] = "2y" cert = mksignedcert assert_equal(2 * 365 * 24 * 60 * 60, cert.not_after - cert.not_before) Puppet[:ca_ttl] = "1h" cert = mksignedcert assert_equal(60 * 60, cert.not_after - cert.not_before) Puppet[:ca_ttl] = "900s" cert = mksignedcert assert_equal(900, cert.not_after - cert.not_before) # This needs to be last, to make sure that setting ca_days # overrides setting ca_ttl Puppet[:ca_days] = 3 cert = mksignedcert assert_equal(3 * 24 * 60 * 60, cert.not_after - cert.not_before) end end # $Id$ diff --git a/test/language/ast.rb b/test/language/ast.rb index 9e00c610d..dbc1d04ed 100755 --- a/test/language/ast.rb +++ b/test/language/ast.rb @@ -1,193 +1,188 @@ #!/usr/bin/env ruby $:.unshift("../lib").unshift("../../lib") if __FILE__ =~ /\.rb$/ require 'puppettest' require 'puppet/parser/interpreter' require 'puppet/parser/parser' require 'puppettest/resourcetesting' require 'puppettest/parsertesting' require 'puppettest/support/collection' class TestAST < Test::Unit::TestCase include PuppetTest::ParserTesting include PuppetTest::ResourceTesting include PuppetTest::Support::Collection def test_if astif = nil astelse = nil fakeelse = FakeAST.new(:else) faketest = FakeAST.new(true) fakeif = FakeAST.new(:if) assert_nothing_raised { astelse = AST::Else.new(:statements => fakeelse) } assert_nothing_raised { astif = AST::IfStatement.new( :test => faketest, :statements => fakeif, :else => astelse ) } # We initialized it to true, so we should get that first ret = nil assert_nothing_raised { ret = astif.evaluate(:scope => "yay") } assert_equal(:if, ret) # Now set it to false and check that faketest.evaluate = false assert_nothing_raised { ret = astif.evaluate(:scope => "yay") } assert_equal(:else, ret) end # Make sure our override object behaves "correctly" def test_override - interp, scope, source = mkclassframing + scope = mkscope ref = nil assert_nothing_raised do - ref = resourceoverride("resource", "yaytest", "one" => "yay", "two" => "boo") + ref = resourceoverride("file", "/yayness", "owner" => "blah", "group" => "boo") end + Puppet::Parser::Resource.expects(:new).with { |o| o.is_a?(Hash) }.returns(:override) + scope.compile.expects(:store_override).with(:override) ret = nil assert_nothing_raised do ret = ref.evaluate :scope => scope end - assert_instance_of(Puppet::Parser::Resource, ret) - - assert(ret.override?, "Resource was not an override resource") - - assert(scope.overridetable[ret.ref].include?(ret), - "Was not stored in the override table") + assert_equal(:override, ret, "Did not return override") end # make sure our resourcedefaults ast object works correctly. def test_resourcedefaults - interp, scope, source = mkclassframing + scope = mkscope # Now make some defaults for files args = {:source => "/yay/ness", :group => "yayness"} assert_nothing_raised do obj = defaultobj "file", args obj.evaluate :scope => scope end hash = nil assert_nothing_raised do hash = scope.lookupdefaults("file") end hash.each do |name, value| assert_instance_of(Symbol, name) # params always convert assert_instance_of(Puppet::Parser::Resource::Param, value) end args.each do |name, value| assert(hash[name], "Did not get default %s" % name) assert_equal(value, hash[name].value) end end def test_node - interp = mkinterp - scope = mkscope(:interp => interp) + scope = mkscope + parser = scope.compile.parser # Define a base node - basenode = interp.newnode "basenode", :code => AST::ASTArray.new(:children => [ + basenode = parser.newnode "basenode", :code => AST::ASTArray.new(:children => [ resourcedef("file", "/tmp/base", "owner" => "root") ]) # Now define a subnode - nodes = interp.newnode ["mynode", "othernode"], + nodes = parser.newnode ["mynode", "othernode"], :code => AST::ASTArray.new(:children => [ resourcedef("file", "/tmp/mynode", "owner" => "root"), resourcedef("file", "/tmp/basenode", "owner" => "daemon") ]) assert_instance_of(Array, nodes) # Make sure we can find them all. %w{mynode othernode}.each do |node| - assert(interp.nodesearch_code(node), "Could not find %s" % node) + assert(parser.nodes[node], "Could not find %s" % node) end - mynode = interp.nodesearch_code("mynode") + mynode = parser.nodes["mynode"] # Now try evaluating the node assert_nothing_raised do - mynode.evaluate :scope => scope + mynode.evaluate :scope => scope, :resource => scope.resource end # Make sure that we can find each of the files myfile = scope.findresource "File[/tmp/mynode]" assert(myfile, "Could not find file from node") assert_equal("root", myfile[:owner]) basefile = scope.findresource "File[/tmp/basenode]" assert(basefile, "Could not find file from base node") assert_equal("daemon", basefile[:owner]) # Now make sure we can evaluate nodes with parents - child = interp.newnode(%w{child}, :parent => "basenode").shift + child = parser.newnode(%w{child}, :parent => "basenode").shift - newscope = mkscope :interp => interp + newscope = mkscope :parser => parser assert_nothing_raised do - child.evaluate :scope => newscope + child.evaluate :scope => newscope, :resource => scope.resource end assert(newscope.findresource("File[/tmp/base]"), "Could not find base resource") end def test_collection - interp = mkinterp - scope = mkscope(:interp => interp) + scope = mkscope coll = nil assert_nothing_raised do coll = AST::Collection.new(:type => "file", :form => :virtual) end assert_instance_of(AST::Collection, coll) ret = nil assert_nothing_raised do ret = coll.evaluate :scope => scope end assert_instance_of(Puppet::Parser::Collector, ret) # Now make sure we get it back from the scope - assert_equal([ret], scope.collections) + colls = scope.compile.instance_variable_get("@collections") + assert_equal([ret], colls, "Did not store collector in config's collection list") end def test_virtual_collexp - @interp, @scope, @source = mkclassframing + scope = mkscope # make a resource resource = mkresource(:type => "file", :title => "/tmp/testing", - :params => {:owner => "root", :group => "bin", :mode => "644"}) + :scope => scope, :params => {:owner => "root", :group => "bin", :mode => "644"}) run_collection_queries(:virtual) do |string, result, query| code = nil assert_nothing_raised do - str, code = query.evaluate :scope => @scope + str, code = query.evaluate :scope => scope end assert_instance_of(Proc, code) assert_nothing_raised do assert_equal(result, code.call(resource), "'#{string}' failed") end end end end - -# $Id$ diff --git a/test/language/ast/component.rb b/test/language/ast/definition.rb similarity index 51% rename from test/language/ast/component.rb rename to test/language/ast/definition.rb index 40543e9ab..d4c987362 100755 --- a/test/language/ast/component.rb +++ b/test/language/ast/definition.rb @@ -1,142 +1,166 @@ #!/usr/bin/env ruby # # Created by Luke A. Kanies on 2006-02-20. # Copyright (c) 2006. All rights reserved. $:.unshift("../../lib") if __FILE__ =~ /\.rb$/ require 'puppettest' require 'mocha' require 'puppettest/parsertesting' require 'puppettest/resourcetesting' -class TestASTComponent < Test::Unit::TestCase +class TestASTDefinition < Test::Unit::TestCase include PuppetTest include PuppetTest::ParserTesting include PuppetTest::ResourceTesting AST = Puppet::Parser::AST - def test_component - interp, scope, source = mkclassframing + def test_initialize + parser = mkparser # Create a new definition - klass = interp.newdefine "yayness", + klass = parser.newdefine "yayness", :arguments => [["owner", stringobj("nobody")], %w{mode}], :code => AST::ASTArray.new( :children => [resourcedef("file", "/tmp/$name", "owner" => varref("owner"), "mode" => varref("mode"))] ) # Test validattr? a couple different ways [:owner, "owner", :schedule, "schedule"].each do |var| assert(klass.validattr?(var), "%s was not considered valid" % var.inspect) end [:random, "random"].each do |var| assert(! klass.validattr?(var), "%s was considered valid" % var.inspect) end - # Now call it a couple of times - # First try it without a required param - assert_raise(Puppet::ParseError) do - klass.evaluate(:scope => scope, - :name => "bad", - :arguments => {"owner" => "nobody"} - ) - end - # And make sure it didn't create the file - assert_nil(scope.findresource("File[/tmp/bad]"), - "Made file with invalid params") + end - assert_nothing_raised do - klass.evaluate(:scope => scope, - :title => "first", - :arguments => {"mode" => "755"} + def test_evaluate + parser = mkparser + config = mkcompile + config.send(:evaluate_main) + scope = config.topscope + klass = parser.newdefine "yayness", + :arguments => [["owner", stringobj("nobody")], %w{mode}], + :code => AST::ASTArray.new( + :children => [resourcedef("file", "/tmp/$name", + "owner" => varref("owner"), "mode" => varref("mode"))] ) + + resource = Puppet::Parser::Resource.new( + :title => "first", + :type => "yayness", + :exported => false, + :virtual => false, + :scope => scope, + :source => scope.source + ) + resource.send(:set_parameter, "name", "first") + resource.send(:set_parameter, "mode", "755") + + resource.stubs(:title) + assert_nothing_raised do + klass.evaluate(:scope => scope, :resource => resource) end - firstobj = scope.findresource("File[/tmp/first]") + firstobj = config.findresource("File[/tmp/first]") assert(firstobj, "Did not create /tmp/first obj") assert_equal("file", firstobj.type) assert_equal("/tmp/first", firstobj.title) assert_equal("nobody", firstobj[:owner]) assert_equal("755", firstobj[:mode]) # Make sure we can't evaluate it with the same args assert_raise(Puppet::ParseError) do - klass.evaluate(:scope => scope, - :title => "first", - :arguments => {"mode" => "755"} - ) + klass.evaluate(:scope => scope, :resource => resource) end # Now create another with different args + resource2 = Puppet::Parser::Resource.new( + :title => "second", + :type => "yayness", + :exported => false, + :virtual => false, + :scope => scope, + :source => scope.source + ) + resource2.send(:set_parameter, "name", "second") + resource2.send(:set_parameter, "mode", "755") + resource2.send(:set_parameter, "owner", "daemon") + assert_nothing_raised do - klass.evaluate(:scope => scope, - :title => "second", - :arguments => {"mode" => "755", "owner" => "daemon"} - ) + klass.evaluate(:scope => scope, :resource => resource2) end - secondobj = scope.findresource("File[/tmp/second]") + secondobj = config.findresource("File[/tmp/second]") assert(secondobj, "Did not create /tmp/second obj") assert_equal("file", secondobj.type) assert_equal("/tmp/second", secondobj.title) assert_equal("daemon", secondobj[:owner]) assert_equal("755", secondobj[:mode]) end # #539 - definitions should support both names and titles def test_names_and_titles - interp, scope, source = mkclassframing + parser = mkparser + scope = mkscope :parser => parser [ - {:name => "one", :title => "two"}, - {:title => "mytitle"}, + {:name => "one", :title => "two"}, + {:title => "mytitle"} ].each_with_index do |hash, i| + # Create a definition that uses both name and title. Put this + # inside the loop so the subscope expectations work. + klass = parser.newdefine "yayness%s" % i + + resource = Puppet::Parser::Resource.new( + :title => hash[:title], + :type => "yayness%s" % i, + :exported => false, + :virtual => false, + :scope => scope, + :source => scope.source + ) - # Create a definition that uses both name and title - klass = interp.newdefine "yayness%s" % i - - subscope = klass.subscope(scope, "yayness%s" % i) + subscope = klass.subscope(scope, resource) klass.expects(:subscope).returns(subscope) - args = {:title => hash[:title]} if hash[:name] - args[:arguments] = {:name => hash[:name]} + resource.stubs(:to_hash).returns({:name => hash[:name]}) end - args[:scope] = scope + assert_nothing_raised("Could not evaluate definition with %s" % hash.inspect) do - klass.evaluate(args) + klass.evaluate(:scope => scope, :resource => resource) end name = hash[:name] || hash[:title] title = hash[:title] - args[:name] ||= name assert_equal(name, subscope.lookupvar("name"), "Name did not get set correctly") assert_equal(title, subscope.lookupvar("title"), "title did not get set correctly") [:name, :title].each do |param| - val = args[param] + val = resource.send(param) assert(subscope.tags.include?(val), - "Scope was not tagged with %s" % val) + "Scope was not tagged with %s '%s'" % [param, val]) end end end # Testing the root cause of #615. We should be using the fqname for the type, instead # of just the short name. def test_fully_qualified_types - interp = mkinterp - klass = interp.newclass("one::two") + parser = mkparser + klass = parser.newclass("one::two") assert_equal("one::two", klass.classname, "Class did not get fully qualified class name") end end -# $Id$ diff --git a/test/language/ast/hostclass.rb b/test/language/ast/hostclass.rb index 051bee36c..f747779b3 100755 --- a/test/language/ast/hostclass.rb +++ b/test/language/ast/hostclass.rb @@ -1,163 +1,184 @@ #!/usr/bin/env ruby # # Created by Luke A. Kanies on 2006-02-20. # Copyright (c) 2006. All rights reserved. $:.unshift("../../lib") if __FILE__ =~ /\.rb$/ require 'puppettest' require 'puppettest/parsertesting' require 'puppettest/resourcetesting' require 'mocha' class TestASTHostClass < Test::Unit::TestCase include PuppetTest include PuppetTest::ParserTesting include PuppetTest::ResourceTesting AST = Puppet::Parser::AST def test_hostclass - interp, scope, source = mkclassframing + scope = mkscope + parser = scope.compile.parser # Create the class we're testing, first with no parent - klass = interp.newclass "first", + klass = parser.newclass "first", :code => AST::ASTArray.new( :children => [resourcedef("file", "/tmp", "owner" => "nobody", "mode" => "755")] ) + resource = Puppet::Parser::Resource.new(:type => "class", :title => "first", :scope => scope) assert_nothing_raised do - klass.evaluate(:scope => scope) + klass.evaluate(:scope => scope, :resource => resource) end # Then try it again assert_nothing_raised do - klass.evaluate(:scope => scope) + klass.evaluate(:scope => scope, :resource => resource) end - assert(scope.class_scope(klass), "Class was not considered evaluated") + assert(scope.compile.class_scope(klass), "Class was not considered evaluated") tmp = scope.findresource("File[/tmp]") assert(tmp, "Could not find file /tmp") assert_equal("nobody", tmp[:owner]) assert_equal("755", tmp[:mode]) # Now create a couple more classes. - newbase = interp.newclass "newbase", + newbase = parser.newclass "newbase", :code => AST::ASTArray.new( :children => [resourcedef("file", "/tmp/other", "owner" => "nobody", "mode" => "644")] ) - newsub = interp.newclass "newsub", + newsub = parser.newclass "newsub", :parent => "newbase", :code => AST::ASTArray.new( :children => [resourcedef("file", "/tmp/yay", "owner" => "nobody", "mode" => "755"), resourceoverride("file", "/tmp/other", "owner" => "daemon") ] ) # Override a different variable in the top scope. - moresub = interp.newclass "moresub", + moresub = parser.newclass "moresub", :parent => "newbase", :code => AST::ASTArray.new( :children => [resourceoverride("file", "/tmp/other", "mode" => "755")] ) assert_nothing_raised do - newsub.evaluate(:scope => scope) + newsub.evaluate(:scope => scope, :resource => resource) end assert_nothing_raised do - moresub.evaluate(:scope => scope) + moresub.evaluate(:scope => scope, :resource => resource) end - assert(scope.class_scope(newbase), "Did not eval newbase") - assert(scope.class_scope(newsub), "Did not eval newsub") + assert(scope.compile.class_scope(newbase), "Did not eval newbase") + assert(scope.compile.class_scope(newsub), "Did not eval newsub") yay = scope.findresource("File[/tmp/yay]") assert(yay, "Did not find file /tmp/yay") assert_equal("nobody", yay[:owner]) assert_equal("755", yay[:mode]) other = scope.findresource("File[/tmp/other]") assert(other, "Did not find file /tmp/other") assert_equal("daemon", other[:owner]) assert_equal("755", other[:mode]) end # Make sure that classes set their namespaces to themselves. This # way they start looking for definitions in their own namespace. def test_hostclass_namespace - interp, scope, source = mkclassframing + scope = mkscope + parser = scope.compile.parser # Create a new class klass = nil assert_nothing_raised do - klass = interp.newclass "funtest" + klass = parser.newclass "funtest" end # Now define a definition in that namespace define = nil assert_nothing_raised do - define = interp.newdefine "funtest::mydefine" + define = parser.newdefine "funtest::mydefine" end assert_equal("funtest", klass.namespace, "component namespace was not set in the class") assert_equal("funtest", define.namespace, "component namespace was not set in the definition") - newscope = klass.subscope(scope) + newscope = klass.subscope(scope, mock("resource")) assert_equal(["funtest"], newscope.namespaces, "Scope did not inherit namespace") # Now make sure we can find the define assert(newscope.finddefine("mydefine"), "Could not find definition in my enclosing class") end # Make sure that our scope is a subscope of the parentclass's scope. # At the same time, make sure definitions in the parent class can be # found within the subclass (#517). def test_parent_scope_from_parentclass - interp = mkinterp + scope = mkscope + parser = scope.compile.parser - interp.newclass("base") - fun = interp.newdefine("base::fun") - interp.newclass("middle", :parent => "base") - interp.newclass("sub", :parent => "middle") - scope = mkscope :interp => interp + source = parser.newclass "" + parser.newclass("base") + fun = parser.newdefine("base::fun") + parser.newclass("middle", :parent => "base") + parser.newclass("sub", :parent => "middle") + scope = mkscope :parser => parser ret = nil assert_nothing_raised do - ret = scope.evalclasses("sub") + ret = scope.compile.evaluate_classes(["sub"], scope) end + scope.compile.send(:evaluate_generators) - subscope = scope.class_scope(scope.findclass("sub")) + subscope = scope.compile.class_scope(scope.findclass("sub")) assert(subscope, "could not find sub scope") - mscope = scope.class_scope(scope.findclass("middle")) + mscope = scope.compile.class_scope(scope.findclass("middle")) assert(mscope, "could not find middle scope") - pscope = scope.class_scope(scope.findclass("base")) + pscope = scope.compile.class_scope(scope.findclass("base")) assert(pscope, "could not find parent scope") assert(pscope == mscope.parent, "parent scope of middle was not set correctly") assert(mscope == subscope.parent, "parent scope of sub was not set correctly") result = mscope.finddefine("fun") assert(result, "could not find parent-defined definition from middle") assert(fun == result, "found incorrect parent-defined definition from middle") result = subscope.finddefine("fun") assert(result, "could not find parent-defined definition from sub") assert(fun == result, "found incorrect parent-defined definition from sub") end -end -# $Id$ + # #795 - make sure the subclass's tags get set before we + # evaluate the parent class, so we can be sure that the parent + # class can switch based on the sub classes. + def test_tags_set_before_parent_is_evaluated + scope = mkscope + parser = scope.compile.parser + base = parser.newclass "base" + sub = parser.newclass "sub", :parent => "base" + + base.expects(:safeevaluate).with do |args| + assert(scope.compile.configuration.tags.include?("sub"), "Did not tag with sub class name before evaluating base class") + base.evaluate(args) + true + end + sub.evaluate :scope => scope, :resource => scope.resource + end +end diff --git a/test/language/ast/resource.rb b/test/language/ast/resource.rb new file mode 100755 index 000000000..9ef5181af --- /dev/null +++ b/test/language/ast/resource.rb @@ -0,0 +1,59 @@ +#!/usr/bin/env ruby +# +# Created by Luke A. Kanies on 2007-07-8. +# Copyright (c) 2007. All rights reserved. + +$:.unshift("../lib").unshift("../../lib") if __FILE__ =~ /\.rb$/ + +require 'puppettest' +require 'puppettest/parsertesting' + +class TestASTResource< Test::Unit::TestCase + include PuppetTest + include PuppetTest::ParserTesting + AST = Puppet::Parser::AST + + def setup + super + @scope = mkscope + @parser = @scope.compile.parser + @scope.compile.send(:evaluate_main) + end + + def newdef(type, title, params = nil) + params ||= AST::ASTArray.new(:children => []) + AST::Resource.new(:type => type, :title => AST::String.new(:value => title), :params => params) + end + + # Related to #806, make sure resources always look up the full path to the resource. + def test_scoped_types + @parser.newdefine "one" + @parser.newdefine "one::two" + @parser.newdefine "three" + twoscope = @scope.newscope(:namespace => "one") + twoscope.resource = @scope.resource + assert(twoscope.finddefine("two"), "Could not find 'two' definition") + title = "title" + + # First try a qualified type + assert_equal("one::two", newdef("two", title).evaluate(:scope => twoscope)[0].type, + "Defined type was not made fully qualified") + + # Then try a type that does not need to be qualified + assert_equal("one", newdef("one", title).evaluate(:scope => twoscope)[0].type, + "Unqualified defined type was not handled correctly") + + # Then an unqualified type from within the one namespace + assert_equal("three", newdef("three", title).evaluate(:scope => twoscope)[0].type, + "Defined type was not made fully qualified") + + # Then a builtin type + assert_equal("file", newdef("file", title).evaluate(:scope => twoscope)[0].type, + "Builtin type was not handled correctly") + + # Now try a type that does not exist, which should throw an error. + assert_raise(Puppet::ParseError, "Did not fail on a missing type in a resource reference") do + newdef("nosuchtype", title).evaluate(:scope => twoscope) + end + end +end diff --git a/test/language/ast/resourceref.rb b/test/language/ast/resource_reference.rb similarity index 85% rename from test/language/ast/resourceref.rb rename to test/language/ast/resource_reference.rb index 7b7889dc1..e8883afb9 100755 --- a/test/language/ast/resourceref.rb +++ b/test/language/ast/resource_reference.rb @@ -1,95 +1,93 @@ #!/usr/bin/env ruby # # Created by Luke A. Kanies on 2007-07-8. # Copyright (c) 2007. All rights reserved. $:.unshift("../lib").unshift("../../lib") if __FILE__ =~ /\.rb$/ require 'puppettest' require 'puppettest/parsertesting' -class TestASTResourceRef < Test::Unit::TestCase +class TestASTResourceReference < Test::Unit::TestCase include PuppetTest include PuppetTest::ParserTesting AST = Puppet::Parser::AST def newref(type, title) - AST::ResourceRef.new(:type => type, :title => AST::String.new(:value => title)) + AST::ResourceReference.new(:type => type, :title => AST::String.new(:value => title)) end def setup super - @interp = mkinterp - @scope = mkscope :interp => @interp + @scope = mkscope + @parser = @scope.compile.parser end def test_evaluate - @interp.newdefine "one::two" - @interp.newdefine "one-two" + @parser.newdefine "one::two" + @parser.newdefine "one-two" [%w{file /tmp/yay}, %w{one::two three}, %w{one-two three}].each do |type, title| ref = newref(type, title) evaled = nil assert_nothing_raised("Could not evaluate resource ref") do evaled = ref.evaluate(:scope => @scope) end assert_equal(type, evaled.type, "Type did not translate correctly") assert_equal(title, evaled.title, "Title did not translate correctly") end end # Related to #706, make sure resource references correctly translate to qualified types. def test_scoped_references - @interp.newdefine "one" - @interp.newdefine "one::two" - @interp.newdefine "three" - twoscope = @scope.newscope(:type => "one", :namespace => "one") + @parser.newdefine "one" + @parser.newdefine "one::two" + @parser.newdefine "three" + twoscope = @scope.newscope(:namespace => "one") assert(twoscope.finddefine("two"), "Could not find 'two' definition") title = "title" # First try a qualified type assert_equal("one::two", newref("two", title).evaluate(:scope => twoscope).type, "Defined type was not made fully qualified") # Then try a type that does not need to be qualified assert_equal("one", newref("one", title).evaluate(:scope => twoscope).type, "Unqualified defined type was not handled correctly") # Then an unqualified type from within the one namespace assert_equal("three", newref("three", title).evaluate(:scope => twoscope).type, "Defined type was not made fully qualified") # Then a builtin type assert_equal("file", newref("file", title).evaluate(:scope => twoscope).type, "Builtin type was not handled correctly") # Now try a type that does not exist, which should throw an error. assert_raise(Puppet::ParseError, "Did not fail on a missing type in a resource reference") do newref("nosuchtype", title).evaluate(:scope => twoscope) end # Now run the same tests, but with the classes - @interp.newclass "four" - @interp.newclass "one::five" + @parser.newclass "four" + @parser.newclass "one::five" # First try an unqualified type assert_equal("four", newref("class", "four").evaluate(:scope => twoscope).title, "Unqualified class was not found") # Then a qualified class assert_equal("one::five", newref("class", "five").evaluate(:scope => twoscope).title, "Class was not made fully qualified") # Then try a type that does not need to be qualified assert_equal("four", newref("class", "four").evaluate(:scope => twoscope).title, "Unqualified class was not handled correctly") # Now try a type that does not exist, which should throw an error. assert_raise(Puppet::ParseError, "Did not fail on a missing type in a resource reference") do newref("class", "nosuchclass").evaluate(:scope => twoscope) end end end - -# $Id$ diff --git a/test/language/collector.rb b/test/language/collector.rb index bdcaf4aec..55c93c2d5 100755 --- a/test/language/collector.rb +++ b/test/language/collector.rb @@ -1,179 +1,178 @@ #!/usr/bin/env ruby $:.unshift("../lib").unshift("../../lib") if __FILE__ =~ /\.rb$/ require 'puppettest' require 'puppettest/parsertesting' require 'puppettest/resourcetesting' class TestCollector < Test::Unit::TestCase include PuppetTest include PuppetTest::ParserTesting include PuppetTest::ResourceTesting Parser = Puppet::Parser AST = Parser::AST def setup super Puppet[:trace] = false - @interp, @scope, @source = mkclassframing + @scope = mkscope + @compile = @scope.compile end # Test just collecting a specific resource. This is used by the 'realize' # function, and it's much faster than iterating over all of the resources. def test_collect_resource # Make a collector coll = nil assert_nothing_raised do coll = Puppet::Parser::Collector.new(@scope, "file", nil, nil, :virtual) end # Now set the resource in the collector assert_nothing_raised do coll.resources = ["File[/tmp/virtual1]", "File[/tmp/virtual3]"] end - @scope.newcollection(coll) + @compile.add_collection(coll) # Evaluate the collector and make sure it doesn't fail with no resources # found yet assert_nothing_raised("Resource collection with no results failed") do assert_equal(false, coll.evaluate) end # Make a couple of virtual resources one = mkresource(:type => "file", :title => "/tmp/virtual1", :virtual => true, :params => {:owner => "root"}) two = mkresource(:type => "file", :title => "/tmp/virtual2", :virtual => true, :params => {:owner => "root"}) - @scope.setresource one - @scope.setresource two + @scope.compile.store_resource @scope, one + @scope.compile.store_resource @scope, two # Now run the collector again and make sure it finds our resource assert_nothing_raised do assert_equal([one], coll.evaluate, "did not find resource") end # And make sure the resource is no longer virtual assert(! one.virtual?, "Resource is still virtual") # But the other still is assert(two.virtual?, "Resource got realized") # Make sure that the collection is still there - assert(@scope.collections.include?(coll), "collection was deleted too soon") + assert(@compile.collections.include?(coll), "collection was deleted too soon") # Now add our third resource three = mkresource(:type => "file", :title => "/tmp/virtual3", :virtual => true, :params => {:owner => "root"}) - @scope.setresource three + @scope.compile.store_resource @scope, three # Run the collection assert_nothing_raised do assert_equal([three], coll.evaluate, "did not find resource") end assert(! three.virtual?, "three is still virtual") # And make sure that the collection got deleted from the scope's list - assert(@scope.collections.empty?, "collection was not deleted") + assert(@compile.collections.empty?, "collection was not deleted") end def test_virtual # Make a virtual resource virtual = mkresource(:type => "file", :title => "/tmp/virtual", :virtual => true, :params => {:owner => "root"}) - @scope.setresource virtual + @scope.compile.store_resource @scope, virtual # And a non-virtual real = mkresource(:type => "file", :title => "/tmp/real", :params => {:owner => "root"}) - @scope.setresource real + @scope.compile.store_resource @scope, real # Now make a collector coll = nil # Make a fake query code = proc do |res| true end assert_nothing_raised do coll = Puppet::Parser::Collector.new(@scope, "file", nil, code, :virtual) end # Set it in our scope - @scope.newcollection(coll) + @compile.add_collection(coll) # Make sure it's in the collections - assert(@scope.collections.include?(coll), "collection was not added") + assert(@compile.collections.include?(coll), "collection was not added") # And try to collect the virtual resources. ret = nil assert_nothing_raised do ret = coll.collect_virtual end assert_equal([virtual], ret) # Now make sure evaluate does the right thing. assert_nothing_raised do ret = coll.evaluate end # And make sure our virtual object is no longer virtual assert(! virtual.virtual?, "Virtual object did not get realized") # Now make a new collector of a different type and make sure it # finds nothing. assert_nothing_raised do coll = Puppet::Parser::Collector.new(@scope, "exec", nil, nil, :virtual) end # Remark this as virtual virtual.virtual = true assert_nothing_raised do ret = coll.evaluate end assert_equal(false, ret) end # Collections that specify resources should be deleted when they succeed, # but others should remain until the very end. def test_normal_collections_remain # Make a collector coll = nil assert_nothing_raised do coll = Puppet::Parser::Collector.new(@scope, "file", nil, nil, :virtual) end - @scope.newcollection(coll) + @compile.add_collection(coll) # run the collection and make sure it doesn't get deleted, since it # didn't return anything assert_nothing_raised do assert_equal(false, coll.evaluate, "Evaluate returned incorrect value") end - assert_equal([coll], @scope.collections, "Collection was deleted") + assert_equal([coll], @compile.collections, "Collection was deleted") # Make a resource one = mkresource(:type => "file", :title => "/tmp/virtual1", :virtual => true, :params => {:owner => "root"}) - @scope.setresource one + @scope.compile.store_resource @scope, one # Now perform the collection again, and it should still be there assert_nothing_raised do assert_equal([one], coll.evaluate, "Evaluate returned incorrect value") end - assert_equal([coll], @scope.collections, "Collection was deleted") + assert_equal([coll], @compile.collections, "Collection was deleted") assert_equal(false, one.virtual?, "One was not realized") end end - -# $Id$ diff --git a/test/language/compile.rb b/test/language/compile.rb new file mode 100755 index 000000000..5732acba3 --- /dev/null +++ b/test/language/compile.rb @@ -0,0 +1,669 @@ +#!/usr/bin/env ruby + +$:.unshift("../lib").unshift("../../lib") if __FILE__ =~ /\.rb$/ + +require 'mocha' +require 'puppettest' +require 'puppettest/parsertesting' +require 'puppet/parser/compile' + +# Test our compile object. +class TestCompile < Test::Unit::TestCase + include PuppetTest + include PuppetTest::ParserTesting + + Compile = Puppet::Parser::Compile + Scope = Puppet::Parser::Scope + Node = Puppet::Network::Handler.handler(:node) + SimpleNode = Puppet::Node + + def mknode(name = "foo") + @node = SimpleNode.new(name) + end + + def mkparser + # This should mock an interpreter + @parser = stub 'parser', :version => "1.0" + end + + def mkcompile(options = {}) + if node = options[:node] + options.delete(:node) + else + node = mknode + end + @compile = Compile.new(node, mkparser, options) + end + + def test_initialize + compile = nil + node = stub 'node', :name => "foo" + parser = stub 'parser', :version => "1.0" + assert_nothing_raised("Could not init compile with all required options") do + compile = Compile.new(node, parser) + end + + assert_equal(node, compile.node, "Did not set node correctly") + assert_equal(parser, compile.parser, "Did not set parser correctly") + + # We're not testing here whether we call initvars, because it's too difficult to + # mock. + + # Now try it with some options + assert_nothing_raised("Could not init compile with extra options") do + compile = Compile.new(node, parser, :ast_nodes => false) + end + + assert_equal(false, compile.ast_nodes?, "Did not set ast_nodes? correctly") + end + + def test_initvars + compile = mkcompile + [:class_scopes, :resource_table, :exported_resources, :resource_overrides].each do |table| + assert_instance_of(Hash, compile.send(:instance_variable_get, "@#{table}"), "Did not set %s table correctly" % table) + end + assert_instance_of(Scope, compile.topscope, "Did not create a topscope") + graph = compile.instance_variable_get("@scope_graph") + assert_instance_of(GRATR::Digraph, graph, "Did not create scope graph") + assert(graph.vertex?(compile.topscope), "Did not add top scope as a vertex in the graph") + end + + # Make sure we store and can retrieve references to classes and their scopes. + def test_class_set_and_class_scope + klass = mock 'ast_class' + klass.expects(:classname).returns("myname") + + compile = mkcompile + compile.configuration.expects(:tag).with("myname") + + assert_nothing_raised("Could not set class") do + compile.class_set "myname", "myscope" + end + # First try to retrieve it by name. + assert_equal("myscope", compile.class_scope("myname"), "Could not retrieve class scope by name") + + # Then by object + assert_equal("myscope", compile.class_scope(klass), "Could not retrieve class scope by object") + end + + def test_classlist + compile = mkcompile + + compile.class_set "", "empty" + compile.class_set "one", "yep" + compile.class_set "two", "nope" + + # Make sure our class list is correct + assert_equal(%w{one two}.sort, compile.classlist.sort, "Did not get correct class list") + end + + # Make sure collections get added to our internal array + def test_add_collection + compile = mkcompile + assert_nothing_raised("Could not add collection") do + compile.add_collection "nope" + end + assert_equal(%w{nope}, compile.instance_variable_get("@collections"), "Did not add collection") + end + + # Make sure we create a graph of scopes. + def test_newscope + compile = mkcompile + graph = compile.instance_variable_get("@scope_graph") + assert_instance_of(Scope, compile.topscope, "Did not create top scope") + assert_instance_of(GRATR::Digraph, graph, "Did not create graph") + + assert(graph.vertex?(compile.topscope), "The top scope is not a vertex in the graph") + + # Now that we've got the top scope, create a new, subscope + subscope = nil + assert_nothing_raised("Could not create subscope") do + subscope = compile.newscope(compile.topscope) + end + assert_instance_of(Scope, subscope, "Did not create subscope") + assert(graph.edge?(compile.topscope, subscope), "An edge between top scope and subscope was not added") + + # Make sure a scope can find its parent. + assert(compile.parent(subscope), "Could not look up parent scope on compile") + assert_equal(compile.topscope.object_id, compile.parent(subscope).object_id, "Did not get correct parent scope from compile") + assert_equal(compile.topscope.object_id, subscope.parent.object_id, "Scope did not correctly retrieve its parent scope") + + # Now create another, this time specifying options + another = nil + assert_nothing_raised("Could not create subscope") do + another = compile.newscope(subscope, :level => 5) + end + assert_equal(5, another.level, "did not set scope option correctly") + assert_instance_of(Scope, another, "Did not create second subscope") + assert(graph.edge?(subscope, another), "An edge between parent scope and second subscope was not added") + + # Make sure it can find its parent. + assert(compile.parent(another), "Could not look up parent scope of second subscope on compile") + assert_equal(subscope.object_id, compile.parent(another).object_id, "Did not get correct parent scope of second subscope from compile") + assert_equal(subscope.object_id, another.parent.object_id, "Second subscope did not correctly retrieve its parent scope") + + # And make sure both scopes show up in the right order in the search path + assert_equal([another.object_id, subscope.object_id, compile.topscope.object_id], another.scope_path.collect { |p| p.object_id }, + "Did not get correct scope path") + end + + # The heart of the action. + def test_compile + compile = mkcompile + [:set_node_parameters, :evaluate_main, :evaluate_ast_node, :evaluate_node_classes, :evaluate_generators, :fail_on_unevaluated, :finish].each do |method| + compile.expects(method) + end + assert_instance_of(Puppet::Node::Configuration, compile.compile, "Did not return the configuration") + end + + # Test setting the node's parameters into the top scope. + def test_set_node_parameters + compile = mkcompile + @node.parameters = {"a" => "b", "c" => "d"} + scope = compile.topscope + @node.parameters.each do |param, value| + scope.expects(:setvar).with(param, value) + end + + assert_nothing_raised("Could not call 'set_node_parameters'") do + compile.send(:set_node_parameters) + end + end + + # Test that we can evaluate the main class, which is the one named "" in namespace + # "". + def test_evaluate_main + compile = mkcompile + main = mock 'main_class' + compile.topscope.expects(:source=).with(main) + @parser.expects(:findclass).with("", "").returns(main) + + assert_nothing_raised("Could not call evaluate_main") do + compile.send(:evaluate_main) + end + + assert(compile.resources.find { |r| r.to_s == "Class[main]" }, "Did not create a 'main' resource") + end + + # Make sure we either don't look for nodes, or that we find and evaluate the right object. + def test_evaluate_ast_node + # First try it with ast_nodes disabled + compile = mkcompile :ast_nodes => false + name = compile.node.name + compile.expects(:ast_nodes?).returns(false) + compile.parser.expects(:nodes).never + + assert_nothing_raised("Could not call evaluate_ast_node when ast nodes are disabled") do + compile.send(:evaluate_ast_node) + end + + assert_nil(compile.resources.find { |r| r.to_s == "Node[#{name}]" }, "Created node object when ast_nodes was false") + + # Now try it with them enabled, but no node found. + nodes = mock 'node_hash' + compile = mkcompile :ast_nodes => true + name = compile.node.name + compile.expects(:ast_nodes?).returns(true) + compile.parser.stubs(:nodes).returns(nodes) + + # Set some names for our test + @node.names = %w{a b c} + nodes.expects(:[]).with("a").returns(nil) + nodes.expects(:[]).with("b").returns(nil) + nodes.expects(:[]).with("c").returns(nil) + + # It should check this last, of course. + nodes.expects(:[]).with("default").returns(nil) + + # And make sure the lack of a node throws an exception + assert_raise(Puppet::ParseError, "Did not fail when we couldn't find an ast node") do + compile.send(:evaluate_ast_node) + end + + # Finally, make sure it works dandily when we have a node + compile = mkcompile :ast_nodes => true + compile.expects(:ast_nodes?).returns(true) + + node = stub 'node', :classname => "c" + nodes = {"c" => node} + compile.parser.stubs(:nodes).returns(nodes) + + # Set some names for our test + @node.names = %w{a b c} + + # And make sure we throw no exceptions. + assert_nothing_raised("Failed when a node was found") do + compile.send(:evaluate_ast_node) + end + + assert_instance_of(Puppet::Parser::Resource, compile.resources.find { |r| r.to_s == "Node[c]" }, + "Did not create node resource") + + # Lastly, check when we actually find the default. + compile = mkcompile :ast_nodes => true + compile.expects(:ast_nodes?).returns(true) + + node = stub 'node', :classname => "default" + nodes = {"default" => node} + compile.parser.stubs(:nodes).returns(nodes) + + # Set some names for our test + @node.names = %w{a b c} + + # And make sure the lack of a node throws an exception + assert_nothing_raised("Failed when a node was found") do + compile.send(:evaluate_ast_node) + end + assert_instance_of(Puppet::Parser::Resource, compile.resources.find { |r| r.to_s == "Node[default]" }, + "Did not create default node resource") + end + + def test_evaluate_node_classes + compile = mkcompile + @node.classes = %w{one two three four} + compile.expects(:evaluate_classes).with(%w{one two three four}, compile.topscope) + assert_nothing_raised("could not call evaluate_node_classes") do + compile.send(:evaluate_node_classes) + end + end + + def test_evaluate_classes + compile = mkcompile + compile.parser.expects(:findclass).with("", "").returns(stub('main', :classname => "")) + compile.send :evaluate_main + classes = { + "one" => stub('class one', :classname => "one"), + "three" => stub('class three', :classname => "three") + } + + classes.each do |name, obj| + compile.parser.expects(:findclass).with("", name).returns(obj) + end + %w{two four}.each do |name| + compile.parser.expects(:findclass).with("", name).returns(nil) + end + + %w{one two three four}.each do |name| + compile.configuration.expects(:tag).with(name) + end + + result = nil + assert_nothing_raised("could not call evaluate_node_classes") do + result = compile.send(:evaluate_classes, %w{one two three four}, compile.topscope) + end + %w{one three}.each do |found| + assert(compile.resources.find { |r| r.to_s == "Class[#{found}]" }, "Did not create a class resource for %s" % found) + end + assert_equal(%w{one three}, result, "Did not return the list of evaluated classes") + end + + def test_evaluate_collections + compile = mkcompile + + colls = [] + + # Make sure we return false when there's nothing there. + assert(! compile.send(:evaluate_collections), "Returned true when there were no collections") + + # And when the collections fail to evaluate. + colls << mock("coll1-false") + colls << mock("coll2-false") + colls.each { |c| c.expects(:evaluate).returns(false) } + + compile.instance_variable_set("@collections", colls) + assert(! compile.send(:evaluate_collections), "Returned true when collections both evaluated nothing") + + # Now have one of the colls evaluate + colls.clear + colls << mock("coll1-one-true") + colls << mock("coll2-one-true") + colls[0].expects(:evaluate).returns(true) + colls[1].expects(:evaluate).returns(false) + assert(compile.send(:evaluate_collections), "Did not return true when one collection evaluated true") + + # And have them both eval true + colls.clear + colls << mock("coll1-both-true") + colls << mock("coll2-both-true") + colls[0].expects(:evaluate).returns(true) + colls[1].expects(:evaluate).returns(true) + assert(compile.send(:evaluate_collections), "Did not return true when both collections evaluated true") + end + + def test_unevaluated_resources + compile = mkcompile + resources = {} + compile.instance_variable_set("@resource_table", resources) + + # First test it when the table is empty + assert_nil(compile.send(:unevaluated_resources), "Somehow found unevaluated resources in an empty table") + + # Then add a builtin resources + resources["one"] = mock("builtin only") + resources["one"].expects(:builtin?).returns(true) + assert_nil(compile.send(:unevaluated_resources), "Considered a builtin resource unevaluated") + + # And do both builtin and non-builtin but already evaluated + resources.clear + resources["one"] = mock("builtin (with eval)") + resources["one"].expects(:builtin?).returns(true) + resources["two"] = mock("evaled (with builtin)") + resources["two"].expects(:builtin?).returns(false) + resources["two"].expects(:evaluated?).returns(true) + assert_nil(compile.send(:unevaluated_resources), "Considered either a builtin or evaluated resource unevaluated") + + # Now a single unevaluated resource. + resources.clear + resources["one"] = mock("unevaluated") + resources["one"].expects(:builtin?).returns(false) + resources["one"].expects(:evaluated?).returns(false) + assert_equal([resources["one"]], compile.send(:unevaluated_resources), "Did not find unevaluated resource") + + # With two uneval'ed resources, and an eval'ed one thrown in + resources.clear + resources["one"] = mock("unevaluated one") + resources["one"].expects(:builtin?).returns(false) + resources["one"].expects(:evaluated?).returns(false) + resources["two"] = mock("unevaluated two") + resources["two"].expects(:builtin?).returns(false) + resources["two"].expects(:evaluated?).returns(false) + resources["three"] = mock("evaluated") + resources["three"].expects(:builtin?).returns(false) + resources["three"].expects(:evaluated?).returns(true) + + result = compile.send(:unevaluated_resources) + %w{one two}.each do |name| + assert(result.include?(resources[name]), "Did not find %s in the unevaluated list" % name) + end + end + + def test_evaluate_definitions + # First try the case where there's nothing to return + compile = mkcompile + compile.expects(:unevaluated_resources).returns(nil) + + assert_nothing_raised("Could not test for unevaluated resources") do + assert(! compile.send(:evaluate_definitions), "evaluate_definitions returned true when no resources were evaluated") + end + + # Now try it with resources left to evaluate + resources = [] + res1 = mock("resource1") + res1.expects(:evaluate) + res2 = mock("resource2") + res2.expects(:evaluate) + resources << res1 << res2 + compile = mkcompile + compile.expects(:unevaluated_resources).returns(resources) + + assert_nothing_raised("Could not test for unevaluated resources") do + assert(compile.send(:evaluate_definitions), "evaluate_definitions returned false when resources were evaluated") + end + end + + def test_evaluate_generators + # First try the case where we have nothing to do + compile = mkcompile + compile.expects(:evaluate_definitions).returns(false) + compile.expects(:evaluate_collections).returns(false) + + assert_nothing_raised("Could not call :eval_iterate") do + compile.send(:evaluate_generators) + end + + # FIXME I could not get this test to work, but the code is short + # enough that I'm ok with it. + # It's important that collections are evaluated before definitions, + # so make sure that's the case by verifying that collections get tested + # twice but definitions only once. + #compile = mkcompile + #compile.expects(:evaluate_collections).returns(true).returns(false) + #compile.expects(:evaluate_definitions).returns(false) + #compile.send(:eval_iterate) + end + + def test_store + compile = mkcompile + Puppet.features.expects(:rails?).returns(true) + Puppet::Rails.expects(:connect) + + node = mock 'node' + resource_table = mock 'resources' + resource_table.expects(:values).returns(:resources) + compile.instance_variable_set("@node", node) + compile.instance_variable_set("@resource_table", resource_table) + compile.expects(:store_to_active_record).with(node, :resources) + compile.send(:store) + end + + def test_store_to_active_record + compile = mkcompile + node = mock 'node' + node.expects(:name).returns("myname") + Puppet::Rails::Host.stubs(:transaction).yields + Puppet::Rails::Host.expects(:store).with(node, :resources) + compile.send(:store_to_active_record, node, :resources) + end + + # Make sure that 'finish' gets called on all of our resources. + def test_finish + compile = mkcompile + table = compile.instance_variable_get("@resource_table") + + # Add a resource that does respond to :finish + yep = mock("finisher") + yep.expects(:respond_to?).with(:finish).returns(true) + yep.expects(:finish) + table["yep"] = yep + + # And one that does not + dnf = mock("dnf") + dnf.expects(:respond_to?).with(:finish).returns(false) + table["dnf"] = dnf + + compile.send(:finish) + end + + def test_verify_uniqueness + compile = mkcompile + + resources = compile.instance_variable_get("@resource_table") + resource = mock("noconflict") + resource.expects(:ref).returns("File[yay]") + assert_nothing_raised("Raised an exception when there should have been no conflict") do + compile.send(:verify_uniqueness, resource) + end + + # Now try the case where our type is isomorphic + resources["thing"] = true + + isoconflict = mock("isoconflict") + isoconflict.expects(:ref).returns("thing") + isoconflict.expects(:type).returns("testtype") + faketype = mock("faketype") + faketype.expects(:isomorphic?).returns(false) + faketype.expects(:name).returns("whatever") + Puppet::Type.expects(:type).with("testtype").returns(faketype) + assert_nothing_raised("Raised an exception when was a conflict in non-isomorphic types") do + compile.send(:verify_uniqueness, isoconflict) + end + + # Now test for when we actually have an exception + initial = mock("initial") + resources["thing"] = initial + initial.expects(:file).returns(false) + + conflict = mock("conflict") + conflict.expects(:ref).returns("thing").times(2) + conflict.expects(:type).returns("conflict") + conflict.expects(:file).returns(false) + conflict.expects(:line).returns(false) + + faketype = mock("faketype") + faketype.expects(:isomorphic?).returns(true) + Puppet::Type.expects(:type).with("conflict").returns(faketype) + assert_raise(Puppet::ParseError, "Did not fail when two isomorphic resources conflicted") do + compile.send(:verify_uniqueness, conflict) + end + end + + def test_store_resource + # Run once when there's no conflict + compile = mkcompile + table = compile.instance_variable_get("@resource_table") + resource = mock("resource") + resource.expects(:ref).returns("yay") + compile.expects(:verify_uniqueness).with(resource) + scope = stub("scope", :resource => mock('resource')) + + compile.configuration.expects(:add_edge!).with(scope.resource, resource) + + assert_nothing_raised("Could not store resource") do + compile.store_resource(scope, resource) + end + assert_equal(resource, table["yay"], "Did not store resource in table") + + # Now for conflicts + compile = mkcompile + table = compile.instance_variable_get("@resource_table") + resource = mock("resource") + compile.expects(:verify_uniqueness).with(resource).raises(ArgumentError) + + assert_raise(ArgumentError, "Did not raise uniqueness exception") do + compile.store_resource(scope, resource) + end + assert(table.empty?, "Conflicting resource was stored in table") + end + + def test_fail_on_unevaluated + compile = mkcompile + compile.expects(:fail_on_unevaluated_overrides) + compile.expects(:fail_on_unevaluated_resource_collections) + compile.send :fail_on_unevaluated + end + + def test_store_override + # First test the case when the resource is not present. + compile = mkcompile + overrides = compile.instance_variable_get("@resource_overrides") + override = Object.new + override.expects(:ref).returns(:myref).times(2) + override.expects(:override=).with(true) + + assert_nothing_raised("Could not call store_override") do + compile.store_override(override) + end + assert_instance_of(Array, overrides[:myref], "Overrides table is not a hash of arrays") + assert_equal(override, overrides[:myref][0], "Did not store override in appropriately named array") + + # And when the resource already exists. + resource = mock 'resource' + resources = compile.instance_variable_get("@resource_table") + resources[:resref] = resource + + override = mock 'override' + resource.expects(:merge).with(override) + override.expects(:override=).with(true) + override.expects(:ref).returns(:resref) + assert_nothing_raised("Could not call store_override when the resource already exists.") do + compile.store_override(override) + end + end + + def test_resource_overrides + compile = mkcompile + overrides = compile.instance_variable_get("@resource_overrides") + overrides[:test] = :yay + resource = mock 'resource' + resource.expects(:ref).returns(:test) + + assert_equal(:yay, compile.resource_overrides(resource), "Did not return overrides from table") + end + + def test_fail_on_unevaluated_resource_collections + compile = mkcompile + collections = compile.instance_variable_get("@collections") + + # Make sure we're fine when the list is empty + assert_nothing_raised("Failed when no collections were present") do + compile.send :fail_on_unevaluated_resource_collections + end + + # And that we're fine when we've got collections but with no resources + collections << mock('coll') + collections[0].expects(:resources).returns(nil) + assert_nothing_raised("Failed when no resource collections were present") do + compile.send :fail_on_unevaluated_resource_collections + end + + # But that we do fail when we've got resource collections left. + collections.clear + + # return both an array and a string, because that's tested internally + collections << mock('coll returns one') + collections[0].expects(:resources).returns(:something) + + collections << mock('coll returns many') + collections[1].expects(:resources).returns([:one, :two]) + + assert_raise(Puppet::ParseError, "Did not fail on unevaluated resource collections") do + compile.send :fail_on_unevaluated_resource_collections + end + end + + def test_fail_on_unevaluated_overrides + compile = mkcompile + overrides = compile.instance_variable_get("@resource_overrides") + + # Make sure we're fine when the list is empty + assert_nothing_raised("Failed when no collections were present") do + compile.send :fail_on_unevaluated_overrides + end + + # But that we fail if there are any overrides left in the table. + overrides[:yay] = [] + overrides[:foo] = [] + overrides[:bar] = [mock("override")] + overrides[:bar][0].expects(:ref).returns("yay") + assert_raise(Puppet::ParseError, "Failed to fail when overrides remain") do + compile.send :fail_on_unevaluated_overrides + end + end + + def test_find_resource + compile = mkcompile + resources = compile.instance_variable_get("@resource_table") + + assert_nothing_raised("Could not call findresource when the resource table was empty") do + assert_nil(compile.findresource("yay", "foo"), "Returned a non-existent resource") + assert_nil(compile.findresource("yay[foo]"), "Returned a non-existent resource") + end + + resources["Foo[bar]"] = :yay + assert_nothing_raised("Could not call findresource when the resource table was not empty") do + assert_equal(:yay, compile.findresource("foo", "bar"), "Returned a non-existent resource") + assert_equal(:yay, compile.findresource("Foo[bar]"), "Returned a non-existent resource") + end + end + + # #620 - Nodes and classes should conflict, else classes don't get evaluated + def test_nodes_and_classes_name_conflict + # Test node then class + compile = mkcompile + node = stub :nodescope? => true + klass = stub :nodescope? => false + compile.class_set("one", node) + assert_raise(Puppet::ParseError, "Did not fail when replacing node with class") do + compile.class_set("one", klass) + end + + # and class then node + compile = mkcompile + node = stub :nodescope? => true + klass = stub :nodescope? => false + compile.class_set("two", klass) + assert_raise(Puppet::ParseError, "Did not fail when replacing node with class") do + compile.class_set("two", node) + end + end +end diff --git a/test/language/configuration.rb b/test/language/configuration.rb deleted file mode 100755 index 4cbba8063..000000000 --- a/test/language/configuration.rb +++ /dev/null @@ -1,131 +0,0 @@ -#!/usr/bin/env ruby - -$:.unshift("../lib").unshift("../../lib") if __FILE__ =~ /\.rb$/ - -require 'mocha' -require 'puppettest' -require 'puppettest/parsertesting' -require 'puppet/parser/configuration' - -# Test our configuration object. -class TestConfiguration < Test::Unit::TestCase - include PuppetTest - include PuppetTest::ParserTesting - - Config = Puppet::Parser::Configuration - Scope = Puppet::Parser::Scope - - def mkconfig - Config.new(:host => "foo", :interpreter => "interp") - end - - def test_initialize - # Make sure we get an error if we don't send an interpreter - assert_raise(ArgumentError, "Did not fail when missing host") do - Config.new(:interpreter => "yay" ) - end - assert_raise(ArgumentError, "Did not fail when missing interp") do - Config.new(:host => "foo") - end - - # Now check the defaults - config = nil - assert_nothing_raised("Could not init config with all required options") do - config = Config.new(:host => "foo", :interpreter => "interp") - end - - assert_equal("foo", config.host, "Did not set host correctly") - assert_equal("interp", config.interpreter, "Did not set interpreter correctly") - assert_equal({}, config.facts, "Did not set default facts") - - # Now make a new one with facts, to make sure the facts get set appropriately - assert_nothing_raised("Could not init config with all required options") do - config = Config.new(:host => "foo", :interpreter => "interp", :facts => {"a" => "b"}) - end - assert_equal({"a" => "b"}, config.facts, "Did not set facts") - end - - def test_initvars - config = mkconfig - [:class_scopes, :resource_table, :exported_resources, :resource_overrides].each do |table| - assert_instance_of(Hash, config.send(:instance_variable_get, "@#{table}"), "Did not set %s table correctly" % table) - end - end - - # Make sure we store and can retrieve references to classes and their scopes. - def test_class_set_and_class_scope - klass = Object.new - klass.expects(:classname).returns("myname") - - config = mkconfig - - assert_nothing_raised("Could not set class") do - config.class_set "myname", "myscope" - end - # First try to retrieve it by name. - assert_equal("myscope", config.class_scope("myname"), "Could not retrieve class scope by name") - - # Then by object - assert_equal("myscope", config.class_scope(klass), "Could not retrieve class scope by object") - end - - def test_classlist - config = mkconfig - - config.class_set "", "empty" - config.class_set "one", "yep" - config.class_set "two", "nope" - - # Make sure our class list is correct - assert_equal(%w{one two}.sort, config.classlist.sort, "Did not get correct class list") - end - - # Make sure collections get added to our internal array - def test_add_collection - config = mkconfig - assert_nothing_raised("Could not add collection") do - config.add_collection "nope" - end - assert_equal(%w{nope}, config.instance_variable_get("@collections"), "Did not add collection") - end - - # Make sure we create a graph of scopes. - def test_newscope - config = mkconfig - graph = config.instance_variable_get("@graph") - assert_instance_of(Scope, config.topscope, "Did not create top scope") - assert_instance_of(GRATR::Digraph, graph, "Did not create graph") - - assert(graph.vertex?(config.topscope), "The top scope is not a vertex in the graph") - - # Now that we've got the top scope, create a new, subscope - subscope = nil - assert_nothing_raised("Could not create subscope") do - subscope = config.newscope - end - assert_instance_of(Scope, subscope, "Did not create subscope") - assert(graph.edge?(config.topscope, subscope), "An edge between top scope and subscope was not added") - - # Make sure a scope can find its parent. - assert(config.parent(subscope), "Could not look up parent scope on configuration") - assert_equal(config.topscope.object_id, config.parent(subscope).object_id, "Did not get correct parent scope from configuration") - assert_equal(config.topscope.object_id, subscope.parent.object_id, "Scope did not correctly retrieve its parent scope") - - # Now create another, this time specifying the parent scope - another = nil - assert_nothing_raised("Could not create subscope") do - another = config.newscope(subscope) - end - assert_instance_of(Scope, another, "Did not create second subscope") - assert(graph.edge?(subscope, another), "An edge between parent scope and second subscope was not added") - - # Make sure it can find its parent. - assert(config.parent(another), "Could not look up parent scope of second subscope on configuration") - assert_equal(subscope.object_id, config.parent(another).object_id, "Did not get correct parent scope of second subscope from configuration") - assert_equal(subscope.object_id, another.parent.object_id, "Second subscope did not correctly retrieve its parent scope") - - # And make sure both scopes show up in the right order in the search path - assert_equal([another.object_id, subscope.object_id, config.topscope.object_id], another.scope_path.collect { |p| p.object_id }, - "Did not get correct scope path") - end -end diff --git a/test/language/functions.rb b/test/language/functions.rb index 34207de17..be06d8eac 100755 --- a/test/language/functions.rb +++ b/test/language/functions.rb @@ -1,532 +1,539 @@ #!/usr/bin/env ruby $:.unshift("../lib").unshift("../../lib") if __FILE__ =~ /\.rb$/ require 'puppet' -require 'puppet/parser/interpreter' require 'puppet/parser/parser' require 'puppet/network/client' require 'puppettest' require 'puppettest/resourcetesting' class TestLangFunctions < Test::Unit::TestCase include PuppetTest::ParserTesting include PuppetTest::ResourceTesting def test_functions assert_raise(Puppet::ParseError) do Puppet::Parser::AST::Function.new( :name => "fakefunction", :arguments => AST::ASTArray.new( :children => [nameobj("avalue")] ) ) end assert_nothing_raised do Puppet::Parser::Functions.newfunction(:fakefunction, :type => :rvalue) do |input| return "output %s" % input[0] end end func = nil assert_nothing_raised do func = Puppet::Parser::AST::Function.new( :name => "fakefunction", :ftype => :rvalue, :arguments => AST::ASTArray.new( :children => [nameobj("avalue")] ) ) end scope = mkscope val = nil assert_nothing_raised do val = func.evaluate(:scope => scope) end assert_equal("output avalue", val) end def test_taggedfunction scope = mkscope + scope.resource.tag("yayness") - tag = "yayness" - scope.tag(tag) - + # Make sure the ast stuff does what it's supposed to {"yayness" => true, "booness" => false}.each do |tag, retval| func = taggedobj(tag, :rvalue) val = nil assert_nothing_raised do val = func.evaluate(:scope => scope) end assert_equal(retval, val, "'tagged' returned %s for %s" % [val, tag]) end + + # Now make sure we correctly get tags. + scope.resource.tag("resourcetag") + assert(scope.function_tagged("resourcetag"), "tagged function did not catch resource tags") + scope.compile.configuration.tag("configtag") + assert(scope.function_tagged("configtag"), "tagged function did not catch configuration tags") end def test_failfunction func = nil assert_nothing_raised do func = Puppet::Parser::AST::Function.new( :name => "fail", :ftype => :statement, :arguments => AST::ASTArray.new( :children => [stringobj("this is a failure"), stringobj("and another")] ) ) end scope = mkscope val = nil assert_raise(Puppet::ParseError) do val = func.evaluate(:scope => scope) end end def test_multipletemplates Dir.mkdir(Puppet[:templatedir]) onep = File.join(Puppet[:templatedir], "one") twop = File.join(Puppet[:templatedir], "two") File.open(onep, "w") do |f| f.puts "template <%= one %>" end File.open(twop, "w") do |f| f.puts "template <%= two %>" end func = nil assert_nothing_raised do func = Puppet::Parser::AST::Function.new( :name => "template", :ftype => :rvalue, :arguments => AST::ASTArray.new( :children => [stringobj("one"), stringobj("two")] ) ) end ast = varobj("output", func) scope = mkscope assert_raise(Puppet::ParseError) do ast.evaluate(:scope => scope) end scope.setvar("one", "One") assert_raise(Puppet::ParseError) do ast.evaluate(:scope => scope) end scope.setvar("two", "Two") assert_nothing_raised do ast.evaluate(:scope => scope) end assert_equal("template One\ntemplate Two\n", scope.lookupvar("output"), "Templates were not handled correctly") end # Now make sure we can fully qualify files, and specify just one def test_singletemplates template = tempfile() File.open(template, "w") do |f| f.puts "template <%= yayness %>" end func = nil assert_nothing_raised do func = Puppet::Parser::AST::Function.new( :name => "template", :ftype => :rvalue, :arguments => AST::ASTArray.new( :children => [stringobj(template)] ) ) end ast = varobj("output", func) scope = mkscope assert_raise(Puppet::ParseError) do ast.evaluate(:scope => scope) end scope.setvar("yayness", "this is yayness") assert_nothing_raised do ast.evaluate(:scope => scope) end assert_equal("template this is yayness\n", scope.lookupvar("output"), "Templates were not handled correctly") end def test_tempatefunction_cannot_see_scopes template = tempfile() File.open(template, "w") do |f| f.puts "<%= lookupvar('myvar') %>" end func = nil assert_nothing_raised do func = Puppet::Parser::AST::Function.new( :name => "template", :ftype => :rvalue, :arguments => AST::ASTArray.new( :children => [stringobj(template)] ) ) end ast = varobj("output", func) scope = mkscope scope.setvar("myvar", "this is yayness") assert_raise(Puppet::ParseError) do ast.evaluate(:scope => scope) end end def test_template_reparses template = tempfile() File.open(template, "w") do |f| f.puts "original text" end manifest = tempfile() file = tempfile() File.open(manifest, "w") do |f| f.puts %{file { "#{file}": content => template("#{template}") }} end - interpreter = Puppet::Parser::Interpreter.new( + interp = Puppet::Parser::Interpreter.new( :Manifest => manifest, :UseNodes => false ) + node = mknode + node.stubs(:environment).returns("yay") - parsedate = interpreter.parsedate() + Puppet[:environment] = "yay" - objects = nil + configuration = nil assert_nothing_raised { - objects = interpreter.run("myhost", {}) + configuration = interp.compile(node) } - fileobj = objects[0] + version = configuration.version + + fileobj = configuration.vertices.find { |r| r.title == file } + assert(fileobj, "File was not in configuration") assert_equal("original text\n", fileobj["content"], "Template did not work") - Puppet[:filetimeout] = 0 + Puppet[:filetimeout] = -5 # Have to sleep because one second is the fs's time granularity. sleep(1) # Now modify the template File.open(template, "w") do |f| f.puts "new text" end - assert_nothing_raised { - objects = interpreter.run("myhost", {}) - } - newdate = interpreter.parsedate() + newversion = interp.compile(node).version - assert(parsedate != newdate, "Parse date did not change") + assert(version != newversion, "Parse date did not change") end def test_template_defined_vars template = tempfile() File.open(template, "w") do |f| f.puts "template <%= yayness %>" end func = nil assert_nothing_raised do func = Puppet::Parser::AST::Function.new( :name => "template", :ftype => :rvalue, :arguments => AST::ASTArray.new( :children => [stringobj(template)] ) ) end ast = varobj("output", func) { "" => "", false => "false", }.each do |string, value| scope = mkscope assert_raise(Puppet::ParseError) do ast.evaluate(:scope => scope) end scope.setvar("yayness", string) assert_equal(string, scope.lookupvar("yayness", false)) assert_nothing_raised("An empty string was not a valid variable value") do ast.evaluate(:scope => scope) end assert_equal("template #{value}\n", scope.lookupvar("output"), "%s did not get evaluated correctly" % string.inspect) end end def test_autoloading_functions assert_equal(false, Puppet::Parser::Functions.function(:autofunc), "Got told autofunc already exists") dir = tempfile() $: << dir newpath = File.join(dir, "puppet", "parser", "functions") FileUtils.mkdir_p(newpath) File.open(File.join(newpath, "autofunc.rb"), "w") { |f| f.puts %{ Puppet::Parser::Functions.newfunction(:autofunc, :type => :rvalue) do |vals| Puppet.wanring vals.inspect end } } obj = nil assert_nothing_raised { obj = Puppet::Parser::Functions.function(:autofunc) } assert(obj, "Did not autoload function") assert(Puppet::Parser::Scope.method_defined?(:function_autofunc), "Did not set function correctly") end def test_realize - @interp, @scope, @source = mkclassframing + scope = mkscope + parser = scope.compile.parser # Make a definition - @interp.newdefine("mytype") + parser.newdefine("mytype") [%w{file /tmp/virtual}, %w{mytype yay}].each do |type, title| # Make a virtual resource virtual = mkresource(:type => type, :title => title, - :virtual => true, :params => {}) + :virtual => true, :params => {}, :scope => scope) - @scope.setresource virtual + scope.compile.store_resource(scope, virtual) ref = Puppet::Parser::Resource::Reference.new( :type => type, :title => title, - :scope => @scope + :scope => scope ) # Now call the realize function assert_nothing_raised do - @scope.function_realize(ref) + scope.function_realize(ref) end # Make sure it created a collection - assert_equal(1, @scope.collections.length, + assert_equal(1, scope.compile.collections.length, "Did not set collection") assert_nothing_raised do - @scope.collections.each do |coll| coll.evaluate end + scope.compile.collections.each do |coll| coll.evaluate end end - @scope.collections.clear + scope.compile.collections.clear # Now make sure the virtual resource is no longer virtual assert(! virtual.virtual?, "Did not make virtual resource real") end # Make sure we puke on any resource that doesn't exist none = Puppet::Parser::Resource::Reference.new( :type => "file", :title => "/tmp/nosuchfile", - :scope => @scope + :scope => scope ) # The function works assert_nothing_raised do - @scope.function_realize(none.to_s) + scope.function_realize(none.to_s) end # Make sure it created a collection - assert_equal(1, @scope.collections.length, + assert_equal(1, scope.compile.collections.length, "Did not set collection") # And the collection has our resource in it - assert_equal([none.to_s], @scope.collections[0].resources, + assert_equal([none.to_s], scope.compile.collections[0].resources, "Did not set resources in collection") end def test_defined - interp = mkinterp - scope = mkscope(:interp => interp) + scope = mkscope + parser = scope.compile.parser - interp.newclass("yayness") - interp.newdefine("rahness") + parser.newclass("yayness") + parser.newdefine("rahness") assert_nothing_raised do assert(scope.function_defined("yayness"), "yayness class was not considered defined") assert(scope.function_defined("rahness"), "rahness definition was not considered defined") assert(scope.function_defined("service"), "service type was not considered defined") assert(! scope.function_defined("fakness"), "fakeness was considered defined") end # Now make sure any match in a list will work assert(scope.function_defined(["booness", "yayness", "fakeness"]), "A single answer was not sufficient to return true") # and make sure multiple falses are still false assert(! scope.function_defined(%w{no otherno stillno}), "Multiple falses were somehow true") # Now make sure we can test resources - scope.setresource mkresource(:type => "file", :title => "/tmp/rahness", + scope.compile.store_resource(scope, mkresource(:type => "file", :title => "/tmp/rahness", :scope => scope, :source => scope.source, - :params => {:owner => "root"}) + :params => {:owner => "root"})) yep = Puppet::Parser::Resource::Reference.new(:type => "file", :title => "/tmp/rahness") nope = Puppet::Parser::Resource::Reference.new(:type => "file", :title => "/tmp/fooness") assert(scope.function_defined([yep]), "valid resource was not considered defined") assert(! scope.function_defined([nope]), "invalid resource was considered defined") end def test_search - interp = mkinterp - scope = mkscope(:interp => interp) + parser = mkparser + scope = mkscope(:parser => parser) - fun = interp.newdefine("yay::ness") - foo = interp.newdefine("foo::bar") + fun = parser.newdefine("yay::ness") + foo = parser.newdefine("foo::bar") search = Puppet::Parser::Functions.function(:search) assert_nothing_raised do scope.function_search(["foo", "yay"]) end ffun = ffoo = nil assert_nothing_raised("Search path change did not work") do ffun = scope.finddefine("ness") ffoo = scope.finddefine('bar') end assert(ffun, "Could not find definition in 'fun' namespace") assert(ffoo, "Could not find definition in 'foo' namespace") end def test_include - interp = mkinterp - scope = mkscope(:interp => interp) + scope = mkscope + parser = scope.compile.parser assert_raise(Puppet::ParseError, "did not throw error on missing class") do scope.function_include("nosuchclass") end - interp.newclass("myclass") + parser.newclass("myclass") assert_nothing_raised do scope.function_include "myclass" end - assert(scope.classlist.include?("myclass"), + assert(scope.compile.resources.find { |r| r.to_s == "Class[myclass]" }, "class was not evaluated") # Now try multiple classes at once - classes = %w{one two three}.each { |c| interp.newclass(c) } + classes = %w{one two three}.each { |c| parser.newclass(c) } assert_nothing_raised do scope.function_include classes end classes.each do |c| - assert(scope.classlist.include?(c), + assert(scope.compile.resources.find { |r| r.to_s == "Class[#{c}]" }, "class %s was not evaluated" % c) end # Now try a scoped class - interp.newclass("os::redhat") + parser.newclass("os::redhat") assert_nothing_raised("Could not include qualified class name") do scope.function_include("os::redhat") end end def test_file - interp = mkinterp - scope = mkscope(:interp => interp) + parser = mkparser + scope = mkscope(:parser => parser) file1 = tempfile file2 = tempfile file3 = tempfile File.open(file2, "w") { |f| f.puts "yaytest" } val = nil assert_nothing_raised("Failed to call file with one arg") do val = scope.function_file([file2]) end assert_equal("yaytest\n", val, "file() failed") assert_nothing_raised("Failed to call file with two args") do val = scope.function_file([file1, file2]) end assert_equal("yaytest\n", val, "file() failed") assert_raise(Puppet::ParseError, "did not fail when files are missing") do val = scope.function_file([file1, file3]) end end def test_generate command = tempfile sh = %x{which sh} File.open(command, "w") do |f| f.puts %{#!#{sh} if [ -n "$1" ]; then echo "yay-$1" else echo yay fi } end File.chmod(0755, command) assert_equal("yay\n", %x{#{command}}, "command did not work") assert_equal("yay-foo\n", %x{#{command} foo}, "command did not work") - interp = mkinterp - scope = mkscope(:interp => interp) + scope = mkscope + parser = scope.compile.parser val = nil assert_nothing_raised("Could not call generator with no args") do val = scope.function_generate([command]) end assert_equal("yay\n", val, "generator returned wrong results") assert_nothing_raised("Could not call generator with args") do val = scope.function_generate([command, "foo"]) end assert_equal("yay-foo\n", val, "generator returned wrong results") assert_raise(Puppet::ParseError, "Did not fail with an unqualified path") do val = scope.function_generate([File.basename(command), "foo"]) end assert_raise(Puppet::ParseError, "Did not fail when command failed") do val = scope.function_generate([%x{which touch}.chomp, "/this/dir/does/not/exist"]) end fake = File.join(File.dirname(command), "..") dir = File.dirname(command) dirname = File.basename(dir) bad = File.join(dir, "..", dirname, File.basename(command)) assert_raise(Puppet::ParseError, "Did not fail when command failed") do val = scope.function_generate([bad]) end end end # $Id$ diff --git a/test/language/interpreter.rb b/test/language/interpreter.rb deleted file mode 100755 index 070e2e77e..000000000 --- a/test/language/interpreter.rb +++ /dev/null @@ -1,537 +0,0 @@ -#!/usr/bin/env ruby - -$:.unshift("../lib").unshift("../../lib") if __FILE__ =~ /\.rb$/ - -require 'facter' - -require 'puppet' -require 'puppet/parser/interpreter' -require 'puppet/parser/parser' -require 'puppet/network/client' -require 'puppettest' -require 'puppettest/resourcetesting' -require 'puppettest/parsertesting' -require 'puppettest/servertest' -require 'timeout' - -class TestInterpreter < PuppetTest::TestCase - include PuppetTest - include PuppetTest::ServerTest - include PuppetTest::ParserTesting - include PuppetTest::ResourceTesting - AST = Puppet::Parser::AST - NodeDef = Puppet::Parser::Interpreter::NodeDef - - # create a simple manifest that uses nodes to create a file - def mknodemanifest(node, file) - createdfile = tempfile() - - File.open(file, "w") { |f| - f.puts "node %s { file { \"%s\": ensure => file, mode => 755 } }\n" % - [node, createdfile] - } - - return [file, createdfile] - end - - def test_simple - file = tempfile() - File.open(file, "w") { |f| - f.puts "file { \"/etc\": owner => root }" - } - assert_nothing_raised { - Puppet::Parser::Interpreter.new(:Manifest => file) - } - end - - def test_reloadfiles - hostname = Facter["hostname"].value - - file = tempfile() - - # Create a first version - createdfile = mknodemanifest(hostname, file) - - interp = nil - assert_nothing_raised { - interp = Puppet::Parser::Interpreter.new(:Manifest => file) - } - - config = nil - assert_nothing_raised { - config = interp.run(hostname, {}) - } - sleep(1) - - # Now create a new file - createdfile = mknodemanifest(hostname, file) - - newconfig = nil - assert_nothing_raised { - newconfig = interp.run(hostname, {}) - } - - assert(config != newconfig, "Configs are somehow the same") - end - - # Make sure searchnode behaves as we expect. - def test_nodesearch - # We use two sources here to catch a weird bug where the default - # node is used if the host isn't in the first source. - interp = mkinterp - - # Make some nodes - names = %w{node1 node2 node2.domain.com} - interp.newnode names - interp.newnode %w{default} - - nodes = {} - # Make sure we can find them all, using the direct method - names.each do |name| - nodes[name] = interp.nodesearch_code(name) - assert(nodes[name], "Could not find %s" % name) - nodes[name].file = __FILE__ - end - - # Now let's try it with the nodesearch method - names.each do |name| - node = interp.nodesearch(name) - assert(node, "Could not find #{name} via nodesearch") - end - - # Make sure we find the default node when we search for nonexistent nodes - assert_nothing_raised do - default = interp.nodesearch("nosuchnode") - assert(default, "Did not find default node") - assert_equal("default", default.classname) - end - - # Now make sure the longest match always wins - node = interp.nodesearch(*%w{node2 node2.domain.com}) - - assert(node, "Did not find node2") - assert_equal("node2.domain.com", node.classname, - "Did not get longest match") - end - - def test_parsedate - Puppet[:filetimeout] = 0 - main = tempfile() - sub = tempfile() - mainfile = tempfile() - subfile = tempfile() - count = 0 - updatemain = proc do - count += 1 - File.open(main, "w") { |f| - f.puts "import '#{sub}' - file { \"#{mainfile}\": content => #{count} } - " - } - end - updatesub = proc do - count += 1 - File.open(sub, "w") { |f| - f.puts "file { \"#{subfile}\": content => #{count} } - " - } - end - - updatemain.call - updatesub.call - - interp = Puppet::Parser::Interpreter.new( - :Manifest => main, - :Local => true - ) - - date = interp.parsedate - - # Now update the site file and make sure we catch it - sleep 1 - updatemain.call - newdate = interp.parsedate - assert(date != newdate, "Parsedate was not updated") - date = newdate - - # And then the subfile - sleep 1 - updatesub.call - newdate = interp.parsedate - assert(date != newdate, "Parsedate was not updated") - end - - # Make sure class, node, and define methods are case-insensitive - def test_structure_case_insensitivity - interp = mkinterp - - result = nil - assert_nothing_raised do - result = interp.newclass "Yayness" - end - assert_equal(result, interp.findclass("", "yayNess")) - - assert_nothing_raised do - result = interp.newdefine "FunTest" - end - assert_equal(result, interp.finddefine("", "fUntEst"), - "%s was not matched" % "fUntEst") - - assert_nothing_raised do - result = interp.newnode("MyNode").shift - end - assert_equal(result, interp.nodesearch("mYnOde"), - "mYnOde was not matched") - - assert_nothing_raised do - result = interp.newnode("YayTest.Domain.Com").shift - end - assert_equal(result, interp.nodesearch("yaYtEst.domAin.cOm"), - "yaYtEst.domAin.cOm was not matched") - end - - # Make sure our whole chain works. - def test_evaluate - interp, scope, source = mkclassframing - - # Create a define that we'll be using - interp.newdefine("wrapper", :code => AST::ASTArray.new(:children => [ - resourcedef("file", varref("name"), "owner" => "root") - ])) - - # Now create a resource that uses that define - define = mkresource(:type => "wrapper", :title => "/tmp/testing", - :scope => scope, :source => source, :params => :none) - - scope.setresource define - - # And a normal resource - scope.setresource mkresource(:type => "file", :title => "/tmp/rahness", - :scope => scope, :source => source, - :params => {:owner => "root"}) - - # Now evaluate everything - objects = nil - interp.usenodes = false - assert_nothing_raised do - objects = interp.evaluate(nil, {}) - end - - assert_instance_of(Puppet::TransBucket, objects) - end - - # Test evaliterate. It's a very simple method, but it's pretty tough - # to test. It iterates over collections and instances of defined types - # until there's no more work to do. - def test_evaliterate - interp, scope, source = mkclassframing - - # Create a top-level definition that creates a builtin object - interp.newdefine("one", :arguments => [%w{owner}], - :code => AST::ASTArray.new(:children => [ - resourcedef("file", varref("name"), - "owner" => varref("owner") - ) - ]) - ) - - # Create another definition to call that one - interp.newdefine("two", :arguments => [%w{owner}], - :code => AST::ASTArray.new(:children => [ - resourcedef("one", varref("name"), - "owner" => varref("owner") - ) - ]) - ) - - # And then a third - interp.newdefine("three", :arguments => [%w{owner}], - :code => AST::ASTArray.new(:children => [ - resourcedef("two", varref("name"), - "owner" => varref("owner") - ) - ]) - ) - - # And create a definition that creates a virtual resource - interp.newdefine("virtualizer", :arguments => [%w{owner}], - :code => AST::ASTArray.new(:children => [ - virt_resourcedef("one", varref("name"), - "owner" => varref("owner") - ) - ]) - ) - - # Now create an instance of three - three = Puppet::Parser::Resource.new( - :type => "three", :title => "one", - :scope => scope, :source => source, - :params => paramify(source, :owner => "root") - ) - scope.setresource(three) - - # An instance of the virtualizer - virt = Puppet::Parser::Resource.new( - :type => "virtualizer", :title => "two", - :scope => scope, :source => source, - :params => paramify(source, :owner => "root") - ) - scope.setresource(virt) - - # And a virtual instance of three - virt_three = Puppet::Parser::Resource.new( - :type => "three", :title => "three", - :scope => scope, :source => source, - :params => paramify(source, :owner => "root") - ) - virt_three.virtual = true - scope.setresource(virt_three) - - # Create a normal, virtual resource - plainvirt = Puppet::Parser::Resource.new( - :type => "user", :title => "five", - :scope => scope, :source => source, - :params => paramify(source, :uid => "root") - ) - plainvirt.virtual = true - scope.setresource(plainvirt) - - # Now create some collections for our virtual resources - %w{Three[three] One[two]}.each do |ref| - coll = Puppet::Parser::Collector.new(scope, "file", nil, nil, :virtual) - coll.resources = [ref] - scope.newcollection(coll) - end - - # And create a generic user collector for our plain resource - coll = Puppet::Parser::Collector.new(scope, "user", nil, nil, :virtual) - scope.newcollection(coll) - - ret = nil - assert_nothing_raised do - ret = scope.unevaluated - end - - - assert_instance_of(Array, ret) - assert_equal(3, ret.length, - "did not get the correct number of unevaled resources") - - # Now translate the whole tree - assert_nothing_raised do - Timeout::timeout(2) do - interp.evaliterate(scope) - end - end - - # Now make sure we've got all of our files - %w{one two three}.each do |name| - file = scope.findresource("File[%s]" % name) - assert(file, "Could not find file %s" % name) - - assert_equal("root", file[:owner]) - assert(! file.virtual?, "file %s is still virtual" % name) - end - - # Now make sure we found the user - assert(! plainvirt.virtual?, "user was not realized") - end - - # Make sure we fail if there are any leftover overrides to perform. - # This would normally mean that someone is trying to override an object - # that does not exist. - def test_failonleftovers - interp, scope, source = mkclassframing - - # Make sure we don't fail, since there are no overrides - assert_nothing_raised do - interp.failonleftovers(scope) - end - - # Add an override, and make sure it causes a failure - over1 = mkresource :scope => scope, :source => source, - :params => {:one => "yay"} - - scope.setoverride(over1) - - assert_raise(Puppet::ParseError) do - interp.failonleftovers(scope) - end - - # Make a new scope to test leftover collections - scope = mkscope :interp => interp - interp.meta_def(:check_resource_collections) do - raise ArgumentError, "yep" - end - - assert_raise(ArgumentError, "did not call check_resource_colls") do - interp.failonleftovers(scope) - end - end - - def test_evalnode - interp = mkinterp - interp.usenodes = false - scope = Parser::Scope.new(:interp => interp) - facts = Facter.to_hash - - # First make sure we get no failures when client is nil - assert_nothing_raised do - interp.evalnode(nil, scope, facts) - end - - # Now define a node - interp.newnode "mynode", :code => AST::ASTArray.new(:children => [ - resourcedef("file", "/tmp/testing", "owner" => "root") - ]) - - # Eval again, and make sure it does nothing - assert_nothing_raised do - interp.evalnode("mynode", scope, facts) - end - - assert_nil(scope.findresource("File[/tmp/testing]"), - "Eval'ed node with nodes off") - - # Now enable usenodes and make sure it works. - interp.usenodes = true - assert_nothing_raised do - interp.evalnode("mynode", scope, facts) - end - file = scope.findresource("File[/tmp/testing]") - - assert_instance_of(Puppet::Parser::Resource, file, - "Could not find file") - end - - # This is mostly used for the cfengine module - def test_specificclasses - interp = mkinterp :Classes => %w{klass1 klass2}, :UseNodes => false - - # Make sure it's not a failure to be missing classes, since - # we're using the cfengine class list, which is huge. - assert_nothing_raised do - interp.evaluate(nil, {}) - end - - interp.newclass("klass1", :code => AST::ASTArray.new(:children => [ - resourcedef("file", "/tmp/klass1", "owner" => "root") - ])) - interp.newclass("klass2", :code => AST::ASTArray.new(:children => [ - resourcedef("file", "/tmp/klass2", "owner" => "root") - ])) - - ret = nil - assert_nothing_raised do - ret = interp.evaluate(nil, {}) - end - - found = ret.flatten.collect do |res| res.name end - - assert(found.include?("/tmp/klass1"), "Did not evaluate klass1") - assert(found.include?("/tmp/klass2"), "Did not evaluate klass2") - end - - def test_check_resource_collections - interp = mkinterp - scope = mkscope :interp => interp - coll = Puppet::Parser::Collector.new(scope, "file", nil, nil, :virtual) - coll.resources = ["File[/tmp/virtual1]", "File[/tmp/virtual2]"] - scope.newcollection(coll) - - assert_raise(Puppet::ParseError, "Did not fail on remaining resource colls") do - interp.check_resource_collections(scope) - end - end - - def test_nodedef - interp = mkinterp - interp.newclass("base") - interp.newclass("sub", :parent => "base") - interp.newclass("other") - - node = nil - assert_nothing_raised("Could not create a node definition") do - node = NodeDef.new :name => "yay", :classes => "sub", :parameters => {"one" => "two", "three" => "four"} - end - - scope = mkscope :interp => interp - assert_nothing_raised("Could not evaluate the node definition") do - node.evaluate(:scope => scope) - end - - assert_equal("two", scope.lookupvar("one"), "NodeDef did not set variable") - assert_equal("four", scope.lookupvar("three"), "NodeDef did not set variable") - - assert(scope.classlist.include?("sub"), "NodeDef did not evaluate class") - assert(scope.classlist.include?("base"), "NodeDef did not evaluate base class") - - # Now try a node def with multiple classes - assert_nothing_raised("Could not create a node definition") do - node = NodeDef.new :name => "yay", :classes => %w{sub other base}, :parameters => {"one" => "two", "three" => "four"} - end - - scope = mkscope :interp => interp - assert_nothing_raised("Could not evaluate the node definition") do - node.evaluate(:scope => scope) - end - - assert_equal("two", scope.lookupvar("one"), "NodeDef did not set variable") - assert_equal("four", scope.lookupvar("three"), "NodeDef did not set variable") - - assert(scope.classlist.include?("sub"), "NodeDef did not evaluate class") - assert(scope.classlist.include?("other"), "NodeDef did not evaluate other class") - - # And a node def with no params - assert_nothing_raised("Could not create a node definition with no params") do - node = NodeDef.new :name => "yay", :classes => %w{sub other base} - end - - scope = mkscope :interp => interp - assert_nothing_raised("Could not evaluate the node definition") do - node.evaluate(:scope => scope) - end - - assert(scope.classlist.include?("sub"), "NodeDef did not evaluate class") - assert(scope.classlist.include?("other"), "NodeDef did not evaluate other class") - - # Now make sure nodedef doesn't fail when some classes are not defined (#687). - assert_nothing_raised("Could not create a node definition with some invalid classes") do - node = NodeDef.new :name => "yay", :classes => %w{base unknown} - end - - scope = mkscope :interp => interp - assert_nothing_raised("Could not evaluate the node definition with some invalid classes") do - node.evaluate(:scope => scope) - end - - assert(scope.classlist.include?("base"), "NodeDef did not evaluate class") - end - - # Make sure that reparsing is atomic -- failures don't cause a broken state, and we aren't subject - # to race conditions if someone contacts us while we're reparsing. - def test_atomic_reparsing - Puppet[:filetimeout] = -10 - file = tempfile - File.open(file, "w") { |f| f.puts %{file { '/tmp': ensure => directory }} } - interp = mkinterp :Manifest => file, :UseNodes => false - - assert_nothing_raised("Could not compile the first time") do - interp.run("yay", {}) - end - - oldparser = interp.send(:instance_variable_get, "@parser") - - # Now add a syntax failure - File.open(file, "w") { |f| f.puts %{file { /tmp: ensure => directory }} } - assert_nothing_raised("Could not compile the first time") do - interp.run("yay", {}) - end - - # And make sure the old parser is still there - newparser = interp.send(:instance_variable_get, "@parser") - assert_equal(oldparser.object_id, newparser.object_id, "Failed parser still replaced existing parser") - end -end - -# $Id$ diff --git a/test/language/parser.rb b/test/language/parser.rb index afffa9293..1cef72dcb 100755 --- a/test/language/parser.rb +++ b/test/language/parser.rb @@ -1,1180 +1,1197 @@ #!/usr/bin/env ruby $:.unshift("../lib").unshift("../../lib") if __FILE__ =~ /\.rb$/ require 'mocha' require 'puppet' require 'puppet/parser/parser' require 'puppettest' class TestParser < Test::Unit::TestCase include PuppetTest::ParserTesting def setup super Puppet[:parseonly] = true #@lexer = Puppet::Parser::Lexer.new() end def test_each_file textfiles { |file| parser = mkparser Puppet.debug("parsing %s" % file) if __FILE__ == $0 assert_nothing_raised() { parser.file = file parser.parse } Puppet::Type.eachtype { |type| type.each { |obj| assert(obj.file, "File is not set on %s" % obj.ref) assert(obj.name, "Name is not set on %s" % obj.ref) assert(obj.line, "Line is not set on %s" % obj.ref) } } Puppet::Type.allclear } end def test_failers failers { |file| parser = mkparser - interp = mkinterp Puppet.debug("parsing failer %s" % file) if __FILE__ == $0 - assert_raise(Puppet::ParseError) { + assert_raise(Puppet::ParseError, "Did not fail while parsing %s" % file) { parser.file = file ast = parser.parse - scope = mkscope :interp => interp - ast.classes[""].evaluate :scope => scope + config = mkcompile(parser) + config.compile + #ast.classes[""].evaluate :scope => config.topscope } Puppet::Type.allclear } end def test_arrayrvalues parser = mkparser ret = nil file = tempfile() assert_nothing_raised { parser.string = "file { \"#{file}\": mode => [755, 640] }" } assert_nothing_raised { ret = parser.parse } end def mkmanifest(file) name = File.join(tmpdir, "file%s" % rand(100)) @@tmpfiles << name File.open(file, "w") { |f| f.puts "file { \"%s\": ensure => file, mode => 755 }\n" % name } end def test_importglobbing basedir = File.join(tmpdir(), "importesting") @@tmpfiles << basedir Dir.mkdir(basedir) subdir = "subdir" Dir.mkdir(File.join(basedir, subdir)) manifest = File.join(basedir, "manifest") File.open(manifest, "w") { |f| f.puts "import \"%s/*\"" % subdir } 4.times { |i| path = File.join(basedir, subdir, "subfile%s" % i) mkmanifest(path) } assert_nothing_raised("Could not parse multiple files") { parser = mkparser parser.file = manifest parser.parse } end def test_nonexistent_import basedir = File.join(tmpdir(), "importesting") @@tmpfiles << basedir Dir.mkdir(basedir) manifest = File.join(basedir, "manifest") File.open(manifest, "w") do |f| f.puts "import \" no such file \"" end assert_raise(Puppet::ParseError) { parser = mkparser parser.file = manifest parser.parse } end def test_trailingcomma path = tempfile() str = %{file { "#{path}": ensure => file, } } parser = mkparser parser.string = str assert_nothing_raised("Could not parse trailing comma") { parser.parse } end def test_importedclasses imported = tempfile() importer = tempfile() made = tempfile() File.open(imported, "w") do |f| f.puts %{class foo { file { "#{made}": ensure => file }}} end File.open(importer, "w") do |f| f.puts %{import "#{imported}"\ninclude foo} end parser = mkparser parser.file = importer # Make sure it parses fine assert_nothing_raised { parser.parse } # Now make sure it actually does the work assert_creates(importer, made) end # Make sure fully qualified and unqualified files can be imported def test_fqfilesandlocalfiles dir = tempfile() Dir.mkdir(dir) importer = File.join(dir, "site.pp") fullfile = File.join(dir, "full.pp") localfile = File.join(dir, "local.pp") files = [] File.open(importer, "w") do |f| f.puts %{import "#{fullfile}"\ninclude full\nimport "local.pp"\ninclude local} end fullmaker = tempfile() files << fullmaker File.open(fullfile, "w") do |f| f.puts %{class full { file { "#{fullmaker}": ensure => file }}} end localmaker = tempfile() files << localmaker File.open(localfile, "w") do |f| f.puts %{class local { file { "#{localmaker}": ensure => file }}} end parser = mkparser parser.file = importer # Make sure it parses assert_nothing_raised { parser.parse } # Now make sure it actually does the work assert_creates(importer, *files) end # Make sure the parser adds '.pp' when necessary def test_addingpp dir = tempfile() Dir.mkdir(dir) importer = File.join(dir, "site.pp") localfile = File.join(dir, "local.pp") files = [] File.open(importer, "w") do |f| f.puts %{import "local"\ninclude local} end file = tempfile() files << file File.open(localfile, "w") do |f| f.puts %{class local { file { "#{file}": ensure => file }}} end parser = mkparser parser.file = importer assert_nothing_raised { parser.parse } end # Make sure that file importing changes file relative names. def test_changingrelativenames dir = tempfile() Dir.mkdir(dir) Dir.mkdir(File.join(dir, "subdir")) top = File.join(dir, "site.pp") subone = File.join(dir, "subdir/subone") subtwo = File.join(dir, "subdir/subtwo") files = [] file = tempfile() files << file File.open(subone + ".pp", "w") do |f| f.puts %{class one { file { "#{file}": ensure => file }}} end otherfile = tempfile() files << otherfile File.open(subtwo + ".pp", "w") do |f| f.puts %{import "subone"\n class two inherits one { file { "#{otherfile}": ensure => file } }} end File.open(top, "w") do |f| f.puts %{import "subdir/subtwo"} end parser = mkparser parser.file = top assert_nothing_raised { parser.parse } end # Defaults are purely syntactical, so it doesn't make sense to be able to # collect them. def test_uncollectabledefaults string = "@Port { protocols => tcp }" assert_raise(Puppet::ParseError) { mkparser.parse(string) } end # Verify that we can parse collections def test_collecting text = "Port <| |>" parser = mkparser parser.string = text ret = nil assert_nothing_raised { ret = parser.parse } ret.classes[""].code.each do |obj| assert_instance_of(AST::Collection, obj) end end def test_emptyfile file = tempfile() File.open(file, "w") do |f| f.puts %{} end parser = mkparser parser.file = file assert_nothing_raised { parser.parse } end def test_multiple_nodes_named file = tempfile() other = tempfile() File.open(file, "w") do |f| f.puts %{ node nodeA, nodeB { file { "#{other}": ensure => file } } } end parser = mkparser parser.file = file ast = nil assert_nothing_raised { ast = parser.parse } end def test_emptyarrays str = %{$var = []\n} parser = mkparser parser.string = str # Make sure it parses fine assert_nothing_raised { parser.parse } end # Make sure function names aren't reserved words. def test_functionnamecollision str = %{tag yayness tag(rahness) file { "/tmp/yayness": tag => "rahness", ensure => exists } } parser = mkparser parser.string = str # Make sure it parses fine assert_nothing_raised { parser.parse } end def test_metaparams_in_definition_prototypes parser = mkparser assert_raise(Puppet::ParseError) { parser.parse %{define mydef($schedule) {}} } assert_nothing_raised { parser.parse %{define adef($schedule = false) {}} parser.parse %{define mydef($schedule = daily) {}} } end def test_parsingif parser = mkparser exec = proc do |val| %{exec { "/bin/echo #{val}": logoutput => true }} end str1 = %{if true { #{exec.call("true")} }} ret = nil assert_nothing_raised { ret = parser.parse(str1).classes[""].code[0] } assert_instance_of(Puppet::Parser::AST::IfStatement, ret) - parser.clear + parser = mkparser str2 = %{if true { #{exec.call("true")} } else { #{exec.call("false")} }} assert_nothing_raised { ret = parser.parse(str2).classes[""].code[0] } assert_instance_of(Puppet::Parser::AST::IfStatement, ret) assert_instance_of(Puppet::Parser::AST::Else, ret.else) end def test_hostclass parser = mkparser assert_nothing_raised { parser.parse %{class myclass { class other {} }} } assert(parser.classes["myclass"], "Could not find myclass") assert(parser.classes["myclass::other"], "Could not find myclass::other") assert_nothing_raised { parser.parse "class base {} class container { class deep::sub inherits base {} }" } sub = parser.classes["container::deep::sub"] assert(sub, "Could not find sub") # Now try it with a parent class being a fq class assert_nothing_raised { parser.parse "class container::one inherits container::deep::sub {}" } sub = parser.classes["container::one"] assert(sub, "Could not find one") assert_equal("container::deep::sub", sub.parentclass) # Finally, try including a qualified class assert_nothing_raised("Could not include fully qualified class") { parser.parse "include container::deep::sub" } end def test_topnamespace parser = mkparser # Make sure we put the top-level code into a class called "" in # the "" namespace assert_nothing_raised do out = parser.parse "" assert_instance_of(Puppet::Parser::Parser::ASTSet, out) assert_nil(parser.classes[""], "Got a 'main' class when we had no code") end # Now try something a touch more complicated parser.initvars assert_nothing_raised do out = parser.parse "Exec { path => '/usr/bin:/usr/sbin' }" assert_instance_of(Puppet::Parser::Parser::ASTSet, out) assert_equal("", parser.classes[""].classname) assert_equal("", parser.classes[""].namespace) end end # Make sure virtual and exported resources work appropriately. def test_virtualresources tests = [:virtual] if Puppet.features.rails? Puppet[:storeconfigs] = true tests << :exported end tests.each do |form| parser = mkparser if form == :virtual at = "@" else at = "@@" end check = proc do |res, msg| if res.is_a?(Puppet::Parser::Resource) txt = res.ref else txt = res.class end # Real resources get marked virtual when exported if form == :virtual or res.is_a?(Puppet::Parser::Resource) assert(res.virtual, "#{msg} #{at}#{txt} is not virtual") end if form == :virtual assert(! res.exported, "#{msg} #{at}#{txt} is exported") else assert(res.exported, "#{msg} #{at}#{txt} is not exported") end end ret = nil assert_nothing_raised do ret = parser.parse("#{at}file { '/tmp/testing': owner => root }") end assert_instance_of(AST::ASTArray, ret.classes[""].code) resdef = ret.classes[""].code[0] - assert_instance_of(AST::ResourceDef, resdef) + assert_instance_of(AST::Resource, resdef) assert_equal("/tmp/testing", resdef.title.value) # We always get an astarray back, so... check.call(resdef, "simple resource") # Now let's try it with multiple resources in the same spec assert_nothing_raised do ret = parser.parse("#{at}file { ['/tmp/1', '/tmp/2']: owner => root }") end ret.classes[""].each do |res| - assert_instance_of(AST::ResourceDef, res) + assert_instance_of(AST::Resource, res) check.call(res, "multiresource") end # Now evaluate these scope = mkscope klass = parser.newclass "" scope.source = klass assert_nothing_raised do - ret.classes[""].evaluate :scope => scope + ret.classes[""].evaluate :scope => scope, :resource => Puppet::Parser::Resource.new(:type => "mydefine", :title => 'whatever', :scope => scope, :source => scope.source) end # Make sure we can find both of them %w{/tmp/1 /tmp/2}.each do |title| res = scope.findresource("File[#{title}]") assert(res, "Could not find %s" % title) check.call(res, "found multiresource") end end end def test_collections tests = [:virtual] if Puppet.features.rails? Puppet[:storeconfigs] = true tests << :exported end tests.each do |form| parser = mkparser if form == :virtual arrow = "<||>" else arrow = "<<||>>" end ret = nil assert_nothing_raised do ret = parser.parse("File #{arrow}") end coll = ret.classes[""].code[0] assert_instance_of(AST::Collection, coll) assert_equal(form, coll.form) end end def test_collectionexpressions %w{== !=}.each do |oper| str = "File <| title #{oper} '/tmp/testing' |>" parser = mkparser res = nil assert_nothing_raised do res = parser.parse(str).classes[""].code[0] end assert_instance_of(AST::Collection, res) query = res.query assert_instance_of(AST::CollExpr, query) assert_equal(:virtual, query.form) assert_equal("title", query.test1.value) assert_equal("/tmp/testing", query.test2.value) assert_equal(oper, query.oper) end end def test_collectionstatements %w{and or}.each do |joiner| str = "File <| title == '/tmp/testing' #{joiner} owner == root |>" parser = mkparser res = nil assert_nothing_raised do res = parser.parse(str).classes[""].code[0] end assert_instance_of(AST::Collection, res) query = res.query assert_instance_of(AST::CollExpr, query) assert_equal(joiner, query.oper) assert_instance_of(AST::CollExpr, query.test1) assert_instance_of(AST::CollExpr, query.test2) end end def test_collectionstatements_with_parens [ "(title == '/tmp/testing' and owner == root) or owner == wheel", "(title == '/tmp/testing')" ].each do |test| str = "File <| #{test} |>" parser = mkparser res = nil assert_nothing_raised("Could not parse '#{test}'") do res = parser.parse(str).classes[""].code[0] end assert_instance_of(AST::Collection, res) query = res.query assert_instance_of(AST::CollExpr, query) #assert_equal(joiner, query.oper) #assert_instance_of(AST::CollExpr, query.test1) #assert_instance_of(AST::CollExpr, query.test2) end end # We've had problems with files other than site.pp importing into main. def test_importing_into_main top = tempfile() other = tempfile() File.open(top, "w") do |f| f.puts "import '#{other}'" end file = tempfile() File.open(other, "w") do |f| f.puts "file { '#{file}': ensure => present }" end interp = mkinterp :Manifest => top, :UseNodes => false code = nil assert_nothing_raised do - code = interp.run("hostname.domain.com", {}).flatten + code = interp.compile(mknode).extract.flatten end assert(code.length == 1, "Did not get the file") assert_instance_of(Puppet::TransObject, code[0]) end def test_fully_qualified_definitions parser = mkparser assert_nothing_raised("Could not parse fully-qualified definition") { parser.parse %{define one::two { }} } assert(parser.definitions["one::two"], "Could not find one::two with no namespace") # Now try using the definition assert_nothing_raised("Could not parse fully-qualified definition usage") { parser.parse %{one::two { yayness: }} } end # #524 def test_functions_with_no_arguments parser = mkparser assert_nothing_raised("Could not parse statement function with no args") { parser.parse %{tag()} } assert_nothing_raised("Could not parse rvalue function with no args") { parser.parse %{$testing = template()} } end def test_module_import basedir = File.join(tmpdir(), "module-import") @@tmpfiles << basedir Dir.mkdir(basedir) modfiles = [ "init.pp", "mani1.pp", "mani2.pp", "sub/smani1.pp", "sub/smani2.pp" ] modpath = File.join(basedir, "modules") Puppet[:modulepath] = modpath modname = "amod" manipath = File::join(modpath, modname, Puppet::Module::MANIFESTS) FileUtils::mkdir_p(File::join(manipath, "sub")) targets = [] modfiles.each do |fname| target = File::join(basedir, File::basename(fname, '.pp')) targets << target txt = %[ file { '#{target}': content => "#{fname}" } ] if fname == "init.pp" - txt = %[import 'mani1' \nimport '#{modname}/mani2'\nimport '#{modname}/sub/*.pp' ] + txt + txt = %[import 'mani1' \nimport '#{modname}/mani2'\nimport '#{modname}/sub/*.pp'\n ] + txt end File::open(File::join(manipath, fname), "w") do |f| f.puts txt end end manifest_texts = [ "import '#{modname}'", "import '#{modname}/init'", "import '#{modname}/init.pp'" ] manifest = File.join(modpath, "manifest.pp") manifest_texts.each do |txt| File.open(manifest, "w") { |f| f.puts txt } assert_nothing_raised { parser = mkparser parser.file = manifest parser.parse } assert_creates(manifest, *targets) end end # #544 def test_ignoreimports parser = mkparser assert(! Puppet[:ignoreimport], ":ignoreimport defaulted to true") assert_raise(Puppet::ParseError, "Did not fail on missing import") do parser.parse("import 'nosuchfile'") end assert_nothing_raised("could not set :ignoreimport") do Puppet[:ignoreimport] = true end assert_nothing_raised("Parser did not follow :ignoreimports") do parser.parse("import 'nosuchfile'") end end def test_multiple_imports_on_one_line one = tempfile two = tempfile base = tempfile File.open(one, "w") { |f| f.puts "$var = value" } File.open(two, "w") { |f| f.puts "$var = value" } File.open(base, "w") { |f| f.puts "import '#{one}', '#{two}'" } parser = mkparser parser.file = base # Importing is logged at debug time. Puppet::Util::Log.level = :debug assert_nothing_raised("Parser could not import multiple files at once") do parser.parse end [one, two].each do |file| assert(@logs.detect { |l| l.message =~ /importing '#{file}'/}, "did not import %s" % file) end end def test_cannot_assign_qualified_variables parser = mkparser assert_raise(Puppet::ParseError, "successfully assigned a qualified variable") do parser.parse("$one::two = yay") end end # #588 def test_globbing_with_directories dir = tempfile Dir.mkdir(dir) subdir = File.join(dir, "subdir") Dir.mkdir(subdir) file = File.join(dir, "file.pp") maker = tempfile File.open(file, "w") { |f| f.puts "file { '#{maker}': ensure => file }" } parser = mkparser assert_nothing_raised("Globbing failed when it matched a directory") do parser.import("%s/*" % dir) end end # #629 - undef keyword def test_undef parser = mkparser result = nil assert_nothing_raised("Could not parse assignment to undef") { result = parser.parse %{$variable = undef} } main = result.classes[""].code children = main.children assert_instance_of(AST::VarDef, main.children[0]) assert_instance_of(AST::Undef, main.children[0].value) end # Prompted by #729 -- parsing should not modify the interpreter. def test_parse parser = mkparser str = "file { '/tmp/yay': ensure => file }\nclass yay {}\nnode foo {}\ndefine bar {}\n" result = nil assert_nothing_raised("Could not parse") do result = parser.parse(str) end assert_instance_of(Puppet::Parser::Parser::ASTSet, result, "Did not get a ASTSet back from parsing") assert_instance_of(AST::HostClass, result.classes["yay"], "Did not create 'yay' class") assert_instance_of(AST::HostClass, result.classes[""], "Did not create main class") - assert_instance_of(AST::Component, result.definitions["bar"], "Did not create 'bar' definition") + assert_instance_of(AST::Definition, result.definitions["bar"], "Did not create 'bar' definition") assert_instance_of(AST::Node, result.nodes["foo"], "Did not create 'foo' node") end # Make sure our node gets added to the node table. def test_newnode parser = mkparser # First just try calling it directly assert_nothing_raised { parser.newnode("mynode", :code => :yay) } assert_equal(:yay, parser.nodes["mynode"].code) # Now make sure that trying to redefine it throws an error. assert_raise(Puppet::ParseError) { parser.newnode("mynode", {}) } # Now try one with no code assert_nothing_raised { parser.newnode("simplenode", :parent => :foo) } # Now define the parent node parser.newnode(:foo) # And make sure we get things back correctly assert_equal(:foo, parser.nodes["simplenode"].parentclass) assert_nil(parser.nodes["simplenode"].code) # Now make sure that trying to redefine it throws an error. assert_raise(Puppet::ParseError) { parser.newnode("mynode", {}) } # Test multiple names names = ["one", "two", "three"] assert_nothing_raised { parser.newnode(names, {:code => :yay, :parent => :foo}) } names.each do |name| assert_equal(:yay, parser.nodes[name].code) assert_equal(:foo, parser.nodes[name].parentclass) # Now make sure that trying to redefine it throws an error. assert_raise(Puppet::ParseError) { parser.newnode(name, {}) } end end def test_newdefine parser = mkparser assert_nothing_raised { parser.newdefine("mydefine", :code => :yay, :arguments => ["a", stringobj("b")]) } mydefine = parser.definitions["mydefine"] assert(mydefine, "Could not find definition") assert_equal("", mydefine.namespace) assert_equal("mydefine", mydefine.classname) assert_raise(Puppet::ParseError) do parser.newdefine("mydefine", :code => :yay, :arguments => ["a", stringobj("b")]) end # Now define the same thing in a different scope assert_nothing_raised { parser.newdefine("other::mydefine", :code => :other, :arguments => ["a", stringobj("b")]) } other = parser.definitions["other::mydefine"] assert(other, "Could not find definition") assert(parser.definitions["other::mydefine"], "Could not find other::mydefine") assert_equal(:other, other.code) assert_equal("other", other.namespace) assert_equal("other::mydefine", other.classname) end def test_newclass - parser = mkparser + scope = mkscope + parser = scope.compile.parser mkcode = proc do |ary| classes = ary.collect do |string| AST::FlatString.new(:value => string) end AST::ASTArray.new(:children => classes) end - scope = Puppet::Parser::Scope.new(:interp => mkinterp) # First make sure that code is being appended code = mkcode.call(%w{original code}) klass = nil assert_nothing_raised { klass = parser.newclass("myclass", :code => code) } assert(klass, "Did not return class") assert(parser.classes["myclass"], "Could not find definition") assert_equal("myclass", parser.classes["myclass"].classname) assert_equal(%w{original code}, parser.classes["myclass"].code.evaluate(:scope => scope)) # Newclass behaves differently than the others -- it just appends # the code to the existing class. code = mkcode.call(%w{something new}) assert_nothing_raised do klass = parser.newclass("myclass", :code => code) end assert(klass, "Did not return class when appending") assert_equal(%w{original code something new}, parser.classes["myclass"].code.evaluate(:scope => scope)) # Now create the same class name in a different scope assert_nothing_raised { klass = parser.newclass("other::myclass", :code => mkcode.call(%w{something diff})) } assert(klass, "Did not return class") other = parser.classes["other::myclass"] assert(other, "Could not find class") assert_equal("other::myclass", other.classname) assert_equal("other::myclass", other.namespace) assert_equal(%w{something diff}, other.code.evaluate(:scope => scope)) # Make sure newclass deals correctly with nodes with no code klass = parser.newclass("nocode") assert(klass, "Did not return class") assert_nothing_raised do klass = parser.newclass("nocode", :code => mkcode.call(%w{yay test})) end assert(klass, "Did not return class with no code") assert_equal(%w{yay test}, parser.classes["nocode"].code.evaluate(:scope => scope)) # Then try merging something into nothing parser.newclass("nocode2", :code => mkcode.call(%w{foo test})) assert(klass, "Did not return class with no code") assert_nothing_raised do klass = parser.newclass("nocode2") end assert(klass, "Did not return class with no code") assert_equal(%w{foo test}, parser.classes["nocode2"].code.evaluate(:scope => scope)) # And lastly, nothing and nothing klass = parser.newclass("nocode3") assert(klass, "Did not return class with no code") assert_nothing_raised do klass = parser.newclass("nocode3") end assert(klass, "Did not return class with no code") assert_nil(parser.classes["nocode3"].code) end # Make sure you can't have classes and defines with the same name in the # same scope. def test_classes_beat_defines parser = mkparser assert_nothing_raised { parser.newclass("yay::funtest") } assert_raise(Puppet::ParseError) do parser.newdefine("yay::funtest") end assert_nothing_raised { parser.newdefine("yay::yaytest") } assert_raise(Puppet::ParseError) do parser.newclass("yay::yaytest") end end def test_namesplit parser = mkparser assert_nothing_raised do {"base::sub" => %w{base sub}, "main" => ["", "main"], "one::two::three::four" => ["one::two::three", "four"], }.each do |name, ary| result = parser.namesplit(name) assert_equal(ary, result, "%s split to %s" % [name, result]) end end end # Now make sure we get appropriate behaviour with parent class conflicts. def test_newclass_parentage parser = mkparser parser.newclass("base1") parser.newclass("one::two::three") # First create it with no parentclass. assert_nothing_raised { parser.newclass("sub") } assert(parser.classes["sub"], "Could not find definition") assert_nil(parser.classes["sub"].parentclass) # Make sure we can't set the parent class to ourself. assert_raise(Puppet::ParseError) { parser.newclass("sub", :parent => "sub") } # Now create another one, with a parentclass. assert_nothing_raised { parser.newclass("sub", :parent => "base1") } # Make sure we get the right parent class, and make sure it's not an object. assert_equal("base1", parser.classes["sub"].parentclass) # Now make sure we get a failure if we try to conflict. assert_raise(Puppet::ParseError) { parser.newclass("sub", :parent => "one::two::three") } # Make sure that failure didn't screw us up in any way. assert_equal("base1", parser.classes["sub"].parentclass) # But make sure we can create a class with a fq parent assert_nothing_raised { parser.newclass("another", :parent => "one::two::three") } assert_equal("one::two::three", parser.classes["another"].parentclass) end def test_fqfind parser = mkparser table = {} # Define a bunch of things. %w{a c a::b a::b::c a::c a::b::c::d a::b::c::d::e::f c::d}.each do |string| table[string] = string end check = proc do |namespace, hash| hash.each do |thing, result| assert_equal(result, parser.fqfind(namespace, thing, table), "Could not find %s in %s" % [thing, namespace]) end end # Now let's do some test lookups. # First do something really simple check.call "a", "b" => "a::b", "b::c" => "a::b::c", "d" => nil, "::c" => "c" check.call "a::b", "c" => "a::b::c", "b" => "a::b", "a" => "a" check.call "a::b::c::d::e", "c" => "a::b::c", "::c" => "c", "c::d" => "a::b::c::d", "::c::d" => "c::d" check.call "", "a" => "a", "a::c" => "a::c" end # Setup a module. def mk_module(name, files = {}) mdir = File.join(@dir, name) mandir = File.join(mdir, "manifests") FileUtils.mkdir_p mandir if defs = files[:define] files.delete(:define) end Dir.chdir(mandir) do files.each do |file, classes| File.open("%s.pp" % file, "w") do |f| classes.each { |klass| if defs f.puts "define %s {}" % klass else f.puts "class %s {}" % klass end } end end end end # #596 - make sure classes and definitions load automatically if they're in modules, so we don't have to manually load each one. def test_module_autoloading @dir = tempfile Puppet[:modulepath] = @dir FileUtils.mkdir_p @dir parser = mkparser # Make sure we fail like normal for actually missing classes assert_nil(parser.findclass("", "nosuchclass"), "Did not return nil on missing classes") # test the simple case -- the module class itself name = "simple" mk_module(name, :init => [name]) # Try to load the module automatically now klass = parser.findclass("", name) assert_instance_of(AST::HostClass, klass, "Did not autoload class from module init file") assert_equal(name, klass.classname, "Incorrect class was returned") # Try loading the simple module when we're in something other than the base namespace. parser = mkparser klass = parser.findclass("something::else", name) assert_instance_of(AST::HostClass, klass, "Did not autoload class from module init file") assert_equal(name, klass.classname, "Incorrect class was returned") # Now try it with a definition as the base file name = "simpdef" mk_module(name, :define => true, :init => [name]) klass = parser.finddefine("", name) - assert_instance_of(AST::Component, klass, "Did not autoload class from module init file") + assert_instance_of(AST::Definition, klass, "Did not autoload class from module init file") assert_equal(name, klass.classname, "Incorrect class was returned") # Now try it with namespace classes where both classes are in the init file parser = mkparser modname = "both" name = "sub" mk_module(modname, :init => %w{both both::sub}) # First try it with a namespace klass = parser.findclass("both", name) assert_instance_of(AST::HostClass, klass, "Did not autoload sub class from module init file with a namespace") assert_equal("both::sub", klass.classname, "Incorrect class was returned") # Now try it using the fully qualified name parser = mkparser klass = parser.findclass("", "both::sub") assert_instance_of(AST::HostClass, klass, "Did not autoload sub class from module init file with no namespace") assert_equal("both::sub", klass.classname, "Incorrect class was returned") # Now try it with the class in a different file parser = mkparser modname = "separate" name = "sub" mk_module(modname, :init => %w{separate}, :sub => %w{separate::sub}) # First try it with a namespace klass = parser.findclass("separate", name) assert_instance_of(AST::HostClass, klass, "Did not autoload sub class from separate file with a namespace") assert_equal("separate::sub", klass.classname, "Incorrect class was returned") # Now try it using the fully qualified name parser = mkparser klass = parser.findclass("", "separate::sub") assert_instance_of(AST::HostClass, klass, "Did not autoload sub class from separate file with no namespace") assert_equal("separate::sub", klass.classname, "Incorrect class was returned") # Now make sure we don't get a failure when there's no module file parser = mkparser modname = "alone" name = "sub" mk_module(modname, :sub => %w{alone::sub}) # First try it with a namespace assert_nothing_raised("Could not autoload file when module file is missing") do klass = parser.findclass("alone", name) end assert_instance_of(AST::HostClass, klass, "Did not autoload sub class from alone file with a namespace") assert_equal("alone::sub", klass.classname, "Incorrect class was returned") # Now try it using the fully qualified name parser = mkparser klass = parser.findclass("", "alone::sub") assert_instance_of(AST::HostClass, klass, "Did not autoload sub class from alone file with no namespace") assert_equal("alone::sub", klass.classname, "Incorrect class was returned") end + + # Make sure class, node, and define methods are case-insensitive + def test_structure_case_insensitivity + parser = mkparser + + result = nil + assert_nothing_raised do + result = parser.newclass "Yayness" + end + assert_equal(result, parser.findclass("", "yayNess")) + + assert_nothing_raised do + result = parser.newdefine "FunTest" + end + assert_equal(result, parser.finddefine("", "fUntEst"), + "%s was not matched" % "fUntEst") + end end # $Id$ diff --git a/test/language/resource.rb b/test/language/resource.rb index 039c67216..f25be93b5 100755 --- a/test/language/resource.rb +++ b/test/language/resource.rb @@ -1,526 +1,507 @@ #!/usr/bin/env ruby $:.unshift("../lib").unshift("../../lib") if __FILE__ =~ /\.rb$/ require 'puppettest' require 'puppettest/resourcetesting' class TestResource < PuppetTest::TestCase include PuppetTest include PuppetTest::ParserTesting include PuppetTest::ResourceTesting Parser = Puppet::Parser AST = Parser::AST + Resource = Puppet::Parser::Resource Reference = Puppet::Parser::Resource::Reference def setup super Puppet[:trace] = false - @interp, @scope, @source = mkclassframing + end + + def teardown + mocha_verify end def test_initialize args = {:type => "resource", :title => "testing", - :source => @source, :scope => @scope} + :scope => mkscope} # Check our arg requirements args.each do |name, value| try = args.dup try.delete(name) - assert_raise(Puppet::DevError) do + assert_raise(ArgumentError, "Did not fail when %s was missing" % name) do Parser::Resource.new(try) end end - args[:params] = paramify @source, :one => "yay", :three => "rah" - res = nil assert_nothing_raised do res = Parser::Resource.new(args) end - # Make sure it got the parameters correctly. - assert_equal("yay", res[:one]) - assert_equal("rah", res[:three]) - - assert_equal({:one => "yay", :three => "rah"}, res.to_hash) + ref = res.instance_variable_get("@ref") + assert_equal("resource", ref.type, "did not set resource type") + assert_equal("testing", ref.title, "did not set resource title") end - def test_override + def test_merge res = mkresource + other = mkresource - # Now verify we can't override with any random class - assert_raise(Puppet::ParseError) do - res.set paramify(@scope.findclass("other"), "one" => "boo").shift + # First try the case where the resource is not allowed to override + res.source = "source1" + other.source = "source2" + other.source.expects(:child_of?).with("source1").returns(false) + assert_raise(Puppet::ParseError, "Allowed unrelated resources to override") do + res.merge(other) end - # And that we can with a subclass - assert_nothing_raised do - res.set paramify(@scope.findclass("sub1"), "one" => "boo").shift - end + # Next try it when the sources are equal. + res.source = "source3" + other.source = res.source + other.source.expects(:child_of?).with("source3").never + params = {:a => :b, :c => :d} + other.expects(:params).returns(params) + res.expects(:override_parameter).with(:b) + res.expects(:override_parameter).with(:d) + res.merge(other) + + # And then parentage is involved + other = mkresource + res.source = "source3" + other.source = "source4" + other.source.expects(:child_of?).with("source3").returns(true) + params = {:a => :b, :c => :d} + other.expects(:params).returns(params) + res.expects(:override_parameter).with(:b) + res.expects(:override_parameter).with(:d) + res.merge(other) + end - # And that a different subclass can override a different parameter - assert_nothing_raised do - res.set paramify(@scope.findclass("sub2"), "three" => "boo").shift - end + # the [] method + def test_array_accessors + res = mkresource + params = res.instance_variable_get("@params") + assert_nil(res[:missing], "Found a missing parameter somehow") + params[:something] = stub(:value => "yay") + assert_equal("yay", res[:something], "Did not correctly call value on the parameter") - # But not the same one - assert_raise(Puppet::ParseError) do - res.set paramify(@scope.findclass("sub2"), "one" => "something").shift - end + res.expects(:title).returns(:mytitle) + assert_equal(:mytitle, res[:title], "Did not call title when asked for it as a param") end - def check_paramadd(val1, val2, merged_val) - res = mkresource :params => {"one" => val1} - assert_nothing_raised do - res.set Parser::Resource::Param.new( - :name => "one", :value => val2, - :add => true, :source => @scope.findclass("sub1")) - end - assert_equal(merged_val, res[:one]) + # Make sure any defaults stored in the scope get added to our resource. + def test_add_defaults + res = mkresource + params = res.instance_variable_get("@params") + params[:a] = :b + res.scope.expects(:lookupdefaults).with(res.type).returns(:a => :replaced, :c => :d) + res.expects(:debug) + + res.send(:add_defaults) + assert_equal(:d, params[:c], "Did not set default") + assert_equal(:b, params[:a], "Replaced parameter with default") end - def test_paramadd - check_paramadd([], [], []) - check_paramadd([], "rah", ["rah"]) - check_paramadd([], ["rah", "bah"], ["rah", "bah"]) - - check_paramadd("yay", [], ["yay"]) - check_paramadd("yay", "rah", ["yay", "rah"]) - check_paramadd("yay", ["rah", "bah"], ["yay", "rah", "bah"]) - - check_paramadd(["yay", "boo"], [], ["yay", "boo"]) - check_paramadd(["yay", "boo"], "rah", ["yay", "boo", "rah"]) - check_paramadd(["yay", "boo"], ["rah", "bah"], - ["yay", "boo", "rah", "bah"]) + def test_finish + res = mkresource + res.expects(:add_overrides) + res.expects(:add_defaults) + res.expects(:add_metaparams) + res.expects(:validate) + res.finish end - def test_merge - # Start with the normal one + # Make sure we paramcheck our params + def test_validate res = mkresource + params = res.instance_variable_get("@params") + params[:one] = :two + params[:three] = :four + res.expects(:paramcheck).with(:one) + res.expects(:paramcheck).with(:three) + res.send(:validate) + end - # Now create a resource from a different scope - other = mkresource :source => other, :params => {"one" => "boo"} - - # Make sure we can't merge it - assert_raise(Puppet::ParseError) do - res.merge(other) - end - - # Make one from a subscope - other = mkresource :source => "sub1", :params => {"one" => "boo"} - - # Make sure it merges - assert_nothing_raised do - res.merge(other) - end + def test_override_parameter + res = mkresource + params = res.instance_variable_get("@params") + + # There are three cases, with the second having two options: + + # No existing parameter. + param = stub(:name => "myparam") + res.send(:override_parameter, param) + assert_equal(param, params["myparam"], "Override was not added to param list") + + # An existing parameter that we can override. + source = stub(:child_of? => true) + # Start out without addition + params["param2"] = stub(:source => :whatever) + param = stub(:name => "param2", :source => source, :add => false) + res.send(:override_parameter, param) + assert_equal(param, params["param2"], "Override was not added to param list") + + # Try with addition. + params["param2"] = stub(:value => :a, :source => :whatever) + param = stub(:name => "param2", :source => source, :add => true, :value => :b) + param.expects(:value=).with([:a, :b]) + res.send(:override_parameter, param) + assert_equal(param, params["param2"], "Override was not added to param list") + + # And finally, make sure we throw an exception when the sources aren't related + source = stub(:child_of? => false) + params["param2"] = stub(:source => :whatever, :file => :f, :line => :l) + old = params["param2"] + param = stub(:name => "param2", :source => source, :file => :f, :line => :l) + assert_raise(Puppet::ParseError, "Did not fail when params conflicted") do + res.send(:override_parameter, param) + end + assert_equal(old, params["param2"], "Param was replaced irrespective of conflict") + end - assert_equal("boo", res["one"]) + def test_set_parameter + res = mkresource + params = res.instance_variable_get("@params") + + # First test the simple case: It's already a parameter + param = mock('param') + param.expects(:is_a?).with(Resource::Param).returns(true) + param.expects(:name).returns("pname") + res.send(:set_parameter, param) + assert_equal(param, params["pname"], "Parameter was not added to hash") + + # Now the case where there's no value but it's not a param + param = mock('param') + param.expects(:is_a?).with(Resource::Param).returns(false) + assert_raise(ArgumentError, "Did not fail when a non-param was passed") do + res.send(:set_parameter, param) + end + + # and the case where a value is passed in + param = stub :name => "pname", :value => "whatever" + Resource::Param.expects(:new).with(:name => "pname", :value => "myvalue", :source => res.source).returns(param) + res.send(:set_parameter, "pname", "myvalue") + assert_equal(param, params["pname"], "Did not put param in hash") end def test_paramcheck - # First make a builtin resource - res = nil - assert_nothing_raised do - res = Parser::Resource.new :type => "file", :title => tempfile(), - :source => @source, :scope => @scope - end - - %w{path group source schedule subscribe}.each do |param| - assert_nothing_raised("Param %s was considered invalid" % param) do - res.paramcheck(param) - end - end - - %w{this bad noness}.each do |param| - assert_raise(Puppet::ParseError, "%s was considered valid" % param) do - res.paramcheck(param) - end - end - - # Now create a defined resource - assert_nothing_raised do - res = Parser::Resource.new :type => "resource", :title => "yay", - :source => @source, :scope => @scope - end + # There are three cases here: - %w{one two three schedule subscribe}.each do |param| - assert_nothing_raised("Param %s was considered invalid" % param) do - res.paramcheck(param) - end - end - - %w{this bad noness}.each do |param| - assert_raise(Puppet::ParseError, "%s was considered valid" % param) do - res.paramcheck(param) - end - end + # It's a valid parameter + res = mkresource + ref = mock('ref') + res.instance_variable_set("@ref", ref) + klass = mock("class") + ref.expects(:typeclass).returns(klass).times(4) + klass.expects(:validattr?).with("good").returns(true) + assert(res.send(:paramcheck, :good), "Did not allow valid param") + + # It's name or title + klass.expects(:validattr?).with("name").returns(false) + assert(res.send(:paramcheck, :name), "Did not allow name") + klass.expects(:validattr?).with("title").returns(false) + assert(res.send(:paramcheck, :title), "Did not allow title") + + # It's not actually allowed + klass.expects(:validattr?).with("other").returns(false) + res.expects(:fail) + ref.expects(:type) + res.send(:paramcheck, :other) end - def test_to_trans + def test_to_transobject # First try translating a builtin resource. Make sure we use some references # and arrays, to make sure they translate correctly. + source = mock("source") + scope = mkscope + scope.stubs(:tags).returns([]) refs = [] 4.times { |i| refs << Puppet::Parser::Resource::Reference.new(:title => "file%s" % i, :type => "file") } res = Parser::Resource.new :type => "file", :title => "/tmp", - :source => @source, :scope => @scope, - :params => paramify(@source, :owner => "nobody", :group => %w{you me}, + :source => source, :scope => scope, + :params => paramify(source, :owner => "nobody", :group => %w{you me}, :require => refs[0], :ignore => %w{svn}, :subscribe => [refs[1], refs[2]], :notify => [refs[3]]) obj = nil assert_nothing_raised do obj = res.to_trans end assert_instance_of(Puppet::TransObject, obj) assert_equal(obj.type, res.type) assert_equal(obj.name, res.title) # TransObjects use strings, resources use symbols assert_equal("nobody", obj["owner"], "Single-value string was not passed correctly") assert_equal(%w{you me}, obj["group"], "Array of strings was not passed correctly") assert_equal("svn", obj["ignore"], "Array with single string was not turned into single value") assert_equal(["file", refs[0].title], obj["require"], "Resource reference was not passed correctly") assert_equal([["file", refs[1].title], ["file", refs[2].title]], obj["subscribe"], "Array of resource references was not passed correctly") assert_equal(["file", refs[3].title], obj["notify"], "Array with single resource reference was not turned into single value") end - def test_adddefaults - # Set some defaults at the top level - top = {:one => "fun", :two => "shoe"} - - @scope.setdefaults("resource", paramify(@source, top)) - - # Make a resource at that level - res = Parser::Resource.new :type => "resource", :title => "yay", - :source => @source, :scope => @scope - - # Add the defaults - assert_nothing_raised do - res.adddefaults - end - - # And make sure we got them - top.each do |p, v| - assert_equal(v, res[p]) - end - - # Now got a bit lower - other = @scope.newscope - - # And create a resource - lowerres = Parser::Resource.new :type => "resource", :title => "funtest", - :source => @source, :scope => other - - assert_nothing_raised do - lowerres.adddefaults - end - - # And check - top.each do |p, v| - assert_equal(v, lowerres[p]) - end - - # Now add some of our own defaults - lower = {:one => "shun", :three => "free"} - other.setdefaults("resource", paramify(@source, lower)) - otherres = Parser::Resource.new :type => "resource", :title => "yaytest", - :source => @source, :scope => other - - should = top.dup - # Make sure the lower defaults beat the higher ones. - lower.each do |p, v| should[p] = v end + # FIXME This isn't a great test, but I need to move on. + def test_to_transbucket + bucket = mock("transbucket") + source = mock("source") + scope = mkscope + res = Parser::Resource.new :type => "mydefine", :title => "yay", + :source => source, :scope => scope - otherres.adddefaults - should.each do |p,v| - assert_equal(v, otherres[p]) - end + result = res.to_trans + assert_equal("yay", result.name, "did not set bucket name correctly") + assert_equal("mydefine", result.type, "did not set bucket type correctly") end def test_evaluate - # Make a definition that we know will, um, do something - @interp.newdefine "evaltest", - :arguments => [%w{one}, ["two", stringobj("755")]], - :code => resourcedef("file", "/tmp", - "owner" => varref("one"), "mode" => varref("two")) - - res = Parser::Resource.new :type => "evaltest", :title => "yay", - :source => @source, :scope => @scope, - :params => paramify(@source, :one => "nobody") - - # Now try evaluating - ret = nil - assert_nothing_raised do - ret = res.evaluate - end - - # Make sure we can find our object now - result = @scope.findresource("File[/tmp]") - - # Now make sure we got the code we expected. - assert_instance_of(Puppet::Parser::Resource, result) - - assert_equal("file", result.type) - assert_equal("/tmp", result.title) - assert_equal("nobody", result["owner"]) - assert_equal("755", result["mode"]) - - # And that we cannot find the old resource - assert_nil(@scope.findresource("Evaltest[yay]"), - "Evaluated resource was not deleted") + # First try the most common case, we're not a builtin type. + res = mkresource + ref = res.instance_variable_get("@ref") + type = mock("type") + ref.expects(:definedtype).returns(type) + res.expects(:finish) + res.scope = mock("scope") + config = mock("config") + res.scope.expects(:compile).returns(config) + config.expects(:delete_resource).with(res) + + args = {:scope => res.scope, :resource => res} + type.expects(:evaluate).with(args) + + res.evaluate end - def test_addoverrides - # First create an override for an object that doesn't yet exist - over1 = mkresource :source => "sub1", :params => {:one => "yay"} - - assert_nothing_raised do - @scope.setoverride(over1) - end - - assert(over1.override, "Override was not marked so") - - # Now make the resource - res = mkresource :source => "base", :params => {:one => "rah", - :three => "foo"} - - # And add it to our scope - @scope.setresource(res) - - # And make sure over1 has not yet taken affect - assert_equal("foo", res[:three], "Lost value") - - # Now add an immediately binding override - over2 = mkresource :source => "sub1", :params => {:three => "yay"} - - assert_nothing_raised do - @scope.setoverride(over2) - end - - # And make sure it worked - assert_equal("yay", res[:three], "Override 2 was ignored") - - # Now add our late-binding override - assert_nothing_raised do - res.addoverrides - end - - # And make sure they're still around - assert_equal("yay", res[:one], "Override 1 lost") - assert_equal("yay", res[:three], "Override 2 lost") - - # And finally, make sure that there are no remaining overrides - assert_nothing_raised do - res.addoverrides - end + def test_add_overrides + # Try it with nil + res = mkresource + res.scope = mock('scope') + config = mock("config") + res.scope.expects(:compile).returns(config) + config.expects(:resource_overrides).with(res).returns(nil) + res.expects(:merge).never + res.send(:add_overrides) + + # And an empty array + res = mkresource + res.scope = mock('scope') + config = mock("config") + res.scope.expects(:compile).returns(config) + config.expects(:resource_overrides).with(res).returns([]) + res.expects(:merge).never + res.send(:add_overrides) + + # And with some overrides + res = mkresource + res.scope = mock('scope') + config = mock("config") + res.scope.expects(:compile).returns(config) + returns = %w{a b} + config.expects(:resource_overrides).with(res).returns(returns) + res.expects(:merge).with("a") + res.expects(:merge).with("b") + res.send(:add_overrides) + assert(returns.empty?, "Did not clear overrides") end def test_proxymethods res = Parser::Resource.new :type => "evaltest", :title => "yay", - :source => @source, :scope => @scope + :source => mock("source"), :scope => mkscope assert_equal("evaltest", res.type) assert_equal("yay", res.title) assert_equal(false, res.builtin?) end - def test_addmetaparams - mkevaltest @interp - res = Parser::Resource.new :type => "evaltest", :title => "yay", - :source => @source, :scope => @scope, - :params => paramify(@source, :tag => "yay") - - assert_nil(res[:schedule], "Got schedule already") - assert_nothing_raised do - res.addmetaparams - end - @scope.setvar("schedule", "daily") - - # This is so we can test that it won't override already-set metaparams - @scope.setvar("tag", "funtest") + def test_add_metaparams + res = mkresource + params = res.instance_variable_get("@params") + params[:a] = :b + Puppet::Type.expects(:eachmetaparam).multiple_yields(:a, :b, :c) + res.scope.expects(:lookupvar).with("b", false).returns(:something) + res.scope.expects(:lookupvar).with("c", false).returns(:undefined) + res.expects(:set_parameter).with(:b, :something) - assert_nothing_raised do - res.addmetaparams - end + res.send(:add_metaparams) - assert_equal("daily", res[:schedule], "Did not get metaparam") - assert_equal("yay", res[:tag], "Overrode explicitly-set metaparam") - assert_nil(res[:noop], "Got invalid metaparam") + assert_nil(params[:c], "A value was created somehow for an unset metaparam") end def test_reference_conversion # First try it as a normal string ref = Parser::Resource::Reference.new(:type => "file", :title => "/tmp/ref1") # Now create an obj that uses it res = mkresource :type => "file", :title => "/tmp/resource", :params => {:require => ref} + res.scope = mkscope trans = nil assert_nothing_raised do trans = res.to_trans end assert_instance_of(Array, trans["require"]) assert_equal(["file", "/tmp/ref1"], trans["require"]) # Now try it when using an array of references. two = Parser::Resource::Reference.new(:type => "file", :title => "/tmp/ref2") res = mkresource :type => "file", :title => "/tmp/resource2", :params => {:require => [ref, two]} + res.scope = mkscope trans = nil assert_nothing_raised do trans = res.to_trans end assert_instance_of(Array, trans["require"][0]) trans["require"].each do |val| assert_instance_of(Array, val) assert_equal("file", val[0]) assert(val[1] =~ /\/tmp\/ref[0-9]/, "Was %s instead of the file name" % val[1]) end end # This is a bit of a weird one -- the user should not actually know # that components exist, so we want references to act like they're not # builtin def test_components_are_not_builtin ref = Parser::Resource::Reference.new(:type => "component", :title => "yay") - assert_nil(ref.builtintype, "Component was considered builtin") - end - - # #472. Really, this still isn't the best behaviour, but at least - # it's consistent with what we have elsewhere. - def test_defaults_from_parent_classes - # Make a parent class with some defaults in it - @interp.newclass("base", - :code => defaultobj("file", :owner => "root", :group => "root") - ) - - # Now a mid-level class with some different values - @interp.newclass("middle", :parent => "base", - :code => defaultobj("file", :owner => "bin", :mode => "755") - ) - - # Now a lower class with its own defaults plus a resource - @interp.newclass("bottom", :parent => "middle", - :code => AST::ASTArray.new(:children => [ - defaultobj("file", :owner => "adm", :recurse => "true"), - resourcedef("file", "/tmp/yayness", {}) - ]) - ) - - # Now evaluate the class. - assert_nothing_raised("Failed to evaluate class tree") do - @scope.evalclasses("bottom") - end - - # Make sure our resource got created. - res = @scope.findresource("File[/tmp/yayness]") - assert_nothing_raised("Could not add defaults") do - res.adddefaults - end - assert(res, "could not find resource") - {:owner => "adm", :recurse => "true", :group => "root", :mode => "755"}.each do |param, value| - assert_equal(value, res[param], "%s => %s did not inherit correctly" % - [param, value]) - end + assert_nil(ref.builtintype, "Definition was considered builtin") end # The second part of #539 - make sure resources pass the arguments # correctly. def test_title_with_definitions - define = @interp.newdefine "yayness", + parser = mkparser + define = parser.newdefine "yayness", :code => resourcedef("file", "/tmp", "owner" => varref("name"), "mode" => varref("title")) - klass = @interp.findclass("", "") + + klass = parser.findclass("", "") should = {:name => :owner, :title => :mode} [ {:name => "one", :title => "two"}, {:title => "three"}, ].each do |hash| - scope = mkscope :interp => @interp + config = mkcompile parser args = {:type => "yayness", :title => hash[:title], - :source => klass, :scope => scope} + :source => klass, :scope => config.topscope} if hash[:name] args[:params] = {:name => hash[:name]} else args[:params] = {} # override the defaults end res = nil assert_nothing_raised("Could not create res with %s" % hash.inspect) do res = mkresource(args) end assert_nothing_raised("Could not eval res with %s" % hash.inspect) do res.evaluate end - made = scope.findresource("File[/tmp]") + made = config.topscope.findresource("File[/tmp]") assert(made, "Did not create resource with %s" % hash.inspect) should.each do |orig, param| assert_equal(hash[orig] || hash[:title], made[param], "%s was not set correctly with %s" % [param, hash.inspect]) end end end # part of #629 -- the undef keyword. Make sure 'undef' params get skipped. def test_undef_and_to_hash res = mkresource :type => "file", :title => "/tmp/testing", - :source => @source, :scope => @scope, + :source => mock("source"), :scope => mkscope, :params => {:owner => :undef, :mode => "755"} hash = nil assert_nothing_raised("Could not convert resource with undef to hash") do hash = res.to_hash end assert_nil(hash[:owner], "got a value for an undef parameter") end # #643 - Make sure virtual defines result in virtual resources def test_virtual_defines - define = @interp.newdefine("yayness", + parser = mkparser + define = parser.newdefine("yayness", :code => resourcedef("file", varref("name"), "mode" => "644")) - res = mkresource :type => "yayness", :title => "foo", :params => {} + config = mkcompile(parser) + + res = mkresource :type => "yayness", :title => "foo", :params => {}, :scope => config.topscope res.virtual = true result = nil assert_nothing_raised("Could not evaluate defined resource") do result = res.evaluate end scope = res.scope newres = scope.findresource("File[foo]") assert(newres, "Could not find resource") assert(newres.virtual?, "Virtual defined resource generated non-virtual resources") # Now try it with exported resources - res = mkresource :type => "yayness", :title => "bar", :params => {} + res = mkresource :type => "yayness", :title => "bar", :params => {}, :scope => config.topscope res.exported = true result = nil assert_nothing_raised("Could not evaluate exported resource") do result = res.evaluate end scope = res.scope newres = scope.findresource("File[bar]") assert(newres, "Could not find resource") assert(newres.exported?, "Exported defined resource generated non-exported resources") assert(newres.virtual?, "Exported defined resource generated non-virtual resources") end -end -# $Id$ + # Make sure tags behave appropriately. + def test_tags + scope_resource = stub 'scope_resource', :tags => %w{srone srtwo} + scope = stub 'scope', :resource => scope_resource + resource = Puppet::Parser::Resource.new(:type => "file", :title => "yay", :scope => scope, :source => mock('source')) + + # Make sure we get the scope resource's tags, plus the type and title + %w{srone srtwo yay file}.each do |tag| + assert(resource.tags.include?(tag), "Did not tag resource with %s" % tag) + end + + # make sure we can only set legal tags + ["an invalid tag", "-anotherinvalid", "bad*tag"].each do |tag| + assert_raise(Puppet::ParseError, "Tag #{tag} was considered valid") do + resource.tag tag + end + end + + # make sure good tags make it through. + tags = %w{good-tag yaytag GoodTag another_tag a ab A} + tags.each do |tag| + assert_nothing_raised("Tag #{tag} was considered invalid") do + resource.tag tag + end + end + + # make sure we get each of them. + ptags = resource.tags + tags.each do |tag| + assert(ptags.include?(tag), "missing #{tag}") + end + end +end diff --git a/test/language/scope.rb b/test/language/scope.rb index f0feee156..74f47b136 100755 --- a/test/language/scope.rb +++ b/test/language/scope.rb @@ -1,785 +1,518 @@ #!/usr/bin/env ruby $:.unshift("../lib").unshift("../../lib") if __FILE__ =~ /\.rb$/ require 'mocha' require 'puppettest' require 'puppettest/parsertesting' require 'puppettest/resourcetesting' # so, what kind of things do we want to test? # we don't need to test function, since we're confident in the # library tests. We do, however, need to test how things are actually # working in the language. # so really, we want to do things like test that our ast is correct # and test whether we've got things in the right scopes class TestScope < Test::Unit::TestCase include PuppetTest::ParserTesting include PuppetTest::ResourceTesting def to_ary(hash) hash.collect { |key,value| [key,value] } end def test_variables - scope = nil - over = "over" + config = mkcompile + topscope = config.topscope + midscope = config.newscope(topscope) + botscope = config.newscope(midscope) - scopes = [] - vars = [] - values = {} - ovalues = [] + scopes = {:top => topscope, :mid => midscope, :bot => botscope} - 10.times { |index| - # slap some recursion in there - scope = mkscope(:parent => scope) - scopes.push scope - - var = "var%s" % index - value = rand(1000) - ovalue = rand(1000) - - ovalues.push ovalue - - vars.push var - values[var] = value - - # set the variable in the current scope - assert_nothing_raised { - scope.setvar(var,value) - } + # Set a variable in the top and make sure all three can get it + topscope.setvar("first", "topval") + scopes.each do |name, scope| + assert_equal("topval", scope.lookupvar("first", false), "Could not find var in %s" % name) + end - # this should override previous values - assert_nothing_raised { - scope.setvar(over,ovalue) - } + # Now set a var in the midscope and make sure the mid and bottom can see it but not the top + midscope.setvar("second", "midval") + assert_equal(:undefined, scopes[:top].lookupvar("second", false), "Found child var in top scope") + [:mid, :bot].each do |name| + assert_equal("midval", scopes[name].lookupvar("second", false), "Could not find var in %s" % name) + end - assert_equal(value,scope.lookupvar(var)) - - #puts "%s vars, %s scopes" % [vars.length,scopes.length] - i = 0 - vars.zip(scopes) { |v,s| - # this recurses all the way up the tree as necessary - val = nil - oval = nil - - # look up the values using the bottom scope - assert_nothing_raised { - val = scope.lookupvar(v) - oval = scope.lookupvar(over) - } - - # verify they're correct - assert_equal(values[v],val) - assert_equal(ovalue,oval) - - # verify that we get the most recent value - assert_equal(ovalue,scope.lookupvar(over)) - - # verify that they aren't available in upper scopes - if parent = s.parent - val = nil - assert_nothing_raised { - val = parent.lookupvar(v) - } - assert_equal("", val, "Did not get empty string on missing var") - - # and verify that the parent sees its correct value - assert_equal(ovalues[i - 1],parent.lookupvar(over)) - end - i += 1 - } - } + # And set something in the bottom, and make sure we only find it there. + botscope.setvar("third", "botval") + [:top, :mid].each do |name| + assert_equal(:undefined, scopes[name].lookupvar("third", false), "Found child var in top scope") + end + assert_equal("botval", scopes[:bot].lookupvar("third", false), "Could not find var in bottom scope") end def test_lookupvar - interp = mkinterp - scope = mkscope :interp => interp + parser = mkparser + scope = mkscope :parser => parser # first do the plain lookups assert_equal("", scope.lookupvar("var"), "scope did not default to string") assert_equal("", scope.lookupvar("var", true), "scope ignored usestring setting") assert_equal(:undefined, scope.lookupvar("var", false), "scope ignored usestring setting when false") # Now set the var scope.setvar("var", "yep") assert_equal("yep", scope.lookupvar("var"), "did not retrieve value correctly") # Now test the parent lookups - subscope = mkscope :interp => interp + subscope = mkscope :parser => parser subscope.parent = scope assert_equal("", subscope.lookupvar("nope"), "scope did not default to string with parent") assert_equal("", subscope.lookupvar("nope", true), "scope ignored usestring setting with parent") assert_equal(:undefined, subscope.lookupvar("nope", false), "scope ignored usestring setting when false with parent") assert_equal("yep", subscope.lookupvar("var"), "did not retrieve value correctly from parent") # Now override the value in the subscope subscope.setvar("var", "sub") assert_equal("sub", subscope.lookupvar("var"), "did not retrieve overridden value correctly") # Make sure we punt when the var is qualified. Specify the usestring value, so we know it propagates. scope.expects(:lookup_qualified_var).with("one::two", false).returns(:punted) assert_equal(:punted, scope.lookupvar("one::two", false), "did not return the value of lookup_qualified_var") end def test_lookup_qualified_var - interp = mkinterp - scope = mkscope :interp => interp + parser = mkparser + scope = mkscope :parser => parser scopes = {} classes = ["", "one", "one::two", "one::two::three"].each do |name| - klass = interp.newclass(name) - klass.evaluate(:scope => scope) - scopes[name] = scope.class_scope(klass) + klass = parser.newclass(name) + Puppet::Parser::Resource.new(:type => "class", :title => name, :scope => scope, :source => mock('source')).evaluate + scopes[name] = scope.compile.class_scope(klass) end classes.each do |name| var = [name, "var"].join("::") scopes[name].expects(:lookupvar).with("var", false).returns(name) assert_equal(name, scope.send(:lookup_qualified_var, var, false), "did not get correct value from lookupvar") end end def test_declarative # set to declarative - top = mkscope(:declarative => true) + top = mkscope sub = mkscope(:parent => top) assert_nothing_raised { top.setvar("test","value") } assert_raise(Puppet::ParseError) { top.setvar("test","other") } assert_nothing_raised { sub.setvar("test","later") } assert_raise(Puppet::ParseError) { top.setvar("test","yeehaw") } end - def test_notdeclarative - # set to not declarative - top = mkscope(:declarative => false) - sub = mkscope(:parent => top) + def test_setdefaults + config = mkcompile - assert_nothing_raised { - top.setvar("test","value") - } - assert_nothing_raised { - top.setvar("test","other") - } - assert_nothing_raised { - sub.setvar("test","later") - } - assert_nothing_raised { - sub.setvar("test","yayness") - } - end + scope = config.topscope - def test_setdefaults - interp, scope, source = mkclassframing + defaults = scope.instance_variable_get("@defaults") - # The setdefaults method doesn't really check what we're doing, - # so we're just going to use fake defaults here. + # First the case where there are no defaults and we pass a single param + param = stub :name => "myparam", :file => "f", :line => "l" + scope.setdefaults(:mytype, param) + assert_equal({"myparam" => param}, defaults[:mytype], "Did not set default correctly") - # First do a simple local lookup - params = paramify(source, :one => "fun", :two => "shoe") - origshould = {} - params.each do |p| origshould[p.name] = p end - assert_nothing_raised do - scope.setdefaults(:file, params) - end + # Now the case where we pass in multiple parameters + param1 = stub :name => "one", :file => "f", :line => "l" + param2 = stub :name => "two", :file => "f", :line => "l" + scope.setdefaults(:newtype, [param1, param2]) + assert_equal({"one" => param1, "two" => param2}, defaults[:newtype], "Did not set multiple defaults correctly") - ret = nil - assert_nothing_raised do - ret = scope.lookupdefaults(:file) + # And the case where there's actually a conflict. Use the first default for this. + newparam = stub :name => "myparam", :file => "f", :line => "l" + assert_raise(Puppet::ParseError, "Allowed resetting of defaults") do + scope.setdefaults(:mytype, param) end + assert_equal({"myparam" => param}, defaults[:mytype], "Replaced default even though there was a failure") + end - assert_equal(origshould, ret) - - # Now create a subscope and add some more params. - newscope = scope.newscope + def test_lookupdefaults + config = mkcompile + top = config.topscope - newparams = paramify(source, :one => "shun", :three => "free") - assert_nothing_raised { - newscope.setdefaults(:file, newparams) - } + # Make a subscope + sub = config.newscope(top) - # And make sure we get the appropriate ones back - should = {} - params.each do |p| should[p.name] = p end - newparams.each do |p| should[p.name] = p end + topdefs = top.instance_variable_get("@defaults") + subdefs = sub.instance_variable_get("@defaults") - assert_nothing_raised do - ret = newscope.lookupdefaults(:file) - end + # First add some defaults to our top scope + topdefs[:t1] = {:p1 => :p2, :p3 => :p4} + topdefs[:t2] = {:p5 => :p6} - assert_equal(should, ret) + # Then the sub scope + subdefs[:t1] = {:p1 => :p7, :p8 => :p9} + subdefs[:t2] = {:p5 => :p10, :p11 => :p12} - # Make sure we still only get the originals from the top scope - assert_nothing_raised do - ret = scope.lookupdefaults(:file) + # Now make sure we get the correct list back + result = nil + assert_nothing_raised("Could not get defaults") do + result = sub.lookupdefaults(:t1) end + assert_equal(:p9, result[:p8], "Did not get child defaults") + assert_equal(:p4, result[:p3], "Did not override parent defaults with child default") + assert_equal(:p7, result[:p1], "Did not get parent defaults") + end - assert_equal(origshould, ret) + def test_parent + config = mkcompile + top = config.topscope - # Now create another scope and make sure we only get the top defaults - otherscope = scope.newscope - assert_equal(origshould, otherscope.lookupdefaults(:file)) + # Make a subscope + sub = config.newscope(top) - # And make sure none of the scopes has defaults for other types - [scope, newscope, otherscope].each do |sc| - assert_equal({}, sc.lookupdefaults(:exec)) - end + assert_equal(top, sub.parent, "Did not find parent scope correctly") + assert_equal(top, sub.parent, "Did not find parent scope on second call") end def test_strinterp # Make and evaluate our classes so the qualified lookups work - interp = mkinterp - klass = interp.newclass("") - scope = mkscope(:interp => interp) - klass.evaluate(:scope => scope) - - klass = interp.newclass("one") - klass.evaluate(:scope => scope) - - klass = interp.newclass("one::two") - klass.evaluate(:scope => scope) - + parser = mkparser + klass = parser.newclass("") + scope = mkscope(:parser => parser) + Puppet::Parser::Resource.new(:type => "class", :title => :main, :scope => scope, :source => mock('source')).evaluate - scope = scope.class_scope("") assert_nothing_raised { scope.setvar("test","value") } scopes = {"" => scope} %w{one one::two one::two::three}.each do |name| - klass = interp.newclass(name) - klass.evaluate(:scope => scope) - scopes[name] = scope.class_scope(klass) + klass = parser.newclass(name) + Puppet::Parser::Resource.new(:type => "class", :title => name, :scope => scope, :source => mock('source')).evaluate + scopes[name] = scope.compile.class_scope(klass) scopes[name].setvar("test", "value-%s" % name.sub(/.+::/,'')) end assert_equal("value", scope.lookupvar("::test"), "did not look up qualified value correctly") tests = { "string ${test}" => "string value", "string ${one::two::three::test}" => "string value-three", "string $one::two::three::test" => "string value-three", "string ${one::two::test}" => "string value-two", "string $one::two::test" => "string value-two", "string ${one::test}" => "string value-one", "string $one::test" => "string value-one", "string ${::test}" => "string value", "string $::test" => "string value", "string ${test} ${test} ${test}" => "string value value value", "string $test ${test} $test" => "string value value value", "string \\$test" => "string $test", '\\$test string' => "$test string", '$test string' => "value string", 'a testing $' => "a testing $", 'a testing \$' => "a testing $", "an escaped \\\n carriage return" => "an escaped carriage return", '\$' => "$", '\s' => "\s", '\t' => "\t", '\n' => "\n" } tests.each do |input, output| assert_nothing_raised("Failed to scan %s" % input.inspect) do assert_equal(output, scope.strinterp(input), - 'did not interpret %s correctly' % input.inspect) + 'did not parserret %s correctly' % input.inspect) end end logs = [] Puppet::Util::Log.close Puppet::Util::Log.newdestination(logs) # #523 %w{d f h l w z}.each do |l| string = "\\" + l assert_nothing_raised do assert_equal(string, scope.strinterp(string), - 'did not interpret %s correctly' % string) + 'did not parserret %s correctly' % string) end assert(logs.detect { |m| m.message =~ /Unrecognised escape/ }, "Did not get warning about escape sequence with %s" % string) logs.clear end end - def test_setclass - interp, scope, source = mkclassframing - - base = scope.findclass("base") - assert(base, "Could not find base class") - assert(! scope.class_scope(base), "Class incorrectly set") - assert(! scope.classlist.include?("base"), "Class incorrectly in classlist") - assert_nothing_raised do - scope.setclass base - end - - assert(scope.class_scope(base), "Class incorrectly unset") - assert(scope.classlist.include?("base"), "Class not in classlist") - - # Make sure we can retrieve the scope. - assert_equal(scope, scope.class_scope(base), - "class scope was not set correctly") - - # Now try it with a normal string - Puppet[:trace] = false - assert_raise(Puppet::DevError) do - scope.setclass "string" - end - - assert(! scope.class_scope("string"), "string incorrectly set") - - # Set "" in the class list, and make sure it doesn't show up in the return - top = scope.findclass("") - assert(top, "Could not find top class") - scope.setclass top - - assert(! scope.classlist.include?(""), "Class list included empty") - end - - def test_validtags - scope = mkscope() - - ["a class", "a.class"].each do |bad| - assert_raise(Puppet::ParseError, "Incorrectly allowed %s" % bad.inspect) do - scope.tag(bad) - end - end - - ["a-class", "a_class", "Class", "class", "yayNess"].each do |good| - assert_nothing_raised("Incorrectly banned %s" % good.inspect) do - scope.tag(good) - end - end - - end - def test_tagfunction - scope = mkscope() - - assert_nothing_raised { - scope.function_tag(["yayness", "booness"]) - } - - assert(scope.tags.include?("yayness"), "tag 'yayness' did not get set") - assert(scope.tags.include?("booness"), "tag 'booness' did not get set") - - # Now verify that the 'tagged' function works correctly - assert(scope.function_tagged("yayness"), - "tagged function incorrectly returned false") - assert(scope.function_tagged("booness"), - "tagged function incorrectly returned false") + scope = mkscope + resource = mock 'resource' + scope.resource = resource + resource.expects(:tag).with("yayness", "booness") - assert(! scope.function_tagged("funtest"), - "tagged function incorrectly returned true") + scope.function_tag(%w{yayness booness}) end def test_includefunction - interp = mkinterp - scope = mkscope :interp => interp + parser = mkparser + scope = mkscope :parser => parser - myclass = interp.newclass "myclass" - otherclass = interp.newclass "otherclass" + myclass = parser.newclass "myclass" + otherclass = parser.newclass "otherclass" function = Puppet::Parser::AST::Function.new( :name => "include", :ftype => :statement, :arguments => AST::ASTArray.new( :children => [nameobj("myclass"), nameobj("otherclass")] ) ) assert_nothing_raised do function.evaluate :scope => scope end + scope.compile.send(:evaluate_generators) + [myclass, otherclass].each do |klass| - assert(scope.class_scope(klass), + assert(scope.compile.class_scope(klass), "%s was not set" % klass.classname) end end def test_definedfunction - interp = mkinterp + parser = mkparser %w{one two}.each do |name| - interp.newdefine name + parser.newdefine name end - scope = mkscope :interp => interp + scope = mkscope :parser => parser assert_nothing_raised { %w{one two file user}.each do |type| assert(scope.function_defined([type]), "Class #{type} was not considered defined") end assert(!scope.function_defined(["nopeness"]), "Class 'nopeness' was incorrectly considered defined") } end # Make sure we know what we consider to be truth. def test_truth assert_equal(true, Puppet::Parser::Scope.true?("a string"), "Strings not considered true") assert_equal(true, Puppet::Parser::Scope.true?(true), "True considered true") assert_equal(false, Puppet::Parser::Scope.true?(""), "Empty strings considered true") assert_equal(false, Puppet::Parser::Scope.true?(false), "false considered true") assert_equal(false, Puppet::Parser::Scope.true?(:undef), "undef considered true") end - # Verify scope context is handled correctly. - def test_scopeinside - scope = mkscope() - - one = :one - two = :two - - # First just test the basic functionality. - assert_nothing_raised { - scope.inside :one do - assert_equal(:one, scope.inside, "Context did not get set") - end - assert_nil(scope.inside, "Context did not revert") - } - - # Now make sure error settings work. - assert_raise(RuntimeError) { - scope.inside :one do - raise RuntimeError, "This is a failure, yo" - end - } - assert_nil(scope.inside, "Context did not revert") - - # Now test it a bit deeper in. - assert_nothing_raised { - scope.inside :one do - scope.inside :two do - assert_equal(:two, scope.inside, "Context did not get set") - end - assert_equal(:one, scope.inside, "Context did not get set") - end - assert_nil(scope.inside, "Context did not revert") - } - - # And lastly, check errors deeper in - assert_nothing_raised { - scope.inside :one do - begin - scope.inside :two do - raise "a failure" - end - rescue - end - assert_equal(:one, scope.inside, "Context did not get set") - end - assert_nil(scope.inside, "Context did not revert") - } - - end - if defined? ActiveRecord # Verify that we recursively mark as exported the results of collectable # components. def test_exportedcomponents - interp, scope, source = mkclassframing - children = [] + config = mkcompile + parser = config.parser + + # Create a default source + config.topscope.source = parser.newclass "", "" + + # And a scope resource + scope_res = stub 'scope_resource', :virtual? => true, :exported? => false, :tags => [] + config.topscope.resource = scope_res args = AST::ASTArray.new( :file => tempfile(), :line => rand(100), :children => [nameobj("arg")] ) # Create a top-level component - interp.newdefine "one", :arguments => [%w{arg}], + parser.newdefine "one", :arguments => [%w{arg}], :code => AST::ASTArray.new( :children => [ resourcedef("file", "/tmp", {"owner" => varref("arg")}) ] ) # And a component that calls it - interp.newdefine "two", :arguments => [%w{arg}], + parser.newdefine "two", :arguments => [%w{arg}], :code => AST::ASTArray.new( :children => [ resourcedef("one", "ptest", {"arg" => varref("arg")}) ] ) # And then a third component that calls the second - interp.newdefine "three", :arguments => [%w{arg}], + parser.newdefine "three", :arguments => [%w{arg}], :code => AST::ASTArray.new( :children => [ resourcedef("two", "yay", {"arg" => varref("arg")}) ] ) # lastly, create an object that calls our third component obj = resourcedef("three", "boo", {"arg" => "parentfoo"}) # And mark it as exported obj.exported = true - obj.evaluate :scope => scope - # And then evaluate it - interp.evaliterate(scope) + obj.evaluate :scope => config.topscope + + # And run the loop. + config.send(:evaluate_generators) %w{file}.each do |type| - objects = scope.lookupexported(type) + objects = config.resources.find_all { |r| r.type == type and r.exported } assert(!objects.empty?, "Did not get an exported %s" % type) end end # Verify that we can both store and collect an object in the same # run, whether it's in the same scope as a collection or a different # scope. def test_storeandcollect Puppet[:storeconfigs] = true Puppet::Rails.init sleep 1 children = [] file = tempfile() File.open(file, "w") { |f| f.puts " class yay { @@host { myhost: ip => \"192.168.0.2\" } } include yay @@host { puppet: ip => \"192.168.0.3\" } Host <<||>>" } interp = nil assert_nothing_raised { interp = Puppet::Parser::Interpreter.new( :Manifest => file, :UseNodes => false, :ForkSave => false ) } - objects = nil + config = nil # We run it twice because we want to make sure there's no conflict # if we pull it up from the database. + node = mknode + node.parameters = {"hostname" => node.name} 2.times { |i| assert_nothing_raised { - objects = interp.run("localhost", {"hostname" => "localhost"}) + config = interp.compile(node) } - flat = objects.flatten + flat = config.extract.flatten %w{puppet myhost}.each do |name| assert(flat.find{|o| o.name == name }, "Did not find #{name}") end } end else $stderr.puts "No ActiveRecord -- skipping collection tests" end - # Make sure tags behave appropriately. - def test_tags - interp, scope, source = mkclassframing - - # First make sure we can only set legal tags - ["an invalid tag", "-anotherinvalid", "bad*tag"].each do |tag| - assert_raise(Puppet::ParseError, "Tag #{tag} was considered valid") do - scope.tag tag - end - end - - # Now make sure good tags make it through. - tags = %w{good-tag yaytag GoodTag another_tag a ab A} - tags.each do |tag| - assert_nothing_raised("Tag #{tag} was considered invalid") do - scope.tag tag - end - end - - # And make sure we get each of them. - ptags = scope.tags - tags.each do |tag| - assert(ptags.include?(tag), "missing #{tag}") - end - - - # Now create a subscope and set some tags there - newscope = scope.newscope(:type => 'subscope') - - # set some tags - newscope.tag "onemore", "yaytag" - - # And make sure we get them plus our parent tags - assert_equal((ptags + %w{onemore subscope}).sort, newscope.tags.sort) - end - - # Make sure we successfully translate objects - def test_translate - interp, scope, source = mkclassframing - - # Create a define that we'll be using - interp.newdefine("wrapper", :code => AST::ASTArray.new(:children => [ - resourcedef("file", varref("name"), "owner" => "root") - ])) - - # Now create a resource that uses that define - define = mkresource(:type => "wrapper", :title => "/tmp/testing", - :scope => scope, :source => source, :params => :none) - - scope.setresource define - - # And a normal resource - scope.setresource mkresource(:type => "file", :title => "/tmp/rahness", - :scope => scope, :source => source, - :params => {:owner => "root"}) - - # Evaluate the the define thing. - define.evaluate - - # Now the scope should have a resource and a subscope. Translate the - # whole thing. - ret = nil - assert_nothing_raised do - ret = scope.translate - end - - assert_instance_of(Puppet::TransBucket, ret) - - ret.each do |obj| - assert(obj.is_a?(Puppet::TransBucket) || obj.is_a?(Puppet::TransObject), - "Got a non-transportable object %s" % obj.class) - end - - rahness = ret.find { |c| c.type == "file" and c.name == "/tmp/rahness" } - assert(rahness, "Could not find top-level file") - assert_equal("root", rahness["owner"]) - - bucket = ret.find { |c| c.class == Puppet::TransBucket and c.name == "/tmp/testing" } - assert(bucket, "Could not find define bucket") - - testing = bucket.find { |c| c.type == "file" and c.name == "/tmp/testing" } - assert(testing, "Could not find define file") - assert_equal("root", testing["owner"]) - - end - def test_namespaces - interp, scope, source = mkclassframing + scope = mkscope assert_equal([""], scope.namespaces, "Started out with incorrect namespaces") assert_nothing_raised { scope.add_namespace("fun::test") } assert_equal(["fun::test"], scope.namespaces, "Did not add namespace correctly") assert_nothing_raised { scope.add_namespace("yay::test") } assert_equal(["fun::test", "yay::test"], scope.namespaces, "Did not add extra namespace correctly") end def test_findclass_and_finddefine - interp = mkinterp + parser = mkparser - # Make sure our scope calls the interp findclass method with + # Make sure our scope calls the parser findclass method with # the right namespaces - scope = mkscope :interp => interp + scope = mkscope :parser => parser - interp.metaclass.send(:attr_accessor, :last) + parser.metaclass.send(:attr_accessor, :last) methods = [:findclass, :finddefine] methods.each do |m| - interp.meta_def(m) do |namespace, name| + parser.meta_def(m) do |namespace, name| @checked ||= [] @checked << [namespace, name] # Only return a value on the last call. if @last == namespace ret = @checked.dup @checked.clear return ret else return nil end end end test = proc do |should| - interp.last = scope.namespaces[-1] + parser.last = scope.namespaces[-1] methods.each do |method| result = scope.send(method, "testing") assert_equal(should, result, "did not get correct value from %s with namespaces %s" % [method, scope.namespaces.inspect]) end end # Start with the empty namespace assert_nothing_raised { test.call([["", "testing"]]) } # Now add a namespace scope.add_namespace("a") assert_nothing_raised { test.call([["a", "testing"]]) } # And another scope.add_namespace("b") assert_nothing_raised { test.call([["a", "testing"], ["b", "testing"]]) } end # #629 - undef should be "" or :undef def test_lookupvar_with_undef scope = mkscope scope.setvar("testing", :undef) assert_equal(:undef, scope.lookupvar("testing", false), "undef was not returned as :undef when not string") assert_equal("", scope.lookupvar("testing", true), "undef was not returned as '' when string") end - - # #620 - Nodes and classes should conflict, else classes don't get evaluated - def test_nodes_and_classes_name_conflict - scope = mkscope - - node = AST::Node.new :classname => "test", :namespace => "" - scope.setclass(node) - - assert(scope.nodescope?, "Scope was not marked a node scope when a node was set") - - # Now make a subscope that will be a class scope - klass = AST::HostClass.new :classname => "test", :namespace => "" - kscope = klass.subscope(scope) - - # Now make sure we throw a failure, because we're trying to do a class and node - # with the same name - assert_raise(Puppet::ParseError, "Did not fail on class and node with same name") do - kscope.class_scope(klass) - end - end end # $Id$ diff --git a/test/language/snippets.rb b/test/language/snippets.rb index 5fb11e8cd..2c74543e7 100755 --- a/test/language/snippets.rb +++ b/test/language/snippets.rb @@ -1,512 +1,519 @@ #!/usr/bin/env ruby $:.unshift("../lib").unshift("../../lib") if __FILE__ =~ /\.rb$/ require 'puppet' require 'puppet/parser/interpreter' require 'puppet/parser/parser' require 'puppet/network/client' require 'puppet/network/handler' require 'puppettest' class TestSnippets < Test::Unit::TestCase include PuppetTest - include ObjectSpace def setup super @file = Puppet::Type.type(:file) end def self.snippetdir PuppetTest.datadir "snippets" end def assert_file(path, msg = nil) unless file = @file[path] msg ||= "Could not find file %s" % path raise msg end end def assert_mode_equal(mode, path) unless file = @file[path] raise "Could not find file %s" % path end unless mode == file.should(:mode) raise "Mode for %s is incorrect: %o vs %o" % [path, mode, file.should(:mode)] end end def snippet(name) File.join(self.class.snippetdir, name) end def file2ast(file) parser = Puppet::Parser::Parser.new() parser.file = file ast = parser.parse return ast end def snippet2ast(text) parser = Puppet::Parser::Parser.new() parser.string = text ast = parser.parse return ast end def client args = { :Listen => false } Puppet::Network::Client.new(args) end def ast2scope(ast) interp = Puppet::Parser::Interpreter.new( :ast => ast, :client => client() ) scope = Puppet::Parser::Scope.new() ast.evaluate(scope) return scope end def scope2objs(scope) objs = scope.to_trans end def snippet2scope(snippet) ast = snippet2ast(snippet) scope = ast2scope(ast) end def snippet2objs(snippet) ast = snippet2ast(snippet) scope = ast2scope(ast) objs = scope2objs(scope) end def properties(type) properties = type.validproperties end def metaparams(type) mparams = [] Puppet::Type.eachmetaparam { |param| mparams.push param } mparams end def params(type) params = [] type.parameters.each { |name,property| params.push name } params end def randthing(thing,type) list = self.send(thing,type) list[rand(list.length)] end def randeach(type) [:properties, :metaparams, :params].collect { |thing| randthing(thing,type) } end @@snippets = { true => [ %{File { mode => 755 }} ], } def disabled_test_defaults Puppet::Type.eachtype { |type| next if type.name == :puppet or type.name == :component rands = randeach(type) name = type.name.to_s.capitalize [0..1, 0..2].each { |range| params = rands[range] paramstr = params.collect { |param| "%s => fake" % param }.join(", ") str = "%s { %s }" % [name, paramstr] scope = nil assert_nothing_raised { scope = snippet2scope(str) } defaults = nil assert_nothing_raised { defaults = scope.lookupdefaults(name) } p defaults params.each { |param| puts "%s => '%s'" % [name,param] assert(defaults.include?(param)) } } } end # this is here in case no tests get defined; otherwise we get a warning def test_nothing end def snippet_filecreate %w{a b c d}.each { |letter| path = "/tmp/create%stest" % letter assert_file(path) if %w{a b}.include?(letter) assert_mode_equal(0755, path) end } end def snippet_simpledefaults path = "/tmp/defaulttest" assert_file(path) assert_mode_equal(0755, path) end def snippet_simpleselector files = %w{a b c d}.collect { |letter| path = "/tmp/snippetselect%stest" % letter assert_file(path) assert_mode_equal(0755, path) } end def snippet_classpathtest path = "/tmp/classtest" file = @file[path] assert(file, "did not create file %s" % path) assert_nothing_raised { assert_equal( "//testing/component[componentname]/File[/tmp/classtest]", file.path) } end def snippet_argumentdefaults path1 = "/tmp/argumenttest1" path2 = "/tmp/argumenttest2" file1 = @file[path1] file2 = @file[path2] assert_file(path1) assert_mode_equal(0755, path1) assert_file(path2) assert_mode_equal(0644, path2) end def snippet_casestatement paths = %w{ /tmp/existsfile /tmp/existsfile2 /tmp/existsfile3 /tmp/existsfile4 /tmp/existsfile5 } paths.each { |path| file = @file[path] assert(file, "File %s is missing" % path) assert_mode_equal(0755, path) } end def snippet_implicititeration paths = %w{a b c d e f g h}.collect { |l| "/tmp/iteration%stest" % l } paths.each { |path| file = @file[path] assert_file(path) assert_mode_equal(0755, path) } end def snippet_multipleinstances paths = %w{a b c}.collect { |l| "/tmp/multipleinstances%s" % l } paths.each { |path| assert_file(path) assert_mode_equal(0755, path) } end def snippet_namevartest file = "/tmp/testfiletest" dir = "/tmp/testdirtest" assert_file(file) assert_file(dir) assert_equal(:directory, @file[dir].should(:ensure), "Directory is not set to be a directory") end def snippet_scopetest file = "/tmp/scopetest" assert_file(file) assert_mode_equal(0755, file) end def snippet_failmissingexecpath file = "/tmp/exectesting1" execfile = "/tmp/execdisttesting" assert_file(file) assert_nil(Puppet::Type.type(:exec)["exectest"], "invalid exec was created") end def snippet_selectorvalues nums = %w{1 2 3 4 5} files = nums.collect { |n| "/tmp/selectorvalues%s" % n } files.each { |f| assert_file(f) assert_mode_equal(0755, f) } end def snippet_singleselector nums = %w{1 2 3} files = nums.collect { |n| "/tmp/singleselector%s" % n } files.each { |f| assert_file(f) assert_mode_equal(0755, f) } end def snippet_falsevalues file = "/tmp/falsevaluesfalse" assert_file(file) end def disabled_snippet_classargtest [1,2].each { |num| file = "/tmp/classargtest%s" % num assert_file(file) assert_mode_equal(0755, file) } end def snippet_classheirarchy [1,2,3].each { |num| file = "/tmp/classheir%s" % num assert_file(file) assert_mode_equal(0755, file) } end def snippet_singleary [1,2,3,4].each { |num| file = "/tmp/singleary%s" % num assert_file(file) } end def snippet_classincludes [1,2,3].each { |num| file = "/tmp/classincludes%s" % num assert_file(file) assert_mode_equal(0755, file) } end def snippet_componentmetaparams ["/tmp/component1", "/tmp/component2"].each { |file| assert_file(file) } end def snippet_aliastest %w{/tmp/aliastest /tmp/aliastest2 /tmp/aliastest3}.each { |file| assert_file(file) } end def snippet_singlequote { 1 => 'a $quote', 2 => 'some "\yayness\"' }.each { |count, str| path = "/tmp/singlequote%s" % count assert_file(path) assert_equal(str, @file[path].should(:content)) } end # There's no way to actually retrieve the list of classes from the # transaction. def snippet_tag end # Make sure that set tags are correctly in place, yo. def snippet_tagged tags = {"testing" => true, "yayness" => false, "both" => false, "bothtrue" => true, "define" => true} tags.each do |tag, retval| assert_file("/tmp/tagged#{tag}#{retval.to_s}") end end def snippet_defineoverrides file = "/tmp/defineoverrides1" assert_file(file) assert_mode_equal(0755, file) end def snippet_deepclassheirarchy 5.times { |i| i += 1 file = "/tmp/deepclassheir%s" % i assert_file(file) } end def snippet_emptyclass # There's nothing to check other than that it works end def snippet_emptyexec assert(Puppet::Type.type(:exec)["touch /tmp/emptyexectest"], "Did not create exec") end def snippet_multisubs path = "/tmp/multisubtest" assert_file(path) file = @file[path] assert_equal("sub2", file.should(:content), "sub2 did not override content") assert_mode_equal(0755, path) end def snippet_collection assert_file("/tmp/colltest1") assert_nil(@file["/tmp/colltest2"], "Incorrectly collected file") end def snippet_virtualresources %w{1 2 3 4}.each do |num| assert_file("/tmp/virtualtest#{num}") end end def snippet_componentrequire %w{1 2}.each do |num| assert_file("/tmp/testing_component_requires#{num}", "#{num} does not exist") end end def snippet_realize_defined_types assert_file("/tmp/realize_defined_test1") assert_file("/tmp/realize_defined_test2") end def snippet_fqparents assert_file("/tmp/fqparent1", "Did not make file from parent class") assert_file("/tmp/fqparent2", "Did not make file from subclass") end def snippet_fqdefinition assert_file("/tmp/fqdefinition", "Did not make file from fully-qualified definition") end def snippet_subclass_name_duplication assert_file("/tmp/subclass_name_duplication1", "Did not make first file from duplicate subclass names") assert_file("/tmp/subclass_name_duplication2", "Did not make second file from duplicate subclass names") end # Iterate across each of the snippets and create a test. Dir.entries(snippetdir).sort.each { |file| next if file =~ /^\./ mname = "snippet_" + file.sub(/\.pp$/, '') if self.method_defined?(mname) #eval("alias %s %s" % [testname, mname]) testname = ("test_" + mname).intern self.send(:define_method, testname) { + facts = { + "hostname" => "testhost", + "domain" => "domain.com", + "ipaddress" => "127.0.0.1", + "fqdn" => "testhost.domain.com" + } + Facter.stubs(:each) + facts.each do |name, value| + Facter.stubs(:value).with(name).returns(value) + end # first parse the file server = Puppet::Network::Handler.master.new( :Manifest => snippet(file), :Local => true ) + server.send(:fact_handler).stubs(:set) + server.send(:fact_handler).stubs(:get).returns(facts) client = Puppet::Network::Client.master.new( :Master => server, :Cache => false ) + client.class.stubs(:facts).returns(facts) assert(client.local) assert_nothing_raised { client.getconfig() } client = Puppet::Network::Client.master.new( :Master => server, :Cache => false ) assert(client.local) # Now do it again Puppet::Type.allclear assert_nothing_raised { client.getconfig() } - #assert_nothing_raised { - # trans = client.apply() - #} Puppet::Type.eachtype { |type| type.each { |obj| # don't worry about this for now #unless obj.name == "puppet[top]" or # obj.is_a?(Puppet.type(:schedule)) # assert(obj.parent, "%s has no parent" % obj.name) #end assert(obj.name) } } assert_nothing_raised { self.send(mname) } client.clear } mname = mname.intern end } end - -# $Id$ diff --git a/test/lib/puppettest.rb b/test/lib/puppettest.rb index 8b85966f5..b56bc563e 100755 --- a/test/lib/puppettest.rb +++ b/test/lib/puppettest.rb @@ -1,307 +1,309 @@ # Add .../test/lib $LOAD_PATH.unshift(File.expand_path(File.dirname(__FILE__))) # Add .../lib $LOAD_PATH.unshift(File.expand_path(File.join(File.dirname(__FILE__), '../../lib'))) require 'puppet' +require 'mocha' require 'test/unit' # Yay; hackish but it works if ARGV.include?("-d") ARGV.delete("-d") $console = true end module PuppetTest # Munge cli arguments, so we can enable debugging if we want # and so we can run just specific methods. def self.munge_argv require 'getoptlong' result = GetoptLong.new( [ "--debug", "-d", GetoptLong::NO_ARGUMENT ], [ "--resolve", "-r", GetoptLong::REQUIRED_ARGUMENT ], [ "-n", GetoptLong::REQUIRED_ARGUMENT ], [ "--help", "-h", GetoptLong::NO_ARGUMENT ] ) usage = "USAGE: TESTOPTS='[-n -n ...] [-d]' rake [target] [target] ..." opts = [] dir = method = nil result.each { |opt,arg| case opt when "--resolve" dir, method = arg.split(",") when "--debug" $puppet_debug = true Puppet::Util::Log.level = :debug Puppet::Util::Log.newdestination(:console) when "--help" puts usage exit else opts << opt << arg end } suites = nil args = ARGV.dup # Reset the options, so the test suite can deal with them (this is # what makes things like '-n' work). opts.each { |o| ARGV << o } return args end # Find the root of the Puppet tree; this is not the test directory, but # the parent of that dir. def basedir(*list) unless defined? @@basedir Dir.chdir(File.dirname(__FILE__)) do @@basedir = File.dirname(File.dirname(Dir.getwd)) end end if list.empty? @@basedir else File.join(@@basedir, *list) end end def cleanup(&block) @@cleaners << block end def datadir(*list) File.join(basedir, "test", "data", *list) end def exampledir(*args) unless defined? @@exampledir @@exampledir = File.join(basedir, "examples") end if args.empty? return @@exampledir else return File.join(@@exampledir, *args) end end module_function :basedir, :datadir, :exampledir # Rails clobbers RUBYLIB, thanks def libsetup curlibs = ENV["RUBYLIB"].split(":") $:.reject do |dir| dir =~ /^\/usr/ end.each do |dir| unless curlibs.include?(dir) curlibs << dir end end ENV["RUBYLIB"] = curlibs.join(":") end def logcollector collector = [] Puppet::Util::Log.newdestination(collector) cleanup do Puppet::Util::Log.close(collector) end collector end def rake? $0 =~ /test_loader/ end # Redirect stdout and stderr def redirect @stderr = tempfile @stdout = tempfile $stderr = File.open(@stderr, "w") $stdout = File.open(@stdout, "w") cleanup do $stderr = STDERR $stdout = STDOUT end end def setup @memoryatstart = Puppet::Util.memory if defined? @@testcount @@testcount += 1 else @@testcount = 0 end @configpath = File.join(tmpdir, self.class.to_s + "configdir" + @@testcount.to_s + "/" ) unless defined? $user and $group $user = nonrootuser().uid.to_s $group = nonrootgroup().gid.to_s end Puppet.config.clear Puppet[:user] = $user Puppet[:group] = $group Puppet[:confdir] = @configpath Puppet[:vardir] = @configpath unless File.exists?(@configpath) Dir.mkdir(@configpath) end @@tmpfiles = [@configpath, tmpdir()] @@tmppids = [] @@cleaners = [] @logs = [] # If we're running under rake, then disable debugging and such. #if rake? or ! Puppet[:debug] if defined?($puppet_debug) or ! rake? if textmate? Puppet[:color] = false end Puppet::Util::Log.newdestination(@logs) if defined? $console Puppet.info @method_name Puppet::Util::Log.newdestination(:console) Puppet[:trace] = true end Puppet::Util::Log.level = :debug #$VERBOSE = 1 else Puppet::Util::Log.close Puppet::Util::Log.newdestination(@logs) Puppet[:httplog] = tempfile() end Puppet[:ignoreschedules] = true end def tempfile if defined? @@tmpfilenum @@tmpfilenum += 1 else @@tmpfilenum = 1 end f = File.join(self.tmpdir(), self.class.to_s + "_" + @method_name.to_s + @@tmpfilenum.to_s) @@tmpfiles << f return f end def textmate? if ENV["TM_FILENAME"] return true else return false end end def tstdir dir = tempfile() Dir.mkdir(dir) return dir end def tmpdir unless defined? @tmpdir and @tmpdir @tmpdir = case Facter["operatingsystem"].value when "Darwin": "/private/tmp" when "SunOS": "/var/tmp" else "/tmp" end @tmpdir = File.join(@tmpdir, "puppettesting") unless File.exists?(@tmpdir) FileUtils.mkdir_p(@tmpdir) File.chmod(01777, @tmpdir) end end @tmpdir end def teardown @@cleaners.each { |cleaner| cleaner.call() } @@tmpfiles.each { |file| unless file =~ /tmp/ puts "Not deleting tmpfile %s" % file next end if FileTest.exists?(file) system("chmod -R 755 %s" % file) system("rm -rf %s" % file) end } @@tmpfiles.clear @@tmppids.each { |pid| %x{kill -INT #{pid} 2>/dev/null} } @@tmppids.clear Puppet::Type.allclear Puppet::Util::Storage.clear Puppet.clear @memoryatend = Puppet::Util.memory diff = @memoryatend - @memoryatstart if diff > 1000 Puppet.info "%s#%s memory growth (%s to %s): %s" % [self.class, @method_name, @memoryatstart, @memoryatend, diff] end # reset all of the logs Puppet::Util::Log.close @logs.clear # Just in case there are processes waiting to die... require 'timeout' begin Timeout::timeout(5) do Process.waitall end rescue Timeout::Error # just move on end + mocha_verify if File.stat("/dev/null").mode & 007777 != 0666 File.open("/tmp/nullfailure", "w") { |f| f.puts self.class } exit(74) end end def logstore @logs = [] Puppet::Util::Log.newdestination(@logs) end end require 'puppettest/support' require 'puppettest/filetesting' require 'puppettest/fakes' require 'puppettest/exetest' require 'puppettest/parsertesting' require 'puppettest/servertest' require 'puppettest/testcase' # $Id$ diff --git a/test/lib/puppettest/parsertesting.rb b/test/lib/puppettest/parsertesting.rb index d66367ada..eef0cd8bc 100644 --- a/test/lib/puppettest/parsertesting.rb +++ b/test/lib/puppettest/parsertesting.rb @@ -1,392 +1,408 @@ require 'puppettest' require 'puppet/rails' module PuppetTest::ParserTesting include PuppetTest AST = Puppet::Parser::AST + Compile = Puppet::Parser::Compile + # A fake class that we can use for testing evaluation. class FakeAST attr_writer :evaluate def evaluated? defined? @evaluated and @evaluated end def evaluate(*args) @evaluated = true return @evaluate end def initialize(val = nil) if val @evaluate = val end end def reset @evaluated = nil end def safeevaluate(*args) evaluate() end end def astarray(*args) AST::ASTArray.new( :children => args ) end + def mkcompile(parser = nil) + require 'puppet/network/handler/node' + parser ||= mkparser + node = mknode + return Compile.new(node, parser) + end + + def mknode(name = nil) + require 'puppet/node' + name ||= "nodename" + Puppet::Network::Handler.handler(:node) + Puppet::Node.new(name) + end + def mkinterp(args = {}) args[:Code] ||= "" unless args.include?(:Manifest) args[:Local] ||= true Puppet::Parser::Interpreter.new(args) end def mkparser Puppet::Parser::Parser.new() end def mkscope(hash = {}) - hash[:interp] ||= mkinterp - hash[:source] ||= (hash[:interp].findclass("", "") || - hash[:interp].newclass("")) + hash[:parser] ||= mkparser + compile ||= mkcompile(hash[:parser]) + compile.topscope.source = (hash[:parser].findclass("", "") || hash[:parser].newclass("")) - unless hash[:source] + unless compile.topscope.source raise "Could not find source for scope" end - Puppet::Parser::Scope.new(hash) + # Make the 'main' stuff + compile.send(:evaluate_main) + compile.topscope end def classobj(name, hash = {}) hash[:file] ||= __FILE__ hash[:line] ||= __LINE__ hash[:type] ||= name AST::HostClass.new(hash) end def tagobj(*names) args = {} newnames = names.collect do |name| if name.is_a? AST name else nameobj(name) end end args[:type] = astarray(*newnames) assert_nothing_raised("Could not create tag %s" % names.inspect) { return AST::Tag.new(args) } end def resourcedef(type, title, params) unless title.is_a?(AST) title = stringobj(title) end assert_nothing_raised("Could not create %s %s" % [type, title]) { - return AST::ResourceDef.new( + return AST::Resource.new( :file => __FILE__, :line => __LINE__, :title => title, :type => type, :params => resourceinst(params) ) } end def virt_resourcedef(*args) res = resourcedef(*args) res.virtual = true res end def resourceoverride(type, title, params) assert_nothing_raised("Could not create %s %s" % [type, name]) { return AST::ResourceOverride.new( :file => __FILE__, :line => __LINE__, :object => resourceref(type, title), :type => type, :params => resourceinst(params) ) } end def resourceref(type, title) assert_nothing_raised("Could not create %s %s" % [type, title]) { - return AST::ResourceRef.new( + return AST::ResourceReference.new( :file => __FILE__, :line => __LINE__, :type => type, :title => stringobj(title) ) } end def fileobj(path, hash = {"owner" => "root"}) assert_nothing_raised("Could not create file %s" % path) { return resourcedef("file", path, hash) } end def nameobj(name) assert_nothing_raised("Could not create name %s" % name) { return AST::Name.new( :file => tempfile(), :line => rand(100), :value => name ) } end def typeobj(name) assert_nothing_raised("Could not create type %s" % name) { return AST::Type.new( :file => tempfile(), :line => rand(100), :value => name ) } end def nodedef(name) assert_nothing_raised("Could not create node %s" % name) { return AST::NodeDef.new( :file => tempfile(), :line => rand(100), :names => nameobj(name), :code => AST::ASTArray.new( :children => [ varobj("%svar" % name, "%svalue" % name), fileobj("/%s" % name) ] ) ) } end def resourceinst(hash) assert_nothing_raised("Could not create resource instance") { params = hash.collect { |param, value| resourceparam(param, value) } - return AST::ResourceInst.new( + return AST::ResourceInstance.new( :file => tempfile(), :line => rand(100), :children => params ) } end def resourceparam(param, value) # Allow them to pass non-strings in if value.is_a?(String) value = stringobj(value) end assert_nothing_raised("Could not create param %s" % param) { return AST::ResourceParam.new( :file => tempfile(), :line => rand(100), :param => param, :value => value ) } end def stringobj(value) AST::String.new( :file => tempfile(), :line => rand(100), :value => value ) end def varobj(name, value) unless value.is_a? AST value = stringobj(value) end assert_nothing_raised("Could not create %s code" % name) { return AST::VarDef.new( :file => tempfile(), :line => rand(100), :name => nameobj(name), :value => value ) } end def varref(name) assert_nothing_raised("Could not create %s variable" % name) { return AST::Variable.new( :file => __FILE__, :line => __LINE__, :value => name ) } end def argobj(name, value) assert_nothing_raised("Could not create %s compargument" % name) { return AST::CompArgument.new( :children => [nameobj(name), stringobj(value)] ) } end def defaultobj(type, params) pary = [] params.each { |p,v| pary << AST::ResourceParam.new( :file => __FILE__, :line => __LINE__, :param => p, :value => stringobj(v) ) } past = AST::ASTArray.new( :file => __FILE__, :line => __LINE__, :children => pary ) assert_nothing_raised("Could not create defaults for %s" % type) { return AST::ResourceDefaults.new( :file => __FILE__, :line => __LINE__, :type => type, :params => past ) } end def taggedobj(name, ftype = :statement) functionobj("tagged", name, ftype) end def functionobj(function, name, ftype = :statement) func = nil assert_nothing_raised do func = Puppet::Parser::AST::Function.new( :name => function, :ftype => ftype, :arguments => AST::ASTArray.new( :children => [nameobj(name)] ) ) end return func end # This assumes no nodes def assert_creates(manifest, *files) interp = nil assert_nothing_raised { interp = Puppet::Parser::Interpreter.new( :Manifest => manifest, :UseNodes => false ) } config = nil assert_nothing_raised { - config = interp.run(Facter["hostname"].value, {}) + config = interp.compile(mknode) } comp = nil assert_nothing_raised { - comp = config.to_type + comp = config.extract.to_type } assert_apply(comp) files.each do |file| assert(FileTest.exists?(file), "Did not create %s" % file) end end def mk_transobject(file = "/etc/passwd") obj = nil assert_nothing_raised { obj = Puppet::TransObject.new("file", file) obj["owner"] = "root" obj["mode"] = "644" } return obj end def mk_transbucket(*resources) bucket = nil assert_nothing_raised { bucket = Puppet::TransBucket.new bucket.name = "yayname" bucket.type = "yaytype" } resources.each { |o| bucket << o } return bucket end # Make a tree of resources, yielding if desired def mk_transtree(depth = 4, width = 2) top = nil assert_nothing_raised { top = Puppet::TransBucket.new top.name = "top" top.type = "bucket" } bucket = top file = tempfile() depth.times do |i| resources = [] width.times do |j| path = tempfile + i.to_s obj = Puppet::TransObject.new("file", path) obj["owner"] = "root" obj["mode"] = "644" # Yield, if they want if block_given? yield(obj, i, j) end resources << obj end newbucket = mk_transbucket(*resources) bucket.push newbucket bucket = newbucket end return top end # Take a list of AST resources, evaluate them, and return the results def assert_evaluate(children) top = nil assert_nothing_raised("Could not create top object") { top = AST::ASTArray.new( :children => children ) } trans = nil scope = nil assert_nothing_raised { scope = Puppet::Parser::Scope.new() trans = scope.evaluate(:ast => top) } return trans end end - -# $Id$ diff --git a/test/lib/puppettest/railstesting.rb b/test/lib/puppettest/railstesting.rb index 8b5f074a2..403bdb756 100644 --- a/test/lib/puppettest/railstesting.rb +++ b/test/lib/puppettest/railstesting.rb @@ -1,56 +1,54 @@ module PuppetTest::RailsTesting Parser = Puppet::Parser AST = Puppet::Parser::AST include PuppetTest::ParserTesting def teardown super # If we don't clean up the connection list, then the rails # lib will still think it's connected. if Puppet.features.rails? ActiveRecord::Base.clear_active_connections! end end def railsinit Puppet::Rails.init end def railsteardown if Puppet[:dbadapter] != "sqlite3" Puppet::Rails.teardown end end def railsresource(type = "file", title = "/tmp/testing", params = {}) railsteardown railsinit # We need a host for resources #host = Puppet::Rails::Host.new(:name => Facter.value("hostname")) # Now build a resource resources = [] resources << mkresource(:type => type, :title => title, :exported => true, :params => params) # Now collect our facts facts = Facter.to_hash # Now try storing our crap host = nil + node = mknode(facts["hostname"]) + node.parameters = facts assert_nothing_raised { - host = Puppet::Rails::Host.store( - :resources => resources, - :facts => facts, - :name => facts["hostname"] - ) + host = Puppet::Rails::Host.store(node, resources) } # Now save the whole thing host.save end end # $Id$ diff --git a/test/lib/puppettest/resourcetesting.rb b/test/lib/puppettest/resourcetesting.rb index 8cb59b83d..325602c22 100644 --- a/test/lib/puppettest/resourcetesting.rb +++ b/test/lib/puppettest/resourcetesting.rb @@ -1,73 +1,46 @@ module PuppetTest::ResourceTesting Parser = Puppet::Parser AST = Puppet::Parser::AST - def mkclassframing(interp = nil) - interp ||= mkinterp - interp.newdefine("resource", :arguments => [%w{one}, %w{two value}, %w{three}]) - interp.newclass("") - source = interp.newclass("base") - interp.newclass("sub1", :parent => "base") - interp.newclass("sub2", :parent => "base") - interp.newclass("other") - - scope = Parser::Scope.new(:interp => interp) - scope.source = source - - return interp, scope, source - end - - def mkevaltest(interp = nil) - interp ||= mkinterp - @interp.newdefine("evaltest", + def mkevaltest(parser = nil) + parser ||= mkparser + @parser.newdefine("evaltest", :arguments => [%w{one}, ["two", stringobj("755")]], :code => resourcedef("file", "/tmp", "owner" => varref("one"), "mode" => varref("two")) ) end def mkresource(args = {}) - - if args[:scope] and ! args[:source] - args[:source] = args[:scope].source - end - - unless args[:scope] - unless defined? @scope - raise "Must set @scope to mkresource" - end - end + args[:source] ||= "source" + args[:scope] ||= mkscope {:type => "resource", :title => "testing", - :source => @source, :scope => @scope}.each do |param, value| + :source => "source", :scope => "scope"}.each do |param, value| args[param] ||= value end - unless args[:source].is_a?(Puppet::Parser::AST::HostClass) - args[:source] = args[:scope].findclass(args[:source]) - end - params = args[:params] || {:one => "yay", :three => "rah"} if args[:params] == :none args.delete(:params) else args[:params] = paramify args[:source], params end Parser::Resource.new(args) end def param(name, value, source) Parser::Resource::Param.new(:name => name, :value => value, :source => source) end def paramify(source, hash) hash.collect do |name, value| Parser::Resource::Param.new( :name => name, :value => value, :source => source ) end end end # $Id$ diff --git a/test/lib/spec/callback.rb b/test/lib/spec/callback.rb deleted file mode 100644 index aa7ecec0f..000000000 --- a/test/lib/spec/callback.rb +++ /dev/null @@ -1,11 +0,0 @@ -require 'spec/callback/callback_container' -require 'spec/callback/extensions/module' -require 'spec/callback/extensions/object' - -# Callback is a fork of Brian Takita's "callback library" (see http://callback.rubyforge.org), -# which Brian graciously contributed to RSpec in order to avoid the dependency. -# -# RSpec uses Callback internally to create hooks to Spec::Runner events. If you're interested -# in a simple, powerful API for generating callback events, check out http://callback.rubyforge.org. -module Callback -end diff --git a/test/lib/spec/callback/callback_container.rb b/test/lib/spec/callback/callback_container.rb deleted file mode 100644 index 24d4c0ced..000000000 --- a/test/lib/spec/callback/callback_container.rb +++ /dev/null @@ -1,60 +0,0 @@ -module Callback - class CallbackContainer - def initialize - @callback_registry = Hash.new do |hash, key| - hash[key] = Array.new - end - end - - # Defines the callback with the key in this container. - def define(key, callback_proc=nil, &callback_block) - callback = extract_callback(callback_block, callback_proc) do - raise "You must define the callback that accepts the call method." - end - @callback_registry[key] << callback - callback - end - - # Undefines the callback with the key in this container. - def undefine(key, callback_proc) - callback = extract_callback(callback_proc) do - raise "You may only undefine callbacks that use the call method." - end - @callback_registry[key].delete callback - callback - end - - # Notifies the callbacks for the key. Arguments may be passed. - # An error handler may be passed in as a block. If there is an error, the block is called with - # error object as an argument. - # An array of the return values of the callbacks is returned. - def notify(key, *args, &error_handler) - @callback_registry[key].collect do |callback| - begin - callback.call(*args) - rescue Exception => e - yield(e) if error_handler - end - end - end - - # Clears all of the callbacks in this container. - def clear - @callback_registry.clear - end - - protected - def extract_callback(first_choice_callback, second_choice_callback = nil) - callback = nil - if first_choice_callback - callback = first_choice_callback - elsif second_choice_callback - callback = second_choice_callback - end - unless callback.respond_to? :call - yield - end - return callback - end - end -end diff --git a/test/lib/spec/callback/extensions/module.rb b/test/lib/spec/callback/extensions/module.rb deleted file mode 100644 index 429268ed1..000000000 --- a/test/lib/spec/callback/extensions/module.rb +++ /dev/null @@ -1,24 +0,0 @@ -module Callback - module ModuleMethods - # For each event_name submitted, defines a callback event with this name. - # Client code can then register as a callback listener using object.event_name. - def callback_events(*event_names) - event_names.each do |event_name| - define_callback_event(event_name) - end - end - - private - def define_callback_event(event_name) - module_eval <<-EOS - def #{event_name}(&block) - register_callback(:#{event_name}, &block) - end - EOS - end - end -end - -class Module - include Callback::ModuleMethods -end diff --git a/test/lib/spec/callback/extensions/object.rb b/test/lib/spec/callback/extensions/object.rb deleted file mode 100644 index c6ac6fd14..000000000 --- a/test/lib/spec/callback/extensions/object.rb +++ /dev/null @@ -1,37 +0,0 @@ -module Callback - module InstanceMethods - # Registers a callback for the event on the object. The callback can either be a block or a proc. - # When the callbacks are notified, the return value of the proc is passed to the caller. - def register_callback(event, callback_proc=nil, &callback_block) - callbacks.define(event, callback_proc, &callback_block) - end - - # Removes the callback from the event. The callback proc must be the same - # object as the one that was passed to register_callback. - def unregister_callback(event, callback_proc) - callbacks.undefine(event, callback_proc) - end - - protected - # Notifies the callbacks registered with the event on the object. Arguments can be passed to the callbacks. - # An error handler may be passed in as a block. If there is an error, the block is called with - # error object as an argument. - # An array of the return values of the callbacks is returned. - def notify_callbacks(event, *args, &error_handler) - callbacks.notify(event, *args, &error_handler) - end - - def notify_class_callbacks(event, *args, &error_handler) - self.class.send(:notify_callbacks, event, *args, &error_handler) - end - - # The CallbackContainer for this object. - def callbacks - @callbacks ||= CallbackContainer.new - end - end -end - -class Object - include Callback::InstanceMethods -end \ No newline at end of file diff --git a/test/lib/spec/deprecated.rb b/test/lib/spec/deprecated.rb deleted file mode 100644 index e9c1cd829..000000000 --- a/test/lib/spec/deprecated.rb +++ /dev/null @@ -1,3 +0,0 @@ -def deprecated(&block) - block.call unless ENV['RSPEC_DISABLE_DEPRECATED_FEATURES'] == 'true' -end diff --git a/test/lib/spec/expectations/extensions/object.rb b/test/lib/spec/expectations/extensions/object.rb deleted file mode 100644 index dd5498fdd..000000000 --- a/test/lib/spec/expectations/extensions/object.rb +++ /dev/null @@ -1,109 +0,0 @@ -module Spec - module Expectations - # rspec adds #should and #should_not to every Object (and, - # implicitly, every Class). - module ObjectExpectations - - # :call-seq: - # should(matcher) - # should == expected - # should =~ expected - # - # receiver.should(matcher) - # => Passes if matcher.matches?(receiver) - # - # receiver.should == expected #any value - # => Passes if (receiver == expected) - # - # receiver.should =~ regexp - # => Passes if (receiver =~ regexp) - # - # See Spec::Matchers for more information about matchers - # - # == Warning - # - # NOTE that this does NOT support receiver.should != expected. - # Instead, use receiver.should_not == expected - def should(matcher=nil, &block) - return ExpectationMatcherHandler.handle_matcher(self, matcher, &block) if matcher - Should::Should.new(self) - end - - # :call-seq: - # should_not(matcher) - # should_not == expected - # should_not =~ expected - # - # receiver.should_not(matcher) - # => Passes unless matcher.matches?(receiver) - # - # receiver.should_not == expected - # => Passes unless (receiver == expected) - # - # receiver.should_not =~ regexp - # => Passes unless (receiver =~ regexp) - # - # See Spec::Matchers for more information about matchers - def should_not(matcher=nil, &block) - return NegativeExpectationMatcherHandler.handle_matcher(self, matcher, &block) if matcher - should.not - end - - deprecated do - # Deprecated: use should have(n).items (see Spec::Matchers) - # This will be removed in 0.9 - def should_have(expected) - should.have(expected) - end - alias_method :should_have_exactly, :should_have - - # Deprecated: use should have_at_least(n).items (see Spec::Matchers) - # This will be removed in 0.9 - def should_have_at_least(expected) - should.have.at_least(expected) - end - - # Deprecated: use should have_at_most(n).items (see Spec::Matchers) - # This will be removed in 0.9 - def should_have_at_most(expected) - should.have.at_most(expected) - end - - # Deprecated: use should include(expected) (see Spec::Matchers) - # This will be removed in 0.9 - def should_include(expected) - should.include(expected) - end - - # Deprecated: use should_not include(expected) (see Spec::Matchers) - # This will be removed in 0.9 - def should_not_include(expected) - should.not.include(expected) - end - - # Deprecated: use should be(expected) (see Spec::Matchers) - # This will be removed in 0.9 - def should_be(expected = :___no_arg) - should.be(expected) - end - - # Deprecated: use should_not be(expected) (see Spec::Matchers) - # This will be removed in 0.9 - def should_not_be(expected = :___no_arg) - should_not.be(expected) - end - end - end - end -end - -class Object - include Spec::Expectations::ObjectExpectations - deprecated do - include Spec::Expectations::UnderscoreSugar - end -end - -deprecated do - Object.handle_underscores_for_rspec! -end \ No newline at end of file diff --git a/test/lib/spec/expectations/extensions/proc.rb b/test/lib/spec/expectations/extensions/proc.rb deleted file mode 100644 index 8286708ed..000000000 --- a/test/lib/spec/expectations/extensions/proc.rb +++ /dev/null @@ -1,57 +0,0 @@ -module Spec - module Expectations - module ProcExpectations - # Given a receiver and a message (Symbol), specifies that the result - # of sending that message that receiver should change after - # executing the proc. - # - # lambda { @team.add player }.should_change(@team.players, :size) - # lambda { @team.add player }.should_change(@team.players, :size).by(1) - # lambda { @team.add player }.should_change(@team.players, :size).to(23) - # lambda { @team.add player }.should_change(@team.players, :size).from(22).to(23) - # - # You can use a block instead of a message and receiver. - # - # lambda { @team.add player }.should_change{@team.players.size} - # lambda { @team.add player }.should_change{@team.players.size}.by(1) - # lambda { @team.add player }.should_change{@team.players.size}.to(23) - # lambda { @team.add player }.should_change{@team.players.size}.from(22).to(23) - def should_change(receiver=nil, message=nil, &block) - should.change(receiver, message, &block) - end - - # Given a receiver and a message (Symbol), specifies that the result - # of sending that message that receiver should NOT change after - # executing the proc. - # - # lambda { @team.add player }.should_not_change(@team.players, :size) - # - # You can use a block instead of a message and receiver. - # - # lambda { @team.add player }.should_not_change{@team.players.size} - def should_not_change(receiver, message) - should.not.change(receiver, message) - end - - def should_raise(exception=Exception, message=nil) - should.raise(exception, message) - end - - def should_not_raise(exception=Exception, message=nil) - should.not.raise(exception, message) - end - - def should_throw(symbol) - should.throw(symbol) - end - - def should_not_throw(symbol=:___this_is_a_symbol_that_will_likely_never_occur___) - should.not.throw(symbol) - end - end - end -end - -class Proc - include Spec::Expectations::ProcExpectations -end \ No newline at end of file diff --git a/test/lib/spec/expectations/handler.rb b/test/lib/spec/expectations/handler.rb deleted file mode 100644 index 9d3fd1f88..000000000 --- a/test/lib/spec/expectations/handler.rb +++ /dev/null @@ -1,47 +0,0 @@ -module Spec - module Expectations - - module MatcherHandlerHelper - def describe(matcher) - matcher.respond_to?(:description) ? matcher.description : "[#{matcher.class.name} does not provide a description]" - end - end - - class ExpectationMatcherHandler - class << self - include MatcherHandlerHelper - def handle_matcher(actual, matcher, &block) - unless matcher.nil? - match = matcher.matches?(actual, &block) - ::Spec::Matchers.generated_description = "should #{describe(matcher)}" - Spec::Expectations.fail_with(matcher.failure_message) unless match - end - end - end - end - - class NegativeExpectationMatcherHandler - class << self - include MatcherHandlerHelper - def handle_matcher(actual, matcher, &block) - unless matcher.nil? - unless matcher.respond_to?(:negative_failure_message) - Spec::Expectations.fail_with( - <<-EOF - Matcher does not support should_not. - See Spec::Matchers for more information - about matchers. - EOF - ) - end - match = matcher.matches?(actual, &block) - ::Spec::Matchers.generated_description = "should not #{describe(matcher)}" - Spec::Expectations.fail_with(matcher.negative_failure_message) if match - end - end - end - end - - end -end - diff --git a/test/lib/spec/expectations/should.rb b/test/lib/spec/expectations/should.rb deleted file mode 100644 index f64e6ff78..000000000 --- a/test/lib/spec/expectations/should.rb +++ /dev/null @@ -1,5 +0,0 @@ -require 'spec/expectations/should/base' -require 'spec/expectations/should/have' -require 'spec/expectations/should/not' -require 'spec/expectations/should/should' -require 'spec/expectations/should/change' diff --git a/test/lib/spec/expectations/should/base.rb b/test/lib/spec/expectations/should/base.rb deleted file mode 100755 index 1be4677e8..000000000 --- a/test/lib/spec/expectations/should/base.rb +++ /dev/null @@ -1,64 +0,0 @@ -module Spec - module Expectations - module Should - class Base - - #== and =~ will stay after the new syntax - def ==(expected) - __delegate_method_missing_to_target "==", "==", expected - end - - def =~(expected) - __delegate_method_missing_to_target "=~", "=~", expected - end - - #<, <=, >=, > are all implemented in Spec::Matchers::Be - # and will be removed with 0.9 - deprecated do - def <(expected) - __delegate_method_missing_to_target "<", "<", expected - end - - def <=(expected) - __delegate_method_missing_to_target "<=", "<=", expected - end - - def >=(expected) - __delegate_method_missing_to_target ">=", ">=", expected - end - - def >(expected) - __delegate_method_missing_to_target ">", ">", expected - end - end - - def default_message(expectation, expected=nil) - return "expected #{expected.inspect}, got #{@target.inspect} (using #{expectation})" if expectation == '==' - "expected #{expectation} #{expected.inspect}, got #{@target.inspect}" unless expectation == '==' - end - - def fail_with_message(message, expected=nil, target=nil) - Spec::Expectations.fail_with(message, expected, target) - end - - def find_supported_sym(original_sym) - ["#{original_sym}?", "#{original_sym}s?"].each do |alternate_sym| - return alternate_sym.to_s if @target.respond_to?(alternate_sym.to_s) - end - end - - deprecated do - def method_missing(original_sym, *args, &block) - if original_sym.to_s =~ /^not_/ - return Not.new(@target).__send__(sym, *args, &block) - end - if original_sym.to_s =~ /^have_/ - return have.__send__(original_sym.to_s[5..-1].to_sym, *args, &block) - end - __delegate_method_missing_to_target original_sym, find_supported_sym(original_sym), *args - end - end - end - end - end -end diff --git a/test/lib/spec/expectations/should/change.rb b/test/lib/spec/expectations/should/change.rb deleted file mode 100644 index 98304f1b3..000000000 --- a/test/lib/spec/expectations/should/change.rb +++ /dev/null @@ -1,69 +0,0 @@ -module Spec - module Expectations - module Should - class Change < Base - - def initialize(target, receiver=nil, message=nil, &block) - @block = block - @target = target - @receiver = receiver - @message = message - execute_change - evaluate_change - end - - def execute_change - @before_change = @block.nil? ? @receiver.send(@message) : @block.call - @target.call - @after_change = @block.nil? ? @receiver.send(@message) : @block.call - end - - def message - @message.nil? ? 'result' : @message - end - - def evaluate_change - if @before_change == @after_change - fail_with_message "#{message} should have changed, but is still #{@after_change.inspect}" - end - end - - def from(value) - if @before_change != value - fail_with_message "#{message} should have initially been #{value.inspect}, but was #{@before_change.inspect}" - end - self - end - - def to(value) - if @after_change != value - fail_with_message "#{message} should have been changed to #{value.inspect}, but is now #{@after_change.inspect}" - end - self - end - - def by(expected_delta) - if actual_delta != expected_delta - fail_with_message "#{message} should have been changed by #{expected_delta}, but was changed by #{actual_delta}" - end - self - end - - private - def actual_delta - @after_change - @before_change - end - end - - class NotChange < Change - def evaluate_change - if @before_change != @after_change - fail_with_message "#{@message} should not have changed, but did change from #{@before_change.inspect} to #{@after_change.inspect}" - end - end - end - - end - end -end - diff --git a/test/lib/spec/expectations/should/have.rb b/test/lib/spec/expectations/should/have.rb deleted file mode 100644 index 47ebe81db..000000000 --- a/test/lib/spec/expectations/should/have.rb +++ /dev/null @@ -1,128 +0,0 @@ -module Spec - module Expectations - module Should - class Have - def initialize(target, relativity=:exactly, expected=nil) - @target = target - init_collection_handler(target, relativity, expected) - init_item_handler(target) - end - - def init_collection_handler(target, relativity, expected) - @collection_handler = CollectionHandler.new(target, relativity, expected) - end - - def init_item_handler(target) - @item_handler = PositiveItemHandler.new(target) - end - - def method_missing(sym, *args) - if @collection_handler.wants_to_handle(sym) - @collection_handler.handle_message(sym, *args) - elsif @item_handler.wants_to_handle(sym) - @item_handler.handle_message(sym, *args) - else - Spec::Expectations.fail_with("target does not respond to #has_#{sym}?") - end - end - end - - class NotHave < Have - def init_item_handler(target) - @item_handler = NegativeItemHandler.new(target) - end - end - - class CollectionHandler - def initialize(target, relativity=:exactly, expected=nil) - @target = target - @expected = expected == :no ? 0 : expected - @at_least = (relativity == :at_least) - @at_most = (relativity == :at_most) - end - - def at_least(expected_number=nil) - @at_least = true - @at_most = false - @expected = expected_number == :no ? 0 : expected_number - self - end - - def at_most(expected_number=nil) - @at_least = false - @at_most = true - @expected = expected_number == :no ? 0 : expected_number - self - end - - def method_missing(sym, *args) - if @target.respond_to?(sym) - handle_message(sym, *args) - end - end - - def wants_to_handle(sym) - respond_to?(sym) || @target.respond_to?(sym) - end - - def handle_message(sym, *args) - return at_least(args[0]) if sym == :at_least - return at_most(args[0]) if sym == :at_most - Spec::Expectations.fail_with(build_message(sym, args)) unless as_specified?(sym, args) - end - - def build_message(sym, args) - message = "expected" - message += " at least" if @at_least - message += " at most" if @at_most - message += " #{@expected} #{sym}, got #{actual_size_of(collection(sym, args))}" - end - - def as_specified?(sym, args) - return actual_size_of(collection(sym, args)) >= @expected if @at_least - return actual_size_of(collection(sym, args)) <= @expected if @at_most - return actual_size_of(collection(sym, args)) == @expected - end - - def collection(sym, args) - @target.send(sym, *args) - end - - def actual_size_of(collection) - return collection.length if collection.respond_to? :length - return collection.size if collection.respond_to? :size - end - end - - class ItemHandler - def wants_to_handle(sym) - @target.respond_to?("has_#{sym}?") - end - - def initialize(target) - @target = target - end - - def fail_with(message) - Spec::Expectations.fail_with(message) - end - end - - class PositiveItemHandler < ItemHandler - def handle_message(sym, *args) - fail_with( - "expected #has_#{sym}?(#{args.collect{|arg| arg.inspect}.join(', ')}) to return true, got false" - ) unless @target.send("has_#{sym}?", *args) - end - end - - class NegativeItemHandler < ItemHandler - def handle_message(sym, *args) - fail_with( - "expected #has_#{sym}?(#{args.collect{|arg| arg.inspect}.join(', ')}) to return false, got true" - ) if @target.send("has_#{sym}?", *args) - end - end - end - end -end diff --git a/test/lib/spec/expectations/should/not.rb b/test/lib/spec/expectations/should/not.rb deleted file mode 100755 index 5ad530be6..000000000 --- a/test/lib/spec/expectations/should/not.rb +++ /dev/null @@ -1,74 +0,0 @@ -module Spec - module Expectations - module Should - - class Not < Base #:nodoc: - def initialize(target) - @target = target - @be_seen = false - end - - deprecated do - #Gone for 0.9 - def be(expected = :___no_arg) - @be_seen = true - return self if (expected == :___no_arg) - fail_with_message(default_message("should not be", expected)) if (@target.equal?(expected)) - end - - #Gone for 0.9 - def have(expected_number=nil) - NotHave.new(@target, :exactly, expected_number) - end - - #Gone for 0.9 - def change(receiver, message) - NotChange.new(@target, receiver, message) - end - - #Gone for 0.9 - def raise(exception=Exception, message=nil) - begin - @target.call - rescue exception => e - return unless message.nil? || e.message == message || (message.is_a?(Regexp) && e.message =~ message) - if e.kind_of?(exception) - failure_message = "expected no " - failure_message << exception.to_s - unless message.nil? - failure_message << " with " - failure_message << "message matching " if message.is_a?(Regexp) - failure_message << message.inspect - end - failure_message << ", got " << e.inspect - fail_with_message(failure_message) - end - rescue - true - end - end - - #Gone for 0.9 - def throw(symbol=:___this_is_a_symbol_that_will_likely_never_occur___) - begin - catch symbol do - @target.call - return true - end - fail_with_message("expected #{symbol.inspect} not to be thrown, but it was") - rescue NameError - true - end - end - - def __delegate_method_missing_to_target original_sym, actual_sym, *args - ::Spec::Matchers.generated_description = "should not #{original_sym} #{args[0].inspect}" - return unless @target.__send__(actual_sym, *args) - fail_with_message(default_message("not #{original_sym}", args[0])) - end - end - end - - end - end -end diff --git a/test/lib/spec/expectations/should/should.rb b/test/lib/spec/expectations/should/should.rb deleted file mode 100755 index cb9f3c4ce..000000000 --- a/test/lib/spec/expectations/should/should.rb +++ /dev/null @@ -1,81 +0,0 @@ -module Spec - module Expectations - module Should # :nodoc: - - class Should < Base - - def initialize(target, expectation=nil) - @target = target - @be_seen = false - end - - deprecated do - #Gone for 0.9 - def not - Not.new(@target) - end - - #Gone for 0.9 - def be(expected = :___no_arg) - @be_seen = true - return self if (expected == :___no_arg) - if Symbol === expected - fail_with_message(default_message("should be", expected)) unless (@target.equal?(expected)) - else - fail_with_message("expected #{expected}, got #{@target} (using .equal?)") unless (@target.equal?(expected)) - end - end - - #Gone for 0.9 - def have(expected_number=nil) - Have.new(@target, :exactly, expected_number) - end - - #Gone for 0.9 - def change(receiver=nil, message=nil, &block) - Change.new(@target, receiver, message, &block) - end - - #Gone for 0.9 - def raise(exception=Exception, message=nil) - begin - @target.call - rescue exception => e - unless message.nil? - if message.is_a?(Regexp) - e.message.should =~ message - else - e.message.should == message - end - end - return - rescue => e - fail_with_message("expected #{exception}#{message.nil? ? "" : " with #{message.inspect}"}, got #{e.inspect}") - end - fail_with_message("expected #{exception}#{message.nil? ? "" : " with #{message.inspect}"} but nothing was raised") - end - - #Gone for 0.9 - def throw(symbol) - begin - catch symbol do - @target.call - fail_with_message("expected #{symbol.inspect} to be thrown, but nothing was thrown") - end - rescue NameError => e - fail_with_message("expected #{symbol.inspect} to be thrown, got #{e.inspect}") - end - end - end - - private - def __delegate_method_missing_to_target(original_sym, actual_sym, *args) - ::Spec::Matchers.generated_description = "should #{original_sym} #{args[0].inspect}" - return if @target.send(actual_sym, *args) - fail_with_message(default_message(original_sym, args[0]), args[0], @target) - end - end - - end - end -end diff --git a/test/lib/spec/expectations/sugar.rb b/test/lib/spec/expectations/sugar.rb deleted file mode 100644 index 906111f0e..000000000 --- a/test/lib/spec/expectations/sugar.rb +++ /dev/null @@ -1,47 +0,0 @@ -deprecated do -module Spec - module Expectations - # This module adds syntactic sugar that allows usage of should_* instead of should.* - module UnderscoreSugar - def handle_underscores_for_rspec! # :nodoc: - original_method_missing = instance_method(:method_missing) - class_eval do - def method_missing(sym, *args, &block) - _method_missing(sym, args, block) - end - - define_method :_method_missing do |sym, args, block| - return original_method_missing.bind(self).call(sym, *args, &block) unless sym.to_s =~ /^should_/ - if sym.to_s =~ /^should_not_/ - if __matcher.respond_to?(__strip_should_not(sym)) - return should_not(__matcher.__send__(__strip_should_not(sym), *args, &block)) - else - return Spec::Expectations::Should::Not.new(self).__send__(__strip_should_not(sym), *args, &block) if sym.to_s =~ /^should_not_/ - end - else - if __matcher.respond_to?(__strip_should(sym)) - return should(__matcher.__send__(__strip_should(sym), *args, &block)) - else - return Spec::Expectations::Should::Should.new(self).__send__(__strip_should(sym), *args, &block) - end - end - end - - def __strip_should(sym) # :nodoc - sym.to_s[7..-1] - end - - def __strip_should_not(sym) # :nodoc - sym.to_s[11..-1] - end - - def __matcher - @matcher ||= Spec::Matchers::Matcher.new - end - end - end - end - end -end - -end \ No newline at end of file diff --git a/test/lib/spec/matchers/include.rb b/test/lib/spec/matchers/include.rb deleted file mode 100644 index 0d387f323..000000000 --- a/test/lib/spec/matchers/include.rb +++ /dev/null @@ -1,50 +0,0 @@ -module Spec - module Matchers - - class Include #:nodoc: - - def initialize(expected) - @expected = expected - end - - def matches?(actual) - @actual = actual - actual.include?(@expected) - end - - def failure_message - _message - end - - def negative_failure_message - _message("not ") - end - - def description - "include #{@expected.inspect}" - end - - private - def _message(maybe_not="") - "expected #{@actual.inspect} #{maybe_not}to include #{@expected.inspect}" - end - end - - # :call-seq: - # should include(expected) - # should_not include(expected) - # - # Passes if actual includes expected. This works for - # collections and Strings - # - # == Examples - # - # [1,2,3].should include(3) - # [1,2,3].should_not include(4) - # "spread".should include("read") - # "spread".should_not include("red") - def include(expected) - Matchers::Include.new(expected) - end - end -end diff --git a/test/lib/spec/matchers/respond_to.rb b/test/lib/spec/matchers/respond_to.rb deleted file mode 100644 index 013a36f1d..000000000 --- a/test/lib/spec/matchers/respond_to.rb +++ /dev/null @@ -1,35 +0,0 @@ -module Spec - module Matchers - - class RespondTo #:nodoc: - def initialize(sym) - @sym = sym - end - - def matches?(target) - return target.respond_to?(@sym) - end - - def failure_message - "expected target to respond to #{@sym.inspect}" - end - - def negative_failure_message - "expected target not to respond to #{@sym.inspect}" - end - - def description - "respond to ##{@sym.to_s}" - end - end - - # :call-seq: - # should respond_to(:sym) - # should_not respond_to(:sym) - # - # Matches if the target object responds to :sym - def respond_to(sym) - Matchers::RespondTo.new(sym) - end - end -end diff --git a/test/lib/spec/mocks/methods.rb b/test/lib/spec/mocks/methods.rb deleted file mode 100644 index a5f102fcf..000000000 --- a/test/lib/spec/mocks/methods.rb +++ /dev/null @@ -1,40 +0,0 @@ -module Spec - module Mocks - module Methods - def should_receive(sym, opts={}, &block) - __mock_handler.add_message_expectation(opts[:expected_from] || caller(1)[0], sym, opts, &block) - end - - def should_not_receive(sym, &block) - __mock_handler.add_negative_message_expectation(caller(1)[0], sym, &block) - end - - def stub!(sym) - __mock_handler.add_stub(caller(1)[0], sym) - end - - def received_message?(sym, *args, &block) #:nodoc: - __mock_handler.received_message?(sym, *args, &block) - end - - def __verify #:nodoc: - __mock_handler.verify - end - - def __reset_mock #:nodoc: - __mock_handler.reset - end - - def method_missing(sym, *args, &block) #:nodoc: - __mock_handler.instance_eval {@messages_received << [sym, args, block]} - super(sym, *args, &block) - end - - private - - def __mock_handler - @mock_handler ||= MockHandler.new(self, @name, @options) - end - end - end -end \ No newline at end of file diff --git a/test/lib/spec/rake/spectask.rb b/test/lib/spec/rake/spectask.rb deleted file mode 100644 index 5c9b365c1..000000000 --- a/test/lib/spec/rake/spectask.rb +++ /dev/null @@ -1,173 +0,0 @@ -#!/usr/bin/env ruby - -# Define a task library for running RSpec contexts. - -require 'rake' -require 'rake/tasklib' - -module Spec - module Rake - - # A Rake task that runs a set of RSpec contexts. - # - # Example: - # - # Spec::Rake::SpecTask.new do |t| - # t.warning = true - # t.rcov = true - # end - # - # This will create a task that can be run with: - # - # rake spec - # - class SpecTask < ::Rake::TaskLib - - # Name of spec task. (default is :spec) - attr_accessor :name - - # Array of directories to be added to $LOAD_PATH before running the - # specs. Defaults to [''] - attr_accessor :libs - - # If true, requests that the specs be run with the warning flag set. - # E.g. warning=true implies "ruby -w" used to run the specs. Defaults to false. - attr_accessor :warning - - # Glob pattern to match spec files. (default is 'spec/**/*_spec.rb') - attr_accessor :pattern - - # Array of commandline options to pass to RSpec. Defaults to []. - attr_accessor :spec_opts - - # Where RSpec's output is written. Defaults to STDOUT. - attr_accessor :out - - # Whether or not to use RCov (default is false) - # See http://eigenclass.org/hiki.rb?rcov - attr_accessor :rcov - - # Array of commandline options to pass to RCov. Defaults to ['--exclude', 'lib\/spec,bin\/spec']. - # Ignored if rcov=false - attr_accessor :rcov_opts - - # Directory where the RCov report is written. Defaults to "coverage" - # Ignored if rcov=false - attr_accessor :rcov_dir - - # Array of commandline options to pass to ruby. Defaults to []. - attr_accessor :ruby_opts - - # Whether or not to fail Rake when an error occurs (typically when specs fail). - # Defaults to true. - attr_accessor :fail_on_error - - # A message to print to stdout when there are failures. - attr_accessor :failure_message - - # Explicitly define the list of spec files to be included in a - # spec. +list+ is expected to be an array of file names (a - # FileList is acceptable). If both +pattern+ and +spec_files+ are - # used, then the list of spec files is the union of the two. - def spec_files=(list) - @spec_files = list - end - - # Create a specing task. - def initialize(name=:spec) - @name = name - @libs = [File.expand_path(File.dirname(__FILE__) + '/../../../lib')] - @pattern = nil - @spec_files = nil - @spec_opts = [] - @warning = false - @ruby_opts = [] - @out = nil - @fail_on_error = true - @rcov = false - @rcov_opts = ['--exclude', 'lib\/spec,bin\/spec,config\/boot.rb'] - @rcov_dir = "coverage" - - yield self if block_given? - @pattern = 'spec/**/*_spec.rb' if @pattern.nil? && @spec_files.nil? - define - end - - def define - spec_script = File.expand_path(File.dirname(__FILE__) + '/../../../bin/spec') - - lib_path = @libs.join(File::PATH_SEPARATOR) - actual_name = Hash === name ? name.keys.first : name - unless ::Rake.application.last_comment - desc "Run RSpec for #{actual_name}" + (@rcov ? " using RCov" : "") - end - task @name do - RakeFileUtils.verbose(@verbose) do - ruby_opts = @ruby_opts.clone - ruby_opts.push( "-I\"#{lib_path}\"" ) - ruby_opts.push( "-S rcov" ) if @rcov - ruby_opts.push( "-w" ) if @warning - - redirect = @out.nil? ? "" : " > \"#{@out}\"" - - unless spec_file_list.empty? - # ruby [ruby_opts] -Ilib -S rcov [rcov_opts] bin/spec -- [spec_opts] examples - # or - # ruby [ruby_opts] -Ilib bin/spec [spec_opts] examples - begin - ruby( - ruby_opts.join(" ") + " " + - rcov_option_list + - (@rcov ? %[ -o "#{@rcov_dir}" ] : "") + - '"' + spec_script + '"' + " " + - (@rcov ? "-- " : "") + - spec_file_list.collect { |fn| %["#{fn}"] }.join(' ') + " " + - spec_option_list + " " + - redirect - ) - rescue => e - puts @failure_message if @failure_message - raise e if @fail_on_error - end - end - end - end - - if @rcov - desc "Remove rcov products for #{actual_name}" - task paste("clobber_", actual_name) do - rm_r @rcov_dir rescue nil - end - - clobber_task = paste("clobber_", actual_name) - task :clobber => [clobber_task] - - task actual_name => clobber_task - end - self - end - - def rcov_option_list # :nodoc: - return "" unless @rcov - ENV['RCOVOPTS'] || @rcov_opts.join(" ") || "" - end - - def spec_option_list # :nodoc: - ENV['RSPECOPTS'] || @spec_opts.join(" ") || "" - end - - def spec_file_list # :nodoc: - if ENV['SPEC'] - FileList[ ENV['SPEC'] ] - else - result = [] - result += @spec_files.to_a if @spec_files - result += FileList[ @pattern ].to_a if @pattern - FileList[result] - end - end - - end - end -end - diff --git a/test/lib/spec/runner.rb b/test/lib/spec/runner.rb deleted file mode 100644 index 976802bd1..000000000 --- a/test/lib/spec/runner.rb +++ /dev/null @@ -1,132 +0,0 @@ -require 'spec/runner/formatter' -require 'spec/runner/context' -require 'spec/runner/context_eval' -require 'spec/runner/specification' -require 'spec/runner/execution_context' -require 'spec/runner/context_runner' -require 'spec/runner/option_parser' -require 'spec/runner/command_line' -require 'spec/runner/drb_command_line' -require 'spec/runner/backtrace_tweaker' -require 'spec/runner/reporter' -require 'spec/runner/spec_matcher' -require 'spec/runner/extensions/object' -require 'spec/runner/extensions/kernel' -require 'spec/runner/spec_should_raise_handler' -require 'spec/runner/spec_parser' - -module Spec - # == Contexts and Specifications - # - # Rather than expressing examples in classes, RSpec uses a custom domain specific language to express - # examples using contexts and specifications. - # - # A context is the equivalent of a fixture in xUnit-speak. It is a metaphor for the context - # in which you will run your executable example - a set of known objects in a known starting state. - # - # context "A new account" do - # - # setup do - # @account = Account.new - # end - # - # specify "should have a balance of $0" do - # @account.balance.should_eql Money.new(0, :dollars) - # end - # - # end - # - # We use the setup block to set up the context (given), and then the specify method to - # hold the example code that expresses the event (when) and the expected outcome (then). - # - # == Helper Methods - # - # A primary goal of RSpec is to keep the examples clear. We therefore prefer - # less indirection than you might see in xUnit examples and in well factored, DRY production code. We feel - # that duplication is OK if removing it makes it harder to understand an example without - # having to look elsewhere to understand its context. - # - # That said, RSpec does support some level of encapsulating common code in helper - # methods that can exist within a context or within an included module. - # - # == Setup and Teardown - # - # You can use setup, teardown, context_setup and context_teardown within a context: - # - # context "..." do - # context_setup do - # ... - # end - # - # setup do - # ... - # end - # - # specify "number one" do - # ... - # end - # - # specify "number two" do - # ... - # end - # - # teardown do - # ... - # end - # - # context_teardown do - # ... - # end - # - # end - # - # The setup block will run before each of the specs, once for each spec. Likewise, - # the teardown block will run after each of the specs. - # - # It is also possible to specify a context_setup and context_teardown - # block that will run only once for each context, respectively before the first setup - # and after the last teardown. The use of these is generally discouraged, because it - # introduces dependencies between the specs. Still, it might prove useful for very expensive operations - # if you know what you are doing. - # - # == Local helper methods - # - # You can include local helper methods by simply expressing them within a context: - # - # context "..." do - # - # specify "..." do - # helper_method - # end - # - # def helper_method - # ... - # end - # - # end - # - # == Included helper methods - # - # You can include helper methods in multiple contexts by expressing them within - # a module, and then including that module in your context: - # - # module AccountExampleHelperMethods - # def helper_method - # ... - # end - # end - # - # context "A new account" do - # include AccountExampleHelperMethods - # setup do - # @account = Account.new - # end - # - # specify "should have a balance of $0" do - # helper_method - # @account.balance.should eql(Money.new(0, :dollars)) - # end - # end - module Runner - end -end diff --git a/test/lib/spec/runner/command_line.rb b/test/lib/spec/runner/command_line.rb deleted file mode 100644 index db928ad9b..000000000 --- a/test/lib/spec/runner/command_line.rb +++ /dev/null @@ -1,34 +0,0 @@ -require 'spec/runner/option_parser' - -module Spec - module Runner - # Facade to run specs without having to fork a new ruby process (using `spec ...`) - class CommandLine - # Runs specs. +argv+ is the commandline args as per the spec commandline API, +err+ - # and +out+ are the streams output will be written to. +exit+ tells whether or - # not a system exit should be called after the specs are run and - # +warn_if_no_files+ tells whether or not a warning (the help message) - # should be printed to +err+ in case no files are specified. - def self.run(argv, err, out, exit=true, warn_if_no_files=true) - old_context_runner = defined?($context_runner) ? $context_runner : nil - $context_runner = OptionParser.new.create_context_runner(argv, err, out, warn_if_no_files) - return if $context_runner.nil? # This is the case if we use --drb - - # If ARGV is a glob, it will actually each over each one of the matching files. - argv.each do |file_or_dir| - if File.directory?(file_or_dir) - Dir["#{file_or_dir}/**/*.rb"].each do |file| - load file - end - elsif File.file?(file_or_dir) - load file_or_dir - else - raise "File or directory not found: #{file_or_dir}" - end - end - $context_runner.run(exit) - $context_runner = old_context_runner - end - end - end -end \ No newline at end of file diff --git a/test/lib/spec/runner/context.rb b/test/lib/spec/runner/context.rb deleted file mode 100644 index 3155e169a..000000000 --- a/test/lib/spec/runner/context.rb +++ /dev/null @@ -1,154 +0,0 @@ -module Spec - module Runner - class ContextEvalModule < Module - end - class Context - module InstanceMethods - def initialize(description, &context_block) - @description = description - - @context_eval_module = ContextEvalModule.new - @context_eval_module.extend ContextEval::ModuleMethods - @context_eval_module.include ContextEval::InstanceMethods - before_context_eval - @context_eval_module.class_eval(&context_block) - end - - def before_context_eval - end - - def inherit_context_eval_module_from(klass) - @context_eval_module.inherit klass - end - alias :inherit :inherit_context_eval_module_from - - def include(mod) - @context_eval_module.include(mod) - end - - def run(reporter, dry_run=false) - reporter.add_context(@description) - prepare_execution_context_class - errors = run_context_setup(reporter, dry_run) - - specifications.each do |specification| - specification_execution_context = execution_context(specification) - specification_execution_context.copy_instance_variables_from(@once_only_execution_context_instance, []) unless context_setup_block.nil? - specification.run(reporter, setup_block, teardown_block, dry_run, specification_execution_context) - end unless errors.length > 0 - - run_context_teardown(reporter, dry_run) - end - - def number_of_specs - specifications.length - end - - def matches?(full_description) - matcher ||= SpecMatcher.new(@description) - specifications.each do |spec| - return true if spec.matches?(matcher, full_description) - end - return false - end - - def run_single_spec(full_description) - return if @description == full_description - matcher = SpecMatcher.new(@description) - specifications.reject! do |spec| - !spec.matches?(matcher, full_description) - end - end - - def methods - my_methods = super - my_methods |= @context_eval_module.methods - my_methods - end - - protected - - def method_missing(*args) - @context_eval_module.method_missing(*args) - end - - def context_setup_block - @context_eval_module.send :context_setup_block - end - - def context_teardown_block - @context_eval_module.send :context_teardown_block - end - - def specifications - @context_eval_module.send :specifications - end - - def setup_block - @context_eval_module.send :setup_block - end - - def teardown_block - @context_eval_module.send :teardown_block - end - - def prepare_execution_context_class - weave_in_context_modules - execution_context_class - end - - def weave_in_context_modules - mods = context_modules - context_eval_module = @context_eval_module - execution_context_class.class_eval do - include context_eval_module - mods.each do |mod| - include mod - end - end - end - - def context_modules - @context_eval_module.send :context_modules - end - - def execution_context_class - @context_eval_module.send :execution_context_class - end - - def execution_context specification - execution_context_class.new(specification) - end - - def run_context_setup(reporter, dry_run) - errors = [] - unless dry_run - begin - @once_only_execution_context_instance = execution_context(nil) - @once_only_execution_context_instance.instance_eval(&context_setup_block) - rescue => e - errors << e - location = "context_setup" - reporter.spec_finished(location, e, location) if reporter - end - end - errors - end - - def run_context_teardown(reporter, dry_run) - unless dry_run - begin - @once_only_execution_context_instance ||= execution_context(nil) - @once_only_execution_context_instance.instance_eval(&context_teardown_block) - rescue => e - location = "context_teardown" - reporter.spec_finished(location, e, location) if reporter - end - end - end - - end - include InstanceMethods - end - end -end diff --git a/test/lib/spec/runner/context_eval.rb b/test/lib/spec/runner/context_eval.rb deleted file mode 100644 index 2cee8f1cd..000000000 --- a/test/lib/spec/runner/context_eval.rb +++ /dev/null @@ -1,142 +0,0 @@ -module Spec - module Runner - module ContextEval - module ModuleMethods - def inherit(klass) - @context_superclass = klass - derive_execution_context_class_from_context_superclass - end - - def include(mod) - context_modules << mod - mod.send :included, self - end - - def context_setup(&block) - context_setup_parts << block - end - - def context_teardown(&block) - context_teardown_parts << block - end - - def setup(&block) - setup_parts << block - end - - def teardown(&block) - teardown_parts << block - end - - def specify(spec_name=:__generate_description, opts={}, &block) - specifications << Specification.new(spec_name, opts, &block) - end - - def methods - my_methods = super - my_methods |= context_superclass.methods - my_methods - end - - protected - - def method_missing(method_name, *args) - if context_superclass.respond_to?(method_name) - return execution_context_class.send(method_name, *args) - end - super - end - - private - - def context_setup_block - parts = context_setup_parts.dup - add_context_superclass_method(:context_setup, parts) - create_block_from_parts(parts) - end - - def context_teardown_block - parts = context_teardown_parts.dup - add_context_superclass_method(:context_teardown, parts) - create_block_from_parts(parts) - end - - def setup_block - parts = setup_parts.dup - add_context_superclass_method(:setup, parts) - create_block_from_parts(parts) - end - - def teardown_block - parts = teardown_parts.dup - add_context_superclass_method(:teardown, parts) - create_block_from_parts(parts) - end - - def execution_context_class - @execution_context_class ||= derive_execution_context_class_from_context_superclass - end - - def derive_execution_context_class_from_context_superclass - @execution_context_class = Class.new(context_superclass) - @execution_context_class.class_eval do - include ::Spec::Runner::ExecutionContext::InstanceMethods - end - end - - def context_superclass - @context_superclass ||= Object - end - - def context_modules - @context_modules ||= [Spec::Matchers, Spec::Mocks] - end - - def specifications - @specifications ||= [] - end - - def context_setup_parts - @context_setup_parts ||= [] - end - - def context_teardown_parts - @context_teardown_parts ||= [] - end - - def setup_parts - @setup_parts ||= [] - end - - def teardown_parts - @teardown_parts ||= [] - end - - def add_context_superclass_method sym, parts - superclass_method = begin - context_superclass.instance_method(sym) - rescue - nil - end - parts.unshift superclass_method if superclass_method - end - - def create_block_from_parts(parts) - proc do - parts.each do |part| - if part.is_a?(UnboundMethod) - part.bind(self).call - else - instance_eval(&part) - end - end - end - end - end - - module InstanceMethods - end - - end - end -end diff --git a/test/lib/spec/runner/context_runner.rb b/test/lib/spec/runner/context_runner.rb deleted file mode 100644 index 0a4d7e6e9..000000000 --- a/test/lib/spec/runner/context_runner.rb +++ /dev/null @@ -1,55 +0,0 @@ -module Spec - module Runner - class ContextRunner - - def initialize(options) - @contexts = [] - @options = options - end - - def add_context(context) - return unless spec_description.nil? || context.matches?(spec_description) - context.run_single_spec(spec_description) if context.matches?(spec_description) - @contexts << context - end - - # Runs all contexts and returns the number of failures. - def run(exit_when_done) - @options.reporter.start(number_of_specs) - begin - @contexts.each do |context| - context.run(@options.reporter, @options.dry_run) - end - rescue Interrupt - ensure - @options.reporter.end - end - failure_count = @options.reporter.dump - - if(failure_count == 0 && !@options.heckle_runner.nil?) - heckle_runner = @options.heckle_runner - @options.heckle_runner = nil - context_runner = self.class.new(@options) - context_runner.instance_variable_set(:@contexts, @contexts) - heckle_runner.heckle_with(context_runner) - end - - if(exit_when_done) - exit_code = (failure_count == 0) ? 0 : 1 - exit(exit_code) - end - failure_count - end - - def number_of_specs - @contexts.inject(0) {|sum, context| sum + context.number_of_specs} - end - - private - def spec_description - @options.spec_name - end - - end - end -end diff --git a/test/lib/spec/runner/execution_context.rb b/test/lib/spec/runner/execution_context.rb deleted file mode 100644 index 484c55830..000000000 --- a/test/lib/spec/runner/execution_context.rb +++ /dev/null @@ -1,17 +0,0 @@ -module Spec - module Runner - class ExecutionContext - module InstanceMethods - def initialize(*args) #:nodoc: - #necessary for RSpec's own specs - end - - def violated(message="") - raise Spec::Expectations::ExpectationNotMetError.new(message) - end - - end - include InstanceMethods - end - end -end \ No newline at end of file diff --git a/test/lib/spec/runner/extensions/kernel.rb b/test/lib/spec/runner/extensions/kernel.rb deleted file mode 100644 index f060ec859..000000000 --- a/test/lib/spec/runner/extensions/kernel.rb +++ /dev/null @@ -1,17 +0,0 @@ -module Kernel - def context(name, &block) - context = Spec::Runner::Context.new(name, &block) - context_runner.add_context(context) - end - -private - - def context_runner - # TODO: Figure out a better way to get this considered "covered" and keep this statement on multiple lines - unless $context_runner; \ - $context_runner = ::Spec::Runner::OptionParser.new.create_context_runner(ARGV.dup, STDERR, STDOUT, false); \ - at_exit { $context_runner.run(false) }; \ - end - $context_runner - end -end diff --git a/test/lib/spec/runner/formatter/base_text_formatter.rb b/test/lib/spec/runner/formatter/base_text_formatter.rb deleted file mode 100644 index 31d1c3132..000000000 --- a/test/lib/spec/runner/formatter/base_text_formatter.rb +++ /dev/null @@ -1,118 +0,0 @@ -module Spec - module Runner - module Formatter - # Baseclass for text-based formatters. Can in fact be used for - # non-text based ones too - just ignore the +output+ constructor - # argument. - class BaseTextFormatter - def initialize(output, dry_run=false, colour=false) - @output = output - @dry_run = dry_run - @colour = colour - begin ; require 'Win32/Console/ANSI' if @colour && PLATFORM =~ /win32/ ; rescue LoadError ; raise "You must gem install win32console to use colour on Windows" ; end - end - - # This method is invoked before any specs are run, right after - # they have all been collected. This can be useful for special - # formatters that need to provide progress on feedback (graphical ones) - # - # This method will only be invoked once, and the next one to be invoked - # is #add_context - def start(spec_count) - end - - # This method is invoked at the beginning of the execution of each context. - # +name+ is the name of the context and +first+ is true if it is the - # first context - otherwise it's false. - # - # The next method to be invoked after this is #spec_started - def add_context(name, first) - end - - # This method is invoked right before a spec is executed. - # The next method to be invoked after this one is one of #spec_failed - # or #spec_passed. - def spec_started(name) - end - - # This method is invoked when a spec fails, i.e. an exception occurred - # inside it (such as a failed should or other exception). +name+ is the name - # of the specification. +counter+ is the sequence number of the failure - # (starting at 1) and +failure+ is the associated Failure object. - def spec_failed(name, counter, failure) - end - - # This method is invoked when a spec passes. +name+ is the name of the - # specification. - def spec_passed(name) - end - - # This method is invoked after all of the specs have executed. The next method - # to be invoked after this one is #dump_failure (once for each failed spec), - def start_dump - end - - # Dumps detailed information about a spec failure. - # This method is invoked for each failed spec after all specs have run. +counter+ is the sequence number - # of the associated spec. +failure+ is a Failure object, which contains detailed - # information about the failure. - def dump_failure(counter, failure) - @output.puts - @output.puts "#{counter.to_s})" - if(failure.expectation_not_met?) - @output.puts red(failure.header) - @output.puts red(failure.exception.message) - else - @output.puts magenta(failure.header) - @output.puts magenta(failure.exception.message) - end - @output.puts format_backtrace(failure.exception.backtrace) - STDOUT.flush - end - - # This method is invoked at the very end. - def dump_summary(duration, spec_count, failure_count) - return if @dry_run - @output.puts - @output.puts "Finished in #{duration} seconds" - @output.puts - summary = "#{spec_count} specification#{'s' unless spec_count == 1}, #{failure_count} failure#{'s' unless failure_count == 1}" - if failure_count == 0 - @output.puts green(summary) - else - @output.puts red(summary) - end - end - - def format_backtrace(backtrace) - return "" if backtrace.nil? - backtrace.map { |line| backtrace_line(line) }.join("\n") - end - - protected - - def backtrace_line(line) - line.sub(/\A([^:]+:\d+)$/, '\\1:') - end - - def colour(text, colour_code) - return text unless @colour && output_to_tty? - "#{colour_code}#{text}\e[0m" - end - - def output_to_tty? - begin - @output == Kernel || @output.tty? - rescue NoMethodError - false - end - end - - def red(text); colour(text, "\e[31m"); end - def green(text); colour(text, "\e[32m"); end - def magenta(text); colour(text, "\e[35m"); end - - end - end - end -end diff --git a/test/lib/spec/runner/formatter/html_formatter.rb b/test/lib/spec/runner/formatter/html_formatter.rb deleted file mode 100644 index 13b796581..000000000 --- a/test/lib/spec/runner/formatter/html_formatter.rb +++ /dev/null @@ -1,219 +0,0 @@ -module Spec - module Runner - module Formatter - class HtmlFormatter < BaseTextFormatter - attr_reader :current_spec_number, :current_context_number - - def initialize(output, dry_run=false, colour=false) - super - @current_spec_number = 0 - @current_context_number = 0 - end - - def start(spec_count) - @spec_count = spec_count - - @output.puts HEADER_1 - @output.puts extra_header_content unless extra_header_content.nil? - @output.puts HEADER_2 - STDOUT.flush - end - - def add_context(name, first) - @current_context_number += 1 - unless first - @output.puts " " - @output.puts "
" - end - @output.puts "
" - @output.puts "
" - @output.puts "
#{name}
" - STDOUT.flush - end - - def start_dump - @output.puts "
" - @output.puts "
" - STDOUT.flush - end - - def spec_started(name) - @current_spec_number += 1 - STDOUT.flush - end - - def spec_passed(name) - move_progress - @output.puts "
#{escape(name)}
" - STDOUT.flush - end - - def spec_failed(name, counter, failure) - @output.puts " " - @output.puts " " - move_progress - @output.puts "
" - @output.puts " #{escape(name)}" - @output.puts "
" - @output.puts "
#{escape(failure.exception.message)}
" unless failure.exception.nil? - @output.puts "
#{format_backtrace(failure.exception.backtrace)}
" unless failure.exception.nil? - @output.puts extra_failure_content unless extra_failure_content.nil? - @output.puts "
" - @output.puts "
" - STDOUT.flush - end - - # Override this method if you wish to output extra HTML in the header - # - def extra_header_content - end - - # Override this method if you wish to output extra HTML for a failed spec. For example, you - # could output links to images or other files produced during the specs. - # - def extra_failure_content - end - - def move_progress - percent_done = @spec_count == 0 ? 100.0 : (@current_spec_number.to_f / @spec_count.to_f * 1000).to_i / 10.0 - @output.puts " " - end - - def escape(string) - string.gsub(/&/n, '&').gsub(/\"/n, '"').gsub(/>/n, '>').gsub(/document.getElementById('duration').innerHTML = \"Finished in #{duration} seconds\";" - @output.puts "" - @output.puts "
" - @output.puts "" - @output.puts "" - STDOUT.flush - end - - HEADER_1 = <<-EOF - - - - - - RSpec results - - - -EOF - - HEADER_2 = <<-EOF - - - - - - - -
-EOF - end - end - end -end diff --git a/test/lib/spec/runner/formatter/progress_bar_formatter.rb b/test/lib/spec/runner/formatter/progress_bar_formatter.rb deleted file mode 100644 index fe519d4d8..000000000 --- a/test/lib/spec/runner/formatter/progress_bar_formatter.rb +++ /dev/null @@ -1,27 +0,0 @@ -module Spec - module Runner - module Formatter - class ProgressBarFormatter < BaseTextFormatter - def add_context(name, first) - @output.puts if first - STDOUT.flush - end - - def spec_failed(name, counter, failure) - @output.print failure.expectation_not_met? ? red('F') : magenta('F') - STDOUT.flush - end - - def spec_passed(name) - @output.print green('.') - STDOUT.flush - end - - def start_dump - @output.puts - STDOUT.flush - end - end - end - end -end \ No newline at end of file diff --git a/test/lib/spec/runner/formatter/rdoc_formatter.rb b/test/lib/spec/runner/formatter/rdoc_formatter.rb deleted file mode 100644 index eae55c3ea..000000000 --- a/test/lib/spec/runner/formatter/rdoc_formatter.rb +++ /dev/null @@ -1,22 +0,0 @@ -module Spec - module Runner - module Formatter - class RdocFormatter < BaseTextFormatter - def add_context(name, first) - @output.print "# #{name}\n" - STDOUT.flush - end - - def spec_passed(name) - @output.print "# * #{name}\n" - STDOUT.flush - end - - def spec_failed(name, counter, failure) - @output.print "# * #{name} [#{counter} - FAILED]\n" - STDOUT.flush - end - end - end - end -end \ No newline at end of file diff --git a/test/lib/spec/runner/formatter/specdoc_formatter.rb b/test/lib/spec/runner/formatter/specdoc_formatter.rb deleted file mode 100644 index 67b4312bf..000000000 --- a/test/lib/spec/runner/formatter/specdoc_formatter.rb +++ /dev/null @@ -1,23 +0,0 @@ -module Spec - module Runner - module Formatter - class SpecdocFormatter < BaseTextFormatter - def add_context(name, first) - @output.puts - @output.puts name - STDOUT.flush - end - - def spec_failed(name, counter, failure) - @output.puts failure.expectation_not_met? ? red("- #{name} (FAILED - #{counter})") : magenta("- #{name} (ERROR - #{counter})") - STDOUT.flush - end - - def spec_passed(name) - @output.print green("- #{name}\n") - STDOUT.flush - end - end - end - end -end \ No newline at end of file diff --git a/test/lib/spec/runner/option_parser.rb b/test/lib/spec/runner/option_parser.rb deleted file mode 100644 index 38725d848..000000000 --- a/test/lib/spec/runner/option_parser.rb +++ /dev/null @@ -1,224 +0,0 @@ -require 'ostruct' -require 'optparse' -require 'spec/runner/spec_parser' -require 'spec/runner/formatter' -require 'spec/runner/backtrace_tweaker' -require 'spec/runner/reporter' -require 'spec/runner/context_runner' - -module Spec - module Runner - class OptionParser - def initialize - @spec_parser = SpecParser.new - @file_factory = File - end - - def create_context_runner(args, err, out, warn_if_no_files) - options = parse(args, err, out, warn_if_no_files) - # Some exit points in parse (--generate-options, --drb) don't return the options, - # but hand over control. In that case we don't want to continue. - return nil unless options.is_a?(OpenStruct) - - formatter = options.formatter_type.new(options.out, options.dry_run, options.colour) - options.reporter = Reporter.new(formatter, options.backtrace_tweaker) - - # this doesn't really belong here. - # it should, but the way things are coupled, it doesn't - if options.differ_class - Spec::Expectations.differ = options.differ_class.new(options.diff_format, options.context_lines, options.colour) - end - - unless options.generate - ContextRunner.new(options) - end - end - - def parse(args, err, out, warn_if_no_files) - options_file = nil - args_copy = args.dup - options = OpenStruct.new - options.out = (out == STDOUT ? Kernel : out) - options.formatter_type = Formatter::ProgressBarFormatter - options.backtrace_tweaker = QuietBacktraceTweaker.new - options.spec_name = nil - - opts = ::OptionParser.new do |opts| - opts.banner = "Usage: spec [options] (FILE|DIRECTORY|GLOB)+" - opts.separator "" - - opts.on("-D", "--diff [FORMAT]", "Show diff of objects that are expected to be equal when they are not", - "Builtin formats: unified|u|context|c", - "You can also specify a custom differ class", - "(in which case you should also specify --require)") do |format| - - # TODO make context_lines settable - options.context_lines = 3 - - case format - when 'context', 'c' - options.diff_format = :context - when 'unified', 'u', '', nil - options.diff_format = :unified - end - - if [:context,:unified].include? options.diff_format - require 'spec/expectations/differs/default' - options.differ_class = Spec::Expectations::Differs::Default - else - begin - options.diff_format = :custom - options.differ_class = eval(format) - rescue NameError - err.puts "Couldn't find differ class #{format}" - err.puts "Make sure the --require option is specified *before* --diff" - exit if out == $stdout - end - end - - end - - opts.on("-c", "--colour", "--color", "Show coloured (red/green) output") do - options.colour = true - end - - opts.on("-s", "--spec SPECIFICATION_NAME", "Execute context or specification with matching name") do |spec_name| - options.spec_name = spec_name - end - - opts.on("-l", "--line LINE_NUMBER", Integer, "Execute context or specification at given line") do |line_number| - options.line_number = line_number.to_i - end - - opts.on("-f", "--format FORMAT", "Builtin formats: specdoc|s|rdoc|r|html|h", - "You can also specify a custom formatter class", - "(in which case you should also specify --require)") do |format| - case format - when 'specdoc', 's' - options.formatter_type = Formatter::SpecdocFormatter - when 'html', 'h' - options.formatter_type = Formatter::HtmlFormatter - when 'rdoc', 'r' - options.formatter_type = Formatter::RdocFormatter - options.dry_run = true - else - begin - options.formatter_type = eval(format) - rescue NameError - err.puts "Couldn't find formatter class #{format}" - err.puts "Make sure the --require option is specified *before* --format" - exit if out == $stdout - end - end - end - - opts.on("-r", "--require FILE", "Require FILE before running specs", - "Useful for loading custom formatters or other extensions", - "If this option is used it must come before the others") do |req| - req.split(",").each{|file| require file} - end - - opts.on("-b", "--backtrace", "Output full backtrace") do - options.backtrace_tweaker = NoisyBacktraceTweaker.new - end - - opts.on("-H", "--heckle CODE", "If all specs pass, this will run your specs many times, mutating", - "the specced code a little each time. The intent is that specs", - "*should* fail, and RSpec will tell you if they don't.", - "CODE should be either Some::Module, Some::Class or Some::Fabulous#method}") do |heckle| - heckle_runner = PLATFORM == 'i386-mswin32' ? 'spec/runner/heckle_runner_win' : 'spec/runner/heckle_runner' - require heckle_runner - options.heckle_runner = HeckleRunner.new(heckle) - end - - opts.on("-d", "--dry-run", "Don't execute specs") do - options.dry_run = true - end - - opts.on("-o", "--out OUTPUT_FILE", "Path to output file (defaults to STDOUT)") do |out_file| - options.out = File.new(out_file, 'w') - end - - opts.on("-O", "--options PATH", "Read options from a file") do |options_file| - # Remove the --options option and the argument before writing to file - index = args_copy.index("-O") || args_copy.index("--options") - args_copy.delete_at(index) - args_copy.delete_at(index) - - new_args = args_copy + IO.readlines(options_file).each {|s| s.chomp!} - return CommandLine.run(new_args, err, out, true, warn_if_no_files) - end - - opts.on("-G", "--generate-options PATH", "Generate an options file for --options") do |options_file| - # Remove the --generate-options option and the argument before writing to file - index = args_copy.index("-G") || args_copy.index("--generate-options") - args_copy.delete_at(index) - args_copy.delete_at(index) - - File.open(options_file, 'w') do |io| - io.puts args_copy.join("\n") - end - out.puts "\nOptions written to #{options_file}. You can now use these options with:" - out.puts "spec --options #{options_file}" - options.generate = true - end - - opts.on("-X", "--drb", "Run specs via DRb. (For example against script/rails_spec_server)") do |options_file| - # Remove the --options option and the argument before writing to file - index = args_copy.index("-X") || args_copy.index("--drb") - args_copy.delete_at(index) - - return DrbCommandLine.run(args_copy, err, out, true, warn_if_no_files) - end - - opts.on("-v", "--version", "Show version") do - out.puts ::Spec::VERSION::DESCRIPTION - exit if out == $stdout - end - - opts.on_tail("-h", "--help", "You're looking at it") do - out.puts opts - exit if out == $stdout - end - - end - opts.parse!(args) - - if args.empty? && warn_if_no_files - err.puts "No files specified." - err.puts opts - exit(6) if err == $stderr - end - - if options.line_number - set_spec_from_line_number(options, args, err) - end - - options - end - - def set_spec_from_line_number(options, args, err) - unless options.spec_name - if args.length == 1 - if @file_factory.file?(args[0]) - source = @file_factory.open(args[0]) - options.spec_name = @spec_parser.spec_name_for(source, options.line_number) - elsif @file_factory.directory?(args[0]) - err.puts "You must specify one file, not a directory when using the --line option" - exit(1) if err == $stderr - else - err.puts "#{args[0]} does not exist" - exit(2) if err == $stderr - end - else - err.puts "Only one file can be specified when using the --line option: #{args.inspect}" - exit(3) if err == $stderr - end - else - err.puts "You cannot use both --line and --spec" - exit(4) if err == $stderr - end - end - end - end -end diff --git a/test/lib/spec/runner/reporter.rb b/test/lib/spec/runner/reporter.rb deleted file mode 100644 index e4fb1cb0e..000000000 --- a/test/lib/spec/runner/reporter.rb +++ /dev/null @@ -1,105 +0,0 @@ -module Spec - module Runner - class Reporter - - def initialize(formatter, backtrace_tweaker) - @formatter = formatter - @backtrace_tweaker = backtrace_tweaker - clear! - end - - def add_context(name) - #TODO - @context_names.empty? tells the formatter whether this is the first context or not - that's a little slippery - @formatter.add_context(name, @context_names.empty?) - @context_names << name - end - - def spec_started(name) - @spec_names << name - @formatter.spec_started(name) - end - - def spec_finished(name, error=nil, failure_location=nil) - if error.nil? - spec_passed(name) - else - @backtrace_tweaker.tweak_backtrace(error, failure_location) - spec_failed(name, Failure.new(@context_names.last, name, error)) - end - end - - def start(number_of_specs) - clear! - @start_time = Time.new - @formatter.start(number_of_specs) - end - - def end - @end_time = Time.new - end - - # Dumps the summary and returns the total number of failures - def dump - @formatter.start_dump - dump_failures - @formatter.dump_summary(duration, @spec_names.length, @failures.length) - @failures.length - end - - private - - def clear! - @context_names = [] - @failures = [] - @spec_names = [] - @start_time = nil - @end_time = nil - end - - def dump_failures - return if @failures.empty? - @failures.inject(1) do |index, failure| - @formatter.dump_failure(index, failure) - index + 1 - end - end - - def duration - return @end_time - @start_time unless (@end_time.nil? or @start_time.nil?) - return "0.0" - end - - def spec_passed(name) - @formatter.spec_passed(name) - end - - def spec_failed(name, failure) - @failures << failure - @formatter.spec_failed(name, @failures.length, failure) - end - - class Failure - attr_reader :exception - - def initialize(context_name, spec_name, exception) - @context_name = context_name - @spec_name = spec_name - @exception = exception - end - - def header - if expectation_not_met? - "'#{@context_name} #{@spec_name}' FAILED" - else - "#{@exception.class.name} in '#{@context_name} #{@spec_name}'" - end - end - - def expectation_not_met? - @exception.is_a?(Spec::Expectations::ExpectationNotMetError) - end - - end - end - end -end diff --git a/test/lib/spec/runner/spec_matcher.rb b/test/lib/spec/runner/spec_matcher.rb deleted file mode 100755 index 687fdaa00..000000000 --- a/test/lib/spec/runner/spec_matcher.rb +++ /dev/null @@ -1,25 +0,0 @@ -module Spec - module Runner - class SpecMatcher - - attr_writer :spec_desc - def initialize(context_desc, spec_desc=nil) - @context_desc = context_desc - @spec_desc = spec_desc - end - - def matches?(desc) - desc =~ /(^#{context_regexp} #{spec_regexp}$|^#{context_regexp}$|^#{spec_regexp}$)/ - end - - private - def context_regexp - Regexp.escape(@context_desc) - end - - def spec_regexp - Regexp.escape(@spec_desc) - end - end - end -end diff --git a/test/lib/spec/runner/spec_parser.rb b/test/lib/spec/runner/spec_parser.rb deleted file mode 100644 index 2cb8518fc..000000000 --- a/test/lib/spec/runner/spec_parser.rb +++ /dev/null @@ -1,41 +0,0 @@ -module Spec - module Runner - # Parses a spec file and finds the nearest spec for a given line number. - class SpecParser - def spec_name_for(io, line_number) - source = io.read - context = context_at_line(source, line_number) - spec = spec_at_line(source, line_number) - if context && spec - "#{context} #{spec}" - elsif context - context - else - nil - end - end - - protected - - def context_at_line(source, line_number) - find_above(source, line_number, /^\s*context\s+['|"](.*)['|"]/) - end - - def spec_at_line(source, line_number) - find_above(source, line_number, /^\s*specify\s+['|"](.*)['|"]/) - end - - def find_above(source, line_number, pattern) - lines_above_reversed(source, line_number).each do |line| - return $1 if line =~ pattern - end - nil - end - - def lines_above_reversed(source, line_number) - lines = source.split("\n") - lines[0...line_number].reverse - end - end - end -end \ No newline at end of file diff --git a/test/lib/spec/runner/specification.rb b/test/lib/spec/runner/specification.rb deleted file mode 100644 index de8d750fd..000000000 --- a/test/lib/spec/runner/specification.rb +++ /dev/null @@ -1,114 +0,0 @@ -module Spec - module Runner - class Specification - - class << self - attr_accessor :current, :generated_description - protected :current= - - callback_events :before_setup, :after_teardown - end - - attr_reader :spec_block - callback_events :before_setup, :after_teardown - - def initialize(name, opts={}, &spec_block) - @from = caller(0)[3] - @description = name - @options = opts - @spec_block = spec_block - @description_generated_callback = lambda { |desc| @generated_description = desc } - end - - def run(reporter, setup_block, teardown_block, dry_run, execution_context) - reporter.spec_started(name) if reporter - return reporter.spec_finished(name) if dry_run - - errors = [] - begin - set_current - setup_ok = setup_spec(execution_context, errors, &setup_block) - spec_ok = execute_spec(execution_context, errors) if setup_ok - teardown_ok = teardown_spec(execution_context, errors, &teardown_block) - ensure - clear_current - end - - SpecShouldRaiseHandler.new(@from, @options).handle(errors) - reporter.spec_finished(name, errors.first, failure_location(setup_ok, spec_ok, teardown_ok)) if reporter - end - - def matches?(matcher, description) - matcher.spec_desc = name - matcher.matches?(description) - end - - private - def name - @description == :__generate_description ? generated_description : @description - end - - def generated_description - @generated_description || "NAME NOT GENERATED" - end - - def setup_spec(execution_context, errors, &setup_block) - notify_before_setup(errors) - execution_context.instance_eval(&setup_block) if setup_block - return errors.empty? - rescue => e - errors << e - return false - end - - def execute_spec(execution_context, errors) - begin - execution_context.instance_eval(&spec_block) - return true - rescue Exception => e - errors << e - return false - end - end - - def teardown_spec(execution_context, errors, &teardown_block) - execution_context.instance_eval(&teardown_block) if teardown_block - notify_after_teardown(errors) - return errors.empty? - rescue => e - errors << e - return false - end - - def notify_before_setup(errors) - notify_class_callbacks(:before_setup, self, &append_errors(errors)) - notify_callbacks(:before_setup, self, &append_errors(errors)) - end - - def notify_after_teardown(errors) - notify_callbacks(:after_teardown, self, &append_errors(errors)) - notify_class_callbacks(:after_teardown, self, &append_errors(errors)) - end - - def append_errors(errors) - proc {|error| errors << error} - end - - def set_current - Spec::Matchers.description_generated(&@description_generated_callback) - self.class.send(:current=, self) - end - - def clear_current - Spec::Matchers.unregister_callback(:description_generated, @description_generated_callback) - self.class.send(:current=, nil) - end - - def failure_location(setup_ok, spec_ok, teardown_ok) - return 'setup' unless setup_ok - return name unless spec_ok - return 'teardown' unless teardown_ok - end - end - end -end diff --git a/test/lib/spec/translator.rb b/test/lib/spec/translator.rb deleted file mode 100644 index 970c8ca00..000000000 --- a/test/lib/spec/translator.rb +++ /dev/null @@ -1,87 +0,0 @@ -require 'fileutils' - -module Spec - class Translator - def translate_dir(from, to) - from = File.expand_path(from) - to = File.expand_path(to) - if File.directory?(from) - FileUtils.mkdir_p(to) unless File.directory?(to) - Dir["#{from}/*"].each do |sub_from| - path = sub_from[from.length+1..-1] - sub_to = File.join(to, path) - translate_dir(sub_from, sub_to) - end - else - translate_file(from, to) - end - end - - def translate_file(from, to) - translation = "" - File.open(from) do |io| - io.each_line do |line| - translation << translate(line) - end - end - File.open(to, "w") do |io| - io.write(translation) - end - end - - def translate(line) - return line if line =~ /(should_not|should)_receive/ - - if line =~ /(.*\.)(should_not|should)(?:_be)(?!_)(.*)/m - pre = $1 - should = $2 - post = $3 - be_or_equal = post =~ /(<|>)/ ? "be" : "equal" - - return "#{pre}#{should} #{be_or_equal}#{post}" - end - - if line =~ /(.*\.)(should_not|should)_(?!not)(.*)/m - pre = $1 - should = $2 - post = $3 - - post.gsub!(/^raise/, 'raise_error') - post.gsub!(/^throw/, 'throw_symbol') - - unless standard_matcher?(post) - post = "be_#{post}" - end - - line = "#{pre}#{should} #{post}" - end - - line - end - - def standard_matcher?(matcher) - patterns = [ - /^be/, - /^be_close/, - /^eql/, - /^equal/, - /^has/, - /^have/, - /^change/, - /^include/, - /^match/, - /^raise_error/, - /^respond_to/, - /^satisfy/, - /^throw_symbol/, - # Extra ones that we use in spec_helper - /^pass/, - /^fail/, - /^fail_with/, - ] - matched = patterns.detect{ |p| matcher =~ p } - !matched.nil? - end - - end -end \ No newline at end of file diff --git a/test/lib/spec/version.rb b/test/lib/spec/version.rb deleted file mode 100644 index e692a87ee..000000000 --- a/test/lib/spec/version.rb +++ /dev/null @@ -1,30 +0,0 @@ -module Spec - module VERSION - def self.build_tag - tag = "REL_" + [MAJOR, MINOR, TINY].join('_') - if defined?(RELEASE_CANDIDATE) - tag << "_" << RELEASE_CANDIDATE - end - tag - end - - unless defined? MAJOR - MAJOR = 0 - MINOR = 8 - TINY = 2 - # RELEASE_CANDIDATE = "RC1" - - # RANDOM_TOKEN: 0.375509844656552 - REV = "$LastChangedRevision: 2283$".match(/LastChangedRevision: (\d+)/)[1] - - STRING = [MAJOR, MINOR, TINY].join('.') - FULL_VERSION = "#{STRING} (r#{REV})" - TAG = build_tag - - NAME = "RSpec" - URL = "http://rspec.rubyforge.org/" - - DESCRIPTION = "#{NAME}-#{FULL_VERSION} - BDD for Ruby\n#{URL}" - end - end -end diff --git a/test/network/client/client.rb b/test/network/client/client.rb index 14c90f2a9..93c63d637 100755 --- a/test/network/client/client.rb +++ b/test/network/client/client.rb @@ -1,261 +1,259 @@ #!/usr/bin/env ruby $:.unshift("../../lib") if __FILE__ =~ /\.rb$/ require 'puppettest' require 'mocha' require 'puppet/network/client' class TestClient < Test::Unit::TestCase include PuppetTest::ServerTest class FakeClient < Puppet::Network::Client @drivername = :Test end class FakeDriver end # a single run through of connect, auth, etc. def disabled_test_sslInitWithAutosigningLocalServer # autosign everything, for simplicity Puppet[:autosign] = true # create a server to which to connect mkserver() # create our client client = nil assert_nothing_raised { client = Puppet::Network::Client.master.new( :Server => "localhost", :Port => @@port ) } # get our certs assert_nothing_raised { client.initcerts } # make sure all of our cert files exist certfile = File.join(Puppet[:certdir], [client.fqdn, "pem"].join(".")) keyfile = File.join(Puppet[:privatekeydir], [client.fqdn, "pem"].join(".")) publickeyfile = File.join(Puppet[:publickeydir], [client.fqdn, "pem"].join(".")) assert(File.exists?(keyfile)) assert(File.exists?(certfile)) assert(File.exists?(publickeyfile)) # verify we can retrieve the configuration assert_nothing_raised("Client could not retrieve configuration") { client.getconfig } # and apply it assert_nothing_raised("Client could not apply configuration") { client.apply } # and verify that it did what it was supposed to assert(FileTest.exists?(@createdfile), "Applied file does not exist") end # here we create two servers; we def disabled_test_failureWithUntrustedCerts Puppet[:autosign] = true # create a pair of clients with no certs nonemaster = nil assert_nothing_raised { nonemaster = Puppet::Network::Client.master.new( :Server => "localhost", :Port => @@port ) } nonebucket = nil assert_nothing_raised { nonebucket = Puppet::Network::Client.dipper.new( :Server => "localhost", :Port => @@port ) } # create a ca so we can create a set of certs # make a new ssldir for it ca = nil assert_nothing_raised { ca = Puppet::Network::Client.ca.new( :CA => true, :Local => true ) ca.requestcert } # initialize our clients with this set of certs certmaster = nil assert_nothing_raised { certmaster = Puppet::Network::Client.master.new( :Server => "localhost", :Port => @@port ) } certbucket = nil assert_nothing_raised { certbucket = Puppet::Network::Client.dipper.new( :Server => "localhost", :Port => @@port ) } # Create a new ssl root. confdir = tempfile() Puppet[:ssldir] = confdir Puppet.config.mkdir(:ssldir) Puppet.config.clearused Puppet.config.use(:ssl, :ca) mkserver # now verify that our client cannot do non-cert operations # because its certs are signed by a different CA assert_raise(Puppet::Error, "Client was allowed to call getconfig with no certs") { nonemaster.getconfig } assert_raise(Puppet::Error, "Client was allowed to call getconfig with untrusted certs") { certmaster.getconfig } assert_raise(Puppet::Network::XMLRPCClientError, "Client was allowed to call backup with no certs") { nonebucket.backup("/etc/passwd") } assert_raise(Puppet::Network::XMLRPCClientError, "Client was allowed to call backup with untrusted certs") { certbucket.backup("/etc/passwd") } end def test_classfile manifest = tempfile() File.open(manifest, "w") do |file| file.puts "class yaytest {}\n class bootest {}\n include yaytest, bootest" end master = client = nil assert_nothing_raised() { master = Puppet::Network::Handler.master.new( :Manifest => manifest, :UseNodes => false, :Local => false ) } assert_nothing_raised() { client = Puppet::Network::Client.master.new( :Master => master ) } # Fake that it's local, so it creates the class file client.local = false + # We can't guarantee class ordering + client.expects(:setclasses).with do |array| + array.length == 2 and array.include?("yaytest") and array.include?("bootest") + end assert_nothing_raised { client.getconfig } - - assert(FileTest.exists?(Puppet[:classfile]), "Class file does not exist") - - classes = File.read(Puppet[:classfile]).split("\n") - - assert_equal(%w{bootest yaytest}, classes.sort) end def test_client_loading # Make sure we don't get a failure but that we also get nothing back assert_nothing_raised do assert_nil(Puppet::Network::Client.client(:fake), "Got something back from a missing client") assert_nil(Puppet::Network::Client.fake, "Got something back from missing client method") end # Make a fake client dir = tempfile() libdir = File.join([dir, %w{puppet network client}].flatten) FileUtils.mkdir_p(libdir) file = File.join(libdir, "faker.rb") File.open(file, "w") do |f| f.puts %{class Puppet::Network::Client class Faker < Client end end } end $: << dir cleanup { $:.delete(dir) if $:.include?(dir) } client = nil assert_nothing_raised do client = Puppet::Network::Client.client(:faker) end assert(client, "did not load client") assert_nothing_raised do assert_equal(client, Puppet::Network::Client.faker, "Did not get client back from client method") end # Now make sure the client behaves correctly assert_equal(:Faker, client.name, "name was not calculated correctly") end # Make sure we get a client class for each handler type. def test_loading_all_clients %w{ca dipper file master report resource runner status}.each do |name| client = nil assert_nothing_raised do client = Puppet::Network::Client.client(name) end assert(client, "did not get client for %s" % name) [:name, :handler, :drivername].each do |thing| assert(client.send(thing), "did not get %s for %s" % [thing, name]) end end end # Make sure that reading the cert in also sets up the cert stuff for the driver def test_read_cert Puppet::Util::SUIDManager.stubs(:asuser).yields ca = Puppet::Network::Handler.ca.new caclient = Puppet::Network::Client.ca.new :CA => ca caclient.request_cert # First make sure it doesn't get called when the driver doesn't support :cert_setup client = FakeClient.new :Test => FakeDriver.new driver = client.driver assert_nothing_raised("Could not read cert") do client.read_cert end # And then that it does when the driver supports it client = FakeClient.new :Test => FakeDriver.new driver = client.driver driver.meta_def(:cert_setup) { |c| } driver.expects(:cert_setup).with(client) assert_nothing_raised("Could not read cert") do client.read_cert end end end # $Id$ diff --git a/test/network/client/master.rb b/test/network/client/master.rb index 78a1a0a11..a29254d16 100755 --- a/test/network/client/master.rb +++ b/test/network/client/master.rb @@ -1,714 +1,732 @@ #!/usr/bin/env ruby $:.unshift("../../lib") if __FILE__ =~ /\.rb$/ require 'puppettest' require 'mocha' class TestMasterClient < Test::Unit::TestCase include PuppetTest::ServerTest class FakeTrans def initialize @counters = Hash.new { |h,k| h[k] = 0 } end [:evaluate, :report, :cleanup, :addtimes, :tags, :ignoreschedules].each do |m| define_method(m.to_s + "=") do |*args| @counters[m] += 1 end define_method(m) do |*args| @counters[m] += 1 end define_method(m.to_s + "?") do @counters[m] end end end class FakeComponent attr_accessor :trans def evaluate @trans = FakeTrans.new @trans end def finalize @finalized = true end def finalized? @finalized end end def setup super @master = Puppet::Network::Client.master end - def mkmaster(file = nil) - master = nil - - file ||= mktestmanifest() + def mkmaster(options = {}) + options[:UseNodes] = false + options[:Local] = true + unless options[:Code] + options[:Manifest] ||= mktestmanifest + end # create our master - assert_nothing_raised() { - # this is the default server setup - master = Puppet::Network::Handler.master.new( - :Manifest => file, - :UseNodes => false, - :Local => true - ) - } + # this is the default server setup + master = Puppet::Network::Handler.master.new(options) return master end def mkclient(master = nil) master ||= mkmaster() - client = nil - assert_nothing_raised() { - client = Puppet::Network::Client.master.new( - :Master => master - ) - } + client = Puppet::Network::Client.master.new( + :Master => master + ) return client end def mk_fake_client server = Puppet::Network::Handler.master.new :Code => "" - master = Puppet::Network::Client.master.new :Server => server, :Local => true + master = Puppet::Network::Client.master.new :Master => server, :Local => true # Now create some objects objects = FakeComponent.new master.send(:instance_variable_set, "@objects", objects) class << master def report(r) @reported ||= 0 @reported += 1 end def reported @reported ||= 0 @reported end end return master, objects end def test_apply master, objects = mk_fake_client check = Proc.new do |hash| assert(objects.trans, "transaction was not created") trans = objects.trans hash[:yes].each do |m| assert_equal(1, trans.send(m.to_s + "?"), "did not call #{m} enough times") end hash[:no].each do |m| assert_equal(0, trans.send(m.to_s + "?"), "called #{m} too many times") end end # First try it with no arguments assert_nothing_raised do master.apply end check.call :yes => %w{evaluate cleanup addtimes}, :no => %w{report tags ignoreschedules} assert_equal(0, master.reported, "master sent report with reports disabled") # Now enable reporting and make sure the report method gets called Puppet[:report] = true assert_nothing_raised do master.apply end check.call :yes => %w{evaluate cleanup addtimes}, :no => %w{tags ignoreschedules} assert_equal(1, master.reported, "master did not send report") # Now try it with tags enabled assert_nothing_raised do master.apply("tags") end check.call :yes => %w{evaluate cleanup tags addtimes}, :no => %w{ignoreschedules} assert_equal(2, master.reported, "master did not send report") # and ignoreschedules assert_nothing_raised do master.apply("tags", true) end check.call :yes => %w{evaluate cleanup tags ignoreschedules addtimes}, :no => %w{} assert_equal(3, master.reported, "master did not send report") end def test_getconfig client = mkclient $methodsrun = [] cleanup { $methodsrun = nil } client.meta_def(:getplugins) do $methodsrun << :getplugins end client.meta_def(:get_actual_config) do $methodsrun << :get_actual_config result = Puppet::TransBucket.new() result.type = "testing" result.name = "yayness" result end assert_nothing_raised do client.getconfig end [:get_actual_config].each do |method| assert($methodsrun.include?(method), "method %s was not run" % method) end assert(! $methodsrun.include?(:getplugins), "plugins were synced even tho disabled") # Now set pluginsync Puppet[:pluginsync] = true $methodsrun.clear assert_nothing_raised do client.getconfig end [:getplugins, :get_actual_config].each do |method| assert($methodsrun.include?(method), "method %s was not run" % method) end objects = client.objects assert(objects.finalized?, "objects were not finalized") end def test_disable FileUtils.mkdir_p(Puppet[:statedir]) manifest = mktestmanifest - master = mkmaster(manifest) + master = mkmaster(:Manifest => manifest) client = mkclient(master) assert(! FileTest.exists?(@createdfile)) assert_nothing_raised { client.disable } assert_nothing_raised { client.run } assert(! FileTest.exists?(@createdfile), "Disabled client ran") assert_nothing_raised { client.enable } assert_nothing_raised { client.run } assert(FileTest.exists?(@createdfile), "Enabled client did not run") end # Make sure we're getting the client version in our list of facts def test_clientversionfact facts = nil assert_nothing_raised { facts = Puppet::Network::Client.master.facts } assert_equal(Puppet.version.to_s, facts["clientversion"]) end # Make sure non-string facts don't make things go kablooie def test_nonstring_facts FileUtils.mkdir_p(Puppet[:statedir]) # Add a nonstring fact Facter.add("nonstring") do setcode { 1 } end assert_equal(1, Facter.nonstring, "Fact was a string from facter") client = mkclient() assert(! FileTest.exists?(@createdfile)) assert_nothing_raised { client.run } end # This method is supposed def test_download source = tempfile() dest = tempfile() sfile = File.join(source, "file") dfile = File.join(dest, "file") Dir.mkdir(source) File.open(sfile, "w") {|f| f.puts "yay"} files = [] assert_nothing_raised do files = Puppet::Network::Client.master.download(:dest => dest, :source => source, :name => "testing") end assert(FileTest.directory?(dest), "dest dir was not created") assert(FileTest.file?(dfile), "dest file was not created") assert_equal(File.read(sfile), File.read(dfile), "Dest file had incorrect contents") assert_equal([dest, dfile].sort, files.sort, "Changed files were not returned correctly") end def test_getplugins Puppet[:filetimeout] = -1 Puppet[:pluginsource] = tempfile() Dir.mkdir(Puppet[:pluginsource]) Dir.mkdir(File.join(Puppet[:pluginsource], "testing")) $loaded = [] loader = Puppet::Util::Autoload.new(self, "testing") myplugin = File.join(Puppet[:pluginsource], "testing", "myplugin.rb") File.open(myplugin, "w") do |f| f.puts %{$loaded << :myplugin} end assert_nothing_raised("Could not get plugins") { Puppet::Network::Client.master.getplugins } destfile = File.join(Puppet[:plugindest], "testing", "myplugin.rb") assert(File.exists?(destfile), "Did not get plugin") assert(loader.load(:myplugin), "Did not load downloaded plugin") assert($loaded.include?(:myplugin), "Downloaded code was not evaluated") # Now modify the file and make sure the type is replaced File.open(myplugin, "w") do |f| f.puts %{$loaded << :changed} end assert_nothing_raised("Could not get plugin changes") { Puppet::Network::Client.master.getplugins } assert($loaded.include?(:changed), "Changed code was not evaluated") # Now try it again, to make sure we don't have any objects lying around assert_nothing_raised { Puppet::Network::Client.master.getplugins } end def test_getfacts Puppet[:filetimeout] = -1 Puppet[:factsource] = tempfile() Dir.mkdir(Puppet[:factsource]) hostname = Facter.value(:hostname) myfact = File.join(Puppet[:factsource], "myfact.rb") File.open(myfact, "w") do |f| f.puts %{Facter.add("myfact") do setcode { "yayness" } end } end assert_nothing_raised { Puppet::Network::Client.master.getfacts } destfile = File.join(Puppet[:factdest], "myfact.rb") assert(File.exists?(destfile), "Did not get fact") assert_equal(hostname, Facter.value(:hostname), "Lost value to hostname") assert_equal("yayness", Facter.value(:myfact), "Did not get correct fact value") # Now modify the file and make sure the type is replaced File.open(myfact, "w") do |f| f.puts %{Facter.add("myfact") do setcode { "funtest" } end } end assert_nothing_raised { Puppet::Network::Client.master.getfacts } assert_equal("funtest", Facter.value(:myfact), "Did not reload fact") assert_equal(hostname, Facter.value(:hostname), "Lost value to hostname") # Now run it again and make sure the fact still loads assert_nothing_raised { Puppet::Network::Client.master.getfacts } assert_equal("funtest", Facter.value(:myfact), "Did not reload fact") assert_equal(hostname, Facter.value(:hostname), "Lost value to hostname") end # Make sure we load all facts on startup. def test_loadfacts dirs = [tempfile(), tempfile()] count = 0 names = [] dirs.each do |dir| Dir.mkdir(dir) name = "fact%s" % count names << name file = File.join(dir, "%s.rb" % name) # Write out a plugin file File.open(file, "w") do |f| f.puts %{Facter.add("#{name}") do setcode { "#{name}" } end } end count += 1 end Puppet[:factpath] = dirs.join(":") names.each do |name| assert_nil(Facter.value(name), "Somehow retrieved invalid fact") end assert_nothing_raised { Puppet::Network::Client.master.loadfacts } names.each do |name| assert_equal(name, Facter.value(name), "Did not retrieve facts") end end if Process.uid == 0 # Testing #283. Make sure plugins et al are downloaded as the running user. def test_download_ownership dir = tstdir() dest = tstdir() file = File.join(dir, "file") File.open(file, "w") { |f| f.puts "funtest" } user = nonrootuser() group = nonrootgroup() chowner = Puppet::Type.type(:file).create :path => dir, :owner => user.name, :group => group.name, :recurse => true assert_apply(chowner) chowner.remove assert_equal(user.uid, File.stat(file).uid) assert_equal(group.gid, File.stat(file).gid) assert_nothing_raised { Puppet::Network::Client.master.download(:dest => dest, :source => dir, :name => "testing" ) {} } destfile = File.join(dest, "file") assert(FileTest.exists?(destfile), "Did not create destfile") assert_equal(Process.uid, File.stat(destfile).uid) end end # Test retrieving all of the facts. def test_facts facts = nil assert_nothing_raised do facts = Puppet::Network::Client.master.facts end Facter.to_hash.each do |fact, value| assert_equal(facts[fact.downcase], value, "%s is not equal" % fact.inspect) end # Make sure the puppet version got added assert_equal(Puppet::PUPPETVERSION, facts["clientversion"], "client version did not get added") # And make sure the ruby version is in there assert_equal(RUBY_VERSION, facts["rubyversion"], "ruby version did not get added") end # #424 def test_caching_of_compile_time file = tempfile() manifest = tempfile() File.open(manifest, "w") { |f| f.puts "file { '#{file}': content => yay }" } - driver = mkmaster(manifest) + driver = mkmaster(:Manifest => manifest) driver.local = false master = mkclient(driver) # We have to make everything thinks it's remote, because there's no local caching info master.local = false assert(! master.fresh?(master.class.facts), "Considered fresh with no compile at all") assert_nothing_raised { master.run } assert(master.fresh?(master.class.facts), "not considered fresh after compile") # Now make sure the config time is cached assert(master.compile_time, "No stored config time") assert_equal(master.compile_time, Puppet::Util::Storage.cache(:configuration)[:compile_time], "times did not match") time = master.compile_time master.clear File.unlink(file) Puppet::Util::Storage.store # Now make a new master Puppet::Util::Storage.clear master = mkclient(driver) master.run assert_equal(time, master.compile_time, "time was not retrieved from cache") assert(FileTest.exists?(file), "file was not created on second run") end def test_default_objects # Make sure they start out missing assert_nil(Puppet::Type.type(:filebucket)["puppet"], "default filebucket already exists") assert_nil(Puppet::Type.type(:schedule)["daily"], "default schedules already exists") master = mkclient() # Now make sure they got created assert(Puppet::Type.type(:filebucket)["puppet"], "default filebucket not found") assert(Puppet::Type.type(:schedule)["daily"], "default schedules not found") # clear everything, and make sure we can recreate them Puppet::Type.allclear assert_nil(Puppet::Type.type(:filebucket)["puppet"], "default filebucket not removed") assert_nil(Puppet::Type.type(:schedule)["daily"], "default schedules not removed") assert_nothing_raised { master.mkdefault_objects } assert(Puppet::Type.type(:filebucket)["puppet"], "default filebucket not found") assert(Puppet::Type.type(:schedule)["daily"], "default schedules not found") # Make sure we've got schedules assert(Puppet::Type.type(:schedule)["hourly"], "Could not retrieve hourly schedule") assert(Puppet::Type.type(:filebucket)["puppet"], "Could not retrieve default bucket") end # #540 - make sure downloads aren't affected by noop def test_download_in_noop source = tempfile File.open(source, "w") { |f| f.puts "something" } dest = tempfile Puppet[:noop] = true assert_nothing_raised("Could not download in noop") do @master.download(:dest => dest, :source => source, :tag => "yay") end assert(FileTest.exists?(dest), "did not download in noop mode") assert(Puppet[:noop], "noop got disabled in run") end # #491 - make sure a missing config doesn't kill us def test_missing_localconfig master = mkclient master.local = false driver = master.send(:instance_variable_get, "@driver") driver.local = false # Retrieve the configuration master.getconfig # Now the config is up to date, so get rid of the @objects var and # the cached config master.clear File.unlink(master.cachefile) assert_nothing_raised("Missing cache file threw error") do master.getconfig end assert(! @logs.detect { |l| l.message =~ /Could not load/}, "Tried to load cache when it is non-existent") end # #519 - cache the facts so that we notice if they change. def test_factchanges_cause_recompile $value = "one" Facter.add(:testfact) do setcode { $value } end assert_equal("one", Facter.value(:testfact), "fact was not set correctly") master = mkclient master.local = false driver = master.send(:instance_variable_get, "@driver") driver.local = false assert_nothing_raised("Could not compile config") do master.getconfig end $value = "two" Facter.clear Facter.loadfacts Facter.add(:testfact) do setcode { $value } end facts = master.class.facts assert_equal("two", Facter.value(:testfact), "fact did not change") assert(master.send(:facts_changed?, facts), "master does not think facts changed") assert(! master.fresh?(facts), "master is considered fresh after facts changed") assert_nothing_raised("Could not recompile when facts changed") do master.getconfig end end def test_locking master = mkclient class << master def getconfig raise ArgumentError, "Just testing" end end master.run assert(! master.send(:lockfile).locked?, "Master is still locked after failure") end # Make sure we get a value for timeout def test_config_timeout master = Puppet::Network::Client.client(:master) time = Integer(Puppet[:configtimeout]) assert_equal(time, master.timeout, "Did not get default value for timeout") assert_equal(time, master.timeout, "Did not get default value for timeout on second run") # Reset it Puppet[:configtimeout] = "50" assert_equal(50, master.timeout, "Did not get changed default value for timeout") assert_equal(50, master.timeout, "Did not get changed default value for timeout on second run") # Now try an integer Puppet[:configtimeout] = 100 assert_equal(100, master.timeout, "Did not get changed integer default value for timeout") assert_equal(100, master.timeout, "Did not get changed integer default value for timeout on second run") end # #569 -- Make sure we can ignore dynamic facts. def test_dynamic_facts client = mkclient assert_equal(%w{memorysize memoryfree swapsize swapfree}, client.class.dynamic_facts, "Did not get correct defaults for dynamic facts") # Cache some values for comparison cached = {"one" => "yep", "two" => "nope"} Puppet::Util::Storage.cache(:configuration)[:facts] = cached assert(! client.send(:facts_changed?, cached), "Facts incorrectly considered to be changed") # Now add some values to the passed result and make sure we get a positive newfacts = cached.dup newfacts["changed"] = "something" assert(client.send(:facts_changed?, newfacts), "Did not catch changed fact") # Now add a dynamic fact and make sure it's ignored newfacts = cached.dup newfacts["memorysize"] = "something" assert(! client.send(:facts_changed?, newfacts), "Dynamic facts resulted in a false positive") # And try it with both cached["memorysize"] = "something else" assert(! client.send(:facts_changed?, newfacts), "Dynamic facts resulted in a false positive") # And finally, with only in the cache newfacts.delete("memorysize") assert(! client.send(:facts_changed?, newfacts), "Dynamic facts resulted in a false positive") end def test_splay client = mkclient # Make sure we default to no splay client.expects(:sleep).never assert_nothing_raised("Failed to call splay") do client.send(:splay) end # Now set it to true and make sure we get the right value client = mkclient client.expects(:sleep) Puppet[:splay] = true assert_nothing_raised("Failed to call sleep when splay is true") do client.send(:splay) end time = Puppet::Util::Storage.cache(:configuration)[:splay_time] assert(time, "Splay time was not cached") # Now try it again client = mkclient client.expects(:sleep).with(time) assert_nothing_raised("Failed to call sleep when splay is true with a cached value") do client.send(:splay) end end + def test_environment_is_added_to_facts + facts = Puppet::Network::Client::Master.facts + assert_equal(facts["environment"], Puppet[:environment], "Did not add environment to client facts") + + # Now set it to a real value + Puppet[:environment] = "something" + facts = Puppet::Network::Client::Master.facts + assert_equal(facts["environment"], Puppet[:environment], "Did not add environment to client facts") + end + # This is partially to fix #532, but also to save on memory. def test_remove_objects_after_every_run client = mkclient ftype = Puppet::Type.type(:file) assert_nil(ftype[@createdfile], "file object already exists") assert(! FileTest.exists?(@createdfile), "File already exists on disk") assert_nothing_raised("Could not apply config") do client.run end assert(FileTest.exists?(@createdfile), "File does not exist on disk") assert_nil(ftype[@createdfile], "file object was not removed from memory") end # #685 def test_http_failures_do_not_kill_puppetd client = mkclient client.meta_def(:getconfig) { raise "A failure" } assert_nothing_raised("Failure in getconfig threw an error") do client.run end end -end -# $Id$ + # #800 -- we cannot fix this using the current design. + def disabled_test_invalid_relationships_do_not_get_cached + # Make a master with an invalid relationship + master = mkmaster :Code => "notify { one: require => File[yaytest] }" + master.local = false # so it gets cached + client = mkclient(master) + client.local = false + + client.getconfig + # Doesn't throw an exception, but definitely fails. + client.apply + + # Make sure the config is not cached. + config = Puppet.config[:localconfig] + ".yaml" + assert(! File.exists?(config), "Cached an invalid configuration") + end +end diff --git a/test/network/handler/configuration.rb b/test/network/handler/configuration.rb new file mode 100755 index 000000000..072fdc053 --- /dev/null +++ b/test/network/handler/configuration.rb @@ -0,0 +1,191 @@ +#!/usr/bin/env ruby + +$:.unshift("../../lib") if __FILE__ =~ /\.rb$/ + +require 'puppettest' +require 'puppet/network/handler/configuration' + +class TestHandlerConfiguration < Test::Unit::TestCase + include PuppetTest + + Config = Puppet::Network::Handler.handler(:configuration) + + # Check all of the setup stuff. + def test_initialize + config = nil + assert_nothing_raised("Could not create local config") do + config = Config.new(:Local => true) + end + + assert(config.local?, "Config is not considered local after being started that way") + end + + # Make sure we create the node handler when necessary. + def test_node_handler + config = Config.new + handler = nil + assert_nothing_raised("Could not create node handler") do + handler = config.send(:node_handler) + end + assert_instance_of(Puppet::Network::Handler.handler(:node), handler, "Did not create node handler") + + # Now make sure we get the same object back again + assert_equal(handler.object_id, config.send(:node_handler).object_id, "Did not cache node handler") + end + + # Test creation/returning of the interpreter + def test_interpreter + config = Config.new + + # First test the defaults + args = {} + config.instance_variable_set("@options", args) + config.expects(:create_interpreter).with(args).returns(:interp) + assert_equal(:interp, config.send(:interpreter), "Did not return the interpreter") + + # Now run it again and make sure we get the same thing + assert_equal(:interp, config.send(:interpreter), "Did not cache the interpreter") + end + + def test_create_interpreter + config = Config.new(:Local => false) + args = {} + + # Try it first with defaults. + Puppet::Parser::Interpreter.expects(:new).with(:Local => config.local?).returns(:interp) + assert_equal(:interp, config.send(:create_interpreter, args), "Did not return the interpreter") + + # Now reset it and make sure a specified manifest passes through + file = tempfile + args[:Manifest] = file + Puppet::Parser::Interpreter.expects(:new).with(:Local => config.local?, :Manifest => file).returns(:interp) + assert_equal(:interp, config.send(:create_interpreter, args), "Did not return the interpreter") + + # And make sure the code does, too + args.delete(:Manifest) + args[:Code] = "yay" + Puppet::Parser::Interpreter.expects(:new).with(:Local => config.local?, :Code => "yay").returns(:interp) + assert_equal(:interp, config.send(:create_interpreter, args), "Did not return the interpreter") + end + + # Make sure node objects get appropriate data added to them. + def test_add_node_data + # First with no classes + config = Config.new + + fakenode = Object.new + # Set the server facts to something + config.instance_variable_set("@server_facts", :facts) + fakenode.expects(:fact_merge).with(:facts) + config.send(:add_node_data, fakenode) + + # Now try it with classes. + config.instance_variable_set("@options", {:Classes => %w{a b}}) + list = [] + fakenode = Object.new + fakenode.expects(:fact_merge).with(:facts) + fakenode.expects(:classes).returns(list).times(2) + config.send(:add_node_data, fakenode) + assert_equal(%w{a b}, list, "Did not add classes to node") + end + + def test_compile + config = Config.new + + # First do a local + node = mock 'node' + node.stubs(:name).returns(:mynode) + node.stubs(:environment).returns(:myenv) + + interp = mock 'interpreter' + interp.stubs(:environment) + interp.expects(:compile).with(node).returns(:config) + config.expects(:interpreter).returns(interp) + + Puppet.expects(:notice) # The log message from benchmarking + + assert_equal(:config, config.send(:compile, node), "Did not return config") + + # Now try it non-local + node = mock 'node' + node.stubs(:name).returns(:mynode) + node.stubs(:environment).returns(:myenv) + + interp = mock 'interpreter' + interp.stubs(:environment) + interp.expects(:compile).with(node).returns(:config) + + config = Config.new(:Local => true) + config.expects(:interpreter).returns(interp) + + assert_equal(:config, config.send(:compile, node), "Did not return config") + end + + def test_set_server_facts + config = Config.new + assert_nothing_raised("Could not call :set_server_facts") do + config.send(:set_server_facts) + end + facts = config.instance_variable_get("@server_facts") + %w{servername serverversion serverip}.each do |fact| + assert(facts.include?(fact), "Config did not set %s fact" % fact) + end + end + + def test_translate + # First do a local config + config = Config.new(:Local => true) + assert_equal(:plain, config.send(:translate, :plain), "Attempted to translate local config") + + # Now a non-local + config = Config.new(:Local => false) + obj = Object.new + yamld = Object.new + obj.expects(:to_yaml).with(:UseBlock => true).returns(yamld) + CGI.expects(:escape).with(yamld).returns(:translated) + assert_equal(:translated, config.send(:translate, obj), "Did not return translated config") + end + + # Check that we're storing the node freshness into the rails db. Hackilicious. + def test_update_node_check + # This is stupid. + config = Config.new + node = Object.new + node.expects(:name).returns(:hostname) + now = Object.new + Time.expects(:now).returns(now) + host = Object.new + host.expects(:last_freshcheck=).with(now) + host.expects(:save) + + # Only test the case where rails is there + Puppet[:storeconfigs] = true + Puppet.features.expects(:rails?).returns(true) + Puppet::Rails.expects(:connect) + Puppet::Rails::Host.expects(:find_or_create_by_name).with(:hostname).returns(host) + + config.send(:update_node_check, node) + end + + def test_version + # First try the case where we can't look up the node + config = Config.new + handler = Object.new + handler.expects(:details).with(:client).returns(false) + config.expects(:node_handler).returns(handler) + interp = Object.new + assert_instance_of(Bignum, config.version(:client), "Did not return configuration version") + + # And then when we find the node. + config = Config.new + node = Object.new + handler = Object.new + handler.expects(:details).with(:client).returns(node) + config.expects(:update_node_check).with(node) + config.expects(:node_handler).returns(handler) + interp = Object.new + interp.expects(:configuration_version).returns(:version) + config.expects(:interpreter).returns(interp) + assert_equal(:version, config.version(:client), "Did not return configuration version") + end +end diff --git a/test/network/handler/master.rb b/test/network/handler/master.rb index 08e17373b..a976726ef 100755 --- a/test/network/handler/master.rb +++ b/test/network/handler/master.rb @@ -1,305 +1,156 @@ #!/usr/bin/env ruby $:.unshift("../../lib") if __FILE__ =~ /\.rb$/ require 'puppettest' require 'puppet/network/handler/master' class TestMaster < Test::Unit::TestCase include PuppetTest::ServerTest - # run through all of the existing test files and make sure everything - # works - def test_files - count = 0 - textfiles { |file| - Puppet.debug("parsing %s" % file) - client = nil - master = nil - - # create our master - assert_nothing_raised() { - # this is the default server setup - master = Puppet::Network::Handler.master.new( - :Manifest => file, - :UseNodes => false, - :Local => true - ) - } - - # and our client - assert_nothing_raised() { - client = Puppet::Network::Client.master.new( - :Master => master - ) - } - - # pull our configuration a few times - assert_nothing_raised() { - client.getconfig - stopservices - Puppet::Type.allclear - } - assert_nothing_raised() { - client.getconfig - stopservices - Puppet::Type.allclear - } - assert_nothing_raised() { - client.getconfig - stopservices - Puppet::Type.allclear - } - # only test three files; that's plenty - if count > 3 - break - end - count += 1 - } - end - def test_defaultmanifest textfiles { |file| Puppet[:manifest] = file client = nil master = nil assert_nothing_raised() { # this is the default server setup master = Puppet::Network::Handler.master.new( :Manifest => file, :UseNodes => false, :Local => true ) } assert_nothing_raised() { client = Puppet::Network::Client.master.new( :Master => master ) } # pull our configuration assert_nothing_raised() { client.getconfig stopservices Puppet::Type.allclear } break } end def test_filereread # Start with a normal setting Puppet[:filetimeout] = 15 manifest = mktestmanifest() facts = Puppet::Network::Client.master.facts # Store them, so we don't determine frshness based on facts. Puppet::Util::Storage.cache(:configuration)[:facts] = facts file2 = @createdfile + "2" @@tmpfiles << file2 client = master = nil assert_nothing_raised() { # this is the default server setup master = Puppet::Network::Handler.master.new( :Manifest => manifest, :UseNodes => false, :Local => true ) } assert_nothing_raised() { client = Puppet::Network::Client.master.new( :Master => master ) } assert(client, "did not create master client") # The client doesn't have a config, so it can't be up to date assert(! client.fresh?(facts), "Client is incorrectly up to date") Puppet.config.use(:main) assert_nothing_raised { client.getconfig client.apply } # Now it should be up to date assert(client.fresh?(facts), "Client is not up to date") # Cache this value for later - parse1 = master.freshness + parse1 = master.freshness("mynode") # Verify the config got applied assert(FileTest.exists?(@createdfile), "Created file %s does not exist" % @createdfile) Puppet::Type.allclear sleep 1.5 # Create a new manifest File.open(manifest, "w") { |f| f.puts "file { \"%s\": ensure => file }\n" % file2 } # Verify that the master doesn't immediately reparse the file; we # want to wait through the timeout - assert_equal(parse1, master.freshness, "Master did not wait through timeout") + assert_equal(parse1, master.freshness("mynode"), "Master did not wait through timeout") assert(client.fresh?(facts), "Client is not up to date") # Then eliminate it Puppet[:filetimeout] = 0 # Now make sure the master does reparse #Puppet.notice "%s vs %s" % [parse1, master.freshness] - assert(parse1 != master.freshness, "Master did not reparse file") + assert(parse1 != master.freshness("mynode"), "Master did not reparse file") assert(! client.fresh?(facts), "Client is incorrectly up to date") # Retrieve and apply the new config assert_nothing_raised { client.getconfig client.apply } assert(client.fresh?(facts), "Client is not up to date") assert(FileTest.exists?(file2), "Second file %s does not exist" % file2) end - def test_addfacts - master = nil - file = mktestmanifest() - # create our master - assert_nothing_raised() { - # this is the default server setup - master = Puppet::Network::Handler.master.new( - :Manifest => file, - :UseNodes => false, - :Local => true - ) - } - - facts = {} - - assert_nothing_raised { - master.addfacts(facts) - } - - %w{serverversion servername serverip}.each do |fact| - assert(facts.include?(fact), "Fact %s was not set" % fact) - end - end - - # Make sure we're using the hostname as configured with :node_name - def test_hostname_in_getconfig - master = nil - file = tempfile() - #@createdfile = File.join(tmpdir(), self.class.to_s + "manifesttesting" + - # "_" + @method_name) - file_cert = tempfile() - file_fact = tempfile() - - certname = "y4yn3ss" - factname = Facter.value("hostname") - - File.open(file, "w") { |f| - f.puts %{ - node #{certname} { file { "#{file_cert}": ensure => file, mode => 755 } } - node #{factname} { file { "#{file_fact}": ensure => file, mode => 755 } } -} - } - # create our master - assert_nothing_raised() { - # this is the default server setup - master = Puppet::Network::Handler.master.new( - :Manifest => file, - :UseNodes => true, - :Local => true - ) - } - - result = nil - - # Use the hostname from facter - Puppet[:node_name] = 'facter' - assert_nothing_raised { - result = master.getconfig({"hostname" => factname}, "yaml", certname, "127.0.0.1") - } - - result = result.flatten - - assert(result.find { |obj| obj.name == file_fact }, - "Could not find correct file") - assert(!result.find { |obj| obj.name == file_cert }, - "Found incorrect file") - - # Use the hostname from the cert - Puppet[:node_name] = 'cert' - assert_nothing_raised { - result = master.getconfig({"hostname" => factname}, "yaml", certname, "127.0.0.1") - } - - result = result.flatten - - assert(!result.find { |obj| obj.name == file_fact }, - "Could not find correct file") - assert(result.find { |obj| obj.name == file_cert }, - "Found incorrect file") - end - # Make sure we're correctly doing clientname manipulations. # Testing to make sure we always get a hostname and IP address. def test_clientname - master = nil - file = tempfile() - - File.open(file, "w") { |f| - f.puts %{ - node yay { file { "/something": ensure => file, mode => 755 } } -} - } # create our master - assert_nothing_raised() { - # this is the default server setup - master = Puppet::Network::Handler.master.new( - :Manifest => file, - :UseNodes => true, - :Local => true - ) - } + master = Puppet::Network::Handler.master.new( + :Manifest => tempfile, + :UseNodes => true, + :Local => true + ) - Puppet[:node_name] = "cert" - # First act like we're local - fakename = nil - fakeip = nil - name = ip = nil - facts = Facter.to_hash - assert_nothing_raised do - name, ip = master.clientname(fakename, fakeip, facts) - end - - assert(facts["hostname"], "Removed hostname fact") - assert(facts["ipaddress"], "Removed ipaddress fact") - - assert_equal(facts["hostname"], name) - assert_equal(facts["ipaddress"], ip) - - # Now set them to something real, and make sure we get them back - fakename = "yayness" - fakeip = "192.168.0.1" - facts = Facter.to_hash - assert_nothing_raised do - name, ip = master.clientname(fakename, fakeip, facts) - end - - assert(facts["hostname"], "Removed hostname fact") - assert(facts["ipaddress"], "Removed ipaddress fact") + # First check that 'cert' works + Puppet[:node_name] = "cert" - assert_equal(fakename, name) - assert_equal(fakeip, ip) + # Make sure we get the fact data back when nothing is set + facts = {"hostname" => "fact_hostname", "ipaddress" => "fact_ip"} + certname = "cert_hostname" + certip = "cert_ip" + + resname, resip = master.send(:clientname, nil, nil, facts) + assert_equal(facts["hostname"], resname, "Did not use fact hostname when no certname was present") + assert_equal(facts["ipaddress"], resip, "Did not use fact ip when no certname was present") + + # Now try it with the cert stuff present + resname, resip = master.send(:clientname, certname, certip, facts) + assert_equal(certname, resname, "Did not use cert hostname when certname was present") + assert_equal(certip, resip, "Did not use cert ip when certname was present") + + # And reset the node_name stuff and make sure we use it. + Puppet[:node_name] = :facter + resname, resip = master.send(:clientname, certname, certip, facts) + assert_equal(facts["hostname"], resname, "Did not use fact hostname when nodename was set to facter") + assert_equal(facts["ipaddress"], resip, "Did not use fact ip when nodename was set to facter") end end # $Id$ diff --git a/test/network/handler/node.rb b/test/network/handler/node.rb index 9fad3f765..6b8ab9290 100755 --- a/test/network/handler/node.rb +++ b/test/network/handler/node.rb @@ -1,677 +1,640 @@ #!/usr/bin/env ruby $:.unshift("../lib").unshift("../../lib") if __FILE__ =~ /\.rb$/ require 'mocha' require 'puppettest' require 'puppettest/resourcetesting' require 'puppettest/parsertesting' require 'puppettest/servertest' require 'puppet/network/handler/node' module NodeTesting include PuppetTest Node = Puppet::Network::Handler::Node - SimpleNode = Puppet::Network::Handler::Node::SimpleNode + SimpleNode = Puppet::Node def mk_node_mapper # First, make sure our nodesearch command works as we expect # Make a nodemapper mapper = tempfile() ruby = %x{which ruby}.chomp File.open(mapper, "w") { |f| f.puts "#!#{ruby} require 'yaml' name = ARGV.last.chomp result = {} if name =~ /a/ result[:parameters] = {'one' => ARGV.last + '1', 'two' => ARGV.last + '2'} end if name =~ /p/ result['classes'] = [1,2,3].collect { |n| ARGV.last + n.to_s } end puts YAML.dump(result) " } File.chmod(0755, mapper) mapper end def mk_searcher(name) searcher = Object.new searcher.extend(Node.node_source(name)) searcher.meta_def(:newnode) do |name, *args| SimpleNode.new(name, *args) end searcher end def mk_node_source @node_info = {} @node_source = Node.newnode_source(:testing, :fact_merge => true) do def nodesearch(key) if info = @node_info[key] SimpleNode.new(info) else nil end end end Puppet[:node_source] = "testing" cleanup { Node.rm_node_source(:testing) } end end -class TestNodeInterface < Test::Unit::TestCase +class TestNodeHandler < Test::Unit::TestCase include NodeTesting def setup super mk_node_source end # Make sure that the handler includes the appropriate # node source. def test_initialize # First try it when passing in the node source handler = nil assert_nothing_raised("Could not specify a node source") do handler = Node.new(:Source => :testing) end assert(handler.metaclass.included_modules.include?(@node_source), "Handler did not include node source") # Now use the Puppet[:node_source] Puppet[:node_source] = "testing" assert_nothing_raised("Could not specify a node source") do handler = Node.new() end assert(handler.metaclass.included_modules.include?(@node_source), "Handler did not include node source") # And make sure we throw an exception when an invalid node source is used assert_raise(ArgumentError, "Accepted an invalid node source") do handler = Node.new(:Source => "invalid") end end # Make sure we can find and we cache a fact handler. def test_fact_handler handler = Node.new fhandler = nil assert_nothing_raised("Could not retrieve the fact handler") do fhandler = handler.send(:fact_handler) end assert_instance_of(Puppet::Network::Handler::Facts, fhandler, "Did not get a fact handler back") # Now call it again, making sure we're caching the value. fhandler2 = nil assert_nothing_raised("Could not retrieve the fact handler") do fhandler2 = handler.send(:fact_handler) end assert_instance_of(Puppet::Network::Handler::Facts, fhandler2, "Did not get a fact handler on the second run") assert_equal(fhandler.object_id, fhandler2.object_id, "Did not cache fact handler") end # Make sure we can get node facts from the fact handler. def test_node_facts # Check the case where we find the node. handler = Node.new fhandler = handler.send(:fact_handler) fhandler.expects(:get).with("present").returns("a" => "b") result = nil assert_nothing_raised("Could not get facts from fact handler") do result = handler.send(:node_facts, "present") end assert_equal({"a" => "b"}, result, "Did not get correct facts back") # Now try the case where the fact handler knows nothing about our host fhandler.expects(:get).with('missing').returns(nil) result = nil assert_nothing_raised("Could not get facts from fact handler when host is missing") do result = handler.send(:node_facts, "missing") end assert_equal({}, result, "Did not get empty hash when no facts are known") end # Test our simple shorthand def test_newnode SimpleNode.expects(:new).with("stuff") handler = Node.new handler.send(:newnode, "stuff") end # Make sure we can build up the correct node names to search for def test_node_names handler = Node.new # Verify that the handler asks for the facts if we don't pass them in handler.expects(:node_facts).with("testing").returns({}) handler.send(:node_names, "testing") handler = Node.new # Test it first with no parameters assert_equal(%w{testing}, handler.send(:node_names, "testing"), "Node names did not default to an array including just the node name") # Now test it with a fully qualified name assert_equal(%w{testing.domain.com testing}, handler.send(:node_names, "testing.domain.com"), "Fully qualified names did not get turned into multiple names, longest first") # And try it with a short name + domain fact assert_equal(%w{testing host.domain.com host}, handler.send(:node_names, "testing", "domain" => "domain.com", "hostname" => "host"), "The domain fact was not used to build up an fqdn") # And with an fqdn assert_equal(%w{testing host.domain.com host}, handler.send(:node_names, "testing", "fqdn" => "host.domain.com"), "The fqdn was not used") # And make sure the fqdn beats the domain assert_equal(%w{testing host.other.com host}, handler.send(:node_names, "testing", "domain" => "domain.com", "fqdn" => "host.other.com"), "The domain was used in preference to the fqdn") end # Make sure we can retrieve a whole node by name. def test_details_when_we_find_nodes handler = Node.new # Make sure we get the facts first handler.expects(:node_facts).with("host").returns(:facts) # Find the node names handler.expects(:node_names).with("host", :facts).returns(%w{a b c}) # Iterate across them handler.expects(:nodesearch).with("a").returns(nil) handler.expects(:nodesearch).with("b").returns(nil) # Create an example node to return node = SimpleNode.new("host") # Make sure its source is set node.expects(:source=).with(handler.source) + # And that the names are retained + node.expects(:names=).with(%w{a b c}) + # And make sure we actually get it back handler.expects(:nodesearch).with("c").returns(node) handler.expects(:fact_merge?).returns(true) # Make sure we merge the facts with the node's parameters. node.expects(:fact_merge).with(:facts) # Now call the method result = nil assert_nothing_raised("could not call 'details'") do result = handler.details("host") end assert_equal(node, result, "Did not get correct node back") end # But make sure we pass through to creating default nodes when appropriate. def test_details_using_default_node handler = Node.new # Make sure we get the facts first handler.expects(:node_facts).with("host").returns(:facts) # Find the node names handler.expects(:node_names).with("host", :facts).returns([]) # Create an example node to return node = SimpleNode.new("host") # Make sure its source is set node.expects(:source=).with(handler.source) # And make sure we actually get it back handler.expects(:nodesearch).with("default").returns(node) # This time, have it return false handler.expects(:fact_merge?).returns(false) # And because fact_merge was false, we don't merge them. node.expects(:fact_merge).never # Now call the method result = nil assert_nothing_raised("could not call 'details'") do result = handler.details("host") end assert_equal(node, result, "Did not get correct node back") end # Make sure our handler behaves rationally when it comes to getting environment data. def test_environment # What happens when we can't find the node handler = Node.new handler.expects(:details).with("fake").returns(nil) result = nil assert_nothing_raised("Could not call 'Node.environment'") do result = handler.environment("fake") end assert_nil(result, "Got an environment for a node we could not find") # Now for nodes we can find handler = Node.new node = SimpleNode.new("fake") handler.expects(:details).with("fake").returns(node) node.expects(:environment).returns("dev") result = nil assert_nothing_raised("Could not call 'Node.environment'") do result = handler.environment("fake") end assert_equal("dev", result, "Did not get environment back") end # Make sure our handler behaves rationally when it comes to getting parameter data. def test_parameters # What happens when we can't find the node handler = Node.new handler.expects(:details).with("fake").returns(nil) result = nil assert_nothing_raised("Could not call 'Node.parameters'") do result = handler.parameters("fake") end assert_nil(result, "Got parameters for a node we could not find") # Now for nodes we can find handler = Node.new node = SimpleNode.new("fake") handler.expects(:details).with("fake").returns(node) node.expects(:parameters).returns({"a" => "b"}) result = nil assert_nothing_raised("Could not call 'Node.parameters'") do result = handler.parameters("fake") end assert_equal({"a" => "b"}, result, "Did not get parameters back") end def test_classes # What happens when we can't find the node handler = Node.new handler.expects(:details).with("fake").returns(nil) result = nil assert_nothing_raised("Could not call 'Node.classes'") do result = handler.classes("fake") end assert_nil(result, "Got classes for a node we could not find") # Now for nodes we can find handler = Node.new node = SimpleNode.new("fake") handler.expects(:details).with("fake").returns(node) node.expects(:classes).returns(%w{yay foo}) result = nil assert_nothing_raised("Could not call 'Node.classes'") do result = handler.classes("fake") end assert_equal(%w{yay foo}, result, "Did not get classes back") end -end - -class TestSimpleNode < Test::Unit::TestCase - include NodeTesting - - # Make sure we get all the defaults correctly. - def test_simplenode_initialize - node = nil - assert_nothing_raised("could not create a node without classes or parameters") do - node = SimpleNode.new("testing") - end - assert_equal("testing", node.name, "Did not set name correctly") - assert_equal({}, node.parameters, "Node parameters did not default correctly") - assert_equal([], node.classes, "Node classes did not default correctly") - - # Now test it with values for both - params = {"a" => "b"} - classes = %w{one two} - assert_nothing_raised("could not create a node with classes and parameters") do - node = SimpleNode.new("testing", :parameters => params, :classes => classes) - end - assert_equal("testing", node.name, "Did not set name correctly") - assert_equal(params, node.parameters, "Node parameters did not get set correctly") - assert_equal(classes, node.classes, "Node classes did not get set correctly") - - # And make sure a single class gets turned into an array - assert_nothing_raised("could not create a node with a class as a string") do - node = SimpleNode.new("testing", :classes => "test") - end - assert_equal(%w{test}, node.classes, "A node class string was not converted to an array") - # Make sure we get environments - assert_nothing_raised("could not create a node with an environment") do - node = SimpleNode.new("testing", :environment => "test") - end - assert_equal("test", node.environment, "Environment was not set") + # We reuse the filetimeout for the node caching timeout. + def test_node_caching + handler = Node.new - # Now make sure we get the default env - Puppet[:environment] = "prod" - assert_nothing_raised("could not create a node with no environment") do - node = SimpleNode.new("testing") + node = Object.new + node.metaclass.instance_eval do + attr_accessor :time, :name end - assert_equal("prod", node.environment, "Did not get default environment") + node.time = Time.now + node.name = "yay" - # But that it stays nil if there's no default env set - Puppet[:environment] = "" - assert_nothing_raised("could not create a node with no environment and no default env") do - node = SimpleNode.new("testing") + # Make sure caching works normally + assert_nothing_raised("Could not cache node") do + handler.send(:cache, node) end - assert_nil(node.environment, "Got a default env when none was set") - - end + assert_equal(node.object_id, handler.send(:cached?, "yay").object_id, "Did not get node back from the cache") - # Verify that the node source wins over facter. - def test_fact_merge - node = SimpleNode.new("yay", :parameters => {"a" => "one", "b" => "two"}) + # And that it's returned if we ask for it, instead of creating a new node. + assert_equal(node.object_id, handler.details("yay").object_id, "Did not use cached node") - assert_nothing_raised("Could not merge parameters") do - node.fact_merge("b" => "three", "c" => "yay") - end - params = node.parameters - assert_equal("one", params["a"], "Lost nodesource parameters in parameter merge") - assert_equal("two", params["b"], "Overrode nodesource parameters in parameter merge") - assert_equal("yay", params["c"], "Did not get facts in parameter merge") + # Now set the node's time to be a long time ago + node.time = Time.now - 50000 + assert(! handler.send(:cached?, "yay"), "Timed-out node was returned from cache") end end # Test our configuration object. class TestNodeSources < Test::Unit::TestCase include NodeTesting def test_node_sources mod = nil assert_nothing_raised("Could not add new search type") do mod = Node.newnode_source(:testing) do def nodesearch(name) end end end assert_equal(mod, Node.node_source(:testing), "Did not get node_source back") cleanup do Node.rm_node_source(:testing) assert(! Node.const_defined?("Testing"), "Did not remove constant") end end def test_external_node_source source = Node.node_source(:external) assert(source, "Could not find external node source") mapper = mk_node_mapper searcher = mk_searcher(:external) assert(searcher.fact_merge?, "External node source does not merge facts") # Make sure it gives the right response assert_equal({'classes' => %w{apple1 apple2 apple3}, :parameters => {"one" => "apple1", "two" => "apple2"}}, YAML.load(%x{#{mapper} apple})) # First make sure we get nil back by default assert_nothing_raised { assert_nil(searcher.nodesearch("apple"), "Interp#nodesearch_external defaulted to a non-nil response") } assert_nothing_raised { Puppet[:external_nodes] = mapper } node = nil # Both 'a' and 'p', so we get classes and parameters assert_nothing_raised { node = searcher.nodesearch("apple") } assert_equal("apple", node.name, "node name was not set correctly for apple") assert_equal(%w{apple1 apple2 apple3}, node.classes, "node classes were not set correctly for apple") assert_equal( {"one" => "apple1", "two" => "apple2"}, node.parameters, "node parameters were not set correctly for apple") # A 'p' but no 'a', so we only get classes assert_nothing_raised { node = searcher.nodesearch("plum") } assert_equal("plum", node.name, "node name was not set correctly for plum") assert_equal(%w{plum1 plum2 plum3}, node.classes, "node classes were not set correctly for plum") assert_equal({}, node.parameters, "node parameters were not set correctly for plum") # An 'a' but no 'p', so we only get parameters. assert_nothing_raised { node = searcher.nodesearch("guava")} # no p's, thus no classes assert_equal("guava", node.name, "node name was not set correctly for guava") assert_equal([], node.classes, "node classes were not set correctly for guava") assert_equal({"one" => "guava1", "two" => "guava2"}, node.parameters, "node parameters were not set correctly for guava") assert_nothing_raised { node = searcher.nodesearch("honeydew")} # neither, thus nil assert_nil(node) end # Make sure a nodesearch with arguments works def test_nodesearch_external_arguments mapper = mk_node_mapper Puppet[:external_nodes] = "#{mapper} -s something -p somethingelse" searcher = mk_searcher(:external) node = nil assert_nothing_raised do node = searcher.nodesearch("apple") end assert_instance_of(SimpleNode, node, "did not create node") end # A wrapper test, to make sure we're correctly calling the external search method. def test_nodesearch_external_functional mapper = mk_node_mapper searcher = mk_searcher(:external) Puppet[:external_nodes] = mapper node = nil assert_nothing_raised do node = searcher.nodesearch("apple") end assert_instance_of(SimpleNode, node, "did not create node") end # This can stay in the main test suite because it doesn't actually use ldapsearch, # it just overrides the method so it behaves as though it were hitting ldap. def test_ldap_nodesearch source = Node.node_source(:ldap) assert(source, "Could not find ldap node source") searcher = mk_searcher(:ldap) assert(searcher.fact_merge?, "LDAP node source does not merge facts") nodetable = {} # Override the ldapsearch definition, so we don't have to actually set it up. searcher.meta_def(:ldapsearch) do |name| nodetable[name] end # Make sure we get nothing for nonexistent hosts node = nil assert_nothing_raised do node = searcher.nodesearch("nosuchhost") end assert_nil(node, "Got a node for a non-existent host") # Now add a base node with some classes and parameters nodetable["base"] = [nil, %w{one two}, {"base" => "true"}] assert_nothing_raised do node = searcher.nodesearch("base") end assert_instance_of(SimpleNode, node, "Did not get node from ldap nodesearch") assert_equal("base", node.name, "node name was not set") assert_equal(%w{one two}, node.classes, "node classes were not set") assert_equal({"base" => "true"}, node.parameters, "node parameters were not set") # Now use a different with this as the base nodetable["middle"] = ["base", %w{three}, {"center" => "boo"}] assert_nothing_raised do node = searcher.nodesearch("middle") end assert_instance_of(SimpleNode, node, "Did not get node from ldap nodesearch") assert_equal("middle", node.name, "node name was not set") assert_equal(%w{one two three}.sort, node.classes.sort, "node classes were not set correctly with a parent node") assert_equal({"base" => "true", "center" => "boo"}, node.parameters, "node parameters were not set correctly with a parent node") # And one further, to make sure we fully recurse nodetable["top"] = ["middle", %w{four five}, {"master" => "far"}] assert_nothing_raised do node = searcher.nodesearch("top") end assert_instance_of(SimpleNode, node, "Did not get node from ldap nodesearch") assert_equal("top", node.name, "node name was not set") assert_equal(%w{one two three four five}.sort, node.classes.sort, "node classes were not set correctly with the top node") assert_equal({"base" => "true", "center" => "boo", "master" => "far"}, node.parameters, "node parameters were not set correctly with the top node") end # Make sure we always get a node back from the 'none' nodesource. def test_nodesource_none source = Node.node_source(:none) assert(source, "Could not find 'none' node source") searcher = mk_searcher(:none) assert(searcher.fact_merge?, "'none' node source does not merge facts") # Run a couple of node names through it node = nil %w{192.168.0.1 0:0:0:3:a:f host host.domain.com}.each do |name| assert_nothing_raised("Could not create an empty node with name '%s'" % name) do node = searcher.nodesearch(name) end assert_instance_of(SimpleNode, node, "Did not get a simple node back for %s" % name) assert_equal(name, node.name, "Name was not set correctly") end end end class LdapNodeTest < PuppetTest::TestCase include NodeTesting include PuppetTest::ServerTest include PuppetTest::ParserTesting include PuppetTest::ResourceTesting AST = Puppet::Parser::AST confine "LDAP is not available" => Puppet.features.ldap? confine "No LDAP test data for networks other than Luke's" => Facter.value(:domain) == "madstop.com" def ldapconnect @ldap = LDAP::Conn.new("ldap", 389) @ldap.set_option( LDAP::LDAP_OPT_PROTOCOL_VERSION, 3 ) @ldap.simple_bind("", "") return @ldap end def ldaphost(name) - node = NodeDef.new(:name => name) + node = Puppet::Node.new(name) parent = nil found = false @ldap.search( "ou=hosts, dc=madstop, dc=com", 2, "(&(objectclass=puppetclient)(cn=%s))" % name ) do |entry| node.classes = entry.vals("puppetclass") || [] node.parameters = entry.to_hash.inject({}) do |hash, ary| if ary[1].length == 1 hash[ary[0]] = ary[1].shift else hash[ary[0]] = ary[1] end hash end parent = node.parameters["parentnode"] found = true end raise "Could not find node %s" % name unless found return node, parent end def test_ldapsearch Puppet[:ldapbase] = "ou=hosts, dc=madstop, dc=com" Puppet[:ldapnodes] = true searcher = Object.new searcher.extend(Node.node_source(:ldap)) ldapconnect() # Make sure we get nil and nil back when we search for something missing parent, classes, parameters = nil assert_nothing_raised do parent, classes, parameters = searcher.ldapsearch("nosuchhost") end assert_nil(parent, "Got a parent for a non-existent host") assert_nil(classes, "Got classes for a non-existent host") # Make sure we can find 'culain' in ldap assert_nothing_raised do parent, classes, parameters = searcher.ldapsearch("culain") end node, realparent = ldaphost("culain") assert_equal(realparent, parent, "did not get correct parent node from ldap") assert_equal(node.classes, classes, "did not get correct ldap classes from ldap") assert_equal(node.parameters, parameters, "did not get correct ldap parameters from ldap") # Now compare when we specify the attributes to get. Puppet[:ldapattrs] = "cn" assert_nothing_raised do parent, classes, parameters = searcher.ldapsearch("culain") end assert_equal(realparent, parent, "did not get correct parent node from ldap") assert_equal(node.classes, classes, "did not get correct ldap classes from ldap") list = %w{cn puppetclass parentnode dn} should = node.parameters.inject({}) { |h, a| h[a[0]] = a[1] if list.include?(a[0]); h } assert_equal(should, parameters, "did not get correct ldap parameters from ldap") end end class LdapReconnectTests < PuppetTest::TestCase include NodeTesting include PuppetTest::ServerTest include PuppetTest::ParserTesting include PuppetTest::ResourceTesting AST = Puppet::Parser::AST confine "Not running on culain as root" => (Puppet::Util::SUIDManager.uid == 0 and Facter.value("hostname") == "culain") def test_ldapreconnect Puppet[:ldapbase] = "ou=hosts, dc=madstop, dc=com" Puppet[:ldapnodes] = true searcher = Object.new searcher.extend(Node.node_source(:ldap)) hostname = "culain.madstop.com" # look for our host assert_nothing_raised { parent, classes = searcher.nodesearch(hostname) } # Now restart ldap system("/etc/init.d/slapd restart 2>/dev/null >/dev/null") sleep(1) # and look again assert_nothing_raised { parent, classes = searcher.nodesearch(hostname) } # Now stop ldap system("/etc/init.d/slapd stop 2>/dev/null >/dev/null") cleanup do system("/etc/init.d/slapd start 2>/dev/null >/dev/null") end # And make sure we actually fail here assert_raise(Puppet::Error) { parent, classes = searcher.nodesearch(hostname) } end end diff --git a/test/network/handler/resource.rb b/test/network/handler/resource.rb index 589d22d83..18f52dbd6 100755 --- a/test/network/handler/resource.rb +++ b/test/network/handler/resource.rb @@ -1,294 +1,294 @@ #!/usr/bin/env ruby $:.unshift("../../lib") if __FILE__ =~ /\.rb$/ require 'puppettest' require 'base64' require 'cgi' class TestResourceServer < Test::Unit::TestCase include PuppetTest::ServerTest def verify_described(type, described) described.each do |name, trans| type.clear obj = nil assert_nothing_raised do obj = trans.to_type end assert(obj, "Could not create object") assert_nothing_raised do obj.retrieve end if trans.type == :package assert_equal(Puppet::Type.type(:package).defaultprovider.name, obj[:provider]) end end type.clear end def test_describe_file # Make a file to describe file = tempfile() str = "yayness\n" server = nil assert_nothing_raised do server = Puppet::Network::Handler.resource.new() end # The first run we create the file on the copy, the second run # the file is already there so the object should be in sync 2.times do |i| [ [nil], [[:content, :mode], []], [[], [:content]], [[:content], [:mode]] ].each do |ary| retrieve = ary[0] || [] ignore = ary[1] || [] File.open(file, "w") { |f| f.print str } result = nil assert_nothing_raised do result = server.describe("file", file, *ary) end assert(result, "Could not retrieve file information") assert_instance_of(Puppet::TransObject, result) # Now we have to clear, so that the server's object gets removed Puppet::Type.type(:file).clear # And remove the file, so we can verify it gets recreated if i == 0 File.unlink(file) end object = nil assert_nothing_raised do object = result.to_type end assert(object, "Could not create type") retrieve.each do |property| assert(object.should(property), "Did not retrieve %s" % property) end ignore.each do |property| assert(! object.should(property), "Incorrectly retrieved %s" % property) end if i == 0 assert_events([:file_created], object) else assert_nothing_raised { assert(object.insync?(object.retrieve), "Object was not in sync") } end assert(FileTest.exists?(file), "File did not get recreated") if i == 0 if object.should(:content) assert_equal(str, File.read(file), "File contents are not the same") else assert_equal("", File.read(file), "File content was incorrectly made") end end if FileTest.exists? file File.unlink(file) end end end end def test_describe_directory # Make a file to describe file = tempfile() server = nil assert_nothing_raised do server = Puppet::Network::Handler.resource.new() end [ [nil], [[:ensure, :checksum, :mode], []], [[], [:checksum]], [[:ensure, :checksum], [:mode]] ].each do |ary| retrieve = ary[0] || [] ignore = ary[1] || [] Dir.mkdir(file) result = nil assert_nothing_raised do result = server.describe("file", file, *ary) end assert(result, "Could not retrieve file information") assert_instance_of(Puppet::TransObject, result) # Now we have to clear, so that the server's object gets removed Puppet::Type.type(:file).clear # And remove the file, so we can verify it gets recreated Dir.rmdir(file) object = nil assert_nothing_raised do object = result.to_type end assert(object, "Could not create type") retrieve.each do |property| assert(object.should(property), "Did not retrieve %s" % property) end ignore.each do |property| assert(! object.should(property), "Incorrectly retrieved %s" % property) end #assert_apply(object) assert_events([:directory_created], object) assert(FileTest.directory?(file), "Directory did not get recreated") Dir.rmdir(file) end end def test_describe_alltypes # Systems get pretty retarded, so I'm going to set the path to some fake # data for ports #Puppet::Type::ParsedType::Port.path = File.join(basedir, # "test/data/types/ports/1") #Puppet.err Puppet::Type::ParsedType::Port.path server = nil assert_nothing_raised do server = Puppet::Network::Handler.resource.new() end require 'etc' # Make the example schedules, for testing Puppet::Type.type(:schedule).mkdefaultschedules Puppet::Type.eachtype do |type| unless type.respond_to? :instances Puppet.warning "%s does not respond to :instances" % type.name next end next unless type.name == :package Puppet.info "Describing each %s" % type.name # First do a listing from the server bucket = nil assert_nothing_raised { bucket = server.list(type.name) } #type.clear count = 0 described = {} bucket.each do |obj| assert_instance_of(Puppet::TransObject, obj) break if count > 5 described[obj.name] = server.describe(obj.type, obj.name) count += 1 end verify_described(type, described) count = 0 described = {} Puppet.info "listing again" type.instances.each do |obj| assert_instance_of(type, obj) break if count > 5 trans = nil assert_nothing_raised do described[obj.name] = server.describe(type.name, obj.name) end count += 1 end if described.empty? Puppet.notice "Got no example objects for %s" % type.name end # We separate these, in case the list operation creates objects verify_described(type, described) end end def test_apply server = nil assert_nothing_raised do - server = Puppet::Network::Handler.resource.new() + server = Puppet::Network::Handler.resource.new(:Local => false) end file = tempfile() str = "yayness\n" File.open(file, "w") { |f| f.print str } filetrans = nil assert_nothing_raised { filetrans = server.describe("file", file) } Puppet::Type.type(:file).clear bucket = Puppet::TransBucket.new bucket.type = "file" bucket.push filetrans oldbucket = bucket.dup File.unlink(file) assert_nothing_raised { server.apply(bucket) } assert(FileTest.exists?(file), "File did not get recreated") # Now try it as a "nonlocal" server server.local = false yaml = nil assert_nothing_raised { yaml = Base64.encode64(YAML::dump(bucket)) } Puppet::Type.type(:file).clear File.unlink(file) if Base64.decode64(yaml) =~ /(.{20}Loglevel.{20})/ Puppet.warning "YAML is broken on this machine" return end # puts Base64.decode64(yaml) objects = nil assert_nothing_raised("Could not reload yaml") { YAML::load(Base64.decode64(yaml)) } # The server is supposed to accept yaml and execute it. assert_nothing_raised { server.apply(yaml) } assert(FileTest.exists?(file), "File did not get recreated from YAML") end end # $Id$ diff --git a/test/network/server/webrick.rb b/test/network/server/webrick.rb index 69f23f3c2..a9448fe6c 100755 --- a/test/network/server/webrick.rb +++ b/test/network/server/webrick.rb @@ -1,146 +1,147 @@ #!/usr/bin/env ruby $:.unshift("../../lib") if __FILE__ =~ /\.rb$/ require 'puppettest' require 'puppet/network/server/webrick' require 'mocha' class TestWebrickServer < Test::Unit::TestCase include PuppetTest::ServerTest def setup Puppet::Util::SUIDManager.stubs(:asuser).yields super end # Make sure we can create a server, and that it knows how to create its # certs by default. def test_basics server = nil assert_raise(Puppet::Error, "server succeeded with no cert") do server = Puppet::Network::Server::WEBrick.new( :Port => @@port, :Handlers => { :Status => nil } ) end assert_nothing_raised("Could not create simple server") do server = Puppet::Network::Server::WEBrick.new( :Port => @@port, :Handlers => { :CA => {}, # so that certs autogenerate :Status => nil } ) end assert(server, "did not create server") assert(server.cert, "did not retrieve cert") end # test that we can connect to the server # we have to use fork here, because we apparently can't use threads # to talk to other threads def test_connect_with_fork Puppet[:autosign] = true serverpid, server = mk_status_server # create a status client, and verify it can talk client = mk_status_client assert(client.cert, "did not get cert for client") retval = nil assert_nothing_raised("Could not connect to server") { retval = client.status } assert_equal(1, retval) end # Test that a client whose cert has been revoked really can't connect def test_certificate_revocation Puppet[:autosign] = true serverpid, server = mk_status_server client = mk_status_client status = nil assert_nothing_raised() { status = client.status } assert_equal(1, status) client.shutdown # Revoke the client's cert ca = Puppet::SSLCertificates::CA.new() ca.revoke(ca.getclientcert(Puppet[:certname])[0].serial) # Restart the server @@port += 1 Puppet[:autosign] = false kill_and_wait(serverpid, server.pidfile) serverpid, server = mk_status_server - client = mk_status_client - # This time the client should be denied - assert_raise(Puppet::Network::XMLRPCClientError) { - client.status + # This time the client should be denied. With keep-alive, + # the client starts its connection immediately, thus throwing + # the error. + assert_raise(OpenSSL::SSL::SSLError) { + client = Puppet::Network::Client.status.new(:Server => "localhost", :Port => @@port) } end def mk_status_client client = nil # Otherwise, the client initalization will trip over itself # since elements created in the last run are still around Puppet::Type::allclear assert_nothing_raised() { client = Puppet::Network::Client.status.new( :Server => "localhost", :Port => @@port ) } client end def mk_status_server server = nil assert_nothing_raised() { server = Puppet::Network::Server::WEBrick.new( :Port => @@port, :Handlers => { :CA => {}, # so that certs autogenerate :Status => nil } ) } pid = fork { Puppet[:name] = "puppetmasterd" assert_nothing_raised() { trap(:INT) { server.shutdown } server.start } } @@tmppids << pid [pid, server] end def kill_and_wait(pid, file) %x{kill -INT #{pid} 2>/dev/null} count = 0 while count < 30 && File::exist?(file) count += 1 sleep(1) end assert(count < 30, "Killing server #{pid} failed") end end # $Id$ diff --git a/test/network/xmlrpc/processor.rb b/test/network/xmlrpc/processor.rb index 101d268b2..6808d0100 100755 --- a/test/network/xmlrpc/processor.rb +++ b/test/network/xmlrpc/processor.rb @@ -1,81 +1,81 @@ #!/usr/bin/env ruby $:.unshift("../../lib") if __FILE__ =~ /\.rb$/ require 'puppettest' require 'puppet/network/xmlrpc/processor' require 'mocha' class TestXMLRPCProcessor < Test::Unit::TestCase include PuppetTest class BaseProcessor def add_handler(interface, handler) @handlers ||= {} @handlers[interface] = handler end end # We use a base class just so super() works with add_handler. class Processor < BaseProcessor include Puppet::Network::XMLRPCProcessor def set_service_hook(&block) meta_def(:service, &block) end end def setup super Puppet::Util::SUIDManager.stubs(:asuser).yields @processor = Processor.new end def test_handlers ca = Puppet::Network::Handler.ca @processor.send(:setup_processor) assert(! @processor.handler_loaded?(:ca), "already have ca handler loaded") assert_nothing_raised do @processor.add_handler(ca.interface, ca.new()) end assert(@processor.handler_loaded?(:puppetca), "ca handler not loaded by symbol") assert(@processor.handler_loaded?("puppetca"), "ca handler not loaded by string") end def test_process ca = Puppet::Network::Handler.ca @processor.send(:setup_processor) assert_nothing_raised do @processor.add_handler(ca.interface, ca.new()) end fakeparser = Class.new do def parseMethodCall(data) return data end end request = Puppet::Network::ClientRequest.new("fake", "192.168.0.1", false) request.handler = "myhandler" request.method = "mymethod" @processor.expects(:parser).returns(fakeparser.new) request.expects(:handler=).with("myhandler") request.expects(:method=).with("mymethod") - @processor.expects(:verify).times(2) + @processor.stubs(:verify) @processor.expects(:handle).with(request.call, "params", request.name, request.ip) @processor.send(:process, ["myhandler.mymethod", ["params"]], request) end def test_setup_processor @processor.expects(:set_service_hook) @processor.send(:setup_processor) end end # $Id$ diff --git a/test/puppet/defaults.rb b/test/puppet/defaults.rb index 19db3fe05..383153e45 100755 --- a/test/puppet/defaults.rb +++ b/test/puppet/defaults.rb @@ -1,99 +1,73 @@ #!/usr/bin/env ruby $:.unshift("../lib").unshift("../../lib") if __FILE__ =~ /\.rb$/ require 'puppet' require 'puppettest' # $Id$ class TestPuppetDefaults < Test::Unit::TestCase include PuppetTest @@dirs = %w{rrddir confdir vardir logdir statedir} @@files = %w{statefile manifest masterlog} @@normals = %w{puppetport masterport server} @@booleans = %w{rrdgraph noop} def testVersion assert( Puppet.version =~ /^[0-9]+(\.[0-9]+)*/, "got invalid version number %s" % Puppet.version ) end def testStringOrParam [@@dirs,@@files,@@booleans].flatten.each { |param| assert_nothing_raised { Puppet[param] } assert_nothing_raised { Puppet[param.intern] } } end def test_valuesForEach [@@dirs,@@files,@@booleans].flatten.each { |param| param = param.intern assert_nothing_raised { Puppet[param] } } end def testValuesForEach [@@dirs,@@files,@@booleans].flatten.each { |param| assert_nothing_raised { Puppet[param] } } end - if __FILE__ == $0 - def disabled_testContained - confdir = Regexp.new(Puppet[:confdir]) - vardir = Regexp.new(Puppet[:vardir]) - [@@dirs,@@files].flatten.each { |param| - value = Puppet[param] - - unless value =~ confdir or value =~ vardir - assert_nothing_raised { raise "%s is in wrong dir: %s" % - [param,value] } - end - } - end - end - - def testArgumentTypes - assert_raise(ArgumentError) { Puppet[["string"]] } - assert_raise(ArgumentError) { Puppet[[:symbol]] } - end - - def testFailOnBogusArgs - [0, "ashoweklj", ";"].each { |param| - assert_raise(ArgumentError, "No error on %s" % param) { Puppet[param] } - } - end - # we don't want user defaults in /, or root defaults in ~ def testDefaultsInCorrectRoots notval = nil if Puppet::Util::SUIDManager.uid == 0 notval = Regexp.new(File.expand_path("~")) else notval = /^\/var|^\/etc/ end [@@dirs,@@files].flatten.each { |param| value = Puppet[param] unless value !~ notval assert_nothing_raised { raise "%s is incorrectly set to %s" % [param,value] } end } end def test_settingdefaults testvals = { :fakeparam => "$confdir/yaytest", :anotherparam => "$vardir/goodtest", :string => "a yay string", :boolean => true } testvals.each { |param, default| assert_nothing_raised { Puppet.setdefaults("testing", param => [default, "a value"]) } } end end diff --git a/test/puppet/modules.rb b/test/puppet/modules.rb deleted file mode 100755 index 15976c64a..000000000 --- a/test/puppet/modules.rb +++ /dev/null @@ -1,59 +0,0 @@ -#!/usr/bin/env ruby - -$:.unshift("../lib").unshift("../../lib") if __FILE__ =~ /\.rb$/ - -require 'puppettest' - -class TestModules < Test::Unit::TestCase - include PuppetTest - - def setup - super - @varmods = File::join(Puppet[:vardir], "modules") - FileUtils::mkdir_p(@varmods) - end - - def test_modulepath - Puppet[:modulepath] = "$vardir/modules:/no/such/path/anywhere:.::" - assert_equal([ @varmods ], Puppet::Module.modulepath) - end - - def test_find - assert_nil(Puppet::Module::find("/tmp")) - - file = "testmod/something" - assert_nil(Puppet::Module::find(file)) - - path = File::join(@varmods, "testmod") - FileUtils::mkdir_p(path) - - mod = Puppet::Module::find("testmod") - assert_not_nil(mod) - assert_equal("testmod", mod.name) - assert_equal(path, mod.path) - - mod = Puppet::Module::find(file) - assert_not_nil(mod) - assert_equal("testmod", mod.name) - assert_equal(path, mod.path) - end - - def test_find_template - templ = "testmod/templ.erb" - assert_equal(File::join(Puppet[:templatedir], templ), - Puppet::Module::find_template(templ)) - - templ_path = File::join(@varmods, "testmod", - Puppet::Module::TEMPLATES, "templ.erb") - FileUtils::mkdir_p(File::dirname(templ_path)) - File::open(templ_path, "w") { |f| f.puts "Howdy" } - - assert_equal(templ_path, Puppet::Module::find_template(templ)) - - mod = Puppet::Module::find(templ) - assert_not_nil(mod) - assert_equal(templ_path, mod.template(templ)) - end -end - -# $Id$ diff --git a/test/rails/ast.rb b/test/rails/ast.rb index b205aa0d5..fb6374401 100755 --- a/test/rails/ast.rb +++ b/test/rails/ast.rb @@ -1,73 +1,74 @@ #!/usr/bin/env ruby $:.unshift("../lib").unshift("../../lib") if __FILE__ =~ /\.rb$/ require 'puppettest' require 'puppet/rails' require 'puppet/parser/parser' require 'puppettest/resourcetesting' require 'puppettest/parsertesting' require 'puppettest/railstesting' require 'puppettest/support/collection' class TestRailsAST < PuppetTest::TestCase confine "Missing rails" => Puppet.features.rails? include PuppetTest::RailsTesting include PuppetTest::ParserTesting include PuppetTest::ResourceTesting include PuppetTest::Support::Collection def test_exported_collexp railsinit Puppet[:storeconfigs] = true - @interp, @scope, @source = mkclassframing + + @scope = mkscope # make a rails resource railsresource "file", "/tmp/testing", :owner => "root", :group => "bin", :mode => "644" run_collection_queries(:exported) do |string, result, query| code = nil str = nil # We don't support more than one search criteria at the moment. retval = nil bad = false # Figure out if the search is for anything rails will ignore if string =~ /\band\b|\bor\b/ bad = true else bad = false end # And if it is, make sure we throw an error. if bad assert_raise(Puppet::ParseError, "Evaluated '#{string}'") do str, code = query.evaluate :scope => @scope end next else assert_nothing_raised("Could not evaluate '#{string}'") do str, code = query.evaluate :scope => @scope end end assert_nothing_raised("Could not find resource") do retval = Puppet::Rails::Resource.find(:all, :include => {:param_values => :param_name}, :conditions => str) end if result assert_equal(1, retval.length, "Did not find resource with '#{string}'") res = retval.shift assert_equal("file", res.restype) assert_equal("/tmp/testing", res.title) else assert_equal(0, retval.length, "found a resource with '#{string}'") end end end end # $Id$ diff --git a/test/rails/collection.rb b/test/rails/collection.rb index d878641be..56f71e635 100755 --- a/test/rails/collection.rb +++ b/test/rails/collection.rb @@ -1,247 +1,241 @@ #!/usr/bin/env ruby $:.unshift("../lib").unshift("../../lib") if __FILE__ =~ /\.rb$/ require 'puppet' require 'puppet/rails' require 'puppettest' require 'puppettest/railstesting' require 'puppettest/resourcetesting' # A separate class for testing rails integration class TestRailsCollection < PuppetTest::TestCase confine "Missing rails support" => Puppet.features.rails? include PuppetTest include PuppetTest::ParserTesting include PuppetTest::ResourceTesting include PuppetTest::RailsTesting Parser = Puppet::Parser AST = Parser::AST def setup super Puppet[:trace] = false - @interp, @scope, @source = mkclassframing + @scope = mkscope + @scope.compile.send(:evaluate_main) end def test_collect_exported railsinit - # Set a hostname - @scope.host = Facter.value(:hostname) - # make an exported resource exported = mkresource(:type => "file", :title => "/tmp/exported", :exported => true, :params => {:owner => "root"}) - @scope.setresource exported + @scope.compile.store_resource @scope, exported assert(exported.exported?, "Object was not marked exported") assert(exported.virtual?, "Object was not marked virtual") # And a non-exported real = mkresource(:type => "file", :title => "/tmp/real", :params => {:owner => "root"}) - @scope.setresource real + @scope.compile.store_resource @scope, real # Now make a collector coll = nil assert_nothing_raised do coll = Puppet::Parser::Collector.new(@scope, "file", nil, nil, :exported) end # Set it in our scope - @scope.newcollection(coll) + @scope.compile.add_collection(coll) # Make sure it's in the collections - assert_equal([coll], @scope.collections) + assert_equal([coll], @scope.compile.collections) # And try to collect the virtual resources. ret = nil assert_nothing_raised do ret = coll.collect_exported end assert_equal([exported], ret) # Now make sure evaluate does the right thing. assert_nothing_raised do ret = coll.evaluate end # Make sure that the collection does not find the resource on the # next run. ret = nil assert_nothing_raised do ret = coll.collect_exported end assert(ret.empty?, "Exported resource was collected on the second run") # And make sure our exported object is no longer exported assert(! exported.virtual?, "Virtual object did not get realized") # But it should still be marked exported. assert(exported.exported?, "Resource got un-exported") # Now make a new collector of a different type and make sure it # finds nothing. assert_nothing_raised do coll = Puppet::Parser::Collector.new(@scope, "exec", nil, nil, :exported) end # Remark this as virtual exported.virtual = true assert_nothing_raised do ret = coll.evaluate end assert(! ret, "got resources back") # Now create a whole new scope and make sure we can actually retrieve # the resource from the database, not just from the scope. # First create a host object and store our resource in it. # Now collect our facts facts = {} Facter.each do |fact, value| facts[fact] = value end # Now try storing our crap # Remark this as exported exported.exported = true - host = Puppet::Rails::Host.store( - :resources => [exported], - :facts => facts, - :name => facts["hostname"] - ) + exported.scope.stubs(:tags).returns([]) + node = mknode(facts["hostname"]) + node.parameters = facts + host = Puppet::Rails::Host.store(node, [exported]) assert(host, "did not get rails host") host.save # And make sure it's in there newres = host.resources.find_by_restype_and_title_and_exported("file", "/tmp/exported", true) assert(newres, "Did not find resource in db") - interp, scope, source = mkclassframing - scope.host = "two" + assert(newres.exported?, "Resource was not exported") + + # Make a new set with a different node name + node = mknode("other") + compile = Puppet::Parser::Compile.new(node, mkparser) + compile.send(:evaluate_main) + compile.topscope.source = mock("source") + + # It's important that it's a different name, since same-name resources are ignored. + assert_equal("other", compile.node.name, "Did not get correct node name") # Now make a collector coll = nil assert_nothing_raised do - coll = Puppet::Parser::Collector.new(scope, "file", nil, nil, :exported) + coll = Puppet::Parser::Collector.new(compile.topscope, "file", nil, nil, :exported) end - # Set it in our scope - scope.newcollection(coll) - - # Make sure it's in the collections - assert_equal([coll], scope.collections) - # And try to collect the virtual resources. ret = nil - assert_nothing_raised do + assert_nothing_raised("Could not collect exported resources") do ret = coll.collect_exported end - assert_equal(["/tmp/exported"], ret.collect { |f| f.title }) + assert_equal(["/tmp/exported"], ret.collect { |f| f.title }, "Did not find resource in collction") # Make sure we can evaluate the same collection multiple times and # that later collections do nothing assert_nothing_raised("Collection found same resource twice") do ret = coll.evaluate end end def test_collection_conflicts railsinit # First make a railshost we can conflict with host = Puppet::Rails::Host.new(:name => "myhost") host.resources.build(:title => "/tmp/conflicttest", :restype => "file", :exported => true) host.save # Now make a normal resource normal = mkresource(:type => "file", :title => "/tmp/conflicttest", :params => {:owner => "root"}) - @scope.setresource normal - @scope.host = "otherhost" + @scope.compile.store_resource @scope, normal # Now make a collector coll = nil assert_nothing_raised do coll = Puppet::Parser::Collector.new(@scope, "file", nil, nil, :exported) end # And try to collect the virtual resources. assert_raise(Puppet::ParseError) do ret = coll.collect_exported end end # Make sure we do not collect resources from the host we're on def test_no_resources_from_me railsinit # Make our configuration - host = Puppet::Rails::Host.new(:name => "myhost") + host = Puppet::Rails::Host.new(:name => @scope.host) host.resources.build(:title => "/tmp/hosttest", :restype => "file", :exported => true) host.save - @scope.host = "myhost" - # Now make a collector coll = nil assert_nothing_raised do coll = Puppet::Parser::Collector.new(@scope, "file", nil, nil, :exported) end # And make sure we get nada back ret = nil assert_nothing_raised do ret = coll.collect_exported end assert(ret.empty?, "Found exports from our own host") end # #731 -- we're collecting all resources, not just exported resources. def test_only_collecting_exported_resources railsinit # Make our configuration host = Puppet::Rails::Host.new(:name => "myhost") host.resources.build(:title => "/tmp/exporttest1", :restype => "file", :exported => true) host.resources.build(:title => "/tmp/exporttest2", :restype => "file", :exported => false) host.save - @scope.host = "otherhost" - # Now make a collector coll = nil assert_nothing_raised do coll = Puppet::Parser::Collector.new(@scope, "file", nil, nil, :exported) end # And make sure we get nada back ret = nil assert_nothing_raised do ret = coll.collect_exported end names = ret.collect { |res| res.title } assert_equal(%w{/tmp/exporttest1}, names, "Collected incorrect resource list") end end # $Id$ diff --git a/test/rails/interpreter.rb b/test/rails/configuration.rb similarity index 57% rename from test/rails/interpreter.rb rename to test/rails/configuration.rb index 0eba3f590..752ea5375 100755 --- a/test/rails/interpreter.rb +++ b/test/rails/configuration.rb @@ -1,91 +1,82 @@ #!/usr/bin/env ruby $:.unshift("../lib").unshift("../../lib") if __FILE__ =~ /\.rb$/ require 'puppettest' -require 'puppet/parser/interpreter' require 'puppet/parser/parser' require 'puppet/network/client' require 'puppet/rails' require 'puppettest/resourcetesting' require 'puppettest/parsertesting' require 'puppettest/servertest' require 'puppettest/railstesting' -class InterpreterRailsTests < PuppetTest::TestCase +class ConfigurationRailsTests < PuppetTest::TestCase include PuppetTest include PuppetTest::ServerTest include PuppetTest::ParserTesting include PuppetTest::ResourceTesting include PuppetTest::RailsTesting AST = Puppet::Parser::AST - NodeDef = Puppet::Parser::Interpreter::NodeDef confine "No rails support" => Puppet.features.rails? # We need to make sure finished objects are stored in the db. def test_finish_before_store railsinit - interp = mkinterp + compile = mkcompile + compile.ast_nodes = true + parser = compile.parser - node = interp.newnode ["myhost"], :code => AST::ASTArray.new(:children => [ + node = parser.newnode [compile.node.name], :code => AST::ASTArray.new(:children => [ resourcedef("file", "/tmp/yay", :group => "root"), defaultobj("file", :owner => "root") ]) - interp.newclass "myclass", :code => AST::ASTArray.new(:children => [ - ]) - - interp.newclass "sub", :parent => "myclass", - :code => AST::ASTArray.new(:children => [ - resourceoverride("file", "/tmp/yay", :owner => "root") - ] - ) - # Now do the rails crap Puppet[:storeconfigs] = true - interp.evaluate("myhost", {}) - - # And then retrieve the object from rails - #res = Puppet::Rails::Resource.find_by_restype_and_title("file", "/tmp/yay", :include => {:param_values => :param_names}) - res = Puppet::Rails::Resource.find_by_restype_and_title("file", "/tmp/yay") - - assert(res, "Did not get resource from rails") - - params = res.parameters - - assert_equal(["root"], params["owner"], "Did not get correct value for owner param") + Puppet::Rails::Host.expects(:store).with do |node, resources| + if res = resources.find { |r| r.type == "file" and r.title == "/tmp/yay" } + assert_equal("root", res["owner"], "Did not set default on resource") + true + else + raise "Resource was not passed to store()" + end + end + compile.compile end def test_hoststorage assert_nothing_raised { Puppet[:storeconfigs] = true } file = tempfile() File.open(file, "w") { |f| f.puts "file { \"/etc\": owner => root }" } interp = nil assert_nothing_raised { interp = Puppet::Parser::Interpreter.new( :Manifest => file, :UseNodes => false, :ForkSave => false ) } facts = {} Facter.each { |fact, val| facts[fact] = val } + node = mknode(facts["hostname"]) + node.parameters = facts objects = nil assert_nothing_raised { - objects = interp.run(facts["hostname"], facts) + objects = interp.compile(node) } - obj = Puppet::Rails::Host.find_by_name(facts["hostname"]) + obj = Puppet::Rails::Host.find_by_name(node.name) assert(obj, "Could not find host object") end end diff --git a/test/rails/host.rb b/test/rails/host.rb index f3190c047..5ac2f763e 100755 --- a/test/rails/host.rb +++ b/test/rails/host.rb @@ -1,202 +1,192 @@ #!/usr/bin/env ruby $:.unshift("../lib").unshift("../../lib") if __FILE__ =~ /\.rb$/ require 'puppet' require 'puppet/rails' require 'puppet/parser/interpreter' require 'puppet/parser/parser' require 'puppet/network/client' require 'puppettest' require 'puppettest/parsertesting' require 'puppettest/resourcetesting' require 'puppettest/railstesting' class TestRailsHost < PuppetTest::TestCase confine "Missing ActiveRecord" => Puppet.features.rails? include PuppetTest::ParserTesting include PuppetTest::ResourceTesting include PuppetTest::RailsTesting def setup super railsinit if Puppet.features.rails? end def teardown railsteardown if Puppet.features.rails? super end def test_includerails assert_nothing_raised { require 'puppet/rails' } end def test_store - @interp, @scope, @source = mkclassframing + @scope = mkscope # First make some objects resources = [] 4.times { |i| # Make a file resources << mkresource(:type => "file", :title => "/tmp/file#{i.to_s}", :params => {:owner => "user#{i}"}) # And an exec, so we're checking multiple types resources << mkresource(:type => "exec", :title => "/bin/echo file#{i.to_s}", :params => {:user => "user#{i}"}) } # Now collect our facts facts = {"hostname" => Facter.value(:hostname), "test1" => "funtest", "ipaddress" => Facter.value(:ipaddress)} # Now try storing our crap host = nil + node = mknode(facts["hostname"]) + node.parameters = facts assert_nothing_raised { - host = Puppet::Rails::Host.store( - :resources => resources, - :facts => facts, - :name => facts["hostname"], - :classes => ["one", "two::three", "four"] - ) + host = Puppet::Rails::Host.store(node, resources) } assert(host, "Did not create host") host = nil assert_nothing_raised { host = Puppet::Rails::Host.find_by_name(facts["hostname"]) } assert(host, "Could not find host object") assert(host.resources, "No objects on host") facts.each do |fact, value| assert_equal(value, host.fact(fact)[0].value, "fact %s is wrong" % fact) end assert_equal(facts["ipaddress"], host.ip, "IP did not get set") count = 0 host.resources.each do |resource| assert_equal(host, resource.host) count += 1 i = nil if resource[:title] =~ /file([0-9]+)/ i = $1 else raise "Got weird resource %s" % resource.inspect end assert(resource[:restype] != "", "Did not get a type from the resource") case resource["restype"] when "file": assert_equal("user#{i}", resource.parameter("owner"), "got no owner for %s" % resource.ref) when "exec": assert_equal("user#{i}", resource.parameter("user"), "got no user for %s" % resource.ref) else raise "Unknown type %s" % resource[:restype].inspect end end assert_equal(8, count, "Did not get enough resources") # Now remove a couple of resources resources.reject! { |r| r.title =~ /file3/ } # Change a few resources resources.find_all { |r| r.title =~ /file2/ }.each do |r| - r.set("loglevel", "notice", r.source) + r.send(:set_parameter, "loglevel", "notice") end # And add a new resource resources << mkresource(:type => "file", :title => "/tmp/file_added", :params => {:owner => "user_added"}) # And change some facts facts["test2"] = "yaytest" facts["test3"] = "funtest" facts["test1"] = "changedfact" facts.delete("ipaddress") host = nil + node = mknode(facts["hostname"]) + node.parameters = facts assert_nothing_raised { - host = Puppet::Rails::Host.store( - :resources => resources, - :facts => facts, - :name => facts["hostname"], - :classes => ["one", "two::three", "four"] - ) + host = Puppet::Rails::Host.store(node, resources) } # Make sure it sets the last_compile time assert_nothing_raised do assert_instance_of(Time, host.last_compile, "did not set last_compile") end assert_equal(0, host.fact('ipaddress').size, "removed fact was not deleted") facts.each do |fact, value| assert_equal(value, host.fact(fact)[0].value, "fact %s is wrong" % fact) end # And check the changes we made. assert(! host.resources.find(:all).detect { |r| r.title =~ /file3/ }, "Removed resources are still present") res = host.resources.find_by_title("/tmp/file_added") assert(res, "New resource was not added") assert_equal("user_added", res.parameter("owner"), "user info was not stored") host.resources.find(:all, :conditions => [ "title like ?", "%file2%"]).each do |r| assert_equal("notice", r.parameter("loglevel"), "loglevel was not added") end end def test_freshness_connect_update Puppet::Rails.init Puppet[:storeconfigs] = true # this is the default server setup - master = Puppet::Network::Handler.master.new( + master = Puppet::Network::Handler.configuration.new( :Code => "", :UseNodes => true, :Local => true ) # Create a host Puppet::Rails::Host.new(:name => "test", :ip => "192.168.0.3").save assert_nothing_raised("Failed to update last_connect for unknown host") do - master.freshness("created",'192.168.0.1') + master.version("created",'192.168.0.1') end # Make sure it created the host created = Puppet::Rails::Host.find_by_name("created") assert(created, "Freshness did not create host") assert(created.last_freshcheck, "Did not set last_freshcheck on created host") - assert_equal("192.168.0.1", created.ip, - "Did not set IP address on created host") # Now check on the existing host assert_nothing_raised("Failed to update last_connect for unknown host") do - master.freshness("test",'192.168.0.2') + master.version("test",'192.168.0.2') end # Recreate it, so we're not using the cached object. host = Puppet::Rails::Host.find_by_name("test") # Make sure it created the host assert(host.last_freshcheck, "Did not set last_freshcheck on existing host") - assert_equal("192.168.0.3", host.ip, - "Overrode IP on found host") end end # $Id$ diff --git a/test/rails/railsparameter.rb b/test/rails/railsparameter.rb index 82d978bb4..89c81ad30 100755 --- a/test/rails/railsparameter.rb +++ b/test/rails/railsparameter.rb @@ -1,73 +1,73 @@ #!/usr/bin/env ruby $:.unshift("../lib").unshift("../../lib") if __FILE__ =~ /\.rb$/ require 'puppet' require 'puppet/rails' require 'puppettest' require 'puppettest/railstesting' # Don't do any tests w/out this class if defined? ActiveRecord::Base class TestRailsParameter < Test::Unit::TestCase include PuppetTest::RailsTesting def params {"myname" => "myval", "multiple" => %w{one two three}} end # Create a resource param from a rails parameter def test_to_resourceparam railsinit # Now create a source - interp = mkinterp - source = interp.newclass "myclass" + parser = mkparser + source = parser.newclass "myclass" host = Puppet::Rails::Host.new(:name => "myhost") resource = host.resources.create( :title => "/tmp/to_resource", :restype => "file", :exported => true) # Use array and non-array values, to make sure we get things back in # the same form. params.each do |name, value| param = Puppet::Rails::ParamName.find_or_create_by_name(name) if value.is_a? Array values = value else values = [value] end valueobjects = values.collect do |v| resource.param_values.create(:value => v, :param_name => param) end assert(param, "Did not create rails parameter") # The id doesn't get assigned until we save end resource.save # And try to convert our parameter params.each do |name, value| param = Puppet::Rails::ParamName.find_by_name(name) pp = nil assert_nothing_raised do pp = param.to_resourceparam(resource, source) end assert_instance_of(Puppet::Parser::Resource::Param, pp) assert_equal(name.to_sym, pp.name, "parameter name was not equal") assert_equal(value, pp.value, "value was not equal for %s" % value.inspect) end end end else $stderr.puts "Install Rails for Rails and Caching tests" end # $Id$ diff --git a/test/rails/railsresource.rb b/test/rails/railsresource.rb index b8e5450b3..ca582b8b0 100755 --- a/test/rails/railsresource.rb +++ b/test/rails/railsresource.rb @@ -1,249 +1,251 @@ #!/usr/bin/env ruby $:.unshift("../lib").unshift("../../lib") if __FILE__ =~ /\.rb$/ require 'puppet' require 'puppet/rails' require 'puppettest' require 'puppettest/railstesting' require 'puppettest/resourcetesting' +require 'puppettest/parsertesting' # Don't do any tests w/out this class if Puppet.features.rails? class TestRailsResource < Test::Unit::TestCase include PuppetTest::RailsTesting include PuppetTest::ResourceTesting + include PuppetTest::ParserTesting def setup super railsinit end def teardown railsteardown super end def mktest_resource # We need a host for resources host = Puppet::Rails::Host.new(:name => "myhost") # Now build a resource resource = host.resources.create( :title => "/tmp/to_resource", :restype => "file", :exported => true) # Now add some params params.each do |param, value| pn = Puppet::Rails::ParamName.find_or_create_by_name(param) pv = resource.param_values.create(:value => value, :param_name => pn) end host.save return resource end def params {"owner" => "root", "mode" => "644"} end # Create a resource param from a rails parameter def test_to_resource resource = mktest_resource # We need a scope - interp, scope, source = mkclassframing + scope = mkscope # Find the new resource and include all it's parameters. resource = Puppet::Rails::Resource.find_by_id(resource.id) # Now, try to convert our resource to a real resource res = nil assert_nothing_raised do res = resource.to_resource(scope) end assert_instance_of(Puppet::Parser::Resource, res) assert_equal("root", res[:owner]) assert_equal("644", res[:mode]) assert_equal("/tmp/to_resource", res.title) - assert_equal(source, res.source) + assert_equal(scope.source, res.source) end def test_parameters resource = mktest_resource setparams = nil assert_nothing_raised do setparams = resource.parameters.inject({}) { |h, a| h[a[0]] = a[1][0] h } end assert_equal(params, setparams, "Did not get the right answer from #parameters") end # Make sure we can retrieve individual parameters by name. def test_parameter resource = mktest_resource params.each do |p,v| assert_equal(v, resource.parameter(p), "%s is not correct" % p) end end end else $stderr.puts "Install Rails for Rails and Caching tests" end # A separate class for testing rails integration class TestExportedResources < PuppetTest::TestCase include PuppetTest include PuppetTest::ParserTesting include PuppetTest::ResourceTesting include PuppetTest::RailsTesting Parser = Puppet::Parser AST = Parser::AST Reference = Puppet::Parser::Resource::Reference def setup super Puppet[:trace] = false - @interp, @scope, @source = mkclassframing + @scope = mkscope end confine "Missing rails support" => Puppet.features.rails? # Compare a parser resource to a rails resource. def compare_resources(host, res, updating, options = {}) newobj = nil # If the resource is in the db, then use modify_rails, else use to_rails if newobj = Puppet::Rails::Resource.find_by_restype_and_title(res.type, res.title) assert_nothing_raised("Call to modify_rails failed") do res.modify_rails(newobj) end else assert_nothing_raised("Call to to_rails failed") do newobj = res.to_rails(host) end end assert_instance_of(Puppet::Rails::Resource, newobj) newobj.save if updating tail = "on update" else tail = "" end # Make sure we find our object and only our object count = 0 obj = nil Puppet::Rails::Resource.find(:all).each do |obj| assert_equal(newobj.id, obj.id, "A new resource was created instead of modifying an existing resource") count += 1 [:title, :restype, :line, :exported].each do |param| if param == :restype method = :type else method = param end assert_equal(res.send(method), obj[param], "attribute %s was not set correctly in rails %s" % [param, tail]) end end assert_equal(1, count, "Got too many resources %s" % tail) # Now make sure we can find it again assert_nothing_raised do obj = Puppet::Rails::Resource.find_by_restype_and_title( res.type, res.title, :include => :param_names ) end assert_instance_of(Puppet::Rails::Resource, obj) # Make sure we get the parameters back params = options[:params] || [obj.param_names.collect { |p| p.name }, res.to_hash.keys].flatten.collect { |n| n.to_s }.uniq params.each do |name| param = obj.param_names.find_by_name(name) if res[name] assert(param, "resource did not keep %s %s" % [name, tail]) else assert(! param, "resource did not delete %s %s" % [name, tail]) end if param values = param.param_values.collect { |pv| pv.value } should = res[param.name] should = [should] unless should.is_a?(Array) assert_equal(should, values, "%s was different %s" % [param.name, tail]) end end return obj end def test_to_rails railsteardown railsinit ref1 = Reference.new :type => "exec", :title => "one" ref2 = Reference.new :type => "exec", :title => "two" res = mkresource :type => "file", :title => "/tmp/testing", :source => @source, :scope => @scope, :params => {:owner => "root", :source => ["/tmp/A", "/tmp/B"], :mode => "755", :require => [ref1, ref2], :subscribe => ref1} res.line = 50 # We also need a Rails Host to store under host = Puppet::Rails::Host.new(:name => Facter.hostname) railsres = compare_resources(host, res, false, :params => %w{owner source mode}) # Now make sure our parameters did not change assert_instance_of(Array, res[:require], "Parameter array changed") res[:require].each do |ref| assert_instance_of(Reference, ref, "Resource reference changed") end assert_instance_of(Reference, res[:subscribe], "Resource reference changed") # And make sure that the rails resource actually has resource references params = railsres.parameters [params["subscribe"], params["require"]].flatten.each do |ref| assert_instance_of(Reference, ref, "Resource reference is no longer a reference") end # Now make some changes to our resource. We're removing the mode, # changing the source, and adding 'check'. res = mkresource :type => "file", :title => "/tmp/testing", :source => @source, :scope => @scope, :params => {:owner => "bin", :source => ["/tmp/A", "/tmp/C"], :check => "checksum", :require => [ref1, ref2], :subscribe => ref2} res.line = 75 res.exported = true railsres = compare_resources(host, res, true, :params => %w{owner source mode check}) # Again make sure our parameters did not change assert_instance_of(Array, res[:require], "Parameter array changed") res[:require].each do |ref| assert_instance_of(Reference, ref, "Resource reference changed") end # Again with the serialization checks params = railsres.parameters [params["subscribe"], params["require"]].flatten.each do |ref| assert_instance_of(Reference, ref, "Resource reference is no longer a reference") end end end # $Id$ diff --git a/test/ral/manager/type.rb b/test/ral/manager/type.rb index cbe969fb3..534c35759 100755 --- a/test/ral/manager/type.rb +++ b/test/ral/manager/type.rb @@ -1,820 +1,837 @@ #!/usr/bin/env ruby $:.unshift("../../lib") if __FILE__ =~ /\.rb$/ require 'puppettest' require 'mocha' class TestType < Test::Unit::TestCase include PuppetTest def test_typemethods Puppet::Type.eachtype { |type| name = nil assert_nothing_raised("Searching for name for %s caused failure" % type.to_s) { name = type.name } assert(name, "Could not find name for %s" % type.to_s) assert_equal( type, Puppet::Type.type(name), "Failed to retrieve %s by name" % name ) # Skip types with no parameters or valid properties #unless ! type.parameters.empty? or ! type.validproperties.empty? # next #end assert_nothing_raised { assert( type.namevar, "Failed to retrieve namevar for %s" % name ) assert_not_nil( type.properties, "Properties for %s are nil" % name ) assert_not_nil( type.validproperties, "Valid properties for %s are nil" % name ) } } end def test_stringvssymbols file = nil path = tempfile() assert_nothing_raised() { system("rm -f %s" % path) file = Puppet.type(:file).create( :path => path, :ensure => "file", :recurse => true, :checksum => "md5" ) } assert_nothing_raised() { file.retrieve } assert_nothing_raised() { file.evaluate } Puppet.type(:file).clear assert_nothing_raised() { system("rm -f %s" % path) file = Puppet.type(:file).create( "path" => path, "ensure" => "file", "recurse" => true, "checksum" => "md5" ) } assert_nothing_raised() { file.retrieve } assert_nothing_raised() { file[:path] } assert_nothing_raised() { file["path"] } assert_nothing_raised() { file[:recurse] } assert_nothing_raised() { file["recurse"] } assert_nothing_raised() { file.evaluate } end # This was supposed to test objects whose name was a property, but that # fundamentally doesn't make much sense, and we now don't have any such # types. def disabled_test_nameasproperty # currently groups are the only objects with the namevar as a property group = nil assert_nothing_raised { group = Puppet.type(:group).create( :name => "testing" ) } assert_equal("testing", group.name, "Could not retrieve name") end # Verify that values get merged correctly def test_mergepropertyvalues file = tempfile() # Create the first version assert_nothing_raised { Puppet.type(:file).create( :path => file, :owner => ["root", "bin"] ) } # Make sure no other statements are allowed assert_raise(Puppet::Error) { Puppet.type(:file).create( :path => file, :group => "root" ) } end # Verify that aliasing works def test_aliasing file = tempfile() baseobj = nil assert_nothing_raised { baseobj = Puppet.type(:file).create( :name => file, :ensure => "file", :alias => ["funtest"] ) } # Verify our adding ourselves as an alias isn't an error. assert_nothing_raised { baseobj[:alias] = file } assert_instance_of(Puppet.type(:file), Puppet.type(:file)["funtest"], "Could not retrieve alias") end # Verify that requirements don't depend on file order def test_prereqorder one = tempfile() two = tempfile() twoobj = nil oneobj = nil assert_nothing_raised("Could not create prereq that doesn't exist yet") { twoobj = Puppet.type(:file).create( :name => two, :require => [:file, one] ) } assert_nothing_raised { oneobj = Puppet.type(:file).create( :name => one ) } comp = newcomp(twoobj, oneobj) assert_nothing_raised { comp.finalize } assert(twoobj.requires?(oneobj), "Requirement was not created") end # Verify that names are aliases, not equivalents def test_nameasalias file = nil # Create the parent dir, so we make sure autorequiring the parent dir works parentdir = tempfile() dir = Puppet.type(:file).create( :name => parentdir, :ensure => "directory" ) assert_apply(dir) path = File.join(parentdir, "subdir") name = "a test file" transport = Puppet::TransObject.new(name, "file") transport[:path] = path transport[:ensure] = "file" assert_nothing_raised { file = transport.to_type } assert_equal(path, file[:path]) assert_equal(name, file.title) assert_nothing_raised { file.retrieve } assert_apply(file) assert(Puppet.type(:file)[name], "Could not look up object by name") end def test_ensuredefault user = nil assert_nothing_raised { user = Puppet.type(:user).create( :name => "pptestAA", :check => [:uid] ) } # make sure we don't get :ensure for unmanaged files assert(! user.property(:ensure), "User got an ensure property") assert_nothing_raised { user = Puppet.type(:user).create( :name => "pptestAB", :comment => "Testingness" ) } # but make sure it gets added once we manage them assert(user.property(:ensure), "User did not add ensure property") assert_nothing_raised { user = Puppet.type(:user).create( :name => "pptestBC", :comment => "A fake user" ) } # and make sure managed objects start with them assert(user.property(:ensure), "User did not get an ensure property") end # Make sure removal works def test_remove objects = {} top = Puppet.type(:component).create(:name => "top") objects[top.class] = top base = tempfile() # now make a two-tier, 5 piece tree %w{a b}.each do |letter| name = "comp%s" % letter comp = Puppet.type(:component).create(:name => name) top.push comp objects[comp.class] = comp 5.times do |i| file = base + letter + i.to_s obj = Puppet.type(:file).create(:name => file, :ensure => "file") comp.push obj objects[obj.class] = obj end end assert_nothing_raised do top.remove end objects.each do |klass, obj| assert_nil(klass[obj.name], "object %s was not removed" % obj.name) end end # Verify that objects can't be their own children. def test_object_recursion comp = Puppet.type(:component).create(:name => "top") file = Puppet.type(:component).create(:name => "middle") assert_raise(Puppet::DevError) do comp.push(comp) end assert_raise(Puppet::DevError) do file.push(file) end assert_raise(Puppet::DevError) do comp.parent = comp end assert_raise(Puppet::DevError) do file.parent = file end assert_nothing_raised { comp.push(file) } assert_raise(Puppet::DevError) do file.push(comp) end assert_raise(Puppet::DevError) do comp.parent = file end end def test_newtype_methods assert_nothing_raised { Puppet::Type.newtype(:mytype) do newparam(:wow) do isnamevar end end } assert(Puppet::Type.respond_to?(:newmytype), "new method did not get created") obj = nil assert_nothing_raised { obj = Puppet::Type.newmytype(:wow => "yay") } assert(obj.is_a?(Puppet::Type.type(:mytype)), "Obj is not the correct type") # Now make the type again, just to make sure it works on refreshing. assert_nothing_raised { Puppet::Type.newtype(:mytype) do newparam(:yay) do isnamevar end end } obj = nil # Make sure the old class was thrown away and only the new one is sitting # around. assert_raise(Puppet::Error) { obj = Puppet::Type.newmytype(:wow => "yay") } assert_nothing_raised { obj = Puppet::Type.newmytype(:yay => "yay") } # Now make sure that we don't replace existing, non-type methods parammethod = Puppet::Type.method(:newparam) assert_nothing_raised { Puppet::Type.newtype(:param) do newparam(:rah) do isnamevar end end } assert_equal(parammethod, Puppet::Type.method(:newparam), "newparam method got replaced by newtype") end def test_newproperty_options # Create a type with a fake provider providerclass = Class.new do def self.supports_parameter?(prop) return true end def method_missing(method, *args) return method end end self.class.const_set("ProviderClass", providerclass) type = Puppet::Type.newtype(:mytype) do newparam(:name) do isnamevar end def provider @provider ||= ProviderClass.new @provider end end # Now make a property with no options. property = nil assert_nothing_raised do property = type.newproperty(:noopts) do end end # Now create an instance obj = type.create(:name => :myobj) inst = property.new(:resource => obj) # And make sure it's correctly setting @is ret = nil assert_nothing_raised { ret = inst.retrieve } assert_equal(:noopts, inst.retrieve) # Now create a property with a different way of doing it property = nil assert_nothing_raised do property = type.newproperty(:setretrieve, :retrieve => :yayness) end inst = property.new(:resource => obj) # And make sure it's correctly setting @is ret = nil assert_nothing_raised { ret = inst.retrieve } assert_equal(:yayness, ret) end def test_name_vs_title path = tempfile() trans = nil assert_nothing_raised { trans = Puppet::TransObject.new(path, :file) } file = nil assert_nothing_raised { file = Puppet::Type.newfile(trans) } assert(file.respond_to?(:title), "No 'title' method") assert(file.respond_to?(:name), "No 'name' method") assert_equal(file.title, file.name, "Name and title were not marked equal") assert_nothing_raised { file.title = "My file" } assert_equal("My file", file.title) assert_equal(path, file.name) end # Make sure the title is sufficiently differentiated from the namevar. def test_title_at_creation_with_hash file = nil fileclass = Puppet::Type.type(:file) path = tempfile() assert_nothing_raised do file = fileclass.create( :title => "Myfile", :path => path ) end assert_equal("Myfile", file.title, "Did not get correct title") assert_equal(path, file[:name], "Did not get correct name") file = nil Puppet::Type.type(:file).clear # Now make sure we can specify both and still get the right answers assert_nothing_raised do file = fileclass.create( :title => "Myfile", :name => path ) end assert_instance_of(fileclass, file) assert_equal("Myfile", file.title, "Did not get correct title") assert_equal(path, file[:name], "Did not get correct name") end # Make sure the "create" class method behaves appropriately. def test_class_create title = "Myfile" validate = proc do |element| assert(element, "Did not create file") assert_instance_of(Puppet::Type.type(:file), element) assert_equal(title, element.title, "Title is not correct") end type = :file args = {:path => tempfile(), :owner => "root"} trans = Puppet::TransObject.new(title, type) args.each do |name, val| trans[name] = val end # First call it on the appropriate typeclass obj = nil assert_nothing_raised do obj = Puppet::Type.type(:file).create(trans) end validate.call(obj) # Now try it using the class method on Type oldid = obj.object_id obj = nil Puppet::Type.type(:file).clear assert_nothing_raised { obj = Puppet::Type.create(trans) } validate.call(obj) assert(oldid != obj.object_id, "Got same object back") # Now try the same things with hashes instead of a transobject oldid = obj.object_id obj = nil Puppet::Type.type(:file).clear hash = { :type => :file, :title => "Myfile", :path => tempfile(), :owner => "root" } # First call it on the appropriate typeclass obj = nil assert_nothing_raised do obj = Puppet::Type.type(:file).create(hash) end validate.call(obj) assert_equal(:file, obj.should(:type), "Type param did not pass through") assert(oldid != obj.object_id, "Got same object back") # Now try it using the class method on Type oldid = obj.object_id obj = nil Puppet::Type.type(:file).clear assert_nothing_raised { obj = Puppet::Type.create(hash) } validate.call(obj) assert(oldid != obj.object_id, "Got same object back") assert_nil(obj.should(:type), "Type param passed through") end def test_multiplenames obj = nil path = tempfile() assert_raise ArgumentError do obj = Puppet::Type.type(:file).create( :name => path, :path => path ) end end def test_title_and_name obj = nil path = tempfile() fileobj = Puppet::Type.type(:file) assert_nothing_raised do obj = fileobj.create( :title => "myfile", :path => path ) end assert_equal(obj, fileobj["myfile"], "Could not retrieve obj by title") assert_equal(obj, fileobj[path], "Could not retrieve obj by name") end # Make sure default providers behave correctly def test_defaultproviders # Make a fake type type = Puppet::Type.newtype(:defaultprovidertest) do newparam(:name) do end end basic = type.provide(:basic) do defaultfor :operatingsystem => :somethingelse, :operatingsystemrelease => :yayness end assert_equal(basic, type.defaultprovider) type.defaultprovider = nil greater = type.provide(:greater) do defaultfor :operatingsystem => Facter.value("operatingsystem") end assert_equal(greater, type.defaultprovider) end # Make sure that we can have multiple isomorphic objects with the same name, # but not with non-isomorphic objects. def test_isomorphic_names # First do execs, since they're not isomorphic. echo = Puppet::Util.binary "echo" exec1 = exec2 = nil assert_nothing_raised do exec1 = Puppet::Type.type(:exec).create( :title => "exec1", :command => "#{echo} funtest" ) end assert_nothing_raised do exec2 = Puppet::Type.type(:exec).create( :title => "exec2", :command => "#{echo} funtest" ) end assert_apply(exec1, exec2) # Now do files, since they are. This should fail. file1 = file2 = nil path = tempfile() assert_nothing_raised do file1 = Puppet::Type.type(:file).create( :title => "file1", :path => path, :content => "yayness" ) end # This will fail, but earlier systems will catch it. assert_raise(Puppet::Error) do file2 = Puppet::Type.type(:file).create( :title => "file2", :path => path, :content => "rahness" ) end assert(file1, "Did not create first file") assert_nil(file2, "Incorrectly created second file") end def test_tags obj = Puppet::Type.type(:file).create(:path => tempfile()) tags = [:some, :test, :tags] obj.tags = tags assert_equal(tags + [:file], obj.tags) end def disabled_test_list Puppet::Type.loadall Puppet::Type.eachtype do |type| next if type.name == :symlink next if type.name == :component next if type.name == :tidy assert(type.respond_to?(:list), "%s does not respond to list" % type.name) end end def test_to_hash file = Puppet::Type.newfile :path => tempfile(), :owner => "luke", :recurse => true, :loglevel => "warning" hash = nil assert_nothing_raised do hash = file.to_hash end [:path, :owner, :recurse, :loglevel].each do |param| assert(hash[param], "Hash did not include %s" % param) end end # Make sure that classes behave like hashes. def test_class_hash_behaviour path = tempfile() filetype = Puppet::Type.type(:file) one = Puppet::Type.newfile :path => path assert_equal(one, filetype[path], "Did not get file back") assert_raise(Puppet::Error) do filetype[path] = one end end def test_ref path = tempfile() Puppet::Type.type(:exec) # uggh, the methods need to load the types file = Puppet::Type.newfile(:path => path) assert_equal("File[#{path}]", file.ref) exec = Puppet::Type.newexec(:title => "yay", :command => "/bin/echo yay") assert_equal("Exec[yay]", exec.ref) end def test_noop_metaparam file = Puppet::Type.newfile :path => tempfile assert(!file.noop, "file incorrectly in noop") assert_nothing_raised do file[:noop] = true end assert(file.noop, "file should be in noop") # Now set the main one Puppet[:noop] = true assert(file.noop, "file should be in noop") file[:noop] = false assert(file.noop, "file should be in noop") end def test_path # Check that our paths are built correctly. Just pick a random, "normal" type. type = Puppet::Type.type(:exec) mk = Proc.new do |i, hash| hash[:title] = "exec%s" % i hash[:command] = "/bin/echo" type.create(hash) end exec = mk.call(1, {}) assert_equal("/Exec[exec1]", exec.path) comp = Puppet::Type.newcomponent :title => "My[component]", :type => "Yay" exec = mk.call(2, :parent => comp) assert_equal("/My[component]/Exec[exec2]", exec.path) comp = Puppet::Type.newcomponent :name => "Other[thing]" exec = mk.call(3, :parent => comp) assert_equal("/Other[thing]/Exec[exec3]", exec.path) comp = Puppet::Type.newcomponent :type => "server", :name => "server" exec = mk.call(4, :parent => comp) assert_equal("/server/Exec[exec4]", exec.path) - comp = Puppet::Type.newcomponent :type => "whatever", :name => "main[top]" + comp = Puppet::Type.newcomponent :type => "whatever", :name => "class[main]" exec = mk.call(5, :parent => comp) assert_equal("//Exec[exec5]", exec.path) comp = Puppet::Type.newcomponent :type => "yay", :name => "Good[bad]", :parent => comp exec = mk.call(6, :parent => comp) assert_equal("//Good[bad]/Exec[exec6]", exec.path) end def test_evaluate faketype = Puppet::Type.newtype(:faketype) do newparam(:name) {} end cleanup { Puppet::Type.rmtype(:faketype) } faketype.provide(:fake) do def prefetch end end obj = faketype.create :name => "yayness", :provider => :fake assert(obj, "did not create object") obj.provider.expects(:prefetch) obj.expects(:retrieve) obj.expects(:propertychanges).returns([]) obj.expects(:cache) obj.evaluate end # Partially test #704, but also cover the rest of the schedule management bases. def test_schedule Puppet::Type.type(:schedule).create(:name => "maint") {"maint" => true, nil => false, :fail => :fail}.each do |name, should| args = {:name => tempfile, :ensure => :file} if name args[:schedule] = name end resource = Puppet::Type.type(:file).create(args) if should == :fail assert_raise(Puppet::Error, "Did not fail on missing schedule") do resource.schedule end else sched = nil assert_nothing_raised("Failed when schedule was %s" % sched) do sched = resource.schedule end if should assert_equal(name, sched.name, "did not get correct schedule back") end end end end -end -# $Id$ + # #801 -- resources only checked in noop should be rescheduled immediately. + def test_reschedule_when_noop + Puppet::Type.type(:schedule).mkdefaultschedules + file = Puppet::Type.type(:file).create(:path => "/tmp/whatever", :mode => "755", :noop => true, :schedule => :daily, :ensure => :file) + + assert(file.noop?, "File not considered in noop") + assert(file.scheduled?, "File is not considered scheduled") + + file.evaluate + + assert_nil(file.cached(:checked), "Stored a checked time when running in noop mode when there were changes") + file.cache(:checked, nil) + + file.stubs(:propertychanges).returns([]) + + file.evaluate + assert_instance_of(Time, file.cached(:checked), "Did not store a checked time when running in noop mode when there were no changes") + end +end diff --git a/test/ral/types/cron.rb b/test/ral/types/cron.rb index 9a3466821..7b2e770f0 100755 --- a/test/ral/types/cron.rb +++ b/test/ral/types/cron.rb @@ -1,505 +1,504 @@ #!/usr/bin/env ruby $:.unshift("../../lib") if __FILE__ =~ /\.rb$/ require 'puppettest' -require 'spec' # Test cron job creation, modification, and destruction class TestCron < Test::Unit::TestCase include PuppetTest def setup super setme() @crontype = Puppet::Type.type(:cron) @provider = @crontype.defaultprovider if @provider.respond_to?(:filetype=) @oldfiletype = @provider.filetype @provider.filetype = :ram end @crontype = Puppet::Type.type(:cron) end def teardown super @crontype.defaultprovider = nil if defined? @oldfiletype @provider.filetype = @oldfiletype end Puppet::Util::FileType.filetype(:ram).clear end def eachprovider @crontype.suitableprovider.each do |provider| yield provider end end # Back up the user's existing cron tab if they have one. def cronback tab = nil assert_nothing_raised { tab = Puppet.type(:cron).filetype.read(@me) } if $? == 0 @currenttab = tab else @currenttab = nil end end # Restore the cron tab to its original form. def cronrestore assert_nothing_raised { if @currenttab @crontype.filetype.new(@me).write(@currenttab) else @crontype.filetype.new(@me).remove end } end # Create a cron job with all fields filled in. def mkcron(name, addargs = true) cron = nil command = "date > %s/crontest%s" % [tmpdir(), name] args = nil if addargs args = { :command => command, :name => name, :user => @me, :minute => rand(59), :month => "1", :monthday => "1", :hour => "1" } else args = {:command => command, :name => name} end assert_nothing_raised { cron = @crontype.create(args) } return cron end # Run the cron through its paces -- install it then remove it. def cyclecron(cron) obj = Puppet::Type::Cron.cronobj(@me) text = obj.read name = cron.name comp = newcomp(name, cron) assert_events([:cron_created], comp) cron.provider.class.prefetch currentvalue = cron.retrieve assert(cron.insync?(currentvalue), "Cron is not in sync") assert_events([], comp) curtext = obj.read text.split("\n").each do |line| assert(curtext.include?(line), "Missing '%s'" % line) end obj = Puppet::Type::Cron.cronobj(@me) cron[:ensure] = :absent assert_events([:cron_removed], comp) cron.provider.class.prefetch currentvalue = cron.retrieve assert(cron.insync?(currentvalue), "Cron is not in sync") assert_events([], comp) end # Test that a cron job with spaces at the end doesn't get rewritten def test_trailingspaces eachprovider do |provider| cron = nil # make the cron name = "yaytest" command = "date > /dev/null " assert_nothing_raised { cron = @crontype.create( :name => name, :command => "date > /dev/null ", :month => "May", :user => @me ) } property = cron.send(:property, :command) cron.provider.command = command cron.provider.ensure = :present cron.provider.user = @me cron.provider.month = ["4"] cron.provider.class.prefetch currentvalue = cron.retrieve currentvalue.each do |prop, value| # We're only interested in comparing the command. next unless prop.name.to_s == "command" assert(prop.insync?(value), "Property %s is not considered in sync with value %s" % [prop.name, value.inspect]) end @crontype.clear end end def test_makeandretrievecron %w{storeandretrieve a-name another-name more_naming SomeName}.each do |name| cron = mkcron(name) comp = newcomp(name, cron) trans = assert_events([:cron_created], comp, name) cron.provider.class.prefetch cron = nil assert(cron = Puppet.type(:cron)[name], "Could not retrieve named cron") assert_instance_of(Puppet.type(:cron), cron) end end # Do input validation testing on all of the parameters. def test_arguments values = { :monthday => { :valid => [ 1, 13, "1" ], :invalid => [ -1, 0, 32 ] }, :weekday => { :valid => [ 0, 3, 6, "1", "tue", "wed", "Wed", "MOnday", "SaTurday" ], :invalid => [ -1, 7, "13", "tues", "teusday", "thurs" ] }, :hour => { :valid => [ 0, 21, 23 ], :invalid => [ -1, 24 ] }, :minute => { :valid => [ 0, 34, 59 ], :invalid => [ -1, 60 ] }, :month => { :valid => [ 1, 11, 12, "mar", "March", "apr", "October", "DeCeMbEr" ], :invalid => [ -1, 0, 13, "marc", "sept" ] } } cron = mkcron("valtesting") values.each { |param, hash| # We have to test the valid ones first, because otherwise the # property will fail to create at all. [:valid, :invalid].each { |type| hash[type].each { |value| case type when :valid: assert_nothing_raised { cron[param] = value } if value.is_a?(Integer) assert_equal([value.to_s], cron.should(param), "Cron value was not set correctly") end when :invalid: assert_raise(Puppet::Error, "%s is incorrectly a valid %s" % [value, param]) { cron[param] = value } end if value.is_a?(Integer) value = value.to_s redo end } } } end # Verify that comma-separated numbers are not resulting in rewrites def test_comma_separated_vals_work eachprovider do |provider| cron = nil assert_nothing_raised { cron = @crontype.create( :command => "/bin/date > /dev/null", :minute => [0, 30], :name => "crontest", :provider => provider.name ) } cron.provider.ensure = :present cron.provider.command = '/bin/date > /dev/null' cron.provider.minute = %w{0 30} cron.provider.class.prefetch currentvalue = cron.retrieve currentvalue.each do |prop, value| # We're only interested in comparing minutes. next unless prop.name.to_s == "minute" assert(prop.insync?(value), "Property %s is not considered in sync with value %s" % [prop.name, value.inspect]) end @crontype.clear end end def test_fieldremoval cron = nil assert_nothing_raised { cron = @crontype.create( :command => "/bin/date > /dev/null", :minute => [0, 30], :name => "crontest", :provider => :crontab ) } assert_events([:cron_created], cron) cron.provider.class.prefetch cron[:minute] = :absent assert_events([:cron_changed], cron) current_values = nil assert_nothing_raised { cron.provider.class.prefetch current_values = cron.retrieve } assert_equal(:absent, current_values[cron.property(:minute)]) end def test_listing # Make a crontab cron for testing provider = @crontype.provider(:crontab) return unless provider.suitable? ft = provider.filetype provider.filetype = :ram cleanup { provider.filetype = ft } setme cron = @crontype.create(:name => "testing", :minute => [0, 30], :command => "/bin/testing", :user => @me ) # Write it to our file assert_apply(cron) @crontype.clear crons = [] assert_nothing_raised { @crontype.instances.each do |cron| crons << cron end } crons.each do |cron| assert_instance_of(@crontype, cron, "Did not receive a real cron object") assert_instance_of(String, cron.value(:user), "Cron user is not a string") end end def verify_failonnouser assert_raise(Puppet::Error) do @crontype.retrieve("nosuchuser") end end def test_divisionnumbers cron = mkcron("divtest") cron[:minute] = "*/5" assert_apply(cron) cron.provider.class.prefetch currentvalue = cron.retrieve assert_equal(["*/5"], currentvalue[cron.property(:minute)]) end def test_ranges cron = mkcron("rangetest") cron[:minute] = "2-4" assert_apply(cron) current_values = nil assert_nothing_raised { cron.provider.class.prefetch current_values = cron.retrieve } assert_equal(["2-4"], current_values[cron.property(:minute)]) end def provider_set(cron, param, value) unless param =~ /=$/ param = "%s=" % param end cron.provider.send(param, value) end def test_value cron = mkcron("valuetesting", false) # First, test the normal properties [:minute, :hour, :month].each do |param| cron.newattr(param) property = cron.property(param) assert(property, "Did not get %s property" % param) assert_nothing_raised { # property.is = :absent provider_set(cron, param, :absent) } val = "*" assert_equal(val, cron.value(param)) # Make sure arrays work, too provider_set(cron, param, ["1"]) assert_equal(%w{1}, cron.value(param)) # Make sure values get comma-joined provider_set(cron, param, %w{2 3}) assert_equal(%w{2 3}, cron.value(param)) # Make sure "should" values work, too cron[param] = "4" assert_equal(%w{4}, cron.value(param)) cron[param] = ["4"] assert_equal(%w{4}, cron.value(param)) cron[param] = ["4", "5"] assert_equal(%w{4 5}, cron.value(param)) provider_set(cron, param, :absent) assert_equal(%w{4 5}, cron.value(param)) end Puppet[:trace] = false # Now make sure that :command works correctly cron.delete(:command) cron.newattr(:command) property = cron.property(:command) assert_nothing_raised { provider_set(cron, :command, :absent) } param = :command # Make sure arrays work, too provider_set(cron, param, ["/bin/echo"]) assert_equal("/bin/echo", cron.value(param)) # Make sure values are not comma-joined provider_set(cron, param, %w{/bin/echo /bin/test}) assert_equal("/bin/echo", cron.value(param)) # Make sure "should" values work, too cron[param] = "/bin/echo" assert_equal("/bin/echo", cron.value(param)) cron[param] = ["/bin/echo"] assert_equal("/bin/echo", cron.value(param)) cron[param] = %w{/bin/echo /bin/test} assert_equal("/bin/echo", cron.value(param)) provider_set(cron, param, :absent) assert_equal("/bin/echo", cron.value(param)) end def test_multiple_users crons = [] users = ["root", nonrootuser.name] users.each do |user| cron = Puppet::Type.type(:cron).create( :name => "testcron-#{user}", :user => user, :command => "/bin/echo", :minute => [0,30] ) crons << cron assert_equal(cron.should(:user), cron.should(:target), "Target was not set correctly for %s" % user) end provider = crons[0].provider.class assert_apply(*crons) users.each do |user| users.each do |other| next if user == other text = provider.target_object(other).read assert(text !~ /testcron-#{user}/, "%s's cron job is in %s's tab" % [user, other]) end end end # Make sure the user stuff defaults correctly. def test_default_user crontab = @crontype.provider(:crontab) if crontab.suitable? inst = @crontype.create( :name => "something", :command => "/some/thing", :provider => :crontab) assert_equal(ENV["USER"], inst.should(:user), "user did not default to current user with crontab") assert_equal(ENV["USER"], inst.should(:target), "target did not default to current user with crontab") # Now make a new cron with a user, and make sure it gets copied # over inst = @crontype.create(:name => "yay", :command => "/some/thing", :user => "bin", :provider => :crontab) assert_equal("bin", inst.should(:target), "target did not default to user with crontab") end end # #705 - make sure extra spaces don't screw things up def test_spaces_in_command string = "echo multiple spaces" cron = @crontype.create(:name => "space testing", :command => string) assert_apply(cron) cron.class.clear cron = @crontype.create(:name => "space testing", :command => string) # Now make sure that it's correctly in sync cron.provider.class.prefetch("testing" => cron) properties = cron.retrieve command, result = properties.find { |prop, value| prop.name == :command } assert_equal(string, result, "Cron did not pick up extra spaces in command") assert(command.insync?(string), "Command changed with multiple spaces") end end # $Id$ diff --git a/test/tagging/tagging.rb b/test/tagging/tagging.rb deleted file mode 100755 index afab3faa4..000000000 --- a/test/tagging/tagging.rb +++ /dev/null @@ -1,170 +0,0 @@ -#!/usr/bin/env ruby - -$:.unshift("../lib").unshift("../../lib") if __FILE__ =~ /\.rb$/ - -require 'puppet' -require 'puppettest' -require 'puppettest/parsertesting' -require 'puppettest/resourcetesting' - -class TestTagging < Test::Unit::TestCase - include PuppetTest - include PuppetTest::ParserTesting - include PuppetTest::ResourceTesting - - # Make sure the scopes are getting the right tags - def test_scopetags - scope = nil - assert_nothing_raised { - scope = mkscope - scope.name = "yayness" - scope.type = "solaris" - } - - assert_nothing_raised { - assert_equal(%w{solaris}, scope.tags, "Incorrect scope tags") - } - end - - # Test deeper tags, where a scope gets all of its parent scopes' tags - def test_deepscopetags - scope = nil - assert_nothing_raised { - scope = mkscope - scope.name = "yayness" - scope.type = "solaris" - scope = scope.newscope - scope.name = "booness" - scope.type = "apache" - } - - assert_nothing_raised { - # Scopes put their own tags first - assert_equal(%w{apache solaris}, scope.tags, "Incorrect scope tags") - } - end - - # Verify that the tags make their way to the objects - def test_objecttags - scope = nil - assert_nothing_raised { - scope = mkscope - scope.name = "yayness" - scope.type = "solaris" - } - - resource = mkresource :type => "file", :title => "/tmp/testing", - :params => {:owner => "root"}, :file => "/yay", :line => 1, - :scope => scope - - assert_nothing_raised { - scope.setresource(resource) - } - - assert_nothing_raised { - assert_equal(%w{solaris file}, resource.tags, - "Incorrect tags") - } - end - - # Make sure that specifying tags results in only those objects getting - # run. - def test_tagspecs - a = tempfile() - b = tempfile() - - afile = Puppet.type(:file).create( - :path => a, - :ensure => :file - ) - afile.tag("a") - - bfile = Puppet.type(:file).create( - :path => b, - :ensure => :file - ) - bfile.tag(:b) - - # First, make sure they get created when no spec'ed tags - assert_events([:file_created,:file_created], afile, bfile) - assert(FileTest.exists?(a), "A did not get created") - assert(FileTest.exists?(b), "B did not get created") - File.unlink(a) - File.unlink(b) - - # Set the tags to a - assert_nothing_raised { - Puppet[:tags] = "a" - } - - assert_events([:file_created], afile, bfile) - assert(FileTest.exists?(a), "A did not get created") - assert(!FileTest.exists?(b), "B got created") - File.unlink(a) - - # Set the tags to b - assert_nothing_raised { - Puppet[:tags] = "b" - } - - assert_events([:file_created], afile, bfile) - assert(!FileTest.exists?(a), "A got created") - assert(FileTest.exists?(b), "B did not get created") - File.unlink(b) - - # Set the tags to something else - assert_nothing_raised { - Puppet[:tags] = "c" - } - - assert_events([], afile, bfile) - assert(!FileTest.exists?(a), "A got created") - assert(!FileTest.exists?(b), "B got created") - - # Now set both tags - assert_nothing_raised { - Puppet[:tags] = "b, a" - } - - assert_events([:file_created, :file_created], afile, bfile) - assert(FileTest.exists?(a), "A did not get created") - assert(FileTest.exists?(b), "B did not get created") - File.unlink(a) - - end - - def test_metaparamtag - path = tempfile() - - start = %w{some tags} - tags = %w{a list of tags} - - obj = nil - assert_nothing_raised do - obj = Puppet.type(:file).create( - :path => path, - :ensure => "file", - :tag => start - ) - end - - - assert(obj, "Did not make object") - - start.each do |tag| - assert(obj.tagged?(tag), "Object was not tagged with %s" % tag) - end - - tags.each do |tag| - assert_nothing_raised { - obj[:tag] = tag - } - end - - tags.each do |tag| - assert(obj.tagged?(tag), "Object was not tagged with %s" % tag) - end - end -end - -# $Id$ diff --git a/test/util/autoload.rb b/test/util/autoload.rb index 493fd7f60..bae6d37d4 100755 --- a/test/util/autoload.rb +++ b/test/util/autoload.rb @@ -1,130 +1,105 @@ #!/usr/bin/env ruby $:.unshift("../lib").unshift("../../lib") if __FILE__ =~ /\.rb$/ require 'puppet' require 'puppet/util/autoload' require 'puppettest' class TestAutoload < Test::Unit::TestCase include PuppetTest @things = [] def self.newthing(name) @things << name end def self.thing?(name) @things.include? name end def self.clear @things.clear end def mkfile(name, path) # Now create a file to load File.open(path, "w") do |f| f.puts %{ TestAutoload.newthing(:#{name.to_s}) } end end def mk_loader(name) dir = tempfile() $: << dir cleanup do $:.delete(dir) end Dir.mkdir(dir) rbdir = File.join(dir, name.to_s) Dir.mkdir(rbdir) loader = nil assert_nothing_raised { loader = Puppet::Util::Autoload.new(self.class, name) } return rbdir, loader end - def teardown - super - Puppet::Util::Autoload.clear - end - def test_load dir, loader = mk_loader(:yayness) assert_equal(loader.object_id, Puppet::Util::Autoload[self.class].object_id, "Did not retrieve loader object by class") # Make sure we don't fail on missing files assert_nothing_raised { assert_equal(false, loader.load(:mything), "got incorrect return on failed load") } # Now create a couple of files for testing path = File.join(dir, "mything.rb") mkfile(:mything, path) opath = File.join(dir, "othing.rb") mkfile(:othing, opath) # Now try to actually load it. assert_nothing_raised { assert_equal(true, loader.load(:mything), "got incorrect return on load") } assert(loader.loaded?(:mything), "Not considered loaded") assert(self.class.thing?(:mything), "Did not get loaded thing") - # Now clear everything, and test loadall - assert_nothing_raised { - Puppet::Util::Autoload.clear - } - self.class.clear - assert_nothing_raised { - loader.loadall - } - [:mything, :othing].each do |thing| + loader.load(thing) assert(loader.loaded?(thing), "#{thing.to_s} not considered loaded") assert(loader.loaded?("%s.rb" % thing), "#{thing.to_s} not considered loaded with .rb") assert(Puppet::Util::Autoload.loaded?("yayness/%s" % thing), "%s not considered loaded by the main class" % thing) assert(Puppet::Util::Autoload.loaded?("yayness/%s.rb" % thing), "%s not considered loaded by the main class with .rb" % thing) - loaded = Puppet::Util::Autoload.loaded?("yayness/%s.rb" % thing) - assert_equal("%s/%s.rb" % [dir, thing], loaded[:file], "File path was not set correctly in loaded store") - assert_equal(self.class, loaded[:autoloader], "Loader was not set correctly in loaded store") - assert(self.class.thing?(thing), "Did not get loaded #{thing.to_s}") end - - Puppet::Util::Autoload.clear - [:mything, :othing].each do |thing| - assert(! loader.loaded?(thing), "#{thing.to_s} considered loaded after clear") - assert(! Puppet::Util::Autoload.loaded?("yayness/%s" % thing), "%s considered loaded by the main class after clear" % thing) - end end # Make sure that autoload dynamically modifies $: with the libdir as # appropriate. def test_searchpath dir = Puppet[:libdir] loader = Puppet::Util::Autoload.new(self, "testing") assert(loader.send(:searchpath).include?(dir), "searchpath does not include the libdir") end end - -# $Id$ diff --git a/test/util/config.rb b/test/util/config.rb index 9a1017058..f99ad54b4 100755 --- a/test/util/config.rb +++ b/test/util/config.rb @@ -1,1311 +1,1245 @@ #!/usr/bin/env ruby $:.unshift("../lib").unshift("../../lib") if __FILE__ =~ /\.rb$/ require 'mocha' require 'puppettest' require 'puppet/util/config' require 'puppettest/parsertesting' class TestConfig < Test::Unit::TestCase include PuppetTest include PuppetTest::ParserTesting CElement = Puppet::Util::Config::CElement CBoolean = Puppet::Util::Config::CBoolean def setup super @config = mkconfig end def set_configs(config = nil) config ||= @config config.setdefaults("main", :one => ["a", "one"], :two => ["a", "two"], :yay => ["/default/path", "boo"], :mkusers => [true, "uh, yeah"], :name => ["testing", "a"] ) config.setdefaults("section1", :attr => ["a", "one"], :attrdir => ["/another/dir", "two"], :attr3 => ["$attrdir/maybe", "boo"] ) end def check_for_users count = Puppet::Type.type(:user).inject(0) { |c,o| c + 1 } assert(count > 0, "Found no users") end def test_to_transportable set_configs trans = nil assert_nothing_raised("Could not convert to a transportable") { trans = @config.to_transportable } comp = nil assert_nothing_raised("Could not convert transportable to component") { comp = trans.to_type } assert_nothing_raised("Could not retrieve transported config") { comp.retrieve } end + # #795 - when --config=relative, we want to fully expand file paths. + def test_relative_paths_when_to_transportable + config = mkconfig + config.setdefaults :yay, :transtest => ["/what/ever", "yo"] + file = config.element(:transtest) + + # Now override it with a relative path name + config[:transtest] = "here" + + should = File.join(Dir.getwd, "here") + + object = file.to_transportable[0] + assert_equal(should, object.name, "Did not translate relative pathnames to full path names") + end + def test_to_manifest set_configs manifest = nil assert_nothing_raised("Could not convert to a manifest") { manifest = @config.to_manifest } Puppet[:parseonly] = true interp = nil assert_nothing_raised do interp = mkinterp :Code => manifest, :UseNodes => false end trans = nil + node = Puppet::Node.new("node") assert_nothing_raised do - trans = interp.evaluate(nil, {}) + trans = interp.compile(node) end assert_nothing_raised("Could not instantiate objects") { - trans.to_type + trans.extract.to_type } end def test_to_comp set_configs comp = nil assert_nothing_raised("Could not convert to a component") { comp = @config.to_component } assert_nothing_raised("Could not retrieve component") { comp.retrieve } end def test_to_config set_configs newc = mkconfig set_configs(newc) # Reset all of the values, so we know they're changing. newc.each do |name, obj| next if name == :name newc[name] = true end newfile = tempfile() File.open(newfile, "w") { |f| @config.to_config.split("\n").each do |line| # Uncomment the settings, so they actually take. if line =~ / = / f.puts line.sub(/^\s*#/, '') else f.puts line end end } assert_nothing_raised("Could not parse generated configuration") { newc.parse(newfile) } @config.each do |name, object| assert_equal(@config[name], newc[name], "Parameter %s is not the same" % name) end end def mkconfig c = nil assert_nothing_raised { c = Puppet::Util::Config.new } return c end def test_addbools assert_nothing_raised { @config.setdefaults(:testing, :booltest => [true, "testing"]) } assert(@config[:booltest]) @config = mkconfig assert_nothing_raised { @config.setdefaults(:testing, :booltest => ["true", "testing"]) } assert(@config[:booltest]) assert_nothing_raised { @config[:booltest] = false } assert(! @config[:booltest], "Booltest is not false") assert_nothing_raised { @config[:booltest] = "false" } assert(! @config[:booltest], "Booltest is not false") - assert_raise(Puppet::Error) { + assert_raise(ArgumentError) { @config[:booltest] = "yayness" } - assert_raise(Puppet::Error) { + assert_raise(ArgumentError) { @config[:booltest] = "/some/file" } end def test_strings val = "this is a string" assert_nothing_raised { @config.setdefaults(:testing, :strtest => [val, "testing"]) } assert_equal(val, @config[:strtest]) # Verify that variables are interpolated assert_nothing_raised { @config.setdefaults(:testing, :another => ["another $strtest", "testing"]) } assert_equal("another #{val}", @config[:another]) end def test_files c = mkconfig parent = "/puppet" assert_nothing_raised { @config.setdefaults(:testing, :parentdir => [parent, "booh"]) } assert_nothing_raised { @config.setdefaults(:testing, :child => ["$parent/child", "rah"]) } assert_equal(parent, @config[:parentdir]) assert_equal("/puppet/child", File.join(@config[:parentdir], "child")) end def test_getset initial = "an initial value" - assert_raise(Puppet::Error) { + assert_raise(ArgumentError) { @config[:yayness] = initial } default = "this is a default" assert_nothing_raised { @config.setdefaults(:testing, :yayness => [default, "rah"]) } assert_equal(default, @config[:yayness]) assert_nothing_raised { @config[:yayness] = initial } assert_equal(initial, @config[:yayness]) assert_nothing_raised { @config.clear } - assert_equal(default, @config[:yayness]) + assert_equal(default, @config[:yayness], "'clear' did not remove old values") assert_nothing_raised { @config[:yayness] = "not default" } assert_equal("not default", @config[:yayness]) end def test_parse_file text = %{ one = this is a test two = another test owner = root group = root yay = /a/path [main] four = five six = seven [section1] attr = value owner = puppet group = puppet attrdir = /some/dir attr3 = $attrdir/other } file = tempfile() File.open(file, "w") { |f| f.puts text } @config.expects(:settimer) result = nil assert_nothing_raised { result = @config.send(:parse_file, file) } main = result[:main] assert(main, "Did not get section for main") { :one => "this is a test", :two => "another test", :owner => "root", :group => "root", :yay => "/a/path", :four => "five", :six => "seven" }.each do |param, value| assert_equal(value, main[param], "Param %s was not set correctly in main" % param) end section1 = result[:section1] assert(section1, "Did not get section1") { :attr => "value", :owner => "puppet", :group => "puppet", :attrdir => "/some/dir", :attr3 => "$attrdir/other" }.each do |param, value| assert_equal(value, section1[param], "Param %s was not set correctly in section1" % param) end end def test_old_parse text = %{ one = this is a test two = another test owner = root group = root yay = /a/path [section1] attr = value owner = puppet group = puppet attrdir = /some/dir attr3 = $attrdir/other } file = tempfile() File.open(file, "w") { |f| f.puts text } assert_nothing_raised { @config.setdefaults("puppet", :one => ["a", "one"], :two => ["a", "two"], :yay => ["/default/path", "boo"], :mkusers => [true, "uh, yeah"] ) } assert_nothing_raised { @config.setdefaults("section1", :attr => ["a", "one"], :attrdir => ["/another/dir", "two"], :attr3 => ["$attrdir/maybe", "boo"] ) } assert_nothing_raised { @config.old_parse(file) } assert_equal("value", @config[:attr]) assert_equal("/some/dir", @config[:attrdir]) assert_equal(:directory, @config.element(:attrdir).type) assert_equal("/some/dir/other", @config[:attr3]) elem = nil assert_nothing_raised { elem = @config.element(:attr3) } assert(elem) assert_equal("puppet", elem.owner) config = nil assert_nothing_raised { config = @config.to_config } assert_nothing_raised("Could not create transportable config") { @config.to_transportable } end def test_parse result = { :main => {:main => "main", :bad => "invalid", :cliparam => "reset"}, :puppet => {:other => "puppet", :cliparam => "reset"}, :puppetd => {:other => "puppetd", :cliparam => "reset"} } # Set our defaults, so they're valid. Don't define 'bad', since we want to test for failures. @config.setdefaults(:main, :main => ["whatever", "a"], :cliparam => ["default", "y"], :other => ["a", "b"], :name => ["puppet", "b"] # our default name ) @config.setdefaults(:other, :one => ["whatever", "a"], :two => ["default", "y"], :apple => ["a", "b"], :shoe => ["puppet", "b"] # our default name ) @config.handlearg("--cliparam", "changed") - @config.expects(:parse_file).returns(result).times(2) + @config.stubs(:parse_file).returns(result) # First do it with our name being 'puppet' assert_nothing_raised("Could not handle parse results") do @config.parse(tempfile) end - assert_logged(:warning, /unknown configuration parameter bad/, "Did not log invalid config param") - + assert_equal(:puppet, @config.name, "Did not get correct name") assert_equal("main", @config[:main], "Did not get main value") assert_equal("puppet", @config[:other], "Did not get name value") assert_equal("changed", @config[:cliparam], "CLI values were overridden by config") # Now switch names and make sure the parsing switches, too. @config.clear(true) - @config[:name] = :puppetd assert_nothing_raised("Could not handle parse results") do @config.parse(tempfile) end - assert_logged(:warning, /unknown configuration parameter bad/, "Did not log invalid config param") + @config[:name] = :puppetd + assert_equal(:puppetd, @config.name, "Did not get correct name") assert_equal("main", @config[:main], "Did not get main value") assert_equal("puppetd", @config[:other], "Did not get name value") assert_equal("changed", @config[:cliparam], "CLI values were overridden by config") end # Make sure we can extract file options correctly. def test_parsing_file_options @config.setdefaults(:whev, :file => { :desc => "whev", :default => "/default", :owner => "me", :group => "me", :mode => "755" } ) file = tempfile + count = 0 { :pass => { " {owner = you}" => {:owner => "you"}, " {owner = you, group = you}" => {:owner => "you", :group => "you"}, " {owner = you, group = you, mode = 755}" => {:owner => "you", :group => "you", :mode => "755"}, " { owner = you, group = you } " => {:owner => "you", :group => "you"}, "{owner=you,group=you} " => {:owner => "you", :group => "you"}, "{owner=you,} " => {:owner => "you"} }, :fail => [ %{{owner = you group = you}}, %{{owner => you, group => you}}, %{{user => you}}, %{{random => you}}, %{{mode => you}}, # make sure modes are numbers %{{owner => you}} ] }.each do |type, list| + count += 1 list.each do |value| if type == :pass value, should = value[0], value[1] end + path = "/other%s" % count # Write our file out File.open(file, "w") do |f| - f.puts %{[main]\nfile = /other%s} % value + f.puts %{[main]\nfile = #{path}#{value}} end if type == :fail - assert_raise(Puppet::Error, "Did not fail on %s" % value.inspect) do + assert_raise(ArgumentError, "Did not fail on %s" % value.inspect) do @config.send(:parse_file, file) end else result = nil assert_nothing_raised("Failed to parse %s" % value.inspect) do result = @config.send(:parse_file, file) end assert_equal(should, result[:main][:_meta][:file], "Got incorrect return for %s" % value.inspect) + assert_equal(path, result[:main][:file], "Got incorrect value for %s" % value.inspect) end end end end # Make sure file options returned from parse_file are handled correctly. def test_parsed_file_options @config.setdefaults(:whev, :file => { :desc => "whev", :default => "/default", :owner => "me", :group => "me", :mode => "755" } ) result = { :main => { :file => "/other", :_meta => { :file => { :owner => "you", :group => "you", :mode => "644" } } } } @config.expects(:parse_file).returns(result) assert_nothing_raised("Could not handle file options") do @config.parse("/whatever") end # Get the actual object, so we can verify metadata file = @config.element(:file) + assert_equal("/other", @config[:file], "Did not get correct value") assert_equal("you", file.owner, "Did not pass on user") assert_equal("you", file.group, "Did not pass on group") assert_equal("644", file.mode, "Did not pass on mode") end def test_arghandling c = mkconfig assert_nothing_raised { @config.setdefaults("testing", :onboolean => [true, "An on bool"], :offboolean => [false, "An off bool"], :string => ["a string", "A string arg"], :file => ["/path/to/file", "A file arg"] ) } data = { :onboolean => [true, false], :offboolean => [true, false], :string => ["one string", "another string"], :file => %w{/a/file /another/file} } data.each { |param, values| values.each { |val| opt = nil arg = nil if @config.boolean?(param) if val opt = "--%s" % param else opt = "--no-%s" % param end else opt = "--%s" % param arg = val end assert_nothing_raised("Could not handle arg %s with value %s" % [opt, val]) { @config.handlearg(opt, arg) } } } end def test_addargs @config.setdefaults("testing", :onboolean => [true, "An on bool"], :offboolean => [false, "An off bool"], :string => ["a string", "A string arg"], :file => ["/path/to/file", "A file arg"] ) should = [] @config.each { |name, element| element.expects(:getopt_args).returns([name]) should << name } result = [] assert_nothing_raised("Add args failed") do @config.addargs(result) end assert_equal(should, result, "Did not call addargs correctly.") end def test_addargs_functional @config.setdefaults("testing", :onboolean => [true, "An on bool"], :string => ["a string", "A string arg"] ) result = [] should = [] assert_nothing_raised("Add args failed") do @config.addargs(result) end @config.each do |name, element| if name == :onboolean should << ["--onboolean", GetoptLong::NO_ARGUMENT] should << ["--no-onboolean", GetoptLong::NO_ARGUMENT] elsif name == :string should << ["--string", GetoptLong::REQUIRED_ARGUMENT] end end assert_equal(should, result, "Add args functional test failed") end def test_usesection # We want to make sure that config processes do not result in graphing. Puppet[:graphdir] = tempfile() Puppet[:graph] = true Dir.mkdir(Puppet[:graphdir]) c = mkconfig dir = tempfile() file = "$mydir/myfile" realfile = File.join(dir, "myfile") otherfile = File.join(dir, "otherfile") section = "testing" assert_nothing_raised { @config.setdefaults(section, :mydir => [dir, "A dir arg"], :otherfile => { :default => "$mydir/otherfile", :create => true, :desc => "A file arg" }, :myfile => [file, "A file arg"] ) } assert_nothing_raised("Could not use a section") { @config.use(section) } assert_nothing_raised("Could not reuse a section") { @config.use(section) } # Make sure it didn't graph anything, which is the only real way # to test that the transaction was marked as a configurator. assert(Dir.entries(Puppet[:graphdir]).reject { |f| f =~ /^\.\.?$/ }.empty?, "Graphed config process") assert(FileTest.directory?(dir), "Did not create directory") assert(FileTest.exists?(otherfile), "Did not create file") assert(!FileTest.exists?(realfile), "Created file") end def test_setdefaultsarray c = mkconfig assert_nothing_raised { @config.setdefaults("yay", :a => [false, "some value"], :b => ["/my/file", "a file"] ) } assert_equal(false, @config[:a], "Values are not equal") assert_equal("/my/file", @config[:b], "Values are not equal") end def test_setdefaultshash c = mkconfig assert_nothing_raised { @config.setdefaults("yay", :a => {:default => false, :desc => "some value"}, :b => {:default => "/my/file", :desc => "a file"} ) } assert_equal(false, @config[:a], "Values are not equal") assert_equal("/my/file", @config[:b], "Values are not equal") end def test_reuse c = mkconfig file = tempfile() section = "testing" assert_nothing_raised { @config.setdefaults(section, :myfile => {:default => file, :create => true, :desc => "yay"} ) } assert_nothing_raised("Could not use a section") { @config.use(section) } assert(FileTest.exists?(file), "Did not create file") assert(! Puppet::Type.type(:file)[file], "File obj still exists") File.unlink(file) @config.reuse assert(FileTest.exists?(file), "Did not create file") end def test_mkusers c = mkconfig file = tempfile() section = "testing" assert_nothing_raised { @config.setdefaults(section, :mkusers => [false, "yay"], :myfile => { :default => file, :owner => "pptest", :group => "pptest", :desc => "yay", :create => true } ) } comp = nil assert_nothing_raised { comp = @config.to_component } [:user, :group].each do |type| # The objects might get created internally by Puppet::Util; just # make sure they're not being managed if obj = Puppet.type(type)["pptest"] assert(! obj.managed?, "%s objectis managed" % type) end end comp.each { |o| o.remove } @config[:mkusers] = true assert_nothing_raised { @config.to_component } user = Puppet.type(:user)["pptest"] assert(user, "User object did not get created") assert(user.managed?, "User object is not managed.") assert(user.should(:comment), "user does not have a comment set") group = Puppet.type(:group)["pptest"] assert(group, "Group object did not get created") assert(group.managed?, "Group object is not managed." ) if Process.uid == 0 cleanup do user[:ensure] = :absent group[:ensure] = :absent assert_apply(user, group) end assert_apply(user, group) end end def test_notmanagingdev c = mkconfig path = "/dev/testing" @config.setdefaults(:test, :file => { :default => path, :mode => 0640, :desc => 'yay' } ) assert_nothing_raised { @config.to_component } assert(! Puppet.type(:file)["/dev/testing"], "Created dev file") end def test_groupsetting cfile = tempfile() group = "yayness" File.open(cfile, "w") do |f| f.puts "[main] group = #{group} " end config = mkconfig config.setdefaults(Puppet[:name], :group => ["puppet", "a group"]) assert_nothing_raised { config.parse(cfile) } assert_equal(group, config[:group], "Group did not take") end # provide a method to modify and create files w/out specifying the info # already stored in a config def test_writingfiles File.umask(0022) path = tempfile() mode = 0644 config = mkconfig args = { :default => path, :mode => mode, :desc => "yay" } user = nonrootuser() group = nonrootgroup() if Puppet::Util::SUIDManager.uid == 0 args[:owner] = user.name args[:group] = group.name end config.setdefaults(:testing, :myfile => args) assert_nothing_raised { config.write(:myfile) do |file| file.puts "yay" end } assert_equal(mode, filemode(path), "Modes are not equal") # OS X is broken in how it chgrps files if Puppet::Util::SUIDManager.uid == 0 assert_equal(user.uid, File.stat(path).uid, "UIDS are not equal") case Facter["operatingsystem"].value when /BSD/, "Darwin": # nothing else assert_equal(group.gid, File.stat(path).gid, "GIDS are not equal") end end end def test_mkdir File.umask(0022) path = tempfile() mode = 0755 config = mkconfig args = { :default => path, :mode => mode, :desc => "a file" } user = nonrootuser() group = nonrootgroup() if Puppet::Util::SUIDManager.uid == 0 args[:owner] = user.name args[:group] = group.name end config.setdefaults(:testing, :mydir => args) assert_nothing_raised { config.mkdir(:mydir) } assert_equal(mode, filemode(path), "Modes are not equal") # OS X and *BSD is broken in how it chgrps files if Puppet::Util::SUIDManager.uid == 0 assert_equal(user.uid, File.stat(path).uid, "UIDS are not equal") case Facter["operatingsystem"].value when /BSD/, "Darwin": # nothing else assert_equal(group.gid, File.stat(path).gid, "GIDS are not equal") end end end - def test_booleans_and_integers - config = mkconfig - config.setdefaults(:mysection, - :booltest => [false, "yay"], - :inttest => [14, "yay"] - ) - - file = tempfile() - - File.open(file, "w") do |f| - f.puts %{ -[main] -booltest = true -inttest = 27 -} - end - - assert_nothing_raised { - config.parse(file) - } - - assert_equal(true, config[:booltest], "Boolean was not converted") - assert_equal(27, config[:inttest], "Integer was not converted") - - # Now make sure that they get converted through handlearg - config.handlearg("--inttest", "true") - assert_equal(true, config[:inttest], "Boolean was not converted") - config.handlearg("--no-booltest", "false") - assert_equal(false, config[:booltest], "Boolean was not converted") - end - # Make sure that tags are ignored when configuring def test_configs_ignore_tags config = mkconfig file = tempfile() config.setdefaults(:mysection, :mydir => [file, "a file"] ) Puppet[:tags] = "yayness" assert_nothing_raised { config.use(:mysection) } assert(FileTest.directory?(file), "Directory did not get created") assert_equal("yayness", Puppet[:tags], "Tags got changed during config") end def test_configs_replace_in_url config = mkconfig config.setdefaults(:mysection, :name => ["yayness", "yay"]) config.setdefaults(:mysection, :url => ["http://$name/rahness", "yay"]) val = nil assert_nothing_raised { val = config[:url] } assert_equal("http://yayness/rahness", val, "Config got messed up") end def test_correct_type_assumptions config = mkconfig file = Puppet::Util::Config::CFile element = Puppet::Util::Config::CElement bool = Puppet::Util::Config::CBoolean # We have to keep these ordered, unfortunately. [ ["/this/is/a/file", file], ["true", bool], [true, bool], ["false", bool], ["server", element], ["http://$server/yay", element], ["$server/yayness", file], ["$server/yayness.conf", file] ].each do |ary| value, type = ary - elem = nil assert_nothing_raised { - elem = config.newelement( - :name => value, - :default => value, - :desc => name.to_s, - :section => :yayness - ) + config.setdefaults(:yayness, value => { :default => value, :desc => name.to_s}) } + elem = config.element(value) assert_instance_of(type, elem, "%s got created as wrong type" % value.inspect) end end # Make sure we correctly reparse our config files but don't lose CLI values. def test_reparse Puppet[:filetimeout] = 0 config = mkconfig() config.setdefaults(:mysection, :default => ["default", "yay"]) config.setdefaults(:mysection, :clichange => ["clichange", "yay"]) config.setdefaults(:mysection, :filechange => ["filechange", "yay"]) - file = tempfile() - # Set one parameter in the file - File.open(file, "w") { |f| - f.puts %{[main]\nfilechange = filevalue} - } + config.stubs(:read_file).returns(%{[main]\nfilechange = filevalue\n}) + file = mock 'file' + file.stubs(:changed?).returns(true) + assert_nothing_raised { config.parse(file) } # Set another "from the cli" assert_nothing_raised { config.handlearg("clichange", "clivalue") } # And leave the other unset assert_equal("default", config[:default]) - assert_equal("filevalue", config[:filechange]) + assert_equal("filevalue", config[:filechange], "Did not get value from file") assert_equal("clivalue", config[:clichange]) - # Now rewrite the file - File.open(file, "w") { |f| - f.puts %{[main]\nfilechange = newvalue} - } - - cfile = config.file - cfile.send("tstamp=".intern, Time.now - 50) + # Now reparse + config.stubs(:read_file).returns(%{[main]\nfilechange = newvalue\n}) + file = mock 'file' + file.stubs(:changed?).returns(true) + config.parse(file) # And check all of the values assert_equal("default", config[:default]) assert_equal("clivalue", config[:clichange]) assert_equal("newvalue", config[:filechange]) end def test_parse_removes_quotes config = mkconfig() config.setdefaults(:mysection, :singleq => ["single", "yay"]) config.setdefaults(:mysection, :doubleq => ["double", "yay"]) config.setdefaults(:mysection, :none => ["noquote", "yay"]) config.setdefaults(:mysection, :middle => ["midquote", "yay"]) file = tempfile() # Set one parameter in the file File.open(file, "w") { |f| f.puts %{[main]\n singleq = 'one' doubleq = "one" none = one middle = mid"quote } } assert_nothing_raised { config.parse(file) } %w{singleq doubleq none}.each do |p| assert_equal("one", config[p], "%s did not match" % p) end assert_equal('mid"quote', config["middle"], "middle did not match") end def test_timer Puppet[:filetimeout] = 0.1 origpath = tempfile() config = mkconfig() config.setdefaults(:mysection, :paramdir => [tempfile(), "yay"]) file = tempfile() # Set one parameter in the file File.open(file, "w") { |f| f.puts %{[main]\n paramdir = #{origpath} } } assert_nothing_raised { config.parse(file) config.use(:mysection) } assert(FileTest.directory?(origpath), "dir did not get created") # Now start the timer assert_nothing_raised { EventLoop.current.monitor_timer config.timer } newpath = tempfile() File.open(file, "w") { |f| f.puts %{[main]\n paramdir = #{newpath} } } config.file.send("tstamp=".intern, Time.now - 50) sleep 1 assert_equal(newpath, config["paramdir"], "File did not get reparsed from timer") assert(FileTest.directory?(newpath), "new dir did not get created") end # Test that config parameters correctly call passed-in blocks when the value # is set. def test_paramblocks config = mkconfig() testing = nil - elem = nil assert_nothing_raised do - elem = config.newelement :default => "yay", - :name => :blocktest, - :desc => "boo", - :section => :test, - :hook => proc { |value| testing = value } + config.setdefaults :test, :blocktest => {:default => "yay", :desc => "boo", :hook => proc { |value| testing = value }} end + elem = config.element(:blocktest) assert_nothing_raised do assert_equal("yay", elem.value) end assert_nothing_raised do - elem.value = "yaytest" + config[:blocktest] = "yaytest" end assert_nothing_raised do assert_equal("yaytest", elem.value) end assert_equal("yaytest", testing) assert_nothing_raised do - elem.value = "another" + config[:blocktest] = "another" end assert_nothing_raised do assert_equal("another", elem.value) end assert_equal("another", testing) # Now verify it works from setdefault assert_nothing_raised do config.setdefaults :test, :blocktest2 => { :default => "yay", :desc => "yay", :hook => proc { |v| testing = v } } end assert_equal("yay", config[:blocktest2]) assert_nothing_raised do config[:blocktest2] = "footest" end assert_equal("footest", config[:blocktest2]) assert_equal("footest", testing) end def test_no_modify_root config = mkconfig config.setdefaults(:yay, :mydir => {:default => tempfile(), :mode => 0644, :owner => "root", :group => "root", :desc => "yay" }, :mkusers => [false, "yay"] ) assert_nothing_raised do config.use(:yay) end # Now enable it so they'll be added config[:mkusers] = true comp = config.to_component Puppet::Type.type(:user).each do |u| assert(u.name != "root", "Tried to manage root user") end Puppet::Type.type(:group).each do |u| assert(u.name != "root", "Tried to manage root group") assert(u.name != "wheel", "Tried to manage wheel group") end # assert(yay, "Did not find yay component") # yay.each do |c| # puts @config.ref # end # assert(! yay.find { |o| o.class.name == :user and o.name == "root" }, # "Found root user") # assert(! yay.find { |o| o.class.name == :group and o.name == "root" }, # "Found root group") end # #415 def test_remove_trailing_spaces config = mkconfig() config.setdefaults(:yay, :rah => ["testing", "a desc"]) file = tempfile() File.open(file, "w") { |f| f.puts "rah = something " } assert_nothing_raised { config.parse(file) } assert_equal("something", config[:rah], "did not remove trailing whitespace in parsing") end # #484 def test_parsing_unknown_variables logstore() config = mkconfig() config.setdefaults(:mysection, :one => ["yay", "yay"]) file = tempfile() File.open(file, "w") { |f| f.puts %{[main]\n one = one two = yay } } assert_nothing_raised("Unknown parameter threw an exception") do config.parse(file) end - - assert(@logs.detect { |l| l.message =~ /unknown configuration/ and l.level == :warning }, - "Did not generate warning message") end def test_multiple_interpolations @config.setdefaults(:section, :one => ["oneval", "yay"], :two => ["twoval", "yay"], :three => ["$one/$two", "yay"] ) assert_equal("oneval/twoval", @config[:three], "Did not interpolate multiple variables") end # Make sure we can replace ${style} var names def test_curly_replacements @config.setdefaults(:section, :one => ["oneval", "yay"], :two => ["twoval", "yay"], :three => ["$one/${two}/${one}/$two", "yay"] ) assert_equal("oneval/twoval/oneval/twoval", @config[:three], "Did not interpolate curlied variables") end - # Discovered from #734 - def test_set_parameter_hash - @config.setdefaults(:section, - :unchanged => ["unval", "yay"], - :normal => ["normalval", "yay"], - :cliparam => ["clival", "yay"], - :file => ["/my/file", "yay"] - ) - - # Set the cli param using the cli method - @config.handlearg("--cliparam", "other") - - # Make sure missing params just throw warnings, not errors - assert_nothing_raised("Could not call set_parameter_hash with an invalid option") do - @config.send(:set_parameter_hash, :missing => "something") - end - - # Make sure normal values get set - assert_nothing_raised("Could not call set_parameter_hash with a normal value") do - @config.send(:set_parameter_hash, :normal => "abnormal") - end - assert_equal("abnormal", @config[:normal], "Value did not get set") - - # Make sure cli-set values don't get overridden - assert_nothing_raised("Could not call set_parameter_hash with an override of a cli value") do - @config.send(:set_parameter_hash, :cliparam => "something else") - end - assert_equal("other", @config[:cliparam], "CLI value was overridden by config value") - - # Make sure the meta stuff works - assert_nothing_raised("Could not call set_parameter_hash with meta info") do - @config.send(:set_parameter_hash, :file => "/other/file", :_meta => {:file => {:mode => "0755"}}) - end - assert_equal("/other/file", @config[:file], "value with meta info was overridden by config value") - assert_equal("0755", @config.element(:file).mode, "Did not set mode from meta info") - - # And make sure other params are unchanged - assert_equal("unval", @config[:unchanged], "Unchanged value has somehow changed") - end - # Test to make sure that we can set and get a short name def test_celement_short_name element = nil assert_nothing_raised("Could not create celement") do element = CElement.new :short => "n", :desc => "anything" end assert_equal("n", element.short, "Short value is not retained") assert_raise(ArgumentError,"Allowed multicharactered short names.") do element = CElement.new :short => "no", :desc => "anything" end end # Test to make sure that no two celements have the same short name def test_celement_short_name_not_duplicated config = mkconfig assert_nothing_raised("Could not create celement with short name.") do config.setdefaults(:main, :one => { :default => "blah", :desc => "anything", :short => "o" }) end assert_nothing_raised("Could not create second celement with short name.") do config.setdefaults(:main, :two => { :default => "blah", :desc => "anything", :short => "i" }) end assert_raise(ArgumentError, "Could create second celement with duplicate short name.") do config.setdefaults(:main, :three => { :default => "blah", :desc => "anything", :short => "i" }) end # make sure that when the above raises an expection that the config is not included assert(!config.include?(:three), "Invalid configuration item was retained") end # Tell getopt which arguments are valid def test_get_getopt_args element = CElement.new :name => "foo", :desc => "anything" assert_equal([["--foo", GetoptLong::REQUIRED_ARGUMENT]], element.getopt_args, "Did not produce appropriate getopt args") element.short = "n" assert_equal([["--foo", "-n", GetoptLong::REQUIRED_ARGUMENT]], element.getopt_args, "Did not produce appropriate getopt args") element = CBoolean.new :name => "foo", :desc => "anything" assert_equal([["--foo", GetoptLong::NO_ARGUMENT], ["--no-foo", GetoptLong::NO_ARGUMENT]], element.getopt_args, "Did not produce appropriate getopt args") element.short = "n" assert_equal([["--foo", "-n", GetoptLong::NO_ARGUMENT],["--no-foo", GetoptLong::NO_ARGUMENT]], element.getopt_args, "Did not produce appropriate getopt args") end end # $Id$