diff --git a/CHANGELOG b/CHANGELOG index 1bc13f303..99a69c676 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,1681 +1,1699 @@ 0.25.0 Fixed #198 - Puppet man pages added Moved puppetd, puppetmasterd, puppetrun, puppetca from bin to sbin Fixed #2071 - Updated LDAP schema Fixed #1849 - Ruby 1.9 portability: `when' doesn't like colons, replace with semicolons Fixed #1910 - Updated logcheck regex Fixed #1879 - Added to tidy documentation Fixed #1881 - Added md5lite explanation Fixed #1877 - Tidy type reference update for use of 0 Fix autotest on win32 Doc strings update for REST Fixed #1840 - Bug fixes and improvements for Emacs puppet-mode.el 0.24.8 + Fixed #2077 - ralsh user broken on OSX + + Fixed #2004 - ssh_authorized_key fails if no target is defined + + Fixed #1629 - incorrect permissions on ssh_authorized_keys created files + + Fixed #2000 - No default specified for checksum + + Fixed #2026 - Red Hat ignoring stop method + + Added ext/dbfix.sql script - fixes common errors in stored configuration databases + + Fixed #1963 - Failing to read /proc/mounts for selinux kills file downloads + + Fixed #2025 - gentoo service provider handle only default init level + + Fixed #1910 - updated logcheck + Fixed #1871 - Sensitive information leaked in log reports Fixed #1956 - Cleaned up variable names to be more sane, clarified error messages and fixed incorrect use of 'value' variable rather than 'member'. Fixed #1831 - Added sprintf function Fixed #1830 - Added regsubst function Updated up2date and service confines to add support for Oracle EL and VM Fixing #1948 and #1953 - augeas ins bug: wrong number of arguments (1 for 3) Fixing #944 - changing error message from warning to info - connection recycled Fixed #961 - puppetd creating too many/not closing TCP connections Fixed #1959 - Added column protection for environment schema migration Fixing #1869 - autoloaded files should never leak exceptions Fixing #1543 - Nagios parse errors no longer kill Puppet Fixed #1420 - nagios_serviceescalation not allowing host_name more than one type Fixed #1884 - Exported resources are marked as unexported when collected on the exporting host Fixed #1922 - Functions squash all arguments into a single hash Fixed #1538 - Yumrepo sets permissions wrongly on files in /etc/yum.repos.d Fixed #1936 - Added /* */ support to the vim file Fixed #1541 - nagios objects write files to clientbucket on every change Fixed #1542 - cannot purge nagios objects Fixing #1912 - gid still works with no 'should' value fixing ralsh issues Fixing the Rakefile to use 'git format-patch' Added README.rst file Enhancements to Stored Configuration performance Added Reductive Labs build library to tasks directory Fixed #1852 - Correct behaviour when no SELinux bindings Updated Red Hat spec file 0.24.7 Fixed #1920 - Shadow password corruption 0.24.7 Fixed #1804 - Added VDev and MultiVDev properties to the ZPool type Fixed #1496 - nagios_servicedependency needs a unique host_name? Fixed #1420 - nagios_serviceescalation not allowing host_name more than one type Bug #1803 Zfs should auto require the ancestor file systems Refactor #1802 Use 'zfs get -H -o value' instead of parsing output for value Fixing #1800 - tidy now correctly ignores missing files and directories Fixing #1794 - returning sync when it is already initialized Fixing #1750 again - All of the properties and now :ensure check replace? Deprecate the NetInfo nameservice provider. Use directoryservice instead Add macauthorization type Fixing #1703 - using a mutex around the sending of the tagmails Fix #1788 - allow rspec rake to run only some tests Fixing the AST constant warnings, using a variable instead of a constant Feature #1696 Add support for branded zones Feature #1783 - Add ZFS support type/mcx.rb Feature #1026 - MCX Type Fixing #1749 - Splay now hopefully behaves "better" for small values Fix #1741 - Add inline_template function Slight denormalisation to store a host's environment as a first class Added Rake :ci namespace and CI tasks Refactoring the thread-safety in Puppet::Util Removing the included testing gems; you must now install them yourself Refactoring of SELinux functions to use native Ruby SELinux interface Removing all mention of EPM, RPM, or Sun packages. Fixed #1496 - nagios_servicedependency needs a unique host_name? Fixed #1420 - nagios_serviceescalation not allowing host_name more than one type Fixed #1695 - Solaris 10 zone provider doesn't properly handle unknown zone attributes in newer releases Fixed #1776 - Trivial fix for gentoo service provider Fixed #1767 - Minor fix to emacs mode Fixed #1711 - fileserver test fails due to incorrect mocking Fixed #1751 - Mac OS X DirectoryService nameservice provider support for plist output and password hash fil Fixed #1752 - Add an optional argument to Puppet::Util.execute to determine whether stderr and stdout are combined in the output Added versionable feature to the RPM provider Fixed #1668 - puppetca can't clean unsigned certs Moved RRD feature from util/metric.rb to feature/base.rb Fixed #1735 and #1747 - Fixes to confine system Fixed #1681 - Add filesystem type check to test for per-file SELinux context support Fixed #1746 - Sync SELinux file attributes after file contents created/modified Replaced SELInux calls to binaries with Ruby SELinux bindings Fixed #1748 - Include spec directory in packages Fixes #1672 - unsafe crontab handling in Solaris Fixed #1718 - Added preseed to apt uninstall and purge Fixed #1739 - Added uninstall functionality to yum provider Fixed #1710 - Spurious output in test run Fixed #1667 - Documentation should specify natural language regexs, not Regexp objects Fixed #1692 - k5login fails to set mode when file is created Fixed #1660 - Added specific recurse values for tidy Fixed #1698 - All logs should now show up in the reports Fixed #1661 - Type reference: tidy should specify manditory parameters Fixed #1104 - Classes and nodes should set $name variables Updated Red Hat spec file for 0.24.6 Removed conf/debian directory - Debian packaging information now maintained downstream Added augeas type Added support for @doc type and manifest documentation support - see: http://reductivelabs.com/trac/puppet/wiki/PuppetManifestDocumentation Added multiline comment support 0.24.6 Adding support to the user type for: profiles, auths, project, key/value pairs (extension to Solaris RBAC support added in 0.24.6) Fixed #1662 - Configuration Reference still references 'section' Fixed #1460 - enhance redhat puppetmaster init.d script to easy start puppetmaster as a mongrel cluster Fixed #1663 - Regression relating to facter fact naming from 0.24.5 Fixed #1655 - Provider::Confine::Variable tests are broken Fixed #1646 - service puppet status does not work as non-root on redhat system Fixed #1649 - Updated OSX package cleanup Fixed #1647 - puppetdoc -r providers now working again Fixed #1639 - uninitialized constant Puppet::Type::User::ProviderUseradd Fixed #1637 - With an inexistant (global) templatedir, modules can't access their templates Fixed #1202 - Collection attribute matching doesn't parse arrays Fixed #1473 - Puppetd stops with error after puppetmasterd is unavailable Fixed #1354 - yum provider problems with RHEL 3 Fixed #1633 - Added support for --detailed-exits to bin/puppet Fixed #381 - Allow Allow multiple overrides in one statement Fixing #947 - pluginsync no longer fails poorly when no plugins exist Fixed #981 - Removed 'Adding aliases' info message Fixing #1089 - Log messages are now tagged with the log level, making it easier to match messages in the 'tagmail' report. Fixing #1098 - Multiline strings now correctly increment the line count Fixing #1614 - Environments no longer have to be listed out Fixed #1628 - Changed node search to use certname rather than Facter hostname Fixed #1613 - The client environment will be substituted when looking up settings. Updated puppet binary documentation Feature #1624 - Added RBAC roles to solaris user provider Fixed #1586 - Specifying "fully qualified" package names in Gentoo Fixed #1620 - Add 'sles' to Puppet confines when 'suse' is used Fixed #1585 - Allow complex 'if' and variable expressions Fixed #1564 - Saving File#checksum to state.yaml broken Fixed #1603 - Added support for running Puppet inside a Rack application (mod_rails) with Passenger and Apache Fixed #1596 - Deploying file resources with ++ generates error Modified the group and zone resource types to no longer call 'currentpropvalues' as a means of setting all values to absent. There should be no behaviour change from this change. Modified the behaviour of resource-level 'retrieve' -- it only calls 'retrieve' on each property if the resource exists. Fixed #1622 - Users and their groups should again add in one transaction Fixed #791 - You should now be able to create and find a user/group in one transaction. Fixed #1610 - Raise "Filebucketed" messages to Notice priority FIxed #1530 - ssh_authorized_keys provider does not crash anymore on SSH type 1 keys Added a number of confines to package providers Fixed #1609 - Added confines for the Gentoo, FreeBSD and SMF (Solaris) service providers Fixed #1608 - Added ubuntu to defaultfor for apt provider Fixed #1607 - Added ubuntu to defaultfor for Debian service provider Fixed #1045 - Multiple metaparams all get added to resources. Fixed #1472 -- defined, exported resources in the current compile now get expanded correctly. Fixed #1595 - Internally, Property#retrieve is no longer called when no 'should' value is available for a resource. Fixed #1588 - Fixed puppetca --clean --all Fixed #1584 - Added support for appended variables Fixed #1554 - Added support for multiple template directories Fixed #1500 - puppetrun not working Fixed #1579 and #1580 - errors in the Puppet RPM spec file Fixed #1572 -- file purging now fails if remote sources do not exist. Fixed #1521 -- ldap user and password are now used with the default connection. Fixed issues with file descriptors leaking into subprocesses Fixed #1568 - createpackage.sh Fixed #1571 - Puppet::Util::binary returns incorrect results Fixed #1553 - Puppet and Facter cannot both install the plist module into two different locations Adjusted hpuxuseradd user provider to confine to HP-UX and fixed HP-UX user provider path regression Fixed debug messages in package type - thanks to Todd Zullinger for this fix Fixed #1566 - changed password property of the user type Fixed debug messages in package type Updated Red Hat spec file Fixes #1455 - Adds HP-UX support for user type Fixes #1551 puppetmaster.freshness xmlrpc call returns incorrect type Fixes #1554 - Fix exception for undefined hostname Fixed #1533 - changed permissions for man directory Added daemontools and runit providers for service type Added simple rake task for running unit tests Added spec Rake task Fixed #1526 - Fixed leak in template Fixed #1506 - Removed storeconfig duplicate indexes Fixed #1457 - case insensitive match for error Fixed #1488 - Moved individual functions out of functions.rb into lib/puppet/parser/functions directory. New functions should be create in this directory. Fixed #1508 - Added HP-UX package provider Fixed #1502 - Fixed poor stored configuration performance Fixed #1510 - Storeconfiguration fixed for Rails 2.1 Add the -P/--ping option to puppetrun, fixes #1501 Fixed #1394 - Added stored configuration clearing script to /ext Fixed #1442 - replaced use of Facter for report titling with certname Fixed $1456 - add proxy configuration capability to yum repo Fixed #1457 - removed confine warning A working script to create an OS X pkg out of the Puppet repository Fixed #1441 - Updated console colours Expose all puppet variables as instance member variables of the template wrapper. This helps resolve redmine #1427, by providing a safe mechanism to access variables. * Implement Puppet::Parser::Scope#to_hash, which returns a hash containing all the variable bindings in the current and, optionally, parent scope. * Use that to set instance member variables into Puppet::Parser::Templatewrapper * Report the time taken for variable binding at debug level, to help identify any performance regression that is encountered in the real world. * Rename the @scope and @file members of the template wrapper, to avoid clashing with a scope variable exposed within puppet. Ensure that we consistently use either string #{} interpolation or String.% interpolation, not both, to avoid issues where a #{} interpolated value contains a % character. Feature #1476: Allow specification of --bindir --sbindir --sitelibdir --mandir --destdir in installation Added feature #1241 : Improve performance of group lookups Fixed bug #1448: Puppet CA incorrectly writes out all certs to inventory .txt on each certificate signing Fixing puppetlast to make it work with 0.24.5 / 0.25. Made puppetlast work on 0.24.5 by using the YAML indirector 0.24.5 You can now select the encoding format when transferring the catalog, with 'yaml' still being the default but 'marshal' being an option. This is because testing has shown drastic performance differences between the two, with up to 70% of compile time being spent in YAML code. Use the 'catalog_format' setting to choose your format, and the setting must be set on the client. Fixed #1431 - Provider confines must now specify similar tests in one call. I.e., you can't do confine :operatingsystem => %w{a b} and then confine :operatingsystem => %w{b c}; you'd need to do them in one command. This now-obsolete behaviour does not seem to be used anywhere. The fix for #1431 is actually just removing the tests that exposed this change; the change happened when I refactored how confines work. Removed faulty interface type Updated /spec/unit/rails.rb test Fix #1426 - services on redhat are restarted again and status is called from the Red Hat provider Fixed #1414 - Return code from waitpid now right shifted 8 bits Fixed #174 - a native type type for managing ssh authorized_keys files is available. Further moves from the examples directory and ext directory Fixed #1397 One line fix, fail instead of log Moved debian to conf and updated examples directory Fixed #1368 - updated Red Hat init scripts Added message referencing ReductiveLabs build library Fixed #1396 - Added sha1 function from DavidS to core Fixed #1399 - the ldap user provider now knows it can manage passwords. Fixed #1272 - if you provide a group name as the gid to an ldap user, the name will be converted to a gid. Note that this only looks up ldap groups, at this point; if you want to set an ldap user's primary group to a local group, you have to specify the GID. Fixed #1226 - gems can now specify source repositories. Fixed #1232 - the rundir no longer specifies a user/group, and there are now client- and server-specific yaml directories. Fixed 1240 - puppet will function more like puppetd if graphing or reporting are enabled. Fixed #1231 - Exceptions during initialization should now be clearer. Fixed #1006 - puppetrun --class works again. I added the class membership testing to the Ldap node terminus, and added tests, so it shouldn't break again. Fixed #1114 - Facts in plugin directories should now be autoloaded, as long as you're using Facter 1.5. Removed support for the 'node_name' setting in LDAP and external node lookups. Fixed #1195 - Updated Gentoo init scripts Fixed #1367 - Updated Rakefile for new daily builds Fixed #1370 - removed test/util/loadedfile.rb tests Fixed #1221 - aliases to titles now work for resources. Fixed #1012 - templates in the templatedir are preferred to module templates. Fixed #707 - special '@reboot'-style cron jobs work again. Fixed #1360 - allowdupe works on groups again. Fixed #1369 - the init service provider now supports HP-UX. Removed support for the 'node_name' setting in LDAP and external node lookups. Also removed support for 'default' nodes in external nodes. LDAP nodes now use the certificate name, the short name, and 'default', but external nodes just use the certificate name and any custom terminus types will use just the certificate name. Fixing #1168 (for 0.24.x) -- automatically downcasing the fqdn. Also requiring that passed in certnames be downcased; the setting system isn't currently flexible enough to automatically downcase it for the user. Adding a ResourceTemplate class for using templates directly within resources (i.e., client-side templates). This would really only be used for composite resources that pass the results of the template on to generated resources. Exporting or collecting resources no longer raises an exception when no storeconfigs is enabled, it just produces a warning. Always using the cert name to store yaml files, which fixes #1178. The Master handler previously provided the support for the :node_name setting, and that functionality has now been moved into the Node class. At the same time, the names to search through have been changed somewhat: Previously, the certificate name and the hostname were both used for searching, but now, the cert name is always searched first (unless node_name == facter), but only the Facter hostname, domain, and fqdn are used otherwise. We no longer split the cert name, only the hostname/domain/fqdn. Fixing transaction support for prefetching generated resources. Adding support for settings within the existing Facter provider confines. Moving all confine code out of the Provider class, and fixing #1197. Created a Confiner module for the Provider class methods, enhanced the interface between it and the Confine class to make sure binary paths are searched for fresh each time. Modified the 'factpath' setting to automatically configure Facter to load facts there if a new enough version of Facter is used. Crontab provider: fix a parse error when a line begins with a space character (fixes #1216) Instead of deleting the init scripts (with --del) we should simply disable it with chkconfig service off, and respectfully do the same for enable => true; Added ldap providers for users and groups. Added support for the --all option to puppetca --clean. If puppetca --clean --all is issued then all client certificates are removed. Resources now return the 'should' value for properties from the [] accessor method (they previously threw an exception when this method was used with properties). This shouldn't have any affect functionally; it just makes the method equivalent to 'should' for properties, but it works for all attribute types now. Modified the 'master' handler to use the Catalog class to compile node configurations, rather than using the Configuration handler, which was never used directly. I removed the Configuration handler as a result. Modified the 'master' handler (responsible for sending configurations to clients) to always return Time.now as its compile date, so configurations will always get recompiled. Fixed #1184 -- definitions now autoload correctly all of the time. Removed the code from the client that tries to avoid recompiling the catalog. The client will now always recompile, assuming it can reach the server. It will still use the cached config if there's a failure. Fixing #1173 -- classes and definitions can now have the same name as a directory with no failures. Saving new facts now expires any cached node information. Switching how caching is handled, so that objects now all have an expiration date associated with them. This makes it much easier to know whether a given cached object should be used or if it should be regenerated. Changing the default environment to production. 0.24.4 Pass source to pkg_add via the PKG_PATH environment variable if it ends in a '/' indicating it is a directory. Allows pkg_add to resolve dependancies, and make it possible to specify packages without version numbers. Fixing #571 -- provider suitability is now checked at resource evaluation time, rather than resource instantiation time. This means that you don't catch your "errors" as early, but it also means you should be able to realistically configure a whole host in one run. Moved the configuration of the Node cache to the puppetmasterd executable, since it otherwise causes caches to be used in all cases, which we don't want (e.g., bin/puppet was using them). Ported #198 man page creation functionality to 0.24.x branch and added man pages and man page creation logic to install.rb. The man pages are stored in man/man8 and will install to config::CONFIG mandir/man8. Fixing #1138 -- the yamldir is automatically created by the server now that it's in the :puppetmasterd section rather than a separate :yaml section. Disabling http keep-alive as a means of preventing #1010. There is now a constant in Puppet::Network::HttpPool that will disable or enable this feature, but note that we determined that it can cause corruption, especially in file serving (but it's client-side corruption). Applying patch by Ryan McBride to fix OpenBSD package matching. The actual problem was caused by the fix to #1001. Found all instances of methods where split() is used without any local variables and added a local variable -- see http://snurl.com/21zf8. My own testing showed that this caused memory growth to level off at a reasonable level. Note that the link above says the problem is only with class methods, but my own testing showed that it's any method that meets these criteria. This is not a functional change, but should hopefully be the last nail in the coffin of #1131. Found an array that leaked pretty quickly between reparsing files, thanks to work by Adam Jacob and Arjuna Christenson (the finding, not the leak). I'm going to act like this fixes #1131, at least for now, but I doubt it does, since that shows general memory growth over time, whereas the leak here should go away as soon as files are reparsed (because the parser is holding the reference to the leaking array). Fixed #1147: Cached nodes are correctly considered out of date if the node facts have been updated (thus causing node facts to again be available in manifests, for those cases where they were not). Fixed #1137: The certificate name is correctly being added to the facts hash. Fixed #1136: Verbose and Debug no longer clobber each other. Hopefully *finally* fixed the "already being managed" problem (#1036). The problem only cropped up if there was a failure when trying to manage the system -- this would cause the setting-based resources not to get cleaned up. 0.24.3 Modified the ldap node terminus to also use the facts version as the version for a node, which should similarly encourage the use of the yaml cache. (Related to #1130) Caching node information in yaml (I figured caching in memory will cause ever-larger memory growth), and changing the external node terminus to use the version of the facts as their version. This will usually result in the cached node information being used, instead of always hitting the external node app during file serving. Note that if the facts aren't changed by the client, then this will result in the cached node being used, but at this point, the client always updates its facts. (#1130) Fixing #1132 -- host names can now have dashes anywhere. (Patch by freiheit.) Fixing #1118 -- downloading plugins and facts now ignores noop. Note that this changes the behaviour a bit -- the resource's noop setting always beats the global setting (previously, whichever was true would win). The change in checksums from 'timestamp' to 'mtime' no longer result in updates on every run (#1116). Aliases again work in relationships (#1094). The CA serial file will no longer ever be owned by root (#1041). Fixing the rest of #1113: External node commands can specify an environment and Puppet will now use it. Partially fixing #1113: LDAP nodes now support environments, and the schema has been updated accordingly. Always duplicating resource defaults in the parser, so that stacked metaparameter values do not result in all resources that receive a given default also getting those stacked values. 0.24.2 Fixing #1062 by moving the yamldir setting to its own yaml section. This should keep the yamldir from being created on clients. Fixed #1047 -- Puppet's parser no longer changes the order in which statements are evaluated, which means that case statements can now set variables that are used by other variables. Fixed #1063 -- the master correctly logs syntax errors when reparsing during a single run. 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'. The environment is now available as a variable in the manifests. 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/ext/dbfix.sql b/ext/dbfix.sql new file mode 100644 index 000000000..960369117 --- /dev/null +++ b/ext/dbfix.sql @@ -0,0 +1,132 @@ +-- MySQL DB consistency check/fix +-- +-- Usage: +-- cat dbfix.sql | mysql -u user -p puppet +-- +-- WARNING: perform a database backup before running this script + +-- Remove duplicate resources, and keep the latest one +DELETE bad_rows.* +FROM resources AS bad_rows + INNER JOIN ( + SELECT title,restype,host_id, MAX(id) as max_id + FROM resources + GROUP BY title,restype,host_id + HAVING count(*) > 1 + ) AS good_rows + ON + good_rows.title = bad_rows.title AND + good_rows.restype = bad_rows.restype AND + good_rows.host_id = bad_rows.host_id AND + good_rows.max_id <> bad_rows.id; + +-- Remove duplicate param_values, and keep the latest one +DELETE bad_rows.* +FROM param_values AS bad_rows + INNER JOIN ( + SELECT value,param_name_id,resource_id, MAX(id) as max_id + FROM param_values + GROUP BY value,param_name_id,resource_id + HAVING count(*) > 1 + ) AS good_rows + ON + good_rows.value = bad_rows.value AND + good_rows.param_name_id = bad_rows.param_name_id AND + good_rows.resource_id = bad_rows.resource_id AND + good_rows.max_id <> bad_rows.id; + +-- rewrite param_values that points to duplicated param_names +-- to point to the highest param_name id. +UPDATE + param_values v + INNER JOIN + param_names n + ON n.id = v.param_name_id + INNER JOIN + ( + SELECT name, MAX(id) as max_id + FROM param_names + GROUP BY name + HAVING count(*) > 1 + ) nmax ON n.name = nmax.name +SET + v.param_name_id = nmax.max_id; + +-- Remove duplicate param_names, and keep the latest one +DELETE bad_rows.* +FROM param_names AS bad_rows + INNER JOIN ( + SELECT name, MAX(id) as max_id + FROM param_names + GROUP BY name + HAVING count(*) > 1 + ) AS good_rows + ON + good_rows.name = bad_rows.name AND + good_rows.max_id <> bad_rows.id; + +-- Remove duplicate resource_tags, and keep the highest one +DELETE bad_rows.* +FROM resource_tags AS bad_rows + INNER JOIN ( + SELECT resource_id,puppet_tag_id, MAX(id) as max_id + FROM resource_tags + GROUP BY resource_id,puppet_tag_id + HAVING count(*) > 1 + ) AS good_rows + ON + good_rows.resource_id = bad_rows.resource_id AND + good_rows.puppet_tag_id = bad_rows.puppet_tag_id AND + good_rows.max_id <> bad_rows.id; + +-- rewrite resource_tags that points to duplicated puppet_tags +-- to point to the highest puppet_tags id. +UPDATE + resource_tags v + INNER JOIN + puppet_tags n + ON n.id = v.puppet_tag_id + INNER JOIN + ( + SELECT name, MAX(id) as max_id + FROM puppet_tags + GROUP BY name + HAVING count(*) > 1 + ) nmax ON n.name = nmax.name +SET + v.puppet_tag_id = nmax.max_id; + +-- Remove duplicate puppet_tags, and keep the highest one +DELETE bad_rows.* +FROM puppet_tags AS bad_rows + INNER JOIN ( + SELECT name, MAX(id) as max_id + FROM puppet_tags + GROUP BY name + HAVING count(*) > 1 + ) AS good_rows + ON + good_rows.name = bad_rows.name AND + good_rows.max_id <> bad_rows.id; + +-- Fix dangling resources +-- note: we use a table to not exceed the number of InnoDB locks if there are two much +-- rows to delete. +-- this is an alternative to: DELETE resources FROM resources r LEFT JOIN hosts h ON h.id=r.host_id WHERE h.id IS NULL; +-- +CREATE TABLE resources_c LIKE resources; +INSERT INTO resources_c SELECT r.* FROM resources r INNER JOIN hosts h ON h.id=r.host_id; +RENAME TABLE resources TO resources_old, resources_c TO resources; +DROP TABLE resources_old; + +-- Fix dangling param_values +CREATE TABLE param_values_c LIKE param_values; +INSERT INTO param_values_c SELECT v.* FROM param_values v INNER JOIN resources r ON r.id=v.resource_id; +RENAME TABLE param_values TO param_values_old, param_values_c TO param_values; +DROP TABLE param_values_old; + +-- Fix dangling resource_tags +CREATE TABLE resource_tags_c LIKE resource_tags; +INSERT INTO resource_tags_c SELECT t.* FROM resource_tags t INNER JOIN resources r ON r.id=t.resource_id; +RENAME TABLE resource_tags TO resource_tags_old, resource_tags_c TO resource_tags; +DROP TABLE resource_tags_old; diff --git a/ext/logcheck/puppet b/ext/logcheck/puppet index 528eb42ce..d1b1b27bd 100644 --- a/ext/logcheck/puppet +++ b/ext/logcheck/puppet @@ -1,22 +1,23 @@ +^\w{3} [ :0-9]{11} [._[:alnum:]-]+ puppetmasterd\[[0-9]+\]: (Handled resources in|Resource comparison took|Searched for (host|resources|resource params and tags) in) [0-9.]+ seconds ^\w{3} [ :0-9]{11} [._[:alnum:]-]+ puppetmasterd\[[0-9]+\]: Starting Puppet server version [.0-9]+$ ^\w{3} [ :0-9]{11} [._[:alnum:]-]+ puppetmasterd\[[0-9]+\]: Compiled catalog for [._[:alnum:]-]+ in [.0-9]+ seconds$ ^\w{3} [ :0-9]{11} [._[:alnum:]-]+ puppetmasterd\[[0-9]+\]: Caught TERM; shutting down$ ^\w{3} [ :0-9]{11} [._[:alnum:]-]+ puppetmasterd\[[0-9]+\]: Shutting down$ ^\w{3} [ :0-9]{11} [._[:alnum:]-]+ puppetd\[[0-9]+\]: Starting Puppet client version [.0-9]+$ ^\w{3} [ :0-9]{11} [._[:alnum:]-]+ puppetd\[[0-9]+\]: getting config$ ^\w{3} [ :0-9]{11} [._[:alnum:]-]+ puppetd\[[0-9]+\]: Caching configuration at [\/._[:alnum:]-]+$ ^\w{3} [ :0-9]{11} [._[:alnum:]-]+ puppetd\[[0-9]+\]: Loaded state in [.0-9]+ seconds$ ^\w{3} [ :0-9]{11} [._[:alnum:]-]+ puppetd\[[0-9]+\]: Calling puppetmaster.getconfig$ ^\w{3} [ :0-9]{11} [._[:alnum:]-]+ puppetd\[[0-9]+\]: Retrieved configuration in [.0-9]+ seconds$ ^\w{3} [ :0-9]{11} [._[:alnum:]-]+ puppetd\[[0-9]+\]: Starting configuration run$ ^\w{3} [ :0-9]{11} [._[:alnum:]-]+ puppetd\[[0-9]+\]: Finished configuration run in [.0-9]+ seconds$ ^\w{3} [ :0-9]{11} [._[:alnum:]-]+ puppetd\[[0-9]+\]: Caught (TERM|INT); shutting down$ ^\w{3} [ :0-9]{11} [._[:alnum:]-]+ puppetd\[[0-9]+\]: Shutting down$ ^\w{3} [ :0-9]{11} [._[:alnum:]-]+ puppetd\[[0-9]+\]: Restarting with .*$ ^\w{3} [ :0-9]{11} [._[:alnum:]-]+ puppetd\[[0-9]+\]: Starting catalog run$ ^\w{3} [ :0-9]{11} [._[:alnum:]-]+ puppetd\[[0-9]+\]: Finished catalog run in [.0-9]+ seconds$ ^\w{3} [ :0-9]{11} [._[:alnum:]-]+ puppetd\[[0-9]+\]: Loading fact .*$ ^\w{3} [ :0-9]{11} [._[:alnum:]-]+ puppetd\[[0-9]+\]: Ignoring cache$ ^\w{3} [ :0-9]{11} [._[:alnum:]-]+ puppetd\[[0-9]+\]: Ignoring --listen on onetime run$ ^\w{3} [ :0-9]{11} [._[:alnum:]-]+ puppetd\[[0-9]+\]: Retrieving plugins$ diff --git a/install.rb b/install.rb index c99d7dacd..bc7df98b3 100755 --- a/install.rb +++ b/install.rb @@ -1,458 +1,458 @@ #! /usr/bin/env ruby #-- # Copyright 2004 Austin Ziegler # Install utility. Based on the original installation script for rdoc by the # Pragmatic Programmers. # # This program is free software. It may be redistributed and/or modified under # the terms of the GPL version 2 (or later) or the Ruby licence. # # Usage # ----- # In most cases, if you have a typical project layout, you will need to do # absolutely nothing to make this work for you. This layout is: # # bin/ # executable files -- "commands" # lib/ # the source of the library # tests/ # unit tests # # The default behaviour: # 1) Run all unit test files (ending in .rb) found in all directories under # tests/. # 2) Build Rdoc documentation from all files in bin/ (excluding .bat and .cmd), # all .rb files in lib/, ./README, ./ChangeLog, and ./Install. # 3) Build ri documentation from all files in bin/ (excluding .bat and .cmd), # and all .rb files in lib/. This is disabled by default on Win32. # 4) Install commands from bin/ into the Ruby bin directory. On Windows, if a # if a corresponding batch file (.bat or .cmd) exists in the bin directory, # it will be copied over as well. Otherwise, a batch file (always .bat) will # be created to run the specified command. # 5) Install all library files ending in .rb from lib/ into Ruby's # site_lib/version directory. # #++ require 'rbconfig' require 'find' require 'fileutils' require 'ftools' # apparently on some system ftools doesn't get loaded require 'optparse' require 'ostruct' begin require 'rdoc/rdoc' $haverdoc = true rescue LoadError puts "Missing rdoc; skipping documentation" $haverdoc = false end begin if $haverdoc rst2man = %x{which rst2man.py} $haveman = true else $haveman = false end rescue puts "Missing rst2man; skipping man page creation" $haveman = false end PREREQS = %w{openssl facter xmlrpc/client xmlrpc/server cgi} MIN_FACTER_VERSION = 1.5 InstallOptions = OpenStruct.new def glob(list) g = list.map { |i| Dir.glob(i) } g.flatten! g.compact! g.reject! { |e| e =~ /\.svn/ } g end # Set these values to what you want installed. sbins = glob(%w{sbin/*}) bins = glob(%w{bin/*}) rdoc = glob(%w{bin/* sbin/* lib/**/*.rb README README-library CHANGELOG TODO Install}).reject { |e| e=~ /\.(bat|cmd)$/ } ri = glob(%w(bin/*.rb sbin/* lib/**/*.rb)).reject { |e| e=~ /\.(bat|cmd)$/ } man = glob(%w{man/man8/*}) libs = glob(%w{lib/**/*.rb lib/**/*.py}) tests = glob(%w{test/**/*.rb}) def do_bins(bins, target, strip = 's?bin/') bins.each do |bf| obf = bf.gsub(/#{strip}/, '') install_binfile(bf, obf, target) end end def do_libs(libs, strip = 'lib/') libs.each do |lf| olf = File.join(InstallOptions.site_dir, lf.gsub(/#{strip}/, '')) op = File.dirname(olf) File.makedirs(op, true) File.chmod(0755, op) - File.install(lf, olf, 0755, true) + File.install(lf, olf, 0644, true) end end def do_man(man, strip = 'man/') man.each do |mf| omf = File.join(InstallOptions.man_dir, mf.gsub(/#{strip}/, '')) om = File.dirname(omf) File.makedirs(om, true) File.chmod(0755, om) File.install(mf, omf, 0644, true) gzip = %x{which gzip} gzip.chomp! %x{#{gzip} -f #{omf}} end end # Verify that all of the prereqs are installed def check_prereqs PREREQS.each { |pre| begin require pre if pre == "facter" # to_f isn't quite exact for strings like "1.5.1" but is good # enough for this purpose. facter_version = Facter.version.to_f if facter_version < MIN_FACTER_VERSION puts "Facter version: %s; minimum required: %s; cannot install" % [facter_version, MIN_FACTER_VERSION] exit -1 end end rescue LoadError puts "Could not load %s; cannot install" % pre exit -1 end } end ## # Prepare the file installation. # def prepare_installation # Only try to do docs if we're sure they have rdoc if $haverdoc InstallOptions.rdoc = true if RUBY_PLATFORM == "i386-mswin32" InstallOptions.ri = false else InstallOptions.ri = true end else InstallOptions.rdoc = false InstallOptions.ri = false end if $haveman InstallOptions.man = true if RUBY_PLATFORM == "i386-mswin32" InstallOptions.man = false end else InstallOptions.man = false end InstallOptions.tests = true if $haveman InstallOptions.man = true if RUBY_PLATFORM == "i386-mswin32" InstallOptions.man = false end else InstallOptions.man = false end ARGV.options do |opts| opts.banner = "Usage: #{File.basename($0)} [options]" opts.separator "" opts.on('--[no-]rdoc', 'Prevents the creation of RDoc output.', 'Default on.') do |onrdoc| InstallOptions.rdoc = onrdoc end opts.on('--[no-]ri', 'Prevents the creation of RI output.', 'Default off on mswin32.') do |onri| InstallOptions.ri = onri end - opts.on('--[no-]man', 'Presents the creation of man pages.', 'Default on.') do |onman| + opts.on('--[no-]man', 'Prevents the creation of man pages.', 'Default on.') do |onman| InstallOptions.man = onman end opts.on('--[no-]tests', 'Prevents the execution of unit tests.', 'Default on.') do |ontest| InstallOptions.tests = ontest end opts.on('--destdir[=OPTIONAL]', 'Installation prefix for all targets', 'Default essentially /') do |destdir| InstallOptions.destdir = destdir end opts.on('--bindir[=OPTIONAL]', 'Installation directory for binaries', 'overrides Config::CONFIG["bindir"]') do |bindir| InstallOptions.bindir = bindir end opts.on('--sbindir[=OPTIONAL]', 'Installation directory for system binaries', 'overrides Config::CONFIG["sbindir"]') do |sbindir| InstallOptions.sbindir = sbindir end opts.on('--sitelibdir[=OPTIONAL]', 'Installation directory for libraries', 'overrides Config::CONFIG["sitelibdir"]') do |sitelibdir| InstallOptions.sitelibdir = sitelibdir end opts.on('--mandir[=OPTIONAL]', 'Installation directory for man pages', 'overrides Config::CONFIG["mandir"]') do |mandir| InstallOptions.mandir = mandir end opts.on('--quick', 'Performs a quick installation. Only the', 'installation is done.') do |quick| InstallOptions.rdoc = false InstallOptions.ri = false InstallOptions.tests = false end opts.on('--full', 'Performs a full installation. All', 'optional installation steps are run.') do |full| InstallOptions.rdoc = true InstallOptions.man = true InstallOptions.ri = true InstallOptions.tests = true end opts.separator("") opts.on_tail('--help', "Shows this help text.") do $stderr.puts opts exit end opts.parse! end tmpdirs = [".", ENV['TMP'], ENV['TEMP'], "/tmp", "/var/tmp"] version = [Config::CONFIG["MAJOR"], Config::CONFIG["MINOR"]].join(".") libdir = File.join(Config::CONFIG["libdir"], "ruby", version) # Mac OS X 10.5 declares bindir and sbindir as # /System/Library/Frameworks/Ruby.framework/Versions/1.8/usr/bin # /System/Library/Frameworks/Ruby.framework/Versions/1.8/usr/sbin # which is not generally where people expect executables to be installed if RUBY_PLATFORM == "universal-darwin9.0" Config::CONFIG['bindir'] = "/usr/bin" Config::CONFIG['sbindir'] = "/usr/sbin" end if not InstallOptions.bindir.nil? bindir = InstallOptions.bindir else bindir = Config::CONFIG['bindir'] end if not InstallOptions.sbindir.nil? sbindir = InstallOptions.sbindir else sbindir = Config::CONFIG['sbindir'] end if not InstallOptions.sitelibdir.nil? sitelibdir = InstallOptions.sitelibdir else sitelibdir = Config::CONFIG["sitelibdir"] if sitelibdir.nil? sitelibdir = $:.find { |x| x =~ /site_ruby/ } if sitelibdir.nil? sitelibdir = File.join(libdir, "site_ruby") elsif sitelibdir !~ Regexp.quote(version) sitelibdir = File.join(sitelibdir, version) end end end if not InstallOptions.mandir.nil? mandir = InstallOptions.mandir else mandir = Config::CONFIG['mandir'] end # To be deprecated once people move over to using --destdir option if (destdir = ENV['DESTDIR']) bindir = "#{destdir}#{bindir}" sbindir = "#{destdir}#{sbindir}" mandir = "#{destdir}#{mandir}" sitelibdir = "#{destdir}#{sitelibdir}" FileUtils.makedirs(bindir) FileUtils.makedirs(sbindir) FileUtils.makedirs(mandir) FileUtils.makedirs(sitelibdir) # This is the new way forward elsif (destdir = InstallOptions.destdir) bindir = "#{destdir}#{bindir}" sbindir = "#{destdir}#{sbindir}" mandir = "#{destdir}#{mandir}" sitelibdir = "#{destdir}#{sitelibdir}" FileUtils.makedirs(bindir) FileUtils.makedirs(sbindir) FileUtils.makedirs(mandir) FileUtils.makedirs(sitelibdir) end tmpdirs << bindir InstallOptions.tmp_dirs = tmpdirs.compact InstallOptions.site_dir = sitelibdir InstallOptions.bin_dir = bindir InstallOptions.sbin_dir = sbindir InstallOptions.lib_dir = libdir InstallOptions.man_dir = mandir end ## # Build the rdoc documentation. Also, try to build the RI documentation. # def build_rdoc(files) return unless $haverdoc begin r = RDoc::RDoc.new r.document(["--main", "README", "--title", "Puppet -- Site Configuration Management", "--line-numbers"] + files) rescue RDoc::RDocError => e $stderr.puts e.message rescue Exception => e $stderr.puts "Couldn't build RDoc documentation\n#{e.message}" end end def build_ri(files) return unless $haverdoc begin ri = RDoc::RDoc.new #ri.document(["--ri-site", "--merge"] + files) ri.document(["--ri-site"] + files) rescue RDoc::RDocError => e $stderr.puts e.message rescue Exception => e $stderr.puts "Couldn't build Ri documentation\n#{e.message}" $stderr.puts "Continuing with install..." end end def build_man(bins) return unless $haveman begin # Locate rst2man rst2man = %x{which rst2man.py} rst2man.chomp! # Create puppet.conf.8 man page %x{bin/puppetdoc --reference configuration > ./puppet.conf.rst} %x{#{rst2man} ./puppet.conf.rst ./man/man8/puppet.conf.8} File.unlink("./puppet.conf.rst") # Create binary man pages bins.each do |bin| b = bin.gsub( "bin/", "") %x{#{bin} --help > ./#{b}.rst} %x{#{rst2man} ./#{b}.rst ./man/man8/#{b}.8} File.unlink("./#{b}.rst") end rescue SystemCallError $stderr.puts "Couldn't build man pages: " + $! $stderr.puts "Continuing with install..." end end def run_tests(test_list) begin require 'test/unit/ui/console/testrunner' $:.unshift "lib" test_list.each do |test| next if File.directory?(test) require test end tests = [] ObjectSpace.each_object { |o| tests << o if o.kind_of?(Class) } tests.delete_if { |o| !o.ancestors.include?(Test::Unit::TestCase) } tests.delete_if { |o| o == Test::Unit::TestCase } tests.each { |test| Test::Unit::UI::Console::TestRunner.run(test) } $:.shift rescue LoadError puts "Missing testrunner library; skipping tests" end end ## # Install file(s) from ./bin to Config::CONFIG['bindir']. Patch it on the way # to insert a #! line; on a Unix install, the command is named as expected # (e.g., bin/rdoc becomes rdoc); the shebang line handles running it. Under # windows, we add an '.rb' extension and let file associations do their stuff. def install_binfile(from, op_file, target) tmp_dir = nil InstallOptions.tmp_dirs.each do |t| if File.directory?(t) and File.writable?(t) tmp_dir = t break end end fail "Cannot find a temporary directory" unless tmp_dir tmp_file = File.join(tmp_dir, '_tmp') ruby = File.join(Config::CONFIG['bindir'], Config::CONFIG['ruby_install_name']) File.open(from) do |ip| File.open(tmp_file, "w") do |op| ruby = File.join(Config::CONFIG['bindir'], Config::CONFIG['ruby_install_name']) op.puts "#!#{ruby}" contents = ip.readlines if contents[0] =~ /^#!/ contents.shift end op.write contents.join() end end if Config::CONFIG["target_os"] =~ /win/io and Config::CONFIG["target_os"] !~ /darwin/io installed_wrapper = false if File.exists?("#{from}.bat") FileUtils.install("#{from}.bat", File.join(target, "#{op_file}.bat"), :mode => 0755, :verbose => true) installed_wrapper = true end if File.exists?("#{from}.cmd") FileUtils.install("#{from}.cmd", File.join(target, "#{op_file}.cmd"), :mode => 0755, :verbose => true) installed_wrapper = true end if not installed_wrapper tmp_file2 = File.join(tmp_dir, '_tmp_wrapper') cwn = File.join(Config::CONFIG['bindir'], op_file) cwv = CMD_WRAPPER.gsub('', ruby.gsub(%r{/}) { "\\" }).gsub!('', cwn.gsub(%r{/}) { "\\" } ) File.open(tmp_file2, "wb") { |cw| cw.puts cwv } FileUtils.install(tmp_file2, File.join(target, "#{op_file}.bat"), :mode => 0755, :verbose => true) File.unlink(tmp_file2) installed_wrapper = true end end FileUtils.install(tmp_file, File.join(target, op_file), :mode => 0755, :verbose => true) File.unlink(tmp_file) end CMD_WRAPPER = <<-EOS @echo off if "%OS%"=="Windows_NT" goto WinNT -x "" %1 %2 %3 %4 %5 %6 %7 %8 %9 goto done :WinNT -x "" %* goto done :done EOS check_prereqs prepare_installation #run_tests(tests) if InstallOptions.tests #build_rdoc(rdoc) if InstallOptions.rdoc #build_ri(ri) if InstallOptions.ri #build_man(bins) if InstallOptions.man do_bins(sbins, InstallOptions.sbin_dir) do_bins(bins, InstallOptions.bin_dir) do_libs(libs) do_man(man) diff --git a/lib/puppet.rb b/lib/puppet.rb index e9ba2f2be..7bd3ed1bc 100644 --- a/lib/puppet.rb +++ b/lib/puppet.rb @@ -1,178 +1,178 @@ # Try to load rubygems. Hey rubygems, I hate you. begin require 'rubygems' rescue LoadError end # see the bottom of the file for further inclusions require 'singleton' require 'facter' require 'puppet/error' require 'puppet/util' require 'puppet/util/log' require 'puppet/util/autoload' require 'puppet/util/settings' require 'puppet/util/feature' require 'puppet/util/suidmanager' #------------------------------------------------------------ # the top-level module # # all this really does is dictate how the whole system behaves, through # preferences for things like debugging # # it's also a place to find top-level commands like 'debug' module Puppet - PUPPETVERSION = '0.24.7' + PUPPETVERSION = '0.24.8' def Puppet.version return PUPPETVERSION end class << self include Puppet::Util attr_reader :features attr_writer :name end # the hash that determines how our system behaves @@settings = Puppet::Util::Settings.new # The services running in this process. @services ||= [] # define helper messages for each of the message levels Puppet::Util::Log.eachlevel { |level| define_method(level,proc { |args| if args.is_a?(Array) args = args.join(" ") end Puppet::Util::Log.create( :level => level, :message => args ) }) module_function level } # I keep wanting to use Puppet.error # XXX this isn't actually working right now alias :error :err # The feature collection @features = Puppet::Util::Feature.new('puppet/feature') # Load the base features. require 'puppet/feature/base' # Store a new default value. def self.setdefaults(section, hash) @@settings.setdefaults(section, hash) end # configuration parameter access and stuff def self.[](param) case param when :debug if Puppet::Util::Log.level == :debug return true else return false end else return @@settings[param] end end # configuration parameter access and stuff def self.[]=(param,value) @@settings[param] = value end def self.clear @@settings.clear end def self.debug=(value) if value Puppet::Util::Log.level=(:debug) else Puppet::Util::Log.level=(:notice) end end def self.settings @@settings end # Load all of the configuration parameters. require 'puppet/defaults' def self.genmanifest if Puppet[:genmanifest] puts Puppet.settings.to_manifest exit(0) end end # Parse the config file for this process. def self.parse_config Puppet.settings.parse end # XXX this should all be done using puppet objects, not using # normal mkdir def self.recmkdir(dir,mode = 0755) if FileTest.exist?(dir) return false else tmp = dir.sub(/^\//,'') path = [File::SEPARATOR] tmp.split(File::SEPARATOR).each { |dir| path.push dir if ! FileTest.exist?(File.join(path)) begin Dir.mkdir(File.join(path), mode) rescue Errno::EACCES => detail Puppet.err detail.to_s return false rescue => detail Puppet.err "Could not create %s: %s" % [path, detail.to_s] return false end elsif FileTest.directory?(File.join(path)) next else FileTest.exist?(File.join(path)) raise Puppet::Error, "Cannot create %s: basedir %s is a file" % [dir, File.join(path)] end } return true end end # Create a new type. Just proxy to the Type class. def self.newtype(name, options = {}, &block) Puppet::Type.newtype(name, options, &block) end # Retrieve a type by name. Just proxy to the Type class. def self.type(name) # LAK:DEP Deprecation notice added 12/17/2008 Puppet.warning "Puppet.type is deprecated; use Puppet::Type.type" Puppet::Type.type(name) end end require 'puppet/type' require 'puppet/network' require 'puppet/ssl' require 'puppet/module' require 'puppet/util/storage' require 'puppet/parser/interpreter' if Puppet[:storeconfigs] require 'puppet/rails' end diff --git a/lib/puppet/defaults.rb b/lib/puppet/defaults.rb index 06469431d..fcd4346fb 100644 --- a/lib/puppet/defaults.rb +++ b/lib/puppet/defaults.rb @@ -1,679 +1,688 @@ # The majority of the system configuration parameters are set in this file. module Puppet # If we're running the standalone puppet process as a non-root user, # use basedirs that are in the user's home directory. conf = nil var = nil name = $0.gsub(/.+#{File::SEPARATOR}/,'').sub(/\.rb$/, '') # Make File.expand_path happy require 'etc' ENV["HOME"] ||= Etc.getpwuid(Process.uid).dir if name != "puppetmasterd" and Puppet::Util::SUIDManager.uid != 0 conf = File.expand_path("~/.puppet") var = File.expand_path("~/.puppet/var") else # Else, use system-wide directories. conf = "/etc/puppet" var = "/var/puppet" end self.setdefaults(:main, :confdir => [conf, "The main Puppet configuration directory. The default for this parameter is calculated based on the user. If the process is runnig as root or the user that ``puppetmasterd`` is supposed to run as, it defaults to a system directory, but if it's running as any other user, it defaults to being in ``~``."], :vardir => [var, "Where Puppet stores dynamic and growing data. The default for this parameter is calculated specially, like `confdir`_."], :name => [name, "The name of the service, if we are running as one. The default is essentially $0 without the path or ``.rb``."] ) if name == "puppetmasterd" logopts = {:default => "$vardir/log", :mode => 0750, :owner => "$user", :group => "$group", :desc => "The Puppet log directory." } else logopts = ["$vardir/log", "The Puppet log directory."] end setdefaults(:main, :logdir => logopts) # This name hackery is necessary so that the rundir is set reasonably during # unit tests. if Process.uid == 0 and %w{puppetd puppetmasterd}.include?(self.name) rundir = "/var/run/puppet" else rundir = "$vardir/run" end self.setdefaults(:main, :trace => [false, "Whether to print stack traces on some errors"], :autoflush => [false, "Whether log files should always flush to disk."], :syslogfacility => ["daemon", "What syslog facility to use when logging to syslog. Syslog has a fixed list of valid facilities, and you must choose one of those; you cannot just make one up."], :statedir => { :default => "$vardir/state", :mode => 01755, :desc => "The directory where Puppet state is stored. Generally, this directory can be removed without causing harm (although it might result in spurious service restarts)." }, :rundir => { :default => rundir, :mode => 01777, :desc => "Where Puppet PID files are kept." }, :genconfig => [false, "Whether to just print a configuration to stdout and exit. Only makes sense when used interactively. Takes into account arguments specified on the CLI."], :genmanifest => [false, "Whether to just print a manifest to stdout and exit. Only makes sense when used interactively. Takes into account arguments specified on the CLI."], :configprint => ["", "Print the value of a specific configuration parameter. If a parameter is provided for this, then the value is printed and puppet exits. Comma-separate multiple values. For a list of all values, specify 'all'. This feature is only available in Puppet versions higher than 0.18.4."], :color => ["ansi", "Whether to use colors when logging to the console. Valid values are ``ansi`` (equivalent to ``true``), ``html`` (mostly used during testing with TextMate), and ``false``, which produces no color."], :mkusers => [false, "Whether to create the necessary user and group that puppetd will run as."], :path => {:default => "none", :desc => "The shell search path. Defaults to whatever is inherited from the parent process.", + :call_on_define => true, # Call our hook with the default value, so we always get the libdir set. :hook => proc do |value| ENV["PATH"] = value unless value == "none" + + paths = ENV["PATH"].split(File::PATH_SEPARATOR) + %w{/usr/sbin /sbin}.each do |path| + unless paths.include?(path) + ENV["PATH"] += File::PATH_SEPARATOR + path + end + end + value end }, :libdir => {:default => "$vardir/lib", :desc => "An extra search path for Puppet. This is only useful for those files that Puppet will load on demand, and is only guaranteed to work for those cases. In fact, the autoload mechanism is responsible for making sure this directory is in Ruby's search path", :call_on_define => true, # Call our hook with the default value, so we always get the libdir set. :hook => proc do |value| if defined? @oldlibdir and $:.include?(@oldlibdir) $:.delete(@oldlibdir) end @oldlibdir = value $: << value end }, :ignoreimport => [false, "A parameter that can be used in commit hooks, since it enables you to parse-check a single file rather than requiring that all files exist."], :authconfig => [ "$confdir/namespaceauth.conf", "The configuration file that defines the rights to the different namespaces and methods. This can be used as a coarse-grained authorization system for both ``puppetd`` and ``puppetmasterd``." ], :environment => {:default => "production", :desc => "The environment Puppet is running in. For clients (e.g., ``puppetd``) this determines the environment itself, which is used to find modules and much more. For servers (i.e., ``puppetmasterd``) this provides the default environment for nodes we know nothing about." }, :diff_args => ["", "Which arguments to pass to the diff command when printing differences between files."], :diff => ["diff", "Which diff command to use when printing differences between files."], :show_diff => [false, "Whether to print a contextual diff when files are being replaced. The diff is printed on stdout, so this option is meaningless unless you are running Puppet interactively. This feature currently requires the ``diff/lcs`` Ruby library."], :daemonize => { :default => true, :desc => "Send the process into the background. This is the default.", :short => "D" }, :maximum_uid => [4294967290, "The maximum allowed UID. Some platforms use negative UIDs but then ship with tools that do not know how to handle signed ints, so the UIDs show up as huge numbers that can then not be fed back into the system. This is a hackish way to fail in a slightly more useful way when that happens."], :node_terminus => ["plain", "Where to find information about nodes."], :httplog => { :default => "$logdir/http.log", :owner => "root", :mode => 0640, :desc => "Where the puppetd web server logs." }, :http_proxy_host => ["none", "The HTTP proxy host to use for outgoing connections. Note: You may need to use a FQDN for the server hostname when using a proxy."], :http_proxy_port => [3128, "The HTTP proxy port to use for outgoing connections"], :http_enable_post_connection_check => [true, "Boolean; wheter or not puppetd should validate the server SSL certificate against the request hostname."], :filetimeout => [ 15, "The minimum time to wait (in seconds) between checking for updates in configuration files. This timeout determines how quickly Puppet checks whether a file (such as manifests or templates) has changed on disk." ] ) hostname = Facter["hostname"].value domain = Facter["domain"].value if domain and domain != "" fqdn = [hostname, domain].join(".") else fqdn = hostname end Puppet.setdefaults(:main, # We have to downcase the fqdn, because the current ssl stuff (as oppsed to in master) doesn't have good facilities for # manipulating naming. :certname => {:default => fqdn.downcase, :desc => "The name to use when handling certificates. Defaults to the fully qualified domain name.", :call_on_define => true, # Call our hook with the default value, so we're always downcased :hook => proc { |value| raise(ArgumentError, "Certificate names must be lower case; see #1168") unless value == value.downcase }}, :certdnsnames => ['', "The DNS names on the Server certificate as a colon-separated list. If it's anything other than an empty string, it will be used as an alias in the created certificate. By default, only the server gets an alias set up, and only for 'puppet'."], :certdir => ["$ssldir/certs", "The certificate directory."], :ssldir => { :default => "$confdir/ssl", :mode => 0771, :owner => "root", :desc => "Where SSL certificates are kept." }, :publickeydir => ["$ssldir/public_keys", "The public key directory."], :requestdir => ["$ssldir/certificate_requests", "Where host certificate requests are stored."], :privatekeydir => { :default => "$ssldir/private_keys", :mode => 0750, :desc => "The private key directory." }, :privatedir => { :default => "$ssldir/private", :mode => 0750, :desc => "Where the client stores private certificate information." }, :passfile => { :default => "$privatedir/password", :mode => 0640, :desc => "Where puppetd stores the password for its private key. Generally unused." }, :hostcsr => { :default => "$ssldir/csr_$certname.pem", :mode => 0644, :desc => "Where individual hosts store and look for their certificate requests." }, :hostcert => { :default => "$certdir/$certname.pem", :mode => 0644, :desc => "Where individual hosts store and look for their certificates." }, :hostprivkey => { :default => "$privatekeydir/$certname.pem", :mode => 0600, :desc => "Where individual hosts store and look for their private key." }, :hostpubkey => { :default => "$publickeydir/$certname.pem", :mode => 0644, :desc => "Where individual hosts store and look for their public key." }, :localcacert => { :default => "$certdir/ca.pem", :mode => 0644, :desc => "Where each client stores the CA certificate." }, :hostcrl => { :default => "$ssldir/crl.pem", :mode => 0644, :desc => "Where the host's certificate revocation list can be found. This is distinct from the certificate authority's CRL." } ) setdefaults(:ca, :cadir => { :default => "$ssldir/ca", :owner => "$user", :group => "$group", :mode => 0770, :desc => "The root directory for the certificate authority." }, :cacert => { :default => "$cadir/ca_crt.pem", :owner => "$user", :group => "$group", :mode => 0660, :desc => "The CA certificate." }, :cakey => { :default => "$cadir/ca_key.pem", :owner => "$user", :group => "$group", :mode => 0660, :desc => "The CA private key." }, :capub => { :default => "$cadir/ca_pub.pem", :owner => "$user", :group => "$group", :desc => "The CA public key." }, :cacrl => { :default => "$cadir/ca_crl.pem", :owner => "$user", :group => "$group", :mode => 0664, :desc => "The certificate revocation list (CRL) for the CA. Will be used if present but otherwise ignored.", :hook => proc do |value| if value == 'false' Puppet.warning "Setting the :cacrl to 'false' is deprecated; Puppet will just ignore the crl if yours is missing" end end }, :caprivatedir => { :default => "$cadir/private", :owner => "$user", :group => "$group", :mode => 0770, :desc => "Where the CA stores private certificate information." }, :csrdir => { :default => "$cadir/requests", :owner => "$user", :group => "$group", :desc => "Where the CA stores certificate requests" }, :signeddir => { :default => "$cadir/signed", :owner => "$user", :group => "$group", :mode => 0770, :desc => "Where the CA stores signed certificates." }, :capass => { :default => "$caprivatedir/ca.pass", :owner => "$user", :group => "$group", :mode => 0660, :desc => "Where the CA stores the password for the private key" }, :serial => { :default => "$cadir/serial", :owner => "$user", :group => "$group", :mode => 0644, :desc => "Where the serial number for certificates is stored." }, :autosign => { :default => "$confdir/autosign.conf", :mode => 0644, :desc => "Whether to enable autosign. Valid values are true (which autosigns any key request, and is a very bad idea), false (which never autosigns any key request), and the path to a file, which uses that configuration file to determine which keys to sign."}, :ca_days => ["", "How long a certificate should be valid. This parameter is deprecated, use ca_ttl instead"], :ca_ttl => ["5y", "The default TTL for new certificates; valid values must be an integer, optionally followed by one of the units 'y' (years of 365 days), 'd' (days), 'h' (hours), or 's' (seconds). The unit defaults to seconds. If this parameter is set, ca_days is ignored. Examples are '3600' (one hour) and '1825d', which is the same as '5y' (5 years) "], :ca_md => ["md5", "The type of hash used in certificates."], :req_bits => [2048, "The bit length of the certificates."], :keylength => [1024, "The bit length of keys."], :cert_inventory => { :default => "$cadir/inventory.txt", :mode => 0644, :owner => "$user", :group => "$group", :desc => "A Complete listing of all certificates" } ) # Define the config default. self.setdefaults(self.settings[:name], :config => ["$confdir/puppet.conf", "The configuration file for #{Puppet[:name]}."], :pidfile => ["$rundir/$name.pid", "The pid file"], :bindaddress => ["", "The address a listening server should bind to. Mongrel servers default to 127.0.0.1 and WEBrick defaults to 0.0.0.0."], :servertype => {:default => "webrick", :desc => "The type of server to use. Currently supported options are webrick and mongrel. If you use mongrel, you will need a proxy in front of the process or processes, since Mongrel cannot speak SSL.", :call_on_define => true, # Call our hook with the default value, so we always get the correct bind address set. :hook => proc { |value| value == "webrick" ? Puppet.settings[:bindaddress] = "0.0.0.0" : Puppet.settings[:bindaddress] = "127.0.0.1" if Puppet.settings[:bindaddress] == "" } } ) self.setdefaults(:puppetmasterd, :user => ["puppet", "The user puppetmasterd should run as."], :group => ["puppet", "The group puppetmasterd should run as."], :manifestdir => ["$confdir/manifests", "Where puppetmasterd looks for its manifests."], :manifest => ["$manifestdir/site.pp", "The entry-point manifest for puppetmasterd."], :code => ["", "Code to parse directly. This is essentially only used by ``puppet``, and should only be set if you're writing your own Puppet executable"], :masterlog => { :default => "$logdir/puppetmaster.log", :owner => "$user", :group => "$group", :mode => 0660, :desc => "Where puppetmasterd logs. This is generally not used, since syslog is the default log destination." }, :masterhttplog => { :default => "$logdir/masterhttp.log", :owner => "$user", :group => "$group", :mode => 0660, :create => true, :desc => "Where the puppetmasterd web server logs." }, :masterport => [8140, "Which port puppetmasterd listens on."], :parseonly => [false, "Just check the syntax of the manifests."], :node_name => ["cert", "How the puppetmaster determines the client's identity and sets the 'hostname', 'fqdn' and 'domain' facts for use in the manifest, in particular for determining which 'node' statement applies to the client. Possible values are 'cert' (use the subject's CN in the client's certificate) and 'facter' (use the hostname that the client reported in its facts)"], :bucketdir => { :default => "$vardir/bucket", :mode => 0750, :owner => "$user", :group => "$group", :desc => "Where FileBucket files are stored." }, :ca => [true, "Wether the master should function as a certificate authority."], :modulepath => {:default => "$confdir/modules:/usr/share/puppet/modules", :desc => "The search path for modules as a colon-separated list of directories.", :type => :element }, # We don't want this to be considered a file, since it's multiple files. :ssl_client_header => ["HTTP_X_CLIENT_DN", "The header containing an authenticated client's SSL DN. Only used with Mongrel. This header must be set by the proxy to the authenticated client's SSL DN (e.g., ``/CN=puppet.reductivelabs.com``). See http://reductivelabs.com/puppet/trac/wiki/UsingMongrel for more information."], :ssl_client_verify_header => ["HTTP_X_CLIENT_VERIFY", "The header containing the status message of the client verification. Only used with Mongrel. This header must be set by the proxy to 'SUCCESS' if the client successfully authenticated, and anything else otherwise. See http://reductivelabs.com/puppet/trac/wiki/UsingMongrel for more information."], # To make sure this directory is created before we try to use it on the server, we need # it to be in the server section (#1138). :yamldir => {:default => "$vardir/yaml", :owner => "$user", :group => "$user", :mode => "750", :desc => "The directory in which YAML data is stored, usually in a subdirectory."}, :reports => ["store", "The list of reports to generate. All reports are looked for in puppet/reports/.rb, and multiple report names should be comma-separated (whitespace is okay)." ], :reportdir => {:default => "$vardir/reports", :mode => 0750, :owner => "$user", :group => "$group", :desc => "The directory in which to store reports received from the client. Each client gets a separate subdirectory."}, :fileserverconfig => ["$confdir/fileserver.conf", "Where the fileserver configuration is stored."], :rrddir => {:default => "$vardir/rrd", :owner => "$user", :group => "$group", :desc => "The directory where RRD database files are stored. Directories for each reporting host will be created under this directory." }, :rrdgraph => [false, "Whether RRD information should be graphed."], :rrdinterval => ["$runinterval", "How often RRD should expect data. This should match how often the hosts report back to the server."] ) self.setdefaults(:puppetd, :localconfig => { :default => "$statedir/localconfig", :owner => "root", :mode => 0660, :desc => "Where puppetd caches the local configuration. An extension indicating the cache format is added automatically."}, :statefile => { :default => "$statedir/state.yaml", :mode => 0660, :desc => "Where puppetd and puppetmasterd store state associated with the running configuration. In the case of puppetmasterd, this file reflects the state discovered through interacting with clients." }, :clientyamldir => {:default => "$vardir/client_yaml", :mode => "750", :desc => "The directory in which client-side YAML data is stored."}, :classfile => { :default => "$statedir/classes.txt", :owner => "root", :mode => 0644, :desc => "The file in which puppetd stores a list of the classes associated with the retrieved configuration. Can be loaded in the separate ``puppet`` executable using the ``--loadclasses`` option."}, :puppetdlog => { :default => "$logdir/puppetd.log", :owner => "root", :mode => 0640, :desc => "The log file for puppetd. This is generally not used." }, :server => ["puppet", "The server to which server puppetd should connect"], :ignoreschedules => [false, "Boolean; whether puppetd should ignore schedules. This is useful for initial puppetd runs."], :puppetport => [8139, "Which port puppetd listens on."], :noop => [false, "Whether puppetd should be run in noop mode."], :runinterval => [1800, # 30 minutes "How often puppetd applies the client configuration; in seconds."], :listen => [false, "Whether puppetd should listen for connections. If this is true, then by default only the ``runner`` server is started, which allows remote authorized and authenticated nodes to connect and trigger ``puppetd`` runs."], :ca_server => ["$server", "The server to use for certificate authority requests. It's a separate server because it cannot and does not need to horizontally scale."], :ca_port => ["$masterport", "The port to use for the certificate authority."], :catalog_format => ["yaml", "What format to use to dump the catalog. Only supports 'marshal' and 'yaml'. Only matters on the client, since it asks the server for a specific format."], :puppetdlockfile => [ "$statedir/puppetdlock", "A lock file to temporarily stop puppetd from doing anything."], :usecacheonfailure => [true, "Whether to use the cached configuration when the remote configuration will not compile. This option is useful for testing new configurations, where you want to fix the broken configuration rather than reverting to a known-good one." ], :ignorecache => [false, "Ignore cache and always recompile the configuration. This is useful for testing new configurations, where the local cache may in fact be stale even if the timestamps are up to date - if the facts change or if the server changes." ], :downcasefacts => [false, "Whether facts should be made all lowercase when sent to the server."], :dynamicfacts => ["memorysize,memoryfree,swapsize,swapfree", "Facts that are dynamic; these facts will be ignored when deciding whether changed facts should result in a recompile. Multiple facts should be comma-separated."], :splaylimit => ["$runinterval", "The maximum time to delay before runs. Defaults to being the same as the run interval."], :splay => [false, "Whether to sleep for a pseudo-random (but consistent) amount of time before a run."], :clientbucketdir => { :default => "$vardir/clientbucket", :mode => 0750, :desc => "Where FileBucket files are stored locally." }, :configtimeout => [120, "How long the client should wait for the configuration to be retrieved before considering it a failure. This can help reduce flapping if too many clients contact the server at one time." ], :reportserver => ["$server", "The server to which to send transaction reports." ], :report => [false, "Whether to send reports after every transaction." ], :graph => [false, "Whether to create dot graph files for the different configuration graphs. These dot files can be interpreted by tools like OmniGraffle or dot (which is part of ImageMagick)."], :graphdir => ["$statedir/graphs", "Where to store dot-outputted graphs."], :storeconfigs => [false, "Whether to store each client's configuration. This requires ActiveRecord from Ruby on Rails."] ) # Plugin information. self.setdefaults(:main, :pluginpath => {:default => "$vardir/plugins/", :desc => "Where Puppet should look for plugins. Multiple directories should be colon-separated, like normal PATH variables. As of 0.23.1, this option is deprecated; download your custom libraries to the $libdir instead.", :type => :element}, # Don't consider this a file, since it's a colon-separated list. :plugindest => ["$libdir", "Where Puppet should store plugins that it pulls down from the central server."], :pluginsource => ["puppet://$server/plugins", "From where to retrieve plugins. The standard Puppet ``file`` type is used for retrieval, so anything that is a valid file source can be used here."], :pluginsync => [false, "Whether plugins should be synced with the central server."], :pluginsignore => [".svn CVS", "What files to ignore when pulling down plugins."] ) # Central fact information. self.setdefaults(:main, :factpath => {:default => "$vardir/facts/", :desc => "Where Puppet should look for facts. Multiple directories should be colon-separated, like normal PATH variables.", :call_on_define => true, # Call our hook with the default value, so we always get the value added to facter. :type => :element, # Don't consider it a file, because it could be multiple colon-separated files :hook => proc { |value| Facter.search(value) if Facter.respond_to?(:search) }}, :factdest => ["$vardir/facts/", "Where Puppet should store facts that it pulls down from the central server."], :factsource => ["puppet://$server/facts/", "From where to retrieve facts. The standard Puppet ``file`` type is used for retrieval, so anything that is a valid file source can be used here."], :factsync => [false, "Whether facts should be synced with the central server."], :factsignore => [".svn CVS", "What files to ignore when pulling down facts."] ) self.setdefaults(:tagmail, :tagmap => ["$confdir/tagmail.conf", "The mapping between reporting tags and email addresses."], :sendmail => [%x{which sendmail 2>/dev/null}.chomp, "Where to find the sendmail binary with which to send email."], :reportfrom => ["report@" + [Facter["hostname"].value, Facter["domain"].value].join("."), "The 'from' email address for the reports."], :smtpserver => ["none", "The server through which to send email reports."] ) self.setdefaults(:rails, :dblocation => { :default => "$statedir/clientconfigs.sqlite3", :mode => 0660, :owner => "$user", :group => "$group", :desc => "The database cache for client configurations. Used for querying within the language." }, :dbadapter => [ "sqlite3", "The type of database to use." ], :dbmigrate => [ false, "Whether to automatically migrate the database." ], :dbname => [ "puppet", "The name of the database to use." ], :dbserver => [ "localhost", "The database server for Client caching. Only used when networked databases are used."], :dbuser => [ "puppet", "The database user for Client caching. Only used when networked databases are used."], :dbpassword => [ "puppet", "The database password for Client caching. Only used when networked databases are used."], :dbsocket => [ "", "The database socket location. Only used when networked databases are used. Will be ignored if the value is an empty string."], :railslog => {:default => "$logdir/rails.log", :mode => 0600, :owner => "$user", :group => "$group", :desc => "Where Rails-specific logs are sent" }, :rails_loglevel => ["info", "The log level for Rails connections. The value must be a valid log level within Rails. Production environments normally use ``info`` and other environments normally use ``debug``."] ) setdefaults(:transaction, :tags => ["", "Tags to use to find resources. If this is set, then only resources tagged with the specified tags will be applied. Values must be comma-separated."], :evaltrace => [false, "Whether each resource should log when it is being evaluated. This allows you to interactively see exactly what is being done."], :summarize => [false, "Whether to print a transaction summary." ] ) setdefaults(:parser, :typecheck => [true, "Whether to validate types during parsing."], :paramcheck => [true, "Whether to validate parameters during parsing."] ) setdefaults(:main, :casesensitive => [false, "Whether matching in case statements and selectors should be case-sensitive. Case insensitivity is handled by downcasing all values before comparison."], :external_nodes => ["none", "An external command that can produce node information. The output must be a YAML dump of a hash, and that hash must have one or both of ``classes`` and ``parameters``, where ``classes`` is an array and ``parameters`` is a hash. For unknown nodes, the commands should exit with a non-zero exit code. This command makes it straightforward to store your node mapping information in other data sources like databases."]) setdefaults(:ldap, :ldapnodes => [false, "Whether to search for node configurations in LDAP. See http://reductivelabs.com/puppet/trac/wiki/LdapNodes/ for more information."], :ldapssl => [false, "Whether SSL should be used when searching for nodes. Defaults to false because SSL usually requires certificates to be set up on the client side."], :ldaptls => [false, "Whether TLS should be used when searching for nodes. Defaults to false because TLS usually requires certificates to be set up on the client side."], :ldapserver => ["ldap", "The LDAP server. Only used if ``ldapnodes`` is enabled."], :ldapport => [389, "The LDAP port. Only used if ``ldapnodes`` is enabled."], :ldapstring => ["(&(objectclass=puppetClient)(cn=%s))", "The search string used to find an LDAP node."], :ldapclassattrs => ["puppetclass", "The LDAP attributes to use to define Puppet classes. Values should be comma-separated."], :ldapstackedattrs => ["puppetvar", "The LDAP attributes that should be stacked to arrays by adding the values in all hierarchy elements of the tree. Values should be comma-separated."], :ldapattrs => ["all", "The LDAP attributes to include when querying LDAP for nodes. All returned attributes are set as variables in the top-level scope. Multiple values should be comma-separated. The value 'all' returns all attributes."], :ldapparentattr => ["parentnode", "The attribute to use to define the parent node."], :ldapuser => ["", "The user to use to connect to LDAP. Must be specified as a full DN."], :ldappassword => ["", "The password to use to connect to LDAP."], :ldapbase => ["", "The search base for LDAP searches. It's impossible to provide a meaningful default here, although the LDAP libraries might have one already set. Generally, it should be the 'ou=Hosts' branch under your main directory."] ) # This doesn't actually work right now. setdefaults(:parser, :lexical => [false, "Whether to use lexical scoping (vs. dynamic)."], :templatedir => ["$vardir/templates", "Where Puppet looks for template files. Can be a list of colon-seperated directories." ] ) end diff --git a/lib/puppet/file_collection.rb b/lib/puppet/file_collection.rb new file mode 100644 index 000000000..7db2600c0 --- /dev/null +++ b/lib/puppet/file_collection.rb @@ -0,0 +1,30 @@ +# A simple way to turn file names into singletons, +# so we don't have tons of copies of each file path around. +class Puppet::FileCollection + require 'puppet/file_collection/lookup' + + def self.collection + @collection + end + + def initialize + @paths = [] + @inverse = {} + end + + def index(path) + if i = @inverse[path] + return i + else + @paths << path + i = @inverse[path] = @paths.length - 1 + return i + end + end + + def path(index) + @paths[index] + end + + @collection = self.new +end diff --git a/lib/puppet/file_collection/lookup.rb b/lib/puppet/file_collection/lookup.rb new file mode 100644 index 000000000..ddb0c8431 --- /dev/null +++ b/lib/puppet/file_collection/lookup.rb @@ -0,0 +1,20 @@ +require 'puppet/file_collection' + +# A simple module for looking up file paths and indexes +# in a file collection. +module Puppet::FileCollection::Lookup + attr_accessor :line, :file_index + + def file_collection + Puppet::FileCollection.collection + end + + def file=(path) + @file_index = file_collection.index(path) + end + + def file + return nil unless file_index + file_collection.path(file_index) + end +end diff --git a/lib/puppet/indirector/node/ldap.rb b/lib/puppet/indirector/node/ldap.rb index 01010a2af..ab5d47b1c 100644 --- a/lib/puppet/indirector/node/ldap.rb +++ b/lib/puppet/indirector/node/ldap.rb @@ -1,227 +1,263 @@ require 'puppet/node' require 'puppet/indirector/ldap' class Puppet::Node::Ldap < Puppet::Indirector::Ldap desc "Search in LDAP for node configuration information. See the `LdapNodes`:trac: page for more information. This will first search for whatever the certificate name is, then (if that name contains a '.') for the short name, then 'default'." # The attributes that Puppet class information is stored in. def class_attributes # LAK:NOTE See http://snurl.com/21zf8 [groups_google_com] x = Puppet[:ldapclassattrs].split(/\s*,\s*/) end # Separate this out so it's relatively atomic. It's tempting to call # process() instead of name2hash() here, but it ends up being # difficult to test because all exceptions get caught by ldapsearch. # LAK:NOTE Unfortunately, the ldap support is too stupid to throw anything # but LDAP::ResultError, even on bad connections, so we are rough handed # with our error handling. def name2hash(name) info = nil ldapsearch(search_filter(name)) { |entry| info = entry2hash(entry) } return info end # Look for our node in ldap. def find(request) names = [request.key] if request.key.include?(".") # we assume it's an fqdn names << request.key.sub(/\..+/, '') end names << "default" node = nil names.each do |name| next unless info = name2hash(name) break if node = info2node(request.key, info) end return node end # Find more than one node. LAK:NOTE This is a bit of a clumsy API, because the 'search' # method currently *requires* a key. It seems appropriate in some cases but not others, # and I don't really know how to get rid of it as a requirement but allow it when desired. def search(request) if classes = request.options[:class] classes = [classes] unless classes.is_a?(Array) filter = "(&(objectclass=puppetClient)(puppetclass=" + classes.join(")(puppetclass=") + "))" else filter = "(objectclass=puppetClient)" end infos = [] ldapsearch(filter) { |entry| infos << entry2hash(entry) } return infos.collect do |info| info2node(info[:name], info) end end # The parent attribute, if we have one. def parent_attribute if pattr = Puppet[:ldapparentattr] and ! pattr.empty? pattr else nil end end # The attributes that Puppet will stack as array over the full # hierarchy. def stacked_attributes Puppet[:ldapstackedattrs].split(/\s*,\s*/) end # Convert the found entry into a simple hash. def entry2hash(entry) result = {} result[:name] = entry.dn.split(',')[0].split("=")[1] - if pattr = parent_attribute - if values = entry.vals(pattr) - if values.length > 1 - raise Puppet::Error, - "Node entry %s specifies more than one parent: %s" % [entry.dn, values.inspect] - end - unless values.empty? - result[:parent] = values.shift - end - end - end - - result[:classes] = [] - class_attributes.each { |attr| - if values = entry.vals(attr) - values.each do |v| result[:classes] << v end - end - } - result[:classes].uniq! - - result[:stacked] = [] - stacked_params = stacked_attributes - stacked_params.each { |attr| - if values = entry.vals(attr) - result[:stacked] = result[:stacked] + values - end - } - - - result[:parameters] = entry.to_hash.inject({}) do |hash, ary| - unless stacked_params.include?(ary[0]) # don't add our stacked parameters to the main param list - if ary[1].length == 1 - hash[ary[0]] = ary[1].shift - else - hash[ary[0]] = ary[1] - end - end - hash - end + result[:parent] = get_parent_from_entry(entry) if parent_attribute + result[:classes] = get_classes_from_entry(entry) + result[:stacked] = get_stacked_values_from_entry(entry) + result[:parameters] = get_parameters_from_entry(entry) result[:environment] = result[:parameters]["environment"] if result[:parameters]["environment"] result[:stacked_parameters] = {} if result[:stacked] result[:stacked].each do |value| param = value.split('=', 2) result[:stacked_parameters][param[0]] = param[1] end end if result[:stacked_parameters] result[:stacked_parameters].each do |param, value| result[:parameters][param] = value unless result[:parameters].include?(param) end end + result[:parameters] = convert_parameters(result[:parameters]) + result end # Default to all attributes. def search_attributes ldapattrs = Puppet[:ldapattrs] # results in everything getting returned return nil if ldapattrs == "all" search_attrs = class_attributes + ldapattrs.split(/\s*,\s*/) if pattr = parent_attribute search_attrs << pattr end search_attrs end # The ldap search filter to use. def search_filter(name) filter = Puppet[:ldapstring] if filter.include? "%s" # Don't replace the string in-line, since that would hard-code our node # info. filter = filter.gsub('%s', name) end filter end private # Add our hash of ldap information to the node instance. def add_to_node(node, information) node.classes = information[:classes].uniq unless information[:classes].nil? or information[:classes].empty? node.parameters = information[:parameters] unless information[:parameters].nil? or information[:parameters].empty? node.environment = information[:environment] if information[:environment] end + def convert_parameters(parameters) + result = {} + parameters.each do |param, value| + if value.is_a?(Array) + result[param] = value.collect { |v| convert(v) } + else + result[param] = convert(value) + end + end + result + end + + # Convert any values if necessary. + def convert(value) + case value + when Integer, Fixnum, Bignum; value + when "true"; true + when "false"; false + else + value + end + end + # Find information for our parent and merge it into the current info. def find_and_merge_parent(parent, information) unless parent_info = name2hash(parent) raise Puppet::Error.new("Could not find parent node '%s'" % parent) end information[:classes] += parent_info[:classes] parent_info[:parameters].each do |param, value| # Specifically test for whether it's set, so false values are handled # correctly. information[:parameters][param] = value unless information[:parameters].include?(param) end information[:environment] ||= parent_info[:environment] parent_info[:parent] end # Take a name and a hash, and return a node instance. def info2node(name, info) merge_parent(info) if info[:parent] node = Puppet::Node.new(name) add_to_node(node, info) node.fact_merge node end def merge_parent(info) parent_info = nil parent = info[:parent] # Preload the parent array with the node name. parents = [info[:name]] while parent if parents.include?(parent) raise ArgumentError, "Found loop in LDAP node parents; %s appears twice" % parent end parents << parent parent = find_and_merge_parent(parent, info) end return info end + + def get_classes_from_entry(entry) + result = class_attributes.inject([]) do |array, attr| + if values = entry.vals(attr) + values.each do |v| array << v end + end + array + end + result.uniq + end + + def get_parameters_from_entry(entry) + stacked_params = stacked_attributes + entry.to_hash.inject({}) do |hash, ary| + unless stacked_params.include?(ary[0]) # don't add our stacked parameters to the main param list + if ary[1].length == 1 + hash[ary[0]] = ary[1].shift + else + hash[ary[0]] = ary[1] + end + end + hash + end + end + + def get_parent_from_entry(entry) + pattr = parent_attribute + + return nil unless values = entry.vals(pattr) + + if values.length > 1 + raise Puppet::Error, + "Node entry %s specifies more than one parent: %s" % [entry.dn, values.inspect] + end + return nil if values.empty? + return values.shift + end + + def get_stacked_values_from_entry(entry) + stacked_attributes.inject([]) do |result, attr| + if values = entry.vals(attr) + result += values + end + result + end + end end diff --git a/lib/puppet/parser/ast.rb b/lib/puppet/parser/ast.rb index 303d75bac..ab23dd1f8 100644 --- a/lib/puppet/parser/ast.rb +++ b/lib/puppet/parser/ast.rb @@ -1,118 +1,119 @@ # the parent class for all of our syntactical objects require 'puppet' require 'puppet/util/autoload' +require 'puppet/file_collection/lookup' # The base class for all of the objects that make up the parse trees. # Handles things like file name, line #, and also does the initialization # for all of the parameters of all of the child objects. class Puppet::Parser::AST # Do this so I don't have to type the full path in all of the subclasses AST = Puppet::Parser::AST + include Puppet::FileCollection::Lookup + include Puppet::Util::Errors include Puppet::Util::MethodHelper include Puppet::Util::Docs - attr_accessor :line, :file, :parent, :scope + attr_accessor :parent, :scope # don't fetch lexer comment by default def use_docs self.class.use_docs end # allow our subclass to specify they want documentation class << self attr_accessor :use_docs def associates_doc self.use_docs = true end end # Does this ast object set something? If so, it gets evaluated first. def self.settor? if defined? @settor @settor else false end end # Evaluate the current object. Just a stub method, since the subclass # should override this method. # of the contained children and evaluates them in turn, returning a # list of all of the collected values, rejecting nil values def evaluate(*options) raise Puppet::DevError, "Did not override #evaluate in %s" % self.class end # Throw a parse error. def parsefail(message) self.fail(Puppet::ParseError, message) end # Wrap a statemp in a reusable way so we always throw a parse error. def parsewrap exceptwrap :type => Puppet::ParseError do yield end end # The version of the evaluate method that should be called, because it # correctly handles errors. It is critical to use this method because # it can enable you to catch the error where it happens, rather than # much higher up the stack. def safeevaluate(*options) # We duplicate code here, rather than using exceptwrap, because this # is called so many times during parsing. begin return self.evaluate(*options) rescue Puppet::Error => detail raise adderrorcontext(detail) rescue => detail error = Puppet::Error.new(detail.to_s) # We can't use self.fail here because it always expects strings, # not exceptions. raise adderrorcontext(error, detail) end end # Initialize the object. Requires a hash as the argument, and # takes each of the parameters of the hash and calls the settor # method for them. This is probably pretty inefficient and should # likely be changed at some point. def initialize(args) - @file = nil - @line = nil set_options(args) end end # And include all of the AST subclasses. require 'puppet/parser/ast/arithmetic_operator' require 'puppet/parser/ast/astarray' require 'puppet/parser/ast/branch' require 'puppet/parser/ast/boolean_operator' require 'puppet/parser/ast/caseopt' require 'puppet/parser/ast/casestatement' require 'puppet/parser/ast/collection' require 'puppet/parser/ast/collexpr' require 'puppet/parser/ast/comparison_operator' require 'puppet/parser/ast/definition' require 'puppet/parser/ast/else' require 'puppet/parser/ast/function' require 'puppet/parser/ast/hostclass' require 'puppet/parser/ast/ifstatement' require 'puppet/parser/ast/leaf' require 'puppet/parser/ast/minus' require 'puppet/parser/ast/node' require 'puppet/parser/ast/nop' require 'puppet/parser/ast/not' require 'puppet/parser/ast/resource' require 'puppet/parser/ast/resource_defaults' require 'puppet/parser/ast/resource_override' require 'puppet/parser/ast/resource_reference' require 'puppet/parser/ast/resourceparam' require 'puppet/parser/ast/selector' require 'puppet/parser/ast/tag' require 'puppet/parser/ast/vardef' diff --git a/lib/puppet/parser/ast/leaf.rb b/lib/puppet/parser/ast/leaf.rb index c545c1e47..dba9812a7 100644 --- a/lib/puppet/parser/ast/leaf.rb +++ b/lib/puppet/parser/ast/leaf.rb @@ -1,90 +1,90 @@ class Puppet::Parser::AST # The base class for all of the leaves of the parse trees. These # basically just have types and values. Both of these parameters # are simple values, not AST objects. class Leaf < AST attr_accessor :value, :type # Return our value. def evaluate(scope) return @value end def to_s return @value end end # The boolean class. True or false. Converts the string it receives # to a Ruby boolean. class Boolean < AST::Leaf # Use the parent method, but then convert to a real boolean. def initialize(hash) super unless @value == true or @value == false raise Puppet::DevError, "'%s' is not a boolean" % @value end @value end end # The base string class. class String < AST::Leaf # Interpolate the string looking for variables, and then return # the result. def evaluate(scope) - return scope.strinterp(@value, @file, @line) + return scope.strinterp(@value, file, line) end end # An uninterpreted string. class FlatString < AST::Leaf def evaluate(scope) return @value end end # The 'default' option on case statements and selectors. class Default < AST::Leaf; end # Capitalized words; used mostly for type-defaults, but also # get returned by the lexer any other time an unquoted capitalized # word is found. class Type < AST::Leaf; end # Lower-case words. class Name < AST::Leaf; end # double-colon separated class names class ClassName < AST::Leaf; end # undef values; equiv to nil class Undef < AST::Leaf; end # Host names, either fully qualified or just the short name class HostName < AST::Leaf def initialize(hash) super unless @value =~ %r{^[0-9a-zA-Z\-]+(\.[0-9a-zA-Z\-]+)*$} raise Puppet::DevError, "'%s' is not a valid hostname" % @value end end end # A simple variable. This object is only used during interpolation; # the VarDef class is used for assignment. class Variable < Name # Looks up the value of the object in the scope tree (does # not include syntactical constructs, like '$' and '{}'). def evaluate(scope) parsewrap do return scope.lookupvar(@value) end end end end diff --git a/lib/puppet/parser/ast/resource_override.rb b/lib/puppet/parser/ast/resource_override.rb index 5c4a2410f..f8cf3a81e 100644 --- a/lib/puppet/parser/ast/resource_override.rb +++ b/lib/puppet/parser/ast/resource_override.rb @@ -1,68 +1,68 @@ require 'puppet/parser/ast/resource' class Puppet::Parser::AST # Set a parameter on a resource specification created somewhere else in the # configuration. The object is responsible for verifying that this is allowed. class ResourceOverride < Resource associates_doc attr_accessor :object attr_reader :params # Iterate across all of our children. def each [@object,@params].flatten.each { |param| #Puppet.debug("yielding param %s" % param) yield param } end # Does not actually return an object; instead sets an object # in the current scope. def evaluate(scope) # Get our object reference. resource = @object.safeevaluate(scope) hash = {} # Evaluate all of the specified params. params = @params.collect { |param| param.safeevaluate(scope) } # Now we just create a normal resource, but we call a very different # method on the scope. resource = [resource] unless resource.is_a?(Array) resource = resource.collect do |r| res = Puppet::Parser::Resource.new( :type => r.type, :title => r.title, :params => params, - :file => @file, - :line => @line, + :file => file, + :line => line, :source => scope.source, :scope => scope ) # Now we tell the scope that it's an override, and it behaves as # necessary. scope.compiler.add_override(res) res end # decapsulate array in case of only one item return resource.pop if resource.length == 1 return resource end # Create our ResourceDef. Handles type checking for us. def initialize(hash) @checked = false super #self.typecheck(@type.value) end end end diff --git a/lib/puppet/parser/resource.rb b/lib/puppet/parser/resource.rb index 47dc798b3..e016a3047 100644 --- a/lib/puppet/parser/resource.rb +++ b/lib/puppet/parser/resource.rb @@ -1,447 +1,451 @@ # 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' + require 'puppet/file_collection/lookup' + + include Puppet::FileCollection::Lookup + 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 :source, :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 return if finished? @finished = true add_defaults() add_metaparams() add_scope_tags() validate() end # Has this resource already been finished? def finished? defined?(@finished) and @finished 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.reject { |name, param| param.value == :undef }.inject({}) do |hash, ary| hash[ary[0].to_s] = ary[1] hash end db_resource.ar_hash_merge(db_resource.get_params_hash(), updated_params, :create => Proc.new { |name, parameter| parameter.to_rails(db_resource) }, :delete => Proc.new { |values| values.each { |value| Puppet::Rails::ParamValue.delete(value['id']) } }, :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| Puppet::Rails::ResourceTag.delete(tag['id']) }, :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 # 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 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| next if param.value == :undef param.to_rails(db_resource) } tags.each { |tag| db_resource.add_resource_tag(tag) } return db_resource end # Create a Puppet::Resource instance from this parser resource. # We plan, at some point, on not needing to do this conversion, but # it's sufficient for now. def to_resource result = Puppet::Resource.new(type, title) to_hash.each do |p, v| if v.is_a?(Puppet::Parser::Resource::Reference) v = Puppet::Resource::Reference.new(v.type, v.title) elsif v.is_a?(Array) # flatten resource references arrays if v.flatten.find { |av| av.is_a?(Puppet::Parser::Resource::Reference) } v = v.flatten end v = v.collect do |av| if av.is_a?(Puppet::Parser::Resource::Reference) av = Puppet::Resource::Reference.new(av.type, av.title) end av end end # If the value is an array with only one value, then # convert it to a single value. This is largely so that # the database interaction doesn't have to worry about # whether it returns an array or a string. result[p] = if v.is_a?(Array) and v.length == 1 v[0] else v end end result.file = self.file result.line = self.line result.tag(*self.tags) return result end def to_s self.ref end # Translate our object to a transportable object. def to_trans return nil if virtual? return to_resource.to_trans end # Convert this resource to a RAL resource. We hackishly go via the # transportable stuff. def to_ral to_resource.to_ral 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.dup 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) next if @params[name] and @params[name].value == :undef # 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 set_parameter(name, val) and next 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) puts caller if Puppet[:trace] 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 # Make sure the resource's parameters are all valid for the type. def validate @params.each do |name, param| # Make sure it's a valid parameter. paramcheck(name) end end end diff --git a/lib/puppet/parser/resource/param.rb b/lib/puppet/parser/resource/param.rb index 1a5cfe8fb..7ce58f4c4 100644 --- a/lib/puppet/parser/resource/param.rb +++ b/lib/puppet/parser/resource/param.rb @@ -1,97 +1,101 @@ +require 'puppet/file_collection/lookup' + # The parameters we stick in Resources. class Puppet::Parser::Resource::Param - attr_accessor :name, :value, :source, :line, :file, :add + attr_accessor :name, :value, :source, :add include Puppet::Util include Puppet::Util::Errors include Puppet::Util::MethodHelper + include Puppet::FileCollection::Lookup + def initialize(hash) set_options(hash) requiredopts(:name, :value, :source) @name = symbolize(@name) end def inspect "#<#{self.class} @name => #{name}, @value => #{value}, @source => #{source.name}, @line => #{line}>" end def line_to_i return line ? Integer(line) : nil end # Make sure an array (or possibly not an array) of values is correctly # set up for Rails. The main thing is that Resource::Reference objects # should stay objects, so they just get serialized. def munge_for_rails(values) values = value.is_a?(Array) ? value : [value] values.map do |v| if v.is_a?(Puppet::Parser::Resource::Reference) v else v.to_s end end end # Store a new parameter in a Rails db. def to_rails(db_resource) values = munge_for_rails(value) param_name = Puppet::Rails::ParamName.find_or_create_by_name(self.name.to_s) line_number = line_to_i() return values.collect do |v| db_resource.param_values.create(:value => v, :line => line_number, :param_name => param_name) end end def modify_rails_values(db_values) #dev_warn if db_values.nil? || db_values.empty? values_to_remove(db_values).each { |remove_me| Puppet::Rails::ParamValue.delete(remove_me['id']) } line_number = line_to_i() db_param_name = db_values[0]['param_name_id'] values_to_add(db_values).each { |add_me| Puppet::Rails::ParamValue.create(:value => add_me, :line => line_number, :param_name_id => db_param_name, :resource_id => db_values[0]['resource_id'] ) } end def to_s "%s => %s" % [self.name, self.value] end def compare(v,db_value) if (v.is_a?(Puppet::Parser::Resource::Reference)) return v.to_s == db_value.to_s else return v == db_value end end def values_to_remove(db_values) values = munge_for_rails(value) line_number = line_to_i() db_values.collect do |db| db unless (db['line'] == line_number && values.find { |v| compare(v,db['value']) } ) end.compact end def values_to_add(db_values) values = munge_for_rails(value) line_number = line_to_i() values.collect do |v| v unless db_values.find { |db| (compare(v,db['value']) && line_number == db['line']) } end.compact end end diff --git a/lib/puppet/parser/resource/reference.rb b/lib/puppet/parser/resource/reference.rb index a37f8400d..0c28cf0df 100644 --- a/lib/puppet/parser/resource/reference.rb +++ b/lib/puppet/parser/resource/reference.rb @@ -1,90 +1,93 @@ +# A reference to a resource. Mostly just the type and title. require 'puppet/resource/reference' +require 'puppet/file_collection/lookup' # A reference to a resource. Mostly just the type and title. class Puppet::Parser::Resource::Reference < Puppet::Resource::Reference + include Puppet::FileCollection::Lookup include Puppet::Util::MethodHelper include Puppet::Util::Errors attr_accessor :builtin, :file, :line, :scope # Are we a builtin type? def builtin? unless defined? @builtin if builtintype() @builtin = true else @builtin = false end end @builtin end def builtintype if t = Puppet::Type.type(self.type.downcase) and t.name != :component t else nil end end # Return the defined type for our obj. This can return classes, # definitions or nodes. def definedtype unless defined? @definedtype case self.type when "Class" # look for host classes if self.title == :main tmp = @scope.findclass("") else unless tmp = @scope.parser.classes[self.title] fail Puppet::ParseError, "Could not find class '%s'" % self.title end end when "Node" # look for node definitions unless tmp = @scope.parser.nodes[self.title] fail Puppet::ParseError, "Could not find node '%s'" % self.title end else # normal definitions # The resource type is capitalized, so we have to downcase. Really, # we should have a better interface for finding these, but eh. tmp = @scope.parser.definitions[self.type.downcase] end if tmp @definedtype = tmp else fail Puppet::ParseError, "Could not find resource type '%s'" % self.type end end @definedtype end def initialize(hash) set_options(hash) requiredopts(:type, :title) end def to_ref # We have to return different cases to provide backward compatibility # from 0.24.x to 0.23.x. if builtin? return [type.to_s.downcase, title.to_s] else return [type.to_s, title.to_s] end end def typeclass unless defined? @typeclass if tmp = builtintype || definedtype @typeclass = tmp else fail Puppet::ParseError, "Could not find type %s" % self.type end end @typeclass end end diff --git a/lib/puppet/property/list.rb b/lib/puppet/property/list.rb index 0c933f164..b7db8b42a 100644 --- a/lib/puppet/property/list.rb +++ b/lib/puppet/property/list.rb @@ -1,83 +1,87 @@ require 'puppet/property' module Puppet class Property class List < Property def should_to_s(should_value) #just return the should value should_value end def is_to_s(currentvalue) - currentvalue.join(delimiter) + if currentvalue == :absent + return "absent" + else + return currentvalue.join(delimiter) + end end def membership :membership end def add_should_with_current(should, current) if current.is_a?(Array) should += current end should.uniq end def inclusive? @resource[membership] == :inclusive end #dearrayify was motivated because to simplify the implementation of the OrderedList property def dearrayify(array) array.sort.join(delimiter) end def should unless defined? @should and @should return nil end members = @should #inclusive means we are managing everything so if it isn't in should, its gone if ! inclusive? members = add_should_with_current(members, retrieve) end dearrayify(members) end def delimiter "," end def retrieve #ok, some 'convention' if the list property is named groups, provider should implement a groups method if tmp = provider.send(name) and tmp != :absent return tmp.split(delimiter) else return :absent end end def prepare_is_for_comparison(is) if is.is_a? Array is = dearrayify(is) end is end def insync?(is) unless defined? @should and @should return true end unless is return true end return (prepare_is_for_comparison(is) == self.should) end end end end diff --git a/lib/puppet/provider/augeas/augeas.rb b/lib/puppet/provider/augeas/augeas.rb index efa8c2a3c..841bdc6c1 100644 --- a/lib/puppet/provider/augeas/augeas.rb +++ b/lib/puppet/provider/augeas/augeas.rb @@ -1,227 +1,290 @@ #-- # Copyright (C) 2008 Red Hat Inc. # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public # License as published by the Free Software Foundation; either # version 2 of the License, or (at your option) any later version. # # This library is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # General Public License for more details. # # You should have received a copy of the GNU General Public # License along with this library; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # # Author: Bryan Kearney require 'augeas' if Puppet.features.augeas? Puppet::Type.type(:augeas).provide(:augeas) do -#class Puppet::Provider::Augeas < Puppet::Provider include Puppet::Util confine :true => Puppet.features.augeas? has_features :parse_commands, :need_to_run?,:execute_changes + SAVE_NOOP = "noop" + SAVE_OVERWRITE = "overwrite" + + attr_accessor :aug + # Extracts an 2 dimensional array of commands which are in the # form of command path value. # The input can be # - A string with one command # - A string with many commands per line # - An array of strings. def parse_commands(data) commands = Array.new() if data.is_a?(String) data.each_line do |line| cmd_array = Array.new() single = line.index("'") double = line.index('"') tokens = nil delim = " " if ((single != nil) or (double != nil)) single = 99999 if single == nil double = 99999 if double == nil delim = '"' if double < single delim = "'" if single < double end tokens = line.split(delim) # If the length of tokens is 2, thn that means the pattern was # command file "some text", therefore we need to re-split # the first line if tokens.length == 2 tokens = (tokens[0].split(" ")) << tokens[1] end cmd = tokens.shift().strip() delim = "" if delim == " " file = tokens.shift().strip() other = tokens.join(" ").strip() cmd_array << cmd if !cmd.nil? cmd_array << file if !file.nil? cmd_array << other if other != "" commands << cmd_array end elsif data.is_a?(Array) data.each do |datum| commands.concat(parse_commands(datum)) end end return commands end + def open_augeas - flags = 0 - (flags = 1 << 2 ) if self.resource[:type_check] == :true - root = self.resource[:root] - load_path = self.resource[:load_path] - debug("Opening augeas with root #{root}, lens path #{load_path}, flags #{flags}") - Augeas.open(root, load_path,flags) + if (@aug.nil?) + flags = 0 + (flags = 1 << 2 ) if self.resource[:type_check] == :true + root = self.resource[:root] + load_path = self.resource[:load_path] + debug("Opening augeas with root #{root}, lens path #{load_path}, flags #{flags}") + @aug = Augeas.open(root, load_path,flags) + + if (self.get_augeas_version() >= "0.3.6") + debug("Augeas version #{self.get_augeas_version()} is installed") + end + end + @aug + end + + def close_augeas + if (!@aug.nil?) + @aug.close() + debug("Closed the augeas connection") + @aug = nil; + end end # Used by the need_to_run? method to process get filters. Returns # true if there is a match, false if otherwise # Assumes a syntax of get /files/path [COMPARATOR] value def process_get(cmd_array) return_value = false #validate and tear apart the command fail ("Invalid command: #{cmd_array.join(" ")}") if cmd_array.length < 4 cmd = cmd_array.shift() path = cmd_array.shift() comparator = cmd_array.shift() arg = cmd_array.join(" ") #check the value in augeas - aug = open_augeas() - result = aug.get(path) || '' + result = @aug.get(path) || '' unless result.nil? case comparator when "!=" return_value = true if !(result == arg) when "=~" regex = Regexp.new(arg) loc = result=~ regex return_value = true if ! loc.nil? else return_value = true if (result.send(comparator, arg)) end end return_value end # Used by the need_to_run? method to process match filters. Returns # true if there is a match, false if otherwise def process_match(cmd_array) return_value = false #validate and tear apart the command fail("Invalid command: #{cmd_array.join(" ")}") if cmd_array.length < 4 cmd = cmd_array.shift() path = cmd_array.shift() verb = cmd_array.shift() #Get the values from augeas - aug = open_augeas() - result = aug.match(path) || '' + result = @aug.match(path) || '' # Now do the work - if (!result.nil?) + unless (result.nil?) case verb when "size" fail("Invalid command: #{cmd_array.join(" ")}") if cmd_array.length != 2 comparator = cmd_array.shift() arg = cmd_array.shift().to_i return_value = true if (result.size.send(comparator, arg)) when "include" arg = cmd_array.join(" ") return_value = true if result.include?(arg) when "==" begin arg = cmd_array.join(" ") new_array = eval arg return_value = true if result == new_array rescue fail("Invalid array in command: #{cmd_array.join(" ")}") end end end return_value end + def get_augeas_version + return @aug.get("/augeas/version") || "" + end + + def set_augeas_save_mode(mode) + return @aug.set("/augeas/save", mode) + end + + def files_changed? + saved_files = @aug.match("/augeas/events/saved") + return saved_files.size() > 0 + end + # Determines if augeas acutally needs to run. def need_to_run? + force = resource[:force] return_value = true + self.open_augeas() filter = resource[:onlyif] unless (filter == "") cmd_array = filter.split command = cmd_array[0]; cmd_array[1]= File.join(resource[:context], cmd_array[1]) begin data = nil case command when "get"; return_value = process_get(cmd_array) when "match"; return_value = process_match(cmd_array) end rescue Exception => e fail("Error sending command '#{command}' with params #{cmd_array[1..-1].inspect}/#{e.message}") end end - return_value + + unless (force) + # If we have a verison of augeas which is at least 0.3.6 then we + # can make the changes now, see if changes were made, and + # actually do the save. + if ((return_value) and (self.get_augeas_version() >= "0.3.6")) + debug("Will attempt to save and only run if files changed") + self.set_augeas_save_mode(SAVE_NOOP) + self.do_execute_changes() + save_result = @aug.save() + saved_files = @aug.match("/augeas/events/saved") + if ((save_result) and (not files_changed?)) + debug("Skipping becuase no files were changed") + return_value = false + else + debug("Files changed, should execute") + end + end + end + self.close_augeas() + return return_value end - # Actually execute the augeas changes. def execute_changes - aug = open_augeas - commands = resource[:changes] + # Re-connect to augeas, and re-execute the changes + self.open_augeas() + if (self.get_augeas_version() >= "0.3.6") + self.set_augeas_save_mode(SAVE_OVERWRITE) + end + + self.do_execute_changes() + + success = @aug.save() + if (success != true) + fail("Save failed with return code #{success}") + end + self.close_augeas() + + return :executed + end + + # Actually execute the augeas changes. + def do_execute_changes + commands = resource[:changes].clone() context = resource[:context] commands.each do |cmd_array| + cmd_array = cmd_array.clone() fail("invalid command #{cmd_array.join[" "]}") if cmd_array.length < 2 command = cmd_array[0] cmd_array.shift() begin case command when "set" cmd_array[0]=File.join(context, cmd_array[0]) debug("sending command '#{command}' with params #{cmd_array.inspect}") aug.set(cmd_array[0], cmd_array[1]) when "rm", "remove" cmd_array[0]=File.join(context, cmd_array[0]) debug("sending command '#{command}' with params #{cmd_array.inspect}") aug.rm(cmd_array[0]) when "clear" cmd_array[0]=File.join(context, cmd_array[0]) - debug("sending command '#{command}' with params #{cmd_array.inspect}") - aug.clear(cmd_array[0]) + debug("sending command '#{command}' with params #{cmd_array.inspect}") + @aug.clear(cmd_array[0]) when "insert", "ins" - ext_array = cmd_array[1].split(" ") ; if cmd_array.size < 2 or ext_array.size < 2 fail("ins requires 3 parameters") end label = cmd_array[0] where = ext_array[0] - path = File.join(context, ext_array[1]) + path = File.join(context, ext_array[1]) case where when "before"; before = true when "after"; before = false else fail("Invalid value '#{where}' for where param") end - debug("sending command '#{command}' with params #{[label, where, path].inspect()}") + debug("sending command '#{command}' with params #{[label, where, path].inspect()}") aug.insert(path, label, before) else fail("Command '#{command}' is not supported") end rescue Exception => e fail("Error sending command '#{command}' with params #{cmd_array.inspect}/#{e.message}") end end - success = aug.save() - if (success != true) - fail("Save failed with return code #{success}") - end - - return :executed end - end diff --git a/lib/puppet/provider/nameservice/directoryservice.rb b/lib/puppet/provider/nameservice/directoryservice.rb index 07b01bbe0..cd1e442d5 100644 --- a/lib/puppet/provider/nameservice/directoryservice.rb +++ b/lib/puppet/provider/nameservice/directoryservice.rb @@ -1,536 +1,536 @@ # Created by Jeff McCune on 2007-07-22 # Copyright (c) 2007. All rights reserved. # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License # as published by the Free Software Foundation (version 2 of the License) # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston MA 02110-1301 USA require 'puppet' require 'puppet/provider/nameservice' require 'facter/util/plist' require 'cgi' class Puppet::Provider::NameService class DirectoryService < Puppet::Provider::NameService # JJM: Dive into the eigenclass class << self # JJM: This allows us to pass information when calling # Puppet::Type.type # e.g. Puppet::Type.type(:user).provide :directoryservice, :ds_path => "Users" # This is referenced in the get_ds_path class method attr_writer :ds_path attr_writer :macosx_version_major end # JJM 2007-07-24: Not yet sure what initvars() does. I saw it in netinfo.rb # I do know, however, that it makes methods "work" =) # e.g. addcmd isn't available if this method call isn't present. # # JJM: Also, where this method is defined seems to impact the visibility # of methods. If I put initvars after commands, confine and defaultfor, # then getinfo is called from the parent class, not this class. initvars() commands :dscl => "/usr/bin/dscl" commands :dseditgroup => "/usr/sbin/dseditgroup" commands :sw_vers => "/usr/bin/sw_vers" confine :operatingsystem => :darwin defaultfor :operatingsystem => :darwin # JJM 2007-07-25: This map is used to map NameService attributes to their # corresponding DirectoryService attribute names. # See: http://images.apple.com/server/docs/Open_Directory_v10.4.pdf # JJM: Note, this is de-coupled from the Puppet::Type, and must # be actively maintained. There may also be collisions with different # types (Users, Groups, Mounts, Hosts, etc...) @@ds_to_ns_attribute_map = { 'RecordName' => :name, 'PrimaryGroupID' => :gid, 'NFSHomeDirectory' => :home, 'UserShell' => :shell, 'UniqueID' => :uid, 'RealName' => :comment, 'Password' => :password, 'GeneratedUID' => :guid, 'IPAddress' => :ip_address, 'ENetAddress' => :en_address, 'GroupMembership' => :members, } # JJM The same table as above, inverted. @@ns_to_ds_attribute_map = { :name => 'RecordName', :gid => 'PrimaryGroupID', :home => 'NFSHomeDirectory', :shell => 'UserShell', :uid => 'UniqueID', :comment => 'RealName', :password => 'Password', :guid => 'GeneratedUID', :en_address => 'ENetAddress', :ip_address => 'IPAddress', :members => 'GroupMembership', } @@password_hash_dir = "/var/db/shadow/hash" def self.instances # JJM Class method that provides an array of instance objects of this # type. # JJM: Properties are dependent on the Puppet::Type we're managine. type_property_array = [:name] + @resource_type.validproperties # Create a new instance of this Puppet::Type for each object present # on the system. list_all_present.collect do |name_string| self.new(single_report(name_string, *type_property_array)) end end def self.get_ds_path # JJM: 2007-07-24 This method dynamically returns the DS path we're concerned with. # For example, if we're working with an user type, this will be /Users # with a group type, this will be /Groups. # @ds_path is an attribute of the class itself. if defined? @ds_path return @ds_path end # JJM: "Users" or "Groups" etc ... (Based on the Puppet::Type) # Remember this is a class method, so self.class is Class # Also, @resource_type seems to be the reference to the # Puppet::Type this class object is providing for. return @resource_type.name.to_s.capitalize + "s" end def self.get_macosx_version_major if defined? @macosx_version_major return @macosx_version_major end begin product_version = Facter.value(:macosx_productversion) if product_version.nil? raise Puppet::Error, "Could not determine OS X version from Facter" end product_version_major = product_version.scan(/(\d+)\.(\d+)./).join(".") if %w{10.0 10.1 10.2 10.3}.include?(product_version_major) raise Puppet::Error, "%s is not supported by the directoryservice provider" % product_version_major end @macosx_version_major = product_version_major return @macosx_version_major rescue Puppet::ExecutionFailure => detail raise Puppet::Error, "Could not determine OS X version: %s" % detail end end def self.list_all_present # JJM: List all objects of this Puppet::Type already present on the system. begin dscl_output = execute(get_exec_preamble("-list")) rescue Puppet::ExecutionFailure => detail raise Puppet::Error, "Could not get %s list from DirectoryService" % [ @resource_type.name.to_s ] end return dscl_output.split("\n") end def self.parse_dscl_url_data(dscl_output) # we need to construct a Hash from the dscl -url output to match # that returned by the dscl -plist output for 10.5+ clients. # # Nasty assumptions: # a) no values *end* in a colon ':', only keys # b) if a line ends in a colon and the next line does start with # a space, then the second line is a value of the first. # c) (implied by (b)) keys don't start with spaces. dscl_plist = {} dscl_output.split("\n").inject([]) do |array, line| if line =~ /^\s+/ # it's a value array[-1] << line # add the value to the previous key else array << line end array end.compact dscl_output.each do |line| # This should be a 'normal' entry. key and value on one line. # We split on ': ' to deal with keys/values with a colon in them. split_array = line.split(/:\s+/) key = split_array.first value = CGI::unescape(split_array.last.strip.chomp) # We need to treat GroupMembership separately as it is currently # the only attribute we care about multiple values for, and # the values can never contain spaces (shortnames) # We also make every value an array to be consistent with the # output of dscl -plist under 10.5 if key == "GroupMembership" dscl_plist[key] = value.split(/\s/) else dscl_plist[key] = [value] end end return dscl_plist end def self.parse_dscl_plist_data(dscl_output) return Plist.parse_xml(dscl_output) end def self.generate_attribute_hash(input_hash, *type_properties) attribute_hash = {} input_hash.keys().each do |key| ds_attribute = key.sub("dsAttrTypeStandard:", "") next unless (@@ds_to_ns_attribute_map.keys.include?(ds_attribute) and type_properties.include? @@ds_to_ns_attribute_map[ds_attribute]) ds_value = input_hash[key] case @@ds_to_ns_attribute_map[ds_attribute] when :members ds_value = ds_value # only members uses arrays so far when :gid, :uid # OS X stores objects like uid/gid as strings. # Try casting to an integer for these cases to be # consistent with the other providers and the group type # validation begin ds_value = Integer(ds_value[0]) rescue ArgumentError ds_value = ds_value[0] end else ds_value = ds_value[0] end attribute_hash[@@ds_to_ns_attribute_map[ds_attribute]] = ds_value end # NBK: need to read the existing password here as it's not actually # stored in the user record. It is stored at a path that involves the # UUID of the user record for non-Mobile local acccounts. # Mobile Accounts are out of scope for this provider for now if @resource_type.validproperties.include?(:password) attribute_hash[:password] = self.get_password(attribute_hash[:guid]) end return attribute_hash end def self.single_report(resource_name, *type_properties) # JJM 2007-07-24: # Given a the name of an object and a list of properties of that # object, return all property values in a hash. # # This class method returns nil if the object doesn't exist # Otherwise, it returns a hash of the object properties. all_present_str_array = list_all_present() # NBK: shortcut the process if the resource is missing return nil unless all_present_str_array.include? resource_name dscl_vector = get_exec_preamble("-read", resource_name) begin dscl_output = execute(dscl_vector) rescue Puppet::ExecutionFailure => detail raise Puppet::Error, "Could not get report. command execution failed." end # Two code paths is ugly, but until we can drop 10.4 support we don't # have a lot of choice. Ultimately this should all be done using Ruby # to access the DirectoryService APIs directly, but that's simply not # feasible for a while yet. case self.get_macosx_version_major when "10.4" dscl_plist = self.parse_dscl_url_data(dscl_output) when "10.5", "10.6" dscl_plist = self.parse_dscl_plist_data(dscl_output) end return self.generate_attribute_hash(dscl_plist, *type_properties) end def self.get_exec_preamble(ds_action, resource_name = nil) # JJM 2007-07-24 # DSCL commands are often repetitive and contain the same positional # arguments over and over. See http://developer.apple.com/documentation/Porting/Conceptual/PortingUnix/additionalfeatures/chapter_10_section_9.html # for an example of what I mean. # This method spits out proper DSCL commands for us. # We EXPECT name to be @resource[:name] when called from an instance object. # 10.4 doesn't support the -plist option for dscl, and 10.5 has a # different format for the -url output with objects with spaces in # their values. *sigh*. Use -url for 10.4 in the hope this can be # deprecated one day, and use -plist for 10.5 and higher. case self.get_macosx_version_major when "10.4" command_vector = [ command(:dscl), "-url", "." ] when "10.5", "10.6" command_vector = [ command(:dscl), "-plist", "." ] end # JJM: The actual action to perform. See "man dscl" # Common actiosn: -create, -delete, -merge, -append, -passwd command_vector << ds_action # JJM: get_ds_path will spit back "Users" or "Groups", # etc... Depending on the Puppet::Type of our self. if resource_name command_vector << "/%s/%s" % [ get_ds_path, resource_name ] else command_vector << "/%s" % [ get_ds_path ] end # JJM: This returns most of the preamble of the command. # e.g. 'dscl / -create /Users/mccune' return command_vector end def self.set_password(resource_name, guid, password_hash) password_hash_file = "#{@@password_hash_dir}/#{guid}" begin File.open(password_hash_file, 'w') { |f| f.write(password_hash)} rescue Errno::EACCES => detail raise Puppet::Error, "Could not write to password hash file: #{detail}" end # NBK: For shadow hashes, the user AuthenticationAuthority must contain a value of # ";ShadowHash;". The LKDC in 10.5 makes this more interesting though as it # will dynamically generate ;Kerberosv5;;username@LKDC:SHA1 attributes if # missing. Thus we make sure we only set ;ShadowHash; if it is missing, and # we can do this with the merge command. This allows people to continue to # use other custom AuthenticationAuthority attributes without stomping on them. # # There is a potential problem here in that we're only doing this when setting # the password, and the attribute could get modified at other times while the # hash doesn't change and so this doesn't get called at all... but # without switching all the other attributes to merge instead of create I can't # see a simple enough solution for this that doesn't modify the user record # every single time. This should be a rather rare edge case. (famous last words) dscl_vector = self.get_exec_preamble("-merge", resource_name) dscl_vector << "AuthenticationAuthority" << ";ShadowHash;" begin dscl_output = execute(dscl_vector) rescue Puppet::ExecutionFailure => detail raise Puppet::Error, "Could not set AuthenticationAuthority." end end def self.get_password(guid) password_hash = nil password_hash_file = "#{@@password_hash_dir}/#{guid}" - if File.exists?(password_hash_file) + if File.exists?(password_hash_file) and File.file?(password_hash_file) if not File.readable?(password_hash_file) raise Puppet::Error("Could not read password hash file at #{password_hash_file} for #{@resource[:name]}") end f = File.new(password_hash_file) password_hash = f.read f.close - end + end password_hash end def ensure=(ensure_value) super # JJM: Modeled after nameservice/netinfo.rb, we need to # loop over all valid properties for the type we're managing # and call the method which sets that property value # Like netinfo, dscl can't create everything at once, afaik. if ensure_value == :present @resource.class.validproperties.each do |name| next if name == :ensure # LAK: We use property.sync here rather than directly calling # the settor method because the properties might do some kind # of conversion. In particular, the user gid property might # have a string and need to convert it to a number if @resource.should(name) @resource.property(name).sync elsif value = autogen(name) self.send(name.to_s + "=", value) else next end end end end def password=(passphrase) exec_arg_vector = self.class.get_exec_preamble("-read", @resource.name) exec_arg_vector << @@ns_to_ds_attribute_map[:guid] begin guid_output = execute(exec_arg_vector) guid_plist = Plist.parse_xml(guid_output) # Although GeneratedUID like all DirectoryService values can be multi-valued # according to the schema, in practice user accounts cannot have multiple UUIDs # otherwise Bad Things Happen, so we just deal with the first value. guid = guid_plist["dsAttrTypeStandard:#{@@ns_to_ds_attribute_map[:guid]}"][0] self.class.set_password(@resource.name, guid, passphrase) rescue Puppet::ExecutionFailure => detail raise Puppet::Error, "Could not set %s on %s[%s]: %s" % [param, @resource.class.name, @resource.name, detail] end end # NBK: we override @parent.set as we need to execute a series of commands # to deal with array values, rather than the single command nameservice.rb # expects to be returned by modifycmd. Thus we don't bother defining modifycmd. def set(param, value) self.class.validate(param, value) current_members = @property_value_cache_hash[:members] if param == :members # If we are meant to be authoritative for the group membership # then remove all existing members who haven't been specified # in the manifest. if @resource[:auth_membership] and not current_members.nil? remove_unwanted_members(current_members, value) end # if they're not a member, make them one. add_members(current_members, value) else exec_arg_vector = self.class.get_exec_preamble("-create", @resource[:name]) # JJM: The following line just maps the NS name to the DS name # e.g. { :uid => 'UniqueID' } exec_arg_vector << @@ns_to_ds_attribute_map[symbolize(param)] # JJM: The following line sends the actual value to set the property to exec_arg_vector << value.to_s begin execute(exec_arg_vector) rescue Puppet::ExecutionFailure => detail raise Puppet::Error, "Could not set %s on %s[%s]: %s" % [param, @resource.class.name, @resource.name, detail] end end end # NBK: we override @parent.create as we need to execute a series of commands # to create objects with dscl, rather than the single command nameservice.rb # expects to be returned by addcmd. Thus we don't bother defining addcmd. def create if exists? info "already exists" return nil end # NBK: First we create the object with a known guid so we can set the contents # of the password hash if required # Shelling out sucks, but for a single use case it doesn't seem worth # requiring people install a UUID library that doesn't come with the system. # This should be revisited if Puppet starts managing UUIDs for other platform # user records. guid = %x{/usr/bin/uuidgen}.chomp exec_arg_vector = self.class.get_exec_preamble("-create", @resource[:name]) exec_arg_vector << @@ns_to_ds_attribute_map[:guid] << guid begin execute(exec_arg_vector) rescue Puppet::ExecutionFailure => detail raise Puppet::Error, "Could not set GeneratedUID for %s %s: %s" % [@resource.class.name, @resource.name, detail] end if value = @resource.should(:password) and value != "" self.class.set_password(@resource[:name], guid, value) end # Now we create all the standard properties Puppet::Type.type(@resource.class.name).validproperties.each do |property| next if property == :ensure if value = @resource.should(property) and value != "" if property == :members add_members(nil, value) else exec_arg_vector = self.class.get_exec_preamble("-create", @resource[:name]) exec_arg_vector << @@ns_to_ds_attribute_map[symbolize(property)] next if property == :password # skip setting the password here exec_arg_vector << value.to_s begin execute(exec_arg_vector) rescue Puppet::ExecutionFailure => detail raise Puppet::Error, "Could not create %s %s: %s" % [@resource.class.name, @resource.name, detail] end end end end end def remove_unwanted_members(current_members, new_members) current_members.each do |member| if not new_members.include?(member) cmd = [:dseditgroup, "-o", "edit", "-n", ".", "-d", member, @resource[:name]] begin execute(cmd) rescue Puppet::ExecutionFailure => detail raise Puppet::Error, "Could not remove %s from group: %s, %s" % [member, @resource.name, detail] end end end end def add_members(current_members, new_members) new_members.each do |new_member| if current_members.nil? or not current_members.include?(new_member) cmd = [:dseditgroup, "-o", "edit", "-n", ".", "-a", new_member, @resource[:name]] begin execute(cmd) rescue Puppet::ExecutionFailure => detail raise Puppet::Error, "Could not add %s to group: %s, %s" % [new_member, @resource.name, detail] end end end end def deletecmd # JJM: Like addcmd, only called when deleting the object itself # Note, this isn't used to delete properties of the object, # at least that's how I understand it... self.class.get_exec_preamble("-delete", @resource[:name]) end def getinfo(refresh = false) # JJM 2007-07-24: # Override the getinfo method, which is also defined in nameservice.rb # This method returns and sets @infohash, which looks like: # (NetInfo provider, user type...) # @infohash = {:comment=>"Jeff McCune", :home=>"/Users/mccune", # :shell=>"/bin/zsh", :password=>"********", :uid=>502, :gid=>502, # :name=>"mccune"} # # I'm not re-factoring the name "getinfo" because this method will be # most likely called by nameservice.rb, which I didn't write. if refresh or (! defined?(@property_value_cache_hash) or ! @property_value_cache_hash) # JJM 2007-07-24: OK, there's a bit of magic that's about to # happen... Let's see how strong my grip has become... =) # # self is a provider instance of some Puppet::Type, like # Puppet::Type::User::ProviderDirectoryservice for the case of the # user type and this provider. # # self.class looks like "user provider directoryservice", if that # helps you ... # # self.class.resource_type is a reference to the Puppet::Type class, # probably Puppet::Type::User or Puppet::Type::Group, etc... # # self.class.resource_type.validproperties is a class method, # returning an Array of the valid properties of that specific # Puppet::Type. # # So... something like [:comment, :home, :password, :shell, :uid, # :groups, :ensure, :gid] # # Ultimately, we add :name to the list, delete :ensure from the # list, then report on the remaining list. Pretty whacky, ehh? type_properties = [:name] + self.class.resource_type.validproperties type_properties.delete(:ensure) if type_properties.include? :ensure type_properties << :guid # append GeneratedUID so we just get the report here @property_value_cache_hash = self.class.single_report(@resource[:name], *type_properties) [:uid, :gid].each do |param| @property_value_cache_hash[param] = @property_value_cache_hash[param].to_i if @property_value_cache_hash and @property_value_cache_hash.include?(param) end end return @property_value_cache_hash end end end diff --git a/lib/puppet/provider/service/gentoo.rb b/lib/puppet/provider/service/gentoo.rb index 4067dee5e..d62df1a38 100644 --- a/lib/puppet/provider/service/gentoo.rb +++ b/lib/puppet/provider/service/gentoo.rb @@ -1,58 +1,58 @@ # Manage gentoo services. Start/stop is the same as InitSvc, but enable/disable # is special. Puppet::Type.type(:service).provide :gentoo, :parent => :init do desc "Gentoo's form of ``init``-style service management. Uses ``rc-update`` for service enabling and disabling. " commands :update => "/sbin/rc-update" confine :operatingsystem => :gentoo defaultfor :operatingsystem => :gentoo def self.defpath superclass.defpath end def disable begin output = update :del, @resource[:name], :default rescue Puppet::ExecutionFailure raise Puppet::Error, "Could not disable %s: %s" % [self.name, output] end end def enabled? begin output = update :show rescue Puppet::ExecutionFailure return :false end line = output.split(/\n/).find { |l| l.include?(@resource[:name]) } return :false unless line # If it's enabled then it will print output showing service | runlevel - if output =~ /#{@resource[:name]}\s*\|\s*default/ + if output =~ /#{@resource[:name]}\s*\|\s*(boot|default)/ return :true else return :false end end def enable begin output = update :add, @resource[:name], :default rescue Puppet::ExecutionFailure raise Puppet::Error, "Could not enable %s: %s" % [self.name, output] end end end # $Id $ diff --git a/lib/puppet/provider/service/redhat.rb b/lib/puppet/provider/service/redhat.rb index 031db46c1..f31903e84 100755 --- a/lib/puppet/provider/service/redhat.rb +++ b/lib/puppet/provider/service/redhat.rb @@ -1,85 +1,85 @@ # Manage Red Hat services. Start/stop uses /sbin/service and enable/disable uses chkconfig Puppet::Type.type(:service).provide :redhat, :parent => :init do desc "Red Hat's (and probably many others) form of ``init``-style service management: Uses ``chkconfig`` for service enabling and disabling. " commands :chkconfig => "/sbin/chkconfig", :service => "/sbin/service" defaultfor :operatingsystem => [:redhat, :fedora, :suse, :centos, :sles, :oel, :ovm] def self.defpath superclass.defpath end # Remove the symlinks def disable begin output = chkconfig(@resource[:name], :off) rescue Puppet::ExecutionFailure raise Puppet::Error, "Could not disable %s: %s" % [self.name, output] end end def enabled? begin output = chkconfig(@resource[:name]) rescue Puppet::ExecutionFailure return :false end # If it's disabled on SuSE, then it will print output showing "off" # at the end if output =~ /.* off$/ return :false end return :true end # Don't support them specifying runlevels; always use the runlevels # in the init scripts. def enable begin output = chkconfig(@resource[:name], :on) rescue Puppet::ExecutionFailure => detail raise Puppet::Error, "Could not enable %s: %s" % [self.name, detail] end end def restart if @resource[:hasrestart] == :true service(@resource[:name], "restart") else super end end def status if @resource[:hasstatus] == :true begin service(@resource[:name], "status") return :running rescue return :stopped end else super end end - def start - service(@resource[:name], "start") + def startcmd + [command(:service), @resource[:name], "start"] end - def stop - service(@resource[:name], "stop") + def stopcmd + [command(:service), @resource[:name], "stop"] end end diff --git a/lib/puppet/provider/ssh_authorized_key/parsed.rb b/lib/puppet/provider/ssh_authorized_key/parsed.rb index 5604ba32a..6df7f8ae0 100644 --- a/lib/puppet/provider/ssh_authorized_key/parsed.rb +++ b/lib/puppet/provider/ssh_authorized_key/parsed.rb @@ -1,135 +1,156 @@ require 'puppet/provider/parsedfile' Puppet::Type.type(:ssh_authorized_key).provide(:parsed, :parent => Puppet::Provider::ParsedFile, :filetype => :flat, :default_target => '' ) do desc "Parse and generate authorized_keys files for SSH." text_line :comment, :match => /^#/ text_line :blank, :match => /^\s+/ record_line :parsed, :fields => %w{options type key name}, :optional => %w{options}, :rts => /^\s+/, :match => /^(?:(.+) )?(ssh-dss|ssh-rsa) ([^ ]+)(?: (.+))?$/, :post_parse => proc { |record| if record[:options].nil? record[:options] = [:absent] else record[:options] = Puppet::Type::Ssh_authorized_key::ProviderParsed.parse_options(record[:options]) end }, :pre_gen => proc { |record| if record[:options].include?(:absent) record[:options] = "" else record[:options] = record[:options].join(',') end } record_line :key_v1, :fields => %w{options bits exponent modulus name}, :optional => %w{options}, :rts => /^\s+/, :match => /^(?:(.+) )?(\d+) (\d+) (\d+)(?: (.+))?$/ - def prefetch - # This was done in the type class but path expansion was failing for - # not yet existing users, the only workaround I found was to move that - # in the provider. - @resource[:target] = target + def target + @resource.should(:target) + end - super + def user + @resource.should(:user) + end + + def dir_perm + # Determine correct permission for created directory and file + # we can afford more restrictive permissions when the user is known + if target + if user + 0700 + else + 0755 + end + end + end + + def file_perm + if target + if user + 0600 + else + 0644 + end + end end def target if user File.expand_path("~%s/.ssh/authorized_keys" % user) elsif target = @resource.should(:target) target end end def user @resource.should(:user) end def dir_perm # Determine correct permission for created directory and file # we can afford more restrictive permissions when the user is known if target if user 0700 else 0755 end end end def file_perm if target if user 0600 else 0644 end end end def flush # As path expansion had to be moved in the provider, we cannot generate new file # resources and thus have to chown and chmod here. It smells hackish. # Create target's parent directory if nonexistant if target dir = File.dirname(target) if not File.exist? dir Puppet.debug("Creating directory %s which did not exist" % dir) Dir.mkdir(dir, dir_perm) end end # Generate the file super # Ensure correct permissions if target and user uid = Puppet::Util.uid(user) if uid File.chown(uid, nil, dir) File.chown(uid, nil, target) else raise Puppet::Error, "Specified user does not exist" end end if target File.chmod(file_perm, target) end end # parse sshv2 option strings, wich is a comma separated list of # either key="values" elements or bare-word elements def self.parse_options(options) result = [] scanner = StringScanner.new(options) while !scanner.eos? scanner.skip(/[ \t]*/) # scan a long option if out = scanner.scan(/[-a-z0-9A-Z_]+=\".*?\"/) or out = scanner.scan(/[-a-z0-9A-Z_]+/) result << out else # found an unscannable token, let's abort break end # eat a comma scanner.skip(/[ \t]*,[ \t]*/) end result end end diff --git a/lib/puppet/provider/user/useradd.rb b/lib/puppet/provider/user/useradd.rb index 6996dd69a..15b3b1379 100644 --- a/lib/puppet/provider/user/useradd.rb +++ b/lib/puppet/provider/user/useradd.rb @@ -1,70 +1,70 @@ require 'puppet/provider/nameservice/objectadd' Puppet::Type.type(:user).provide :useradd, :parent => Puppet::Provider::NameService::ObjectAdd do desc "User management via ``useradd`` and its ilk. Note that you will need to install the ``Shadow Password`` Ruby library often known as ruby-libshadow to manage user passwords." commands :add => "useradd", :delete => "userdel", :modify => "usermod" options :home, :flag => "-d", :method => :dir options :comment, :method => :gecos options :groups, :flag => "-G" verify :gid, "GID must be an integer" do |value| value.is_a? Integer end verify :groups, "Groups must be comma-separated" do |value| value !~ /\s/ end has_features :manages_homedir, :allows_duplicates if Puppet.features.libshadow? has_feature :manages_passwords end def check_allow_dup @resource.allowdupe? ? ["-o"] : [] end def check_manage_home cmd = [] if @resource.managehome? cmd << "-m" - elsif %w{Fedora RedHat}.include?(Facter.value("operatingsystem")) + elsif %w{Fedora RedHat CentOS OEL OVS}.include?(Facter.value("operatingsystem")) cmd << "-M" end cmd end def add_properties cmd = [] Puppet::Type.type(:user).validproperties.each do |property| next if property == :ensure # the value needs to be quoted, mostly because -c might # have spaces in it if value = @resource.should(property) and value != "" cmd << flag(property) << value end end cmd end def addcmd cmd = [command(:add)] cmd += add_properties cmd += check_allow_dup cmd += check_manage_home cmd << @resource[:name] end # Retrieve the password using the Shadow Password library def password if ent = Shadow::Passwd.getspnam(@resource.name) return ent.sp_pwdp else return :absent end end end diff --git a/lib/puppet/rails/host.rb b/lib/puppet/rails/host.rb index a429cbfb1..851cc21d9 100644 --- a/lib/puppet/rails/host.rb +++ b/lib/puppet/rails/host.rb @@ -1,194 +1,206 @@ require 'puppet/rails/resource' require 'puppet/rails/fact_name' require 'puppet/rails/source_file' require 'puppet/util/rails/collection_merger' -Puppet::TIME_DEBUG = true - class Puppet::Rails::Host < ActiveRecord::Base include Puppet::Util include Puppet::Util::CollectionMerger has_many :fact_values, :dependent => :destroy has_many :fact_names, :through => :fact_values belongs_to :source_file has_many :resources, :dependent => :destroy # If the host already exists, get rid of its objects def self.clean(host) if obj = self.find_by_name(host) obj.rails_objects.clear return obj else return nil end end # Store our host in the database. def self.store(node, resources) args = {} host = nil transaction do #unless host = find_by_name(name) seconds = Benchmark.realtime { unless host = find_by_name(node.name) host = new(:name => node.name) end } - Puppet.notice("Searched for host in %0.2f seconds" % seconds) if defined?(Puppet::TIME_DEBUG) + Puppet.debug("Searched for host in %0.2f seconds" % seconds) if ip = node.parameters["ipaddress"] host.ip = ip end if env = node.environment host.environment = env end # Store the facts into the database. host.setfacts node.parameters seconds = Benchmark.realtime { host.setresources(resources) } - Puppet.notice("Handled resources in %0.2f seconds" % seconds) if defined?(Puppet::TIME_DEBUG) + Puppet.debug("Handled resources in %0.2f seconds" % seconds) host.last_compile = Time.now host.save end return host end # Return the value of a fact. def fact(name) if fv = self.fact_values.find(:all, :include => :fact_name, :conditions => "fact_names.name = '#{name}'") return fv else return nil end end # returns a hash of fact_names.name => [ fact_values ] for this host. def get_facts_hash fact_values = self.fact_values.find(:all, :include => :fact_name) return fact_values.inject({}) do | hash, value | hash[value.fact_name.name] ||= [] hash[value.fact_name.name] << value hash end end def setfacts(facts) facts = facts.dup ar_hash_merge(get_facts_hash(), facts, :create => Proc.new { |name, values| fact_name = Puppet::Rails::FactName.find_or_create_by_name(name) values = [values] unless values.is_a?(Array) values.each do |value| fact_values.build(:value => value, :fact_name => fact_name) end }, :delete => Proc.new { |values| values.each { |value| self.fact_values.delete(value) } }, :modify => Proc.new { |db, mem| mem = [mem].flatten fact_name = db[0].fact_name db_values = db.collect { |fact_value| fact_value.value } (db_values - (db_values & mem)).each do |value| db.find_all { |fact_value| fact_value.value == value }.each { |fact_value| fact_values.delete(fact_value) } end (mem - (db_values & mem)).each do |value| fact_values.build(:value => value, :fact_name => fact_name) end }) end # Set our resources. def setresources(list) - existing = nil + resource_by_id = nil seconds = Benchmark.realtime { - existing = find_resources() + resource_by_id = find_resources() } - Puppet.notice("Searched for resources in %0.2f seconds" % seconds) if defined?(Puppet::TIME_DEBUG) + Puppet.debug("Searched for resources in %0.2f seconds" % seconds) seconds = Benchmark.realtime { - find_resources_parameters_tags(existing) + find_resources_parameters_tags(resource_by_id) } if id - Puppet.notice("Searched for resource params and tags in %0.2f seconds" % seconds) if defined?(Puppet::TIME_DEBUG) + Puppet.debug("Searched for resource params and tags in %0.2f seconds" % seconds) seconds = Benchmark.realtime { - compare_to_catalog(existing, list) + compare_to_catalog(resource_by_id, list) } - Puppet.notice("Resource comparison took %0.2f seconds" % seconds) if defined?(Puppet::TIME_DEBUG) + Puppet.debug("Resource comparison took %0.2f seconds" % seconds) end def find_resources resources.find(:all, :include => :source_file).inject({}) do | hash, resource | - hash[resource.ref] = resource + hash[resource.id] = resource hash end end def find_resources_parameters_tags(resources) # initialize all resource parameters resources.each do |key,resource| resource.params_hash = [] end - resources_by_id = resources.inject({}) do |hash, res| - hash[res[1]['id']] = res[1] + find_resources_parameters(resources) + find_resources_tags(resources) + end + + # it seems that it can happen (see bug #2010) some resources are duplicated in the + # database (ie logically corrupted database), in which case we remove the extraneous + # entries. + def compare_to_catalog(existing, list) + extra_db_resources = [] + resources = existing.inject({}) do |hash, res| + resource = res[1] + if hash.include?(resource.ref) + extra_db_resources << hash[resource.ref] + end + hash[resource.ref] = resource hash end - find_resources_parameters(resources_by_id) - find_resources_tags(resources_by_id) - end - - def compare_to_catalog(resources, list) compiled = list.inject({}) do |hash, resource| hash[resource.ref] = resource hash end + ar_hash_merge(resources, compiled, :create => Proc.new { |ref, resource| resource.to_rails(self) }, :delete => Proc.new { |resource| self.resources.delete(resource) }, :modify => Proc.new { |db, mem| mem.modify_rails(db) }) + + # fix-up extraneous resources + extra_db_resources.each do |resource| + self.resources.delete(resource) + end end def find_resources_parameters(resources) params = Puppet::Rails::ParamValue.find_all_params_from_host(self) # assign each loaded parameters/tags to the resource it belongs to params.each do |param| - resources[param['resource_id']].add_param_to_hash(param) + resources[param['resource_id']].add_param_to_hash(param) if resources.include?(param['resource_id']) end end def find_resources_tags(resources) tags = Puppet::Rails::ResourceTag.find_all_tags_from_host(self) tags.each do |tag| - resources[tag['resource_id']].add_tag_to_hash(tag) + resources[tag['resource_id']].add_tag_to_hash(tag) if resources.include?(tag['resource_id']) end end def update_connect_time self.last_connect = Time.now save end end diff --git a/lib/puppet/reference/report.rb b/lib/puppet/reference/report.rb index be8e64751..2ea00b6d6 100644 --- a/lib/puppet/reference/report.rb +++ b/lib/puppet/reference/report.rb @@ -1,23 +1,24 @@ require 'puppet/reports' report = Puppet::Util::Reference.newreference :report, :doc => "All available transaction reports" do Puppet::Reports.reportdocs end report.header = " Puppet clients can report back to the server after each transaction. This transaction report is sent as a YAML dump of the ``Puppet::Transaction::Report`` class and includes every log message that was generated during the transaction along with as many metrics as Puppet knows how to collect. See `ReportsAndReporting Reports and Reporting`:trac: for more information on how to use reports. Currently, clients default to not sending in reports; you can enable reporting by setting the ``report`` parameter to true. To use a report, set the ``reports`` parameter on the server; multiple reports must be comma-separated. You can also specify ``none`` to disable reports entirely. Puppet provides multiple report handlers that will process client reports: + " diff --git a/lib/puppet/transaction.rb b/lib/puppet/transaction.rb index 884206b4c..bb5adc383 100644 --- a/lib/puppet/transaction.rb +++ b/lib/puppet/transaction.rb @@ -1,695 +1,695 @@ # the class that actually walks our resource/property tree, collects the changes, # and performs them require 'puppet' module Puppet class Transaction require 'puppet/transaction/change' require 'puppet/transaction/event' attr_accessor :component, :catalog, :ignoreschedules attr_accessor :sorted_resources, :configurator # The report, once generated. attr_reader :report # The list of events generated in this transaction. attr_reader :events include Puppet::Util # Add some additional times for reporting def addtimes(hash) hash.each do |name, num| @timemetrics[name] = num end end # Check to see if we should actually allow processing, but this really only # matters when a resource is getting deleted. def allow_processing?(resource, changes) # If a resource is going to be deleted but it still has # dependencies, then don't delete it unless it's implicit or the # dependency is itself being deleted. if resource.purging? and resource.deleting? if deps = relationship_graph.dependents(resource) and ! deps.empty? and deps.detect { |d| ! d.deleting? } resource.warning "%s still depend%s on me -- not purging" % [deps.collect { |r| r.ref }.join(","), deps.length > 1 ? "":"s"] return false end end return true end # Are there any failed resources in this transaction? def any_failed? failures = @failures.inject(0) { |failures, array| failures += array[1]; failures } if failures > 0 failures else false end end # Apply all changes for a resource, returning a list of the events # generated. def apply(resource) begin changes = resource.evaluate rescue => detail if Puppet[:trace] puts detail.backtrace end resource.err "Failed to retrieve current state of resource: %s" % detail # Mark that it failed @failures[resource] += 1 # And then return return [] end changes = [changes] unless changes.is_a?(Array) if changes.length > 0 @resourcemetrics[:out_of_sync] += 1 end return [] if changes.empty? or ! allow_processing?(resource, changes) resourceevents = apply_changes(resource, changes) # If there were changes and the resource isn't in noop mode... unless changes.empty? or resource.noop # Record when we last synced resource.cache(:synced, Time.now) # Flush, if appropriate if resource.respond_to?(:flush) resource.flush end # And set a trigger for refreshing this resource if it's a # self-refresher if resource.self_refresh? and ! resource.deleting? # Create an edge with this resource as both the source and # target. The triggering method treats these specially for # logging. events = resourceevents.collect { |e| e.name } set_trigger(Puppet::Relationship.new(resource, resource, :callback => :refresh, :event => events)) end end resourceevents end # Apply each change in turn. def apply_changes(resource, changes) changes.collect { |change| @changes << change @count += 1 events = nil begin # use an array, so that changes can return more than one # event if they want events = [change.forward].flatten.reject { |e| e.nil? } rescue => detail if Puppet[:trace] puts detail.backtrace end change.property.err "change from %s to %s failed: %s" % [change.property.is_to_s(change.is), change.property.should_to_s(change.should), detail] @failures[resource] += 1 next # FIXME this should support using onerror to determine # behaviour; or more likely, the client calling us # should do so end # Mark that our change happened, so it can be reversed # if we ever get to that point unless events.nil? or (events.is_a?(Array) and (events.empty?) or events.include?(:noop)) change.changed = true @resourcemetrics[:applied] += 1 end events }.flatten.reject { |e| e.nil? } end # Find all of the changed resources. def changed? @changes.find_all { |change| change.changed }.collect { |change| unless change.property.resource raise "No resource for %s" % change.inspect end change.property.resource }.uniq end # Do any necessary cleanup. If we don't get rid of the graphs, the # contained resources might never get cleaned up. def cleanup if defined? @generated catalog.remove_resource(*@generated) end end # Copy an important relationships from the parent to the newly-generated # child resource. def make_parent_child_relationship(resource, children) depthfirst = resource.depthfirst? children.each do |gen_child| if depthfirst edge = [gen_child, resource] else edge = [resource, gen_child] end relationship_graph.add_vertex(gen_child) unless relationship_graph.edge?(edge[1], edge[0]) relationship_graph.add_edge(*edge) else resource.debug "Skipping automatic relationship to %s" % gen_child end end end # Are we deleting this resource? def deleting?(changes) changes.detect { |change| change.property.name == :ensure and change.should == :absent } end # See if the resource generates new resources at evaluation time. def eval_generate(resource) generate_additional_resources(resource, :eval_generate) end # Evaluate a single resource. def eval_resource(resource, checkskip = true) events = [] if resource.is_a?(Puppet::Type::Component) raise Puppet::DevError, "Got a component to evaluate" end if checkskip and skip?(resource) @resourcemetrics[:skipped] += 1 else @resourcemetrics[:scheduled] += 1 changecount = @changes.length # We need to generate first regardless, because the recursive # actions sometimes change how the top resource is applied. children = eval_generate(resource) if children and resource.depthfirst? children.each do |child| # The child will never be skipped when the parent isn't events += eval_resource(child, false) end end # Perform the actual changes seconds = thinmark do events += apply(resource) end if children and ! resource.depthfirst? children.each do |child| events += eval_resource(child, false) end end # A bit of hackery here -- if skipcheck is true, then we're the # top-level resource. If that's the case, then make sure all of # the changes list this resource as a proxy. This is really only # necessary for rollback, since we know the generating resource # during forward changes. if children and checkskip @changes[changecount..-1].each { |change| change.proxy = resource } end # Keep track of how long we spend in each type of resource @timemetrics[resource.class.name] += seconds end # Check to see if there are any events for this resource if triggedevents = trigger(resource) events += triggedevents end # Collect the targets of any subscriptions to those events. We pass # the parent resource in so it will override the source in the events, # since eval_generated children can't have direct relationships. relationship_graph.matching_edges(events, resource).each do |orig_edge| # We have to dup the label here, else we modify the original edge label, # which affects whether a given event will match on the next run, which is, # of course, bad. edge = orig_edge.class.new(orig_edge.source, orig_edge.target, orig_edge.label) edge.event = events.collect { |e| e.name } set_trigger(edge) end # And return the events for collection events end # This method does all the actual work of running a transaction. It # collects all of the changes, executes them, and responds to any # necessary events. def evaluate @count = 0 # Start logging. Puppet::Util::Log.newdestination(@report) prepare() begin allevents = @sorted_resources.collect { |resource| if resource.is_a?(Puppet::Type::Component) Puppet.warning "Somehow left a component in the relationship graph" next end ret = nil seconds = thinmark do ret = eval_resource(resource) end if Puppet[:evaltrace] and @catalog.host_config? resource.info "Evaluated in %0.2f seconds" % seconds end ret }.flatten.reject { |e| e.nil? } ensure # And then close the transaction log. Puppet::Util::Log.close(@report) end Puppet.debug "Finishing transaction %s with %s changes" % [self.object_id, @count] @events = allevents allevents end # Determine whether a given resource has failed. def failed?(obj) if @failures[obj] > 0 return @failures[obj] else return false end end # Does this resource have any failed dependencies? def failed_dependencies?(resource) # First make sure there are no failed dependencies. To do this, # we check for failures in any of the vertexes above us. It's not # enough to check the immediate dependencies, which is why we use # a tree from the reversed graph. skip = false deps = relationship_graph.dependencies(resource) deps.each do |dep| if fails = failed?(dep) resource.notice "Dependency %s[%s] has %s failures" % [dep.class.name, dep.name, @failures[dep]] skip = true end end return skip end # A general method for recursively generating new resources from a # resource. def generate_additional_resources(resource, method) return [] unless resource.respond_to?(method) begin made = resource.send(method) rescue => detail puts detail.backtrace if Puppet[:trace] resource.err "Failed to generate additional resources using '%s': %s" % [method, detail] end return [] unless made made = [made] unless made.is_a?(Array) made.uniq! made.each do |res| @catalog.add_resource(res) { |r| r.finish } end make_parent_child_relationship(resource, made) made end # Collect any dynamically generated resources. This method is called # before the transaction starts. def generate list = @catalog.vertices newlist = [] while ! list.empty? list.each do |resource| newlist += generate_additional_resources(resource, :generate) end list = newlist newlist = [] end end # Generate a transaction report. def generate_report @resourcemetrics[:failed] = @failures.find_all do |name, num| num > 0 end.length # Get the total time spent @timemetrics[:total] = @timemetrics.inject(0) do |total, vals| total += vals[1] total end # Add all of the metrics related to resource count and status @report.newmetric(:resources, @resourcemetrics) # Record the relative time spent in each resource. @report.newmetric(:time, @timemetrics) # Then all of the change-related metrics @report.newmetric(:changes, :total => @changes.length ) @report.time = Time.now return @report end # Should we ignore tags? def ignore_tags? ! (@catalog.host_config? or Puppet[:name] == "puppet") end # this should only be called by a Puppet::Type::Component resource now # and it should only receive an array def initialize(resources) if resources.is_a?(Puppet::Resource::Catalog) @catalog = resources elsif resources.is_a?(Puppet::SimpleGraph) raise "Transactions should get catalogs now, not SimpleGraph" else raise "Transactions require catalogs" end @resourcemetrics = { :total => @catalog.vertices.length, :out_of_sync => 0, # The number of resources that had changes :applied => 0, # The number of resources fixed :skipped => 0, # The number of resources skipped :restarted => 0, # The number of resources triggered :failed_restarts => 0, # The number of resources that fail a trigger :scheduled => 0 # The number of resources scheduled } # Metrics for distributing times across the different types. @timemetrics = Hash.new(0) # The number of resources that were triggered in this run @triggered = Hash.new { |hash, key| hash[key] = Hash.new(0) } # Targets of being triggered. @targets = Hash.new do |hash, key| hash[key] = [] end # The changes we're performing @changes = [] # The resources that have failed and the number of failures each. This # is used for skipping resources because of failed dependencies. @failures = Hash.new do |h, key| h[key] = 0 end @report = Report.new @count = 0 end # Prefetch any providers that support it. We don't support prefetching # types, just providers. def prefetch prefetchers = {} @catalog.vertices.each do |resource| if provider = resource.provider and provider.class.respond_to?(:prefetch) prefetchers[provider.class] ||= {} - prefetchers[provider.class][resource.title] = resource + prefetchers[provider.class][resource.name] = resource end end # Now call prefetch, passing in the resources so that the provider instances can be replaced. prefetchers.each do |provider, resources| Puppet.debug "Prefetching %s resources for %s" % [provider.name, provider.resource_type.name] begin provider.prefetch(resources) rescue => detail if Puppet[:trace] puts detail.backtrace end Puppet.err "Could not prefetch %s provider '%s': %s" % [provider.resource_type.name, provider.name, detail] end end end # Prepare to evaluate the resources in a transaction. def prepare # Now add any dynamically generated resources generate() # Then prefetch. It's important that we generate and then prefetch, # so that any generated resources also get prefetched. prefetch() # This will throw an error if there are cycles in the graph. @sorted_resources = relationship_graph.topsort end def relationship_graph catalog.relationship_graph end # Send off the transaction report. def send_report begin report = generate_report() rescue => detail Puppet.err "Could not generate report: %s" % detail return end if Puppet[:rrdgraph] == true report.graph() end if Puppet[:summarize] puts report.summary end if Puppet[:report] begin report.save() rescue => detail Puppet.err "Reporting failed: %s" % detail end end end # Roll all completed changes back. def rollback @targets.clear @triggered.clear allevents = @changes.reverse.collect { |change| # skip changes that were never actually run unless change.changed Puppet.debug "%s was not changed" % change.to_s next end begin events = change.backward rescue => detail Puppet.err("%s rollback failed: %s" % [change,detail]) if Puppet[:trace] puts detail.backtrace end next # at this point, we would normally do error handling # but i haven't decided what to do for that yet # so just record that a sync failed for a given resource #@@failures[change.property.parent] += 1 # this still could get hairy; what if file contents changed, # but a chmod failed? how would i handle that error? dern end # FIXME This won't work right now. relationship_graph.matching_edges(events).each do |edge| @targets[edge.target] << edge end # Now check to see if there are any events for this child. # Kind of hackish, since going backwards goes a change at a # time, not a child at a time. trigger(change.property.resource) # And return the events for collection events }.flatten.reject { |e| e.nil? } end # Is the resource currently scheduled? def scheduled?(resource) self.ignoreschedules or resource.scheduled? end # Set an edge to be triggered when we evaluate its target. def set_trigger(edge) return unless method = edge.callback return unless edge.target.respond_to?(method) if edge.target.respond_to?(:ref) unless edge.source == edge.target edge.source.info "Scheduling %s of %s" % [edge.callback, edge.target.ref] end end @targets[edge.target] << edge end # Should this resource be skipped? def skip?(resource) skip = false if missing_tags?(resource) resource.debug "Not tagged with %s" % tags.join(", ") elsif ! scheduled?(resource) resource.debug "Not scheduled" elsif failed_dependencies?(resource) resource.warning "Skipping because of failed dependencies" else return false end return true end # The tags we should be checking. def tags unless defined? @tags tags = Puppet[:tags] if tags.nil? or tags == "" @tags = [] else @tags = tags.split(/\s*,\s*/) end end @tags end def tags=(tags) tags = [tags] unless tags.is_a?(Array) @tags = tags end # Is this resource tagged appropriately? def missing_tags?(resource) return false if self.ignore_tags? or tags.empty? return true unless resource.tagged?(tags) end # Are there any edges that target this resource? def targeted?(resource) # The default value is a new array so we have to test the length of it. @targets.include?(resource) and @targets[resource].length > 0 end # Trigger any subscriptions to a child. This does an upwardly recursive # search -- it triggers the passed resource, but also the resource's parent # and so on up the tree. def trigger(resource) return nil unless targeted?(resource) callbacks = Hash.new { |hash, key| hash[key] = [] } trigged = [] @targets[resource].each do |edge| # Collect all of the subs for each callback callbacks[edge.callback] << edge end callbacks.each do |callback, subs| noop = true subs.each do |edge| if edge.event.nil? or ! edge.event.include?(:noop) noop = false end end if noop resource.notice "Would have triggered %s from %s dependencies" % [callback, subs.length] # And then add an event for it. return [Puppet::Transaction::Event.new(:noop, resource)] end if subs.length == 1 and subs[0].source == resource message = "Refreshing self" else message = "Triggering '%s' from %s dependencies" % [callback, subs.length] end resource.notice message # At this point, just log failures, don't try to react # to them in any way. begin resource.send(callback) @resourcemetrics[:restarted] += 1 rescue => detail resource.err "Failed to call %s on %s: %s" % [callback, resource, detail] @resourcemetrics[:failed_restarts] += 1 if Puppet[:trace] puts detail.backtrace end end # And then add an event for it. trigged << Puppet::Transaction::Event.new(:triggered, resource) triggered(resource, callback) end if trigged.empty? return nil else return trigged end end def triggered(resource, method) @triggered[resource][method] += 1 end def triggered?(resource, method) @triggered[resource][method] end end end require 'puppet/transaction/report' diff --git a/lib/puppet/type.rb b/lib/puppet/type.rb index 0fa1697d1..0c7266c92 100644 --- a/lib/puppet/type.rb +++ b/lib/puppet/type.rb @@ -1,2110 +1,2110 @@ require 'puppet' require 'puppet/util/log' require 'puppet/util/metric' require 'puppet/property' require 'puppet/parameter' require 'puppet/util' require 'puppet/util/autoload' require 'puppet/metatype/manager' require 'puppet/util/errors' require 'puppet/util/log_paths' require 'puppet/util/logging' require 'puppet/resource/reference' require 'puppet/util/cacher' +require 'puppet/file_collection/lookup' # see the bottom of the file for the rest of the inclusions module Puppet class Type include Puppet::Util include Puppet::Util::Errors include Puppet::Util::LogPaths include Puppet::Util::Logging include Puppet::Util::Cacher + include Puppet::FileCollection::Lookup ############################### # Code related to resource type attributes. class << self include Puppet::Util::ClassGen include Puppet::Util::Warnings attr_reader :properties end def self.states warnonce "The states method is deprecated; use properties" properties() end # All parameters, in the appropriate order. The namevar comes first, then # the provider, then the properties, and finally the params and metaparams # in the order they were specified in the files. def self.allattrs # Cache this, since it gets called multiple times namevar = self.namevar order = [namevar] if self.parameters.include?(:provider) order << :provider end order << [self.properties.collect { |property| property.name }, self.parameters - [:provider], self.metaparams].flatten.reject { |param| # we don't want our namevar in there multiple times param == namevar } order.flatten! return order end # Retrieve an attribute alias, if there is one. def self.attr_alias(param) @attr_aliases[symbolize(param)] end # Create an alias to an existing attribute. This will cause the aliased # attribute to be valid when setting and retrieving values on the instance. def self.set_attr_alias(hash) hash.each do |new, old| @attr_aliases[symbolize(new)] = symbolize(old) end end # Find the class associated with any given attribute. def self.attrclass(name) @attrclasses ||= {} # We cache the value, since this method gets called such a huge number # of times (as in, hundreds of thousands in a given run). unless @attrclasses.include?(name) @attrclasses[name] = case self.attrtype(name) when :property; @validproperties[name] when :meta; @@metaparamhash[name] when :param; @paramhash[name] end end @attrclasses[name] end # What type of parameter are we dealing with? Cache the results, because # this method gets called so many times. def self.attrtype(attr) @attrtypes ||= {} unless @attrtypes.include?(attr) @attrtypes[attr] = case when @validproperties.include?(attr); :property when @paramhash.include?(attr); :param when @@metaparamhash.include?(attr); :meta end end @attrtypes[attr] end # Copy an existing class parameter. This allows other types to avoid # duplicating a parameter definition, and is mostly used by subclasses # of the File class. def self.copyparam(klass, name) param = klass.attrclass(name) unless param raise Puppet::DevError, "Class %s has no param %s" % [klass, name] end @parameters << param @parameters.each { |p| @paramhash[name] = p } if param.isnamevar? @namevar = param.name end end def self.eachmetaparam @@metaparams.each { |p| yield p.name } end # Create the 'ensure' class. This is a separate method so other types # can easily call it and create their own 'ensure' values. def self.ensurable(&block) if block_given? self.newproperty(:ensure, :parent => Puppet::Property::Ensure, &block) else self.newproperty(:ensure, :parent => Puppet::Property::Ensure) do self.defaultvalues end end end # Should we add the 'ensure' property to this class? def self.ensurable? # If the class has all three of these methods defined, then it's # ensurable. ens = [:exists?, :create, :destroy].inject { |set, method| set &&= self.public_method_defined?(method) } return ens end # Deal with any options passed into parameters. def self.handle_param_options(name, options) # If it's a boolean parameter, create a method to test the value easily if options[:boolean] define_method(name.to_s + "?") do val = self[name] if val == :true or val == true return true end end end end # Is the parameter in question a meta-parameter? def self.metaparam?(param) @@metaparamhash.include?(symbolize(param)) end # Find the metaparameter class associated with a given metaparameter name. def self.metaparamclass(name) @@metaparamhash[symbolize(name)] end def self.metaparams @@metaparams.collect { |param| param.name } end def self.metaparamdoc(metaparam) @@metaparamhash[metaparam].doc end # Create a new metaparam. Requires a block and a name, stores it in the # @parameters array, and does some basic checking on it. def self.newmetaparam(name, options = {}, &block) @@metaparams ||= [] @@metaparamhash ||= {} name = symbolize(name) param = genclass(name, :parent => options[:parent] || Puppet::Parameter, :prefix => "MetaParam", :hash => @@metaparamhash, :array => @@metaparams, :attributes => options[:attributes], &block ) # Grr. if options[:required_features] param.required_features = options[:required_features] end handle_param_options(name, options) param.metaparam = true return param end # Find the namevar def self.namevar unless defined? @namevar params = @parameters.find_all { |param| param.isnamevar? or param.name == :name } if params.length > 1 raise Puppet::DevError, "Found multiple namevars for %s" % self.name elsif params.length == 1 @namevar = params[0].name else raise Puppet::DevError, "No namevar for %s" % self.name end end @namevar end # Create a new parameter. Requires a block and a name, stores it in the # @parameters array, and does some basic checking on it. def self.newparam(name, options = {}, &block) options[:attributes] ||= {} param = genclass(name, :parent => options[:parent] || Puppet::Parameter, :attributes => options[:attributes], :block => block, :prefix => "Parameter", :array => @parameters, :hash => @paramhash ) handle_param_options(name, options) # Grr. if options[:required_features] param.required_features = options[:required_features] end param.isnamevar if options[:namevar] if param.isnamevar? @namevar = param.name end return param end def self.newstate(name, options = {}, &block) Puppet.warning "newstate() has been deprecrated; use newproperty(%s)" % name newproperty(name, options, &block) end # Create a new property. The first parameter must be the name of the property; # this is how users will refer to the property when creating new instances. # The second parameter is a hash of options; the options are: # * :parent: The parent class for the property. Defaults to Puppet::Property. # * :retrieve: The method to call on the provider or @parent object (if # the provider is not set) to retrieve the current value. def self.newproperty(name, options = {}, &block) name = symbolize(name) # This is here for types that might still have the old method of defining # a parent class. unless options.is_a? Hash raise Puppet::DevError, "Options must be a hash, not %s" % options.inspect end if @validproperties.include?(name) raise Puppet::DevError, "Class %s already has a property named %s" % [self.name, name] end if parent = options[:parent] options.delete(:parent) else parent = Puppet::Property end # We have to create our own, new block here because we want to define # an initial :retrieve method, if told to, and then eval the passed # block if available. prop = genclass(name, :parent => parent, :hash => @validproperties, :attributes => options) do # If they've passed a retrieve method, then override the retrieve # method on the class. if options[:retrieve] define_method(:retrieve) do provider.send(options[:retrieve]) end end if block class_eval(&block) end end # If it's the 'ensure' property, always put it first. if name == :ensure @properties.unshift prop else @properties << prop end return prop end def self.paramdoc(param) @paramhash[param].doc end # Return the parameter names def self.parameters return [] unless defined? @parameters @parameters.collect { |klass| klass.name } end # Find the parameter class associated with a given parameter name. def self.paramclass(name) @paramhash[name] end # Return the property class associated with a name def self.propertybyname(name) @validproperties[name] end def self.validattr?(name) name = symbolize(name) return true if name == :name @validattrs ||= {} unless @validattrs.include?(name) if self.validproperty?(name) or self.validparameter?(name) or self.metaparam?(name) @validattrs[name] = true else @validattrs[name] = false end end @validattrs[name] end # does the name reflect a valid property? def self.validproperty?(name) name = symbolize(name) if @validproperties.include?(name) return @validproperties[name] else return false end end # Return the list of validproperties def self.validproperties return {} unless defined? @parameters return @validproperties.keys end # does the name reflect a valid parameter? def self.validparameter?(name) unless defined? @parameters raise Puppet::DevError, "Class %s has not defined parameters" % self end if @paramhash.include?(name) or @@metaparamhash.include?(name) return true else return false end end # Return either the attribute alias or the attribute. def attr_alias(name) name = symbolize(name) if synonym = self.class.attr_alias(name) return synonym else return name end end # Are we deleting this resource? def deleting? obj = @parameters[:ensure] and obj.should == :absent end # Create a new property if it is valid but doesn't exist # Returns: true if a new parameter was added, false otherwise def add_property_parameter(prop_name) if self.class.validproperty?(prop_name) && !@parameters[prop_name] self.newattr(prop_name) return true end return false end # abstract accessing parameters and properties, and normalize # access to always be symbols, not strings # This returns a value, not an object. It returns the 'is' # value, but you can also specifically return 'is' and 'should' # values using 'object.is(:property)' or 'object.should(:property)'. def [](name) name = attr_alias(name) unless self.class.validattr?(name) fail("Invalid parameter %s(%s)" % [name, name.inspect]) end if name == :name name = self.class.namevar end if obj = @parameters[name] # Note that if this is a property, then the value is the "should" value, # not the current value. obj.value else return nil end end # Abstract setting parameters and properties, and normalize # access to always be symbols, not strings. This sets the 'should' # value on properties, and otherwise just sets the appropriate parameter. def []=(name,value) name = attr_alias(name) unless self.class.validattr?(name) fail("Invalid parameter %s" % [name]) end if name == :name name = self.class.namevar end if value.nil? raise Puppet::Error.new("Got nil value for %s" % name) end if obj = @parameters[name] obj.value = value return nil else self.newattr(name, :value => value) end nil end # remove a property from the object; useful in testing or in cleanup # when an error has been encountered def delete(attr) attr = symbolize(attr) if @parameters.has_key?(attr) @parameters.delete(attr) else raise Puppet::DevError.new("Undefined attribute '#{attr}' in #{self}") end end # iterate across the existing properties def eachproperty # properties() is a private method properties().each { |property| yield property } end # Let the catalog determine whether a given cached value is # still valid or has expired. def expirer catalog end # retrieve the 'should' value for a specified property def should(name) name = attr_alias(name) if prop = @parameters[name] and prop.is_a?(Puppet::Property) return prop.should else return nil end end # Create the actual attribute instance. Requires either the attribute # name or class as the first argument, then an optional hash of # attributes to set during initialization. def newattr(name, options = {}) if name.is_a?(Class) klass = name name = klass.name end unless klass = self.class.attrclass(name) raise Puppet::Error, "Resource type %s does not support parameter %s" % [self.class.name, name] end if @parameters.include?(name) raise Puppet::Error, "Parameter '%s' is already defined in %s" % [name, self.ref] end if provider and ! provider.class.supports_parameter?(klass) missing = klass.required_features.find_all { |f| ! provider.class.feature?(f) } info "Provider %s does not support features %s; not managing attribute %s" % [provider.class.name, missing.join(", "), name] return nil end # Add resource information at creation time, so it's available # during validation. options[:resource] = self begin # make sure the parameter doesn't have any errors return @parameters[name] = klass.new(options) rescue => detail error = Puppet::Error.new("Parameter %s failed: %s" % [name, detail]) error.set_backtrace(detail.backtrace) raise error end end # return the value of a parameter def parameter(name) @parameters[name.to_sym] end # Is the named property defined? def propertydefined?(name) unless name.is_a? Symbol name = name.intern end return @parameters.include?(name) end # Return an actual property instance by name; to return the value, use 'resource[param]' # LAK:NOTE(20081028) Since the 'parameter' method is now a superset of this method, # this one should probably go away at some point. def property(name) if obj = @parameters[symbolize(name)] and obj.is_a?(Puppet::Property) return obj else return nil end end # For any parameters or properties that have defaults and have not yet been # set, set them now. This method can be handed a list of attributes, # and if so it will only set defaults for those attributes. def set_default(attr) return unless klass = self.class.attrclass(attr) return unless klass.method_defined?(:default) return if @parameters.include?(klass.name) return unless parameter = newattr(klass.name) if value = parameter.default and ! value.nil? parameter.value = value else @parameters.delete(parameter.name) end end # Convert our object to a hash. This just includes properties. def to_hash rethash = {} @parameters.each do |name, obj| rethash[name] = obj.value end rethash end # Return a specific value for an attribute. def value(name) name = attr_alias(name) if obj = @parameters[name] and obj.respond_to?(:value) return obj.value else return nil end end # Meta-parameter methods: These methods deal with the results # of specifying metaparameters private # Return all of the property objects, in the order specified in the # class. def properties #debug "%s has %s properties" % [self,@parameters.length] props = self.class.properties.collect { |prop| @parameters[prop.name] }.find_all { |p| ! p.nil? }.each do |prop| unless prop.is_a?(Puppet::Property) raise Puppet::DevError, "got a non-property %s(%s)" % [prop.class, prop.class.name] end end props end public ############################### # Code related to the closure-like behaviour of the resource classes. attr_accessor :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 ############################### # Code related to the container behaviour. # this is a retarded hack method to get around the difference between # component children and file children def self.depthfirst? if defined? @depthfirst return @depthfirst else return false end end def depthfirst? self.class.depthfirst? end # Remove an object. The argument determines whether the object's # subscriptions get eliminated, too. def remove(rmdeps = true) # This is hackish (mmm, cut and paste), but it works for now, and it's # better than warnings. @parameters.each do |name, obj| obj.remove end @parameters.clear @parent = nil # Remove the reference to the provider. if self.provider @provider.clear @provider = nil end end ############################### # Code related to evaluating the resources. # This method is responsible for collecting property changes we always # descend into the children before we evaluate our current properties. # This returns any changes resulting from testing, thus 'collect' rather # than 'each'. def evaluate if self.provider.is_a?(Puppet::Provider) unless provider.class.suitable? raise Puppet::Error, "Provider %s is not functional on this platform" % provider.class.name end end # this only operates on properties, not properties + children # it's important that we call retrieve() on the type instance, # not directly on the property, because it allows the type to override # the method, like pfile does currentvalues = self.retrieve changes = propertychanges(currentvalues).flatten # now record how many changes we've resulted in if changes.length > 0 self.debug "%s change(s)" % [changes.length] end # If we're in noop mode, we don't want to store the checked time, # because it will result in the resource not getting scheduled if # someone were to apply the catalog in non-noop mode. # We're going to go ahead and record that we checked if there were # no changes, since it's unlikely it will affect the scheduling. noop = noop? if ! noop or (noop && changes.length == 0) self.cache(:checked, Time.now) end return changes.flatten end # Flush the provider, if it supports it. This is called by the # transaction. def flush if self.provider and self.provider.respond_to?(:flush) self.provider.flush end end # if all contained objects are in sync, then we're in sync # FIXME I don't think this is used on the type instances any more, # it's really only used for testing def insync?(is) insync = true if property = @parameters[:ensure] unless is.include? property raise Puppet::DevError, "The is value is not in the is array for '%s'" % [property.name] end ensureis = is[property] if property.insync?(ensureis) and property.should == :absent return true end end properties.each { |property| unless is.include? property raise Puppet::DevError, "The is value is not in the is array for '%s'" % [property.name] end propis = is[property] unless property.insync?(propis) property.debug("Not in sync: %s vs %s" % [propis.inspect, property.should.inspect]) insync = false #else # property.debug("In sync") end } #self.debug("%s sync status is %s" % [self,insync]) return insync end # retrieve the current value of all contained properties def retrieve return currentpropvalues end # Get a hash of the current properties. Returns a hash with # the actual property instance as the key and the current value # as the, um, value. def currentpropvalues # It's important to use the 'properties' method here, as it follows the order # in which they're defined in the class. It also guarantees that 'ensure' # is the first property, which is important for skipping 'retrieve' on # all the properties if the resource is absent. ensure_state = false return properties().inject({}) do | prophash, property| if property.name == :ensure ensure_state = property.retrieve prophash[property] = ensure_state else if ensure_state == :absent prophash[property] = :absent else prophash[property] = property.retrieve end end prophash end end # Are we running in noop mode? def noop? if defined?(@noop) @noop else Puppet[:noop] end end def noop noop? end # Retrieve the changes associated with all of the properties. def propertychanges(currentvalues) # If we are changing the existence of the object, then none of # the other properties matter. changes = [] ensureparam = @parameters[:ensure] # This allows resource types to have 'ensure' be a parameter, which allows them to # just pass the parameter on to other generated resources. ensureparam = nil unless ensureparam.is_a?(Puppet::Property) if ensureparam && !currentvalues.include?(ensureparam) raise Puppet::DevError, "Parameter ensure defined but missing from current values" end if ensureparam and ! ensureparam.insync?(currentvalues[ensureparam]) changes << Puppet::Transaction::Change.new(ensureparam, currentvalues[ensureparam]) # Else, if the 'ensure' property is correctly absent, then do # nothing elsif ensureparam and currentvalues[ensureparam] == :absent return [] else changes = properties().find_all { |property| currentvalues[property] ||= :absent ! property.insync?(currentvalues[property]) }.collect { |property| Puppet::Transaction::Change.new(property, currentvalues[property]) } end if Puppet[:debug] and changes.length > 0 self.debug("Changing " + changes.collect { |ch| ch.property.name }.join(",")) end changes end ############################### # Code related to managing resource instances. require 'puppet/transportable' # retrieve a named instance of the current type def self.[](name) raise "Global resource access is deprecated" @objects[name] || @aliases[name] end # add an instance by name to the class list of instances def self.[]=(name,object) raise "Global resource storage is deprecated" newobj = nil if object.is_a?(Puppet::Type) newobj = object else raise Puppet::DevError, "must pass a Puppet::Type object" end if exobj = @objects[name] and self.isomorphic? msg = "Object '%s[%s]' already exists" % [newobj.class.name, name] if exobj.file and exobj.line msg += ("in file %s at line %s" % [object.file, object.line]) end if object.file and object.line msg += ("and cannot be redefined in file %s at line %s" % [object.file, object.line]) end error = Puppet::Error.new(msg) raise error else #Puppet.info("adding %s of type %s to class list" % # [name,object.class]) @objects[name] = newobj end end # Create an alias. We keep these in a separate hash so that we don't encounter # the objects multiple times when iterating over them. def self.alias(name, obj) raise "Global resource aliasing is deprecated" if @objects.include?(name) unless @objects[name] == obj raise Puppet::Error.new( "Cannot create alias %s: object already exists" % [name] ) end end if @aliases.include?(name) unless @aliases[name] == obj raise Puppet::Error.new( "Object %s already has alias %s" % [@aliases[name].name, name] ) end end @aliases[name] = obj end # remove all of the instances of a single type def self.clear raise "Global resource removal is deprecated" if defined? @objects @objects.each do |name, obj| obj.remove(true) end @objects.clear end if defined? @aliases @aliases.clear end end # Force users to call this, so that we can merge objects if # necessary. def self.create(args) # LAK:DEP Deprecation notice added 12/17/2008 Puppet.warning "Puppet::Type.create is deprecated; use Puppet::Type.new" new(args) end # remove a specified object def self.delete(resource) raise "Global resource removal is deprecated" return unless defined? @objects if @objects.include?(resource.title) @objects.delete(resource.title) end if @aliases.include?(resource.title) @aliases.delete(resource.title) end if @aliases.has_value?(resource) names = [] @aliases.each do |name, otherres| if otherres == resource names << name end end names.each { |name| @aliases.delete(name) } end end # iterate across each of the type's instances def self.each raise "Global resource iteration is deprecated" return unless defined? @objects @objects.each { |name,instance| yield instance } end # does the type have an object with the given name? def self.has_key?(name) raise "Global resource access is deprecated" return @objects.has_key?(name) end # Retrieve all known instances. Either requires providers or must be overridden. def self.instances unless defined?(@providers) and ! @providers.empty? raise Puppet::DevError, "%s has no providers and has not overridden 'instances'" % self.name end # Put the default provider first, then the rest of the suitable providers. provider_instances = {} providers_by_source.collect do |provider| provider.instances.collect do |instance| # We always want to use the "first" provider instance we find, unless the resource # is already managed and has a different provider set if other = provider_instances[instance.name] Puppet.warning "%s %s found in both %s and %s; skipping the %s version" % [self.name.to_s.capitalize, instance.name, other.class.name, instance.class.name, instance.class.name] next end provider_instances[instance.name] = instance create(:name => instance.name, :provider => instance, :check => :all) end end.flatten.compact end # Return a list of one suitable provider per source, with the default provider first. def self.providers_by_source # Put the default provider first, then the rest of the suitable providers. sources = [] [defaultprovider, suitableprovider].flatten.uniq.collect do |provider| next if sources.include?(provider.source) sources << provider.source provider end.compact end # Convert a simple hash into a Resource instance. This is a convenience method, # so people can create RAL resources with a hash and get the same behaviour # as we get internally when we use Resource instances. # This should only be used directly from Ruby -- it's not used when going through # normal Puppet usage. def self.hash2resource(hash) hash = hash.inject({}) { |result, ary| result[ary[0].to_sym] = ary[1]; result } if title = hash[:title] hash.delete(:title) else if self.namevar != :name if hash.include?(:name) and hash.include?(self.namevar) raise Puppet::Error, "Cannot provide both name and %s to resources of type %s" % [self.namevar, self.name] end if title = hash[self.namevar] hash.delete(self.namevar) end end unless title ||= hash[:name] raise Puppet::Error, "You must specify a name or title for resources" end end # Now create our resource. resource = Puppet::Resource.new(self.name, title) [:catalog, :implicit].each do |attribute| if value = hash[attribute] hash.delete(attribute) resource.send(attribute.to_s + "=", value) end end hash.each do |param, value| resource[param] = value end return resource end # Create the path for logging and such. def pathbuilder if p = parent [p.pathbuilder, self.ref].flatten else [self.ref] end end ############################### # Add all of the meta parameters. newmetaparam(:noop) do desc "Boolean flag indicating whether work should actually be done." newvalues(:true, :false) munge do |value| case value when true, :true, "true"; @resource.noop = true when false, :false, "false"; @resource.noop = false end end end newmetaparam(:schedule) do desc "On what schedule the object should be managed. You must create a schedule object, and then reference the name of that object to use that for your schedule:: schedule { daily: period => daily, range => \"2-4\" } exec { \"/usr/bin/apt-get update\": schedule => daily } The creation of the schedule object does not need to appear in the configuration before objects that use it." end newmetaparam(:check) do desc "Propertys which should have their values retrieved but which should not actually be modified. This is currently used internally, but will eventually be used for querying, so that you could specify that you wanted to check the install state of all packages, and then query the Puppet client daemon to get reports on all packages." munge do |args| # If they've specified all, collect all known properties if args == :all args = @resource.class.properties.find_all do |property| # Only get properties supported by our provider if @resource.provider @resource.provider.class.supports_parameter?(property) else true end end.collect do |property| property.name end end unless args.is_a?(Array) args = [args] end unless defined? @resource self.devfail "No parent for %s, %s?" % [self.class, self.name] end args.each { |property| unless property.is_a?(Symbol) property = property.intern end next if @resource.propertydefined?(property) unless propertyklass = @resource.class.validproperty?(property) if @resource.class.validattr?(property) next else raise Puppet::Error, "%s is not a valid attribute for %s" % [property, self.class.name] end end next unless propertyklass.checkable? @resource.newattr(property) } end end newmetaparam(:loglevel) do desc "Sets the level that information will be logged. The log levels have the biggest impact when logs are sent to syslog (which is currently the default)." defaultto :notice newvalues(*Puppet::Util::Log.levels) newvalues(:verbose) munge do |loglevel| val = super(loglevel) if val == :verbose val = :info end val end end newmetaparam(:alias) do desc "Creates an alias for the object. Puppet uses this internally when you provide a symbolic name:: file { sshdconfig: path => $operatingsystem ? { solaris => \"/usr/local/etc/ssh/sshd_config\", default => \"/etc/ssh/sshd_config\" }, source => \"...\" } service { sshd: subscribe => file[sshdconfig] } When you use this feature, the parser sets ``sshdconfig`` as the name, and the library sets that as an alias for the file so the dependency lookup for ``sshd`` works. You can use this parameter yourself, but note that only the library can use these aliases; for instance, the following code will not work:: file { \"/etc/ssh/sshd_config\": owner => root, group => root, alias => sshdconfig } file { sshdconfig: mode => 644 } There's no way here for the Puppet parser to know that these two stanzas should be affecting the same file. See the `LanguageTutorial language tutorial`:trac: for more information. " munge do |aliases| unless aliases.is_a?(Array) aliases = [aliases] end raise(ArgumentError, "Cannot add aliases without a catalog") unless @resource.catalog aliases.each do |other| if obj = @resource.catalog.resource(@resource.class.name, other) unless obj.object_id == @resource.object_id self.fail("%s can not create alias %s: object already exists" % [@resource.title, other]) end next end # Newschool, add it to the catalog. @resource.catalog.alias(@resource, other) end end end newmetaparam(:tag) do desc "Add the specified tags to the associated resource. While all resources are automatically tagged with as much information as possible (e.g., each class and definition containing the resource), it can be useful to add your own tags to a given resource. Tags are currently useful for things like applying a subset of a host's configuration:: puppetd --test --tags mytag This way, when you're testing a configuration you can run just the portion you're testing." munge do |tags| tags = [tags] unless tags.is_a? Array tags.each do |tag| @resource.tag(tag) end end end class RelationshipMetaparam < Puppet::Parameter class << self attr_accessor :direction, :events, :callback, :subclasses end @subclasses = [] def self.inherited(sub) @subclasses << sub end def munge(references) references = [references] unless references.is_a?(Array) references.collect do |ref| if ref.is_a?(Puppet::Resource::Reference) ref else Puppet::Resource::Reference.new(ref) end end end def validate_relationship @value.each do |ref| unless @resource.catalog.resource(ref.to_s) description = self.class.direction == :in ? "dependency" : "dependent" fail "Could not find %s %s for %s" % [description, ref.to_s, resource.ref] end end end # Create edges from each of our relationships. :in # relationships are specified by the event-receivers, and :out # relationships are specified by the event generator. This # way 'source' and 'target' are consistent terms in both edges # and events -- that is, an event targets edges whose source matches # the event's source. The direction of the relationship determines # which resource is applied first and which resource is considered # to be the event generator. def to_edges @value.collect do |reference| reference.catalog = resource.catalog # Either of the two retrieval attempts could have returned # nil. unless related_resource = reference.resolve self.fail "Could not retrieve dependency '%s' of %s" % [reference, @resource.ref] end # Are we requiring them, or vice versa? See the method docs # for futher info on this. if self.class.direction == :in source = related_resource target = @resource else source = @resource target = related_resource end if method = self.class.callback subargs = { :event => self.class.events, :callback => method } self.debug("subscribes to %s" % [related_resource.ref]) else # If there's no callback, there's no point in even adding # a label. subargs = nil self.debug("requires %s" % [related_resource.ref]) end rel = Puppet::Relationship.new(source, target, subargs) end end end def self.relationship_params RelationshipMetaparam.subclasses end # Note that the order in which the relationships params is defined # matters. The labelled params (notify and subcribe) must be later, # so that if both params are used, those ones win. It's a hackish # solution, but it works. newmetaparam(:require, :parent => RelationshipMetaparam, :attributes => {:direction => :in, :events => :NONE}) do desc "One or more objects that this object depends on. This is used purely for guaranteeing that changes to required objects happen before the dependent object. For instance:: # Create the destination directory before you copy things down file { \"/usr/local/scripts\": ensure => directory } file { \"/usr/local/scripts/myscript\": source => \"puppet://server/module/myscript\", mode => 755, require => File[\"/usr/local/scripts\"] } Multiple dependencies can be specified by providing a comma-seperated list of resources, enclosed in square brackets:: require => [ File[\"/usr/local\"], File[\"/usr/local/scripts\"] ] Note that Puppet will autorequire everything that it can, and there are hooks in place so that it's easy for resources to add new ways to autorequire objects, so if you think Puppet could be smarter here, let us know. In fact, the above code was redundant -- Puppet will autorequire any parent directories that are being managed; it will automatically realize that the parent directory should be created before the script is pulled down. Currently, exec resources will autorequire their CWD (if it is specified) plus any fully qualified paths that appear in the command. For instance, if you had an ``exec`` command that ran the ``myscript`` mentioned above, the above code that pulls the file down would be automatically listed as a requirement to the ``exec`` code, so that you would always be running againts the most recent version. " end newmetaparam(:subscribe, :parent => RelationshipMetaparam, :attributes => {:direction => :in, :events => :ALL_EVENTS, :callback => :refresh}) do desc "One or more objects that this object depends on. Changes in the subscribed to objects result in the dependent objects being refreshed (e.g., a service will get restarted). For instance:: class nagios { file { \"/etc/nagios/nagios.conf\": source => \"puppet://server/module/nagios.conf\", alias => nagconf # just to make things easier for me } service { nagios: running => true, subscribe => File[nagconf] } } Currently the ``exec``, ``mount`` and ``service`` type support refreshing. " end newmetaparam(:before, :parent => RelationshipMetaparam, :attributes => {:direction => :out, :events => :NONE}) do desc %{This parameter is the opposite of **require** -- it guarantees that the specified object is applied later than the specifying object:: file { "/var/nagios/configuration": source => "...", recurse => true, before => Exec["nagios-rebuid"] } exec { "nagios-rebuild": command => "/usr/bin/make", cwd => "/var/nagios/configuration" } This will make sure all of the files are up to date before the make command is run.} end newmetaparam(:notify, :parent => RelationshipMetaparam, :attributes => {:direction => :out, :events => :ALL_EVENTS, :callback => :refresh}) do desc %{This parameter is the opposite of **subscribe** -- it sends events to the specified object:: file { "/etc/sshd_config": source => "....", notify => Service[sshd] } service { sshd: ensure => running } This will restart the sshd service if the sshd config file changes.} end ############################### # All of the provider plumbing for the resource types. require 'puppet/provider' require 'puppet/util/provider_features' # Add the feature handling module. extend Puppet::Util::ProviderFeatures attr_reader :provider # the Type class attribute accessors class << self attr_accessor :providerloader attr_writer :defaultprovider end # Find the default provider. def self.defaultprovider unless defined? @defaultprovider and @defaultprovider suitable = suitableprovider() # Find which providers are a default for this system. defaults = suitable.find_all { |provider| provider.default? } # If we don't have any default we use suitable providers defaults = suitable if defaults.empty? max = defaults.collect { |provider| provider.defaultnum }.max defaults = defaults.find_all { |provider| provider.defaultnum == max } retval = nil if defaults.length > 1 Puppet.warning( "Found multiple default providers for %s: %s; using %s" % [self.name, defaults.collect { |i| i.name.to_s }.join(", "), defaults[0].name] ) retval = defaults.shift elsif defaults.length == 1 retval = defaults.shift else raise Puppet::DevError, "Could not find a default provider for %s" % self.name end @defaultprovider = retval end return @defaultprovider end # Retrieve a provider by name. def self.provider(name) name = Puppet::Util.symbolize(name) # If we don't have it yet, try loading it. unless @providers.has_key?(name) @providerloader.load(name) end return @providers[name] end # Just list all of the providers. def self.providers @providers.keys end def self.validprovider?(name) name = Puppet::Util.symbolize(name) return (@providers.has_key?(name) && @providers[name].suitable?) end # Create a new provider of a type. This method must be called # directly on the type that it's implementing. def self.provide(name, options = {}, &block) name = Puppet::Util.symbolize(name) if obj = @providers[name] Puppet.debug "Reloading %s %s provider" % [name, self.name] unprovide(name) end parent = if pname = options[:parent] options.delete(:parent) if pname.is_a? Class pname else if provider = self.provider(pname) provider else raise Puppet::DevError, "Could not find parent provider %s of %s" % [pname, name] end end else Puppet::Provider end options[:resource_type] ||= self self.providify provider = genclass(name, :parent => parent, :hash => @providers, :prefix => "Provider", :block => block, :include => feature_module, :extend => feature_module, :attributes => options ) return provider end # Make sure we have a :provider parameter defined. Only gets called if there # are providers. def self.providify return if @paramhash.has_key? :provider newparam(:provider) do desc "The specific backend for #{self.name.to_s} to use. You will seldom need to specify this -- Puppet will usually discover the appropriate provider for your platform." # This is so we can refer back to the type to get a list of # providers for documentation. class << self attr_accessor :parenttype end # We need to add documentation for each provider. def self.doc @doc + " Available providers are:\n\n" + parenttype().providers.sort { |a,b| a.to_s <=> b.to_s }.collect { |i| "* **%s**: %s" % [i, parenttype().provider(i).doc] }.join("\n") end defaultto { @resource.class.defaultprovider.name } validate do |provider_class| provider_class = provider_class[0] if provider_class.is_a? Array if provider_class.is_a?(Puppet::Provider) provider_class = provider_class.class.name end unless provider = @resource.class.provider(provider_class) raise ArgumentError, "Invalid %s provider '%s'" % [@resource.class.name, provider_class] end end munge do |provider| provider = provider[0] if provider.is_a? Array if provider.is_a? String provider = provider.intern end @resource.provider = provider if provider.is_a?(Puppet::Provider) provider.class.name else provider end end end.parenttype = self end def self.unprovide(name) if @providers.has_key? name rmclass(name, :hash => @providers, :prefix => "Provider" ) if @defaultprovider and @defaultprovider.name == name @defaultprovider = nil end end end # Return an array of all of the suitable providers. def self.suitableprovider if @providers.empty? providerloader.loadall end @providers.find_all { |name, provider| provider.suitable? }.collect { |name, provider| provider }.reject { |p| p.name == :fake } # For testing end def provider=(name) if name.is_a?(Puppet::Provider) @provider = name @provider.resource = self elsif klass = self.class.provider(name) @provider = klass.new(self) else raise ArgumentError, "Could not find %s provider of %s" % [name, self.class.name] end end ############################### # All of the relationship code. # Specify a block for generating a list of objects to autorequire. This # makes it so that you don't have to manually specify things that you clearly # require. def self.autorequire(name, &block) @autorequires ||= {} @autorequires[name] = block end # Yield each of those autorequires in turn, yo. def self.eachautorequire @autorequires ||= {} @autorequires.each { |type, block| yield(type, block) } end # Figure out of there are any objects we can automatically add as # dependencies. def autorequire(rel_catalog = nil) rel_catalog ||= catalog raise(Puppet::DevError, "You cannot add relationships without a catalog") unless rel_catalog reqs = [] self.class.eachautorequire { |type, block| # Ignore any types we can't find, although that would be a bit odd. next unless typeobj = Puppet::Type.type(type) # Retrieve the list of names from the block. next unless list = self.instance_eval(&block) unless list.is_a?(Array) list = [list] end # Collect the current prereqs list.each { |dep| obj = nil # Support them passing objects directly, to save some effort. unless dep.is_a? Puppet::Type # Skip autorequires that we aren't managing unless dep = rel_catalog.resource(type, dep) next end end reqs << Puppet::Relationship.new(dep, self) } } return reqs end # Build the dependencies associated with an individual object. def builddepends # Handle the requires self.class.relationship_params.collect do |klass| if param = @parameters[klass.name] param.to_edges end end.flatten.reject { |r| r.nil? } end ############################### # All of the scheduling code. # Look up the schedule and set it appropriately. This is done after # the instantiation phase, so that the schedule can be anywhere in the # file. def schedule unless catalog warning "Cannot schedule without a schedule-containing catalog" return nil end unless defined? @schedule if name = self[:schedule] if sched = catalog.resource(:schedule, name) @schedule = sched else self.fail "Could not find schedule %s" % name end else @schedule = nil end end @schedule end # Check whether we are scheduled to run right now or not. def scheduled? return true if Puppet[:ignoreschedules] return true unless schedule = self.schedule # We use 'checked' here instead of 'synced' because otherwise we'll # end up checking most resources most times, because they will generally # have been synced a long time ago (e.g., a file only gets updated # once a month on the server and its schedule is daily; the last sync time # will have been a month ago, so we'd end up checking every run). return schedule.match?(self.cached(:checked).to_i) end ############################### # All of the tagging code. attr_reader :tags # Add a new tag. def tag(tag) tag = tag.intern if tag.is_a? String unless @tags.include? tag @tags << tag end end # Define the initial list of tags. def tags=(list) list = [list] unless list.is_a? Array @tags = list.collect do |t| case t when String; t.intern when Symbol; t else self.warning "Ignoring tag %s of type %s" % [tag.inspect, tag.class] end end @tags << self.class.name unless @tags.include?(self.class.name) end # Figure out of any of the specified tags apply to this object. This is an # OR operation. def tagged?(tags) tags = [tags] unless tags.is_a? Array tags = tags.collect { |t| t.intern } return tags.find { |tag| @tags.include? tag } end # Types (which map to resources in the languages) are entirely composed of # attribute value pairs. Generally, Puppet calls any of these things an # 'attribute', but these attributes always take one of three specific # forms: parameters, metaparams, or properties. # In naming methods, I have tried to consistently name the method so # that it is clear whether it operates on all attributes (thus has 'attr' in # the method name, or whether it operates on a specific type of attributes. - attr_accessor :file, :line - attr_writer :title attr_writer :noop include Enumerable # class methods dealing with Type management public # the Type class attribute accessors class << self attr_reader :name attr_accessor :self_refresh include Enumerable, Puppet::Util::ClassGen include Puppet::MetaType::Manager include Puppet::Util include Puppet::Util::Logging end # all of the variables that must be initialized for each subclass def self.initvars # all of the instances of this class @objects = Hash.new @aliases = Hash.new @providers = Hash.new @defaults = {} unless defined? @parameters @parameters = [] end @validproperties = {} @properties = [] @parameters = [] @paramhash = {} @attr_aliases = {} @paramdoc = Hash.new { |hash,key| if key.is_a?(String) key = key.intern end if hash.include?(key) hash[key] else "Param Documentation for %s not found" % key end } unless defined? @doc @doc = "" end end def self.to_s if defined? @name "Puppet::Type::" + @name.to_s.capitalize else super end end # Create a block to validate that our object is set up entirely. This will # be run before the object is operated on. def self.validate(&block) define_method(:validate, &block) #@validate = block end # The catalog that this resource is stored in. attr_accessor :catalog # create a log at specified level def log(msg) Puppet::Util::Log.create( :level => @parameters[:loglevel].value, :message => msg, :source => self ) end # instance methods related to instance intrinsics # e.g., initialize() and name() public attr_reader :original_parameters # initialize the type instance def initialize(resource) raise Puppet::DevError, "Got TransObject instead of Resource or hash" if resource.is_a?(Puppet::TransObject) resource = self.class.hash2resource(resource) unless resource.is_a?(Puppet::Resource) # The list of parameter/property instances. @parameters = {} # Set the title first, so any failures print correctly. if resource.type.to_s.downcase.to_sym == self.class.name self.title = resource.title else # This should only ever happen for components self.title = resource.ref end [:file, :line, :catalog, :implicit].each do |getter| setter = getter.to_s + "=" if val = resource.send(getter) self.send(setter, val) end end @tags = resource.tags @original_parameters = resource.to_hash set_name(@original_parameters) set_default(:provider) set_parameters(@original_parameters) self.validate if self.respond_to?(:validate) end private # Set our resource's name. def set_name(hash) n = self.class.namevar self[n] = hash[n] hash.delete(n) end # Set all of the parameters from a hash, in the appropriate order. def set_parameters(hash) # Use the order provided by allattrs, but add in any # extra attributes from the resource so we get failures # on invalid attributes. no_values = [] (self.class.allattrs + hash.keys).uniq.each do |attr| begin # Set any defaults immediately. This is mostly done so # that the default provider is available for any other # property validation. if hash.has_key?(attr) self[attr] = hash[attr] else no_values << attr end rescue ArgumentError, Puppet::Error, TypeError raise rescue => detail error = Puppet::DevError.new( "Could not set %s on %s: %s" % [attr, self.class.name, detail]) error.set_backtrace(detail.backtrace) raise error end end no_values.each do |attr| set_default(attr) end end public # Set up all of our autorequires. def finish # Scheduling has to be done when the whole config is instantiated, so # that file order doesn't matter in finding them. self.schedule # Make sure all of our relationships are valid. Again, must be done # when the entire catalog is instantiated. self.class.relationship_params.collect do |klass| if param = @parameters[klass.name] param.validate_relationship end end.flatten.reject { |r| r.nil? } end # Return a cached value def cached(name) Puppet::Util::Storage.cache(self)[name] #@cache[name] ||= nil end # Cache a value def cache(name, value) Puppet::Util::Storage.cache(self)[name] = value #@cache[name] = value end # For now, leave the 'name' method functioning like it used to. Once 'title' # works everywhere, I'll switch it. def name return self[:name] end # Look up our parent in the catalog, if we have one. def parent return nil unless catalog unless defined?(@parent) # This is kinda weird. if implicit? parents = catalog.relationship_graph.adjacent(self, :direction => :in) else parents = catalog.adjacent(self, :direction => :in) end if parents # We should never have more than one parent, so let's just ignore # it if we happen to. @parent = parents.shift else @parent = nil end end @parent end # Return the "type[name]" style reference. def ref "%s[%s]" % [self.class.name.to_s.capitalize, self.title] end def self_refresh? self.class.self_refresh end # Mark that we're purging. def purging @purging = true end # Is this resource being purged? Used by transactions to forbid # deletion when there are dependencies. def purging? if defined? @purging @purging else false end end # Retrieve the title of an object. If no title was set separately, # then use the object's name. def title unless defined? @title and @title namevar = self.class.namevar if self.class.validparameter?(namevar) @title = self[:name] elsif self.class.validproperty?(namevar) @title = self.should(namevar) else self.devfail "Could not find namevar %s for %s" % [namevar, self.class.name] end end return @title end # convert to a string def to_s self.ref end # Convert to a transportable object def to_trans(ret = true) trans = TransObject.new(self.title, self.class.name) values = retrieve() values.each do |name, value| trans[name.name] = value end @parameters.each do |name, param| # Avoid adding each instance name as both the name and the namevar next if param.class.isnamevar? and param.value == self.title # We've already got property values next if param.is_a?(Puppet::Property) trans[name] = param.value end trans.tags = self.tags # FIXME I'm currently ignoring 'parent' and 'path' return trans end end # Puppet::Type end require 'puppet/provider' # Always load these types. require 'puppet/type/component' diff --git a/lib/puppet/type/augeas.rb b/lib/puppet/type/augeas.rb index c89400b5e..da9cff369 100644 --- a/lib/puppet/type/augeas.rb +++ b/lib/puppet/type/augeas.rb @@ -1,157 +1,163 @@ #-- # Copyright (C) 2008 Red Hat Inc. # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public # License as published by the Free Software Foundation; either # version 2 of the License, or (at your option) any later version. # # This library is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # General Public License for more details. # # You should have received a copy of the GNU General Public # License along with this library; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # # Author: Bryan Kearney Puppet::Type.newtype(:augeas) do include Puppet::Util - + feature :parse_commands, "Parse the command string" feature :need_to_run?, "If the command should run" - feature :execute_changes, "Actually make the changes" + feature :execute_changes, "Actually make the changes" @doc = "Apply the changes (single or array of changes) to the filesystem via the augeas tool. Requires: - augeas to be installed (http://www.augeas.net) - ruby-augeas bindings Sample usage with a string:: augeas{\"test1\" : context => \"/files/etc/sysconfig/firstboot\", changes => \"set RUN_FIRSTBOOT YES\", onlyif => \"match other_value size > 0\", } Sample usage with an array and custom lenses:: augeas{\"jboss_conf\": context => \"/files\", changes => [ \"set /etc/jbossas/jbossas.conf/JBOSS_IP $ipaddress\", \"set /etc/jbossas/jbossas.conf/JAVA_HOME /usr\" ], load_path => \"$/usr/share/jbossas/lenses\", } " newparam (:name) do desc "The name of this task. Used for uniqueness" isnamevar end newparam (:context) do desc "Optional context path. This value is pre-pended to the paths of all changes" - defaultto "" + defaultto "" end newparam (:onlyif) do desc "Optional augeas command and comparisons to control the execution of this type. Supported onlyif syntax:: get [AUGEAS_PATH] [COMPARATOR] [STRING] match [MATCH_PATH] size [COMPARATOR] [INT] match [MATCH_PATH] include [STRING] match [MATCH_PATH] == [AN_ARRAY] where:: AUGEAS_PATH is a valid path scoped by the context MATCH_PATH is a valid match synatx scoped by the context COMPARATOR is in the set [> >= != == <= <] STRING is a string INT is a number AN_ARRAY is in the form ['a string', 'another']" defaultto "" end - + newparam(:changes) do desc "The changes which should be applied to the filesystem. This can be either a string which contains a command or an array of commands. Commands supported are:: set [PATH] [VALUE] Sets the value VALUE at loction PATH rm [PATH] Removes the node at location PATH remove [PATH] Synonym for rm clear [PATH] Keeps the node at PATH, but removes the value. - ins [LABEL] [WHERE] [PATH] + ins [LABEL] [WHERE] [PATH] Inserts an empty node LABEL either [WHERE={before|after}] PATH. - insert [LABEL] [WHERE] [PATH] + insert [LABEL] [WHERE] [PATH] Synonym for ins - If the parameter 'context' is set that value is prepended to PATH" - munge do |value| + munge do |value| provider.parse_commands(value) end end newparam(:root) do desc "A file system path; all files loaded by Augeas are loaded underneath ROOT" defaultto "/" end newparam(:load_path) do desc "Optional colon separated list of directories; these directories are searched for schema definitions" defaultto "" end + newparam(:force) do + desc "Optional command to force the augeas type to execute even if it thinks changes + will not be made. This does not overide the only setting. If onlyif is set, then the + foce setting will not override that result" + + defaultto false + end newparam(:type_check) do desc "Set to true if augeas should perform typechecking. Optional, defaults to false" newvalues(:true, :false) defaultto :false end - + # This is the acutal meat of the code. It forces # augeas to be run and fails or not based on the augeas return # code. newproperty(:returns) do |property| include Puppet::Util desc "The expected return code from the augeas command. Should not be set" defaultto 0 - + # Make output a bit prettier def change_to_s(currentvalue, newvalue) return "executed successfully" - end - + end + # if the onlyif resource is provided, then the value is parsed. # a return value of 0 will stop exection becuase it matches the # default value. def retrieve if @resource.provider.need_to_run?() :need_to_run else 0 end - end + end # Actually execute the command. def sync @resource.provider.execute_changes() end - end + end end diff --git a/lib/puppet/type/file/checksum.rb b/lib/puppet/type/file/checksum.rb index 76e27e55d..54d1c463b 100755 --- a/lib/puppet/type/file/checksum.rb +++ b/lib/puppet/type/file/checksum.rb @@ -1,281 +1,249 @@ require 'puppet/util/checksums' # Keep a copy of the file checksums, and notify when they change. This # property never actually modifies the system, it only notices when the system # changes on its own. Puppet::Type.type(:file).newproperty(:checksum) do include Puppet::Util::Checksums desc "How to check whether a file has changed. This state is used internally for file copying, but it can also be used to monitor files somewhat like Tripwire without managing the file contents in any way. You can specify that a file's checksum should be monitored and then subscribe to the file from another object and receive events to signify - checksum changes, for instance. - + checksum changes, for instance. + There are a number of checksum types available including MD5 hashing (and an md5lite variation that only hashes the first 500 characters of the - file." + file. + + The default checksum parameter, if checksums are enabled, is md5." @event = :file_changed @unmanaged = true @validtypes = %w{md5 md5lite timestamp mtime time} def self.validtype?(type) @validtypes.include?(type) end @validtypes.each do |ctype| newvalue(ctype) do handlesum() end end str = @validtypes.join("|") # This is here because Puppet sets this internally, using # {md5}...... newvalue(/^\{#{str}\}/) do handlesum() end - newvalue(:nosum) do - # nothing - :nochange - end - # If they pass us a sum type, behave normally, but if they pass # us a sum type + sum, stick the sum in the cache. munge do |value| if value =~ /^\{(\w+)\}(.+)$/ type = symbolize($1) sum = $2 cache(type, sum) return type else if FileTest.directory?(@resource[:path]) return :time elsif @resource[:source] and value.to_s != "md5" self.warning("Files with source set must use md5 as checksum. Forcing to md5 from %s for %s" % [ value, @resource[:path] ]) return :md5 else return symbolize(value) end end end # Store the checksum in the data cache, or retrieve it if only the # sum type is provided. def cache(type, sum = nil) return unless c = resource.catalog and c.host_config? unless type raise ArgumentError, "A type must be specified to cache a checksum" end type = symbolize(type) type = :mtime if type == :timestamp type = :ctime if type == :time unless state = @resource.cached(:checksums) self.debug "Initializing checksum hash" state = {} @resource.cache(:checksums, state) end if sum unless sum =~ /\{\w+\}/ sum = "{%s}%s" % [type, sum] end state[type] = sum else return state[type] end end # Because source and content and whomever else need to set the checksum # and do the updating, we provide a simple mechanism for doing so. def checksum=(value) munge(@should) self.updatesum(value) end def checktype self.should || :md5 end # Checksums need to invert how changes are printed. def change_to_s(currentvalue, newvalue) begin if currentvalue == :absent return "defined '%s' as '%s'" % [self.name, self.currentsum] elsif newvalue == :absent return "undefined %s from '%s'" % [self.name, self.is_to_s(currentvalue)] else if defined? @cached and @cached return "%s changed '%s' to '%s'" % [self.name, @cached, self.is_to_s(currentvalue)] else return "%s changed '%s' to '%s'" % [self.name, self.currentsum, self.is_to_s(currentvalue)] end end rescue Puppet::Error, Puppet::DevError raise rescue => detail raise Puppet::DevError, "Could not convert change %s to string: %s" % [self.name, detail] end end def currentsum cache(checktype()) end - # Retrieve the cached sum - def getcachedsum - hash = nil - unless hash = @resource.cached(:checksums) - hash = {} - @resource.cache(:checksums, hash) - end - - sumtype = self.should - - if hash.include?(sumtype) - #self.notice "Found checksum %s for %s" % - # [hash[sumtype] ,@resource[:path]] - sum = hash[sumtype] - - unless sum =~ /^\{\w+\}/ - sum = "{%s}%s" % [sumtype, sum] - end - return sum - elsif hash.empty? - #self.notice "Could not find sum of type %s" % sumtype - return :nosum - else - #self.notice "Found checksum for %s but not of type %s" % - # [@resource[:path],sumtype] - return :nosum - end - end - # Calculate the sum from disk. def getsum(checktype, file = nil) sum = "" checktype = :mtime if checktype == :timestamp checktype = :ctime if checktype == :time self.should = checktype = :md5 if @resource.property(:source) file ||= @resource[:path] return nil unless FileTest.exist?(file) if ! FileTest.file?(file) checktype = :mtime end method = checktype.to_s + "_file" self.fail("Invalid checksum type %s" % checktype) unless respond_to?(method) return "{%s}%s" % [checktype, send(method, file)] end # At this point, we don't actually modify the system, we modify # the stored state to reflect the current state, and then kick # off an event to mark any changes. def handlesum currentvalue = self.retrieve if currentvalue.nil? raise Puppet::Error, "Checksum state for %s is somehow nil" % @resource.title end if self.insync?(currentvalue) self.debug "Checksum is already in sync" return nil end # If we still can't retrieve a checksum, it means that # the file still doesn't exist if currentvalue == :absent # if they're copying, then we won't worry about the file # not existing yet return nil unless @resource.property(:source) end # If the sums are different, then return an event. if self.updatesum(currentvalue) return :file_changed else return nil end end def insync?(currentvalue) @should = [checktype()] if cache(checktype()) return currentvalue == currentsum() else # If there's no cached sum, then we don't want to generate # an event. return true end end # Even though they can specify multiple checksums, the insync? # mechanism can really only test against one, so we'll just retrieve # the first specified sum type. def retrieve(usecache = false) # When the 'source' is retrieving, it passes "true" here so # that we aren't reading the file twice in quick succession, yo. currentvalue = currentsum() return currentvalue if usecache and currentvalue stat = nil return :absent unless stat = @resource.stat if stat.ftype == "link" and @resource[:links] != :follow self.debug "Not checksumming symlink" # @resource.delete(:checksum) return currentvalue end # Just use the first allowed check type currentvalue = getsum(checktype()) # If there is no sum defined, then store the current value # into the cache, so that we're not marked as being # out of sync. We don't want to generate an event the first # time we get a sum. self.updatesum(currentvalue) unless cache(checktype()) # @resource.debug "checksum state is %s" % self.is return currentvalue end # Store the new sum to the state db. def updatesum(newvalue) return unless c = resource.catalog and c.host_config? result = false # if we're replacing, vs. updating if sum = cache(checktype()) return false if newvalue == sum self.debug "Replacing %s checksum %s with %s" % [@resource.title, sum, newvalue] result = true else @resource.debug "Creating checksum %s" % newvalue result = false end # Cache the sum so the log message can be right if possible. @cached = sum cache(checktype(), newvalue) return result end end diff --git a/lib/puppet/type/ssh_authorized_key.rb b/lib/puppet/type/ssh_authorized_key.rb index 66cf3e733..997afb81e 100644 --- a/lib/puppet/type/ssh_authorized_key.rb +++ b/lib/puppet/type/ssh_authorized_key.rb @@ -1,72 +1,86 @@ module Puppet newtype(:ssh_authorized_key) do @doc = "Manages SSH authorized keys. Currently only type 2 keys are supported." ensurable newparam(:name) do desc "The SSH key comment." isnamevar end newproperty(:type) do desc "The encryption type used: ssh-dss or ssh-rsa." newvalue("ssh-dss") newvalue("ssh-rsa") aliasvalue(:dsa, "ssh-dss") aliasvalue(:rsa, "ssh-rsa") end newproperty(:key) do desc "The key itself; generally a long string of hex digits." end newproperty(:user) do desc "The user account in which the SSH key should be installed." end newproperty(:target) do desc "The file in which to store the SSH key." + + defaultto :absent + + def should + if defined? @should and @should[0] != :absent + return super + end + + if user = resource[:user] + return File.expand_path("~%s/.ssh/authorized_keys" % user) + end + + return nil + end end newproperty(:options, :array_matching => :all) do desc "Key options, see sshd(8) for possible values. Multiple values should be specified as an array." defaultto do :absent end def is_to_s(value) if value == :absent or value.include?(:absent) super else value.join(",") end end def should_to_s(value) if value == :absent or value.include?(:absent) super else value.join(",") end end end autorequire(:user) do if should(:user) should(:user) end end validate do unless should(:target) or should(:user) raise Puppet::Error, "Attribute 'user' or 'target' is mandatory" end end end end diff --git a/lib/puppet/util/selinux.rb b/lib/puppet/util/selinux.rb index 70f244507..f13600680 100644 --- a/lib/puppet/util/selinux.rb +++ b/lib/puppet/util/selinux.rb @@ -1,211 +1,213 @@ # Provides utility functions to help interfaces Puppet to SELinux. # # This requires the very new SELinux Ruby bindings. These bindings closely # mirror the SELinux C library interface. # # Support for the command line tools is not provided because the performance # was abysmal. At this time (2008-11-02) the only distribution providing # these Ruby SELinux bindings which I am aware of is Fedora (in libselinux-ruby). begin require 'selinux' rescue LoadError # Nothing end module Puppet::Util::SELinux def selinux_support? unless defined? Selinux return false end if Selinux.is_selinux_enabled == 1 return true end return false end # Retrieve and return the full context of the file. If we don't have # SELinux support or if the SELinux call fails then return nil. def get_selinux_current_context(file) unless selinux_support? return nil end retval = Selinux.lgetfilecon(file) if retval == -1 return nil end return retval[1] end # Retrieve and return the default context of the file. If we don't have # SELinux support or if the SELinux call fails to file a default then return nil. def get_selinux_default_context(file) unless selinux_support? return nil end # If the filesystem has no support for SELinux labels, return a default of nil # instead of what matchpathcon would return unless selinux_label_support?(file) return nil end # If the file exists we should pass the mode to matchpathcon for the most specific # matching. If not, we can pass a mode of 0. begin filestat = File.lstat(file) mode = filestat.mode rescue Errno::ENOENT mode = 0 end retval = Selinux.matchpathcon(file, mode) if retval == -1 return nil end return retval[1] end # Take the full SELinux context returned from the tools and parse it # out to the three (or four) component parts. Supports :seluser, :selrole, # :seltype, and on systems with range support, :selrange. def parse_selinux_context(component, context) if context.nil? or context == "unlabeled" return nil end unless context =~ /^([a-z0-9_]+):([a-z0-9_]+):([a-z0-9_]+)(?::([a-zA-Z0-9:,._-]+))?/ raise Puppet::Error, "Invalid context to parse: #{context}" end ret = { :seluser => $1, :selrole => $2, :seltype => $3, :selrange => $4, } return ret[component] end # This updates the actual SELinux label on the file. You can update # only a single component or update the entire context. # The caveat is that since setting a partial context makes no sense the # file has to already exist. Puppet (via the File resource) will always # just try to set components, even if all values are specified by the manifest. # I believe that the OS should always provide at least a fall-through context # though on any well-running system. def set_selinux_context(file, value, component = false) unless selinux_support? return nil end if component # Must first get existing context to replace a single component context = Selinux.lgetfilecon(file)[1] if context == -1 # We can't set partial context components when no context exists # unless/until we can find a way to make Puppet call this method # once for all selinux file label attributes. Puppet.warning "Can't set SELinux context on file unless the file already has some kind of context" return nil end context = context.split(':') case component when :seluser context[0] = value when :selrole context[1] = value when :seltype context[2] = value when :selrange context[3] = value else raise ArguementError, "set_selinux_context component must be one of :seluser, :selrole, :seltype, or :selrange" end context = context.join(':') else context = value end retval = Selinux.lsetfilecon(file, context) if retval == 0 return true else Puppet.warning "Failed to set SELinux context %s on %s" % [context, file] return false end end # Since this call relies on get_selinux_default_context it also needs a # full non-relative path to the file. Fortunately, that seems to be all # Puppet uses. This will set the file's SELinux context to the policy's # default context (if any) if it differs from the context currently on # the file. def set_selinux_default_context(file) new_context = get_selinux_default_context(file) unless new_context return nil end cur_context = get_selinux_current_context(file) if new_context != cur_context set_selinux_context(file, new_context) return new_context end return nil end # Internal helper function to read and parse /proc/mounts def read_mounts begin - mounts = File.read("/proc/mounts") + mountfh = File.open("/proc/mounts", File::NONBLOCK) + mounts = mountfh.read + mountfh.close rescue return nil end mntpoint = {} # Read all entries in /proc/mounts. The second column is the # mountpoint and the third column is the filesystem type. # We skip rootfs because it is always mounted at / mounts.collect do |line| params = line.split(' ') next if params[2] == 'rootfs' mntpoint[params[1]] = params[2] end return mntpoint end # Internal helper function to return which type of filesystem a # given file path resides on def find_fs(file) unless mnts = read_mounts() return nil end # For a given file: # Check if the filename is in the data structure; # return the fstype if it is. # Just in case: return something if you're down to "/" or "" # Remove the last slash and everything after it, # and repeat with that as the file for the next loop through. ary = file.split('/') while not ary.empty? do path = ary.join('/') if mnts.has_key?(path) return mnts[path] end ary.pop end return mnts['/'] end # Check filesystem a path resides on for SELinux support against # whitelist of known-good filesystems. # Returns true if the filesystem can support SELinux labels and # false if not. def selinux_label_support?(file) fstype = find_fs(file) if fstype.nil? return false end filesystems = ['ext2', 'ext3', 'ext4', 'gfs', 'gfs2', 'xfs', 'jfs'] return filesystems.include?(fstype) end end diff --git a/spec/integration/bin/puppetmasterd.rb b/spec/integration/bin/puppetmasterd.rb index 447344472..b5a3f96da 100755 --- a/spec/integration/bin/puppetmasterd.rb +++ b/spec/integration/bin/puppetmasterd.rb @@ -1,109 +1,110 @@ #!/usr/bin/env ruby require File.dirname(__FILE__) + '/../../spec_helper' describe "puppetmasterd" do before do # Get a safe temporary file file = Tempfile.new("puppetmaster_integration_testing") @dir = file.path file.delete Dir.mkdir(@dir) Puppet.settings[:confdir] = @dir Puppet.settings[:vardir] = @dir Puppet[:certdnsnames] = "localhost" @@port = 12345 end after { stop Puppet::SSL::Host.ca_location = :none system("rm -rf %s" % @dir) Puppet.settings.clear } def arguments rundir = File.join(Puppet[:vardir], "run") @pidfile = File.join(rundir, "testing.pid") args = "" args += " --confdir %s" % Puppet[:confdir] args += " --rundir %s" % rundir args += " --pidfile %s" % @pidfile args += " --vardir %s" % Puppet[:vardir] args += " --certdnsnames %s" % Puppet[:certdnsnames] args += " --masterport %s" % @@port args += " --user %s" % Puppet::Util::SUIDManager.uid args += " --group %s" % Puppet::Util::SUIDManager.gid args += " --autosign true" end def start(addl_args = "") Puppet.settings.mkdir(:manifestdir) Puppet.settings.write(:manifest) do |f| f.puts { "notify { testing: }" } end args = arguments + addl_args - output = %x{puppetmasterd #{args}}.chomp + bin = File.join(File.dirname(__FILE__), "..", "..", "..", "sbin", "puppetmasterd") + output = %x{#{bin} #{args}}.chomp end def stop if @pidfile and FileTest.exist?(@pidfile) pid = File.read(@pidfile).chomp.to_i Process.kill(:TERM, pid) end end it "should create a PID file" do start FileTest.exist?(@pidfile).should be_true end it "should be serving status information over REST" it "should be serving status information over xmlrpc" do start sleep 0.5 client = Puppet::Network::Client.status.new(:Server => "localhost", :Port => @@port) FileUtils.mkdir_p(File.dirname(Puppet[:autosign])) File.open(Puppet[:autosign], "w") { |f| f.puts Puppet[:certname] } client.cert retval = client.status retval.should == 1 end it "should exit with return code 0 after parsing if --parseonly is set and there are no errors" do start(" --parseonly > /dev/null") sleep(1) ps = Facter["ps"].value || "ps -ef" pid = nil %x{#{ps}}.chomp.split(/\n/).each { |line| next if line =~ /^puppet/ # skip normal master procs if line =~ /puppetmasterd.+--manifest/ ary = line.split(" ") pid = ary[1].to_i end } $?.should == 0 pid.should be_nil end it "should exit with return code 1 after parsing if --parseonly is set and there are errors" end diff --git a/spec/integration/checksum.rb b/spec/integration/checksum.rb index c94f3e47e..49ae8d2b7 100755 --- a/spec/integration/checksum.rb +++ b/spec/integration/checksum.rb @@ -1,47 +1,48 @@ #!/usr/bin/env ruby # # Created by Luke Kanies on 2007-9-22. # Copyright (c) 2007. All rights reserved. require File.dirname(__FILE__) + '/../spec_helper' require 'puppet/checksum' describe Puppet::Checksum, " when using the file terminus" do before do + Puppet.settings.stubs(:use) Puppet::Checksum.terminus_class = :file @content = "this is some content" @sum = Puppet::Checksum.new(@content) @file = Puppet::Checksum.indirection.terminus.path(@sum.checksum) end it "should store content at a path determined by its checksum" do File.stubs(:directory?).returns(true) filehandle = mock 'filehandle' filehandle.expects(:print).with(@content) File.expects(:open).with(@file, "w").yields(filehandle) @sum.save end it "should retrieve stored content when the checksum is provided as the key" do File.stubs(:exist?).returns(true) File.expects(:read).with(@file).returns(@content) newsum = Puppet::Checksum.find(@sum.checksum) newsum.content.should == @content end it "should remove specified files when asked" do File.stubs(:exist?).returns(true) File.expects(:unlink).with(@file) Puppet::Checksum.destroy(@sum.name) end after do Puppet.settings.clear end end diff --git a/spec/integration/defaults.rb b/spec/integration/defaults.rb index b3f3a7066..46b5fd64e 100755 --- a/spec/integration/defaults.rb +++ b/spec/integration/defaults.rb @@ -1,86 +1,95 @@ #!/usr/bin/env ruby require File.dirname(__FILE__) + '/../spec_helper' require 'puppet/defaults' describe "Puppet defaults" do + include Puppet::Util::Execution after { Puppet.settings.clear } describe "when setting the :factpath" do it "should add the :factpath to Facter's search paths" do Facter.expects(:search).with("/my/fact/path") Puppet.settings[:factpath] = "/my/fact/path" end end describe "when setting the :certname" do it "should fail if the certname is not downcased" do lambda { Puppet.settings[:certname] = "Host.Domain.Com" }.should raise_error(ArgumentError) end end describe "when configuring the :crl" do it "should warn if :cacrl is set to false" do Puppet.expects(:warning) Puppet.settings[:cacrl] = 'false' end end it "should have a clientyamldir setting" do Puppet.settings[:clientyamldir].should_not be_nil end it "should have different values for the yamldir and clientyamldir" do Puppet.settings[:yamldir].should_not == Puppet.settings[:clientyamldir] end # See #1232 it "should not specify a user or group for the clientyamldir" do Puppet.settings.element(:clientyamldir).owner.should be_nil Puppet.settings.element(:clientyamldir).group.should be_nil end # See #1232 it "should not specify a user or group for the rundir" do Puppet.settings.element(:rundir).owner.should be_nil Puppet.settings.element(:rundir).group.should be_nil end it "should default to yaml as the catalog format" do Puppet.settings[:catalog_format].should == "yaml" end it "should default to 0.0.0.0 for its bind address and 'webrick' for its server type" do Puppet.settings[:servertype] = "webrick" Puppet.settings[:bindaddress].should == "0.0.0.0" end it "should default to 0.0.0.0 for its bind address if the server is webrick" do Puppet.settings[:servertype] = "webrick" Puppet.settings[:bindaddress].should == "0.0.0.0" end it "should default to 127.0.0.1 for its bind address if the server is mongrel" do Puppet.settings[:servertype] = "mongrel" Puppet.settings[:bindaddress].should == "127.0.0.1" end it "should allow specification of a different bind address" do Puppet.settings[:bindaddress] = "192.168.0.1" Puppet.settings[:bindaddress].should == "192.168.0.1" end [:factdest, :pluginpath].each do |setting| it "should force the #{setting} to be a directory" do Puppet.settings[setting].should =~ /\/$/ end end [:modulepath, :pluginpath, :factpath].each do |setting| it "should configure '#{setting}' not to be a file setting, so multi-directory settings are acceptable" do Puppet.settings.element(setting).should be_instance_of(Puppet::Util::Settings::CElement) end end + + it "should add /usr/sbin and /sbin to the path if they're not there" do + withenv("PATH" => "/usr/bin:/usr/local/bin") do + Puppet.settings[:path] = "none" # this causes it to ignore the setting + ENV["PATH"].split(File::PATH_SEPARATOR).should be_include("/usr/sbin") + ENV["PATH"].split(File::PATH_SEPARATOR).should be_include("/sbin") + end + end end diff --git a/spec/integration/reports.rb b/spec/integration/reports.rb index 7351c3da1..cc4ae8f4c 100755 --- a/spec/integration/reports.rb +++ b/spec/integration/reports.rb @@ -1,14 +1,18 @@ #!/usr/bin/env ruby # # Created by Luke Kanies on 2007-10-12. # Copyright (c) 2007. All rights reserved. require File.dirname(__FILE__) + '/../spec_helper' require 'puppet/reports' describe Puppet::Reports, " when using report types" do + before do + Puppet.settings.stubs(:use) + end + it "should load report types as modules" do Puppet::Reports.report(:store).should be_instance_of(Module) end end diff --git a/spec/integration/transaction/report.rb b/spec/integration/transaction/report.rb index 68533b503..b704e56b8 100755 --- a/spec/integration/transaction/report.rb +++ b/spec/integration/transaction/report.rb @@ -1,26 +1,29 @@ #!/usr/bin/env ruby # # Created by Luke Kanies on 2008-4-8. # Copyright (c) 2008. All rights reserved. require File.dirname(__FILE__) + '/../../spec_helper' describe Puppet::Transaction::Report do describe "when using the indirector" do - after { Puppet::Util::Cacher.expire } + after do + Puppet::Util::Cacher.expire + Puppet.settings.stubs(:use) + end it "should be able to delegate to the :processor terminus" do Puppet::Transaction::Report.indirection.stubs(:terminus_class).returns :processor terminus = Puppet::Transaction::Report.indirection.terminus(:processor) Facter.stubs(:value).returns "host.domain.com" report = Puppet::Transaction::Report.new terminus.expects(:process).with(report) report.save end end end diff --git a/spec/integration/type/package.rb b/spec/integration/type/package.rb index def44ad97..a3d8eb278 100755 --- a/spec/integration/type/package.rb +++ b/spec/integration/type/package.rb @@ -1,22 +1,25 @@ #!/usr/bin/env ruby require File.dirname(__FILE__) + '/../../spec_helper' describe Puppet::Type.type(:package), "when choosing a default package provider" do before do # the default provider is cached. Puppet::Type.type(:package).defaultprovider = nil end def provider_name(os) - {"Debian" => :apt, "Darwin" => :apple, "RedHat" => :up2date, "Fedora" => :yum, "FreeBSD" => :ports, "OpenBSD" => :openbsd, "Solaris" => :sun}[os] + {"Ubuntu" => :apt, "Debian" => :apt, "Darwin" => :apple, "RedHat" => :up2date, "Fedora" => :yum, "FreeBSD" => :ports, "OpenBSD" => :openbsd, "Solaris" => :sun}[os] end it "should have a default provider" do Puppet::Type.type(:package).defaultprovider.should_not be_nil end it "should choose the correct provider each platform" do - Puppet::Type.type(:package).defaultprovider.name.should == provider_name(Facter.value(:operatingsystem)) + unless default_provider = provider_name(Facter.value(:operatingsystem)) + pending("No default provider specified in this test for %s" % Facter.value(:operatingsystem)) + end + Puppet::Type.type(:package).defaultprovider.name.should == default_provider end end diff --git a/spec/unit/file_collection.rb b/spec/unit/file_collection.rb new file mode 100755 index 000000000..81bcc2d2d --- /dev/null +++ b/spec/unit/file_collection.rb @@ -0,0 +1,53 @@ +#!/usr/bin/env ruby + +require File.dirname(__FILE__) + '/../spec_helper' + +require 'puppet/file_collection' + +describe Puppet::FileCollection do + before do + @collection = Puppet::FileCollection.new + end + + it "should be able to convert a file name into an index" do + @collection.index("/my/file").should be_instance_of(Fixnum) + end + + it "should be able to convert an index into a file name" do + index = @collection.index("/path/to/file") + @collection.path(index).should == "/path/to/file" + end + + it "should always give the same file name for a given index" do + index = @collection.index("/path/to/file") + @collection.path(index).should == @collection.path(index) + end + + it "should always give the same index for a given file name" do + @collection.index("/my/file").should == @collection.index("/my/file") + end + + it "should always correctly relate a file name and its index even when multiple files are in the collection" do + indexes = %w{a b c d e f}.inject({}) do |hash, letter| + hash[letter] = @collection.index("/path/to/file/%s" % letter) + hash + end + + indexes.each do |letter, index| + @collection.index("/path/to/file/%s" % letter).should == indexes[letter] + @collection.path(index).should == @collection.path(index) + end + end + + it "should return nil as the file name when an unknown index is provided" do + @collection.path(50).should be_nil + end + + it "should provide a global collection" do + Puppet::FileCollection.collection.should be_instance_of(Puppet::FileCollection) + end + + it "should reuse the global collection" do + Puppet::FileCollection.collection.should equal(Puppet::FileCollection.collection) + end +end diff --git a/spec/unit/file_collection/lookup.rb b/spec/unit/file_collection/lookup.rb new file mode 100755 index 000000000..81cc61872 --- /dev/null +++ b/spec/unit/file_collection/lookup.rb @@ -0,0 +1,46 @@ +#!/usr/bin/env ruby + +require File.dirname(__FILE__) + '/../../spec_helper' +require 'puppet/file_collection/lookup' + +class LookupTester + include Puppet::FileCollection::Lookup +end + +describe Puppet::FileCollection::Lookup do + before do + @tester = LookupTester.new + + @file_collection = mock 'file_collection' + Puppet::FileCollection.stubs(:collection).returns @file_collection + end + + it "should use the file collection to determine the index of the file name" do + @file_collection.expects(:index).with("/my/file").returns 50 + + @tester.file = "/my/file" + @tester.file_index.should == 50 + end + + it "should return nil as the file name if no index is set" do + @tester.file.should be_nil + end + + it "should use the file collection to convert the index to a file name" do + @file_collection.expects(:path).with(25).returns "/path/to/file" + + @tester.file_index = 25 + + @tester.file.should == "/path/to/file" + end + + it "should support a line attribute" do + @tester.line = 50 + @tester.line.should == 50 + end + + it "should default to the global file collection" do + Puppet::FileCollection.expects(:collection).returns "collection" + @tester.file_collection.should == "collection" + end +end diff --git a/spec/unit/indirector/node/ldap.rb b/spec/unit/indirector/node/ldap.rb index ed8809e73..d407796c6 100755 --- a/spec/unit/indirector/node/ldap.rb +++ b/spec/unit/indirector/node/ldap.rb @@ -1,424 +1,444 @@ #!/usr/bin/env ruby require File.dirname(__FILE__) + '/../../../spec_helper' require 'puppet/indirector/node/ldap' describe Puppet::Node::Ldap do before do Puppet::Node::Facts.stubs(:terminus_class).returns :yaml end describe "when searching for a single node" do before :each do @searcher = Puppet::Node::Ldap.new @name = "mynode.domain.com" @node = stub 'node', :name => @name, :name= => nil @node.stub_everything Puppet::Node.stubs(:new).returns(@node) @request = stub 'request', :key => @name end it "should convert the hostname into a search filter" do entry = stub 'entry', :dn => 'cn=mynode.domain.com,ou=hosts,dc=madstop,dc=com', :vals => %w{}, :to_hash => {} @searcher.expects(:ldapsearch).with("(&(objectclass=puppetClient)(cn=#{@name}))").yields entry @searcher.name2hash(@name) end it "should convert any found entry into a hash" do entry = stub 'entry', :dn => 'cn=mynode.domain.com,ou=hosts,dc=madstop,dc=com', :vals => %w{}, :to_hash => {} @searcher.expects(:ldapsearch).with("(&(objectclass=puppetClient)(cn=#{@name}))").yields entry @searcher.expects(:entry2hash).with(entry).returns "myhash" @searcher.name2hash(@name).should == "myhash" end # This heavily tests our entry2hash method, so we don't have to stub out the stupid entry information any more. describe "when an ldap entry is found" do before do @entry = stub 'entry', :dn => 'cn=mynode.domain.com,ou=hosts,dc=madstop,dc=com', :vals => %w{}, :to_hash => {} @searcher.stubs(:ldapsearch).yields @entry end it "should convert the entry to a hash" do @searcher.entry2hash(@entry).should be_instance_of(Hash) end it "should add the entry's common name to the hash" do @searcher.entry2hash(@entry)[:name].should == "mynode.domain.com" end it "should add all of the entry's classes to the hash" do @entry.stubs(:vals).with("puppetclass").returns %w{one two} @searcher.entry2hash(@entry)[:classes].should == %w{one two} end it "should deduplicate class values" do @entry.stubs(:to_hash).returns({}) @searcher.stubs(:class_attributes).returns(%w{one two}) @entry.stubs(:vals).with("one").returns(%w{a b}) @entry.stubs(:vals).with("two").returns(%w{b c}) @searcher.entry2hash(@entry)[:classes].should == %w{a b c} end it "should add the entry's environment to the hash" do @entry.stubs(:to_hash).returns("environment" => %w{production}) @searcher.entry2hash(@entry)[:environment].should == "production" end it "should add all stacked parameters as parameters in the hash" do @entry.stubs(:vals).with("puppetvar").returns(%w{one=two three=four}) result = @searcher.entry2hash(@entry) result[:parameters]["one"].should == "two" result[:parameters]["three"].should == "four" end it "should not add the stacked parameter as a normal parameter" do @entry.stubs(:vals).with("puppetvar").returns(%w{one=two three=four}) @entry.stubs(:to_hash).returns("puppetvar" => %w{one=two three=four}) @searcher.entry2hash(@entry)[:parameters]["puppetvar"].should be_nil end it "should add all other attributes as parameters in the hash" do @entry.stubs(:to_hash).returns("foo" => %w{one two}) @searcher.entry2hash(@entry)[:parameters]["foo"].should == %w{one two} end it "should return single-value parameters as strings, not arrays" do @entry.stubs(:to_hash).returns("foo" => %w{one}) @searcher.entry2hash(@entry)[:parameters]["foo"].should == "one" end + it "should convert 'true' values to the boolean 'true'" do + @entry.stubs(:to_hash).returns({"one" => ["true"]}) + @searcher.entry2hash(@entry)[:parameters]["one"].should == true + end + + it "should convert 'false' values to the boolean 'false'" do + @entry.stubs(:to_hash).returns({"one" => ["false"]}) + @searcher.entry2hash(@entry)[:parameters]["one"].should == false + end + + it "should convert 'true' values to the boolean 'true' inside an array" do + @entry.stubs(:to_hash).returns({"one" => ["true", "other"]}) + @searcher.entry2hash(@entry)[:parameters]["one"].should == [true, "other"] + end + + it "should convert 'false' values to the boolean 'false' inside an array" do + @entry.stubs(:to_hash).returns({"one" => ["false", "other"]}) + @searcher.entry2hash(@entry)[:parameters]["one"].should == [false, "other"] + end + it "should add the parent's name if present" do @entry.stubs(:vals).with("parentnode").returns(%w{foo}) @searcher.entry2hash(@entry)[:parent].should == "foo" end it "should fail if more than one parent is specified" do @entry.stubs(:vals).with("parentnode").returns(%w{foo}) @searcher.entry2hash(@entry)[:parent].should == "foo" end end it "should search first for the provided key" do @searcher.expects(:name2hash).with("mynode.domain.com").returns({}) @searcher.find(@request) end it "should search for the short version of the provided key if the key looks like a hostname and no results are found for the key itself" do @searcher.expects(:name2hash).with("mynode.domain.com").returns(nil) @searcher.expects(:name2hash).with("mynode").returns({}) @searcher.find(@request) end it "should search for default information if no information can be found for the key" do @searcher.expects(:name2hash).with("mynode.domain.com").returns(nil) @searcher.expects(:name2hash).with("mynode").returns(nil) @searcher.expects(:name2hash).with("default").returns({}) @searcher.find(@request) end it "should return nil if no results are found in ldap" do @searcher.stubs(:name2hash).returns nil @searcher.find(@request).should be_nil end it "should return a node object if results are found in ldap" do @searcher.stubs(:name2hash).returns({}) @searcher.find(@request).should equal(@node) end describe "and node information is found in LDAP" do before do @result = {} @searcher.stubs(:name2hash).returns @result end it "should create the node with the correct name, even if it was found by a different name" do @searcher.expects(:name2hash).with("mynode.domain.com").returns nil @searcher.expects(:name2hash).with("mynode").returns @result Puppet::Node.expects(:new).with("mynode.domain.com").returns @node @searcher.find(@request) end it "should add any classes from ldap" do @result[:classes] = %w[a b c d] @node.expects(:classes=).with(%w{a b c d}) @searcher.find(@request) end it "should add all entry attributes as node parameters" do @result[:parameters] = {"one" => "two", "three" => "four"} @node.expects(:parameters=).with("one" => "two", "three" => "four") @searcher.find(@request) end it "should set the node's environment to the environment of the results" do @result[:environment] = "test" @node.expects(:environment=).with("test") @searcher.find(@request) end it "should retain false parameter values" do @result[:parameters] = {} @result[:parameters]["one"] = false @node.expects(:parameters=).with("one" => false) @searcher.find(@request) end it "should merge the node's facts after the parameters from ldap are assigned" do # Make sure we've got data to start with, so the parameters are actually set. @result[:parameters] = {} @result[:parameters]["one"] = "yay" # A hackish way to enforce order. set = false @node.expects(:parameters=).with { |*args| set = true } @node.expects(:fact_merge).with { |*args| raise "Facts were merged before parameters were set" unless set; true } @searcher.find(@request) end describe "and a parent node is specified" do before do @entry = {:classes => [], :parameters => {}} @parent = {:classes => [], :parameters => {}} @parent_parent = {:classes => [], :parameters => {}} @searcher.stubs(:name2hash).with(@name).returns(@entry) @searcher.stubs(:name2hash).with('parent').returns(@parent) @searcher.stubs(:name2hash).with('parent_parent').returns(@parent_parent) @searcher.stubs(:parent_attribute).returns(:parent) end it "should search for the parent node" do @entry[:parent] = "parent" @searcher.expects(:name2hash).with(@name).returns @entry @searcher.expects(:name2hash).with('parent').returns @parent @searcher.find(@request) end it "should fail if the parent cannot be found" do @entry[:parent] = "parent" @searcher.expects(:name2hash).with('parent').returns nil proc { @searcher.find(@request) }.should raise_error(Puppet::Error) end it "should add any parent classes to the node's classes" do @entry[:parent] = "parent" @entry[:classes] = %w{a b} @parent[:classes] = %w{c d} @node.expects(:classes=).with(%w{a b c d}) @searcher.find(@request) end it "should add any parent parameters to the node's parameters" do @entry[:parent] = "parent" @entry[:parameters]["one"] = "two" @parent[:parameters]["three"] = "four" @node.expects(:parameters=).with("one" => "two", "three" => "four") @searcher.find(@request) end it "should prefer node parameters over parent parameters" do @entry[:parent] = "parent" @entry[:parameters]["one"] = "two" @parent[:parameters]["one"] = "three" @node.expects(:parameters=).with("one" => "two") @searcher.find(@request) end it "should use the parent's environment if the node has none" do @entry[:parent] = "parent" @parent[:environment] = "parent" @node.stubs(:parameters=) @node.expects(:environment=).with("parent") @searcher.find(@request) end it "should prefer the node's environment to the parent's" do @entry[:parent] = "parent" @entry[:environment] = "child" @parent[:environment] = "parent" @node.stubs(:parameters=) @node.expects(:environment=).with("child") @searcher.find(@request) end it "should recursively look up parent information" do @entry[:parent] = "parent" @entry[:parameters]["one"] = "two" @parent[:parent] = "parent_parent" @parent[:parameters]["three"] = "four" @parent_parent[:parameters]["five"] = "six" @node.expects(:parameters=).with("one" => "two", "three" => "four", "five" => "six") @searcher.find(@request) end it "should not allow loops in parent declarations" do @entry[:parent] = "parent" @parent[:parent] = @name proc { @searcher.find(@request) }.should raise_error(ArgumentError) end end end end describe "when searching for multiple nodes" do before :each do @searcher = Puppet::Node::Ldap.new @options = {} @request = stub 'request', :key => "foo", :options => @options Puppet::Node::Facts.stubs(:terminus_class).returns :yaml end it "should find all nodes if no arguments are provided" do @searcher.expects(:ldapsearch).with("(objectclass=puppetClient)") # LAK:NOTE The search method requires an essentially bogus key. It's # an API problem that I don't really know how to fix. @searcher.search @request end describe "and a class is specified" do it "should find all nodes that are members of that class" do @searcher.expects(:ldapsearch).with("(&(objectclass=puppetClient)(puppetclass=one))") @options[:class] = "one" @searcher.search @request end end describe "multiple classes are specified" do it "should find all nodes that are members of all classes" do @searcher.expects(:ldapsearch).with("(&(objectclass=puppetClient)(puppetclass=one)(puppetclass=two))") @options[:class] = %w{one two} @searcher.search @request end end it "should process each found entry" do # .yields can't be used to yield multiple values :/ @searcher.expects(:ldapsearch).yields("one") @searcher.expects(:entry2hash).with("one").returns(:name => "foo") @searcher.search @request end it "should return a node for each processed entry with the name from the entry" do @searcher.expects(:ldapsearch).yields("whatever") @searcher.expects(:entry2hash).with("whatever").returns(:name => "foo") result = @searcher.search(@request) result[0].should be_instance_of(Puppet::Node) result[0].name.should == "foo" end it "should merge each node's facts" do node = mock 'node' Puppet::Node.expects(:new).with("foo").returns node node.expects(:fact_merge) @searcher.stubs(:ldapsearch).yields("one") @searcher.stubs(:entry2hash).with("one").returns(:name => "foo") @searcher.search(@request) end end end describe Puppet::Node::Ldap, " when developing the search query" do before do @searcher = Puppet::Node::Ldap.new end it "should return the value of the :ldapclassattrs split on commas as the class attributes" do Puppet.stubs(:[]).with(:ldapclassattrs).returns("one,two") @searcher.class_attributes.should == %w{one two} end it "should return nil as the parent attribute if the :ldapparentattr is set to an empty string" do Puppet.stubs(:[]).with(:ldapparentattr).returns("") @searcher.parent_attribute.should be_nil end it "should return the value of the :ldapparentattr as the parent attribute" do Puppet.stubs(:[]).with(:ldapparentattr).returns("pere") @searcher.parent_attribute.should == "pere" end it "should use the value of the :ldapstring as the search filter" do Puppet.stubs(:[]).with(:ldapstring).returns("mystring") @searcher.search_filter("testing").should == "mystring" end it "should replace '%s' with the node name in the search filter if it is present" do Puppet.stubs(:[]).with(:ldapstring).returns("my%sstring") @searcher.search_filter("testing").should == "mytestingstring" end it "should not modify the global :ldapstring when replacing '%s' in the search filter" do filter = mock 'filter' filter.expects(:include?).with("%s").returns(true) filter.expects(:gsub).with("%s", "testing").returns("mynewstring") Puppet.stubs(:[]).with(:ldapstring).returns(filter) @searcher.search_filter("testing").should == "mynewstring" end end describe Puppet::Node::Ldap, " when deciding attributes to search for" do before do @searcher = Puppet::Node::Ldap.new end it "should use 'nil' if the :ldapattrs setting is 'all'" do Puppet.stubs(:[]).with(:ldapattrs).returns("all") @searcher.search_attributes.should be_nil end it "should split the value of :ldapattrs on commas and use the result as the attribute list" do Puppet.stubs(:[]).with(:ldapattrs).returns("one,two") @searcher.stubs(:class_attributes).returns([]) @searcher.stubs(:parent_attribute).returns(nil) @searcher.search_attributes.should == %w{one two} end it "should add the class attributes to the search attributes if not returning all attributes" do Puppet.stubs(:[]).with(:ldapattrs).returns("one,two") @searcher.stubs(:class_attributes).returns(%w{three four}) @searcher.stubs(:parent_attribute).returns(nil) # Sort them so i don't have to care about return order @searcher.search_attributes.sort.should == %w{one two three four}.sort end it "should add the parent attribute to the search attributes if not returning all attributes" do Puppet.stubs(:[]).with(:ldapattrs).returns("one,two") @searcher.stubs(:class_attributes).returns([]) @searcher.stubs(:parent_attribute).returns("parent") @searcher.search_attributes.sort.should == %w{one two parent}.sort end it "should not add nil parent attributes to the search attributes" do Puppet.stubs(:[]).with(:ldapattrs).returns("one,two") @searcher.stubs(:class_attributes).returns([]) @searcher.stubs(:parent_attribute).returns(nil) @searcher.search_attributes.should == %w{one two} end end diff --git a/spec/unit/other/transbucket.rb b/spec/unit/other/transbucket.rb index 63de962e5..63b5137b5 100755 --- a/spec/unit/other/transbucket.rb +++ b/spec/unit/other/transbucket.rb @@ -1,188 +1,188 @@ #!/usr/bin/env ruby require File.dirname(__FILE__) + '/../../spec_helper' describe Puppet::TransBucket do before do @bucket = Puppet::TransBucket.new end it "should be able to produce a RAL component" do @bucket.name = "luke" @bucket.type = "foo" resource = nil proc { resource = @bucket.to_ral }.should_not raise_error resource.should be_instance_of(Puppet::Type::Component) resource.title.should == "Foo[luke]" end it "should accept TransObjects into its children list" do object = Puppet::TransObject.new("luke", "user") proc { @bucket.push(object) }.should_not raise_error @bucket.each do |o| o.should equal(object) end end it "should accept TransBuckets into its children list" do object = Puppet::TransBucket.new() proc { @bucket.push(object) }.should_not raise_error @bucket.each do |o| o.should equal(object) end end it "should refuse to accept any children that are not TransObjects or TransBuckets" do proc { @bucket.push "a test" }.should raise_error end it "should return use 'node' as the type and the provided name as the title if only a type is provided" do @bucket.type = "mystuff" @bucket.to_ref.should == "Node[mystuff]" end it "should return use 'component' as the type and the provided type as the title if only a name is provided" do @bucket.name = "mystuff" @bucket.to_ref.should == "Class[mystuff]" end it "should return nil as its reference when type and name are missing" do @bucket.to_ref.should be_nil end it "should return the title as its reference" do @bucket.name = "luke" @bucket.type = "user" @bucket.to_ref.should == "User[luke]" end it "should canonize resource references when the type is 'component'" do @bucket.name = 'something' @bucket.type = 'foo::bar' @bucket.to_ref.should == "Foo::Bar[something]" end end describe Puppet::TransBucket, " when generating a catalog" do before do @bottom = Puppet::TransBucket.new @bottom.type = "fake" @bottom.name = "bottom" - @bottomobj = Puppet::TransObject.new("bottom", "user") + @bottomobj = Puppet::TransObject.new("bottom", "notify") @bottom.push @bottomobj @middle = Puppet::TransBucket.new @middle.type = "fake" @middle.name = "middle" - @middleobj = Puppet::TransObject.new("middle", "user") + @middleobj = Puppet::TransObject.new("middle", "notify") @middle.push(@middleobj) @middle.push(@bottom) @top = Puppet::TransBucket.new @top.type = "fake" @top.name = "top" - @topobj = Puppet::TransObject.new("top", "user") + @topobj = Puppet::TransObject.new("top", "notify") @top.push(@topobj) @top.push(@middle) @users = %w{top middle bottom} @fakes = %w{Fake[bottom] Fake[middle] Fake[top]} end it "should convert all transportable objects to RAL resources" do @catalog = @top.to_catalog @users.each do |name| - @catalog.vertices.find { |r| r.class.name == :user and r.title == name }.should be_instance_of(Puppet::Type.type(:user)) + @catalog.vertices.find { |r| r.class.name == :notify and r.title == name }.should be_instance_of(Puppet::Type.type(:notify)) end end it "should fail if any transportable resources fail to convert to RAL resources" do @bottomobj.expects(:to_ral).raises ArgumentError lambda { @bottom.to_catalog }.should raise_error(ArgumentError) end it "should convert all transportable buckets to RAL components" do @catalog = @top.to_catalog @fakes.each do |name| @catalog.vertices.find { |r| r.class.name == :component and r.title == name }.should be_instance_of(Puppet::Type.type(:component)) end end it "should add all resources to the graph's resource table" do @catalog = @top.to_catalog @catalog.resource("fake[top]").should equal(@top) end it "should finalize all resources" do @catalog = @top.to_catalog @catalog.vertices.each do |vertex| vertex.should be_finalized end end it "should only call to_ral on each resource once" do # We just raise exceptions here because we're not interested in # what happens with the result, only that the method only # gets called once. resource = @topobj.to_ral @topobj.expects(:to_ral).once.returns resource @top.to_catalog end it "should set each TransObject's catalog before converting to a RAL resource" do @middleobj.expects(:catalog=).with { |c| c.is_a?(Puppet::Resource::Catalog) } @top.to_catalog end it "should set each TransBucket's catalog before converting to a RAL resource" do # each bucket is seen twice in the loop, so we have to handle the case where the config # is set twice @bottom.expects(:catalog=).with { |c| c.is_a?(Puppet::Resource::Catalog) }.at_least_once @top.to_catalog end end describe Puppet::TransBucket, " when serializing" do before do @bucket = Puppet::TransBucket.new(%w{one two}) @bucket.name = "one" @bucket.type = "two" end it "should be able to be dumped to yaml" do proc { YAML.dump(@bucket) }.should_not raise_error end it "should dump YAML that produces an equivalent object" do result = YAML.dump(@bucket) newobj = YAML.load(result) newobj.name.should == "one" newobj.type.should == "two" children = [] newobj.each do |o| children << o end children.should == %w{one two} end end describe Puppet::TransBucket, " when converting to a Puppet::Resource" do before do @trans = Puppet::TransBucket.new @trans.name = "foo" @trans.type = "bar" @trans.param(:noop, true) end it "should create a resource with the correct type and title" do result = @trans.to_resource result.type.should == "Bar" result.title.should == "foo" end it "should add all of its parameters to the created resource" do @trans.param(:noop, true) @trans.to_resource[:noop].should be_true end end diff --git a/spec/unit/parser/ast.rb b/spec/unit/parser/ast.rb index 513943725..35e2b1eff 100644 --- a/spec/unit/parser/ast.rb +++ b/spec/unit/parser/ast.rb @@ -1,37 +1,41 @@ #!/usr/bin/env ruby require File.dirname(__FILE__) + '/../../spec_helper' require 'puppet/parser/ast' describe Puppet::Parser::AST do + + it "should use the file lookup module" do + Puppet::Parser::AST.ancestors.should be_include(Puppet::FileCollection::Lookup) + end it "should have a doc accessor" do ast = Puppet::Parser::AST.new({}) ast.should respond_to(:doc) end it "should have a use_docs accessor to indicate it wants documentation" do ast = Puppet::Parser::AST.new({}) ast.should respond_to(:use_docs) end [ Puppet::Parser::AST::Collection, Puppet::Parser::AST::Definition, Puppet::Parser::AST::Else, Puppet::Parser::AST::Function, Puppet::Parser::AST::HostClass, Puppet::Parser::AST::IfStatement, Puppet::Parser::AST::Node, Puppet::Parser::AST::Resource, Puppet::Parser::AST::ResourceDefaults, Puppet::Parser::AST::ResourceOverride, Puppet::Parser::AST::VarDef ].each do |k| it "#{k}.use_docs should return true" do ast = k.new({}) ast.use_docs.should be_true end end describe "when initializing" do it "should store the doc argument if passed" do ast = Puppet::Parser::AST.new(:doc => "documentation") ast.doc.should == "documentation" 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 92699e040..a042b4e72 100755 --- a/spec/unit/parser/resource.rb +++ b/spec/unit/parser/resource.rb @@ -1,437 +1,441 @@ #!/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 do before do @parser = Puppet::Parser::Parser.new :Code => "" @source = @parser.newclass "" @node = Puppet::Node.new("yaynode") @compiler = Puppet::Parser::Compiler.new(@node, @parser) @scope = @compiler.topscope end def mkresource(args = {}) args[:source] ||= "source" args[:scope] ||= stub('scope', :source => mock('source')) {:type => "resource", :title => "testing", :source => "source", :scope => "scope"}.each do |param, value| args[param] ||= value end params = args[:params] || {:one => "yay", :three => "rah"} if args[:params] == :none args.delete(:params) else args[:params] = paramify(args[:source], params) end Puppet::Parser::Resource.new(args) end def param(name, value, source) Puppet::Parser::Resource::Param.new(:name => name, :value => value, :source => source) end def paramify(source, hash) hash.collect do |name, value| Puppet::Parser::Resource::Param.new( :name => name, :value => value, :source => source ) end end + it "should use the file lookup module" do + Puppet::Parser::Resource.ancestors.should be_include(Puppet::FileCollection::Lookup) + end + 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 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 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 it "should have a array-indexing method for retrieving parameter values" do @resource = mkresource @resource[:one].should == "yay" end it "should use a Puppet::Resource for converting to a ral resource" do trans = mock 'resource', :to_ral => "yay" @resource = mkresource @resource.expects(:to_resource).returns trans @resource.to_ral.should == "yay" end describe "when initializing" do before do @arguments = {:type => "resource", :title => "testing", :scope => stub('scope', :source => mock('source'))} end [:type, :title, :scope].each do |name| it "should fail unless #{name.to_s} is specified" do try = @arguments.dup try.delete(name) lambda { Puppet::Parser::Resource.new(try) }.should raise_error(ArgumentError) end end it "should set the reference correctly" do res = Puppet::Parser::Resource.new(@arguments) res.ref.should == "Resource[testing]" end end describe "when evaluating" do before do @type = Puppet::Parser::Resource @definition = @parser.newdefine "mydefine" @class = @parser.newclass "myclass" @nodedef = @parser.newnode("mynode")[0] 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 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 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 describe "when finishing" do before do @class = @parser.newclass "myclass" @nodedef = @parser.newnode("mynode")[0] @resource = Puppet::Parser::Resource.new(:type => "file", :title => "whatever", :scope => @scope, :source => @source) end it "should do nothing if it has already been finished" do @resource.finish @resource.expects(:add_metaparams).never @resource.finish end it "should add all defaults available from the scope" do @resource.scope.expects(:lookupdefaults).with(@resource.type).returns(:owner => param(:owner, "default", @resource.source)) @resource.finish @resource[:owner].should == "default" end it "should not replace existing parameters with defaults" do @resource.set_parameter :owner, "oldvalue" @resource.scope.expects(:lookupdefaults).with(@resource.type).returns(:owner => :replaced) @resource.finish @resource[:owner].should == "oldvalue" end it "should add a copy of each default, rather than the actual default parameter instance" do newparam = param(:owner, "default", @resource.source) other = newparam.dup other.value = "other" newparam.expects(:dup).returns(other) @resource.scope.expects(:lookupdefaults).with(@resource.type).returns(:owner => newparam) @resource.finish @resource[:owner].should == "other" end it "should copy metaparams from its scope" do @scope.setvar("noop", "true") @resource.class.publicize_methods(:add_metaparams) { @resource.add_metaparams } @resource["noop"].should == "true" end it "should not copy metaparams that it already has" do @resource.set_parameter("noop", "false") @scope.setvar("noop", "true") @resource.class.publicize_methods(:add_metaparams) { @resource.add_metaparams } @resource["noop"].should == "false" end it "should copy all metaparams that it finds" do @scope.setvar("require", "container") @scope.setvar("notify", "container") @resource.class.publicize_methods(:add_metaparams) { @resource.add_metaparams } @resource["require"].should == "container" @resource["notify"].should == "container" end it "should stack relationship metaparams from its container if it already has them" do @resource.set_parameter("require", "resource") @scope.setvar("require", "container") @resource.class.publicize_methods(:add_metaparams) { @resource.add_metaparams } @resource["require"].sort.should == %w{container resource} end it "should not stack relationship metaparams that are set to 'undef'" do @resource.set_parameter("require", :undef) @scope.setvar("require", "container") @resource.class.publicize_methods(:add_metaparams) { @resource.add_metaparams } @resource["require"].should == :undef end it "should flatten the array resulting from stacking relationship metaparams" do @resource.set_parameter("require", ["resource1", "resource2"]) @scope.setvar("require", %w{container1 container2}) @resource.class.publicize_methods(:add_metaparams) { @resource.add_metaparams } @resource["require"].sort.should == %w{container1 container2 resource1 resource2} 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) @resource.class.publicize_methods(:add_scope_tags) { @resource.add_scope_tags } @resource.tags.should be_include("one") @resource.tags.should be_include("two") end 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 get tagged with the resource type" do @resource.tags.should be_include("file") end it "should get tagged with the title" do @resource.tags.should be_include("yay") 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) end end describe "when merging overrides" do before do @source = "source1" @resource = mkresource :source => @source @override = mkresource :source => @source end it "should fail when the override was not created by a parent class" do @override.source = "source2" @override.source.expects(:child_of?).with("source1").returns(false) lambda { @resource.merge(@override) }.should raise_error(Puppet::ParseError) end it "should succeed when the override was created in the current scope" do @resource.source = "source3" @override.source = @resource.source @override.source.expects(:child_of?).with("source3").never params = {:a => :b, :c => :d} @override.expects(:params).returns(params) @resource.expects(:override_parameter).with(:b) @resource.expects(:override_parameter).with(:d) @resource.merge(@override) end it "should succeed when a parent class created the override" do @resource.source = "source3" @override.source = "source4" @override.source.expects(:child_of?).with("source3").returns(true) params = {:a => :b, :c => :d} @override.expects(:params).returns(params) @resource.expects(:override_parameter).with(:b) @resource.expects(:override_parameter).with(:d) @resource.merge(@override) end it "should add new parameters when the parameter is not set" do @source.stubs(:child_of?).returns true @override.set_parameter(:testing, "value") @resource.merge(@override) @resource[:testing].should == "value" end it "should replace existing parameter values" do @source.stubs(:child_of?).returns true @resource.set_parameter(:testing, "old") @override.set_parameter(:testing, "value") @resource.merge(@override) @resource[:testing].should == "value" end it "should add values to the parameter when the override was created with the '+>' syntax" do @source.stubs(:child_of?).returns true param = Puppet::Parser::Resource::Param.new(:name => :testing, :value => "testing", :source => @resource.source) param.add = true @override.set_parameter(param) @resource.set_parameter(:testing, "other") @resource.merge(@override) @resource[:testing].should == %w{other testing} end end it "should be able to be converted to a normal resource" do @source = stub 'scope', :name => "myscope" @resource = mkresource :source => @source @resource.should respond_to(:to_resource) end it "should use its resource converter to convert to a transportable resource" do @source = stub 'scope', :name => "myscope" @resource = mkresource :source => @source newresource = Puppet::Resource.new(:file, "/my") Puppet::Resource.expects(:new).returns(newresource) newresource.expects(:to_trans).returns "mytrans" @resource.to_trans.should == "mytrans" end it "should return nil if converted to a transportable resource and it is virtual" do @source = stub 'scope', :name => "myscope" @resource = mkresource :source => @source @resource.expects(:virtual?).returns true @resource.to_trans.should be_nil end describe "when being converted to a resource" do before do @source = stub 'scope', :name => "myscope" @parser_resource = mkresource :source => @source, :params => {:foo => "bar", :fee => "fum"} end it "should create an instance of Puppet::Resource" do @parser_resource.to_resource.should be_instance_of(Puppet::Resource) end it "should set the type correctly on the Puppet::Resource" do @parser_resource.to_resource.type.should == @parser_resource.type end it "should set the title correctly on the Puppet::Resource" do @parser_resource.to_resource.title.should == @parser_resource.title end it "should copy over all of the parameters" do result = @parser_resource.to_resource.to_hash # The name will be in here, also. result[:foo].should == "bar" result[:fee].should == "fum" end it "should copy over the tags" do @parser_resource.tag "foo" @parser_resource.tag "bar" @parser_resource.to_resource.tags.should == @parser_resource.tags end it "should copy over the line" do @parser_resource.line = 40 @parser_resource.to_resource.line.should == 40 end it "should copy over the file" do @parser_resource.file = "/my/file" @parser_resource.to_resource.file.should == "/my/file" end it "should convert any parser resource references to Puppet::Resource::Reference instances" do ref = Puppet::Parser::Resource::Reference.new(:title => "/my/file", :type => "file") @parser_resource = mkresource :source => @source, :params => {:foo => "bar", :fee => ref} result = @parser_resource.to_resource result[:fee].should == Puppet::Resource::Reference.new(:file, "/my/file") end it "should convert any parser resource references to Puppet::Resource::Reference instances even if they are in an array" do ref = Puppet::Parser::Resource::Reference.new(:title => "/my/file", :type => "file") @parser_resource = mkresource :source => @source, :params => {:foo => "bar", :fee => ["a", ref]} result = @parser_resource.to_resource result[:fee].should == ["a", Puppet::Resource::Reference.new(:file, "/my/file")] end it "should convert any parser resource references to Puppet::Resource::Reference instances even if they are in an array of array, and even deeper" do ref1 = Puppet::Parser::Resource::Reference.new(:title => "/my/file1", :type => "file") ref2 = Puppet::Parser::Resource::Reference.new(:title => "/my/file2", :type => "file") @parser_resource = mkresource :source => @source, :params => {:foo => "bar", :fee => ["a", [ref1,ref2]]} result = @parser_resource.to_resource result[:fee].should == ["a", Puppet::Resource::Reference.new(:file, "/my/file1"), Puppet::Resource::Reference.new(:file, "/my/file2")] end end end diff --git a/spec/unit/parser/resource/reference.rb b/spec/unit/parser/resource/reference.rb index bb1452692..6284e67cc 100755 --- a/spec/unit/parser/resource/reference.rb +++ b/spec/unit/parser/resource/reference.rb @@ -1,95 +1,99 @@ #!/usr/bin/env ruby require File.dirname(__FILE__) + '/../../../spec_helper' describe Puppet::Parser::Resource::Reference do before do @type = Puppet::Parser::Resource::Reference end + it "should use the file lookup module" do + Puppet::Parser::Resource::Reference.ancestors.should be_include(Puppet::FileCollection::Lookup) + end + it "should require a type" do proc { @type.new(:title => "yay") }.should raise_error(Puppet::DevError) end it "should require a title" do proc { @type.new(:type => "file") }.should raise_error(Puppet::DevError) end it "should know when it refers to a builtin type" do ref = @type.new(:type => "file", :title => "/tmp/yay") ref.builtin?.should be_true ref.builtintype.should equal(Puppet::Type.type(:file)) end it "should return a downcased relationship-style resource reference for defined types" do ref = @type.new(:type => "file", :title => "/tmp/yay") ref.to_ref.should == ["file", "/tmp/yay"] end it "should return a capitalized relationship-style resource reference for defined types" do ref = @type.new(:type => "whatever", :title => "/tmp/yay") ref.to_ref.should == ["Whatever", "/tmp/yay"] end it "should return a resource reference string when asked" do ref = @type.new(:type => "file", :title => "/tmp/yay") ref.to_s.should == "File[/tmp/yay]" end it "should canonize resource references" do ref = @type.new(:type => "foo::bar", :title => "/tmp/yay") ref.to_s.should == "Foo::Bar[/tmp/yay]" end end describe Puppet::Parser::Resource::Reference, " when modeling defined types" do before do @type = Puppet::Parser::Resource::Reference @parser = Puppet::Parser::Parser.new :Code => "" @definition = @parser.newdefine "mydefine" @class = @parser.newclass "myclass" @nodedef = @parser.newnode("mynode")[0] @node = Puppet::Node.new("yaynode") @compiler = Puppet::Parser::Compiler.new(@node, @parser) end it "should be able to find defined types" do ref = @type.new(:type => "mydefine", :title => "/tmp/yay", :scope => @compiler.topscope) ref.builtin?.should be_false ref.definedtype.should equal(@definition) end it "should be able to find classes" do ref = @type.new(:type => "class", :title => "myclass", :scope => @compiler.topscope) ref.builtin?.should be_false ref.definedtype.should equal(@class) end it "should be able to find nodes" do ref = @type.new(:type => "node", :title => "mynode", :scope => @compiler.topscope) ref.builtin?.should be_false ref.definedtype.object_id.should == @nodedef.object_id end it "should only look for fully qualified classes" do top = @parser.newclass "top" sub = @parser.newclass "other::top" scope = @compiler.topscope.class.new(:parent => @compiler.topscope, :namespace => "other", :parser => @parser) ref = @type.new(:type => "class", :title => "top", :scope => scope) ref.definedtype.classname.should equal(top.classname) end it "should only look for fully qualified definitions" do top = @parser.newdefine "top" sub = @parser.newdefine "other::top" scope = @compiler.topscope.class.new(:parent => @compiler.topscope, :namespace => "other", :parser => @parser) ref = @type.new(:type => "top", :title => "foo", :scope => scope) ref.definedtype.classname.should equal(top.classname) end end diff --git a/spec/unit/property/list.rb b/spec/unit/property/list.rb index 2fab868db..854ab4874 100644 --- a/spec/unit/property/list.rb +++ b/spec/unit/property/list.rb @@ -1,156 +1,160 @@ #!/usr/bin/env ruby Dir.chdir(File.dirname(__FILE__)) { (s = lambda { |f| File.exist?(f) ? require(f) : Dir.chdir("..") { s.call(f) } }).call("spec/spec_helper.rb") } require 'puppet/property/list' list_class = Puppet::Property::List describe list_class do it "should be a subclass of Property" do list_class.superclass.must == Puppet::Property end describe "as an instance" do before do # Wow that's a messy interface to the resource. list_class.initvars @resource = stub 'resource', :[]= => nil, :property => nil @property = list_class.new(:resource => @resource) end it "should have a , as default delimiter" do @property.delimiter.should == "," end it "should have a :membership as default membership" do @property.membership.should == :membership end it "should return the same value passed into should_to_s" do @property.should_to_s("foo") == "foo" end it "should return the passed in array values joined with the delimiter from is_to_s" do @property.is_to_s(["foo","bar"]).should == "foo,bar" end + it "should be able to correctly convert ':absent' to a string" do + @property.is_to_s(:absent).should == "absent" + end + describe "when adding should to current" do it "should add the arrays when current is an array" do @property.add_should_with_current(["foo"], ["bar"]).should == ["foo", "bar"] end it "should return should if current is not a array" do @property.add_should_with_current(["foo"], :absent).should == ["foo"] end it "should return only the uniq elements" do @property.add_should_with_current(["foo", "bar"], ["foo", "baz"]).should == ["foo", "bar", "baz"] end end describe "when calling inclusive?" do it "should use the membership method to look up on the @resource" do @property.expects(:membership).returns(:membership) @resource.expects(:[]).with(:membership) @property.inclusive? end it "should return true when @resource[membership] == inclusive" do @property.stubs(:membership).returns(:membership) @resource.stubs(:[]).with(:membership).returns(:inclusive) @property.inclusive?.must == true end it "should return false when @resource[membership] != inclusive" do @property.stubs(:membership).returns(:membership) @resource.stubs(:[]).with(:membership).returns(:minimum) @property.inclusive?.must == false end end describe "when calling should" do it "should return nil if @should is nil" do @property.should.must == nil end it "should return the sorted values of @should as a string if inclusive" do @property.should = ["foo", "bar"] @property.expects(:inclusive?).returns(true) @property.should.must == "bar,foo" end it "should return the uniq sorted values of @should + retrieve as a string if !inclusive" do @property.should = ["foo", "bar"] @property.expects(:inclusive?).returns(false) @property.expects(:retrieve).returns(["foo","baz"]) @property.should.must == "bar,baz,foo" end end describe "when calling retrieve" do before do @provider = mock("provider") @property.stubs(:provider).returns(@provider) end it "should send 'name' to the provider" do @provider.expects(:send).with(:group) @property.expects(:name).returns(:group) @property.retrieve end it "should return an array with the provider returned info" do @provider.stubs(:send).with(:group).returns("foo,bar,baz") @property.stubs(:name).returns(:group) @property.retrieve == ["foo", "bar", "baz"] end it "should return :absent when the provider returns :absent" do @provider.stubs(:send).with(:group).returns(:absent) @property.stubs(:name).returns(:group) @property.retrieve == :absent end end describe "when calling insync?" do it "should return true unless @should is defined and not nil" do @property.insync?("foo") == true end it "should return true unless the passed in values is not nil" do @property.should = "foo" @property.insync?(nil) == true end it "should call prepare_is_for_comparison with value passed in and should" do @property.should = "foo" @property.expects(:prepare_is_for_comparison).with("bar") @property.expects(:should) @property.insync?("bar") end it "should return true if prepared value == should value" do @property.should = "bar,foo" @property.expects(:inclusive?).returns(true) @property.insync?(["bar","foo"]).must == true end it "should return false if prepared value != should value" do @property.should = "bar,baz,foo" @property.expects(:inclusive?).returns(true) @property.insync?(["bar","foo"]).must == false end end describe "when calling dearrayify" do it "should sort and join the array with 'delimiter'" do array = mock "array" array.expects(:sort).returns(array) array.expects(:join).with(@property.delimiter) @property.dearrayify(array) end end end end diff --git a/spec/unit/provider/augeas/augeas.rb b/spec/unit/provider/augeas/augeas.rb index affc66676..43fca723c 100644 --- a/spec/unit/provider/augeas/augeas.rb +++ b/spec/unit/provider/augeas/augeas.rb @@ -1,328 +1,366 @@ #!/usr/bin/env ruby require File.dirname(__FILE__) + '/../../../spec_helper' provider_class = Puppet::Type.type(:augeas).provider(:augeas) describe provider_class do describe "command parsing" do it "should break apart a single line into three tokens" do provider = provider_class.new() tokens = provider.parse_commands("set /Jar/Jar Binks") tokens.size.should == 1 tokens[0].size.should == 3 tokens[0][0].should == "set" tokens[0][1].should == "/Jar/Jar" tokens[0][2].should == "Binks" end it "should break apart a multiple line into six tokens" do provider = provider_class.new() tokens = provider.parse_commands("set /Jar/Jar Binks\nrm anakin skywalker") tokens.size.should == 2 tokens[0].size.should == 3 tokens[1].size.should == 3 tokens[0][0].should == "set" tokens[0][1].should == "/Jar/Jar" tokens[0][2].should == "Binks" tokens[1][0].should == "rm" tokens[1][1].should == "anakin" tokens[1][2].should == "skywalker" end it "should handle arrays" do provider = provider_class.new() commands = ["set /Jar/Jar Binks", "rm anakin skywalker"] tokens = provider.parse_commands(commands) tokens.size.should == 2 tokens[0].size.should == 3 tokens[1].size.should == 3 tokens[0][0].should == "set" tokens[0][1].should == "/Jar/Jar" tokens[0][2].should == "Binks" tokens[1][0].should == "rm" tokens[1][1].should == "anakin" tokens[1][2].should == "skywalker" end it "should concat the last values" do provider = provider_class.new() tokens = provider.parse_commands("set /Jar/Jar Binks is my copilot") tokens.size.should == 1 tokens[0].size.should == 3 tokens[0][0].should == "set" tokens[0][1].should == "/Jar/Jar" tokens[0][2].should == "Binks is my copilot" end it "should accept spaces and and single ticks" do provider = provider_class.new() tokens = provider.parse_commands("set 'Jar Jar' Binks") tokens.size.should == 1 tokens[0].size.should == 3 tokens[0][0].should == "set" tokens[0][1].should == "Jar Jar" tokens[0][2].should == "Binks" end it "should accept spaces in the value and and single ticks" do provider = provider_class.new() tokens = provider.parse_commands("set 'Jar Jar' 'Binks is my copilot'") tokens.size.should == 1 tokens[0].size.should == 3 tokens[0][0].should == "set" tokens[0][1].should == "Jar Jar" tokens[0][2].should == "Binks is my copilot" end it "should accept spaces and and double ticks" do provider = provider_class.new() tokens = provider.parse_commands('set "Jar Jar" Binks') tokens.size.should == 1 tokens[0].size.should == 3 tokens[0][0].should == "set" tokens[0][1].should == 'Jar Jar' tokens[0][2].should == 'Binks' end it "should accept spaces in the value and and double ticks" do provider = provider_class.new() tokens = provider.parse_commands('set "Jar Jar" "Binks is my copilot"') tokens.size.should == 1 tokens[0].size.should == 3 tokens[0][0].should == "set" tokens[0][1].should == 'Jar Jar' tokens[0][2].should == 'Binks is my copilot' end it "should accept mixed ticks" do provider = provider_class.new() tokens = provider.parse_commands('set "Jar Jar" "Some \'Test\'"') tokens.size.should == 1 tokens[0].size.should == 3 tokens[0][0].should == "set" tokens[0][1].should == 'Jar Jar' tokens[0][2].should == "Some \'Test\'" end it "should accept only the last value using ticks" do provider = provider_class.new() tokens = provider.parse_commands('set /Jar/Jar "Binks is my copilot"') tokens.size.should == 1 tokens[0].size.should == 3 tokens[0][0].should == "set" tokens[0][1].should == '/Jar/Jar' tokens[0][2].should == "Binks is my copilot" end it "should accept only the first value using ticks" do provider = provider_class.new() tokens = provider.parse_commands('set "Jar Jar" copilot') tokens.size.should == 1 tokens[0].size.should == 3 tokens[0][0].should == "set" tokens[0][1].should == 'Jar Jar' tokens[0][2].should == "copilot" end it "should accept only the first value using ticks and the last values being concatenated" do provider = provider_class.new() tokens = provider.parse_commands('set "Jar Jar" Binks is my copilot') tokens.size.should == 1 tokens[0].size.should == 3 tokens[0][0].should == "set" tokens[0][1].should == 'Jar Jar' tokens[0][2].should == "Binks is my copilot" - end + end end describe "get filters" do before do augeas_stub = stub("augeas", :get => "value") @provider = provider_class.new() - @provider.stubs(:open_augeas).returns(augeas_stub) + @provider.aug= augeas_stub end it "should return false for a = nonmatch" do command = ["get", "fake value", "==", "value"] @provider.process_get(command).should == true end it "should return true for a != match" do command = ["get", "fake value", "!=", "value"] @provider.process_get(command).should == false end it "should return true for a =~ match" do command = ["get", "fake value", "=~", "val*"] @provider.process_get(command).should == true end it "should return false for a == nonmatch" do command = ["get", "fake value", "=~", "num*"] @provider.process_get(command).should == false end end describe "match filters" do before do augeas_stub = stub("augeas", :match => ["set", "of", "values"]) @provider = provider_class.new() - @provider.stubs(:open_augeas).returns(augeas_stub) + @provider.aug= augeas_stub end it "should return true for size match" do command = ["match", "fake value", "size", "==", "3"] @provider.process_match(command).should == true end it "should return false for a size non match" do command = ["match", "fake value", "size", "<", "3"] @provider.process_match(command).should == false end it "should return true for includes match" do command = ["get", "fake value", "include", "values"] @provider.process_match(command).should == true end it "should return false for includes non match" do command = ["get", "fake value", "include", "JarJar"] @provider.process_match(command).should == false end it "should return true for an array match" do command = ["get", "fake value", "==", "['set', 'of', 'values']"] @provider.process_match(command).should == true end it "should return false for an array non match" do command = ["get", "fake value", "==", "['this', 'should', 'not', 'match']"] @provider.process_match(command).should == false end end - describe "need_to_run" do + describe "need to run" do it "should handle no filters" do - resource = stub("resource", :[] => "") + resource = stub("resource") + resource.stubs(:[]).returns(false).then.returns("") + augeas_stub = stub("augeas", :match => ["set", "of", "values"]) + augeas_stub.stubs("close") provider = provider_class.new(resource) + provider.aug= augeas_stub + provider.stubs(:get_augeas_version).returns("0.3.5") provider.need_to_run?.should == true end it "should return true when a get filter matches" do - resource = stub("resource", :[] => "get path == value") + resource = stub("resource") + resource.stubs(:[]).returns(false).then.returns("get path == value") provider = provider_class.new(resource) augeas_stub = stub("augeas", :get => "value") - provider.stubs(:open_augeas).returns(augeas_stub) + augeas_stub.stubs("close") + provider.aug= augeas_stub + provider.stubs(:get_augeas_version).returns("0.3.5") provider.need_to_run?.should == true end it "should return false when a get filter does not match" do - resource = stub("resource", :[] => "get path == another value") + resource = stub("resource") + resource.stubs(:[]).returns(false).then.returns("get path == another value") provider = provider_class.new(resource) augeas_stub = stub("augeas", :get => "value") - provider.stubs(:open_augeas).returns(augeas_stub) + augeas_stub.stubs("close") + provider.aug= augeas_stub + provider.stubs(:get_augeas_version).returns("0.3.5") provider.need_to_run?.should == false end it "should return true when a match filter matches" do - resource = stub("resource", :[] => "match path size == 3") + resource = stub("resource") + resource.stubs(:[]).returns(false).then.returns("match path size == 3") provider = provider_class.new(resource) augeas_stub = stub("augeas", :match => ["set", "of", "values"]) - provider.stubs(:open_augeas).returns(augeas_stub) + augeas_stub.stubs("close") + provider.aug= augeas_stub + provider.stubs(:get_augeas_version).returns("0.3.5") provider.need_to_run?.should == true end it "should return false when a match filter does not match" do - resource = stub("resource", :[] => "match path size == 2") + resource = stub("resource") + resource.stubs(:[]).returns(false).then.returns("match path size == 2") + provider = provider_class.new(resource) + augeas_stub = stub("augeas", :match => ["set", "of", "values"]) + augeas_stub.stubs("close") + provider.aug= augeas_stub + provider.stubs(:get_augeas_version).returns("0.3.5") + provider.need_to_run?.should == false + end + + #This is a copy of the last one, with setting the force to true + it "setting force should not change the above logic" do + resource = stub("resource") + resource.stubs(:[]).returns(true).then.returns("match path size == 2") provider = provider_class.new(resource) augeas_stub = stub("augeas", :match => ["set", "of", "values"]) - provider.stubs(:open_augeas).returns(augeas_stub) + augeas_stub.stubs("close") + provider.aug= augeas_stub + provider.stubs(:get_augeas_version).returns("0.3.5") provider.need_to_run?.should == false end end - describe "augeas integration" do + describe "augeas execution integration" do before do @resource = stub("resource") @provider = provider_class.new(@resource) @augeas = stub("augeas") - @provider.stubs(:open_augeas).returns(@augeas) + @provider.aug= @augeas + @provider.stubs(:get_augeas_version).returns("0.3.5") end it "should handle set commands" do command = [["set", "/Jar/Jar", "Binks"]] context = "/some/path" @resource.expects(:[]).times(2).returns(command).then.returns(context) @augeas.expects(:set).with("/some/path/Jar/Jar", "Binks") @augeas.expects(:save).returns(true) + @augeas.expects(:close) @provider.execute_changes.should == :executed end it "should handle rm commands" do command = [["rm", "/Jar/Jar"]] context = "" @resource.expects(:[]).times(2).returns(command).then.returns(context) @augeas.expects(:rm).with("/Jar/Jar") @augeas.expects(:save).returns(true) + @augeas.expects(:close) @provider.execute_changes.should == :executed end it "should handle remove commands" do command = [["remove", "Jar/Jar"]] context = "" @resource.expects(:[]).times(2).returns(command).then.returns(context) @augeas.expects(:rm).with("/Jar/Jar") @augeas.expects(:save).returns(true) + @augeas.expects(:close) @provider.execute_changes.should == :executed end it "should handle clear commands" do command = [["clear", "/Jar/Jar"]] context = "/foo" @resource.expects(:[]).times(2).returns(command).then.returns(context) @augeas.expects(:clear).with("/foo/Jar/Jar") @augeas.expects(:save).returns(true) + @augeas.expects(:close) @provider.execute_changes.should == :executed - end - + end + it "should handle ins commands with before" do command = [["ins", "Binks", "before /Jar/Jar"]] context = "/foo" @resource.expects(:[]).times(2).returns(command).then.returns(context) @augeas.expects(:insert).with("/foo/Jar/Jar", "Binks", true) @augeas.expects(:save).returns(true) + @augeas.expects(:close) @provider.execute_changes.should == :executed end it "should handle ins commands with before" do command = [["ins", "Binks", "after /Jar/Jar"]] context = "/foo" @resource.expects(:[]).times(2).returns(command).then.returns(context) @augeas.expects(:insert).with("/foo/Jar/Jar", "Binks", false) @augeas.expects(:save).returns(true) + @augeas.expects(:close) @provider.execute_changes.should == :executed end it "should handle ins with no context" do command = [["ins", "Binks", "after /Jar/Jar"]] context = "" # this is the default @resource.expects(:[]).times(2).returns(command).then.returns(context) @augeas.expects(:insert).with("/Jar/Jar", "Binks", false) @augeas.expects(:save).returns(true) + @augeas.expects(:close) @provider.execute_changes.should == :executed - end - + end + it "should handle multiple commands" do command = [["ins", "Binks", "after /Jar/Jar"], ["clear", "/Jar/Jar"]] context = "/foo" @resource.expects(:[]).times(2).returns(command).then.returns(context) @augeas.expects(:insert).with("/foo/Jar/Jar", "Binks", false) - @augeas.expects(:clear).with("/foo/Jar/Jar") + @augeas.expects(:clear).with("/foo/Jar/Jar") @augeas.expects(:save).returns(true) + @augeas.expects(:close) @provider.execute_changes.should == :executed end end end diff --git a/spec/unit/provider/mount/parsed.rb b/spec/unit/provider/mount/parsed.rb index d2c992f33..364e145be 100755 --- a/spec/unit/provider/mount/parsed.rb +++ b/spec/unit/provider/mount/parsed.rb @@ -1,181 +1,182 @@ #!/usr/bin/env ruby # # Created by Luke Kanies on 2007-9-12. # Copyright (c) 2006. All rights reserved. require File.dirname(__FILE__) + '/../../../spec_helper' require 'puppettest/support/utils' require 'puppettest/fileparsing' module ParsedMountTesting include PuppetTest::Support::Utils include PuppetTest::FileParsing def fake_fstab os = Facter['operatingsystem'] if os == "Solaris" name = "solaris.fstab" elsif os == "FreeBSD" name = "freebsd.fstab" else # Catchall for other fstabs name = "linux.fstab" end oldpath = @provider_class.default_target return fakefile(File::join("data/types/mount", name)) end def mkmountargs mount = nil if defined? @pcount @pcount += 1 else @pcount = 1 end args = { :name => "/fspuppet%s" % @pcount, :device => "/dev/dsk%s" % @pcount, } @provider_class.fields(:parsed).each do |field| unless args.include? field args[field] = "fake%s%s" % [field, @pcount] end end return args end def mkmount hash = mkmountargs() #hash[:provider] = @provider_class.name fakeresource = stub :type => :mount, :name => hash[:name] fakeresource.stubs(:[]).with(:name).returns(hash[:name]) fakeresource.stubs(:should).with(:target).returns(nil) mount = @provider_class.new(fakeresource) hash[:record_type] = :parsed hash[:ensure] = :present mount.property_hash = hash return mount end # Here we just create a fake host type that answers to all of the methods # but does not modify our actual system. def mkfaketype @provider.stubs(:filetype).returns(Puppet::Util::FileType.filetype(:ram)) end end provider_class = Puppet::Type.type(:mount).provider(:parsed) describe provider_class do before :each do @mount_class = Puppet::Type.type(:mount) @provider_class = @mount_class.provider(:parsed) end describe provider_class do include ParsedMountTesting it "should be able to parse all of the example mount tabs" do tab = fake_fstab @provider = @provider_class # LAK:FIXME Again, a relatively bad test, but I don't know how to rspec-ify this. # I suppose this is more of an integration test? I dunno. fakedataparse(tab) do # Now just make we've got some mounts we know will be there hashes = @provider_class.target_records(tab).find_all { |i| i.is_a? Hash } (hashes.length > 0).should be_true root = hashes.find { |i| i[:name] == "/" } proc { @provider_class.to_file(hashes) }.should_not raise_error end end # LAK:FIXME I can't mock Facter because this test happens at parse-time. it "should default to /etc/vfstab on Solaris and /etc/fstab everywhere else" do should = case Facter.value(:operatingsystem) when "Solaris"; "/etc/vfstab" else "/etc/fstab" end Puppet::Type.type(:mount).provider(:parsed).default_target.should == should end end describe provider_class, " when mounting an absent filesystem" do include ParsedMountTesting # #730 - Make sure 'flush' is called when a mount is moving from absent to mounted it "should flush the fstab to disk" do mount = mkmount # Mark the mount as absent mount.property_hash[:ensure] = :absent mount.stubs(:mountcmd) # just so we don't actually try to mount anything mount.expects(:flush) mount.mount end end describe provider_class, " when modifying the filesystem tab" do include ParsedMountTesting before do + Puppet.settings.stubs(:use) # Never write to disk, only to RAM. @provider_class.stubs(:filetype).returns(Puppet::Util::FileType.filetype(:ram)) @mount = mkmount @target = @provider_class.default_target end it "should write the mount to disk when :flush is called" do @mount.flush text = @provider_class.target_object(@provider_class.default_target).read text.should == @mount.class.to_line(@mount.property_hash) + "\n" end end describe provider_class, " when parsing information about the root filesystem" do confine "Mount type not tested on Darwin" => Facter["operatingsystem"].value != "Darwin" include ParsedMountTesting before do @mount = @mount_class.new :name => "/" @provider = @mount.provider end it "should have a filesystem tab" do FileTest.should be_exist(@provider_class.default_target) end it "should find the root filesystem" do @provider_class.prefetch("/" => @mount) @mount.provider.property_hash[:ensure].should == :present end it "should determine that the root fs is mounted" do @provider_class.prefetch("/" => @mount) @mount.provider.should be_mounted end end describe provider_class, " when mounting and unmounting" do include ParsedMountTesting it "should call the 'mount' command to mount the filesystem" it "should call the 'unmount' command to unmount the filesystem" it "should specify the filesystem when remounting a filesystem" end end diff --git a/spec/unit/provider/ssh_authorized_key/parsed.rb b/spec/unit/provider/ssh_authorized_key/parsed.rb index ec8f2b96a..18f3be126 100755 --- a/spec/unit/provider/ssh_authorized_key/parsed.rb +++ b/spec/unit/provider/ssh_authorized_key/parsed.rb @@ -1,166 +1,210 @@ #!/usr/bin/env ruby require File.dirname(__FILE__) + '/../../../spec_helper' require 'puppettest' require 'puppettest/support/utils' require 'puppettest/fileparsing' provider_class = Puppet::Type.type(:ssh_authorized_key).provider(:parsed) describe provider_class do include PuppetTest include PuppetTest::FileParsing before :each do @sshauthkey_class = Puppet::Type.type(:ssh_authorized_key) @provider = @sshauthkey_class.provider(:parsed) end after :each do @provider.initvars end def mkkey(args) fakeresource = fakeresource(:ssh_authorized_key, args[:name]) key = @provider.new(fakeresource) args.each do |p,v| key.send(p.to_s + "=", v) end return key end def genkey(key) @provider.filetype = :ram file = @provider.default_target key.flush text = @provider.target_object(file).read return text end it "should be able to parse each example" do fakedata("data/providers/ssh_authorized_key/parsed").each { |file| puts "Parsing %s" % file fakedataparse(file) } end it "should be able to generate a basic authorized_keys file" do key = mkkey({ :name => "Just Testing", :key => "AAAAfsfddsjldjgksdflgkjsfdlgkj", :type => "ssh-dss", :ensure => :present, :options => [:absent] }) genkey(key).should == "ssh-dss AAAAfsfddsjldjgksdflgkjsfdlgkj Just Testing\n" end it "should be able to generate a authorized_keys file with options" do key = mkkey({ :name => "root@localhost", :key => "AAAAfsfddsjldjgksdflgkjsfdlgkj", :type => "ssh-rsa", :ensure => :present, :options => ['from="192.168.1.1"', "no-pty", "no-X11-forwarding"] }) genkey(key).should == "from=\"192.168.1.1\",no-pty,no-X11-forwarding ssh-rsa AAAAfsfddsjldjgksdflgkjsfdlgkj root@localhost\n" end - it "should prefetch ~user/.ssh/authorized_keys when user is given" do - key = Puppet::Type.type(:ssh_authorized_key).create( - :name => "Test", - :key => "AA", - :type => "rsa", - :ensure => :present, - :user => "root") - prov = @provider.new key + it "'s parse_options method should be able to parse options containing commas" do + options = %w{from="host1.reductlivelabs.com,host.reductivelabs.com" command="/usr/local/bin/run" ssh-pty} + optionstr = options.join(", ") - prov.prefetch - prov.target.should == File.expand_path("~root/.ssh/authorized_keys") + @provider.parse_options(optionstr).should == options end +end - it "should create destination dir" do - # No idea how to test the flush method +describe provider_class do + before :each do + @resource = stub("resource", :name => "foo") + @resource.stubs(:[]).returns "foo" + @provider = provider_class.new(@resource) end - it "should set correct default permissions" do - # No idea how to test the flush method - end + describe "when flushing" do + before :each do + # Stub file and directory operations + Dir.stubs(:mkdir) + File.stubs(:chmod) + File.stubs(:chown) + end - it "'s parse_options method should be able to parse options containing commas" do - options = %w{from="host1.reductlivelabs.com,host.reductivelabs.com" command="/usr/local/bin/run" ssh-pty} - optionstr = options.join(", ") + describe "and a user has been specified" do + before :each do + @resource.stubs(:should).with(:user).returns "nobody" + target = File.expand_path("~nobody/.ssh/authorized_keys") + @resource.stubs(:should).with(:target).returns target + end + + it "should create the directory" do + Dir.expects(:mkdir).with(File.expand_path("~nobody/.ssh"), 0700) + @provider.flush + end + + it "should chown the directory to the user" do + uid = Puppet::Util.uid("nobody") + File.expects(:chown).with(uid, nil, File.expand_path("~nobody/.ssh")) + @provider.flush + end + + it "should chown the key file to the user" do + uid = Puppet::Util.uid("nobody") + File.expects(:chown).with(uid, nil, File.expand_path("~nobody/.ssh/authorized_keys")) + @provider.flush + end + + it "should chmod the key file to 0600" do + File.chmod(0600, File.expand_path("~nobody/.ssh/authorized_keys")) + @provider.flush + end + end + + describe "and a target has been specified" do + before :each do + @resource.stubs(:should).with(:user).returns nil + @resource.stubs(:should).with(:target).returns "/tmp/.ssh/authorized_keys" + end + + it "should make the directory" do + Dir.expects(:mkdir).with("/tmp/.ssh", 0755) + @provider.flush + end + + it "should chmod the key file to 0644" do + File.expects(:chmod).with(0644, "/tmp/.ssh/authorized_keys") + @provider.flush + end + end - @provider.parse_options(optionstr).should == options end end describe provider_class do before :each do @resource = stub("resource", :name => "foo") @resource.stubs(:[]).returns "foo" @provider = provider_class.new(@resource) end describe "when flushing" do before :each do # Stub file and directory operations Dir.stubs(:mkdir) File.stubs(:chmod) File.stubs(:chown) end describe "and a user has been specified" do before :each do @resource.stubs(:should).with(:user).returns "nobody" @resource.stubs(:should).with(:target).returns nil end it "should create the directory" do Dir.expects(:mkdir).with(File.expand_path("~nobody/.ssh"), 0700) @provider.flush end it "should chown the directory to the user" do uid = Puppet::Util.uid("nobody") File.expects(:chown).with(uid, nil, File.expand_path("~nobody/.ssh")) @provider.flush end it "should chown the key file to the user" do uid = Puppet::Util.uid("nobody") File.expects(:chown).with(uid, nil, File.expand_path("~nobody/.ssh/authorized_keys")) @provider.flush end it "should chmod the key file to 0600" do File.chmod(0600, File.expand_path("~nobody/.ssh/authorized_keys")) @provider.flush end end describe "and a target has been specified" do before :each do @resource.stubs(:should).with(:user).returns nil @resource.stubs(:should).with(:target).returns "/tmp/.ssh/authorized_keys" end it "should make the directory" do Dir.expects(:mkdir).with("/tmp/.ssh", 0755) @provider.flush end it "should chmod the key file to 0644" do File.expects(:chmod).with(0644, "/tmp/.ssh/authorized_keys") @provider.flush end end end end diff --git a/spec/unit/rails.rb b/spec/unit/rails.rb index f0ba2b942..48e0ab321 100755 --- a/spec/unit/rails.rb +++ b/spec/unit/rails.rb @@ -1,125 +1,126 @@ #!/usr/bin/env ruby require File.dirname(__FILE__) + '/../spec_helper' require 'puppet/rails' describe Puppet::Rails, "when initializing any connection" do confine "Cannot test without ActiveRecord" => Puppet.features.rails? before do - @logger = stub 'logger', :level= => nil + Puppet.settings.stubs(:use) + @logger = mock 'logger' @logger.stub_everything Logger.stubs(:new).returns(@logger) ActiveRecord::Base.stubs(:logger).returns(@logger) end it "should use settings" do Puppet.settings.expects(:use).with(:main, :rails, :puppetmasterd) Puppet::Rails.connect end it "should set up a logger with the appropriate Rails log file" do logger = mock 'logger' Logger.expects(:new).with(Puppet[:railslog]).returns(logger) ActiveRecord::Base.expects(:logger=).with(logger) Puppet::Rails.connect end it "should set the log level to whatever the value is in the settings" do Puppet.settings.stubs(:use) Puppet.settings.stubs(:value).with(:rails_loglevel).returns("debug") Puppet.settings.stubs(:value).with(:railslog).returns("/my/file") logger = mock 'logger' Logger.stubs(:new).returns(logger) ActiveRecord::Base.stubs(:logger).returns(logger) logger.expects(:level=).with(Logger::DEBUG) ActiveRecord::Base.stubs(:allow_concurrency=) ActiveRecord::Base.stubs(:verify_active_connections!) ActiveRecord::Base.stubs(:establish_connection) Puppet::Rails.stubs(:database_arguments) Puppet::Rails.connect end it "should set ActiveRecord::Base.allow_concurrency" do ActiveRecord::Base.expects(:allow_concurrency=).with(true) Puppet::Rails.connect end it "should call ActiveRecord::Base.verify_active_connections!" do ActiveRecord::Base.expects(:verify_active_connections!) Puppet::Rails.connect end it "should call ActiveRecord::Base.establish_connection with database_arguments" do Puppet::Rails.expects(:database_arguments) ActiveRecord::Base.expects(:establish_connection) Puppet::Rails.connect end end describe Puppet::Rails, "when initializing a sqlite3 connection" do confine "Cannot test without ActiveRecord" => Puppet.features.rails? it "should provide the adapter, log_level, and dbfile arguments" do Puppet.settings.expects(:value).with(:dbadapter).returns("sqlite3") Puppet.settings.expects(:value).with(:rails_loglevel).returns("testlevel") Puppet.settings.expects(:value).with(:dblocation).returns("testlocation") Puppet::Rails.database_arguments.should == { :adapter => "sqlite3", :log_level => "testlevel", :dbfile => "testlocation" } end end describe Puppet::Rails, "when initializing a mysql or postgresql connection" do confine "Cannot test without ActiveRecord" => Puppet.features.rails? it "should provide the adapter, log_level, and host, username, password, and database arguments" do Puppet.settings.stubs(:value).with(:dbadapter).returns("mysql") Puppet.settings.stubs(:value).with(:rails_loglevel).returns("testlevel") Puppet.settings.stubs(:value).with(:dbserver).returns("testserver") Puppet.settings.stubs(:value).with(:dbuser).returns("testuser") Puppet.settings.stubs(:value).with(:dbpassword).returns("testpassword") Puppet.settings.stubs(:value).with(:dbname).returns("testname") Puppet.settings.stubs(:value).with(:dbsocket).returns("") Puppet::Rails.database_arguments.should == { :adapter => "mysql", :log_level => "testlevel", :host => "testserver", :username => "testuser", :password => "testpassword", :database => "testname" } end it "should provide the adapter, log_level, and host, username, password, database, and socket arguments" do Puppet.settings.stubs(:value).with(:dbadapter).returns("mysql") Puppet.settings.stubs(:value).with(:rails_loglevel).returns("testlevel") Puppet.settings.stubs(:value).with(:dbserver).returns("testserver") Puppet.settings.stubs(:value).with(:dbuser).returns("testuser") Puppet.settings.stubs(:value).with(:dbpassword).returns("testpassword") Puppet.settings.stubs(:value).with(:dbname).returns("testname") Puppet.settings.stubs(:value).with(:dbsocket).returns("testsocket") Puppet::Rails.database_arguments.should == { :adapter => "mysql", :log_level => "testlevel", :host => "testserver", :username => "testuser", :password => "testpassword", :database => "testname", :socket => "testsocket" } end end diff --git a/spec/unit/transaction.rb b/spec/unit/transaction.rb index 554d20ce9..60705c7fb 100755 --- a/spec/unit/transaction.rb +++ b/spec/unit/transaction.rb @@ -1,33 +1,48 @@ #!/usr/bin/env ruby require File.dirname(__FILE__) + '/../spec_helper' require 'puppet/transaction' +describe Puppet::Transaction do + it "should match resources by name, not title, when prefetching" do + @catalog = Puppet::Resource::Catalog.new + @transaction = Puppet::Transaction.new(@catalog) + + # Have both a title and name + resource = Puppet::Type.type(:sshkey).create :title => "foo", :name => "bar", :type => :dsa, :key => "eh" + @catalog.add_resource resource + + resource.provider.class.expects(:prefetch).with("bar" => resource) + + @transaction.prefetch + end +end + describe Puppet::Transaction, " when determining tags" do before do @config = Puppet::Resource::Catalog.new @transaction = Puppet::Transaction.new(@config) end it "should default to the tags specified in the :tags setting" do Puppet.expects(:[]).with(:tags).returns("one") @transaction.tags.should == %w{one} end it "should split tags based on ','" do Puppet.expects(:[]).with(:tags).returns("one,two") @transaction.tags.should == %w{one two} end it "should use any tags set after creation" do Puppet.expects(:[]).with(:tags).never @transaction.tags = %w{one two} @transaction.tags.should == %w{one two} end it "should always convert assigned tags to an array" do @transaction.tags = "one::two" @transaction.tags.should == %w{one::two} end end diff --git a/spec/unit/type/file.rb b/spec/unit/type/file.rb index 0187cc41d..094596966 100755 --- a/spec/unit/type/file.rb +++ b/spec/unit/type/file.rb @@ -1,653 +1,655 @@ #!/usr/bin/env ruby require File.dirname(__FILE__) + '/../../spec_helper' describe Puppet::Type.type(:file) do before do + Puppet.settings.stubs(:use) + @path = Tempfile.new("puppetspec") pathname = @path.path @path.close!() @path = pathname @file = Puppet::Type::File.new(:name => @path) @catalog = mock 'catalog' @catalog.stub_everything @file.catalog = @catalog end it "should have a method for determining if the file is present" do @file.must respond_to(:exist?) end it "should be considered existent if it can be stat'ed" do @file.expects(:stat).returns mock('stat') @file.must be_exist end it "should be considered nonexistent if it can not be stat'ed" do @file.expects(:stat).returns nil @file.must_not be_exist end it "should have a method for determining if the file should be a normal file" do @file.must respond_to(:should_be_file?) end it "should be a file if :ensure is set to :file" do @file[:ensure] = :file @file.must be_should_be_file end it "should be a file if :ensure is set to :present and the file exists as a normal file" do @file.stubs(:stat).returns(mock('stat', :ftype => "file")) @file[:ensure] = :present @file.must be_should_be_file end it "should not be a file if :ensure is set to something other than :file" do @file[:ensure] = :directory @file.must_not be_should_be_file end it "should not be a file if :ensure is set to :present and the file exists but is not a normal file" do @file.stubs(:stat).returns(mock('stat', :ftype => "directory")) @file[:ensure] = :present @file.must_not be_should_be_file end it "should be a file if :ensure is not set and :content is" do @file[:content] = "foo" @file.must be_should_be_file end it "should be a file if neither :ensure nor :content is set but the file exists as a normal file" do @file.stubs(:stat).returns(mock("stat", :ftype => "file")) @file.must be_should_be_file end it "should not be a file if neither :ensure nor :content is set but the file exists but not as a normal file" do @file.stubs(:stat).returns(mock("stat", :ftype => "directory")) @file.must_not be_should_be_file end describe "when validating attributes" do %w{path backup recurse recurselimit source replace force ignore links purge sourceselect}.each do |attr| it "should have a '#{attr}' parameter" do Puppet::Type.type(:file).attrtype(attr.intern).should == :param end end %w{checksum content target ensure owner group mode type}.each do |attr| it "should have a '#{attr}' property" do Puppet::Type.type(:file).attrtype(attr.intern).should == :property end end it "should have its 'path' attribute set as its namevar" do Puppet::Type.type(:file).namevar.should == :path end end describe "when managing links" do require 'puppettest/support/assertions' include PuppetTest require 'tempfile' before do @basedir = tempfile Dir.mkdir(@basedir) @file = File.join(@basedir, "file") @link = File.join(@basedir, "link") File.open(@file, "w", 0644) { |f| f.puts "yayness"; f.flush } File.symlink(@file, @link) @resource = Puppet::Type.type(:file).new( :path => @link, :mode => "755" ) @catalog = Puppet::Resource::Catalog.new @catalog.add_resource @resource end after do remove_tmp_files end it "should default to managing the link" do @catalog.apply # I convert them to strings so they display correctly if there's an error. ("%o" % (File.stat(@file).mode & 007777)).should == "%o" % 0644 end it "should be able to follow links" do @resource[:links] = :follow @catalog.apply ("%o" % (File.stat(@file).mode & 007777)).should == "%o" % 0755 end end it "should be able to retrieve a stat instance for the file it is managing" do Puppet::Type.type(:file).new(:path => "/foo/bar", :source => "/bar/foo").should respond_to(:stat) end describe "when stat'ing its file" do before do @resource = Puppet::Type.type(:file).new(:path => "/foo/bar") @resource[:links] = :manage # so we always use :lstat end it "should use :stat if it is following links" do @resource[:links] = :follow File.expects(:stat) @resource.stat end it "should use :lstat if is it not following links" do @resource[:links] = :manage File.expects(:lstat) @resource.stat end it "should stat the path of the file" do File.expects(:lstat).with("/foo/bar") @resource.stat end # This only happens in testing. it "should return nil if the stat does not exist" do File.expects(:lstat).returns nil @resource.stat.should be_nil end it "should return nil if the file does not exist" do File.expects(:lstat).raises(Errno::ENOENT) @resource.stat.should be_nil end it "should return nil if the file cannot be stat'ed" do File.expects(:lstat).raises(Errno::EACCES) @resource.stat.should be_nil end it "should return the stat instance" do File.expects(:lstat).returns "mystat" @resource.stat.should == "mystat" end it "should cache the stat instance if it has a catalog and is applying" do stat = mock 'stat' File.expects(:lstat).returns stat catalog = Puppet::Resource::Catalog.new @resource.catalog = catalog catalog.stubs(:applying?).returns true @resource.stat.should equal(@resource.stat) end end describe "when flushing" do it "should flush all properties that respond to :flush" do @resource = Puppet::Type.type(:file).new(:path => "/foo/bar", :source => "/bar/foo") @resource.parameter(:source).expects(:flush) @resource.flush end it "should reset its stat reference" do @resource = Puppet::Type.type(:file).new(:path => "/foo/bar") File.expects(:lstat).times(2).returns("stat1").then.returns("stat2") @resource.stat.should == "stat1" @resource.flush @resource.stat.should == "stat2" end end it "should have a method for performing recursion" do @file.must respond_to(:perform_recursion) end describe "when executing a recursive search" do it "should use Metadata to do its recursion" do Puppet::FileServing::Metadata.expects(:search) @file.perform_recursion(@file[:path]) end it "should use the provided path as the key to the search" do Puppet::FileServing::Metadata.expects(:search).with { |key, options| key == "/foo" } @file.perform_recursion("/foo") end it "should return the results of the metadata search" do Puppet::FileServing::Metadata.expects(:search).returns "foobar" @file.perform_recursion(@file[:path]).should == "foobar" end it "should pass its recursion value to the search" do @file[:recurse] = true Puppet::FileServing::Metadata.expects(:search).with { |key, options| options[:recurse] == true } @file.perform_recursion(@file[:path]) end it "should pass true if recursion is remote" do @file[:recurse] = :remote Puppet::FileServing::Metadata.expects(:search).with { |key, options| options[:recurse] == true } @file.perform_recursion(@file[:path]) end it "should pass its recursion limit value to the search" do @file[:recurselimit] = 10 Puppet::FileServing::Metadata.expects(:search).with { |key, options| options[:recurselimit] == 10 } @file.perform_recursion(@file[:path]) end it "should configure the search to ignore or manage links" do @file[:links] = :manage Puppet::FileServing::Metadata.expects(:search).with { |key, options| options[:links] == :manage } @file.perform_recursion(@file[:path]) end it "should pass its 'ignore' setting to the search if it has one" do @file[:ignore] = %w{.svn CVS} Puppet::FileServing::Metadata.expects(:search).with { |key, options| options[:ignore] == %w{.svn CVS} } @file.perform_recursion(@file[:path]) end end it "should have a method for performing local recursion" do @file.must respond_to(:recurse_local) end describe "when doing local recursion" do before do @metadata = stub 'metadata', :relative_path => "my/file" end it "should pass its path to the :perform_recursion method" do @file.expects(:perform_recursion).with(@file[:path]).returns [@metadata] @file.stubs(:newchild) @file.recurse_local end it "should return an empty hash if the recursion returns nothing" do @file.expects(:perform_recursion).returns nil @file.recurse_local.should == {} end it "should create a new child resource with each generated metadata instance's relative path" do @file.expects(:perform_recursion).returns [@metadata] @file.expects(:newchild).with(@metadata.relative_path).returns "fiebar" @file.recurse_local end it "should not create a new child resource for the '.' directory" do @metadata.stubs(:relative_path).returns "." @file.expects(:perform_recursion).returns [@metadata] @file.expects(:newchild).never @file.recurse_local end it "should return a hash of the created resources with the relative paths as the hash keys" do @file.expects(:perform_recursion).returns [@metadata] @file.expects(:newchild).with("my/file").returns "fiebar" @file.recurse_local.should == {"my/file" => "fiebar"} end end it "should have a method for performing link recursion" do @file.must respond_to(:recurse_link) end describe "when doing link recursion" do before do @first = stub 'first', :relative_path => "first", :full_path => "/my/first", :ftype => "directory" @second = stub 'second', :relative_path => "second", :full_path => "/my/second", :ftype => "file" @resource = stub 'file', :[]= => nil end it "should pass its target to the :perform_recursion method" do @file[:target] = "mylinks" @file.expects(:perform_recursion).with("mylinks").returns [@first] @file.stubs(:newchild).returns @resource @file.recurse_link({}) end it "should ignore the recursively-found '.' file and configure the top-level file to create a directory" do @first.stubs(:relative_path).returns "." @file[:target] = "mylinks" @file.expects(:perform_recursion).with("mylinks").returns [@first] @file.stubs(:newchild).never @file.expects(:[]=).with(:ensure, :directory) @file.recurse_link({}) end it "should create a new child resource for each generated metadata instance's relative path that doesn't already exist in the children hash" do @file.expects(:perform_recursion).returns [@first, @second] @file.expects(:newchild).with(@first.relative_path).returns @resource @file.recurse_link("second" => @resource) end it "should not create a new child resource for paths that already exist in the children hash" do @file.expects(:perform_recursion).returns [@first] @file.expects(:newchild).never @file.recurse_link("first" => @resource) end it "should set the target to the full path of discovered file and set :ensure to :link if the file is not a directory" do file = stub 'file' file.expects(:[]=).with(:target, "/my/second") file.expects(:[]=).with(:ensure, :link) @file.stubs(:perform_recursion).returns [@first, @second] @file.recurse_link("first" => @resource, "second" => file) end it "should :ensure to :directory if the file is a directory" do file = stub 'file' file.expects(:[]=).with(:ensure, :directory) @file.stubs(:perform_recursion).returns [@first, @second] @file.recurse_link("first" => file, "second" => @resource) end it "should return a hash with both created and existing resources with the relative paths as the hash keys" do file = stub 'file', :[]= => nil @file.expects(:perform_recursion).returns [@first, @second] @file.stubs(:newchild).returns file @file.recurse_link("second" => @resource).should == {"second" => @resource, "first" => file} end end it "should have a method for performing remote recursion" do @file.must respond_to(:recurse_remote) end describe "when doing remote recursion" do before do @file[:source] = "puppet://foo/bar" @first = Puppet::FileServing::Metadata.new("/my", :relative_path => "first") @second = Puppet::FileServing::Metadata.new("/my", :relative_path => "second") @first.stubs(:ftype).returns "directory" @second.stubs(:ftype).returns "directory" @parameter = stub 'property', :metadata= => nil @resource = stub 'file', :[]= => nil, :parameter => @parameter end it "should pass its source to the :perform_recursion method" do data = Puppet::FileServing::Metadata.new("/whatever", :relative_path => "foobar") @file.expects(:perform_recursion).with("puppet://foo/bar").returns [data] @file.stubs(:newchild).returns @resource @file.recurse_remote({}) end it "should not recurse when the remote file is not a directory" do data = Puppet::FileServing::Metadata.new("/whatever", :relative_path => ".") data.stubs(:ftype).returns "file" @file.expects(:perform_recursion).with("puppet://foo/bar").returns [data] @file.expects(:newchild).never @file.recurse_remote({}) end it "should set the source of each returned file to the searched-for URI plus the found relative path" do @first.expects(:source=).with File.join("puppet://foo/bar", @first.relative_path) @file.expects(:perform_recursion).returns [@first] @file.stubs(:newchild).returns @resource @file.recurse_remote({}) end it "should create a new resource for any relative file paths that do not already have a resource" do @file.stubs(:perform_recursion).returns [@first] @file.expects(:newchild).with("first").returns @resource @file.recurse_remote({}).should == {"first" => @resource} end it "should not create a new resource for any relative file paths that do already have a resource" do @file.stubs(:perform_recursion).returns [@first] @file.expects(:newchild).never @file.recurse_remote("first" => @resource) end it "should set the source of each resource to the source of the metadata" do @file.stubs(:perform_recursion).returns [@first] @resource.stubs(:[]=) @resource.expects(:[]=).with(:source, File.join("puppet://foo/bar", @first.relative_path)) @file.recurse_remote("first" => @resource) end # LAK:FIXME This is a bug, but I can't think of a fix for it. Fortunately it's already # filed, and when it's fixed, we'll just fix the whole flow. it "should set the checksum type to :md5 if the remote file is a file" do @first.stubs(:ftype).returns "file" @file.stubs(:perform_recursion).returns [@first] @resource.stubs(:[]=) @resource.expects(:[]=).with(:checksum, :md5) @file.recurse_remote("first" => @resource) end it "should store the metadata in the source property for each resource so the source does not have to requery the metadata" do @file.stubs(:perform_recursion).returns [@first] @resource.expects(:parameter).with(:source).returns @parameter @parameter.expects(:metadata=).with(@first) @file.recurse_remote("first" => @resource) end it "should not create a new resource for the '.' file" do @first.stubs(:relative_path).returns "." @file.stubs(:perform_recursion).returns [@first] @file.expects(:newchild).never @file.recurse_remote({}) end it "should store the metadata in the main file's source property if the relative path is '.'" do @first.stubs(:relative_path).returns "." @file.stubs(:perform_recursion).returns [@first] @file.parameter(:source).expects(:metadata=).with @first @file.recurse_remote("first" => @resource) end describe "and purging is enabled" do before do @file[:purge] = true end it "should configure each file not on the remote system to be removed" do @file.stubs(:perform_recursion).returns [@second] @resource.expects(:[]=).with(:ensure, :absent) @file.expects(:newchild).returns stub('secondfile', :[]= => nil, :parameter => @parameter) @file.recurse_remote("first" => @resource) end end describe "and multiple sources are provided" do describe "and :sourceselect is set to :first" do it "should create file instances for the results for the first source to return any values" do data = Puppet::FileServing::Metadata.new("/whatever", :relative_path => "foobar") @file[:source] = %w{/one /two /three /four} @file.expects(:perform_recursion).with("/one").returns nil @file.expects(:perform_recursion).with("/two").returns [] @file.expects(:perform_recursion).with("/three").returns [data] @file.expects(:perform_recursion).with("/four").never @file.expects(:newchild).with("foobar").returns @resource @file.recurse_remote({}) end end describe "and :sourceselect is set to :all" do before do @file[:sourceselect] = :all end it "should return every found file that is not in a previous source" do klass = Puppet::FileServing::Metadata @file[:source] = %w{/one /two /three /four} @file.stubs(:newchild).returns @resource one = [klass.new("/one", :relative_path => "a")] @file.expects(:perform_recursion).with("/one").returns one @file.expects(:newchild).with("a").returns @resource two = [klass.new("/two", :relative_path => "a"), klass.new("/two", :relative_path => "b")] @file.expects(:perform_recursion).with("/two").returns two @file.expects(:newchild).with("b").returns @resource three = [klass.new("/three", :relative_path => "a"), klass.new("/three", :relative_path => "c")] @file.expects(:perform_recursion).with("/three").returns three @file.expects(:newchild).with("c").returns @resource @file.expects(:perform_recursion).with("/four").returns [] @file.recurse_remote({}) end end end end describe "when returning resources with :eval_generate" do before do @catalog = mock 'catalog' @catalog.stub_everything @graph = stub 'graph', :add_edge => nil @catalog.stubs(:relationship_graph).returns @graph @file.catalog = @catalog @file[:recurse] = true end it "should recurse if recursion is enabled" do resource = stub('resource', :[] => "resource") @file.expects(:recurse?).returns true @file.expects(:recurse).returns [resource] @file.eval_generate.should == [resource] end it "should not recurse if recursion is disabled" do @file.expects(:recurse?).returns false @file.expects(:recurse).never @file.eval_generate.should == [] end it "should return each resource found through recursion" do foo = stub 'foo', :[] => "/foo" bar = stub 'bar', :[] => "/bar" bar2 = stub 'bar2', :[] => "/bar" @file.expects(:recurse).returns [foo, bar] @file.eval_generate.should == [foo, bar] end end describe "when recursing" do before do @file[:recurse] = true @metadata = Puppet::FileServing::Metadata end describe "and a source is set" do before { @file[:source] = "/my/source" } it "should pass the already-discovered resources to recurse_remote" do @file.stubs(:recurse_local).returns(:foo => "bar") @file.expects(:recurse_remote).with(:foo => "bar").returns [] @file.recurse end end describe "and a target is set" do before { @file[:target] = "/link/target" } it "should use recurse_link" do @file.stubs(:recurse_local).returns(:foo => "bar") @file.expects(:recurse_link).with(:foo => "bar").returns [] @file.recurse end end it "should use recurse_local if recurse is not remote" do @file.expects(:recurse_local).returns({}) @file.recurse end it "should not use recurse_local if recurse remote" do @file[:recurse] = :remote @file.expects(:recurse_local).never @file.recurse end it "should return the generated resources as an array sorted by file path" do one = stub 'one', :[] => "/one" two = stub 'two', :[] => "/one/two" three = stub 'three', :[] => "/three" @file.expects(:recurse_local).returns(:one => one, :two => two, :three => three) @file.recurse.should == [one, two, three] end describe "and making a new child resource" do it "should create an implicit resource using the provided relative path joined with the file's path" do @file.newchild("my/path").should be_implicit end it "should not copy the parent resource's parent" do Puppet::Type.type(:file).expects(:new).with { |options| ! options.include?(:parent) } @file.newchild("my/path") end {:recurse => true, :target => "/foo/bar", :ensure => :present, :alias => "yay", :source => "/foo/bar"}.each do |param, value| it "should not pass on #{param} to the sub resource" do @file = Puppet::Type::File.new(:name => @path, param => value, :catalog => @catalog) @file.class.expects(:new).with { |params| params[param].nil? } @file.newchild("sub/file") end end it "should copy all of the parent resource's 'should' values that were set at initialization" do file = @file.class.new(:path => "/foo/bar", :owner => "root", :group => "wheel") @catalog.add_resource(file) file.class.expects(:new).with { |options| options[:owner] == "root" and options[:group] == "wheel" } file.newchild("my/path") end it "should not copy default values to the new child" do @file.class.expects(:new).with { |params| params[:backup].nil? } @file.newchild("my/path") end it "should not copy values to the child which were set by the source" do @file[:source] = "/foo/bar" metadata = stub 'metadata', :owner => "root", :group => "root", :mode => 0755, :ftype => "file", :checksum => "{md5}whatever" @file.parameter(:source).stubs(:metadata).returns metadata @file.parameter(:source).copy_source_values @file.class.expects(:new).with { |params| params[:group].nil? } @file.newchild("my/path") end end end end diff --git a/spec/unit/type/group.rb b/spec/unit/type/group.rb index c9a7fc904..fad7a0c48 100755 --- a/spec/unit/type/group.rb +++ b/spec/unit/type/group.rb @@ -1,36 +1,39 @@ #!/usr/bin/env ruby require File.dirname(__FILE__) + '/../../spec_helper' describe Puppet::Type.type(:group) do before do + unless ENV["PATH"].split(File::PATH_SEPARATOR).include?("/usr/sbin") + ENV["PATH"] += File::PATH_SEPARATOR + "/usr/sbin" + end @class = Puppet::Type.type(:group) end it "should have a default provider" do @class.defaultprovider.should_not be_nil end it "should have a default provider inheriting from Puppet::Provider" do @class.defaultprovider.ancestors.should be_include(Puppet::Provider) end describe "when validating attributes" do [:name, :allowdupe].each do |param| it "should have a #{param} parameter" do @class.attrtype(param).should == :param end end [:ensure, :gid].each do |param| it "should have a #{param} property" do @class.attrtype(param).should == :property end end end # #1407 - we need to declare the allowdupe param as boolean. it "should have a boolean method for determining if duplicates are allowed" do @class.new(:name => "foo").methods.should be_include("allowdupe?") end end diff --git a/spec/unit/type/noop_metaparam.rb b/spec/unit/type/noop_metaparam.rb index 98cb0409e..2c4b6dc49 100755 --- a/spec/unit/type/noop_metaparam.rb +++ b/spec/unit/type/noop_metaparam.rb @@ -1,36 +1,37 @@ #!/usr/bin/env ruby require File.dirname(__FILE__) + '/../../spec_helper' require 'puppet/type' describe Puppet::Type.type(:file).attrclass(:noop) do before do + Puppet.settings.stubs(:use) @file = Puppet::Type.newfile :path => "/what/ever" end it "should accept true as a value" do lambda { @file[:noop] = true }.should_not raise_error end it "should accept false as a value" do lambda { @file[:noop] = false }.should_not raise_error end describe "when set on a resource" do it "should default to the :noop setting" do Puppet.settings.expects(:value).with(:noop).returns "myval" @file.noop.should == "myval" end it "should prefer true values from the attribute" do @file[:noop] = true @file.noop.should be_true end it "should prefer false values from the attribute" do @file[:noop] = false @file.noop.should be_false end end end diff --git a/spec/unit/type/ssh_authorized_key.rb b/spec/unit/type/ssh_authorized_key.rb index 9a2389eef..db389864e 100755 --- a/spec/unit/type/ssh_authorized_key.rb +++ b/spec/unit/type/ssh_authorized_key.rb @@ -1,101 +1,124 @@ #!/usr/bin/env ruby require File.dirname(__FILE__) + '/../../spec_helper' ssh_authorized_key = Puppet::Type.type(:ssh_authorized_key) describe ssh_authorized_key do before do @class = Puppet::Type.type(:ssh_authorized_key) @provider_class = stub 'provider_class', :name => "fake", :suitable? => true, :supports_parameter? => true @class.stubs(:defaultprovider).returns(@provider_class) @class.stubs(:provider).returns(@provider_class) @provider = stub 'provider', :class => @provider_class, :file_path => "/tmp/whatever", :clear => nil @provider_class.stubs(:new).returns(@provider) @catalog = Puppet::Resource::Catalog.new end it "should have a name parameter" do @class.attrtype(:name).should == :param end it "should have :name be its namevar" do @class.namevar.should == :name end it "should have a :provider parameter" do @class.attrtype(:provider).should == :param end it "should have an ensure property" do @class.attrtype(:ensure).should == :property end it "should support :present as a value for :ensure" do proc { @class.new(:name => "whev", :ensure => :present, :user => "nobody") }.should_not raise_error end it "should support :absent as a value for :ensure" do proc { @class.new(:name => "whev", :ensure => :absent, :user => "nobody") }.should_not raise_error end it "should have an type property" do @class.attrtype(:type).should == :property end it "should support ssh-dss as an type value" do proc { @class.new(:name => "whev", :type => "ssh-dss", :user => "nobody") }.should_not raise_error end it "should support ssh-rsa as an type value" do proc { @class.new(:name => "whev", :type => "ssh-rsa", :user => "nobody") }.should_not raise_error end it "should support :dsa as an type value" do proc { @class.new(:name => "whev", :type => :dsa, :user => "nobody") }.should_not raise_error end it "should support :rsa as an type value" do proc { @class.new(:name => "whev", :type => :rsa, :user => "nobody") }.should_not raise_error end it "should not support values other than ssh-dss, ssh-rsa, dsa, rsa in the ssh_authorized_key_type" do proc { @class.new(:name => "whev", :type => :something) }.should raise_error(Puppet::Error) end it "should have an key property" do @class.attrtype(:key).should == :property end it "should have an user property" do @class.attrtype(:user).should == :property end it "should have an options property" do @class.attrtype(:options).should == :property end it "'s options property should return well formed string of arrays from is_to_s" do resource = @class.new(:name => "whev", :type => :rsa, :user => "nobody", :options => ["a","b","c"]) resource.property(:options).is_to_s(["a","b","c"]).should == "a,b,c" end it "'s options property should return well formed string of arrays from is_to_s" do resource = @class.new(:name => "whev", :type => :rsa, :user => "nobody", :options => ["a","b","c"]) resource.property(:options).should_to_s(["a","b","c"]).should == "a,b,c" end it "should have a target property" do @class.attrtype(:target).should == :property end - it "should raise an error when neither user nor target is given" do - proc do - @class.new( - :name => "Test", - :key => "AAA", - :type => "ssh-rsa", - :ensure => :present) - end.should raise_error(Puppet::Error) + describe "when neither user nor target is specified" do + it "should raise an error" do + proc do + @class.create( + :name => "Test", + :key => "AAA", + :type => "ssh-rsa", + :ensure => :present) + end.should raise_error(Puppet::Error) + end + end + + describe "when both target and user are specified" do + it "should use target" do + resource = @class.create( + :name => "Test", + :user => "root", + :target => "/tmp/blah") + resource.should(:target).should == "/tmp/blah" + end + end + + + describe "when user is specified" do + it "should determine target" do + resource = @class.create( + :name => "Test", + :user => "root") + target = File.expand_path("~root/.ssh/authorized_keys") + resource.should(:target).should == target + end end end diff --git a/spec/unit/type/tidy.rb b/spec/unit/type/tidy.rb index 72f6b0f10..cf244ca0e 100755 --- a/spec/unit/type/tidy.rb +++ b/spec/unit/type/tidy.rb @@ -1,374 +1,380 @@ #!/usr/bin/env ruby require File.dirname(__FILE__) + '/../../spec_helper' -describe Puppet::Type.type(:tidy) do +tidy = Puppet::Type.type(:tidy) + +describe tidy do + before do + Puppet.settings.stubs(:use) + end + it "should use :lstat when stating a file" do - tidy = Puppet::Type.type(:tidy).new :path => "/foo/bar", :age => "1d" + resource = tidy.new :path => "/foo/bar", :age => "1d" stat = mock 'stat' File.expects(:lstat).with("/foo/bar").returns stat - tidy.stat("/foo/bar").should == stat + resource.stat("/foo/bar").should == stat end - + [:age, :size, :path, :matches, :type, :recurse, :rmdirs].each do |param| it "should have a %s parameter" % param do Puppet::Type.type(:tidy).attrclass(param).ancestors.should be_include(Puppet::Parameter) end it "should have documentation for its %s param" % param do Puppet::Type.type(:tidy).attrclass(param).doc.should be_instance_of(String) end end describe "when validating parameter values" do describe "for 'recurse'" do before do @tidy = Puppet::Type.type(:tidy).new :path => "/tmp", :age => "100d" end it "should allow 'true'" do lambda { @tidy[:recurse] = true }.should_not raise_error end it "should allow 'false'" do lambda { @tidy[:recurse] = false }.should_not raise_error end it "should allow integers" do lambda { @tidy[:recurse] = 10 }.should_not raise_error end it "should allow string representations of integers" do lambda { @tidy[:recurse] = "10" }.should_not raise_error end it "should allow 'inf'" do lambda { @tidy[:recurse] = "inf" }.should_not raise_error end it "should not allow arbitrary values" do lambda { @tidy[:recurse] = "whatever" }.should raise_error end end end describe "when matching files by age" do convertors = { :second => 1, :minute => 60 } convertors[:hour] = convertors[:minute] * 60 convertors[:day] = convertors[:hour] * 24 convertors[:week] = convertors[:day] * 7 convertors.each do |unit, multiple| it "should consider a %s to be %s seconds" % [unit, multiple] do tidy = Puppet::Type.type(:tidy).new :path => "/what/ever", :age => "5%s" % unit.to_s[0..0] tidy[:age].should == 5 * multiple end end end describe "when matching files by size" do convertors = { :b => 0, :kb => 1, :mb => 2, :gb => 3 } convertors.each do |unit, multiple| it "should consider a %s to be 1024^%s bytes" % [unit, multiple] do tidy = Puppet::Type.type(:tidy).new :path => "/what/ever", :size => "5%s" % unit total = 5 multiple.times { total *= 1024 } tidy[:size].should == total end end end describe "when tidying" do before do @tidy = Puppet::Type.type(:tidy).new :path => "/what/ever" @stat = stub 'stat', :ftype => "directory" File.stubs(:lstat).with("/what/ever").returns @stat end describe "and generating files" do it "should set the backup on the file if backup is set on the tidy instance" do @tidy[:backup] = "whatever" Puppet::Type.type(:file).expects(:new).with { |args| args[:backup] == "whatever" } @tidy.mkfile("/what/ever") end it "should set the file's path to the tidy's path" do Puppet::Type.type(:file).expects(:new).with { |args| args[:path] == "/what/ever" } @tidy.mkfile("/what/ever") end it "should configure the file for deletion" do Puppet::Type.type(:file).expects(:new).with { |args| args[:ensure] == :absent } @tidy.mkfile("/what/ever") end it "should force deletion on the file" do Puppet::Type.type(:file).expects(:new).with { |args| args[:force] == true } @tidy.mkfile("/what/ever") end it "should do nothing if the targeted file does not exist" do File.expects(:lstat).with("/what/ever").raises Errno::ENOENT @tidy.generate.should == [] end end describe "and recursion is not used" do it "should generate a file resource if the file should be tidied" do @tidy.expects(:tidy?).with("/what/ever").returns true file = Puppet::Type.type(:file).new(:path => "/eh") @tidy.expects(:mkfile).with("/what/ever").returns file @tidy.generate.should == [file] end it "should do nothing if the file should not be tidied" do @tidy.expects(:tidy?).with("/what/ever").returns false @tidy.expects(:mkfile).never @tidy.generate.should == [] end end describe "and recursion is used" do before do @tidy[:recurse] = true Puppet::FileServing::Fileset.any_instance.stubs(:stat).returns mock("stat") @fileset = Puppet::FileServing::Fileset.new("/what/ever") Puppet::FileServing::Fileset.stubs(:new).returns @fileset end it "should use a Fileset for recursion" do Puppet::FileServing::Fileset.expects(:new).with("/what/ever", :recurse => true).returns @fileset @fileset.expects(:files).returns %w{. one two} @tidy.stubs(:tidy?).returns false @tidy.generate end it "should generate a file resource for every file that should be tidied but not for files that should not be tidied" do @fileset.expects(:files).returns %w{. one two} @tidy.expects(:tidy?).with("/what/ever").returns true @tidy.expects(:tidy?).with("/what/ever/one").returns true @tidy.expects(:tidy?).with("/what/ever/two").returns false file = Puppet::Type.type(:file).new(:path => "/eh") @tidy.expects(:mkfile).with("/what/ever").returns file @tidy.expects(:mkfile).with("/what/ever/one").returns file @tidy.generate end end describe "and determining whether a file matches provided glob patterns" do before do @tidy = Puppet::Type.type(:tidy).new :path => "/what/ever" @tidy[:matches] = %w{*foo* *bar*} @stat = mock 'stat' @matcher = @tidy.parameter(:matches) end it "should always convert the globs to an array" do @matcher.value = "*foo*" @matcher.value.should == %w{*foo*} end it "should return true if any pattern matches the last part of the file" do @matcher.value = %w{*foo* *bar*} @matcher.must be_tidy("/file/yaybarness", @stat) end it "should return false if no pattern matches the last part of the file" do @matcher.value = %w{*foo* *bar*} @matcher.should_not be_tidy("/file/yayness", @stat) end end describe "and determining whether a file is too old" do before do @tidy = Puppet::Type.type(:tidy).new :path => "/what/ever" @stat = stub 'stat' @tidy[:age] = "1s" @tidy[:type] = "mtime" @ager = @tidy.parameter(:age) end it "should use the age type specified" do @tidy[:type] = :ctime @stat.expects(:ctime).returns(Time.now) @ager.tidy?("/what/ever", @stat) end it "should return false if the file is more recent than the specified age" do @stat.expects(:mtime).returns(Time.now) @ager.should_not be_tidy("/what/ever", @stat) end it "should return true if the file is older than the specified age" do @stat.expects(:mtime).returns(Time.now - 10) @ager.must be_tidy("/what/ever", @stat) end end describe "and determining whether a file is too large" do before do @tidy = Puppet::Type.type(:tidy).new :path => "/what/ever" @stat = stub 'stat', :ftype => "file" @tidy[:size] = "1kb" @sizer = @tidy.parameter(:size) end it "should return false if the file is smaller than the specified size" do @stat.expects(:size).returns(4) # smaller than a kilobyte @sizer.should_not be_tidy("/what/ever", @stat) end it "should return true if the file is larger than the specified size" do @stat.expects(:size).returns(1500) # larger than a kilobyte @sizer.must be_tidy("/what/ever", @stat) end end describe "and determining whether a file should be tidied" do before do @tidy = Puppet::Type.type(:tidy).new :path => "/what/ever" @stat = stub 'stat', :ftype => "file" File.stubs(:lstat).with("/what/ever").returns @stat end it "should not try to recurse if the file does not exist" do @tidy[:recurse] = true File.stubs(:lstat).with("/what/ever").returns nil @tidy.generate.should == [] end it "should not be tidied if the file does not exist" do File.expects(:lstat).with("/what/ever").raises Errno::ENOENT @tidy.should_not be_tidy("/what/ever") end it "should not be tidied if the user has no access to the file" do File.expects(:lstat).with("/what/ever").raises Errno::EACCES @tidy.should_not be_tidy("/what/ever") end it "should not be tidied if it is a directory and rmdirs is set to false" do stat = mock 'stat', :ftype => "directory" File.expects(:lstat).with("/what/ever").returns stat @tidy.should_not be_tidy("/what/ever") end it "should return false if it does not match any provided globs" do @tidy[:matches] = "globs" matches = @tidy.parameter(:matches) matches.expects(:tidy?).with("/what/ever", @stat).returns false @tidy.should_not be_tidy("/what/ever") end it "should return false if it does not match aging requirements" do @tidy[:age] = "1d" ager = @tidy.parameter(:age) ager.expects(:tidy?).with("/what/ever", @stat).returns false @tidy.should_not be_tidy("/what/ever") end it "should return false if it does not match size requirements" do @tidy[:size] = "1b" sizer = @tidy.parameter(:size) sizer.expects(:tidy?).with("/what/ever", @stat).returns false @tidy.should_not be_tidy("/what/ever") end it "should tidy a file if age and size are set but only size matches" do @tidy[:size] = "1b" @tidy[:age] = "1d" @tidy.parameter(:size).stubs(:tidy?).returns true @tidy.parameter(:age).stubs(:tidy?).returns false @tidy.should be_tidy("/what/ever") end it "should tidy a file if age and size are set but only age matches" do @tidy[:size] = "1b" @tidy[:age] = "1d" @tidy.parameter(:size).stubs(:tidy?).returns false @tidy.parameter(:age).stubs(:tidy?).returns true @tidy.should be_tidy("/what/ever") end it "should tidy all files if neither age nor size is set" do @tidy.must be_tidy("/what/ever") end it "should sort the results inversely by path length, so files are added to the catalog before their directories" do @tidy[:recurse] = true @tidy[:rmdirs] = true fileset = Puppet::FileServing::Fileset.new("/what/ever") Puppet::FileServing::Fileset.expects(:new).returns fileset fileset.expects(:files).returns %w{. one one/two} @tidy.stubs(:tidy?).returns true @tidy.generate.collect { |r| r[:path] }.should == %w{/what/ever/one/two /what/ever/one /what/ever} end end it "should configure directories to require their contained files if rmdirs is enabled, so the files will be deleted first" do @tidy[:recurse] = true @tidy[:rmdirs] = true fileset = mock 'fileset' Puppet::FileServing::Fileset.expects(:new).with("/what/ever", :recurse => true).returns fileset fileset.expects(:files).returns %w{. one two one/subone two/subtwo one/subone/ssone} @tidy.stubs(:tidy?).returns true result = @tidy.generate.inject({}) { |hash, res| hash[res[:path]] = res; hash } { "/what/ever" => %w{/what/ever/one /what/ever/two}, "/what/ever/one" => ["/what/ever/one/subone"], "/what/ever/two" => ["/what/ever/two/subtwo"], "/what/ever/one/subone" => ["/what/ever/one/subone/ssone"] }.each do |parent, children| children.each do |child| ref = Puppet::Resource::Reference.new(:file, child) result[parent][:require].find { |req| req.to_s == ref.to_s }.should_not be_nil end end end end end diff --git a/spec/unit/type/user.rb b/spec/unit/type/user.rb index 57af7f093..bf2af23ab 100755 --- a/spec/unit/type/user.rb +++ b/spec/unit/type/user.rb @@ -1,276 +1,279 @@ #!/usr/bin/env ruby require File.dirname(__FILE__) + '/../../spec_helper' user = Puppet::Type.type(:user) describe user do before do + unless ENV["PATH"].split(File::PATH_SEPARATOR).include?("/usr/sbin") + ENV["PATH"] += File::PATH_SEPARATOR + "/usr/sbin" + end @provider = stub 'provider' @resource = stub 'resource', :resource => nil, :provider => @provider, :line => nil, :file => nil end it "should have a default provider inheriting from Puppet::Provider" do user.defaultprovider.ancestors.should be_include(Puppet::Provider) end it "should be able to create a instance" do user.new(:name => "foo").should_not be_nil end it "should have an allows_duplicates feature" do user.provider_feature(:allows_duplicates).should_not be_nil end it "should have an manages_homedir feature" do user.provider_feature(:manages_homedir).should_not be_nil end it "should have an manages_passwords feature" do user.provider_feature(:manages_passwords).should_not be_nil end it "should have a manages_solaris_rbac feature" do user.provider_feature(:manages_solaris_rbac).should_not be_nil end describe "instances" do it "should have a valid provider" do user.new(:name => "foo").provider.class.ancestors.should be_include(Puppet::Provider) end end properties = [:ensure, :uid, :gid, :home, :comment, :shell, :password, :groups, :roles, :auths, :profiles, :project, :keys] properties.each do |property| it "should have a %s property" % property do user.attrclass(property).ancestors.should be_include(Puppet::Property) end it "should have documentation for its %s property" % property do user.attrclass(property).doc.should be_instance_of(String) end end list_properties = [:groups, :roles, :auths] list_properties.each do |property| it "should have a list '%s'" % property do user.attrclass(property).ancestors.should be_include(Puppet::Property::List) end end it "should have an ordered list 'profiles'" do user.attrclass(:profiles).ancestors.should be_include(Puppet::Property::OrderedList) end it "should have key values 'keys'" do user.attrclass(:keys).ancestors.should be_include(Puppet::Property::KeyValue) end describe "when retrieving all current values" do before do @user = user.new(:name => "foo", :uid => 10) end it "should return a hash containing values for all set properties" do @user[:gid] = 10 @user.property(:ensure).expects(:retrieve).returns :present @user.property(:uid).expects(:retrieve).returns 15 @user.property(:gid).expects(:retrieve).returns 15 values = @user.retrieve [@user.property(:uid), @user.property(:gid)].each { |property| values.should be_include(property) } end it "should set all values to :absent if the user is absent" do @user.property(:ensure).expects(:retrieve).returns :absent @user.property(:uid).expects(:retrieve).never @user.retrieve[@user.property(:uid)].should == :absent end it "should include the result of retrieving each property's current value if the user is present" do @user.property(:ensure).expects(:retrieve).returns :present @user.property(:uid).expects(:retrieve).returns 15 @user.retrieve[@user.property(:uid)].should == 15 end end describe "when managing the ensure property" do before do @ensure = user.attrclass(:ensure).new(:resource => @resource) end it "should support a :present value" do lambda { @ensure.should = :present }.should_not raise_error end it "should support an :absent value" do lambda { @ensure.should = :absent }.should_not raise_error end it "should call :create on the provider when asked to sync to the :present state" do @provider.expects(:create) @ensure.should = :present @ensure.sync end it "should call :delete on the provider when asked to sync to the :absent state" do @provider.expects(:delete) @ensure.should = :absent @ensure.sync end describe "and determining the current state" do it "should return :present when the provider indicates the user exists" do @provider.expects(:exists?).returns true @ensure.retrieve.should == :present end it "should return :absent when the provider indicates the user does not exist" do @provider.expects(:exists?).returns false @ensure.retrieve.should == :absent end end end describe "when managing the uid property" do it "should convert number-looking strings into actual numbers" do uid = user.attrclass(:uid).new(:resource => @resource) uid.should = "50" uid.should.must == 50 end it "should support UIDs as numbers" do uid = user.attrclass(:uid).new(:resource => @resource) uid.should = 50 uid.should.must == 50 end it "should :absent as a value" do uid = user.attrclass(:uid).new(:resource => @resource) uid.should = :absent uid.should.must == :absent end end describe "when managing the gid" do it "should :absent as a value" do gid = user.attrclass(:gid).new(:resource => @resource) gid.should = :absent gid.should.must == :absent end it "should convert number-looking strings into actual numbers" do gid = user.attrclass(:gid).new(:resource => @resource) gid.should = "50" gid.should.must == 50 end it "should support GIDs specified as integers" do gid = user.attrclass(:gid).new(:resource => @resource) gid.should = 50 gid.should.must == 50 end it "should support groups specified by name" do gid = user.attrclass(:gid).new(:resource => @resource) gid.should = "foo" gid.should.must == "foo" end describe "when testing whether in sync" do before do @gid = user.attrclass(:gid).new(:resource => @resource, :should => %w{foo bar}) end it "should return true if no 'should' values are set" do @gid = user.attrclass(:gid).new(:resource => @resource) @gid.must be_insync(500) end it "should return true if any of the specified groups are equal to the current integer" do Puppet::Util.expects(:gid).with("foo").returns 300 Puppet::Util.expects(:gid).with("bar").returns 500 @gid.must be_insync(500) end it "should return false if none of the specified groups are equal to the current integer" do Puppet::Util.expects(:gid).with("foo").returns 300 Puppet::Util.expects(:gid).with("bar").returns 500 @gid.should_not be_insync(700) end end describe "when syncing" do before do @gid = user.attrclass(:gid).new(:resource => @resource, :should => %w{foo bar}) end it "should use the first found, specified group as the desired value and send it to the provider" do Puppet::Util.expects(:gid).with("foo").returns nil Puppet::Util.expects(:gid).with("bar").returns 500 @provider.expects(:gid=).with 500 @gid.sync end end end describe "when managing passwords" do before do @password = user.attrclass(:password).new(:resource => @resource, :should => "mypass") end it "should not include the password in the change log when adding the password" do @password.change_to_s(:absent, "mypass").should_not be_include("mypass") end it "should not include the password in the change log when changing the password" do @password.change_to_s("other", "mypass").should_not be_include("mypass") end it "should fail if a ':' is included in the password" do lambda { @password.should = "some:thing" }.should raise_error(Puppet::Error) end it "should allow the value to be set to :absent" do lambda { @password.should = :absent }.should_not raise_error end end describe "when manages_solaris_rbac is enabled" do before do @provider.stubs(:satisfies?).returns(false) @provider.expects(:satisfies?).with(:manages_solaris_rbac).returns(true) end it "should support a :role value for ensure" do @ensure = user.attrclass(:ensure).new(:resource => @resource) lambda { @ensure.should = :role }.should_not raise_error end end describe "when user has roles" do it "should autorequire roles" do #this is a little funky because the autorequire depends on a property with a feature testuser = Puppet::Type.type(:user).new(:name => "testuser") testuser.provider.class.expects(:feature?).with(:manages_solaris_rbac).returns(true) testuser[:roles] = "testrole" testrole = Puppet::Type.type(:user).new(:name => "testrole") config = Puppet::Resource::Catalog.new :testing do |conf| [testuser, testrole].each { |resource| conf.add_resource resource } end Puppet::Type::User::ProviderDirectoryservice.stubs(:get_macosx_version_major).returns "10.5" rel = testuser.autorequire[0] rel.source.ref.should == testrole.ref rel.target.ref.should == testuser.ref end end end diff --git a/spec/unit/util/selinux.rb b/spec/unit/util/selinux.rb index dacf9f503..db3ee413a 100644 --- a/spec/unit/util/selinux.rb +++ b/spec/unit/util/selinux.rb @@ -1,219 +1,222 @@ #!/usr/bin/env ruby require File.dirname(__FILE__) + '/../../spec_helper' require 'puppet/util/selinux' include Puppet::Util::SELinux unless defined?(Selinux) module Selinux def self.is_selinux_enabled false end end end describe Puppet::Util::SELinux do describe "selinux_support?" do before do end it "should return :true if this system has SELinux enabled" do Selinux.expects(:is_selinux_enabled).returns 1 selinux_support?.should be_true end it "should return :false if this system lacks SELinux" do Selinux.expects(:is_selinux_enabled).returns 0 selinux_support?.should be_false end end describe "filesystem detection" do before :each do - File.expects(:read).with("/proc/mounts").returns "rootfs / rootfs rw 0 0\n/dev/root / ext3 rw,relatime,errors=continue,user_xattr,acl,data=ordered 0 0\n/dev /dev tmpfs rw,relatime,mode=755 0 0\n/proc /proc proc rw,relatime 0 0\n/sys /sys sysfs rw,relatime 0 0\n192.168.1.1:/var/export /mnt/nfs nfs rw,relatime,vers=3,rsize=32768,wsize=32768,namlen=255,hard,nointr,proto=tcp,timeo=600,retrans=2,sec=sys,mountaddr=192.168.1.1,mountvers=3,mountproto=udp,addr=192.168.1.1 0 0\n" + fh = stub 'fh', :close => nil + File.stubs(:open).with("/proc/mounts", File::NONBLOCK).returns fh + fh.stubs(:read).returns "rootfs / rootfs rw 0 0\n/dev/root / ext3 rw,relatime,errors=continue,user_xattr,acl,data=ordered 0 0\n/dev /dev tmpfs rw,relatime,mode=755 0 0\n/proc /proc proc rw,relatime 0 0\n/sys /sys sysfs rw,relatime 0 0\n192.168.1.1:/var/export /mnt/nfs nfs rw,relatime,vers=3,rsize=32768,wsize=32768,namlen=255,hard,nointr,proto=tcp,timeo=600,retrans=2,sec=sys,mountaddr=192.168.1.1,mountvers=3,mountproto=udp,addr=192.168.1.1 0 0\n" + fh.stubs(:close) end it "should parse the contents of /proc/mounts" do read_mounts().should == { '/' => 'ext3', '/sys' => 'sysfs', '/mnt/nfs' => 'nfs', '/proc' => 'proc', '/dev' => 'tmpfs' } end it "should match a path on / to ext3" do find_fs('/etc/puppet/testfile').should == "ext3" end it "should match a path on /mnt/nfs to nfs" do find_fs('/mnt/nfs/testfile/foobar').should == "nfs" end it "should reture true for a capable filesystem" do selinux_label_support?('/etc/puppet/testfile').should be_true end it "should return false for a noncapable filesystem" do selinux_label_support?('/mnt/nfs/testfile').should be_false end end describe "get_selinux_current_context" do it "should return nil if no SELinux support" do self.expects(:selinux_support?).returns false get_selinux_current_context("/foo").should be_nil end it "should return a context" do self.expects(:selinux_support?).returns true Selinux.expects(:lgetfilecon).with("/foo").returns [0, "user_u:role_r:type_t:s0"] get_selinux_current_context("/foo").should == "user_u:role_r:type_t:s0" end it "should return nil if lgetfilecon fails" do self.expects(:selinux_support?).returns true Selinux.expects(:lgetfilecon).with("/foo").returns -1 get_selinux_current_context("/foo").should be_nil end end describe "get_selinux_default_context" do it "should return nil if no SELinux support" do self.expects(:selinux_support?).returns false get_selinux_default_context("/foo").should be_nil end it "should return a context if a default context exists" do self.expects(:selinux_support?).returns true fstat = stub 'File::Stat', :mode => 0 File.expects(:lstat).with("/foo").returns fstat self.expects(:find_fs).with("/foo").returns "ext3" Selinux.expects(:matchpathcon).with("/foo", 0).returns [0, "user_u:role_r:type_t:s0"] get_selinux_default_context("/foo").should == "user_u:role_r:type_t:s0" end it "should return nil if matchpathcon returns failure" do self.expects(:selinux_support?).returns true fstat = stub 'File::Stat', :mode => 0 File.expects(:lstat).with("/foo").returns fstat self.expects(:find_fs).with("/foo").returns "ext3" Selinux.expects(:matchpathcon).with("/foo", 0).returns -1 get_selinux_default_context("/foo").should be_nil end it "should return nil if selinux_label_support returns false" do self.expects(:selinux_support?).returns true self.expects(:find_fs).with("/foo").returns "nfs" get_selinux_default_context("/foo").should be_nil end end describe "parse_selinux_context" do it "should return nil if no context is passed" do parse_selinux_context(:seluser, nil).should be_nil end it "should return nil if the context is 'unlabeled'" do parse_selinux_context(:seluser, "unlabeled").should be_nil end it "should return the user type when called with :seluser" do parse_selinux_context(:seluser, "user_u:role_r:type_t:s0").should == "user_u" end it "should return the role type when called with :selrole" do parse_selinux_context(:selrole, "user_u:role_r:type_t:s0").should == "role_r" end it "should return the type type when called with :seltype" do parse_selinux_context(:seltype, "user_u:role_r:type_t:s0").should == "type_t" end it "should return nil for :selrange when no range is returned" do parse_selinux_context(:selrange, "user_u:role_r:type_t").should be_nil end it "should return the range type when called with :selrange" do parse_selinux_context(:selrange, "user_u:role_r:type_t:s0").should == "s0" end describe "with a variety of SELinux range formats" do ['s0', 's0:c3', 's0:c3.c123', 's0:c3,c5,c8', 'TopSecret', 'TopSecret,Classified', 'Patient_Record'].each do |range| it "should parse range '#{range}'" do parse_selinux_context(:selrange, "user_u:role_r:type_t:#{range}").should == range end end end end describe "set_selinux_context" do it "should return nil if there is no SELinux support" do self.expects(:selinux_support?).returns false set_selinux_context("/foo", "user_u:role_r:type_t:s0").should be_nil end it "should use lsetfilecon to set a context" do self.expects(:selinux_support?).returns true Selinux.expects(:lsetfilecon).with("/foo", "user_u:role_r:type_t:s0").returns 0 set_selinux_context("/foo", "user_u:role_r:type_t:s0").should be_true end it "should use lsetfilecon to set user_u user context" do self.expects(:selinux_support?).returns true Selinux.expects(:lgetfilecon).with("/foo").returns [0, "foo:role_r:type_t:s0"] Selinux.expects(:lsetfilecon).with("/foo", "user_u:role_r:type_t:s0").returns 0 set_selinux_context("/foo", "user_u", :seluser).should be_true end it "should use lsetfilecon to set role_r role context" do self.expects(:selinux_support?).returns true Selinux.expects(:lgetfilecon).with("/foo").returns [0, "user_u:foo:type_t:s0"] Selinux.expects(:lsetfilecon).with("/foo", "user_u:role_r:type_t:s0").returns 0 set_selinux_context("/foo", "role_r", :selrole).should be_true end it "should use lsetfilecon to set type_t type context" do self.expects(:selinux_support?).returns true Selinux.expects(:lgetfilecon).with("/foo").returns [0, "user_u:role_r:foo:s0"] Selinux.expects(:lsetfilecon).with("/foo", "user_u:role_r:type_t:s0").returns 0 set_selinux_context("/foo", "type_t", :seltype).should be_true end it "should use lsetfilecon to set s0:c3,c5 range context" do self.expects(:selinux_support?).returns true Selinux.expects(:lgetfilecon).with("/foo").returns [0, "user_u:role_r:type_t:s0"] Selinux.expects(:lsetfilecon).with("/foo", "user_u:role_r:type_t:s0:c3,c5").returns 0 set_selinux_context("/foo", "s0:c3,c5", :selrange).should be_true end end describe "set_selinux_default_context" do it "should return nil if there is no SELinux support" do self.expects(:selinux_support?).returns false set_selinux_default_context("/foo").should be_nil end it "should return nil if no default context exists" do self.expects(:get_selinux_default_context).with("/foo").returns nil set_selinux_default_context("/foo").should be_nil end it "should do nothing and return nil if the current context matches the default context" do self.expects(:get_selinux_default_context).with("/foo").returns "user_u:role_r:type_t" self.expects(:get_selinux_current_context).with("/foo").returns "user_u:role_r:type_t" set_selinux_default_context("/foo").should be_nil end it "should set and return the default context if current and default do not match" do self.expects(:get_selinux_default_context).with("/foo").returns "user_u:role_r:type_t" self.expects(:get_selinux_current_context).with("/foo").returns "olduser_u:role_r:type_t" self.expects(:set_selinux_context).with("/foo", "user_u:role_r:type_t").returns true set_selinux_default_context("/foo").should == "user_u:role_r:type_t" end end end diff --git a/tasks/rake/reductive.rb b/tasks/rake/reductive.rb index fb0921768..4a3e18bc1 100644 --- a/tasks/rake/reductive.rb +++ b/tasks/rake/reductive.rb @@ -1,540 +1,540 @@ #!/usr/bin/env ruby # The tasks associated with building Reductive Labs projects require 'rbconfig' require 'rake' require 'rake/tasklib' require 'rake/clean' require 'rake/testtask' $features = {} begin require 'rubygems' require 'rake/gempackagetask' $features[:gem] = true rescue Exception $features[:gem] = false $stderr.puts "No Gems; skipping" nil end begin require 'rdoc/rdoc' $features[:rdoc] = true rescue => detail $features[:rdoc] = false puts "No rdoc: %s" % detail end if $features[:rdoc] require 'rake/rdoctask' end # Create all of the standard targets for a Reductive Labs project. # NOTE: The reason so many of the rake tasks are generated, rather than being # declared directly, is that they need information from the project instance. # Any rake task with an instance variable (e.g., @name or @version) needs # to have that variable assigned *before* the task is defined. Suckage. class Rake::RedLabProject < Rake::TaskLib # The project name. attr_accessor :name # The project version. attr_accessor :version # The directory to which to publish packages and html and such. attr_accessor :publishdir # The package-specific publishing directory attr_accessor :pkgpublishdir # Create a Gem file. attr_accessor :mkgem # The hosts to run all of our tests on. attr_accessor :testhosts # The summary of this project. attr_accessor :summary # The description of this project. attr_accessor :description # The author of this project. attr_accessor :author # A Contact email address. attr_accessor :email # The URL for the project. attr_accessor :url # Where to get the source code. attr_accessor :source # Who the vendor is. attr_accessor :vendor # The copyright for this project attr_accessor :copyright # The RubyForge project. attr_accessor :rfproject # The list of files. Only used for gem tasks. attr_writer :filelist # The directory in which to store packages. Defaults to "pkg". attr_accessor :package_dir # The default task. Defaults to the 'alltests' task. attr_accessor :defaulttask # The defined requirements attr_reader :requires # The file containing the version string. attr_accessor :versionfile # Print messages on stdout def announce(msg = nil) puts msg end # Print messages on stderr def warn(msg = nil) $stderr.puts msg end def add_dependency(name, version) @requires[name] = version end # Where we'll be putting the code. def codedir unless defined? @codedir @codedir = File.join(self.package_dir, "#{@name}-#{@version}") end return @codedir end # Retrieve the current version from the code. def currentversion unless defined? @currentversion ver = %x{ruby -Ilib ./bin/#{@name} --version}.chomp if $? == 0 and ver != "" @currentversion = ver else warn "Could not retrieve current version; using 0.0.0" @currentversion = "0.0.0" end end return @currentversion end # Define all of our package tasks. We just search through all of our # defined methods and call anything that's listed as making tasks. def define self.methods.find_all { |method| method.to_s =~ /^mktask/ }.each { |method| self.send(method) } end def egrep(pattern) Dir['**/*.rb'].each do |fn| count = 0 open(fn) do |f| while line = f.gets count += 1 if line =~ pattern puts "#{fn}:#{count}:#{line}" end end end end end # List all of the files. def filelist unless defined? @createdfilelist # If they passed in a file list as an array, then create a FileList # object out of it. if defined? @filelist unless @filelist.is_a? FileList @filelist = FileList[@filelist] end else # Use a default file list. @filelist = FileList[ 'install.rb', '[A-Z]*', 'lib/**/*.rb', 'test/**/*.rb', 'bin/**/*', 'ext/**/*', 'examples/**/*', 'conf/**/*' ] end @filelist.delete_if {|item| item.include?(".git")} @createdfilelist = true end @filelist end def has?(feature) feature = feature.intern if feature.is_a? String if $features.include?(feature) return $features[feature] else return true end end def initialize(name, version = nil) @name = name if ENV['REL'] @version = ENV['REL'] else @version = version || self.currentversion end @defaulttask = :alltests @publishdir = ENV['DOWNLOAD_DIR'] || "/opt/rl/docroots/reductivelabs.com/htdocs/downloads" @pkgpublishdir = "#{@publishdir}/#{@name}" @email = "dev@reductivelabs.com" @url = "http://reductivelabs.com/projects/#{@name}" @source = "http://reductivelabs.com/downloads/#{@name}/#{@name}-#{@version}.tgz" @vendor = "Reductive Labs, LLC" @copyright = "Copyright 2003-2008, Reductive Labs, LLC. Some Rights Reserved." @rfproject = @name @defaulttask = :package @package_dir = "pkg" @requires = {} @versionfile = "lib/#{@name}.rb" CLOBBER.include('doc/*') yield self if block_given? define if block_given? end def mktaskhtml if $features[:rdoc] Rake::RDocTask.new(:html) { |rdoc| rdoc.rdoc_dir = 'html' rdoc.template = 'html' rdoc.title = @name.capitalize rdoc.options << '--line-numbers' << '--inline-source' << '--main' << 'README' rdoc.rdoc_files.include('README', 'COPYING', 'CHANGELOG') rdoc.rdoc_files.include('lib/**/*.rb') CLEAN.include("html") } # Publish the html. task :publish => [:package, :html] do puts Dir.getwd apidocsdir = "#{@pkgpublishdir}/apidocs" File.makedirs(apidocsdir) sh %{cp -r html #{apidocsdir}} end else warn "No rdoc; skipping html" end end # Create a release task. def mktaskrelease desc "Make a new release" task :release => [ :prerelease, :clobber, :update_version, :commit_newversion, :trac_version, :tag, # tag everything before we make a bunch of extra dirs :html, :package, :publish ] do announce announce "**************************************************************" announce "* Release #{@version} Complete." announce "* Packages ready to upload." announce "**************************************************************" announce end end # Do any prerelease work. def mktaskprerelease # Validate that everything is ready to go for a release. task :prerelease do announce announce "**************************************************************" announce "* Making Release #{@version}" announce "* (current version #{self.currentversion})" announce "**************************************************************" announce # Is a release number supplied? unless ENV['REL'] warn "You must provide a release number when releasing" fail "Usage: rake release REL=x.y.z [REUSE=tag_suffix]" end # Is the release different than the current release. # (or is REUSE set?) if @version == self.currentversion && ! ENV['REUSE'] fail "Current version is #{@version}, must specify REUSE=tag_suffix to reuse version" end # Are all source files checked in? if ENV['RELTEST'] announce "Release Task Testing, skipping checked-in file test" else announce "Checking for unchecked-in files..." data = %x{git status} unless data.include?("nothing to commit") fail "git status is not clean ... do you have unchecked-in files?" end announce "No outstanding checkins found ... OK" end end end # Create the task to update versions. def mktaskupdateversion task :update_version => [:prerelease] do if @version == self.currentversion announce "No version change ... skipping version update" else announce "Updating #{@versionfile} version to #{@version}" open(@versionfile) do |rakein| open("#{@versionfile}.new", "w") do |rakeout| rakein.each do |line| if line =~ /^(\s*)#{@name.upcase}VERSION\s*=\s*/ rakeout.puts "#{$1}#{@name.upcase}VERSION = '#{@version}'" else rakeout.puts line end end end end mv "#{@versionfile}.new", @versionfile end end desc "Commit the new versions to SVN." task :commit_newversion => [:update_version] do if ENV['RELTEST'] announce "Release Task Testing, skipping commiting of new version" else sh %{git commit -m "Updated to version #{@version}" #{@versionfile}} end end end def mktasktrac_version task :trac_version => [:update_version] do tracpath = "/opt/rl/trac/#{@name}" unless FileTest.exists?(tracpath) announce "No Trac instance at %s" % tracpath else output = %x{sudo trac-admin #{tracpath} version list}.chomp.split("\n") versions = {} output[3..-1].each do |line| name, time = line.chomp.split(/\s+/) versions[name] = time end if versions.include?(@version) announce "Version #{@version} already in Trac" else announce "Adding #{@name} version #{@version} to Trac" date = [Time.now.year.to_s, Time.now.month.to_s, Time.now.day.to_s].join("-") system("sudo trac-admin #{tracpath} version add #{@version} #{date}") end end end end # Create the tag task. def mktasktag desc "Tag all the files with the latest release number (REL=x.y.z)" task :tag => [:prerelease] do reltag = @version announce "Tagging with [#{reltag}]" if ENV['RELTEST'] announce "Release Task Testing, skipping tagging" else sh %{git tag #{reltag}} end end end # Create the task for testing across all hosts. def mktaskhosttest desc "Test Puppet on each test host" task :hosttest do out = "" TESTHOSTS.each { |host| puts "testing %s" % host cwd = Dir.getwd file = "/tmp/#{@name}-#{host}test.out" system("ssh #{host} 'cd git/#{@name}/test; sudo rake' 2>&1 >#{file}") if $? != 0 puts "%s failed; output is in %s" % [host, file] end } end end def mktaskri # Create a task to build the RDOC documentation tree. #Rake::RDocTask.new("ri") { |rdoc| # #rdoc.rdoc_dir = 'html' # #rdoc.template = 'html' # rdoc.title = "Puppet" # rdoc.options << '--ri' << '--line-numbers' << '--inline-source' << '--main' << 'README' # rdoc.rdoc_files.include('README', 'COPYING', 'CHANGELOG') # rdoc.rdoc_files.include('lib/**/*.rb', 'doc/**/*.rdoc') #} if $features[:rdoc] task :ri do |ri| files = ['README', 'COPYING', 'CHANGELOG'] + Dir.glob('lib/**/*.rb') puts "files are \n%s" % files.join("\n") begin ri = RDoc::RDoc.new ri.document(["--ri-site"] + files) rescue RDoc::RDocError => detail puts "Failed to build docs: %s" % detail return nil rescue LoadError puts "Missing rdoc; cannot build documentation" return nil end end else warn "No rdoc; skipping ri." end end desc "Install the application using the standard install.rb script" task :install do ruby "install.rb" end def mktaskdefault if dtask = self.defaulttask desc "Default task" task :default => dtask end end desc "Run all unit tests." task :alltests do - if FileTest.exists?("test/Rakefile") - sh %{cd test; rake} + if FileTest.exists?("spec/Rakefile") + sh %{cd spec; rake} else - Dir.chdir("test") do + Dir.chdir("spec") do Dir.entries(".").find_all { |f| f =~ /\.rb/ }.each do |f| sh %{ruby #{f}} end end end end desc "List all ruby files" task :rubyfiles do puts Dir['**/*.rb'].reject { |fn| fn =~ /^pkg/ } puts Dir['**/bin/*'].reject { |fn| fn =~ /svn|(~$)|(\.rb$)/ } end desc "Look for TODO and FIXME tags in the code" task :todo do egrep "/#.*(FIXME|TODO|TBD)/" end # This task requires extra information from the Rake file. def mkgemtask # ==================================================================== # Create a task that will package the Rake software into distributable # tar, zip and gem files. if ! defined?(Gem) puts "Package Target requires RubyGEMs" else spec = Gem::Specification.new { |s| #### Basic information. s.name = self.name s.version = self.version s.summary = self.summary s.description = self.description s.platform = Gem::Platform::RUBY #### Dependencies and requirements. # I'd love to explicitly list all of the libraries that I need, # but gems seem to only be able to handle dependencies on other # gems, which is, um, stupid. self.requires.each do |name, version| s.add_dependency(name, ">= #{version}") end s.files = filelist.to_a #### Signing key and cert chain #s.signing_key = '/..../gem-private_key.pem' #s.cert_chain = ['gem-public_cert.pem'] #### Author and project details. s.author = [self.author] s.email = self.email s.homepage = self.url s.rubyforge_project = self.rfproject yield s } Rake::GemPackageTask.new(spec) { |pkg| pkg.need_tar = true } desc "Copy the newly created package into the downloads directory" task :publish => [:package] do puts Dir.getwd sh %{cp pkg/#{@name}-#{@version}.gem #{self.publishdir}/gems} sh %{gem generate_index -d #{self.publishdir}} sh %{cp pkg/#{@name}-#{@version}.tgz #{self.pkgpublishdir}} sh %{ln -sf #{@name}-#{@version}.tgz #{self.pkgpublishdir}/#{@name}-latest.tgz} end CLEAN.include("pkg") end end end diff --git a/test/Rakefile b/test/Rakefile index a8118c1c5..a1f8c26ec 100644 --- a/test/Rakefile +++ b/test/Rakefile @@ -1,97 +1,98 @@ # let Emacs know it's -*- ruby -*- begin require 'rake/testtask' rescue LoadError $stderr.puts "You must have 'rake' installed to use this file" exit(1) end require 'find' include Find include FileTest $exclusions = %W(lib data) $test_library_paths = %W(lib ../lib) $: << File.join(Dir.getwd, "lib") require 'rake/puppet_testtask' filemap = Hash.new { |hash, key| hash[key] = [] } allfiles = [] # First collect the entire file list. find(".") do |f| # Get rid of the leading ./ f = f.sub(/^\.\//, '') file = File.basename(f) dir = File.dirname(f) # Prune . directories and excluded dirs if (file =~ /^\./ and f != ".") or $exclusions.include?(File.basename(file)) prune next end next if f == "." next if dir == "." # If we're a ruby script, then add it to the list of files for that dir if file =~ /\.rb$/ allfiles << f # Add it to all of the parent dirs, not just our own parts = File.split(dir) if parts[0] == "." parts.shift end parts.each_with_index { |part, i| path = File.join(parts[0..i]) filemap[path] << f } end end desc "Run the full test suite" Rake::PuppetTestTask.new :test do |t| - t.libs << $test_library_paths + t.libs += $test_library_paths # Add every file as a test file to run t.test_files = allfiles t.verbose = true end task :default => :test # Now create a task for every directory filemap.each do |dir, files| ns = dir.gsub "/", ":" # First create a separate task for each file in the namespace. namespace ns do files.each do |file| Rake::PuppetTestTask.new File.basename(file, '.rb').to_sym do |t| - t.libs << $test_library_paths + ['..'] + t.libs += $test_library_paths + ['..'] + t.libs << '..' t.test_files = [ file ] t.verbose = true end end end # Then create a task that matches the directory itself. Rake::PuppetTestTask.new dir do |t| - t.libs << $test_library_paths + t.libs += $test_library_paths if ENV["TESTFILES"] t.test_files = ENV["TESTFILES"].split(/\s+/) else t.test_files = files.sort end t.verbose = true end # And alias it with a slash on the end task(dir + "/" => dir) end # $Id$ diff --git a/test/lib/puppettest.rb b/test/lib/puppettest.rb index 3c6083fa6..2f84a6c28 100755 --- a/test/lib/puppettest.rb +++ b/test/lib/puppettest.rb @@ -1,344 +1,347 @@ # Add .../test/lib testlib = File.expand_path(File.dirname(__FILE__)) $LOAD_PATH.unshift(testlib) unless $LOAD_PATH.include?(testlib) # Add .../lib mainlib = File.expand_path(File.join(File.dirname(__FILE__), '../../lib')) $LOAD_PATH.unshift(mainlib) unless $LOAD_PATH.include?(mainlib) require 'puppet' # include any gems in vendor/gems Dir["#{mainlib}/../vendor/gems/**"].each do |path| libpath = File.join(path, "lib") if File.directory?(libpath) $LOAD_PATH.unshift(libpath) else $LOAD_PATH.unshift(path) end end require 'mocha' # Only load the test/unit class if we're not in the spec directory. # Else we get the bogus 'no tests, no failures' message. unless Dir.getwd =~ /spec/ require 'test/unit' end # Yay; hackish but it works if ARGV.include?("-d") ARGV.delete("-d") $console = true end # Some monkey-patching to allow us to test private methods. class Class def publicize_methods(*methods) saved_private_instance_methods = methods.empty? ? self.private_instance_methods : methods self.class_eval { public(*saved_private_instance_methods) } yield self.class_eval { private(*saved_private_instance_methods) } end end module PuppetTest # These need to be here for when rspec tests use these # support methods. @@tmpfiles = [] # Munge cli arguments, so we can enable debugging if we want # and so we can run just specific methods. def self.munge_argv require 'getoptlong' result = GetoptLong.new( [ "--debug", "-d", GetoptLong::NO_ARGUMENT ], [ "--resolve", "-r", GetoptLong::REQUIRED_ARGUMENT ], [ "-n", GetoptLong::REQUIRED_ARGUMENT ], [ "--help", "-h", GetoptLong::NO_ARGUMENT ] ) usage = "USAGE: TESTOPTS='[-n -n ...] [-d]' rake [target] [target] ..." opts = [] dir = method = nil result.each { |opt,arg| case opt when "--resolve" dir, method = arg.split(",") when "--debug" $puppet_debug = true Puppet::Util::Log.level = :debug Puppet::Util::Log.newdestination(:console) when "--help" puts usage exit else opts << opt << arg end } suites = nil args = ARGV.dup # Reset the options, so the test suite can deal with them (this is # what makes things like '-n' work). opts.each { |o| ARGV << o } return args end # Find the root of the Puppet tree; this is not the test directory, but # the parent of that dir. def basedir(*list) unless defined? @@basedir Dir.chdir(File.dirname(__FILE__)) do @@basedir = File.dirname(File.dirname(Dir.getwd)) end end if list.empty? @@basedir else File.join(@@basedir, *list) end end def datadir(*list) File.join(basedir, "test", "data", *list) end def exampledir(*args) unless defined? @@exampledir @@exampledir = File.join(basedir, "examples") end if args.empty? return @@exampledir else return File.join(@@exampledir, *args) end end module_function :basedir, :datadir, :exampledir def cleanup(&block) @@cleaners << block end # Rails clobbers RUBYLIB, thanks def libsetup curlibs = ENV["RUBYLIB"].split(":") $:.reject do |dir| dir =~ /^\/usr/ end.each do |dir| unless curlibs.include?(dir) curlibs << dir end end ENV["RUBYLIB"] = curlibs.join(":") end def logcollector collector = [] Puppet::Util::Log.newdestination(collector) cleanup do Puppet::Util::Log.close(collector) end collector end def rake? $0 =~ /test_loader/ end # Redirect stdout and stderr def redirect @stderr = tempfile @stdout = tempfile $stderr = File.open(@stderr, "w") $stdout = File.open(@stdout, "w") cleanup do $stderr = STDERR $stdout = STDOUT end end def setup + unless ENV["PATH"].split(File::PATH_SEPARATOR).include?("/usr/sbin") + ENV["PATH"] += File::PATH_SEPARATOR + "/usr/sbin" + end @memoryatstart = Puppet::Util.memory if defined? @@testcount @@testcount += 1 else @@testcount = 0 end @configpath = File.join(tmpdir, "configdir" + @@testcount.to_s + "/" ) unless defined? $user and $group $user = nonrootuser().uid.to_s $group = nonrootgroup().gid.to_s end Puppet.settings.clear Puppet[:user] = $user Puppet[:group] = $group Puppet[:confdir] = @configpath Puppet[:vardir] = @configpath unless File.exists?(@configpath) Dir.mkdir(@configpath) end @@tmpfiles << @configpath << tmpdir() @@tmppids = [] @@cleaners = [] @logs = [] # If we're running under rake, then disable debugging and such. #if rake? or ! Puppet[:debug] #if defined?($puppet_debug) or ! rake? if textmate? Puppet[:color] = false end Puppet::Util::Log.newdestination(@logs) if defined? $console Puppet.info @method_name Puppet::Util::Log.newdestination(:console) Puppet[:trace] = true end Puppet::Util::Log.level = :debug #$VERBOSE = 1 #else # Puppet::Util::Log.close # Puppet::Util::Log.newdestination(@logs) # Puppet[:httplog] = tempfile() #end Puppet[:ignoreschedules] = true #@start = Time.now end def tempfile if defined? @@tmpfilenum @@tmpfilenum += 1 else @@tmpfilenum = 1 end f = File.join(self.tmpdir(), "tempfile_" + @@tmpfilenum.to_s) @@tmpfiles ||= [] @@tmpfiles << f return f end def textmate? if ENV["TM_FILENAME"] return true else return false end end def tstdir dir = tempfile() Dir.mkdir(dir) return dir end def tmpdir unless defined? @tmpdir and @tmpdir @tmpdir = case Facter["operatingsystem"].value when "Darwin"; "/private/tmp" when "SunOS"; "/var/tmp" else "/tmp" end @tmpdir = File.join(@tmpdir, "puppettesting" + Process.pid.to_s) unless File.exists?(@tmpdir) FileUtils.mkdir_p(@tmpdir) File.chmod(01777, @tmpdir) end end @tmpdir end def remove_tmp_files @@tmpfiles.each { |file| unless file =~ /tmp/ puts "Not deleting tmpfile %s" % file next end if FileTest.exists?(file) system("chmod -R 755 %s" % file) system("rm -rf %s" % file) end } @@tmpfiles.clear end def teardown #@stop = Time.now #File.open("/tmp/test_times.log", ::File::WRONLY|::File::CREAT|::File::APPEND) { |f| f.puts "%0.4f %s %s" % [@stop - @start, @method_name, self.class] } @@cleaners.each { |cleaner| cleaner.call() } remove_tmp_files @@tmppids.each { |pid| %x{kill -INT #{pid} 2>/dev/null} } @@tmppids.clear Puppet::Util::Storage.clear Puppet.clear Puppet.settings.clear Puppet::Util::Cacher.expire @memoryatend = Puppet::Util.memory diff = @memoryatend - @memoryatstart if diff > 1000 Puppet.info "%s#%s memory growth (%s to %s): %s" % [self.class, @method_name, @memoryatstart, @memoryatend, diff] end # reset all of the logs Puppet::Util::Log.close @logs.clear # Just in case there are processes waiting to die... require 'timeout' begin Timeout::timeout(5) do Process.waitall end rescue Timeout::Error # just move on end end def logstore @logs = [] Puppet::Util::Log.newdestination(@logs) end end require 'puppettest/support' require 'puppettest/filetesting' require 'puppettest/fakes' require 'puppettest/exetest' require 'puppettest/parsertesting' require 'puppettest/servertest' require 'puppettest/testcase' diff --git a/test/network/client/resource.rb b/test/network/client/resource.rb index ba84fc27e..c73690170 100755 --- a/test/network/client/resource.rb +++ b/test/network/client/resource.rb @@ -1,55 +1,52 @@ #!/usr/bin/env ruby require File.dirname(__FILE__) + '/../../lib/puppettest' require 'puppettest' require 'puppettest/support/utils' require 'puppettest/support/assertions' require 'puppet/network/client/resource' class TestResourceClient < Test::Unit::TestCase include PuppetTest::ServerTest include PuppetTest::Support::Utils def setup super Puppet::Type.type(:user).provider(:directoryservice).stubs(:get_macosx_version_major).returns "10.5" end def mkresourceserver Puppet::Network::Handler.resource.new end def mkclient client = Puppet::Network::Client.resource.new(:Resource => mkresourceserver) end def test_resources file = tempfile() text = "yayness\n" File.open(file, "w") { |f| f.print text } mkresourceserver() client = mkclient() # Test describing tresource = client.describe("file", file) assert(tresource, "Did not get response") assert_instance_of(Puppet::TransObject, tresource) resource = tresource.to_ral - assert_events([], resource) - File.unlink(file) - assert_events([:file_created], resource) - File.unlink(file) + assert_equal(File.stat(file).mode & 007777, resource[:mode], "Did not get mode") # Now test applying result = client.apply(tresource) assert(FileTest.exists?(file), "File was not created on apply") end end