diff --git a/CHANGELOG b/CHANGELOG index 234b24393..59dde24a2 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,984 +1,990 @@ + Removed the loglevels from the valid values for `logoutput` + in the Exec resource type -- the log levels are specified + using the `loglevel` parameter, not `logoutput`. This never + worked, or at least hasn`t for ages, and now the docs are + just correct. + Somewhat refactored fileserving so that it no longer caches any objects, nor does it use Puppet's RAL resources. In the process, I fixed #894 (you can now copy links) and refactored other classes as necessary. Mostly it was fixing tests. Hopefully partially fixed #1010 -- clients should now fail to install files whose checksums do not match the checksum from the server. Fixed #1018 -- resources now have their namevars added as aliases in the resource catalog, just like they were added in the resource classes. Fixed #1037 -- remote unreadable files no longer have the permission denied exceptions caught, thus forbidding them from being replaced with 'nil'. Fixed #1043 -- autoloading now searches the plugins directory in each module, in addition to the lib directory. The 'lib' directory is also deprecated, but supported for now to give people a chance to convert. Fixed #1003 -- Applying DavidS's patch to fix searching for tags in sql. Fixed #992 -- Puppet is now compatible with gems 1.0.1. Fixed #968 again, this time with tests -- parseonly works, including not compiling the configurations, and also storeconfigs is no longer required during parse-testing. Fixed #1021 -- the problem was that my method of determining the in-degree sometimes resulted in a lower number than the number of in-edges. Fixed #997 -- virtual defined types are no longer evaluated. NOTE: This introduces a behaviour change, in that you previously could realize a resource within a virtual defined resource, and now you must realize the entire defined resource, rather than just the contained resource. Fixed #1030 - class and definition evaluation has been significantly refactored, fixing this problem and making the whole interplay between the classes, definitions, and nodes, and the Compile class much cleaner. Exec resources must now have unique names, although the commands can still be duplicated. This is easily accomplished by just specifying a unique name with whatever (unique or otherwise) command you need. Fixed #989 -- missing CRL files are correctly ignored, and the value should be set to 'false' to explicitly not look for these files. Fixed #1017 -- environment-specific modulepath is no longer ignored. Fixing #794 -- consolidating the gentoo configuration files. Fixing #976 -- both the full name of qualified classes and the class parts are now added as tags. I've also created a Tagging module that we should push throughout the rest of the system that uses tags. Fixing #995 -- puppetd no longer dies at startup if the server is not running. Fixing #977 -- the rundir is again set to 1777. Fixed #971 -- classes can once again be included multiple times. Added builtin support for Nagios types using Naginator to parse and generate the files. 0.24.1 Updated vim filetype detection. (#900 and #963) Default resources like schedules no longer conflict with managed resources. (#965) Removing the ability to disable http keep-alive, since it didn't really work anyway and it should no longer be necessary. Refactored http keep-alive so it actually works again. This should be sufficient enough that we no longer need the ability to disable keep-alive. There is now a central module responsible for managing HTTP instances, along with all certificates in those instances. Fixed a backward compatibility issue when running 0.23.x clients against 0.24.0 servers -- relationships would consistently not work. (#967) Closing existing http connections when opening a new one, and closing all connections after each run. (#961) Removed warning about deprecated explicit plugins mounts. 0.24.0 (misspiggy) Modifying the behaviour of the certdnsnames setting. It now defaults to an empty string, and will only be used if it is set to something else. If it is set, then the host's FQDN will also be added as an alias. The default behaviour is now to add 'puppet' and 'puppet.$domain' as DNS aliases when the name for the cert being signed is equal to the signing machine's name, which will only be the case for CA servers. This should result in servers always having the alias set up and no one else, but you can still override the aliases if you want. External node support now requires that you set the 'node_terminus' setting to 'exec'. See the IndirectionReference on the wiki for more information. http_enable_post_connection_check added as a configuration option for puppetd. This defaults to true, which validates the server SSL certificate against the requested host name in new versions of ruby. See #896 for more information. Mounts no longer remount swap filesystems. Slightly modifying how services manage their list of paths (and adding documention for it). Services now default to the paths specified by the provider classes. Removed 'type' as a valid attribute for services, since it's been deprecated since the creation of providers. Removed 'running' as a valid attribute for services, since it's been deprecated since February 2006. Added modified patch by Matt Palmer which adds a 'plugins' mount, fixing #891. See PluginsInModules on the wiki for information on usage. Empty dbserver and dbpassword settings will now be ignored when initializing Rails connections (patch by womble). Configuration settings can now be blank (patch by womble). Added calls to endpwent/endgrent when searching for user and group IDs, which fixes #791. Obviated 'target' in interfaces, as all file paths were automatically calculated anyway. The parameter is still there, but it's not used and just generates a warning. Fixing some of the problems with interface management on Red Hat. Puppet now uses the :netmask property and does not try to set the bootproto (#762). You now must specify an environment and you are required to specify the valid environments for your site. (#911) Certificates now always specify a subjectAltName, but it defaults to '*', meaning that it doesn't require DNS names to match. You can override that behaviour by specifying a value for 'certdnsnames', which will then require that hostname as a match (#896). Relationship metaparams (:notify, :require, :subscribe, and :before) now stack when they are collecting metaparam values from their containers (#446). For instance, if a resource inside a definition has a value set for 'require', and you call the definition with 'require', the resource gets both requires, where before it would only retain its initial value. Changed the behavior of --debug to include Mongrel client debugging information. Mongrel output will be written to the terminal only, not to the puppet debug log. This should help anyone working with reverse HTTP SSL proxies. (#905) Fixed #800 -- invalid configurations are no longer cached. This was done partially by adding a relationship validation step once the entire configuration is created, but it also required the previously-mentioned changes to how the configuration retrieval process works. Removed some functionality from the Master client, since the local functionality has been replaced with the Indirector already, and rearranging how configuration retrieval is done to fix ordering and caching bugs. The node scope is now above all other scopes besides the 'main' scope, which should help make its variables visible to other classes, assuming those classes were not included in the node's parent. Replaced GRATR::Digraph with Puppet::SimpleGraph as the base class for Puppet's graphing. Functionality should be equivalent but with dramatically better performance. The --use-nodes and --no-nodes options are now obsolete. Puppet automatically detects when nodes are defined, and if they are defined it will require that a node be found, else it will not look for a node nor will it fail if it fails to find one. Fixed #832. Added the '--no-daemonize' option to puppetd and puppetmasterd. NOTE: The default behavior of 'verbose' and 'debug' no longer cause puppetd and puppetmasterd to not daemonize. Added k5login type. (#759) Fixed CA race condition. (#693) Added shortname support to config.rb and refactored addargs 0.23.2 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/conf/namespaceauth.conf b/conf/namespaceauth.conf new file mode 100644 index 000000000..837235769 --- /dev/null +++ b/conf/namespaceauth.conf @@ -0,0 +1,20 @@ +# This is an example namespaceauth.conf file, +# which you'll need if you want to start a client +# in --listen mode. +[fileserver] + allow *.domain.com + +[puppetmaster] + allow *.domain.com + +[puppetrunner] + allow culain.domain.com + +[puppetbucket] + allow *.domain.com + +[puppetreports] + allow *.domain.com + +[resource] + allow server.domain.com diff --git a/lib/puppet/metatype/closure.rb b/lib/puppet/metatype/closure.rb index 259854411..673a2359d 100644 --- a/lib/puppet/metatype/closure.rb +++ b/lib/puppet/metatype/closure.rb @@ -1,45 +1,49 @@ class Puppet::Type attr_writer :implicit # Is this type's name isomorphic with the object? That is, if the # name conflicts, does it necessarily mean that the objects conflict? # Defaults to true. def self.isomorphic? if defined? @isomorphic return @isomorphic else return true end end def implicit? if defined? @implicit and @implicit return true else return false end end + def isomorphic? + self.class.isomorphic? + end + # is the instance a managed instance? A 'yes' here means that # the instance was created from the language, vs. being created # in order resolve other questions, such as finding a package # in a list def managed? # Once an object is managed, it always stays managed; but an object # that is listed as unmanaged might become managed later in the process, # so we have to check that every time if defined? @managed and @managed return @managed else @managed = false properties.each { |property| s = property.should if s and ! property.class.unmanaged @managed = true break end } return @managed end end end diff --git a/lib/puppet/node/catalog.rb b/lib/puppet/node/catalog.rb index d680de9a0..f885a41ee 100644 --- a/lib/puppet/node/catalog.rb +++ b/lib/puppet/node/catalog.rb @@ -1,512 +1,512 @@ require 'puppet/indirector' require 'puppet/pgraph' require 'puppet/transaction' require 'puppet/util/tagging' # This class models a node catalog. It is the thing # meant to be passed from server to client, and it contains all # of the information in the catalog, including the resources # and the relationships between them. class Puppet::Node::Catalog < Puppet::PGraph extend Puppet::Indirector indirects :catalog, :terminus_class => :compiler include Puppet::Util::Tagging # The host name this is a catalog for. attr_accessor :name # The catalog version. Used for testing whether a catalog # is up to date. attr_accessor :version # How long this catalog took to retrieve. Used for reporting stats. attr_accessor :retrieval_duration # How we should extract the catalog for sending to the client. attr_reader :extraction_format # We need the ability to set this externally, so we can yaml-dump the # catalog. attr_accessor :edgelist_class # Whether this is a host catalog, which behaves very differently. # In particular, reports are sent, graphs are made, and state is # stored in the state database. If this is set incorrectly, then you often # end up in infinite loops, because catalogs are used to make things # that the host catalog needs. attr_accessor :host_config # Whether this graph is another catalog's relationship graph. # We don't want to accidentally create a relationship graph for another # relationship graph. attr_accessor :is_relationship_graph # Whether this catalog was retrieved from the cache, which affects # whether it is written back out again. attr_accessor :from_cache # 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 # Add one or more resources to our graph and to our resource table. def add_resource(*resources) resources.each do |resource| unless resource.respond_to?(:ref) raise ArgumentError, "Can only add objects that respond to :ref" end fail_unless_unique(resource) ref = resource.ref @resource_table[ref] = resource # If the name and title differ, set up an alias #self.alias(resource, resource.name) if resource.respond_to?(:name) and resource.respond_to?(:title) and resource.name != resource.title if resource.respond_to?(:name) and resource.respond_to?(:title) and resource.name != resource.title - self.alias(resource, resource.name) if resource.class.isomorphic? + self.alias(resource, resource.name) if resource.isomorphic? end resource.catalog = self if resource.respond_to?(:catalog=) and ! is_relationship_graph add_vertex(resource) end end # Create an alias for a resource. def alias(resource, name) resource.ref =~ /^(.+)\[/ newref = "%s[%s]" % [$1 || resource.class.name, name] if existing = @resource_table[newref] return if existing == resource raise(ArgumentError, "Cannot alias %s to %s; resource %s already exists" % [resource.ref, name, newref]) end @resource_table[newref] = resource @aliases[resource.ref] << newref end # Apply our catalog to the local host. Valid options # are: # :tags - set the tags that restrict what resources run # during the transaction # :ignoreschedules - tell the transaction to ignore schedules # when determining the resources to run def apply(options = {}) @applying = true Puppet::Util::Storage.load if host_config? transaction = Puppet::Transaction.new(self) transaction.tags = options[:tags] if options[:tags] transaction.ignoreschedules = true if options[:ignoreschedules] transaction.addtimes :config_retrieval => @retrieval_duration begin transaction.evaluate rescue Puppet::Error => detail Puppet.err "Could not apply complete catalog: %s" % detail rescue => detail puts detail.backtrace if Puppet[:trace] Puppet.err "Got an uncaught exception of type %s: %s" % [detail.class, detail] ensure # Don't try to store state unless we're a host config # too recursive. Puppet::Util::Storage.store if host_config? end yield transaction if block_given? transaction.send_report if host_config and (Puppet[:report] or Puppet[:summarize]) return transaction ensure @applying = false cleanup() transaction.cleanup if defined? transaction and transaction end # Are we in the middle of applying the catalog? def applying? @applying end def clear(remove_resources = true) super() # We have to do this so that the resources clean themselves up. @resource_table.values.each { |resource| resource.remove } if remove_resources @resource_table.clear if defined?(@relationship_graph) and @relationship_graph @relationship_graph.clear(false) @relationship_graph = nil end end def classes @classes.dup end # Create an implicit resource, meaning that it will lose out # to any explicitly defined resources. This method often returns # nil. # The quirk of this method is that it's not possible to create # an implicit resource before an explicit resource of the same name, # because all explicit resources are created before any generate() # methods are called on the individual resources. Thus, this # method can safely just check if an explicit resource already exists # and toss this implicit resource if so. def create_implicit_resource(type, options) unless options.include?(:implicit) options[:implicit] = true end # This will return nil if an equivalent explicit resource already exists. # When resource classes no longer retain references to resource instances, # this will need to be modified to catch that conflict and discard # implicit resources. if resource = create_resource(type, options) resource.implicit = true return resource else return nil end end # Create a new resource and register it in the catalog. def create_resource(type, options) unless klass = Puppet::Type.type(type) raise ArgumentError, "Unknown resource type %s" % type end return unless resource = klass.create(options) @transient_resources << resource if applying? add_resource(resource) if @relationship_graph @relationship_graph.add_resource(resource) unless @relationship_graph.resource(resource.ref) end resource 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 catalog graph into whatever the client is expecting. def extract send("extract_to_%s" % extraction_format) end # Create the traditional TransBuckets and TransObjects from our catalog # 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 catalog" end # Create a proc for examining edges, which we'll use to build our tree # of TransBuckets and TransObjects. bucket = nil walk(main, :out) do |source, target| # The sources are always non-builtins. 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 # Retrieve the bucket for the top-level scope and set the appropriate metadata. unless result = buckets[main.to_s] # This only happens when the catalog is entirely empty. result = buckets[main.to_s] = main.to_trans end result.classes = classes # Clear the cache to encourage the GC buckets.clear return result end # Make sure all of our resources are "finished". def finalize make_default_resources @resource_table.values.each { |resource| resource.finish } write_graph(:resources) end def host_config? host_config || false end def initialize(name = nil) super() @name = name if name @extraction_format ||= :transportable @classes = [] @resource_table = {} @transient_resources = [] @applying = false @relationship_graph = nil @aliases = Hash.new { |hash, key| hash[key] = [] } if block_given? yield(self) finalize() end end # Make the default objects necessary for function. def make_default_resources # We have to add the resources to the catalog, or else they won't get cleaned up after # the transaction. # First create the default scheduling objects Puppet::Type.type(:schedule).mkdefaultschedules.each { |res| add_resource(res) unless resource(res.ref) } # And filebuckets if bucket = Puppet::Type.type(:filebucket).mkdefaultbucket add_resource(bucket) end end # Create a graph of all of the relationships in our catalog. def relationship_graph raise(Puppet::DevError, "Tried get a relationship graph for a relationship graph") if self.is_relationship_graph unless defined? @relationship_graph and @relationship_graph # It's important that we assign the graph immediately, because # the debug messages below use the relationships in the # relationship graph to determine the path to the resources # spitting out the messages. If this is not set, # then we get into an infinite loop. @relationship_graph = Puppet::Node::Catalog.new @relationship_graph.host_config = host_config? @relationship_graph.is_relationship_graph = true # First create the dependency graph self.vertices.each do |vertex| @relationship_graph.add_vertex vertex vertex.builddepends.each do |edge| @relationship_graph.add_edge(edge) end end # Lastly, add in any autorequires @relationship_graph.vertices.each do |vertex| vertex.autorequire.each do |edge| unless @relationship_graph.edge?(edge.source, edge.target) # don't let automatic relationships conflict with manual ones. unless @relationship_graph.edge?(edge.target, edge.source) vertex.debug "Autorequiring %s" % [edge.source] @relationship_graph.add_edge(edge) else vertex.debug "Skipping automatic relationship with %s" % (edge.source == vertex ? edge.target : edge.source) end end end end @relationship_graph.write_graph(:relationships) # Then splice in the container information @relationship_graph.splice!(self, Puppet::Type::Component) @relationship_graph.write_graph(:expanded_relationships) end @relationship_graph end # Remove the resource from our catalog. Notice that we also call # 'remove' on the resource, at least until resource classes no longer maintain # references to the resource instances. def remove_resource(*resources) resources.each do |resource| @resource_table.delete(resource.ref) @aliases[resource.ref].each { |res_alias| @resource_table.delete(res_alias) } @aliases[resource.ref].clear remove_vertex!(resource) if vertex?(resource) @relationship_graph.remove_vertex!(resource) if @relationship_graph and @relationship_graph.vertex?(resource) resource.remove end end # Look a resource up by its reference (e.g., File[/etc/passwd]). def resource(type, title = nil) # Always create a resource reference, so that it always canonizes how we # are referring to them. if title ref = Puppet::ResourceReference.new(type, title).to_s else # If they didn't provide a title, then we expect the first # argument to be of the form 'Class[name]', which our # Reference class canonizes for us. ref = Puppet::ResourceReference.new(nil, type).to_s end if resource = @resource_table[ref] return resource elsif defined?(@relationship_graph) and @relationship_graph @relationship_graph.resource(ref) end end # Return an array of all resources. def resources @resource_table.keys end # Convert our catalog into a RAL catalog. def to_ral to_catalog :to_type end # Turn our parser catalog into a transportable catalog. def to_transportable to_catalog :to_transobject end # Produce the graph files if requested. def write_graph(name) # We only want to graph the main host catalog. return unless host_config? return unless Puppet[:graph] Puppet.settings.use(:graphing) file = File.join(Puppet[:graphdir], "%s.dot" % name.to_s) File.open(file, "w") { |f| f.puts to_dot("name" => name.to_s.capitalize) } end # LAK:NOTE We cannot yaml-dump the class in the edgelist_class, because classes cannot be # dumped by default, nor does yaml-dumping # the edge-labels work at this point (I don't # know why). # Neither of these matters right now, but I suppose it could at some point. # We also have to have the vertex_dict dumped after the resource table, because yaml can't # seem to handle the output of yaml-dumping the vertex_dict. def to_yaml_properties props = instance_variables.reject { |v| %w{@edgelist_class @edge_labels @vertex_dict}.include?(v) } props << "@vertex_dict" props end private def cleanup unless @transient_resources.empty? remove_resource(*@transient_resources) @transient_resources.clear @relationship_graph = nil end end # Verify that the given resource isn't defined elsewhere. def fail_unless_unique(resource) # Short-curcuit the common case, return unless existing_resource = @resource_table[resource.ref] # 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 ArgumentError.new(msg) end # An abstracted method for converting one catalog into another type of catalog. # This pretty much just converts all of the resources from one class to another, using # a conversion method. def to_catalog(convert) result = self.class.new(self.name) map = {} vertices.each do |resource| next if resource.respond_to?(:virtual?) and resource.virtual? newres = resource.send(convert) # We can't guarantee that resources don't munge their names # (like files do with trailing slashes), so we have to keep track # of what a resource got converted to. map[resource.ref] = newres result.add_resource newres end message = convert.to_s.gsub "_", " " edges.each do |edge| # Skip edges between virtual resources. next if edge.source.respond_to?(:virtual?) and edge.source.virtual? next if edge.target.respond_to?(:virtual?) and edge.target.virtual? unless source = map[edge.source.ref] raise Puppet::DevError, "Could not find resource %s when converting %s resources" % [edge.source.ref, message] end unless target = map[edge.target.ref] raise Puppet::DevError, "Could not find resource %s when converting %s resources" % [edge.target.ref, message] end result.add_edge(source, target, edge.label) end map.clear result.add_class(*self.classes) result.tag(*self.tags) return result end end diff --git a/lib/puppet/parser/ast/definition.rb b/lib/puppet/parser/ast/definition.rb index 2b7506446..0c65c702c 100644 --- a/lib/puppet/parser/ast/definition.rb +++ b/lib/puppet/parser/ast/definition.rb @@ -1,204 +1,201 @@ require 'puppet/parser/ast/branch' require 'puppet/util/warnings' # The AST class for defined types, which is also the base class # nodes and classes. class Puppet::Parser::AST::Definition < Puppet::Parser::AST::Branch include Puppet::Util::Warnings class << self attr_accessor :name end # The class name @name = :definition attr_accessor :classname, :arguments, :code, :scope, :keyword attr_accessor :exported, :namespace, :parser, :virtual, :name attr_reader :parentclass def child_of?(klass) false end # Create a resource that knows how to evaluate our actual code. def evaluate(scope) - # Do nothing if the resource already exists; this provides the singleton nature classes need. - return if scope.catalog.resource(self.class.name, self.classname) - resource = Puppet::Parser::Resource.new(:type => self.class.name, :title => self.classname, :scope => scope, :source => scope.source) scope.catalog.tag(*resource.tags) scope.compiler.add_resource(scope, resource) return resource end # Now evaluate the code associated with this class or definition. def evaluate_code(resource) # Create a new scope. scope = subscope(resource.scope, resource) set_resource_parameters(scope, resource) if self.code return self.code.safeevaluate(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 resources" % arg else raise Puppet::ParseError, "%s is a metaparameter; please choose another parameter name in the %s definition" % [arg, self.classname] 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 return nil unless @parentclass # Cache our result, since it should never change. unless defined?(@parentobj) unless tmp = find_parentclass parsefail "Could not find %s parent %s" % [self.class.name, @parentclass] end if tmp == self parsefail "Parent classes must have dissimilar names" end @parentobj = tmp end @parentobj end # Create a new subscope in which to evaluate our code. def subscope(scope, resource) args = { :resource => resource, :keyword => self.keyword, :namespace => self.namespace, :source => self } 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 = arg.to_sym unless args.include?(arg) if defined? default and ! default.nil? default = default.safeevaluate 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 diff --git a/lib/puppet/parser/ast/hostclass.rb b/lib/puppet/parser/ast/hostclass.rb index 8d4d01660..7f89f8151 100644 --- a/lib/puppet/parser/ast/hostclass.rb +++ b/lib/puppet/parser/ast/hostclass.rb @@ -1,80 +1,87 @@ require 'puppet/parser/ast/definition' # 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 Puppet::Parser::AST::HostClass < Puppet::Parser::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 # Make sure our parent class has been evaluated, if we have one. def evaluate(scope) if parentclass and ! scope.catalog.resource(self.class.name, parentclass) - resource = parentobj.evaluate(scope) + parent_resource = parentobj.evaluate(scope) + end + + # Do nothing if the resource already exists; this makes sure we don't + # get multiple copies of the class resource, which helps provide the + # singleton nature of classes. + if resource = scope.catalog.resource(self.class.name, self.classname) + return resource end super end # Evaluate the code associated with this class. def evaluate_code(resource) scope = resource.scope # Verify that we haven't already been evaluated. This is # what provides the singleton aspect. if existing_scope = scope.compiler.class_scope(self) Puppet.debug "Class '%s' already evaluated; not evaluating again" % (classname == "" ? "main" : classname) return nil end pnames = nil if pklass = self.parentobj parent_resource = resource.scope.compiler.catalog.resource(self.class.name, pklass.classname) # This shouldn't evaluate if the class has already been evaluated. pklass.evaluate_code(parent_resource) scope = parent_scope(scope, pklass) pnames = scope.namespaces end # Don't create a subscope for the top-level class, since it already # has its own scope. scope = subscope(scope, resource) unless resource.title == :main # Add the parent scope namespaces to our own. if pnames pnames.each do |ns| scope.add_namespace(ns) end end # Set the class before we evaluate the code, so that it's set during # the evaluation and can be inspected. scope.compiler.class_set(self.classname, scope) # Now evaluate our code, yo. if self.code return self.code.safeevaluate(scope) else return nil end end def parent_scope(scope, klass) if s = scope.compiler.class_scope(klass) return s else raise Puppet::DevError, "Could not find scope for %s" % klass.classname end end end diff --git a/lib/puppet/parser/interpreter.rb b/lib/puppet/parser/interpreter.rb index 33f66e8c2..d4655c403 100644 --- a/lib/puppet/parser/interpreter.rb +++ b/lib/puppet/parser/interpreter.rb @@ -1,97 +1,95 @@ require 'puppet' require 'timeout' require 'puppet/rails' require 'puppet/util/methodhelper' require 'puppet/parser/parser' require 'puppet/parser/compiler' require 'puppet/parser/scope' # 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 catalog back. class Puppet::Parser::Interpreter include Puppet::Util attr_accessor :usenodes include Puppet::Util::Errors # Determine the configuration version for a given node's environment. def configuration_version(node) parser(node.environment).version end # evaluate our whole tree def compile(node) raise Puppet::ParseError, "Could not parse configuration; cannot compile on node %s" % node.name unless env_parser = parser(node.environment) begin return Puppet::Parser::Compiler.new(node, env_parser).compile rescue => detail + puts detail.backtrace if Puppet[:trace] raise Puppet::Error, detail.to_s + " on node %s" % node.name - if Puppet[:trace] - puts detail.backtrace - end end end # create our interpreter def initialize # 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 @parsers = {} end # 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 tmp = create_parser(environment) @parsers[environment].clear if @parsers[environment] @parsers[environment] = tmp rescue => detail # If a parser already exists, than assume that we logged the # exception elsewhere and reuse the parser. If one doesn't # exist, then reraise. raise detail unless @parsers[environment] end end @parsers[environment] end private # Create a new parser object and pre-parse the configuration. def create_parser(environment) begin parser = Puppet::Parser::Parser.new(:environment => environment) if code = Puppet.settings.value(:code, environment) and code != "" parser.string = code else file = Puppet.settings.value(:manifest, environment) parser.file = file end parser.parse return parser rescue => detail msg = "Could not parse" if environment and environment != "" msg += " for environment %s" % environment end msg += ": %s" % detail.to_s error = Puppet::Error.new(msg) error.set_backtrace(detail.backtrace) raise error end end end diff --git a/lib/puppet/parser/resource.rb b/lib/puppet/parser/resource.rb index fb0799011..46be89ca2 100644 --- a/lib/puppet/parser/resource.rb +++ b/lib/puppet/parser/resource.rb @@ -1,434 +1,443 @@ # 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' require 'puppet/util/tagging' include Puppet::Util include Puppet::Util::MethodHelper include Puppet::Util::Errors include Puppet::Util::Logging include Puppet::Util::Tagging attr_accessor :source, :line, :file, :scope, :rails_id attr_accessor :virtual, :override, :translated attr_reader :exported, :evaluated, :params # Determine whether the provided parameter name is a relationship parameter. def self.relationship_parameter?(name) unless defined?(@relationship_names) @relationship_names = Puppet::Type.relationship_params.collect { |p| p.name } end @relationship_names.include?(name) end # 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, :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 def builtin=(bool) @ref.builtin = bool end # Retrieve the associated definition and evaluate it. def evaluate if klass = @ref.definedtype finish() return klass.evaluate_code(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 # Do any finishing work on this object, called before evaluation or # before storage/translation. def finish add_defaults() add_metaparams() add_scope_tags() validate() end def initialize(options) # 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 unless self.scope raise ArgumentError, "Resources require a scope" end @source ||= scope.source options = symbolize_options(options) # 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 @ref = Reference.new(:type => type, :title => title, :scope => self.scope) @params = {} # 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 tag(@ref.type) tag(@ref.title) if valid_tag?(@ref.title.to_s) end + # Is this resource modeling an isomorphic resource type? + def isomorphic? + if builtin? + return @ref.builtintype.isomorphic? + else + return true + end + end + # 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| 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 # 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 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 db_resource.save @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 return nil if virtual? if builtin? to_transobject else to_transbucket end end 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 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, unless they're relationship metaparams. # LAK:NOTE Relationship metaparams get treated specially -- we stack them, instead of # overriding. next if @params[name] and not self.class.relationship_parameter?(name) # Skip metaparams for which we get no value. next unless val = scope.lookupvar(name.to_s, false) and val != :undefined # The default case: just set the value return set_parameter(name, val) unless @params[name] # For relationship params, though, join the values (a la #446). @params[name].value = [@params[name].value, val].flatten end end def add_scope_tags if scope_resource = scope.resource tag(*scope_resource.tags) 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. (@params[param.name] = param and return) unless current = @params[param.name] # The parameter is already set. Fail if they're not allowed to override it. unless param.source.child_of?(current.source) 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 # If we've gotten this far, we're allowed to override. # Merge with previous value, if the parameter was generated with the +> syntax. # It's important that we use the new param instance here, not the old one, # so that the source is registered correctly for later overrides. param.value = [current.value, param.value].flatten if param.add @params[param.name] = param 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 # 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/provider/interface/redhat.rb b/lib/puppet/provider/interface/redhat.rb index aa217620e..4a9fcb491 100644 --- a/lib/puppet/provider/interface/redhat.rb +++ b/lib/puppet/provider/interface/redhat.rb @@ -1,250 +1,250 @@ require 'puppet/provider/parsedfile' require 'erb' Puppet::Type.type(:interface).provide(:redhat) do desc "Manage network interfaces on Red Hat operating systems. This provider parses and generates configuration files in ``/etc/sysconfig/network-scripts``." - INTERFACE_DIR = "/etc/sysconfig/network-scripts" + INTERFACE_DIR = "/etc/sysconfig/network-scripts" confine :exists => INTERFACE_DIR defaultfor :operatingsystem => [:fedora, :centos, :redhat] # Create the setter/gettor methods to match the model. mk_resource_methods @templates = {} # Register a template. def self.register_template(name, string) @templates[name] = ERB.new(string) end # Retrieve a template by name. def self.template(name) @templates[name] end register_template :alias, <<-ALIAS DEVICE=<%= self.device %> ONBOOT=<%= self.on_boot %> BOOTPROTO=none IPADDR=<%= self.name %> NETMASK=<%= self.netmask %> BROADCAST= ALIAS register_template :normal, <<-LOOPBACKDUMMY DEVICE=<%= self.device %> ONBOOT=<%= self.on_boot %> BOOTPROTO=static IPADDR=<%= self.name %> NETMASK=<%= self.netmask %> BROADCAST= LOOPBACKDUMMY - # maximum number of dummy interfaces - @max_dummies = 10 + # maximum number of dummy interfaces + @max_dummies = 10 - # maximum number of aliases per interface - @max_aliases_per_iface = 10 + # maximum number of aliases per interface + @max_aliases_per_iface = 10 - @@dummies = [] - @@aliases = Hash.new { |hash, key| hash[key] = [] } + @@dummies = [] + @@aliases = Hash.new { |hash, key| hash[key] = [] } - # calculate which dummy interfaces are currently already in - # use prior to needing to call self.next_dummy later on. - def self.instances - # parse all of the config files at once - Dir.glob("%s/ifcfg-*" % INTERFACE_DIR).collect do |file| - record = parse(file) + # calculate which dummy interfaces are currently already in + # use prior to needing to call self.next_dummy later on. + def self.instances + # parse all of the config files at once + Dir.glob("%s/ifcfg-*" % INTERFACE_DIR).collect do |file| + record = parse(file) - # store the existing dummy interfaces + # store the existing dummy interfaces @@dummies << record[:ifnum] if (record[:interface_type] == :dummy and ! @@dummies.include?(record[:ifnum])) @@aliases[record[:interface]] << record[:ifnum] if record[:interface_type] == :alias new(record) - end - end - - # return the next avaliable dummy interface number, in the case where - # ifnum is not manually specified - def self.next_dummy - @max_dummies.times do |i| - unless @@dummies.include?(i.to_s) - @@dummies << i.to_s - return i.to_s - end - end - end - - # return the next available alias on a given interface, in the case - # where ifnum if not manually specified - def self.next_alias(interface) - @max_aliases_per_iface.times do |i| - unless @@aliases[interface].include?(i.to_s) - @@aliases[interface] << i.to_s - return i.to_s - end - end - end + end + end + + # return the next avaliable dummy interface number, in the case where + # ifnum is not manually specified + def self.next_dummy + @max_dummies.times do |i| + unless @@dummies.include?(i.to_s) + @@dummies << i.to_s + return i.to_s + end + end + end + + # return the next available alias on a given interface, in the case + # where ifnum if not manually specified + def self.next_alias(interface) + @max_aliases_per_iface.times do |i| + unless @@aliases[interface].include?(i.to_s) + @@aliases[interface] << i.to_s + return i.to_s + end + end + end # base the ifnum, for dummy / loopback interface in linux # on the last octect of the IP address # Parse the existing file. def self.parse(file) instance = new() return instance unless FileTest.exist?(file) File.readlines(file).each do |line| if line =~ /^(\w+)=(.+)$/ instance.send($1.downcase + "=", $2) end end return instance end # Prefetch our interface list, yo. def self.prefetch(resources) instances.each do |prov| if resource = resources[prov.name] resource.provider = prov end end end def create self.class.resource_type.validproperties.each do |property| if value = @resource.should(property) @property_hash[property] = value end end @property_hash[:name] = @resource.name return (@resource.class.name.to_s + "_created").intern end def destroy File.unlink(file_path) end def exists? FileTest.exist?(file_path) end # generate the content for the interface file, so this is dependent # on whether we are adding an alias to a real interface, or a loopback # address (also dummy) on linux. For linux it's quite involved, and we # will use an ERB template - def generate + def generate itype = self.interface_type == :alias ? :alias : :normal self.class.template(itype).result(binding) - end + end # Where should the file be written out? - # This defaults to INTERFACE_DIR/ifcfg-, but can have a - # more symbolic name by setting interface_desc in the type. + # This defaults to INTERFACE_DIR/ifcfg-, but can have a + # more symbolic name by setting interface_desc in the type. def file_path if resource and val = resource[:interface_desc] desc = val else desc = self.name end self.fail("Could not get name for interface") unless desc if self.interface_type == :alias return File.join(INTERFACE_DIR, "ifcfg-" + self.interface + ":" + desc) else return File.join(INTERFACE_DIR, "ifcfg-" + desc) end end # Use the device value to figure out all kinds of nifty things. def device=(value) case value when /:/: @property_hash[:interface], @property_hash[:ifnum] = value.split(":") @property_hash[:interface_type] = :alias when /^dummy/: @property_hash[:interface_type] = :loopback @property_hash[:interface] = "dummy" # take the number of the dummy interface, as this is used # when working out whether to call next_dummy when dynamically # creating these @property_hash[:ifnum] = value.sub("dummy",'') @@dummies << @property_hash[:ifnum].to_s unless @@dummies.include?(@property_hash[:ifnum].to_s) else @property_hash[:interface_type] = :normal @property_hash[:interface] = value end end - # create the device name, so this based on the IP, and interface + type - def device - case @resource.should(:interface_type) - when :loopback - @property_hash[:ifnum] ||= self.class.next_dummy - return "dummy" + @property_hash[:ifnum] - when :alias - @property_hash[:ifnum] ||= self.class.next_alias(@resource[:interface]) - return @resource[:interface] + ":" + @property_hash[:ifnum] - end + # create the device name, so this based on the IP, and interface + type + def device + case @resource.should(:interface_type) + when :loopback + @property_hash[:ifnum] ||= self.class.next_dummy + return "dummy" + @property_hash[:ifnum] + when :alias + @property_hash[:ifnum] ||= self.class.next_alias(@resource[:interface]) + return @resource[:interface] + ":" + @property_hash[:ifnum] + end end # Set the name to our ip address. def ipaddr=(value) @property_hash[:name] = value end - # whether the device is to be brought up on boot or not. converts - # the true / false of the type, into yes / no values respectively - # writing out the ifcfg-* files - def on_boot - case @property_hash[:onboot].to_s - when "true" - return "yes" - when "false" - return "no" - else - return "neither" - end - end + # whether the device is to be brought up on boot or not. converts + # the true / false of the type, into yes / no values respectively + # writing out the ifcfg-* files + def on_boot + case @property_hash[:onboot].to_s + when "true" + return "yes" + when "false" + return "no" + else + return "neither" + end + end # Mark whether the interface should be started on boot. def on_boot=(value) # translate whether we come up on boot to true/false case value.downcase when "yes": @property_hash[:onboot] = :true else @property_hash[:onboot] = :false end end # Write the new file out. def flush # Don't flush to disk if we're removing the config. return if self.ensure == :absent @property_hash.each do |name, val| if val == :absent raise ArgumentError, "Propety %s must be provided" % val end end File.open(file_path, "w") do |f| f.puts generate() end end def prefetch @property_hash = self.class.parse(file_path) end end diff --git a/lib/puppet/type/exec.rb b/lib/puppet/type/exec.rb index f8049236f..2772b54a8 100755 --- a/lib/puppet/type/exec.rb +++ b/lib/puppet/type/exec.rb @@ -1,620 +1,617 @@ module Puppet newtype(:exec) do include Puppet::Util::Execution require 'timeout' @doc = "Executes external commands. It is critical that all commands executed using this mechanism can be run multiple times without harm, i.e., they are *idempotent*. One useful way to create idempotent commands is to use the checks like ``creates`` to avoid running the command unless some condition is met. Note also that you can restrict an ``exec`` to only run when it receives events by using the ``refreshonly`` parameter; this is a useful way to have your configuration respond to events with arbitrary commands. 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. Note that if an ``exec`` receives an event from another resource, it will get executed again (or execute the command specified in ``refresh``, if there is one). There is a strong tendency to use ``exec`` to do whatever work Puppet can't already do; while this is obviously acceptable (and unavoidable) in the short term, it is highly recommended to migrate work from ``exec`` to native Puppet types as quickly as possible. If you find that you are doing a lot of work with ``exec``, please at least notify us at Reductive Labs what you are doing, and hopefully we can work with you to get a native resource type for the work you are doing." require 'open3' # Create a new check mechanism. It's basically just a parameter that # provides one extra 'check' method. def self.newcheck(name, &block) @checks ||= {} check = newparam(name, &block) @checks[name] = check end def self.checks @checks.keys end newproperty(:returns) do |property| include Puppet::Util::Execution munge do |value| value.to_s end defaultto "0" attr_reader :output desc "The expected return code. An error will be returned if the executed command returns something else. Defaults to 0." # Make output a bit prettier def change_to_s(currentvalue, newvalue) return "executed successfully" end # First verify that all of our checks pass. def retrieve # Default to somethinng if @resource.check return :notrun else return self.should end end # Actually execute the command. def sync olddir = nil # We need a dir to change to, even if it's just the cwd dir = self.resource[:cwd] || Dir.pwd event = :executed_command begin @output, status = @resource.run(self.resource[:command]) rescue Timeout::Error self.fail "Command exceeded timeout" % value.inspect end if log = @resource[:logoutput] case log when :true log = @resource[:loglevel] when :on_failure if status.exitstatus.to_s != self.should.to_s log = @resource[:loglevel] else log = :false end end unless log == :false @output.split(/\n/).each { |line| self.send(log, line) } end end if status.exitstatus.to_s != self.should.to_s self.fail("%s returned %s instead of %s" % [self.resource[:command], status.exitstatus, self.should.to_s]) end return event end end newparam(:command) do isnamevar desc "The actual command to execute. Must either be fully qualified or a search path for the command must be provided. If the command succeeds, any output produced will be logged at the instance's normal log level (usually ``notice``), but if the command fails (meaning its return code does not match the specified code) then any output is logged at the ``err`` log level." end newparam(:path) do desc "The search path used for command execution. Commands must be fully qualified if no path is specified. Paths can be specified as an array or as a colon-separated list." # Support both arrays and colon-separated fields. def value=(*values) @value = values.flatten.collect { |val| val.split(":") }.flatten end end newparam(:user) do desc "The user to run the command as. Note that if you use this then any error output is not currently captured. This is because of a bug within Ruby. If you are using Puppet to create this user, the exec will automatically require the user, as long as it is specified by name." # Most validation is handled by the SUIDManager class. validate do |user| unless Puppet.features.root? self.fail "Only root can execute commands as other users" end end end newparam(:group) do desc "The group to run the command as. This seems to work quite haphazardly on different platforms -- it is a platform issue not a Ruby or Puppet one, since the same variety exists when running commnands as different users in the shell." # Validation is handled by the SUIDManager class. end newparam(:cwd) do desc "The directory from which to run the command. If this directory does not exist, the command will fail." validate do |dir| unless dir =~ /^#{File::SEPARATOR}/ self.fail("CWD must be a fully qualified path") end end munge do |dir| if dir.is_a?(Array) dir = dir[0] end dir end end newparam(:logoutput) do desc "Whether to log output. Defaults to logging output at the loglevel for the ``exec`` resource. Use *on_failure* to only log the output when the command reports an error. Values are **true**, *false*, *on_failure*, and any legal log level." - values = [:true, :false, :on_failure] - # And all of the log levels - Puppet::Util::Log.eachlevel { |level| values << level } - newvalues(*values) + newvalues(:true, :false, :on_failure) end newparam(:refresh) do desc "How to refresh this command. By default, the exec is just called again when it receives an event from another resource, but this parameter allows you to define a different command for refreshing." validate do |command| @resource.validatecmd(command) end end newparam(:env) do desc "This parameter is deprecated. Use 'environment' instead." munge do |value| warning "'env' is deprecated on exec; use 'environment' instead." resource[:environment] = value end end newparam(:environment) do desc "Any additional environment variables you want to set for a command. Note that if you use this to set PATH, it will override the ``path`` attribute. Multiple environment variables should be specified as an array." validate do |values| values = [values] unless values.is_a? Array values.each do |value| unless value =~ /\w+=/ raise ArgumentError, "Invalid environment setting '%s'" % value end end end end newparam(:timeout) do desc "The maximum time the command should take. If the command takes longer than the timeout, the command is considered to have failed and will be stopped. Use any negative number to disable the timeout. The time is specified in seconds." munge do |value| value = value.shift if value.is_a?(Array) if value.is_a?(String) unless value =~ /^[-\d.]+$/ raise ArgumentError, "The timeout must be a number." end Float(value) else value end end defaultto 300 end newcheck(:refreshonly) do desc "The command should only be run as a refresh mechanism for when a dependent object is changed. It only makes sense to use this option when this command depends on some other object; it is useful for triggering an action:: # Pull down the main aliases file file { \"/etc/aliases\": source => \"puppet://server/module/aliases\" } # Rebuild the database, but only when the file changes exec { newaliases: path => [\"/usr/bin\", \"/usr/sbin\"], subscribe => File[\"/etc/aliases\"], refreshonly => true } Note that only ``subscribe`` and ``notify`` can trigger actions, not ``require``, so it only makes sense to use ``refreshonly`` with ``subscribe`` or ``notify``." newvalues(:true, :false) # We always fail this test, because we're only supposed to run # on refresh. def check(value) # We have to invert the values. if value == :true false else true end end end newcheck(:creates) do desc "A file that this command creates. If this parameter is provided, then the command will only be run if the specified file does not exist:: exec { \"tar xf /my/tar/file.tar\": cwd => \"/var/tmp\", creates => \"/var/tmp/myfile\", path => [\"/usr/bin\", \"/usr/sbin\"] } " # FIXME if they try to set this and fail, then we should probably # fail the entire exec, right? validate do |files| files = [files] unless files.is_a? Array files.each do |file| self.fail("'creates' must be set to a fully qualified path") unless file unless file =~ %r{^#{File::SEPARATOR}} self.fail "'creates' files must be fully qualified." end end end # If the file exists, return false (i.e., don't run the command), # else return true def check(value) return ! FileTest.exists?(value) end end newcheck(:unless) do desc "If this parameter is set, then this ``exec`` will run unless the command returns 0. For example:: exec { \"/bin/echo root >> /usr/lib/cron/cron.allow\": path => \"/usr/bin:/usr/sbin:/bin\", unless => \"grep root /usr/lib/cron/cron.allow 2>/dev/null\" } This would add ``root`` to the cron.allow file (on Solaris) unless ``grep`` determines it's already there. Note that this command follows the same rules as the main command, which is to say that it must be fully qualified if the path is not set. " validate do |cmds| cmds = [cmds] unless cmds.is_a? Array cmds.each do |cmd| @resource.validatecmd(cmd) end end # Return true if the command does not return 0. def check(value) begin output, status = @resource.run(value, true) rescue Timeout::Error err "Check %s exceeded timeout" % value.inspect return false end return status.exitstatus != 0 end end newcheck(:onlyif) do desc "If this parameter is set, then this ``exec`` will only run if the command returns 0. For example:: exec { \"logrotate\": path => \"/usr/bin:/usr/sbin:/bin\", onlyif => \"test `du /var/log/messages | cut -f1` -gt 100000\" } This would run ``logrotate`` only if that test returned true. Note that this command follows the same rules as the main command, which is to say that it must be fully qualified if the path is not set. " validate do |cmds| cmds = [cmds] unless cmds.is_a? Array cmds.each do |cmd| @resource.validatecmd(cmd) end end # Return true if the command returns 0. def check(value) begin output, status = @resource.run(value, true) rescue Timeout::Error err "Check %s exceeded timeout" % value.inspect return false end return status.exitstatus == 0 end end # Exec names are not isomorphic with the objects. @isomorphic = false validate do validatecmd(self[:command]) end # FIXME exec should autorequire any exec that 'creates' our cwd autorequire(:file) do reqs = [] # Stick the cwd in there if we have it if self[:cwd] reqs << self[:cwd] end self[:command].scan(/^(#{File::SEPARATOR}\S+)/) { |str| reqs << str } [:onlyif, :unless].each { |param| next unless tmp = self[param] tmp = [tmp] unless tmp.is_a? Array tmp.each do |line| # And search the command line for files, adding any we # find. This will also catch the command itself if it's # fully qualified. It might not be a bad idea to add # unqualified files, but, well, that's a bit more annoying # to do. reqs += line.scan(%r{(#{File::SEPARATOR}\S+)}) end } # For some reason, the += isn't causing a flattening reqs.flatten! reqs end autorequire(:user) do # Autorequire users if they are specified by name if user = self[:user] and user !~ /^\d+$/ user end end def self.instances [] end # Verify that we pass all of the checks. The argument determines whether # we skip the :refreshonly check, which is necessary because we now check # within refresh() def check(refreshing = false) self.class.checks.each { |check| next if refreshing and check == :refreshonly if @parameters.include?(check) val = @parameters[check].value val = [val] unless val.is_a? Array val.each do |value| unless @parameters[check].check(value) return false end end end } return true end # Verify that we have the executable def checkexe(cmd) if cmd =~ /^\// exe = cmd.split(/ /)[0] unless FileTest.exists?(exe) raise ArgumentError, "Could not find executable %s" % exe end unless FileTest.executable?(exe) raise ArgumentError, "%s is not executable" % exe end elsif path = self[:path] exe = cmd.split(/ /)[0] withenv :PATH => self[:path].join(":") do path = %{which #{exe}}.chomp if path == "" raise ArgumentError, "Could not find command '%s'" % exe end end else raise ArgumentError, "%s is somehow not qualified with no search path" % self[:command] end end def output if self.property(:returns).nil? return nil else return self.property(:returns).output end end # Run the command, or optionally run a separately-specified command. def refresh if self.check(true) if cmd = self[:refresh] self.run(cmd) else self.property(:returns).sync end end end # Run a command. def run(command, check = false) output = nil status = nil dir = nil checkexe(command) if dir = self[:cwd] unless File.directory?(dir) if check dir = nil else self.fail "Working directory '%s' does not exist" % dir end end end dir ||= Dir.pwd if check debug "Executing check '#{command}'" else debug "Executing '#{command}'" end begin # Do our chdir Dir.chdir(dir) do environment = {} if self[:path] environment[:PATH] = self[:path].join(":") end if envlist = self[:environment] envlist = [envlist] unless envlist.is_a? Array envlist.each do |setting| if setting =~ /^(\w+)=((.|\n)+)$/ name = $1 value = $2 if environment.include? name warning( "Overriding environment setting '%s' with '%s'" % [name, value] ) end environment[name] = value else warning "Cannot understand environment setting %s" % setting.inspect end end end withenv environment do Timeout::timeout(self[:timeout]) do output, status = Puppet::Util::SUIDManager.run_and_capture( [command], self[:user], self[:group] ) end # The shell returns 127 if the command is missing. if status.exitstatus == 127 raise ArgumentError, output end end end rescue Errno::ENOENT => detail self.fail detail.to_s end return output, status end def validatecmd(cmd) # if we're not fully qualified, require a path if cmd !~ /^\// if self[:path].nil? self.fail "'%s' is both unqualifed and specified no search path" % cmd end end end end end diff --git a/lib/puppet/type/mailalias.rb b/lib/puppet/type/mailalias.rb index 92f609267..50ca26ef6 100755 --- a/lib/puppet/type/mailalias.rb +++ b/lib/puppet/type/mailalias.rb @@ -1,49 +1,49 @@ module Puppet newtype(:mailalias) do @doc = "Creates an email alias in the local alias database." ensurable newparam(:name, :namevar => true) do desc "The alias name." end newproperty(:recipient, :array_matching => :all) do - desc "Where email should should be sent. Multiple values + desc "Where email should be sent. Multiple values should be specified as an array." def is_to_s(value) if value.include?(:absent) super else value.join(",") end end def should @should end def should_to_s(value) if value.include?(:absent) super else value.join(",") end end end newproperty(:target) do desc "The file in which to store the aliases. Only used by those providers that write to disk (i.e., not NetInfo)." defaultto { if @resource.class.defaultprovider.ancestors.include?(Puppet::Provider::ParsedFile) @resource.class.defaultprovider.default_target else nil end } end end end diff --git a/lib/puppet/type/package.rb b/lib/puppet/type/package.rb index ee2871ce2..f004f7c42 100644 --- a/lib/puppet/type/package.rb +++ b/lib/puppet/type/package.rb @@ -1,350 +1,342 @@ # Define the different packaging systems. Each package system is implemented # in a module, which then gets used to individually extend each package object. # This allows packages to exist on the same machine using different packaging # systems. module Puppet newtype(:package) do @doc = "Manage packages. There is a basic dichotomy in package support right now: Some package types (e.g., yum and apt) can retrieve their own package files, while others (e.g., rpm and sun) cannot. For those package formats that cannot retrieve their own files, you can use the ``source`` parameter to point to the correct file. Puppet will automatically guess the packaging format that you are using based on the platform you are on, but you can override it using the ``provider`` parameter; each provider defines what it requires in order to function, and you must meet those requirements to use a given provider." feature :installable, "The provider can install packages.", :methods => [:install] feature :uninstallable, "The provider can uninstall packages.", :methods => [:uninstall] feature :upgradeable, "The provider can upgrade to the latest version of a package. This feature is used by specifying ``latest`` as the desired value for the package.", :methods => [:update, :latest] feature :purgeable, "The provider can purge packages. This generally means that all traces of the package are removed, including existing configuration files. This feature is thus destructive and should be used with the utmost care.", :methods => [:purge] feature :versionable, "The provider is capable of interrogating the package database for installed version(s), and can select which out of a set of available versions of a package to install if asked." ensurable do desc "What state the package should be in. *latest* only makes sense for those packaging formats that can retrieve new packages on their own and will throw an error on those that cannot. For those packaging systems that allow you to specify package versions, specify them here. Similarly, *purged* is only useful for packaging systems that support the notion of managing configuration files separately from 'normal' system files." attr_accessor :latest newvalue(:present, :event => :package_installed) do provider.install end newvalue(:absent, :event => :package_removed) do provider.uninstall end newvalue(:purged, :event => :package_purged, :required_features => :purgeable) do provider.purge end # Alias the 'present' value. aliasvalue(:installed, :present) newvalue(:latest, :required_features => :upgradeable) do # Because yum always exits with a 0 exit code, there's a retrieve # in the "install" method. So, check the current state now, # to compare against later. current = self.retrieve begin provider.update rescue => detail self.fail "Could not update: %s" % detail end if current == :absent :package_installed else :package_changed end end newvalue(/./, :required_features => :versionable) do begin provider.install rescue => detail self.fail "Could not update: %s" % detail end if self.retrieve == :absent :package_installed else :package_changed end end defaultto :installed # Override the parent method, because we've got all kinds of # funky definitions of 'in sync'. def insync?(is) @should ||= [] @latest = nil unless defined? @latest @lateststamp ||= (Time.now.to_i - 1000) # Iterate across all of the should values, and see how they # turn out. @should.each { |should| case should when :present return true unless [:absent, :purged].include?(is) when :latest # Short-circuit packages that are not present return false if is == :absent or is == :purged # Don't run 'latest' more than about every 5 minutes if @latest and ((Time.now.to_i - @lateststamp) / 60) < 5 #self.debug "Skipping latest check" else begin @latest = provider.latest @lateststamp = Time.now.to_i rescue => detail error = Puppet::Error.new("Could not get latest version: %s" % detail.to_s) error.set_backtrace(detail.backtrace) raise error end end case is when @latest: return true when :present: # This will only happen on retarded packaging systems # that can't query versions. return true else self.debug "is is %s, latest %s is %s" % [is.inspect, @resource.name, @latest.inspect] end when :absent return true if is == :absent or is == :purged when :purged return true if is == :purged when is return true end } return false end # This retrieves the current state. LAK: I think this method is unused. def retrieve return provider.properties[:ensure] end # Provide a bit more information when logging upgrades. def should_to_s(newvalue = @should) if @latest @latest.to_s else super(newvalue) end end end newparam(:name) do desc "The package name. This is the name that the packaging system uses internally, which is sometimes (especially on Solaris) a name that is basically useless to humans. If you want to abstract package installation, then you can use aliases to provide a common name to packages:: # In the 'openssl' class $ssl = $operatingsystem ? { solaris => SMCossl, default => openssl } # It is not an error to set an alias to the same value as the # object name. package { $ssl: ensure => installed, alias => openssl } . etc. . $ssh = $operatingsystem ? { solaris => SMCossh, default => openssh } # Use the alias to specify a dependency, rather than # having another selector to figure it out again. package { $ssh: ensure => installed, alias => openssh, require => Package[openssl] } " isnamevar end newparam(:source) do desc "Where to find the actual package. This must be a local file (or on a network file system) or a URL that your specific packaging type understands; Puppet will not retrieve files for you." - - validate do |value| - unless value =~ /^#{File::SEPARATOR}/ or value =~ /\w+:\/\// - self.fail( - "Package sources must be fully qualified files or URLs, depending on the platform." - ) - end - end end newparam(:instance) do desc "A read-only parameter set by the package." end newparam(:status) do desc "A read-only parameter set by the package." end newparam(:type) do desc "Deprecated form of ``provider``." munge do |value| warning "'type' is deprecated; use 'provider' instead" @resource[:provider] = value @resource[:provider] end end newparam(:adminfile) do desc "A file containing package defaults for installing packages. This is currently only used on Solaris. The value will be validated according to system rules, which in the case of Solaris means that it should either be a fully qualified path or it should be in /var/sadm/install/admin." end newparam(:responsefile) do desc "A file containing any necessary answers to questions asked by the package. This is currently used on Solaris and Debian. The value will be validated according to system rules, but it should generally be a fully qualified path." end newparam(:configfiles) do desc "Whether configfiles should be kept or replaced. Most packages types do not support this parameter." defaultto :keep newvalues(:keep, :replace) end newparam(:category) do desc "A read-only parameter set by the package." end newparam(:platform) do desc "A read-only parameter set by the package." end newparam(:root) do desc "A read-only parameter set by the package." end newparam(:vendor) do desc "A read-only parameter set by the package." end newparam(:description) do desc "A read-only parameter set by the package." end newparam(:allowcdrom) do desc "Tells apt to allow cdrom sources in the sources.list file. Normally apt will bail if you try this." newvalues(:true, :false) end autorequire(:file) do autos = [] [:responsefile, :adminfile].each { |param| if val = self[param] autos << val end } if source = self[:source] if source =~ /^#{File::SEPARATOR}/ autos << source end end autos end # This only exists for testing. def clear if obj = @parameters[:ensure] obj.latest = nil end end # The 'query' method returns a hash of info if the package # exists and returns nil if it does not. def exists? @provider.get(:ensure) != :absent end def initialize(params) self.initvars provider = nil [:provider, "use"].each { |label| if params.include?(label) provider = params[label] params.delete(label) end } if provider self[:provider] = provider else self.setdefaults(:provider) end super(params) unless @parameters.include?(:provider) raise Puppet::DevError, "No package provider set" end end def retrieve @provider.properties.inject({}) do |props, ary| name, value = ary if prop = @parameters[name] props[prop] = value end props end end end # Puppet.type(:package) end diff --git a/lib/puppet/type/service.rb b/lib/puppet/type/service.rb index c41a7883b..1b625cc41 100644 --- a/lib/puppet/type/service.rb +++ b/lib/puppet/type/service.rb @@ -1,183 +1,192 @@ # This is our main way of managing processes right now. # # a service is distinct from a process in that services # can only be managed through the interface of an init script # which is why they have a search path for initscripts and such module Puppet newtype(:service) do @doc = "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. Note that if a ``service`` receives an event from another resource, the service will get restarted. The actual command to restart the service depends on the platform. You can provide a special command for restarting with the ``restart`` attribute." feature :refreshable, "The provider can restart the service.", :methods => [:restart] feature :enableable, "The provider can enable and disable the service", :methods => [:disable, :enable, :enabled?] + feature :controllable, "The provider uses a control variable." + newproperty(:enable, :required_features => :enableable) do desc "Whether a service should be enabled to start at boot. This property behaves quite differently depending on the platform; wherever possible, it relies on local tools to enable or disable a given service." newvalue(:true, :event => :service_enabled) do provider.enable end newvalue(:false, :event => :service_disabled) do provider.disable end def retrieve return provider.enabled? end end # Handle whether the service should actually be running right now. newproperty(:ensure) do desc "Whether a service should be running." newvalue(:stopped, :event => :service_stopped) do provider.stop end newvalue(:running, :event => :service_started) do provider.start end aliasvalue(:false, :stopped) aliasvalue(:true, :running) def retrieve return provider.status end def sync event = super() if property = @resource.property(:enable) val = property.retrieve property.sync unless property.insync?(val) end return event end end newparam(:binary) do desc "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." end newparam(:hasstatus) do desc "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." newvalues(:true, :false) end newparam(:name) do desc "The name of the service to run. This name is used to find the service in whatever service subsystem it is in." isnamevar end newparam(:path) do desc "The search path for finding init scripts. Multiple values should be separated by colons or provided as an array." munge do |value| value = [value] unless value.is_a?(Array) paths = value.flatten.collect { |p| p.split(":") }.flatten.find_all do |path| if FileTest.directory?(path) true else if FileTest.exist?(path) and ! FileTest.directory?(path) @resource.debug "Search path %s is not a directory" % [path] else @resource.debug("Search path %s does not exist" % [path]) end false end end paths end defaultto { provider.class.defpath if provider.class.respond_to?(:defpath) } end newparam(:pattern) do desc "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." defaultto { @resource[:binary] || @resource[:name] } end newparam(:restart) do desc "Specify a *restart* command manually. If left unspecified, the service will be stopped and then started." end newparam(:start) do desc "Specify a *start* command manually. Most service subsystems support a ``start`` command, so this will not need to be specified." end newparam(:status) do desc "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." end newparam(:stop) do desc "Specify a *stop* command manually." end + newparam(:control) do + desc "The control variable used to manage services (originally for HP-UX). + Defaults to the upcased service name plus ``START`` replacing dots with + underscores, for those providers that support the ``controllable`` feature." + defaultto { resource.name.gsub(".","_").upcase + "_START" if resource.provider.controllable? } + end + newparam :hasrestart do desc "Specify that an init script has a ``restart`` option. Otherwise, the init script's ``stop`` and ``start`` methods are used." newvalues(:true, :false) end # Basically just a synonym for restarting. Used to respond # to events. def refresh # Only restart if we're actually running if (@parameters[:ensure] || newattr(:ensure)).retrieve == :running provider.restart else debug "Skipping restart; service is not running" end end end end diff --git a/spec/unit/node/catalog.rb b/spec/unit/node/catalog.rb index b1bf5abaa..604dabbb6 100755 --- a/spec/unit/node/catalog.rb +++ b/spec/unit/node/catalog.rb @@ -1,829 +1,831 @@ #!/usr/bin/env ruby require File.dirname(__FILE__) + '/../../spec_helper' describe Puppet::Node::Catalog, " when compiling" do it "should accept tags" do config = Puppet::Node::Catalog.new("mynode") config.tag("one") config.tags.should == %w{one} end it "should accept multiple tags at once" do config = Puppet::Node::Catalog.new("mynode") config.tag("one", "two") config.tags.should == %w{one two} end it "should convert all tags to strings" do config = Puppet::Node::Catalog.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::Catalog.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::Catalog.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::Catalog.new("mynode") config.add_class("one") config.tags.should == %w{one} end end describe Puppet::Node::Catalog, " when extracting" do it "should return extraction result as the method result" do config = Puppet::Node::Catalog.new("mynode") config.expects(:extraction_format).returns(:whatever) config.expects(:extract_to_whatever).returns(:result) config.extract.should == :result end end describe Puppet::Node::Catalog, " when extracting transobjects" do def mkscope @parser = Puppet::Parser::Parser.new :Code => "" @node = Puppet::Node.new("mynode") @compiler = Puppet::Parser::Compiler.new(@node, @parser) # XXX This is ridiculous. @compiler.send(:evaluate_main) @scope = @compiler.topscope end def mkresource(type, name) Puppet::Parser::Resource.new(:type => type, :title => name, :source => @source, :scope => @scope) end it "should always create a TransBucket for the 'main' class" do config = Puppet::Node::Catalog.new("mynode") @scope = mkscope @source = mock 'source' main = mkresource("class", :main) config.add_vertex(main) bucket = mock 'bucket' bucket.expects(:classes=).with(config.classes) main.stubs(:builtin?).returns(false) main.expects(:to_transbucket).returns(bucket) config.extract_to_transportable.should equal(bucket) 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::Catalog.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::Catalog.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 describe Puppet::Node::Catalog, " when converting to a transobject catalog" do class TestResource attr_accessor :name, :virtual, :builtin def initialize(name, options = {}) @name = name options.each { |p,v| send(p.to_s + "=", v) } end def ref if builtin? "File[%s]" % name else "Class[%s]" % name end end def virtual? virtual end def builtin? builtin end def to_transobject Puppet::TransObject.new(name, builtin? ? "file" : "class") end end before do @original = Puppet::Node::Catalog.new("mynode") @original.tag(*%w{one two three}) @original.add_class *%w{four five six} @top = TestResource.new 'top' @topobject = TestResource.new 'topobject', :builtin => true @virtual = TestResource.new 'virtual', :virtual => true @virtualobject = TestResource.new 'virtualobject', :builtin => true, :virtual => true @middle = TestResource.new 'middle' @middleobject = TestResource.new 'middleobject', :builtin => true @bottom = TestResource.new 'bottom' @bottomobject = TestResource.new 'bottomobject', :builtin => true @resources = [@top, @topobject, @middle, @middleobject, @bottom, @bottomobject] @original.add_edge(@top, @topobject) @original.add_edge(@top, @virtual) @original.add_edge(@virtual, @virtualobject) @original.add_edge(@top, @middle) @original.add_edge(@middle, @middleobject) @original.add_edge(@middle, @bottom) @original.add_edge(@bottom, @bottomobject) @catalog = @original.to_transportable end it "should add all resources as TransObjects" do @resources.each { |resource| @catalog.resource(resource.ref).should be_instance_of(Puppet::TransObject) } end it "should not extract defined virtual resources" do @catalog.vertices.find { |v| v.name == "virtual" }.should be_nil end it "should not extract builtin virtual resources" do @catalog.vertices.find { |v| v.name == "virtualobject" }.should be_nil end it "should copy the tag list to the new catalog" do @catalog.tags.sort.should == @original.tags.sort end it "should copy the class list to the new catalog" do @catalog.classes.should == @original.classes end it "should duplicate the original edges" do @original.edges.each do |edge| next if edge.source.virtual? or edge.target.virtual? source = @catalog.resource(edge.source.ref) target = @catalog.resource(edge.target.ref) source.should_not be_nil target.should_not be_nil @catalog.edge?(source, target).should be_true end end it "should set itself as the catalog for each converted resource" do @catalog.vertices.each { |v| v.catalog.object_id.should equal(@catalog.object_id) } end end describe Puppet::Node::Catalog, " when converting to a RAL catalog" do before do @original = Puppet::Node::Catalog.new("mynode") @original.tag(*%w{one two three}) @original.add_class *%w{four five six} @top = Puppet::TransObject.new 'top', "class" @topobject = Puppet::TransObject.new '/topobject', "file" @middle = Puppet::TransObject.new 'middle', "class" @middleobject = Puppet::TransObject.new '/middleobject', "file" @bottom = Puppet::TransObject.new 'bottom', "class" @bottomobject = Puppet::TransObject.new '/bottomobject', "file" @resources = [@top, @topobject, @middle, @middleobject, @bottom, @bottomobject] @original.add_resource(*@resources) @original.add_edge(@top, @topobject) @original.add_edge(@top, @middle) @original.add_edge(@middle, @middleobject) @original.add_edge(@middle, @bottom) @original.add_edge(@bottom, @bottomobject) @catalog = @original.to_ral end it "should add all resources as RAL instances" do @resources.each { |resource| @catalog.resource(resource.ref).should be_instance_of(Puppet::Type) } end it "should copy the tag list to the new catalog" do @catalog.tags.sort.should == @original.tags.sort end it "should copy the class list to the new catalog" do @catalog.classes.should == @original.classes end it "should duplicate the original edges" do @original.edges.each do |edge| @catalog.edge?(@catalog.resource(edge.source.ref), @catalog.resource(edge.target.ref)).should be_true end end it "should set itself as the catalog for each converted resource" do @catalog.vertices.each { |v| v.catalog.object_id.should equal(@catalog.object_id) } end # This tests #931. it "should not lose track of resources whose names vary" do changer = Puppet::TransObject.new 'changer', 'test' config = Puppet::Node::Catalog.new('test') config.add_resource(changer) config.add_resource(@top) config.add_edge(@top, changer) resource = stub 'resource', :name => "changer2", :title => "changer2", :ref => "Test[changer2]", :catalog= => nil, :remove => nil changer.expects(:to_type).returns(resource) newconfig = nil Puppet::Type.allclear proc { @catalog = config.to_ral }.should_not raise_error @catalog.resource("Test[changer2]").should equal(resource) end after do # Remove all resource instances. @catalog.clear(true) end end describe Puppet::Node::Catalog, " when functioning as a resource container" do before do @catalog = Puppet::Node::Catalog.new("host") @one = stub 'resource1', :ref => "Me[one]", :catalog= => nil @two = stub 'resource2', :ref => "Me[two]", :catalog= => nil @dupe = stub 'resource3', :ref => "Me[one]", :catalog= => nil end it "should provide a method to add one or more resources" do @catalog.add_resource @one, @two @catalog.resource(@one.ref).should equal(@one) @catalog.resource(@two.ref).should equal(@two) end it "should set itself as the resource's catalog if it is not a relationship graph" do @one.expects(:catalog=).with(@catalog) @catalog.add_resource @one end it "should not set itself as the resource's catalog if it is a relationship graph" do @one.expects(:catalog=).never @catalog.is_relationship_graph = true @catalog.add_resource @one end it "should make all vertices available by resource reference" do @catalog.add_resource(@one) @catalog.resource(@one.ref).should equal(@one) @catalog.vertices.find { |r| r.ref == @one.ref }.should equal(@one) end it "should canonize how resources are referred to during retrieval when both type and title are provided" do @catalog.add_resource(@one) @catalog.resource("me", "one").should equal(@one) end it "should canonize how resources are referred to during retrieval when just the title is provided" do @catalog.add_resource(@one) @catalog.resource("me[one]", nil).should equal(@one) end it "should not allow two resources with the same resource reference" do @catalog.add_resource(@one) # These are used to build the failure @dupe.stubs(:file) @dupe.stubs(:line) @one.stubs(:file) @one.stubs(:line) proc { @catalog.add_resource(@dupe) }.should raise_error(ArgumentError) end it "should not store objects that do not respond to :ref" do proc { @catalog.add_resource("thing") }.should raise_error(ArgumentError) end it "should remove all resources when asked" do @catalog.add_resource @one @catalog.add_resource @two @one.expects :remove @two.expects :remove @catalog.clear(true) end it "should support a mechanism for finishing resources" do @one.expects :finish @two.expects :finish @catalog.add_resource @one @catalog.add_resource @two @catalog.finalize end it "should make default resources when finalizing" do @catalog.expects(:make_default_resources) @catalog.finalize end it "should add default resources to the catalog upon creation" do @catalog.make_default_resources @catalog.resource(:schedule, "daily").should_not be_nil end it "should optionally support an initialization block and should finalize after such blocks" do @one.expects :finish @two.expects :finish config = Puppet::Node::Catalog.new("host") do |conf| conf.add_resource @one conf.add_resource @two end end it "should inform the resource that it is the resource's catalog" do @one.expects(:catalog=).with(@catalog) @catalog.add_resource @one end it "should be able to find resources by reference" do @catalog.add_resource @one @catalog.resource(@one.ref).should equal(@one) end it "should be able to find resources by reference or by type/title tuple" do @catalog.add_resource @one @catalog.resource("me", "one").should equal(@one) end it "should have a mechanism for removing resources" do @catalog.add_resource @one @one.expects :remove @catalog.remove_resource(@one) @catalog.resource(@one.ref).should be_nil @catalog.vertex?(@one).should be_false end it "should have a method for creating aliases for resources" do @catalog.add_resource @one @catalog.alias(@one, "other") @catalog.resource("me", "other").should equal(@one) end # This test is the same as the previous, but the behaviour should be explicit. it "should alias using the class name from the resource reference, not the resource class name" do @catalog.add_resource @one @catalog.alias(@one, "other") @catalog.resource("me", "other").should equal(@one) end it "should fail to add an alias if the aliased name already exists" do @catalog.add_resource @one proc { @catalog.alias @two, "one" }.should raise_error(ArgumentError) end it "should not fail when a resource has duplicate aliases created" do @catalog.add_resource @one proc { @catalog.alias @one, "one" }.should_not raise_error end it "should be able to look resources up by their aliases" do @catalog.add_resource @one @catalog.alias @one, "two" @catalog.resource(:me, "two").should equal(@one) end it "should remove resource aliases when the target resource is removed" do @catalog.add_resource @one @catalog.alias(@one, "other") @one.expects :remove @catalog.remove_resource(@one) @catalog.resource("me", "other").should be_nil end it "should add an alias for the namevar when the title and name differ on isomorphic resource types" do resource = Puppet::Type.type(:file).create :path => "/something", :title => "other", :content => "blah" + resource.expects(:isomorphic?).returns(true) @catalog.add_resource(resource) @catalog.resource(:file, "other").should equal(resource) @catalog.resource(:file, "/something").ref.should == resource.ref end it "should not add an alias for the namevar when the title and name differ on non-isomorphic resource types" do - resource = Puppet::Type.type(:exec).create :command => "/bin/true", :title => "other" + resource = Puppet::Type.type(:file).create :path => "/something", :title => "other", :content => "blah" + resource.expects(:isomorphic?).returns(false) @catalog.add_resource(resource) - @catalog.resource(:exec, resource.title).should equal(resource) + @catalog.resource(:file, resource.title).should equal(resource) # We can't use .should here, because the resources respond to that method. - if @catalog.resource(:exec, resource.name) + if @catalog.resource(:file, resource.name) raise "Aliased non-isomorphic resource" end end after do Puppet::Type.allclear end end describe Puppet::Node::Catalog do before :each do @catalog = Puppet::Node::Catalog.new("host") @catalog.retrieval_duration = Time.now @transaction = mock 'transaction' Puppet::Transaction.stubs(:new).returns(@transaction) @transaction.stubs(:evaluate) @transaction.stubs(:cleanup) @transaction.stubs(:addtimes) end describe Puppet::Node::Catalog, " when applying" do it "should create and evaluate a transaction" do @transaction.expects(:evaluate) @catalog.apply end it "should provide the catalog time to the transaction" do @transaction.expects(:addtimes).with do |arg| arg[:config_retrieval].should be_instance_of(Time) true end @catalog.apply end it "should clean up the transaction" do @transaction.expects :cleanup @catalog.apply end it "should return the transaction" do @catalog.apply.should equal(@transaction) end it "should yield the transaction if a block is provided" do @catalog.apply do |trans| trans.should equal(@transaction) end end it "should default to not being a host catalog" do @catalog.host_config.should be_nil end it "should pass supplied tags on to the transaction" do @transaction.expects(:tags=).with(%w{one two}) @catalog.apply(:tags => %w{one two}) end it "should set ignoreschedules on the transaction if specified in apply()" do @transaction.expects(:ignoreschedules=).with(true) @catalog.apply(:ignoreschedules => true) end end describe Puppet::Node::Catalog, " when applying host catalogs" do # super() doesn't work in the setup method for some reason before do @catalog.host_config = true end it "should send a report if reporting is enabled" do Puppet[:report] = true @transaction.expects :send_report @transaction.stubs :any_failed? => false @catalog.apply end it "should send a report if report summaries are enabled" do Puppet[:summarize] = true @transaction.expects :send_report @transaction.stubs :any_failed? => false @catalog.apply end it "should initialize the state database before applying a catalog" do Puppet::Util::Storage.expects(:load) # Short-circuit the apply, so we know we're loading before the transaction Puppet::Transaction.expects(:new).raises ArgumentError proc { @catalog.apply }.should raise_error(ArgumentError) end it "should sync the state database after applying" do Puppet::Util::Storage.expects(:store) @transaction.stubs :any_failed? => false @catalog.apply end after { Puppet.settings.clear } end describe Puppet::Node::Catalog, " when applying non-host catalogs" do before do @catalog.host_config = false end it "should never send reports" do Puppet[:report] = true Puppet[:summarize] = true @transaction.expects(:send_report).never @catalog.apply end it "should never modify the state database" do Puppet::Util::Storage.expects(:load).never Puppet::Util::Storage.expects(:store).never @catalog.apply end after { Puppet.settings.clear } end end describe Puppet::Node::Catalog, " when creating a relationship graph" do before do Puppet::Type.type(:component) @catalog = Puppet::Node::Catalog.new("host") @compone = Puppet::Type::Component.create :name => "one" @comptwo = Puppet::Type::Component.create :name => "two", :require => ["class", "one"] @file = Puppet::Type.type(:file) @one = @file.create :path => "/one" @two = @file.create :path => "/two" @catalog.add_edge @compone, @one @catalog.add_edge @comptwo, @two @three = @file.create :path => "/three" @four = @file.create :path => "/four", :require => ["file", "/three"] @five = @file.create :path => "/five" @catalog.add_resource @compone, @comptwo, @one, @two, @three, @four, @five @relationships = @catalog.relationship_graph end it "should fail when trying to create a relationship graph for a relationship graph" do proc { @relationships.relationship_graph }.should raise_error(Puppet::DevError) end it "should be able to create a relationship graph" do @relationships.should be_instance_of(Puppet::Node::Catalog) end it "should copy its host_config setting to the relationship graph" do config = Puppet::Node::Catalog.new config.host_config = true config.relationship_graph.host_config.should be_true end it "should not have any components" do @relationships.vertices.find { |r| r.instance_of?(Puppet::Type::Component) }.should be_nil end it "should have all non-component resources from the catalog" do # The failures print out too much info, so i just do a class comparison @relationships.vertex?(@five).should be_true end it "should have all resource relationships set as edges" do @relationships.edge?(@three, @four).should be_true end it "should copy component relationships to all contained resources" do @relationships.edge?(@one, @two).should be_true end it "should get removed when the catalog is cleaned up" do @relationships.expects(:clear).with(false) @catalog.clear @catalog.instance_variable_get("@relationship_graph").should be_nil end it "should create a new relationship graph after clearing the old one" do @relationships.expects(:clear).with(false) @catalog.clear @catalog.relationship_graph.should be_instance_of(Puppet::Node::Catalog) end it "should look up resources in the relationship graph if not found in the main catalog" do five = stub 'five', :ref => "File[five]", :catalog= => nil @relationships.add_resource five @catalog.resource(five.ref).should equal(five) end it "should provide a method to create additional resources that also registers the resource" do args = {:name => "/yay", :ensure => :file} resource = stub 'file', :ref => "File[/yay]", :catalog= => @catalog Puppet::Type.type(:file).expects(:create).with(args).returns(resource) @catalog.create_resource :file, args @catalog.resource("File[/yay]").should equal(resource) end it "should provide a mechanism for creating implicit resources" do args = {:name => "/yay", :ensure => :file} resource = stub 'file', :ref => "File[/yay]", :catalog= => @catalog Puppet::Type.type(:file).expects(:create).with(args).returns(resource) resource.expects(:implicit=).with(true) @catalog.create_implicit_resource :file, args @catalog.resource("File[/yay]").should equal(resource) end it "should add implicit resources to the relationship graph if there is one" do args = {:name => "/yay", :ensure => :file} resource = stub 'file', :ref => "File[/yay]", :catalog= => @catalog resource.expects(:implicit=).with(true) Puppet::Type.type(:file).expects(:create).with(args).returns(resource) # build the graph relgraph = @catalog.relationship_graph @catalog.create_implicit_resource :file, args relgraph.resource("File[/yay]").should equal(resource) end it "should remove resources created mid-transaction" do args = {:name => "/yay", :ensure => :file} resource = stub 'file', :ref => "File[/yay]", :catalog= => @catalog @transaction = mock 'transaction' Puppet::Transaction.stubs(:new).returns(@transaction) @transaction.stubs(:evaluate) @transaction.stubs(:cleanup) @transaction.stubs(:addtimes) Puppet::Type.type(:file).expects(:create).with(args).returns(resource) resource.expects :remove @catalog.apply do |trans| @catalog.create_resource :file, args @catalog.resource("File[/yay]").should equal(resource) end @catalog.resource("File[/yay]").should be_nil end it "should remove resources from the relationship graph if it exists" do @catalog.remove_resource(@one) @catalog.relationship_graph.vertex?(@one).should be_false end after do Puppet::Type.allclear end end describe Puppet::Node::Catalog, " when writing dot files" do before do @catalog = Puppet::Node::Catalog.new("host") @name = :test @file = File.join(Puppet[:graphdir], @name.to_s + ".dot") end it "should only write when it is a host catalog" do File.expects(:open).with(@file).never @catalog.host_config = false Puppet[:graph] = true @catalog.write_graph(@name) end it "should only write when graphing is enabled" do File.expects(:open).with(@file).never @catalog.host_config = true Puppet[:graph] = false @catalog.write_graph(@name) end it "should write a dot file based on the passed name" do File.expects(:open).with(@file, "w").yields(stub("file", :puts => nil)) @catalog.expects(:to_dot).with("name" => @name.to_s.capitalize) @catalog.host_config = true Puppet[:graph] = true @catalog.write_graph(@name) end after do Puppet.settings.clear end end describe Puppet::Node::Catalog, " when indirecting" do before do @indirection = mock 'indirection' Puppet::Indirector::Indirection.clear_cache end it "should redirect to the indirection for retrieval" do Puppet::Node::Catalog.stubs(:indirection).returns(@indirection) @indirection.expects(:find).with(:myconfig) Puppet::Node::Catalog.find(:myconfig) end it "should default to the 'compiler' terminus" do Puppet::Node::Catalog.indirection.terminus_class.should == :compiler end after do mocha_verify Puppet::Indirector::Indirection.clear_cache end end describe Puppet::Node::Catalog, " when converting to yaml" do before do @catalog = Puppet::Node::Catalog.new("me") @catalog.add_edge("one", "two") end it "should be able to be dumped to yaml" do YAML.dump(@catalog).should be_instance_of(String) end end describe Puppet::Node::Catalog, " when converting from yaml" do before do @catalog = Puppet::Node::Catalog.new("me") @catalog.add_edge("one", "two") text = YAML.dump(@catalog) @newcatalog = YAML.load(text) end it "should get converted back to a catalog" do @newcatalog.should be_instance_of(Puppet::Node::Catalog) end it "should have all vertices" do @newcatalog.vertex?("one").should be_true @newcatalog.vertex?("two").should be_true end it "should have all edges" do @newcatalog.edge?("one", "two").should be_true end end diff --git a/spec/unit/parser/ast/hostclass.rb b/spec/unit/parser/ast/hostclass.rb index a53c3b092..0abc174d9 100755 --- a/spec/unit/parser/ast/hostclass.rb +++ b/spec/unit/parser/ast/hostclass.rb @@ -1,129 +1,135 @@ #!/usr/bin/env ruby require File.dirname(__FILE__) + '/../../../spec_helper' describe Puppet::Parser::AST::HostClass do before :each do @node = Puppet::Node.new "testnode" @parser = Puppet::Parser::Parser.new :environment => "development" @scope_resource = stub 'scope_resource', :builtin? => true @compiler = Puppet::Parser::Compiler.new(@node, @parser) @scope = @compiler.topscope end describe Puppet::Parser::AST::HostClass, "when evaluating" do before do @top = @parser.newclass "top" @middle = @parser.newclass "middle", :parent => "top" end it "should create a resource that references itself" do @top.evaluate(@scope) @compiler.catalog.resource(:class, "top").should be_instance_of(Puppet::Parser::Resource) end it "should evaluate the parent class if one exists" do @middle.evaluate(@scope) @compiler.catalog.resource(:class, "top").should be_instance_of(Puppet::Parser::Resource) end it "should fail to evaluate if a parent class is defined but cannot be found" do othertop = @parser.newclass "something", :parent => "yay" lambda { othertop.evaluate(@scope) }.should raise_error(Puppet::ParseError) end it "should not create a new resource if one already exists" do @compiler.catalog.expects(:resource).with(:class, "top").returns("something") @compiler.catalog.expects(:add_resource).never @top.evaluate(@scope) end + it "should return the existing resource when not creating a new one" do + @compiler.catalog.expects(:resource).with(:class, "top").returns("something") + @compiler.catalog.expects(:add_resource).never + @top.evaluate(@scope).should == "something" + end + it "should not create a new parent resource if one already exists and it has a parent class" do @top.evaluate(@scope) top_resource = @compiler.catalog.resource(:class, "top") @middle.evaluate(@scope) @compiler.catalog.resource(:class, "top").should equal(top_resource) end # #795 - tag before evaluation. it "should tag the catalog with the resource tags when it is evaluated" do @middle.evaluate(@scope) @compiler.catalog.should be_tagged("middle") end it "should tag the catalog with the parent class tags when it is evaluated" do @middle.evaluate(@scope) @compiler.catalog.should be_tagged("top") end end describe Puppet::Parser::AST::HostClass, "when evaluating code" do before do @top_resource = stub "top_resource" @top = @parser.newclass "top", :code => @top_resource @middle_resource = stub "middle_resource" @middle = @parser.newclass "top::middle", :parent => "top", :code => @middle_resource end it "should set its namespace to its fully qualified name" do @middle.namespace.should == "top::middle" end it "should evaluate the code referred to by the class" do @top_resource.expects(:safeevaluate) resource = @top.evaluate(@scope) @top.evaluate_code(resource) end it "should evaluate the parent class's code if it has a parent" do @top_resource.expects(:safeevaluate) @middle_resource.expects(:safeevaluate) resource = @middle.evaluate(@scope) @middle.evaluate_code(resource) end it "should not evaluate the parent class's code if the parent has already been evaluated" do @top_resource.stubs(:safeevaluate) resource = @top.evaluate(@scope) @top.evaluate_code(resource) @top_resource.expects(:safeevaluate).never @middle_resource.stubs(:safeevaluate) resource = @middle.evaluate(@scope) @middle.evaluate_code(resource) end it "should use the parent class's scope as its parent scope" do @top_resource.stubs(:safeevaluate) @middle_resource.stubs(:safeevaluate) resource = @middle.evaluate(@scope) @middle.evaluate_code(resource) @compiler.class_scope(@middle).parent.should equal(@compiler.class_scope(@top)) end it "should add the parent class's namespace to its namespace search path" do @top_resource.stubs(:safeevaluate) @middle_resource.stubs(:safeevaluate) resource = @middle.evaluate(@scope) @middle.evaluate_code(resource) @compiler.class_scope(@middle).namespaces.should be_include(@top.namespace) end end -end \ No newline at end of file +end diff --git a/spec/unit/parser/resource.rb b/spec/unit/parser/resource.rb index a5a49e2a6..035590341 100755 --- a/spec/unit/parser/resource.rb +++ b/spec/unit/parser/resource.rb @@ -1,149 +1,164 @@ #!/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 +describe Puppet::Parser::Resource 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") @compiler = Puppet::Parser::Compiler.new(@node, @parser) @scope = @compiler.topscope end - it "should evaluate the associated AST definition" do - res = @type.new(:type => "mydefine", :title => "whatever", :scope => @scope, :source => @source) - @definition.expects(:evaluate_code).with(res) - - res.evaluate + it "should be isomorphic if it is builtin and models an isomorphic type" do + Puppet::Type.type(:file).expects(:isomorphic?).returns(true) + @resource = Puppet::Parser::Resource.new(:type => "file", :title => "whatever", :scope => @scope, :source => @source).isomorphic?.should be_true end - it "should evaluate the associated AST class" do - res = @type.new(:type => "class", :title => "myclass", :scope => @scope, :source => @source) - @class.expects(:evaluate_code).with(res) - res.evaluate + it "should not be isomorphic if it is builtin and models a non-isomorphic type" do + Puppet::Type.type(:file).expects(:isomorphic?).returns(false) + @resource = Puppet::Parser::Resource.new(:type => "file", :title => "whatever", :scope => @scope, :source => @source).isomorphic?.should be_false end - it "should evaluate the associated AST node" do - res = @type.new(:type => "node", :title => "mynode", :scope => @scope, :source => @source) - @nodedef.expects(:evaluate_code).with(res) - res.evaluate + it "should be isomorphic if it is not builtin" do + @parser.newdefine "whatever" + @resource = Puppet::Parser::Resource.new(:type => "whatever", :title => "whatever", :scope => @scope, :source => @source).isomorphic?.should be_true end -end -describe Puppet::Parser::Resource, " when finishing" do - before do - @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") - @compiler = Puppet::Parser::Compiler.new(@node, @parser) - @scope = @compiler.topscope + describe "when evaluating" do + before do + @type = Puppet::Parser::Resource - @resource = Puppet::Parser::Resource.new(:type => "mydefine", :title => "whatever", :scope => @scope, :source => @source) - end + @definition = @parser.newdefine "mydefine" + @class = @parser.newclass "myclass" + @nodedef = @parser.newnode("mynode")[0] + end - it "should copy metaparams from its scope" do - @scope.setvar("noop", "true") + it "should evaluate the associated AST definition" do + res = @type.new(:type => "mydefine", :title => "whatever", :scope => @scope, :source => @source) + @definition.expects(:evaluate_code).with(res) - @resource.class.publicize_methods(:add_metaparams) { @resource.add_metaparams } + res.evaluate + end - @resource["noop"].should == "true" + it "should evaluate the associated AST class" do + res = @type.new(:type => "class", :title => "myclass", :scope => @scope, :source => @source) + @class.expects(:evaluate_code).with(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_code).with(res) + res.evaluate + end end - it "should not copy metaparams that it already has" do - @resource.class.publicize_methods(:set_parameter) { @resource.set_parameter("noop", "false") } - @scope.setvar("noop", "true") + describe "when finishing" do + before do + @definition = @parser.newdefine "mydefine" + @class = @parser.newclass "myclass" + @nodedef = @parser.newnode("mynode")[0] - @resource.class.publicize_methods(:add_metaparams) { @resource.add_metaparams } + @resource = Puppet::Parser::Resource.new(:type => "mydefine", :title => "whatever", :scope => @scope, :source => @source) + end - @resource["noop"].should == "false" - end + it "should copy metaparams from its scope" do + @scope.setvar("noop", "true") - it "should stack relationship metaparams from its container if it already has them" do - @resource.class.publicize_methods(:set_parameter) { @resource.set_parameter("require", "resource") } - @scope.setvar("require", "container") + @resource.class.publicize_methods(:add_metaparams) { @resource.add_metaparams } - @resource.class.publicize_methods(:add_metaparams) { @resource.add_metaparams } + @resource["noop"].should == "true" + end - @resource["require"].sort.should == %w{container resource} - end + it "should not copy metaparams that it already has" do + @resource.class.publicize_methods(:set_parameter) { @resource.set_parameter("noop", "false") } + @scope.setvar("noop", "true") - it "should flatten the array resulting from stacking relationship metaparams" do - @resource.class.publicize_methods(:set_parameter) { @resource.set_parameter("require", ["resource1", "resource2"]) } - @scope.setvar("require", %w{container1 container2}) + @resource.class.publicize_methods(:add_metaparams) { @resource.add_metaparams } - @resource.class.publicize_methods(:add_metaparams) { @resource.add_metaparams } + @resource["noop"].should == "false" + end - @resource["require"].sort.should == %w{container1 container2 resource1 resource2} - end + it "should stack relationship metaparams from its container if it already has them" do + @resource.class.publicize_methods(:set_parameter) { @resource.set_parameter("require", "resource") } + @scope.setvar("require", "container") - it "should add any tags from the scope resource" do - scope_resource = stub 'scope_resource', :tags => %w{one two} - @scope.stubs(:resource).returns(scope_resource) + @resource.class.publicize_methods(:add_metaparams) { @resource.add_metaparams } - @resource.class.publicize_methods(:add_scope_tags) { @resource.add_scope_tags } + @resource["require"].sort.should == %w{container resource} + end - @resource.tags.should be_include("one") - @resource.tags.should be_include("two") - end -end + it "should flatten the array resulting from stacking relationship metaparams" do + @resource.class.publicize_methods(:set_parameter) { @resource.set_parameter("require", ["resource1", "resource2"]) } + @scope.setvar("require", %w{container1 container2}) -describe Puppet::Parser::Resource, "when being tagged" do - before do - @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')) - end + @resource.class.publicize_methods(:add_metaparams) { @resource.add_metaparams } - it "should get tagged with the resource type" do - @resource.tags.should be_include("file") - end + @resource["require"].sort.should == %w{container1 container2 resource1 resource2} + end - it "should get tagged with the title" do - @resource.tags.should be_include("yay") - end + it "should add any tags from the scope resource" do + scope_resource = stub 'scope_resource', :tags => %w{one two} + @scope.stubs(:resource).returns(scope_resource) - it "should get tagged with each name in the title if the title is a qualified class name" do - resource = Puppet::Parser::Resource.new(:type => "file", :title => "one::two", :scope => @scope, :source => mock('source')) - resource.tags.should be_include("one") - resource.tags.should be_include("two") - end + @resource.class.publicize_methods(:add_scope_tags) { @resource.add_scope_tags } - it "should get tagged with each name in the type if the type is a qualified class name" do - resource = Puppet::Parser::Resource.new(:type => "one::two", :title => "whatever", :scope => @scope, :source => mock('source')) - resource.tags.should be_include("one") - resource.tags.should be_include("two") + @resource.tags.should be_include("one") + @resource.tags.should be_include("two") + end end - it "should not get tagged with non-alphanumeric titles" do - resource = Puppet::Parser::Resource.new(:type => "file", :title => "this is a test", :scope => @scope, :source => mock('source')) - resource.tags.should_not be_include("this is a test") - end + describe "when being tagged" do + before do + @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')) + end - it "should fail on tags containing '*' characters" do - lambda { @resource.tag("bad*tag") }.should raise_error(Puppet::ParseError) - end + it "should get tagged with the resource type" do + @resource.tags.should be_include("file") + end - it "should fail on tags starting with '-' characters" do - lambda { @resource.tag("-badtag") }.should raise_error(Puppet::ParseError) - end + it "should get tagged with the title" do + @resource.tags.should be_include("yay") + end - it "should fail on tags containing ' ' characters" do - lambda { @resource.tag("bad tag") }.should raise_error(Puppet::ParseError) - end + it "should get tagged with each name in the title if the title is a qualified class name" do + resource = Puppet::Parser::Resource.new(:type => "file", :title => "one::two", :scope => @scope, :source => mock('source')) + resource.tags.should be_include("one") + resource.tags.should be_include("two") + end + + it "should get tagged with each name in the type if the type is a qualified class name" do + resource = Puppet::Parser::Resource.new(:type => "one::two", :title => "whatever", :scope => @scope, :source => mock('source')) + resource.tags.should be_include("one") + resource.tags.should be_include("two") + end + + it "should not get tagged with non-alphanumeric titles" do + resource = Puppet::Parser::Resource.new(:type => "file", :title => "this is a test", :scope => @scope, :source => mock('source')) + resource.tags.should_not be_include("this is a test") + end + + it "should fail on tags containing '*' characters" do + lambda { @resource.tag("bad*tag") }.should raise_error(Puppet::ParseError) + end + + it "should fail on tags starting with '-' characters" do + lambda { @resource.tag("-badtag") }.should raise_error(Puppet::ParseError) + end + + it "should fail on tags containing ' ' characters" do + lambda { @resource.tag("bad tag") }.should raise_error(Puppet::ParseError) + end - it "should allow alpha tags" do - lambda { @resource.tag("good_tag") }.should_not raise_error(Puppet::ParseError) + it "should allow alpha tags" do + lambda { @resource.tag("good_tag") }.should_not raise_error(Puppet::ParseError) + end end end diff --git a/spec/unit/ral/types/package.rb b/spec/unit/ral/types/package.rb index 785d2eb37..5d96dc4ae 100755 --- a/spec/unit/ral/types/package.rb +++ b/spec/unit/ral/types/package.rb @@ -1,247 +1,247 @@ #!/usr/bin/env ruby require File.dirname(__FILE__) + '/../../../spec_helper' require 'puppet/type/package' describe Puppet::Type::Package do it "should have an :installable feature that requires the :install method" do Puppet::Type::Package.provider_feature(:installable).methods.should == [:install] end it "should have an :uninstallable feature that requires the :uninstall method" do Puppet::Type::Package.provider_feature(:uninstallable).methods.should == [:uninstall] end it "should have an :upgradeable feature that requires :update and :latest methods" do Puppet::Type::Package.provider_feature(:upgradeable).methods.should == [:update, :latest] end it "should have a :purgeable feature that requires the :purge latest method" do Puppet::Type::Package.provider_feature(:purgeable).methods.should == [:purge] end it "should have a :versionable feature" do Puppet::Type::Package.provider_feature(:versionable).should_not be_nil end it "should default to being installed" do pkg = Puppet::Type::Package.create(:name => "yay") pkg.should(:ensure).should == :present end after { Puppet::Type::Package.clear } end describe Puppet::Type::Package, "when validating attributes" do [:name, :source, :instance, :status, :adminfile, :responsefile, :configfiles, :category, :platform, :root, :vendor, :description, :allowcdrom].each do |param| it "should have a #{param} parameter" do Puppet::Type::Package.attrtype(param).should == :param end end it "should have an ensure property" do Puppet::Type::Package.attrtype(:ensure).should == :property end end describe Puppet::Type::Package, "when validating attribute values" do before do @provider = stub 'provider', :class => Puppet::Type::Package.defaultprovider, :clear => nil Puppet::Type::Package.defaultprovider.expects(:new).returns(@provider) end it "should support :present as a value to :ensure" do Puppet::Type::Package.create(:name => "yay", :ensure => :present) end it "should alias :installed to :present as a value to :ensure" do pkg = Puppet::Type::Package.create(:name => "yay", :ensure => :installed) pkg.should(:ensure).should == :present end it "should support :absent as a value to :ensure" do Puppet::Type::Package.create(:name => "yay", :ensure => :absent) end it "should support :purged as a value to :ensure if the provider has the :purgeable feature" do @provider.expects(:satisfies?).with(:purgeable).returns(true) Puppet::Type::Package.create(:name => "yay", :ensure => :purged) end it "should not support :purged as a value to :ensure if the provider does not have the :purgeable feature" do @provider.expects(:satisfies?).with(:purgeable).returns(false) proc { Puppet::Type::Package.create(:name => "yay", :ensure => :purged) }.should raise_error(Puppet::Error) end it "should support :latest as a value to :ensure if the provider has the :upgradeable feature" do @provider.expects(:satisfies?).with(:upgradeable).returns(true) Puppet::Type::Package.create(:name => "yay", :ensure => :latest) end it "should not support :latest as a value to :ensure if the provider does not have the :upgradeable feature" do @provider.expects(:satisfies?).with(:upgradeable).returns(false) proc { Puppet::Type::Package.create(:name => "yay", :ensure => :latest) }.should raise_error(Puppet::Error) end it "should support version numbers as a value to :ensure if the provider has the :versionable feature" do @provider.expects(:satisfies?).with(:versionable).returns(true) Puppet::Type::Package.create(:name => "yay", :ensure => "1.0") end it "should not support version numbers as a value to :ensure if the provider does not have the :versionable feature" do @provider.expects(:satisfies?).with(:versionable).returns(false) proc { Puppet::Type::Package.create(:name => "yay", :ensure => "1.0") }.should raise_error(Puppet::Error) end - it "should only accept files and URLs as values to :source" do - proc { Puppet::Type::Package.create(:name => "yay", :source => "stuff") }.should raise_error(Puppet::Error) + it "should accept any string as an argument to :source" do + proc { Puppet::Type::Package.create(:name => "yay", :source => "stuff") }.should_not raise_error(Puppet::Error) end after { Puppet::Type::Package.clear } end module PackageEvaluationTesting def setprops(properties) @provider.stubs(:properties).returns(properties) end end describe Puppet::Type::Package do before :each do @provider = stub 'provider', :class => Puppet::Type::Package.defaultprovider, :clear => nil, :satisfies? => true, :name => :mock Puppet::Type::Package.defaultprovider.stubs(:new).returns(@provider) @package = Puppet::Type::Package.create(:name => "yay") @catalog = Puppet::Node::Catalog.new @catalog.add_resource(@package) end after :each do @catalog.clear(true) Puppet::Type::Package.clear end describe Puppet::Type::Package, "when it should be purged" do include PackageEvaluationTesting before { @package[:ensure] = :purged } it "should do nothing if it is :purged" do @provider.expects(:properties).returns(:ensure => :purged) @catalog.apply end [:absent, :installed, :present, :latest].each do |state| it "should purge if it is #{state.to_s}" do @provider.stubs(:properties).returns(:ensure => state) @provider.expects(:purge) @catalog.apply end end end describe Puppet::Type::Package, "when it should be absent" do include PackageEvaluationTesting before { @package[:ensure] = :absent } [:purged, :absent].each do |state| it "should do nothing if it is #{state.to_s}" do @provider.expects(:properties).returns(:ensure => state) @catalog.apply end end [:installed, :present, :latest].each do |state| it "should uninstall if it is #{state.to_s}" do @provider.stubs(:properties).returns(:ensure => state) @provider.expects(:uninstall) @catalog.apply end end end describe Puppet::Type::Package, "when it should be present" do include PackageEvaluationTesting before { @package[:ensure] = :present } [:present, :latest, "1.0"].each do |state| it "should do nothing if it is #{state.to_s}" do @provider.expects(:properties).returns(:ensure => state) @catalog.apply end end [:purged, :absent].each do |state| it "should install if it is #{state.to_s}" do @provider.stubs(:properties).returns(:ensure => state) @provider.expects(:install) @catalog.apply end end end describe Puppet::Type::Package, "when it should be latest" do include PackageEvaluationTesting before { @package[:ensure] = :latest } [:purged, :absent].each do |state| it "should upgrade if it is #{state.to_s}" do @provider.stubs(:properties).returns(:ensure => state) @provider.expects(:update) @catalog.apply end end it "should upgrade if the current version is not equal to the latest version" do @provider.stubs(:properties).returns(:ensure => "1.0") @provider.stubs(:latest).returns("2.0") @provider.expects(:update) @catalog.apply end it "should do nothing if it is equal to the latest version" do @provider.stubs(:properties).returns(:ensure => "1.0") @provider.stubs(:latest).returns("1.0") @provider.expects(:update).never @catalog.apply end it "should do nothing if the provider returns :present as the latest version" do @provider.stubs(:properties).returns(:ensure => :present) @provider.stubs(:latest).returns("1.0") @provider.expects(:update).never @catalog.apply end end describe Puppet::Type::Package, "when it should be a specific version" do include PackageEvaluationTesting before { @package[:ensure] = "1.0" } [:purged, :absent].each do |state| it "should install if it is #{state.to_s}" do @provider.stubs(:properties).returns(:ensure => state) @provider.expects(:install) @catalog.apply end end it "should do nothing if the current version is equal to the desired version" do @provider.stubs(:properties).returns(:ensure => "1.0") @provider.expects(:install).never @catalog.apply end it "should install if the current version is not equal to the specified version" do @provider.stubs(:properties).returns(:ensure => "2.0") @provider.expects(:install) @catalog.apply end end end diff --git a/spec/unit/ral/types/service.rb b/spec/unit/ral/types/service.rb index 981d38a15..0f00992fa 100755 --- a/spec/unit/ral/types/service.rb +++ b/spec/unit/ral/types/service.rb @@ -1,249 +1,256 @@ #!/usr/bin/env ruby require File.dirname(__FILE__) + '/../../../spec_helper' require 'puppet/type/service' describe Puppet::Type::Service do it "should have an :enableable feature that requires the :enable, :disable, and :enabled? methods" do Puppet::Type::Service.provider_feature(:enableable).methods.should == [:disable, :enable, :enabled?] end it "should have a :refreshable feature that requires the :restart method" do Puppet::Type::Service.provider_feature(:refreshable).methods.should == [:restart] end end describe Puppet::Type::Service, "when validating attributes" do - [:name, :binary, :hasstatus, :path, :pattern, :start, :restart, :stop, :status, :hasrestart].each do |param| + [:name, :binary, :hasstatus, :path, :pattern, :start, :restart, :stop, :status, :hasrestart, :control].each do |param| it "should have a #{param} parameter" do Puppet::Type::Service.attrtype(param).should == :param end end [:ensure, :enable].each do |param| it "should have an #{param} property" do Puppet::Type::Service.attrtype(param).should == :property end end end describe Puppet::Type::Service, "when validating attribute values" do before do - @provider = stub 'provider', :class => Puppet::Type::Service.defaultprovider, :clear => nil + @provider = stub 'provider', :class => Puppet::Type::Service.defaultprovider, :clear => nil, :controllable? => false Puppet::Type::Service.defaultprovider.stubs(:new).returns(@provider) end it "should support :running as a value to :ensure" do Puppet::Type::Service.create(:name => "yay", :ensure => :running) end it "should support :stopped as a value to :ensure" do Puppet::Type::Service.create(:name => "yay", :ensure => :stopped) end it "should alias the value :true to :running in :ensure" do svc = Puppet::Type::Service.create(:name => "yay", :ensure => true) svc.should(:ensure).should == :running end it "should alias the value :false to :stopped in :ensure" do svc = Puppet::Type::Service.create(:name => "yay", :ensure => false) svc.should(:ensure).should == :stopped end it "should support :true as a value to :enable" do Puppet::Type::Service.create(:name => "yay", :enable => :true) end it "should support :false as a value to :enable" do Puppet::Type::Service.create(:name => "yay", :enable => :false) end it "should support :true as a value to :hasstatus" do Puppet::Type::Service.create(:name => "yay", :hasstatus => :true) end it "should support :false as a value to :hasstatus" do Puppet::Type::Service.create(:name => "yay", :hasstatus => :false) end it "should support :true as a value to :hasrestart" do Puppet::Type::Service.create(:name => "yay", :hasrestart => :true) end it "should support :false as a value to :hasrestart" do Puppet::Type::Service.create(:name => "yay", :hasrestart => :false) end it "should allow setting the :enable parameter if the provider has the :enableable feature" do Puppet::Type::Service.defaultprovider.stubs(:supports_parameter?).returns(true) Puppet::Type::Service.defaultprovider.expects(:supports_parameter?).with(Puppet::Type::Service.attrclass(:enable)).returns(true) svc = Puppet::Type::Service.create(:name => "yay", :enable => true) svc.should(:enable).should == :true end it "should not allow setting the :enable parameter if the provider is missing the :enableable feature" do Puppet::Type::Service.defaultprovider.stubs(:supports_parameter?).returns(true) Puppet::Type::Service.defaultprovider.expects(:supports_parameter?).with(Puppet::Type::Service.attrclass(:enable)).returns(false) svc = Puppet::Type::Service.create(:name => "yay", :enable => true) svc.should(:enable).should be_nil end it "should discard paths that do not exist" do FileTest.stubs(:exist?).returns(false) FileTest.stubs(:directory?).returns(false) svc = Puppet::Type::Service.create(:name => "yay", :path => "/one/two") svc[:path].should be_empty end it "should discard paths that are not directories" do FileTest.stubs(:exist?).returns(true) FileTest.stubs(:directory?).returns(false) svc = Puppet::Type::Service.create(:name => "yay", :path => "/one/two") svc[:path].should be_empty end it "should split paths on ':'" do FileTest.stubs(:exist?).returns(true) FileTest.stubs(:directory?).returns(true) svc = Puppet::Type::Service.create(:name => "yay", :path => "/one/two:/three/four") svc[:path].should == %w{/one/two /three/four} end it "should accept arrays of paths joined by ':'" do FileTest.stubs(:exist?).returns(true) FileTest.stubs(:directory?).returns(true) svc = Puppet::Type::Service.create(:name => "yay", :path => ["/one:/two", "/three:/four"]) svc[:path].should == %w{/one /two /three /four} end after { Puppet::Type::Service.clear } end describe Puppet::Type::Service, "when setting default attribute values" do it "should default to the provider's default path if one is available" do FileTest.stubs(:directory?).returns(true) FileTest.stubs(:exist?).returns(true) Puppet::Type::Service.defaultprovider.stubs(:respond_to?).returns(true) Puppet::Type::Service.defaultprovider.stubs(:defpath).returns("testing") svc = Puppet::Type::Service.create(:name => "other") svc[:path].should == ["testing"] end - it "should default to the binary for the pattern if one is provided" do + it "should default 'pattern' to the binary if one is provided" do svc = Puppet::Type::Service.create(:name => "other", :binary => "/some/binary") svc[:pattern].should == "/some/binary" end - it "should default to the name for the pattern if no pattern is provided" do + it "should default 'pattern' to the name if no pattern is provided" do svc = Puppet::Type::Service.create(:name => "other") svc[:pattern].should == "other" end + it "should default 'control' to the upcased service name with periods replaced by underscores if the provider supports the 'controllable' feature" do + provider = stub 'provider', :controllable? => true, :class => Puppet::Type::Service.defaultprovider, :clear => nil + Puppet::Type::Service.defaultprovider.stubs(:new).returns(provider) + svc = Puppet::Type::Service.create(:name => "nfs.client") + svc[:control].should == "NFS_CLIENT_START" + end + after { Puppet::Type::Service.clear } end describe Puppet::Type::Service, "when retrieving the host's current state" do before do @service = Puppet::Type::Service.create(:name => "yay") end it "should use the provider's status to determine whether the service is running" do @service.provider.expects(:status).returns(:yepper) @service[:ensure] = :running @service.property(:ensure).retrieve.should == :yepper end it "should ask the provider whether it is enabled" do @service.provider.class.stubs(:supports_parameter?).returns(true) @service.provider.expects(:enabled?).returns(:yepper) @service[:enable] = true @service.property(:enable).retrieve.should == :yepper end after { Puppet::Type::Service.clear } end describe Puppet::Type::Service, "when changing the host" do before do @service = Puppet::Type::Service.create(:name => "yay") end it "should start the service if it is supposed to be running" do @service[:ensure] = :running @service.provider.expects(:start) @service.property(:ensure).sync end it "should stop the service if it is supposed to be stopped" do @service[:ensure] = :stopped @service.provider.expects(:stop) @service.property(:ensure).sync end it "should enable the service if it is supposed to be enabled" do @service.provider.class.stubs(:supports_parameter?).returns(true) @service[:enable] = true @service.provider.expects(:enable) @service.property(:enable).sync end it "should disable the service if it is supposed to be disabled" do @service.provider.class.stubs(:supports_parameter?).returns(true) @service[:enable] = false @service.provider.expects(:disable) @service.property(:enable).sync end it "should sync the service's enable state when changing the state of :ensure if :enable is being managed" do @service.provider.class.stubs(:supports_parameter?).returns(true) @service[:enable] = false @service[:ensure] = :stopped @service.property(:enable).expects(:retrieve).returns("whatever") @service.property(:enable).expects(:insync?).returns(false) @service.property(:enable).expects(:sync) @service.provider.stubs(:stop) @service.property(:ensure).sync end after { Puppet::Type::Service.clear } end describe Puppet::Type::Service, "when refreshing the service" do before do @service = Puppet::Type::Service.create(:name => "yay") end it "should restart the service if it is running" do @service[:ensure] = :running @service.provider.expects(:status).returns(:running) @service.provider.expects(:restart) @service.refresh end it "should restart the service if it is running, even if it is supposed to stopped" do @service[:ensure] = :stopped @service.provider.expects(:status).returns(:running) @service.provider.expects(:restart) @service.refresh end it "should not restart the service if it is not running" do @service[:ensure] = :running @service.provider.expects(:status).returns(:stopped) @service.refresh end it "should add :ensure as a property if it is not being managed" do @service.provider.expects(:status).returns(:running) @service.provider.expects(:restart) @service.refresh end after { Puppet::Type::Service.clear } end diff --git a/test/ral/types/exec.rb b/test/ral/types/exec.rb index 4133d8519..e2a3dd9ed 100755 --- a/test/ral/types/exec.rb +++ b/test/ral/types/exec.rb @@ -1,770 +1,770 @@ #!/usr/bin/env ruby require File.dirname(__FILE__) + '/../../lib/puppettest' require 'puppettest' class TestExec < Test::Unit::TestCase include PuppetTest def test_execution command = nil output = nil assert_nothing_raised { command = Puppet.type(:exec).create( :command => "/bin/echo" ) } assert_nothing_raised { command.evaluate } assert_events([:executed_command], command) end def test_numvsstring [0, "0"].each { |val| Puppet.type(:exec).clear Puppet.type(:component).clear command = nil output = nil assert_nothing_raised { command = Puppet.type(:exec).create( :command => "/bin/echo", :returns => val ) } assert_events([:executed_command], command) } end def test_path_or_qualified command = nil output = nil assert_raise(Puppet::Error) { command = Puppet.type(:exec).create( :command => "echo" ) } assert_nothing_raised { command = Puppet.type(:exec).create( :command => "echo", :path => "/usr/bin:/bin:/usr/sbin:/sbin" ) } Puppet.type(:exec).clear assert_nothing_raised { command = Puppet.type(:exec).create( :command => "/bin/echo" ) } Puppet.type(:exec).clear assert_nothing_raised { command = Puppet.type(:exec).create( :command => "/bin/echo", :path => "/usr/bin:/bin:/usr/sbin:/sbin" ) } end def test_nonzero_returns assert_nothing_raised { command = Puppet.type(:exec).create( :command => "mkdir /this/directory/does/not/exist", :path => "/usr/bin:/bin:/usr/sbin:/sbin", :returns => 1 ) } assert_nothing_raised { command = Puppet.type(:exec).create( :command => "touch /etc", :path => "/usr/bin:/bin:/usr/sbin:/sbin", :returns => 1 ) } assert_nothing_raised { command = Puppet.type(:exec).create( :command => "thiscommanddoesnotexist", :path => "/usr/bin:/bin:/usr/sbin:/sbin", :returns => 127 ) } end def test_cwdsettings command = nil dir = "/tmp" wd = Dir.chdir(dir) { Dir.getwd } assert_nothing_raised { command = Puppet.type(:exec).create( :command => "pwd", :cwd => dir, :path => "/usr/bin:/bin:/usr/sbin:/sbin", :returns => 0 ) } assert_events([:executed_command], command) assert_equal(wd,command.output.chomp) end def test_refreshonly_functional file = nil cmd = nil tmpfile = tempfile() @@tmpfiles.push tmpfile trans = nil file = Puppet.type(:file).create( :path => tmpfile, :content => "yay" ) # Get the file in sync assert_apply(file) # Now make an exec maker = tempfile() assert_nothing_raised { cmd = Puppet.type(:exec).create( :command => "touch %s" % maker, :path => "/usr/bin:/bin:/usr/sbin:/sbin", :subscribe => file, :refreshonly => true ) } assert(cmd, "did not make exec") assert_nothing_raised do assert(! cmd.check, "Check passed when refreshonly is set") end assert_events([], file, cmd) assert(! FileTest.exists?(maker), "made file without refreshing") # Now change our content, so we throw a refresh file[:content] = "yayness" assert_events([:file_changed, :triggered], file, cmd) assert(FileTest.exists?(maker), "file was not made in refresh") end def test_refreshonly cmd = true assert_nothing_raised { cmd = Puppet.type(:exec).create( :command => "pwd", :path => "/usr/bin:/bin:/usr/sbin:/sbin", :refreshonly => true ) } # Checks should always fail when refreshonly is enabled assert(!cmd.check, "Check passed with refreshonly true") # Now make sure it passes if we pass in "true" assert(cmd.check(true), "Check failed with refreshonly true while refreshing") # Now set it to false cmd[:refreshonly] = false assert(cmd.check, "Check failed with refreshonly false") end def test_creates file = tempfile() exec = nil assert(! FileTest.exists?(file), "File already exists") assert_nothing_raised { exec = Puppet.type(:exec).create( :command => "touch %s" % file, :path => "/usr/bin:/bin:/usr/sbin:/sbin", :creates => file ) } comp = mk_catalog("createstest", exec) assert_events([:executed_command], comp, "creates") assert_events([], comp, "creates") end # Verify that we can download the file that we're going to execute. def test_retrievethenmkexe exe = tempfile() oexe = tempfile() sh = %x{which sh} File.open(exe, "w") { |f| f.puts "#!#{sh}\necho yup" } file = Puppet.type(:file).create( :path => oexe, :source => exe, :mode => 0755 ) exec = Puppet.type(:exec).create( :command => oexe, :require => [:file, oexe] ) comp = mk_catalog("Testing", file, exec) assert_events([:file_created, :executed_command], comp) end # Verify that we auto-require any managed scripts. def test_autorequire_files exe = tempfile() oexe = tempfile() sh = %x{which sh} File.open(exe, "w") { |f| f.puts "#!#{sh}\necho yup" } file = Puppet.type(:file).create( :path => oexe, :source => exe, :mode => 755 ) basedir = File.dirname(oexe) baseobj = Puppet.type(:file).create( :path => basedir, :source => exe, :mode => 755 ) ofile = Puppet.type(:file).create( :path => exe, :mode => 755 ) exec = Puppet.type(:exec).create( :command => oexe, :path => ENV["PATH"], :cwd => basedir ) cat = Puppet.type(:exec).create( :command => "cat %s %s" % [exe, oexe], :path => ENV["PATH"] ) rels = nil assert_nothing_raised do rels = exec.autorequire end # Verify we get the script itself assert(rels.detect { |r| r.source == file }, "Exec did not autorequire its command") # Verify we catch the cwd assert(rels.detect { |r| r.source == baseobj }, "Exec did not autorequire its cwd") # Verify we don't require ourselves assert(! rels.detect { |r| r.source == ofile }, "Exec incorrectly required mentioned file") assert(!exec.requires?(ofile), "Exec incorrectly required file") # We not longer autorequire inline files assert_nothing_raised do rels = cat.autorequire end assert(! rels.detect { |r| r.source == ofile }, "Exec required second inline file") assert(! rels.detect { |r| r.source == file }, "Exec required inline file") end def test_ifonly afile = tempfile() bfile = tempfile() exec = nil assert_nothing_raised { exec = Puppet.type(:exec).create( :command => "touch %s" % bfile, :onlyif => "test -f %s" % afile, :path => ENV['PATH'] ) } assert_events([], exec) system("touch %s" % afile) assert_events([:executed_command], exec) assert_events([:executed_command], exec) system("rm %s" % afile) assert_events([], exec) end def test_unless afile = tempfile() bfile = tempfile() exec = nil assert_nothing_raised { exec = Puppet.type(:exec).create( :command => "touch %s" % bfile, :unless => "test -f %s" % afile, :path => ENV['PATH'] ) } comp = mk_catalog(exec) assert_events([:executed_command], comp) assert_events([:executed_command], comp) system("touch %s" % afile) assert_events([], comp) assert_events([], comp) system("rm %s" % afile) assert_events([:executed_command], comp) assert_events([:executed_command], comp) end if Puppet::Util::SUIDManager.uid == 0 # Verify that we can execute commands as a special user def mknverify(file, user, group = nil, id = true) File.umask(0022) args = { :command => "touch %s" % file, :path => "/usr/bin:/bin:/usr/sbin:/sbin", } if user #Puppet.warning "Using user %s" % user.name if id # convert to a string, because that's what the object expects args[:user] = user.uid.to_s else args[:user] = user.name end end if group #Puppet.warning "Using group %s" % group.name if id args[:group] = group.gid.to_s else args[:group] = group.name end end exec = nil assert_nothing_raised { exec = Puppet.type(:exec).create(args) } comp = mk_catalog("usertest", exec) assert_events([:executed_command], comp, "usertest") assert(FileTest.exists?(file), "File does not exist") if user assert_equal(user.uid, File.stat(file).uid, "File UIDs do not match") end # We can't actually test group ownership, unfortunately, because # behaviour changes wildlly based on platform. Puppet::Type.allclear end def test_userngroup file = tempfile() [ [nonrootuser()], # just user, by name [nonrootuser(), nil, true], # user, by uid [nil, nonrootgroup()], # just group [nil, nonrootgroup(), true], # just group, by id [nonrootuser(), nonrootgroup()], # user and group, by name [nonrootuser(), nonrootgroup(), true], # user and group, by id ].each { |ary| mknverify(file, *ary) { } } end end def test_logoutput exec = nil assert_nothing_raised { exec = Puppet.type(:exec).create( :title => "logoutputesting", :path => "/usr/bin:/bin", :command => "echo logoutput is false", :logoutput => false ) } assert_apply(exec) assert_nothing_raised { exec[:command] = "echo logoutput is true" exec[:logoutput] = true } assert_apply(exec) assert_nothing_raised { - exec[:command] = "echo logoutput is warning" - exec[:logoutput] = "warning" + exec[:command] = "echo logoutput is on_failure" + exec[:logoutput] = "on_failure" } assert_apply(exec) end def test_execthenfile exec = nil file = nil basedir = tempfile() path = File.join(basedir, "subfile") assert_nothing_raised { exec = Puppet.type(:exec).create( :title => "mkdir", :path => "/usr/bin:/bin", :creates => basedir, :command => "mkdir %s; touch %s" % [basedir, path] ) } assert_nothing_raised { file = Puppet.type(:file).create( :path => basedir, :recurse => true, :mode => "755", :require => ["exec", "mkdir"] ) } comp = mk_catalog(file, exec) comp.finalize assert_events([:executed_command, :file_changed], comp) assert(FileTest.exists?(path), "Exec ran first") assert(File.stat(path).mode & 007777 == 0755) end # Make sure all checks need to be fully qualified. def test_falsevals exec = nil assert_nothing_raised do exec = Puppet.type(:exec).create( :command => "/bin/touch yayness" ) end Puppet.type(:exec).checks.each do |check| klass = Puppet.type(:exec).paramclass(check) next if klass.values.include? :false assert_raise(Puppet::Error, "Check '%s' did not fail on false" % check) do exec[check] = false end end end def test_createcwdandexe exec1 = exec2 = nil dir = tempfile() file = tempfile() assert_nothing_raised { exec1 = Puppet.type(:exec).create( :title => "one", :path => ENV["PATH"], :command => "mkdir #{dir}" ) } assert_nothing_raised("Could not create exec w/out existing cwd") { exec2 = Puppet.type(:exec).create( :title => "two", :path => ENV["PATH"], :command => "touch #{file}", :cwd => dir ) } # Throw a check in there with our cwd and make sure it works assert_nothing_raised("Could not check with a missing cwd") do exec2[:unless] = "test -f /this/file/does/not/exist" exec2.retrieve end assert_raise(Puppet::Error) do exec2.property(:returns).sync end assert_nothing_raised do exec2[:require] = exec1 end assert_apply(exec1, exec2) assert(FileTest.exists?(file)) end def test_checkarrays exec = nil file = tempfile() test = "test -f #{file}" assert_nothing_raised { exec = Puppet.type(:exec).create( :path => ENV["PATH"], :command => "touch #{file}" ) } assert_nothing_raised { exec[:unless] = test } assert_nothing_raised { assert(exec.check, "Check did not pass") } assert_nothing_raised { exec[:unless] = [test, test] } assert_nothing_raised { exec.finish } assert_nothing_raised { assert(exec.check, "Check did not pass") } assert_apply(exec) assert_nothing_raised { assert(! exec.check, "Check passed") } end def test_missing_checks_cause_failures # Solaris's sh exits with 1 here instead of 127 return if Facter.value(:operatingsystem) == "Solaris" exec = Puppet::Type.type(:exec).create( :command => "echo true", :path => ENV["PATH"], :onlyif => "/bin/nosuchthingexists" ) assert_raise(ArgumentError, "Missing command did not raise error") { exec.run("/bin/nosuchthingexists") } end def test_envparam exec = Puppet::Type.newexec( :command => "echo $envtest", :path => ENV["PATH"], :env => "envtest=yayness" ) assert(exec, "Could not make exec") output = status = nil assert_nothing_raised { output, status = exec.run("echo $envtest") } assert_equal("yayness\n", output) # Now check whether we can do multiline settings assert_nothing_raised do exec[:env] = "envtest=a list of things and stuff" end output = status = nil assert_nothing_raised { output, status = exec.run('echo "$envtest"') } assert_equal("a list of things\nand stuff\n", output) # Now test arrays assert_nothing_raised do exec[:env] = ["funtest=A", "yaytest=B"] end output = status = nil assert_nothing_raised { output, status = exec.run('echo "$funtest" "$yaytest"') } assert_equal("A B\n", output) end def test_environmentparam exec = Puppet::Type.newexec( :command => "echo $environmenttest", :path => ENV["PATH"], :environment => "environmenttest=yayness" ) assert(exec, "Could not make exec") output = status = nil assert_nothing_raised { output, status = exec.run("echo $environmenttest") } assert_equal("yayness\n", output) # Now check whether we can do multiline settings assert_nothing_raised do exec[:environment] = "environmenttest=a list of things and stuff" end output = status = nil assert_nothing_raised { output, status = exec.run('echo "$environmenttest"') } assert_equal("a list of things\nand stuff\n", output) # Now test arrays assert_nothing_raised do exec[:environment] = ["funtest=A", "yaytest=B"] end output = status = nil assert_nothing_raised { output, status = exec.run('echo "$funtest" "$yaytest"') } assert_equal("A B\n", output) end def test_timeout exec = Puppet::Type.type(:exec).create(:command => "sleep 1", :path => ENV["PATH"], :timeout => "0.2") time = Time.now assert_raise(Timeout::Error) { exec.run("sleep 1") } Puppet.info "%s seconds, vs a timeout of %s" % [Time.now.to_f - time.to_f, exec[:timeout]] assert_apply(exec) end # Testing #470 def test_run_as_created_user exec = nil if Process.uid == 0 user = "nosuchuser" assert_nothing_raised("Could not create exec with non-existent user") do exec = Puppet::Type.type(:exec).create( :command => "/bin/echo yay", :user => user ) end end # Now try the group group = "nosuchgroup" assert_nothing_raised("Could not create exec with non-existent user") do exec = Puppet::Type.type(:exec).create( :command => "/bin/echo yay", :group => group ) end end # make sure paths work both as arrays and strings def test_paths_as_arrays path = %w{/usr/bin /usr/sbin /sbin} exec = nil assert_nothing_raised("Could not use an array for the path") do exec = Puppet::Type.type(:exec).create(:command => "echo yay", :path => path) end assert_equal(path, exec[:path], "array-based path did not match") assert_nothing_raised("Could not use a string for the path") do exec = Puppet::Type.type(:exec).create(:command => "echo yay", :path => path.join(":")) end assert_equal(path, exec[:path], "string-based path did not match") assert_nothing_raised("Could not use a colon-separated strings in an array for the path") do exec = Puppet::Type.type(:exec).create(:command => "echo yay", :path => ["/usr/bin", "/usr/sbin:/sbin"]) end assert_equal(path, exec[:path], "colon-separated array path did not match") end def test_checks_apply_to_refresh file = tempfile() maker = tempfile() exec = Puppet::Type.type(:exec).create( :title => "maker", :command => "touch #{maker}", :path => ENV["PATH"] ) # Make sure it runs normally assert_apply(exec) assert(FileTest.exists?(maker), "exec did not run") File.unlink(maker) # Now make sure it refreshes assert_nothing_raised("Failed to refresh exec") do exec.refresh end assert(FileTest.exists?(maker), "exec did not run refresh") File.unlink(maker) # Now add the checks exec[:creates] = file # Make sure it runs when the file doesn't exist assert_nothing_raised("Failed to refresh exec") do exec.refresh end assert(FileTest.exists?(maker), "exec did not refresh when checks passed") File.unlink(maker) # Now create the file and make sure it doesn't refresh File.open(file, "w") { |f| f.puts "" } assert_nothing_raised("Failed to refresh exec") do exec.refresh end assert(! FileTest.exists?(maker), "exec refreshed with failing checks") end def test_explicit_refresh refresher = tempfile() maker = tempfile() exec = Puppet::Type.type(:exec).create( :title => "maker", :command => "touch #{maker}", :path => ENV["PATH"] ) # Call refresh normally assert_nothing_raised do exec.refresh end # Make sure it created the normal file assert(FileTest.exists?(maker), "normal refresh did not work") File.unlink(maker) # Now reset refresh, and make sure it wins assert_nothing_raised("Could not set refresh parameter") do exec[:refresh] = "touch #{refresher}" end assert_nothing_raised do exec.refresh end # Make sure it created the normal file assert(FileTest.exists?(refresher), "refresh param was ignored") assert(! FileTest.exists?(maker), "refresh param also ran command") end if Puppet.features.root? def test_autorequire_user user = Puppet::Type.type(:user).create(:name => "yay") exec = Puppet::Type.type(:exec).create(:command => "/bin/echo fun", :user => "yay") rels = nil assert_nothing_raised("Could not evaluate autorequire") do rels = exec.autorequire end assert(rels.find { |r| r.source == user and r.target == exec }, "Exec did not autorequire user") end end end