diff --git a/CHANGELOG b/CHANGELOG index e0a693307..6334bfeac 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,1673 +1,1675 @@ 0.25.0 + 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 #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/puppetstoredconfigclean.rb b/ext/puppetstoredconfigclean.rb index f286df2df..a96ec4faf 100644 --- a/ext/puppetstoredconfigclean.rb +++ b/ext/puppetstoredconfigclean.rb @@ -1,87 +1,87 @@ #!/usr/bin/env ruby # Script to clean up stored configs for (a) given host(s) # # Credits: # Script was taken from http://reductivelabs.com/trac/puppet/attachment/wiki/UsingStoredConfiguration/kill_node_in_storedconfigs_db.rb # which haven been initially posted by James Turnbull # duritong adapted and improved the script a bit. require 'getoptlong' config = '/etc/puppet/puppet.conf' def printusage(error_code) puts "Usage: #{$0} [ list of hostnames as stored in hosts table ]" puts "\n Options:" puts "--config " exit(error_code) end opts = GetoptLong.new( [ "--config", "-c", GetoptLong::REQUIRED_ARGUMENT ], [ "--help", "-h", GetoptLong::NO_ARGUMENT ], [ "--usage", "-u", GetoptLong::NO_ARGUMENT ], [ "--version", "-v", GetoptLong::NO_ARGUMENT ] ) begin opts.each do |opt, arg| case opt when "--config" config = arg when "--help" printusage(0) when "--usage" printusage(0) when "--version" puts "%s" % Puppet.version exit end end rescue GetoptLong::InvalidOption => detail $stderr.puts "Try '#{$0} --help'" exit(1) end printusage(1) unless ARGV.size > 0 require 'puppet/rails' Puppet[:config] = config Puppet.parse_config pm_conf = Puppet.settings.instance_variable_get(:@values)[:puppetmasterd] adapter = pm_conf[:dbadapter] args = {:adapter => adapter, :log_level => pm_conf[:rails_loglevel]} case adapter - when "sqlite3": + when "sqlite3" args[:dbfile] = pm_conf[:dblocation] - when "mysql", "postgresql": + when "mysql", "postgresql" args[:host] = pm_conf[:dbserver] unless pm_conf[:dbserver].to_s.empty? args[:username] = pm_conf[:dbuser] unless pm_conf[:dbuser].to_s.empty? args[:password] = pm_conf[:dbpassword] unless pm_conf[:dbpassword].to_s.empty? args[:database] = pm_conf[:dbname] unless pm_conf[:dbname].to_s.empty? socket = pm_conf[:dbsocket] args[:socket] = socket unless socket.to_s.empty? else raise ArgumentError, "Invalid db adapter %s" % adapter end args[:database] = "puppet" unless not args[:database].to_s.empty? ActiveRecord::Base.establish_connection(args) ARGV.each { |hostname| if @host = Puppet::Rails::Host.find_by_name(hostname.strip) print "Killing #{hostname}..." $stdout.flush @host.destroy puts "done." else puts "Can't find host #{hostname}." end } exit 0 diff --git a/lib/puppet.rb b/lib/puppet.rb index 4e0266703..e9ba2f2be 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' 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: + 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/application/puppetrun.rb b/lib/puppet/application/puppetrun.rb index 42f02c796..59ace826a 100644 --- a/lib/puppet/application/puppetrun.rb +++ b/lib/puppet/application/puppetrun.rb @@ -1,216 +1,216 @@ begin require 'rubygems' rescue LoadError # Nothing; we were just doing this just in case end begin require 'ldap' rescue LoadError $stderr.puts "Failed to load ruby LDAP library. LDAP functionality will not be available" end require 'puppet' require 'puppet/application' Puppet::Application.new(:puppetrun) do should_not_parse_config attr_accessor :hosts, :tags, :classes option("--all","-a") option("--foreground","-f") option("--debug","-d") option("--ping","-P") option("--test") option("--host HOST") do |arg| @hosts << arg end option("--tag TAG", "-t") do |arg| @tags << arg end option("--class CLASS", "-c") do |arg| @classes << arg end option("--no-fqdn", "-n") do |arg| options[:fqdn] = false end option("--parallel PARALLEL", "-p") do |arg| begin options[:parallel] = Integer(arg) rescue $stderr.puts "Could not convert %s to an integer" % arg.inspect exit(23) end end dispatch do options[:test] ? :test : :main end command(:test) do puts "Skipping execution in test mode" exit(0) end command(:main) do require 'puppet/network/client' require 'puppet/util/ldap/connection' todo = @hosts.dup failures = [] # Now do the actual work go = true while go # If we don't have enough children in process and we still have hosts left to # do, then do the next host. if @children.length < options[:parallel] and ! todo.empty? host = todo.shift pid = fork do run_for_host(host) end @children[pid] = host else # Else, see if we can reap a process. begin pid = Process.wait if host = @children[pid] # Remove our host from the list of children, so the parallelization # continues working. @children.delete(pid) if $?.exitstatus != 0 failures << host end print "%s finished with exit code %s\n" % [host, $?.exitstatus] else $stderr.puts "Could not find host for PID %s with status %s" % [pid, $?.exitstatus] end rescue Errno::ECHILD # There are no children left, so just exit unless there are still # children left to do. next unless todo.empty? if failures.empty? puts "Finished" exit(0) else puts "Failed: %s" % failures.join(", ") exit(3) end end end end end def run_for_host(host) if options[:ping] out = %x{ping -c 1 #{host}} unless $? == 0 $stderr.print "Could not contact %s\n" % host next end end client = Puppet::Network::Client.runner.new( :Server => host, :Port => Puppet[:puppetport] ) print "Triggering %s\n" % host begin result = client.run(@tags, options[:ignoreschedules], options[:foreground]) rescue => detail $stderr.puts "Host %s failed: %s\n" % [host, detail] exit(2) end case result - when "success": exit(0) - when "running": + when "success"; exit(0) + when "running" $stderr.puts "Host %s is already running" % host exit(3) else $stderr.puts "Host %s returned unknown answer '%s'" % [host, result] exit(12) end end preinit do [:INT, :TERM].each do |signal| trap(signal) do $stderr.puts "Cancelling" exit(1) end end options[:parallel] = 1 options[:verbose] = true options[:fqdn] = true @hosts = [] @classes = [] @tags = [] end setup do if options[:debug] Puppet::Util::Log.level = :debug else Puppet::Util::Log.level = :info end # Now parse the config Puppet.parse_config if Puppet[:node_terminus] == "ldap" and (options[:all] or @classes) if options[:all] @hosts = Puppet::Node.search("whatever").collect { |node| node.name } puts "all: %s" % @hosts.join(", ") else @hosts = [] @classes.each do |klass| list = Puppet::Node.search("whatever", :class => klass).collect { |node| node.name } puts "%s: %s" % [klass, list.join(", ")] @hosts += list end end elsif ! @classes.empty? $stderr.puts "You must be using LDAP to specify host classes" exit(24) end if @tags.empty? @tags = "" else @tags = @tags.join(",") end @children = {} # If we get a signal, then kill all of our children and get out. [:INT, :TERM].each do |signal| trap(signal) do Puppet.notice "Caught #{signal}; shutting down" @children.each do |pid, host| Process.kill("INT", pid) end waitall exit(1) end end end end diff --git a/lib/puppet/configurer.rb b/lib/puppet/configurer.rb index b29082d5a..e6fdbee15 100644 --- a/lib/puppet/configurer.rb +++ b/lib/puppet/configurer.rb @@ -1,166 +1,166 @@ # The client for interacting with the puppetmaster config server. require 'sync' require 'timeout' require 'puppet/network/http_pool' require 'puppet/util' class Puppet::Configurer require 'puppet/configurer/fact_handler' require 'puppet/configurer/plugin_handler' include Puppet::Configurer::FactHandler include Puppet::Configurer::PluginHandler # For benchmarking include Puppet::Util attr_accessor :catalog attr_reader :compile_time # Provide more helpful strings to the logging that the Agent does def self.to_s "Puppet configuration client" end class << self # Puppetd should only have one instance running, and we need a way # to retrieve it. attr_accessor :instance include Puppet::Util end # How to lock instances of this class. def self.lockfile_path Puppet[:puppetdlockfile] end def clear @catalog.clear(true) if @catalog @catalog = nil end # Initialize and load storage def dostorage begin Puppet::Util::Storage.load @compile_time ||= Puppet::Util::Storage.cache(:configuration)[:compile_time] rescue => detail if Puppet[:trace] puts detail.backtrace end Puppet.err "Corrupt state file %s: %s" % [Puppet[:statefile], detail] begin ::File.unlink(Puppet[:statefile]) retry rescue => detail raise Puppet::Error.new("Cannot remove %s: %s" % [Puppet[:statefile], detail]) end end end # Just so we can specify that we are "the" instance. def initialize Puppet.settings.use(:main, :ssl, :puppetd) self.class.instance = self @running = false @splayed = false end # Prepare for catalog retrieval. Downloads everything necessary, etc. def prepare dostorage() download_plugins() download_fact_plugins() upload_facts() end # Get the remote catalog, yo. Returns nil if no catalog can be found. def retrieve_catalog name = Facter.value("hostname") catalog_class = Puppet::Resource::Catalog # First try it with no cache, then with the cache. result = nil begin duration = thinmark do result = catalog_class.find(name, :ignore_cache => true) end rescue => detail puts detail.backtrace if Puppet[:trace] Puppet.err "Could not retrieve catalog from remote server: %s" % detail end unless result begin duration = thinmark do result = catalog_class.find(name, :ignore_terminus => true) end rescue => detail puts detail.backtrace if Puppet[:trace] Puppet.err "Could not retrieve catalog from cache: %s" % detail end end return nil unless result convert_catalog(result, duration) end # Convert a plain resource catalog into our full host catalog. def convert_catalog(result, duration) catalog = result.to_ral catalog.retrieval_duration = duration catalog.host_config = true catalog.write_class_file return catalog end # The code that actually runs the catalog. # This just passes any options on to the catalog, # which accepts :tags and :ignoreschedules. def run(options = {}) prepare() unless catalog = retrieve_catalog Puppet.err "Could not retrieve catalog; skipping run" return end begin benchmark(:notice, "Finished catalog run") do catalog.apply(options) end rescue => detail puts detail.backtrace if Puppet[:trace] Puppet.err "Failed to apply catalog: %s" % detail end # Now close all of our existing http connections, since there's no # reason to leave them lying open. Puppet::Network::HttpPool.clear_http_instances end private def self.timeout timeout = Puppet[:configtimeout] case timeout - when String: + when String if timeout =~ /^\d+$/ timeout = Integer(timeout) else raise ArgumentError, "Configuration timeout must be an integer" end - when Integer: # nothing + when Integer # nothing else raise ArgumentError, "Configuration timeout must be an integer" end return timeout end end diff --git a/lib/puppet/configurer/downloader.rb b/lib/puppet/configurer/downloader.rb index 8a2eb0b82..9653c3a58 100644 --- a/lib/puppet/configurer/downloader.rb +++ b/lib/puppet/configurer/downloader.rb @@ -1,79 +1,79 @@ require 'puppet/configurer' require 'puppet/resource/catalog' class Puppet::Configurer::Downloader attr_reader :name, :path, :source, :ignore # Determine the timeout value to use. def self.timeout timeout = Puppet[:configtimeout] case timeout - when String: + when String if timeout =~ /^\d+$/ timeout = Integer(timeout) else raise ArgumentError, "Configuration timeout must be an integer" end - when Integer: # nothing + when Integer # nothing else raise ArgumentError, "Configuration timeout must be an integer" end return timeout end # Evaluate our download, returning the list of changed values. def evaluate Puppet.info "Retrieving #{name}" files = [] begin Timeout.timeout(self.class.timeout) do catalog.apply do |trans| trans.changed?.find_all do |resource| yield resource if block_given? files << resource[:path] end end end rescue Puppet::Error, Timeout::Error => detail puts detail.backtrace if Puppet[:debug] Puppet.err "Could not retrieve #{name}: %s" % detail end return files end def initialize(name, path, source, ignore = nil) @name, @path, @source, @ignore = name, path, source, ignore end def catalog catalog = Puppet::Resource::Catalog.new catalog.add_resource(file) catalog end def file args = default_arguments.merge(:path => path, :source => source) args[:ignore] = ignore if ignore Puppet::Type.type(:file).new(args) end private def default_arguments { :path => path, :recurse => true, :source => source, :tag => name, :owner => Process.uid, :group => Process.gid, :purge => true, :force => true, :backup => false, :noop => false } end end diff --git a/lib/puppet/file_serving/configuration/parser.rb b/lib/puppet/file_serving/configuration/parser.rb index f36bef639..c86e00a62 100644 --- a/lib/puppet/file_serving/configuration/parser.rb +++ b/lib/puppet/file_serving/configuration/parser.rb @@ -1,129 +1,129 @@ require 'puppet/file_serving/configuration' require 'puppet/util/loadedfile' class Puppet::FileServing::Configuration::Parser < Puppet::Util::LoadedFile Mount = Puppet::FileServing::Mount MODULES = 'modules' # Parse our configuration file. def parse raise("File server configuration %s does not exist" % self.file) unless FileTest.exists?(self.file) raise("Cannot read file server configuration %s" % self.file) unless FileTest.readable?(self.file) @mounts = {} @count = 0 File.open(self.file) { |f| mount = nil f.each { |line| # Have the count increment at the top, in case we throw exceptions. @count += 1 case line - when /^\s*#/: next # skip comments - when /^\s*$/: next # skip blank lines - when /\[([-\w]+)\]/: + when /^\s*#/; next # skip comments + when /^\s*$/; next # skip blank lines + when /\[([-\w]+)\]/ mount = newmount($1) - when /^\s*(\w+)\s+(.+)$/: + when /^\s*(\w+)\s+(.+)$/ var = $1 value = $2 raise(ArgumentError, "Fileserver configuration file does not use '=' as a separator") if value =~ /^=/ case var - when "path": + when "path" path(mount, value) - when "allow": + when "allow" allow(mount, value) - when "deny": + when "deny" deny(mount, value) else raise ArgumentError.new("Invalid argument '%s'" % var, @count, file) end else raise ArgumentError.new("Invalid line '%s'" % line.chomp, @count, file) end } } mk_default_mounts validate() return @mounts end private # Allow a given pattern access to a mount. def allow(mount, value) # LAK:NOTE See http://snurl.com/21zf8 [groups_google_com] x = value.split(/\s*,\s*/).each { |val| begin mount.info "allowing %s access" % val mount.allow(val) rescue AuthStoreError => detail raise ArgumentError.new(detail.to_s, @count, file) end } end # Deny a given pattern access to a mount. def deny(mount, value) # LAK:NOTE See http://snurl.com/21zf8 [groups_google_com] x = value.split(/\s*,\s*/).each { |val| begin mount.info "denying %s access" % val mount.deny(val) rescue AuthStoreError => detail raise ArgumentError.new(detail.to_s, @count, file) end } end def mk_default_mounts ["plugins", "modules"].each do |name| newmount(name) unless @mounts[name] end end # Create a new mount. def newmount(name) if @mounts.include?(name) raise ArgumentError, "%s is already mounted at %s" % [@mounts[name], name], @count, file end case name when "modules" mount = Mount::Modules.new(name) when "plugins" mount = Mount::Plugins.new(name) else mount = Mount::File.new(name) end @mounts[name] = mount return mount end # Set the path for a mount. def path(mount, value) if mount.respond_to?(:path=) begin mount.path = value rescue ArgumentError => detail Puppet.err "Removing mount %s: %s" % [mount.name, detail] @mounts.delete(mount.name) end else Puppet.warning "The '#{mount.name}' module can not have a path. Ignoring attempt to set it" end end # Make sure all of our mounts are valid. We have to do this after the fact # because details are added over time as the file is parsed. def validate @mounts.each { |name, mount| mount.validate } end end diff --git a/lib/puppet/file_serving/metadata.rb b/lib/puppet/file_serving/metadata.rb index 1fc2b40ab..335dad497 100644 --- a/lib/puppet/file_serving/metadata.rb +++ b/lib/puppet/file_serving/metadata.rb @@ -1,78 +1,78 @@ # # Created by Luke Kanies on 2007-10-16. # Copyright (c) 2007. All rights reserved. require 'puppet' require 'puppet/indirector' require 'puppet/file_serving' require 'puppet/file_serving/base' require 'puppet/util/checksums' require 'puppet/file_serving/indirection_hooks' # A class that handles retrieving file metadata. class Puppet::FileServing::Metadata < Puppet::FileServing::Base include Puppet::Util::Checksums extend Puppet::Indirector indirects :file_metadata, :extend => Puppet::FileServing::IndirectionHooks attr_reader :path, :owner, :group, :mode, :checksum_type, :checksum, :ftype, :destination PARAM_ORDER = [:mode, :ftype, :owner, :group] def attributes_with_tabs desc = [] PARAM_ORDER.each { |check| check = :ftype if check == :type desc << send(check) } case ftype - when "file", "directory": desc << checksum - when "link": desc << @destination + when "file", "directory"; desc << checksum + when "link"; desc << @destination else raise ArgumentError, "Cannot manage files of type %s" % ftype end return desc.join("\t") end def checksum_type=(type) raise(ArgumentError, "Unsupported checksum type %s" % type) unless respond_to?("%s_file" % type) @checksum_type = type end # Retrieve the attributes for this file, relative to a base directory. # Note that File.stat raises Errno::ENOENT if the file is absent and this # method does not catch that exception. def collect real_path = full_path() stat = stat() @owner = stat.uid @group = stat.gid @ftype = stat.ftype # We have to mask the mode, yay. @mode = stat.mode & 007777 case stat.ftype - when "file": + when "file" @checksum = ("{%s}" % @checksum_type) + send("%s_file" % @checksum_type, real_path).to_s - when "directory": # Always just timestamp the directory. + when "directory" # Always just timestamp the directory. @checksum_type = "ctime" @checksum = ("{%s}" % @checksum_type) + send("%s_file" % @checksum_type, path).to_s - when "link": + when "link" @destination = File.readlink(real_path) else raise ArgumentError, "Cannot manage files of type %s" % stat.ftype end end def initialize(*args) @checksum_type = "md5" super end end diff --git a/lib/puppet/indirector/facts/facter.rb b/lib/puppet/indirector/facts/facter.rb index e664e17c7..9df71fcac 100644 --- a/lib/puppet/indirector/facts/facter.rb +++ b/lib/puppet/indirector/facts/facter.rb @@ -1,79 +1,79 @@ require 'puppet/node/facts' require 'puppet/indirector/code' class Puppet::Node::Facts::Facter < Puppet::Indirector::Code desc "Retrieve facts from Facter. This provides a somewhat abstract interface between Puppet and Facter. It's only `somewhat` abstract because it always returns the local host's facts, regardless of what you attempt to find." def self.load_fact_plugins # Add any per-module fact directories to the factpath module_fact_dirs = Puppet[:modulepath].split(":").collect do |d| Dir.glob("%s/*/plugins/facter" % d) end.flatten dirs = module_fact_dirs + Puppet[:factpath].split(":") x = dirs.each do |dir| load_facts_in_dir(dir) end end def self.load_facts_in_dir(dir) return unless FileTest.directory?(dir) Dir.chdir(dir) do Dir.glob("*.rb").each do |file| fqfile = ::File.join(dir, file) begin Puppet.info "Loading facts in %s" % [::File.basename(file.sub(".rb",''))] Timeout::timeout(self.timeout) do load file end rescue => detail Puppet.warning "Could not load fact file %s: %s" % [fqfile, detail] end end end end def self.timeout timeout = Puppet[:configtimeout] case timeout - when String: + when String if timeout =~ /^\d+$/ timeout = Integer(timeout) else raise ArgumentError, "Configuration timeout must be an integer" end - when Integer: # nothing + when Integer # nothing else raise ArgumentError, "Configuration timeout must be an integer" end return timeout end def initialize(*args) super self.class.load_fact_plugins end def destroy(facts) raise Puppet::DevError, "You cannot destroy facts in the code store; it is only used for getting facts from Facter" end # Look a host's facts up in Facter. def find(request) result = Puppet::Node::Facts.new(request.key, Facter.to_hash) result.add_local_facts result.stringify result.downcase_if_necessary result end def save(facts) raise Puppet::DevError, "You cannot save facts to the code store; it is only used for getting facts from Facter" end end diff --git a/lib/puppet/network/authconfig.rb b/lib/puppet/network/authconfig.rb index 8e3773719..dc67723c4 100644 --- a/lib/puppet/network/authconfig.rb +++ b/lib/puppet/network/authconfig.rb @@ -1,167 +1,167 @@ require 'puppet/util/loadedfile' require 'puppet/network/rights' module Puppet class ConfigurationError < Puppet::Error; end class Network::AuthConfig < Puppet::Util::LoadedFile def self.main unless defined? @main @main = self.new() end @main end # Just proxy the setting methods to our rights stuff [:allow, :deny].each do |method| define_method(method) do |*args| @rights.send(method, *args) end end # Here we add a little bit of semantics. They can set auth on a whole # namespace or on just a single method in the namespace. def allowed?(request) name = request.call.intern namespace = request.handler.intern method = request.method.intern read() if @rights.include?(name) return @rights[name].allowed?(request.name, request.ip) elsif @rights.include?(namespace) return @rights[namespace].allowed?(request.name, request.ip) else return false end end # Does the file exist? Puppetmasterd does not require it, but # puppetd does. def exists? FileTest.exists?(@file) end def initialize(file = nil, parsenow = true) @file ||= Puppet[:authconfig] unless @file raise Puppet::DevError, "No authconfig file defined" end return unless self.exists? super(@file) @rights = Puppet::Network::Rights.new @configstamp = @configstatted = nil @configtimeout = 60 if parsenow read() end end # Read the configuration file. def read return unless FileTest.exists?(@file) if @configstamp if @configtimeout and @configstatted if Time.now - @configstatted > @configtimeout @configstatted = Time.now tmp = File.stat(@file).ctime if tmp == @configstamp return else Puppet.notice "%s vs %s" % [tmp, @configstamp] end else return end else Puppet.notice "%s and %s" % [@configtimeout, @configstatted] end end parse() @configstamp = File.stat(@file).ctime @configstatted = Time.now end private def parse newrights = Puppet::Network::Rights.new begin File.open(@file) { |f| right = nil count = 1 f.each { |line| case line - when /^\s*#/: next # skip comments - when /^\s*$/: next # skip blank lines - when /\[([\w.]+)\]/: # "namespace" or "namespace.method" + when /^\s*#/; next # skip comments + when /^\s*$/; next # skip blank lines + when /\[([\w.]+)\]/ # "namespace" or "namespace.method" name = $1 if newrights.include?(name) raise FileServerError, "%s is already set at %s" % [newrights[name], name] end newrights.newright(name) right = newrights[name] - when /^\s*(\w+)\s+(.+)$/: + when /^\s*(\w+)\s+(.+)$/ var = $1 value = $2 case var - when "allow": + when "allow" value.split(/\s*,\s*/).each { |val| begin right.info "allowing %s access" % val right.allow(val) rescue AuthStoreError => detail raise ConfigurationError, "%s at line %s of %s" % [detail.to_s, count, @config] end } - when "deny": + when "deny" value.split(/\s*,\s*/).each { |val| begin right.info "denying %s access" % val right.deny(val) rescue AuthStoreError => detail raise ConfigurationError, "%s at line %s of %s" % [detail.to_s, count, @config] end } else raise ConfigurationError, "Invalid argument '%s' at line %s" % [var, count] end else raise ConfigurationError, "Invalid line %s: %s" % [count, line] end count += 1 } } rescue Errno::EACCES => detail Puppet.err "Configuration error: Cannot read %s; cannot serve" % @file #raise Puppet::Error, "Cannot read %s" % @config rescue Errno::ENOENT => detail Puppet.err "Configuration error: '%s' does not exit; cannot serve" % @file #raise Puppet::Error, "%s does not exit" % @config #rescue FileServerError => detail # Puppet.err "FileServer error: %s" % detail end # Verify each of the rights are valid. # We let the check raise an error, so that it can raise an error # pointing to the specific problem. newrights.each { |name, right| right.valid? } @rights = newrights end end end diff --git a/lib/puppet/network/authstore.rb b/lib/puppet/network/authstore.rb index cb1fdc57d..7341f8a1e 100755 --- a/lib/puppet/network/authstore.rb +++ b/lib/puppet/network/authstore.rb @@ -1,297 +1,297 @@ # standard module for determining whether a given hostname or IP has access to # the requested resource require 'ipaddr' require 'puppet/util/logging' module Puppet class AuthStoreError < Puppet::Error; end class AuthorizationError < Puppet::Error; end class Network::AuthStore include Puppet::Util::Logging # Mark a given pattern as allowed. def allow(pattern) # a simple way to allow anyone at all to connect if pattern == "*" @globalallow = true else store(:allow, pattern) end return nil end # Is a given combination of name and ip address allowed? If either input # is non-nil, then both inputs must be provided. If neither input # is provided, then the authstore is considered local and defaults to "true". def allowed?(name, ip) if name or ip # This is probably unnecessary, and can cause some weirdnesses in # cases where we're operating over localhost but don't have a real # IP defined. unless name and ip raise Puppet::DevError, "Name and IP must be passed to 'allowed?'" end # else, we're networked and such else # we're local return true end # yay insecure overrides if globalallow? return true end if decl = @declarations.find { |d| d.match?(name, ip) } return decl.result end self.info "defaulting to no access for %s" % name return false end # Deny a given pattern. def deny(pattern) store(:deny, pattern) end # Is global allow enabled? def globalallow? @globalallow end def initialize @globalallow = nil @declarations = [] end def to_s "authstore" end private # Store the results of a pattern into our hash. Basically just # converts the pattern and sticks it into the hash. def store(type, pattern) @declarations << Declaration.new(type, pattern) @declarations.sort! return nil end # A single declaration. Stores the info for a given declaration, # provides the methods for determining whether a declaration matches, # and handles sorting the declarations appropriately. class Declaration include Puppet::Util include Comparable # The type of declaration: either :allow or :deny attr_reader :type # The name: :ip or :domain attr_accessor :name # The pattern we're matching against. Can be an IPAddr instance, # or an array of strings, resulting from reversing a hostname # or domain name. attr_reader :pattern # The length. Only used for iprange and domain. attr_accessor :length # Sort the declarations specially. def <=>(other) # Sort first based on whether the matches are exact. if r = compare(exact?, other.exact?) return r end # Then by type if r = compare(self.ip?, other.ip?) return r end # Next sort based on length unless self.length == other.length # Longer names/ips should go first, because they're more # specific. return other.length <=> self.length end # Then sort deny before allow if r = compare(self.deny?, other.deny?) return r end # We've already sorted by name and length, so all that's left # is the pattern if ip? return self.pattern.to_s <=> other.pattern.to_s else return self.pattern <=> other.pattern end end def deny? self.type == :deny end # Are we an exact match? def exact? self.length.nil? end def initialize(type, pattern) self.type = type self.pattern = pattern end # Are we an IP type? def ip? self.name == :ip end # Does this declaration match the name/ip combo? def match?(name, ip) if self.ip? return pattern.include?(IPAddr.new(ip)) else return matchname?(name) end end # Set the pattern appropriately. Also sets the name and length. def pattern=(pattern) parse(pattern) @orig = pattern end # Mapping a type of statement into a return value. def result case @type - when :allow: true + when :allow; true else false end end def to_s "%s: %s" % [self.type, self.pattern] end # Set the declaration type. Either :allow or :deny. def type=(type) type = symbolize(type) unless [:allow, :deny].include?(type) raise ArgumentError, "Invalid declaration type %s" % type end @type = type end private # Returns nil if both values are true or both are false, returns # -1 if the first is true, and 1 if the second is true. Used # in the <=> operator. def compare(me, them) unless me and them if me return -1 elsif them return 1 else return false end end return nil end # Does the name match our pattern? def matchname?(name) name = munge_name(name) return true if self.pattern == name # If it's an exact match, then just return false, since the # exact didn't match. if exact? return false end # If every field in the pattern matches, then we consider it # a match. pattern.zip(name) do |p,n| unless p == n return false end end return true end # Convert the name to a common pattern. def munge_name(name) # LAK:NOTE http://snurl.com/21zf8 [groups_google_com] x = name.downcase.split(".").reverse end # Parse our input pattern and figure out what kind of allowal # statement it is. The output of this is used for later matching. def parse(value) case value - when /^(\d+\.){1,3}\*$/: # an ip address with a '*' at the end + when /^(\d+\.){1,3}\*$/ # an ip address with a '*' at the end @name = :ip match = $1 match.sub!(".", '') ary = value.split(".") mask = case ary.index(match) - when 0: 8 - when 1: 16 - when 2: 24 + when 0; 8 + when 1; 16 + when 2; 24 else raise AuthStoreError, "Invalid IP pattern %s" % value end @length = mask ary.pop while ary.length < 4 ary.push("0") end begin @pattern = IPAddr.new(ary.join(".") + "/" + mask.to_s) rescue ArgumentError => detail raise AuthStoreError, "Invalid IP address pattern %s" % value end - when /^([a-zA-Z][-\w]*\.)+[-\w]+$/: # a full hostname + when /^([a-zA-Z][-\w]*\.)+[-\w]+$/ # a full hostname @name = :domain @pattern = munge_name(value) - when /^\*(\.([a-zA-Z][-\w]*)){1,}$/: # *.domain.com + when /^\*(\.([a-zA-Z][-\w]*)){1,}$/ # *.domain.com @name = :domain @pattern = munge_name(value) @pattern.pop # take off the '*' @length = @pattern.length else # Else, use the IPAddr class to determine if we've got a # valid IP address. if value =~ /\/(\d+)$/ @length = Integer($1) end begin @pattern = IPAddr.new(value) rescue ArgumentError => detail raise AuthStoreError, "Invalid pattern %s" % value end @name = :ip end end end end end diff --git a/lib/puppet/network/handler/fileserver.rb b/lib/puppet/network/handler/fileserver.rb index b39396091..86b0b5bb6 100755 --- a/lib/puppet/network/handler/fileserver.rb +++ b/lib/puppet/network/handler/fileserver.rb @@ -1,752 +1,752 @@ require 'puppet' require 'puppet/network/authstore' require 'webrick/httpstatus' require 'cgi' require 'delegate' require 'sync' require 'puppet/file_serving' require 'puppet/file_serving/metadata' class Puppet::Network::Handler AuthStoreError = Puppet::AuthStoreError class FileServerError < Puppet::Error; end class FileServer < Handler desc "The interface to Puppet's fileserving abilities." attr_accessor :local CHECKPARAMS = [:mode, :type, :owner, :group, :checksum] # Special filserver module for puppet's module system MODULES = "modules" PLUGINS = "plugins" @interface = XMLRPC::Service::Interface.new("fileserver") { |iface| iface.add_method("string describe(string, string)") iface.add_method("string list(string, string, boolean, array)") iface.add_method("string retrieve(string, string)") } def self.params CHECKPARAMS.dup end # If the configuration file exists, then create (if necessary) a LoadedFile # object to manage it; else, return nil. def configuration # Short-circuit the default case. return @configuration if defined?(@configuration) config_path = @passed_configuration_path || Puppet[:fileserverconfig] return nil unless FileTest.exist?(config_path) # The file exists but we don't have a LoadedFile instance for it. @configuration = Puppet::Util::LoadedFile.new(config_path) end # Create our default mounts for modules and plugins. This is duplicated code, # but I'm not really worried about that. def create_default_mounts @mounts = {} Puppet.debug "No file server configuration file; autocreating #{MODULES} mount with default permissions" mount = Mount.new(MODULES) mount.allow("*") @mounts[MODULES] = mount Puppet.debug "No file server configuration file; autocreating #{PLUGINS} mount with default permissions" mount = PluginMount.new(PLUGINS) mount.allow("*") @mounts[PLUGINS] = mount end # Describe a given file. This returns all of the manageable aspects # of that file. def describe(url, links = :follow, client = nil, clientip = nil) links = links.intern if links.is_a? String mount, path = convert(url, client, clientip) mount.debug("Describing %s for %s" % [url, client]) if client # use the mount to resolve the path for us. return "" unless full_path = mount.file_path(path, client) metadata = Puppet::FileServing::Metadata.new(url, :path => full_path, :links => links) return "" unless metadata.exist? begin metadata.collect rescue => detail puts detail.backtrace if Puppet[:trace] Puppet.err detail return "" end return metadata.attributes_with_tabs end # Create a new fileserving module. def initialize(hash = {}) @mounts = {} @files = {} if hash[:Local] @local = hash[:Local] else @local = false end if hash[:Config] == false @noreadconfig = true end @passed_configuration_path = hash[:Config] if hash.include?(:Mount) @passedconfig = true unless hash[:Mount].is_a?(Hash) raise Puppet::DevError, "Invalid mount hash %s" % hash[:Mount].inspect end hash[:Mount].each { |dir, name| if FileTest.exists?(dir) self.mount(dir, name) end } self.mount(nil, MODULES) self.mount(nil, PLUGINS) else @passedconfig = false if configuration readconfig(false) # don't check the file the first time. else create_default_mounts() end end end # List a specific directory's contents. def list(url, links = :ignore, recurse = false, ignore = false, client = nil, clientip = nil) mount, path = convert(url, client, clientip) mount.debug "Listing %s for %s" % [url, client] if client return "" unless mount.path_exists?(path, client) desc = mount.list(path, recurse, ignore, client) if desc.length == 0 mount.notice "Got no information on //%s/%s" % [mount, path] return "" end desc.collect { |sub| sub.join("\t") }.join("\n") end def local? self.local end # Is a given mount available? def mounted?(name) @mounts.include?(name) end # Mount a new directory with a name. def mount(path, name) if @mounts.include?(name) if @mounts[name] != path raise FileServerError, "%s is already mounted at %s" % [@mounts[name].path, name] else # it's already mounted; no problem return end end # Let the mounts do their own error-checking. @mounts[name] = Mount.new(name, path) @mounts[name].info "Mounted %s" % path return @mounts[name] end # Retrieve a file from the local disk and pass it to the remote # client. def retrieve(url, links = :ignore, client = nil, clientip = nil) links = links.intern if links.is_a? String mount, path = convert(url, client, clientip) if client mount.info "Sending %s to %s" % [url, client] end unless mount.path_exists?(path, client) mount.debug "#{mount} reported that #{path} does not exist" return "" end links = links.intern if links.is_a? String if links == :ignore and FileTest.symlink?(path) mount.debug "I think that #{path} is a symlink and we're ignoring them" return "" end str = mount.read_file(path, client) if @local return str else return CGI.escape(str) end end def umount(name) @mounts.delete(name) if @mounts.include? name end private def authcheck(file, mount, client, clientip) # If we're local, don't bother passing in information. if local? client = nil clientip = nil end unless mount.allowed?(client, clientip) mount.warning "%s cannot access %s" % [client, file] raise Puppet::AuthorizationError, "Cannot access %s" % mount end end # Take a URL and some client info and return a mount and relative # path pair. # def convert(url, client, clientip) readconfig url = URI.unescape(url) mount, stub = splitpath(url, client) authcheck(url, mount, client, clientip) return mount, stub end # Return the mount for the Puppet modules; allows file copying from # the modules. def modules_mount(module_name, client) # Find our environment, if we have one. unless hostname = (client || Facter.value("hostname")) raise ArgumentError, "Could not find hostname" end if node = Puppet::Node.find(hostname) env = node.environment else env = nil end # And use the environment to look up the module. if mod = Puppet::Node::Environment.new(env).module(module_name) return @mounts[MODULES].copy(mod.name, mod.files) else return nil end end # Read the configuration file. def readconfig(check = true) return if @noreadconfig return unless configuration if check and ! @configuration.changed? return end newmounts = {} begin File.open(@configuration.file) { |f| mount = nil count = 1 f.each { |line| case line - when /^\s*#/: next # skip comments - when /^\s*$/: next # skip blank lines - when /\[([-\w]+)\]/: + when /^\s*#/; next # skip comments + when /^\s*$/; next # skip blank lines + when /\[([-\w]+)\]/ name = $1 if newmounts.include?(name) raise FileServerError, "%s is already mounted at %s" % [newmounts[name], name], count, @configuration.file end mount = Mount.new(name) newmounts[name] = mount - when /^\s*(\w+)\s+(.+)$/: + when /^\s*(\w+)\s+(.+)$/ var = $1 value = $2 case var - when "path": + when "path" if mount.name == MODULES Puppet.warning "The '#{mount.name}' module can not have a path. Ignoring attempt to set it" else begin mount.path = value rescue FileServerError => detail Puppet.err "Removing mount %s: %s" % [mount.name, detail] newmounts.delete(mount.name) end end - when "allow": + when "allow" value.split(/\s*,\s*/).each { |val| begin mount.info "allowing %s access" % val mount.allow(val) rescue AuthStoreError => detail raise FileServerError.new(detail.to_s, count, @configuration.file) end } - when "deny": + when "deny" value.split(/\s*,\s*/).each { |val| begin mount.info "denying %s access" % val mount.deny(val) rescue AuthStoreError => detail raise FileServerError.new(detail.to_s, count, @configuration.file) end } else raise FileServerError.new("Invalid argument '%s'" % var, count, @configuration.file) end else raise FileServerError.new("Invalid line '%s'" % line.chomp, count, @configuration.file) end count += 1 } } rescue Errno::EACCES => detail Puppet.err "FileServer error: Cannot read %s; cannot serve" % @configuration #raise Puppet::Error, "Cannot read %s" % @configuration rescue Errno::ENOENT => detail Puppet.err "FileServer error: '%s' does not exist; cannot serve" % @configuration end unless newmounts[MODULES] Puppet.debug "No #{MODULES} mount given; autocreating with default permissions" mount = Mount.new(MODULES) mount.allow("*") newmounts[MODULES] = mount end unless newmounts[PLUGINS] Puppet.debug "No #{PLUGINS} mount given; autocreating with default permissions" mount = PluginMount.new(PLUGINS) mount.allow("*") newmounts[PLUGINS] = mount end unless newmounts[PLUGINS].valid? Puppet.debug "No path given for #{PLUGINS} mount; creating a special PluginMount" # We end up here if the user has specified access rules for # the plugins mount, without specifying a path (which means # they want to have the default behaviour for the mount, but # special access control). So we need to move all the # user-specified access controls into the new PluginMount # object... mount = PluginMount.new(PLUGINS) # Yes, you're allowed to hate me for this. mount.instance_variable_set(:@declarations, newmounts[PLUGINS].instance_variable_get(:@declarations) ) newmounts[PLUGINS] = mount end # Verify each of the mounts are valid. # We let the check raise an error, so that it can raise an error # pointing to the specific problem. newmounts.each { |name, mount| unless mount.valid? raise FileServerError, "Invalid mount %s" % name end } @mounts = newmounts end # Split the path into the separate mount point and path. def splitpath(dir, client) # the dir is based on one of the mounts # so first retrieve the mount path mount = nil path = nil if dir =~ %r{/([-\w]+)} # Strip off the mount name. mount_name, path = dir.sub(%r{^/}, '').split(File::Separator, 2) unless mount = modules_mount(mount_name, client) unless mount = @mounts[mount_name] raise FileServerError, "Fileserver module '%s' not mounted" % mount_name end end else raise FileServerError, "Fileserver error: Invalid path '%s'" % dir end if path.nil? or path == '' path = '/' elsif path # Remove any double slashes that might have occurred path = URI.unescape(path.gsub(/\/\//, "/")) end return mount, path end def to_s "fileserver" end # A simple class for wrapping mount points. Instances of this class # don't know about the enclosing object; they're mainly just used for # authorization. class Mount < Puppet::Network::AuthStore attr_reader :name @@syncs = {} @@files = {} Puppet::Util.logmethods(self, true) # Create a map for a specific client. def clientmap(client) { "h" => client.sub(/\..*$/, ""), "H" => client, "d" => client.sub(/[^.]+\./, "") # domain name } end # Replace % patterns as appropriate. def expand(path, client = nil) # This map should probably be moved into a method. map = nil if client map = clientmap(client) else Puppet.notice "No client; expanding '%s' with local host" % path # Else, use the local information map = localmap() end path.gsub(/%(.)/) do |v| key = $1 if key == "%" "%" else map[key] || v end end end # Do we have any patterns in our path, yo? def expandable? if defined? @expandable @expandable else false end end # Return a fully qualified path, given a short path and # possibly a client name. def file_path(relative_path, node = nil) full_path = path(node) unless full_path p self raise ArgumentError.new("Mounts without paths are not usable") unless full_path end # If there's no relative path name, then we're serving the mount itself. return full_path unless relative_path and relative_path != "/" return File.join(full_path, relative_path) end # Create out object. It must have a name. def initialize(name, path = nil) unless name =~ %r{^[-\w]+$} raise FileServerError, "Invalid name format '%s'" % name end @name = name if path self.path = path else @path = nil end @files = {} super() end def fileobj(path, links, client) obj = nil if obj = @files[file_path(path, client)] # This can only happen in local fileserving, but it's an # important one. It'd be nice if we didn't just set # the check params every time, but I'm not sure it's worth # the effort. obj[:check] = CHECKPARAMS else obj = Puppet::Type.type(:file).new( :name => file_path(path, client), :check => CHECKPARAMS ) @files[file_path(path, client)] = obj end if links == :manage links = :follow end # This, ah, might be completely redundant unless obj[:links] == links obj[:links] = links end return obj end # Read the contents of the file at the relative path given. def read_file(relpath, client) File.read(file_path(relpath, client)) end # Cache this manufactured map, since if it's used it's likely # to get used a lot. def localmap unless defined? @@localmap @@localmap = { "h" => Facter.value("hostname"), "H" => [Facter.value("hostname"), Facter.value("domain")].join("."), "d" => Facter.value("domain") } end @@localmap end # Return the path as appropriate, expanding as necessary. def path(client = nil) if expandable? return expand(@path, client) else return @path end end # Set the path. def path=(path) # FIXME: For now, just don't validate paths with replacement # patterns in them. if path =~ /%./ # Mark that we're expandable. @expandable = true else unless FileTest.exists?(path) raise FileServerError, "%s does not exist" % path end unless FileTest.directory?(path) raise FileServerError, "%s is not a directory" % path end unless FileTest.readable?(path) raise FileServerError, "%s is not readable" % path end @expandable = false end @path = path end # Verify that the path given exists within this mount's subtree. # def path_exists?(relpath, client = nil) File.exists?(file_path(relpath, client)) end # Return the current values for the object. def properties(obj) obj.retrieve.inject({}) { |props, ary| props[ary[0].name] = ary[1]; props } end # Retrieve a specific directory relative to a mount point. # If they pass in a client, then expand as necessary. def subdir(dir = nil, client = nil) basedir = self.path(client) dirname = if dir File.join(basedir, *dir.split("/")) else basedir end dirname end def sync(path) @@syncs[path] ||= Sync.new @@syncs[path] end def to_s "mount[%s]" % @name end # Verify our configuration is valid. This should really check to # make sure at least someone will be allowed, but, eh. def valid? if name == MODULES return @path.nil? else return ! @path.nil? end end # Return a new mount with the same properties as +self+, except # with a different name and path. def copy(name, path) result = self.clone result.path = path result.instance_variable_set(:@name, name) return result end # List the contents of the relative path +relpath+ of this mount. # # +recurse+ is the number of levels to recurse into the tree, # or false to provide no recursion or true if you just want to # go for broke. # # +ignore+ is an array of filenames to ignore when traversing # the list. # # The return value of this method is a complex nest of arrays, # which describes a directory tree. Each file or directory is # represented by an array, where the first element is the path # of the file (relative to the root of the mount), and the # second element is the type. A directory is represented by an # array as well, where the first element is a "directory" array, # while the remaining elements are other file or directory # arrays. Confusing? Hell yes. As an added bonus, all names # must start with a slash, because... well, I'm fairly certain # a complete explanation would involve the words "crack pipe" # and "bad batch". # def list(relpath, recurse, ignore, client = nil) abspath = file_path(relpath, client) if FileTest.exists?(abspath) if FileTest.directory?(abspath) and recurse return reclist(abspath, recurse, ignore) else return [["/", File.stat(abspath).ftype]] end end return nil end def reclist(abspath, recurse, ignore) require 'puppet/file_serving' require 'puppet/file_serving/fileset' args = { :recurse => recurse, :links => :follow } args[:ignore] = ignore if ignore fs = Puppet::FileServing::Fileset.new(abspath, args) ary = fs.files.collect do |file| if file == "." file = "/" else file = File.join("/", file ) end stat = fs.stat(File.join(abspath, file)) next if stat.nil? [ file, stat.ftype ] end return ary.compact end end # A special mount class specifically for the plugins mount -- just # has some magic to effectively do a union mount of the 'plugins' # directory of all modules. # class PluginMount < Mount def path(client) '' end def mod_path_exists?(mod, relpath, client = nil) ! mod.plugin(relpath).nil? end def path_exists?(relpath, client = nil) !valid_modules(client).find { |mod| mod.plugin(relpath) }.nil? end def valid? true end def mod_file_path(mod, relpath, client = nil) File.join(mod, PLUGINS, relpath) end def file_path(relpath, client = nil) return nil unless mod = valid_modules(client).find { |m| m.plugin(relpath) } mod.plugin(relpath) end # create a list of files by merging all modules def list(relpath, recurse, ignore, client = nil) result = [] valid_modules(client).each do |mod| if modpath = mod.plugin(relpath) if FileTest.directory?(modpath) and recurse ary = reclist(modpath, recurse, ignore) ary = [] if ary.nil? result += ary else result += [["/", File.stat(modpath).ftype]] end end end result end private def valid_modules(client) Puppet::Node::Environment.new.modules.find_all { |mod| mod.plugins? } end def add_to_filetree(f, filetree) first, rest = f.split(File::SEPARATOR, 2) end end end end diff --git a/lib/puppet/network/handler/master.rb b/lib/puppet/network/handler/master.rb index 7bde0af73..0d36c1e88 100644 --- a/lib/puppet/network/handler/master.rb +++ b/lib/puppet/network/handler/master.rb @@ -1,102 +1,102 @@ require 'openssl' require 'puppet' require 'puppet/parser/interpreter' require 'puppet/sslcertificates' require 'xmlrpc/server' require 'yaml' class Puppet::Network::Handler class MasterError < Puppet::Error; end class Master < Handler desc "Puppet's configuration interface. Used for all interactions related to generating client configurations." include Puppet::Util attr_accessor :ast attr_reader :ca @interface = XMLRPC::Service::Interface.new("puppetmaster") { |iface| iface.add_method("string getconfig(string)") iface.add_method("int freshness()") } # Tell a client whether there's a fresh config for it def freshness(client = nil, clientip = nil) # Always force a recompile. Newer clients shouldn't do this (as of April 2008). return Time.now.to_i end def initialize(hash = {}) args = {} if hash[:Local] @local = hash[:Local] else @local = false end args[:Local] = true if hash.include?(:CA) and hash[:CA] @ca = Puppet::SSLCertificates::CA.new() else @ca = nil end Puppet.debug("Creating interpreter") # This is only used by the cfengine module, or if --loadclasses was # specified in +puppet+. if hash.include?(:Classes) args[:Classes] = hash[:Classes] end end # Call our various handlers; this handler is getting deprecated. def getconfig(facts, format = "marshal", client = nil, clientip = nil) facts = decode_facts(facts) client ||= facts["hostname"] # Pass the facts to the fact handler Puppet::Node::Facts.new(client, facts).save unless local? catalog = Puppet::Resource::Catalog.find(client) case format - when "yaml": + when "yaml" return CGI.escape(catalog.extract.to_yaml(:UseBlock => true)) - when "marshal": + when "marshal" return CGI.escape(Marshal.dump(catalog.extract)) else raise "Invalid markup format '%s'" % format end end # def decode_facts(facts) if @local # we don't need to do anything, since we should already # have raw objects Puppet.debug "Our client is local" else Puppet.debug "Our client is remote" begin facts = YAML.load(CGI.unescape(facts)) rescue => detail raise XMLRPC::FaultException.new( 1, "Could not rebuild facts" ) end end return facts end # Translate our configuration appropriately for sending back to a client. def translate(config) end end end diff --git a/lib/puppet/network/handler/resource.rb b/lib/puppet/network/handler/resource.rb index e7ecbbdf2..54391c3de 100755 --- a/lib/puppet/network/handler/resource.rb +++ b/lib/puppet/network/handler/resource.rb @@ -1,190 +1,190 @@ require 'puppet' require 'puppet/network/handler' # Serve Puppet elements. Useful for querying, copying, and, um, other stuff. class Puppet::Network::Handler class Resource < Handler desc "An interface for interacting with client-based resources that can be used for querying or managing remote machines without using Puppet's central server tools. The ``describe`` and ``list`` methods return TransBuckets containing TransObject instances (``describe`` returns a single TransBucket), and the ``apply`` method accepts a TransBucket of TransObjects and applies them locally. " attr_accessor :local @interface = XMLRPC::Service::Interface.new("resource") { |iface| iface.add_method("string apply(string, string)") iface.add_method("string describe(string, string, array, array)") iface.add_method("string list(string, array, string)") } side :client # Apply a TransBucket as a transaction. def apply(bucket, format = "yaml", client = nil, clientip = nil) unless local? begin case format - when "yaml": + when "yaml" bucket = YAML::load(Base64.decode64(bucket)) else raise Puppet::Error, "Unsupported format '%s'" % format end rescue => detail raise Puppet::Error, "Could not load YAML TransBucket: %s" % detail end end catalog = bucket.to_catalog # And then apply the catalog. This way we're reusing all # the code in there. It should probably just be separated out, though. transaction = catalog.apply # And then clean up catalog.clear(true) # It'd be nice to return some kind of report, but... at this point # we have no such facility. return "success" end # Describe a given object. This returns the 'is' values for every property # available on the object type. def describe(type, name, retrieve = nil, ignore = [], format = "yaml", client = nil, clientip = nil) Puppet.info "Describing %s[%s]" % [type.to_s.capitalize, name] @local = true unless client typeklass = nil unless typeklass = Puppet::Type.type(type) raise Puppet::Error, "Puppet type %s is unsupported" % type end obj = nil retrieve ||= :all ignore ||= [] begin obj = typeklass.create(:name => name, :check => retrieve) rescue Puppet::Error => detail raise Puppet::Error, "%s[%s] could not be created: %s" % [type, name, detail] end unless obj raise XMLRPC::FaultException.new( 1, "Could not create %s[%s]" % [type, name] ) end trans = obj.to_trans # Now get rid of any attributes they specifically don't want ignore.each do |st| if trans.include? st trans.delete(st) end end # And get rid of any attributes that are nil trans.each do |attr, value| if value.nil? trans.delete(attr) end end unless @local case format - when "yaml": + when "yaml" trans = Base64.encode64(YAML::dump(trans)) else raise XMLRPC::FaultException.new( 1, "Unavailable config format %s" % format ) end end return trans end # Create a new fileserving module. def initialize(hash = {}) if hash[:Local] @local = hash[:Local] else @local = false end end # List all of the elements of a given type. def list(type, ignore = [], base = nil, format = "yaml", client = nil, clientip = nil) @local = true unless client typeklass = nil unless typeklass = Puppet::Type.type(type) raise Puppet::Error, "Puppet type %s is unsupported" % type end # They can pass in false ignore ||= [] ignore = [ignore] unless ignore.is_a? Array bucket = Puppet::TransBucket.new bucket.type = typeklass.name typeklass.instances.each do |obj| next if ignore.include? obj.name #object = Puppet::TransObject.new(obj.name, typeklass.name) bucket << obj.to_trans end unless @local case format - when "yaml": + when "yaml" begin bucket = Base64.encode64(YAML::dump(bucket)) rescue => detail Puppet.err detail raise XMLRPC::FaultException.new( 1, detail.to_s ) end else raise XMLRPC::FaultException.new( 1, "Unavailable config format %s" % format ) end end return bucket end private def authcheck(file, mount, client, clientip) unless mount.allowed?(client, clientip) mount.warning "%s cannot access %s" % [client, file] raise Puppet::AuthorizationError, "Cannot access %s" % mount end end # Deal with ignore parameters. def handleignore(children, path, ignore) ignore.each { |ignore| Dir.glob(File.join(path,ignore), File::FNM_DOTMATCH) { |match| children.delete(File.basename(match)) } } return children end def to_s "resource" end end end diff --git a/lib/puppet/network/http.rb b/lib/puppet/network/http.rb index 3b81d38b5..844af909f 100644 --- a/lib/puppet/network/http.rb +++ b/lib/puppet/network/http.rb @@ -1,15 +1,15 @@ module Puppet::Network::HTTP def self.server_class_by_type(kind) case kind.to_sym - when :webrick: + when :webrick require 'puppet/network/http/webrick' return Puppet::Network::HTTP::WEBrick - when :mongrel: + when :mongrel raise ArgumentError, "Mongrel is not installed on this platform" unless Puppet.features.mongrel? require 'puppet/network/http/mongrel' return Puppet::Network::HTTP::Mongrel else raise ArgumentError, "Unknown HTTP server name [#{kind}]" end end end diff --git a/lib/puppet/parser/ast/collexpr.rb b/lib/puppet/parser/ast/collexpr.rb index baed325cb..6ade58b7e 100644 --- a/lib/puppet/parser/ast/collexpr.rb +++ b/lib/puppet/parser/ast/collexpr.rb @@ -1,82 +1,82 @@ require 'puppet' require 'puppet/parser/ast/branch' require 'puppet/parser/collector' # An object that collects stored objects from the central cache and returns # them to the current host, yo. class Puppet::Parser::AST class CollExpr < AST::Branch attr_accessor :test1, :test2, :oper, :form, :type, :parens # We return an object that does a late-binding evaluation. def evaluate(scope) # Make sure our contained expressions have all the info they need. [@test1, @test2].each do |t| if t.is_a?(self.class) t.form ||= self.form t.type ||= self.type end end # The code is only used for virtual lookups str1, code1 = @test1.safeevaluate scope str2, code2 = @test2.safeevaluate scope # First build up the virtual code. # If we're a conjunction operator, then we're calling code. I did # some speed comparisons, and it's at least twice as fast doing these # case statements as doing an eval here. code = proc do |resource| case @oper - when "and": code1.call(resource) and code2.call(resource) - when "or": code1.call(resource) or code2.call(resource) - when "==": + when "and"; code1.call(resource) and code2.call(resource) + when "or"; code1.call(resource) or code2.call(resource) + when "==" if resource[str1].is_a?(Array) && form != :exported resource[str1].include?(str2) else resource[str1] == str2 end - when "!=": resource[str1] != str2 + when "!="; resource[str1] != str2 end end # Now build up the rails conditions code if self.parens and self.form == :exported Puppet.warning "Parentheses are ignored in Rails searches" end case @oper - when "and", "or": + when "and", "or" if form == :exported raise Puppet::ParseError, "Puppet does not currently support collecting exported resources with more than one condition" end oper = @oper.upcase - when "==": oper = "=" + when "=="; oper = "=" else oper = @oper end if oper == "=" or oper == "!=" # Add the rails association info where necessary if str1 == "title" str = "title #{oper} '#{str2}'" else str = "param_values.value #{oper} '#{str2}' and " + "param_names.name = '#{str1}'" end else str = "(%s) %s (%s)" % [str1, oper, str2] end return str, code end def initialize(hash = {}) super unless %w{== != and or}.include?(@oper) raise ArgumentError, "Invalid operator %s" % @oper end end end end diff --git a/lib/puppet/parser/ast/function.rb b/lib/puppet/parser/ast/function.rb index fc3797f15..2f768ebdb 100644 --- a/lib/puppet/parser/ast/function.rb +++ b/lib/puppet/parser/ast/function.rb @@ -1,55 +1,55 @@ require 'puppet/parser/ast/branch' class Puppet::Parser::AST # An AST object to call a function. class Function < AST::Branch associates_doc attr_accessor :name, :arguments @settor = true def evaluate(scope) # Make sure it's a defined function unless @fname raise Puppet::ParseError, "Unknown function %s" % @name end # Now check that it's been used correctly case @ftype - when :rvalue: + when :rvalue unless Puppet::Parser::Functions.rvalue?(@name) raise Puppet::ParseError, "Function '%s' does not return a value" % @name end - when :statement: + when :statement if Puppet::Parser::Functions.rvalue?(@name) raise Puppet::ParseError, "Function '%s' must be the value of a statement" % @name end else raise Puppet::DevError, "Invalid function type %s" % @ftype.inspect end # We don't need to evaluate the name, because it's plaintext args = @arguments.safeevaluate(scope) return scope.send("function_" + @name, args) end def initialize(hash) @ftype = hash[:ftype] || :rvalue hash.delete(:ftype) if hash.include? :ftype super(hash) @fname = Puppet::Parser::Functions.function(@name) # Lastly, check the parity end end end diff --git a/lib/puppet/parser/functions.rb b/lib/puppet/parser/functions.rb index b9e49131c..0434e9b48 100644 --- a/lib/puppet/parser/functions.rb +++ b/lib/puppet/parser/functions.rb @@ -1,134 +1,134 @@ require 'puppet/util/autoload' require 'puppet/parser/scope' module Puppet::Parser module Functions # A module for managing parser functions. Each specified function # becomes an instance method on the Scope class. @functions = {} class << self include Puppet::Util end def self.autoloader unless defined? @autoloader @autoloader = Puppet::Util::Autoload.new(self, "puppet/parser/functions", :wrap => false ) end @autoloader end # Create a new function type. def self.newfunction(name, options = {}, &block) name = symbolize(name) if @functions.include? name raise Puppet::DevError, "Function %s already defined" % name end # We want to use a separate, hidden module, because we don't want # people to be able to call them directly. unless defined? FCollection eval("module FCollection; end") end ftype = options[:type] || :statement unless ftype == :statement or ftype == :rvalue raise Puppet::DevError, "Invalid statement type %s" % ftype.inspect end fname = "function_" + name.to_s Puppet::Parser::Scope.send(:define_method, fname, &block) # Someday we'll support specifying an arity, but for now, nope #@functions[name] = {:arity => arity, :type => ftype} @functions[name] = {:type => ftype, :name => fname} if options[:doc] @functions[name][:doc] = options[:doc] end end # Remove a function added by newfunction def self.rmfunction(name) name = symbolize(name) unless @functions.include? name raise Puppet::DevError, "Function %s is not defined" % name end @functions.delete(name) fname = "function_" + name.to_s Puppet::Parser::Scope.send(:remove_method, fname) end # Determine if a given name is a function def self.function(name) name = symbolize(name) unless @functions.include? name autoloader.load(name) end if @functions.include? name return @functions[name][:name] else return false end end def self.functiondocs autoloader.loadall ret = "" @functions.sort { |a,b| a[0].to_s <=> b[0].to_s }.each do |name, hash| #ret += "%s\n%s\n" % [name, hash[:type]] ret += "%s\n%s\n" % [name, "-" * name.to_s.length] if hash[:doc] ret += Puppet::Util::Docs.scrub(hash[:doc]) else ret += "Undocumented.\n" end ret += "\n\n- **Type**: %s\n\n" % hash[:type] end return ret end def self.functions @functions.keys end # Determine if a given function returns a value or not. def self.rvalue?(name) name = symbolize(name) if @functions.include? name case @functions[name][:type] - when :statement: return false - when :rvalue: return true + when :statement; return false + when :rvalue; return true end else return false end end # Runs a newfunction to create a function for each of the log levels Puppet::Util::Log.levels.each do |level| newfunction(level, :doc => "Log a message on the server at level #{level.to_s}.") do |vals| send(level, vals.join(" ")) end end end end diff --git a/lib/puppet/parser/functions/defined.rb b/lib/puppet/parser/functions/defined.rb index 4e369ae48..b25368ccc 100644 --- a/lib/puppet/parser/functions/defined.rb +++ b/lib/puppet/parser/functions/defined.rb @@ -1,27 +1,27 @@ # Test whether a given class or definition is defined Puppet::Parser::Functions::newfunction(:defined, :type => :rvalue, :doc => "Determine whether a given type is defined, either as a native type or a defined type, or whether a class is defined. This is useful for checking whether a class is defined and only including it if it is. This function can also test whether a resource has been defined, using resource references (e.g., ``if defined(File['/tmp/myfile']) { ... }``). This function is unfortunately dependent on the parse order of the configuration when testing whether a resource is defined.") do |vals| result = false vals.each do |val| case val - when String: + when String # For some reason, it doesn't want me to return from here. if Puppet::Type.type(val) or finddefine(val) or findclass(val) result = true break end - when Puppet::Parser::Resource::Reference: + when Puppet::Parser::Resource::Reference if findresource(val.to_s) result = true break end else raise ArgumentError, "Invalid argument of type %s to 'defined'" % val.class end end result end diff --git a/lib/puppet/parser/resource/reference.rb b/lib/puppet/parser/resource/reference.rb index e552b51fe..a37f8400d 100644 --- a/lib/puppet/parser/resource/reference.rb +++ b/lib/puppet/parser/resource/reference.rb @@ -1,90 +1,90 @@ require 'puppet/resource/reference' # A reference to a resource. Mostly just the type and title. class Puppet::Parser::Resource::Reference < Puppet::Resource::Reference 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 + 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 + 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.rb b/lib/puppet/property.rb index f8a17ac07..3bb1a4f0c 100644 --- a/lib/puppet/property.rb +++ b/lib/puppet/property.rb @@ -1,507 +1,507 @@ # The virtual base class for properties, which are the self-contained building # blocks for actually doing work on the system. require 'puppet' require 'puppet/parameter' class Puppet::Property < Puppet::Parameter # Because 'should' uses an array, we have a special method for handling # it. We also want to keep copies of the original values, so that # they can be retrieved and compared later when merging. attr_reader :shouldorig attr_writer :noop class << self attr_accessor :unmanaged attr_reader :name # Return array matching info, defaulting to just matching # the first value. def array_matching unless defined?(@array_matching) @array_matching = :first end @array_matching end # Set whether properties should match all values or just the first one. def array_matching=(value) value = value.intern if value.is_a?(String) unless [:first, :all].include?(value) raise ArgumentError, "Supported values for Property#array_matching are 'first' and 'all'" end @array_matching = value end def checkable @checkable = true end def uncheckable @checkable = false end def checkable? if defined? @checkable return @checkable else return true end end end # Look up a value's name, so we can find options and such. def self.value_name(name) if value = value_collection.match?(name) value.name end end # Retrieve an option set when a value was defined. def self.value_option(name, option) if value = value_collection.value(name) value.send(option) end end # Define a new valid value for a property. You must provide the value itself, # usually as a symbol, or a regex to match the value. # # The first argument to the method is either the value itself or a regex. # The second argument is an option hash; valid options are: # * :method: The name of the method to define. Defaults to 'set_'. # * :required_features: A list of features this value requires. # * :event: The event that should be returned when this value is set. # * :call: When to call any associated block. The default value # is ``instead``, which means to call the value instead of calling the # provider. You can also specify ``before`` or ``after``, which will # call both the block and the provider, according to the order you specify # (the ``first`` refers to when the block is called, not the provider). def self.newvalue(name, options = {}, &block) value = value_collection.newvalue(name, options, &block) if value.method and value.block define_method(value.method, &value.block) end value end # Call the provider method. def call_provider(value) begin provider.send(self.class.name.to_s + "=", value) rescue NoMethodError self.fail "The %s provider can not handle attribute %s" % [provider.class.name, self.class.name] end end # Call the dynamically-created method associated with our value, if # there is one. def call_valuemethod(name, value) event = nil if method = self.class.value_option(name, :method) and self.respond_to?(method) #self.debug "setting %s (currently %s)" % [value, self.retrieve] begin event = self.send(method) rescue Puppet::Error raise rescue => detail if Puppet[:trace] puts detail.backtrace end error = Puppet::Error.new("Could not set %s on %s: %s" % [value, self.class.name, detail], @resource.line, @resource.file) error.set_backtrace detail.backtrace raise error end elsif block = self.class.value_option(name, :block) # FIXME It'd be better here to define a method, so that # the blocks could return values. event = self.instance_eval(&block) else devfail "Could not find method for value '%s'" % name end return event, name end # How should a property change be printed as a string? def change_to_s(currentvalue, newvalue) begin if currentvalue == :absent return "defined '%s' as '%s'" % [self.name, self.should_to_s(newvalue)] elsif newvalue == :absent or newvalue == [:absent] return "undefined %s from '%s'" % [self.name, self.is_to_s(currentvalue)] else return "%s changed '%s' to '%s'" % [self.name, self.is_to_s(currentvalue), self.should_to_s(newvalue)] 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 # Figure out which event to return. def event(name, event = nil) if value_event = self.class.value_option(name, :event) return value_event end if event and event.is_a?(Symbol) if event == :nochange return nil else return event end end if self.class.name == :ensure event = case self.should - when :present: (@resource.class.name.to_s + "_created").intern - when :absent: (@resource.class.name.to_s + "_removed").intern + when :present; (@resource.class.name.to_s + "_created").intern + when :absent; (@resource.class.name.to_s + "_removed").intern else (@resource.class.name.to_s + "_changed").intern end else event = (@resource.class.name.to_s + "_changed").intern end return event end attr_reader :shadow # initialize our property def initialize(hash = {}) super if ! self.metaparam? and klass = Puppet::Type.metaparamclass(self.class.name) setup_shadow(klass) end end # Determine whether the property is in-sync or not. If @should is # not defined or is set to a non-true value, then we do not have # a valid value for it and thus consider the property to be in-sync # since we cannot fix it. Otherwise, we expect our should value # to be an array, and if @is matches any of those values, then # we consider it to be in-sync. def insync?(is) #debug "%s value is '%s', should be '%s'" % # [self,self.is.inspect,self.should.inspect] unless defined? @should and @should return true end unless @should.is_a?(Array) self.devfail "%s's should is not array" % self.class.name end # an empty array is analogous to no should values if @should.empty? return true end # Look for a matching value if match_all? return (is == @should or is == @should.collect { |v| v.to_s }) else @should.each { |val| if is == val or is == val.to_s return true end } end # otherwise, return false return false end # because the @should and @is vars might be in weird formats, # we need to set up a mechanism for pretty printing of the values # default to just the values, but this way individual properties can # override these methods def is_to_s(currentvalue) currentvalue end # Send a log message. def log(msg) unless @resource[:loglevel] self.devfail "Parent %s has no loglevel" % @resource.name end Puppet::Util::Log.create( :level => @resource[:loglevel], :message => msg, :source => self ) end # Should we match all values, or just the first? def match_all? self.class.array_matching == :all end # Execute our shadow's munge code, too, if we have one. def munge(value) self.shadow.munge(value) if self.shadow super end # each property class must define the name() method, and property instances # do not change that name # this implicitly means that a given object can only have one property # instance of a given property class def name return self.class.name end # for testing whether we should actually do anything def noop # This is only here to make testing easier. if @resource.respond_to?(:noop?) @resource.noop? else if defined?(@noop) @noop else Puppet[:noop] end end end # By default, call the method associated with the property name on our # provider. In other words, if the property name is 'gid', we'll call # 'provider.gid' to retrieve the current value. def retrieve provider.send(self.class.name) end # Set our value, using the provider, an associated block, or both. def set(value) # Set a name for looking up associated options like the event. name = self.class.value_name(value) call = self.class.value_option(name, :call) || :none if call == :instead event, tmp = call_valuemethod(name, value) elsif call == :none if @resource.provider call_provider(value) else # They haven't provided a block, and our parent does not have # a provider, so we have no idea how to handle this. self.fail "%s cannot handle values of type %s" % [self.class.name, value.inspect] end else # LAK:NOTE 20081031 This is a change in behaviour -- you could # previously specify :call => [;before|:after], which would call # the setter *in addition to* the block. I'm convinced this # was never used, and it makes things unecessarily complicated. # If you want to specify a block and still call the setter, then # do so in the block. devfail "Cannot use obsolete :call value '%s' for property '%s'" % [call, self.class.name] end return event(name, event) end # If there's a shadowing metaparam, instantiate it now. # This allows us to create a property or parameter with the # same name as a metaparameter, and the metaparam will only be # stored as a shadow. def setup_shadow(klass) @shadow = klass.new(:resource => self.resource) end # Only return the first value def should if defined? @should unless @should.is_a?(Array) self.devfail "should for %s on %s is not an array" % [self.class.name, @resource.name] end if match_all? return @should else return @should[0] end else return nil end end # Set the should value. def should=(values) unless values.is_a?(Array) values = [values] end @shouldorig = values values.each { |val| validate(val) } @should = values.collect { |val| self.munge(val) } end def should_to_s(newvalue) newvalue = [newvalue] unless newvalue.is_a? Array if defined? newvalue newvalue.join(" ") else return nil end end def sync if value = self.should set(value) else self.devfail "Got a nil value for should" end end # The properties need to return tags so that logs correctly collect them. def tags unless defined? @tags @tags = [] # This might not be true in testing if @resource.respond_to? :tags @tags = @resource.tags end @tags << self.name.to_s end @tags end def to_s return "%s(%s)" % [@resource.name,self.name] end # Verify that the passed value is valid. # If the developer uses a 'validate' hook, this method will get overridden. def unsafe_validate(value) super validate_features_per_value(value) end # Make sure that we've got all of the required features for a given value. def validate_features_per_value(value) if features = self.class.value_option(self.class.value_name(value), :required_features) raise ArgumentError, "Provider must have features '%s' to set '%s' to '%s'" % [features.collect { |f| f.to_s }.join(", "), self.class.name, value] unless provider.satisfies?(features) end end # Just return any should value we might have. def value self.should end # Match the Parameter interface, but we really just use 'should' internally. # Note that the should= method does all of the validation and such. def value=(value) self.should = value end # This property will get automatically added to any type that responds # to the methods 'exists?', 'create', and 'destroy'. class Ensure < Puppet::Property @name = :ensure def self.defaultvalues newvalue(:present) do if @resource.provider and @resource.provider.respond_to?(:create) @resource.provider.create else @resource.create end nil # return nil so the event is autogenerated end newvalue(:absent) do if @resource.provider and @resource.provider.respond_to?(:destroy) @resource.provider.destroy else @resource.destroy end nil # return nil so the event is autogenerated end defaultto do if @resource.managed? :present else nil end end # This doc will probably get overridden @doc ||= "The basic property that the resource should be in." end def self.inherited(sub) # Add in the two properties that everyone will have. sub.class_eval do end end def change_to_s(currentvalue, newvalue) begin if currentvalue == :absent or currentvalue.nil? return "created" elsif newvalue == :absent return "removed" else return "%s changed '%s' to '%s'" % [self.name, self.is_to_s(currentvalue), self.should_to_s(newvalue)] 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 retrieve # XXX This is a problem -- whether the object exists or not often # depends on the results of other properties, yet we're the first property # to get checked, which means that those other properties do not have # @is values set. This seems to be the source of quite a few bugs, # although they're mostly logging bugs, not functional ones. if prov = @resource.provider and prov.respond_to?(:exists?) result = prov.exists? elsif @resource.respond_to?(:exists?) result = @resource.exists? else raise Puppet::DevError, "No ability to determine if %s exists" % @resource.class.name end if result return :present else return :absent end end # If they're talking about the thing at all, they generally want to # say it should exist. #defaultto :present defaultto do if @resource.managed? :present else nil end end end end diff --git a/lib/puppet/provider/augeas/augeas.rb b/lib/puppet/provider/augeas/augeas.rb index 2457840d1..efa8c2a3c 100644 --- a/lib/puppet/provider/augeas/augeas.rb +++ b/lib/puppet/provider/augeas/augeas.rb @@ -1,227 +1,227 @@ #-- # 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 # 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) 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) || '' unless result.nil? case comparator - when "!=": + when "!=" return_value = true if !(result == arg) - when "=~": + 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) || '' # Now do the work if (!result.nil?) case verb - when "size": + 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": + when "include" arg = cmd_array.join(" ") return_value = true if result.include?(arg) - when "==": + 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 # Determines if augeas acutally needs to run. def need_to_run? return_value = true 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" then return_value = process_get(cmd_array) - when "match" then return_value = process_match(cmd_array) + 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 end # Actually execute the augeas changes. def execute_changes aug = open_augeas commands = resource[:changes] context = resource[:context] commands.each do |cmd_array| fail("invalid command #{cmd_array.join[" "]}") if cmd_array.length < 2 command = cmd_array[0] cmd_array.shift() begin case command - when "set": + 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": + 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": + 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]) 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]) case where - when "before": before = true - when "after": before = false + 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()}") 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/cron/crontab.rb b/lib/puppet/provider/cron/crontab.rb index 1cfa0f5fd..d0f139cab 100755 --- a/lib/puppet/provider/cron/crontab.rb +++ b/lib/puppet/provider/cron/crontab.rb @@ -1,204 +1,204 @@ require 'puppet/provider/parsedfile' tab = case Facter.value(:operatingsystem) - when "Solaris": :suntab + when "Solaris"; suntab else :crontab end Puppet::Type.type(:cron).provide(:crontab, :parent => Puppet::Provider::ParsedFile, :default_target => ENV["USER"] || "root", :filetype => tab ) do commands :crontab => "crontab" text_line :comment, :match => %r{^#}, :post_parse => proc { |record| if record[:line] =~ /Puppet Name: (.+)\s*$/ record[:name] = $1 end } text_line :blank, :match => %r{^\s*$} text_line :environment, :match => %r{^\w+=} record_line :freebsd_special, :fields => %w{special command}, :match => %r{^@(\w+)\s+(.+)$}, :pre_gen => proc { |record| record[:special] = "@" + record[:special] } crontab = record_line :crontab, :fields => %w{minute hour monthday month weekday command}, :match => %r{^\s*(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(.+)$}, :optional => %w{minute hour weekday month monthday}, :absent => "*" class << crontab def numeric_fields fields - [:command] end # Do some post-processing of the parsed record. Basically just # split the numeric fields on ','. def post_parse(record) numeric_fields.each do |field| if val = record[field] and val != :absent record[field] = record[field].split(",") end end end # Join the fields back up based on ','. def pre_gen(record) numeric_fields.each do |field| if vals = record[field] and vals.is_a?(Array) record[field] = vals.join(",") end end end # Add name and environments as necessary. def to_line(record) str = "" if record[:name] str = "# Puppet Name: %s\n" % record[:name] end if record[:environment] and record[:environment] != :absent and record[:environment] != [:absent] record[:environment].each do |env| str += env + "\n" end end if record[:special] str += "@%s %s" % [record[:special], record[:command]] else str += join(record) end str end end # Return the header placed at the top of each generated file, warning # users that modifying this file manually is probably a bad idea. def self.header %{# HEADER: This file was autogenerated at #{Time.now} by puppet. # HEADER: While it can still be managed manually, it is definitely not recommended. # HEADER: Note particularly that the comments starting with 'Puppet Name' should # HEADER: not be deleted, as doing so could cause duplicate cron jobs.\n} end # See if we can match the record against an existing cron job. def self.match(record, resources) resources.each do |name, resource| # Match the command first, since it's the most important one. next unless record[:target] == resource.value(:target) next unless record[:command] == resource.value(:command) # Then check the @special stuff if record[:special] next unless resource.value(:special) == record[:special] end # Then the normal fields. matched = true record_type(record[:record_type]).fields().each do |field| next if field == :command next if field == :special if record[field] and ! resource.value(field) #Puppet.info "Cron is missing %s: %s and %s" % # [field, record[field].inspect, resource.value(field).inspect] matched = false break end if ! record[field] and resource.value(field) #Puppet.info "Hash is missing %s: %s and %s" % # [field, resource.value(field).inspect, record[field].inspect] matched = false break end # Yay differing definitions of absent. next if (record[field] == :absent and resource.value(field) == "*") # Everything should be in the form of arrays, not the normal text. next if (record[field] == resource.value(field)) #Puppet.info "Did not match %s: %s vs %s" % # [field, resource.value(field).inspect, record[field].inspect] matched = false break end return resource if matched end return false end # Collapse name and env records. def self.prefetch_hook(records) name = nil envs = nil result = records.each { |record| case record[:record_type] - when :comment: + when :comment if record[:name] name = record[:name] record[:skip] = true # Start collecting env values envs = [] end - when :environment: + when :environment # If we're collecting env values (meaning we're in a named cronjob), # store the line and skip the record. if envs envs << record[:line] record[:skip] = true end - when :blank: + when :blank # nothing else if name record[:name] = name name = nil end if envs.nil? or envs.empty? record[:environment] = :absent else # Collect all of the environment lines, and mark the records to be skipped, # since their data is included in our crontab record. record[:environment] = envs # And turn off env collection again envs = nil end end }.reject { |record| record[:skip] } result end def self.to_file(records) text = super # Apparently Freebsd will "helpfully" add a new TZ line to every # single cron line, but not in all cases (e.g., it doesn't do it # on my machine). This is my attempt to fix it so the TZ lines don't # multiply. if text =~ /(^TZ=.+\n)/ tz = $1 text.sub!(tz, '') text = tz + text end return text end def user=(user) @property_hash[:user] = user @property_hash[:target] = user end def user @property_hash[:user] || @property_hash[:target] end end diff --git a/lib/puppet/provider/host/parsed.rb b/lib/puppet/provider/host/parsed.rb index a4812babe..dfa60cc70 100644 --- a/lib/puppet/provider/host/parsed.rb +++ b/lib/puppet/provider/host/parsed.rb @@ -1,72 +1,72 @@ require 'puppet/provider/parsedfile' hosts = nil case Facter.value(:operatingsystem) -when "Solaris": hosts = "/etc/inet/hosts" +when "Solaris"; hosts = "/etc/inet/hosts" else hosts = "/etc/hosts" end Puppet::Type.type(:host).provide(:parsed, :parent => Puppet::Provider::ParsedFile, :default_target => hosts, :filetype => :flat ) do confine :exists => hosts text_line :comment, :match => /^#/ text_line :blank, :match => /^\s*$/ record_line :parsed, :fields => %w{ip name alias}, :optional => %w{alias}, :rts => true do |line| hash = {} if line.sub!(/^(\S+)\s+(\S+)\s*/, '') hash[:ip] = $1 hash[:name] = $2 unless line == "" line.sub!(/\s*/, '') line.sub!(/^([^#]+)\s*/) do |value| aliases = $1 unless aliases =~ /^\s*$/ hash[:alias] = aliases.split(/\s+/) end "" end end else raise Puppet::Error, "Could not match '%s'" % line end if hash[:alias] == "" hash.delete(:alias) end return hash end # Convert the current object into a host-style string. def self.to_line(hash) return super unless hash[:record_type] == :parsed [:ip, :name].each do |n| unless hash[n] and hash[n] != :absent raise ArgumentError, "%s is a required attribute for hosts" % n end end str = "%s\t%s" % [hash[:ip], hash[:name]] if hash.include? :alias if hash[:alias].is_a? Array str += "\t%s" % hash[:alias].join("\t") else raise ArgumentError, "Aliases must be specified as an array" end end str end end diff --git a/lib/puppet/provider/mount/parsed.rb b/lib/puppet/provider/mount/parsed.rb index 9bc1cf5a5..c660807ce 100755 --- a/lib/puppet/provider/mount/parsed.rb +++ b/lib/puppet/provider/mount/parsed.rb @@ -1,36 +1,36 @@ require 'puppet/provider/parsedfile' require 'puppet/provider/mount' fstab = nil case Facter.value(:operatingsystem) -when "Solaris": fstab = "/etc/vfstab" +when "Solaris"; fstab = "/etc/vfstab" else fstab = "/etc/fstab" end Puppet::Type.type(:mount).provide(:parsed, :parent => Puppet::Provider::ParsedFile, :default_target => fstab, :filetype => :flat ) do include Puppet::Provider::Mount #confine :exists => fstab commands :mountcmd => "mount", :umount => "umount" @platform = Facter["operatingsystem"].value case @platform - when "Solaris": + when "Solaris" @fields = [:device, :blockdevice, :name, :fstype, :pass, :atboot, :options] else @fields = [:device, :name, :fstype, :options, :dump, :pass] @fielddefaults = [ nil ] * 4 + [ "0", "2" ] end text_line :comment, :match => /^\s*#/ text_line :blank, :match => /^\s*$/ record_line self.name, :fields => @fields, :separator => /\s+/, :joiner => "\t", :optional => [:pass, :dump] end diff --git a/lib/puppet/provider/nameservice.rb b/lib/puppet/provider/nameservice.rb index c1e21af08..31572cc47 100644 --- a/lib/puppet/provider/nameservice.rb +++ b/lib/puppet/provider/nameservice.rb @@ -1,315 +1,315 @@ require 'puppet' # This is the parent class of all NSS classes. They're very different in # their backend, but they're pretty similar on the front-end. This class # provides a way for them all to be as similar as possible. class Puppet::Provider::NameService < Puppet::Provider class << self def autogen_default(param) if defined? @autogen_defaults return @autogen_defaults[symbolize(param)] else return nil end end def autogen_defaults(hash) @autogen_defaults ||= {} hash.each do |param, value| @autogen_defaults[symbolize(param)] = value end end def initvars @checks = {} super end def instances objects = [] listbyname do |name| objects << new(:name => name, :ensure => :present) end objects end def option(name, option) name = name.intern if name.is_a? String if defined? @options and @options.include? name and @options[name].include? option return @options[name][option] else return nil end end def options(name, hash) unless resource_type.validattr?(name) raise Puppet::DevError, "%s is not a valid attribute for %s" % [name, resource_type.name] end @options ||= {} @options[name] ||= {} # Set options individually, so we can call the options method # multiple times. hash.each do |param, value| @options[name][param] = value end end # List everything out by name. Abstracted a bit so that it works # for both users and groups. def listbyname names = [] Etc.send("set%sent" % section()) begin while ent = Etc.send("get%sent" % section()) names << ent.name if block_given? yield ent.name end end ensure Etc.send("end%sent" % section()) end return names end def resource_type=(resource_type) super @resource_type.validproperties.each do |prop| next if prop == :ensure unless public_method_defined?(prop) define_method(prop) { get(prop) || :absent} end unless public_method_defined?(prop.to_s + "=") define_method(prop.to_s + "=") { |*vals| set(prop, *vals) } end end end # This is annoying, but there really aren't that many options, # and this *is* built into Ruby. def section unless defined? @resource_type raise Puppet::DevError, "Cannot determine Etc section without a resource type" end if @resource_type.name == :group "gr" else "pw" end end def validate(name, value) name = name.intern if name.is_a? String if @checks.include? name block = @checks[name][:block] unless block.call(value) raise ArgumentError, "Invalid value %s: %s" % [value, @checks[name][:error]] end end end def verify(name, error, &block) name = name.intern if name.is_a? String @checks[name] = {:error => error, :block => block} end private def op(property) @ops[property.name] || ("-" + property.name) end end # Autogenerate a value. Mostly used for uid/gid, but also used heavily # with netinfo, because netinfo is stupid. def autogen(field) field = symbolize(field) id_generators = {:user => :uid, :group => :gid} if id_generators[@resource.class.name] == field return autogen_id(field) else if value = self.class.autogen_default(field) return value elsif respond_to?("autogen_%s" % [field]) return send("autogen_%s" % field) else return nil end end end # Autogenerate either a uid or a gid. This is hard-coded: we can only # generate one field type per class. def autogen_id(field) highest = 0 group = method = nil case @resource.class.name - when :user: group = :passwd; method = :uid - when :group: group = :group; method = :gid + when :user; group = :passwd; method = :uid + when :group; group = :group; method = :gid else raise Puppet::DevError, "Invalid resource name %s" % resource end # Make sure we don't use the same value multiple times if defined? @@prevauto @@prevauto += 1 else Etc.send(group) { |obj| if obj.gid > highest unless obj.send(method) > 65000 highest = obj.send(method) end end } @@prevauto = highest + 1 end return @@prevauto end def create if exists? info "already exists" # The object already exists return nil end begin execute(self.addcmd) rescue Puppet::ExecutionFailure => detail raise Puppet::Error, "Could not create %s %s: %s" % [@resource.class.name, @resource.name, detail] end end def delete unless exists? info "already absent" # the object already doesn't exist return nil end begin execute(self.deletecmd) rescue Puppet::ExecutionFailure => detail raise Puppet::Error, "Could not delete %s %s: %s" % [@resource.class.name, @resource.name, detail] end end def ensure if exists? :present else :absent end end # Does our object exist? def exists? if getinfo(true) return true else return false end end # Retrieve a specific value by name. def get(param) if hash = getinfo(false) return hash[param] else return nil end end # Retrieve what we can about our object def getinfo(refresh) if @objectinfo.nil? or refresh == true @etcmethod ||= ("get" + self.class.section().to_s + "nam").intern begin @objectinfo = Etc.send(@etcmethod, @resource[:name]) rescue ArgumentError => detail @objectinfo = nil end end # Now convert our Etc struct into a hash. if @objectinfo return info2hash(@objectinfo) else return nil end end # The list of all groups the user is a member of. Different # user mgmt systems will need to override this method. def groups groups = [] # Reset our group list Etc.setgrent user = @resource[:name] # Now iterate across all of the groups, adding each one our # user is a member of while group = Etc.getgrent members = group.mem if members.include? user groups << group.name end end # We have to close the file, so each listing is a separate # reading of the file. Etc.endgrent groups.join(",") end # Convert the Etc struct into a hash. def info2hash(info) hash = {} self.class.resource_type.validproperties.each do |param| method = posixmethod(param) if info.respond_to? method hash[param] = info.send(posixmethod(param)) end end return hash end def initialize(resource) super @objectinfo = nil end def set(param, value) self.class.validate(param, value) cmd = modifycmd(param, value) unless cmd.is_a?(Array) raise Puppet::DevError, "Nameservice command must be an array" end begin execute(cmd) 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 diff --git a/lib/puppet/provider/nameservice/directoryservice.rb b/lib/puppet/provider/nameservice/directoryservice.rb index 71e6ff33e..07b01bbe0 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: + when :members ds_value = ds_value # only members uses arrays so far - when :gid, :uid: + 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 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 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/nameservice/netinfo.rb b/lib/puppet/provider/nameservice/netinfo.rb index 70491da57..8de141068 100644 --- a/lib/puppet/provider/nameservice/netinfo.rb +++ b/lib/puppet/provider/nameservice/netinfo.rb @@ -1,224 +1,224 @@ # Manage NetInfo POSIX objects. # # This provider has been deprecated. You should be using the directoryservice # nameservice provider instead. require 'puppet' require 'puppet/provider/nameservice' class Puppet::Provider::NameService class NetInfo < Puppet::Provider::NameService class << self attr_writer :netinfodir end # We have to initialize manually because we're not using # classgen() here. initvars() commands :lookupd => "/usr/sbin/lookupd" # Attempt to flush the database, but this doesn't seem to work at all. def self.flush begin lookupd "-flushcache" rescue Puppet::ExecutionFailure # Don't throw an error; it's just a failed cache flush Puppet.err "Could not flush lookupd cache: %s" % output end end # Similar to posixmethod, what key do we use to get data? Defaults # to being the object name. def self.netinfodir if defined? @netinfodir return @netinfodir else return @resource_type.name.to_s + "s" end end def self.finish case self.name - when :uid: + when :uid noautogen - when :gid: + when :gid noautogen end end def self.instances warnonce "The NetInfo provider is deprecated; use directoryservice instead" report(@resource_type.validproperties).collect do |hash| self.new(hash) end end # Convert a NetInfo line into a hash of data. def self.line2hash(line, params) values = line.split(/\t/) hash = {} params.zip(values).each do |param, value| next if value == '#NoValue#' hash[param] = if value =~ /^[-0-9]+$/ Integer(value) else value end end hash end # What field the value is stored under. def self.netinfokey(name) name = symbolize(name) self.option(name, :key) || name end # Retrieve the data, yo. # FIXME This should retrieve as much information as possible, # rather than retrieving it one at a time. def self.report(*params) dir = self.netinfodir() cmd = [command(:nireport), "/", "/%s" % dir] params.flatten! # We require the name in order to know if we match. There's no # way to just report on our individual object, we have to get the # whole list. params.unshift :name unless params.include? :name params.each do |param| if key = netinfokey(param) cmd << key.to_s else raise Puppet::DevError, "Could not find netinfokey for property %s" % self.class.name end end begin output = execute(cmd) rescue Puppet::ExecutionFailure => detail Puppet.err "Failed to call nireport: %s" % detail return nil end return output.split("\n").collect { |line| line2hash(line, params) } end # How to add an object. def addcmd creatorcmd("-create") end def creatorcmd(arg) cmd = [command(:niutil)] cmd << arg cmd << "/" << "/%s/%s" % [self.class.netinfodir(), @resource[:name]] return cmd end def deletecmd creatorcmd("-destroy") end def destroy delete() end def ensure=(arg) warnonce "The NetInfo provider is deprecated; use directoryservice instead" super # Because our stupid type can't create the whole thing at once, # we have to do this hackishness. Yay. if arg == :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 # Retrieve a specific value by name. def get(param) hash = getinfo(false) if hash return hash[param] else return :absent end end # Retrieve everything about this object at once, instead of separately. def getinfo(refresh = false) if refresh or (! defined? @infohash or ! @infohash) properties = [:name] + self.class.resource_type.validproperties properties.delete(:ensure) if properties.include? :ensure @infohash = single_report(*properties) end return @infohash end def modifycmd(param, value) cmd = [command(:niutil)] # if value.is_a?(Array) # warning "Netinfo providers cannot currently handle multiple values" # end cmd << "-createprop" << "/" << "/%s/%s" % [self.class.netinfodir, @resource[:name]] value = [value] unless value.is_a?(Array) if key = netinfokey(param) cmd << key cmd += value else raise Puppet::DevError, "Could not find netinfokey for property %s" % self.class.name end cmd end # Determine the flag to pass to our command. def netinfokey(name) self.class.netinfokey(name) end # Get a report for a single resource, not the whole table def single_report(*properties) warnonce "The NetInfo provider is deprecated; use directoryservice instead" self.class.report(*properties).find do |hash| hash[:name] == self.name end end def setuserlist(group, list) cmd = [command(:niutil), "-createprop", "/", "/groups/%s" % group, "users", list.join(",")] begin output = execute(cmd) rescue Puppet::ExecutionFailure => detail raise Puppet::Error, "Failed to set user list on %s: %s" % [group, detail] end end end end diff --git a/lib/puppet/provider/package/gem.rb b/lib/puppet/provider/package/gem.rb index 133243c86..2f1aebcb3 100755 --- a/lib/puppet/provider/package/gem.rb +++ b/lib/puppet/provider/package/gem.rb @@ -1,128 +1,128 @@ require 'puppet/provider/package' require 'uri' # Ruby gems support. Puppet::Type.type(:package).provide :gem, :parent => Puppet::Provider::Package do desc "Ruby Gem support. If a URL is passed via ``source``, then that URL is used as the remote gem repository; if a source is present but is not a valid URL, it will be interpreted as the path to a local gem file. If source is not present at all, the gem will be installed from the default gem repositories." has_feature :versionable commands :gemcmd => "gem" def self.gemlist(hash) command = [command(:gemcmd), "list"] if hash[:local] command << "--local" else command << "--remote" end if name = hash[:justme] command << name end begin list = execute(command).split("\n").collect do |set| if gemhash = gemsplit(set) gemhash[:provider] = :gem gemhash else nil end end.compact rescue Puppet::ExecutionFailure => detail raise Puppet::Error, "Could not list gems: %s" % detail end if hash[:justme] return list.shift else return list end end def self.gemsplit(desc) case desc when /^\*\*\*/, /^\s*$/, /^\s+/; return nil when /^(\S+)\s+\((.+)\)/ name = $1 version = $2.split(/,\s*/)[0] return { :name => name, :ensure => version } else Puppet.warning "Could not match %s" % desc nil end end def self.instances(justme = false) gemlist(:local => true).collect do |hash| new(hash) end end def install(useversion = true) command = [command(:gemcmd), "install"] if (! resource[:ensure].is_a? Symbol) and useversion command << "-v" << resource[:ensure] end # Always include dependencies command << "--include-dependencies" if source = resource[:source] begin uri = URI.parse(source) rescue => detail fail "Invalid source '%s': %s" % [uri, detail] end case uri.scheme - when nil: + when nil # no URI scheme => interpret the source as a local file command << source when /file/i command << uri.path when 'puppet' # we don't support puppet:// URLs (yet) raise Puppet::Error.new("puppet:// URLs are not supported as gem sources") else # interpret it as a gem repository command << "--source" << "#{source}" << resource[:name] end else command << resource[:name] end output = execute(command) # Apparently some stupid gem versions don't exit non-0 on failure if output.include?("ERROR") self.fail "Could not install: %s" % output.chomp end end def latest # This always gets the latest version available. hash = self.class.gemlist(:justme => resource[:name]) return hash[:ensure] end def query self.class.gemlist(:justme => resource[:name], :local => true) end def uninstall gemcmd "uninstall", "-x", "-a", resource[:name] end def update self.install(false) end end diff --git a/lib/puppet/provider/package/sun.rb b/lib/puppet/provider/package/sun.rb index 0d366388a..d916eb78c 100755 --- a/lib/puppet/provider/package/sun.rb +++ b/lib/puppet/provider/package/sun.rb @@ -1,169 +1,169 @@ # Sun packaging. require 'puppet/provider/package' Puppet::Type.type(:package).provide :sun, :parent => Puppet::Provider::Package do desc "Sun's packaging system. Requires that you specify the source for the packages you're managing." commands :pkginfo => "/usr/bin/pkginfo", :pkgadd => "/usr/sbin/pkgadd", :pkgrm => "/usr/sbin/pkgrm" confine :operatingsystem => :solaris defaultfor :operatingsystem => :solaris def self.instances packages = [] hash = {} names = { "PKGINST" => :name, "NAME" => nil, "CATEGORY" => :category, "ARCH" => :platform, "VERSION" => :ensure, "BASEDIR" => :root, "HOTLINE" => nil, "EMAIL" => nil, "VENDOR" => :vendor, "DESC" => :description, "PSTAMP" => nil, "INSTDATE" => nil, "STATUS" => nil, "FILES" => nil } cmd = "#{command(:pkginfo)} -l" # list out all of the packages execpipe(cmd) { |process| # we're using the long listing, so each line is a separate # piece of information process.each { |line| case line - when /^$/: + when /^$/ hash[:provider] = :sun packages << new(hash) hash = {} - when /\s*(\w+):\s+(.+)/: + when /\s*(\w+):\s+(.+)/ name = $1 value = $2 if names.include?(name) unless names[name].nil? hash[names[name]] = value end end - when /\s+\d+.+/: + when /\s+\d+.+/ # nothing; we're ignoring the FILES info end } } return packages end # Get info on a package, optionally specifying a device. def info2hash(device = nil) names = { "PKGINST" => :name, "NAME" => nil, "CATEGORY" => :category, "ARCH" => :platform, "VERSION" => :ensure, "BASEDIR" => :root, "HOTLINE" => nil, "EMAIL" => nil, "VSTOCK" => nil, "VENDOR" => :vendor, "DESC" => :description, "PSTAMP" => nil, "INSTDATE" => nil, "STATUS" => nil, "FILES" => nil } hash = {} cmd = "#{command(:pkginfo)} -l" if device cmd += " -d #{device}" end cmd += " #{@resource[:name]}" begin # list out all of the packages execpipe(cmd) { |process| # we're using the long listing, so each line is a separate # piece of information process.each { |line| case line - when /^$/: # ignore - when /\s*([A-Z]+):\s+(.+)/: + when /^$/ # ignore + when /\s*([A-Z]+):\s+(.+)/ name = $1 value = $2 if names.include?(name) unless names[name].nil? hash[names[name]] = value end end - when /\s+\d+.+/: + when /\s+\d+.+/ # nothing; we're ignoring the FILES info end } } return hash rescue Puppet::ExecutionFailure return nil end end def install unless @resource[:source] raise Puppet::Error, "Sun packages must specify a package source" end cmd = [] if @resource[:adminfile] cmd << "-a" << @resource[:adminfile] end if @resource[:responsefile] cmd << "-r" << @resource[:responsefile] end cmd << "-d" << @resource[:source] cmd << "-n" << @resource[:name] pkgadd cmd end # Retrieve the version from the current package file. def latest hash = info2hash(@resource[:source]) hash[:ensure] end def query info2hash() end def uninstall command = ["-n"] if @resource[:adminfile] command << "-a" << @resource[:adminfile] end command << @resource[:name] pkgrm command end # Remove the old package, and install the new one. This will probably # often fail. def update if (@property_hash[:ensure] || info2hash()[:ensure]) != :absent self.uninstall end self.install end end diff --git a/lib/puppet/provider/port/parsed.rb b/lib/puppet/provider/port/parsed.rb index 68be0aa7c..624c267b9 100755 --- a/lib/puppet/provider/port/parsed.rb +++ b/lib/puppet/provider/port/parsed.rb @@ -1,173 +1,173 @@ require 'puppet/provider/parsedfile' #services = nil #case Facter.value(:operatingsystem) -#when "Solaris": services = "/etc/inet/services" +#when "Solaris"; services = "/etc/inet/services" #else # services = "/etc/services" #end # #Puppet::Type.type(:port).provide(:parsed, # :parent => Puppet::Provider::ParsedFile, # :default_target => services, # :filetype => :flat #) do # text_line :comment, :match => /^\s*#/ # text_line :blank, :match => /^\s*$/ # # # We're cheating horribly here -- we don't support ddp, because it assigns # # the same number to already-used names, and the same name to different # # numbers. # text_line :ddp, :match => /^\S+\s+\d+\/ddp/ # # # Also, just ignore the lines on OS X that don't have service names. # text_line :funky_darwin, :match => /^\s+\d+\// # # # We have to manually parse the line, since it's so darn complicated. # record_line :parsed, :fields => %w{name port protocols alias description}, # :optional => %w{alias description} do |line| # if line =~ /\/ddp/ # raise "missed ddp in %s" % line # end # # The record might contain multiple port lines separated by \n. # hashes = line.split("\n").collect { |l| parse_port(l) } # # # It's easy if there's just one hash. # if hashes.length == 1 # return hashes.shift # end # # # Else, merge the two records into one. # return port_merge(*hashes) # end # # # Override how we split into lines, so that we always treat both protocol # # lines as a single line. This drastically simplifies merging the two lines # # into one record. # def self.lines(text) # names = {} # lines = [] # # # We organize by number, because that's apparently how the ports work. # # You'll never be able to use Puppet to manage multiple entries # # with the same name but different numbers, though. # text.split("\n").each do |line| # if line =~ /^([-\w]+)\s+(\d+)\/[^d]/ # We want to skip ddp proto stuff # names[$1] ||= [] # names[$1] << line # lines << [:special, $1] # else # lines << line # end # end # # # Now, return each line in order, but join the ones with the same name # lines.collect do |line| # if line.is_a?(Array) # name = line[1] # if names[name] # t = names[name].join("\n") # names.delete(name) # t # end # else # line # end # end.reject { |l| l.nil? } # end # # # Parse a single port line, returning a hash. # def self.parse_port(line) # hash = {} # if line.sub!(/^(\S+)\s+(\d+)\/(\w+)\s*/, '') # hash[:name] = $1 # hash[:number] = $2 # hash[:protocols] = [$3] # # unless line == "" # line.sub!(/^([^#]+)\s*/) do |value| # aliases = $1 # # # Remove any trailing whitespace # aliases.strip! # unless aliases =~ /^\s*$/ # hash[:alias] = aliases.split(/\s+/) # end # # "" # end # # line.sub!(/^\s*#\s*(.+)$/) do |value| # desc = $1 # unless desc =~ /^\s*$/ # hash[:description] = desc.sub(/\s*$/, '') # end # # "" # end # end # else # if line =~ /^\s+\d+/ and # Facter["operatingsystem"].value == "Darwin" # #Puppet.notice "Skipping wonky OS X port entry %s" % # # line.inspect # next # end # Puppet.notice "Ignoring unparseable line '%s' in %s" % [line, self.target] # end # # if hash.empty? # return nil # else # return hash # end # end # # # Merge two records into one. # def self.port_merge(one, two) # keys = [one.keys, two.keys].flatten.uniq # # # We'll be returning the 'one' hash. so make any necessary modifications # # to it. # keys.each do |key| # # The easy case # if one[key] == two[key] # next # elsif one[key] and ! two[key] # next # elsif ! one[key] and two[key] # one[key] = two[key] # elsif one[key].is_a?(Array) and two[key].is_a?(Array) # one[key] = [one[key], two[key]].flatten.uniq # else # # Keep the info from the first hash, so don't do anything # #Puppet.notice "Cannot merge %s in %s with %s" % # # [key, one.inspect, two.inspect] # end # end # # return one # end # # # Convert the current object into one or more services entry. # def self.to_line(hash) # unless hash[:record_type] == :parsed # return super # end # # # Strangely, most sites seem to use tabs as separators. # hash[:protocols].collect { |proto| # str = "%s\t\t%s/%s" % [hash[:name], hash[:number], proto] # # if value = hash[:alias] and value != :absent # str += "\t\t%s" % value.join(" ") # end # # if value = hash[:description] and value != :absent # str += "\t# %s" % value # end # str # }.join("\n") # end #end diff --git a/lib/puppet/provider/service/init.rb b/lib/puppet/provider/service/init.rb index cbc7afdc4..f70b12c42 100755 --- a/lib/puppet/provider/service/init.rb +++ b/lib/puppet/provider/service/init.rb @@ -1,137 +1,137 @@ # The standard init-based service type. Many other service types are # customizations of this module. Puppet::Type.type(:service).provide :init, :parent => :base do desc "Standard init service management. This provider assumes that the init script has no ``status`` command, because so few scripts do, so you need to either provide a status command or specify via ``hasstatus`` that one already exists in the init script. " class << self attr_accessor :defpath end case Facter["operatingsystem"].value - when "FreeBSD": + when "FreeBSD" @defpath = ["/etc/rc.d", "/usr/local/etc/rc.d"] - when "HP-UX": + when "HP-UX" @defpath = "/sbin/init.d" else @defpath = "/etc/init.d" end # We can't confine this here, because the init path can be overridden. #confine :exists => @defpath # List all services of this type. def self.instances self.defpath = [self.defpath] unless self.defpath.is_a? Array instances = [] self.defpath.each do |path| unless FileTest.directory?(path) Puppet.debug "Service path %s does not exist" % path next end check = [:ensure] if public_method_defined? :enabled? check << :enable end Dir.entries(path).each do |name| fullpath = File.join(path, name) next if name =~ /^\./ next if not FileTest.executable?(fullpath) instances << new(:name => name, :path => path) end end instances end # Mark that our init script supports 'status' commands. def hasstatus=(value) case value - when true, "true": @parameters[:hasstatus] = true - when false, "false": @parameters[:hasstatus] = false + when true, "true"; @parameters[:hasstatus] = true + when false, "false"; @parameters[:hasstatus] = false else raise Puppet::Error, "Invalid 'hasstatus' value %s" % value.inspect end end # Where is our init script? def initscript if defined? @initscript return @initscript else @initscript = self.search(@resource[:name]) end end def restart if @resource[:hasrestart] == :true command = [self.initscript, :restart] texecute("restart", command) else super end end def search(name) @resource[:path].each { |path| fqname = File.join(path,name) begin stat = File.stat(fqname) rescue # should probably rescue specific errors... self.debug("Could not find %s in %s" % [name,path]) next end # if we've gotten this far, we found a valid script return fqname } @resource[:path].each { |path| fqname_sh = File.join(path,"#{name}.sh") begin stat = File.stat(fqname_sh) rescue # should probably rescue specific errors... self.debug("Could not find %s.sh in %s" % [name,path]) next end # if we've gotten this far, we found a valid script return fqname_sh } raise Puppet::Error, "Could not find init script for '%s'" % name end # The start command is just the init scriptwith 'start'. def startcmd [self.initscript, :start] end # If it was specified that the init script has a 'status' command, then # we just return that; otherwise, we return false, which causes it to # fallback to other mechanisms. def statuscmd if @resource[:hasstatus] == :true return [self.initscript, :status] else return false end end # The stop command is just the init script with 'stop'. def stopcmd [self.initscript, :stop] end end diff --git a/lib/puppet/provider/service/smf.rb b/lib/puppet/provider/service/smf.rb index 4010e7457..965c9a17c 100755 --- a/lib/puppet/provider/service/smf.rb +++ b/lib/puppet/provider/service/smf.rb @@ -1,89 +1,89 @@ # Solaris 10 SMF-style services. Puppet::Type.type(:service).provide :smf, :parent => :base do desc "Support for Sun's new Service Management Framework. Starting a service is effectively equivalent to enabling it, so there is only support for starting and stopping services, which also enables and disables them, respectively. " defaultfor :operatingsystem => :solaris confine :operatingsystem => :solaris commands :adm => "/usr/sbin/svcadm", :svcs => "/usr/bin/svcs" def enable self.start end def enabled? case self.status - when :running: + when :running return :true else return :false end end def disable self.stop end def restartcmd [command(:adm), :restart, @resource[:name]] end def startcmd [command(:adm), :enable, @resource[:name]] end def status if @resource[:status] super return end begin output = svcs "-l", @resource[:name] rescue Puppet::ExecutionFailure warning "Could not get status on service %s" % self.name return :stopped end output.split("\n").each { |line| var = nil value = nil if line =~ /^(\w+)\s+(.+)/ var = $1 value = $2 else Puppet.err "Could not match %s" % line.inspect next end case var - when "state": + when "state" case value - when "online": + when "online" #self.warning "matched running %s" % line.inspect return :running when "offline", "disabled", "uninitialized" #self.warning "matched stopped %s" % line.inspect return :stopped - when "legacy_run": + when "legacy_run" raise Puppet::Error, "Cannot manage legacy services through SMF" else raise Puppet::Error, "Unmanageable state '%s' on service %s" % [value, self.name] end end } end def stopcmd [command(:adm), :disable, @resource[:name]] end end diff --git a/lib/puppet/provider/sshkey/parsed.rb b/lib/puppet/provider/sshkey/parsed.rb index 6f7d98f56..57cfb5b67 100755 --- a/lib/puppet/provider/sshkey/parsed.rb +++ b/lib/puppet/provider/sshkey/parsed.rb @@ -1,37 +1,37 @@ require 'puppet/provider/parsedfile' known = nil case Facter.value(:operatingsystem) -when "Darwin": known = "/etc/ssh_known_hosts" +when "Darwin"; known = "/etc/ssh_known_hosts" else known = "/etc/ssh/ssh_known_hosts" end Puppet::Type.type(:sshkey).provide(:parsed, :parent => Puppet::Provider::ParsedFile, :default_target => known, :filetype => :flat ) do desc "Parse and generate host-wide known hosts files for SSH." text_line :comment, :match => /^#/ text_line :blank, :match => /^\s+/ record_line :parsed, :fields => %w{name type key}, :post_parse => proc { |hash| if hash[:name] =~ /,/ names = hash[:name].split(",") hash[:name] = names.shift hash[:alias] = names end }, :pre_gen => proc { |hash| if hash[:alias] names = [hash[:name], hash[:alias]].flatten hash[:name] = [hash[:name], hash[:alias]].flatten.join(",") hash.delete(:alias) end } end diff --git a/lib/puppet/provider/user/directoryservice.rb b/lib/puppet/provider/user/directoryservice.rb index 4d6bf7d29..706840822 100644 --- a/lib/puppet/provider/user/directoryservice.rb +++ b/lib/puppet/provider/user/directoryservice.rb @@ -1,117 +1,117 @@ # 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/provider/nameservice/directoryservice' Puppet::Type.type(:user).provide :directoryservice, :parent => Puppet::Provider::NameService::DirectoryService do desc "User management using DirectoryService on OS X." commands :dscl => "/usr/bin/dscl" confine :operatingsystem => :darwin defaultfor :operatingsystem => :darwin # JJM: DirectoryService can manage passwords. # This needs to be a special option to dscl though (-passwd) has_feature :manages_passwords # JJM: comment matches up with the /etc/passwd concept of an user options :comment, :key => "realname" options :password, :key => "passwd" autogen_defaults :home => "/var/empty", :shell => "/usr/bin/false" verify :gid, "GID must be an integer" do |value| value.is_a? Integer end verify :uid, "UID must be an integer" do |value| value.is_a? Integer end def autogen_comment return @resource[:name].capitalize end # The list of all groups the user is a member of. Different # user mgmt systems will need to override this method. # JJM: FIXME: Override this method... def groups groups = [] # user = @resource[:name] # # Retrieve them all from netinfo # open("| #{command(:nireport)} / /groups name users") do |file| # file.each do |line| # name, members = line.split(/\s+/) # next unless members # next if members =~ /NoValue/ # members = members.split(",") # # if members.include? user # groups << name # end # end # end groups.join(",") end # This is really lame. We have to iterate over each # of the groups and add us to them. def groups=(groups) # case groups - # when Fixnum: + # when Fixnum # groups = [groups.to_s] # when String # groups = groups.split(/\s*,\s*/) # else # raise Puppet::DevError, "got invalid groups value %s of type %s" % [groups.class, groups] # end # # Get just the groups we need to modify # diff = groups - (@is || []) # # data = {} # open("| #{command(:nireport)} / /groups name users") do |file| # file.each do |line| # name, members = line.split(/\s+/) # # if members.nil? or members =~ /NoValue/ # data[name] = [] # else # # Add each diff group's current members # data[name] = members.split(/,/) # end # end # end # # user = @resource[:name] # data.each do |name, members| # if members.include? user and groups.include? name # # I'm in the group and should be # next # elsif members.include? user # # I'm in the group and shouldn't be # setuserlist(name, members - [user]) # elsif groups.include? name # # I'm not in the group and should be # setuserlist(name, members + [user]) # else # # I'm not in the group and shouldn't be # next # end # end end end diff --git a/lib/puppet/provider/user/netinfo.rb b/lib/puppet/provider/user/netinfo.rb index 067017258..22fb95056 100644 --- a/lib/puppet/provider/user/netinfo.rb +++ b/lib/puppet/provider/user/netinfo.rb @@ -1,111 +1,111 @@ # Manage NetInfo POSIX objects. # # This provider has been deprecated. You should be using the directoryservice # nameservice provider instead. require 'puppet/provider/nameservice/netinfo' Puppet::Type.type(:user).provide :netinfo, :parent => Puppet::Provider::NameService::NetInfo do desc "User management in NetInfo. Note that NetInfo is not smart enough to fill in default information for users, so this provider will use default settings for home (``/var/empty``), shell (``/usr/bin/false``), comment (the user name, capitalized), and password ('********'). These defaults are only used when the user is created. Note that password management probably does not really work -- OS X does not store the password in NetInfo itself, yet we cannot figure out how to store the encrypted password where OS X will look for it. The main reason the password support is even there is so that a default password is created, which effectively locks people out, even if it does not enable us to set a password." commands :nireport => "nireport", :niutil => "niutil" options :comment, :key => "realname" options :password, :key => "passwd" autogen_defaults :home => "/var/empty", :shell => "/usr/bin/false", :password => '********' has_feature :manages_passwords verify :gid, "GID must be an integer" do |value| value.is_a? Integer end verify :uid, "UID must be an integer" do |value| value.is_a? Integer end def autogen_comment return @resource[:name].capitalize end # The list of all groups the user is a member of. Different # user mgmt systems will need to override this method. def groups warnonce "The NetInfo provider is deprecated; use directoryservice instead" groups = [] user = @resource[:name] # Retrieve them all from netinfo open("| #{command(:nireport)} / /groups name users") do |file| file.each do |line| name, members = line.split(/\s+/) next unless members next if members =~ /NoValue/ members = members.split(",") if members.include? user groups << name end end end groups.join(",") end # This is really lame. We have to iterate over each # of the groups and add us to them. def groups=(groups) warnonce "The NetInfo provider is deprecated; use directoryservice instead" case groups - when Fixnum: + when Fixnum groups = [groups.to_s] when String groups = groups.split(/\s*,\s*/) else raise Puppet::DevError, "got invalid groups value %s of type %s" % [groups.class, groups] end # Get just the groups we need to modify diff = groups - (@is || []) data = {} open("| #{command(:nireport)} / /groups name users") do |file| file.each do |line| name, members = line.split(/\s+/) if members.nil? or members =~ /NoValue/ data[name] = [] else # Add each diff group's current members data[name] = members.split(/,/) end end end user = @resource[:name] data.each do |name, members| if members.include? user and groups.include? name # I'm in the group and should be next elsif members.include? user # I'm in the group and shouldn't be setuserlist(name, members - [user]) elsif groups.include? name # I'm not in the group and should be setuserlist(name, members + [user]) else # I'm not in the group and shouldn't be next end end end end diff --git a/lib/puppet/provider/zone/solaris.rb b/lib/puppet/provider/zone/solaris.rb index a5a18198c..9d6e24338 100644 --- a/lib/puppet/provider/zone/solaris.rb +++ b/lib/puppet/provider/zone/solaris.rb @@ -1,247 +1,247 @@ Puppet::Type.type(:zone).provide(:solaris) do desc "Provider for Solaris Zones." commands :adm => "/usr/sbin/zoneadm", :cfg => "/usr/sbin/zonecfg" defaultfor :operatingsystem => :solaris mk_resource_methods # Convert the output of a list into a hash def self.line2hash(line) fields = [:id, :name, :ensure, :path] properties = {} line.split(":").each_with_index { |value, index| next unless fields[index] properties[fields[index]] = value } # Configured but not installed zones do not have IDs if properties[:id] == "-" properties.delete(:id) end properties[:ensure] = symbolize(properties[:ensure]) return properties end def self.instances # LAK:NOTE See http://snurl.com/21zf8 [groups_google_com] x = adm(:list, "-cp").split("\n").collect do |line| new(line2hash(line)) end end # Perform all of our configuration steps. def configure # If the thing is entirely absent, then we need to create the config. # Is there someway to get this on one line? str = "create -b #{@resource[:create_args]}\nset zonepath=%s\n" % @resource[:path] # Then perform all of our configuration steps. It's annoying # that we need this much internal info on the resource. @resource.send(:properties).each do |property| if property.is_a? ZoneConfigProperty and ! property.insync?(properties[property.name]) str += property.configtext + "\n" end end str += "commit\n" setconfig(str) end def destroy zonecfg :delete, "-F" end def exists? properties[:ensure] != :absent end # Clear out the cached values. def flush @property_hash.clear end def install if @resource[:install_args] zoneadm :install, @resource[:install_args].split(" ") else zoneadm :install end end # Look up the current status. def properties if @property_hash.empty? @property_hash = status || {} if @property_hash.empty? @property_hash[:ensure] = :absent else @resource.class.validproperties.each do |name| @property_hash[name] ||= :absent end end end @property_hash.dup end # We need a way to test whether a zone is in process. Our 'ensure' # property models the static states, but we need to handle the temporary ones. def processing? if hash = status() case hash[:ensure] when "incomplete", "ready", "shutting_down" true else false end else false end end # Collect the configuration of the zone. def getconfig output = zonecfg :info name = nil current = nil hash = {} output.split("\n").each do |line| case line - when /^(\S+):\s*$/: + when /^(\S+):\s*$/ name = $1 current = nil # reset it - when /^(\S+):\s*(.+)$/: + when /^(\S+):\s*(.+)$/ hash[$1.intern] = $2 - when /^\s+(\S+):\s*(.+)$/: + when /^\s+(\S+):\s*(.+)$/ if name unless hash.include? name hash[name] = [] end unless current current = {} hash[name] << current end current[$1.intern] = $2 else err "Ignoring '%s'" % line end else debug "Ignoring zone output '%s'" % line end end return hash end # Execute a configuration string. Can't be private because it's called # by the properties. def setconfig(str) command = "#{command(:cfg)} -z %s -f -" % @resource[:name] debug "Executing '%s' in zone %s with '%s'" % [command, @resource[:name], str] IO.popen(command, "w") do |pipe| pipe.puts str end unless $? == 0 raise ArgumentError, "Failed to apply configuration" end end def start # Check the sysidcfg stuff if cfg = @resource[:sysidcfg] path = File.join(@resource[:path], "root", "etc", "sysidcfg") unless File.exists?(path) begin File.open(path, "w", 0600) do |f| f.puts cfg end rescue => detail if Puppet[:debug] puts detail.stacktrace end raise Puppet::Error, "Could not create sysidcfg: %s" % detail end end end zoneadm :boot end # Return a hash of the current status of this zone. def status begin output = adm "-z", @resource[:name], :list, "-p" rescue Puppet::ExecutionFailure return nil end main = self.class.line2hash(output.chomp) # Now add in the configuration information config_status.each do |name, value| main[name] = value end main end def stop zoneadm :halt end def unconfigure zonecfg :delete, "-F" end def uninstall zoneadm :uninstall, "-F" end private # Turn the results of getconfig into status information. def config_status config = getconfig() result = {} result[:autoboot] = config[:autoboot] ? config[:autoboot].intern : :absent result[:pool] = config[:pool] result[:shares] = config[:shares] if dir = config["inherit-pkg-dir"] result[:inherit] = dir.collect { |dirs| dirs[:dir] } end if net = config["net"] result[:ip] = net.collect { |params| "%s:%s" % [params[:physical], params[:address]] } end result end def zoneadm(*cmd) begin adm("-z", @resource[:name], *cmd) rescue Puppet::ExecutionFailure => detail self.fail "Could not %s zone: %s" % [cmd[0], detail] end end def zonecfg(*cmd) # You apparently can't get the configuration of the global zone return "" if self.name == "global" begin cfg("-z", self.name, *cmd) rescue Puppet::ExecutionFailure => detail self.fail "Could not %s zone: %s" % [cmd[0], detail] end end end diff --git a/lib/puppet/provider/zpool/solaris.rb b/lib/puppet/provider/zpool/solaris.rb index aaa79c15f..8fc01ba81 100644 --- a/lib/puppet/provider/zpool/solaris.rb +++ b/lib/puppet/provider/zpool/solaris.rb @@ -1,116 +1,116 @@ Puppet::Type.type(:zpool).provide(:solaris) do desc "Provider for Solaris zpool." commands :zpool => "/usr/sbin/zpool" defaultfor :operatingsystem => :solaris def process_zpool_data(pool_array) if pool_array == [] return Hash.new(:absent) end #get the name and get rid of it pool = Hash.new pool[:pool] = pool_array[0] pool_array.shift tmp = [] #order matters here :( pool_array.reverse.each do |value| sym = nil case value - when "spares": sym = :spare - when "logs": sym = :log - when "mirror", "raidz1", "raidz2": + when "spares"; sym = :spare + when "logs"; sym = :log + when "mirror", "raidz1", "raidz2" sym = value == "mirror" ? :mirror : :raidz pool[:raid_parity] = "raidz2" if value == "raidz2" else tmp << value sym = :disk if value == pool_array.first end if sym pool[sym] = pool[sym] ? pool[sym].unshift(tmp.reverse.join(' ')) : [tmp.reverse.join(' ')] tmp.clear end end pool end def get_pool_data #this is all voodoo dependent on the output from zpool zpool_data = %x{ zpool status #{@resource[:pool]}}.split("\n").select { |line| line.index("\t") == 0 }.collect { |l| l.strip.split("\s")[0] } zpool_data.shift zpool_data end def current_pool unless (defined?(@current_pool) and @current_pool) @current_pool = process_zpool_data(get_pool_data) end @current_pool end def flush @current_pool= nil end #Adds log and spare def build_named(name) if prop = @resource[name.intern] [name] + prop.collect { |p| p.split(' ') }.flatten else [] end end #query for parity and set the right string def raidzarity @resource[:raid_parity] ? @resource[:raid_parity] : "raidz1" end #handle mirror or raid def handle_multi_arrays(prefix, array) array.collect{ |a| [prefix] + a.split(' ') }.flatten end #builds up the vdevs for create command def build_vdevs if disk = @resource[:disk] disk.collect { |d| d.split(' ') }.flatten elsif mirror = @resource[:mirror] handle_multi_arrays("mirror", mirror) elsif raidz = @resource[:raidz] handle_multi_arrays(raidzarity, raidz) end end def create zpool(*([:create, @resource[:pool]] + build_vdevs + build_named("spare") + build_named("log"))) end def delete zpool :destroy, @resource[:pool] end def exists? if current_pool[:pool] == :absent false else true end end [:disk, :mirror, :raidz, :log, :spare].each do |field| define_method(field) do current_pool[field] end define_method(field.to_s + "=") do |should| Puppet.warning "NO CHANGES BEING MADE: zpool %s does not match, should be '%s' currently is '%s'" % [field, should, current_pool[field]] end end end diff --git a/lib/puppet/rails.rb b/lib/puppet/rails.rb index a021c773a..e006f8190 100644 --- a/lib/puppet/rails.rb +++ b/lib/puppet/rails.rb @@ -1,136 +1,136 @@ # Load the appropriate libraries, or set a class indicating they aren't available require 'facter' require 'puppet' module Puppet::Rails def self.connect # This global init does not work for testing, because we remove # the state dir on every test. return if ActiveRecord::Base.connected? Puppet.settings.use(:main, :rails, :puppetmasterd) ActiveRecord::Base.logger = Logger.new(Puppet[:railslog]) begin loglevel = Logger.const_get(Puppet[:rails_loglevel].upcase) ActiveRecord::Base.logger.level = loglevel rescue => detail Puppet.warning "'%s' is not a valid Rails log level; using debug" % Puppet[:rails_loglevel] ActiveRecord::Base.logger.level = Logger::DEBUG end ActiveRecord::Base.allow_concurrency = true ActiveRecord::Base.verify_active_connections! begin ActiveRecord::Base.establish_connection(database_arguments()) rescue => detail if Puppet[:trace] puts detail.backtrace end raise Puppet::Error, "Could not connect to database: %s" % detail end end # The arguments for initializing the database connection. def self.database_arguments adapter = Puppet[:dbadapter] args = {:adapter => adapter, :log_level => Puppet[:rails_loglevel]} case adapter - when "sqlite3": + when "sqlite3" args[:dbfile] = Puppet[:dblocation] - when "mysql", "postgresql": + when "mysql", "postgresql" args[:host] = Puppet[:dbserver] unless Puppet[:dbserver].empty? args[:username] = Puppet[:dbuser] unless Puppet[:dbuser].empty? args[:password] = Puppet[:dbpassword] unless Puppet[:dbpassword].empty? args[:database] = Puppet[:dbname] socket = Puppet[:dbsocket] args[:socket] = socket unless socket.empty? else raise ArgumentError, "Invalid db adapter %s" % adapter end args end # Set up our database connection. It'd be nice to have a "use" system # that could make callbacks. def self.init unless Puppet.features.rails? raise Puppet::DevError, "No activerecord, cannot init Puppet::Rails" end connect() unless ActiveRecord::Base.connection.tables.include?("resources") require 'puppet/rails/database/schema' Puppet::Rails::Schema.init end if Puppet[:dbmigrate] migrate() end end # Migrate to the latest db schema. def self.migrate dbdir = nil $:.each { |d| tmp = File.join(d, "puppet/rails/database") if FileTest.directory?(tmp) dbdir = tmp break end } unless dbdir raise Puppet::Error, "Could not find Puppet::Rails database dir" end unless ActiveRecord::Base.connection.tables.include?("resources") raise Puppet::Error, "Database has problems, can't migrate." end Puppet.notice "Migrating" begin ActiveRecord::Migrator.migrate(dbdir) rescue => detail if Puppet[:trace] puts detail.backtrace end raise Puppet::Error, "Could not migrate database: %s" % detail end end # Tear down the database. Mostly only used during testing. def self.teardown unless Puppet.features.rails? raise Puppet::DevError, "No activerecord, cannot init Puppet::Rails" end Puppet.settings.use(:puppetmasterd, :rails) begin ActiveRecord::Base.establish_connection(database_arguments()) rescue => detail if Puppet[:trace] puts detail.backtrace end raise Puppet::Error, "Could not connect to database: %s" % detail end ActiveRecord::Base.connection.tables.each do |t| ActiveRecord::Base.connection.drop_table t end end end if Puppet.features.rails? require 'puppet/rails/host' end diff --git a/lib/puppet/reference/providers.rb b/lib/puppet/reference/providers.rb index 8fd2dbadc..f9f83a0fc 100644 --- a/lib/puppet/reference/providers.rb +++ b/lib/puppet/reference/providers.rb @@ -1,124 +1,124 @@ # This doesn't get stored in trac, since it changes every time. providers = Puppet::Util::Reference.newreference :providers, :title => "Provider Suitability Report", :depth => 1, :dynamic => true, :doc => "Which providers are valid for this machine" do types = [] Puppet::Type.loadall Puppet::Type.eachtype do |klass| next unless klass.providers.length > 0 types << klass end types.sort! { |a,b| a.name.to_s <=> b.name.to_s } unless ARGV.empty? types.reject! { |type| ! ARGV.include?(type.name.to_s) } end ret = "Details about this host:\n\n" # Throw some facts in there, so we know where the report is from. ["Ruby Version", "Puppet Version", "Operating System", "Operating System Release"].each do |label| name = label.gsub(/\s+/, '') value = Facter.value(name) ret += option(label, value) end ret += "\n" count = 1 # Produce output for each type. types.each do |type| features = type.features ret += "\n" # add a trailing newline # Now build up a table of provider suitability. headers = %w{Provider Suitable?} + features.collect { |f| f.to_s }.sort table_data = {} functional = false notes = [] begin default = type.defaultprovider.name rescue Puppet::DevError default = "none" end type.providers.sort { |a,b| a.to_s <=> b.to_s }.each do |pname| data = [] table_data[pname] = data provider = type.provider(pname) # Add the suitability note if missing = provider.suitable?(false) and missing.empty? data << "**X**" suit = true functional = true else data << "[%s]_" % [count] # A pointer to the appropriate footnote suit = false end # Add a footnote with the details about why this provider is unsuitable, if that's the case unless suit details = ".. [%s]\n" % count missing.each do |test, values| case test - when :exists: + when :exists details += " - Missing files %s\n" % values.join(", ") - when :variable: + when :variable values.each do |name, facts| if Puppet.settings.valid?(name) details += " - Setting %s (currently %s) not in list %s\n" % [name, Puppet.settings.value(name).inspect, facts.join(", ")] else details += " - Fact %s (currently %s) not in list %s\n" % [name, Facter.value(name).inspect, facts.join(", ")] end end - when :true: + when :true details += " - Got %s true tests that should have been false\n" % values - when :false: + when :false details += " - Got %s false tests that should have been true\n" % values - when :feature: + when :feature details += " - Missing features %s\n" % values.collect { |f| f.to_s }.join(",") end end notes << details count += 1 end # Add a note for every feature features.each do |feature| if provider.features.include?(feature) data << "**X**" else data << "" end end end ret += h(type.name.to_s + "_", 2) ret += ".. _%s: %s\n\n" % [type.name, "http://reductivelabs.com/trac/puppet/wiki/TypeReference#%s" % type.name] ret += option("Default provider", default) ret += doctable(headers, table_data) notes.each do |note| ret += note + "\n" end ret += "\n" end ret += "\n" ret end providers.header = " Puppet resource types are usually backed by multiple implementations called ``providers``, which handle variance between platforms and tools. Different providers are suitable or unsuitable on different platforms based on things like the presence of a given tool. Here are all of the provider-backed types and their different providers. Any unmentioned types do not use providers yet. " diff --git a/lib/puppet/reports/tagmail.rb b/lib/puppet/reports/tagmail.rb index 00571a8be..963cc0c19 100644 --- a/lib/puppet/reports/tagmail.rb +++ b/lib/puppet/reports/tagmail.rb @@ -1,187 +1,187 @@ require 'puppet' require 'pp' require 'net/smtp' Puppet::Reports.register_report(:tagmail) do desc "This report sends specific log messages to specific email addresses based on the tags in the log messages. See the `UsingTags tag documentation`:trac: for more information on tags. To use this report, you must create a ``tagmail.conf`` (in the location specified by ``tagmap``). This is a simple file that maps tags to email addresses: Any log messages in the report that match the specified tags will be sent to the specified email addresses. Tags must be comma-separated, and they can be negated so that messages only match when they do not have that tag. The tags are separated from the email addresses by a colon, and the email addresses should also be comma-separated. Lastly, there is an ``all`` tag that will always match all log messages. Here is an example tagmail.conf:: all: me@domain.com webserver, !mailserver: httpadmins@domain.com This will send all messages to ``me@domain.com``, and all messages from webservers that are not also from mailservers to ``httpadmins@domain.com``. If you are using anti-spam controls, such as grey-listing, on your mail server you should whitelist the sending email (controlled by ``reportform`` configuration option) to ensure your email is not discarded as spam. " # Find all matching messages. def match(taglists) reports = [] taglists.each do |emails, pos, neg| # First find all of the messages matched by our positive tags messages = nil if pos.include?("all") messages = self.logs else # Find all of the messages that are tagged with any of our # tags. messages = self.logs.find_all do |log| pos.detect { |tag| log.tagged?(tag) } end end # Now go through and remove any messages that match our negative tags messages = messages.reject do |log| if neg.detect do |tag| log.tagged?(tag) end true end end if messages.empty? Puppet.info "No messages to report to %s" % emails.join(",") next else reports << [emails, messages.collect { |m| m.to_report }.join("\n")] end end return reports end # Load the config file def parse(text) taglists = [] text.split("\n").each do |line| taglist = emails = nil case line.chomp - when /^\s*#/: next - when /^\s*$/: next - when /^\s*(.+)\s*:\s*(.+)\s*$/: + when /^\s*#/; next + when /^\s*$/; next + when /^\s*(.+)\s*:\s*(.+)\s*$/ taglist = $1 emails = $2.sub(/#.*$/,'') else raise ArgumentError, "Invalid tagmail config file" end pos = [] neg = [] taglist.sub(/\s+$/,'').split(/\s*,\s*/).each do |tag| unless tag =~ /^!?[-\w]+$/ raise ArgumentError, "Invalid tag %s" % tag.inspect end case tag - when /^\w+/: pos << tag - when /^!\w+/: neg << tag.sub("!", '') + when /^\w+/; pos << tag + when /^!\w+/; neg << tag.sub("!", '') else raise Puppet::Error, "Invalid tag '%s'" % tag end end # Now split the emails emails = emails.sub(/\s+$/,'').split(/\s*,\s*/) taglists << [emails, pos, neg] end return taglists end # Process the report. This just calls the other associated messages. def process unless FileTest.exists?(Puppet[:tagmap]) Puppet.notice "Cannot send tagmail report; no tagmap file %s" % Puppet[:tagmap] return end taglists = parse(File.read(Puppet[:tagmap])) # Now find any appropriately tagged messages. reports = match(taglists) send(reports) end # Send the email reports. def send(reports) pid = fork do if Puppet[:smtpserver] != "none" begin Net::SMTP.start(Puppet[:smtpserver]) do |smtp| reports.each do |emails, messages| Puppet.info "Sending report to %s" % emails.join(", ") smtp.open_message_stream(Puppet[:reportfrom], *emails) do |p| p.puts "From: #{Puppet[:reportfrom]}" p.puts "Subject: Puppet Report for %s" % self.host p.puts "To: " + emails.join(", ") p.puts "Date: " + Time.now.rfc2822 p.puts p.puts messages end end end rescue => detail if Puppet[:debug] puts detail.backtrace end raise Puppet::Error, "Could not send report emails through smtp: %s" % detail end elsif Puppet[:sendmail] != "" begin reports.each do |emails, messages| Puppet.info "Sending report to %s" % emails.join(", ") # We need to open a separate process for every set of email addresses sync.synchronize do IO.popen(Puppet[:sendmail] + " " + emails.join(" "), "w") do |p| p.puts "From: #{Puppet[:reportfrom]}" p.puts "Subject: Puppet Report for %s" % self.host p.puts "To: " + emails.join(", ") p.puts messages end end end rescue => detail if Puppet[:debug] puts detail.backtrace end raise Puppet::Error, "Could not send report emails via sendmail: %s" % detail end else raise Puppet::Error, "SMTP server is unset and could not find sendmail" end end # Don't bother waiting for the pid to return. Process.detach(pid) end def sync unless defined?(@sync) @sync = Sync.new end @sync end end diff --git a/lib/puppet/simple_graph.rb b/lib/puppet/simple_graph.rb index b9ea0f394..cfcf13131 100644 --- a/lib/puppet/simple_graph.rb +++ b/lib/puppet/simple_graph.rb @@ -1,456 +1,456 @@ # Created by Luke A. Kanies on 2007-11-07. # Copyright (c) 2007. All rights reserved. require 'puppet/external/dot' require 'puppet/relationship' # A hopefully-faster graph class to replace the use of GRATR. class Puppet::SimpleGraph # An internal class for handling a vertex's edges. class VertexWrapper attr_accessor :in, :out, :vertex # Remove all references to everything. def clear @adjacencies[:in].clear @adjacencies[:out].clear @vertex = nil end def initialize(vertex) @vertex = vertex @adjacencies = {:in => {}, :out => {}} end # Find adjacent vertices or edges. def adjacent(options) direction = options[:direction] || :out options[:type] ||= :vertices return @adjacencies[direction].values.flatten if options[:type] == :edges return @adjacencies[direction].keys end # Add an edge to our list. def add_edge(direction, edge) opposite_adjacencies(direction, edge) << edge end # Return all known edges. def edges [:in, :out].collect { |dir| @adjacencies[dir].values }.flatten end # Test whether we share an edge with a given vertex. def has_edge?(direction, vertex) return true if vertex_adjacencies(direction, vertex).length > 0 return false end # Create methods for returning the degree and edges. [:in, :out].each do |direction| # LAK:NOTE If you decide to create methods for directly # testing the degree, you'll have to get the values and flatten # the results -- you might have duplicate edges, which can give # a false impression of what the degree is. That's just # as expensive as just getting the edge list, so I've decided # to only add this method. define_method("%s_edges" % direction) do @adjacencies[direction].values.flatten end end # The other vertex in the edge. def other_vertex(direction, edge) case direction - when :in: edge.source + when :in; edge.source else edge.target end end # Remove an edge from our list. Assumes that we've already checked # that the edge is valid. def remove_edge(direction, edge) opposite_adjacencies(direction, edge).delete(edge) end def to_s vertex.to_s end private # These methods exist so we don't need a Hash with a default proc. # Look up the adjacencies for a vertex at the other end of an # edge. def opposite_adjacencies(direction, edge) opposite_vertex = other_vertex(direction, edge) vertex_adjacencies(direction, opposite_vertex) end # Look up the adjacencies for a given vertex. def vertex_adjacencies(direction, vertex) @adjacencies[direction][vertex] ||= [] @adjacencies[direction][vertex] end end def initialize @vertices = {} @edges = [] end # Clear our graph. def clear @vertices.each { |vertex, wrapper| wrapper.clear } @vertices.clear @edges.clear end # Which resources a given resource depends upon. def dependents(resource) tree_from_vertex(resource).keys end # Which resources depend upon the given resource. def dependencies(resource) # Cache the reversal graph, because it's somewhat expensive # to create. unless defined? @reversal and @reversal @reversal = reversal end # Strangely, it's significantly faster to search a reversed # tree in the :out direction than to search a normal tree # in the :in direction. @reversal.tree_from_vertex(resource, :out).keys end # Whether our graph is directed. Always true. Used to produce dot files. def directed? true end # Determine all of the leaf nodes below a given vertex. def leaves(vertex, direction = :out) tree = tree_from_vertex(vertex, direction) l = tree.keys.find_all { |c| adjacent(c, :direction => direction).empty? } return l end # Collect all of the edges that the passed events match. Returns # an array of edges. def matching_edges(events, base = nil) events.collect do |event| source = base || event.source unless vertex?(source) Puppet.warning "Got an event from invalid vertex %s" % source.ref next end # Get all of the edges that this vertex should forward events # to, which is the same thing as saying all edges directly below # This vertex in the graph. adjacent(source, :direction => :out, :type => :edges).find_all do |edge| edge.match?(event.name) end end.compact.flatten end # Return a reversed version of this graph. def reversal result = self.class.new vertices.each { |vertex| result.add_vertex(vertex) } edges.each do |edge| newedge = edge.class.new(edge.target, edge.source, edge.label) result.add_edge(newedge) end result end # Return the size of the graph. def size @vertices.length end # Return the graph as an array. def to_a @vertices.keys end # Provide a topological sort. def topsort degree = {} zeros = [] result = [] # Collect each of our vertices, with the number of in-edges each has. @vertices.each do |name, wrapper| edges = wrapper.in_edges zeros << wrapper if edges.length == 0 degree[wrapper.vertex] = edges end # Iterate over each 0-degree vertex, decrementing the degree of # each of its out-edges. while wrapper = zeros.pop do result << wrapper.vertex wrapper.out_edges.each do |edge| degree[edge.target].delete(edge) zeros << @vertices[edge.target] if degree[edge.target].length == 0 end end # If we have any vertices left with non-zero in-degrees, then we've found a cycle. if cycles = degree.find_all { |vertex, edges| edges.length > 0 } and cycles.length > 0 message = cycles.collect { |vertex, edges| edges.collect { |e| e.to_s }.join(", ") }.join(", ") raise Puppet::Error, "Found dependency cycles in the following relationships: %s" % message end return result end # Add a new vertex to the graph. def add_vertex(vertex) @reversal = nil return false if vertex?(vertex) setup_vertex(vertex) true # don't return the VertexWrapper instance. end # Remove a vertex from the graph. def remove_vertex!(vertex) return nil unless vertex?(vertex) @vertices[vertex].edges.each { |edge| remove_edge!(edge) } @vertices[vertex].clear @vertices.delete(vertex) end # Test whether a given vertex is in the graph. def vertex?(vertex) @vertices.include?(vertex) end # Return a list of all vertices. def vertices @vertices.keys end # Add a new edge. The graph user has to create the edge instance, # since they have to specify what kind of edge it is. def add_edge(source, target = nil, label = nil) @reversal = nil if target edge = Puppet::Relationship.new(source, target, label) else edge = source end [edge.source, edge.target].each { |vertex| setup_vertex(vertex) unless vertex?(vertex) } @vertices[edge.source].add_edge :out, edge @vertices[edge.target].add_edge :in, edge @edges << edge true end # Find a matching edge. Note that this only finds the first edge, # not all of them or whatever. def edge(source, target) @edges.each_with_index { |test_edge, index| return test_edge if test_edge.source == source and test_edge.target == target } end def edge_label(source, target) return nil unless edge = edge(source, target) edge.label end # Is there an edge between the two vertices? def edge?(source, target) return false unless vertex?(source) and vertex?(target) @vertices[source].has_edge?(:out, target) end def edges @edges.dup end # Remove an edge from our graph. def remove_edge!(edge) @vertices[edge.source].remove_edge(:out, edge) @vertices[edge.target].remove_edge(:in, edge) # Here we are looking for an exact edge, so we don't want to use ==, because # it's too darn expensive (in testing, deleting 3000 edges went from 6 seconds to # 0.05 seconds with this change). @edges.each_with_index { |test_edge, index| @edges.delete_at(index) and break if edge.equal?(test_edge) } nil end # Find adjacent edges. def adjacent(vertex, options = {}) return [] unless wrapper = @vertices[vertex] return wrapper.adjacent(options) end private # An internal method that skips the validation, so we don't have # duplicate validation calls. def setup_vertex(vertex) @vertices[vertex] = VertexWrapper.new(vertex) end public # # For some reason, unconnected vertices do not show up in # # this graph. # def to_jpg(path, name) # gv = vertices() # Dir.chdir(path) do # induced_subgraph(gv).write_to_graphic_file('jpg', name) # end # end # Take container information from another graph and use it # to replace any container vertices with their respective leaves. # This creates direct relationships where there were previously # indirect relationships through the containers. def splice!(other, type) # We have to get the container list via a topological sort on the # configuration graph, because otherwise containers that contain # other containers will add those containers back into the # graph. We could get a similar affect by only setting relationships # to container leaves, but that would result in many more # relationships. containers = other.topsort.find_all { |v| v.is_a?(type) and vertex?(v) } containers.each do |container| # Get the list of children from the other graph. children = other.adjacent(container, :direction => :out) # Just remove the container if it's empty. if children.empty? remove_vertex!(container) next end # First create new edges for each of the :in edges [:in, :out].each do |dir| edges = adjacent(container, :direction => dir, :type => :edges) edges.each do |edge| children.each do |child| if dir == :in s = edge.source t = child else s = child t = edge.target end add_edge(s, t, edge.label) end # Now get rid of the edge, so remove_vertex! works correctly. remove_edge!(edge) end end remove_vertex!(container) end end def to_yaml_properties instance_variables end # Just walk the tree and pass each edge. def walk(source, direction, &block) adjacent(source, :direction => direction).each do |target| yield source, target walk(target, direction, &block) end end # A different way of walking a tree, and a much faster way than the # one that comes with GRATR. def tree_from_vertex(start, direction = :out) predecessor={} walk(start, direction) do |parent, child| predecessor[child] = parent end predecessor end # LAK:FIXME This is just a paste of the GRATR code with slight modifications. # Return a DOT::DOTDigraph for directed graphs or a DOT::DOTSubgraph for an # undirected Graph. _params_ can contain any graph property specified in # rdot.rb. If an edge or vertex label is a kind of Hash then the keys # which match +dot+ properties will be used as well. def to_dot_graph (params = {}) params['name'] ||= self.class.name.gsub(/:/,'_') fontsize = params['fontsize'] ? params['fontsize'] : '8' graph = (directed? ? DOT::DOTDigraph : DOT::DOTSubgraph).new(params) edge_klass = directed? ? DOT::DOTDirectedEdge : DOT::DOTEdge vertices.each do |v| name = v.to_s params = {'name' => '"'+name+'"', 'fontsize' => fontsize, 'label' => name} v_label = v.to_s params.merge!(v_label) if v_label and v_label.kind_of? Hash graph << DOT::DOTNode.new(params) end edges.each do |e| params = {'from' => '"'+ e.source.to_s + '"', 'to' => '"'+ e.target.to_s + '"', 'fontsize' => fontsize } e_label = e.to_s params.merge!(e_label) if e_label and e_label.kind_of? Hash graph << edge_klass.new(params) end graph end # Output the dot format as a string def to_dot (params={}) to_dot_graph(params).to_s; end # Call +dotty+ for the graph which is written to the file 'graph.dot' # in the # current directory. def dotty (params = {}, dotfile = 'graph.dot') File.open(dotfile, 'w') {|f| f << to_dot(params) } system('dotty', dotfile) end # Just walk the tree and pass each edge. def walk(source, direction, &block) adjacent(source, :direction => direction).each do |target| yield source, target walk(target, direction, &block) end end # Use +dot+ to create a graphical representation of the graph. Returns the # filename of the graphics file. def write_to_graphic_file (fmt='png', dotfile='graph') src = dotfile + '.dot' dot = dotfile + '.' + fmt File.open(src, 'w') {|f| f << self.to_dot << "\n"} system( "dot -T#{fmt} #{src} -o #{dot}" ) dot end # Produce the graph files if requested. def write_graph(name) return unless Puppet[:graph] Puppet.settings.use(:graphing) file = File.join(Puppet[:graphdir], "%s.dot" % name.to_s) File.open(file, "w") { |f| f.puts to_dot("name" => name.to_s.capitalize) } end end diff --git a/lib/puppet/sslcertificates.rb b/lib/puppet/sslcertificates.rb index 0c579d0ad..5155b3d5b 100755 --- a/lib/puppet/sslcertificates.rb +++ b/lib/puppet/sslcertificates.rb @@ -1,149 +1,149 @@ # The library for manipulating SSL certs. require 'puppet' begin require 'openssl' rescue LoadError raise Puppet::Error, "You must have the Ruby openssl library installed" end module Puppet::SSLCertificates #def self.mkcert(type, name, dnsnames, ttl, issuercert, issuername, serial, publickey) def self.mkcert(hash) [:type, :name, :ttl, :issuer, :serial, :publickey].each { |param| unless hash.include?(param) raise ArgumentError, "mkcert called without %s" % param end } cert = OpenSSL::X509::Certificate.new # Make the certificate valid as of yesterday, because # so many people's clocks are out of sync. from = Time.now - (60*60*24) cert.subject = hash[:name] if hash[:issuer] cert.issuer = hash[:issuer].subject else # we're a self-signed cert cert.issuer = hash[:name] end cert.not_before = from cert.not_after = from + hash[:ttl] cert.version = 2 # X509v3 cert.public_key = hash[:publickey] cert.serial = hash[:serial] basic_constraint = nil key_usage = nil ext_key_usage = nil subject_alt_name = [] ef = OpenSSL::X509::ExtensionFactory.new ef.subject_certificate = cert if hash[:issuer] ef.issuer_certificate = hash[:issuer] else ef.issuer_certificate = cert end ex = [] case hash[:type] - when :ca: + when :ca basic_constraint = "CA:TRUE" key_usage = %w{cRLSign keyCertSign} - when :terminalsubca: + when :terminalsubca basic_constraint = "CA:TRUE,pathlen:0" key_usage = %w{cRLSign keyCertSign} - when :server: + when :server basic_constraint = "CA:FALSE" dnsnames = Puppet[:certdnsnames] name = hash[:name].to_s.sub(%r{/CN=},'') if dnsnames != "" dnsnames.split(':').each { |d| subject_alt_name << 'DNS:' + d } subject_alt_name << 'DNS:' + name # Add the fqdn as an alias elsif name == Facter.value(:fqdn) # we're a CA server, and thus probably the server subject_alt_name << 'DNS:' + "puppet" # Add 'puppet' as an alias subject_alt_name << 'DNS:' + name # Add the fqdn as an alias subject_alt_name << 'DNS:' + name.sub(/^[^.]+./, "puppet.") # add puppet.domain as an alias end key_usage = %w{digitalSignature keyEncipherment} ext_key_usage = %w{serverAuth clientAuth emailProtection} - when :ocsp: + when :ocsp basic_constraint = "CA:FALSE" key_usage = %w{nonRepudiation digitalSignature} ext_key_usage = %w{serverAuth OCSPSigning} - when :client: + when :client basic_constraint = "CA:FALSE" key_usage = %w{nonRepudiation digitalSignature keyEncipherment} ext_key_usage = %w{clientAuth emailProtection} ex << ef.create_extension("nsCertType", "client,email") else raise Puppet::Error, "unknown cert type '%s'" % hash[:type] end ex << ef.create_extension("nsComment", "Puppet Ruby/OpenSSL Generated Certificate") ex << ef.create_extension("basicConstraints", basic_constraint, true) ex << ef.create_extension("subjectKeyIdentifier", "hash") ex << ef.create_extension("keyUsage", key_usage.join(",")) if key_usage ex << ef.create_extension("extendedKeyUsage", ext_key_usage.join(",")) if ext_key_usage ex << ef.create_extension("subjectAltName", subject_alt_name.join(",")) if ! subject_alt_name.empty? #if @ca_config[:cdp_location] then # ex << ef.create_extension("crlDistributionPoints", # @ca_config[:cdp_location]) #end #if @ca_config[:ocsp_location] then # ex << ef.create_extension("authorityInfoAccess", # "OCSP;" << @ca_config[:ocsp_location]) #end cert.extensions = ex # for some reason this _must_ be the last extension added ex << ef.create_extension("authorityKeyIdentifier", "keyid:always,issuer:always") if hash[:type] == :ca return cert end def self.mkhash(dir, cert, certfile) # Make sure the hash is zero-padded to 8 chars hash = "%08x" % cert.issuer.hash hashpath = nil 10.times { |i| path = File.join(dir, "%s.%s" % [hash, i]) if FileTest.exists?(path) if FileTest.symlink?(path) dest = File.readlink(path) if dest == certfile # the correct link already exists hashpath = path break else next end else next end end File.symlink(certfile, path) hashpath = path break } return hashpath end require 'puppet/sslcertificates/certificate' require 'puppet/sslcertificates/inventory' require 'puppet/sslcertificates/ca' end diff --git a/lib/puppet/sslcertificates/certificate.rb b/lib/puppet/sslcertificates/certificate.rb index a632cbd6c..f348cdcb8 100644 --- a/lib/puppet/sslcertificates/certificate.rb +++ b/lib/puppet/sslcertificates/certificate.rb @@ -1,282 +1,282 @@ class Puppet::SSLCertificates::Certificate SSLCertificates = Puppet::SSLCertificates attr_accessor :certfile, :keyfile, :name, :dir, :hash, :type attr_accessor :key, :cert, :csr, :cacert @@params2names = { :name => "CN", :state => "ST", :country => "C", :email => "emailAddress", :org => "O", :city => "L", :ou => "OU" } def certname OpenSSL::X509::Name.new self.subject end def delete [@certfile,@keyfile].each { |file| if FileTest.exists?(file) File.unlink(file) end } if defined? @hash and @hash if FileTest.symlink?(@hash) File.unlink(@hash) end end end def exists? return FileTest.exists?(@certfile) end def getkey unless FileTest.exists?(@keyfile) self.mkkey() end if @password @key = OpenSSL::PKey::RSA.new( File.read(@keyfile), @password ) else @key = OpenSSL::PKey::RSA.new( File.read(@keyfile) ) end end def initialize(hash) unless hash.include?(:name) raise Puppet::Error, "You must specify the common name for the certificate" end @name = hash[:name] # init a few variables @cert = @key = @csr = nil if hash.include?(:cert) @certfile = hash[:cert] @dir = File.dirname(@certfile) else @dir = hash[:dir] || Puppet[:certdir] @certfile = File.join(@dir, @name) end @cacertfile ||= File.join(Puppet[:certdir], "ca.pem") unless FileTest.directory?(@dir) Puppet.recmkdir(@dir) end unless @certfile =~ /\.pem$/ @certfile += ".pem" end @keyfile = hash[:key] || File.join( Puppet[:privatekeydir], [@name,"pem"].join(".") ) unless FileTest.directory?(@dir) Puppet.recmkdir(@dir) end [@keyfile].each { |file| dir = File.dirname(file) unless FileTest.directory?(dir) Puppet.recmkdir(dir) end } @ttl = hash[:ttl] || 365 * 24 * 60 * 60 @selfsign = hash[:selfsign] || false @encrypt = hash[:encrypt] || false @replace = hash[:replace] || false @issuer = hash[:issuer] || nil if hash.include?(:type) case hash[:type] - when :ca, :client, :server: @type = hash[:type] + when :ca, :client, :server; @type = hash[:type] else raise "Invalid Cert type %s" % hash[:type] end else @type = :client end @params = {:name => @name} [:state, :country, :email, :org, :ou].each { |param| if hash.include?(param) @params[param] = hash[param] end } if @encrypt if @encrypt =~ /^\// File.open(@encrypt) { |f| @password = f.read.chomp } else raise Puppet::Error, ":encrypt must be a path to a pass phrase file" end else @password = nil end if hash.include?(:selfsign) @selfsign = hash[:selfsign] else @selfsign = false end end # this only works for servers, not for users def mkcsr unless defined? @key and @key self.getkey end name = OpenSSL::X509::Name.new self.subject @csr = OpenSSL::X509::Request.new @csr.version = 0 @csr.subject = name @csr.public_key = @key.public_key @csr.sign(@key, OpenSSL::Digest::SHA1.new) #File.open(@csrfile, "w") { |f| # f << @csr.to_pem #} unless @csr.verify(@key.public_key) raise Puppet::Error, "CSR sign verification failed" end return @csr end def mkkey # @key is the file @key = OpenSSL::PKey::RSA.new(1024) # { |p,n| # case p # when 0; Puppet.info "key info: ." # BN_generate_prime # when 1; Puppet.info "key info: +" # BN_generate_prime # when 2; Puppet.info "key info: *" # searching good prime, # # n = #of try, # # but also data from BN_generate_prime # when 3; Puppet.info "key info: \n" # found good prime, n==0 - p, n==1 - q, # # but also data from BN_generate_prime # else; Puppet.info "key info: *" # BN_generate_prime # end # } if @password #passwdproc = proc { @password } keytext = @key.export( OpenSSL::Cipher::DES.new(:EDE3, :CBC), @password ) File.open(@keyfile, "w", 0400) { |f| f << keytext } else File.open(@keyfile, "w", 0400) { |f| f << @key.to_pem } end #cmd = "#{ossl} genrsa -out #{@key} 1024" end def mkselfsigned unless defined? @key and @key self.getkey end if defined? @cert and @cert raise Puppet::Error, "Cannot replace existing certificate" end args = { :name => self.certname, :ttl => @ttl, :issuer => nil, :serial => 0x0, :publickey => @key.public_key } if @type args[:type] = @type else args[:type] = :server end @cert = SSLCertificates.mkcert(args) @cert.sign(@key, OpenSSL::Digest::SHA1.new) if @selfsign return @cert end def subject(string = false) subj = @@params2names.collect { |param, name| if @params.include?(param) [name, @params[param]] end }.reject { |ary| ary.nil? } if string return "/" + subj.collect { |ary| "%s=%s" % ary }.join("/") + "/" else return subj end end # verify that we can track down the cert chain or whatever def verify "openssl verify -verbose -CAfile /home/luke/.puppet/ssl/certs/ca.pem -purpose sslserver culain.madstop.com.pem" end def write files = { @certfile => @cert, @keyfile => @key, } if defined? @cacert files[@cacertfile] = @cacert end files.each { |file,thing| if defined? thing and thing if FileTest.exists?(file) next end text = nil if thing.is_a?(OpenSSL::PKey::RSA) and @password text = thing.export( OpenSSL::Cipher::DES.new(:EDE3, :CBC), @password ) else text = thing.to_pem end File.open(file, "w", 0660) { |f| f.print text } end } if defined? @cacert SSLCertificates.mkhash(Puppet[:certdir], @cacert, @cacertfile) end end end diff --git a/lib/puppet/type.rb b/lib/puppet/type.rb index 706ea1386..0fa1697d1 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' # 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 ############################### # 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] + 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 + 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 + 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 + 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/file.rb b/lib/puppet/type/file.rb index b6ad4fe93..d96b5dd91 100644 --- a/lib/puppet/type/file.rb +++ b/lib/puppet/type/file.rb @@ -1,882 +1,882 @@ require 'digest/md5' require 'cgi' require 'etc' require 'uri' require 'fileutils' require 'puppet/network/handler' require 'puppet/util/diff' require 'puppet/util/checksums' require 'puppet/network/client' module Puppet newtype(:file) do include Puppet::Util::MethodHelper include Puppet::Util::Checksums @doc = "Manages local files, including setting ownership and permissions, creation of both files and directories, and retrieving entire files from remote servers. As Puppet matures, it expected that the ``file`` resource will be used less and less to manage content, and instead native resources will be used to do so. If you find that you are often copying files in from a central location, rather than using native resources, please contact Reductive Labs and we can hopefully work with you to develop a native resource to support what you are doing." newparam(:path) do desc "The path to the file to manage. Must be fully qualified." isnamevar validate do |value| unless value =~ /^#{File::SEPARATOR}/ raise Puppet::Error, "File paths must be fully qualified, not '%s'" % value end end end newparam(:backup) do desc "Whether files should be backed up before being replaced. The preferred method of backing files up is via a ``filebucket``, which stores files by their MD5 sums and allows easy retrieval without littering directories with backups. You can specify a local filebucket or a network-accessible server-based filebucket by setting ``backup => bucket-name``. Alternatively, if you specify any value that begins with a ``.`` (e.g., ``.puppet-bak``), then Puppet will use copy the file in the same directory with that value as the extension of the backup. Setting ``backup => false`` disables all backups of the file in question. Puppet automatically creates a local filebucket named ``puppet`` and defaults to backing up there. To use a server-based filebucket, you must specify one in your configuration:: filebucket { main: server => puppet } The ``puppetmasterd`` daemon creates a filebucket by default, so you can usually back up to your main server with this configuration. Once you've described the bucket in your configuration, you can use it in any file:: file { \"/my/file\": source => \"/path/in/nfs/or/something\", backup => main } This will back the file up to the central server. At this point, the benefits of using a filebucket are that you do not have backup files lying around on each of your machines, a given version of a file is only backed up once, and you can restore any given file manually, no matter how old. Eventually, transactional support will be able to automatically restore filebucketed files. " defaultto { "puppet" } munge do |value| # I don't really know how this is happening. value = value.shift if value.is_a?(Array) case value - when false, "false", :false: + when false, "false", :false false - when true, "true", ".puppet-bak", :true: + when true, "true", ".puppet-bak", :true ".puppet-bak" when /^\./ value - when String: + when String # We can't depend on looking this up right now, # we have to do it after all of the objects # have been instantiated. if resource.catalog and bucketobj = resource.catalog.resource(:filebucket, value) @resource.bucket = bucketobj.bucket bucketobj.title else # Set it to the string; finish() turns it into a # filebucket. @resource.bucket = value value end - when Puppet::Network::Client.client(:Dipper): + when Puppet::Network::Client.client(:Dipper) @resource.bucket = value value.name else self.fail "Invalid backup type %s" % value.inspect end end end newparam(:recurse) do desc "Whether and how deeply to do recursive management." newvalues(:true, :false, :inf, /^[0-9]+$/) # Replace the validation so that we allow numbers in # addition to string representations of them. validate { |arg| } munge do |value| newval = super(value) case newval - when :true, :inf: true - when :false: false - when Integer, Fixnum, Bignum: value - when /^\d+$/: Integer(value) + when :true, :inf; true + when :false; false + when Integer, Fixnum, Bignum; value + when /^\d+$/; Integer(value) else raise ArgumentError, "Invalid recurse value %s" % value.inspect end end end newparam(:replace, :boolean => true) do desc "Whether or not to replace a file that is sourced but exists. This is useful for using file sources purely for initialization." newvalues(:true, :false) aliasvalue(:yes, :true) aliasvalue(:no, :false) defaultto :true end newparam(:force, :boolean => true) do desc "Force the file operation. Currently only used when replacing directories with links." newvalues(:true, :false) defaultto false end newparam(:ignore) do desc "A parameter which omits action on files matching specified patterns during recursion. Uses Ruby's builtin globbing engine, so shell metacharacters are fully supported, e.g. ``[a-z]*``. Matches that would descend into the directory structure are ignored, e.g., ``*/*``." validate do |value| unless value.is_a?(Array) or value.is_a?(String) or value == false self.devfail "Ignore must be a string or an Array" end end end newparam(:links) do desc "How to handle links during file actions. During file copying, ``follow`` will copy the target file instead of the link, ``manage`` will copy the link itself, and ``ignore`` will just pass it by. When not copying, ``manage`` and ``ignore`` behave equivalently (because you cannot really ignore links entirely during local recursion), and ``follow`` will manage the file to which the link points." newvalues(:follow, :manage) defaultto :manage end newparam(:purge, :boolean => true) do desc "Whether unmanaged files should be purged. If you have a filebucket configured the purged files will be uploaded, but if you do not, this will destroy data. Only use this option for generated files unless you really know what you are doing. This option only makes sense when recursively managing directories. Note that when using ``purge`` with ``source``, Puppet will purge any files that are not on the remote system." defaultto :false newvalues(:true, :false) end newparam(:sourceselect) do desc "Whether to copy all valid sources, or just the first one. This parameter is only used in recursive copies; by default, the first valid source is the only one used as a recursive source, but if this parameter is set to ``all``, then all valid sources will have all of their contents copied to the local host, and for sources that have the same file, the source earlier in the list will be used." defaultto :first newvalues(:first, :all) end attr_accessor :bucket # Autorequire any parent directories. autorequire(:file) do if self[:path] File.dirname(self[:path]) else Puppet.err "no path for %s, somehow; cannot setup autorequires" % self.ref nil end end # Autorequire the owner and group of the file. {:user => :owner, :group => :group}.each do |type, property| autorequire(type) do if @parameters.include?(property) # The user/group property automatically converts to IDs next unless should = @parameters[property].shouldorig val = should[0] if val.is_a?(Integer) or val =~ /^\d+$/ nil else val end end end end CREATORS = [:content, :source, :target] validate do count = 0 CREATORS.each do |param| count += 1 if self.should(param) end if @parameters.include?(:source) count += 1 end if count > 1 self.fail "You cannot specify more than one of %s" % CREATORS.collect { |p| p.to_s}.join(", ") end end def self.[](path) return nil unless path super(path.gsub(/\/+/, '/').sub(/\/$/, '')) end # List files, but only one level deep. def self.instances(base = "/") unless FileTest.directory?(base) return [] end files = [] Dir.entries(base).reject { |e| e == "." or e == ".." }.each do |name| path = File.join(base, name) if obj = self[path] obj[:check] = :all files << obj else files << self.new( :name => path, :check => :all ) end end files end @depthfirst = false # Determine the user to write files as. def asuser if self.should(:owner) and ! self.should(:owner).is_a?(Symbol) writeable = Puppet::Util::SUIDManager.asuser(self.should(:owner)) { FileTest.writable?(File.dirname(self[:path])) } # If the parent directory is writeable, then we execute # as the user in question. Otherwise we'll rely on # the 'owner' property to do things. if writeable asuser = self.should(:owner) end end return asuser end # Does the file currently exist? Just checks for whether # we have a stat def exist? stat ? true : false end # We have to do some extra finishing, to retrieve our bucket if # there is one. def finish # Look up our bucket, if there is one if bucket = self.bucket case bucket - when String: + when String if catalog and obj = catalog.resource(:filebucket, bucket) self.bucket = obj.bucket elsif bucket == "puppet" obj = Puppet::Network::Client.client(:Dipper).new( :Path => Puppet[:clientbucketdir] ) self.bucket = obj else self.fail "Could not find filebucket '%s'" % bucket end - when Puppet::Network::Client.client(:Dipper): # things are hunky-dorey + when Puppet::Network::Client.client(:Dipper) # things are hunky-dorey when Puppet::Type::Filebucket # things are hunky-dorey self.bucket = bucket.bucket else self.fail "Invalid bucket type %s" % bucket.class end end super end # Create any children via recursion or whatever. def eval_generate return [] unless self.recurse? recurse #recurse.reject do |resource| # catalog.resource(:file, resource[:path]) #end.each do |child| # catalog.add_resource child # catalog.relationship_graph.add_edge self, child #end end def flush # We want to make sure we retrieve metadata anew on each transaction. @parameters.each do |name, param| param.flush if param.respond_to?(:flush) end @stat = nil end # Deal with backups. def handlebackup(file = nil) # let the path be specified file ||= self[:path] # if they specifically don't want a backup, then just say # we're good unless FileTest.exists?(file) return true end unless self[:backup] return true end case File.stat(file).ftype - when "directory": + when "directory" if self[:recurse] # we don't need to backup directories when recurse is on return true else backup = self.bucket || self[:backup] case backup - when Puppet::Network::Client.client(:Dipper): + when Puppet::Network::Client.client(:Dipper) notice "Recursively backing up to filebucket" require 'find' Find.find(self[:path]) do |f| if File.file?(f) sum = backup.backup(f) self.notice "Filebucketed %s to %s with sum %s" % [f, backup.name, sum] end end return true - when String: + when String newfile = file + backup # Just move it, since it's a directory. if FileTest.exists?(newfile) remove_backup(newfile) end begin bfile = file + backup # Ruby 1.8.1 requires the 'preserve' addition, but # later versions do not appear to require it. FileUtils.cp_r(file, bfile, :preserve => true) return true rescue => detail # since they said they want a backup, let's error out # if we couldn't make one self.fail "Could not back %s up: %s" % [file, detail.message] end else self.err "Invalid backup type %s" % backup.inspect return false end end - when "file": + when "file" backup = self.bucket || self[:backup] case backup - when Puppet::Network::Client.client(:Dipper): + when Puppet::Network::Client.client(:Dipper) sum = backup.backup(file) self.notice "Filebucketed to %s with sum %s" % [backup.name, sum] return true - when String: + when String newfile = file + backup if FileTest.exists?(newfile) remove_backup(newfile) end begin # FIXME Shouldn't this just use a Puppet object with # 'source' specified? bfile = file + backup # Ruby 1.8.1 requires the 'preserve' addition, but # later versions do not appear to require it. FileUtils.cp(file, bfile, :preserve => true) return true rescue => detail # since they said they want a backup, let's error out # if we couldn't make one self.fail "Could not back %s up: %s" % [file, detail.message] end else self.err "Invalid backup type %s" % backup.inspect return false end - when "link": return true + when "link"; return true else self.notice "Cannot backup files of type %s" % File.stat(file).ftype return false end end def initialize(hash) # Used for caching clients @clients = {} super # Get rid of any duplicate slashes, and remove any trailing slashes. @title = @title.gsub(/\/+/, "/") @title.sub!(/\/$/, "") unless @title == "/" @stat = nil end # Create a new file or directory object as a child to the current # object. def newchild(path) full_path = File.join(self[:path], path) # Add some new values to our original arguments -- these are the ones # set at initialization. We specifically want to exclude any param # values set by the :source property or any default values. # LAK:NOTE This is kind of silly, because the whole point here is that # the values set at initialization should live as long as the resource # but values set by default or by :source should only live for the transaction # or so. Unfortunately, we don't have a straightforward way to manage # the different lifetimes of this data, so we kludge it like this. # The right-side hash wins in the merge. options = @original_parameters.merge(:path => full_path, :implicit => true).reject { |param, value| value.nil? } # These should never be passed to our children. [:parent, :ensure, :recurse, :target, :alias].each do |param| options.delete(param) if options.include?(param) end return self.class.new(options) end # Files handle paths specially, because they just lengthen their # path names, rather than including the full parent's title each # time. def pathbuilder # We specifically need to call the method here, so it looks # up our parent in the catalog graph. if parent = parent() # We only need to behave specially when our parent is also # a file if parent.is_a?(self.class) # Remove the parent file name list = parent.pathbuilder list.pop # remove the parent's path info return list << self.ref else return super end else return [self.ref] end end # Should we be purging? def purge? @parameters.include?(:purge) and (self[:purge] == :true or self[:purge] == "true") end # Recursively generate a list of file resources, which will # be used to copy remote files, manage local files, and/or make links # to map to another directory. def recurse children = recurse_local if self[:target] recurse_link(children) elsif self[:source] recurse_remote(children) end return children.values.sort { |a, b| a[:path] <=> b[:path] } end # A simple method for determining whether we should be recursing. def recurse? return false unless @parameters.include?(:recurse) val = @parameters[:recurse].value if val and (val == true or val > 0) return true else return false end end # Recurse the target of the link. def recurse_link(children) perform_recursion(self[:target]).each do |meta| if meta.relative_path == "." self[:ensure] = :directory next end children[meta.relative_path] ||= newchild(meta.relative_path) if meta.ftype == "directory" children[meta.relative_path][:ensure] = :directory else children[meta.relative_path][:ensure] = :link children[meta.relative_path][:target] = meta.full_path end end children end # Recurse the file itself, returning a Metadata instance for every found file. def recurse_local result = perform_recursion(self[:path]) return {} unless result result.inject({}) do |hash, meta| next hash if meta.relative_path == "." hash[meta.relative_path] = newchild(meta.relative_path) hash end end # Recurse against our remote file. def recurse_remote(children) sourceselect = self[:sourceselect] total = self[:source].collect do |source| next unless result = perform_recursion(source) return if top = result.find { |r| r.relative_path == "." } and top.ftype != "directory" result.each { |data| data.source = "%s/%s" % [source, data.relative_path] } break result if result and ! result.empty? and sourceselect == :first result end.flatten # This only happens if we have sourceselect == :all unless sourceselect == :first found = [] total.reject! do |data| result = found.include?(data.relative_path) found << data.relative_path unless found.include?(data.relative_path) result end end total.each do |meta| if meta.relative_path == "." parameter(:source).metadata = meta next end children[meta.relative_path] ||= newchild(meta.relative_path) children[meta.relative_path][:source] = meta.source children[meta.relative_path][:checksum] = :md5 if meta.ftype == "file" children[meta.relative_path].parameter(:source).metadata = meta end # If we're purging resources, then delete any resource that isn't on the # remote system. if self.purge? # Make a hash of all of the resources we found remotely -- all we need is the # fast lookup, the values don't matter. remotes = total.inject({}) { |hash, meta| hash[meta.relative_path] = true; hash } children.each do |name, child| unless remotes.include?(name) child[:ensure] = :absent end end end children end def perform_recursion(path) Puppet::FileServing::Metadata.search(path, :links => self[:links], :recurse => self[:recurse], :ignore => self[:ignore]) end # Remove the old backup. def remove_backup(newfile) if self.class.name == :file and self[:links] != :follow method = :lstat else method = :stat end old = File.send(method, newfile).ftype if old == "directory" raise Puppet::Error, "Will not remove directory backup %s; use a filebucket" % newfile end info "Removing old backup of type %s" % File.send(method, newfile).ftype begin File.unlink(newfile) rescue => detail puts detail.backtrace if Puppet[:trace] self.err "Could not remove old backup: %s" % detail return false end end # Remove any existing data. This is only used when dealing with # links or directories. def remove_existing(should) expire() return unless s = stat self.fail "Could not back up; will not replace" unless handlebackup unless should.to_s == "link" return if s.ftype.to_s == should.to_s end case s.ftype - when "directory": + when "directory" if self[:force] == :true debug "Removing existing directory for replacement with %s" % should FileUtils.rmtree(self[:path]) else notice "Not removing directory; use 'force' to override" end - when "link", "file": + when "link", "file" debug "Removing existing %s for replacement with %s" % [s.ftype, should] File.unlink(self[:path]) else self.fail "Could not back up files of type %s" % s.ftype end expire end # a wrapper method to make sure the file exists before doing anything def retrieve if source = parameter(:source) source.copy_source_values end super end # Set the checksum, from another property. There are multiple # properties that modify the contents of a file, and they need the # ability to make sure that the checksum value is in sync. def setchecksum(sum = nil) if @parameters.include? :checksum if sum @parameters[:checksum].checksum = sum else # If they didn't pass in a sum, then tell checksum to # figure it out. currentvalue = @parameters[:checksum].retrieve @parameters[:checksum].checksum = currentvalue end end end # Should this thing be a normal file? This is a relatively complex # way of determining whether we're trying to create a normal file, # and it's here so that the logic isn't visible in the content property. def should_be_file? return true if self[:ensure] == :file # I.e., it's set to something like "directory" return false if e = self[:ensure] and e != :present # The user doesn't really care, apparently if self[:ensure] == :present return true unless s = stat return true if s.ftype == "file" return false end # If we've gotten here, then :ensure isn't set return true if self[:content] return true if stat and stat.ftype == "file" return false end # Stat our file. Depending on the value of the 'links' attribute, we # use either 'stat' or 'lstat', and we expect the properties to use the # resulting stat object accordingly (mostly by testing the 'ftype' # value). cached_attr(:stat) do method = :stat # Files are the only types that support links if (self.class.name == :file and self[:links] != :follow) or self.class.name == :tidy method = :lstat end path = self[:path] begin File.send(method, self[:path]) rescue Errno::ENOENT => error return nil rescue Errno::EACCES => error warning "Could not stat; permission denied" return nil end end # We have to hack this just a little bit, because otherwise we'll get # an error when the target and the contents are created as properties on # the far side. def to_trans(retrieve = true) obj = super if obj[:target] == :notlink obj.delete(:target) end obj end # Write out the file. Requires the content to be written, # the property name for logging, and the checksum for validation. def write(content, property, checksum = nil) if validate = validate_checksum? # Use the appropriate checksum type -- md5, md5lite, etc. sumtype = property(:checksum).checktype checksum ||= "{#{sumtype}}" + property(:checksum).send(sumtype, content) end remove_existing(:file) use_temporary_file = (content.length != 0) path = self[:path] path += ".puppettmp" if use_temporary_file mode = self.should(:mode) # might be nil umask = mode ? 000 : 022 Puppet::Util.withumask(umask) do File.open(path, File::CREAT|File::WRONLY|File::TRUNC, mode) { |f| f.print content } end # And put our new file in place if use_temporary_file # This is only not true when our file is empty. begin fail_if_checksum_is_wrong(path, checksum) if validate File.rename(path, self[:path]) rescue => detail self.err "Could not rename tmp %s for replacing: %s" % [self[:path], detail] ensure # Make sure the created file gets removed File.unlink(path) if FileTest.exists?(path) end end # make sure all of the modes are actually correct property_fix # And then update our checksum, so the next run doesn't find it. self.setchecksum(checksum) end # Should we validate the checksum of the file we're writing? def validate_checksum? if sumparam = @parameters[:checksum] return sumparam.checktype.to_s !~ /time/ else return false end end private # Make sure the file we wrote out is what we think it is. def fail_if_checksum_is_wrong(path, checksum) if checksum =~ /^\{(\w+)\}.+/ sumtype = $1 else # This shouldn't happen, but if it happens to, it's nicer # to just use a default sumtype than fail. sumtype = "md5" end newsum = property(:checksum).getsum(sumtype, path) return if newsum == checksum self.fail "File written to disk did not match checksum; discarding changes (%s vs %s)" % [checksum, newsum] end # Override the parent method, because we don't want to generate changes # when the file is missing and there is no 'ensure' state. def propertychanges(currentvalues) unless self.stat found = false ([:ensure] + CREATORS).each do |prop| if @parameters.include?(prop) found = true break end end unless found return [] end end super end # There are some cases where all of the work does not get done on # file creation/modification, so we have to do some extra checking. def property_fix properties.each do |thing| next unless [:mode, :owner, :group, :seluser, :selrole, :seltype, :selrange].include?(thing.name) # Make sure we get a new stat objct self.stat(true) currentvalue = thing.retrieve unless thing.insync?(currentvalue) thing.sync end end end end # Puppet::Type.type(:pfile) # We put all of the properties in separate files, because there are so many # of them. The order these are loaded is important, because it determines # the order they are in the property lit. require 'puppet/type/file/checksum' require 'puppet/type/file/content' # can create the file require 'puppet/type/file/source' # can create the file require 'puppet/type/file/target' # creates a different type of file require 'puppet/type/file/ensure' # can create the file require 'puppet/type/file/owner' require 'puppet/type/file/group' require 'puppet/type/file/mode' require 'puppet/type/file/type' require 'puppet/type/file/selcontext' # SELinux file context end diff --git a/lib/puppet/type/host.rb b/lib/puppet/type/host.rb index 53365bf40..763841fd8 100755 --- a/lib/puppet/type/host.rb +++ b/lib/puppet/type/host.rb @@ -1,104 +1,104 @@ module Puppet newtype(:host) do ensurable newproperty(:ip) do desc "The host's IP address, IPv4 or IPv6." validate do |value| unless value =~ /((([0-9a-fA-F]+:){7}[0-9a-fA-F]+)|(([0-9a-fA-F]+:)*[0-9a-fA-F]+)?::(([0-9a-fA-F]+:)*[0-9a-fA-F]+)?)|((25[0-5]|2[0-4][\d]|[0-1]?\d?\d)(\.(25[0-5]|2[0-4]\d|[0-1]?\d?\d)){3})/ raise Puppet::Error, "Invalid IP address" end end end newproperty(:alias) do desc "Any alias the host might have. Multiple values must be specified as an array. Note that this state has the same name as one of the metaparams; using this state to set aliases will make those aliases available in your Puppet scripts and also on disk." def insync?(is) is == @should end def is_to_s(currentvalue = @is) currentvalue = [currentvalue] unless currentvalue.is_a? Array currentvalue.join(" ") end def retrieve is = super case is when String is = is.split(/\s*,\s*/) - when Symbol: + when Symbol is = [is] when Array # nothing else raise Puppet::DevError, "Invalid @is type %s" % is.class end return is end # We actually want to return the whole array here, not just the first # value. def should if defined? @should if @should == [:absent] return :absent else return @should end else return nil end end def should_to_s(newvalue = @should) newvalue.join(" ") end validate do |value| if value =~ /\s/ raise Puppet::Error, "Aliases cannot include whitespace" end end end newproperty(:target) do desc "The file in which to store service information. Only used by those providers that write to disk (i.e., not NetInfo)." defaultto { if @resource.class.defaultprovider.ancestors.include?(Puppet::Provider::ParsedFile) @resource.class.defaultprovider.default_target else nil end } end newparam(:name) do desc "The host name." isnamevar validate do |value| # LAK:NOTE See http://snurl.com/21zf8 [groups_google_com] x = value.split('.').each do |hostpart| unless hostpart =~ /^([\d\w]+|[\d\w][\d\w\-]+[\d\w])$/ raise Puppet::Error, "Invalid host name" end end end end @doc = "Installs and manages host entries. For most systems, these entries will just be in ``/etc/hosts``, but some systems (notably OS X) will have different solutions." end end diff --git a/lib/puppet/type/macauthorization.rb b/lib/puppet/type/macauthorization.rb index 0265242dc..800d58d1c 100644 --- a/lib/puppet/type/macauthorization.rb +++ b/lib/puppet/type/macauthorization.rb @@ -1,141 +1,141 @@ Puppet::Type.newtype(:macauthorization) do @doc = "Manage the Mac OS X authorization database. See: http://developer.apple.com/documentation/Security/Conceptual/Security_Overview/Security_Services/chapter_4_section_5.html for more information." ensurable autorequire(:file) do ["/etc/authorization"] end def munge_boolean(value) case value - when true, "true", :true: + when true, "true", :true :true when false, "false", :false :false else raise Puppet::Error("munge_boolean only takes booleans") end end newparam(:name) do desc "The name of the right or rule to be managed. Corresponds to 'key' in Authorization Services. The key is the name of a rule. A key uses the same naming conventions as a right. The Security Server uses a rule’s key to match the rule with a right. Wildcard keys end with a ‘.’. The generic rule has an empty key value. Any rights that do not match a specific rule use the generic rule." isnamevar end newproperty(:auth_type) do desc "type - can be a 'right' or a 'rule'. 'comment' has not yet been implemented." newvalue(:right) newvalue(:rule) # newvalue(:comment) # not yet implemented. end newproperty(:allow_root, :boolean => true) do desc "Corresponds to 'allow-root' in the authorization store, renamed due to hyphens being problematic. Specifies whether a right should be allowed automatically if the requesting process is running with uid == 0. AuthorizationServices defaults this attribute to false if not specified" newvalue(:true) newvalue(:false) munge do |value| @resource.munge_boolean(value) end end newproperty(:authenticate_user, :boolean => true) do desc "Corresponds to 'authenticate-user' in the authorization store, renamed due to hyphens being problematic." newvalue(:true) newvalue(:false) munge do |value| @resource.munge_boolean(value) end end newproperty(:auth_class) do desc "Corresponds to 'class' in the authorization store, renamed due to 'class' being a reserved word." newvalue(:user) newvalue(:'evaluate-mechanisms') end newproperty(:comment) do desc "The 'comment' attribute for authorization resources." end newproperty(:group) do desc "The user must authenticate as a member of this group. This attribute can be set to any one group." end newproperty(:k_of_n) do desc "k-of-n. Built-in rights only show a value of '1' or absent, other values may be acceptable. Undocumented." end newproperty(:mechanisms, :array_matching => :all) do desc "an array of suitable mechanisms." end newproperty(:rule, :array_match => :all) do desc "The rule(s) that this right refers to." end newproperty(:session_owner, :boolean => true) do desc "Corresponds to 'session-owner' in the authorization store, renamed due to hyphens being problematic. Whether the session owner automatically matches this rule or right." newvalue(:true) newvalue(:false) munge do |value| @resource.munge_boolean(value) end end newproperty(:shared, :boolean => true) do desc "If this is set to true, then the Security Server marks the credentials used to gain this right as shared. The Security Server may use any shared credentials to authorize this right. For maximum security, set sharing to false so credentials stored by the Security Server for one application may not be used by another application." newvalue(:true) newvalue(:false) munge do |value| @resource.munge_boolean(value) end end newproperty(:timeout) do desc "The credential used by this rule expires in the specified number of seconds. For maximum security where the user must authenticate every time, set the timeout to 0. For minimum security, remove the timeout attribute so the user authenticates only once per session." end newproperty(:tries) do desc "The number of tries allowed." end end diff --git a/lib/puppet/type/mount.rb b/lib/puppet/type/mount.rb index 4eb149488..bb488c275 100755 --- a/lib/puppet/type/mount.rb +++ b/lib/puppet/type/mount.rb @@ -1,212 +1,212 @@ module Puppet # We want the mount to refresh when it changes. newtype(:mount, :self_refresh => true) do @doc = "Manages mounted filesystems, including putting mount information into the mount table. The actual behavior depends on the value of the 'ensure' parameter. Note that if a ``mount`` receives an event from another resource, it will try to remount the filesystems if ``ensure`` is set to ``mounted``." feature :refreshable, "The provider can remount the filesystem.", :methods => [:remount] # Use the normal parent class, because we actually want to # call code when sync() is called. newproperty(:ensure) do desc "Control what to do with this mount. Set this attribute to ``present`` to make sure the filesystem is in the filesystem table but not mounted (if the filesystem is currently mounted, it will be unmounted). Set it to ``absent`` to unmount (if necessary) and remove the filesystem from the fstab. Set to ``mounted`` to add it to the fstab and mount it." newvalue(:present) do if provider.mounted? syncothers() provider.unmount return :mount_unmounted else provider.create return :mount_created end end aliasvalue :unmounted, :present newvalue(:absent, :event => :mount_deleted) do if provider.mounted? provider.unmount end provider.destroy end newvalue(:mounted, :event => :mount_mounted) do # Create the mount point if it does not already exist. current_value = self.retrieve provider.create if current_value.nil? or current_value == :absent syncothers() # The fs can be already mounted if it was absent but mounted provider.mount unless provider.mounted? end def retrieve # We need to special case :mounted; if we're absent, we still # want curval = super() if curval == :absent return curval elsif provider.mounted? return :mounted else return curval end end def syncothers # We have to flush any changes to disk. currentvalues = @resource.retrieve # Determine if there are any out-of-sync properties. oos = @resource.send(:properties).find_all do |prop| unless currentvalues.include?(prop) raise Puppet::DevError, "Parent has property %s but it doesn't appear in the current values", [prop.name] end if prop.name == :ensure false else ! prop.insync?(currentvalues[prop]) end end.each { |prop| prop.sync }.length if oos > 0 @resource.flush end end end newproperty(:device) do desc "The device providing the mount. This can be whatever device is supporting by the mount, including network devices or devices specified by UUID rather than device path, depending on the operating system." end # Solaris specifies two devices, not just one. newproperty(:blockdevice) do desc "The device to fsck. This is property is only valid on Solaris, and in most cases will default to the correct value." # Default to the device but with "dsk" replaced with "rdsk". defaultto do if Facter["operatingsystem"].value == "Solaris" device = @resource.value(:device) if device =~ %r{/dsk/} device.sub(%r{/dsk/}, "/rdsk/") else nil end else nil end end end newproperty(:fstype) do desc "The mount type. Valid values depend on the operating system." end newproperty(:options) do desc "Mount options for the mounts, as they would appear in the fstab." end newproperty(:pass) do desc "The pass in which the mount is checked." defaultto { if @resource.managed? 0 end } end newproperty(:atboot) do desc "Whether to mount the mount at boot. Not all platforms support this." end newproperty(:dump) do desc "Whether to dump the mount. Not all platforms support this. Valid values are ``1`` or ``0``. Default is ``0``." newvalue(%r{(0|1)}) defaultto { if @resource.managed? 0 end } end newproperty(:target) do desc "The file in which to store the mount table. Only used by those providers that write to disk (i.e., not NetInfo)." defaultto { if @resource.class.defaultprovider.ancestors.include?(Puppet::Provider::ParsedFile) @resource.class.defaultprovider.default_target else nil end } end newparam(:name) do desc "The mount path for the mount." isnamevar end newparam(:path) do desc "The deprecated name for the mount point. Please use ``name`` now." def value=(value) warning "'path' is deprecated for mounts. Please use 'name'." @resource[:name] = value super end end newparam(:remounts) do desc "Whether the mount can be remounted ``mount -o remount``. If this is false, then the filesystem will be unmounted and remounted manually, which is prone to failure." newvalues(:true, :false) defaultto do case Facter.value(:operatingsystem) - when "FreeBSD": false + when "FreeBSD"; false else true end end end def refresh # Only remount if we're supposed to be mounted. provider.remount if self.should(:fstype) != "swap" and provider.mounted? end def value(name) name = symbolize(name) ret = nil if property = @parameters[name] return property.value end end end end diff --git a/lib/puppet/type/notify.rb b/lib/puppet/type/notify.rb index 548cf760c..15911d441 100644 --- a/lib/puppet/type/notify.rb +++ b/lib/puppet/type/notify.rb @@ -1,45 +1,45 @@ # # Simple module for logging messages on the client-side # module Puppet newtype(:notify) do @doc = "Sends an arbitrary message to the puppetd run-time log." newproperty(:message) do desc "The message to be sent to the log." def sync case @resource["withpath"] - when :true: + when :true send(@resource[:loglevel], self.should) else Puppet.send(@resource[:loglevel], self.should) end return end def retrieve return end def insync?(is) false end defaultto { @resource[:name] } end newparam(:withpath) do desc "Whether to not to show the full object path." defaultto :false newvalues(:true, :false) end newparam(:name) do desc "An arbitrary tag for your own reference; the name of the message." isnamevar end end end diff --git a/lib/puppet/type/package.rb b/lib/puppet/type/package.rb index 9ed1bf1c8..655f9e06d 100644 --- a/lib/puppet/type/package.rb +++ b/lib/puppet/type/package.rb @@ -1,320 +1,320 @@ # Define the different packaging systems. Each package system is implemented # in a module, which then gets used to individually extend each package object. # This allows packages to exist on the same machine using different packaging # systems. module Puppet newtype(:package) do @doc = "Manage packages. There is a basic dichotomy in package support right now: Some package types (e.g., yum and apt) can retrieve their own package files, while others (e.g., rpm and sun) cannot. For those package formats that cannot retrieve their own files, you can use the ``source`` parameter to point to the correct file. Puppet will automatically guess the packaging format that you are using based on the platform you are on, but you can override it using the ``provider`` parameter; each provider defines what it requires in order to function, and you must meet those requirements to use a given provider." feature :installable, "The provider can install packages.", :methods => [:install] feature :uninstallable, "The provider can uninstall packages.", :methods => [:uninstall] feature :upgradeable, "The provider can upgrade to the latest version of a package. This feature is used by specifying ``latest`` as the desired value for the package.", :methods => [:update, :latest] feature :purgeable, "The provider can purge packages. This generally means that all traces of the package are removed, including existing configuration files. This feature is thus destructive and should be used with the utmost care.", :methods => [:purge] feature :versionable, "The provider is capable of interrogating the package database for installed version(s), and can select which out of a set of available versions of a package to install if asked." ensurable do desc "What state the package should be in. *latest* only makes sense for those packaging formats that can retrieve new packages on their own and will throw an error on those that cannot. For those packaging systems that allow you to specify package versions, specify them here. Similarly, *purged* is only useful for packaging systems that support the notion of managing configuration files separately from 'normal' system files." attr_accessor :latest newvalue(:present, :event => :package_installed) do provider.install end newvalue(:absent, :event => :package_removed) do provider.uninstall end newvalue(:purged, :event => :package_purged, :required_features => :purgeable) do provider.purge end # Alias the 'present' value. aliasvalue(:installed, :present) newvalue(:latest, :required_features => :upgradeable) do # Because yum always exits with a 0 exit code, there's a retrieve # in the "install" method. So, check the current state now, # to compare against later. current = self.retrieve begin provider.update rescue => detail self.fail "Could not update: %s" % detail end if current == :absent :package_installed else :package_changed end end newvalue(/./, :required_features => :versionable) do begin provider.install rescue => detail self.fail "Could not update: %s" % detail end if self.retrieve == :absent :package_installed else :package_changed end end defaultto :installed # Override the parent method, because we've got all kinds of # funky definitions of 'in sync'. def insync?(is) @should ||= [] @latest = nil unless defined? @latest @lateststamp ||= (Time.now.to_i - 1000) # Iterate across all of the should values, and see how they # turn out. @should.each { |should| case should when :present return true unless [:absent, :purged].include?(is) when :latest # Short-circuit packages that are not present return false if is == :absent or is == :purged # Don't run 'latest' more than about every 5 minutes if @latest and ((Time.now.to_i - @lateststamp) / 60) < 5 #self.debug "Skipping latest check" else begin @latest = provider.latest @lateststamp = Time.now.to_i rescue => detail error = Puppet::Error.new("Could not get latest version: %s" % detail.to_s) error.set_backtrace(detail.backtrace) raise error end end case is - when @latest: + when @latest return true - when :present: + when :present # This will only happen on retarded packaging systems # that can't query versions. return true else self.debug "%s %s is installed, latest is %s" % [@resource.name, is.inspect, @latest.inspect] end when :absent return true if is == :absent or is == :purged when :purged return true if is == :purged when is return true end } return false end # This retrieves the current state. LAK: I think this method is unused. def retrieve return provider.properties[:ensure] end # Provide a bit more information when logging upgrades. def should_to_s(newvalue = @should) if @latest @latest.to_s else super(newvalue) end end end newparam(:name) do desc "The package name. This is the name that the packaging system uses internally, which is sometimes (especially on Solaris) a name that is basically useless to humans. If you want to abstract package installation, then you can use aliases to provide a common name to packages:: # In the 'openssl' class $ssl = $operatingsystem ? { solaris => SMCossl, default => openssl } # It is not an error to set an alias to the same value as the # object name. package { $ssl: ensure => installed, alias => openssl } . etc. . $ssh = $operatingsystem ? { solaris => SMCossh, default => openssh } # Use the alias to specify a dependency, rather than # having another selector to figure it out again. package { $ssh: ensure => installed, alias => openssh, require => Package[openssl] } " isnamevar end newparam(:source) do desc "Where to find the actual package. This must be a local file (or on a network file system) or a URL that your specific packaging type understands; Puppet will not retrieve files for you." end newparam(:instance) do desc "A read-only parameter set by the package." end newparam(:status) do desc "A read-only parameter set by the package." end newparam(:type) do desc "Deprecated form of ``provider``." munge do |value| warning "'type' is deprecated; use 'provider' instead" @resource[:provider] = value @resource[:provider] end end newparam(:adminfile) do desc "A file containing package defaults for installing packages. This is currently only used on Solaris. The value will be validated according to system rules, which in the case of Solaris means that it should either be a fully qualified path or it should be in /var/sadm/install/admin." end newparam(:responsefile) do desc "A file containing any necessary answers to questions asked by the package. This is currently used on Solaris and Debian. The value will be validated according to system rules, but it should generally be a fully qualified path." end newparam(:configfiles) do desc "Whether configfiles should be kept or replaced. Most packages types do not support this parameter." defaultto :keep newvalues(:keep, :replace) end newparam(:category) do desc "A read-only parameter set by the package." end newparam(:platform) do desc "A read-only parameter set by the package." end newparam(:root) do desc "A read-only parameter set by the package." end newparam(:vendor) do desc "A read-only parameter set by the package." end newparam(:description) do desc "A read-only parameter set by the package." end newparam(:allowcdrom) do desc "Tells apt to allow cdrom sources in the sources.list file. Normally apt will bail if you try this." newvalues(:true, :false) end autorequire(:file) do autos = [] [:responsefile, :adminfile].each { |param| if val = self[param] autos << val end } if source = self[:source] if source =~ /^#{File::SEPARATOR}/ autos << source end end autos end # This only exists for testing. def clear if obj = @parameters[:ensure] obj.latest = nil end end # The 'query' method returns a hash of info if the package # exists and returns nil if it does not. def exists? @provider.get(:ensure) != :absent end def retrieve @provider.properties.inject({}) do |props, ary| name, value = ary if prop = @parameters[name] props[prop] = value end props end end end # Puppet::Type.type(:package) end diff --git a/lib/puppet/type/resources.rb b/lib/puppet/type/resources.rb index c0d892bb8..d316b4b13 100644 --- a/lib/puppet/type/resources.rb +++ b/lib/puppet/type/resources.rb @@ -1,151 +1,151 @@ # Created by Luke Kanies on 2006-12-12. # Copyright (c) 2006. All rights reserved. require 'puppet' Puppet::Type.newtype(:resources) do @doc = "This is a metatype that can manage other resource types. Any metaparams specified here will be passed on to any generated resources, so you can purge umanaged resources but set ``noop`` to true so the purging is only logged and does not actually happen." newparam(:name) do desc "The name of the type to be managed." validate do |name| unless Puppet::Type.type(name) raise ArgumentError, "Could not find resource type '%s'" % name end end munge { |v| v.to_s } end newparam(:purge, :boolean => true) do desc "Purge unmanaged resources. This will delete any resource that is not specified in your configuration and is not required by any specified resources." newvalues(:true, :false) validate do |value| if [:true, true, "true"].include?(value) unless @resource.resource_type.respond_to?(:instances) raise ArgumentError, "Purging resources of type %s is not supported, since they cannot be queried from the system" % @resource[:name] end unless @resource.resource_type.validproperty?(:ensure) raise ArgumentError, "Purging is only supported on types that accept 'ensure'" end end end end newparam(:unless_system_user) do desc "This keeps system users from being purged. By default, it does not purge users whose UIDs are less than or equal to 500, but you can specify a different UID as the inclusive limit." newvalues(:true, :false, /^\d+$/) munge do |value| case value when /^\d+/ Integer(value) when :true, true 500 when :false, false false - when Integer: value + when Integer; value else raise ArgumentError, "Invalid value %s" % value.inspect end end defaultto { if @resource[:name] == "user" 500 else nil end } end def check(resource) unless defined? @checkmethod @checkmethod = "%s_check" % self[:name] end unless defined? @hascheck @hascheck = respond_to?(@checkmethod) end if @hascheck return send(@checkmethod, resource) else return true end end # Generate any new resources we need to manage. This is pretty hackish # right now, because it only supports purging. def generate return [] unless self.purge? hascheck = false method = resource_type.instances.find_all do |resource| ! resource.managed? end.find_all do |resource| check(resource) end.each do |resource| begin resource[:ensure] = :absent rescue ArgumentError, Puppet::Error => detail err "The 'ensure' attribute on %s resources does not accept 'absent' as a value" % [self[:name]] return [] end @parameters.each do |name, param| next unless param.metaparam? resource[name] = param.value end # Mark that we're purging, so transactions can handle relationships # correctly resource.purging end end def resource_type unless defined? @resource_type unless type = Puppet::Type.type(self[:name]) raise Puppet::DevError, "Could not find resource type" end @resource_type = type end @resource_type end # Make sure we don't purge users below a certain uid, if the check # is enabled. def user_check(resource) return true unless self[:name] == "user" return true unless self[:unless_system_user] resource[:check] = :uid current_values = resource.retrieve if system_users().include?(resource[:name]) return false end if current_values[resource.property(:uid)] <= self[:unless_system_user] return false else return true end end def system_users %w{root nobody bin noaccess daemon sys} end end diff --git a/lib/puppet/type/tidy.rb b/lib/puppet/type/tidy.rb index 98d69bc3a..eebf333cd 100755 --- a/lib/puppet/type/tidy.rb +++ b/lib/puppet/type/tidy.rb @@ -1,327 +1,327 @@ Puppet::Type.newtype(:tidy) do require 'puppet/file_serving/fileset' @doc = "Remove unwanted files based on specific criteria. Multiple criteria are OR'd together, so a file that is too large but is not old enough will still get tidied. If you don't specify either 'age' or 'size', then all files will be removed. This resource type works by generating a file resource for every file that should be deleted and then letting that resource perform the actual deletion. " newparam(:path) do desc "The path to the file or directory to manage. Must be fully qualified." isnamevar end newparam(:matches) do desc "One or more (shell type) file glob patterns, which restrict the list of files to be tidied to those whose basenames match at least one of the patterns specified. Multiple patterns can be specified using an array. tidy { \"/tmp\": age => \"1w\", recurse => false, matches => [ \"[0-9]pub*.tmp\", \"*.temp\", \"tmpfile?\" ] } The example above removes files from \/tmp if they are one week old or older, are not in a subdirectory and match one of the shell globs given. Note that the patterns are matched against the basename of each file -- that is, your glob patterns should not have any '/' characters in them, since you are only specifying against the last bit of the file." # Make sure we convert to an array. munge do |value| value = [value] unless value.is_a?(Array) value end # Does a given path match our glob patterns, if any? Return true # if no patterns have been provided. def tidy?(path, stat) basename = File.basename(path) flags = File::FNM_DOTMATCH | File::FNM_PATHNAME return true if value.find {|pattern| File.fnmatch(pattern, basename, flags) } return false end end newparam(:backup) do desc "Whether tidied files should be backed up. Any values are passed directly to the file resources used for actual file deletion, so use its backup documentation to determine valid values." end newparam(:age) do desc "Tidy files whose age is equal to or greater than the specified time. You can choose seconds, minutes, hours, days, or weeks by specifying the first letter of any of those words (e.g., '1w'). Specifying 0 will remove all files." @@ageconvertors = { :s => 1, :m => 60 } @@ageconvertors[:h] = @@ageconvertors[:m] * 60 @@ageconvertors[:d] = @@ageconvertors[:h] * 24 @@ageconvertors[:w] = @@ageconvertors[:d] * 7 def convert(unit, multi) if num = @@ageconvertors[unit] return num * multi else self.fail "Invalid age unit '%s'" % unit end end def tidy?(path, stat) # If the file's older than we allow, we should get rid of it. if (Time.now.to_i - stat.send(resource[:type]).to_i) > value return true else return false end end munge do |age| unit = multi = nil case age - when /^([0-9]+)(\w)\w*$/: + when /^([0-9]+)(\w)\w*$/ multi = Integer($1) unit = $2.downcase.intern - when /^([0-9]+)$/: + when /^([0-9]+)$/ multi = Integer($1) unit = :d else self.fail "Invalid tidy age %s" % age end convert(unit, multi) end end newparam(:size) do desc "Tidy files whose size is equal to or greater than the specified size. Unqualified values are in kilobytes, but *b*, *k*, and *m* can be appended to specify *bytes*, *kilobytes*, and *megabytes*, respectively. Only the first character is significant, so the full word can also be used." @@sizeconvertors = { :b => 0, :k => 1, :m => 2, :g => 3 } def convert(unit, multi) if num = @@sizeconvertors[unit] result = multi num.times do result *= 1024 end return result else self.fail "Invalid size unit '%s'" % unit end end def tidy?(path, stat) if stat.size > value return true else return false end end munge do |size| case size - when /^([0-9]+)(\w)\w*$/: + when /^([0-9]+)(\w)\w*$/ multi = Integer($1) unit = $2.downcase.intern - when /^([0-9]+)$/: + when /^([0-9]+)$/ multi = Integer($1) unit = :k else self.fail "Invalid tidy size %s" % age end convert(unit, multi) end end newparam(:type) do desc "Set the mechanism for determining age." newvalues(:atime, :mtime, :ctime) defaultto :atime end newparam(:recurse) do desc "If target is a directory, recursively descend into the directory looking for files to tidy." newvalues(:true, :false, :inf, /^[0-9]+$/) # Replace the validation so that we allow numbers in # addition to string representations of them. validate { |arg| } munge do |value| newval = super(value) case newval - when :true, :inf: true - when :false: false - when Integer, Fixnum, Bignum: value - when /^\d+$/: Integer(value) + when :true, :inf; true + when :false; false + when Integer, Fixnum, Bignum; value + when /^\d+$/; Integer(value) else raise ArgumentError, "Invalid recurse value %s" % value.inspect end end end newparam(:rmdirs, :boolean => true) do desc "Tidy directories in addition to files; that is, remove directories whose age is older than the specified criteria. This will only remove empty directories, so all contained files must also be tidied before a directory gets removed." newvalues :true, :false end # Erase PFile's validate method validate do end def self.instances [] end @depthfirst = true def initialize(hash) super # only allow backing up into filebuckets unless self[:backup].is_a? Puppet::Network::Client.dipper self[:backup] = false end end # Make a file resource to remove a given file. def mkfile(path) # Force deletion, so directories actually get deleted. Puppet::Type.type(:file).new :path => path, :backup => self[:backup], :ensure => :absent, :force => true end def retrieve # Our ensure property knows how to retrieve everything for us. if obj = @parameters[:ensure] return obj.retrieve else return {} end end # Hack things a bit so we only ever check the ensure property. def properties [] end def eval_generate [] end def generate return [] unless stat(self[:path]) if self[:recurse] files = Puppet::FileServing::Fileset.new(self[:path], :recurse => self[:recurse]).files.collect do |f| f == "." ? self[:path] : File.join(self[:path], f) end else files = [self[:path]] end result = files.find_all { |path| tidy?(path) }.collect { |path| mkfile(path) }.each { |file| notice "Tidying %s" % file.ref }.sort { |a,b| b[:path] <=> a[:path] } # No need to worry about relationships if we don't have rmdirs; there won't be # any directories. return result unless rmdirs? # Now make sure that all directories require the files they contain, if all are available, # so that a directory is emptied before we try to remove it. files_by_name = result.inject({}) { |hash, file| hash[file[:path]] = file; hash } files_by_name.keys.sort { |a,b| b <=> b }.each do |path| dir = File.dirname(path) next unless resource = files_by_name[dir] if resource[:require] resource[:require] << Puppet::Resource::Reference.new(:file, path) else resource[:require] = [Puppet::Resource::Reference.new(:file, path)] end end return result end # Does a given path match our glob patterns, if any? Return true # if no patterns have been provided. def matches?(path) return true unless self[:matches] basename = File.basename(path) flags = File::FNM_DOTMATCH | File::FNM_PATHNAME if self[:matches].find {|pattern| File.fnmatch(pattern, basename, flags) } return true else debug "No specified patterns match %s, not tidying" % path return false end end # Should we remove the specified file? def tidy?(path) return false unless stat = self.stat(path) return false if stat.ftype == "directory" and ! rmdirs? # The 'matches' parameter isn't OR'ed with the other tests -- # it's just used to reduce the list of files we can match. return false if param = parameter(:matches) and ! param.tidy?(path, stat) tested = false [:age, :size].each do |name| next unless param = parameter(name) tested = true return true if param.tidy?(path, stat) end # If they don't specify either, then the file should always be removed. return true unless tested return false end def stat(path) begin File.lstat(path) rescue Errno::ENOENT => error info "File does not exist" return nil rescue Errno::EACCES => error warning "Could not stat; permission denied" return nil end end end diff --git a/lib/puppet/type/user.rb b/lib/puppet/type/user.rb index 8efc2ef1c..bc7c243a1 100755 --- a/lib/puppet/type/user.rb +++ b/lib/puppet/type/user.rb @@ -1,391 +1,391 @@ require 'etc' require 'facter' require 'puppet/property/list' require 'puppet/property/ordered_list' require 'puppet/property/keyvalue' module Puppet newtype(:user) do @doc = "Manage users. This type is mostly built to manage system users, so it is lacking some features useful for managing normal users. This resource type uses the prescribed native tools for creating groups and generally uses POSIX APIs for retrieving information about them. It does not directly modify /etc/passwd or anything." feature :allows_duplicates, "The provider supports duplicate users with the same UID." feature :manages_homedir, "The provider can create and remove home directories." feature :manages_passwords, "The provider can modify user passwords, by accepting a password hash." feature :manages_solaris_rbac, "The provider can manage roles and normal users" newproperty(:ensure, :parent => Puppet::Property::Ensure) do newvalue(:present, :event => :user_created) do provider.create end newvalue(:absent, :event => :user_removed) do provider.delete end newvalue(:role, :event => :role_created, :required_features => :manages_solaris_rbac) do provider.create_role end desc "The basic state that the object should be in." # If they're talking about the thing at all, they generally want to # say it should exist. defaultto do if @resource.managed? :present else nil end end def retrieve if provider.exists? if provider.respond_to?(:is_role?) and provider.is_role? return :role else return :present end else return :absent end end end newproperty(:uid) do desc "The user ID. Must be specified numerically. For new users being created, if no user ID is specified then one will be chosen automatically, which will likely result in the same user having different IDs on different systems, which is not recommended. This is especially noteworthy if you use Puppet to manage the same user on both Darwin and other platforms, since Puppet does the ID generation for you on Darwin, but the tools do so on other platforms." munge do |value| case value when String if value =~ /^[-0-9]+$/ value = Integer(value) end end return value end end newproperty(:gid) do desc "The user's primary group. Can be specified numerically or by name." munge do |value| if value.is_a?(String) and value =~ /^[-0-9]+$/ Integer(value) else value end end def insync?(is) return true unless self.should # We know the 'is' is a number, so we need to convert the 'should' to a number, # too. @should.each do |value| return true if number = Puppet::Util.gid(value) and is == number end return false end def sync found = false @should.each do |value| if number = Puppet::Util.gid(value) provider.gid = number found = true break end end fail "Could not find group(s) %s" % @should.join(",") unless found # Use the default event. end end newproperty(:comment) do desc "A description of the user. Generally is a user's full name." end newproperty(:home) do desc "The home directory of the user. The directory must be created separately and is not currently checked for existence." end newproperty(:shell) do desc "The user's login shell. The shell must exist and be executable." end newproperty(:password, :required_features => :manages_passwords) do desc "The user's password, in whatever encrypted format the local machine requires. Be sure to enclose any value that includes a dollar sign ($) in single quotes (\')." validate do |value| raise ArgumentError, "Passwords cannot include ':'" if value.is_a?(String) and value.include?(":") end def change_to_s(currentvalue, newvalue) if currentvalue == :absent return "created password" else return "changed password" end end end newproperty(:groups, :parent => Puppet::Property::List) do desc "The groups of which the user is a member. The primary group should not be listed. Multiple groups should be specified as an array." validate do |value| if value =~ /^\d+$/ raise ArgumentError, "Group names must be provided, not numbers" end if value.include?(",") raise ArgumentError, "Group names must be provided as an array, not a comma-separated list" end end end newparam(:name) do desc "User name. While limitations are determined for each operating system, it is generally a good idea to keep to the degenerate 8 characters, beginning with a letter." isnamevar end newparam(:membership) do desc "Whether specified groups should be treated as the only groups of which the user is a member or whether they should merely be treated as the minimum membership list." newvalues(:inclusive, :minimum) defaultto :minimum end newparam(:allowdupe, :boolean => true) do desc "Whether to allow duplicate UIDs." newvalues(:true, :false) defaultto false end newparam(:managehome, :boolean => true) do desc "Whether to manage the home directory when managing the user." newvalues(:true, :false) defaultto false validate do |val| if val.to_s == "true" unless provider.class.manages_homedir? raise ArgumentError, "User provider %s can not manage home directories" % provider.class.name end end end end # Autorequire the group, if it's around autorequire(:group) do autos = [] if obj = @parameters[:gid] and groups = obj.shouldorig groups = groups.collect { |group| if group =~ /^\d+$/ Integer(group) else group end } groups.each { |group| case group - when Integer: + when Integer if resource = catalog.resources.find { |r| r.is_a?(Puppet::Type.type(:group)) and r.should(:gid) == group } autos << resource end else autos << group end } end if obj = @parameters[:groups] and groups = obj.should autos += groups.split(",") end autos end def retrieve absent = false properties().inject({}) { |prophash, property| current_value = :absent if absent prophash[property] = :absent else current_value = property.retrieve prophash[property] = current_value end if property.name == :ensure and current_value == :absent absent = true end prophash } end newproperty(:roles, :parent => Puppet::Property::List, :required_features => :manages_solaris_rbac) do desc "The roles the user has. Multiple roles should be specified as an array." def membership :role_membership end validate do |value| if value =~ /^\d+$/ raise ArgumentError, "Role names must be provided, not numbers" end if value.include?(",") raise ArgumentError, "Role names must be provided as an array, not a comma-separated list" end end end #autorequire the roles that the user has autorequire(:user) do reqs = [] if roles_property = @parameters[:roles] and roles = roles_property.should reqs += roles.split(',') end reqs end newparam(:role_membership) do desc "Whether specified roles should be treated as the only roles of which the user is a member or whether they should merely be treated as the minimum membership list." newvalues(:inclusive, :minimum) defaultto :minimum end newproperty(:auths, :parent => Puppet::Property::List, :required_features => :manages_solaris_rbac) do desc "The auths the user has. Multiple auths should be specified as an array." def membership :auth_membership end validate do |value| if value =~ /^\d+$/ raise ArgumentError, "Auth names must be provided, not numbers" end if value.include?(",") raise ArgumentError, "Auth names must be provided as an array, not a comma-separated list" end end end newparam(:auth_membership) do desc "Whether specified auths should be treated as the only auths of which the user is a member or whether they should merely be treated as the minimum membership list." newvalues(:inclusive, :minimum) defaultto :minimum end newproperty(:profiles, :parent => Puppet::Property::OrderedList, :required_features => :manages_solaris_rbac) do desc "The profiles the user has. Multiple profiles should be specified as an array." def membership :profile_membership end validate do |value| if value =~ /^\d+$/ raise ArgumentError, "Profile names must be provided, not numbers" end if value.include?(",") raise ArgumentError, "Profile names must be provided as an array, not a comma-separated list" end end end newparam(:profile_membership) do desc "Whether specified roles should be treated as the only roles of which the user is a member or whether they should merely be treated as the minimum membership list." newvalues(:inclusive, :minimum) defaultto :minimum end newproperty(:keys, :parent => Puppet::Property::KeyValue, :required_features => :manages_solaris_rbac) do desc "Specify user attributes in an array of keyvalue pairs" def membership :key_membership end validate do |value| unless value.include?("=") raise ArgumentError, "key value pairs must be seperated by an =" end end end newparam(:key_membership) do desc "Whether specified key value pairs should be treated as the only attributes of the user or whether they should merely be treated as the minimum list." newvalues(:inclusive, :minimum) defaultto :minimum end newproperty(:project, :required_features => :manages_solaris_rbac) do desc "The name of the project associated with a user" end end end diff --git a/lib/puppet/type/zone.rb b/lib/puppet/type/zone.rb index 6e5d784b3..c286ececd 100644 --- a/lib/puppet/type/zone.rb +++ b/lib/puppet/type/zone.rb @@ -1,415 +1,415 @@ Puppet::Type.newtype(:zone) do @doc = "Solaris zones." # These properties modify the zone configuration, and they need to provide # the text separately from syncing it, so all config statements can be rolled # into a single creation statement. class ZoneConfigProperty < Puppet::Property # Perform the config operation. def sync provider.setconfig self.configtext end end # Those properties that can have multiple instances. class ZoneMultiConfigProperty < ZoneConfigProperty def configtext list = @should current_value = self.retrieve unless current_value.is_a? Symbol if current_value.is_a? Array list += current_value else if current_value list << current_value end end end # Some hackery so we can test whether current_value is an array or a symbol if current_value.is_a? Array tmpis = current_value else if current_value tmpis = [current_value] else tmpis = [] end end rms = [] adds = [] # Collect the modifications to make list.sort.uniq.collect do |obj| # Skip objectories that are configured and should be next if tmpis.include?(obj) and @should.include?(obj) if tmpis.include?(obj) rms << obj else adds << obj end end # And then perform all of the removals before any of the adds. (rms.collect { |o| rm(o) } + adds.collect { |o| add(o) }).join("\n") end # We want all specified directories to be included. def insync?(current_value) if current_value.is_a? Array and @should.is_a? Array current_value.sort == @should.sort else current_value == @should end end end ensurable do desc "The running state of the zone. The valid states directly reflect the states that ``zoneadm`` provides. The states are linear, in that a zone must be ``configured`` then ``installed``, and only then can be ``running``. Note also that ``halt`` is currently used to stop zones." @states = {} @parametervalues = [] def self.alias_state(values) @state_aliases ||= {} values.each do |nick, name| @state_aliases[nick] = name end end def self.newvalue(name, hash) if @parametervalues.is_a? Hash @parametervalues = [] end @parametervalues << name @states[name] = hash hash[:name] = name end def self.state_name(name) if other = @state_aliases[name] other else name end end newvalue :absent, :down => :destroy newvalue :configured, :up => :configure, :down => :uninstall newvalue :installed, :up => :install, :down => :stop newvalue :running, :up => :start alias_state :incomplete => :installed, :ready => :installed, :shutting_down => :running defaultto :running def self.state_index(value) @parametervalues.index(state_name(value)) end # Return all of the states between two listed values, exclusive # of the first item. def self.state_sequence(first, second) findex = sindex = nil unless findex = @parametervalues.index(state_name(first)) raise ArgumentError, "'%s' is not a valid zone state" % first end unless sindex = @parametervalues.index(state_name(second)) raise ArgumentError, "'%s' is not a valid zone state" % first end list = nil # Apparently ranges are unidirectional, so we have to reverse # the range op twice. if findex > sindex list = @parametervalues[sindex..findex].collect do |name| @states[name] end.reverse else list = @parametervalues[findex..sindex].collect do |name| @states[name] end end # The first result is the current state, so don't return it. list[1..-1] end def retrieve provider.properties[:ensure] end def sync method = nil if up? direction = :up else direction = :down end # We need to get the state we're currently in and just call # everything between it and us. self.class.state_sequence(self.retrieve, self.should).each do |state| if method = state[direction] warned = false while provider.processing? unless warned info "Waiting for zone to finish processing" warned = true end sleep 1 end provider.send(method) else raise Puppet::DevError, "Cannot move %s from %s" % [direction, st[:name]] end end return ("zone_" + self.should.to_s).intern end # Are we moving up the property tree? def up? current_value = self.retrieve self.class.state_index(current_value) < self.class.state_index(self.should) end end newparam(:name) do desc "The name of the zone." isnamevar end newparam(:id) do desc "The numerical ID of the zone. This number is autogenerated and cannot be changed." end newproperty(:ip, :parent => ZoneMultiConfigProperty) do require 'ipaddr' desc "The IP address of the zone. IP addresses must be specified with the interface, separated by a colon, e.g.: bge0:192.168.0.1. For multiple interfaces, specify them in an array." validate do |value| unless value =~ /:/ raise ArgumentError, "IP addresses must specify the interface and the address, separated by a colon." end interface, address = value.split(':') begin IPAddr.new(address) rescue ArgumentError raise ArgumentError, "'%s' is an invalid IP address" % address end end # Add an interface. def add(str) interface, ip = ipsplit(str) "add net set address=#{ip} set physical=#{interface} end " end # Convert a string into the component interface and address def ipsplit(str) interface, address = str.split(':') return interface, address end # Remove an interface. def rm(str) interface, ip = ipsplit(str) # Reality seems to disagree with the documentation here; the docs # specify that braces are required, but they're apparently only # required if you're specifying multiple values. "remove net address=#{ip}" end end newproperty(:autoboot, :parent => ZoneConfigProperty) do desc "Whether the zone should automatically boot." defaultto true newvalue(:true) {} newvalue(:false) {} def configtext "set autoboot=#{self.should}" end end newproperty(:pool, :parent => ZoneConfigProperty) do desc "The resource pool for this zone." def configtext "set pool=#{self.should}" end end newproperty(:shares, :parent => ZoneConfigProperty) do desc "Number of FSS CPU shares allocated to the zone." def configtext "add rctl\nset name=zone.cpu-shares\nadd value (priv=privileged,limit=#{self.should},action=none)\nend" end end newproperty(:inherit, :parent => ZoneMultiConfigProperty) do desc "The list of directories that the zone inherits from the global zone. All directories must be fully qualified." validate do |value| unless value =~ /^\// raise ArgumentError, "Inherited filesystems must be fully qualified" end end # Add a directory to our list of inherited directories. def add(dir) "add inherit-pkg-dir\nset dir=#{dir}\nend" end def rm(dir) # Reality seems to disagree with the documentation here; the docs # specify that braces are required, but they're apparently only # required if you're specifying multiple values. "remove inherit-pkg-dir dir=#{dir}" end def should @should end end # Specify the sysidcfg file. This is pretty hackish, because it's # only used to boot the zone the very first time. newparam(:sysidcfg) do desc %{The text to go into the sysidcfg file when the zone is first booted. The best way is to use a template:: # $templatedir/sysidcfg system_locale=en_US timezone=GMT terminal=xterms security_policy=NONE root_password=<%= password %> timeserver=localhost name_service=DNS {domain_name=<%= domain %> name_server=<%= nameserver %>} network_interface=primary {hostname=<%= realhostname %> ip_address=<%= ip %> netmask=<%= netmask %> protocol_ipv6=no default_route=<%= defaultroute %>} nfs4_domain=dynamic And then call that:: zone { myzone: ip => "bge0:192.168.0.23", sysidcfg => template(sysidcfg), path => "/opt/zones/myzone", realhostname => "fully.qualified.domain.name" } The sysidcfg only matters on the first booting of the zone, so Puppet only checks for it at that time.} end newparam(:path) do desc "The root of the zone's filesystem. Must be a fully qualified file name. If you include '%s' in the path, then it will be replaced with the zone's name. At this point, you cannot use Puppet to move a zone." validate do |value| unless value =~ /^\// raise ArgumentError, "The zone base must be fully qualified" end end munge do |value| if value =~ /%s/ value % @resource[:name] else value end end end newparam(:create_args) do desc "Arguments to the zonecfg create command. This can be used to create branded zones." end newparam(:install_args) do desc "Arguments to the zoneadm install command. This can be used to create branded zones." end newparam(:realhostname) do desc "The actual hostname of the zone." end # If Puppet is also managing the base dir or its parent dir, list them # both as prerequisites. autorequire(:file) do if @parameters.include? :path [@parameters[:path].value, File.dirname(@parameters[:path].value)] else nil end end def retrieve provider.flush if hash = provider.properties() and hash[:ensure] != :absent result = setstatus(hash) result else # Return all properties as absent. return properties().inject({}) do | prophash, property| prophash[property] = :absent prophash end end end # Take the results of a listing and set everything appropriately. def setstatus(hash) prophash = {} hash.each do |param, value| next if param == :name case self.class.attrtype(param) - when :property: + when :property # Only try to provide values for the properties we're managing if prop = self.property(param) prophash[prop] = value end else self[param] = value end end return prophash end end diff --git a/lib/puppet/util/fileparsing.rb b/lib/puppet/util/fileparsing.rb index 23d02ea60..dca0d51c0 100644 --- a/lib/puppet/util/fileparsing.rb +++ b/lib/puppet/util/fileparsing.rb @@ -1,399 +1,399 @@ # A mini-language for parsing files. This is only used file the ParsedFile # provider, but it makes more sense to split it out so it's easy to maintain # in one place. # # You can use this module to create simple parser/generator classes. For instance, # the following parser should go most of the way to parsing /etc/passwd: # # class Parser # include Puppet::Util::FileParsing # record_line :user, :fields => %w{name password uid gid gecos home shell}, # :separator => ":" # end # # You would use it like this: # # parser = Parser.new # lines = parser.parse(File.read("/etc/passwd")) # # lines.each do |type, hash| # type will always be :user, since we only have one # p hash # end # # Each line in this case would be a hash, with each field set appropriately. # You could then call 'parser.to_line(hash)' on any of those hashes to generate # the text line again. require 'puppet/util/methodhelper' module Puppet::Util::FileParsing include Puppet::Util attr_writer :line_separator, :trailing_separator class FileRecord include Puppet::Util include Puppet::Util::MethodHelper attr_accessor :absent, :joiner, :rts, :separator, :rollup, :name, :match, :block_eval attr_reader :fields, :optional, :type INVALID_FIELDS = [:record_type, :target, :on_disk] # Customize this so we can do a bit of validation. def fields=(fields) @fields = fields.collect do |field| r = symbolize(field) if INVALID_FIELDS.include?(r) raise ArgumentError.new("Cannot have fields named %s" % r) end r end end def initialize(type, options = {}, &block) @type = symbolize(type) unless [:record, :text].include?(@type) raise ArgumentError, "Invalid record type %s" % @type end set_options(options) if self.type == :record # Now set defaults. self.absent ||= "" self.separator ||= /\s+/ self.joiner ||= " " self.optional ||= [] unless defined? @rollup @rollup = true end end if block_given? @block_eval ||= :process # Allow the developer to specify that a block should be instance-eval'ed. if @block_eval == :instance instance_eval(&block) else meta_def(@block_eval, &block) end end end # Convert a record into a line by joining the fields together appropriately. # This is pulled into a separate method so it can be called by the hooks. def join(details) joinchar = self.joiner fields.collect { |field| # If the field is marked absent, use the appropriate replacement if details[field] == :absent or details[field] == [:absent] or details[field].nil? if self.optional.include?(field) self.absent else raise ArgumentError, "Field '%s' is required" % field end else details[field].to_s end }.reject { |c| c.nil?}.join(joinchar) end # Customize this so we can do a bit of validation. def optional=(optional) @optional = optional.collect do |field| symbolize(field) end end # Create a hook that modifies the hash resulting from parsing. def post_parse=(block) meta_def(:post_parse, &block) end # Create a hook that modifies the hash just prior to generation. def pre_gen=(block) meta_def(:pre_gen, &block) end # Are we a text type? def text? type == :text end def to_line=(block) meta_def(:to_line, &block) end end # Clear all existing record definitions. Only used for testing. def clear_records @record_types.clear @record_order.clear end def fields(type) if record = record_type(type) record.fields.dup else nil end end # Try to match a specific text line. def handle_text_line(line, record) if line =~ record.match return {:record_type => record.name, :line => line} else return nil end end # Try to match a record. def handle_record_line(line, record) ret = nil if record.respond_to?(:process) if ret = record.send(:process, line.dup) unless ret.is_a?(Hash) raise Puppet::DevError, "Process record type %s returned non-hash" % record.name end else return nil end elsif regex = record.match # In this case, we try to match the whole line and then use the # match captures to get our fields. if match = regex.match(line) fields = [] ret = {} record.fields.zip(match.captures).each do |field, value| if value == record.absent ret[field] = :absent else ret[field] = value end end else nil end else ret = {} sep = record.separator # String "helpfully" replaces ' ' with /\s+/ in splitting, so we # have to work around it. if sep == " " sep = / / end line_fields = line.split(sep) record.fields.each do |param| value = line_fields.shift if value and value != record.absent ret[param] = value else ret[param] = :absent end end if record.rollup and ! line_fields.empty? last_field = record.fields[-1] val = ([ret[last_field]] + line_fields).join(record.joiner) ret[last_field] = val end end if ret ret[:record_type] = record.name return ret else return nil end end def line_separator unless defined?(@line_separator) @line_separator = "\n" end @line_separator end # Split text into separate lines using the record separator. def lines(text) # Remove any trailing separators, and then split based on them # LAK:NOTE See http://snurl.com/21zf8 [groups_google_com] x = text.sub(/#{self.line_separator}\Q/,'').split(self.line_separator) end # Split a bunch of text into lines and then parse them individually. def parse(text) count = 1 lines(text).collect do |line| count += 1 if val = parse_line(line) val else error = Puppet::Error.new("Could not parse line %s" % line.inspect) error.line = count raise error end end end # Handle parsing a single line. def parse_line(line) unless records? raise Puppet::DevError, "No record types defined; cannot parse lines" end @record_order.each do |record| # These are basically either text or record lines. method = "handle_%s_line" % record.type if respond_to?(method) if result = send(method, line, record) if record.respond_to?(:post_parse) record.send(:post_parse, result) end return result end else raise Puppet::DevError, "Somehow got invalid line type %s" % record.type end end return nil end # Define a new type of record. These lines get split into hashes. Valid # options are: # * :absent: What to use as value within a line, when a field is # absent. Note that in the record object, the literal :absent symbol is # used, and not this value. Defaults to "". # * :fields: The list of fields, as an array. By default, all # fields are considered required. # * :joiner: How to join fields together. Defaults to '\t'. # * :optional: Which fields are optional. If these are missing, # you'll just get the 'absent' value instead of an ArgumentError. # * :rts: Whether to remove trailing whitespace. Defaults to false. # If true, whitespace will be removed; if a regex, then whatever matches # the regex will be removed. # * :separator: The record separator. Defaults to /\s+/. def record_line(name, options, &block) unless options.include?(:fields) raise ArgumentError, "Must include a list of fields" end record = FileRecord.new(:record, options, &block) record.name = symbolize(name) new_line_type(record) end # Are there any record types defined? def records? defined?(@record_types) and ! @record_types.empty? end # Define a new type of text record. def text_line(name, options, &block) unless options.include?(:match) raise ArgumentError, "You must provide a :match regex for text lines" end record = FileRecord.new(:text, options, &block) record.name = symbolize(name) new_line_type(record) end # Generate a file from a bunch of hash records. def to_file(records) text = records.collect { |record| to_line(record) }.join(line_separator) if trailing_separator text += line_separator end return text end # Convert our parsed record into a text record. def to_line(details) unless record = record_type(details[:record_type]) raise ArgumentError, "Invalid record type %s" % details[:record_type].inspect end if record.respond_to?(:pre_gen) details = details.dup record.send(:pre_gen, details) end case record.type - when :text: return details[:line] + when :text; return details[:line] else if record.respond_to?(:to_line) return record.to_line(details) end line = record.join(details) if regex = record.rts # If they say true, then use whitespace; else, use their regex. if regex == true regex = /\s+$/ end return line.sub(regex,'') else return line end end end # Whether to add a trailing separator to the file. Defaults to true def trailing_separator if defined? @trailing_separator return @trailing_separator else return true end end def valid_attr?(type, attr) type = symbolize(type) if record = record_type(type) and record.fields.include?(symbolize(attr)) return true else if symbolize(attr) == :ensure return true else false end end end private # Define a new type of record. def new_line_type(record) @record_types ||= {} @record_order ||= [] if @record_types.include?(record.name) raise ArgumentError, "Line type %s is already defined" % record.name end @record_types[record.name] = record @record_order << record return record end # Retrieve the record object. def record_type(type) @record_types[symbolize(type)] end end diff --git a/lib/puppet/util/ldap/connection.rb b/lib/puppet/util/ldap/connection.rb index 70fe303c5..19c53a2e6 100644 --- a/lib/puppet/util/ldap/connection.rb +++ b/lib/puppet/util/ldap/connection.rb @@ -1,79 +1,79 @@ # # Created by Luke Kanies on 2008-3-23. # Copyright (c) 2008. All rights reserved. require 'puppet/util/ldap' class Puppet::Util::Ldap::Connection attr_accessor :host, :port, :user, :password, :reset, :ssl attr_reader :connection # Return a default connection, using our default settings. def self.instance ssl = if Puppet[:ldaptls] :tls elsif Puppet[:ldapssl] true else false end options = {} options[:ssl] = ssl if user = Puppet.settings[:ldapuser] and user != "" options[:user] = user if pass = Puppet.settings[:ldappassword] and pass != "" options[:password] = pass end end new(Puppet[:ldapserver], Puppet[:ldapport], options) end def close connection.unbind if connection.bound? end def initialize(host, port, options = {}) raise Puppet::Error, "Could not set up LDAP Connection: Missing ruby/ldap libraries" unless Puppet.features.ldap? @host, @port = host, port options.each do |param, value| begin send(param.to_s + "=", value) rescue raise ArgumentError, "LDAP connections do not support %s parameters" % param end end end # Create a per-connection unique name. def name [host, port, user, password, ssl].collect { |p| p.to_s }.join("/") end # Should we reset the connection? def reset? reset end # Start our ldap connection. def start begin case ssl - when :tls: + when :tls @connection = LDAP::SSLConn.new(host, port, true) - when true: + when true @connection = LDAP::SSLConn.new(host, port) else @connection = LDAP::Conn.new(host, port) end @connection.set_option(LDAP::LDAP_OPT_PROTOCOL_VERSION, 3) @connection.set_option(LDAP::LDAP_OPT_REFERRALS, LDAP::LDAP_OPT_ON) @connection.simple_bind(user, password) rescue => detail raise Puppet::Error, "Could not connect to LDAP: %s" % detail end end end diff --git a/lib/puppet/util/log.rb b/lib/puppet/util/log.rb index a74432021..db6177be2 100644 --- a/lib/puppet/util/log.rb +++ b/lib/puppet/util/log.rb @@ -1,539 +1,539 @@ require 'syslog' require 'puppet/util/tagging' # Pass feedback to the user. Log levels are modeled after syslog's, and it is # expected that that will be the most common log destination. Supports # multiple destinations, one of which is a remote server. class Puppet::Util::Log include Puppet::Util include Puppet::Util::Tagging @levels = [:debug,:info,:notice,:warning,:err,:alert,:emerg,:crit] @loglevel = 2 @desttypes = {} # A type of log destination. class Destination class << self attr_accessor :name end def self.initvars @matches = [] end # Mark the things we're supposed to match. def self.match(obj) @matches ||= [] @matches << obj end # See whether we match a given thing. def self.match?(obj) # Convert single-word strings into symbols like :console and :syslog if obj.is_a? String and obj =~ /^\w+$/ obj = obj.downcase.intern end @matches.each do |thing| # Search for direct matches or class matches return true if thing === obj or thing == obj.class.to_s end return false end def name if defined? @name return @name else return self.class.name end end # Set how to handle a message. def self.sethandler(&block) define_method(:handle, &block) end # Mark how to initialize our object. def self.setinit(&block) define_method(:initialize, &block) end end # Create a new destination type. def self.newdesttype(name, options = {}, &block) dest = genclass(name, :parent => Destination, :prefix => "Dest", :block => block, :hash => @desttypes, :attributes => options ) dest.match(dest.name) return dest end @destinations = {} class << self include Puppet::Util include Puppet::Util::ClassGen end # Reset all logs to basics. Basically just closes all files and undefs # all of the other objects. def Log.close(dest = nil) if dest if @destinations.include?(dest) if @destinations.respond_to?(:close) @destinations[dest].close end @destinations.delete(dest) end else @destinations.each { |name, dest| if dest.respond_to?(:flush) dest.flush end if dest.respond_to?(:close) dest.close end } @destinations = {} end end def self.close_all # And close all logs except the console. destinations.each do |dest| close(dest) end end # Flush any log destinations that support such operations. def Log.flush @destinations.each { |type, dest| if dest.respond_to?(:flush) dest.flush end } end # Create a new log message. The primary role of this method is to # avoid creating log messages below the loglevel. def Log.create(hash) unless hash.include?(:level) raise Puppet::DevError, "Logs require a level" end unless @levels.index(hash[:level]) raise Puppet::DevError, "Invalid log level %s" % hash[:level] end if @levels.index(hash[:level]) >= @loglevel return Puppet::Util::Log.new(hash) else return nil end end def Log.destinations return @destinations.keys end # Yield each valid level in turn def Log.eachlevel @levels.each { |level| yield level } end # Return the current log level. def Log.level return @levels[@loglevel] end # Set the current log level. def Log.level=(level) unless level.is_a?(Symbol) level = level.intern end unless @levels.include?(level) raise Puppet::DevError, "Invalid loglevel %s" % level end @loglevel = @levels.index(level) end def Log.levels @levels.dup end newdesttype :syslog do def close Syslog.close end def initialize if Syslog.opened? Syslog.close end name = Puppet[:name] name = "puppet-#{name}" unless name =~ /puppet/ options = Syslog::LOG_PID | Syslog::LOG_NDELAY # XXX This should really be configurable. str = Puppet[:syslogfacility] begin facility = Syslog.const_get("LOG_#{str.upcase}") rescue NameError raise Puppet::Error, "Invalid syslog facility %s" % str end @syslog = Syslog.open(name, options, facility) end def handle(msg) # XXX Syslog currently has a bug that makes it so you # cannot log a message with a '%' in it. So, we get rid # of them. if msg.source == "Puppet" @syslog.send(msg.level, msg.to_s.gsub("%", '%%')) else @syslog.send(msg.level, "(%s) %s" % [msg.source.to_s.gsub("%", ""), msg.to_s.gsub("%", '%%') ] ) end end end newdesttype :file do match(/^\//) def close if defined? @file @file.close @file = nil end end def flush if defined? @file @file.flush end end def initialize(path) @name = path # first make sure the directory exists # We can't just use 'Config.use' here, because they've # specified a "special" destination. unless FileTest.exist?(File.dirname(path)) Puppet.recmkdir(File.dirname(path)) Puppet.info "Creating log directory %s" % File.dirname(path) end # create the log file, if it doesn't already exist file = File.open(path, File::WRONLY|File::CREAT|File::APPEND) @file = file @autoflush = Puppet[:autoflush] end def handle(msg) @file.puts("%s %s (%s): %s" % [msg.time, msg.source, msg.level, msg.to_s]) @file.flush if @autoflush end end newdesttype :console do RED = {:console => "", :html => "FFA0A0"} GREEN = {:console => "", :html => "00CD00"} YELLOW = {:console => "", :html => "FFFF60"} BLUE = {:console => "", :html => "80A0FF"} PURPLE = {:console => "", :html => "FFA500"} CYAN = {:console => "", :html => "40FFFF"} WHITE = {:console => "", :html => "FFFFFF"} HRED = {:console => "", :html => "FFA0A0"} HGREEN = {:console => "", :html => "00CD00"} HYELLOW = {:console => "", :html => "FFFF60"} HBLUE = {:console => "", :html => "80A0FF"} HPURPLE = {:console => "", :html => "FFA500"} HCYAN = {:console => "", :html => "40FFFF"} HWHITE = {:console => "", :html => "FFFFFF"} RESET = {:console => "", :html => ""} @@colormap = { :debug => WHITE, :info => GREEN, :notice => CYAN, :warning => YELLOW, :err => HPURPLE, :alert => RED, :emerg => HRED, :crit => HRED } def colorize(level, str) case Puppet[:color] - when false: str - when true, :ansi, "ansi": console_color(level, str) - when :html, "html": html_color(level, str) + when false; str + when true, :ansi, "ansi"; console_color(level, str) + when :html, "html"; html_color(level, str) end end def console_color(level, str) @@colormap[level][:console] + str + RESET[:console] end def html_color(level, str) %{%s} % [@@colormap[level][:html], str] end def initialize # Flush output immediately. $stdout.sync = true end def handle(msg) if msg.source == "Puppet" puts colorize(msg.level, "%s: %s" % [msg.level, msg.to_s]) else puts colorize(msg.level, "%s: %s: %s" % [msg.level, msg.source, msg.to_s]) end end end newdesttype :host do def initialize(host) Puppet.info "Treating %s as a hostname" % host args = {} if host =~ /:(\d+)/ args[:Port] = $1 args[:Server] = host.sub(/:\d+/, '') else args[:Server] = host end @name = host @driver = Puppet::Network::Client::LogClient.new(args) end def handle(msg) unless msg.is_a?(String) or msg.remote unless defined? @hostname @hostname = Facter["hostname"].value end unless defined? @domain @domain = Facter["domain"].value if @domain @hostname += "." + @domain end end if msg.source =~ /^\// msg.source = @hostname + ":" + msg.source elsif msg.source == "Puppet" msg.source = @hostname + " " + msg.source else msg.source = @hostname + " " + msg.source end begin #puts "would have sent %s" % msg #puts "would have sent %s" % # CGI.escape(YAML.dump(msg)) begin tmp = CGI.escape(YAML.dump(msg)) rescue => detail puts "Could not dump: %s" % detail.to_s return end # Add the hostname to the source @driver.addlog(tmp) rescue => detail if Puppet[:trace] puts detail.backtrace end Puppet.err detail Puppet::Util::Log.close(self) end end end end # Log to a transaction report. newdesttype :report do match "Puppet::Transaction::Report" def initialize(report) @report = report end def handle(msg) @report.newlog(msg) end end # Log to an array, just for testing. newdesttype :array do match "Array" def initialize(array) @array = array end def handle(msg) @array << msg end end # Create a new log destination. def Log.newdestination(dest) # Each destination can only occur once. if @destinations.find { |name, obj| obj.name == dest } return end name, type = @desttypes.find do |name, klass| klass.match?(dest) end unless type raise Puppet::DevError, "Unknown destination type %s" % dest end begin if type.instance_method(:initialize).arity == 1 @destinations[dest] = type.new(dest) else @destinations[dest] = type.new() end rescue => detail if Puppet[:debug] puts detail.backtrace end # If this was our only destination, then add the console back in. if @destinations.empty? and (dest != :console and dest != "console") newdestination(:console) end end end # Route the actual message. FIXME There are lots of things this method # should do, like caching, storing messages when there are not yet # destinations, a bit more. It's worth noting that there's a potential # for a loop here, if the machine somehow gets the destination set as # itself. def Log.newmessage(msg) if @levels.index(msg.level) < @loglevel return end @destinations.each do |name, dest| threadlock(dest) do dest.handle(msg) end end end def Log.sendlevel?(level) @levels.index(level) >= @loglevel end # Reopen all of our logs. def Log.reopen Puppet.notice "Reopening log files" types = @destinations.keys @destinations.each { |type, dest| if dest.respond_to?(:close) dest.close end } @destinations.clear # We need to make sure we always end up with some kind of destination begin types.each { |type| Log.newdestination(type) } rescue => detail if @destinations.empty? Log.newdestination(:syslog) Puppet.err detail.to_s end end end # Is the passed level a valid log level? def self.validlevel?(level) @levels.include?(level) end attr_accessor :level, :message, :time, :remote attr_reader :source def initialize(args) unless args.include?(:level) && args.include?(:message) raise ArgumentError, "Puppet::Util::Log called incorrectly" end if args[:level].class == String @level = args[:level].intern elsif args[:level].class == Symbol @level = args[:level] else raise ArgumentError, "Level is not a string or symbol: #{args[:level].class}" end @message = args[:message].to_s @time = Time.now raise ArgumentError, "Invalid log level %s" % level unless self.class.validlevel?(level) if tags = args[:tags] tags.each { |t| self.tag(t) } end self.source = args[:source] || "Puppet" # Tag myself with my log level tag(level) Log.newmessage(self) end # If they pass a source in to us, we make sure it is a string, and # we retrieve any tags we can. def source=(source) # We can't store the actual source, we just store the path. # We can't just check for whether it responds to :path, because # plenty of providers respond to that in their normal function. if (source.is_a?(Puppet::Type) or source.is_a?(Puppet::Parameter)) and source.respond_to?(:path) @source = source.path else @source = source.to_s end if source.respond_to?(:tags) source.tags.each { |t| tag(t) } end end def to_report "%s %s (%s): %s" % [self.time, self.source, self.level, self.to_s] end def to_s return @message end end # This is for backward compatibility from when we changed the constant to Puppet::Util::Log # because the reports include the constant name. Apparently the alias was created in # March 2007, should could probably be removed soon. Puppet::Log = Puppet::Util::Log diff --git a/lib/puppet/util/posix.rb b/lib/puppet/util/posix.rb index 3f6c1f6e3..40a175eb3 100755 --- a/lib/puppet/util/posix.rb +++ b/lib/puppet/util/posix.rb @@ -1,137 +1,137 @@ # Utility methods for interacting with POSIX objects; mostly user and group module Puppet::Util::POSIX # Retrieve a field from a POSIX Etc object. The id can be either an integer # or a name. This only works for users and groups. It's also broken on # some platforms, unfortunately, which is why we fall back to the other # method search_posix_field in the gid and uid methods if a sanity check # fails def get_posix_field(space, field, id) raise Puppet::DevError, "Did not get id from caller" unless id if id.is_a?(Integer) if id > Puppet[:maximum_uid].to_i Puppet.err "Tried to get %s field for silly id %s" % [field, id] return nil end method = methodbyid(space) else method = methodbyname(space) end begin return Etc.send(method, id).send(field) rescue ArgumentError => detail # ignore it; we couldn't find the object return nil end end # A degenerate method of retrieving name/id mappings. The job of this method is # to retrieve all objects of a certain type, search for a specific entry # and then return a given field from that entry. def search_posix_field(type, field, id) idmethod = idfield(type) integer = false if id.is_a?(Integer) integer = true if id > Puppet[:maximum_uid].to_i Puppet.err "Tried to get %s field for silly id %s" % [field, id] return nil end end Etc.send(type) do |object| if integer and object.send(idmethod) == id return object.send(field) elsif object.name == id return object.send(field) end end # Apparently the group/passwd methods need to get reset; if we skip # this call, then new users aren't found. case type - when :passwd: Etc.send(:endpwent) - when :group: Etc.send(:endgrent) + when :passwd; Etc.send(:endpwent) + when :group; Etc.send(:endgrent) end return nil end # Determine what the field name is for users and groups. def idfield(space) case Puppet::Util.symbolize(space) - when :gr, :group: return :gid - when :pw, :user, :passwd: return :uid + when :gr, :group; return :gid + when :pw, :user, :passwd; return :uid else raise ArgumentError.new("Can only handle users and groups") end end # Determine what the method is to get users and groups by id def methodbyid(space) case Puppet::Util.symbolize(space) - when :gr, :group: return :getgrgid - when :pw, :user, :passwd: return :getpwuid + when :gr, :group; return :getgrgid + when :pw, :user, :passwd; return :getpwuid else raise ArgumentError.new("Can only handle users and groups") end end # Determine what the method is to get users and groups by name def methodbyname(space) case Puppet::Util.symbolize(space) - when :gr, :group: return :getgrnam - when :pw, :user, :passwd: return :getpwnam + when :gr, :group; return :getgrnam + when :pw, :user, :passwd; return :getpwnam else raise ArgumentError.new("Can only handle users and groups") end end # Get the GID of a given group, provided either a GID or a name def gid(group) begin group = Integer(group) rescue ArgumentError # pass end if group.is_a?(Integer) return nil unless name = get_posix_field(:group, :name, group) gid = get_posix_field(:group, :gid, name) check_value = gid else return nil unless gid = get_posix_field(:group, :gid, group) name = get_posix_field(:group, :name, gid) check_value = name end if check_value != group return search_posix_field(:group, :gid, group) else return gid end end # Get the UID of a given user, whether a UID or name is provided def uid(user) begin user = Integer(user) rescue ArgumentError # pass end if user.is_a?(Integer) return nil unless name = get_posix_field(:passwd, :name, user) uid = get_posix_field(:passwd, :uid, name) check_value = uid else return nil unless uid = get_posix_field(:passwd, :uid, user) name = get_posix_field(:passwd, :name, uid) check_value = name end if check_value != user return search_posix_field(:passwd, :uid, user) else return uid end end end diff --git a/lib/puppet/util/settings.rb b/lib/puppet/util/settings.rb index 3ef75a845..c60f1713b 100644 --- a/lib/puppet/util/settings.rb +++ b/lib/puppet/util/settings.rb @@ -1,1161 +1,1161 @@ require 'puppet' require 'sync' require 'puppet/transportable' require 'getoptlong' require 'puppet/external/event-loop' require 'puppet/util/cacher' require 'puppet/util/loadedfile' # The class for handling configuration files. class Puppet::Util::Settings include Enumerable include Puppet::Util::Cacher attr_accessor :file attr_reader :timer # Retrieve a config value def [](param) value(param) end # Set a config value. This doesn't set the defaults, it sets the value itself. def []=(param, value) param = param.to_sym unless element = @config[param] raise ArgumentError, "Attempt to assign a value to unknown configuration parameter %s" % param.inspect end if element.respond_to?(:munge) value = element.munge(value) end if element.respond_to?(:handle) element.handle(value) end # Reset the name, so it's looked up again. if param == :name @name = nil end @sync.synchronize do # yay, thread-safe @values[:memory][param] = value @cache.clear clearused end return value end # Generate the list of valid arguments, in a format that GetoptLong can # understand, and add them to the passed option list. def addargs(options) # Add all of the config parameters as valid options. self.each { |name, element| element.getopt_args.each { |args| options << args } } return options end # Generate the list of valid arguments, in a format that OptionParser can # understand, and add them to the passed option list. def optparse_addargs(options) # Add all of the config parameters as valid options. self.each { |name, element| options << element.optparse_args } return options end # Is our parameter a boolean parameter? def boolean?(param) param = param.to_sym if @config.include?(param) and @config[param].kind_of? CBoolean return true else return false end end # Remove all set values, potentially skipping cli values. def clear(exceptcli = false) @sync.synchronize do @values.each do |name, values| @values.delete(name) unless exceptcli and name == :cli end # Don't clear the 'used' in this case, since it's a config file reparse, # and we want to retain this info. unless exceptcli @used = [] end @cache.clear @name = nil end end # This is mostly just used for testing. def clearused @cache.clear @used = [] end # Do variable interpolation on the value. def convert(value, environment = nil) return value unless value return value unless value.is_a? String newval = value.gsub(/\$(\w+)|\$\{(\w+)\}/) do |value| varname = $2 || $1 if varname == "environment" and environment environment elsif pval = self.value(varname) pval else raise Puppet::DevError, "Could not find value for %s" % value end end return newval end # Return a value's description. def description(name) if obj = @config[name.to_sym] obj.desc else nil end end def each @config.each { |name, object| yield name, object } end # Iterate over each section name. def eachsection yielded = [] @config.each do |name, object| section = object.section unless yielded.include? section yield section yielded << section end end end # Return an object by name. def element(param) param = param.to_sym @config[param] end # Handle a command-line argument. def handlearg(opt, value = nil) @cache.clear value = munge_value(value) if value str = opt.sub(/^--/,'') bool = true newstr = str.sub(/^no-/, '') if newstr != str str = newstr bool = false end str = str.intern if self.valid?(str) @sync.synchronize do if self.boolean?(str) @values[:cli][str] = bool else @values[:cli][str] = value end end else raise ArgumentError, "Invalid argument %s" % opt end end def include?(name) name = name.intern if name.is_a? String @config.include?(name) end # check to see if a short name is already defined def shortinclude?(short) short = short.intern if name.is_a? String @shortnames.include?(short) end # Create a new collection of config settings. def initialize @config = {} @shortnames = {} @created = [] @searchpath = nil # Mutex-like thing to protect @values @sync = Sync.new # Keep track of set values. @values = Hash.new { |hash, key| hash[key] = {} } # And keep a per-environment cache @cache = Hash.new { |hash, key| hash[key] = {} } # A central concept of a name. @name = nil # The list of sections we've used. @used = [] end # NOTE: ACS ahh the util classes. . .sigh # as part of a fix for 1183, I pulled the logic for the following 5 methods out of the executables and puppet.rb # They probably deserve their own class, but I don't want to do that until I can refactor environments # its a little better than where they were # Prints the contents of a config file with the available config elements, or it # prints a single value of a config element. def print_config_options env = value(:environment) val = value(:configprint) if val == "all" hash = {} each do |name, obj| val = value(name,env) val = val.inspect if val == "" hash[name] = val end hash.sort { |a,b| a[0].to_s <=> b[0].to_s }.each do |name, val| puts "%s = %s" % [name, val] end else val.split(/\s*,\s*/).sort.each do |v| if include?(v) #if there is only one value, just print it for back compatibility if v == val puts value(val,env) break end puts "%s = %s" % [v, value(v,env)] else puts "invalid parameter: %s" % v return false end end end true end def generate_config puts to_config true end def generate_manifest puts to_manifest true end def print_configs return print_config_options if value(:configprint) != "" return generate_config if value(:genconfig) return generate_manifest if value(:genmanifest) end def print_configs? return (value(:configprint) != "" || value(:genconfig) || value(:genmanifest)) && true end # Return a given object's file metadata. def metadata(param) if obj = @config[param.to_sym] and obj.is_a?(CFile) return [:owner, :group, :mode].inject({}) do |meta, p| if v = obj.send(p) meta[p] = v end meta end else nil end end # Make a directory with the appropriate user, group, and mode def mkdir(default) obj = get_config_file_default(default) Puppet::Util::SUIDManager.asuser(obj.owner, obj.group) do mode = obj.mode || 0750 Dir.mkdir(obj.value, mode) end end # Figure out our name. def name unless @name unless @config[:name] return nil end searchpath.each do |source| next if source == :name @sync.synchronize do @name = @values[source][:name] end break if @name end unless @name @name = convert(@config[:name].default).intern end end @name end # Return all of the parameters associated with a given section. def params(section = nil) if section section = section.intern if section.is_a? String @config.find_all { |name, obj| obj.section == section }.collect { |name, obj| name } else @config.keys end end # Parse the configuration file. Just provides # thread safety. def parse raise "No :config setting defined; cannot parse unknown config file" unless self[:config] # Create a timer so that this file will get checked automatically # and reparsed if necessary. set_filetimeout_timer() # Retrieve the value now, so that we don't lose it in the 'clear' call. file = self[:config] return unless FileTest.exist?(file) # We have to clear outside of the sync, because it's # also using synchronize(). clear(true) @sync.synchronize do unsafe_parse(file) end end # Unsafely parse the file -- this isn't thread-safe and causes plenty of problems if used directly. def unsafe_parse(file) parse_file(file).each do |area, values| @values[area] = values end # Determine our environment, if we have one. if @config[:environment] env = self.value(:environment).to_sym else env = "none" end # Call any hooks we should be calling. settings_with_hooks.each do |setting| each_source(env) do |source| if value = @values[source][setting.name] # We still have to use value() to retrieve the value, since # we want the fully interpolated value, not $vardir/lib or whatever. # This results in extra work, but so few of the settings # will have associated hooks that it ends up being less work this # way overall. setting.handle(self.value(setting.name, env)) break end end end # We have to do it in the reverse of the search path, # because multiple sections could set the same value # and I'm too lazy to only set the metadata once. searchpath.reverse.each do |source| if meta = @values[source][:_meta] set_metadata(meta) end end end # Create a new element. The value is passed in because it's used to determine # what kind of element we're creating, but the value itself might be either # a default or a value, so we can't actually assign it. def newelement(hash) klass = nil if hash[:section] hash[:section] = hash[:section].to_sym end if type = hash[:type] unless klass = {:element => CElement, :file => CFile, :boolean => CBoolean}[type] raise ArgumentError, "Invalid setting type '%s'" % type end hash.delete(:type) else case hash[:default] - when true, false, "true", "false": + when true, false, "true", "false" klass = CBoolean - when /^\$\w+\//, /^\//: + when /^\$\w+\//, /^\// klass = CFile - when String, Integer, Float: # nothing + when String, Integer, Float # nothing klass = CElement else raise Puppet::Error, "Invalid value '%s' for %s" % [value.inspect, hash[:name]] end end hash[:settings] = self element = klass.new(hash) return element end # This has to be private, because it doesn't add the elements to @config private :newelement # Iterate across all of the objects in a given section. def persection(section) section = section.to_sym self.each { |name, obj| if obj.section == section yield obj end } end # Cache this in an easily clearable way, since we were # having trouble cleaning it up after tests. cached_attr(:file) do if path = self[:config] and FileTest.exist?(path) Puppet::Util::LoadedFile.new(path) end end # Reparse our config file, if necessary. def reparse if file and file.changed? Puppet.notice "Reparsing %s" % file.file @sync.synchronize do parse end reuse() end end def reuse return unless defined? @used @sync.synchronize do # yay, thread-safe new = @used @used = [] self.use(*new) end end # The order in which to search for values. def searchpath(environment = nil) if environment [:cli, :memory, environment, :name, :main] else [:cli, :memory, :name, :main] end end # Get a list of objects per section def sectionlist sectionlist = [] self.each { |name, obj| section = obj.section || "puppet" sections[section] ||= [] unless sectionlist.include?(section) sectionlist << section end sections[section] << obj } return sectionlist, sections end # Set a bunch of defaults in a given section. The sections are actually pretty # pointless, but they help break things up a bit, anyway. def setdefaults(section, defs) section = section.to_sym call = [] defs.each { |name, hash| if hash.is_a? Array unless hash.length == 2 raise ArgumentError, "Defaults specified as an array must contain only the default value and the decription" end tmp = hash hash = {} [:default, :desc].zip(tmp).each { |p,v| hash[p] = v } end name = name.to_sym hash[:name] = name hash[:section] = section if @config.include?(name) raise ArgumentError, "Parameter %s is already defined" % name end tryconfig = newelement(hash) if short = tryconfig.short if other = @shortnames[short] raise ArgumentError, "Parameter %s is already using short name '%s'" % [other.name, short] end @shortnames[short] = tryconfig end @config[name] = tryconfig # Collect the settings that need to have their hooks called immediately. # We have to collect them so that we can be sure we're fully initialized before # the hook is called. call << tryconfig if tryconfig.call_on_define } call.each { |setting| setting.handle(self.value(setting.name)) } end # Create a timer to check whether the file should be reparsed. def set_filetimeout_timer return unless timeout = self[:filetimeout] and timeout = Integer(timeout) and timeout > 0 timer = EventLoop::Timer.new(:interval => timeout, :tolerance => 1, :start? => true) { self.reparse() } end # Convert the settings we manage into a catalog full of resources that model those settings. # We currently have to go through Trans{Object,Bucket} instances, # because this hasn't been ported yet. def to_catalog(*sections) sections = nil if sections.empty? catalog = Puppet::Resource::Catalog.new("Settings") @config.values.find_all { |value| value.is_a?(CFile) }.each do |file| next unless (sections.nil? or sections.include?(file.section)) next unless resource = file.to_resource next if catalog.resource(resource.ref) catalog.add_resource(resource) end add_user_resources(catalog, sections) catalog end # Convert our list of config elements into a configuration file. def to_config str = %{The configuration file for #{Puppet[:name]}. Note that this file is likely to have unused configuration parameters in it; any parameter that's valid anywhere in Puppet can be in any config file, even if it's not used. Every section can specify three special parameters: owner, group, and mode. These parameters affect the required permissions of any files specified after their specification. Puppet will sometimes use these parameters to check its own configured state, so they can be used to make Puppet a bit more self-managing. Generated on #{Time.now}. }.gsub(/^/, "# ") # Add a section heading that matches our name. if @config.include?(:name) str += "[%s]\n" % self[:name] end eachsection do |section| persection(section) do |obj| str += obj.to_config + "\n" end end return str end # Convert to a parseable manifest def to_manifest catalog = to_catalog # The resource list is a list of references, not actual instances. catalog.resources.collect do |ref| catalog.resource(ref).to_manifest end.join("\n\n") end # Create the necessary objects to use a section. This is idempotent; # you can 'use' a section as many times as you want. def use(*sections) sections = sections.collect { |s| s.to_sym } @sync.synchronize do # yay, thread-safe sections = sections.reject { |s| @used.include?(s) } return if sections.empty? begin catalog = to_catalog(*sections).to_ral rescue => detail puts detail.backtrace if Puppet[:trace] Puppet.err "Could not create resources for managing Puppet's files and directories in sections %s: %s" % [sections.inspect, detail] # We need some way to get rid of any resources created during the catalog creation # but not cleaned up. return end begin catalog.host_config = false catalog.apply do |transaction| if transaction.any_failed? report = transaction.report failures = report.logs.find_all { |log| log.level == :err } raise "Got %s failure(s) while initializing: %s" % [failures.length, failures.collect { |l| l.to_s }.join("; ")] end end end sections.each { |s| @used << s } @used.uniq! end end def valid?(param) param = param.to_sym @config.has_key?(param) end # Find the correct value using our search path. Optionally accept an environment # in which to search before the other configuration sections. def value(param, environment = nil) param = param.to_sym environment = environment.to_sym if environment # Short circuit to nil for undefined parameters. return nil unless @config.include?(param) # Yay, recursion. #self.reparse() unless [:config, :filetimeout].include?(param) # Check the cache first. It needs to be a per-environment # cache so that we don't spread values from one env # to another. if cached = @cache[environment||"none"][param] return cached end # See if we can find it within our searchable list of values val = catch :foundval do each_source(environment) do |source| # Look for the value. We have to test the hash for whether # it exists, because the value might be false. @sync.synchronize do if @values[source].include?(param) throw :foundval, @values[source][param] end end end throw :foundval, nil end # If we didn't get a value, use the default val = @config[param].default if val.nil? # Convert it if necessary val = convert(val, environment) # And cache it @cache[environment||"none"][param] = val return val end # Open a file with the appropriate user, group, and mode def write(default, *args, &bloc) obj = get_config_file_default(default) writesub(default, value(obj.name), *args, &bloc) end # Open a non-default file under a default dir with the appropriate user, # group, and mode def writesub(default, file, *args, &bloc) obj = get_config_file_default(default) chown = nil if Puppet::Util::SUIDManager.uid == 0 chown = [obj.owner, obj.group] else chown = [nil, nil] end Puppet::Util::SUIDManager.asuser(*chown) do mode = obj.mode || 0640 if args.empty? args << "w" end args << mode # Update the umask to make non-executable files Puppet::Util.withumask(File.umask ^ 0111) do File.open(file, *args) do |file| yield file end end end end def readwritelock(default, *args, &bloc) file = value(get_config_file_default(default).name) tmpfile = file + ".tmp" sync = Sync.new unless FileTest.directory?(File.dirname(tmpfile)) raise Puppet::DevError, "Cannot create %s; directory %s does not exist" % [file, File.dirname(file)] end sync.synchronize(Sync::EX) do File.open(file, ::File::CREAT|::File::RDWR, 0600) do |rf| rf.lock_exclusive do if File.exist?(tmpfile) raise Puppet::Error, ".tmp file already exists for %s; Aborting locked write. Check the .tmp file and delete if appropriate" % [file] end # If there's a failure, remove our tmpfile begin writesub(default, tmpfile, *args, &bloc) rescue File.unlink(tmpfile) if FileTest.exist?(tmpfile) raise end begin File.rename(tmpfile, file) rescue => detail Puppet.err "Could not rename %s to %s: %s" % [file, tmpfile, detail] File.unlink(tmpfile) if FileTest.exist?(tmpfile) end end end end end private def get_config_file_default(default) obj = nil unless obj = @config[default] raise ArgumentError, "Unknown default %s" % default end unless obj.is_a? CFile raise ArgumentError, "Default %s is not a file" % default end return obj end # Create the transportable objects for users and groups. def add_user_resources(catalog, sections) return unless Puppet.features.root? return unless self[:mkusers] @config.each do |name, element| next unless element.respond_to?(:owner) next unless sections.nil? or sections.include?(element.section) if user = element.owner and user != "root" and catalog.resource(:user, user).nil? resource = Puppet::Resource.new(:user, user, :ensure => :present) if self[:group] resource[:gid] = self[:group] end catalog.add_resource resource end if group = element.group and ! %w{root wheel}.include?(group) and catalog.resource(:group, group).nil? catalog.add_resource Puppet::Resource.new(:group, group, :ensure => :present) end end end # Yield each search source in turn. def each_source(environment) searchpath(environment).each do |source| # Modify the source as necessary. source = self.name if source == :name yield source end end # Return all elements that have associated hooks; this is so # we can call them after parsing the configuration file. def settings_with_hooks @config.values.find_all { |setting| setting.respond_to?(:handle) } end # Extract extra setting information for files. def extract_fileinfo(string) result = {} value = string.sub(/\{\s*([^}]+)\s*\}/) do params = $1 params.split(/\s*,\s*/).each do |str| if str =~ /^\s*(\w+)\s*=\s*([\w\d]+)\s*$/ param, value = $1.intern, $2 result[param] = value unless [:owner, :mode, :group].include?(param) raise ArgumentError, "Invalid file option '%s'" % param end if param == :mode and value !~ /^\d+$/ raise ArgumentError, "File modes must be numbers" end else raise ArgumentError, "Could not parse '%s'" % string end end '' end result[:value] = value.sub(/\s*$/, '') return result return nil end # Convert arguments into booleans, integers, or whatever. def munge_value(value) # Handle different data types correctly return case value - when /^false$/i: false - when /^true$/i: true - when /^\d+$/i: Integer(value) + when /^false$/i; false + when /^true$/i; true + when /^\d+$/i; Integer(value) else value.gsub(/^["']|["']$/,'').sub(/\s+$/, '') end end # This is an abstract method that just turns a file in to a hash of hashes. # We mostly need this for backward compatibility -- as of May 2007 we need to # support parsing old files with any section, or new files with just two # valid sections. def parse_file(file) text = read_file(file) result = Hash.new { |names, name| names[name] = {} } count = 0 # Default to 'main' for the section. section = :main result[section][:_meta] = {} text.split(/\n/).each { |line| count += 1 case line - when /^\s*\[(\w+)\]$/: + when /^\s*\[(\w+)\]$/ section = $1.intern # Section names # Add a meta section result[section][:_meta] ||= {} - when /^\s*#/: next # Skip comments - when /^\s*$/: next # Skip blanks - when /^\s*(\w+)\s*=\s*(.*)$/: # settings + when /^\s*#/; next # Skip comments + when /^\s*$/; next # Skip blanks + when /^\s*(\w+)\s*=\s*(.*)$/ # settings var = $1.intern # We don't want to munge modes, because they're specified in octal, so we'll # just leave them as a String, since Puppet handles that case correctly. if var == :mode value = $2 else value = munge_value($2) end # Check to see if this is a file argument and it has extra options begin if value.is_a?(String) and options = extract_fileinfo(value) value = options[:value] options.delete(:value) result[section][:_meta][var] = options end result[section][var] = value rescue Puppet::Error => detail detail.file = file detail.line = line raise end else error = Puppet::Error.new("Could not match line %s" % line) error.file = file error.line = line raise error end } return result end # Read the file in. def read_file(file) begin return File.read(file) rescue Errno::ENOENT raise ArgumentError, "No such file %s" % file rescue Errno::EACCES raise ArgumentError, "Permission denied to file %s" % file end end # Set file metadata. def set_metadata(meta) meta.each do |var, values| values.each do |param, value| @config[var].send(param.to_s + "=", value) end end end # The base element type. class CElement attr_accessor :name, :section, :default, :setbycli, :call_on_define attr_reader :desc, :short def desc=(value) @desc = value.gsub(/^\s*/, '') end # get the arguments in getopt format def getopt_args if short [["--#{name}", "-#{short}", GetoptLong::REQUIRED_ARGUMENT]] else [["--#{name}", GetoptLong::REQUIRED_ARGUMENT]] end end # get the arguments in OptionParser format def optparse_args if short ["--#{name}", "-#{short}", desc, :REQUIRED] else ["--#{name}", desc, :REQUIRED] end end def hook=(block) meta_def :handle, &block end # Create the new element. Pretty much just sets the name. def initialize(args = {}) unless @settings = args.delete(:settings) raise ArgumentError.new("You must refer to a settings object") end args.each do |param, value| method = param.to_s + "=" unless self.respond_to? method raise ArgumentError, "%s does not accept %s" % [self.class, param] end self.send(method, value) end unless self.desc raise ArgumentError, "You must provide a description for the %s config option" % self.name end end def iscreated @iscreated = true end def iscreated? if defined? @iscreated return @iscreated else return false end end def set? if defined? @value and ! @value.nil? return true else return false end end # short name for the celement def short=(value) if value.to_s.length != 1 raise ArgumentError, "Short names can only be one character." end @short = value.to_s end # Convert the object to a config statement. def to_config str = @desc.gsub(/^/, "# ") + "\n" # Add in a statement about the default. if defined? @default and @default str += "# The default value is '%s'.\n" % @default end # If the value has not been overridden, then print it out commented # and unconverted, so it's clear that that's the default and how it # works. value = @settings.value(self.name) if value != @default line = "%s = %s" % [@name, value] else line = "# %s = %s" % [@name, @default] end str += line + "\n" str.gsub(/^/, " ") end # Retrieves the value, or if it's not set, retrieves the default. def value @settings.value(self.name) end end # A file. class CFile < CElement attr_writer :owner, :group attr_accessor :mode, :create # Should we create files, rather than just directories? def create_files? create end def group if defined? @group return @settings.convert(@group) else return nil end end def owner if defined? @owner return @settings.convert(@owner) else return nil end end # Set the type appropriately. Yep, a hack. This supports either naming # the variable 'dir', or adding a slash at the end. def munge(value) # If it's not a fully qualified path... if value.is_a?(String) and value !~ /^\$/ and value !~ /^\// and value != 'false' # Make it one value = File.join(Dir.getwd, value) end if value.to_s =~ /\/$/ @type = :directory return value.sub(/\/$/, '') end return value end # Return the appropriate type. def type value = @settings.value(self.name) if @name.to_s =~ /dir/ return :directory elsif value.to_s =~ /\/$/ return :directory elsif value.is_a? String return :file else return nil end end # Turn our setting thing into a Puppet::Resource instance. def to_resource return nil unless type = self.type path = self.value return nil unless path.is_a?(String) # Make sure the paths are fully qualified. path = File.join(Dir.getwd, path) unless path =~ /^\// return nil unless type == :directory or create_files? or File.exist?(path) return nil if path =~ /^\/dev/ resource = Puppet::Resource.new(:file, path) resource[:mode] = self.mode if self.mode if Puppet.features.root? resource[:owner] = self.owner if self.owner resource[:group] = self.group if self.group end resource[:ensure] = type resource[:loglevel] = :debug resource[:backup] = false resource.tag(self.section, self.name, "settings") resource end # Make sure any provided variables look up to something. def validate(value) return true unless value.is_a? String value.scan(/\$(\w+)/) { |name| name = $1 unless @settings.include?(name) raise ArgumentError, "Settings parameter '%s' is undefined" % name end } end end # A simple boolean. class CBoolean < CElement # get the arguments in getopt format def getopt_args if short [["--#{name}", "-#{short}", GetoptLong::NO_ARGUMENT], ["--no-#{name}", GetoptLong::NO_ARGUMENT]] else [["--#{name}", GetoptLong::NO_ARGUMENT], ["--no-#{name}", GetoptLong::NO_ARGUMENT]] end end def optparse_args if short ["--[no-]#{name}", "-#{short}", desc, :NONE ] else ["--[no-]#{name}", desc, :NONE] end end def munge(value) case value - when true, "true": return true - when false, "false": return false + when true, "true"; return true + when false, "false"; return false else raise ArgumentError, "Invalid value '%s' for %s" % [value.inspect, @name] end end end end diff --git a/spec/integration/indirector/direct_file_server.rb b/spec/integration/indirector/direct_file_server.rb index 7280cda98..9843c7e65 100755 --- a/spec/integration/indirector/direct_file_server.rb +++ b/spec/integration/indirector/direct_file_server.rb @@ -1,73 +1,73 @@ #!/usr/bin/env ruby # # Created by Luke Kanies on 2007-10-19. # Copyright (c) 2007. All rights reserved. require File.dirname(__FILE__) + '/../../spec_helper' require 'puppet/indirector/file_content/file' describe Puppet::Indirector::DirectFileServer, " when interacting with the filesystem and the model" do before do # We just test a subclass, since it's close enough. @terminus = Puppet::Indirector::FileContent::File.new @filepath = "/path/to/my/file" end it "should return an instance of the model" do FileTest.expects(:exists?).with(@filepath).returns(true) @terminus.find(@terminus.indirection.request(:find, "file://host#{@filepath}")).should be_instance_of(Puppet::FileServing::Content) end it "should return an instance capable of returning its content" do FileTest.expects(:exists?).with(@filepath).returns(true) File.stubs(:lstat).with(@filepath).returns(stub("stat", :ftype => "file")) File.expects(:read).with(@filepath).returns("my content") instance = @terminus.find(@terminus.indirection.request(:find, "file://host#{@filepath}")) instance.content.should == "my content" end end describe Puppet::Indirector::DirectFileServer, " when interacting with FileServing::Fileset and the model" do before do @terminus = Puppet::Indirector::FileContent::File.new @path = Tempfile.new("direct_file_server_testing") path = @path.path @path.close! @path = path Dir.mkdir(@path) File.open(File.join(@path, "one"), "w") { |f| f.print "one content" } File.open(File.join(@path, "two"), "w") { |f| f.print "two content" } @request = @terminus.indirection.request(:search, "file:///%s" % @path, :recurse => true) end after do system("rm -rf %s" % @path) end it "should return an instance for every file in the fileset" do result = @terminus.search(@request) result.should be_instance_of(Array) result.length.should == 3 result.each { |r| r.should be_instance_of(Puppet::FileServing::Content) } end it "should return instances capable of returning their content" do @terminus.search(@request).each do |instance| case instance.full_path - when /one/: instance.content.should == "one content" - when /two/: instance.content.should == "two content" - when @path: + when /one/; instance.content.should == "one content" + when /two/; instance.content.should == "two content" + when @path else raise "No valid key for %s" % instance.path.inspect end end end end diff --git a/spec/unit/provider/mount/parsed.rb b/spec/unit/provider/mount/parsed.rb index 8a7b52531..d2c992f33 100755 --- a/spec/unit/provider/mount/parsed.rb +++ b/spec/unit/provider/mount/parsed.rb @@ -1,181 +1,181 @@ #!/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" + 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 # 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/type/file/selinux.rb b/spec/unit/type/file/selinux.rb index bf3315bf9..be58e636a 100644 --- a/spec/unit/type/file/selinux.rb +++ b/spec/unit/type/file/selinux.rb @@ -1,84 +1,84 @@ #!/usr/bin/env ruby Dir.chdir(File.dirname(__FILE__)) { (s = lambda { |f| File.exist?(f) ? require(f) : Dir.chdir("..") { s.call(f) } }).call("spec/spec_helper.rb") } [:seluser, :selrole, :seltype, :selrange].each do |param| property = Puppet::Type.type(:file).attrclass(param) describe property do before do @resource = mock 'resource' @resource.stubs(:[]).with(:path).returns "/my/file" @sel = property.new :resource => @resource end it "retrieve on #{param} should return :absent if the file isn't statable" do @resource.expects(:stat).returns nil @sel.retrieve.should == :absent end it "should retrieve nil for #{param} if there is no SELinux support" do stat = stub 'stat', :ftype => "foo" @resource.expects(:stat).returns stat @sel.expects(:get_selinux_current_context).with("/my/file").returns nil @sel.retrieve.should be_nil end it "should retrieve #{param} if a SELinux context is found with a range" do stat = stub 'stat', :ftype => "foo" @resource.expects(:stat).returns stat @sel.expects(:get_selinux_current_context).with("/my/file").returns "user_u:role_r:type_t:s0" expectedresult = case param - when :seluser then "user_u" - when :selrole then "role_r" - when :seltype then "type_t" - when :selrange then "s0" + when :seluser; "user_u" + when :selrole; "role_r" + when :seltype; "type_t" + when :selrange; "s0" end @sel.retrieve.should == expectedresult end it "should retrieve #{param} if a SELinux context is found without a range" do stat = stub 'stat', :ftype => "foo" @resource.expects(:stat).returns stat @sel.expects(:get_selinux_current_context).with("/my/file").returns "user_u:role_r:type_t" expectedresult = case param - when :seluser then "user_u" - when :selrole then "role_r" - when :seltype then "type_t" - when :selrange then nil + when :seluser; "user_u" + when :selrole; "role_r" + when :seltype; "type_t" + when :selrange; nil end @sel.retrieve.should == expectedresult end it "should handle no default gracefully" do @sel.expects(:get_selinux_default_context).with("/my/file").returns nil @sel.default.must be_nil end it "should be able to detect matchpathcon defaults" do @sel.stubs(:debug) @sel.expects(:get_selinux_default_context).with("/my/file").returns "user_u:role_r:type_t:s0" expectedresult = case param - when :seluser then "user_u" - when :selrole then "role_r" - when :seltype then "type_t" - when :selrange then "s0" + when :seluser; "user_u" + when :selrole; "role_r" + when :seltype; "type_t" + when :selrange; "s0" end @sel.default.must == expectedresult end it "should be able to set a new context" do stat = stub 'stat', :ftype => "foo" @sel.should = %w{newone} @sel.expects(:set_selinux_context).with("/my/file", ["newone"], param) @sel.sync end it "should do nothing for insync? if no SELinux support" do @sel.should = %{newcontext} @sel.expects(:selinux_support?).returns false @sel.insync?("oldcontext").should == true end end end diff --git a/test/lib/puppettest.rb b/test/lib/puppettest.rb index 42295785e..3c6083fa6 100755 --- a/test/lib/puppettest.rb +++ b/test/lib/puppettest.rb @@ -1,344 +1,344 @@ # 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 @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" + 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/lib/puppettest/support/utils.rb b/test/lib/puppettest/support/utils.rb index adc3b09ff..f23c6483b 100644 --- a/test/lib/puppettest/support/utils.rb +++ b/test/lib/puppettest/support/utils.rb @@ -1,172 +1,172 @@ require 'puppettest' module PuppetTest::Support end module PuppetTest::Support::Utils def gcdebug(type) Puppet.warning "%s: %s" % [type, ObjectSpace.each_object(type) { |o| }] end # # TODO: I think this method needs to be renamed to something a little more # explanatory. # def newobj(type, name, hash) transport = Puppet::TransObject.new(name, "file") transport[:path] = path transport[:ensure] = "file" assert_nothing_raised { file = transport.to_ral } end # Turn a list of resources, or possibly a catalog and some resources, # into a catalog object. def resources2catalog(*resources) if resources[0].is_a?(Puppet::Resource::Catalog) config = resources.shift unless resources.empty? resources.each { |r| config.add_resource r } end elsif resources[0].is_a?(Puppet::Type.type(:component)) raise ArgumentError, "resource2config() no longer accpts components" comp = resources.shift comp.delve else config = Puppet::Resource::Catalog.new resources.each { |res| config.add_resource res } end return config end # stop any services that might be hanging around def stopservices end # TODO: rewrite this to use the 'etc' module. # Define a variable that contains the name of my user. def setme # retrieve the user name id = %x{id}.chomp if id =~ /uid=\d+\(([^\)]+)\)/ @me = $1 else puts id end unless defined? @me raise "Could not retrieve user name; 'id' did not work" end end # Define a variable that contains a group I'm in. def set_mygroup # retrieve the user name group = %x{groups}.chomp.split(/ /)[0] unless group raise "Could not find group to set in @mygroup" end @mygroup = group end def run_events(type, trans, events, msg) case type - when :evaluate, :rollback: # things are hunky-dory + when :evaluate, :rollback # things are hunky-dory else raise Puppet::DevError, "Incorrect run_events type" end method = type newevents = nil assert_nothing_raised("Transaction %s %s failed" % [type, msg]) { newevents = trans.send(method).reject { |e| e.nil? }.collect { |e| e.name } } assert_equal(events, newevents, "Incorrect %s %s events" % [type, msg]) return trans end # If there are any fake data files, retrieve them def fakedata(dir) ary = [basedir, "test"] ary += dir.split("/") dir = File.join(ary) unless FileTest.exists?(dir) raise Puppet::DevError, "No fakedata dir %s" % dir end files = Dir.entries(dir).reject { |f| f =~ /^\./ }.collect { |f| File.join(dir, f) } return files end def fakefile(name) ary = [PuppetTest.basedir, "test"] ary += name.split("/") file = File.join(ary) unless FileTest.exists?(file) raise Puppet::DevError, "No fakedata file %s" % file end return file end # wrap how to retrieve the masked mode def filemode(file) File.stat(file).mode & 007777 end def memory Puppet::Util.memory end # a list of files that we can parse for testing def textfiles textdir = datadir "snippets" Dir.entries(textdir).reject { |f| f =~ /^\./ or f =~ /fail/ }.each { |f| yield File.join(textdir, f) } end def failers textdir = datadir "failers" # only parse this one file now files = Dir.entries(textdir).reject { |file| file =~ %r{\.swp} }.reject { |file| file =~ %r{\.disabled} }.collect { |file| File.join(textdir,file) }.find_all { |file| FileTest.file?(file) }.sort.each { |file| Puppet.debug "Processing %s" % file yield file } end def mk_catalog(*resources) if resources[0].is_a?(String) name = resources.shift else name = :testing end config = Puppet::Resource::Catalog.new :testing do |conf| resources.each { |resource| conf.add_resource resource } end return config end end module PuppetTest include PuppetTest::Support::Utils end diff --git a/test/network/handler/fileserver.rb b/test/network/handler/fileserver.rb index 9bd26ac07..5158f51ca 100755 --- a/test/network/handler/fileserver.rb +++ b/test/network/handler/fileserver.rb @@ -1,1184 +1,1184 @@ #!/usr/bin/env ruby require File.dirname(__FILE__) + '/../../lib/puppettest' require 'puppettest' require 'puppet/network/handler/fileserver' class TestFileServer < Test::Unit::TestCase include PuppetTest def mkmount(path = nil) mount = nil name = "yaytest" base = path || tempfile() unless FileTest.exists?(base) Dir.mkdir(base) end # Create a test file File.open(File.join(base, "file"), "w") { |f| f.puts "bazoo" } assert_nothing_raised { mount = Puppet::Network::Handler.fileserver::Mount.new(name, base) } return mount end # make a simple file source def mktestdir testdir = File.join(tmpdir(), "remotefilecopytesting") @@tmpfiles << testdir # create a tmpfile pattern = "tmpfile" tmpfile = File.join(testdir, pattern) assert_nothing_raised { Dir.mkdir(testdir) File.open(tmpfile, "w") { |f| 3.times { f.puts rand(100) } } } return [testdir, %r{#{pattern}}, tmpfile] end # make a bunch of random test files def mktestfiles(testdir) @@tmpfiles << testdir assert_nothing_raised { files = %w{a b c d e}.collect { |l| name = File.join(testdir, "file%s" % l) File.open(name, "w") { |f| f.puts rand(100) } name } return files } end def assert_describe(base, file, server) file = File.basename(file) assert_nothing_raised { desc = server.describe(base + file) assert(desc, "Got no description for %s" % file) assert(desc != "", "Got no description for %s" % file) assert_match(/^\d+/, desc, "Got invalid description %s" % desc) } end # test for invalid names def test_namefailures server = nil assert_nothing_raised { server = Puppet::Network::Handler.fileserver.new( :Local => true, :Config => false ) } [" ", "=" "+", "&", "#", "*"].each do |char| assert_raise(Puppet::Network::Handler::FileServerError, "'%s' did not throw a failure in fileserver module names" % char) { server.mount("/tmp", "invalid%sname" % char) } end end # verify that listing the root behaves as expected def test_listroot server = nil testdir, pattern, tmpfile = mktestdir() file = nil checks = Puppet::Network::Handler.fileserver::CHECKPARAMS # and make our fileserver assert_nothing_raised { server = Puppet::Network::Handler.fileserver.new( :Local => true, :Config => false ) } # mount the testdir assert_nothing_raised { server.mount(testdir, "test") } # and verify different iterations of 'root' return the same value list = nil assert_nothing_raised { list = server.list("/test/", :manage, true, false) } assert(list =~ pattern) assert_nothing_raised { list = server.list("/test", :manage, true, false) } assert(list =~ pattern) end # test listing individual files def test_getfilelist server = nil testdir, pattern, tmpfile = mktestdir() file = nil assert_nothing_raised { server = Puppet::Network::Handler.fileserver.new( :Local => true, :Config => false ) } assert_nothing_raised { server.mount(testdir, "test") } # get our listing list = nil sfile = "/test/tmpfile" assert_nothing_raised { list = server.list(sfile, :manage, true, false) } output = "/\tfile" # verify it got listed as a file assert_equal(output, list) # verify we got all fields assert(list !~ /\t\t/) # verify that we didn't get the directory itself list.split("\n").each { |line| assert(line !~ %r{remotefile}) } # and then verify that the contents match contents = File.read(tmpfile) ret = nil assert_nothing_raised { ret = server.retrieve(sfile) } assert_equal(contents, ret) end # check that the fileserver is seeing newly created files def test_seenewfiles server = nil testdir, pattern, tmpfile = mktestdir() newfile = File.join(testdir, "newfile") # go through the whole schtick again... file = nil checks = Puppet::Network::Handler.fileserver::CHECKPARAMS assert_nothing_raised { server = Puppet::Network::Handler.fileserver.new( :Local => true, :Config => false ) } assert_nothing_raised { server.mount(testdir, "test") } list = nil sfile = "/test/" assert_nothing_raised { list = server.list(sfile, :manage, true, false) } # create the new file File.open(newfile, "w") { |f| 3.times { f.puts rand(100) } } newlist = nil assert_nothing_raised { newlist = server.list(sfile, :manage, true, false) } # verify the list has changed assert(list != newlist) # and verify that we are specifically seeing the new file assert(newlist =~ /newfile/) end # verify we can mount /, which is what local file servers will # normally do def test_mountroot server = nil assert_nothing_raised { server = Puppet::Network::Handler.fileserver.new( :Local => true, :Config => false ) } assert_nothing_raised { server.mount("/", "root") } testdir, pattern, tmpfile = mktestdir() list = nil assert_nothing_raised { list = server.list("/root/" + testdir, :manage, true, false) } assert(list =~ pattern) assert_nothing_raised { list = server.list("/root" + testdir, :manage, true, false) } assert(list =~ pattern) end # verify that we're correctly recursing the right number of levels def test_recursionlevels server = nil assert_nothing_raised { server = Puppet::Network::Handler.fileserver.new( :Local => true, :Config => false ) } # make our deep recursion basedir = File.join(tmpdir(), "recurseremotetesting") testdir = "%s/with/some/sub/directories/for/the/purposes/of/testing" % basedir oldfile = File.join(testdir, "oldfile") assert_nothing_raised { system("mkdir -p %s" % testdir) File.open(oldfile, "w") { |f| 3.times { f.puts rand(100) } } @@tmpfiles << basedir } assert_nothing_raised { server.mount(basedir, "test") } # get our list list = nil assert_nothing_raised { list = server.list("/test/with", :manage, false, false) } # make sure we only got one line, since we're not recursing assert(list !~ /\n/) # for each level of recursion, make sure we get the right list [0, 1, 2].each { |num| assert_nothing_raised { list = server.list("/test/with", :manage, num, false) } count = 0 while list =~ /\n/ list.sub!(/\n/, '') count += 1 end assert_equal(num, count) } end # verify that we're not seeing the dir we ask for; i.e., that our # list is relative to that dir, not it's parent dir def test_listedpath server = nil assert_nothing_raised { server = Puppet::Network::Handler.fileserver.new( :Local => true, :Config => false ) } # create a deep dir basedir = tempfile() testdir = "%s/with/some/sub/directories/for/testing" % basedir oldfile = File.join(testdir, "oldfile") assert_nothing_raised { system("mkdir -p %s" % testdir) File.open(oldfile, "w") { |f| 3.times { f.puts rand(100) } } @@tmpfiles << basedir } # mounty mounty assert_nothing_raised { server.mount(basedir, "localhost") } list = nil # and then check a few dirs assert_nothing_raised { list = server.list("/localhost/with", :manage, false, false) } assert(list !~ /with/) assert_nothing_raised { list = server.list("/localhost/with/some/sub", :manage, true, false) } assert(list !~ /sub/) end # test many dirs, not necessarily very deep def test_widelists server = nil assert_nothing_raised { server = Puppet::Network::Handler.fileserver.new( :Local => true, :Config => false ) } basedir = tempfile() dirs = %w{a set of directories} assert_nothing_raised { Dir.mkdir(basedir) dirs.each { |dir| Dir.mkdir(File.join(basedir, dir)) } @@tmpfiles << basedir } assert_nothing_raised { server.mount(basedir, "localhost") } list = nil assert_nothing_raised { list = server.list("/localhost/", :manage, 1, false) } assert_instance_of(String, list, "Server returned %s instead of string") list = list.split("\n") assert_equal(dirs.length + 1, list.length) end # verify that 'describe' works as advertised def test_describe server = nil testdir = tstdir() files = mktestfiles(testdir) file = nil checks = Puppet::Network::Handler.fileserver::CHECKPARAMS assert_nothing_raised { server = Puppet::Network::Handler.fileserver.new( :Local => true, :Config => false ) } assert_nothing_raised { server.mount(testdir, "test") } # get our list list = nil sfile = "/test/" assert_nothing_raised { list = server.list(sfile, :manage, true, false) } # and describe each file in the list assert_nothing_raised { list.split("\n").each { |line| file, type = line.split("\t") desc = server.describe(sfile + file) } } # and then make sure we can describe everything that we know is there files.each { |file| assert_describe(sfile, file, server) } # And then describe some files that we know aren't there retval = nil assert_nothing_raised("Describing non-existent files raised an error") { retval = server.describe(sfile + "noexisties") } assert_equal("", retval, "Description of non-existent files returned a value") # Now try to describe some sources that don't even exist retval = nil assert_raise(Puppet::Network::Handler::FileServerError, "Describing non-existent mount did not raise an error") { retval = server.describe("/notmounted/" + "noexisties") } assert_nil(retval, "Description of non-existent mounts returned a value") end def test_describe_does_not_fail_when_mount_does_not_find_file server = Puppet::Network::Handler.fileserver.new(:Local => true, :Config => false) assert_nothing_raised("Failed when describing missing plugins") do server.describe "/plugins" end end # test that our config file is parsing and working as planned def test_configfile server = nil basedir = File.join(tmpdir, "fileserverconfigfiletesting") @@tmpfiles << basedir # make some dirs for mounting Dir.mkdir(basedir) mounts = {} %w{thing thus the-se those}.each { |dir| path = File.join(basedir, dir) Dir.mkdir(path) mounts[dir] = mktestfiles(path) } # create an example file with each of them conffile = tempfile @@tmpfiles << conffile File.open(conffile, "w") { |f| f.print "# a test config file [thing] path #{basedir}/thing allow 192.168.0.* [thus] path #{basedir}/thus allow *.madstop.com, *.kanies.com deny *.sub.madstop.com [the-se] path #{basedir}/the-se [those] path #{basedir}/those " } # create a server with the file assert_nothing_raised { server = Puppet::Network::Handler.fileserver.new( :Local => false, :Config => conffile ) } list = nil # run through once with no host/ip info, to verify everything is working mounts.each { |mount, files| mount = "/#{mount}/" assert_nothing_raised { list = server.list(mount, :manage, true, false) } assert_nothing_raised { list.split("\n").each { |line| file, type = line.split("\t") desc = server.describe(mount + file) } } files.each { |f| assert_describe(mount, f, server) } } # now let's check that things are being correctly forbidden # this is just a map of names and expected results { "thing" => { :deny => [ ["hostname.com", "192.168.1.0"], ["hostname.com", "192.158.0.0"] ], :allow => [ ["hostname.com", "192.168.0.0"], ["hostname.com", "192.168.0.245"], ] }, "thus" => { :deny => [ ["hostname.com", "192.168.1.0"], ["name.sub.madstop.com", "192.158.0.0"] ], :allow => [ ["luke.kanies.com", "192.168.0.0"], ["luke.madstop.com", "192.168.0.245"], ] } }.each { |mount, hash| mount = "/#{mount}/" # run through the map hash.each { |type, ary| ary.each { |sub| host, ip = sub case type - when :deny: + when :deny assert_raise(Puppet::AuthorizationError, "Host %s, ip %s, allowed %s" % [host, ip, mount]) { list = server.list(mount, :manage, true, false, host, ip) } - when :allow: + when :allow assert_nothing_raised("Host %s, ip %s, denied %s" % [host, ip, mount]) { list = server.list(mount, :manage, true, false, host, ip) } end } } } end # Test that we smoothly handle invalid config files def test_configfailures # create an example file with each of them conffile = tempfile() invalidmounts = { "noexist" => "[noexist] path /this/path/does/not/exist allow 192.168.0.* " } invalidconfigs = [ "[not valid] path /this/path/does/not/exist allow 192.168.0.* ", "[valid] invalidstatement path /etc allow 192.168.0.* ", "[valid] allow 192.168.0.* " ] invalidmounts.each { |mount, text| File.open(conffile, "w") { |f| f.print text } # create a server with the file server = nil assert_nothing_raised { server = Puppet::Network::Handler.fileserver.new( :Local => true, :Config => conffile ) } assert_raise(Puppet::Network::Handler::FileServerError, "Invalid mount was mounted") { server.list(mount, :manage) } } invalidconfigs.each_with_index { |text, i| File.open(conffile, "w") { |f| f.print text } # create a server with the file server = nil assert_raise(Puppet::Network::Handler::FileServerError, "Invalid config %s did not raise error" % i) { server = Puppet::Network::Handler.fileserver.new( :Local => true, :Config => conffile ) } } end # verify we reread the config file when it changes def test_filereread server = nil conffile = tempfile() dir = tstdir() files = mktestfiles(dir) File.open(conffile, "w") { |f| f.print "# a test config file [thing] path #{dir} allow test1.domain.com " } # Reset the timeout, so we reload faster Puppet[:filetimeout] = 0.5 # start our server with a fast timeout assert_nothing_raised { server = Puppet::Network::Handler.fileserver.new( :Local => false, :Config => conffile ) } list = nil assert_nothing_raised { list = server.list("/thing/", :manage, false, false, "test1.domain.com", "127.0.0.1") } assert(list != "", "List returned nothing in rereard test") assert_raise(Puppet::AuthorizationError, "List allowed invalid host") { list = server.list("/thing/", :manage, false, false, "test2.domain.com", "127.0.0.1") } sleep 1 File.open(conffile, "w") { |f| f.print "# a test config file [thing] path #{dir} allow test2.domain.com " } assert_raise(Puppet::AuthorizationError, "List allowed invalid host") { list = server.list("/thing/", :manage, false, false, "test1.domain.com", "127.0.0.1") } assert_nothing_raised { list = server.list("/thing/", :manage, false, false, "test2.domain.com", "127.0.0.1") } assert(list != "", "List returned nothing in rereard test") list = nil end # Verify that we get converted to the right kind of string def test_mountstring mount = nil name = "yaytest" path = tmpdir() assert_nothing_raised { mount = Puppet::Network::Handler.fileserver::Mount.new(name, path) } assert_equal("mount[#{name}]", mount.to_s) end def test_servinglinks # Disable the checking, so changes propagate immediately. Puppet[:filetimeout] = -5 server = nil source = tempfile() file = File.join(source, "file") link = File.join(source, "link") Dir.mkdir(source) File.open(file, "w") { |f| f.puts "yay" } File.symlink(file, link) assert_nothing_raised { server = Puppet::Network::Handler.fileserver.new( :Local => true, :Config => false ) } assert_nothing_raised { server.mount(source, "mount") } # First describe the link when following results = {} assert_nothing_raised { server.describe("/mount/link", :follow).split("\t").zip( Puppet::Network::Handler.fileserver::CHECKPARAMS ).each { |v,p| results[p] = v } } assert_equal("file", results[:type]) # Then not results = {} assert_nothing_raised { server.describe("/mount/link", :manage).split("\t").zip( Puppet::Network::Handler.fileserver::CHECKPARAMS ).each { |v,p| results[p] = v } } assert_equal("link", results[:type]) results.each { |p,v| assert(v, "%s has no value" % p) assert(v != "", "%s has no value" % p) } end # Test that substitution patterns in the path are exapanded # properly. Disabled, because it was testing too much of the process # and in a non-portable way. This is a thorough enough test that it should # be kept, but it should be done in a way that is clearly portable (e.g., # no md5 sums of file paths). def test_host_specific client1 = "client1.example.com" client2 = "client2.example.com" ip = "127.0.0.1" # Setup a directory hierarchy for the tests fsdir = File.join(tmpdir(), "host-specific") @@tmpfiles << fsdir hostdir = File.join(fsdir, "host") fqdndir = File.join(fsdir, "fqdn") client1_hostdir = File.join(hostdir, "client1") client2_fqdndir = File.join(fqdndir, client2) contents = { client1_hostdir => "client1\n", client2_fqdndir => client2 + "\n" } [fsdir, hostdir, fqdndir, client1_hostdir, client2_fqdndir].each { |d| Dir.mkdir(d) } [client1_hostdir, client2_fqdndir].each do |d| File.open(File.join(d, "file.txt"), "w") do |f| f.print contents[d] end end conffile = tempfile() File.open(conffile, "w") do |f| f.print(" [host] path #{hostdir}/%h allow * [fqdn] path #{fqdndir}/%H allow * ") end server = nil assert_nothing_raised { server = Puppet::Network::Handler.fileserver.new( :Local => true, :Config => conffile ) } # check that list returns the correct thing for the two clients list = nil sfile = "/host/file.txt" assert_nothing_raised { list = server.list(sfile, :manage, true, false, client1, ip) } assert_equal("/\tfile", list) assert_nothing_raised { list = server.list(sfile, :manage, true, false, client2, ip) } assert_equal("", list) sfile = "/fqdn/file.txt" assert_nothing_raised { list = server.list(sfile, :manage, true, false, client1, ip) } assert_equal("", list) assert_nothing_raised { list = server.list(sfile, :manage, true, false, client2, ip) } assert_equal("/\tfile", list) # check describe sfile = "/host/file.txt" assert_nothing_raised { list = server.describe(sfile, :manage, client1, ip).split("\t") } assert_equal(5, list.size) assert_equal("file", list[1]) md5 = Digest::MD5.hexdigest(contents[client1_hostdir]) assert_equal("{md5}#{md5}", list[4]) assert_nothing_raised { list = server.describe(sfile, :manage, client2, ip).split("\t") } assert_equal([], list) sfile = "/fqdn/file.txt" assert_nothing_raised { list = server.describe(sfile, :manage, client1, ip).split("\t") } assert_equal([], list) assert_nothing_raised { list = server.describe(sfile, :manage, client2, ip).split("\t") } assert_equal(5, list.size) assert_equal("file", list[1]) md5 = Digest::MD5.hexdigest(contents[client2_fqdndir]) assert_equal("{md5}#{md5}", list[4]) # Check retrieve sfile = "/host/file.txt" assert_nothing_raised { list = server.retrieve(sfile, :manage, client1, ip).chomp } assert_equal(contents[client1_hostdir].chomp, list) assert_nothing_raised { list = server.retrieve(sfile, :manage, client2, ip).chomp } assert_equal("", list) sfile = "/fqdn/file.txt" assert_nothing_raised { list = server.retrieve(sfile, :manage, client1, ip).chomp } assert_equal("", list) assert_nothing_raised { list = server.retrieve(sfile, :manage, client2, ip).chomp } assert_equal(contents[client2_fqdndir].chomp, list) end # Make sure the 'subdir' method in Mount works. def test_mount_subdir mount = nil base = tempfile() Dir.mkdir(base) subdir = File.join(base, "subdir") Dir.mkdir(subdir) [base, subdir].each do |d| File.open(File.join(d, "file"), "w") { |f| f.puts "bazoo" } end mount = mkmount(base) assert_equal(base, mount.subdir(), "Did not default to base path") assert_equal(subdir, mount.subdir("subdir"), "Did not default to base path") end # Make sure mounts get correctly marked expandable or not, depending on # the path. def test_expandable name = "yaytest" dir = tempfile() Dir.mkdir(dir) mount = mkmount() assert_nothing_raised { mount.path = dir } assert(! mount.expandable?, "Mount incorrectly called expandable") assert_nothing_raised { mount.path = "/dir/a%a" } assert(mount.expandable?, "Mount not called expandable") # This isn't a valid replacement pattern, so it should throw an error # because the dir doesn't exist assert_raise(Puppet::Network::Handler::FileServerError) { mount.path = "/dir/a%" } # Now send it back to a normal path assert_nothing_raised { mount.path = dir } # Make sure it got reverted assert(! mount.expandable?, "Mount incorrectly called expandable") end def test_mount_expand mount = mkmount() check = proc do |client, pattern, repl| path = "/my/#{pattern}/file" assert_equal("/my/#{repl}/file", mount.expand(path, client)) end # Do a round of checks with a fake client client = "host.domain.com" {"%h" => "host", # Short name "%H" => client, # Full name "%d" => "domain.com", # domain "%%" => "%", # escape "%o" => "%o" # other }.each do |pat, repl| result = check.call(client, pat, repl) end # Now, check that they use Facter info Puppet.notice "The following messages are normal" client = nil Facter.stubs(:value).with(:ipaddress).returns("127.0.0.1") Facter.stubs(:value).with { |v| v.to_s == "hostname" }.returns("myhost") Facter.stubs(:value).with { |v| v.to_s == "domain" }.returns("mydomain.com") Facter.stubs(:value).with(:domain).returns("mydomain.com") {"%h" => "myhost", # Short name "%H" => "myhost.mydomain.com", # Full name "%d" => "mydomain.com", # domain "%%" => "%", # escape "%o" => "%o" # other }.each do |pat, repl| check.call(client, pat, repl) end end # Test that the fileserver expands the %h and %d things. def test_fileserver_expansion server = nil assert_nothing_raised { server = Puppet::Network::Handler.fileserver.new( :Local => true, :Config => false ) } dir = tempfile() # When mocks attack, part 2 kernel_fact = Facter.value(:kernel) Facter.stubs(:value).with(:kernel).returns(kernel_fact) Facter.stubs(:value).with(:ipaddress).returns("127.0.0.1") Facter.stubs(:value).with { |v| v.to_s == "hostname" }.returns("myhost") Facter.stubs(:value).with { |v| v.to_s == "domain" }.returns("mydomain.com") Facter.stubs(:value).with(:domain).returns("mydomain.com") ip = Facter.value(:ipaddress) Dir.mkdir(dir) host = "myhost.mydomain.com" { "%H" => "myhost.mydomain.com", "%h" => "myhost", "%d" => "mydomain.com" }.each do |pattern, string| file = File.join(dir, string) mount = File.join(dir, pattern) File.open(file, "w") do |f| f.puts "yayness: %s" % string end name = "name" obj = nil assert_nothing_raised { obj = server.mount(mount, name) } obj.allow "*" ret = nil assert_nothing_raised do ret = server.list("/name", :manage, false, false, host, ip) end assert_equal("/\tfile", ret) assert_nothing_raised do ret = server.describe("/name", :manage, host, ip) end assert(ret =~ /\tfile\t/, "Did not get valid a description (#{ret.inspect})") assert_nothing_raised do ret = server.retrieve("/name", :manage, host, ip) end assert_equal(ret, File.read(file)) server.umount(name) File.unlink(file) end end # Test the default modules fileserving def test_modules_default moddir = tempfile Dir.mkdir(moddir) mounts = {} Puppet[:modulepath] = moddir mods = %w{green red}.collect do |name| path = File::join(moddir, name, Puppet::Module::FILES) FileUtils::mkdir_p(path) if name == "green" file = File::join(path, "test.txt") File::open(file, "w") { |f| f.print name } end Puppet::Module::find(name) end conffile = tempfile File.open(conffile, "w") { |f| f.puts "# a test config file" } # create a server with the file server = nil assert_nothing_raised { server = Puppet::Network::Handler::FileServer.new( :Local => false , :Config => conffile ) } mods.each do |mod| mount = "/#{mod.name}/" list = nil assert_nothing_raised { list = server.list(mount, :manage, true, false) } list = list.split("\n") if mod.name == "green" assert_equal(2, list.size) assert_equal("/\tdirectory", list[0]) assert_equal("/test.txt\tfile", list[1]) else assert_equal(1, list.size) assert_equal("/\tdirectory", list[0]) end assert_nothing_raised("Host 'allow' denied #{mount}") { server.list(mount, :manage, true, false, 'allow.example.com', "192.168.0.1") } end end # Test that configuring deny/allow for modules works def test_modules_config moddir = tempfile Dir.mkdir(moddir) mounts = {} Puppet[:modulepath] = moddir path = File::join(moddir, "amod", Puppet::Module::FILES) file = File::join(path, "test.txt") FileUtils::mkdir_p(path) File::open(file, "w") { |f| f.print "Howdy" } mod = Puppet::Module::find("amod") conffile = tempfile @@tmpfiles << conffile File.open(conffile, "w") { |f| f.print "# a test config file [modules] path #{basedir}/thing allow 192.168.0.* " } # create a server with the file server = nil assert_nothing_raised { server = Puppet::Network::Handler::FileServer.new( :Local => false, :Config => conffile ) } list = nil mount = "/#{mod.name}/" assert_nothing_raised { list = server.list(mount, :manage, true, false) } assert_nothing_raised { list.split("\n").each { |line| file, type = line.split("\t") server.describe(mount + file) } } assert_describe(mount, file, server) # now let's check that things are being correctly forbidden assert_raise(Puppet::AuthorizationError, "Host 'deny' allowed #{mount}") { server.list(mount, :manage, true, false, 'deny.example.com', "192.168.1.1") } assert_nothing_raised("Host 'allow' denied #{mount}") { server.list(mount, :manage, true, false, 'allow.example.com', "192.168.0.1") } end # Make sure we successfully throw errors -- someone ran into this with # 0.22.4. def test_failures # create a server with the file server = nil config = tempfile [ "[this is invalid]\nallow one.two.com", # invalid name "[valid]\nallow *.testing something.com", # invalid allow "[valid]\nallow one.two.com\ndeny *.testing something.com", # invalid deny ].each do |failer| File.open(config, "w") { |f| f.puts failer } assert_raise(Puppet::Network::Handler::FileServerError, "Did not fail on %s" % failer.inspect) { server = Puppet::Network::Handler::FileServer.new( :Local => false, :Config => config ) } end end def test_can_start_without_configuration Puppet[:fileserverconfig] = tempfile assert_nothing_raised("Could not create fileserver when configuration is absent") do server = Puppet::Network::Handler::FileServer.new( :Local => false ) end end def test_creates_default_mounts_when_no_configuration_is_available Puppet[:fileserverconfig] = tempfile server = Puppet::Network::Handler::FileServer.new(:Local => false) assert(server.mounted?("plugins"), "Did not create default plugins mount when missing configuration file") assert(server.mounted?("modules"), "Did not create default modules mount when missing configuration file") end end diff --git a/test/rails/host.rb b/test/rails/host.rb index 79f0ae398..a7b17c3ae 100755 --- a/test/rails/host.rb +++ b/test/rails/host.rb @@ -1,154 +1,154 @@ #!/usr/bin/env ruby require File.dirname(__FILE__) + '/../lib/puppettest' require 'puppet' require 'puppet/rails' require 'puppet/parser/interpreter' require 'puppet/parser/parser' require 'puppet/network/client' require 'puppettest' require 'puppettest/parsertesting' require 'puppettest/resourcetesting' require 'puppettest/railstesting' class TestRailsHost < PuppetTest::TestCase confine "Missing ActiveRecord" => Puppet.features.rails? include PuppetTest::ParserTesting include PuppetTest::ResourceTesting include PuppetTest::RailsTesting def setup super railsinit if Puppet.features.rails? end def teardown railsteardown if Puppet.features.rails? super end def test_includerails assert_nothing_raised { require 'puppet/rails' } end def test_store @scope = mkscope # First make some objects resources = [] 4.times { |i| # Make a file resources << mkresource(:type => "file", :title => "/tmp/file#{i.to_s}", :params => {:owner => "user#{i}"}) # And an exec, so we're checking multiple types resources << mkresource(:type => "exec", :title => "/bin/echo file#{i.to_s}", :params => {:user => "user#{i}"}) } # Now collect our facts facts = {"hostname" => "myhost", "test1" => "funtest", "ipaddress" => "192.168.0.1"} # Now try storing our crap host = nil node = mknode(facts["hostname"]) node.parameters = facts assert_nothing_raised { host = Puppet::Rails::Host.store(node, resources) } assert(host, "Did not create host") host = nil assert_nothing_raised { host = Puppet::Rails::Host.find_by_name(facts["hostname"]) } assert(host, "Could not find host object") assert(host.resources, "No objects on host") facts.each do |fact, value| assert_equal(value, host.fact(fact)[0].value, "fact %s is wrong" % fact) end assert_equal(facts["ipaddress"], host.ip, "IP did not get set") count = 0 host.resources.each do |resource| assert_equal(host, resource.host) count += 1 i = nil if resource[:title] =~ /file([0-9]+)/ i = $1 else raise "Got weird resource %s" % resource.inspect end assert(resource[:restype] != "", "Did not get a type from the resource") case resource["restype"] - when "File": + when "File" assert_equal("user#{i}", resource.parameter("owner"), "got no owner for %s" % resource.ref) - when "Exec": + when "Exec" assert_equal("user#{i}", resource.parameter("user"), "got no user for %s" % resource.ref) else raise "Unknown type %s" % resource[:restype].inspect end end assert_equal(8, count, "Did not get enough resources") # Now remove a couple of resources resources.reject! { |r| r.title =~ /file3/ } # Change a few resources resources.find_all { |r| r.title =~ /file2/ }.each do |r| r.send(:set_parameter, "loglevel", "notice") end # And add a new resource resources << mkresource(:type => "file", :title => "/tmp/file_added", :params => {:owner => "user_added"}) # And change some facts facts["test2"] = "yaytest" facts["test3"] = "funtest" facts["test1"] = "changedfact" facts.delete("ipaddress") node = mknode(facts["hostname"]) node.parameters = facts newhost = nil assert_nothing_raised { newhost = Puppet::Rails::Host.store(node, resources) } assert_equal(host.id, newhost.id, "Created new host instance)") # Make sure it sets the last_compile time assert_nothing_raised do assert_instance_of(Time, host.last_compile, "did not set last_compile") end assert_equal(0, host.fact('ipaddress').size, "removed fact was not deleted") facts.each do |fact, value| assert_equal(value, host.fact(fact)[0].value, "fact %s is wrong" % fact) end # And check the changes we made. assert(! host.resources.find(:all).detect { |r| r.title =~ /file3/ }, "Removed resources are still present") res = host.resources.find_by_title("/tmp/file_added") assert(res, "New resource was not added") assert_equal("user_added", res.parameter("owner"), "user info was not stored") host.resources.find(:all, :conditions => [ "title like ?", "%file2%"]).each do |r| assert_equal("notice", r.parameter("loglevel"), "loglevel was not added") end end end diff --git a/test/ral/manager/attributes.rb b/test/ral/manager/attributes.rb index 05f8bc97f..ab5ba85d1 100755 --- a/test/ral/manager/attributes.rb +++ b/test/ral/manager/attributes.rb @@ -1,295 +1,295 @@ #!/usr/bin/env ruby # # Created by Luke A. Kanies on 2007-02-05. # Copyright (c) 2007. All rights reserved. require File.dirname(__FILE__) + '/../../lib/puppettest' require 'puppettest' require 'mocha' class TestTypeAttributes < Test::Unit::TestCase include PuppetTest def mktype type = Puppet::Type.newtype(:faketype) {} cleanup { Puppet::Type.rmtype(:faketype) } type end def test_bracket_methods type = mktype # make a namevar type.newparam(:name) {} # make a property type.newproperty(:property) {} # and a param type.newparam(:param) inst = type.new(:name => "yay") # Make sure we can set each of them, including a metaparam [:param, :property, :noop].each do |param| assert_nothing_raised("Failed to set symbol") do inst[param] = true end assert_nothing_raised("Failed to set string") do inst[param.to_s] = true end if param == :property assert(inst.property(param), "did not get obj for %s" % param) assert_equal(true, inst.should(param), "should value did not get set") else assert_equal(true, inst[param], "did not get correct value for %s from symbol" % param) assert_equal(true, inst[param.to_s], "did not get correct value for %s from string" % param) end end end def test_properties type = mktype # make a namevar type.newparam(:name) {} # make a couple of properties props = [:one, :two, :three] props.each do |prop| type.newproperty(prop) {} end inst = type.new(:name => "yay") inst[:one] = "boo" one = inst.property(:one) assert(one, "did not get obj for one") assert_equal([one], inst.send(:properties), "got wrong properties") inst[:three] = "rah" three = inst.property(:three) assert(three, "did not get obj for three") assert_equal([one, three], inst.send(:properties), "got wrong properties") inst[:two] = "whee" two = inst.property(:two) assert(two, "did not get obj for two") assert_equal([one, two, three], inst.send(:properties), "got wrong properties") end def attr_check(type) @num ||= 0 @num += 1 name = "name%s" % @num inst = type.new(:name => name) [:meta, :param, :prop].each do |name| klass = type.attrclass(name) assert(klass, "did not get class for %s" % name) obj = yield inst, klass assert_instance_of(klass, obj, "did not get object back") assert_equal("value", inst.value(klass.name), "value was not correct from value method") assert_equal("value", obj.value, "value was not correct") end end def test_newattr type = mktype type.newparam(:name) {} # Make one of each param type { :meta => :newmetaparam, :param => :newparam, :prop => :newproperty }.each do |name, method| assert_nothing_raised("Could not make %s of type %s" % [name, method]) do type.send(method, name) {} end end # Now set each of them attr_check(type) do |inst, klass| inst.newattr(klass.name, :value => "value") end # Now try it passing the class in attr_check(type) do |inst, klass| inst.newattr(klass, :value => "value") end # Lastly, make sure we can create and then set, separately attr_check(type) do |inst, klass| obj = inst.newattr(klass.name) assert_nothing_raised("Could not set value after creation") do obj.value = "value" end # Make sure we can't create a new param object assert_raise(Puppet::Error, "Did not throw an error when replacing attr") do inst.newattr(klass.name, :value => "yay") end obj end end def test_alias_parameter type = mktype type.newparam(:name) {} type.newparam(:one) {} type.newproperty(:two) {} aliases = { :three => :one, :four => :two } aliases.each do |new, old| assert_nothing_raised("Could not create alias parameter %s" % new) do type.set_attr_alias new => old end end aliases.each do |new, old| assert_equal(old, type.attr_alias(new), "did not return alias info for %s" % new) end assert_nil(type.attr_alias(:name), "got invalid alias info for name") inst = type.new(:name => "my name") assert(inst, "could not create instance") aliases.each do |new, old| val = "value %s" % new assert_nothing_raised do inst[new] = val end case old - when :one: # param + when :one # param assert_equal(val, inst[new], "Incorrect alias value for %s in []" % new) else assert_equal(val, inst.should(new), "Incorrect alias value for %s in should" % new) end assert_equal(val, inst.value(new), "Incorrect alias value for %s" % new) assert_equal(val, inst.value(old), "Incorrect orig value for %s" % old) end end # Make sure newattr handles required features correctly. def test_newattr_and_required_features # Make a type with some features type = mktype type.feature :fone, "Something" type.feature :ftwo, "Something else" type.newparam(:name) {} # Make three properties: one with no requirements, one with one, and one with two none = type.newproperty(:none) {} one = type.newproperty(:one, :required_features => :fone) {} two = type.newproperty(:two, :required_features => [:fone, :ftwo]) {} # Now make similar providers nope = type.provide(:nope) {} maybe = type.provide(:maybe) { has_feature :fone} yep = type.provide(:yep) { has_features :fone, :ftwo} attrs = [:none, :one, :two] # Now make sure that we get warnings and no properties in those cases where our providers do not support the features requested [nope, maybe, yep].each_with_index do |prov, i| resource = type.new(:provider => prov.name, :name => "test%s" % i, :none => "a", :one => "b", :two => "c") case prov.name - when :nope: + when :nope yes = [:none] no = [:one, :two] - when :maybe: + when :maybe yes = [:none, :one] no = [:two] - when :yep: + when :yep yes = [:none, :one, :two] no = [] end yes.each { |a| assert(resource.should(a), "Did not get value for %s in %s" % [a, prov.name]) } no.each do |a| assert_nil(resource.should(a), "Got value for unsupported %s in %s" % [a, prov.name]) if Puppet::Util::Log.sendlevel?(:info) assert(@logs.find { |l| l.message =~ /not managing attribute #{a}/ and l.level == :info }, "No warning about failed %s" % a) end end @logs.clear end end # Make sure the 'check' metaparam just ignores non-properties, rather than failing. def test_check_allows_parameters file = Puppet::Type.type(:file) klass = file.attrclass(:check) resource = file.new(:path => tempfile) inst = klass.new(:resource => resource) {:property => [:owner, :group], :parameter => [:ignore, :recurse], :metaparam => [:require, :subscribe]}.each do |attrtype, attrs| assert_nothing_raised("Could not set check to a single %s value" % attrtype) do inst.value = attrs[0] end if attrtype == :property assert(resource.property(attrs[0]), "Check did not create property instance during single check") end assert_nothing_raised("Could not set check to multiple %s values" % attrtype) do inst.value = attrs end if attrtype == :property assert(resource.property(attrs[1]), "Check did not create property instance during multiple check") end end # But make sure actually invalid attributes fail assert_raise(Puppet::Error, ":check did not fail on invalid attribute") do inst.value = :nosuchattr end end def test_check_ignores_unsupported_params type = Puppet::Type.newtype(:unsupported) do feature :nosuchfeat, "testing" newparam(:name) {} newproperty(:yep) {} newproperty(:nope, :required_features => :nosuchfeat) {} end $yep = :absent type.provide(:only) do def self.supports_parameter?(param) if param.name == :nope return false else return true end end def yep $yep end def yep=(v) $yep = v end end cleanup { Puppet::Type.rmtype(:unsupported) } obj = type.new(:name => "test", :check => :yep) obj.expects(:newattr).with(:nope).never obj[:check] = :all end end diff --git a/test/ral/providers/group.rb b/test/ral/providers/group.rb index 044545be1..e4ee82a74 100755 --- a/test/ral/providers/group.rb +++ b/test/ral/providers/group.rb @@ -1,251 +1,251 @@ #!/usr/bin/env ruby require File.dirname(__FILE__) + '/../../lib/puppettest' require 'puppettest' require 'etc' class TestGroupProvider < Test::Unit::TestCase include PuppetTest def setup super @@tmpgroups = [] @provider = nil assert_nothing_raised { @provider = Puppet::Type.type(:group).defaultprovider } assert(@provider, "Could not find default group provider") assert(@provider.name != :fake, "Got a fake provider") end def teardown super @@tmpgroups.each { |group| unless missing?(group) remove(group) end } end def mkgroup(name, hash = {}) fakeresource = stub 'group', :allowdupe? => false, :name => name fakeresource.stubs(:[]).returns nil fakeresource.stubs(:should).returns nil fakeresource.stubs(:[]).with(:name).returns name hash.each do |name, val| fakeresource.stubs(:should).with(name).returns val fakeresource.stubs(:[]).with(name).returns val end group = nil assert_nothing_raised { group = @provider.new(fakeresource) } assert(group, "Could not create provider group") return group end case Facter["operatingsystem"].value - when "Darwin": + when "Darwin" def missing?(group) output = %x{nidump -r /groups/#{group} / 2>/dev/null}.chomp if output == "" return true else return false end assert_equal("", output, "Group %s is present:\n%s" % [group, output]) end def gid(name) %x{nireport / /groups name gid}.split("\n").each { |line| group, id = line.chomp.split(/\s+/) assert(id =~ /^-?\d+$/, "Group id %s for %s is not a number" % [id.inspect, group]) if group == name return Integer(id) end } return nil end def remove(group) system("niutil -destroy / /groups/%s" % group) end else def missing?(group) begin obj = Etc.getgrnam(group) return false rescue ArgumentError return true end end def gid(name) assert_nothing_raised { obj = Etc.getgrnam(name) return obj.gid } return nil end def remove(group) system("groupdel %s" % group) end end def groupnames %x{groups}.chomp.split(/ /) end def groupids Process.groups end def attrtest_ensure(group) old = group.ensure assert_nothing_raised { group.delete } assert(!group.exists?, "Group was not deleted") assert_nothing_raised { group.create } assert(group.exists?, "Group was not created") unless old == :present assert_nothing_raised { group.delete } end end def attrtest_gid(group) old = gid(group.name) newgid = old while true newgid += 1 if newgid - old > 1000 $stderr.puts "Could not find extra test UID" return end begin Etc.getgrgid(newgid) rescue ArgumentError => detail break end end assert_nothing_raised("Failed to change group id") { group.gid = newgid } curgid = nil assert_nothing_raised { curgid = gid(group.name) } assert_equal(newgid, curgid, "GID was not changed") # Refresh group.getinfo(true) assert_equal(newgid, group.gid, "Object got wrong gid") assert_nothing_raised("Failed to change group id") { group.gid = old } end # Iterate over each of our groups and try to grab the gid. def test_ownprovidergroups groupnames().each { |group| gobj = nil comp = nil fakeresource = fakeresource(:group, group) assert_nothing_raised { gobj = @provider.new(fakeresource) } assert(gobj.gid, "Failed to retrieve gid") } end if Puppet::Util::SUIDManager.uid == 0 def test_mkgroup gobj = nil comp = nil name = "pptestgr" assert(missing?(name), "Group %s is still present" % name) group = mkgroup(name) @@tmpgroups << name assert(group.respond_to?(:addcmd), "no respondo?") assert_nothing_raised { group.create } assert(!missing?(name), "Group %s is missing" % name) tests = Puppet::Type.type(:group).validproperties tests.each { |test| if self.respond_to?("attrtest_%s" % test) self.send("attrtest_%s" % test, group) else $stderr.puts "Not testing attr %s of group" % test end } assert_nothing_raised { group.delete } end # groupadd -o is broken in FreeBSD. unless Facter["operatingsystem"].value == "FreeBSD" def test_duplicateIDs group1 = mkgroup("group1", :gid => 125) @@tmpgroups << "group1" @@tmpgroups << "group2" # Create the first group assert_nothing_raised { group1.create } # Not all OSes fail here, so we can't test that it doesn't work with # it off, only that it does work with it on. group2 = mkgroup("group2", :gid => 125) group2.resource.stubs(:allowdupe?).returns true # Now create the second group assert_nothing_raised { group2.create } assert_equal(:present, group2.ensure, "Group did not get created") end end else $stderr.puts "Not running as root; skipping group creation tests." end def test_autogen provider = nil group = Puppet::Type.type(:group).new(:name => nonrootgroup.name) provider = group.provider assert(provider, "did not get provider") # Everyone should be able to autogenerate a uid assert_instance_of(Fixnum, provider.autogen(:gid)) end end diff --git a/test/ral/providers/package.rb b/test/ral/providers/package.rb index 3b61c1abd..486078ed7 100755 --- a/test/ral/providers/package.rb +++ b/test/ral/providers/package.rb @@ -1,248 +1,248 @@ #!/usr/bin/env ruby require File.dirname(__FILE__) + '/../../lib/puppettest' require 'puppettest' require 'etc' class TestPackageProvider < Test::Unit::TestCase include PuppetTest def setup super Puppet.info @method_name end # Load the testpackages hash. def self.load_test_packages require 'yaml' file = File.join(PuppetTest.datadir(), "providers", "package", "testpackages.yaml") unless FileTest.exists?(file) raise "Could not find file %s" % file end array = YAML::load(File.read(file)).collect { |hash| # Stupid ruby 1.8.1. YAML is sometimes broken such that # symbols end up being strings with the : in them. hash.each do |name, value| if name.is_a?(String) and name =~ /^:/ hash.delete(name) name = name.sub(/^:/, '').intern hash[name] = value end if value.is_a?(String) and value =~ /^:/ hash[name] = value.sub(/^:/, '').intern end end } return array end def self.suitable_test_packages list = load_test_packages providers = {} Puppet::Type.type(:package).suitableprovider.each do |provider| providers[provider.name] = provider end facts = {} Facter.to_hash.each do |fact, value| facts[fact.to_s.downcase.intern] = value.to_s.downcase.intern end list.find_all { |hash| # First find the matching providers hash.include?(:provider) and providers.include?(hash[:provider]) }.reject { |hash| # Then find matching fact sets facts.detect do |fact, value| # We're detecting unmatched facts, but we also want to # delete the facts so they don't show up later. if fval = hash[fact] hash.delete(fact) fval = [fval] unless fval.is_a?(Array) fval = fval.collect { |v| v.downcase.intern } ! fval.include?(value) end end } end def assert_absent(provider, msg = "package not absent") result = nil assert_nothing_raised("Could not query provider") do result = provider.query end if result.nil? assert_nil(result) elsif result.is_a?(Hash) assert (result[:ensure] == :absent or result[:ensure] == :purged), msg else raise "dunno how to handle %s" % result.inspect end end def assert_not_absent(provider, msg = "package not installed") result = nil assert_nothing_raised("Could not query provider") do result = provider.query end assert((result == :listed or result.is_a?(Hash)), "query did not return hash or :listed") if result == :listed assert(provider.resource.is(:ensure) != :absent, msg) else assert(result[:ensure] != :absent, msg) end end # Run a package through all of its paces. FIXME This should use the # provider, not the package, duh. def run_package_installation_test(hash) # Turn the hash into a package if files = hash[:files] hash.delete(:files) if files.is_a?(Array) hash[:source] = files.shift else hash[:source] = files files = [] end else files = [] end if versions = hash[:versions] hash.delete(:versions) else versions = [] end # Start out by just making sure it's installed if versions.empty? hash[:ensure] = :present else hash[:ensure] = versions.shift end if hash[:source] unless FileTest.exists?(hash[:source]) $stderr.puts "Create a package at %s for testing" % hash[:source] return end end if cleancmd = hash[:cleanup] hash.delete(:cleanup) end pkg = nil assert_nothing_raised( "Could not turn %s into a package" % hash.inspect ) do pkg = Puppet::Type.newpackage(hash) end # Make any necessary modifications. modpkg(pkg) provider = pkg.provider assert(provider, "Could not retrieve provider") return if result = provider.query and ! [:absent, :purged].include?(result[:ensure]) assert_absent(provider) if Process.uid != 0 Puppet.info "Run as root for full package tests" return end cleanup do if pkg.provider.respond_to?(:uninstall) pkg.provider.flush if pkg.provider.properties[:ensure] != :absent assert_nothing_raised("Could not clean up package") do pkg.provider.uninstall end end else if cleancmd system(cleancmd) end end end # Now call 'latest' after the package is installed if provider.respond_to?(:latest) assert_nothing_raised("Could not call 'latest'") do provider.latest end end assert_nothing_raised("Could not install package") do provider.install end assert_not_absent(provider, "package did not install") # If there are any remaining files, then test upgrading from there unless files.empty? pkg[:source] = files.shift current = provider.properties assert_nothing_raised("Could not upgrade") do provider.update end provider.flush new = provider.properties assert(current != new, "package was not upgraded: %s did not change" % current.inspect) end unless versions.empty? pkg[:ensure] = versions.shift current = provider.properties assert_nothing_raised("Could not upgrade") do provider.update end provider.flush new = provider.properties assert(current != new, "package was not upgraded: %s did not change" % current.inspect) end # Now call 'latest' after the package is installed if provider.respond_to?(:latest) assert_nothing_raised("Could not call 'latest'") do provider.latest end end # Now remove the package if provider.respond_to?(:uninstall) assert_nothing_raised do provider.uninstall end assert_absent(provider) end end # Now create a separate test method for each package suitable_test_packages.each do |hash| mname = ["test", hash[:name].to_s, hash[:provider].to_s].join("_").intern if method_defined?(mname) warn "Already a test method defined for %s" % mname else define_method(mname) do run_package_installation_test(hash) end end end def modpkg(pkg) case pkg[:provider] - when :sun: + when :sun pkg[:adminfile] = "/usr/local/pkg/admin_file" end end end diff --git a/test/ral/providers/provider.rb b/test/ral/providers/provider.rb index 25f1d605f..4df67807f 100755 --- a/test/ral/providers/provider.rb +++ b/test/ral/providers/provider.rb @@ -1,529 +1,529 @@ #!/usr/bin/env ruby require File.dirname(__FILE__) + '/../../lib/puppettest' require 'puppettest' require 'facter' class TestProvider < Test::Unit::TestCase include PuppetTest def echo echo = Puppet::Util.binary("echo") unless echo raise "Could not find 'echo' binary; cannot complete test" end return echo end def newprovider # Create our provider provider = Class.new(Puppet::Provider) do @name = :fakeprovider end provider.initvars return provider end def setup super @type = Puppet::Type.newtype(:provider_test) do newparam(:name) {} ensurable end cleanup { Puppet::Type.rmtype(:provider_test) } end def test_confine_defaults_to_suitable provider = newprovider assert(provider.suitable?, "Marked unsuitable with no confines") end def test_confine_results { {:true => true} => true, {:true => false} => false, {:false => false} => true, {:false => true} => false, {:operatingsystem => Facter.value(:operatingsystem)} => true, {:operatingsystem => :yayness} => false, {:nothing => :yayness} => false, {:exists => echo} => true, {:exists => "/this/file/does/not/exist"} => false, }.each do |hash, result| provider = newprovider # First test :true hash.each do |test, val| assert_nothing_raised do provider.confine test => val end end assert_equal(result, provider.suitable?, "Failed for %s" % [hash.inspect]) provider.initvars end end def test_multiple_confines_do_not_override provider = newprovider # Make sure multiple confines don't overwrite each other provider.confine :true => false assert(! provider.suitable?) provider.confine :true => true assert(! provider.suitable?) end def test_one_failed_confine_is_sufficient provider = newprovider # Make sure we test multiple of them, and that a single false wins provider.confine :true => true, :false => false assert(provider.suitable?) provider.confine :true => false assert(! provider.suitable?) end # #1197 - the binary should not be def test_command_checks_for_binaries_each_time provider = newprovider provider.commands :testing => "/no/such/path" provider.stubs(:binary).returns "/no/such/path" provider.command(:testing) assert_equal("/no/such/path", provider.command(:testing), "Did not return correct binary path") end def test_command {:echo => "echo", :echo_with_path => echo, :missing => "nosuchcommand", :missing_qualified => "/path/to/nosuchcommand"}.each do |name, command| provider = newprovider assert_nothing_raised("Could not define command %s with argument %s for provider" % [name, command]) do provider.commands(name => command) end if name.to_s =~ /missing/ assert_nil(provider.command(name), "Somehow got a response for missing commands") assert(! provider.suitable?, "Provider was considered suitable with missing command") next # skip, since we don't do any validity checking here. end assert_equal(echo, provider.command(name), "Did not get correct path for echo") assert(provider.suitable?, "Provider was not considered suitable with 'echo'") # Now make sure they both work inst = provider.new(nil) [provider, inst].each do |thing| assert_nothing_raised("Could not call %s on %s" % [command, thing]) do out = thing.send(name, "some", "text") assert_equal("some text\n", out) end end assert(provider.suitable?, "Provider considered unsuitable") # Now add an invalid command assert_nothing_raised do provider.commands :fake => "nosuchcommanddefinitely" end assert(! provider.suitable?, "Provider considered suitable") assert_nil(provider.command(:fake), "Got a value for missing command") assert_raise(Puppet::Error) do provider.fake end Puppet[:trace] = false assert_raise(Puppet::DevError) do provider.command(:nosuchcmd) end # Lastly, verify that we can find our superclass commands newprov = Class.new(provider) newprov.initvars assert_equal(echo, newprov.command(name)) end end def test_default? provider = newprovider assert(! provider.default?, "Was considered default with no settings") assert_nothing_raised do provider.defaultfor :operatingsystem => Facter.value(:operatingsystem) end assert(provider.default?, "Was not considered default") # Make sure any true value is sufficient. assert_nothing_raised do provider.defaultfor :operatingsystem => [ :yayness, :rahness, Facter.value(:operatingsystem) ] end assert(provider.default?, "Was not considered default") # Now make sure that a random setting returns false. assert_nothing_raised do provider.defaultfor :operatingsystem => :yayness end assert(! provider.default?, "Was considered default") end # Make sure that failed commands get their output in the error. def test_outputonfailure provider = newprovider dir = tstdir() file = File.join(dir, "mycmd") sh = Puppet::Util.binary("sh") File.open(file, "w") { |f| f.puts %{#!#{sh} echo A Failure >&2 exit 2 } } File.chmod(0755, file) provider.commands :cmd => file inst = provider.new(nil) assert_raise(Puppet::ExecutionFailure) do inst.cmd "some", "arguments" end out = nil begin inst.cmd "some", "arguments" rescue Puppet::ExecutionFailure => detail out = detail.to_s end assert(out =~ /A Failure/, "Did not receive command output on failure") assert(out =~ /Execution of/, "Did not receive info wrapper on failure") end def test_mk_resource_methods prov = newprovider resourcetype = Struct.new(:validproperties, :parameters) m = resourcetype.new([:prop1, :prop2], [:param1, :param2]) prov.resource_type = m assert_nothing_raised("could not call mk_resource_methods") do prov.mk_resource_methods end obj = prov.new(nil) %w{prop1 prop2 param1 param2}.each do |param| assert(prov.public_method_defined?(param), "no getter for %s" % param) assert(prov.public_method_defined?(param + "="), "no setter for %s" % param) assert_equal(:absent, obj.send(param), "%s did not default to :absent") val = "testing %s" % param assert_nothing_raised("Could not call setter for %s" % param) do obj.send(param + "=", val) end assert_equal(val, obj.send(param), "did not get correct value for %s" % param) end end # Make sure optional commands get looked up but don't affect suitability. def test_optional_commands type = Puppet::Type.newtype(:optional_commands) {} cleanup { Puppet::Type.rmtype(:optional_commands) } # Define a provider with mandatory commands required = type.provide(:required) { commands :missing => "/no/such/binary/definitely" } # And another with optional commands optional = type.provide(:optional) { optional_commands :missing => "/no/such/binary/definitely" } assert(! required.suitable?, "Provider with missing commands considered suitable") assert_nil(required.command(:missing), "Provider returned non-nil from missing command") assert(optional.suitable?, "Provider with optional commands considered unsuitable") assert_nil(optional.command(:missing), "Provider returned non-nil from missing command") assert_raise(Puppet::Error, "Provider did not fail when missing command was called") do optional.missing end end # Disabling, since I might not keep this interface def disabled_test_read_and_each # Create a new provider provider = @type.provide(:testing) assert_raise(Puppet::DevError, "Did not fail when :read was not overridden") do provider.read end children = [:one, :two] provider.meta_def(:read) do children end result = [] assert_nothing_raised("could not call 'each' on provider class") do provider.each { |i| result << i } end assert_equal(children, result, "did not get correct list from each") assert_equal(children, provider.collect { |i| i }, "provider does not include enumerable") end def test_source base = @type.provide(:base) assert_equal(:base, base.source, "source did not default correctly") assert_equal(:base, base.source, "source did not default correctly") sub = @type.provide(:sub, :parent => :base) assert_equal(:sub, sub.source, "source did not default correctly for sub class") assert_equal(:sub, sub.source, "source did not default correctly for sub class") other = @type.provide(:other, :parent => :base, :source => :base) assert_equal(:base, other.source, "source did not override") assert_equal(:base, other.source, "source did not override") end # Make sure we can initialize with either a resource or a hash, or none at all. def test_initialize test = @type.provide(:test) inst = @type.new :name => "boo" prov = nil assert_nothing_raised("Could not init with a resource") do prov = test.new(inst) end assert_equal(prov.resource, inst, "did not set resource correctly") assert_equal(inst.name, prov.name, "did not get resource name") params = {:name => :one, :ensure => :present} assert_nothing_raised("Could not init with a hash") do prov = test.new(params) end assert_equal(params, prov.send(:instance_variable_get, "@property_hash"), "did not set resource correctly") assert_equal(:one, prov.name, "did not get name from hash") assert_nothing_raised("Could not init with no argument") do prov = test.new() end assert_raise(Puppet::DevError, "did not fail when no name is present") do prov.name end end end class TestProviderFeatures < Test::Unit::TestCase include PuppetTest def setup super @type = Puppet::Type.newtype(:feature_test) do newparam(:name) {} ensurable end cleanup { Puppet::Type.rmtype(:feature_test) } @features = {:numeric => [:one, :two], :alpha => [:a, :b]} @features.each do |name, methods| assert_nothing_raised("Could not define features") do @type.feature(name, "boo", :methods => methods) end end end # Give them the basic run-through. def test_method_features @providers = {:numbers => @features[:numeric], :letters => @features[:alpha]} @providers[:both] = @features[:numeric] + @features[:alpha] @providers[:mixed] = [:one, :b] @providers[:neither] = [:something, :else] @providers.each do |name, methods| assert_nothing_raised("Could not create provider %s" % name) do @type.provide(name) do methods.each do |name| define_method(name) {} end end end end resource = @type.new(:name => "foo") {:numbers => [:numeric], :letters => [:alpha], :both => [:numeric, :alpha], :mixed => [], :neither => []}.each do |name, should| should.sort! { |a,b| a.to_s <=> b.to_s } provider = @type.provider(name) assert(provider, "Could not find provider %s" % name) assert_equal(should, provider.features, "Provider %s has incorrect features" % name) inst = provider.new(resource) # Make sure the boolean methods work on both the provider and # instance. @features.keys.each do |feature| method = feature.to_s + "?" assert(inst.respond_to?(method), "No boolean instance method for %s on %s" % [name, feature]) assert(provider.respond_to?(method), "No boolean class method for %s on %s" % [name, feature]) if should.include?(feature) assert(provider.feature?(feature), "class missing feature? %s" % feature) assert(inst.feature?(feature), "instance missing feature? %s" % feature) assert(provider.send(method), "class missing feature %s" % feature) assert(inst.send(method), "instance missing feature %s" % feature) assert(inst.satisfies?(feature), "instance.satisfy %s returned false" % feature) else assert(! provider.feature?(feature), "class has feature? %s" % feature) assert(! inst.feature?(feature), "instance has feature? %s" % feature) assert(! provider.send(method), "class has feature %s" % feature) assert(! inst.send(method), "instance has feature %s" % feature) assert(! inst.satisfies?(feature), "instance.satisfy %s returned true" % feature) end end end Puppet[:trace] = true Puppet::Type.loadall Puppet::Type.eachtype do |type| assert(type.respond_to?(:feature), "No features method defined for %s" % type.name) end end def test_has_feature # Define a new feature that has no methods @type.feature(:nomeths, "desc") # Define a provider with nothing provider = @type.provide(:nothing) {} assert(provider.respond_to?(:has_features), "Provider did not get 'has_features' method added") assert(provider.respond_to?(:has_feature), "Provider did not get the 'has_feature' alias method") # One with the numeric methods and nothing else @type.provide(:numbers) do define_method(:one) {} define_method(:two) {} end # Another with the numbers and a declaration @type.provide(:both) do define_method(:one) {} define_method(:two) {} has_feature :alpha end # And just the declaration @type.provide(:letters) do has_feature :alpha end # And a provider that declares it has our methodless feature. @type.provide(:none) do has_feature :nomeths end should = {:nothing => [], :both => [:numeric, :alpha], :letters => [:alpha], :numbers => [:numeric], :none => [:nomeths]} should.each do |name, features| provider_class = @type.provider(name) provider = provider_class.new({}) assert(provider, "did not get provider named %s" % name) features.sort! { |a,b| a.to_s <=> b.to_s } assert_equal(features, provider.features, "Got incorrect feature list for provider instance %s" % name) assert_equal(features, provider_class.features, "Got incorrect feature list for provider class %s" % name) features.each do |feat| assert(provider.feature?(feat), "Provider instance %s did not have feature %s" % [name, feat]) assert(provider_class.feature?(feat), "Provider class %s did not have feature %s" % [name, feat]) end end end def test_supports_parameter? # Make some parameters for each setting @type.newparam(:neither) {} @type.newparam(:some, :required_features => :alpha) @type.newparam(:both, :required_features => [:alpha, :numeric]) # and appropriate providers nope = @type.provide(:nope) {} maybe = @type.provide(:maybe) { has_feature(:alpha) } yep = @type.provide(:yep) { has_features(:alpha, :numeric) } # Now make sure our providers answer correctly. [nope, maybe, yep].each do |prov| assert(prov.respond_to?(:supports_parameter?), "%s does not respond to :supports_parameter?" % prov.name) case prov.name - when :nope: + when :nope supported = [:neither] un = [:some, :both] - when :maybe: + when :maybe supported = [:neither, :some] un = [:both] - when :yep: + when :yep supported = [:neither, :some, :both] un = [] end supported.each do |param| assert(prov.supports_parameter?(param), "%s was not supported by %s" % [param, prov.name]) end un.each do |param| assert(! prov.supports_parameter?(param), "%s was incorrectly supported by %s" % [param, prov.name]) end end end end diff --git a/test/ral/providers/user.rb b/test/ral/providers/user.rb index 1cb139b7d..3f68469c1 100755 --- a/test/ral/providers/user.rb +++ b/test/ral/providers/user.rb @@ -1,567 +1,567 @@ #!/usr/bin/env ruby require File.dirname(__FILE__) + '/../../lib/puppettest' require 'puppettest' require 'puppettest/support/utils' class TestUserProvider < Test::Unit::TestCase include PuppetTest::Support::Utils include PuppetTest::FileTesting def setup super setme() @@tmpusers = [] @provider = nil assert_nothing_raised { @provider = Puppet::Type.type(:user).defaultprovider } assert(@provider, "Could not find default user provider") end def teardown @@tmpusers.each { |user| unless missing?(user) remove(user) end } super end case Facter["operatingsystem"].value - when "Darwin": + when "Darwin" def missing?(user) output = %x{nidump -r /users/#{user} / 2>/dev/null}.chomp if output == "" return true else return false end assert_equal("", output, "User %s is present:\n%s" % [user, output]) end def current?(param, user) property = Puppet::Type.type(:user).properties.find { |st| st.name == param } prov = Puppet::Type.type(:user).defaultprovider output = prov.report(param) # output = %x{nireport / /users name #{prov.netinfokey(param)}} output.each { |hash| if hash[:name] == user.name val = hash[param] if val =~ /^[-0-9]+$/ return Integer(val) else return val end end } return nil end def remove(user) system("niutil -destroy / /users/%s" % user) end else def missing?(user) begin obj = Etc.getpwnam(user) return false rescue ArgumentError return true end end def current?(param, user) property = Puppet::Type.type(:user).properties.find { |st| st.name == param } assert_nothing_raised { obj = Etc.getpwnam(user.name) return obj.send(user.posixmethod(param)) } return nil end def remove(user) system("userdel %s" % user) end end def eachproperty Puppet::Type.type(:user).validproperties.each do |property| yield property end end def findshell(old = nil) %w{/bin/sh /bin/bash /sbin/sh /bin/ksh /bin/zsh /bin/csh /bin/tcsh /usr/bin/sh /usr/bin/bash /usr/bin/ksh /usr/bin/zsh /usr/bin/csh /usr/bin/tcsh}.find { |shell| if old FileTest.exists?(shell) and shell != old else FileTest.exists?(shell) end } end def fakedata(name, param) case param - when :name: name - when :ensure: :present - when :comment: "Puppet's Testing User %s" % name # use a single quote a la #375 - when :gid: nonrootgroup.gid - when :shell: findshell() - when :home: "/home/%s" % name + when :name; name + when :ensure; :present + when :comment; "Puppet's Testing User %s" % name # use a single quote a la #375 + when :gid; nonrootgroup.gid + when :shell; findshell() + when :home; "/home/%s" % name else return nil end end def fakeresource(*args) resource = super # Set boolean methods as necessary. class << resource def allowdupe? self[:allowdupe] end def managehome? self[:managehome] end end resource end def mkuser(name) fakeresource = fakeresource(:user, name) user = nil assert_nothing_raised { user = @provider.new(fakeresource) } assert(user, "Could not create provider user") return user end def test_list names = nil assert_nothing_raised { names = @provider.listbyname } assert(names.length > 0, "Listed no users") # Now try it by object assert_nothing_raised { names = @provider.instances } assert(names.length > 0, "Listed no users as objects") names.each do |obj| assert_instance_of(@provider, obj) end end def test_infocollection fakeresource = fakeresource(:user, @me) user = nil assert_nothing_raised { user = @provider.new(fakeresource) } assert(user, "Could not create user provider") Puppet::Type.type(:user).validproperties.each do |property| next if property == :ensure # This is mostly in place for the 'password' stuff. next unless user.class.supports_parameter?(property) and Puppet.features.root? val = nil assert_nothing_raised { val = user.send(property) } assert(val != :absent, "Property %s is missing" % property) assert(val, "Did not get value for %s" % property) end end def test_exists user = mkuser("nosuchuserok") assert(! user.exists?, "Fake user exists?") user = mkuser(@me) assert(user.exists?, "I don't exist?") end def attrtest_ensure(user) old = user.ensure assert_nothing_raised { user.delete } assert(missing?(user.name), "User is still present") assert_nothing_raised { user.create } assert(!missing?(user.name), "User is absent") assert_nothing_raised { user.delete } unless old == :absent user.create end end def attrtest_comment(user) old = user.comment newname = "Billy O'Neal" # use a single quote, a la #372 assert_nothing_raised { user.comment = newname } assert_equal(newname, current?(:comment, user), "Comment was not changed") assert_nothing_raised { user.comment = old } assert_equal(old, current?(:comment, user), "Comment was not reverted") end def attrtest_home(user) old = current?(:home, user) assert_nothing_raised { user.home = "/tmp" } assert_equal("/tmp", current?(:home, user), "Home was not changed") assert_nothing_raised { user.home = old } assert_equal(old, current?(:home, user), "Home was not reverted") end def attrtest_shell(user) old = current?(:shell, user) newshell = findshell(old) unless newshell $stderr.puts "Cannot find alternate shell; skipping shell test" return end assert_nothing_raised { user.shell = newshell } assert_equal(newshell, current?(:shell, user), "Shell was not changed") assert_nothing_raised { user.shell = old } assert_equal(old, current?(:shell, user), "Shell was not reverted") end def attrtest_gid(user) old = current?(:gid, user) newgroup = %w{nogroup nobody staff users daemon}.find { |gid| begin group = Etc.getgrnam(gid) rescue ArgumentError => detail next end old != group.gid } group = Etc.getgrnam(newgroup) unless newgroup $stderr.puts "Cannot find alternate group; skipping gid test" return end # Stupid netinfo if Facter.value(:operatingsystem) == "Darwin" assert_raise(ArgumentError, "gid allowed a non-integer value") do user.gid = group.name end end assert_nothing_raised("Failed to specify group by id") { user.gid = group.gid } assert_equal(group.gid, current?(:gid,user), "GID was not changed") assert_nothing_raised("Failed to change back to old gid") { user.gid = old } end def attrtest_uid(user) old = current?(:uid, user) newuid = old while true newuid += 1 if newuid - old > 1000 $stderr.puts "Could not find extra test UID" return end begin newuser = Etc.getpwuid(newuid) rescue ArgumentError => detail break end end assert_nothing_raised("Failed to change user id") { user.uid = newuid } assert_equal(newuid, current?(:uid, user), "UID was not changed") assert_nothing_raised("Failed to change user id") { user.uid = old } assert_equal(old, current?(:uid, user), "UID was not changed back") end def attrtest_groups(user) Etc.setgrent max = 0 while group = Etc.getgrent if group.gid > max and group.gid < 5000 max = group.gid end end groups = [] main = [] extra = [] 5.times do |i| i += 1 name = "pptstgr%s" % i tmpgroup = Puppet::Type.type(:group).new( :name => name, :gid => max + i ) groups << tmpgroup cleanup do tmpgroup.provider.delete if tmpgroup.provider.exists? end if i < 3 main << name else extra << name end end # Create our test groups assert_apply(*groups) # Now add some of them to our user assert_nothing_raised { user.resource[:groups] = extra.join(",") } # Some tests to verify that groups work correctly startig from nothing # Remove our user user.delete # And add it again user.create # Make sure that the group list is added at creation time. # This is necessary because we don't have default fakedata for groups. assert(user.groups, "Did not retrieve group list") list = user.groups.split(",") assert_equal(extra.sort, list.sort, "Group list was not set at creation time") # Now set to our main list of groups assert_nothing_raised { user.groups = main.join(",") } list = user.groups.split(",") assert_equal(main.sort, list.sort, "Group list is not equal") end if Puppet::Util::SUIDManager.uid == 0 def test_simpleuser name = "pptest" assert(missing?(name), "User %s is present" % name) user = mkuser(name) eachproperty do |property| if val = fakedata(user.name, property) user.resource[property] = val end end @@tmpusers << name assert_nothing_raised { user.create } assert_equal("Puppet's Testing User pptest", user.comment, "Comment was not set") assert_nothing_raised { user.delete } assert(missing?(user.name), "User was not deleted") end def test_alluserproperties user = nil name = "pptest" assert(missing?(name), "User %s is present" % name) user = mkuser(name) eachproperty do |property| if val = fakedata(user.name, property) user.resource[property] = val end end @@tmpusers << name assert_nothing_raised { user.create } assert_equal("Puppet's Testing User pptest", user.comment, "Comment was not set") tests = Puppet::Type.type(:user).validproperties just = nil tests.each { |test| if self.respond_to?("attrtest_%s" % test) self.send("attrtest_%s" % test, user) else Puppet.err "Not testing attr %s of user" % test end } assert_nothing_raised { user.delete } end # This is a weird method that shows how annoying the interface between # types and providers is. Grr. def test_duplicateIDs user1 = mkuser("user1") user1.create user1.uid = 125 user2 = mkuser("user2") user2.resource[:uid] = 125 cleanup do user1.delete user2.delete end # Not all OSes fail here, so we can't test that it doesn't work with # it off, only that it does work with it on. assert_nothing_raised { user2.resource[:allowdupe] = :true } assert_nothing_raised { user2.create } assert_equal(:present, user2.ensure, "User did not get created") end else $stderr.puts "Not root; skipping user creation/modification tests" end # Here is where we test individual providers def test_useradd_flags useradd = nil assert_nothing_raised { useradd = Puppet::Type.type(:user).provider(:useradd) } assert(useradd, "Did not retrieve useradd provider") user = nil assert_nothing_raised { fakeresource = fakeresource(:user, @me) user = useradd.new(fakeresource) } assert_equal("-d", user.send(:flag, :home), "Incorrect home flag") assert_equal("-s", user.send(:flag, :shell), "Incorrect shell flag") end def test_autogen provider = nil user = Puppet::Type.type(:user).new(:name => nonrootuser.name) provider = user.provider assert(provider, "did not get provider") # Everyone should be able to autogenerate a uid assert_instance_of(Fixnum, provider.autogen(:uid)) # If we're Darwin, then we should get results, but everyone else should # get nil darwin = (Facter.value(:operatingsystem) == "Darwin") should = { :comment => user[:name].capitalize, :home => "/var/empty", :shell => "/usr/bin/false" } should.each do |param, value| if darwin assert_equal(value, provider.autogen(param), "did not autogen %s for darwin correctly" % param) else assert_nil(provider.autogen(param), "autogenned %s for non-darwin os" % param) end end end end diff --git a/test/ral/type/cron.rb b/test/ral/type/cron.rb index 6eceb273d..20ed773d6 100755 --- a/test/ral/type/cron.rb +++ b/test/ral/type/cron.rb @@ -1,492 +1,492 @@ #!/usr/bin/env ruby require File.dirname(__FILE__) + '/../../lib/puppettest' require 'puppettest' # Test cron job creation, modification, and destruction class TestCron < Test::Unit::TestCase include PuppetTest def setup super setme() @crontype = Puppet::Type.type(:cron) @provider = @crontype.defaultprovider if @provider.respond_to?(:filetype=) @provider.stubs(:filetype).returns(Puppet::Util::FileType.filetype(:ram)) end @crontype = Puppet::Type.type(:cron) end def teardown super @crontype.defaultprovider = nil end def eachprovider @crontype.suitableprovider.each do |provider| yield provider end end # Back up the user's existing cron tab if they have one. def cronback tab = nil assert_nothing_raised { tab = Puppet::Type.type(:cron).filetype.read(@me) } if $? == 0 @currenttab = tab else @currenttab = nil end end # Restore the cron tab to its original form. def cronrestore assert_nothing_raised { if @currenttab @crontype.filetype.new(@me).write(@currenttab) else @crontype.filetype.new(@me).remove end } end # Create a cron job with all fields filled in. def mkcron(name, addargs = true) cron = nil command = "date > %s/crontest%s" % [tmpdir(), name] args = nil if addargs args = { :command => command, :name => name, :user => @me, :minute => rand(59), :month => "1", :monthday => "1", :hour => "1" } else args = {:command => command, :name => name} end assert_nothing_raised { cron = @crontype.new(args) } return cron end # Run the cron through its paces -- install it then remove it. def cyclecron(cron) obj = Puppet::Type::Cron.cronobj(@me) text = obj.read name = cron.name comp = mk_catalog(name, cron) assert_events([:cron_created], comp) cron.provider.class.prefetch currentvalue = cron.retrieve assert(cron.insync?(currentvalue), "Cron is not in sync") assert_events([], comp) curtext = obj.read text.split("\n").each do |line| assert(curtext.include?(line), "Missing '%s'" % line) end obj = Puppet::Type::Cron.cronobj(@me) cron[:ensure] = :absent assert_events([:cron_removed], comp) cron.provider.class.prefetch currentvalue = cron.retrieve assert(cron.insync?(currentvalue), "Cron is not in sync") assert_events([], comp) end # Test that a cron job with spaces at the end doesn't get rewritten def test_trailingspaces eachprovider do |provider| cron = nil # make the cron name = "yaytest" command = "date > /dev/null " assert_nothing_raised { cron = @crontype.new( :name => name, :command => "date > /dev/null ", :month => "May", :user => @me ) } property = cron.send(:property, :command) cron.provider.command = command cron.provider.ensure = :present cron.provider.user = @me cron.provider.month = ["4"] cron.provider.class.prefetch currentvalue = cron.retrieve currentvalue.each do |prop, value| # We're only interested in comparing the command. next unless prop.name.to_s == "command" assert(prop.insync?(value), "Property %s is not considered in sync with value %s" % [prop.name, value.inspect]) end end end def test_makeandretrievecron %w{storeandretrieve a-name another-name more_naming SomeName}.each do |name| cron = mkcron(name) catalog = mk_catalog(name, cron) trans = assert_events([:cron_created], catalog, name) cron.provider.class.prefetch cron = nil assert(cron = catalog.resource(:cron, name), "Could not retrieve named cron") assert_instance_of(Puppet::Type.type(:cron), cron) end end # Do input validation testing on all of the parameters. def test_arguments values = { :monthday => { :valid => [ 1, 13, "1" ], :invalid => [ -1, 0, 32 ] }, :weekday => { :valid => [ 0, 3, 6, "1", "tue", "wed", "Wed", "MOnday", "SaTurday" ], :invalid => [ -1, 7, "13", "tues", "teusday", "thurs" ] }, :hour => { :valid => [ 0, 21, 23 ], :invalid => [ -1, 24 ] }, :minute => { :valid => [ 0, 34, 59 ], :invalid => [ -1, 60 ] }, :month => { :valid => [ 1, 11, 12, "mar", "March", "apr", "October", "DeCeMbEr" ], :invalid => [ -1, 0, 13, "marc", "sept" ] } } cron = mkcron("valtesting") values.each { |param, hash| # We have to test the valid ones first, because otherwise the # property will fail to create at all. [:valid, :invalid].each { |type| hash[type].each { |value| case type - when :valid: + when :valid assert_nothing_raised { cron[param] = value } if value.is_a?(Integer) assert_equal([value.to_s], cron.should(param), "Cron value was not set correctly") end - when :invalid: + when :invalid assert_raise(Puppet::Error, "%s is incorrectly a valid %s" % [value, param]) { cron[param] = value } end if value.is_a?(Integer) value = value.to_s redo end } } } end # Verify that comma-separated numbers are not resulting in rewrites def test_comma_separated_vals_work eachprovider do |provider| cron = nil assert_nothing_raised { cron = @crontype.new( :command => "/bin/date > /dev/null", :minute => [0, 30], :name => "crontest", :provider => provider.name ) } cron.provider.ensure = :present cron.provider.command = '/bin/date > /dev/null' cron.provider.minute = %w{0 30} cron.provider.class.prefetch currentvalue = cron.retrieve currentvalue.each do |prop, value| # We're only interested in comparing minutes. next unless prop.name.to_s == "minute" assert(prop.insync?(value), "Property %s is not considered in sync with value %s" % [prop.name, value.inspect]) end end end def test_fieldremoval cron = nil assert_nothing_raised { cron = @crontype.new( :command => "/bin/date > /dev/null", :minute => [0, 30], :name => "crontest", :provider => :crontab ) } assert_events([:cron_created], cron) cron.provider.class.prefetch cron[:minute] = :absent assert_events([:cron_changed], cron) current_values = nil assert_nothing_raised { cron.provider.class.prefetch current_values = cron.retrieve } assert_equal(:absent, current_values[cron.property(:minute)]) end def test_listing # Make a crontab cron for testing provider = @crontype.provider(:crontab) return unless provider.suitable? ft = provider.filetype provider.filetype = :ram cleanup { provider.filetype = ft } setme cron = @crontype.new(:name => "testing", :minute => [0, 30], :command => "/bin/testing", :user => @me ) # Write it to our file assert_apply(cron) crons = [] assert_nothing_raised { @crontype.instances.each do |cron| crons << cron end } crons.each do |cron| assert_instance_of(@crontype, cron, "Did not receive a real cron object") assert_instance_of(String, cron.value(:user), "Cron user is not a string") end end def verify_failonnouser assert_raise(Puppet::Error) do @crontype.retrieve("nosuchuser") end end def test_divisionnumbers cron = mkcron("divtest") cron[:minute] = "*/5" assert_apply(cron) cron.provider.class.prefetch currentvalue = cron.retrieve assert_equal(["*/5"], currentvalue[cron.property(:minute)]) end def test_ranges cron = mkcron("rangetest") cron[:minute] = "2-4" assert_apply(cron) current_values = nil assert_nothing_raised { cron.provider.class.prefetch current_values = cron.retrieve } assert_equal(["2-4"], current_values[cron.property(:minute)]) end def provider_set(cron, param, value) unless param =~ /=$/ param = "%s=" % param end cron.provider.send(param, value) end def test_value cron = mkcron("valuetesting", false) # First, test the normal properties [:minute, :hour, :month].each do |param| cron.newattr(param) property = cron.property(param) assert(property, "Did not get %s property" % param) assert_nothing_raised { # property.is = :absent provider_set(cron, param, :absent) } val = "*" assert_equal(val, cron.value(param)) # Make sure arrays work, too provider_set(cron, param, ["1"]) assert_equal(%w{1}, cron.value(param)) # Make sure values get comma-joined provider_set(cron, param, %w{2 3}) assert_equal(%w{2 3}, cron.value(param)) # Make sure "should" values work, too cron[param] = "4" assert_equal(%w{4}, cron.value(param)) cron[param] = ["4"] assert_equal(%w{4}, cron.value(param)) cron[param] = ["4", "5"] assert_equal(%w{4 5}, cron.value(param)) provider_set(cron, param, :absent) assert_equal(%w{4 5}, cron.value(param)) end Puppet[:trace] = false # Now make sure that :command works correctly cron.delete(:command) cron.newattr(:command) property = cron.property(:command) assert_nothing_raised { provider_set(cron, :command, :absent) } param = :command # Make sure arrays work, too provider_set(cron, param, ["/bin/echo"]) assert_equal("/bin/echo", cron.value(param)) # Make sure values are not comma-joined provider_set(cron, param, %w{/bin/echo /bin/test}) assert_equal("/bin/echo", cron.value(param)) # Make sure "should" values work, too cron[param] = "/bin/echo" assert_equal("/bin/echo", cron.value(param)) cron[param] = ["/bin/echo"] assert_equal("/bin/echo", cron.value(param)) cron[param] = %w{/bin/echo /bin/test} assert_equal("/bin/echo", cron.value(param)) provider_set(cron, param, :absent) assert_equal("/bin/echo", cron.value(param)) end def test_multiple_users crons = [] users = ["root", nonrootuser.name] users.each do |user| cron = Puppet::Type.type(:cron).new( :name => "testcron-#{user}", :user => user, :command => "/bin/echo", :minute => [0,30] ) crons << cron assert_equal(cron.should(:user), cron.should(:target), "Target was not set correctly for %s" % user) end provider = crons[0].provider.class assert_apply(*crons) users.each do |user| users.each do |other| next if user == other text = provider.target_object(other).read assert(text !~ /testcron-#{user}/, "%s's cron job is in %s's tab" % [user, other]) end end end # Make sure the user stuff defaults correctly. def test_default_user crontab = @crontype.provider(:crontab) if crontab.suitable? inst = @crontype.new( :name => "something", :command => "/some/thing", :provider => :crontab) assert_equal(Etc.getpwuid(Process.uid).name, inst.should(:user), "user did not default to current user with crontab") assert_equal(Etc.getpwuid(Process.uid).name, inst.should(:target), "target did not default to current user with crontab") # Now make a new cron with a user, and make sure it gets copied # over inst = @crontype.new(:name => "yay", :command => "/some/thing", :user => "bin", :provider => :crontab) assert_equal("bin", inst.should(:target), "target did not default to user with crontab") end end # #705 - make sure extra spaces don't screw things up def test_spaces_in_command string = "echo multiple spaces" cron = @crontype.new(:name => "space testing", :command => string) assert_apply(cron) cron = @crontype.new(:name => "space testing", :command => string) # Now make sure that it's correctly in sync cron.provider.class.prefetch("testing" => cron) properties = cron.retrieve command, result = properties.find { |prop, value| prop.name == :command } assert_equal(string, result, "Cron did not pick up extra spaces in command") assert(command.insync?(string), "Command changed with multiple spaces") end end diff --git a/test/ral/type/file.rb b/test/ral/type/file.rb index 1a7813b74..08fdab821 100755 --- a/test/ral/type/file.rb +++ b/test/ral/type/file.rb @@ -1,1173 +1,1173 @@ #!/usr/bin/env ruby require File.dirname(__FILE__) + '/../../lib/puppettest' require 'puppettest' require 'puppettest/support/utils' require 'fileutils' class TestFile < Test::Unit::TestCase include PuppetTest::Support::Utils include PuppetTest::FileTesting def mkfile(hash) file = nil assert_nothing_raised { file = Puppet::Type.type(:file).new(hash) } return file end def mktestfile tmpfile = tempfile() File.open(tmpfile, "w") { |f| f.puts rand(100) } @@tmpfiles.push tmpfile mkfile(:name => tmpfile) end def setup super @file = Puppet::Type.type(:file) $method = @method_name Puppet[:filetimeout] = -1 Facter.stubs(:to_hash).returns({}) end def teardown system("rm -rf %s" % Puppet[:statefile]) super end def initstorage Puppet::Util::Storage.init Puppet::Util::Storage.load end def clearstorage Puppet::Util::Storage.store Puppet::Util::Storage.clear end def test_owner file = mktestfile() users = {} count = 0 # collect five users Etc.passwd { |passwd| if count > 5 break else count += 1 end users[passwd.uid] = passwd.name } fake = {} # find a fake user while true a = rand(1000) begin Etc.getpwuid(a) rescue fake[a] = "fakeuser" break end end uid, name = users.shift us = {} us[uid] = name users.each { |uid, name| assert_apply(file) assert_nothing_raised() { file[:owner] = name } assert_nothing_raised() { file.retrieve } assert_apply(file) } end def test_group file = mktestfile() [%x{groups}.chomp.split(/ /), Process.groups].flatten.each { |group| assert_nothing_raised() { file[:group] = group } assert(file.property(:group)) assert(file.property(:group).should) } end def test_groups_fails_when_invalid assert_raise(Puppet::Error, "did not fail when the group was empty") do Puppet::Type.type(:file).new :path => "/some/file", :group => "" end end if Puppet::Util::SUIDManager.uid == 0 def test_createasuser dir = tmpdir() user = nonrootuser() path = File.join(tmpdir, "createusertesting") @@tmpfiles << path file = nil assert_nothing_raised { file = Puppet::Type.type(:file).new( :path => path, :owner => user.name, :ensure => "file", :mode => "755" ) } comp = mk_catalog("createusertest", file) assert_events([:file_created], comp) end def test_nofollowlinks 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) # First test 'user' user = nonrootuser() inituser = File.lstat(link).uid File.lchown(inituser, nil, link) obj = nil assert_nothing_raised { obj = Puppet::Type.type(:file).new( :title => link, :owner => user.name ) } obj.retrieve # Make sure it defaults to managing the link assert_events([:file_changed], obj) assert_equal(user.uid, File.lstat(link).uid) assert_equal(inituser, File.stat(file).uid) File.chown(inituser, nil, file) File.lchown(inituser, nil, link) # Try following obj[:links] = :follow assert_events([:file_changed], obj) assert_equal(user.uid, File.stat(file).uid) assert_equal(inituser, File.lstat(link).uid) # And then explicitly managing File.chown(inituser, nil, file) File.lchown(inituser, nil, link) obj[:links] = :manage assert_events([:file_changed], obj) assert_equal(user.uid, File.lstat(link).uid) assert_equal(inituser, File.stat(file).uid) obj.delete(:owner) obj[:links] = :follow # And then test 'group' group = nonrootgroup initgroup = File.stat(file).gid obj[:group] = group.name obj[:links] = :follow assert_events([:file_changed], obj) assert_equal(group.gid, File.stat(file).gid) File.chown(nil, initgroup, file) File.lchown(nil, initgroup, link) obj[:links] = :manage assert_events([:file_changed], obj) assert_equal(group.gid, File.lstat(link).gid) assert_equal(initgroup, File.stat(file).gid) end def test_ownerasroot file = mktestfile() users = {} count = 0 # collect five users Etc.passwd { |passwd| if count > 5 break else count += 1 end next if passwd.uid < 0 users[passwd.uid] = passwd.name } fake = {} # find a fake user while true a = rand(1000) begin Etc.getpwuid(a) rescue fake[a] = "fakeuser" break end end users.each { |uid, name| assert_nothing_raised() { file[:owner] = name } changes = [] assert_nothing_raised() { changes << file.evaluate } assert(changes.length > 0) assert_apply(file) currentvalue = file.retrieve assert(file.insync?(currentvalue)) assert_nothing_raised() { file[:owner] = uid } assert_apply(file) currentvalue = file.retrieve # make sure changing to number doesn't cause a sync assert(file.insync?(currentvalue)) } # We no longer raise an error here, because we check at run time #fake.each { |uid, name| # assert_raise(Puppet::Error) { # file[:owner] = name # } # assert_raise(Puppet::Error) { # file[:owner] = uid # } #} end def test_groupasroot file = mktestfile() [%x{groups}.chomp.split(/ /), Process.groups].flatten.each { |group| next unless Puppet::Util.gid(group) # grr. assert_nothing_raised() { file[:group] = group } assert(file.property(:group)) assert(file.property(:group).should) assert_apply(file) currentvalue = file.retrieve assert(file.insync?(currentvalue)) assert_nothing_raised() { file.delete(:group) } } end if Facter.value(:operatingsystem) == "Darwin" def test_sillyowner file = tempfile() File.open(file, "w") { |f| f.puts "" } File.chown(-2, nil, file) assert(File.stat(file).uid > 120000, "eh?") user = nonrootuser obj = Puppet::Type.newfile( :path => file, :owner => user.name ) assert_apply(obj) assert_equal(user.uid, File.stat(file).uid) end end else $stderr.puts "Run as root for complete owner and group testing" end def test_create %w{a b c d}.collect { |name| tempfile() + name.to_s }.each { |path| file =nil assert_nothing_raised() { file = Puppet::Type.type(:file).new( :name => path, :ensure => "file" ) } assert_events([:file_created], file) assert_events([], file) assert(FileTest.file?(path), "File does not exist") assert(file.insync?(file.retrieve)) @@tmpfiles.push path } end def test_create_dir basedir = tempfile() Dir.mkdir(basedir) %w{a b c d}.collect { |name| "#{basedir}/%s" % name }.each { |path| file = nil assert_nothing_raised() { file = Puppet::Type.type(:file).new( :name => path, :ensure => "directory" ) } assert(! FileTest.directory?(path), "Directory %s already exists" % [path]) assert_events([:directory_created], file) assert_events([], file) assert(file.insync?(file.retrieve)) assert(FileTest.directory?(path)) @@tmpfiles.push path } end def test_modes file = mktestfile # Set it to something else initially File.chmod(0775, file.title) [0644,0755,0777,0641].each { |mode| assert_nothing_raised() { file[:mode] = mode } assert_events([:file_changed], file) assert_events([], file) assert(file.insync?(file.retrieve)) assert_nothing_raised() { file.delete(:mode) } } end def cyclefile(path) # i had problems with using :name instead of :path [:name,:path].each { |param| file = nil changes = nil comp = nil trans = nil initstorage assert_nothing_raised { file = Puppet::Type.type(:file).new( param => path, :recurse => true, :checksum => "md5" ) } comp = Puppet::Type.type(:component).new( :name => "component" ) comp.push file assert_nothing_raised { trans = comp.evaluate } assert_nothing_raised { trans.evaluate } clearstorage Puppet::Type.allclear } end def test_recurse? file = Puppet::Type.type(:file).new :path => tempfile # Make sure we default to false assert(! file.recurse?, "Recurse defaulted to true") [true, "true", 10, "inf"].each do |value| file[:recurse] = value assert(file.recurse?, "%s did not cause recursion" % value) end [false, "false", 0].each do |value| file[:recurse] = value assert(! file.recurse?, "%s caused recursion" % value) end end def test_recursion basedir = tempfile() subdir = File.join(basedir, "subdir") tmpfile = File.join(basedir,"testing") FileUtils.mkdir_p(subdir) dir = nil [true, "true", "inf", 50].each do |value| assert_nothing_raised { dir = Puppet::Type.type(:file).new( :path => basedir, :recurse => value, :check => %w{owner mode group} ) } config = mk_catalog dir transaction = Puppet::Transaction.new(config) children = nil assert_nothing_raised { children = transaction.eval_generate(dir) } assert_equal([subdir], children.collect {|c| c.title }, "Incorrect generated children") # Remove our subdir resource, subdir_resource = config.resource(:file, subdir) config.remove_resource(subdir_resource) # Create the test file File.open(tmpfile, "w") { |f| f.puts "yayness" } assert_nothing_raised { children = transaction.eval_generate(dir) } # And make sure we get both resources back. assert_equal([subdir, tmpfile].sort, children.collect {|c| c.title }.sort, "Incorrect generated children when recurse == %s" % value.inspect) File.unlink(tmpfile) end end def test_filetype_retrieval file = nil # Verify it retrieves files of type directory assert_nothing_raised { file = Puppet::Type.type(:file).new( :name => tmpdir(), :check => :type ) } assert_nothing_raised { file.evaluate } assert_equal("directory", file.property(:type).retrieve) # And then check files assert_nothing_raised { file = Puppet::Type.type(:file).new( :name => tempfile(), :ensure => "file" ) } assert_apply(file) file[:check] = "type" assert_apply(file) assert_equal("file", file.property(:type).retrieve) file[:type] = "directory" currentvalues = {} assert_nothing_raised { currentvalues = file.retrieve } # The 'retrieve' method sets @should to @is, so they're never # out of sync. It's a read-only class. assert(file.insync?(currentvalues)) end def test_path dir = tempfile() path = File.join(dir, "subdir") assert_nothing_raised("Could not make file") { FileUtils.mkdir_p(File.dirname(path)) File.open(path, "w") { |f| f.puts "yayness" } } file = nil dirobj = nil assert_nothing_raised("Could not make file object") { dirobj = Puppet::Type.type(:file).new( :path => dir, :recurse => true, :check => %w{mode owner group} ) } catalog = mk_catalog dirobj transaction = Puppet::Transaction.new(catalog) transaction.eval_generate(dirobj) #assert_nothing_raised { # dirobj.eval_generate #} file = catalog.resource(:file, path) assert(file, "Could not retrieve file object") assert_equal("/%s" % file.ref, file.path) end def test_autorequire basedir = tempfile() subfile = File.join(basedir, "subfile") baseobj = Puppet::Type.type(:file).new( :name => basedir, :ensure => "directory" ) subobj = Puppet::Type.type(:file).new( :name => subfile, :ensure => "file" ) catalog = mk_catalog(baseobj, subobj) edge = nil assert_nothing_raised do edge = subobj.autorequire.shift end assert_equal(baseobj, edge.source, "file did not require its parent dir") assert_equal(subobj, edge.target, "file did not require its parent dir") end # Unfortunately, I know this fails def disabled_test_recursivemkdir path = tempfile() subpath = File.join(path, "this", "is", "a", "dir") file = nil assert_nothing_raised { file = Puppet::Type.type(:file).new( :name => subpath, :ensure => "directory", :recurse => true ) } comp = mk_catalog("yay", file) comp.finalize assert_apply(comp) #assert_events([:directory_created], comp) assert(FileTest.directory?(subpath), "Did not create directory") end # Make sure that content updates the checksum on the same run def test_checksumchange_for_content dest = tempfile() File.open(dest, "w") { |f| f.puts "yayness" } file = nil assert_nothing_raised { file = Puppet::Type.type(:file).new( :name => dest, :checksum => "md5", :content => "This is some content" ) } file.retrieve assert_events([:file_changed], file) file.retrieve assert_events([], file) end # Make sure that content updates the checksum on the same run def test_checksumchange_for_ensure dest = tempfile() file = nil assert_nothing_raised { file = Puppet::Type.type(:file).new( :name => dest, :checksum => "md5", :ensure => "file" ) } file.retrieve assert_events([:file_created], file) file.retrieve assert_events([], file) end def test_nameandpath path = tempfile() file = nil assert_nothing_raised { file = Puppet::Type.type(:file).new( :title => "fileness", :path => path, :content => "this is some content" ) } assert_apply(file) assert(FileTest.exists?(path)) end # Make sure that a missing group isn't fatal at object instantiation time. def test_missinggroup file = nil assert_nothing_raised { file = Puppet::Type.type(:file).new( :path => tempfile(), :group => "fakegroup" ) } assert(file.property(:group), "Group property failed") end def test_modecreation path = tempfile() file = Puppet::Type.type(:file).new( :path => path, :ensure => "file", :mode => "0777" ) assert_equal(0777, file.should(:mode), "Mode did not get set correctly") assert_apply(file) assert_equal(0777, File.stat(path).mode & 007777, "file mode is incorrect") File.unlink(path) file[:ensure] = "directory" assert_apply(file) assert_equal(0777, File.stat(path).mode & 007777, "directory mode is incorrect") end # If both 'ensure' and 'content' are used, make sure that all of the other # properties are handled correctly. def test_contentwithmode path = tempfile() file = nil assert_nothing_raised { file = Puppet::Type.type(:file).new( :path => path, :ensure => "file", :content => "some text\n", :mode => 0755 ) } assert_apply(file) assert_equal("%o" % 0755, "%o" % (File.stat(path).mode & 007777)) end def test_backupmodes File.umask(0022) file = tempfile() newfile = tempfile() File.open(file, "w", 0411) { |f| f.puts "yayness" } obj = Puppet::Type.type(:file).new( :path => file, :content => "rahness\n", :backup => ".puppet-bak" ) catalog = mk_catalog(obj) catalog.apply backupfile = file + obj[:backup] @@tmpfiles << backupfile assert(FileTest.exists?(backupfile), "Backup file %s does not exist" % backupfile) assert_equal(0411, filemode(backupfile), "File mode is wrong for backupfile") name = "bucket" bpath = tempfile() Dir.mkdir(bpath) bucket = Puppet::Type.type(:filebucket).new(:title => name, :path => bpath) catalog.add_resource(bucket) obj[:backup] = name obj[:content] = "New content" catalog.finalize catalog.apply md5 = "18cc17fa3047fcc691fdf49c0a7f539a" dir, file, pathfile = Puppet::Network::Handler.filebucket.paths(bpath, md5) assert_equal(0440, filemode(file)) end def test_replacefilewithlink path = tempfile() link = tempfile() File.open(path, "w") { |f| f.puts "yay" } File.open(link, "w") { |f| f.puts "a file" } file = nil assert_nothing_raised { file = Puppet::Type.type(:file).new( :ensure => path, :path => link ) } assert_events([:link_created], file) assert(FileTest.symlink?(link), "Link was not created") assert_equal(path, File.readlink(link), "Link was created incorrectly") end def test_file_with_spaces dir = tempfile() Dir.mkdir(dir) source = File.join(dir, "file spaces") dest = File.join(dir, "another space") File.open(source, "w") { |f| f.puts :yay } obj = Puppet::Type.type(:file).new( :path => dest, :source => source ) assert(obj, "Did not create file") assert_apply(obj) assert(FileTest.exists?(dest), "File did not get created") end # Testing #274. Make sure target can be used without 'ensure'. def test_target_without_ensure source = tempfile() dest = tempfile() File.open(source, "w") { |f| f.puts "funtest" } obj = nil assert_nothing_raised { obj = Puppet::Type.newfile(:path => dest, :target => source) } assert_apply(obj) end def test_autorequire_owner_and_group file = tempfile() comp = nil user = nil group =nil home = nil ogroup = nil assert_nothing_raised { user = Puppet::Type.type(:user).new( :name => "pptestu", :home => file, :gid => "pptestg" ) home = Puppet::Type.type(:file).new( :path => file, :owner => "pptestu", :group => "pptestg", :ensure => "directory" ) group = Puppet::Type.type(:group).new( :name => "pptestg" ) comp = mk_catalog(user, group, home) } # Now make sure we get a relationship for each of these rels = nil assert_nothing_raised { rels = home.autorequire } assert(rels.detect { |e| e.source == user }, "owner was not autorequired") assert(rels.detect { |e| e.source == group }, "group was not autorequired") end # Testing #309 -- //my/file => /my/file def test_slash_deduplication ["/my/////file/for//testing", "//my/file/for/testing///", "/my/file/for/testing"].each do |path| file = nil assert_nothing_raised do file = Puppet::Type.newfile(:path => path) end assert_equal("/my/file/for/testing", file.title) end end # Testing #304 def test_links_to_directories link = tempfile() file = tempfile() dir = tempfile() Dir.mkdir(dir) bucket = Puppet::Type.type(:filebucket).new :name => "main" File.symlink(dir, link) File.open(file, "w") { |f| f.puts "" } assert_equal(dir, File.readlink(link)) obj = Puppet::Type.newfile :path => link, :ensure => :link, :target => file, :recurse => false, :backup => "main" catalog = mk_catalog(bucket, obj) catalog.apply assert_equal(file, File.readlink(link)) end # Testing #303 def test_nobackups_with_links link = tempfile() new = tempfile() File.open(link, "w") { |f| f.puts "old" } File.open(new, "w") { |f| f.puts "new" } obj = Puppet::Type.newfile :path => link, :ensure => :link, :target => new, :recurse => true, :backup => false assert_nothing_raised do obj.handlebackup end bfile = [link, "puppet-bak"].join(".") assert(! FileTest.exists?(bfile), "Backed up when told not to") assert_apply(obj) assert(! FileTest.exists?(bfile), "Backed up when told not to") end # Make sure we consistently handle backups for all cases. def test_ensure_with_backups # We've got three file types, so make sure we can replace any type # with the other type and that backups are done correctly. types = [:file, :directory, :link] dir = tempfile() path = File.join(dir, "test") linkdest = tempfile() creators = { :file => proc { File.open(path, "w") { |f| f.puts "initial" } }, :directory => proc { Dir.mkdir(path) }, :link => proc { File.symlink(linkdest, path) } } bucket = Puppet::Type.type(:filebucket).new :name => "main", :path => tempfile() obj = Puppet::Type.newfile :path => path, :force => true, :links => :manage catalog = mk_catalog(obj, bucket) Puppet[:trace] = true ["main", false].each do |backup| obj[:backup] = backup obj.finish types.each do |should| types.each do |is| # It makes no sense to replace a directory with a directory # next if should == :directory and is == :directory Dir.mkdir(dir) # Make the thing creators[is].call obj[:ensure] = should if should == :link obj[:target] = linkdest else if obj.property(:target) obj.delete(:target) end end # First try just removing the initial data assert_nothing_raised do obj.remove_existing(should) end unless is == should # Make sure the original is gone assert(! FileTest.exists?(obj[:path]), "remove_existing did not work: " + "did not remove %s with %s" % [is, should]) end FileUtils.rmtree(obj[:path]) # Now make it again creators[is].call property = obj.property(:ensure) currentvalue = property.retrieve unless property.insync?(currentvalue) assert_nothing_raised do property.sync end end FileUtils.rmtree(dir) end end end end if Process.uid == 0 # Testing #364. def test_writing_in_directories_with_no_write_access # Make a directory that our user does not have access to dir = tempfile() Dir.mkdir(dir) # Get a fake user user = nonrootuser # and group group = nonrootgroup # First try putting a file in there path = File.join(dir, "file") file = Puppet::Type.newfile :path => path, :owner => user.name, :group => group.name, :content => "testing" # Make sure we can create it assert_apply(file) assert(FileTest.exists?(path), "File did not get created") # And that it's owned correctly assert_equal(user.uid, File.stat(path).uid, "File has the wrong owner") assert_equal(group.gid, File.stat(path).gid, "File has the wrong group") assert_equal("testing", File.read(path), "file has the wrong content") # Now make a dir subpath = File.join(dir, "subdir") subdir = Puppet::Type.newfile :path => subpath, :owner => user.name, :group => group.name, :ensure => :directory # Make sure we can create it assert_apply(subdir) assert(FileTest.directory?(subpath), "File did not get created") # And that it's owned correctly assert_equal(user.uid, File.stat(subpath).uid, "File has the wrong owner") assert_equal(group.gid, File.stat(subpath).gid, "File has the wrong group") assert_equal("testing", File.read(path), "file has the wrong content") end end # #366 def test_replace_aliases file = Puppet::Type.newfile :path => tempfile() file[:replace] = :yes assert_equal(:true, file[:replace], ":replace did not alias :true to :yes") file[:replace] = :no assert_equal(:false, file[:replace], ":replace did not alias :false to :no") end def test_backup path = tempfile() file = Puppet::Type.newfile :path => path, :content => "yay" catalog = mk_catalog(file) catalog.finalize # adds the default resources. [false, :false, "false"].each do |val| assert_nothing_raised do file[:backup] = val end assert_equal(false, file[:backup], "%s did not translate" % val.inspect) end [true, :true, "true", ".puppet-bak"].each do |val| assert_nothing_raised do file[:backup] = val end assert_equal(".puppet-bak", file[:backup], "%s did not translate" % val.inspect) end # Now try a non-bucket string assert_nothing_raised do file[:backup] = ".bak" end assert_equal(".bak", file[:backup], ".bak did not translate") # Now try a non-existent bucket assert_nothing_raised do file[:backup] = "main" end assert_equal("main", file[:backup], "bucket name was not retained") assert_equal("main", file.bucket, "file's bucket was not set") # And then an existing bucket obj = Puppet::Type.type(:filebucket).new :name => "testing" catalog.add_resource(obj) bucket = obj.bucket assert_nothing_raised do file[:backup] = "testing" end assert_equal("testing", file[:backup], "backup value was reset") assert_equal(obj.bucket, file.bucket, "file's bucket was not set") end def test_pathbuilder dir = tempfile() Dir.mkdir(dir) file = File.join(dir, "file") File.open(file, "w") { |f| f.puts "" } obj = Puppet::Type.newfile :path => dir, :recurse => true, :mode => 0755 catalog = mk_catalog obj transaction = Puppet::Transaction.new(catalog) assert_equal("/%s" % obj.ref, obj.path) list = transaction.eval_generate(obj) fileobj = catalog.resource(:file, file) assert(fileobj, "did not generate file object") assert_equal("/%s" % fileobj.ref, fileobj.path, "did not generate correct subfile path") end # Testing #403 def test_removal_with_content_set path = tempfile() File.open(path, "w") { |f| f.puts "yay" } file = Puppet::Type.newfile(:name => path, :ensure => :absent, :content => "foo") assert_apply(file) assert(! FileTest.exists?(path), "File was not removed") end # Testing #438 def test_creating_properties_conflict file = tempfile() first = tempfile() second = tempfile() params = [:content, :source, :target] params.each do |param| assert_nothing_raised("%s conflicted with ensure" % [param]) do Puppet::Type.newfile(:path => file, param => first, :ensure => :file) end params.each do |other| next if other == param assert_raise(Puppet::Error, "%s and %s did not conflict" % [param, other]) do Puppet::Type.newfile(:path => file, other => first, param => second) end end end end # Testing #508 if Process.uid == 0 def test_files_replace_with_right_attrs source = tempfile() File.open(source, "w") { |f| f.puts "some text" } File.chmod(0755, source) user = nonrootuser group = nonrootgroup path = tempfile() good = {:uid => user.uid, :gid => group.gid, :mode => 0640} run = Proc.new do |obj, msg| assert_apply(obj) stat = File.stat(obj[:path]) good.each do |should, sval| if should == :mode current = filemode(obj[:path]) else current = stat.send(should) end assert_equal(sval, current, "Attr %s was not correct %s" % [should, msg]) end end file = Puppet::Type.newfile(:path => path, :owner => user.name, :group => group.name, :mode => 0640, :backup => false) {:source => source, :content => "some content"}.each do |attr, value| file[attr] = value # First create the file run.call(file, "upon creation with %s" % attr) # Now change something so that we replace the file case attr - when :source: + when :source File.open(source, "w") { |f| f.puts "some different text" } - when :content: file[:content] = "something completely different" + when :content; file[:content] = "something completely different" else raise "invalid attr %s" % attr end # Run it again run.call(file, "after modification with %s" % attr) # Now remove the file and the attr file.delete(attr) File.unlink(path) end end end # Make sure we default to the "puppet" filebucket, rather than a string def test_backup_defaults_to_bucket path = tempfile file = Puppet::Type.newfile(:path => path, :content => 'some content') file.finish assert_instance_of(Puppet::Network::Client::Dipper, file.bucket, "did not default to a filebucket for backups") end # #567 def test_missing_files_are_in_sync file = tempfile obj = Puppet::Type.newfile(:path => file, :mode => 0755) changes = obj.evaluate assert(changes.empty?, "Missing file with no ensure resulted in changes") end def test_root_dir_is_named_correctly obj = Puppet::Type.newfile(:path => '/', :mode => 0755) assert_equal("/", obj.title, "/ directory was changed to empty string") end # #1010 and #1037 -- write should fail if the written checksum does not # match the file we thought we were writing. def test_write_validates_checksum file = tempfile inst = Puppet::Type.newfile(:path => file, :content => "something") tmpfile = file + ".puppettmp" wh = mock 'writehandle', :print => nil rh = mock 'readhandle' rh.expects(:read).with(512).times(2).returns("other").then.returns(nil) File.expects(:open).with { |*args| args[0] == tmpfile and args[1] != "r" }.yields(wh) File.expects(:open).with { |*args| args[0] == tmpfile and args[1] == "r" }.yields(rh) File.stubs(:rename) FileTest.stubs(:exist?).returns(true) FileTest.stubs(:file?).returns(true) inst.expects(:fail) inst.write("something", :whatever) end end diff --git a/test/test b/test/test index a8efd50bc..1e0f9fd2b 100755 --- a/test/test +++ b/test/test @@ -1,241 +1,241 @@ #!/usr/bin/env ruby # # = Synopsis # # Run unit tests, usually with the goal of resolving some conflict # between tests. # # = Usage # # test [-d|--debug] [-f|--files] [-h|--help] [-n method] [-v|--verbose] [file] ... # # = Description # # This script is useful for running a specific subset of unit tests, especially # when attempting to find a conflict between tests. By default, it will take # the first argument you pass it and run that test or test directory with each # test directory in turn, looking for a failure. If any tests fail, then # the script will drill into that test directory, looking for the specific conflict. # # In this way, when you have deduced you have two conflicting unit tests (tests which # pass in isolation but fail when run together), you can tell this script where # the failure is and leave it alone to find the conflict. # # This script is different from the Rakefile because it will run all tests in # a single process, whereas if you ask the Rakefile to run multiple tests, it will # run them in separate processes thus making it impossible to find conflicts. # # = Examples # # Attempt to resolve a conflict between a single test suite that could be anywhere: # # ./test ral/providers/user.rb # # Determine whether two individual files conflict: # # ./test --files language/functions.rb ral/providers/provider.rb # # Run the same test, but only run a specific unit test: # # ./test -d -n test_search --files language/functions.rb ral/providers/provider.rb # # = Options # # debug:: # Enable full debugging. # # files:: # Specify exactly which files to test. # # help:: # Print this help message # # n:: # Specify a single unit test to run. You can still specify as many files # as you want. # # verbose:: # Print extra information. # # = Example # # puppet -l /tmp/script.log script.pp # # = Author # # Luke Kanies # # = Copyright # # Copyright (c) 2005 Reductive Labs, LLC # Licensed under the GNU Public License require 'find' require 'getoptlong' include Find result = GetoptLong.new( [ "--debug", "-d", GetoptLong::NO_ARGUMENT ], [ "--verbose", "-v", GetoptLong::NO_ARGUMENT ], [ "-n", GetoptLong::REQUIRED_ARGUMENT ], [ "--files", "-f", GetoptLong::NO_ARGUMENT ], [ "--help", "-h", GetoptLong::NO_ARGUMENT ] ) usage = "USAGE: %s [--help] suite" % $0 $options = {} keep = [] result.each { |opt,arg| case opt when "--verbose" $options[:verbose] = true when "--files" $options[:files] = true when "--debug" $options[:debug] = true $options[:verbose] = true when "--help" puts usage exit else keep << opt keep << arg if arg end } def dirs Dir.glob("*").find_all { |d| FileTest.directory?(d) }.reject { |d| ["lib", "data"].include?(d) } end def rake(*args) print "trying %s..." % args.join(" ") output = %x{rake %s} % args.join(" ") if $?.exitstatus == 0 puts "succeeded" return true else puts "failed" return false end end def resolve(dir) dirs = dirs() # If the passed dir is a subdir or file, put the basedir last if dir.include?(File::SEPARATOR) basedir = dir.split(File::SEPARATOR)[0] if dirs.include?(basedir) dirs.delete(basedir) dirs << basedir end end failed = nil dirs.each do |d| next if d == dir unless run([d, dir]) failed = d break end end puts "%s failed" % failed files = ruby_files(failed) files.each do |file| unless run([file, dir]) puts file exit(0) end end exit(1) end def ruby_files(dir) files = [] # First collect the entire file list. begin find(dir) { |f| files << f if f =~ /\.rb$/ } rescue => detail puts "could not find on %s: %s" % [dir.inspect, detail] end files end def run(files, flags = nil) args = %w{ruby} args << "-Ilib:../lib" args << "lib/rake/puppet_test_loader.rb" if flags args += flags end args += ARGV print files.join(" ") + "... " $stdout.flush files.each do |file| case File.stat(file).ftype - when "file": args << file - when "directory": args += ruby_files(file) + when "file"; args << file + when "directory"; args += ruby_files(file) else $stderr.puts "Skipping %s; can't handle %s" % [file, File.stat(file).ftype] end end args = args.join(" ") if $options[:verbose] p args end output = %x{#{args} 2>&1} if $options[:debug] print output end if $?.exitstatus == 0 puts "succeeded" return true else puts "failed" puts output return false end end if $options[:files] run(ARGV, keep) else dir = ARGV.shift unless dir $stderr.puts usage exit(1) end resolve(dir) end # # #files = [] # #args.each do |test| # if FileTest.directory?(test) # files += ruby_files(test) # end #end ## Now load all of our files. #files.each do |file| # load file unless file =~ /^-/ #end # #runner = Test::Unit::AutoRunner.new(false) #runner.process_args #runner.run diff --git a/test/util/settings.rb b/test/util/settings.rb index f34cbbc46..2ff9e66ec 100755 --- a/test/util/settings.rb +++ b/test/util/settings.rb @@ -1,717 +1,717 @@ #!/usr/bin/env ruby require File.dirname(__FILE__) + '/../lib/puppettest' require 'mocha' require 'puppettest' require 'puppet/util/settings' require 'puppettest/parsertesting' class TestSettings < Test::Unit::TestCase include PuppetTest include PuppetTest::ParserTesting CElement = Puppet::Util::Settings::CElement CBoolean = Puppet::Util::Settings::CBoolean def setup super @config = mkconfig end def set_configs(config = nil) config ||= @config config.setdefaults("main", :one => ["a", "one"], :two => ["a", "two"], :yay => ["/default/path", "boo"], :mkusers => [true, "uh, yeah"], :name => ["testing", "a"] ) config.setdefaults("section1", :attr => ["a", "one"], :attrdir => ["/another/dir", "two"], :attr3 => ["$attrdir/maybe", "boo"] ) end def check_for_users count = Puppet::Type.type(:user).inject(0) { |c,o| c + 1 } assert(count > 0, "Found no users") end def test_to_config set_configs newc = mkconfig set_configs(newc) # Reset all of the values, so we know they're changing. newc.each do |name, obj| next if name == :name newc[name] = true end newfile = tempfile() File.open(newfile, "w") { |f| @config.to_config.split("\n").each do |line| # Uncomment the settings, so they actually take. if line =~ / = / f.puts line.sub(/^\s*#/, '') else f.puts line end end } newc.setdefaults :section, :config => [newfile, "eh"] assert_nothing_raised("Could not parse generated configuration") { newc.parse } @config.each do |name, object| assert_equal(@config[name], newc[name], "Parameter %s is not the same" % name) end end def mkconfig c = nil assert_nothing_raised { c = Puppet::Util::Settings.new } return c end def test_addbools assert_nothing_raised { @config.setdefaults(:testing, :booltest => [true, "testing"]) } assert(@config[:booltest]) @config = mkconfig assert_nothing_raised { @config.setdefaults(:testing, :booltest => ["true", "testing"]) } assert(@config[:booltest]) assert_nothing_raised { @config[:booltest] = false } assert(! @config[:booltest], "Booltest is not false") assert_nothing_raised { @config[:booltest] = "false" } assert(! @config[:booltest], "Booltest is not false") assert_raise(ArgumentError) { @config[:booltest] = "yayness" } assert_raise(ArgumentError) { @config[:booltest] = "/some/file" } end def test_strings val = "this is a string" assert_nothing_raised { @config.setdefaults(:testing, :strtest => [val, "testing"]) } assert_equal(val, @config[:strtest]) # Verify that variables are interpolated assert_nothing_raised { @config.setdefaults(:testing, :another => ["another $strtest", "testing"]) } assert_equal("another #{val}", @config[:another]) end def test_files c = mkconfig parent = "/puppet" assert_nothing_raised { @config.setdefaults(:testing, :parentdir => [parent, "booh"]) } assert_nothing_raised { @config.setdefaults(:testing, :child => ["$parent/child", "rah"]) } assert_equal(parent, @config[:parentdir]) assert_equal("/puppet/child", File.join(@config[:parentdir], "child")) end def test_getset initial = "an initial value" assert_raise(ArgumentError) { @config[:yayness] = initial } default = "this is a default" assert_nothing_raised { @config.setdefaults(:testing, :yayness => [default, "rah"]) } assert_equal(default, @config[:yayness]) assert_nothing_raised { @config[:yayness] = initial } assert_equal(initial, @config[:yayness]) assert_nothing_raised { @config.clear } assert_equal(default, @config[:yayness], "'clear' did not remove old values") assert_nothing_raised { @config[:yayness] = "not default" } assert_equal("not default", @config[:yayness]) end def test_parse_file text = %{ one = this is a test two = another test owner = root group = root yay = /a/path [main] four = five six = seven [section1] attr = value owner = puppet group = puppet attrdir = /some/dir attr3 = $attrdir/other } file = tempfile() File.open(file, "w") { |f| f.puts text } result = nil assert_nothing_raised { result = @config.send(:parse_file, file) } main = result[:main] assert(main, "Did not get section for main") { :one => "this is a test", :two => "another test", :owner => "root", :group => "root", :yay => "/a/path", :four => "five", :six => "seven" }.each do |param, value| assert_equal(value, main[param], "Param %s was not set correctly in main" % param) end section1 = result[:section1] assert(section1, "Did not get section1") { :attr => "value", :owner => "puppet", :group => "puppet", :attrdir => "/some/dir", :attr3 => "$attrdir/other" }.each do |param, value| assert_equal(value, section1[param], "Param %s was not set correctly in section1" % param) end end def test_arghandling c = mkconfig assert_nothing_raised { @config.setdefaults("testing", :onboolean => [true, "An on bool"], :offboolean => [false, "An off bool"], :string => ["a string", "A string arg"], :file => ["/path/to/file", "A file arg"] ) } data = { :onboolean => [true, false], :offboolean => [true, false], :string => ["one string", "another string"], :file => %w{/a/file /another/file} } data.each { |param, values| values.each { |val| opt = nil arg = nil if @config.boolean?(param) if val opt = "--%s" % param else opt = "--no-%s" % param end else opt = "--%s" % param arg = val end assert_nothing_raised("Could not handle arg %s with value %s" % [opt, val]) { @config.handlearg(opt, arg) } } } end def test_addargs @config.setdefaults("testing", :onboolean => [true, "An on bool"], :offboolean => [false, "An off bool"], :string => ["a string", "A string arg"], :file => ["/path/to/file", "A file arg"] ) should = [] @config.each { |name, element| element.expects(:getopt_args).returns([name]) should << name } result = [] assert_nothing_raised("Add args failed") do @config.addargs(result) end assert_equal(should, result, "Did not call addargs correctly.") end def test_addargs_functional @config.setdefaults("testing", :onboolean => [true, "An on bool"], :string => ["a string", "A string arg"] ) result = [] should = [] assert_nothing_raised("Add args failed") do @config.addargs(result) end @config.each do |name, element| if name == :onboolean should << ["--onboolean", GetoptLong::NO_ARGUMENT] should << ["--no-onboolean", GetoptLong::NO_ARGUMENT] elsif name == :string should << ["--string", GetoptLong::REQUIRED_ARGUMENT] end end assert_equal(should, result, "Add args functional test failed") end def test_groupsetting cfile = tempfile() group = "yayness" File.open(cfile, "w") do |f| f.puts "[main] group = #{group} " end config = mkconfig config.setdefaults(Puppet[:name], :group => ["puppet", "a group"], :config => [cfile, "eh"]) assert_nothing_raised { config.parse } assert_equal(group, config[:group], "Group did not take") end # provide a method to modify and create files w/out specifying the info # already stored in a config def test_writingfiles File.umask(0022) path = tempfile() mode = 0644 config = mkconfig args = { :default => path, :mode => mode, :desc => "yay" } user = nonrootuser() group = nonrootgroup() if Puppet::Util::SUIDManager.uid == 0 args[:owner] = user.name args[:group] = group.name end config.setdefaults(:testing, :myfile => args) assert_nothing_raised { config.write(:myfile) do |file| file.puts "yay" end } assert_equal(mode, filemode(path), "Modes are not equal") # OS X is broken in how it chgrps files if Puppet::Util::SUIDManager.uid == 0 assert_equal(user.uid, File.stat(path).uid, "UIDS are not equal") case Facter["operatingsystem"].value - when /BSD/, "Darwin": # nothing + when /BSD/, "Darwin" # nothing else assert_equal(group.gid, File.stat(path).gid, "GIDS are not equal") end end end def test_mkdir File.umask(0022) path = tempfile() mode = 0755 config = mkconfig args = { :default => path, :mode => mode, :desc => "a file" } user = nonrootuser() group = nonrootgroup() if Puppet::Util::SUIDManager.uid == 0 args[:owner] = user.name args[:group] = group.name end config.setdefaults(:testing, :mydir => args) assert_nothing_raised { config.mkdir(:mydir) } assert_equal(mode, filemode(path), "Modes are not equal") # OS X and *BSD is broken in how it chgrps files if Puppet::Util::SUIDManager.uid == 0 assert_equal(user.uid, File.stat(path).uid, "UIDS are not equal") case Facter["operatingsystem"].value - when /BSD/, "Darwin": # nothing + when /BSD/, "Darwin" # nothing else assert_equal(group.gid, File.stat(path).gid, "GIDS are not equal") end end end # Make sure that tags are ignored when configuring def test_configs_ignore_tags config = mkconfig file = tempfile() config.setdefaults(:mysection, :mydir => [file, "a file"] ) Puppet[:tags] = "yayness" assert_nothing_raised { config.use(:mysection) } assert(FileTest.directory?(file), "Directory did not get created") assert_equal("yayness", Puppet[:tags], "Tags got changed during config") end def test_configs_replace_in_url config = mkconfig config.setdefaults(:mysection, :name => ["yayness", "yay"]) config.setdefaults(:mysection, :url => ["http://$name/rahness", "yay"]) val = nil assert_nothing_raised { val = config[:url] } assert_equal("http://yayness/rahness", val, "Settings got messed up") end def test_correct_type_assumptions file = Puppet::Util::Settings::CFile element = Puppet::Util::Settings::CElement bool = Puppet::Util::Settings::CBoolean # We have to keep these ordered, unfortunately. [ ["/this/is/a/file", file], ["true", bool], [true, bool], ["false", bool], ["server", element], ["http://$server/yay", element], ["$server/yayness", file], ["$server/yayness.conf", file] ].each do |ary| config = mkconfig value, type = ary name = value.to_s + "_setting" assert_nothing_raised { config.setdefaults(:yayness, name => { :default => value, :desc => name.to_s}) } elem = config.element(name) assert_instance_of(type, elem, "%s got created as wrong type" % value.inspect) end end def test_parse_removes_quotes config = mkconfig() config.setdefaults(:mysection, :singleq => ["single", "yay"]) config.setdefaults(:mysection, :doubleq => ["double", "yay"]) config.setdefaults(:mysection, :none => ["noquote", "yay"]) config.setdefaults(:mysection, :middle => ["midquote", "yay"]) file = tempfile() # Set one parameter in the file File.open(file, "w") { |f| f.puts %{[main]\n singleq = 'one' doubleq = "one" none = one middle = mid"quote } } config.setdefaults(:mysection, :config => [file, "eh"]) assert_nothing_raised { config.parse } %w{singleq doubleq none}.each do |p| assert_equal("one", config[p], "%s did not match" % p) end assert_equal('mid"quote', config["middle"], "middle did not match") end # Test that config parameters correctly call passed-in blocks when the value # is set. def test_paramblocks config = mkconfig() testing = nil assert_nothing_raised do config.setdefaults :test, :blocktest => {:default => "yay", :desc => "boo", :hook => proc { |value| testing = value }} end elem = config.element(:blocktest) assert_nothing_raised do assert_equal("yay", elem.value) end assert_nothing_raised do config[:blocktest] = "yaytest" end assert_nothing_raised do assert_equal("yaytest", elem.value) end assert_equal("yaytest", testing) assert_nothing_raised do config[:blocktest] = "another" end assert_nothing_raised do assert_equal("another", elem.value) end assert_equal("another", testing) # Now verify it works from setdefault assert_nothing_raised do config.setdefaults :test, :blocktest2 => { :default => "yay", :desc => "yay", :hook => proc { |v| testing = v } } end assert_equal("yay", config[:blocktest2]) assert_nothing_raised do config[:blocktest2] = "footest" end assert_equal("footest", config[:blocktest2]) assert_equal("footest", testing) end def test_no_modify_root config = mkconfig config.setdefaults(:yay, :mydir => {:default => tempfile(), :mode => 0644, :owner => "root", :group => "root", :desc => "yay" }, :mkusers => [false, "yay"] ) assert_nothing_raised do config.use(:yay) end # Now enable it so they'll be added config[:mkusers] = true comp = config.to_catalog comp.vertices.find_all { |r| r.class.name == :user }.each do |u| assert(u.name != "root", "Tried to manage root user") end comp.vertices.find_all { |r| r.class.name == :group }.each do |u| assert(u.name != "root", "Tried to manage root group") assert(u.name != "wheel", "Tried to manage wheel group") end # assert(yay, "Did not find yay component") # yay.each do |c| # puts @config.ref # end # assert(! yay.find { |o| o.class.name == :user and o.name == "root" }, # "Found root user") # assert(! yay.find { |o| o.class.name == :group and o.name == "root" }, # "Found root group") end # #415 def test_remove_trailing_spaces config = mkconfig() file = tempfile() File.open(file, "w") { |f| f.puts "rah = something " } config.setdefaults(:yay, :config => [file, "eh"], :rah => ["testing", "a desc"]) assert_nothing_raised { config.parse } assert_equal("something", config[:rah], "did not remove trailing whitespace in parsing") end # #484 def test_parsing_unknown_variables logstore() config = mkconfig() file = tempfile() File.open(file, "w") { |f| f.puts %{[main]\n one = one two = yay } } config.setdefaults(:mysection, :config => [file, "eh"], :one => ["yay", "yay"]) assert_nothing_raised("Unknown parameter threw an exception") do config.parse end end def test_multiple_interpolations @config.setdefaults(:section, :one => ["oneval", "yay"], :two => ["twoval", "yay"], :three => ["$one/$two", "yay"] ) assert_equal("oneval/twoval", @config[:three], "Did not interpolate multiple variables") end # Make sure we can replace ${style} var names def test_curly_replacements @config.setdefaults(:section, :one => ["oneval", "yay"], :two => ["twoval", "yay"], :three => ["$one/${two}/${one}/$two", "yay"] ) assert_equal("oneval/twoval/oneval/twoval", @config[:three], "Did not interpolate curlied variables") end # Test to make sure that we can set and get a short name def test_celement_short_name element = nil assert_nothing_raised("Could not create celement") do element = CElement.new :short => "n", :desc => "anything", :settings => Puppet::Util::Settings.new end assert_equal("n", element.short, "Short value is not retained") assert_raise(ArgumentError,"Allowed multicharactered short names.") do element = CElement.new :short => "no", :desc => "anything", :settings => Puppet::Util::Settings.new end end # Test to make sure that no two celements have the same short name def test_celement_short_name_not_duplicated config = mkconfig assert_nothing_raised("Could not create celement with short name.") do config.setdefaults(:main, :one => { :default => "blah", :desc => "anything", :short => "o" }) end assert_nothing_raised("Could not create second celement with short name.") do config.setdefaults(:main, :two => { :default => "blah", :desc => "anything", :short => "i" }) end assert_raise(ArgumentError, "Could create second celement with duplicate short name.") do config.setdefaults(:main, :three => { :default => "blah", :desc => "anything", :short => "i" }) end # make sure that when the above raises an expection that the config is not included assert(!config.include?(:three), "Invalid configuration item was retained") end # Tell getopt which arguments are valid def test_get_getopt_args element = CElement.new :name => "foo", :desc => "anything", :settings => Puppet::Util::Settings.new assert_equal([["--foo", GetoptLong::REQUIRED_ARGUMENT]], element.getopt_args, "Did not produce appropriate getopt args") element.short = "n" assert_equal([["--foo", "-n", GetoptLong::REQUIRED_ARGUMENT]], element.getopt_args, "Did not produce appropriate getopt args") element = CBoolean.new :name => "foo", :desc => "anything", :settings => Puppet::Util::Settings.new assert_equal([["--foo", GetoptLong::NO_ARGUMENT], ["--no-foo", GetoptLong::NO_ARGUMENT]], element.getopt_args, "Did not produce appropriate getopt args") element.short = "n" assert_equal([["--foo", "-n", GetoptLong::NO_ARGUMENT],["--no-foo", GetoptLong::NO_ARGUMENT]], element.getopt_args, "Did not produce appropriate getopt args") end end