diff options
author | Stig Sandbeck Mathisen <ssm@debian.org> | 2012-01-26 11:22:40 +0100 |
---|---|---|
committer | Stig Sandbeck Mathisen <ssm@debian.org> | 2012-01-26 11:22:40 +0100 |
commit | c17b3ba16e7013f06416f10b8752ef783f048717 (patch) | |
tree | 790f13f167199b954007e17d1c55a8d1b0218775 | |
parent | 32af6143486ceb24a93636445d1883f5fe2299d7 (diff) | |
download | puppet-upstream/2.7.10.tar.gz |
Imported Upstream version 2.7.10upstream/2.7.10
237 files changed, 8834 insertions, 1201 deletions
@@ -1,3 +1,343 @@ +2.7.10 +=== +e31369a (#6541) Use the same filebucket for backup and restore +0d8a22a (#11767) No longer necessary to delete ssl directory in each test +e1828ba Updated CHANGELOG for 2.7.10rc1 +8da947f (#11996) Fix file content test after hash changes. +884fe75 (#11996) Fix file server recursion test after hash changes. +3fb0938 (#11996) Fix graph cycle reporting order after hash changes. +a758066 (#11996) Fix test failures due to hash processing order changes. +d6d6e60 (#11600) Remove module face +1847228 (#11499) Better validation for IPv4 and IPv6 address in host type. +a406a2e (#11802) Fix module list specs on Windows +13238af (#11803) Fix broken tests for uninstall action on module face +7fdd8a1 (#11888) Revert 483a1d9 for 2.7.x only +97fffa8 (#11803) Add uninstall action for the module face +2d4af0e (#11958) Improve error msg for missing pip command +032043e (#11046) Add support for user expiry in pw user provider +9b8829d (#11046) Improve pw group provider on FreeBSD +fb111ef (#10962) Make sure managehome is respected on FreeBSD +884381f (#11318) Add password management on FreeBSD +acd2f91 (Maint) Fix time-dependent certificate factory test failures +69dfb34 (#5246) Fix spec test expectations in 2.7.x branch +cd56926 (#11802) Add module list action +fecf5d6 (#11803) Add modules_by_path method to environments +dad8697 (#11802) Make Puppet::Module able to find module in specific path +520ac07 maint: Cleanup environment_spec +b27f3cc maint: Fix Puppet::Node::Environment::Helper specs +dfa539a maint: Remove unused method requires +b967da2 maint: Remove Module.modulepath +e1f2f37 (#11929) Always serve files in binary mode +24f2a65 (#2927) Support symbolic file modes. +48726b6 Finer-grained protocol for property `insync?`. +d3a33af Whitespace damage cleanup on property.rb +c58bfbc (#8341) Only load facts once per puppet run +2b0d3b8 (#3419) Fix OS X Ruby supplementary group handling +2ca9f9d (#11888) Switch up2date,systemd,redhat providers to osfamily fact +e0e31d5 (#5246) Puppetd does not remove it's pidfile when it exits +90bdef6 (#4855) Fix group resource in OS X +6c14a28 Build a Rake task for building Apple Packages +d5bef5e (#2773) Use launchctl load -w in launchd provider +5accc69 (#11714) Use `%~dp0` to resolve bat file's install directory +c865a80 Clean up launchd spec tests +24af732 (#11714) Add envpuppet batch file to run Puppet from source on Windows +7edaed5 (#11847) Don't hard code ruby install paths in Windows batch files +1978f52 Match old slightly different version of "hostname was not match" +ed9da67 Fixed #11844 - Typo in exec documentation +0ab4597 (#11764) Fix failing cron test +0f0aa1e Fixup two space merge conflict from 2.7.x => master +d092860 Retry inventory ActiveRecord transaction failure +a966eb3 (#11717) Set password before creating user on Windows +018f36d (#11293) Add password get/set behavior for 10.7 +073ca03 (#11764) Fix cron jobs for passing block to method +4807c6d (#11740) Disable failing test on Windows +c751e01 Revert "Access user password hash in OS X 10.7" +11b8c5a Access user password hash in OS X 10.7 +1e4bc59 (#11741) Use dns_alt_names instead of certdnsnames in acceptance tests +ca73283 (#11641) Properly track blockers when generating additional resources +31eef75 (#7296) Make the Debian service provider handle services that don't conform to the debain policy manual. +0ffe1ac (#4836) - Agent --disable should allow to put a message +7777d91 (#3757) - Refactor enable/disable to its own module +b434e3b (#3757) - Move enable/disable to its own lock +f7c2ea4 Fix failing tests with ruby 1.9.2 in the instrumentation framework +b2411b6 Use all lower-case file name for Puppet::Util::Instrumentation::Instrumentable +c560f71 Maint: Fix typo in usage example for create_resources function +751ef88 Set of faces to manage instrumentation listeners, data and probes +493a1b7 Example probes for the indirector +fc43694 Add probe indirection for probe management +2bf6105 Process name instrumentation listener +6c138d7 Add the 'performance' instrumentation listener +782f341 Add the example 'log' listener +ff36deb Add a way to add probe to puppet code +6b7fcf6 Add indirection (REST usable) to manipulate instrumentation +b743b4d Instrumentation foundation layer +b1af29b (#8119) Write reports to a temporary file and move them into place +735acad (#11414) Test Augeas versions correctly with versioncmp +3239ab3 (#11414) Save/execute changes on versions of Augeas < 0.3.6 +8ec6086 Account for Windows file mode translation for lastrunfile +b28cac8 (#7106) Obey specified owner, group, and permissions for last run summary file +b8c5ee2 (#11408) Fix fact and plugin sync on Windows +d4d3cb3 (#10586) Don't copy owner and group when sourcing files from master +1519d30 (#7428) Fix option parsing for ruby 1.9 in cert application +213cecc Revert "(#11423) Clearer error message about duplicate imported resources." +e8e1f57 (#11423) Clearer error message about duplicate imported resources. +ff396bf Maint: Fix redhatfedorasusecentosslesoelovm in type reference +6682fe7 Maint: Padding should be added when dochook strings are consumed, not hardcoded into them +b569c7e (#11404) Fix broken and unreliable indentation in provider lists in type reference +c15d997 Maint: Make indentation for markdown definition lists more readable and reliable +e80ca2d (#11404) Move markdown_header and markdown_definitionlist to Puppet::Util::Docs +471fb58 (#11333) Make Puppet::Type.ensurable? false when exists? is undefined +31cef94 Add config and puppet version to the last run summary file +f71af6f (#10676) Include all resource statuses in reports regardless of count +2be44d4 (#8062) Consider package epoch version when comparing yum package versions +e9a5116 (#6412) Return :undef when accessing non-existing hash/array elements +fc3f8b9 Updated CHANGELOG for 2.6.13 +cbd78da (#4865) Log when we start evaluating resources at the info level +e8f1407 maint: Add tap to fix Ruby 1.8.5 failures +ddde61e (#10321) Fix array support in schedule's range parameter +87f6f05 Update CHANGELOG for 2.7.9 release +da11dc7 Fix Ruby 1.8.5-incompatible code in FileBucket::Dipper spec +737c2f6 Fix Ruby 1.8.5-incompatible code in Transaction#eval_generate +042925d Fix Ruby 1.8.5-incompatible code in spec setup +0022f47 Revert "Build a Rake task for building Apple Packages" +3265b2c Updating CHANGELOG for 2.7.8 release +8d83c4e (maint) Fix grammar mistakes in README_DEVELOPER +8cca377 Build a Rake task for building Apple Packages +4275fd4 (#11291) Update description of show_diff setting to match behavior as of 2.7.8 +901a6b2 (#11291) Update description of show_diff setting to match behavior as of 2.7.8 +84fdf6f (#10109) Make resourcefile work with composite namevars +15f7a1c (#11246) Add README_DEVELOPER describing UTF-8 in Puppet +1e8e34b (#11276) Fix module install specs that fail on windows +c78d17e (#7110) Better SSL error message certificate doesn't match key +00c76f6 (#11276) Mark module tool tests as failing on Windows +599a146 (#11273) Updates init.pp.erb for style guide. +80e5d03 (#11198) Modulefiles should have a default license +eec7495 (#11246) Fix UTF-8 String#to_yaml exception +a01aab2 (#11246) Add UTF-8 String#to_yaml spec tests +a89fe49 (#9768) Add a defaults argument to create_resources +eb7be18 (#6830) Fix create_resources spec for ruby 1.9 +b2cfe28 (#7656) Rename module_tool face to module +c658e72 (#7656) Add details about module testing to templates +61d894e Merge pull request #61 from jblaine/patch-1 +62f89c4 (#7656) Use core Puppet semver.rb lib +e2a9ab9 (#7656) Replace SystemExit with better exceptions +5bc5c50 (#7656) Remove redundant uri module +22ac5b6 (#7656) Encourage documenting license, and contact +e9538af (#7656) Cleanup repository_spec tests +c79f157 (#7656) Update comment string in the metadata module +7a45cc7 (#7656) Rename `Modulefile` to `ModulefileReader` +f463d90 (#7656) Rename full_name to full_module_name +0843c9e (#7656) Removed spec.opts template +47e7b60 (#7656) Use rspec 2.x in generator templates +5a8dc51 (#7656) Rename contact to make_http_request +90b1fcd (#7656) Use a Puppet setting for default module repo +a44aef9 (#7656) Use Metadata fully qualified module name +27d26ae (#7656) Show the default install-dir in help output +24557b9 (#7656) Code cleanup for the generate action +bad0112 (#7656) Show the format of the status hash for build +6d0e88a (#7656) Add output to action examples +5220af5 (#7656) Fix inaccurate comment in the build action +f33c2e4 (#7656) Refactor the module face build action +62b59c1 (#7656) Bump module face to version 1.0.0 +feeecc4 (#7656) Remove unnecessary require of puppet/face +e45f5a7 Fix the targets in autorequire tests to use expand_path +b22df54 Fix tests for autorequiring links to work on windows +0200629 Fix arity of blocks to validate for file properties +94e9863 (#7004) Correctly form singular for indirections ending in 'es' +6641938 Updating CHANGELOG for 2.6.13rc1 +db962a5 (#5421) Link should autorequire target +bdeb3b7 (#7656) Add search action to module_tool face +41d1034 (#7656) Add install action to module_tool face +6d2fc99 (#7656) Add generate action to module_tool face +626d876 (#7656) Add clean action to module_tool face +5e77157 (#7656) Add changes action to module_tool face +da4a236 (#7656) Add build action to module_tool face +7df5303 (#7656) Port PMT to faces, bundle in Puppet core +bf9e847 (#7656) Port PMT integration test into Puppet core +bce8e43 (#7656) Port PMT unit test into Puppet core +ee4dbf4 (#7656) Port PMT test fixtures into Puppet core +eb617e5 (#7656) Add new PMT settings to core Puppet +ce218ef (#7656) Port PMT generator templates into core +0c61186 (#7656) Port PMT codebase into Puppet core +bb5386c Updated CHANGELOG for 2.7.8rc1 +3eff60a Readying for 2.7.8rc release +e4ee794 (#10739) Provide default subjectAltNames while bootstrapping master +2dedee6 (#2744) Don't automatically enable show_diff in noop mode +ef78358 Give variables more descriptive names +cd3d4ea maint: Rename xgenerate to add_dynamically_generated_resources +000a2d8 (#6907) Prefetch unsuitable providers +70114e9 (#6907) Allow providers to be selected in the run they become suitable +a0ee5c7 maint: Fix incorrect whitespace +4e8a73c Fix description in service provider test for FreeBSD +a2eab4f (#6697) Set service provider default path to /etc/rc.d on Archlinux +da75795 (#6335) Allow optional trailing comma in argument lists. +05b3cac (#10940) Deprecate `--apply` in favor of `--catalog` +47c786e Update CHANGELOG and packaging for 2.7.7 final +8030428 (#8255) Always use string modes when creating resources from FileSetting settings +c804346 (#7274) Output 4-digit file modes in File type +220f2ba (#10799) Regexp escaping too much +4462eb5 Merged 2.6.x into 2.7x +67e048b Updated CHANGELOG for 2.7.7rc2 +93aca5a maint: Fix failing specs for Windows exec provider +4f1f7e4 (#10807) Use SMF's svcadm -s option to wait for errors +3ab4d63 Fix #10066 - when fingerprinting, agent should not daemonize +7f3a1bb (#9617) Use an RbTreeMap to store ready resources +9eff0f4 (#9671) Implement RbTreeMap#each recursively, and #first/#last explicitly +5f7f467 (#9671) Return nodes from internal RbTreeMap recursion +1dc9c72 (#9671) Stop tracking size and height of nodes in RbTreeMap +f180f9b (#9617) Add a red-black tree map +c62e949 (#9617) Keep track of blockers for resources when traversing +2cb6d72 (#9671) Generated resources should not depend on the completed_ whit +a5845b7 (#9671) Exit early from #eval_generate if nothing is created +7002eff (#9617) Be smarter about finding parents when eval_generating +11fda78 maint: Don't File#expand_path when unmunging file paths +ad4316a (#9671) Use Array#concat rather than += +20260f3 maint: Correct the spelling of sentinel +4d9e0c0 Added missing RequestHeader entries to ext/rack/files/apache2.conf +4f03384 (#10614) Detect when trying to managing ACLs on a non-ACL volume +37b9f0f (#10614) Provide default metadata values for Windows ACLs +1cb37c9 (#10614) Add method for detecting Windows volumes that support ACLs +c9ee5a0 (#10614) Fix setting and clearing read-only attribute on Windows +ed27a90 (#10614) Fix error checking for Windows BOOL return values +7f0756d (#10727) Don't rely on Kernel#Pathname +23379d0 (#10614) Detect when trying to managing ACLs on a non-ACL volume +374fee5 (#10614) Provide default metadata values for Windows ACLs +f60e889 (#10614) Add method for detecting Windows volumes that support ACLs +1371dbd (#10614) Fix setting and clearing read-only attribute on Windows +a6996ba (#4865) Debug logging when we start evaluating resources. +7eb0197 (#10614) Fix error checking for Windows BOOL return values +cd2d2f1 (#9158) Support old and new versions of STOMP gem. +9dfd011 (#5617) Puppet queue logging +0a34697 (#2744) Display file diffs through the Puppet log system. +0c28238 (#9508) Be explicit is setting `auth any` for default ACLs. +f140eca Updated CHANGELOG for 2.7.7rc1 +057cda6 (#9508) Default ACL of `auth any` makes sense where we had `auth no` +78670ed (#9983) Checksum file in binary mode when storing to filebucket +da11a78 (#9983) Serve file content in binary mode +13f1054 (#9983) Read file content from disk using binary mode +4b4bb8b (#9983) Use binary mode when reading and writing FileBucketFiles +899833b (#9983) Read file content in binary mode when backing up +dc8bcf8 (#9983) Restore files in binary mode +489a679 (#9983) Checksum files in binary mode +f7bfa05 (#9983) Add method for reading binary files +674068a (#10269) Make directories executable so they can be cleaned up +fd747cc (#10365) Add pending test when file overwrites an executable directory +fe30d8f (#10315) Add pending tests when following symlinks +a22c7aa Maint: Fix test breakage +8576e86 (#10269) Search bit not set on newly created directories +a91cfa1 maint: Fix failing spec on old version of rspec +428e08c Stub File.open to not touch the disk +aa2a762 (#10289) Add an ext script to upload facts to inventory server +a97337f (#10346) Fix storeconfigs spec failures when run alone +5129d38 (#10289) Add a safe alternative to REST for inventory service +5c4daa4 (#7601) Use definition lists in indirection references +7df46a2 (#7601) Use definition lists in type references +ad97dc9 (#7601) Add markdown_definitionlist method to reference.rb +455c9aa Maint: Revise reference text for most types and providers +ced8e19 (#7601) Remove unnecessarily abstracted paramwrap method +a6957ac (#7601) Rename "h" method to "markdown_header" +7a0ade6 (#7601) Use << instead of += in references +7d65796 (#9109) Retrieve request parameters from the request body for POSTs +5a2952c (maint) Fix CA-related specs failing on Windows +42fb76e Fix typo in report debug message +65086c4 (#9544) Stub command in package spec that needs root priviledges +eab5965 missing includes in network XML-RPC handlers +7514d32 missing includes in network XML-RPC handlers +614526a (#10244) Restore Mongrel XMLRPC functionality +397a506 (#10244) Restore Mongrel XMLRPC functionality +fcaf7c5 Updated CHANGELOG for 2.6.12 +f51d221 Improve the error message when a CSR is rejected +d551747 Allow a master to bootstrap itself with dns_alt_names and autosign +0405196 (maint) Remove ssl dir before starting a master with DNS alt names +3ed6499 Backport Enumerable#count to Rubies < 1.8.7 +5f44c23 More 1.8.5 compatibility fixes. +ef1b960 Better 1.8.5 compatible implementation of `lines`. +246e875 (#2848) Config options require '_', not '-'. +3bdeb3a Ruby 1.8.5 compatibility changes in tests and code. +6866d4b Add `lines` alias for `each_line` in Ruby 1.8.5. +2f9ec3c s/not_to/should_not/ for older versions of RSpec 2. +56320ea (#2848) Eliminate redundant `master_dns_alt_names`. +de19861 (#2848) Remove the legacy SSLCertificates code +cf008a6 (#2848) Rework the xmlrpc CA handler to use the modern SSL code +32be180 (#2848) Remove unused xmlrpc code +5f2a44d (#2848) Consistent return values from `subject_alt_names` accessors. +5e507f2 (#2848) Consistently use `subject_alt_names` as accessor name. +5ac2417 (#2848) Don't strip the subjectAltName label when listing. +44cf3a2 (#2848) Don't enable `emailProtection` for server keys. +d66def9 (#2848) Only mark `subjectAltName` critical if `subject` is empty. +8174047 (#2848) Migrate `dns-alt-names` back to settings. +f18df2b Wire up the `setbycli` slot in Puppet settings. +efa61f2 (#2848) rename subject-alt-name option to dns-alt-names +f103b20 (#2848) Rename `certdnsnames` to match new behaviour. +363b47b (#2848) Use `certdnsnames` when bootstrapping a local master. +49334ff (#2848) CSR subjectAltNames handling while signing. +5f2af93 (#2848) List subject alt names in output of puppet cert --list +bb475ec (#7224) Add a helper to Puppet::SSL::Certificate to retrieve alternate names +bab9310 (#2848) Rewrite SSL Certificate Factory, fixing `subjectAltName` leak. +fca1ff0 (#2848) Reject unknown (== all) extensions on the CSR. +443a756 (#2848) extract the subjectAltName value from the CSR. +66101f1 (#2848) Set `certdnsnames` values into the CSR. +77b814f (#6928) Don't blow up when the method is undefined... +5427f1e (#6928) backport Symbol#to_proc for Ruby < 1.8.7 +6ef1d3a (#6371) Update lastchg field in shadow file on Solaris. +c343615 (#10161) Parenthesize method arguments +1912c19 (#8547) Update storeconfigclean script to read puppet.conf +5721ab9 Maint: Remove duplicate path extension code +edc721e (#9636) Always set $CHILD_STATUS when executing on Windows +448d5db (#9636) Fix PATHEXT resolution for paths other than system32 +424379d (#9996) Restore functionality for multi-line commands in exec resources +ad98d47 (#9831) Standardize Windows provider confining +0366789 (#9997) Add mysql2 gem support +cf8fae2 (#9832) General StoreConfigs regression. +1e8a2cd (#9607) Only validate package source when it is set or needed +0258096 (#9461) Resolve executables using PATHEXT on Windows +d78afda (#9938) Allow directory sticky-ness to be set +23b4864 Maint: Document tag metaparameter's ability to take an array +b3c0f1d Stub method for getting roles from the user provider +006a128 Set vardir so that msi package provider runs on Windows +4185b4e Add Windows-specific tests when user parameter specified in exec +aab6b40 Disable mount provider tests on Windows +58f97e3 Update test due to lack of 'true' on Windows +1fd90c3 Change tests to not use 'mount' provider +220f5e0 Added 'touch' method enabling tests to run on Windows +ca0bc4f Change test to not call 'rm -rf' +1883455 Remove 'fails_on_windows' tag for passing tests +0d7c797 (#8414) Create scheduled_task type for use with Windows scheduled tasks +4ddef89 (#8414) Require win32-taskscheduler gem on Windows +0ecf3ab Add ability to look up fully qualified local accounts using Puppet::Util::Adsi.sid_for_account +23d5aeb Add support for displaying hashes to Puppet::Parameter.format_value_for_display +f0c3414 Move parameter formatting rules into helper method +845e05b Wrap long lines in Type::Package +18d65ec Whitespace cleanup in Type::SshAuthorizedKey +b2e2175 Include necessary Facter stubs +f5bc897 Remove test dependencies on QUANTITY of calls +51adf31 Reset the @macosx_version_major variable +4b9dfdd Reset the @job_list variable between tests +446a5bf Remove use of defined?() +0e4079d Use memoization instead of 'unless' +9d504ff (#9796) ssh_authorized_key supports whitespace again +122b8c2 (#9459) Fix problems with Windows 'user' and 'group' providers. +ee107cf Use instance variable for job_list +d1e0fa1 Refactor launchd provider spec tests +3440c10 Refactor status method +62b8d6b Deprecation Warning if using Facter <= 1.5.5 +8a50c3a Test prefetching +f09d264 Stub call to Facter +a6bc5a5 Optimize @product_version variable +af42ff8 Documentation Commit +55610bf Whitespace Commit +cf3d378 Change method used to get Fact Value +ce776b0 Revert launchd_spec +f65b111 Rearrange launchd provider +ed90957 First attempt at launchd spec +637b57b Implement Caching +60482f4 Whitespace and Alignment Commit +5b52bd6 Bring up to date with topic branch +27057a6 Maint: Fix the "provider" parameter documentation +747ffd2 (#8341) Remove duplicate loading of facter files. +c88d22b Fix tests for #1886 with ActiveRecord 3.x +723cd92 Revert "(Maint.) Disable cleaning of storeconfigs." + 2.7.9 === da11dc7 Fix Ruby 1.8.5-incompatible code in FileBucket::Dipper spec @@ -1371,6 +1711,38 @@ d532e6d Fixing #3185 Rakefile is loading puppet.rb twice 5aa596c Fix #3150 - require function doesn't like ::class syntax 3457b87 Added time module to tagmail report +2.6.13 +=== +e4ee794 (#10739) Provide default subjectAltNames while bootstrapping master +9dfd011 (#5617) Puppet queue logging +a91cfa1 maint: Fix failing spec on old version of rspec +aa2a762 (#10289) Add an ext script to upload facts to inventory server +5129d38 (#10289) Add a safe alternative to REST for inventory service +7514d32 missing includes in network XML-RPC handlers +397a506 (#10244) Restore Mongrel XMLRPC functionality +e7a6995 (#9794) k5login can overwrite arbitrary files as root +0a92a70 Resist directory traversal attacks through indirections. +8d86e5a (9547) Minor mods to acceptance tests +2bf6721 Reset indirector state after configurer tests. +bb224dd (#8770) Don't fail to set supplementary groups when changing user to root +2a0de12 (#8770) Always fully drop privileges when changing user +00c4b25 (#8662) Migrate suidmanager test case to rspec +d7c9c76 (#8740) Do not enumerate files in the root directory. +0e00473 (#3553) Explain that cron resources require time attributes +769d432 (#8302) Improve documentation of exec providers +c209f62 Add document outlining preferred contribution methods +fb2ffd6 (#8596) Detect resource alias conflicts when titles do not match +89c021c (#8418) Fix inspect app to have the correct run_mode +3165364 maint: Adding logging to include environment when source fails +f484851 maint: Add debug logging when the master receives a report +e639868 Confine password disclosure acceptance test to hosts with required libraries +a109c90 (maint) Cleanup and strengthen acceptance tests +b268fb3 (#7144) Update Settings#writesub to convert mode to Fixnum +4a2f22c (maint) Fix platform dection for RHEL +111a4b5 (#6857) Password disclosure when changing a user's password + +2.6.12 (CVE-2011-3872 see http://puppetlabs.com/security/hotfixes/cve-2011-3872/) + 2.6.11 === e158b26 (#9793) "secure" indirector file backed terminus base class. diff --git a/README_DEVELOPER.md b/README_DEVELOPER.md new file mode 100644 index 000000000..aa6fb2bf4 --- /dev/null +++ b/README_DEVELOPER.md @@ -0,0 +1,91 @@ +# Developer README # + +This file is intended to provide a place for developers and contributors to +document what other developers need to know about changes made to Puppet. + +# UTF-8 Handling # + +As Ruby 1.9 becomes more commonly used with Puppet, developers should be aware +of major changes to the way Strings and Regexp objects are handled. +Specifically, every instance of these two classes will have an encoding +attribute determined in a number of ways. + + * If the source file has an encoding specified in the magic comment at the + top, the instance will take on that encoding. + * Otherwise, the encoding will be determined by the LC\_LANG or LANG + environment variables. + * Otherwise, the encoding will default to ASCII-8BIT + +## References ## + +Excellent information about the differences between encodings in Ruby 1.8 and +Ruby 1.9 is published in this blog series: +[Understanding M17n](http://links.puppetlabs.com/understanding_m17n) + +## Encodings of Regexp and String instances ## + +In general, please be aware that Ruby 1.9 regular expressions need to be +compatible with the encoding of a string being used to match them. If they are +not compatible you can expect to receive and error such as: + + Encoding::CompatibilityError: incompatible encoding regexp match (ASCII-8BIT + regexp with UTF-8 string) + +In addition, some escape sequences were valid in Ruby 1.8 are no longer valid +in 1.9 if the regular expression is not marked as an ASCII-8BIT object. You +may expect errors like this in this situation: + + SyntaxError: (irb):7: invalid multibyte escape: /\xFF/ + +This error is particularly common when serializing a string to other +representations like JSON or YAML. To resolve the problem you can explicitly +mark the regular expression as ASCII-8BIT using the /n flag: + + "a" =~ /\342\230\203/n + +Finally, any time you're thinking of a string as an array of bytes rather than +an array of characters, common when escaping a string, you should work with +everything in ASCII-8BIT. Changing the encoding will not change the data +itself and allow the Regexp and the String to deal with bytes rather than +characters. + +Puppet provides a monkey patch to String which returns an encoding suitable for +byte manipulations: + + # Example of how to escape non ASCII printable characters for YAML. + >> snowman = "☃" + >> snowman.to_ascii8bit.gsub(/([\x80-\xFF])/n) { |x| "\\x#{x.unpack("C")[0].to_s(16)} } + => "\\xe2\\x98\\x83" + +If the Regexp is not marked as ASCII-8BIT using /n, then you can expect the +SyntaxError, invalid multibyte escape as mentioned above. + +# Windows # + +If you'd like to run Puppet from source on Windows platforms, the +include `ext/envpuppet.bat` will help. All file paths in the Puppet +code base should use a path separator of / regardless of Windows or +Unix filesystem. + +To quickly run Puppet from source, assuming you already have Ruby installed +from [rubyinstaller.org](http://rubyinstaller.org). + + gem install sys-admin win32-process win32-dir win32-taskscheduler --no-rdoc --no-ri + gem install win32-service --platform=mswin32 --no-rdoc --no-ri --version 0.7.1 + net use Z: "\\vmware-host\Shared Folders" /persistent:yes + Z: + cd <path_to_puppet> + set PATH=%PATH%;Z:\<path_to_puppet>\ext + envpuppet puppet --version + 2.7.9 + +Some spec tests are known to fail on Windows, e.g. no mount provider +on Windows, so use the following rspec exclude filter: + + cd <path_to_puppet> + envpuppet rspec --tag ~fails_on_windows spec + +This will give you a shared filesystem with your Mac and allow you to run +Puppet directly from source without using install.rb or copying files around. + +EOF diff --git a/conf/redhat/puppet.spec b/conf/redhat/puppet.spec index 883430027..40d714c3a 100644 --- a/conf/redhat/puppet.spec +++ b/conf/redhat/puppet.spec @@ -5,7 +5,7 @@ %global confdir conf/redhat Name: puppet -Version: 2.7.9 +Version: 2.7.10 #Release: 0.1rc1%{?dist} Release: 1%{?dist} Summary: A network tool for managing many disparate systems @@ -285,6 +285,9 @@ fi rm -rf %{buildroot} %changelog +* Wed Jan 25 2012 Michael Stahnke <stahnma@puppetlabs.com> - 2.7.10-1 +- Update for 2.7.10 + * Fri Dec 9 2011 Matthaus Litteken <matthaus@puppetlabs.com> - 2.7.9-1 - Update for 2.7.9 @@ -325,6 +328,12 @@ rm -rf %{buildroot} * Wed Jul 06 2011 Michael Stahnke <stahnma@puppetlabs.com> - 2.7.2-0.1.rc1 - Update to 2.7.2rc1 +* Mon Dec 12 2011 Matthaus Litteken <matthaus@puppetlabs.com> - 2.6.13-1 +- Release of 2.6.13 + +* Fri Oct 21 2011 Michael Stahnke <stahnma@puppetlabs.com> - 2.6.12-1 +- CVE-2011-3872 fixes + * Wed Jun 15 2011 Todd Zullinger <tmz@pobox.com> - 2.6.9-0.1.rc1 - Update rc versioning to ensure 2.6.9 final is newer to rpm - sync changes with Fedora/EPEL diff --git a/conf/solaris/pkginfo b/conf/solaris/pkginfo index 0e7ff40b9..b58774cdf 100644 --- a/conf/solaris/pkginfo +++ b/conf/solaris/pkginfo @@ -1,6 +1,6 @@ PKG=CSWpuppet NAME=puppet - System Automation Framework -VERSION=2.7.8 +VERSION=2.7.10 CATEGORY=application VENDOR=http://projects.puppetlabs.com/projects/puppet EMAIL=info@puppetlabs.com diff --git a/conf/suse/puppet.spec b/conf/suse/puppet.spec index 65cc5202d..0f1e07eb1 100644 --- a/conf/suse/puppet.spec +++ b/conf/suse/puppet.spec @@ -4,13 +4,13 @@ Summary: A network tool for managing many disparate systems Name: puppet -Version: 2.7.8 -Release: 0.1rc1%{?dist} +Version: 2.7.10 +Release: 1%{?dist} License: Apache 2.0 Group: Productivity/Networking/System URL: http://puppetlabs.com/projects/puppet/ -Source0: http://puppetlabs.com/downloads/puppet/%{name}-%{version}rc1.tar.gz +Source0: http://puppetlabs.com/downloads/puppet/%{name}-%{version}.tar.gz PreReq: %{insserv_prereq} %{fillup_prereq} Requires: ruby >= 1.8.2 @@ -35,7 +35,7 @@ Provides the central puppet server daemon which provides manifests to clients. The server can also function as a certificate authority and file server. %prep -%setup -q -n %{name}-%{version}rc1 +%setup -q -n %{name}-%{version} %build for f in bin/* sbin/*; do @@ -136,6 +136,9 @@ find %{buildroot}%{ruby_sitelibdir} -type f -perm +ugo+x -exec chmod a-x '{}' \; %{__rm} -rf %{buildroot} %changelog +* Wed Jan 25 2012 Michael Stahnke <stahnma@puppetlabs.com> - 2.7.10-1 +- Update for 2.7.10 + * Wed Nov 30 2011 Michael Stahnke <stahnma@puppetlabs.com> - 2.7.8-0.1rc1 - Update for 2.7.8rc1 diff --git a/ext/envpuppet.bat b/ext/envpuppet.bat new file mode 100644 index 000000000..490225f86 --- /dev/null +++ b/ext/envpuppet.bat @@ -0,0 +1,13 @@ +@echo off +SETLOCAL + +REM net use Z: "\\vmware-host\Shared Folders" /persistent:yes + +SET PUPPET_DIR=%~dp0.. +SET FACTER_DIR=%PUPPET_DIR%\..\facter + +SET PATH=%PUPPET_DIR%\bin;%FACTER_DIR%\bin;%PATH% +SET RUBYLIB=%PUPPET_DIR%\lib;%FACTER_DIR%\lib;%RUBYLIB% +SET RUBYLIB=%RUBYLIB:\=/% + +ruby -S %* diff --git a/ext/puppetstoredconfigclean.rb b/ext/puppetstoredconfigclean.rb index dcbefa816..7e7723922 100644 --- a/ext/puppetstoredconfigclean.rb +++ b/ext/puppetstoredconfigclean.rb @@ -8,7 +8,10 @@ # duritong adapted and improved the script a bit. require 'getoptlong' -config = '/etc/puppet/puppet.conf' +require 'puppet' +require 'puppet/rails' + +config = Puppet[:config] def printusage(error_code) puts "Usage: #{$0} [ list of hostnames as stored in hosts table ]" @@ -17,14 +20,11 @@ def printusage(error_code) 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 ] +opts = GetoptLong.new( + [ "--config", "-c", GetoptLong::REQUIRED_ARGUMENT ], + [ "--help", "-h", GetoptLong::NO_ARGUMENT ], + [ "--usage", "-u", GetoptLong::NO_ARGUMENT ], + [ "--version", "-v", GetoptLong::NO_ARGUMENT ] ) begin @@ -51,24 +51,35 @@ end printusage(1) unless ARGV.size > 0 -require 'puppet/rails' -Puppet[:config] = config -Puppet.parse_config -pm_conf = Puppet.settings.instance_variable_get(:@values)[:master] +if config != Puppet[:config] + Puppet[:config]=config + Puppet.settings.parse +end -adapter = pm_conf[:dbadapter] -args = {:adapter => adapter, :log_level => pm_conf[:rails_loglevel]} +master = Puppet.settings.instance_variable_get(:@values)[:master] +main = Puppet.settings.instance_variable_get(:@values)[:main] +db_config = main.merge(master) + +# get default values +[:master, :main, :rails].each do |section| + Puppet.settings.params(section).each do |key| + db_config[key] ||= Puppet[key] + end +end + +adapter = db_config[:dbadapter] +args = {:adapter => adapter, :log_level => db_config[:rails_loglevel]} case adapter when "sqlite3" - args[:dbfile] = pm_conf[:dblocation] - when "mysql", "postgresql" - args[:host] = pm_conf[:dbserver] unless pm_conf[:dbserver].to_s.empty? - args[:username] = pm_conf[:dbuser] unless pm_conf[:dbuser].to_s.empty? - args[:password] = pm_conf[:dbpassword] unless pm_conf[:dbpassword].to_s.empty? - args[:database] = pm_conf[:dbname] unless pm_conf[:dbname].to_s.empty? - args[:port] = pm_conf[:dbport] unless pm_conf[:dbport].to_s.empty? - socket = pm_conf[:dbsocket] + args[:dbfile] = db_config[:dblocation] + when "mysql", "mysql2", "postgresql" + args[:host] = db_config[:dbserver] unless db_config[:dbserver].to_s.empty? + args[:username] = db_config[:dbuser] unless db_config[:dbuser].to_s.empty? + args[:password] = db_config[:dbpassword] unless db_config[:dbpassword].to_s.empty? + args[:database] = db_config[:dbname] unless db_config[:dbname].to_s.empty? + args[:port] = db_config[:dbport] unless db_config[:dbport].to_s.empty? + socket = db_config[:dbsocket] args[:socket] = socket unless socket.to_s.empty? else raise ArgumentError, "Invalid db adapter #{adapter}" @@ -78,14 +89,15 @@ args[:database] = "puppet" unless not args[:database].to_s.empty? ActiveRecord::Base.establish_connection(args) -ARGV.each { |hostname| +ARGV.each do |hostname| if @host = Puppet::Rails::Host.find_by_name(hostname.strip) - print "Killing #{hostname}..." + print "Removing #{hostname} from storedconfig..." $stdout.flush @host.destroy puts "done." else - puts "Can't find host #{hostname}." + puts "Error: Can't find host #{hostname}." end -} +end + exit 0 diff --git a/ext/rack/files/apache2.conf b/ext/rack/files/apache2.conf index 381327c9b..097e3a054 100644 --- a/ext/rack/files/apache2.conf +++ b/ext/rack/files/apache2.conf @@ -26,6 +26,10 @@ Listen 8140 SSLVerifyDepth 1 SSLOptions +StdEnvVars + RequestHeader set X-SSL-Subject %{SSL_CLIENT_S_DN}e + RequestHeader set X-Client-DN %{SSL_CLIENT_S_DN}e + RequestHeader set X-Client-Verify %{SSL_CLIENT_VERIFY}e + DocumentRoot /etc/puppet/rack/public/ RackBaseURI / <Directory /etc/puppet/rack/> diff --git a/install.rb b/install.rb index d015c4d3f..4f8469240 100755 --- a/install.rb +++ b/install.rb @@ -422,16 +422,12 @@ def install_binfile(from, op_file, target) if not installed_wrapper tmp_file2 = File.join(tmp_dir, '_tmp_wrapper') - cwn = File.join(Config::CONFIG['bindir'], op_file) cwv = <<-EOS @echo off -if "%OS%"=="Windows_NT" goto WinNT -#{ruby} -x "#{cwn}" %1 %2 %3 %4 %5 %6 %7 %8 %9 -goto done -:WinNT -#{ruby} -x "#{cwn}" %* -goto done -:done +setlocal +set RUBY_BIN=%~dp0 +set RUBY_BIN=%RUBY_BIN:\\=/% +"%RUBY_BIN%ruby.exe" -x "%RUBY_BIN%puppet" %* EOS File.open(tmp_file2, "w") { |cw| cw.puts cwv } FileUtils.install(tmp_file2, File.join(target, "#{op_file}.bat"), :mode => 0755, :verbose => true) diff --git a/lib/puppet.rb b/lib/puppet.rb index 0329e5b04..9105f7f62 100644 --- a/lib/puppet.rb +++ b/lib/puppet.rb @@ -24,7 +24,7 @@ require 'puppet/util/run_mode' # it's also a place to find top-level commands like 'debug' module Puppet - PUPPETVERSION = '2.7.9' + PUPPETVERSION = '2.7.10' def Puppet.version PUPPETVERSION diff --git a/lib/puppet/agent.rb b/lib/puppet/agent.rb index 47dd44a0e..b684470e3 100644 --- a/lib/puppet/agent.rb +++ b/lib/puppet/agent.rb @@ -8,6 +8,9 @@ class Puppet::Agent require 'puppet/agent/locker' include Puppet::Agent::Locker + require 'puppet/agent/disabler' + include Puppet::Agent::Disabler + attr_reader :client_class, :client, :splayed # Just so we can specify that we are "the" instance. @@ -31,6 +34,10 @@ class Puppet::Agent Puppet.notice "Run of #{client_class} already in progress; skipping" return end + if disabled? + Puppet.notice "Skipping run of #{client_class}; administratively disabled: #{disable_message}" + return + end result = nil block_run = Puppet::Application.controlled_run do splay diff --git a/lib/puppet/agent/disabler.rb b/lib/puppet/agent/disabler.rb new file mode 100644 index 000000000..34ab94bbf --- /dev/null +++ b/lib/puppet/agent/disabler.rb @@ -0,0 +1,27 @@ +require 'puppet/util/anonymous_filelock' + +module Puppet::Agent::Disabler + # Let the daemon run again, freely in the filesystem. + def enable + disable_lockfile.unlock + end + + # Stop the daemon from making any catalog runs. + def disable(msg='') + disable_lockfile.lock(msg) + end + + def disable_lockfile + @disable_lockfile ||= Puppet::Util::AnonymousFilelock.new(lockfile_path+".disabled") + + @disable_lockfile + end + + def disabled? + disable_lockfile.locked? + end + + def disable_message + disable_lockfile.message + end +end diff --git a/lib/puppet/agent/locker.rb b/lib/puppet/agent/locker.rb index 98f5b38d9..d41b12547 100644 --- a/lib/puppet/agent/locker.rb +++ b/lib/puppet/agent/locker.rb @@ -3,16 +3,6 @@ require 'puppet/util/pidlock' # Break out the code related to locking the agent. This module is just # included into the agent, but having it here makes it easier to test. module Puppet::Agent::Locker - # Let the daemon run again, freely in the filesystem. - def enable - lockfile.unlock(:anonymous => true) - end - - # Stop the daemon from making any catalog runs. - def disable - lockfile.lock(:anonymous => true) - end - # Yield if we get a lock, else do nothing. Return # true/false depending on whether we get the lock. def lock diff --git a/lib/puppet/application.rb b/lib/puppet/application.rb index b7cb1169d..f6dac3911 100644 --- a/lib/puppet/application.rb +++ b/lib/puppet/application.rb @@ -263,12 +263,15 @@ class Application end def initialize(command_line = nil) + require 'puppet/util/command_line' @command_line = command_line || Puppet::Util::CommandLine.new set_run_mode self.class.run_mode @options = {} require 'puppet' + require 'puppet/util/instrumentation' + Puppet::Util::Instrumentation.init end # WARNING: This is a totally scary, frightening, and nasty internal API. We diff --git a/lib/puppet/application/agent.rb b/lib/puppet/application/agent.rb index a3854a8fd..055c36df4 100644 --- a/lib/puppet/application/agent.rb +++ b/lib/puppet/application/agent.rb @@ -39,7 +39,12 @@ class Puppet::Application::Agent < Puppet::Application end option("--centrallogging") - option("--disable") + + option("--disable [MESSAGE]") do |message| + options[:disable] = true + options[:disable_message] = message + end + option("--enable") option("--debug","-d") option("--fqdn FQDN","-f") @@ -101,7 +106,7 @@ similar), or run interactively for testing purposes. USAGE ----- puppet agent [--certname <name>] [-D|--daemonize|--no-daemonize] - [-d|--debug] [--detailed-exitcodes] [--digest <digest>] [--disable] [--enable] + [-d|--debug] [--detailed-exitcodes] [--digest <digest>] [--disable [message]] [--enable] [--fingerprint] [-h|--help] [-l|--logdest syslog|<file>|console] [--no-client] [--noop] [-o|--onetime] [--serve <handler>] [-t|--test] [-v|--verbose] [-V|--version] [-w|--waitforcert <seconds>] @@ -205,6 +210,9 @@ configuration options can also be generated by running puppet agent with not want the central configuration to override the local state until everything is tested and committed. + Disable can also take an optional message that will be reported by the + 'puppet agent' at the next disabled run. + 'puppet agent' uses the same lock file while it is running, so no more than one 'puppet agent' process is working at a time. @@ -339,6 +347,8 @@ Copyright (c) 2011 Puppet Labs, LLC Licensed under the Apache 2.0 License Puppet.err detail.to_s end + @daemon.stop(:exit => false) + if not report exit(1) elsif options[:detailed_exitcodes] then @@ -384,7 +394,7 @@ Copyright (c) 2011 Puppet Labs, LLC Licensed under the Apache 2.0 License if options[:enable] agent.enable elsif options[:disable] - agent.disable + agent.disable(options[:disable_message] || 'reason not specified') end exit(0) end @@ -416,6 +426,35 @@ Copyright (c) 2011 Puppet Labs, LLC Licensed under the Apache 2.0 License cert = @host.wait_for_cert(waitforcert) unless options[:fingerprint] end + def setup_agent + # We need tomake the client either way, we just don't start it + # if --no-client is set. + require 'puppet/agent' + require 'puppet/configurer' + @agent = Puppet::Agent.new(Puppet::Configurer) + + enable_disable_client(@agent) if options[:enable] or options[:disable] + + @daemon.agent = agent if options[:client] + + # It'd be nice to daemonize later, but we have to daemonize before the + # waitforcert happens. + @daemon.daemonize if Puppet[:daemonize] + + setup_host + + @objects = [] + + # This has to go after the certs are dealt with. + if Puppet[:listen] + unless Puppet[:onetime] + setup_listen + else + Puppet.notice "Ignoring --listen on onetime run" + end + end + end + def setup setup_test if options[:test] @@ -460,31 +499,10 @@ Copyright (c) 2011 Puppet Labs, LLC Licensed under the Apache 2.0 License Puppet::Resource::Catalog.indirection.cache_class = :yaml - # We need tomake the client either way, we just don't start it - # if --no-client is set. - require 'puppet/agent' - require 'puppet/configurer' - @agent = Puppet::Agent.new(Puppet::Configurer) - - enable_disable_client(@agent) if options[:enable] or options[:disable] - - @daemon.agent = agent if options[:client] - - # It'd be nice to daemonize later, but we have to daemonize before the - # waitforcert happens. - @daemon.daemonize if Puppet[:daemonize] - - setup_host - - @objects = [] - - # This has to go after the certs are dealt with. - if Puppet[:listen] - unless Puppet[:onetime] - setup_listen - else - Puppet.notice "Ignoring --listen on onetime run" - end + unless options[:fingerprint] + setup_agent + else + setup_host end end end diff --git a/lib/puppet/application/apply.rb b/lib/puppet/application/apply.rb index 5df00d76b..e9b4e7a24 100644 --- a/lib/puppet/application/apply.rb +++ b/lib/puppet/application/apply.rb @@ -14,6 +14,14 @@ class Puppet::Application::Apply < Puppet::Application option("--detailed-exitcodes") option("--apply catalog", "-a catalog") do |arg| + Puppet.warning <<EOM +--apply is deprecated and will be removed in the future. Please +use 'puppet apply --catalog <catalog>'. +EOM + options[:catalog] = arg + end + + option("--catalog catalog", "-c catalog") do |arg| options[:catalog] = arg end @@ -46,7 +54,7 @@ USAGE ----- puppet apply [-h|--help] [-V|--version] [-d|--debug] [-v|--verbose] [-e|--execute] [--detailed-exitcodes] [-l|--logdest <file>] - [--apply <catalog>] <file> + [--apply <catalog>] [--catalog <catalog>] <file> DESCRIPTION @@ -107,6 +115,11 @@ configuration options can also be generated by running puppet with * --apply: Apply a JSON catalog (such as one generated with 'puppet master --compile'). You can + either specify a JSON file or pipe in JSON from standard input. Deprecated, please + use --catalog instead. + +* --catalog: + Apply a JSON catalog (such as one generated with 'puppet master --compile'). You can either specify a JSON file or pipe in JSON from standard input. @@ -114,6 +127,7 @@ EXAMPLE ------- $ puppet apply -l /tmp/manifest.log manifest.pp $ puppet apply --modulepath=/root/dev/modules -e "include ntpd::server" + $ puppet apply --catalog catalog.json AUTHOR diff --git a/lib/puppet/application/cert.rb b/lib/puppet/application/cert.rb index 7285d8869..7b1431146 100644 --- a/lib/puppet/application/cert.rb +++ b/lib/puppet/application/cert.rb @@ -17,11 +17,11 @@ class Puppet::Application::Cert < Puppet::Application @subcommand = (sub == :clean ? :destroy : sub) end - option("--clean", "-c") do + option("--clean", "-c") do |arg| self.subcommand = "destroy" end - option("--all", "-a") do + option("--all", "-a") do |arg| @all = true end @@ -29,7 +29,7 @@ class Puppet::Application::Cert < Puppet::Application @digest = arg end - option("--signed", "-s") do + option("--signed", "-s") do |arg| @signed = true end @@ -39,7 +39,7 @@ class Puppet::Application::Cert < Puppet::Application require 'puppet/ssl/certificate_authority/interface' Puppet::SSL::CertificateAuthority::Interface::INTERFACE_METHODS.reject {|m| m == :destroy }.each do |method| - option("--#{method.to_s.gsub('_','-')}", "-#{method.to_s[0,1]}") do + option("--#{method.to_s.gsub('_','-')}", "-#{method.to_s[0,1]}") do |arg| self.subcommand = method end end @@ -48,7 +48,7 @@ class Puppet::Application::Cert < Puppet::Application options[:allow_dns_alt_names] = value end - option("--verbose", "-v") do + option("--verbose", "-v") do |arg| Puppet::Util::Log.level = :info end diff --git a/lib/puppet/application/instrumentation_data.rb b/lib/puppet/application/instrumentation_data.rb new file mode 100644 index 000000000..e4f86f196 --- /dev/null +++ b/lib/puppet/application/instrumentation_data.rb @@ -0,0 +1,4 @@ +require 'puppet/application/indirection_base' + +class Puppet::Application::Instrumentation_data < Puppet::Application::IndirectionBase +end diff --git a/lib/puppet/application/instrumentation_listener.rb b/lib/puppet/application/instrumentation_listener.rb new file mode 100644 index 000000000..64029b5c9 --- /dev/null +++ b/lib/puppet/application/instrumentation_listener.rb @@ -0,0 +1,4 @@ +require 'puppet/application/indirection_base' + +class Puppet::Application::Instrumentation_listener < Puppet::Application::IndirectionBase +end diff --git a/lib/puppet/application/instrumentation_probe.rb b/lib/puppet/application/instrumentation_probe.rb new file mode 100644 index 000000000..b31f95c45 --- /dev/null +++ b/lib/puppet/application/instrumentation_probe.rb @@ -0,0 +1,4 @@ +require 'puppet/application/indirection_base' + +class Puppet::Application::Instrumentation_probe < Puppet::Application::IndirectionBase +end diff --git a/lib/puppet/configurer.rb b/lib/puppet/configurer.rb index 83b5a9ad4..477363730 100644 --- a/lib/puppet/configurer.rb +++ b/lib/puppet/configurer.rb @@ -178,9 +178,17 @@ class Puppet::Configurer end def save_last_run_summary(report) - Puppet::Util::FileLocking.writelock(Puppet[:lastrunfile], 0660) do |file| - file.print YAML.dump(report.raw_summary) - end + last_run = Puppet.settings.setting(:lastrunfile) + last_run.create = true # force file creation + + resource = last_run.to_resource + resource[:content] = YAML.dump(report.raw_summary) + + catalog = Puppet::Resource::Catalog.new("last_run_file") + catalog.add_resource(resource) + ral = catalog.to_ral + ral.host_config = false + ral.apply rescue => detail puts detail.backtrace if Puppet[:trace] Puppet.err "Could not save last run local report: #{detail}" diff --git a/lib/puppet/configurer/downloader.rb b/lib/puppet/configurer/downloader.rb index b3696201a..54075ee7e 100644 --- a/lib/puppet/configurer/downloader.rb +++ b/lib/puppet/configurer/downloader.rb @@ -63,14 +63,16 @@ class Puppet::Configurer::Downloader private + require 'sys/admin' if Puppet.features.microsoft_windows? + def default_arguments { :path => path, :recurse => true, :source => source, :tag => name, - :owner => Process.uid, - :group => Process.gid, + :owner => Puppet.features.microsoft_windows? ? Sys::Admin.get_login : Process.uid, + :group => Puppet.features.microsoft_windows? ? 'S-1-0-0' : Process.gid, :purge => true, :force => true, :backup => false, diff --git a/lib/puppet/configurer/fact_handler.rb b/lib/puppet/configurer/fact_handler.rb index 803495773..7d18aa9f2 100644 --- a/lib/puppet/configurer/fact_handler.rb +++ b/lib/puppet/configurer/fact_handler.rb @@ -15,7 +15,6 @@ module Puppet::Configurer::FactHandler # finding facts and the 'rest' terminus for caching them. Thus, we'll # compile them and then "cache" them on the server. begin - reload_facter facts = Puppet::Node::Facts.indirection.find(Puppet[:node_name_value]) unless Puppet[:node_name_fact].empty? Puppet[:node_name_value] = facts.values[Puppet[:node_name_fact]] @@ -54,24 +53,4 @@ module Puppet::Configurer::FactHandler Puppet::Configurer::Downloader.new("fact", Puppet[:factdest], Puppet[:factsource], Puppet[:factsignore]).evaluate end - - # Clear out all of the loaded facts and reload them from disk. - # NOTE: This is clumsy and shouldn't be required for later (1.5.x) versions - # of Facter. - def reload_facter - Facter.clear - - # Reload everything. - if Facter.respond_to? :loadfacts - Facter.loadfacts - elsif Facter.respond_to? :load - Facter.load - else - Puppet.warning "You should upgrade your version of Facter to at least 1.3.8" - end - - # This loads all existing facts and any new ones. We have to remove and - # reload because there's no way to unload specific facts. - Puppet::Node::Facts::Facter.load_fact_plugins - end end diff --git a/lib/puppet/defaults.rb b/lib/puppet/defaults.rb index 078923754..26ee43c14 100644 --- a/lib/puppet/defaults.rb +++ b/lib/puppet/defaults.rb @@ -199,6 +199,10 @@ module Puppet essentially means that you can't have any code outside of a node, class, or definition other than in the site manifest."] ) + Puppet.setdefaults(:module_tool, + :module_repository => ['http://forge.puppetlabs.com', "The module repository"], + :module_working_dir => ['$vardir/puppet-module', "The directory into which module tool data is stored"] + ) hostname = Facter["hostname"].value domain = Facter["domain"].value @@ -705,11 +709,11 @@ EOT "Whether to send reports after every transaction." ], :lastrunfile => { :default => "$statedir/last_run_summary.yaml", - :mode => 0660, + :mode => 0644, :desc => "Where puppet agent stores the last run report summary in yaml format." }, :lastrunreport => { :default => "$statedir/last_run_report.yaml", - :mode => 0660, + :mode => 0644, :desc => "Where puppet agent stores the last run report in yaml format." }, :graph => [false, "Whether to create dot graph files for the different diff --git a/lib/puppet/face/instrumentation_data.rb b/lib/puppet/face/instrumentation_data.rb new file mode 100644 index 000000000..1abf7cd00 --- /dev/null +++ b/lib/puppet/face/instrumentation_data.rb @@ -0,0 +1,28 @@ +require 'puppet/indirector/face' + +Puppet::Indirector::Face.define(:instrumentation_data, '0.0.1') do + copyright "Puppet Labs", 2011 + license "Apache 2 license; see COPYING" + + summary "Manage instrumentation listener accumulated data." + description <<-EOT + This subcommand allows to retrieve the various listener data. + EOT + + get_action(:destroy).summary "Invalid for this subcommand." + get_action(:save).summary "Invalid for this subcommand." + get_action(:search).summary "Invalid for this subcommand." + + find = get_action(:find) + find.summary "Retrieve listener data." + find.render_as = :pson + find.returns <<-EOT + The data of an instrumentation listener + EOT + find.examples <<-EOT + Retrieve listener data: + + $ puppet instrumentation_data find performance --terminus rest + EOT + +end diff --git a/lib/puppet/face/instrumentation_listener.rb b/lib/puppet/face/instrumentation_listener.rb new file mode 100644 index 000000000..de4a742a1 --- /dev/null +++ b/lib/puppet/face/instrumentation_listener.rb @@ -0,0 +1,96 @@ +require 'puppet/indirector/face' + +Puppet::Indirector::Face.define(:instrumentation_listener, '0.0.1') do + copyright "Puppet Labs", 2011 + license "Apache 2 license; see COPYING" + + summary "Manage instrumentation listeners." + description <<-EOT + This subcommand enables/disables or list instrumentation listeners. + EOT + + get_action(:destroy).summary "Invalid for this subcommand." + + find = get_action(:find) + find.summary "Retrieve a single listener." + find.render_as = :pson + find.returns <<-EOT + The status of an instrumentation listener + EOT + find.examples <<-EOT + Retrieve a given listener: + + $ puppet instrumentation_listener find performance --terminus rest + EOT + + search = get_action(:search) + search.summary "Retrieve all instrumentation listeners statuses." + search.arguments "<dummy_text>" + search.render_as = :pson + search.returns <<-EOT + The statuses of all instrumentation listeners + EOT + search.short_description <<-EOT + This retrieves all instrumentation listeners + EOT + search.notes <<-EOT + Although this action always returns all instrumentation listeners, it requires a dummy search + key; this is a known bug. + EOT + search.examples <<-EOT + Retrieve the state of the listeners running in the remote puppet master: + + $ puppet instrumentation_listener search x --terminus rest + EOT + + def manage(name, activate) + Puppet::Util::Instrumentation::Listener.indirection.terminus_class = :rest + listener = Puppet::Face[:instrumentation_listener, '0.0.1'].find(name) + if listener + listener.enabled = activate + Puppet::Face[:instrumentation_listener, '0.0.1'].save(listener) + end + end + + action :enable do + summary "Enable a given instrumentation listener." + arguments "<listener>" + returns "Nothing." + description <<-EOT + Enable a given instrumentation listener. After being enabled the listener + will start receiving instrumentation notifications from the probes if those + are enabled. + EOT + examples <<-EOT + Enable the "performance" listener in the running master: + + $ puppet instrumentation_listener enable performance --terminus rest + EOT + + when_invoked do |name, options| + manage(name, true) + end + end + + action :disable do + summary "Disable a given instrumentation listener." + arguments "<listener>" + returns "Nothing." + description <<-EOT + Disable a given instrumentation listener. After being disabled the listener + will stop receiving instrumentation notifications from the probes. + EOT + examples <<-EOT + Disable the "performance" listener in the running master: + + $ puppet instrumentation_listener disable performance --terminus rest + EOT + + when_invoked do |name, options| + manage(name, false) + end + end + + get_action(:save).summary "API only: modify an instrumentation listener status." + get_action(:save).arguments "<listener>" +end diff --git a/lib/puppet/face/instrumentation_probe.rb b/lib/puppet/face/instrumentation_probe.rb new file mode 100644 index 000000000..52b331cb3 --- /dev/null +++ b/lib/puppet/face/instrumentation_probe.rb @@ -0,0 +1,77 @@ +require 'puppet/indirector/face' + +Puppet::Indirector::Face.define(:instrumentation_probe, '0.0.1') do + copyright "Puppet Labs", 2011 + license "Apache 2 license; see COPYING" + + summary "Manage instrumentation probes." + description <<-EOT + This subcommand enables/disables or list instrumentation listeners. + EOT + + get_action(:find).summary "Invalid for this subcommand." + + search = get_action(:search) + search.summary "Retrieve all probe statuses." + search.arguments "<dummy_text>" + search.render_as = :pson + search.returns <<-EOT + The statuses of all instrumentation probes + EOT + search.short_description <<-EOT + This retrieves all instrumentation probes + EOT + search.notes <<-EOT + Although this action always returns all instrumentation probes, it requires a dummy search + key; this is a known bug. + EOT + search.examples <<-EOT + Retrieve the state of the probes running in the remote puppet master: + + $ puppet instrumentation_probe search x --terminus rest + EOT + + action :enable do + summary "Enable all instrumentation probes." + arguments "<dummy>" + returns "Nothing." + description <<-EOT + Enable all instrumentation probes. After being enabled, all enabled listeners + will start receiving instrumentation notifications from the probes. + EOT + examples <<-EOT + Enable the probes for the running master: + + $ puppet instrumentation_probe enable x --terminus rest + EOT + + when_invoked do |name, options| + Puppet::Face[:instrumentation_probe, '0.0.1'].save(nil) + end + end + + action :disable do + summary "Disable all instrumentation probes." + arguments "<dummy>" + returns "Nothing." + description <<-EOT + Disable all instrumentation probes. After being disabled, no listeners + will receive instrumentation notifications. + EOT + examples <<-EOT + Disable the probes for the running master: + + $ puppet instrumentation_probe disable x --terminus rest + EOT + + when_invoked do |name, options| + Puppet::Face[:instrumentation_probe, '0.0.1'].destroy(nil) + end + end + + get_action(:save).summary "API only: enable all instrumentation probes." + get_action(:save).arguments "<dummy>" + + get_action(:destroy).summary "API only: disable all instrumentation probes." + get_action(:destroy).arguments "<dummy>" +end diff --git a/lib/puppet/face/module/build.rb b/lib/puppet/face/module/build.rb new file mode 100644 index 000000000..227363448 --- /dev/null +++ b/lib/puppet/face/module/build.rb @@ -0,0 +1,31 @@ +Puppet::Face.define(:module, '1.0.0') do + action(:build) do + summary "Build a module release package." + description <<-EOT + Build a module release archive file by processing the Modulefile in the + module directory. The release archive file will be stored in the pkg + directory of the module directory. + EOT + + returns "Pathname object representing the path to the release archive." + + examples <<-EOT + Build a module release: + + $ puppet module build puppetlabs-apache + notice: Building /Users/kelseyhightower/puppetlabs-apache for release + puppetlabs-apache/pkg/puppetlabs-apache-0.0.1.tar.gz + EOT + + arguments "<path>" + + when_invoked do |path, options| + Puppet::Module::Tool::Applications::Builder.run(path, options) + end + + when_rendering :console do |return_value| + # Get the string representation of the Pathname object. + return_value.to_s + end + end +end diff --git a/lib/puppet/face/module/changes.rb b/lib/puppet/face/module/changes.rb new file mode 100644 index 000000000..026661107 --- /dev/null +++ b/lib/puppet/face/module/changes.rb @@ -0,0 +1,38 @@ +Puppet::Face.define(:module, '1.0.0') do + action(:changes) do + summary "Show modified files of an installed module." + description <<-EOT + Show files that have been modified after installation of a given module + by comparing the on-disk md5 checksum of each file against the module's + metadata. + EOT + + returns "Array of strings representing paths of modified files." + + examples <<-EOT + Show modified files of an installed module: + + $ puppet module changes /etc/puppet/modules/vcsrepo/ + warning: 1 files modified + lib/puppet/provider/vcsrepo.rb + EOT + + arguments "<path>" + + when_invoked do |path, options| + root_path = Puppet::Module::Tool.find_module_root(path) + Puppet::Module::Tool::Applications::Checksummer.run(root_path, options) + end + + when_rendering :console do |return_value| + if return_value.empty? + Puppet.notice "No modified files" + else + Puppet.warning "#{return_value.size} files modified" + end + return_value.map do |changed_file| + "#{changed_file}" + end.join("\n") + end + end +end diff --git a/lib/puppet/face/module/clean.rb b/lib/puppet/face/module/clean.rb new file mode 100644 index 000000000..637263057 --- /dev/null +++ b/lib/puppet/face/module/clean.rb @@ -0,0 +1,30 @@ +Puppet::Face.define(:module, '1.0.0') do + action(:clean) do + summary "Clean the module download cache." + description <<-EOT + Clean the module download cache. + EOT + + returns <<-EOT + Return a status Hash: + + { :status => "success", :msg => "Cleaned module cache." } + EOT + + examples <<-EOT + Clean the module download cache: + + $ puppet module clean + Cleaned module cache. + EOT + + when_invoked do |options| + Puppet::Module::Tool::Applications::Cleaner.run(options) + end + + when_rendering :console do |return_value| + # Print the status message to the console. + return_value[:msg] + end + end +end diff --git a/lib/puppet/face/module/generate.rb b/lib/puppet/face/module/generate.rb new file mode 100644 index 000000000..b9dc354bf --- /dev/null +++ b/lib/puppet/face/module/generate.rb @@ -0,0 +1,40 @@ +Puppet::Face.define(:module, '1.0.0') do + action(:generate) do + summary "Generate boilerplate for a new module." + description <<-EOT + Generate boilerplate for a new module by creating a directory + pre-populated with a directory structure and files recommended for + Puppet best practices. + EOT + + returns "Array of Pathname objects representing paths of generated files." + + examples <<-EOT + Generate a new module in the current directory: + + $ puppet module generate puppetlabs-ssh + notice: Generating module at /Users/kelseyhightower/puppetlabs-ssh + puppetlabs-ssh + puppetlabs-ssh/tests + puppetlabs-ssh/tests/init.pp + puppetlabs-ssh/spec + puppetlabs-ssh/spec/spec_helper.rb + puppetlabs-ssh/spec/spec.opts + puppetlabs-ssh/README + puppetlabs-ssh/Modulefile + puppetlabs-ssh/metadata.json + puppetlabs-ssh/manifests + puppetlabs-ssh/manifests/init.pp + EOT + + arguments "<name>" + + when_invoked do |name, options| + Puppet::Module::Tool::Applications::Generator.run(name, options) + end + + when_rendering :console do |return_value| + return_value.map {|f| f.to_s }.join("\n") + end + end +end diff --git a/lib/puppet/face/module/install.rb b/lib/puppet/face/module/install.rb new file mode 100644 index 000000000..8f95ff485 --- /dev/null +++ b/lib/puppet/face/module/install.rb @@ -0,0 +1,83 @@ +Puppet::Face.define(:module, '1.0.0') do + action(:install) do + summary "Install a module from a repository or release archive." + description <<-EOT + Install a module from a release archive file on-disk or by downloading + one from a repository. Unpack the archive into the install directory + specified by the --install-dir option, which defaults to + #{Puppet.settings[:modulepath].split(File::PATH_SEPARATOR).first} + EOT + + returns "Pathname object representing the path to the installed module." + + examples <<-EOT + Install a module from the default repository: + + $ puppet module install puppetlabs/vcsrepo + notice: Installing puppetlabs-vcsrepo-0.0.4.tar.gz to /etc/puppet/modules/vcsrepo + /etc/puppet/modules/vcsrepo + + Install a specific module version from a repository: + + $ puppet module install puppetlabs/vcsrepo -v 0.0.4 + notice: Installing puppetlabs-vcsrepo-0.0.4.tar.gz to /etc/puppet/modules/vcsrepo + /etc/puppet/modules/vcsrepo + + Install a module into a specific directory: + + $ puppet module install puppetlabs/vcsrepo --install-dir=/usr/share/puppet/modules + notice: Installing puppetlabs-vcsrepo-0.0.4.tar.gz to /usr/share/puppet/modules/vcsrepo + /usr/share/puppet/modules/vcsrepo + + Install a module from a release archive: + + $ puppet module install puppetlabs-vcsrepo-0.0.4.tar.gz + notice: Installing puppetlabs-vcsrepo-0.0.4.tar.gz to /etc/puppet/modules/vcsrepo + /etc/puppet/modules/vcsrepo + EOT + + arguments "<name>" + + option "--force", "-f" do + summary "Force overwrite of existing module, if any." + description <<-EOT + Force overwrite of existing module, if any. + EOT + end + + option "--install-dir=", "-i=" do + default_to { Puppet.settings[:modulepath].split(File::PATH_SEPARATOR).first } + summary "The directory into which modules are installed." + description <<-EOT + The directory into which modules are installed, defaults to the first + directory in the modulepath. + EOT + end + + option "--module-repository=", "-r=" do + default_to { Puppet.settings[:module_repository] } + summary "Module repository to use." + description <<-EOT + Module repository to use. + EOT + end + + option "--version=", "-v=" do + summary "Module version to install." + description <<-EOT + Module version to install, can be a requirement string, eg '>= 1.0.3', + defaults to latest version. + EOT + end + + when_invoked do |name, options| + Puppet::Module::Tool::Applications::Installer.run(name, options) + end + + when_rendering :console do |return_value| + # Get the string representation of the Pathname object and print it to + # the console. + return_value.to_s + end + end +end diff --git a/lib/puppet/face/module/list.rb b/lib/puppet/face/module/list.rb new file mode 100644 index 000000000..772990069 --- /dev/null +++ b/lib/puppet/face/module/list.rb @@ -0,0 +1,64 @@ +Puppet::Face.define(:module, '1.0.0') do + action(:list) do + summary "List installed modules" + description <<-HEREDOC + List puppet modules from a specific environment, specified modulepath or + default to listing modules in the default modulepath: + #{Puppet.settings[:modulepath]} + HEREDOC + returns "hash of paths to module objects" + + option "--env ENVIRONMENT" do + summary "Which environments' modules to list" + end + + option "--modulepath MODULEPATH" do + summary "Which directories to look for modules in" + end + + examples <<-EOT + List installed modules: + + $ puppet module list + /etc/puppet/modules + bacula (0.0.2) + /usr/share/puppet/modules + apache (0.0.3) + bacula (0.0.1) + + List installed modules from a specified environment: + + $ puppet module list --env 'test' + /tmp/puppet/modules + rrd (0.0.2) + + List installed modules from a specified modulepath: + + $ puppet module list --modulepath /tmp/facts1:/tmp/facts2 + /tmp/facts1 + stdlib + /tmp/facts2 + nginx (1.0.0) + EOT + + when_invoked do |options| + Puppet[:modulepath] = options[:modulepath] if options[:modulepath] + environment = Puppet::Node::Environment.new(options[:env]) + + environment.modules_by_path + end + + when_rendering :console do |modules_by_path| + output = '' + modules_by_path.each do |path, modules| + output << "#{path}\n" + modules.each do |mod| + version_string = mod.version ? "(#{mod.version})" : '' + output << " #{mod.name} #{version_string}\n" + end + end + output + end + + end +end diff --git a/lib/puppet/face/module/search.rb b/lib/puppet/face/module/search.rb new file mode 100644 index 000000000..fdc2c983d --- /dev/null +++ b/lib/puppet/face/module/search.rb @@ -0,0 +1,66 @@ +Puppet::Face.define(:module, '1.0.0') do + action(:search) do + summary "Search a repository for a module." + description <<-EOT + Search a repository for modules whose names match a specific substring. + EOT + + returns "Array of module metadata hashes" + + examples <<-EOT + Search the default repository for a module: + + $ puppet module search puppetlabs + notice: Searching http://forge.puppetlabs.com + notice: 24 found. + puppetlabs/apache (0.0.3) + puppetlabs/collectd (0.0.1) + puppetlabs/ruby (0.0.1) + puppetlabs/vcsrepo (0.0.4) + puppetlabs/gcc (0.0.3) + puppetlabs/passenger (0.0.2) + puppetlabs/DeveloperBootstrap (0.0.5) + jeffmccune/tomcat (1.0.1) + puppetlabs/motd (1.0.0) + puppetlabs/lvm (0.1.0) + puppetlabs/rabbitmq (1.0.4) + puppetlabs/prosvc_repo (1.0.1) + puppetlabs/stdlib (2.2.0) + puppetlabs/java (0.1.5) + puppetlabs/activemq (0.1.6) + puppetlabs/mcollective (0.1.8) + puppetlabs/git (0.0.2) + puppetlabs/ntp (0.0.4) + puppetlabs/nginx (0.0.1) + puppetlabs/cloud_provisioner (0.6.0rc1) + puppetlabs/mrepo (0.1.1) + puppetlabs/f5 (0.1.0) + puppetlabs/firewall (0.0.3) + puppetlabs/bprobe (0.0.3) + EOT + + arguments "<term>" + + option "--module-repository=", "-r=" do + default_to { Puppet.settings[:module_repository] } + summary "Module repository to use." + description <<-EOT + Module repository to use. + EOT + end + + when_invoked do |term, options| + Puppet.notice "Searching #{options[:module_repository]}" + Puppet::Module::Tool::Applications::Searcher.run(term, options) + end + + when_rendering :console do |return_value| + Puppet.notice "#{return_value.size} found." + return_value.map do |match| + # We reference the full_name here when referring to the full_module_name, + # because full_name is what is returned from the forge API call. + "#{match['full_name']} (#{match['version']})" + end.join("\n") + end + end +end diff --git a/lib/puppet/face/module/uninstall.rb b/lib/puppet/face/module/uninstall.rb new file mode 100644 index 000000000..19c983ea6 --- /dev/null +++ b/lib/puppet/face/module/uninstall.rb @@ -0,0 +1,50 @@ +Puppet::Face.define(:module, '1.0.0') do + action(:uninstall) do + summary "Uninstall a puppet module." + description <<-EOT + Uninstall a puppet module from the modulepath or a specific + target directory which defaults to + #{Puppet.settings[:modulepath].split(File::PATH_SEPARATOR).join(', ')}. + EOT + + returns "Array of strings representing paths of uninstalled files." + + examples <<-EOT + Uninstall a module from all directories in the modulepath: + + $ puppet module uninstall ssh + Removed /etc/puppet/modules/ssh + + Uninstall a module from a specific directory: + + $ puppet module uninstall --target-directory /usr/share/puppet/modules ssh + Removed /usr/share/puppet/modules/ssh + EOT + + arguments "<name>" + + option "--target-directory=", "-t=" do + default_to { Puppet.settings[:modulepath].split(File::PATH_SEPARATOR) } + summary "The target directory to search from modules." + description <<-EOT + The target directory to search for modules. + EOT + end + + when_invoked do |name, options| + + if options[:target_directory].is_a?(Array) + options[:target_directories] = options[:target_directory] + else + options[:target_directories] = [ options[:target_directory] ] + end + options.delete(:target_directory) + + Puppet::Module::Tool::Applications::Uninstaller.run(name, options) + end + + when_rendering :console do |removed_modules| + removed_modules.map { |path| "Removed #{path}" }.join('\n') + end + end +end diff --git a/lib/puppet/face/node/clean.rb b/lib/puppet/face/node/clean.rb index d2852de04..a4df1bfaf 100644 --- a/lib/puppet/face/node/clean.rb +++ b/lib/puppet/face/node/clean.rb @@ -54,10 +54,7 @@ Puppet::Face.define(:node, '0.0.1') do clean_cached_facts(node) clean_cached_node(node) clean_reports(node) - - # This is roughly functional, but seems to introduce order-dependent test - # failures; this can be re-added when those issues are resolved. - # clean_storeconfigs(node, unexport) + clean_storeconfigs(node, unexport) end # clean signed cert for +host+ diff --git a/lib/puppet/file_serving/content.rb b/lib/puppet/file_serving/content.rb index d8413b557..eda141d91 100644 --- a/lib/puppet/file_serving/content.rb +++ b/lib/puppet/file_serving/content.rb @@ -35,7 +35,7 @@ class Puppet::FileServing::Content < Puppet::FileServing::Base # This stat can raise an exception, too. raise(ArgumentError, "Cannot read the contents of links unless following links") if stat.ftype == "symlink" - @content = ::File.read(full_path) + @content = Puppet::Util.binread(full_path) end @content end diff --git a/lib/puppet/indirector/exec.rb b/lib/puppet/indirector/exec.rb index 8ea13ff95..e6325adaa 100644 --- a/lib/puppet/indirector/exec.rb +++ b/lib/puppet/indirector/exec.rb @@ -39,7 +39,7 @@ class Puppet::Indirector::Exec < Puppet::Indirector::Terminus end if output =~ /\A\s*\Z/ # all whitespace - Puppet.debug "Empty response for #{name} from exec #{self.name} terminus" + Puppet.debug "Empty response for #{name} from #{self.name} terminus" return nil else return output diff --git a/lib/puppet/indirector/facts/facter.rb b/lib/puppet/indirector/facts/facter.rb index 6312a95fb..e41ef02e7 100644 --- a/lib/puppet/indirector/facts/facter.rb +++ b/lib/puppet/indirector/facts/facter.rb @@ -7,6 +7,22 @@ class Puppet::Node::Facts::Facter < Puppet::Indirector::Code returns the local host's facts, regardless of what you attempt to find." + # Clear out all of the loaded facts. Reload facter but not puppet facts. + # NOTE: This is clumsy and shouldn't be required for later (1.5.x) versions + # of Facter. + def self.reload_facter + Facter.clear + + # Reload everything. + if Facter.respond_to? :loadfacts + Facter.loadfacts + elsif Facter.respond_to? :load + Facter.load + else + Puppet.warning "You should upgrade your version of Facter to at least 1.3.8" + end + end + def self.load_fact_plugins # Add any per-module fact directories to the factpath module_fact_dirs = Puppet[:modulepath].split(File::PATH_SEPARATOR).collect do |d| @@ -15,7 +31,7 @@ class Puppet::Node::Facts::Facter < Puppet::Indirector::Code end end.flatten dirs = module_fact_dirs + Puppet[:factpath].split(File::PATH_SEPARATOR) - x = dirs.each do |dir| + x = dirs.uniq.each do |dir| load_facts_in_dir(dir) end end @@ -27,7 +43,7 @@ class Puppet::Node::Facts::Facter < Puppet::Indirector::Code Dir.glob("*.rb").each do |file| fqfile = ::File.join(dir, file) begin - Puppet.info "Loading facts in #{::File.basename(file.sub(".rb",''))}" + Puppet.info "Loading facts in #{fqfile}" Timeout::timeout(self.timeout) do load file end @@ -57,17 +73,14 @@ class Puppet::Node::Facts::Facter < Puppet::Indirector::Code 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) + self.class.reload_facter + self.class.load_fact_plugins result = Puppet::Node::Facts.new(request.key, Facter.to_hash) result.add_local_facts diff --git a/lib/puppet/indirector/facts/inventory_active_record.rb b/lib/puppet/indirector/facts/inventory_active_record.rb index db4c63f00..93eee8a97 100644 --- a/lib/puppet/indirector/facts/inventory_active_record.rb +++ b/lib/puppet/indirector/facts/inventory_active_record.rb @@ -2,6 +2,7 @@ require 'puppet/rails' require 'puppet/rails/inventory_node' require 'puppet/rails/inventory_fact' require 'puppet/indirector/active_record' +require 'puppet/util/retryaction' class Puppet::Node::Facts::InventoryActiveRecord < Puppet::Indirector::ActiveRecord def find(request) @@ -13,19 +14,21 @@ class Puppet::Node::Facts::InventoryActiveRecord < Puppet::Indirector::ActiveRec end def save(request) - facts = request.instance - node = Puppet::Rails::InventoryNode.find_by_name(request.key) || Puppet::Rails::InventoryNode.create(:name => request.key, :timestamp => facts.timestamp) - node.timestamp = facts.timestamp + Puppet::Util::RetryAction.retry_action :retries => 4, :retry_exceptions => {ActiveRecord::StatementInvalid => 'MySQL Error. Retrying'} do + facts = request.instance + node = Puppet::Rails::InventoryNode.find_by_name(request.key) || Puppet::Rails::InventoryNode.create(:name => request.key, :timestamp => facts.timestamp) + node.timestamp = facts.timestamp - ActiveRecord::Base.transaction do - Puppet::Rails::InventoryFact.delete_all(:node_id => node.id) - # We don't want to save internal values as facts, because those are - # metadata that belong on the node - facts.values.each do |name,value| - next if name.to_s =~ /^_/ - node.facts.build(:name => name, :value => value) + ActiveRecord::Base.transaction do + Puppet::Rails::InventoryFact.delete_all(:node_id => node.id) + # We don't want to save internal values as facts, because those are + # metadata that belong on the node + facts.values.each do |name,value| + next if name.to_s =~ /^_/ + node.facts.build(:name => name, :value => value) + end + node.save end - node.save end end diff --git a/lib/puppet/indirector/indirection.rb b/lib/puppet/indirector/indirection.rb index 20b260b83..7296be2b9 100644 --- a/lib/puppet/indirector/indirection.rb +++ b/lib/puppet/indirector/indirection.rb @@ -1,12 +1,19 @@ require 'puppet/util/docs' require 'puppet/indirector/envelope' require 'puppet/indirector/request' +require 'puppet/util/instrumentation/instrumentable' # The class that connects functional classes with their different collection # back-ends. Each indirection has a set of associated terminus classes, # each of which is a subclass of Puppet::Indirector::Terminus. class Puppet::Indirector::Indirection include Puppet::Util::Docs + extend Puppet::Util::Instrumentation::Instrumentable + + probe :find, :label => Proc.new { |parent, key, *args| "find_#{parent.name}_#{parent.terminus_class}" }, :data => Proc.new { |parent, key, *args| { :key => key }} + probe :save, :label => Proc.new { |parent, key, *args| "save_#{parent.name}_#{parent.terminus_class}" }, :data => Proc.new { |parent, key, *args| { :key => key }} + probe :search, :label => Proc.new { |parent, key, *args| "search_#{parent.name}_#{parent.terminus_class}" }, :data => Proc.new { |parent, key, *args| { :key => key }} + probe :destroy, :label => Proc.new { |parent, key, *args| "destroy_#{parent.name}_#{parent.terminus_class}" }, :data => Proc.new { |parent, key, *args| { :key => key }} @@indirections = [] diff --git a/lib/puppet/indirector/instrumentation_data.rb b/lib/puppet/indirector/instrumentation_data.rb new file mode 100644 index 000000000..f1bea330d --- /dev/null +++ b/lib/puppet/indirector/instrumentation_data.rb @@ -0,0 +1,3 @@ +# A stub class, so our constants work. +class Puppet::Indirector::InstrumentationData +end diff --git a/lib/puppet/indirector/instrumentation_data/local.rb b/lib/puppet/indirector/instrumentation_data/local.rb new file mode 100644 index 000000000..6510d7f2d --- /dev/null +++ b/lib/puppet/indirector/instrumentation_data/local.rb @@ -0,0 +1,19 @@ +require 'puppet/indirector/instrumentation_data' + +class Puppet::Indirector::InstrumentationData::Local < Puppet::Indirector::Code + def find(request) + model.new(request.key) + end + + def search(request) + raise Puppet::DevError, "You cannot search for instrumentation data" + end + + def save(request) + raise Puppet::DevError, "You cannot save instrumentation data" + end + + def destroy(request) + raise Puppet::DevError, "You cannot remove instrumentation data" + end +end diff --git a/lib/puppet/indirector/instrumentation_data/rest.rb b/lib/puppet/indirector/instrumentation_data/rest.rb new file mode 100644 index 000000000..28b211781 --- /dev/null +++ b/lib/puppet/indirector/instrumentation_data/rest.rb @@ -0,0 +1,5 @@ +require 'puppet/indirector/rest' +require 'puppet/indirector/instrumentation_data' + +class Puppet::Indirector::InstrumentationData::Rest < Puppet::Indirector::REST +end diff --git a/lib/puppet/indirector/instrumentation_listener.rb b/lib/puppet/indirector/instrumentation_listener.rb new file mode 100644 index 000000000..6aaa71ce6 --- /dev/null +++ b/lib/puppet/indirector/instrumentation_listener.rb @@ -0,0 +1,3 @@ +# A stub class, so our constants work. +class Puppet::Indirector::InstrumentationListener +end diff --git a/lib/puppet/indirector/instrumentation_listener/local.rb b/lib/puppet/indirector/instrumentation_listener/local.rb new file mode 100644 index 000000000..72578014c --- /dev/null +++ b/lib/puppet/indirector/instrumentation_listener/local.rb @@ -0,0 +1,23 @@ +require 'puppet/indirector/instrumentation_listener' + +class Puppet::Indirector::InstrumentationListener::Local < Puppet::Indirector::Code + def find(request) + Puppet::Util::Instrumentation[request.key] + end + + def search(request) + Puppet::Util::Instrumentation.listeners + end + + def save(request) + res = request.instance + Puppet::Util::Instrumentation[res.name] = res + nil # don't leak the listener + end + + def destroy(request) + listener = Puppet::Util::Instrumentation[request.key] + raise "Listener #{request.key} hasn't been subscribed" unless listener + Puppet::Util::Instrumentation.unsubscribe(listener) + end +end diff --git a/lib/puppet/indirector/instrumentation_listener/rest.rb b/lib/puppet/indirector/instrumentation_listener/rest.rb new file mode 100644 index 000000000..0bc8122ea --- /dev/null +++ b/lib/puppet/indirector/instrumentation_listener/rest.rb @@ -0,0 +1,5 @@ +require 'puppet/indirector/instrumentation_listener' +require 'puppet/indirector/rest' + +class Puppet::Indirector::InstrumentationListener::Rest < Puppet::Indirector::REST +end diff --git a/lib/puppet/indirector/instrumentation_probe.rb b/lib/puppet/indirector/instrumentation_probe.rb new file mode 100644 index 000000000..3e514447a --- /dev/null +++ b/lib/puppet/indirector/instrumentation_probe.rb @@ -0,0 +1,3 @@ +# A stub class, so our constants work. +class Puppet::Indirector::InstrumentationProbe +end diff --git a/lib/puppet/indirector/instrumentation_probe/local.rb b/lib/puppet/indirector/instrumentation_probe/local.rb new file mode 100644 index 000000000..dd0a3f707 --- /dev/null +++ b/lib/puppet/indirector/instrumentation_probe/local.rb @@ -0,0 +1,24 @@ +require 'puppet/indirector/instrumentation_probe' +require 'puppet/indirector/code' +require 'puppet/util/instrumentation/indirection_probe' + +class Puppet::Indirector::InstrumentationProbe::Local < Puppet::Indirector::Code + def find(request) + end + + def search(request) + probes = [] + Puppet::Util::Instrumentation::Instrumentable.each_probe do |probe| + probes << Puppet::Util::Instrumentation::IndirectionProbe.new("#{probe.klass}.#{probe.method}") + end + probes + end + + def save(request) + Puppet::Util::Instrumentation::Instrumentable.enable_probes + end + + def destroy(request) + Puppet::Util::Instrumentation::Instrumentable.disable_probes + end +end diff --git a/lib/puppet/indirector/instrumentation_probe/rest.rb b/lib/puppet/indirector/instrumentation_probe/rest.rb new file mode 100644 index 000000000..57e6fcf3d --- /dev/null +++ b/lib/puppet/indirector/instrumentation_probe/rest.rb @@ -0,0 +1,5 @@ +require 'puppet/indirector/rest' +require 'puppet/indirector/instrumentation_probe' + +class Puppet::Indirector::InstrumentationProbe::Rest < Puppet::Indirector::REST +end diff --git a/lib/puppet/indirector/rest.rb b/lib/puppet/indirector/rest.rb index c8a45d76d..d76a2890f 100644 --- a/lib/puppet/indirector/rest.rb +++ b/lib/puppet/indirector/rest.rb @@ -95,7 +95,7 @@ class Puppet::Indirector::REST < Puppet::Indirector::Terminus rescue OpenSSL::SSL::SSLError => error if error.message.include? "certificate verify failed" raise Puppet::Error, "#{error.message}. This is often because the time is out of sync on the server or client" - elsif error.message.include? "hostname was not match" + elsif error.message =~ /hostname (was )?not match/ raise unless cert = peer_certs.find { |c| c.name !~ /^puppet ca/i } valid_certnames = [cert.name, *cert.subject_alt_names].uniq diff --git a/lib/puppet/module.rb b/lib/puppet/module.rb index 00468df96..7f86df83d 100644 --- a/lib/puppet/module.rb +++ b/lib/puppet/module.rb @@ -19,13 +19,6 @@ class Puppet::Module FILETYPES = [MANIFESTS, FILES, TEMPLATES, PLUGINS] - # Return an array of paths by splitting the +modulepath+ config - # parameter. Only consider paths that are absolute and existing - # directories - def self.modulepath(environment = nil) - Puppet::Node::Environment.new(environment).modulepath - end - # Find and return the +module+ that +path+ belongs to. If +path+ is # absolute, or if there is no module whose name is the first component # of +path+, return +nil+ @@ -48,15 +41,16 @@ class Puppet::Module return metadata.is_a?(Hash) && !metadata.keys.empty? end - def initialize(name, environment = nil) + def initialize(name, options = {}) @name = name + @path = options[:path] assert_validity - if environment.is_a?(Puppet::Node::Environment) - @environment = environment + if options[:environment].is_a?(Puppet::Node::Environment) + @environment = options[:environment] else - @environment = Puppet::Node::Environment.new(environment) + @environment = Puppet::Node::Environment.new(options[:environment]) end load_metadata if has_metadata? @@ -141,7 +135,7 @@ class Puppet::Module # Find this module in the modulepath. def path - environment.modulepath.collect { |path| File.join(path, name) }.find { |d| FileTest.directory?(d) } + @path ||= environment.modulepath.collect { |path| File.join(path, name) }.find { |d| FileTest.directory?(d) } end # Find all plugin directories. This is used by the Plugins fileserving mount. @@ -149,11 +143,6 @@ class Puppet::Module subpath("plugins") end - def requires(name, version = nil) - @requires ||= [] - @requires << [name, version] - end - def supports(name, version = nil) @supports ||= [] @supports << [name, version] @@ -204,4 +193,11 @@ class Puppet::Module def assert_validity raise InvalidName, "Invalid module name; module names must be alphanumeric (plus '-'), not '#{name}'" unless name =~ /^[-\w]+$/ end + + def ==(other) + self.name == other.name && + self.version == other.version && + self.path == other.path && + self.environment == other.environment + end end diff --git a/lib/puppet/module_tool.rb b/lib/puppet/module_tool.rb new file mode 100644 index 000000000..c11fd58b1 --- /dev/null +++ b/lib/puppet/module_tool.rb @@ -0,0 +1,97 @@ +# Load standard libraries +require 'pathname' +require 'fileutils' +require 'puppet/module_tool/utils' + +# Define tool +module Puppet + class Module + module Tool + + # Directory names that should not be checksummed. + ARTIFACTS = ['pkg', /^\./, /^~/, /^#/, 'coverage'] + FULL_MODULE_NAME_PATTERN = /\A([^-\/|.]+)[-|\/](.+)\z/ + REPOSITORY_URL = Puppet.settings[:module_repository] + + # Is this a directory that shouldn't be checksummed? + # + # TODO: Should this be part of Checksums? + # TODO: Rename this method to reflect it's purpose? + # TODO: Shouldn't this be used when building packages too? + def self.artifact?(path) + case File.basename(path) + when *ARTIFACTS + true + else + false + end + end + + # Return the +username+ and +modname+ for a given +full_module_name+, or raise an + # ArgumentError if the argument isn't parseable. + def self.username_and_modname_from(full_module_name) + if matcher = full_module_name.match(FULL_MODULE_NAME_PATTERN) + return matcher.captures + else + raise ArgumentError, "Not a valid full name: #{full_module_name}" + end + end + + # Read HTTP proxy configurationm from Puppet's config file, or the + # http_proxy environment variable. + def self.http_proxy_env + proxy_env = ENV["http_proxy"] || ENV["HTTP_PROXY"] || nil + begin + return URI.parse(proxy_env) if proxy_env + rescue URI::InvalidURIError + return nil + end + return nil + end + + def self.http_proxy_host + env = http_proxy_env + + if env and env.host then + return env.host + end + + if Puppet.settings[:http_proxy_host] == 'none' + return nil + end + + return Puppet.settings[:http_proxy_host] + end + + def self.http_proxy_port + env = http_proxy_env + + if env and env.port then + return env.port + end + + return Puppet.settings[:http_proxy_port] + end + + def self.find_module_root(path) + for dir in [path, Dir.pwd].compact + if File.exist?(File.join(dir, 'Modulefile')) + return dir + end + end + raise ArgumentError, "Could not find a valid module at #{path ? path.inspect : 'current directory'}" + end + end + end +end + +# Load remaining libraries +require 'puppet/module_tool/applications' +require 'puppet/module_tool/cache' +require 'puppet/module_tool/checksums' +require 'puppet/module_tool/contents_description' +require 'puppet/module_tool/dependency' +require 'puppet/module_tool/metadata' +require 'puppet/module_tool/modulefile' +require 'puppet/module_tool/repository' +require 'puppet/module_tool/skeleton' diff --git a/lib/puppet/module_tool/applications.rb b/lib/puppet/module_tool/applications.rb new file mode 100644 index 000000000..24bcbe279 --- /dev/null +++ b/lib/puppet/module_tool/applications.rb @@ -0,0 +1,13 @@ +module Puppet::Module::Tool + module Applications + require 'puppet/module_tool/applications/application' + require 'puppet/module_tool/applications/builder' + require 'puppet/module_tool/applications/checksummer' + require 'puppet/module_tool/applications/cleaner' + require 'puppet/module_tool/applications/generator' + require 'puppet/module_tool/applications/installer' + require 'puppet/module_tool/applications/searcher' + require 'puppet/module_tool/applications/unpacker' + require 'puppet/module_tool/applications/uninstaller' + end +end diff --git a/lib/puppet/module_tool/applications/application.rb b/lib/puppet/module_tool/applications/application.rb new file mode 100644 index 000000000..5f8c0c4bf --- /dev/null +++ b/lib/puppet/module_tool/applications/application.rb @@ -0,0 +1,83 @@ +require 'net/http' +require 'semver' + +module Puppet::Module::Tool + module Applications + class Application + include Utils::Interrogation + + def self.run(*args) + new(*args).run + end + + attr_accessor :options + + def initialize(options = {}) + @options = options + end + + def repository + @repository ||= Repository.new(@options[:module_repository]) + end + + def run + raise NotImplementedError, "Should be implemented in child classes." + end + + def discuss(response, success, failure) + case response + when Net::HTTPOK, Net::HTTPCreated + Puppet.notice success + else + errors = PSON.parse(response.body)['error'] rescue "HTTP #{response.code}, #{response.body}" + Puppet.warning "#{failure} (#{errors})" + end + end + + def metadata(require_modulefile = false) + unless @metadata + unless @path + raise ArgumentError, "Could not determine module path" + end + @metadata = Puppet::Module::Tool::Metadata.new + contents = ContentsDescription.new(@path) + contents.annotate(@metadata) + checksums = Checksums.new(@path) + checksums.annotate(@metadata) + modulefile_path = File.join(@path, 'Modulefile') + if File.file?(modulefile_path) + Puppet::Module::Tool::ModulefileReader.evaluate(@metadata, modulefile_path) + elsif require_modulefile + raise ArgumentError, "No Modulefile found." + end + end + @metadata + end + + def load_modulefile! + @metadata = nil + metadata(true) + end + + # Use to extract and validate a module name and version from a + # filename + # Note: Must have @filename set to use this + def parse_filename! + @release_name = File.basename(@filename,'.tar.gz') + match = /^(.*?)-(.*?)-(\d+\.\d+\.\d+.*?)$/.match(@release_name) + if match then + @username, @module_name, @version = match.captures + else + raise ArgumentError, "Could not parse filename to obtain the username, module name and version. (#{@release_name})" + end + @full_module_name = [@username, @module_name].join('-') + unless @username && @module_name + raise ArgumentError, "Username and Module name not provided" + end + unless SemVer.valid?(@version) + raise ArgumentError, "Invalid version format: #{@version} (Semantic Versions are acceptable: http://semver.org)" + end + end + end + end +end diff --git a/lib/puppet/module_tool/applications/builder.rb b/lib/puppet/module_tool/applications/builder.rb new file mode 100644 index 000000000..322020a8b --- /dev/null +++ b/lib/puppet/module_tool/applications/builder.rb @@ -0,0 +1,91 @@ +require 'fileutils' + +module Puppet::Module::Tool + module Applications + class Builder < Application + + def initialize(path, options = {}) + @path = File.expand_path(Puppet::Module::Tool.find_module_root(path)) + @pkg_path = File.join(@path, 'pkg') + super(options) + end + + def run + load_modulefile! + create_directory + copy_contents + add_metadata + Puppet.notice "Building #{@path} for release" + tar + gzip + relative = Pathname.new(File.join(@pkg_path, filename('tar.gz'))).relative_path_from(Pathname.new(Dir.pwd)) + + # Return the Pathname object representing the path to the release + # archive just created. This return value is used by the module_tool + # face build action, and displayed to on the console using the to_s + # method. + # + # Example return value: + # + # <Pathname:puppetlabs-apache/pkg/puppetlabs-apache-0.0.1.tar.gz> + # + relative + end + + private + + def filename(ext) + ext.sub!(/^\./, '') + "#{metadata.release_name}.#{ext}" + end + + def tar + tar_name = filename('tar') + Dir.chdir(@pkg_path) do + FileUtils.rm tar_name rescue nil + unless system "tar -cf #{tar_name} #{metadata.release_name}" + raise RuntimeError, "Could not create #{tar_name}" + end + end + end + + def gzip + Dir.chdir(@pkg_path) do + FileUtils.rm filename('tar.gz') rescue nil + unless system "gzip #{filename('tar')}" + raise RuntimeError, "Could not compress #{filename('tar')}" + end + end + end + + def create_directory + FileUtils.mkdir(@pkg_path) rescue nil + if File.directory?(build_path) + FileUtils.rm_rf(build_path) + end + FileUtils.mkdir(build_path) + end + + def copy_contents + Dir[File.join(@path, '*')].each do |path| + case File.basename(path) + when *Puppet::Module::Tool::ARTIFACTS + next + else + FileUtils.cp_r path, build_path + end + end + end + + def add_metadata + File.open(File.join(build_path, 'metadata.json'), 'w') do |f| + f.write(PSON.pretty_generate(metadata)) + end + end + + def build_path + @build_path ||= File.join(@pkg_path, metadata.release_name) + end + end + end +end diff --git a/lib/puppet/module_tool/applications/checksummer.rb b/lib/puppet/module_tool/applications/checksummer.rb new file mode 100644 index 000000000..2ea1ef587 --- /dev/null +++ b/lib/puppet/module_tool/applications/checksummer.rb @@ -0,0 +1,47 @@ +module Puppet::Module::Tool + module Applications + class Checksummer < Application + + def initialize(path, options = {}) + @path = Pathname.new(path) + super(options) + end + + def run + changes = [] + if metadata_file.exist? + sums = Checksums.new(@path) + (metadata['checksums'] || {}).each do |child_path, canonical_checksum| + path = @path + child_path + if canonical_checksum != sums.checksum(path) + changes << child_path + end + end + else + raise ArgumentError, "No metadata.json found." + end + + # Return an Array of strings representing file paths of files that have + # been modified since this module was installed. All paths are relative + # to the installed module directory. This return value is used by the + # module_tool face changes action, and displayed on the console. + # + # Example return value: + # + # [ "REVISION", "metadata.json", "manifests/init.pp"] + # + changes + end + + private + + def metadata + PSON.parse(metadata_file.read) + end + + def metadata_file + (@path + 'metadata.json') + end + end + end +end diff --git a/lib/puppet/module_tool/applications/cleaner.rb b/lib/puppet/module_tool/applications/cleaner.rb new file mode 100644 index 000000000..c42687fc7 --- /dev/null +++ b/lib/puppet/module_tool/applications/cleaner.rb @@ -0,0 +1,16 @@ +module Puppet::Module::Tool + module Applications + class Cleaner < Application + def run + Puppet::Module::Tool::Cache.clean + + # Return a status Hash containing the status of the clean command + # and a status message. This return value is used by the module_tool + # face clean action, and the status message, return_value[:msg], is + # displayed on the console. + # + { :status => "success", :msg => "Cleaned module cache." } + end + end + end +end diff --git a/lib/puppet/module_tool/applications/generator.rb b/lib/puppet/module_tool/applications/generator.rb new file mode 100644 index 000000000..699fe246d --- /dev/null +++ b/lib/puppet/module_tool/applications/generator.rb @@ -0,0 +1,141 @@ +require 'pathname' +require 'fileutils' +require 'erb' + +module Puppet::Module::Tool + module Applications + class Generator < Application + + def initialize(full_module_name, options = {}) + begin + @metadata = Metadata.new(:full_module_name => full_module_name) + rescue ArgumentError + raise "Could not generate directory #{full_module_name.inspect}, you must specify a dash-separated username and module name." + end + super(options) + end + + def skeleton + @skeleton ||= Skeleton.new + end + + def get_binding + binding + end + + def run + if destination.directory? + raise ArgumentError, "#{destination} already exists." + end + Puppet.notice "Generating module at #{Dir.pwd}/#{@metadata.dashed_name}" + files_created = [] + skeleton.path.find do |path| + if path == skeleton + destination.mkpath + else + node = Node.on(path, self) + if node + node.install! + files_created << node.target + else + Puppet.notice "Could not generate from #{path}" + end + end + end + + # Return an array of Pathname objects representing file paths of files + # and directories just generated. This return value is used by the + # module_tool face generate action, and displayed on the console. + # + # Example return value: + # + # [ + # #<Pathname:puppetlabs-apache>, + # #<Pathname:puppetlabs-apache/tests>, + # #<Pathname:puppetlabs-apache/tests/init.pp>, + # #<Pathname:puppetlabs-apache/spec>, + # #<Pathname:puppetlabs-apache/spec/spec_helper.rb>, + # #<Pathname:puppetlabs-apache/spec/spec.opts>, + # #<Pathname:puppetlabs-apache/README>, + # #<Pathname:puppetlabs-apache/Modulefile>, + # #<Pathname:puppetlabs-apache/metadata.json>, + # #<Pathname:puppetlabs-apache/manifests>, + # #<Pathname:puppetlabs-apache/manifests/init.pp" + # ] + # + files_created + end + + def destination + @destination ||= Pathname.new(@metadata.dashed_name) + end + + class Node + def self.types + @types ||= [] + end + def self.inherited(klass) + types << klass + end + def self.on(path, generator) + klass = types.detect { |t| t.matches?(path) } + if klass + klass.new(path, generator) + end + end + def initialize(source, generator) + @generator = generator + @source = source + end + def read + @source.read + end + def target + target = @generator.destination + @source.relative_path_from(@generator.skeleton.path) + components = target.to_s.split(File::SEPARATOR).map do |part| + part == 'NAME' ? @generator.metadata.name : part + end + Pathname.new(components.join(File::SEPARATOR)) + end + def install! + raise NotImplementedError, "Abstract" + end + end + + class DirectoryNode < Node + def self.matches?(path) + path.directory? + end + def install! + target.mkpath + end + end + + class ParsedFileNode < Node + def self.matches?(path) + path.file? && path.extname == '.erb' + end + def target + path = super + path.parent + path.basename('.erb') + end + def contents + template = ERB.new(read) + template.result(@generator.send(:get_binding)) + end + def install! + target.open('w') { |f| f.write contents } + end + end + + class FileNode < Node + def self.matches?(path) + path.file? + end + def install! + FileUtils.cp(@source, target) + end + end + end + end +end diff --git a/lib/puppet/module_tool/applications/installer.rb b/lib/puppet/module_tool/applications/installer.rb new file mode 100644 index 000000000..ad423bd83 --- /dev/null +++ b/lib/puppet/module_tool/applications/installer.rb @@ -0,0 +1,89 @@ +require 'open-uri' +require 'pathname' +require 'tmpdir' + +module Puppet::Module::Tool + module Applications + class Installer < Application + + def initialize(name, options = {}) + if File.exist?(name) + if File.directory?(name) + # TODO Unify this handling with that of Unpacker#check_clobber! + raise ArgumentError, "Module already installed: #{name}" + end + @source = :filesystem + @filename = File.expand_path(name) + parse_filename! + else + @source = :repository + begin + @username, @module_name = Puppet::Module::Tool::username_and_modname_from(name) + rescue ArgumentError + raise "Could not install module with invalid name: #{name}" + end + @version_requirement = options[:version] + end + super(options) + end + + def force? + options[:force] + end + + def run + case @source + when :repository + if match['file'] + begin + cache_path = repository.retrieve(match['file']) + rescue OpenURI::HTTPError => e + raise RuntimeError, "Could not install module: #{e.message}" + end + module_dir = Unpacker.run(cache_path, options) + else + raise RuntimeError, "Malformed response from module repository." + end + when :filesystem + repository = Repository.new('file:///') + uri = URI.parse("file://#{URI.escape(File.expand_path(@filename))}") + cache_path = repository.retrieve(uri) + module_dir = Unpacker.run(cache_path, options) + else + raise ArgumentError, "Could not determine installation source" + end + + # Return the Pathname object representing the path to the installed + # module. This return value is used by the module_tool face install + # action, and displayed to on the console. + # + # Example return value: + # + # "/etc/puppet/modules/apache" + # + module_dir + end + + private + + def match + return @match ||= begin + url = repository.uri + "/users/#{@username}/modules/#{@module_name}/releases/find.json" + if @version_requirement + url.query = "version=#{URI.escape(@version_requirement)}" + end + begin + raw_result = read_match(url) + rescue => e + raise ArgumentError, "Could not find a release for this module (#{e.message})" + end + @match = PSON.parse(raw_result) + end + end + + def read_match(url) + return url.read + end + end + end +end diff --git a/lib/puppet/module_tool/applications/searcher.rb b/lib/puppet/module_tool/applications/searcher.rb new file mode 100644 index 000000000..0a2267b3c --- /dev/null +++ b/lib/puppet/module_tool/applications/searcher.rb @@ -0,0 +1,40 @@ +module Puppet::Module::Tool + module Applications + class Searcher < Application + + def initialize(term, options = {}) + @term = term + super(options) + end + + def run + request = Net::HTTP::Get.new("/modules.json?q=#{URI.escape(@term)}") + response = repository.make_http_request(request) + case response + when Net::HTTPOK + matches = PSON.parse(response.body) + else + raise RuntimeError, "Could not execute search (HTTP #{response.code})" + matches = [] + end + + # Return a list of module metadata hashes that match the search query. + # This return value is used by the module_tool face install search, + # and displayed to on the console. + # + # Example return value: + # + # [ + # { + # "name" => "nginx", + # "project_url" => "http://github.com/puppetlabs/puppetlabs-nginx", + # "version" => "0.0.1", + # "full_name" => "puppetlabs/nginx" # full_name comes back from + # } # API all to the forge. + # ] + # + matches + end + end + end +end diff --git a/lib/puppet/module_tool/applications/uninstaller.rb b/lib/puppet/module_tool/applications/uninstaller.rb new file mode 100644 index 000000000..9cd4d8bd3 --- /dev/null +++ b/lib/puppet/module_tool/applications/uninstaller.rb @@ -0,0 +1,33 @@ +module Puppet::Module::Tool + module Applications + class Uninstaller < Application + + def initialize(name, options = {}) + @name = name + @target_directories = options[:target_directories] + @removed_dirs = [] + end + + def run + uninstall + Puppet.notice "#{@name} is not installed" if @removed_dirs.empty? + @removed_dirs + end + + private + + def uninstall + # TODO: #11803 Check for broken dependencies before uninstalling modules. + # + # Search each path in the target directories for the specified module + # and delete the directory. + @target_directories.each do |target| + if File.directory? target + module_path = File.join(target, @name) + @removed_dirs << FileUtils.rm_rf(module_path).first if File.directory?(module_path) + end + end + end + end + end +end diff --git a/lib/puppet/module_tool/applications/unpacker.rb b/lib/puppet/module_tool/applications/unpacker.rb new file mode 100644 index 000000000..6dd1fca55 --- /dev/null +++ b/lib/puppet/module_tool/applications/unpacker.rb @@ -0,0 +1,70 @@ +require 'pathname' +require 'tmpdir' + +module Puppet::Module::Tool + module Applications + class Unpacker < Application + + def initialize(filename, options = {}) + @filename = Pathname.new(filename) + parse_filename! + super(options) + @module_dir = Pathname.new(options[:install_dir]) + @module_name + end + + def run + extract_module_to_install_dir + tag_revision + + # Return the Pathname object representing the directory where the + # module release archive was unpacked the to, and the module release + # name. + @module_dir + end + + private + + def tag_revision + File.open("#{@module_dir}/REVISION", 'w') do |f| + f.puts "module: #{@username}/#{@module_name}" + f.puts "version: #{@version}" + f.puts "url: file://#{@filename.expand_path}" + f.puts "installed: #{Time.now}" + end + end + + def extract_module_to_install_dir + delete_existing_installation_or_abort! + + build_dir = Puppet::Module::Tool::Cache.base_path + "tmp-unpacker-#{Digest::SHA1.hexdigest(@filename.basename.to_s)}" + build_dir.mkpath + begin + Puppet.notice "Installing #{@filename.basename} to #{@module_dir.expand_path}" + unless system "tar xzf #{@filename} -C #{build_dir}" + raise RuntimeError, "Could not extract contents of module archive." + end + # grab the first directory + extracted = build_dir.children.detect { |c| c.directory? } + FileUtils.mv extracted, @module_dir + ensure + build_dir.rmtree + end + end + + def delete_existing_installation_or_abort! + return unless @module_dir.exist? + + if !options[:force] + Puppet.warning "Existing module '#{@module_dir.expand_path}' found" + response = prompt "Overwrite module installed at #{@module_dir.expand_path}? [y/N]" + unless response =~ /y/i + raise RuntimeError, "Aborted installation." + end + end + + Puppet.warning "Deleting #{@module_dir.expand_path}" + FileUtils.rm_rf @module_dir + end + end + end +end diff --git a/lib/puppet/module_tool/cache.rb b/lib/puppet/module_tool/cache.rb new file mode 100644 index 000000000..b25478096 --- /dev/null +++ b/lib/puppet/module_tool/cache.rb @@ -0,0 +1,56 @@ +require 'uri' + +module Puppet::Module::Tool + + # = Cache + # + # Provides methods for reading files from local cache, filesystem or network. + class Cache + + # Instantiate new cahe for the +repositry+ instance. + def initialize(repository, options = {}) + @repository = repository + @options = options + end + + # Return filename retrieved from +uri+ instance. Will download this file and + # cache it if needed. + # + # TODO: Add checksum support. + # TODO: Add error checking. + def retrieve(url) + (path + File.basename(url.to_s)).tap do |cached_file| + uri = url.is_a?(::URI) ? url : ::URI.parse(url) + unless cached_file.file? + if uri.scheme == 'file' + FileUtils.cp(URI.unescape(uri.path), cached_file) + else + # TODO: Handle HTTPS; probably should use repository.contact + data = read_retrieve(uri) + cached_file.open('wb') { |f| f.write data } + end + end + end + end + + # Return contents of file at the given URI's +uri+. + def read_retrieve(uri) + return uri.read + end + + # Return Pathname for repository's cache directory, create it if needed. + def path + return @path ||= (self.class.base_path + @repository.cache_key).tap{ |o| o.mkpath } + end + + # Return the base Pathname for all the caches. + def self.base_path + Pathname(Puppet.settings[:module_working_dir]) + 'cache' + end + + # Clean out all the caches. + def self.clean + base_path.rmtree if base_path.exist? + end + end +end diff --git a/lib/puppet/module_tool/checksums.rb b/lib/puppet/module_tool/checksums.rb new file mode 100644 index 000000000..a566090d6 --- /dev/null +++ b/lib/puppet/module_tool/checksums.rb @@ -0,0 +1,52 @@ +require 'digest/md5' + +module Puppet::Module::Tool + + # = Checksums + # + # This class proides methods for generating checksums for data and adding + # them to +Metadata+. + class Checksums + include Enumerable + + # Instantiate object with string +path+ to create checksums from. + def initialize(path) + @path = Pathname.new(path) + end + + # Return checksum for the +Pathname+. + def checksum(pathname) + return Digest::MD5.hexdigest(pathname.read) + end + + # Return checksums for object's +Pathname+, generate if it's needed. + # Result is a hash of path strings to checksum strings. + def data + unless @data + @data = {} + @path.find do |descendant| + if Puppet::Module::Tool.artifact?(descendant) + Find.prune + elsif descendant.file? + path = descendant.relative_path_from(@path) + @data[path.to_s] = checksum(descendant) + end + end + end + return @data + end + + # TODO: Why? + def each(&block) + data.each(&block) + end + + # Update +Metadata+'s checksums with this object's. + def annotate(metadata) + metadata.checksums.replace(data) + end + + # TODO: Move the Checksummer#run checksum checking to here? + + end +end diff --git a/lib/puppet/module_tool/contents_description.rb b/lib/puppet/module_tool/contents_description.rb new file mode 100644 index 000000000..f6a94e22d --- /dev/null +++ b/lib/puppet/module_tool/contents_description.rb @@ -0,0 +1,82 @@ +module Puppet::Module::Tool + + # = ContentsDescription + # + # This class populates +Metadata+'s Puppet type information. + class ContentsDescription + + # Instantiate object for string +module_path+. + def initialize(module_path) + @module_path = module_path + end + + # Update +Metadata+'s Puppet type information. + def annotate(metadata) + metadata.types.replace data.clone + end + + # Return types for this module. Result is an array of hashes, each of which + # describes a Puppet type. The type description hash structure is: + # * :name => Name of this Puppet type. + # * :doc => Documentation for this type. + # * :properties => Array of hashes representing the type's properties, each + # containing :name and :doc. + # * :parameters => Array of hashes representing the type's parameters, each + # containing :name and :doc. + # * :providers => Array of hashes representing the types providers, each + # containing :name and :doc. + # TODO Write a TypeDescription to encapsulate these structures and logic? + def data + unless @data + @data = [] + type_names = [] + for module_filename in Dir[File.join(@module_path, "lib/puppet/type/*.rb")] + require module_filename + type_name = File.basename(module_filename, ".rb") + type_names << type_name + + for provider_filename in Dir[File.join(@module_path, "lib/puppet/provider/#{type_name}/*.rb")] + require provider_filename + end + end + + type_names.each do |type_name| + if type = Puppet::Type.type(type_name.to_sym) + type_hash = {:name => type_name, :doc => type.doc} + type_hash[:properties] = attr_doc(type, :property) + type_hash[:parameters] = attr_doc(type, :param) + if type.providers.size > 0 + type_hash[:providers] = provider_doc(type) + end + @data << type_hash + else + Puppet.warning "Could not find/load type: #{type_name}" + end + end + end + @data + end + + # Return an array of hashes representing this +type+'s attrs of +kind+ + # (e.g. :param or :property), each containing :name and :doc. + def attr_doc(type, kind) + [].tap do |attrs| + type.allattrs.each do |name| + if type.attrtype(name) == kind && name != :provider + attrs.push(:name => name, :doc => type.attrclass(name).doc) + end + end + end + end + + # Return an array of hashes representing this +type+'s providers, each + # containing :name and :doc. + def provider_doc(type) + [].tap do |providers| + type.providers.sort_by{ |o| o.to_s }.each do |prov| + providers.push(:name => prov, :doc => type.provider(prov).doc) + end + end + end + end +end diff --git a/lib/puppet/module_tool/dependency.rb b/lib/puppet/module_tool/dependency.rb new file mode 100644 index 000000000..bb55f5945 --- /dev/null +++ b/lib/puppet/module_tool/dependency.rb @@ -0,0 +1,24 @@ +module Puppet::Module::Tool + + class Dependency + + # Instantiates a new module dependency with a +full_module_name+ (e.g. + # "myuser-mymodule"), and optional +version_requirement+ (e.g. "0.0.1") and + # optional repository (a URL string). + def initialize(full_module_name, version_requirement = nil, repository = nil) + @full_module_name = full_module_name + # TODO: add error checking, the next line raises ArgumentError when +full_module_name+ is invalid + @username, @name = Puppet::Module::Tool.username_and_modname_from(full_module_name) + @version_requirement = version_requirement + @repository = repository ? Repository.new(repository) : nil + end + + # Return PSON representation of this data. + def to_pson(*args) + result = { :name => @full_module_name } + result[:version_requirement] = @version_requirement if @version_requirement && ! @version_requirement.nil? + result[:repository] = @repository.to_s if @repository && ! @repository.nil? + result.to_pson(*args) + end + end +end diff --git a/lib/puppet/module_tool/metadata.rb b/lib/puppet/module_tool/metadata.rb new file mode 100644 index 000000000..a84100a10 --- /dev/null +++ b/lib/puppet/module_tool/metadata.rb @@ -0,0 +1,141 @@ +module Puppet::Module::Tool + + # = Metadata + # + # This class provides a data structure representing a module's metadata. + # It provides some basic parsing, but other data is injected into it using + # +annotate+ methods in other classes. + class Metadata + + # The full name of the module, which is a dash-separated combination of the + # +username+ and module +name+. + attr_reader :full_module_name + + # The name of the user that owns this module. + attr_reader :username + + # The name of this module. See also +full_module_name+. + attr_reader :name + + # The version of this module. + attr_reader :version + + # Instantiate from a hash, whose keys are setters in this class. + def initialize(settings={}) + settings.each do |key, value| + send("#{key}=", value) + end + end + + # Set the full name of this module, and from it, the +username+ and + # module +name+. + def full_module_name=(full_module_name) + @full_module_name = full_module_name + @username, @name = Puppet::Module::Tool::username_and_modname_from(full_module_name) + end + + # Return an array of the module's Dependency objects. + def dependencies + return @dependencies ||= [] + end + + def author + @author || @username + end + + def author=(author) + @author = author + end + + def source + @source || 'UNKNOWN' + end + + def source=(source) + @source = source + end + + def license + @license || 'Apache License, Version 2.0' + end + + def license=(license) + @license = license + end + + def summary + @summary || 'UNKNOWN' + end + + def summary=(summary) + @summary = summary + end + + def description + @description || 'UNKNOWN' + end + + def description=(description) + @description = description + end + + def project_page + @project_page || 'UNKNOWN' + end + + def project_page=(project_page) + @project_page = project_page + end + + # Return an array of the module's Puppet types, each one is a hash + # containing :name and :doc. + def types + return @types ||= [] + end + + # Return module's file checksums. + def checksums + return @checksums ||= {} + end + + # Return the dashed name of the module, which may either be the + # dash-separated combination of the +username+ and module +name+, or just + # the module +name+. + def dashed_name + return [@username, @name].compact.join('-') + end + + # Return the release name, which is the combination of the +dashed_name+ + # of the module and its +version+ number. + def release_name + return [dashed_name, @version].join('-') + end + + # Set the version of this module, ensure a string like '0.1.0' see the + # Semantic Versions here: http://semver.org + def version=(version) + if SemVer.valid?(version) + @version = version + else + raise ArgumentError, "Invalid version format: #{@version} (Semantic Versions are acceptable: http://semver.org)" + end + end + + # Return the PSON record representing this instance. + def to_pson(*args) + return { + :name => @full_module_name, + :version => @version, + :source => source, + :author => author, + :license => license, + :summary => summary, + :description => description, + :project_page => project_page, + :dependencies => dependencies, + :types => types, + :checksums => checksums + }.to_pson(*args) + end + end +end diff --git a/lib/puppet/module_tool/modulefile.rb b/lib/puppet/module_tool/modulefile.rb new file mode 100644 index 000000000..35c1e9973 --- /dev/null +++ b/lib/puppet/module_tool/modulefile.rb @@ -0,0 +1,75 @@ +module Puppet::Module::Tool + + # = Modulefile + # + # This class provides the DSL used for evaluating the module's 'Modulefile'. + # These methods are used to concisely define this module's attributes, which + # are later rendered as PSON into a 'metadata.json' file. + class ModulefileReader + + # Read the +filename+ and eval its Ruby code to set values in the Metadata + # +metadata+ instance. + def self.evaluate(metadata, filename) + returning(new(metadata)) do |builder| + if File.file?(filename) + builder.instance_eval(File.read(filename.to_s), filename.to_s, 1) + else + Puppet.warning "No Modulefile: #{filename}" + end + end + end + + # Instantiate with the Metadata +metadata+ instance. + def initialize(metadata) + @metadata = metadata + end + + # Set the +full_module_name+ (e.g. "myuser-mymodule"), which will also set the + # +username+ and module +name+. Required. + def name(name) + @metadata.full_module_name = name + end + + # Set the module +version+ (e.g., "0.0.1"). Required. + def version(version) + @metadata.version = version + end + + # Add a dependency with the full_module_name +name+ (e.g. "myuser-mymodule"), an + # optional +version_requirement+ (e.g. "0.0.1") and +repository+ (a URL + # string). Optional. Can be called multiple times to add many dependencies. + def dependency(name, version_requirement = nil, repository = nil) + @metadata.dependencies << Dependency.new(name, version_requirement, repository) + end + + # Set the source + def source(source) + @metadata.source = source + end + + # Set the author or default to +username+ + def author(author) + @metadata.author = author + end + + # Set the license + def license(license) + @metadata.license = license + end + + # Set the summary + def summary(summary) + @metadata.summary = summary + end + + # Set the description + def description(description) + @metadata.description = description + end + + # Set the project page + def project_page(project_page) + @metadata.project_page = project_page + end + end +end diff --git a/lib/puppet/module_tool/repository.rb b/lib/puppet/module_tool/repository.rb new file mode 100644 index 000000000..7f0ad849f --- /dev/null +++ b/lib/puppet/module_tool/repository.rb @@ -0,0 +1,79 @@ +require 'net/http' +require 'digest/sha1' +require 'uri' + +module Puppet::Module::Tool + + # = Repository + # + # This class is a file for accessing remote repositories with modules. + class Repository + include Utils::Interrogation + + attr_reader :uri, :cache + + # Instantiate a new repository instance rooted at the optional string + # +url+, else an instance of the default Puppet modules repository. + def initialize(url=Puppet[:module_repository]) + @uri = url.is_a?(::URI) ? url : ::URI.parse(url) + @cache = Cache.new(self) + end + + # Return a Net::HTTPResponse read for this +request+. + # + # Options: + # * :authenticate => Request authentication on the terminal. Defaults to false. + def make_http_request(request, options = {}) + if options[:authenticate] + authenticate(request) + end + if ! @uri.user.nil? && ! @uri.password.nil? + request.basic_auth(@uri.user, @uri.password) + end + return read_response(request) + end + + # Return a Net::HTTPResponse read from this HTTPRequest +request+. + def read_response(request) + begin + Net::HTTP::Proxy( + Puppet::Module::Tool::http_proxy_host, + Puppet::Module::Tool::http_proxy_port + ).start(@uri.host, @uri.port) do |http| + http.request(request) + end + rescue Errno::ECONNREFUSED, SocketError + raise RuntimeError, "Could not reach remote repository" + end + end + + # Set the HTTP Basic Authentication parameters for the Net::HTTPRequest + # +request+ by asking the user for input on the console. + def authenticate(request) + Puppet.notice "Authenticating for #{@uri}" + email = prompt('Email Address') + password = prompt('Password', true) + request.basic_auth(email, password) + end + + # Return the local file name containing the data downloaded from the + # repository at +release+ (e.g. "myuser-mymodule"). + def retrieve(release) + return cache.retrieve(@uri + release) + end + + # Return the URI string for this repository. + def to_s + return @uri.to_s + end + + # Return the cache key for this repository, this a hashed string based on + # the URI. + def cache_key + return @cache_key ||= [ + @uri.to_s.gsub(/[^[:alnum:]]+/, '_').sub(/_$/, ''), + Digest::SHA1.hexdigest(@uri.to_s) + ].join('-') + end + end +end diff --git a/lib/puppet/module_tool/skeleton.rb b/lib/puppet/module_tool/skeleton.rb new file mode 100644 index 000000000..4c41c7611 --- /dev/null +++ b/lib/puppet/module_tool/skeleton.rb @@ -0,0 +1,34 @@ +module Puppet::Module::Tool + + # = Skeleton + # + # This class provides methods for finding templates for the 'generate' action. + class Skeleton + + # TODO Review whether the 'freeze' feature should be fixed or deleted. + # def freeze! + # FileUtils.rm_fr custom_path rescue nil + # FileUtils.cp_r default_path, custom_path + # end + + # Return Pathname with 'generate' templates. + def path + paths.detect { |path| path.directory? } + end + + # Return Pathnames to look for 'generate' templates. + def paths + @paths ||= [ custom_path, default_path ] + end + + # Return Pathname of custom templates directory. + def custom_path + Pathname(Puppet.settings[:module_working_dir]) + 'skeleton' + end + + # Return Pathname of default template directory. + def default_path + Pathname(__FILE__).dirname + 'skeleton/templates/generator' + end + end +end diff --git a/lib/puppet/module_tool/skeleton/templates/generator/Modulefile.erb b/lib/puppet/module_tool/skeleton/templates/generator/Modulefile.erb new file mode 100644 index 000000000..845102d6e --- /dev/null +++ b/lib/puppet/module_tool/skeleton/templates/generator/Modulefile.erb @@ -0,0 +1,11 @@ +name '<%= metadata.full_module_name %>' +version '0.0.1' +source '<%= metadata.source %>' +author '<%= metadata.author %>' +license '<%= metadata.license %>' +summary '<%= metadata.summary %>' +description '<%= metadata.description %>' +project_page '<%= metadata.project_page %>' + +## Add dependencies, if any: +# dependency 'username/name', '>= 1.2.0' diff --git a/lib/puppet/module_tool/skeleton/templates/generator/README.erb b/lib/puppet/module_tool/skeleton/templates/generator/README.erb new file mode 100644 index 000000000..95a6cc472 --- /dev/null +++ b/lib/puppet/module_tool/skeleton/templates/generator/README.erb @@ -0,0 +1,16 @@ +<%= metadata.name %> + +This is the <%= metadata.name %> module. + +License +------- + + +Contact +------- + + +Support +------- + +Please log tickets and issues at our [Projects site](http://projects.example.com) diff --git a/lib/puppet/module_tool/skeleton/templates/generator/manifests/init.pp.erb b/lib/puppet/module_tool/skeleton/templates/generator/manifests/init.pp.erb new file mode 100644 index 000000000..04dd52583 --- /dev/null +++ b/lib/puppet/module_tool/skeleton/templates/generator/manifests/init.pp.erb @@ -0,0 +1,41 @@ +# == Class: <%= metadata.name %> +# +# Full description of class <%= metadata.name %> here. +# +# === Parameters +# +# Document parameters here. +# +# [*sample_parameter*] +# Explanation of what this parameter affects and what it defaults to. +# e.g. "Specify one or more upstream ntp servers as an array." +# +# === Variables +# +# Here you should define a list of variables that this module would require. +# +# [*sample_variable*] +# Explanation of how this variable affects the funtion of this class and if it +# has a default. e.g. "The parameter enc_ntp_servers must be set by the +# External Node Classifier as a comma separated list of hostnames." (Note, +# global variables should not be used in preference to class parameters as of +# Puppet 2.6.) +# +# === Examples +# +# class { <%= metadata.name %>: +# servers => [ 'pool.ntp.org', 'ntp.local.company.com' ] +# } +# +# === Authors +# +# Author Name <author@domain.com> +# +# === Copyright +# +# Copyright 2011 Your name here, unless otherwise noted. +# +class <%= metadata.name %> { + + +} diff --git a/lib/puppet/module_tool/skeleton/templates/generator/metadata.json b/lib/puppet/module_tool/skeleton/templates/generator/metadata.json new file mode 100644 index 000000000..8ce7797ff --- /dev/null +++ b/lib/puppet/module_tool/skeleton/templates/generator/metadata.json @@ -0,0 +1,12 @@ +/* ++-----------------------------------------------------------------------+ +| | +| ==> DO NOT EDIT THIS FILE! <== | +| | +| You should edit the `Modulefile` and run `puppet-module build` | +| to generate the `metadata.json` file for your releases. | +| | ++-----------------------------------------------------------------------+ +*/ + +{} diff --git a/lib/puppet/module_tool/skeleton/templates/generator/spec/spec_helper.rb b/lib/puppet/module_tool/skeleton/templates/generator/spec/spec_helper.rb new file mode 100644 index 000000000..5fda58875 --- /dev/null +++ b/lib/puppet/module_tool/skeleton/templates/generator/spec/spec_helper.rb @@ -0,0 +1,17 @@ +dir = File.expand_path(File.dirname(__FILE__)) +$LOAD_PATH.unshift File.join(dir, 'lib') + +require 'mocha' +require 'puppet' +require 'rspec' +require 'spec/autorun' + +Spec::Runner.configure do |config| + config.mock_with :mocha +end + +# We need this because the RAL uses 'should' as a method. This +# allows us the same behaviour but with a different method name. +class Object + alias :must :should +end diff --git a/lib/puppet/module_tool/skeleton/templates/generator/tests/init.pp.erb b/lib/puppet/module_tool/skeleton/templates/generator/tests/init.pp.erb new file mode 100644 index 000000000..ba4456396 --- /dev/null +++ b/lib/puppet/module_tool/skeleton/templates/generator/tests/init.pp.erb @@ -0,0 +1,11 @@ +# The baseline for module testing used by Puppet Labs is that each manifest +# should have a corresponding test manifest that declares that class or defined +# type. +# +# Tests are then run by using puppet apply --noop (to check for compilation errors +# and view a log of events) or by fully applying the test in a virtual environment +# (to compare the resulting system state to the desired state). +# +# Learn more about module testing here: http://docs.puppetlabs.com/guides/tests_smoke.html +# +include <%= metadata.name %> diff --git a/lib/puppet/module_tool/utils.rb b/lib/puppet/module_tool/utils.rb new file mode 100644 index 000000000..85f57c973 --- /dev/null +++ b/lib/puppet/module_tool/utils.rb @@ -0,0 +1,5 @@ +module Puppet::Module::Tool + module Utils + require 'puppet/module_tool/utils/interrogation' + end +end diff --git a/lib/puppet/module_tool/utils/interrogation.rb b/lib/puppet/module_tool/utils/interrogation.rb new file mode 100644 index 000000000..19450dedd --- /dev/null +++ b/lib/puppet/module_tool/utils/interrogation.rb @@ -0,0 +1,25 @@ +module Puppet::Module::Tool + module Utils + + # = Interrogation + # + # This module contains methods to emit questions to the console. + module Interrogation + def confirms?(question) + $stderr.print "#{question} [y/N]: " + $stdin.gets =~ /y/i + end + + def prompt(question, quiet = false) + $stderr.print "#{question}: " + system 'stty -echo' if quiet + $stdin.gets.strip + ensure + if quiet + system 'stty echo' + say "\n---------" + end + end + end + end +end diff --git a/lib/puppet/network/http/api/v1.rb b/lib/puppet/network/http/api/v1.rb index 388d54961..852568a56 100644 --- a/lib/puppet/network/http/api/v1.rb +++ b/lib/puppet/network/http/api/v1.rb @@ -74,7 +74,8 @@ module Puppet::Network::HTTP::API::V1 result = (indirection =~ /s$|_search$/) ? :plural : :singular - indirection.sub!(/s$|_search$|es$/, '') + indirection.sub!(/s$|_search$/, '') + indirection.sub!(/statuse$/, 'status') result end diff --git a/lib/puppet/node/environment.rb b/lib/puppet/node/environment.rb index 4fc314a6a..326809050 100644 --- a/lib/puppet/node/environment.rb +++ b/lib/puppet/node/environment.rb @@ -54,7 +54,6 @@ class Puppet::Node::Environment @root end - # This is only used for testing. def self.clear @seen.clear end @@ -88,7 +87,7 @@ class Puppet::Node::Environment end def module(name) - mod = Puppet::Module.new(name, self) + mod = Puppet::Module.new(name, :environment => self) return nil unless mod.exist? mod end @@ -107,13 +106,27 @@ class Puppet::Node::Environment module_names = modulepath.collect { |path| Dir.entries(path) }.flatten.uniq module_names.collect do |path| begin - Puppet::Module.new(path, self) + Puppet::Module.new(path, :environment => self) rescue Puppet::Module::Error => e nil end end.compact end + # Modules broken out by directory in the modulepath + def modules_by_path + modules_by_path = {} + modulepath.each do |path| + Dir.chdir(path) do + module_names = Dir.glob('*').select { |d| FileTest.directory? d } + modules_by_path[path] = module_names.map do |name| + Puppet::Module.new(name, :environment => self, :path => File.join(path, name)) + end + end + end + modules_by_path + end + def to_s name.to_s end diff --git a/lib/puppet/parser/ast/leaf.rb b/lib/puppet/parser/ast/leaf.rb index c8ebc9483..34252f7b8 100644 --- a/lib/puppet/parser/ast/leaf.rb +++ b/lib/puppet/parser/ast/leaf.rb @@ -161,7 +161,7 @@ class Puppet::Parser::AST raise Puppet::ParseError, "#{variable} is not an hash or array when accessing it with #{accesskey}" unless object.is_a?(Hash) or object.is_a?(Array) - object[array_index_or_key(object, accesskey)] + object[array_index_or_key(object, accesskey)] || :undef end # Assign value to this hashkey or array index diff --git a/lib/puppet/parser/functions/create_resources.rb b/lib/puppet/parser/functions/create_resources.rb index 646761957..3c91b4111 100644 --- a/lib/puppet/parser/functions/create_resources.rb +++ b/lib/puppet/parser/functions/create_resources.rb @@ -1,7 +1,7 @@ Puppet::Parser::Functions::newfunction(:create_resources, :doc => <<-'ENDHEREDOC') do |args| Converts a hash into a set of resources and adds them to the catalog. - This function takes two arguments: a resource type, and a hash describing + This function takes two mandatory arguments: a resource type, and a hash describing a set of resources. The hash should be in the form `{title => {parameters} }`: # A hash of user resources: @@ -14,13 +14,26 @@ Puppet::Parser::Functions::newfunction(:create_resources, :doc => <<-'ENDHEREDOC groups => ['developers', 'prosvc', 'release'], } } - create_resource(user, $myusers) + create_resources(user, $myusers) + + A third, optional parameter may be given, also as a hash: + + $defaults => { + 'ensure' => present, + 'provider' => 'ldap', + } + + create_resources(user, $myusers, $defaults) + + The values given on the third argument are added to the parameters of each resource + present in the set given on the second argument. If a parameter is present on both + the second and third arguments, the one on the second argument takes precedence. This function can be used to create defined resources and classes, as well as native resources. ENDHEREDOC - raise ArgumentError, ("create_resources(): wrong number of arguments (#{args.length}; must be 2)") if args.length != 2 - #raise ArgumentError, 'requires resource type and param hash' if args.size < 2 + raise ArgumentError, ("create_resources(): wrong number of arguments (#{args.length}; must be 2 or 3)") if args.length < 2 || args.length > 3 + # figure out what kind of resource we are type_of_resource = nil type_name = args[0].downcase @@ -36,8 +49,10 @@ Puppet::Parser::Functions::newfunction(:create_resources, :doc => <<-'ENDHEREDOC end end # iterate through the resources to create + defaults = args[2] || {} args[1].each do |title, params| raise ArgumentError, 'params should not contain title' if(params['title']) + params = defaults.merge(params) case type_of_resource # JJM The only difference between a type and a define is the call to instantiate_resource # for a defined type. @@ -54,7 +69,7 @@ Puppet::Parser::Functions::newfunction(:create_resources, :doc => <<-'ENDHEREDOC klass = find_hostclass(title) raise ArgumentError, "could not find hostclass #{title}" unless klass klass.ensure_in_catalog(self, params) - compiler.catalog.add_class([title]) + compiler.catalog.add_class(title) end end end diff --git a/lib/puppet/parser/type_loader.rb b/lib/puppet/parser/type_loader.rb index 68def068d..3cba89515 100644 --- a/lib/puppet/parser/type_loader.rb +++ b/lib/puppet/parser/type_loader.rb @@ -112,7 +112,7 @@ class Puppet::Parser::TypeLoader # behavior) only load files from the first module of a given name. E.g., # given first/foo and second/foo, only files from first/foo will be loaded. module_names.each do |name| - mod = Puppet::Module.new(name, environment) + mod = Puppet::Module.new(name, :environment => environment) Find.find(File.join(mod.path, "manifests")) do |path| if path =~ /\.pp$/ or path =~ /\.rb$/ import(path) diff --git a/lib/puppet/property.rb b/lib/puppet/property.rb index ee8f3b4c1..9d2d98b0e 100644 --- a/lib/puppet/property.rb +++ b/lib/puppet/property.rb @@ -166,22 +166,43 @@ class Puppet::Property < Puppet::Parameter raise "Puppet::Property#safe_insync? shouldn't be overridden; please override insync? instead" if sym == :safe_insync? end - # This method should be overridden by derived classes if necessary + # This method may be overridden by derived classes if necessary # to provide extra logic to determine whether the property is in - # sync. + # sync. In most cases, however, only `property_matches?` needs to be + # overridden to give the correct outcome - without reproducing all the array + # matching logic, etc, found here. def insync?(is) self.devfail "#{self.class.name}'s should is not array" unless @should.is_a?(Array) # an empty array is analogous to no should values return true if @should.empty? - # Look for a matching value - return (is == @should or is == @should.collect { |v| v.to_s }) if match_all? + # Look for a matching value, either for all the @should values, or any of + # them, depending on the configuration of this property. + if match_all? then + old = (is == @should or is == @should.collect { |v| v.to_s }) + new = Array(is).zip(@should).all? {|is, want| property_matches?(is, want) } - @should.each { |val| return true if is == val or is == val.to_s } + puts "old and new mismatch!" unless old == new + fail "old and new mismatch!" unless old == new - # otherwise, return false - false + # We need to pairwise compare the entries; this preserves the old + # behaviour while using the new pair comparison code. + return Array(is).zip(@should).all? {|is, want| property_matches?(is, want) } + else + return @should.any? {|want| property_matches?(is, want) } + end + end + + # Compare the current and desired value of a property in a property-specific + # way. Invoked by `insync?`; this should be overridden if your property + # has a different comparison type but does not actually differentiate the + # overall insync? logic. + def property_matches?(current, desired) + # This preserves the older Puppet behaviour of doing raw and string + # equality comparisons for all equality. I am not clear this is globally + # desirable, but at least it is not a breaking change. --daniel 2011-11-11 + current == desired or current == desired.to_s end # because the @should and @is vars might be in weird formats, @@ -194,13 +215,10 @@ class Puppet::Property < Puppet::Parameter # Send a log message. def log(msg) - - Puppet::Util::Log.create( - - :level => resource[:loglevel], + Puppet::Util::Log.create( + :level => resource[:loglevel], :message => msg, - - :source => self + :source => self ) end diff --git a/lib/puppet/provider.rb b/lib/puppet/provider.rb index 4456feb4e..295ae8357 100644 --- a/lib/puppet/provider.rb +++ b/lib/puppet/provider.rb @@ -204,15 +204,15 @@ class Puppet::Provider dochook(:defaults) do if @defaults.length > 0 - return " Default for " + @defaults.collect do |f, v| - "`#{f}` == `#{v}`" + return "Default for " + @defaults.collect do |f, v| + "`#{f}` == `#{[v].flatten.join(', ')}`" end.join(" and ") + "." end end dochook(:commands) do if @commands.length > 0 - return " Required binaries: " + @commands.collect do |n, c| + return "Required binaries: " + @commands.collect do |n, c| "`#{c}`" end.join(", ") + "." end @@ -220,7 +220,7 @@ class Puppet::Provider dochook(:features) do if features.length > 0 - return " Supported features: " + features.collect do |f| + return "Supported features: " + features.collect do |f| "`#{f}`" end.join(", ") + "." end diff --git a/lib/puppet/provider/augeas/augeas.rb b/lib/puppet/provider/augeas/augeas.rb index 7214a8b33..703a01bcc 100644 --- a/lib/puppet/provider/augeas/augeas.rb +++ b/lib/puppet/provider/augeas/augeas.rb @@ -17,10 +17,12 @@ require 'augeas' if Puppet.features.augeas? require 'strscan' require 'puppet/util' require 'puppet/util/diff' +require 'puppet/util/package' Puppet::Type.type(:augeas).provide(:augeas) do include Puppet::Util include Puppet::Util::Diff + include Puppet::Util::Package confine :true => Puppet.features.augeas? @@ -149,7 +151,7 @@ Puppet::Type.type(:augeas).provide(:augeas) do debug("Opening augeas with root #{root}, lens path #{load_path}, flags #{flags}") @aug = Augeas::open(root, load_path,flags) - debug("Augeas version #{get_augeas_version} is installed") if get_augeas_version >= "0.3.6" + debug("Augeas version #{get_augeas_version} is installed") if versioncmp(get_augeas_version, "0.3.6") >= 0 if resource[:incl] aug.set("/augeas/load/Xfm/lens", resource[:lens]) @@ -285,7 +287,7 @@ Puppet::Type.type(:augeas).provide(:augeas) do # If we have a verison of augeas which is at least 0.3.6 then we # can make the changes now, see if changes were made, and # actually do the save. - if return_value and get_augeas_version >= "0.3.6" + if return_value and versioncmp(get_augeas_version, "0.3.6") >= 0 debug("Will attempt to save and only run if files changed") set_augeas_save_mode(SAVE_NEWFILE) do_execute_changes @@ -325,7 +327,7 @@ Puppet::Type.type(:augeas).provide(:augeas) do begin open_augeas saved_files = @aug.match("/augeas/events/saved") - if saved_files + unless saved_files.empty? saved_files.each do |key| root = resource[:root].sub(/^\/$/, "") saved_file = @aug.get(key).to_s.sub(/^\/files/, root) @@ -337,7 +339,7 @@ Puppet::Type.type(:augeas).provide(:augeas) do end else debug("No saved files, re-executing augeas") - set_augeas_save_mode(SAVE_OVERWRITE) if get_augeas_version >= "0.3.6" + set_augeas_save_mode(SAVE_OVERWRITE) if versioncmp(get_augeas_version, "0.3.6") >= 0 do_execute_changes success = @aug.save fail("Save failed with return code #{success}") if success != true diff --git a/lib/puppet/provider/group/pw.rb b/lib/puppet/provider/group/pw.rb index a054d1ff1..b6c74f766 100644 --- a/lib/puppet/provider/group/pw.rb +++ b/lib/puppet/provider/group/pw.rb @@ -1,34 +1,48 @@ require 'puppet/provider/nameservice/pw' Puppet::Type.type(:group).provide :pw, :parent => Puppet::Provider::NameService::PW do - desc "Group management via `pw`. + desc "Group management via `pw` on FreeBSD." - Only works on FreeBSD. + commands :pw => "pw" + has_features :manages_members - " - - commands :pw => "/usr/sbin/pw" defaultfor :operatingsystem => :freebsd + options :members, :flag => "-M", :method => :mem + verify :gid, "GID must be an integer" do |value| value.is_a? Integer end def addcmd cmd = [command(:pw), "groupadd", @resource[:name]] + if gid = @resource.should(:gid) unless gid == :absent cmd << flag(:gid) << gid end end - # Apparently, contrary to the man page, groupadd does - # not accept -o. - #if @parent[:allowdupe] == :true - # cmd << "-o" - #end + if members = @resource.should(:members) + unless members == :absent + if members.is_a?(Array) + members = members.join(",") + end + cmd << "-M" << members + end + end + + cmd << "-o" if @resource.allowdupe? cmd end + + def modifycmd(param, value) + # members may be an array, need a comma separated list + if param == :members and value.is_a?(Array) + value = value.join(",") + end + super(param, value) + end end diff --git a/lib/puppet/provider/nameservice/directoryservice.rb b/lib/puppet/provider/nameservice/directoryservice.rb index 35ac8d76a..76c79f685 100644 --- a/lib/puppet/provider/nameservice/directoryservice.rb +++ b/lib/puppet/provider/nameservice/directoryservice.rb @@ -2,7 +2,7 @@ require 'puppet' require 'puppet/provider/nameservice' require 'facter/util/plist' require 'cgi' - +require 'fileutils' class Puppet::Provider::NameService class DirectoryService < Puppet::Provider::NameService @@ -21,6 +21,7 @@ class DirectoryService < Puppet::Provider::NameService commands :dscl => "/usr/bin/dscl" commands :dseditgroup => "/usr/sbin/dseditgroup" commands :sw_vers => "/usr/bin/sw_vers" + commands :plutil => '/usr/bin/plutil' confine :operatingsystem => :darwin defaultfor :operatingsystem => :darwin @@ -60,6 +61,8 @@ class DirectoryService < Puppet::Provider::NameService } @@password_hash_dir = "/var/db/shadow/hash" + @@users_plist_dir = '/var/db/dslocal/nodes/Default/users' + def self.instances # JJM Class method that provides an array of instance objects of this @@ -193,7 +196,7 @@ class DirectoryService < Puppet::Provider::NameService # 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 - attribute_hash[:password] = self.get_password(attribute_hash[:guid]) if @resource_type.validproperties.include?(:password) and Puppet.features.root? + attribute_hash[:password] = self.get_password(attribute_hash[:guid], attribute_hash[:name]) if @resource_type.validproperties.include?(:password) and Puppet.features.root? attribute_hash end @@ -268,46 +271,144 @@ class DirectoryService < Puppet::Provider::NameService 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 - fail("Could not write to password hash file: #{detail}") + # Use Puppet::Util::Package.versioncmp() to catch the scenario where a + # version '10.10' would be < '10.7' with simple string comparison. This + # if-statement only executes if the current version is less-than 10.7 + if (Puppet::Util::Package.versioncmp(get_macosx_version_major, '10.7') == -1) + password_hash_file = "#{@@password_hash_dir}/#{guid}" + begin + File.open(password_hash_file, 'w') { |f| f.write(password_hash)} + rescue Errno::EACCES => detail + fail("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 + fail("Could not set AuthenticationAuthority.") + end + else + # 10.7 uses salted SHA512 password hashes which are 128 characters plus + # an 8 character salt. Previous versions used a SHA1 hash padded with + # zeroes. If someone attempts to use a password hash that worked with + # a previous version of OX X, we will fail early and warn them. + if password_hash.length != 136 + fail("OS X 10.7 requires a Salted SHA512 hash password of 136 characters. \ + Please check your password and try again.") + end + + if File.exists?("#{@@users_plist_dir}/#{resource_name}.plist") + # If a plist already exists in /var/db/dslocal/nodes/Default/users, then + # we will need to extract the binary plist from the 'ShadowHashData' + # key, log the new password into the resultant plist's 'SALTED-SHA512' + # key, and then save the entire structure back. + users_plist = Plist::parse_xml(plutil( '-convert', 'xml1', '-o', '/dev/stdout', \ + "#{@@users_plist_dir}/#{resource_name}.plist")) + + # users_plist['ShadowHashData'][0].string is actually a binary plist + # that's nested INSIDE the user's plist (which itself is a binary + # plist). + password_hash_plist = users_plist['ShadowHashData'][0].string + converted_hash_plist = convert_binary_to_xml(password_hash_plist) + + # converted_hash_plist['SALTED-SHA512'].string expects a Base64 encoded + # string. The password_hash provided as a resource attribute is a + # hex value. We need to convert the provided hex value to a Base64 + # encoded string to nest it in the converted hash plist. + converted_hash_plist['SALTED-SHA512'].string = \ + password_hash.unpack('a2'*(password_hash.size/2)).collect { |i| i.hex.chr }.join + + # Finally, we can convert the nested plist back to binary, embed it + # into the user's plist, and convert the resultant plist back to + # a binary plist. + changed_plist = convert_xml_to_binary(converted_hash_plist) + users_plist['ShadowHashData'][0].string = changed_plist + Plist::Emit.save_plist(users_plist, "#{@@users_plist_dir}/#{resource_name}.plist") + plutil('-convert', 'binary1', "#{@@users_plist_dir}/#{resource_name}.plist") + end + end + end + + def self.get_password(guid, username) + # Use Puppet::Util::Package.versioncmp() to catch the scenario where a + # version '10.10' would be < '10.7' with simple string comparison. This + # if-statement only executes if the current version is less-than 10.7 + if (Puppet::Util::Package.versioncmp(get_macosx_version_major, '10.7') == -1) + password_hash = nil + password_hash_file = "#{@@password_hash_dir}/#{guid}" + if File.exists?(password_hash_file) and File.file?(password_hash_file) + fail("Could not read password hash file at #{password_hash_file}") if not File.readable?(password_hash_file) + f = File.new(password_hash_file) + password_hash = f.read + f.close + end + password_hash + else + if File.exists?("#{@@users_plist_dir}/#{username}.plist") + # If a plist exists in /var/db/dslocal/nodes/Default/users, we will + # extract the binary plist from the 'ShadowHashData' key, decode the + # salted-SHA512 password hash, and then return it. + users_plist = Plist::parse_xml(plutil('-convert', 'xml1', '-o', '/dev/stdout', "#{@@users_plist_dir}/#{username}.plist")) + if users_plist['ShadowHashData'] + # users_plist['ShadowHashData'][0].string is actually a binary plist + # that's nested INSIDE the user's plist (which itself is a binary + # plist). + password_hash_plist = users_plist['ShadowHashData'][0].string + converted_hash_plist = convert_binary_to_xml(password_hash_plist) + + # converted_hash_plist['SALTED-SHA512'].string is a Base64 encoded + # string. The password_hash provided as a resource attribute is a + # hex value. We need to convert the Base64 encoded string to a + # hex value and provide it back to Puppet. + password_hash = converted_hash_plist['SALTED-SHA512'].string.unpack("H*")[0] + password_hash + end + end end + 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 - fail("Could not set AuthenticationAuthority.") + # This method will accept a hash that has been returned from Plist::parse_xml + # and convert it to a binary plist (string value). + def self.convert_xml_to_binary(plist_data) + Puppet.debug('Converting XML plist to binary') + Puppet.debug('Executing: \'plutil -convert binary1 -o - -\'') + IO.popen('plutil -convert binary1 -o - -', mode='r+') do |io| + io.write plist_data.to_plist + io.close_write + @converted_plist = io.read end + @converted_plist end - def self.get_password(guid) - password_hash = nil - password_hash_file = "#{@@password_hash_dir}/#{guid}" - if File.exists?(password_hash_file) and File.file?(password_hash_file) - fail("Could not read password hash file at #{password_hash_file}") if not File.readable?(password_hash_file) - f = File.new(password_hash_file) - password_hash = f.read - f.close + # This method will accept a binary plist (as a string) and convert it to a + # hash via Plist::parse_xml. + def self.convert_binary_to_xml(plist_data) + Puppet.debug('Converting binary plist to XML') + Puppet.debug('Executing: \'plutil -convert xml1 -o - -\'') + IO.popen('plutil -convert xml1 -o - -', mode='r+') do |io| + io.write plist_data + io.close_write + @converted_plist = io.read end - password_hash + Puppet.debug('Converting XML values to a hash.') + @plist_hash = Plist::parse_xml(@converted_plist) + @plist_hash end # Unlike most other *nixes, OS X doesn't provide built in functionality @@ -468,7 +569,14 @@ class DirectoryService < Puppet::Provider::NameService begin execute(cmd) rescue Puppet::ExecutionFailure => detail - fail("Could not remove #{member} from group: #{@resource.name}, #{detail}") + # TODO: We're falling back to removing the member using dscl due to rdar://8481241 + # This bug causes dseditgroup to fail to remove a member if that member doesn't exist + cmd = [:dscl, ".", "-delete", "/Groups/#{@resource.name}", "GroupMembership", member] + begin + execute(cmd) + rescue Puppet::ExecutionFailure => detail + fail("Could not remove #{member} from group: #{@resource.name}, #{detail}") + end end end end @@ -535,3 +643,4 @@ class DirectoryService < Puppet::Provider::NameService end end end + diff --git a/lib/puppet/provider/package/pip.rb b/lib/puppet/provider/package/pip.rb index ccc389564..5f6806e6f 100644 --- a/lib/puppet/provider/package/pip.rb +++ b/lib/puppet/provider/package/pip.rb @@ -102,7 +102,7 @@ Puppet::Type.type(:package).provide :pip, self.class.commands :pip => pathname pip *args else - raise e + raise e, 'Could not locate the pip command.' end end end diff --git a/lib/puppet/provider/package/yum.rb b/lib/puppet/provider/package/yum.rb index c66c46c7e..f072779b9 100755..100644 --- a/lib/puppet/provider/package/yum.rb +++ b/lib/puppet/provider/package/yum.rb @@ -56,7 +56,6 @@ Puppet::Type.type(:package).provide :yum, :parent => :rpm, :source => :rpm do wanted = @resource[:name] operation = :install - # XXX: We don't actually deal with epochs here. case should when true, false, Symbol # pass @@ -87,7 +86,7 @@ Puppet::Type.type(:package).provide :yum, :parent => :rpm, :source => :rpm do unless upd.nil? # FIXME: there could be more than one update for a package # because of multiarch - return "#{upd[:version]}-#{upd[:release]}" + return "#{upd[:epoch]}:#{upd[:version]}-#{upd[:release]}" else # Yum didn't find updates, pretend the current # version is the latest diff --git a/lib/puppet/provider/service/debian.rb b/lib/puppet/provider/service/debian.rb index 6030da1e9..e9ee7e92c 100755 --- a/lib/puppet/provider/service/debian.rb +++ b/lib/puppet/provider/service/debian.rb @@ -42,11 +42,25 @@ Puppet::Type.type(:service).provide :debian, :parent => :init do # See x-man-page://invoke-rc.d if [104, 106].include?($CHILD_STATUS.exitstatus) return :true + elsif [105].include?($CHILD_STATUS.exitstatus) + # 105 is unknown, which generally means the the iniscript does not support query + # The debian policy states that the initscript should support methods of query + # For those that do not, peform the checks manually + # http://www.debian.org/doc/debian-policy/ch-opersys.html + if get_start_link_count >= 4 + return :true + else + return :false + end else return :false end end + def get_start_link_count + Dir.glob("/etc/rc*.d/S*#{@resource[:name]}").length + end + def enable update_rc "-f", @resource[:name], "remove" update_rc @resource[:name], "defaults" diff --git a/lib/puppet/provider/service/launchd.rb b/lib/puppet/provider/service/launchd.rb index 73d4b3c07..79dcf87be 100644 --- a/lib/puppet/provider/service/launchd.rb +++ b/lib/puppet/provider/service/launchd.rb @@ -196,7 +196,7 @@ Puppet::Type.type(:service).provide :launchd, :parent => :base do did_enable_job = false cmds = [] cmds << :launchctl << :load - if self.enabled? == :false # launchctl won't load disabled jobs + if self.enabled? == :false || self.status == :stopped # launchctl won't load disabled jobs cmds << "-w" did_enable_job = true end diff --git a/lib/puppet/provider/service/smf.rb b/lib/puppet/provider/service/smf.rb index c8cc262e9..5567aa45b 100755 --- a/lib/puppet/provider/service/smf.rb +++ b/lib/puppet/provider/service/smf.rb @@ -58,7 +58,7 @@ Puppet::Type.type(:service).provide :smf, :parent => :base do when :maintenance [command(:adm), :clear, @resource[:name]] else - [command(:adm), :enable, @resource[:name]] + [command(:adm), :enable, "-s", @resource[:name]] end end @@ -98,7 +98,7 @@ Puppet::Type.type(:service).provide :smf, :parent => :base do end def stopcmd - [command(:adm), :disable, @resource[:name]] + [command(:adm), :disable, "-s", @resource[:name]] end end diff --git a/lib/puppet/provider/user/pw.rb b/lib/puppet/provider/user/pw.rb index a5988cad1..842397971 100644 --- a/lib/puppet/provider/user/pw.rb +++ b/lib/puppet/provider/user/pw.rb @@ -1,16 +1,18 @@ require 'puppet/provider/nameservice/pw' +require 'open3' Puppet::Type.type(:user).provide :pw, :parent => Puppet::Provider::NameService::PW do desc "User management via `pw` on FreeBSD." commands :pw => "pw" - has_features :manages_homedir, :allows_duplicates + has_features :manages_homedir, :allows_duplicates, :manages_passwords, :manages_expiry defaultfor :operatingsystem => :freebsd options :home, :flag => "-d", :method => :dir options :comment, :method => :gecos options :groups, :flag => "-G" + options :expiry, :method => :expire verify :gid, "GID must be an integer" do |value| value.is_a? Integer @@ -23,10 +25,14 @@ Puppet::Type.type(:user).provide :pw, :parent => Puppet::Provider::NameService:: def addcmd cmd = [command(:pw), "useradd", @resource[:name]] @resource.class.validproperties.each do |property| - next if property == :ensure + next if property == :ensure or property == :password # the value needs to be quoted, mostly because -c might # have spaces in it if value = @resource.should(property) and value != "" + if property == :expiry + # FreeBSD uses DD-MM-YYYY rather than YYYY-MM-DD + value = value.split("-").reverse.join("-") + end cmd << flag(property) << value end end @@ -37,5 +43,53 @@ Puppet::Type.type(:user).provide :pw, :parent => Puppet::Provider::NameService:: cmd end + + def modifycmd(param, value) + if param == :expiry + # FreeBSD uses DD-MM-YYYY rather than YYYY-MM-DD + value = value.split("-").reverse.join("-") + end + cmd = super(param, value) + cmd << "-m" if @resource.managehome? + cmd + end + + def create + super + + # Set the password after create if given + self.password = @resource[:password] if @resource[:password] + end + + # use pw to update password hash + def password=(cryptopw) + Puppet.debug "change password for user '#{@resource[:name]}' method called with hash '#{cryptopw}'" + stdin, stdout, stderr = Open3.popen3("pw user mod #{@resource[:name]} -H 0") + stdin.puts(cryptopw) + stdin.close + Puppet.debug "finished password for user '#{@resource[:name]}' method called with hash '#{cryptopw}'" + end + + # get password from /etc/master.passwd + def password + Puppet.debug "checking password for user '#{@resource[:name]}' method called" + current_passline = `getent passwd #{@resource[:name]}` + current_password = current_passline.chomp.split(':')[1] if current_passline + Puppet.debug "finished password for user '#{@resource[:name]}' method called : '#{current_password}'" + current_password + end + + # Get expiry from system and convert to Puppet-style date + def expiry + expiry = self.get(:expiry) + expiry = :absent if expiry == 0 + + if expiry != :absent + t = Time.at(expiry) + expiry = "%4d-%02d-%02d" % [t.year, t.month, t.mday] + end + + expiry + end end diff --git a/lib/puppet/provider/user/windows_adsi.rb b/lib/puppet/provider/user/windows_adsi.rb index 6b0a9bce7..045a84bdb 100644 --- a/lib/puppet/provider/user/windows_adsi.rb +++ b/lib/puppet/provider/user/windows_adsi.rb @@ -22,6 +22,7 @@ Puppet::Type.type(:user).provide :windows_adsi do def create @user = Puppet::Util::ADSI::User.create(@resource[:name]) + @user.password = @resource[:password] @user.commit [:comment, :home, :groups].each do |prop| diff --git a/lib/puppet/rails.rb b/lib/puppet/rails.rb index f74e63f20..c52b6871b 100644 --- a/lib/puppet/rails.rb +++ b/lib/puppet/rails.rb @@ -47,7 +47,7 @@ module Puppet::Rails case adapter when "sqlite3" args[:database] = Puppet[:dblocation] - when "mysql", "postgresql" + when "mysql", "mysql2", "postgresql" args[:host] = Puppet[:dbserver] unless Puppet[:dbserver].to_s.empty? args[:port] = Puppet[:dbport] unless Puppet[:dbport].to_s.empty? args[:username] = Puppet[:dbuser] unless Puppet[:dbuser].to_s.empty? diff --git a/lib/puppet/rails/database/schema.rb b/lib/puppet/rails/database/schema.rb index 7b75f4216..3eb2589f1 100644 --- a/lib/puppet/rails/database/schema.rb +++ b/lib/puppet/rails/database/schema.rb @@ -22,7 +22,7 @@ class Puppet::Rails::Schema # Thanks, mysql! MySQL requires a length on indexes in text fields. # So, we provide them for mysql and handle everything else specially. # Oracle doesn't index on CLOB fields, so we skip it - if Puppet[:dbadapter] == "mysql" + if ['mysql','mysql2'].include? Puppet[:dbadapter] execute "CREATE INDEX typentitle ON resources (restype,title(50));" elsif Puppet[:dbadapter] != "oracle_enhanced" add_index :resources, [:title, :restype] diff --git a/lib/puppet/reports/store.rb b/lib/puppet/reports/store.rb index 997206ec4..cd188fafd 100644 --- a/lib/puppet/reports/store.rb +++ b/lib/puppet/reports/store.rb @@ -1,4 +1,6 @@ require 'puppet' +require 'fileutils' +require 'tempfile' Puppet::Reports.register_report(:store) do desc "Store the yaml report on disk. Each host sends its report as a YAML dump @@ -29,10 +31,15 @@ Puppet::Reports.register_report(:store) do file = File.join(dir, name) + f = Tempfile.new(name, dir) begin - File.open(file, "w", 0640) do |f| + begin + f.chmod(0640) f.print to_yaml + ensure + f.close end + FileUtils.mv(f.path, file) rescue => detail puts detail.backtrace if Puppet[:trace] Puppet.warning "Could not write report for #{client} at #{file}: #{detail}" diff --git a/lib/puppet/resource/catalog.rb b/lib/puppet/resource/catalog.rb index 8dc0727a9..fc341094e 100644 --- a/lib/puppet/resource/catalog.rb +++ b/lib/puppet/resource/catalog.rb @@ -547,7 +547,11 @@ class Puppet::Resource::Catalog < Puppet::SimpleGraph ::File.open(Puppet[:resourcefile], "w") do |f| to_print = resources.map do |resource| next unless resource.managed? - "#{resource.type}[#{resource[resource.name_var]}]" + if resource.name_var + "#{resource.type}[#{resource[resource.name_var]}]" + else + "#{resource.ref.downcase}" + end end.compact f.puts to_print.join("\n") end diff --git a/lib/puppet/simple_graph.rb b/lib/puppet/simple_graph.rb index 671eef150..6d9365f7e 100644 --- a/lib/puppet/simple_graph.rb +++ b/lib/puppet/simple_graph.rb @@ -136,18 +136,7 @@ class Puppet::SimpleGraph s[:seen][top] = false this_scc << top end until top == vertex - # NOTE: if we don't reverse we get the components in the opposite - # order to what a human being would expect; reverse should be an - # O(1) operation, without even copying, because we know the length - # of the source, but I worry that an implementation will get this - # wrong. Still, the worst case is O(n) for n vertices as we can't - # possibly put a vertex into two SCCs. - # - # Also, my feeling is that most implementations are going to do - # better with a reverse operation than a string of 'unshift' - # insertions at the head of the array; if they were going to mess - # up the performance of one, it would be unshift. - s[:scc] << this_scc.reverse + s[:scc] << this_scc end recur.pop # done with this node, finally. end @@ -181,7 +170,17 @@ class Puppet::SimpleGraph end end - state[:scc].select { |c| c.length > 1 } + # To provide consistent results to the user, given that a hash is never + # assured to return the same order, and given our graph processing is + # based on hash tables, we need to sort the cycles internally, as well as + # the set of cycles. + # + # Given we are in a failure state here, any extra cost is more or less + # irrelevant compared to the cost of a fix - which is on a human + # time-scale. + state[:scc].select { |c| c.length > 1 }.map do |x| + x.sort_by {|a| a.to_s } + end.sort end # Perform a BFS on the sub graph representing the cycle, with a view to diff --git a/lib/puppet/ssl/host.rb b/lib/puppet/ssl/host.rb index d70fe32c7..a4baf5b7c 100644 --- a/lib/puppet/ssl/host.rb +++ b/lib/puppet/ssl/host.rb @@ -199,18 +199,26 @@ class Puppet::SSL::Host return nil unless Certificate.indirection.find("ca") unless ca? return nil unless @certificate = Certificate.indirection.find(name) - unless certificate_matches_key? - raise Puppet::Error, "Retrieved certificate does not match private key; please remove certificate from server and regenerate it with the current key" - end + validate_certificate_with_key end @certificate end - def certificate_matches_key? - return false unless key - return false unless certificate - - certificate.content.check_private_key(key.content) + def validate_certificate_with_key + raise Puppet::Error, "No certificate to validate." unless certificate + raise Puppet::Error, "No private key with which to validate certificate with fingerprint: #{certificate.fingerprint}" unless key + unless certificate.content.check_private_key(key.content) + raise Puppet::Error, <<ERROR_STRING +The certificate retrieved from the master does not match the agent's private key. +Certificate fingerprint: #{certificate.fingerprint} +To fix this, remove the certificate from both the master and the agent and then start a puppet run, which will automatically regenerate a certficate. +On the master: + puppet cert clean #{Puppet[:certname]} +On the agent: + rm -f #{Puppet[:hostcert]} + puppet agent -t +ERROR_STRING + end end # Generate all necessary parts of our ssl host. diff --git a/lib/puppet/transaction.rb b/lib/puppet/transaction.rb index e4bc0f299..20f7108a8 100644 --- a/lib/puppet/transaction.rb +++ b/lib/puppet/transaction.rb @@ -100,6 +100,7 @@ class Puppet::Transaction if resource.is_a?(Puppet::Type::Component) Puppet.warning "Somehow left a component in the relationship graph" else + resource.info "Starting to evaluate the resource" if Puppet[:evaltrace] and @catalog.host_config? seconds = thinmark { eval_resource(resource) } resource.info "Evaluated in %0.2f seconds" % seconds if Puppet[:evaltrace] and @catalog.host_config? end @@ -317,10 +318,6 @@ class Puppet::Transaction @blockers = {} @unguessable_deterministic_key = Hash.new { |h,k| h[k] = Digest::SHA1.hexdigest("NaCl, MgSO4 (salts) and then #{k.ref}") } @providerless_types = [] - vertices.each do |v| - blockers[v] = direct_dependencies_of(v).length - enqueue(v) if blockers[v] == 0 - end end def method_missing(*args,&block) real_graph.send(*args,&block) @@ -335,6 +332,13 @@ class Puppet::Transaction real_graph.add_edge(f,t,label) end + # Enqueue the initial set of resources, those with no dependencies. + def enqueue_roots + vertices.each do |v| + blockers[v] = direct_dependencies_of(v).length + enqueue(v) if blockers[v] == 0 + end + end # Decrement the blocker count for the resource by 1. If the number of # blockers is unknown, count them and THEN decrement by 1. def unblock(resource) @@ -364,6 +368,8 @@ class Puppet::Transaction def traverse(&block) real_graph.report_cycles_in_graph + enqueue_roots + deferred_resources = [] while (resource = next_resource) && !transaction.stop_processing? diff --git a/lib/puppet/transaction/report.rb b/lib/puppet/transaction/report.rb index 807163961..b66183117 100644 --- a/lib/puppet/transaction/report.rb +++ b/lib/puppet/transaction/report.rb @@ -115,7 +115,7 @@ class Puppet::Transaction::Report # Provide a raw hash summary of this report. def raw_summary - report = {} + report = { "version" => { "config" => configuration_version, "puppet" => Puppet.version } } @metrics.each do |name, metric| key = metric.name.to_s @@ -151,7 +151,7 @@ class Puppet::Transaction::Report def calculate_event_metrics metrics = Hash.new(0) - metrics["total"] = 0 + %w{total failure success}.each { |m| metrics[m] = 0 } resource_statuses.each do |name, status| metrics["total"] += status.events.length status.events.each do |event| @@ -163,9 +163,15 @@ class Puppet::Transaction::Report end def calculate_resource_metrics - metrics = Hash.new(0) + metrics = {} metrics["total"] = resource_statuses.length + # force every resource key in the report to be present + # even if no resources is in this given state + Puppet::Resource::Status::STATES.each do |state| + metrics[state.to_s] = 0 + end + resource_statuses.each do |name, status| Puppet::Resource::Status::STATES.each do |state| metrics[state.to_s] += 1 if status.send(state) diff --git a/lib/puppet/type.rb b/lib/puppet/type.rb index 94f26cfe5..f1c891254 100644 --- a/lib/puppet/type.rb +++ b/lib/puppet/type.rb @@ -107,11 +107,9 @@ class Type 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) + [:exists?, :create, :destroy].all? { |method| + self.public_method_defined?(method) } - - ens end def self.apply_to_device @@ -1500,11 +1498,14 @@ class Type # We need to add documentation for each provider. def self.doc - @doc + " Available providers are:\n\n" + parenttype.providers.sort { |a,b| + # Since we're mixing @doc with text from other sources, we must normalize + # its indentation with scrub. But we don't need to manually scrub the + # provider's doc string, since markdown_definitionlist sanitizes its inputs. + scrub(@doc) + "Available providers are:\n\n" + parenttype.providers.sort { |a,b| a.to_s <=> b.to_s }.collect { |i| - "* **#{i}**: #{parenttype().provider(i).doc}" - }.join("\n") + markdown_definitionlist( i, scrub(parenttype().provider(i).doc) ) + }.join end defaultto { diff --git a/lib/puppet/type/exec.rb b/lib/puppet/type/exec.rb index ec497630d..8525e8689 100755 --- a/lib/puppet/type/exec.rb +++ b/lib/puppet/type/exec.rb @@ -170,7 +170,7 @@ module Puppet desc "The group to run the command as. This seems to work quite haphazardly on different platforms -- it is a platform issue not a Ruby or Puppet one, since the same variety exists when - running commnands as different users in the shell." + running commands as different users in the shell." # Validation is handled by the SUIDManager class. end diff --git a/lib/puppet/type/file.rb b/lib/puppet/type/file.rb index 20c774abf..a7608c9e8 100644 --- a/lib/puppet/type/file.rb +++ b/lib/puppet/type/file.rb @@ -9,11 +9,14 @@ require 'puppet/network/handler' require 'puppet/util/diff' require 'puppet/util/checksums' require 'puppet/util/backups' +require 'puppet/util/symbolic_file_mode' Puppet::Type.newtype(:file) do include Puppet::Util::MethodHelper include Puppet::Util::Checksums include Puppet::Util::Backups + include Puppet::Util::SymbolicFileMode + @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 @@ -261,13 +264,18 @@ Puppet::Type.newtype(:file) do # Autorequire the nearest ancestor directory found in the catalog. autorequire(:file) do + req = [] path = Pathname.new(self[:path]) if !path.root? # Start at our parent, to avoid autorequiring ourself parents = path.parent.enum_for(:ascend) - found = parents.find { |p| catalog.resource(:file, p.to_s) } - found and found.to_s + if found = parents.find { |p| catalog.resource(:file, p.to_s) } + req << found.to_s + end end + # if the resource is a link, make sure the target is created first + req << self[:target] if self[:target] + req end # Autorequire the owner and group of the file. @@ -729,7 +737,7 @@ Puppet::Type.newtype(:file) do mode = self.should(:mode) # might be nil umask = mode ? 000 : 022 - mode_int = mode ? mode.to_i(8) : nil + mode_int = mode ? symbolic_mode_to_int(mode, 0644) : nil content_checksum = Puppet::Util.withumask(umask) { ::File.open(path, 'wb', mode_int ) { |f| write_content(f) } } diff --git a/lib/puppet/type/file/ctime.rb b/lib/puppet/type/file/ctime.rb index 90d95da64..5f94863b4 100644 --- a/lib/puppet/type/file/ctime.rb +++ b/lib/puppet/type/file/ctime.rb @@ -10,7 +10,7 @@ module Puppet current_value end - validate do + validate do |val| fail "ctime is read-only" end end diff --git a/lib/puppet/type/file/ensure.rb b/lib/puppet/type/file/ensure.rb index a846856c8..b7614f3fb 100755 --- a/lib/puppet/type/file/ensure.rb +++ b/lib/puppet/type/file/ensure.rb @@ -1,6 +1,10 @@ + module Puppet Puppet::Type.type(:file).ensurable do require 'etc' + require 'puppet/util/symbolic_file_mode' + include Puppet::Util::SymbolicFileMode + desc <<-EOT Whether to create files that don't currently exist. Possible values are *absent*, *present*, *file*, and *directory*. @@ -63,7 +67,7 @@ module Puppet end if mode Puppet::Util.withumask(000) do - Dir.mkdir(@resource[:path], mode.to_i(8)) + Dir.mkdir(@resource[:path], symbolic_mode_to_int(mode, 755, true)) end else Dir.mkdir(@resource[:path]) diff --git a/lib/puppet/type/file/mode.rb b/lib/puppet/type/file/mode.rb index b246652a0..8c7020ba4 100755 --- a/lib/puppet/type/file/mode.rb +++ b/lib/puppet/type/file/mode.rb @@ -3,6 +3,9 @@ # specifying the full mode. module Puppet Puppet::Type.type(:file).newproperty(:mode) do + require 'puppet/util/symbolic_file_mode' + include Puppet::Util::SymbolicFileMode + desc "Mode the file should be. Currently relatively limited: you must specify the exact mode the file should be. @@ -23,23 +26,32 @@ module Puppet mode 644, and all of the directories will have mode 755." validate do |value| - if value.is_a?(String) and value !~ /^[0-7]+$/ - raise Puppet::Error, "File modes can only be octal numbers, not #{should.inspect}" + unless value.nil? or valid_symbolic_mode?(value) + raise Puppet::Error, "The file mode specification is invalid: #{value.inspect}" end end - munge do |should| - if should.is_a?(String) - should.to_i(8).to_s(8) - else - should.to_s(8) + munge do |value| + return nil if value.nil? + + unless valid_symbolic_mode?(value) + raise Puppet::Error, "The file mode specification is invalid: #{value.inspect}" end + + normalize_symbolic_mode(value) + end + + def desired_mode_from_current(desired, current) + current = current.to_i(8) if current.is_a? String + is_a_directory = @resource.stat and @resource.stat.directory? + symbolic_mode_to_int(desired, current, is_a_directory) end # If we're a directory, we need to be executable for all cases # that are readable. This should probably be selectable, but eh. def dirmask(value) - if FileTest.directory?(resource[:path]) + orig = value + if FileTest.directory?(resource[:path]) and value =~ /^\d+$/ then value = value.to_i(8) value |= 0100 if value & 0400 != 0 value |= 010 if value & 040 != 0 @@ -61,6 +73,13 @@ module Puppet end end + def property_matches?(current, desired) + return false unless current + current_bits = normalize_symbolic_mode(current) + desired_bits = desired_mode_from_current(desired, current).to_s(8) + current_bits == desired_bits + end + # Ideally, dirmask'ing could be done at munge time, but we don't know if 'ensure' # will eventually be a directory or something else. And unfortunately, that logic # depends on the ensure, source, and target properties. So rather than duplicate @@ -74,12 +93,28 @@ module Puppet super end + # Finally, when we sync the mode out we need to transform it; since we + # don't have access to the calculated "desired" value here, or the + # "current" value, only the "should" value we need to retrieve again. + def sync + current = @resource.stat ? @resource.stat.mode : 0644 + set(desired_mode_from_current(@should[0], current).to_s(8)) + end + + def change_to_s(old_value, desired) + return super if desired =~ /^\d+$/ + + old_bits = normalize_symbolic_mode(old_value) + new_bits = normalize_symbolic_mode(desired_mode_from_current(desired, old_bits)) + super(old_bits, new_bits) + " (#{desired})" + end + def should_to_s(should_value) - should_value.rjust(4,"0") + should_value.rjust(4, "0") end def is_to_s(currentvalue) - currentvalue.rjust(4,"0") + currentvalue.rjust(4, "0") end end end diff --git a/lib/puppet/type/file/mtime.rb b/lib/puppet/type/file/mtime.rb index 5952b4b84..10867ddf4 100644 --- a/lib/puppet/type/file/mtime.rb +++ b/lib/puppet/type/file/mtime.rb @@ -10,7 +10,7 @@ module Puppet current_value end - validate do + validate do |val| fail "mtime is read-only" end end diff --git a/lib/puppet/type/file/source.rb b/lib/puppet/type/file/source.rb index 9375550a9..5d4fb9b85 100755 --- a/lib/puppet/type/file/source.rb +++ b/lib/puppet/type/file/source.rb @@ -132,6 +132,10 @@ module Puppet next if metadata_method == :checksum and metadata.ftype == "directory" next if metadata_method == :checksum and metadata.ftype == "link" and metadata.links == :manage + if Puppet.features.microsoft_windows? + next if [:owner, :group].include?(metadata_method) and !local? + end + if resource[param_name].nil? or resource[param_name] == :absent resource[param_name] = metadata.send(metadata_method) end diff --git a/lib/puppet/type/file/type.rb b/lib/puppet/type/file/type.rb index 864d3b1a4..38f301573 100755 --- a/lib/puppet/type/file/type.rb +++ b/lib/puppet/type/file/type.rb @@ -11,7 +11,7 @@ module Puppet current_value end - validate do + validate do |val| fail "type is read-only" end end diff --git a/lib/puppet/type/host.rb b/lib/puppet/type/host.rb index 8f6aa9ad3..f4ced3170 100755 --- a/lib/puppet/type/host.rb +++ b/lib/puppet/type/host.rb @@ -7,12 +7,26 @@ module Puppet 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" + + def valid_v4?(addr) + if /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/ =~ addr + return $~.captures.all? {|i| i = i.to_i; i >= 0 and i <= 255 } end + return false end + def valid_v6?(addr) + # http://forums.dartware.com/viewtopic.php?t=452 + # ...and, yes, it is this hard. Doing it programatically is harder. + return true if addr =~ /^\s*((([0-9A-Fa-f]{1,4}:){7}([0-9A-Fa-f]{1,4}|:))|(([0-9A-Fa-f]{1,4}:){6}(:[0-9A-Fa-f]{1,4}|((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){5}(((:[0-9A-Fa-f]{1,4}){1,2})|:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){4}(((:[0-9A-Fa-f]{1,4}){1,3})|((:[0-9A-Fa-f]{1,4})?:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){3}(((:[0-9A-Fa-f]{1,4}){1,4})|((:[0-9A-Fa-f]{1,4}){0,2}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){2}(((:[0-9A-Fa-f]{1,4}){1,5})|((:[0-9A-Fa-f]{1,4}){0,3}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){1}(((:[0-9A-Fa-f]{1,4}){1,6})|((:[0-9A-Fa-f]{1,4}){0,4}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(:(((:[0-9A-Fa-f]{1,4}){1,7})|((:[0-9A-Fa-f]{1,4}){0,5}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:)))(%.+)?\s*$/ + + return false + end + + validate do |value| + return true if valid_v4?(value) or valid_v6?(value) + raise Puppet::Error, "Invalid IP address #{value.inspect}" + end end # for now we use OrderedList to indicate that the order does matter. diff --git a/lib/puppet/type/schedule.rb b/lib/puppet/type/schedule.rb index 2baf726c7..3193c74e3 100755 --- a/lib/puppet/type/schedule.rb +++ b/lib/puppet/type/schedule.rb @@ -81,7 +81,8 @@ module Puppet } This is mostly useful for restricting certain resources to being - applied in maintenance windows or during off-peak hours. + applied in maintenance windows or during off-peak hours. Multiple + ranges can be applied in array context. EOT # This is lame; properties all use arrays as values, but parameters don't. @@ -178,7 +179,7 @@ module Puppet #self.info limits.inspect #self.notice now - return now.between?(*limits) + return true if now.between?(*limits) end # Else, return false, since our current time isn't between diff --git a/lib/puppet/util/anonymous_filelock.rb b/lib/puppet/util/anonymous_filelock.rb new file mode 100644 index 000000000..ff09c5d12 --- /dev/null +++ b/lib/puppet/util/anonymous_filelock.rb @@ -0,0 +1,36 @@ + +class Puppet::Util::AnonymousFilelock + attr_reader :lockfile + + def initialize(lockfile) + @lockfile = lockfile + end + + def anonymous? + true + end + + def lock(msg = '') + return false if locked? + + File.open(@lockfile, 'w') { |fd| fd.print(msg) } + true + end + + def unlock + if locked? + File.unlink(@lockfile) + true + else + false + end + end + + def locked? + File.exists? @lockfile + end + + def message + return File.read(@lockfile) if locked? + end +end
\ No newline at end of file diff --git a/lib/puppet/util/docs.rb b/lib/puppet/util/docs.rb index 4344d67ab..0a7c9bce8 100644 --- a/lib/puppet/util/docs.rb +++ b/lib/puppet/util/docs.rb @@ -20,10 +20,10 @@ module Puppet::Util::Docs def doc extra = methods.find_all { |m| m.to_s =~ /^dochook_.+/ }.sort.collect { |m| self.send(m) - }.join(" ") + }.delete_if {|r| r.nil? }.join(" ") if @doc - @doc + extra + @doc + (extra.empty? ? '' : "\n\n" + extra) else extra end @@ -73,6 +73,22 @@ module Puppet::Util::Docs value.to_s + (" " * (length - value.to_s.length)) end + HEADER_LEVELS = [nil, "#", "##", "###", "####", "#####"] + + def markdown_header(name, level) + "#{HEADER_LEVELS[level]} #{name}\n\n" + end + + def markdown_definitionlist(term, definition) + lines = scrub(definition).split("\n") + str = "#{term}\n: #{lines.shift}\n" + lines.each do |line| + str << " " if line =~ /\S/ + str << "#{line}\n" + end + str << "\n" + end + # Handle the inline indentation in the docs. def scrub(text) # Stupid markdown diff --git a/lib/puppet/util/instrumentation.rb b/lib/puppet/util/instrumentation.rb new file mode 100644 index 000000000..bd0ed3ba5 --- /dev/null +++ b/lib/puppet/util/instrumentation.rb @@ -0,0 +1,173 @@ +require 'puppet' +require 'puppet/util/classgen' +require 'puppet/util/instance_loader' + +class Puppet::Util::Instrumentation + extend Puppet::Util::ClassGen + extend Puppet::Util::InstanceLoader + extend MonitorMixin + + # we're using a ruby lazy autoloader to prevent a loop when requiring listeners + # since this class sets up an indirection which is also used in Puppet::Indirector::Indirection + # which is used to setup indirections... + autoload :Listener, 'puppet/util/instrumentation/listener' + autoload :Data, 'puppet/util/instrumentation/data' + + # Set up autoloading and retrieving of instrumentation listeners. + instance_load :listener, 'puppet/util/instrumentation/listeners' + + class << self + attr_accessor :listeners, :listeners_of + end + + # instrumentation layer + + # Triggers an instrumentation + # + # Call this method around the instrumentation point + # Puppet::Util::Instrumentation.instrument(:my_long_computation) do + # ... a long computation + # end + # + # This will send an event to all the listeners of "my_long_computation". + # Note: this method uses ruby yield directive to call the instrumented code. + # It is usually way slower than calling start and stop directly around the instrumented code. + # For high traffic code path, it is thus advisable to not use this method. + def self.instrument(label, data = {}) + id = self.start(label, data) + yield + ensure + self.stop(label, id, data) + end + + # Triggers a "start" instrumentation event + # + # Important note: + # For proper use, the data hash instance used for start should also + # be used when calling stop. The idea is to use the current scope + # where start is called to retain a reference to 'data' so that it is possible + # to send it back to stop. + # This way listeners can match start and stop events more easily. + def self.start(label, data) + data[:started] = Time.now + publish(label, :start, data) + data[:id] = next_id + end + + # Triggers a "stop" instrumentation event + def self.stop(label, id, data) + data[:finished] = Time.now + publish(label, :stop, data) + end + + def self.publish(label, event, data) + each_listener(label) do |k,l| + l.notify(label, event, data) + end + end + + def self.listeners + @listeners.values + end + + def self.each_listener(label) + synchronize { + @listeners_of[label] ||= @listeners.select do |k,l| + l.listen_to?(label) + end + }.each do |l| + yield l + end + end + + # Adds a new listener + # + # Usage: + # Puppet::Util::Instrumentation.new_listener(:my_instrumentation, pattern) do + # + # def notify(label, data) + # ... do something for data... + # end + # end + # + # It is possible to use a "pattern". The listener will be notified only + # if the pattern match the label of the event. + # The pattern can be a symbol, a string or a regex. + # If no pattern is provided, then the listener will be called for every events + def self.new_listener(name, options = {}, &block) + Puppet.debug "new listener called #{name}" + name = symbolize(name) + listener = genclass(name, :hash => instance_hash(:listener), :block => block) + listener.send(:define_method, :name) do + name + end + subscribe(listener.new, options[:label_pattern], options[:event]) + end + + def self.subscribe(listener, label_pattern, event) + synchronize { + raise "Listener #{listener.name} is already subscribed" if @listeners.include?(listener.name) + Puppet.debug "registering instrumentation listener #{listener.name}" + @listeners[listener.name] = Listener.new(listener, label_pattern, event) + listener.subscribed if listener.respond_to?(:subscribed) + rehash + } + end + + def self.unsubscribe(listener) + synchronize { + Puppet.warning("#{listener.name} hasn't been registered but asked to be unregistered") unless @listeners.include?(listener.name) + Puppet.info "unregistering instrumentation listener #{listener.name}" + @listeners.delete(listener.name) + listener.unsubscribed if listener.respond_to?(:unsubscribed) + rehash + } + end + + def self.init + # let's init our probe indirection + require 'puppet/util/instrumentation/indirection_probe' + synchronize { + @listeners ||= {} + @listeners_of ||= {} + instance_loader(:listener).loadall + } + end + + def self.clear + synchronize { + @listeners = {} + @listeners_of = {} + @id = 0 + } + end + + def self.[](key) + synchronize { + key = symbolize(key) + @listeners[key] + } + end + + def self.[]=(key, value) + synchronize { + key = symbolize(key) + @listeners[key] = value + rehash + } + end + + private + + # should be called only under the guard + # self.synchronize + def self.rehash + @listeners_of = {} + end + + def self.next_id + synchronize { + @id = (@id || 0) + 1 + } + end +end diff --git a/lib/puppet/util/instrumentation/data.rb b/lib/puppet/util/instrumentation/data.rb new file mode 100644 index 000000000..9157f58fc --- /dev/null +++ b/lib/puppet/util/instrumentation/data.rb @@ -0,0 +1,34 @@ +require 'puppet/indirector' +require 'puppet/util/instrumentation' + +# This is just a transport class to be used through the instrumentation_data +# indirection. All the data resides in the real underlying listeners which this +# class delegates to. +class Puppet::Util::Instrumentation::Data + extend Puppet::Indirector + + indirects :instrumentation_data, :terminus_class => :local + + attr_reader :listener + + def initialize(listener_name) + @listener = Puppet::Util::Instrumentation[listener_name] + raise "Listener #{listener_name} wasn't registered" unless @listener + end + + def name + @listener.name + end + + def to_pson(*args) + result = { + 'document_type' => "Puppet::Util::Instrumentation::Data", + 'data' => { :name => name }.merge(@listener.respond_to?(:data) ? @listener.data : {}) + } + result.to_pson(*args) + end + + def self.from_pson(data) + data + end +end diff --git a/lib/puppet/util/instrumentation/indirection_probe.rb b/lib/puppet/util/instrumentation/indirection_probe.rb new file mode 100644 index 000000000..66e5f92ab --- /dev/null +++ b/lib/puppet/util/instrumentation/indirection_probe.rb @@ -0,0 +1,29 @@ +require 'puppet/indirector' +require 'puppet/util/instrumentation' + +# We need to use a class other than Probe for the indirector because +# the Indirection class might declare some probes, and this would be a huge unbreakable +# dependency cycle. +class Puppet::Util::Instrumentation::IndirectionProbe + extend Puppet::Indirector + + indirects :instrumentation_probe, :terminus_class => :local + + attr_reader :probe_name + + def initialize(probe_name) + @probe_name = probe_name + end + + def to_pson(*args) + result = { + :document_type => "Puppet::Util::Instrumentation::IndirectionProbe", + :data => { :name => probe_name } + } + result.to_pson(*args) + end + + def self.from_pson(data) + self.new(data["name"]) + end +end
\ No newline at end of file diff --git a/lib/puppet/util/instrumentation/instrumentable.rb b/lib/puppet/util/instrumentation/instrumentable.rb new file mode 100644 index 000000000..5789dcbe2 --- /dev/null +++ b/lib/puppet/util/instrumentation/instrumentable.rb @@ -0,0 +1,143 @@ +require 'monitor' +require 'puppet/util/instrumentation' + +# This is the central point of all declared probes. +# Every class needed to declare probes should include this module +# and declare the methods that are subject to instrumentation: +# +# class MyClass +# extend Puppet::Util::Instrumentation::Instrumentable +# +# probe :mymethod +# +# def mymethod +# ... this is code to be instrumented ... +# end +# end +module Puppet::Util::Instrumentation::Instrumentable + INSTRUMENTED_CLASSES = {}.extend(MonitorMixin) + + attr_reader :probes + + class Probe + attr_reader :klass, :method, :label, :data + + def initialize(method, klass, options = {}) + @method = method + @klass = klass + + @label = options[:label] || method + @data = options[:data] || {} + end + + def enable + raise "Probe already enabled" if enabled? + + # We're forced to perform this copy because in the class_eval'uated + # block below @method would be evaluated in the class context. It's better + # to close on locally-scoped variables than to resort to complex namespacing + # to get access to the probe instance variables. + method = @method; label = @label; data = @data + klass.class_eval { + alias_method("instrumented_#{method}", method) + define_method(method) do |*args| + id = nil + instrumentation_data = nil + begin + instrumentation_label = label.respond_to?(:call) ? label.call(self, args) : label + instrumentation_data = data.respond_to?(:call) ? data.call(self, args) : data + id = Puppet::Util::Instrumentation.start(instrumentation_label, instrumentation_data) + send("instrumented_#{method}".to_sym, *args) + ensure + Puppet::Util::Instrumentation.stop(instrumentation_label, id, instrumentation_data || {}) + end + end + } + @enabled = true + end + + def disable + raise "Probe is not enabled" unless enabled? + + # For the same reason as in #enable, we're forced to do a local + # copy + method = @method + klass.class_eval do + alias_method(method, "instrumented_#{method}") + remove_method("instrumented_#{method}".to_sym) + end + @enabled = false + end + + def enabled? + !!@enabled + end + end + + # Declares a new probe + # + # It is possible to pass several options that will be later on evaluated + # and sent to the instrumentation layer. + # + # label:: + # this can either be a static symbol/string or a block. If it's a block + # this one will be evaluated on every call of the instrumented method and + # should return a string or a symbol + # + # data:: + # this can be a hash or a block. If it's a block this one will be evaluated + # on every call of the instrumented method and should return a hash. + # + #Example: + # + # class MyClass + # extend Instrumentable + # + # probe :mymethod, :data => Proc.new { |args| { :data => args[1] } }, :label => Proc.new { |args| args[0] } + # + # def mymethod(name, options) + # end + # + # end + # + def probe(method, options = {}) + INSTRUMENTED_CLASSES.synchronize { + (@probes ||= []) << Probe.new(method, self, options) + INSTRUMENTED_CLASSES[self] = @probes + } + end + + def self.probes + @probes + end + + def self.probe_names + probe_names = [] + each_probe { |probe| probe_names << "#{probe.klass}.#{probe.method}" } + probe_names + end + + def self.enable_probes + each_probe { |probe| probe.enable } + end + + def self.disable_probes + each_probe { |probe| probe.disable } + end + + def self.clear_probes + INSTRUMENTED_CLASSES.synchronize { + INSTRUMENTED_CLASSES.clear + } + nil # do not leak our probes to the exterior world + end + + def self.each_probe + INSTRUMENTED_CLASSES.synchronize { + INSTRUMENTED_CLASSES.each_key do |klass| + klass.probes.each { |probe| yield probe } + end + } + nil # do not leak our probes to the exterior world + end +end
\ No newline at end of file diff --git a/lib/puppet/util/instrumentation/listener.rb b/lib/puppet/util/instrumentation/listener.rb new file mode 100644 index 000000000..42ec0c0e9 --- /dev/null +++ b/lib/puppet/util/instrumentation/listener.rb @@ -0,0 +1,60 @@ +require 'puppet/indirector' +require 'puppet/util/instrumentation' +require 'puppet/util/instrumentation/data' + +class Puppet::Util::Instrumentation::Listener + include Puppet::Util + include Puppet::Util::Warnings + extend Puppet::Indirector + + indirects :instrumentation_listener, :terminus_class => :local + + attr_reader :pattern, :listener + attr_accessor :enabled + + def initialize(listener, pattern = nil, enabled = false) + @pattern = pattern.is_a?(Symbol) ? pattern.to_s : pattern + raise "Listener isn't a correct listener (it doesn't provide the notify method)" unless listener.respond_to?(:notify) + @listener = listener + @enabled = enabled + end + + def notify(label, event, data) + listener.notify(label, event, data) + rescue => e + warnonce("Error during instrumentation notification: #{e}") + end + + def listen_to?(label) + enabled? and (!@pattern || @pattern === label.to_s) + end + + def enabled? + !!@enabled + end + + def name + @listener.name.to_s + end + + def data + { :data => @listener.data } + end + + def to_pson(*args) + result = { + :document_type => "Puppet::Util::Instrumentation::Listener", + :data => { + :name => name, + :pattern => pattern, + :enabled => enabled? + } + } + result.to_pson(*args) + end + + def self.from_pson(data) + result = Puppet::Util::Instrumentation[data["name"]] + self.new(result.listener, result.pattern, data["enabled"]) + end +end diff --git a/lib/puppet/util/instrumentation/listeners/log.rb b/lib/puppet/util/instrumentation/listeners/log.rb new file mode 100644 index 000000000..59e9daff9 --- /dev/null +++ b/lib/puppet/util/instrumentation/listeners/log.rb @@ -0,0 +1,29 @@ +require 'monitor' + +# This is an example instrumentation listener that stores the last +# 20 instrumented probe run time. +Puppet::Util::Instrumentation.new_listener(:log) do + + SIZE = 20 + + attr_accessor :last_logs + + def initialize + @last_logs = {}.extend(MonitorMixin) + end + + def notify(label, event, data) + return if event == :start + log_line = "#{label} took #{data[:finished] - data[:started]}" + @last_logs.synchronize { + (@last_logs[label] ||= []) << log_line + @last_logs[label].shift if @last_logs[label].length > SIZE + } + end + + def data + @last_logs.synchronize { + @last_logs.dup + } + end +end
\ No newline at end of file diff --git a/lib/puppet/util/instrumentation/listeners/performance.rb b/lib/puppet/util/instrumentation/listeners/performance.rb new file mode 100644 index 000000000..b3175a0a2 --- /dev/null +++ b/lib/puppet/util/instrumentation/listeners/performance.rb @@ -0,0 +1,30 @@ +require 'monitor' + +Puppet::Util::Instrumentation.new_listener(:performance) do + + attr_reader :samples + + def initialize + @samples = {}.extend(MonitorMixin) + end + + def notify(label, event, data) + return if event == :start + + duration = data[:finished] - data[:started] + samples.synchronize do + @samples[label] ||= { :count => 0, :max => 0, :min => nil, :sum => 0, :average => 0 } + @samples[label][:count] += 1 + @samples[label][:sum] += duration + @samples[label][:max] = [ @samples[label][:max], duration ].max + @samples[label][:min] = [ @samples[label][:min], duration ].reject { |val| val.nil? }.min + @samples[label][:average] = @samples[label][:sum] / @samples[label][:count] + end + end + + def data + samples.synchronize do + @samples.dup + end + end +end
\ No newline at end of file diff --git a/lib/puppet/util/instrumentation/listeners/process_name.rb b/lib/puppet/util/instrumentation/listeners/process_name.rb new file mode 100644 index 000000000..88a185c41 --- /dev/null +++ b/lib/puppet/util/instrumentation/listeners/process_name.rb @@ -0,0 +1,112 @@ +require 'monitor' + +# Unlike the other instrumentation plugins, this one doesn't give back +# data. Instead it changes the process name of the currently running process +# with the last labels and data. +Puppet::Util::Instrumentation.new_listener(:process_name) do + include Sync_m + + # start scrolling when process name is longer than + SCROLL_LENGTH = 50 + + attr_accessor :active, :reason + + def notify(label, event, data) + start(label) if event == :start + stop if event == :stop + end + + def start(activity) + push_activity(Thread.current, activity) + end + + def stop() + pop_activity(Thread.current) + end + + def subscribed + synchronize do + @oldname = $0 + @scroller ||= Thread.new do + loop do + scroll + sleep 1 + end + end + end + end + + def unsubscribed + synchronize do + $0 = @oldname if @oldname + Thread.kill(@scroller) + @scroller = nil + end + end + + def setproctitle + $0 = "#{base}: " + rotate(process_name,@x) + end + + def push_activity(thread, activity) + synchronize do + @reason ||= {} + @reason[thread] ||= [] + @reason[thread].push(activity) + setproctitle + end + end + + def pop_activity(thread) + synchronize do + @reason[thread].pop + if @reason[thread].empty? + @reason.delete(thread) + end + setproctitle + end + end + + def process_name + out = (@reason || {}).inject([]) do |out, reason| + out << "#{thread_id(reason[0])} #{reason[1].join(',')}" + end + out.join(' | ') + end + + # Getting the ruby thread id might not be portable to other ruby + # interpreters than MRI, because Thread#inspect might not return the same + # information on a different runtime. + def thread_id(thread) + thread.inspect.gsub(/^#<.*:0x([a-f0-9]+) .*>$/, '\1') + end + + def rotate(string, steps) + steps ||= 0 + if string.length > 0 && steps > 0 + steps = steps % string.length + return string[steps..-1].concat " -- #{string[0..(steps-1)]}" + end + string + end + + def base + basename = case Puppet.run_mode.name + when :master + "master" + when :agent + "agent" + else + "puppet" + end + end + + def scroll + @x ||= 1 + return if process_name.length < SCROLL_LENGTH + synchronize do + setproctitle + @x += 1 + end + end +end
\ No newline at end of file diff --git a/lib/puppet/util/monkey_patches.rb b/lib/puppet/util/monkey_patches.rb index 81bcdf12f..5186a4e5e 100644 --- a/lib/puppet/util/monkey_patches.rb +++ b/lib/puppet/util/monkey_patches.rb @@ -130,3 +130,11 @@ class IO lines end end + +# Ruby 1.8.5 doesn't have tap +module Kernel + def tap + yield(self) + self + end unless method_defined?(:tap) +end diff --git a/lib/puppet/util/pidlock.rb b/lib/puppet/util/pidlock.rb index fcf0cf296..9ed86352d 100644 --- a/lib/puppet/util/pidlock.rb +++ b/lib/puppet/util/pidlock.rb @@ -1,11 +1,7 @@ require 'fileutils' +require 'puppet/util/anonymous_filelock' -class Puppet::Util::Pidlock - attr_reader :lockfile - - def initialize(lockfile) - @lockfile = lockfile - end +class Puppet::Util::Pidlock < Puppet::Util::AnonymousFilelock def locked? clear_if_stale @@ -17,29 +13,18 @@ class Puppet::Util::Pidlock end def anonymous? - return false unless File.exists?(@lockfile) - File.read(@lockfile) == "" + false end - def lock(opts = {}) - opts = {:anonymous => false}.merge(opts) + def lock + return mine? if locked? - if locked? - mine? - else - if opts[:anonymous] - File.open(@lockfile, 'w') { |fd| true } - else - File.open(@lockfile, "w") { |fd| fd.write(Process.pid) } - end - true - end + File.open(@lockfile, "w") { |fd| fd.write(Process.pid) } + true end def unlock(opts = {}) - opts = {:anonymous => false}.merge(opts) - - if mine? or (opts[:anonymous] and anonymous?) + if mine? File.unlink(@lockfile) true else @@ -47,7 +32,6 @@ class Puppet::Util::Pidlock end end - private def lock_pid if File.exists? @lockfile File.read(@lockfile).to_i @@ -56,6 +40,7 @@ class Puppet::Util::Pidlock end end + private def clear_if_stale return if lock_pid.nil? diff --git a/lib/puppet/util/queue/stomp.rb b/lib/puppet/util/queue/stomp.rb index cabc56627..4a7081bc7 100644 --- a/lib/puppet/util/queue/stomp.rb +++ b/lib/puppet/util/queue/stomp.rb @@ -2,12 +2,14 @@ require 'puppet/util/queue' require 'stomp' require 'uri' -# Implements the Ruby Stomp client as a queue type within the Puppet::Indirector::Queue::Client -# registry, for use with the <tt>:queue</tt> indirection terminus type. +# Implements the Ruby Stomp client as a queue type within the +# Puppet::Indirector::Queue::Client registry, for use with the <tt>:queue</tt> +# indirection terminus type. # -# Looks to <tt>Puppet[:queue_source]</tt> for the sole argument to the underlying Stomp::Client constructor; -# consequently, for this client to work, <tt>Puppet[:queue_source]</tt> must use the Stomp::Client URL-like -# syntax for identifying the Stomp message broker: <em>login:pass@host.port</em> +# Looks to <tt>Puppet[:queue_source]</tt> for the sole argument to the +# underlying Stomp::Client constructor; consequently, for this client to work, +# <tt>Puppet[:queue_source]</tt> must use the Stomp::Client URL-like syntax +# for identifying the Stomp message broker: <em>login:pass@host.port</em> class Puppet::Util::Queue::Stomp attr_accessor :stomp_client @@ -26,10 +28,21 @@ class Puppet::Util::Queue::Stomp rescue => detail raise ArgumentError, "Could not create Stomp client instance with queue source #{Puppet[:queue_source]}: got internal Stomp client error #{detail}" end + + # Identify the supported method for sending messages. + @method = + case + when stomp_client.respond_to?(:publish) + :publish + when stomp_client.respond_to?(:send) + :send + else + raise ArgumentError, "STOMP client does not respond to either publish or send" + end end def publish_message(target, msg) - stomp_client.publish(stompify_target(target), msg, :persistent => true) + stomp_client.__send__(@method, stompify_target(target), msg, :persistent => true) end def subscribe(target) diff --git a/lib/puppet/util/rdoc/parser.rb b/lib/puppet/util/rdoc/parser.rb index a8996ee9a..05e3aab4d 100644 --- a/lib/puppet/util/rdoc/parser.rb +++ b/lib/puppet/util/rdoc/parser.rb @@ -99,7 +99,7 @@ class Parser modpath = $1 name = $2 Puppet.debug "rdoc: module #{name} into #{modpath} ?" - Puppet::Module.modulepath.each do |mp| + Puppet::Node::Environment.new.modulepath.each do |mp| if File.identical?(modpath,mp) Puppet.debug "rdoc: found module #{name}" return name @@ -110,7 +110,7 @@ class Parser # there can be paths we don't want to scan under modules # imagine a ruby or manifest that would be distributed as part as a module # but we don't want those to be hosted under <site> - Puppet::Module.modulepath.each do |mp| + Puppet::Node::Environment.new.modulepath.each do |mp| # check that fullpath is a descendant of mp dirname = fullpath previous = dirname diff --git a/lib/puppet/util/reference.rb b/lib/puppet/util/reference.rb index ae5f2d44c..600325109 100644 --- a/lib/puppet/util/reference.rb +++ b/lib/puppet/util/reference.rb @@ -64,8 +64,6 @@ class Puppet::Util::Reference loaded_instances(:reference).sort { |a,b| a.to_s <=> b.to_s } end - HEADER_LEVELS = [nil, "#", "##", "###", "####", "#####"] - attr_accessor :page, :depth, :header, :title, :dynamic attr_writer :doc @@ -81,20 +79,6 @@ class Puppet::Util::Reference self.dynamic end - def markdown_header(name, level) - "#{HEADER_LEVELS[level]} #{name}\n\n" - end - - def markdown_definitionlist(term, definition) - lines = definition.split("\n") - str = "#{term}\n: #{lines.shift}\n" - lines.each do |line| - str << " " if line =~ /\S/ - str << "#{line}\n" - end - str << "\n" - end - def initialize(name, options = {}, &block) @name = name options.each do |option, value| diff --git a/lib/puppet/util/retryaction.rb b/lib/puppet/util/retryaction.rb new file mode 100644 index 000000000..ba318ec1a --- /dev/null +++ b/lib/puppet/util/retryaction.rb @@ -0,0 +1,48 @@ +module Puppet::Util::RetryAction + class RetryException < Exception; end + class RetryException::NoBlockGiven < RetryException; end + class RetryException::NoRetriesGiven < RetryException;end + class RetryException::RetriesExceeded < RetryException; end + + def self.retry_action( parameters = { :retry_exceptions => nil, :retries => nil } ) + # Retry actions for a specified amount of time. This method will allow the final + # retry to complete even if that extends beyond the timeout period. + unless block_given? + raise RetryException::NoBlockGiven + end + + raise RetryException::NoRetriesGiven if parameters[:retries].nil? + parameters[:retry_exceptions] ||= Hash.new + + start = Time.now + failures = 0 + + begin + yield + rescue Exception => e + # If we were giving exceptions to catch, + # catch the excptions we care about and retry. + # All others fail hard + + raise RetryException::RetriesExceeded if parameters[:retries] == 0 + + if (not parameters[:retry_exceptions].keys.empty?) and parameters[:retry_exceptions].keys.include?(e.class) + Puppet.info("Caught exception #{e.class}:#{e}") + Puppet.info(parameters[:retry_exceptions][e.class]) + elsif (not parameters[:retry_exceptions].keys.empty?) + # If the exceptions is not in the list of retry_exceptions re-raise. + raise e + end + + failures += 1 + parameters[:retries] -= 1 + + # Increase the amount of time that we sleep after every + # failed retry attempt. + sleep (((2 ** failures) -1) * 0.1) + + retry + + end + end +end diff --git a/lib/puppet/util/suidmanager.rb b/lib/puppet/util/suidmanager.rb index d733883b4..82524d031 100644 --- a/lib/puppet/util/suidmanager.rb +++ b/lib/puppet/util/suidmanager.rb @@ -5,7 +5,8 @@ module Puppet::Util::SUIDManager include Puppet::Util::Warnings extend Forwardable - # Note groups= is handled specially due to a bug in OS X 10.6 + # Note groups= is handled specially due to a bug in OS X 10.6, 10.7, + # and probably upcoming releases... to_delegate_to_process = [ :euid=, :euid, :egid=, :egid, :uid=, :uid, :gid=, :gid, :groups ] to_delegate_to_process.each do |method| @@ -28,10 +29,25 @@ module Puppet::Util::SUIDManager module_function :osx_maj_ver def groups=(grouplist) - if osx_maj_ver == '10.6' - return true - else + begin return Process.groups = grouplist + rescue Errno::EINVAL => e + #We catch Errno::EINVAL as some operating systems (OS X in particular) can + # cause troubles when using Process#groups= to change *this* user / process + # list of supplementary groups membership. This is done via Ruby's function + # "static VALUE proc_setgroups(VALUE obj, VALUE ary)" which is effectively + # a wrapper for "int setgroups(size_t size, const gid_t *list)" (part of SVr4 + # and 4.3BSD but not in POSIX.1-2001) that fails and sets errno to EINVAL. + # + # This does not appear to be a problem with Ruby but rather an issue on the + # operating system side. Therefore we catch the exception and look whether + # we run under OS X or not -- if so, then we acknowledge the problem and + # re-throw the exception otherwise. + if osx_maj_ver and not osx_maj_ver.empty? + return true + else + raise e + end end end module_function :groups= diff --git a/lib/puppet/util/symbolic_file_mode.rb b/lib/puppet/util/symbolic_file_mode.rb new file mode 100644 index 000000000..de07b061a --- /dev/null +++ b/lib/puppet/util/symbolic_file_mode.rb @@ -0,0 +1,140 @@ +require 'puppet/util' + +module Puppet::Util::SymbolicFileMode + SetUIDBit = ReadBit = 4 + SetGIDBit = WriteBit = 2 + StickyBit = ExecBit = 1 + SymbolicMode = { 'x' => ExecBit, 'w' => WriteBit, 'r' => ReadBit } + SymbolicSpecialToBit = { + 't' => { 'u' => StickyBit, 'g' => StickyBit, 'o' => StickyBit }, + 's' => { 'u' => SetUIDBit, 'g' => SetGIDBit, 'o' => StickyBit } + } + + def valid_symbolic_mode?(value) + value = normalize_symbolic_mode(value) + return true if value =~ /^0?[0-7]{1,4}$/ + return true if value =~ /^([ugoa]*[-=+][-=+rstwxXugo]*)(,[ugoa]*[-=+][-=+rstwxXugo]*)*$/ + return false + end + + def normalize_symbolic_mode(value) + return nil if value.nil? + + # We need to treat integers as octal numbers. + if value.is_a? Numeric then + return value.to_s(8) + elsif value =~ /^0?[0-7]{1,4}$/ then + return value.to_i(8).to_s(8) + else + return value + end + end + + def symbolic_mode_to_int(modification, to_mode = 0, is_a_directory = false) + if modification.nil? or modification == '' then + raise Puppet::Error, "An empty mode string is illegal" + end + if modification =~ /^[0-7]+$/ then return modification.to_i(8) end + if modification =~ /^\d+$/ then + raise Puppet::Error, "Numeric modes must be in octal, not decimal!" + end + + fail "non-numeric current mode (#{to_mode.inspect})" unless to_mode.is_a?(Numeric) + + original_mode = { + 's' => (to_mode & 07000) >> 9, + 'u' => (to_mode & 00700) >> 6, + 'g' => (to_mode & 00070) >> 3, + 'o' => (to_mode & 00007) >> 0, + # Are there any execute bits set in the original mode? + 'any x?' => (to_mode & 00111) != 0 + } + final_mode = { + 's' => original_mode['s'], + 'u' => original_mode['u'], + 'g' => original_mode['g'], + 'o' => original_mode['o'], + } + + modification.split(/\s*,\s*/).each do |part| + begin + _, to, dsl = /^([ugoa]*)([-+=].*)$/.match(part).to_a + if dsl.nil? then raise Puppet::Error, 'Missing action' end + to = "a" unless to and to.length > 0 + + # We want a snapshot of the mode before we start messing with it to + # make actions like 'a-g' atomic. Various parts of the DSL refer to + # the original mode, the final mode, or the current snapshot of the + # mode, for added fun. + snapshot_mode = {} + final_mode.each {|k,v| snapshot_mode[k] = v } + + to.gsub('a', 'ugo').split('').uniq.each do |who| + value = snapshot_mode[who] + + action = '!' + actions = { + '!' => lambda {|_,_| raise Puppet::Error, 'Missing operation (-, =, or +)' }, + '=' => lambda {|m,v| m | v }, + '+' => lambda {|m,v| m | v }, + '-' => lambda {|m,v| m & ~v }, + } + + dsl.split('').each do |op| + case op + when /[-+=]/ then + action = op + # Clear all bits, if this is assignment + value = 0 if op == '=' + + when /[ugo]/ then + value = actions[action].call(value, snapshot_mode[op]) + + when /[rwx]/ then + value = actions[action].call(value, SymbolicMode[op]) + + when 'X' then + # Only meaningful in combination with "set" actions. + if action != '+' then + raise Puppet::Error, "X only works with the '+' operator" + end + + # As per the BSD manual page, set if this is a directory, or if + # any execute bit is set on the original (unmodified) mode. + # Ignored otherwise; it is "add if", not "add or clear". + if is_a_directory or original_mode['any x?'] then + value = actions[action].call(value, ExecBit) + end + + when /[st]/ then + bit = SymbolicSpecialToBit[op][who] or fail "internal error" + final_mode['s'] = actions[action].call(final_mode['s'], bit) + + else + raise Puppet::Error, 'Unknown operation' + end + end + + # Now, assign back the value. + final_mode[who] = value + end + + rescue Puppet::Error => e + if part.inspect != modification.inspect then + rest = " at #{part.inspect}" + else + rest = '' + end + + raise Puppet::Error, "#{e}#{rest} in symbolic mode #{modification.inspect}" + end + end + + result = + final_mode['s'] << 9 | + final_mode['u'] << 6 | + final_mode['g'] << 3 | + final_mode['o'] << 0 + return result + end +end diff --git a/lib/puppet/util/zaml.rb b/lib/puppet/util/zaml.rb index bbb2af2d2..e07a2d7b9 100644 --- a/lib/puppet/util/zaml.rb +++ b/lib/puppet/util/zaml.rb @@ -1,4 +1,15 @@ +# encoding: UTF-8 # +# The above encoding line is a magic comment to set the default source encoding +# of this file for the Ruby interpreter. It must be on the first or second +# line of the file if an interpreter is in use. In Ruby 1.9 and later, the +# source encoding determines the encoding of String and Regexp objects created +# from this source file. This explicit encoding is important becuase otherwise +# Ruby will pick an encoding based on LANG or LC_CTYPE environment variables. +# These may be different from site to site so it's important for us to +# establish a consistent behavior. For more information on M17n please see: +# http://links.puppetlabs.com/understanding_m17n + # ZAML -- A partial replacement for YAML, writen with speed and code clarity # in mind. ZAML fixes one YAML bug (loading Exceptions) and provides # a replacement for YAML.dump unimaginatively called ZAML.dump, @@ -218,10 +229,16 @@ end class String ZAML_ESCAPES = %w{\x00 \x01 \x02 \x03 \x04 \x05 \x06 \a \x08 \t \n \v \f \r \x0e \x0f \x10 \x11 \x12 \x13 \x14 \x15 \x16 \x17 \x18 \x19 \x1a \e \x1c \x1d \x1e \x1f } def escaped_for_zaml - gsub( /\x5C/, "\\\\\\" ). # Demi-kludge for Maglev/rubinius; the regexp should be /\\/ but parsetree chokes on that. - gsub( /"/, "\\\"" ). - gsub( /([\x00-\x1F])/ ) { |x| ZAML_ESCAPES[ x.unpack("C")[0] ] }. - gsub( /([\x80-\xFF])/ ) { |x| "\\x#{x.unpack("C")[0].to_s(16)}" } + # JJM (Note the trailing dots to construct a multi-line method chain.) This + # code is meant to escape all bytes which are not ASCII-8BIT printable + # characters. Multi-byte unicode characters are handled just fine because + # each byte of the character results in an escaped string emitted to the + # YAML stream. When the YAML is de-serialized back into a String the bytes + # will be reconstructed properly into the unicode character. + self.to_ascii8bit.gsub( /\x5C/n, "\\\\\\" ). # Demi-kludge for Maglev/rubinius; the regexp should be /\\/ but parsetree chokes on that. + gsub( /"/n, "\\\"" ). + gsub( /([\x00-\x1F])/n ) { |x| ZAML_ESCAPES[ x.unpack("C")[0] ] }. + gsub( /([\x80-\xFF])/n ) { |x| "\\x#{x.unpack("C")[0].to_s(16)}" } end def to_zaml(z) z.first_time_only(self) { @@ -238,7 +255,10 @@ class String (self =~ /[\s:]$/) or (self =~ /^[>|][-+\d]*\s/i) or (self[-1..-1] =~ /\s/) or - (self =~ /[\x00-\x08\x0B\x0C\x0E-\x1F\x80-\xFF]/) or + # This regular expression assumes the string is a byte sequence. + # It does not concern itself with characters so we convert the string + # to ASCII-8BIT for Ruby 1.9 to match up encodings. + (self.to_ascii8bit=~ /[\x00-\x08\x0B\x0C\x0E-\x1F\x80-\xFF]/n) or (self =~ /[,\[\]\{\}\r\t]|:\s|\s#/) or (self =~ /\A([-:?!#&*'"]|<<|%.+:.)/) ) @@ -251,6 +271,20 @@ class String end } end + + # Return a guranteed ASCII-8BIT encoding for Ruby 1.9 This is a helper + # method for other methods that perform regular expressions against byte + # sequences deliberately rather than dealing with characters. + # The method may or may not return a new instance. + def to_ascii8bit + if self.respond_to?(:encoding) and self.encoding.name != "ASCII-8BIT" then + str = self.dup + str.force_encoding("ASCII-8BIT") + return str + else + return self + end + end end class Hash diff --git a/spec/fixtures/releases/jamtur01-apache/Modulefile b/spec/fixtures/releases/jamtur01-apache/Modulefile new file mode 100644 index 000000000..35a1ab887 --- /dev/null +++ b/spec/fixtures/releases/jamtur01-apache/Modulefile @@ -0,0 +1,2 @@ +name 'jamtur01-apache' +version '0.0.1' diff --git a/spec/fixtures/releases/jamtur01-apache/files/httpd b/spec/fixtures/releases/jamtur01-apache/files/httpd new file mode 100644 index 000000000..438647cb8 --- /dev/null +++ b/spec/fixtures/releases/jamtur01-apache/files/httpd @@ -0,0 +1,24 @@ +# Configuration file for the httpd service. + +# +# The default processing model (MPM) is the process-based +# 'prefork' model. A thread-based model, 'worker', is also +# available, but does not work with some modules (such as PHP). +# The service must be stopped before changing this variable. +# +#HTTPD=/usr/sbin/httpd.worker + +# +# To pass additional options (for instance, -D definitions) to the +# httpd binary at startup, set OPTIONS here. +# +#OPTIONS= +#OPTIONS=-DDOWN + +# +# By default, the httpd process is started in the C locale; to +# change the locale in which the server runs, the HTTPD_LANG +# variable can be set. +# +#HTTPD_LANG=C +export SHORTHOST=`hostname -s` diff --git a/spec/fixtures/releases/jamtur01-apache/files/test.vhost b/spec/fixtures/releases/jamtur01-apache/files/test.vhost new file mode 100644 index 000000000..801bb85a1 --- /dev/null +++ b/spec/fixtures/releases/jamtur01-apache/files/test.vhost @@ -0,0 +1,18 @@ +# +# Test vhost +# +NameVirtualHost *:80 +<VirtualHost *:80> + ServerName testvhost + DocumentRoot /tmp/testvhost + <Directory /tmp/testvhost> + Options Indexes FollowSymLinks MultiViews + AllowOverride None + Order allow,deny + allow from all + </Directory> + ErrorLog /var/log/apache2/error.log + LogLevel warn + CustomLog /var/log/apache2/access.log combined + ServerSignature On +</VirtualHost> diff --git a/spec/fixtures/releases/jamtur01-apache/lib/puppet/provider/a2mod/debian.rb b/spec/fixtures/releases/jamtur01-apache/lib/puppet/provider/a2mod/debian.rb new file mode 100644 index 000000000..82ced05b7 --- /dev/null +++ b/spec/fixtures/releases/jamtur01-apache/lib/puppet/provider/a2mod/debian.rb @@ -0,0 +1,21 @@ +Puppet::Type.type(:a2mod).provide(:debian) do + desc "Manage Apache 2 modules on Debian-like OSes (e.g. Ubuntu)" + + commands :encmd => "a2enmod" + commands :discmd => "a2dismod" + + defaultfor :operatingsystem => [:debian, :ubuntu] + + def create + encmd resource[:name] + end + + def destroy + discmd resource[:name] + end + + def exists? + mod= "/etc/apache2/mods-enabled/" + resource[:name] + ".load" + File.exists?(mod) + end +end diff --git a/spec/fixtures/releases/jamtur01-apache/lib/puppet/type/a2mod.rb b/spec/fixtures/releases/jamtur01-apache/lib/puppet/type/a2mod.rb new file mode 100644 index 000000000..a53f07fb6 --- /dev/null +++ b/spec/fixtures/releases/jamtur01-apache/lib/puppet/type/a2mod.rb @@ -0,0 +1,12 @@ +Puppet::Type.newtype(:a2mod) do + @doc = "Manage Apache 2 modules" + + ensurable + + newparam(:name) do + desc "The name of the module to be managed" + + isnamevar + + end +end diff --git a/spec/fixtures/releases/jamtur01-apache/manifests/dev.pp b/spec/fixtures/releases/jamtur01-apache/manifests/dev.pp new file mode 100644 index 000000000..42605e505 --- /dev/null +++ b/spec/fixtures/releases/jamtur01-apache/manifests/dev.pp @@ -0,0 +1,5 @@ +class apache::dev { + include apache::params + + package{$apache::params::apache_dev: ensure => installed} +} diff --git a/spec/fixtures/releases/jamtur01-apache/manifests/init.pp b/spec/fixtures/releases/jamtur01-apache/manifests/init.pp new file mode 100644 index 000000000..bd3f7bae1 --- /dev/null +++ b/spec/fixtures/releases/jamtur01-apache/manifests/init.pp @@ -0,0 +1,34 @@ +# ensure apache is installed +class apache { + include apache::params + package{'httpd': + name => $apache::params::apache_name, + ensure => present, + } + service { 'httpd': + name => $apache::params::apache_name, + ensure => running, + enable => true, + subscribe => Package['httpd'], + } + # + # May want to purge all none realize modules using the resources resource type. + # A2mod resource type is broken. Look into fixing it and moving it into apache. + # + A2mod { require => Package['httpd'], notify => Service['httpd']} + @a2mod { + 'rewrite' : ensure => present; + 'headers' : ensure => present; + 'expires' : ensure => present; + } + $vdir = $operatingsystem? { + 'ubuntu' => '/etc/apache2/sites-enabled/', + default => '/etc/httpd/conf.d', + } + file { $vdir: + ensure => directory, + recurse => true, + purge => true, + notify => Service['httpd'], + } +} diff --git a/spec/fixtures/releases/jamtur01-apache/manifests/params.pp b/spec/fixtures/releases/jamtur01-apache/manifests/params.pp new file mode 100644 index 000000000..520eccb44 --- /dev/null +++ b/spec/fixtures/releases/jamtur01-apache/manifests/params.pp @@ -0,0 +1,17 @@ +class apache::params{ + $user = 'www-data' + $group = 'www-data' + + case $operatingsystem { + "centos": { + $apache_name = httpd + $ssl_package = mod_ssl + $apache_dev = httpd-devel + } + "ubuntu": { + $apache_name = apache2 + $ssl_package = apache-ssl + $apache_dev = [ libaprutil1-dev, libapr1-dev, apache2-prefork-dev ] + } + } +} diff --git a/spec/fixtures/releases/jamtur01-apache/manifests/php.pp b/spec/fixtures/releases/jamtur01-apache/manifests/php.pp new file mode 100644 index 000000000..0827e8043 --- /dev/null +++ b/spec/fixtures/releases/jamtur01-apache/manifests/php.pp @@ -0,0 +1,5 @@ +class apache::php{ + package{'libapache2-mod-php5': + ensure => present, + } +} diff --git a/spec/fixtures/releases/jamtur01-apache/manifests/ssl.pp b/spec/fixtures/releases/jamtur01-apache/manifests/ssl.pp new file mode 100644 index 000000000..349f92288 --- /dev/null +++ b/spec/fixtures/releases/jamtur01-apache/manifests/ssl.pp @@ -0,0 +1,15 @@ +class apache::ssl { + include apache + + + case $operatingsystem { + "centos": { + package { $apache::params::ssl_package: + require => Package['httpd'], + } + } + "ubuntu": { + a2mod { "ssl": ensure => present, } + } + } +} diff --git a/spec/fixtures/releases/jamtur01-apache/manifests/vhost.pp b/spec/fixtures/releases/jamtur01-apache/manifests/vhost.pp new file mode 100644 index 000000000..2fe6ed204 --- /dev/null +++ b/spec/fixtures/releases/jamtur01-apache/manifests/vhost.pp @@ -0,0 +1,15 @@ +define apache::vhost( $port, $docroot, $ssl=true, $template='apache/vhost-default.conf.erb', $priority, $serveraliases = '' ) { + include apache + $vdir = $operatingsystem? { + 'ubuntu' => '/etc/apache2/sites-enabled/', + default => '/etc/httpd/conf.d', + } + file{"${vdir}/${priority}-${name}": + content => template($template), + owner => 'root', + group => 'root', + mode => '777', + require => Package['httpd'], + notify => Service['httpd'], + } +} diff --git a/spec/fixtures/releases/jamtur01-apache/metadata.json b/spec/fixtures/releases/jamtur01-apache/metadata.json new file mode 100644 index 000000000..7fb6c0868 --- /dev/null +++ b/spec/fixtures/releases/jamtur01-apache/metadata.json @@ -0,0 +1 @@ +{"dependencies":[],"types":[{"providers":[{"name":"a2mod","doc":"Manage Apache 2 modules on Debian and Ubuntu Required binaries: ``a2enmod``, ``a2dismod``. Default for ``operatingsystem`` == ``debianubuntu``. "}],"parameters":[{"name":"name","doc":"The name of the module to be managed"}],"properties":[{"name":"ensure","doc":"The basic property that the resource should be in. Valid values are ``present``, ``absent``."}],"name":"a2mod","doc":"Manage Apache 2 modules on Debian and Ubuntu"}],"checksums":{"manifests/params.pp":"71734796921dbdbfd58f503622527616","tests/ssl.pp":"191912535199531fd631f911c6329e56","tests/vhost.pp":"1b91e03c8ef89a7ecb6793831ac18399","manifests/php.pp":"b78cc593f1c4cd800c906e0891c9b11f","files/httpd":"295f5e924afe6f752d29327e73fe6d0a","tests/php.pp":"ce7bb9eef69d32b42a32ce32d9653625","lib/puppet/provider/a2mod/a2mod.rb":"18c5bb180b75a2375e95e07f88a94257","files/test.vhost":"0602022c19a7b6b289f218c7b93c1aea","manifests/ssl.pp":"b4334a161a2ba5fa8a62cf7b38f352c8","manifests/dev.pp":"510813942246cc9a7786d8f2d8874a35","manifests/vhost.pp":"cbc4657b0cce5cd432057393d5f6b0c2","tests/init.pp":"4eac4a7ef68499854c54a78879e25535","lib/puppet/type/a2mod.rb":"0e1b4843431413a10320ac1f6a055d15","tests/apache.pp":"4eac4a7ef68499854c54a78879e25535","tests/dev.pp":"4cf15c1fecea3ca86009f182b402c7ab","templates/vhost-default.conf.erb":"9055aed946e1111c30ab81fedac2c8b0","manifests/init.pp":"dc503e26e8021351078813b541c4bd3d","Modulefile":"d43334b4072cd1744121b3b25cd9ed15"},"version":"0.0.1","name":"jamtur01-apache"}
\ No newline at end of file diff --git a/spec/fixtures/releases/jamtur01-apache/templates/vhost-default.conf.erb b/spec/fixtures/releases/jamtur01-apache/templates/vhost-default.conf.erb new file mode 100644 index 000000000..3aaf94594 --- /dev/null +++ b/spec/fixtures/releases/jamtur01-apache/templates/vhost-default.conf.erb @@ -0,0 +1,20 @@ +NameVirtualHost *:<%= port %> +<VirtualHost *:<%= port %>> + ServerName <%= name %> +<%if serveraliases.is_a? Array -%> +<% serveraliases.each do |name| -%><%= " ServerAlias #{name}\n" %><% end -%> +<% elsif serveraliases != '' -%> +<%= " ServerAlias #{serveraliases}" -%> +<% end -%> + DocumentRoot <%= docroot %> + <Directory <%= docroot %>> + Options Indexes FollowSymLinks MultiViews + AllowOverride None + Order allow,deny + allow from all + </Directory> + ErrorLog /var/log/apache2/<%= name %>_error.log + LogLevel warn + CustomLog /var/log/apache2/<%= name %>_access.log combined + ServerSignature On +</VirtualHost> diff --git a/spec/fixtures/releases/jamtur01-apache/tests/apache.pp b/spec/fixtures/releases/jamtur01-apache/tests/apache.pp new file mode 100644 index 000000000..b3f9f13aa --- /dev/null +++ b/spec/fixtures/releases/jamtur01-apache/tests/apache.pp @@ -0,0 +1 @@ +include apache diff --git a/spec/fixtures/releases/jamtur01-apache/tests/dev.pp b/spec/fixtures/releases/jamtur01-apache/tests/dev.pp new file mode 100644 index 000000000..805ad7e37 --- /dev/null +++ b/spec/fixtures/releases/jamtur01-apache/tests/dev.pp @@ -0,0 +1 @@ +include apache::dev diff --git a/spec/fixtures/releases/jamtur01-apache/tests/init.pp b/spec/fixtures/releases/jamtur01-apache/tests/init.pp new file mode 100644 index 000000000..b3f9f13aa --- /dev/null +++ b/spec/fixtures/releases/jamtur01-apache/tests/init.pp @@ -0,0 +1 @@ +include apache diff --git a/spec/fixtures/releases/jamtur01-apache/tests/php.pp b/spec/fixtures/releases/jamtur01-apache/tests/php.pp new file mode 100644 index 000000000..618e0ebd2 --- /dev/null +++ b/spec/fixtures/releases/jamtur01-apache/tests/php.pp @@ -0,0 +1 @@ +include apache::php diff --git a/spec/fixtures/releases/jamtur01-apache/tests/ssl.pp b/spec/fixtures/releases/jamtur01-apache/tests/ssl.pp new file mode 100644 index 000000000..cf2dacfb8 --- /dev/null +++ b/spec/fixtures/releases/jamtur01-apache/tests/ssl.pp @@ -0,0 +1 @@ +include apache::ssl diff --git a/spec/fixtures/releases/jamtur01-apache/tests/vhost.pp b/spec/fixtures/releases/jamtur01-apache/tests/vhost.pp new file mode 100644 index 000000000..c916596a6 --- /dev/null +++ b/spec/fixtures/releases/jamtur01-apache/tests/vhost.pp @@ -0,0 +1,2 @@ +include apache +apache::vhost { 'test.vhost': source => 'puppet:///modules/apache/test.vhost' } diff --git a/spec/integration/configurer_spec.rb b/spec/integration/configurer_spec.rb index f5d8bceb2..abe877072 100755 --- a/spec/integration/configurer_spec.rb +++ b/spec/integration/configurer_spec.rb @@ -47,6 +47,7 @@ describe Puppet::Configurer do Puppet::Transaction::Report.indirection.stubs(:save) Puppet[:lastrunfile] = tmpfile("lastrunfile") + Puppet.settings.setting(:lastrunfile).mode = 0666 Puppet[:report] = true # We only record integer seconds in the timestamp, and truncate @@ -56,6 +57,10 @@ describe Puppet::Configurer do @configurer.run :catalog => @catalog, :report => report t2 = Time.now.tv_sec + file_mode = Puppet.features.microsoft_windows? ? '100644' : '100666' + + File.stat(Puppet[:lastrunfile]).mode.to_s(8).should == file_mode + summary = nil File.open(Puppet[:lastrunfile], "r") do |fd| summary = YAML.load(fd.read) diff --git a/spec/integration/indirector/direct_file_server_spec.rb b/spec/integration/indirector/direct_file_server_spec.rb index 0bb0c9806..9c2e32c57 100755 --- a/spec/integration/indirector/direct_file_server_spec.rb +++ b/spec/integration/indirector/direct_file_server_spec.rb @@ -22,7 +22,7 @@ describe Puppet::Indirector::DirectFileServer, " when interacting with the files 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") + Puppet::Util.expects(:binread).with(@filepath).returns("my content") instance = @terminus.find(@terminus.indirection.request(:find, "file://host#{@filepath}")) diff --git a/spec/integration/indirector/file_content/file_server_spec.rb b/spec/integration/indirector/file_content/file_server_spec.rb index e210cecb3..b4e1c908d 100755 --- a/spec/integration/indirector/file_content/file_server_spec.rb +++ b/spec/integration/indirector/file_content/file_server_spec.rb @@ -24,7 +24,7 @@ describe Puppet::Indirector::FileContent::FileServer, " when finding files" do modpath = File.join(path, "mod") FileUtils.mkdir_p(File.join(modpath, "lib")) file = File.join(modpath, "lib", "file.rb") - File.open(file, "w") { |f| f.puts "1" } + File.open(file, "wb") { |f| f.write "1\r\n" } Puppet.settings[:modulepath] = "/no/such/file" @@ -35,8 +35,8 @@ describe Puppet::Indirector::FileContent::FileServer, " when finding files" do result.should_not be_nil result.length.should == 2 - result[1].should be_instance_of(Puppet::FileServing::Content) - result[1].content.should == "1\n" + result.map {|x| x.should be_instance_of(Puppet::FileServing::Content) } + result.find {|x| x.relative_path == 'file.rb' }.content.should == "1\r\n" end it "should find file content in modules" do @@ -47,7 +47,7 @@ describe Puppet::Indirector::FileContent::FileServer, " when finding files" do modpath = File.join(path, "mymod") FileUtils.mkdir_p(File.join(modpath, "files")) file = File.join(modpath, "files", "myfile") - File.open(file, "w") { |f| f.puts "1" } + File.open(file, "wb") { |f| f.write "1\r\n" } Puppet.settings[:modulepath] = path @@ -55,7 +55,7 @@ describe Puppet::Indirector::FileContent::FileServer, " when finding files" do result.should_not be_nil result.should be_instance_of(Puppet::FileServing::Content) - result.content.should == "1\n" + result.content.should == "1\r\n" end it "should find file content in files when node name expansions are used" do @@ -67,7 +67,7 @@ describe Puppet::Indirector::FileContent::FileServer, " when finding files" do Dir.mkdir(@path) subdir = File.join(@path, "mynode") Dir.mkdir(subdir) - File.open(File.join(subdir, "myfile"), "w") { |f| f.puts "1" } + File.open(File.join(subdir, "myfile"), "wb") { |f| f.write "1\r\n" } # Use a real mount, so the integration is a bit deeper. @mount1 = Puppet::FileServing::Configuration::Mount::File.new("one") @@ -85,6 +85,6 @@ describe Puppet::Indirector::FileContent::FileServer, " when finding files" do result.should_not be_nil result.should be_instance_of(Puppet::FileServing::Content) - result.content.should == "1\n" + result.content.should == "1\r\n" end end diff --git a/spec/integration/module_tool_spec.rb b/spec/integration/module_tool_spec.rb new file mode 100644 index 000000000..1067bfab3 --- /dev/null +++ b/spec/integration/module_tool_spec.rb @@ -0,0 +1,477 @@ +require 'spec_helper' +require 'tmpdir' +require 'fileutils' + +# FIXME This are helper methods that could be used by other tests in the +# future, should we move these to a more central location +def stub_repository_read(code, body) + kind = Net::HTTPResponse.send(:response_class, code.to_s) + response = kind.new('1.0', code.to_s, 'HTTP MESSAGE') + response.stubs(:read_body).returns(body) + Puppet::Module::Tool::Repository.any_instance.stubs(:read_response).returns(response) +end + +def stub_installer_read(body) + Puppet::Module::Tool::Applications::Installer.any_instance.stubs(:read_match).returns(body) +end + +def stub_cache_read(body) + Puppet::Module::Tool::Cache.any_instance.stubs(:read_retrieve).returns(body) +end + +# Return path to temparory directory for testing. +def testdir + return @testdir ||= tmpdir("module_tool_testdir") +end + +# Create a temporary testing directory, change into it, and execute the +# +block+. When the block exists, remove the test directory and change back +# to the previous directory. +def mktestdircd(&block) + previousdir = Dir.pwd + rmtestdir + FileUtils.mkdir_p(testdir) + Dir.chdir(testdir) + block.call +ensure + rmtestdir + Dir.chdir previousdir +end + +# Remove the temporary test directory. +def rmtestdir + FileUtils.rm_rf(testdir) if File.directory?(testdir) +end +# END helper methods + + +# Directory that contains sample releases. +RELEASE_FIXTURES_DIR = File.join(PuppetSpec::FIXTURE_DIR, "releases") + +# Return the pathname string to the directory containing the release fixture called +name+. +def release_fixture(name) + return File.join(RELEASE_FIXTURES_DIR, name) +end + +# Copy the release fixture called +name+ into the current working directory. +def install_release_fixture(name) + release_fixture(name) + FileUtils.cp_r(release_fixture(name), name) +end + +describe "module_tool", :fails_on_windows => true do + include PuppetSpec::Files + before do + @tmp_confdir = Puppet[:confdir] = tmpdir("module_tool_test_confdir") + @tmp_vardir = Puppet[:vardir] = tmpdir("module_tool_test_vardir") + Puppet[:module_repository] = "http://forge.puppetlabs.com" + @mytmpdir = Pathname.new(tmpdir("module_tool_test")) + @options = {} + @options[:install_dir] = @mytmpdir + @options[:module_repository] = "http://forge.puppetlabs.com" + end + + def build_and_install_module + Puppet::Module::Tool::Applications::Generator.run(@full_module_name) + Puppet::Module::Tool::Applications::Builder.run(@full_module_name) + + FileUtils.mv("#{@full_module_name}/pkg/#{@release_name}.tar.gz", "#{@release_name}.tar.gz") + FileUtils.rm_rf(@full_module_name) + + Puppet::Module::Tool::Applications::Installer.run("#{@release_name}.tar.gz", @options) + end + + # Return STDOUT and STDERR output generated from +block+ as it's run within a temporary test directory. + def run(&block) + mktestdircd do + block.call + end + end + + before :all do + @username = "myuser" + @module_name = "mymodule" + @full_module_name = "#{@username}-#{@module_name}" + @version = "0.0.1" + @release_name = "#{@full_module_name}-#{@version}" + end + + before :each do + Puppet.settings.stubs(:parse) + Puppet::Module::Tool::Cache.clean + end + + after :each do + Puppet::Module::Tool::Cache.clean + end + + describe "generate" do + it "should generate a module if given a dashed name" do + run do + Puppet::Module::Tool::Applications::Generator.run(@full_module_name) + + File.directory?(@full_module_name).should == true + modulefile = File.join(@full_module_name, "Modulefile") + File.file?(modulefile).should == true + metadata = Puppet::Module::Tool::Metadata.new + Puppet::Module::Tool::ModulefileReader.evaluate(metadata, modulefile) + metadata.full_module_name.should == @full_module_name + metadata.username.should == @username + metadata.name.should == @module_name + end + end + + it "should fail if given an undashed name" do + run do + lambda { Puppet::Module::Tool::Applications::Generator.run("invalid") }.should raise_error(RuntimeError) + end + end + + it "should fail if directory already exists" do + run do + Puppet::Module::Tool::Applications::Generator.run(@full_module_name) + lambda { Puppet::Module::Tool::Applications::Generator.run(@full_module_name) }.should raise_error(ArgumentError) + end + end + + it "should return an array of Pathname objects representing paths of generated files" do + run do + return_value = Puppet::Module::Tool::Applications::Generator.run(@full_module_name) + return_value.each do |generated_file| + generated_file.should be_kind_of(Pathname) + end + return_value.should be_kind_of(Array) + end + end + end + + describe "build" do + it "should build a module in a directory" do + run do + Puppet::Module::Tool::Applications::Generator.run(@full_module_name) + Puppet::Module::Tool::Applications::Builder.run(@full_module_name) + + File.directory?(File.join(@full_module_name, "pkg", @release_name)).should == true + File.file?(File.join(@full_module_name, "pkg", @release_name + ".tar.gz")).should == true + metadata_file = File.join(@full_module_name, "pkg", @release_name, "metadata.json") + File.file?(metadata_file).should == true + metadata = PSON.parse(File.read(metadata_file)) + metadata["name"].should == @full_module_name + metadata["version"].should == @version + metadata["checksums"].should be_a_kind_of(Hash) + metadata["dependencies"].should == [] + metadata["types"].should == [] + end + end + + it "should build a module's checksums" do + run do + Puppet::Module::Tool::Applications::Generator.run(@full_module_name) + Puppet::Module::Tool::Applications::Builder.run(@full_module_name) + + metadata_file = File.join(@full_module_name, "pkg", @release_name, "metadata.json") + metadata = PSON.parse(File.read(metadata_file)) + metadata["checksums"].should be_a_kind_of(Hash) + + modulefile_path = Pathname.new(File.join(@full_module_name, "Modulefile")) + metadata["checksums"]["Modulefile"].should == Digest::MD5.hexdigest(modulefile_path.read) + end + end + + it "should build a module's types and providers" do + run do + name = "jamtur01-apache" + install_release_fixture name + Puppet::Module::Tool::Applications::Builder.run(name) + + metadata_file = File.join(name, "pkg", "#{name}-0.0.1", "metadata.json") + metadata = PSON.parse(File.read(metadata_file)) + + metadata["types"].size.should == 1 + type = metadata["types"].first + type["name"].should == "a2mod" + type["doc"].should == "Manage Apache 2 modules" + + + type["parameters"].size.should == 1 + type["parameters"].first.tap do |o| + o["name"].should == "name" + o["doc"].should == "The name of the module to be managed" + end + + type["properties"].size.should == 1 + type["properties"].first.tap do |o| + o["name"].should == "ensure" + o["doc"].should =~ /present.+absent/ + end + + type["providers"].size.should == 1 + type["providers"].first.tap do |o| + o["name"].should == "debian" + o["doc"].should =~ /Manage Apache 2 modules on Debian-like OSes/ + end + end + end + + it "should build a module's dependencies" do + run do + Puppet::Module::Tool::Applications::Generator.run(@full_module_name) + modulefile = File.join(@full_module_name, "Modulefile") + + dependency1_name = "anotheruser-anothermodule" + dependency1_requirement = ">= 1.2.3" + dependency2_name = "someuser-somemodule" + dependency2_requirement = "4.2" + dependency2_repository = "http://some.repo" + + File.open(modulefile, "a") do |handle| + handle.puts "dependency '#{dependency1_name}', '#{dependency1_requirement}'" + handle.puts "dependency '#{dependency2_name}', '#{dependency2_requirement}', '#{dependency2_repository}'" + end + + Puppet::Module::Tool::Applications::Builder.run(@full_module_name) + + metadata_file = File.join(@full_module_name, "pkg", "#{@full_module_name}-#{@version}", "metadata.json") + metadata = PSON.parse(File.read(metadata_file)) + + metadata['dependencies'].size.should == 2 + metadata['dependencies'].sort_by{|t| t['name']}.tap do |dependencies| + dependencies[0].tap do |dependency1| + dependency1['name'].should == dependency1_name + dependency1['version_requirement'].should == dependency1_requirement + dependency1['repository'].should be_nil + end + + dependencies[1].tap do |dependency2| + dependency2['name'].should == dependency2_name + dependency2['version_requirement'].should == dependency2_requirement + dependency2['repository'].should == dependency2_repository + end + end + end + end + + it "should rebuild a module in a directory" do + run do + Puppet::Module::Tool::Applications::Generator.run(@full_module_name) + Puppet::Module::Tool::Applications::Builder.run(@full_module_name) + Puppet::Module::Tool::Applications::Builder.run(@full_module_name) + end + end + + it "should build a module in the current directory" do + run do + Puppet::Module::Tool::Applications::Generator.run(@full_module_name) + Dir.chdir(@full_module_name) + Puppet::Module::Tool::Applications::Builder.run(Puppet::Module::Tool.find_module_root(nil)) + + File.file?(File.join("pkg", @release_name + ".tar.gz")).should == true + end + end + + it "should fail to build a module without a Modulefile" do + run do + Puppet::Module::Tool::Applications::Generator.run(@full_module_name) + FileUtils.rm(File.join(@full_module_name, "Modulefile")) + + lambda { Puppet::Module::Tool::Applications::Builder.run(Puppet::Module::Tool.find_module_root(@full_module_name)) }.should raise_error(ArgumentError) + end + end + + it "should fail to build a module directory that doesn't exist" do + run do + lambda { Puppet::Module::Tool::Applications::Builder.run(Puppet::Module::Tool.find_module_root(@full_module_name)) }.should raise_error(ArgumentError) + end + end + + it "should fail to build a module in the current directory that's not a module" do + run do + lambda { Puppet::Module::Tool::Applications::Builder.run(Puppet::Module::Tool.find_module_root(nil)) }.should raise_error(ArgumentError) + end + end + + it "should return a Pathname object representing the path to the release archive." do + run do + Puppet::Module::Tool::Applications::Generator.run(@full_module_name) + Puppet::Module::Tool::Applications::Builder.run(@full_module_name).should be_kind_of(Pathname) + end + end + end + + describe "search" do + it "should display matching modules" do + run do + stub_repository_read 200, <<-HERE + [ + {"full_module_name": "cli", "version": "1.0"}, + {"full_module_name": "web", "version": "2.0"} + ] + HERE + Puppet::Module::Tool::Applications::Searcher.run("mymodule", @options).size.should == 2 + end + end + + it "should display no matches" do + run do + stub_repository_read 200, "[]" + Puppet::Module::Tool::Applications::Searcher.run("mymodule", @options).should == [] + end + end + + it "should fail if can't get a connection" do + run do + stub_repository_read 500, "OH NOES!!1!" + lambda { Puppet::Module::Tool::Applications::Searcher.run("mymodule", @options) }.should raise_error(RuntimeError) + end + end + + it "should return an array of module metadata hashes" do + run do + results = <<-HERE + [ + {"full_module_name": "cli", "version": "1.0"}, + {"full_module_name": "web", "version": "2.0"} + ] + HERE + expected = [ + {"version"=>"1.0", "full_module_name"=>"cli"}, + {"version"=>"2.0", "full_module_name"=>"web"} + ] + stub_repository_read 200, results + return_value = Puppet::Module::Tool::Applications::Searcher.run("mymodule", @options) + return_value.should == expected + return_value.should be_kind_of(Array) + end + end + end + + describe "install" do + it "should install a module to the puppet modulepath by default" do + myothertmpdir = Pathname.new(tmpdir("module_tool_test_myothertmpdir")) + run do + @options[:install_dir] = myothertmpdir + Puppet::Module::Tool.unstub(:install_dir) + + build_and_install_module + + File.directory?(myothertmpdir + @module_name).should == true + File.file?(myothertmpdir + @module_name + 'metadata.json').should == true + end + end + + it "should install a module from a filesystem path" do + run do + build_and_install_module + + File.directory?(@mytmpdir + @module_name).should == true + File.file?(@mytmpdir + @module_name + 'metadata.json').should == true + end + end + + it "should install a module from a webserver URL" do + run do + Puppet::Module::Tool::Applications::Generator.run(@full_module_name) + Puppet::Module::Tool::Applications::Builder.run(@full_module_name) + + stub_cache_read File.read("#{@full_module_name}/pkg/#{@release_name}.tar.gz") + FileUtils.rm_rf(@full_module_name) + + stub_installer_read <<-HERE + {"file": "/foo/bar/#{@release_name}.tar.gz", "version": "#{@version}"} + HERE + + Puppet::Module::Tool::Applications::Installer.run(@full_module_name, @options) + + File.directory?(@mytmpdir + @module_name).should == true + File.file?(@mytmpdir + @module_name + 'metadata.json').should == true + end + end + + it "should install a module from a webserver URL using a version requirement" # TODO + + it "should fail if module isn't a slashed name" do + run do + lambda { Puppet::Module::Tool::Applications::Installer.run("invalid") }.should raise_error(RuntimeError) + end + end + + it "should fail if module doesn't exist on webserver" do + run do + stub_installer_read "{}" + lambda { Puppet::Module::Tool::Applications::Installer.run("not-found", @options) }.should raise_error(RuntimeError) + end + end + + it "should fail gracefully when receiving invalid PSON" do + pending "Implement PSON error wrapper" # TODO + run do + stub_installer_read "1/0" + lambda { Puppet::Module::Tool::Applications::Installer.run("not-found") }.should raise_error(SystemExit) + end + end + + it "should fail if installing a module that's already installed" do + run do + name = "myuser-mymodule" + Dir.mkdir name + lambda { Puppet::Module::Tool::Applications::Installer.run(name) }.should raise_error(ArgumentError) + end + end + + it "should return a Pathname object representing the path to the installed module" do + run do + Puppet::Module::Tool::Applications::Generator.run(@full_module_name) + Puppet::Module::Tool::Applications::Builder.run(@full_module_name) + + stub_cache_read File.read("#{@full_module_name}/pkg/#{@release_name}.tar.gz") + FileUtils.rm_rf(@full_module_name) + + stub_installer_read <<-HERE + {"file": "/foo/bar/#{@release_name}.tar.gz", "version": "#{@version}"} + HERE + + Puppet::Module::Tool::Applications::Installer.run(@full_module_name, @options).should be_kind_of(Pathname) + end + end + + end + + describe "clean" do + require 'puppet/module_tool' + it "should clean cache" do + run do + build_and_install_module + Puppet::Module::Tool::Cache.base_path.directory?.should == true + Puppet::Module::Tool::Applications::Cleaner.run + Puppet::Module::Tool::Cache.base_path.directory?.should == false + end + end + + it "should return a status Hash" do + run do + build_and_install_module + return_value = Puppet::Module::Tool::Applications::Cleaner.run + return_value.should include(:msg) + return_value.should include(:status) + return_value.should be_kind_of(Hash) + end + end + end + + describe "changes" do + it "should return an array of modified files" do + run do + Puppet::Module::Tool::Applications::Generator.run(@full_module_name) + Puppet::Module::Tool::Applications::Builder.run(@full_module_name) + Dir.chdir("#{@full_module_name}/pkg/#{@release_name}") + File.open("Modulefile", "a") do |handle| + handle.puts + handle.puts "# Added" + end + return_value = Puppet::Module::Tool::Applications::Checksummer.run(".") + return_value.should include("Modulefile") + return_value.should be_kind_of(Array) + end + end + end +end diff --git a/spec/integration/provider/package_spec.rb b/spec/integration/provider/package_spec.rb index 9f4ea1eee..e0d628f4b 100755 --- a/spec/integration/provider/package_spec.rb +++ b/spec/integration/provider/package_spec.rb @@ -27,6 +27,13 @@ describe "Package provider", :'fails_on_ruby_1.9.2' => true do Puppet[:vardir] = tmpdir('msi_package_var_dir') end + # the instances method requires root priviledges on gentoo + # if the eix cache is outdated (to run eix-update) so make + # sure we dont actually run eix-update + if provider.name == :portage + provider.stubs(:update_eix).returns('Database contains 15240 packages in 155 categories') + end + provider.instances.each do |package| package.should be_instance_of(provider) package.properties[:provider].should == provider.name diff --git a/spec/unit/agent/disabler_spec.rb b/spec/unit/agent/disabler_spec.rb new file mode 100644 index 000000000..5e43cf964 --- /dev/null +++ b/spec/unit/agent/disabler_spec.rb @@ -0,0 +1,60 @@ +#!/usr/bin/env rspec +require 'spec_helper' +require 'puppet/agent' +require 'puppet/agent/locker' + +class LockerTester + include Puppet::Agent::Disabler +end + +describe Puppet::Agent::Disabler do + before do + @locker = LockerTester.new + @locker.stubs(:lockfile_path).returns "/my/lock" + end + + it "should use an AnonymousFilelock instance as its disable_lockfile" do + @locker.disable_lockfile.should be_instance_of(Puppet::Util::AnonymousFilelock) + end + + it "should use 'lockfile_path' to determine its disable_lockfile path" do + @locker.expects(:lockfile_path).returns "/my/lock" + lock = Puppet::Util::AnonymousFilelock.new("/my/lock") + Puppet::Util::AnonymousFilelock.expects(:new).with("/my/lock.disabled").returns lock + + @locker.disable_lockfile + end + + it "should reuse the same lock file each time" do + @locker.disable_lockfile.should equal(@locker.disable_lockfile) + end + + it "should lock the anonymous lock when disabled" do + @locker.disable_lockfile.expects(:lock) + + @locker.disable + end + + it "should disable with a message" do + @locker.disable_lockfile.expects(:lock).with("disabled because") + + @locker.disable("disabled because") + end + + it "should unlock the anonymous lock when enabled" do + @locker.disable_lockfile.expects(:unlock) + + @locker.enable + end + + it "should check the lock if it is disabled" do + @locker.disable_lockfile.expects(:locked?) + + @locker.disabled? + end + + it "should report the disable message when disabled" do + @locker.disable_lockfile.expects(:message).returns("message") + @locker.disable_message.should == "message" + end +end diff --git a/spec/unit/agent/locker_spec.rb b/spec/unit/agent/locker_spec.rb index 341859e3b..9b530c0d8 100755 --- a/spec/unit/agent/locker_spec.rb +++ b/spec/unit/agent/locker_spec.rb @@ -29,18 +29,6 @@ describe Puppet::Agent::Locker do @locker.lockfile.should equal(@locker.lockfile) end - it "should use the lock file to anonymously lock the process when disabled" do - @locker.lockfile.expects(:lock).with(:anonymous => true) - - @locker.disable - end - - it "should use the lock file to anonymously unlock the process when enabled" do - @locker.lockfile.expects(:unlock).with(:anonymous => true) - - @locker.enable - end - it "should have a method that yields when a lock is attained" do @locker.lockfile.expects(:lock).returns true diff --git a/spec/unit/agent_spec.rb b/spec/unit/agent_spec.rb index d955868a0..9e2840c58 100755 --- a/spec/unit/agent_spec.rb +++ b/spec/unit/agent_spec.rb @@ -24,6 +24,7 @@ describe Puppet::Agent do # So we don't actually try to hit the filesystem. @agent.stubs(:lock).yields + @agent.stubs(:disabled?).returns(false) # make Puppet::Application safe for stubbing; restore in an :after block; silence warnings for this. without_warnings { Puppet::Application = Class.new(Puppet::Application) } @@ -76,6 +77,7 @@ describe Puppet::Agent do describe "when being run" do before do + AgentTestClient.stubs(:lockfile_path).returns "/my/lock" @agent.stubs(:running?).returns false end @@ -92,6 +94,12 @@ describe Puppet::Agent do @agent.run end + it "should do nothing if disabled" do + @agent.expects(:disabled?).returns(true) + AgentTestClient.expects(:new).never + @agent.run + end + it "should use Puppet::Application.controlled_run to manage process state behavior" do calls = sequence('calls') Puppet::Application.expects(:controlled_run).yields.in_sequence(calls) diff --git a/spec/unit/application/agent_spec.rb b/spec/unit/application/agent_spec.rb index 6d31ec34a..13be1a5af 100755 --- a/spec/unit/application/agent_spec.rb +++ b/spec/unit/application/agent_spec.rb @@ -91,7 +91,7 @@ describe Puppet::Application::Agent do @puppetd.command_line.stubs(:args).returns([]) end - [:centrallogging, :disable, :enable, :debug, :fqdn, :test, :verbose, :digest].each do |option| + [:centrallogging, :enable, :debug, :fqdn, :test, :verbose, :digest].each do |option| it "should declare handle_#{option} method" do @puppetd.should respond_to("handle_#{option}".to_sym) end @@ -102,6 +102,24 @@ describe Puppet::Application::Agent do end end + describe "when handling --disable" do + it "should declare handle_disable method" do + @puppetd.should respond_to(:handle_disable) + end + + it "should set disable to true" do + @puppetd.options.stubs(:[]=) + @puppetd.options.expects(:[]=).with(:disable, true) + @puppetd.handle_disable('') + end + + it "should store disable message" do + @puppetd.options.stubs(:[]=) + @puppetd.options.expects(:[]=).with(:disable_message, "message") + @puppetd.handle_disable('message') + end + end + it "should set an existing handler on server" do Puppet::Network::Handler.stubs(:handler).with("handler").returns(true) @@ -349,6 +367,20 @@ describe Puppet::Application::Agent do end end + it "should pass the disable message when disabling" do + @puppetd.options.stubs(:[]).with(:disable).returns(true) + @puppetd.options.stubs(:[]).with(:disable_message).returns("message") + @agent.expects(:disable).with("message") + expect { @puppetd.enable_disable_client(@agent) }.to exit_with 0 + end + + it "should pass the default disable message when disabling without a message" do + @puppetd.options.stubs(:[]).with(:disable).returns(true) + @puppetd.options.stubs(:[]).with(:disable_message).returns(nil) + @agent.expects(:disable).with("reason not specified") + expect { @puppetd.enable_disable_client(@agent) }.to exit_with 0 + end + it "should finally exit" do expect { @puppetd.enable_disable_client(@agent) }.to exit_with 0 end @@ -437,6 +469,32 @@ describe Puppet::Application::Agent do @puppetd.setup_listen end end + + describe "when setting up for fingerprint" do + before(:each) do + @puppetd.options.stubs(:[]).with(:fingerprint).returns(true) + end + + it "should not setup as an agent" do + @puppetd.expects(:setup_agent).never + @puppetd.setup + end + + it "should not create an agent" do + Puppet::Agent.stubs(:new).with(Puppet::Configurer).never + @puppetd.setup + end + + it "should not daemonize" do + @daemon.expects(:daemonize).never + @puppetd.setup + end + + it "should setup our certificate host" do + @puppetd.expects(:setup_host) + @puppetd.setup + end + end end @@ -497,6 +555,11 @@ describe Puppet::Application::Agent do expect { @puppetd.onetime }.to exit_with 0 end + it "should stop the daemon" do + @daemon.expects(:stop).with(:exit => false) + expect { @puppetd.onetime }.to exit_with 0 + end + describe "and --detailed-exitcodes" do before :each do @puppetd.options.stubs(:[]).with(:detailed_exitcodes).returns(true) diff --git a/spec/unit/application/apply_spec.rb b/spec/unit/application/apply_spec.rb index b12c4fae9..07a39223c 100755 --- a/spec/unit/application/apply_spec.rb +++ b/spec/unit/application/apply_spec.rb @@ -20,7 +20,7 @@ describe Puppet::Application::Apply do Puppet::Node.indirection.cache_class = nil end - [:debug,:loadclasses,:verbose,:use_nodes,:detailed_exitcodes].each do |option| + [:debug,:loadclasses,:verbose,:use_nodes,:detailed_exitcodes,:catalog].each do |option| it "should declare handle_#{option} method" do @apply.should respond_to("handle_#{option}".to_sym) end @@ -53,6 +53,17 @@ describe Puppet::Application::Apply do @apply.handle_logdest("console") end + + it "should deprecate --apply" do + Puppet.expects(:warning).with do |arg| + arg.match(/--apply is deprecated/) + end + + command_line = Puppet::Util::CommandLine.new('puppet', ['apply', '--apply', 'catalog.json']) + apply = Puppet::Application::Apply.new(command_line) + apply.stubs(:run_command) + apply.run + end end describe "during setup" do diff --git a/spec/unit/application/cert_spec.rb b/spec/unit/application/cert_spec.rb index 300234c2b..8f0021ae6 100755 --- a/spec/unit/application/cert_spec.rb +++ b/spec/unit/application/cert_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' require 'puppet/application/cert' -describe Puppet::Application::Cert, :'fails_on_ruby_1.9.2' => true do +describe Puppet::Application::Cert => true do before :each do @cert_app = Puppet::Application[:cert] Puppet::Util::Log.stubs(:newdestination) diff --git a/spec/unit/application_spec.rb b/spec/unit/application_spec.rb index fd93ceb00..591358efb 100755 --- a/spec/unit/application_spec.rb +++ b/spec/unit/application_spec.rb @@ -8,6 +8,7 @@ require 'getoptlong' describe Puppet::Application do before do + Puppet::Util::Instrumentation.stubs(:init) @app = Class.new(Puppet::Application).new @appclass = @app.class @@ -117,6 +118,11 @@ describe Puppet::Application do @app.run_command end + it "should initialize the Puppet Instrumentation layer on creation" do + Puppet::Util::Instrumentation.expects(:init) + Class.new(Puppet::Application).new + end + describe 'when invoking clear!' do before :each do Puppet::Application.run_status = :stop_requested diff --git a/spec/unit/configurer/downloader_spec.rb b/spec/unit/configurer/downloader_spec.rb index 215ef897f..69e5e6bf9 100755 --- a/spec/unit/configurer/downloader_spec.rb +++ b/spec/unit/configurer/downloader_spec.rb @@ -60,16 +60,35 @@ describe Puppet::Configurer::Downloader do @dler.file end - it "should always set the owner to the current UID" do - Process.expects(:uid).returns 51 - Puppet::Type.type(:file).expects(:new).with { |opts| opts[:owner] == 51 } - @dler.file - end - - it "should always set the group to the current GID" do - Process.expects(:gid).returns 61 - Puppet::Type.type(:file).expects(:new).with { |opts| opts[:group] == 61 } - @dler.file + describe "on POSIX" do + before :each do + Puppet.features.stubs(:microsoft_windows?).returns false + end + + it "should always set the owner to the current UID" do + Process.expects(:uid).returns 51 + Puppet::Type.type(:file).expects(:new).with { |opts| opts[:owner] == 51 } + @dler.file + end + + it "should always set the group to the current GID" do + Process.expects(:gid).returns 61 + Puppet::Type.type(:file).expects(:new).with { |opts| opts[:group] == 61 } + @dler.file + end + end + + describe "on Windows", :if => Puppet.features.microsoft_windows? do + it "should always set the owner to the current user" do + Sys::Admin.expects(:get_login).returns 'foo' + Puppet::Type.type(:file).expects(:new).with { |opts| opts[:owner] == 'foo' } + @dler.file + end + + it "should always set the group to nobody" do + Puppet::Type.type(:file).expects(:new).with { |opts| opts[:group] == 'S-1-0-0' } + @dler.file + end end it "should always force the download" do diff --git a/spec/unit/configurer/fact_handler_spec.rb b/spec/unit/configurer/fact_handler_spec.rb index 4a3fe8b3b..41b4862ca 100755 --- a/spec/unit/configurer/fact_handler_spec.rb +++ b/spec/unit/configurer/fact_handler_spec.rb @@ -78,12 +78,6 @@ describe Puppet::Configurer::FactHandler do Puppet[:node_name_value].should == 'other_node_name' end - it "should reload Facter before finding facts" do - @facthandler.expects(:reload_facter) - - @facthandler.find_facts - end - it "should fail if finding facts fails" do Puppet[:trace] = false Puppet[:certname] = "myhost" @@ -93,6 +87,11 @@ describe Puppet::Configurer::FactHandler do end end + it "should only load fact plugins once" do + Puppet::Node::Facts.indirection.expects(:find).once + @facthandler.find_facts + end + it "should warn about factsync deprecation when factsync is enabled" do Puppet::Configurer::Downloader.stubs(:new).returns mock("downloader", :evaluate => nil) @@ -145,27 +144,4 @@ describe Puppet::Configurer::FactHandler do @facthandler.facts_for_uploading end - - describe "when reloading Facter" do - before do - Facter.stubs(:clear) - Facter.stubs(:load) - Facter.stubs(:loadfacts) - end - - it "should clear Facter" do - Facter.expects(:clear) - @facthandler.reload_facter - end - - it "should load all Facter facts" do - Facter.expects(:loadfacts) - @facthandler.reload_facter - end - - it "should use the Facter terminus load all Puppet Fact plugins" do - Puppet::Node::Facts::Facter.expects(:load_fact_plugins) - @facthandler.reload_facter - end - end end diff --git a/spec/unit/configurer_spec.rb b/spec/unit/configurer_spec.rb index 5c660cc31..5afed1f58 100755 --- a/spec/unit/configurer_spec.rb +++ b/spec/unit/configurer_spec.rb @@ -407,33 +407,33 @@ describe Puppet::Configurer do end describe "when saving the summary report file" do + include PuppetSpec::Files + before do Puppet.settings.stubs(:use).returns(true) @configurer = Puppet::Configurer.new - @report = stub 'report' - @trans = stub 'transaction' - @lastrunfd = stub 'lastrunfd' - Puppet::Util::FileLocking.stubs(:writelock).yields(@lastrunfd) + @report = stub 'report', :raw_summary => {} + + Puppet[:lastrunfile] = tmpfile('last_run_file') end - it "should write the raw summary to the lastrunfile setting value" do - Puppet::Util::FileLocking.expects(:writelock).with(Puppet[:lastrunfile], 0660) + it "should write the last run file" do @configurer.save_last_run_summary(@report) + FileTest.exists?(Puppet[:lastrunfile]).should be_true end it "should write the raw summary as yaml" do @report.expects(:raw_summary).returns("summary") - @lastrunfd.expects(:print).with(YAML.dump("summary")) @configurer.save_last_run_summary(@report) + File.read(Puppet[:lastrunfile]).should == YAML.dump("summary") end it "should log but not fail if saving the last run summary fails" do - Puppet::Util::FileLocking.expects(:writelock).raises "exception" + Puppet[:lastrunfile] = "/dev/null/inexistant" Puppet.expects(:err) lambda { @configurer.save_last_run_summary(@report) }.should_not raise_error end - end describe "when retrieving a catalog" do diff --git a/spec/unit/face/instrumentation_data.rb b/spec/unit/face/instrumentation_data.rb new file mode 100644 index 000000000..2d4cc74f6 --- /dev/null +++ b/spec/unit/face/instrumentation_data.rb @@ -0,0 +1,7 @@ +#!/usr/bin/env rspec +require 'spec_helper' +require 'puppet/face' + +describe Puppet::Face[:instrumentation_data, '0.0.1'] do + it_should_behave_like "an indirector face" +end diff --git a/spec/unit/face/instrumentation_listener.rb b/spec/unit/face/instrumentation_listener.rb new file mode 100644 index 000000000..87f218855 --- /dev/null +++ b/spec/unit/face/instrumentation_listener.rb @@ -0,0 +1,38 @@ +#!/usr/bin/env rspec +require 'spec_helper' +require 'puppet/face' + +describe Puppet::Face[:instrumentation_listener, '0.0.1'] do + it_should_behave_like "an indirector face" + + [:enable, :disable].each do |m| + describe "when running ##{m}" do + before(:each) do + @listener = stub_everything 'listener' + Puppet::Face[:instrumentation_listener, '0.0.1'].stubs(:find).returns(@listener) + Puppet::Face[:instrumentation_listener, '0.0.1'].stubs(:save) + Puppet::Util::Instrumentation::Listener.indirection.stubs(:terminus_class=) + end + + it "should force the REST terminus" do + Puppet::Util::Instrumentation::Listener.indirection.expects(:terminus_class=).with(:rest) + subject.send(m, "dummy") + end + + it "should find the named listener" do + Puppet::Face[:instrumentation_listener, '0.0.1'].expects(:find).with("dummy").returns(@listener) + subject.send(m, "dummy") + end + + it "should #{m} the named listener" do + @listener.expects(:enabled=).with( m == :enable ) + subject.send(m, "dummy") + end + + it "should save finally the listener" do + Puppet::Face[:instrumentation_listener, '0.0.1'].expects(:save).with(@listener) + subject.send(m, "dummy") + end + end + end +end diff --git a/spec/unit/face/instrumentation_probe.rb b/spec/unit/face/instrumentation_probe.rb new file mode 100644 index 000000000..3e475906d --- /dev/null +++ b/spec/unit/face/instrumentation_probe.rb @@ -0,0 +1,21 @@ +#!/usr/bin/env rspec +require 'spec_helper' +require 'puppet/face' + +describe Puppet::Face[:instrumentation_probe, '0.0.1'] do + it_should_behave_like "an indirector face" + + describe 'when running #enable' do + it 'should invoke #save' do + subject.expects(:save).with(nil) + subject.enable('hostname') + end + end + + describe 'when running #disable' do + it 'should invoke #destroy' do + subject.expects(:destroy).with(nil) + subject.disable('hostname') + end + end +end diff --git a/spec/unit/face/module_spec.rb b/spec/unit/face/module_spec.rb new file mode 100644 index 000000000..33e486e5a --- /dev/null +++ b/spec/unit/face/module_spec.rb @@ -0,0 +1,3 @@ +# For face related tests, look in the spec/unit/faces/module folder. +# For integration tests, which test the behavior of module, look in the +# spec/unit/integration folder. diff --git a/spec/unit/face/node_spec.rb b/spec/unit/face/node_spec.rb index cb36e58a4..b126af6bb 100755 --- a/spec/unit/face/node_spec.rb +++ b/spec/unit/face/node_spec.rb @@ -14,9 +14,7 @@ describe Puppet::Face[:node, '0.0.1'] do "cached_facts" => ['hostname'], "cached_node" => ['hostname'], "reports" => ['hostname'], - - # Support for cleaning storeconfigs has been temporarily suspended. - # "storeconfigs" => ['hostname', :unexport] + "storeconfigs" => ['hostname', :unexport] }.each { |k, v| subject.expects("clean_#{k}".to_sym).with(*v) } subject.cleanup('hostname', :unexport) end @@ -158,114 +156,116 @@ describe Puppet::Face[:node, '0.0.1'] do end end - # describe "when cleaning storeconfigs entries for host", :if => Puppet.features.rails? do - # before :each do - # # Stub this so we don't need access to the DB - # require 'puppet/rails/host' - # - # Puppet.stubs(:[]).with(:storeconfigs).returns(true) - # - # Puppet::Rails.stubs(:connect) - # @rails_node = stub_everything 'rails_node' - # Puppet::Rails::Host.stubs(:find_by_name).returns(@rails_node) - # end - # - # it "should connect to the database" do - # Puppet::Rails.expects(:connect) - # subject.clean_storeconfigs(@host, false) - # end - # - # it "should find the right host entry" do - # Puppet::Rails::Host.expects(:find_by_name).with(@host).returns(@rails_node) - # subject.clean_storeconfigs(@host, false) - # end - # - # describe "without unexport" do - # it "should remove the host and it's content" do - # @rails_node.expects(:destroy) - # subject.clean_storeconfigs(@host, false) - # end - # end - # - # describe "with unexport" do - # before :each do - # @rails_node.stubs(:id).returns(1234) - # - # @type = stub_everything 'type' - # @type.stubs(:validattr?).with(:ensure).returns(true) - # - # @ensure_name = stub_everything 'ensure_name', :id => 23453 - # Puppet::Rails::ParamName.stubs(:find_or_create_by_name).returns(@ensure_name) - # - # @param_values = stub_everything 'param_values' - # @resource = stub_everything 'resource', :param_values => @param_values, :restype => "File" - # Puppet::Rails::Resource.stubs(:find).returns([@resource]) - # end - # - # it "should find all resources" do - # Puppet::Rails::Resource.expects(:find).with(:all, {:include => {:param_values => :param_name}, :conditions => ["exported=? AND host_id=?", true, 1234]}).returns([]) - # - # subject.clean_storeconfigs(@host, true) - # end - # - # describe "with an exported native type" do - # before :each do - # Puppet::Type.stubs(:type).returns(@type) - # @type.expects(:validattr?).with(:ensure).returns(true) - # end - # - # it "should test a native type for ensure as an attribute" do - # subject.clean_storeconfigs(@host, true) - # end - # - # it "should delete the old ensure parameter" do - # ensure_param = stub 'ensure_param', :id => 12345, :line => 12 - # @param_values.stubs(:find).returns(ensure_param) - # Puppet::Rails::ParamValue.expects(:delete).with(12345); - # subject.clean_storeconfigs(@host, true) - # end - # - # it "should add an ensure => absent parameter" do - # @param_values.expects(:create).with(:value => "absent", - # :line => 0, - # :param_name => @ensure_name) - # subject.clean_storeconfigs(@host, true) - # end - # end - # - # describe "with an exported definition" do - # it "should try to lookup a definition and test it for the ensure argument" do - # Puppet::Type.stubs(:type).returns(nil) - # definition = stub_everything 'definition', :arguments => { 'ensure' => 'present' } - # Puppet::Resource::TypeCollection.any_instance.expects(:find_definition).with('', "File").returns(definition) - # subject.clean_storeconfigs(@host, true) - # end - # end - # - # it "should not unexport the resource of an unknown type" do - # Puppet::Type.stubs(:type).returns(nil) - # Puppet::Resource::TypeCollection.any_instance.expects(:find_definition).with('', "File").returns(nil) - # Puppet::Rails::ParamName.expects(:find_or_create_by_name).never - # subject.clean_storeconfigs(@host) - # end - # - # it "should not unexport the resource of a not ensurable native type" do - # Puppet::Type.stubs(:type).returns(@type) - # @type.expects(:validattr?).with(:ensure).returns(false) - # Puppet::Resource::TypeCollection.any_instance.expects(:find_definition).with('', "File").returns(nil) - # Puppet::Rails::ParamName.expects(:find_or_create_by_name).never - # subject.clean_storeconfigs(@host, true) - # end - # - # it "should not unexport the resource of a not ensurable definition" do - # Puppet::Type.stubs(:type).returns(nil) - # definition = stub_everything 'definition', :arguments => { 'foobar' => 'someValue' } - # Puppet::Resource::TypeCollection.any_instance.expects(:find_definition).with('', "File").returns(definition) - # Puppet::Rails::ParamName.expects(:find_or_create_by_name).never - # subject.clean_storeconfigs(@host, true) - # end - # end - # end + describe "when cleaning storeconfigs entries for host", :if => Puppet.features.rails? do + before :each do + # Stub this so we don't need access to the DB + require 'puppet/rails/host' + + Puppet.stubs(:[]).with(:storeconfigs).returns(true) + + Puppet::Rails.stubs(:connect) + @rails_node = stub_everything 'rails_node' + Puppet::Rails::Host.stubs(:find_by_name).returns(@rails_node) + end + + it "should connect to the database" do + Puppet::Rails.expects(:connect) + subject.clean_storeconfigs(@host, false) + end + + it "should find the right host entry" do + Puppet::Rails::Host.expects(:find_by_name).with(@host).returns(@rails_node) + + subject.clean_storeconfigs(@host, false) + end + + describe "without unexport" do + it "should remove the host and it's content" do + @rails_node.expects(:destroy) + + subject.clean_storeconfigs(@host, false) + end + end + + describe "with unexport" do + before :each do + @rails_node.stubs(:id).returns(1234) + + @type = stub_everything 'type' + @type.stubs(:validattr?).with(:ensure).returns(true) + + @ensure_name = stub_everything 'ensure_name', :id => 23453 + Puppet::Rails::ParamName.stubs(:find_or_create_by_name).returns(@ensure_name) + + @param_values = stub_everything 'param_values' + @resource = stub_everything 'resource', :param_values => @param_values, :restype => "File" + Puppet::Rails::Resource.stubs(:find).returns([@resource]) + end + + it "should find all resources" do + Puppet::Rails::Resource.expects(:find).with(:all, {:include => {:param_values => :param_name}, :conditions => ["exported=? AND host_id=?", true, 1234]}).returns([]) + + subject.clean_storeconfigs(@host, true) + end + + describe "with an exported native type" do + before :each do + Puppet::Type.stubs(:type).returns(@type) + @type.expects(:validattr?).with(:ensure).returns(true) + end + + it "should test a native type for ensure as an attribute" do + subject.clean_storeconfigs(@host, true) + end + + it "should delete the old ensure parameter" do + ensure_param = stub 'ensure_param', :id => 12345, :line => 12 + @param_values.stubs(:find).returns(ensure_param) + Puppet::Rails::ParamValue.expects(:delete).with(12345); + subject.clean_storeconfigs(@host, true) + end + + it "should add an ensure => absent parameter" do + @param_values.expects(:create).with(:value => "absent", + :line => 0, + :param_name => @ensure_name) + subject.clean_storeconfigs(@host, true) + end + end + + describe "with an exported definition" do + it "should try to lookup a definition and test it for the ensure argument" do + Puppet::Type.stubs(:type).returns(nil) + definition = stub_everything 'definition', :arguments => { 'ensure' => 'present' } + Puppet::Resource::TypeCollection.any_instance.expects(:find_definition).with('', "File").returns(definition) + subject.clean_storeconfigs(@host, true) + end + end + + it "should not unexport the resource of an unkown type" do + Puppet::Type.stubs(:type).returns(nil) + Puppet::Resource::TypeCollection.any_instance.expects(:find_definition).with('', "File").returns(nil) + Puppet::Rails::ParamName.expects(:find_or_create_by_name).never + subject.clean_storeconfigs(@host, true) + end + + it "should not unexport the resource of a not ensurable native type" do + Puppet::Type.stubs(:type).returns(@type) + @type.expects(:validattr?).with(:ensure).returns(false) + Puppet::Resource::TypeCollection.any_instance.expects(:find_definition).with('', "File").returns(nil) + Puppet::Rails::ParamName.expects(:find_or_create_by_name).never + subject.clean_storeconfigs(@host, true) + end + + it "should not unexport the resource of a not ensurable definition" do + Puppet::Type.stubs(:type).returns(nil) + definition = stub_everything 'definition', :arguments => { 'foobar' => 'someValue' } + Puppet::Resource::TypeCollection.any_instance.expects(:find_definition).with('', "File").returns(definition) + Puppet::Rails::ParamName.expects(:find_or_create_by_name).never + subject.clean_storeconfigs(@host, true) + end + end + end end end end diff --git a/spec/unit/file_serving/content_spec.rb b/spec/unit/file_serving/content_spec.rb index 335f0e701..c1627c18f 100755 --- a/spec/unit/file_serving/content_spec.rb +++ b/spec/unit/file_serving/content_spec.rb @@ -102,13 +102,13 @@ describe Puppet::FileServing::Content, "when returning the contents" do it "should return the contents of the path if the file exists" do File.expects(:stat).with(@path).returns stub("stat", :ftype => "file") - File.expects(:read).with(@path).returns(:mycontent) + Puppet::Util.expects(:binread).with(@path).returns(:mycontent) @content.content.should == :mycontent end it "should cache the returned contents" do File.expects(:stat).with(@path).returns stub("stat", :ftype => "file") - File.expects(:read).with(@path).returns(:mycontent) + Puppet::Util.expects(:binread).with(@path).returns(:mycontent) @content.content # The second run would throw a failure if the content weren't being cached. diff --git a/spec/unit/indirector/facts/facter_spec.rb b/spec/unit/indirector/facts/facter_spec.rb index 3b1574e52..0cb8037aa 100755 --- a/spec/unit/indirector/facts/facter_spec.rb +++ b/spec/unit/indirector/facts/facter_spec.rb @@ -21,14 +21,29 @@ describe Puppet::Node::Facts::Facter do Puppet::Node::Facts::Facter.name.should == :facter end - it "should load facts on initialization" do - Puppet::Node::Facts::Facter.expects(:load_fact_plugins) - Puppet::Node::Facts::Facter.new + describe "when reloading Facter" do + before do + @facter_class = Puppet::Node::Facts::Facter + Facter.stubs(:clear) + Facter.stubs(:load) + Facter.stubs(:loadfacts) + end + + it "should clear Facter" do + Facter.expects(:clear) + @facter_class.reload_facter + end + + it "should load all Facter facts" do + Facter.expects(:loadfacts) + @facter_class.reload_facter + end end end describe Puppet::Node::Facts::Facter do before :each do + Puppet::Node::Facts::Facter.stubs(:reload_facter) @facter = Puppet::Node::Facts::Facter.new Facter.stubs(:to_hash).returns({}) @name = "me" @@ -36,6 +51,13 @@ describe Puppet::Node::Facts::Facter do end describe Puppet::Node::Facts::Facter, " when finding facts" do + it "should reset and load facts" do + clear = sequence 'clear' + Puppet::Node::Facts::Facter.expects(:reload_facter).in_sequence(clear) + Puppet::Node::Facts::Facter.expects(:load_fact_plugins).in_sequence(clear) + @facter.find(@request) + end + it "should return a Facts instance" do @facter.find(@request).should be_instance_of(Puppet::Node::Facts) end diff --git a/spec/unit/indirector/facts/inventory_active_record_spec.rb b/spec/unit/indirector/facts/inventory_active_record_spec.rb index 88e5e5359..43dcc7c6b 100755 --- a/spec/unit/indirector/facts/inventory_active_record_spec.rb +++ b/spec/unit/indirector/facts/inventory_active_record_spec.rb @@ -38,10 +38,22 @@ describe "Puppet::Node::Facts::InventoryActiveRecord", :if => (Puppet.features.r end describe "#save" do + let(:node) { + Puppet::Rails::InventoryNode.new(:name => "foo", :timestamp => Time.now) + } + + let(:facts) { + Puppet::Node::Facts.new("foo", "uptime_days" => "60", "kernel" => "Darwin") + } + + it "should retry on ActiveRecord error" do + Puppet::Rails::InventoryNode.expects(:create).twice.raises(ActiveRecord::StatementInvalid).returns node + + Puppet::Node::Facts.indirection.save(facts) + end + it "should use an existing node if possible" do - node = Puppet::Rails::InventoryNode.new(:name => "foo", :timestamp => Time.now) node.save - facts = Puppet::Node::Facts.new("foo", "uptime_days" => "60", "kernel" => "Darwin") Puppet::Node::Facts.indirection.save(facts) Puppet::Rails::InventoryNode.count.should == 1 @@ -52,7 +64,6 @@ describe "Puppet::Node::Facts::InventoryActiveRecord", :if => (Puppet.features.r # This test isn't valid if there are nodes to begin with Puppet::Rails::InventoryNode.count.should == 0 - facts = Puppet::Node::Facts.new("foo", "uptime_days" => "60", "kernel" => "Darwin") Puppet::Node::Facts.indirection.save(facts) Puppet::Rails::InventoryNode.count.should == 1 @@ -60,7 +71,6 @@ describe "Puppet::Node::Facts::InventoryActiveRecord", :if => (Puppet.features.r end it "should save the facts" do - facts = Puppet::Node::Facts.new("foo", "uptime_days" => "60", "kernel" => "Darwin") Puppet::Node::Facts.indirection.save(facts) Puppet::Rails::InventoryFact.all.map{|f| [f.name,f.value]}.should =~ [["uptime_days","60"],["kernel","Darwin"]] diff --git a/spec/unit/indirector/instrumentation_data/local_spec.rb b/spec/unit/indirector/instrumentation_data/local_spec.rb new file mode 100644 index 000000000..45ca1e07e --- /dev/null +++ b/spec/unit/indirector/instrumentation_data/local_spec.rb @@ -0,0 +1,52 @@ +#!/usr/bin/env rspec +require 'spec_helper' + +require 'puppet/util/instrumentation/listener' +require 'puppet/indirector/instrumentation_data/local' + +describe Puppet::Indirector::InstrumentationData::Local do + it "should be a subclass of the Code terminus" do + Puppet::Indirector::InstrumentationData::Local.superclass.should equal(Puppet::Indirector::Code) + end + + it "should be registered with the configuration store indirection" do + indirection = Puppet::Indirector::Indirection.instance(:instrumentation_data) + Puppet::Indirector::InstrumentationData::Local.indirection.should equal(indirection) + end + + it "should have its name set to :local" do + Puppet::Indirector::InstrumentationData::Local.name.should == :local + end +end + +describe Puppet::Indirector::InstrumentationData::Local do + before :each do + Puppet::Util::Instrumentation.stubs(:listener) + @data = Puppet::Indirector::InstrumentationData::Local.new + @name = "me" + @request = stub 'request', :key => @name + end + + describe "when finding instrumentation data" do + it "should return a Instrumentation Data instance matching the key" do + end + end + + describe "when searching listeners" do + it "should raise an error" do + lambda { @data.search(@request) }.should raise_error(Puppet::DevError) + end + end + + describe "when saving listeners" do + it "should raise an error" do + lambda { @data.save(@request) }.should raise_error(Puppet::DevError) + end + end + + describe "when destroying listeners" do + it "should raise an error" do + lambda { @data.destroy(@reques) }.should raise_error(Puppet::DevError) + end + end +end diff --git a/spec/unit/indirector/instrumentation_data/rest_spec.rb b/spec/unit/indirector/instrumentation_data/rest_spec.rb new file mode 100644 index 000000000..762667ea7 --- /dev/null +++ b/spec/unit/indirector/instrumentation_data/rest_spec.rb @@ -0,0 +1,11 @@ +#!/usr/bin/env rspec +require 'spec_helper' + +require 'puppet/util/instrumentation/data' +require 'puppet/indirector/instrumentation_data/rest' + +describe Puppet::Indirector::InstrumentationData::Rest do + it "should be a subclass of Puppet::Indirector::REST" do + Puppet::Indirector::InstrumentationData::Rest.superclass.should equal(Puppet::Indirector::REST) + end +end diff --git a/spec/unit/indirector/instrumentation_listener/local_spec.rb b/spec/unit/indirector/instrumentation_listener/local_spec.rb new file mode 100644 index 000000000..b251736b4 --- /dev/null +++ b/spec/unit/indirector/instrumentation_listener/local_spec.rb @@ -0,0 +1,65 @@ +#!/usr/bin/env rspec +require 'spec_helper' + +require 'puppet/util/instrumentation/listener' +require 'puppet/indirector/instrumentation_listener/local' + +describe Puppet::Indirector::InstrumentationListener::Local do + it "should be a subclass of the Code terminus" do + Puppet::Indirector::InstrumentationListener::Local.superclass.should equal(Puppet::Indirector::Code) + end + + it "should be registered with the configuration store indirection" do + indirection = Puppet::Indirector::Indirection.instance(:instrumentation_listener) + Puppet::Indirector::InstrumentationListener::Local.indirection.should equal(indirection) + end + + it "should have its name set to :local" do + Puppet::Indirector::InstrumentationListener::Local.name.should == :local + end +end + +describe Puppet::Indirector::InstrumentationListener::Local do + before :each do + Puppet::Util::Instrumentation.stubs(:listener) + @listener = Puppet::Indirector::InstrumentationListener::Local.new + @name = "me" + @request = stub 'request', :key => @name + end + + describe "when finding listeners" do + it "should return a Instrumentation Listener instance matching the key" do + Puppet::Util::Instrumentation.expects(:[]).with("me").returns(:instance) + @listener.find(@request).should == :instance + end + end + + describe "when searching listeners" do + it "should return a list of all loaded Instrumentation Listenesrs irregardless of the given key" do + Puppet::Util::Instrumentation.expects(:listeners).returns([:instance1, :instance2]) + @listener.search(@request).should == [:instance1, :instance2] + end + end + + describe "when saving listeners" do + it "should set the new listener to the global listener list" do + newlistener = stub 'listener', :name => @name + @request.stubs(:instance).returns(newlistener) + Puppet::Util::Instrumentation.expects(:[]=).with("me", newlistener) + @listener.save(@request) + end + end + + describe "when destroying listeners" do + it "should raise an error if listener wasn't subscribed" do + Puppet::Util::Instrumentation.expects(:[]).with("me").returns(nil) + lambda { @listener.destroy(@request) }.should raise_error + end + + it "should unsubscribe the listener" do + Puppet::Util::Instrumentation.expects(:[]).with("me").returns(:instancce) + Puppet::Util::Instrumentation.expects(:unsubscribe).with(:instancce) + @listener.destroy(@request) + end + end +end diff --git a/spec/unit/indirector/instrumentation_listener/rest_spec.rb b/spec/unit/indirector/instrumentation_listener/rest_spec.rb new file mode 100644 index 000000000..6355a1c53 --- /dev/null +++ b/spec/unit/indirector/instrumentation_listener/rest_spec.rb @@ -0,0 +1,11 @@ +#!/usr/bin/env rspec +require 'spec_helper' + +require 'puppet/util/instrumentation/listener' +require 'puppet/indirector/instrumentation_listener/rest' + +describe Puppet::Indirector::InstrumentationListener::Rest do + it "should be a subclass of Puppet::Indirector::REST" do + Puppet::Indirector::InstrumentationListener::Rest.superclass.should equal(Puppet::Indirector::REST) + end +end diff --git a/spec/unit/indirector/instrumentation_probe/local_spec.rb b/spec/unit/indirector/instrumentation_probe/local_spec.rb new file mode 100644 index 000000000..40d64dc2b --- /dev/null +++ b/spec/unit/indirector/instrumentation_probe/local_spec.rb @@ -0,0 +1,65 @@ +#!/usr/bin/env rspec +require 'spec_helper' + +require 'puppet/util/instrumentation/indirection_probe' +require 'puppet/indirector/instrumentation_probe/local' +require 'puppet/util/instrumentation/instrumentable' + +describe Puppet::Indirector::InstrumentationProbe::Local do + it "should be a subclass of the Code terminus" do + Puppet::Indirector::InstrumentationProbe::Local.superclass.should equal(Puppet::Indirector::Code) + end + + it "should be registered with the configuration store indirection" do + indirection = Puppet::Indirector::Indirection.instance(:instrumentation_probe) + Puppet::Indirector::InstrumentationProbe::Local.indirection.should equal(indirection) + end + + it "should have its name set to :local" do + Puppet::Indirector::InstrumentationProbe::Local.name.should == :local + end +end + +describe Puppet::Indirector::InstrumentationProbe::Local do + before :each do + Puppet::Util::Instrumentation.stubs(:listener) + @probe = Puppet::Indirector::InstrumentationProbe::Local.new + @name = "me" + @request = stub 'request', :key => @name + end + + describe "when finding probes" do + it "should do nothing" do + @probe.find(@request).should be_nil + end + end + + describe "when searching probes" do + it "should return a list of all loaded probes irregardless of the given key" do + instance1 = stub 'instance1', :method => "probe1", :klass => "Klass1" + instance2 = stub 'instance2', :method => "probe2", :klass => "Klass2" + Puppet::Util::Instrumentation::IndirectionProbe.expects(:new).with("Klass1.probe1").returns(:instance1) + Puppet::Util::Instrumentation::IndirectionProbe.expects(:new).with("Klass2.probe2").returns(:instance2) + Puppet::Util::Instrumentation::Instrumentable.expects(:each_probe).multiple_yields([instance1], [instance2]) + @probe.search(@request).should == [ :instance1, :instance2 ] + end + end + + describe "when saving probes" do + it "should enable probes" do + newprobe = stub 'probe', :name => @name + @request.stubs(:instance).returns(newprobe) + Puppet::Util::Instrumentation::Instrumentable.expects(:enable_probes) + @probe.save(@request) + end + end + + describe "when destroying probes" do + it "should disable probes" do + newprobe = stub 'probe', :name => @name + @request.stubs(:instance).returns(newprobe) + Puppet::Util::Instrumentation::Instrumentable.expects(:disable_probes) + @probe.destroy(@request) + end + end +end diff --git a/spec/unit/indirector/instrumentation_probe/rest_spec.rb b/spec/unit/indirector/instrumentation_probe/rest_spec.rb new file mode 100644 index 000000000..0b73fbdf5 --- /dev/null +++ b/spec/unit/indirector/instrumentation_probe/rest_spec.rb @@ -0,0 +1,11 @@ +#!/usr/bin/env rspec +require 'spec_helper' + +require 'puppet/util/instrumentation/indirection_probe' +require 'puppet/indirector/instrumentation_probe/rest' + +describe Puppet::Indirector::InstrumentationProbe::Rest do + it "should be a subclass of Puppet::Indirector::REST" do + Puppet::Indirector::InstrumentationProbe::Rest.superclass.should equal(Puppet::Indirector::REST) + end +end diff --git a/spec/unit/module_spec.rb b/spec/unit/module_spec.rb index a0f64c6d3..e1172497d 100755 --- a/spec/unit/module_spec.rb +++ b/spec/unit/module_spec.rb @@ -89,83 +89,6 @@ describe Puppet::Module do lambda { mod.validate_puppet_version }.should raise_error(Puppet::Module::IncompatibleModule) end - describe "when specifying required modules" do - it "should support specifying a required module" do - mod = Puppet::Module.new("mymod") - mod.requires "foobar" - end - - it "should support specifying multiple required modules" do - mod = Puppet::Module.new("mymod") - mod.requires "foobar" - mod.requires "baz" - end - - it "should support specifying a required module and version" do - mod = Puppet::Module.new("mymod") - mod.requires "foobar", 1.0 - end - - it "should fail when required modules are missing" do - mod = Puppet::Module.new("mymod") - mod.requires "foobar" - - mod.environment.expects(:module).with("foobar").returns nil - - lambda { mod.validate_dependencies }.should raise_error(Puppet::Module::MissingModule) - end - - it "should fail when required modules are present but of the wrong version" do - mod = Puppet::Module.new("mymod") - mod.requires "foobar", 1.0 - - foobar = Puppet::Module.new("foobar") - foobar.version = 2.0 - - mod.environment.expects(:module).with("foobar").returns foobar - - lambda { mod.validate_dependencies }.should raise_error(Puppet::Module::IncompatibleModule) - end - - it "should have valid dependencies when no dependencies have been specified" do - mod = Puppet::Module.new("mymod") - - lambda { mod.validate_dependencies }.should_not raise_error - end - - it "should fail when some dependencies are present but others aren't" do - mod = Puppet::Module.new("mymod") - mod.requires "foobar" - mod.requires "baz" - - mod.environment.expects(:module).with("foobar").returns Puppet::Module.new("foobar") - mod.environment.expects(:module).with("baz").returns nil - - lambda { mod.validate_dependencies }.should raise_error(Puppet::Module::MissingModule) - end - - it "should have valid dependencies when all dependencies are met" do - mod = Puppet::Module.new("mymod") - mod.requires "foobar", 1.0 - mod.requires "baz" - - foobar = Puppet::Module.new("foobar") - foobar.version = 1.0 - - baz = Puppet::Module.new("baz") - - mod.environment.expects(:module).with("foobar").returns foobar - mod.environment.expects(:module).with("baz").returns baz - - lambda { mod.validate_dependencies }.should_not raise_error - end - - it "should validate its dependendencies on initialization" do - Puppet::Module.any_instance.expects(:validate_dependencies) - Puppet::Module.new("mymod") - end - end - describe "when managing supported platforms" do it "should support specifying a supported platform" do mod = Puppet::Module.new("mymod") @@ -251,11 +174,11 @@ describe Puppet::Module do end it "should convert an environment name into an Environment instance" do - Puppet::Module.new("foo", "prod").environment.should be_instance_of(Puppet::Node::Environment) + Puppet::Module.new("foo", :environment => "prod").environment.should be_instance_of(Puppet::Node::Environment) end it "should accept an environment at initialization" do - Puppet::Module.new("foo", :prod).environment.name.should == :prod + Puppet::Module.new("foo", :environment => :prod).environment.name.should == :prod end it "should use the default environment if none is provided" do @@ -265,43 +188,53 @@ describe Puppet::Module do it "should use any provided Environment instance" do env = Puppet::Node::Environment.new - Puppet::Module.new("foo", env).environment.should equal(env) + Puppet::Module.new("foo", :environment => env).environment.should equal(env) end - it "should return the path to the first found instance in its environment's module paths as its path" do - dir = tmpdir("deep_path") - first = File.join(dir, "first") - second = File.join(dir, "second") + describe ".path" do + before do + dir = tmpdir("deep_path") - FileUtils.mkdir_p(first) - FileUtils.mkdir_p(second) - Puppet[:modulepath] = "#{first}#{File::PATH_SEPARATOR}#{second}" + @first = File.join(dir, "first") + @second = File.join(dir, "second") + Puppet[:modulepath] = "#{@first}#{File::PATH_SEPARATOR}#{@second}" - modpath = File.join(first, "foo") - FileUtils.mkdir_p(modpath) + FileUtils.mkdir_p(@first) + FileUtils.mkdir_p(@second) + end - # Make a second one, which we shouldn't find - FileUtils.mkdir_p(File.join(second, "foo")) + it "should return the path to the first found instance in its environment's module paths as its path" do + modpath = File.join(@first, "foo") + FileUtils.mkdir_p(modpath) - mod = Puppet::Module.new("foo") - mod.path.should == modpath - end + # Make a second one, which we shouldn't find + FileUtils.mkdir_p(File.join(@second, "foo")) + + mod = Puppet::Module.new("foo") + mod.path.should == modpath + end - it "should be able to find itself in a directory other than the first directory in the module path" do - dir = tmpdir("deep_path") - first = File.join(dir, "first") - second = File.join(dir, "second") + it "should be able to find itself in a directory other than the first directory in the module path" do + modpath = File.join(@second, "foo") + FileUtils.mkdir_p(modpath) - FileUtils.mkdir_p(first) - FileUtils.mkdir_p(second) - Puppet[:modulepath] = "#{first}#{File::PATH_SEPARATOR}#{second}" + mod = Puppet::Module.new("foo") + mod.should be_exist + mod.path.should == modpath + end - modpath = File.join(second, "foo") - FileUtils.mkdir_p(modpath) + it "should be able to find itself in a directory other than the first directory in the module path even when it exists in the first" do + environment = Puppet::Node::Environment.new - mod = Puppet::Module.new("foo") - mod.should be_exist - mod.path.should == modpath + first_modpath = File.join(@first, "foo") + FileUtils.mkdir_p(first_modpath) + second_modpath = File.join(@second, "foo") + FileUtils.mkdir_p(second_modpath) + + mod = Puppet::Module.new("foo", :environment => environment, :path => second_modpath) + mod.path.should == File.join(@second, "foo") + mod.environment.should == environment + end end it "should be considered existent if it exists in at least one module path" do @@ -403,24 +336,6 @@ describe Puppet::Module do end end -describe Puppet::Module, " when building its search path" do - it "should use the current environment's search path if no environment is specified" do - env = mock 'env' - env.expects(:modulepath).returns "eh" - Puppet::Node::Environment.expects(:new).with(nil).returns env - - Puppet::Module.modulepath.should == "eh" - end - - it "should use the specified environment's search path if an environment is specified" do - env = mock 'env' - env.expects(:modulepath).returns "eh" - Puppet::Node::Environment.expects(:new).with("foo").returns env - - Puppet::Module.modulepath("foo").should == "eh" - end -end - describe Puppet::Module, "when finding matching manifests" do before do @mod = Puppet::Module.new("mymod") @@ -592,7 +507,6 @@ describe Puppet::Module do @module.puppetversion.should == @data[:puppetversion] end - it "should fail if the discovered name is different than the metadata name" end end diff --git a/spec/unit/module_tool/application_spec.rb b/spec/unit/module_tool/application_spec.rb new file mode 100644 index 000000000..22d3632fd --- /dev/null +++ b/spec/unit/module_tool/application_spec.rb @@ -0,0 +1,29 @@ +require 'spec_helper' +require 'puppet/module_tool' + +describe Puppet::Module::Tool::Applications::Application do + describe 'app' do + + good_versions = %w{ 1.2.4 0.0.1 0.0.0 0.0.2git-8-g3d316d1 0.0.3b1 10.100.10000 + 0.1.2rc1 0.1.2dev-1 0.1.2svn12345 } + bad_versions = %w{ 0.1.2-3 0.1 0 0.1.2.3 dev } + + before do + @app = Class.new(described_class).new + end + + good_versions.each do |ver| + it "should accept version string #{ver}" do + @app.instance_eval("@filename=%q{puppetlabs-ntp-#{ver}}") + @app.parse_filename! + end + end + + bad_versions.each do |ver| + it "should not accept version string #{ver}" do + @app.instance_eval("@filename=%q{puppetlabs-ntp-#{ver}}") + lambda { @app.parse_filename! }.should raise_error + end + end + end +end diff --git a/spec/unit/module_tool/metadata_spec.rb b/spec/unit/module_tool/metadata_spec.rb new file mode 100644 index 000000000..85d743fbc --- /dev/null +++ b/spec/unit/module_tool/metadata_spec.rb @@ -0,0 +1,11 @@ +require 'spec_helper' +require 'puppet/module_tool' + +describe Puppet::Module::Tool::Metadata do + context "when using default values" do + it "should set license to 'Apache License, Version 2.0'" do + metadata = Puppet::Module::Tool::Metadata.new + metadata.license.should == "Apache License, Version 2.0" + end + end +end diff --git a/spec/unit/module_tool/repository_spec.rb b/spec/unit/module_tool/repository_spec.rb new file mode 100644 index 000000000..69be1661e --- /dev/null +++ b/spec/unit/module_tool/repository_spec.rb @@ -0,0 +1,52 @@ +require 'spec_helper' +require 'net/http' +require 'puppet/module_tool' + +describe Puppet::Module::Tool::Repository do + describe 'instances' do + before do + @repository = described_class.new('http://fake.com') + end + + describe '#make_http_request' do + before do + # Do a mock of the Proxy call so we can do proper expects for + # Net::HTTP + Net::HTTP.expects(:Proxy).returns(Net::HTTP) + Net::HTTP.expects(:start) + end + context "when not given an :authenticate option" do + it "should authenticate" do + @repository.expects(:authenticate).never + @repository.make_http_request(nil) + end + end + context "when given an :authenticate option" do + it "should authenticate" do + @repository.expects(:authenticate) + @repository.make_http_request(nil, :authenticate => true) + end + end + end + + describe '#authenticate' do + it "should set basic auth on the request" do + authenticated_request = stub + authenticated_request.expects(:basic_auth) + @repository.expects(:prompt).twice + @repository.authenticate(authenticated_request) + end + end + + describe '#retrieve' do + before do + @uri = URI.parse('http://some.url.com') + end + + it "should access the cache" do + @repository.cache.expects(:retrieve).with(@uri) + @repository.retrieve(@uri) + end + end + end +end diff --git a/spec/unit/module_tool/uninstaller_spec.rb b/spec/unit/module_tool/uninstaller_spec.rb new file mode 100644 index 000000000..abf2db0f8 --- /dev/null +++ b/spec/unit/module_tool/uninstaller_spec.rb @@ -0,0 +1,44 @@ +require 'spec_helper' +require 'puppet/module_tool' +require 'tmpdir' + +describe Puppet::Module::Tool::Applications::Uninstaller do + include PuppetSpec::Files + + describe "instances" do + let(:tmp_module_path1) { tmpdir("uninstaller_module_path1") } + let(:tmp_module_path2) { tmpdir("uninstaller_module_path2") } + let(:options) do + { :target_directories => [ tmp_module_path1, tmp_module_path2 ] } + end + + it "should return an empty list if the module is not installed" do + described_class.new('foo', options).run.should == [] + end + + it "should uninstall an installed module" do + foo_module_path = File.join(tmp_module_path1, 'foo') + Dir.mkdir(foo_module_path) + described_class.new('foo', options).run.should == [ foo_module_path ] + end + + it "should only uninstall the requested module" do + foo_module_path = File.join(tmp_module_path1, 'foo') + bar_module_path = File.join(tmp_module_path1, 'bar') + Dir.mkdir(foo_module_path) + Dir.mkdir(bar_module_path) + described_class.new('foo', options).run.should == [ foo_module_path ] + end + + it "should uninstall the module from all target directories" do + foo1_module_path = File.join(tmp_module_path1, 'foo') + foo2_module_path = File.join(tmp_module_path2, 'foo') + Dir.mkdir(foo1_module_path) + Dir.mkdir(foo2_module_path) + described_class.new('foo', options).run.should == [ foo1_module_path, foo2_module_path ] + end + + #11803 + it "should check for broken dependencies" + end +end diff --git a/spec/unit/module_tool_spec.rb b/spec/unit/module_tool_spec.rb new file mode 100644 index 000000000..15ca6c766 --- /dev/null +++ b/spec/unit/module_tool_spec.rb @@ -0,0 +1,38 @@ +require 'spec_helper' +require 'puppet/module_tool' + +describe Puppet::Module::Tool do + describe 'http_proxy support' do + before :each do + ENV["http_proxy"] = nil + end + + after :each do + ENV["http_proxy"] = nil + end + + it "should support environment variable for port and host" do + ENV["http_proxy"] = "http://test.com:8011" + described_class.http_proxy_host.should == "test.com" + described_class.http_proxy_port.should == 8011 + end + + it "should support puppet configuration for port and host" do + ENV["http_proxy"] = nil + Puppet.settings.stubs(:[]).with(:http_proxy_host).returns('test.com') + Puppet.settings.stubs(:[]).with(:http_proxy_port).returns(7456) + + described_class.http_proxy_port.should == 7456 + described_class.http_proxy_host.should == "test.com" + end + + it "should use environment variable before puppet settings" do + ENV["http_proxy"] = "http://test1.com:8011" + Puppet.settings.stubs(:[]).with(:http_proxy_host).returns('test2.com') + Puppet.settings.stubs(:[]).with(:http_proxy_port).returns(7456) + + described_class.http_proxy_host.should == "test1.com" + described_class.http_proxy_port.should == 8011 + end + end +end diff --git a/spec/unit/network/http/api/v1_spec.rb b/spec/unit/network/http/api/v1_spec.rb index a952f24e2..039bccfd5 100755 --- a/spec/unit/network/http/api/v1_spec.rb +++ b/spec/unit/network/http/api/v1_spec.rb @@ -123,6 +123,10 @@ describe Puppet::Network::HTTP::API::V1 do @tester.uri2indirection("GET", "/env/statuses/bar", {})[0].should == 'status' end + it "should change indirection name to 'probe' if the http method is a GET and the indirection name is probes" do + @tester.uri2indirection("GET", "/env/probes/bar", {})[0].should == 'probe' + end + it "should choose 'delete' as the indirection method if the http method is a DELETE and the indirection name is singular" do @tester.uri2indirection("DELETE", "/env/foo/bar", {})[1].should == :destroy end diff --git a/spec/unit/node/environment_spec.rb b/spec/unit/node/environment_spec.rb index 78d383440..d5d3068a1 100755 --- a/spec/unit/node/environment_spec.rb +++ b/spec/unit/node/environment_spec.rb @@ -7,6 +7,8 @@ require 'puppet/node/environment' require 'puppet/util/execution' describe Puppet::Node::Environment do + let(:env) { Puppet::Node::Environment.new("testing") } + include PuppetSpec::Files after do Puppet::Node::Environment.clear @@ -44,59 +46,57 @@ describe Puppet::Node::Environment do describe "when managing known resource types" do before do - @env = Puppet::Node::Environment.new("dev") - @collection = Puppet::Resource::TypeCollection.new(@env) - @env.stubs(:perform_initial_import).returns(Puppet::Parser::AST::Hostclass.new('')) + @collection = Puppet::Resource::TypeCollection.new(env) + env.stubs(:perform_initial_import).returns(Puppet::Parser::AST::Hostclass.new('')) Thread.current[:known_resource_types] = nil end it "should create a resource type collection if none exists" do - Puppet::Resource::TypeCollection.expects(:new).with(@env).returns @collection - @env.known_resource_types.should equal(@collection) + Puppet::Resource::TypeCollection.expects(:new).with(env).returns @collection + env.known_resource_types.should equal(@collection) end it "should reuse any existing resource type collection" do - @env.known_resource_types.should equal(@env.known_resource_types) + env.known_resource_types.should equal(env.known_resource_types) end it "should perform the initial import when creating a new collection" do - @env = Puppet::Node::Environment.new("dev") - @env.expects(:perform_initial_import).returns(Puppet::Parser::AST::Hostclass.new('')) - @env.known_resource_types + env.expects(:perform_initial_import).returns(Puppet::Parser::AST::Hostclass.new('')) + env.known_resource_types end it "should return the same collection even if stale if it's the same thread" do Puppet::Resource::TypeCollection.stubs(:new).returns @collection - @env.known_resource_types.stubs(:stale?).returns true + env.known_resource_types.stubs(:stale?).returns true - @env.known_resource_types.should equal(@collection) + env.known_resource_types.should equal(@collection) end it "should return the current thread associated collection if there is one" do Thread.current[:known_resource_types] = @collection - @env.known_resource_types.should equal(@collection) + env.known_resource_types.should equal(@collection) end it "should give to all threads using the same environment the same collection if the collection isn't stale" do - original_thread_type_collection = Puppet::Resource::TypeCollection.new(@env) - Puppet::Resource::TypeCollection.expects(:new).with(@env).returns original_thread_type_collection - @env.known_resource_types.should equal(original_thread_type_collection) + original_thread_type_collection = Puppet::Resource::TypeCollection.new(env) + Puppet::Resource::TypeCollection.expects(:new).with(env).returns original_thread_type_collection + env.known_resource_types.should equal(original_thread_type_collection) original_thread_type_collection.expects(:require_reparse?).returns(false) - Puppet::Resource::TypeCollection.stubs(:new).with(@env).returns @collection + Puppet::Resource::TypeCollection.stubs(:new).with(env).returns @collection t = Thread.new { - @env.known_resource_types.should equal(original_thread_type_collection) + env.known_resource_types.should equal(original_thread_type_collection) } t.join end it "should generate a new TypeCollection if the current one requires reparsing" do - old_type_collection = @env.known_resource_types + old_type_collection = env.known_resource_types old_type_collection.stubs(:require_reparse?).returns true Thread.current[:known_resource_types] = nil - new_type_collection = @env.known_resource_types + new_type_collection = env.known_resource_types new_type_collection.should be_a Puppet::Resource::TypeCollection new_type_collection.should_not equal(old_type_collection) @@ -109,14 +109,11 @@ describe Puppet::Node::Environment do Puppet[:modulepath] = path - env = Puppet::Node::Environment.new("testing") - env.modulepath.should == [real_file] end it "should prefix the value of the 'PUPPETLIB' environment variable to the module path if present" do Puppet::Util::Execution.withenv("PUPPETLIB" => %w{/l1 /l2}.join(File::PATH_SEPARATOR)) do - env = Puppet::Node::Environment.new("testing") module_path = %w{/one /two}.join(File::PATH_SEPARATOR) env.expects(:validate_dirs).with(%w{/l1 /l2 /one /two}).returns %w{/l1 /l2 /one /two} env.expects(:[]).with(:modulepath).returns module_path @@ -132,8 +129,6 @@ describe Puppet::Node::Environment do end it "should not return non-directories" do - env = Puppet::Node::Environment.new("testing") - FileTest.expects(:directory?).with(@path_one).returns true FileTest.expects(:directory?).with(@path_two).returns false @@ -142,7 +137,6 @@ describe Puppet::Node::Environment do it "should use the current working directory to fully-qualify unqualified paths" do FileTest.stubs(:directory?).returns true - env = Puppet::Node::Environment.new("testing") two = File.expand_path(File.join(Dir.getwd, "two")) env.validate_dirs([@path_one, 'two']).should == [@path_one, two] @@ -155,44 +149,75 @@ describe Puppet::Node::Environment do end it "should provide an array-like accessor method for returning any environment-specific setting" do - env = Puppet::Node::Environment.new("testing") env.should respond_to(:[]) end it "should ask the Puppet settings instance for the setting qualified with the environment name" do Puppet.settings.expects(:value).with("myvar", :testing).returns("myval") - env = Puppet::Node::Environment.new("testing") env["myvar"].should == "myval" end it "should be able to return an individual module that exists in its module path" do - env = Puppet::Node::Environment.new("testing") mod = mock 'module' - Puppet::Module.expects(:new).with("one", env).returns mod + Puppet::Module.expects(:new).with("one", :environment => env).returns mod mod.expects(:exist?).returns true env.module("one").should equal(mod) end it "should return nil if asked for a module that does not exist in its path" do - env = Puppet::Node::Environment.new("testing") mod = mock 'module' - Puppet::Module.expects(:new).with("one", env).returns mod + Puppet::Module.expects(:new).with("one", :environment => env).returns mod mod.expects(:exist?).returns false env.module("one").should be_nil end - it "should be able to return its modules" do - Puppet::Node::Environment.new("testing").should respond_to(:modules) + describe ".modules_by_path" do + before do + dir = tmpdir("deep_path") + + @first = File.join(dir, "first") + @second = File.join(dir, "second") + Puppet[:modulepath] = "#{@first}#{File::PATH_SEPARATOR}#{@second}" + + FileUtils.mkdir_p(@first) + FileUtils.mkdir_p(@second) + end + + it "should return an empty list if there are no modules" do + env.modules_by_path.should == { + @first => [], + @second => [] + } + end + + it "should include modules even if they exist in multiple dirs in the modulepath" do + modpath1 = File.join(@first, "foo") + FileUtils.mkdir_p(modpath1) + modpath2 = File.join(@second, "foo") + FileUtils.mkdir_p(modpath2) + + env.modules_by_path.should == { + @first => [Puppet::Module.new('foo', :environment => env, :path => modpath1)], + @second => [Puppet::Module.new('foo', :environment => env, :path => modpath2)] + } + end end describe ".modules" do + it "should return an empty list if there are no modules" do + env.modulepath = %w{/a /b} + Dir.expects(:entries).with("/a").returns [] + Dir.expects(:entries).with("/b").returns [] + + env.modules.should == [] + end + it "should return a module named for every directory in each module path" do - env = Puppet::Node::Environment.new("testing") - env.expects(:modulepath).at_least_once.returns %w{/a /b} + env.modulepath = %w{/a /b} Dir.expects(:entries).with("/a").returns %w{foo bar} Dir.expects(:entries).with("/b").returns %w{bee baz} @@ -200,8 +225,7 @@ describe Puppet::Node::Environment do end it "should remove duplicates" do - env = Puppet::Node::Environment.new("testing") - env.expects(:modulepath).returns( %w{/a /b} ).at_least_once + env.modulepath = %w{/a /b} Dir.expects(:entries).with("/a").returns %w{foo} Dir.expects(:entries).with("/b").returns %w{foo} @@ -209,8 +233,7 @@ describe Puppet::Node::Environment do end it "should ignore invalid modules" do - env = Puppet::Node::Environment.new("testing") - env.stubs(:modulepath).returns %w{/a} + env.modulepath = %w{/a} Dir.expects(:entries).with("/a").returns %w{foo bar} Puppet::Module.expects(:new).with { |name, env| name == "foo" }.returns mock("foomod", :name => "foo") @@ -220,16 +243,14 @@ describe Puppet::Node::Environment do end it "should create modules with the correct environment" do - env = Puppet::Node::Environment.new("testing") - env.expects(:modulepath).at_least_once.returns %w{/a} + env.modulepath = %w{/a} Dir.expects(:entries).with("/a").returns %w{foo} env.modules.each {|mod| mod.environment.should == env } end it "should cache the module list" do - env = Puppet::Node::Environment.new("testing") - env.expects(:modulepath).at_least_once.returns %w{/a} + env.modulepath = %w{/a} Dir.expects(:entries).once.with("/a").returns %w{foo} env.modules @@ -244,20 +265,18 @@ describe Puppet::Node::Environment do @helper.extend(Puppet::Node::Environment::Helper) end - it "should be able to set and retrieve the environment" do + it "should be able to set and retrieve the environment as a symbol" do @helper.environment = :foo @helper.environment.name.should == :foo end it "should accept an environment directly" do - env = Puppet::Node::Environment.new :foo - @helper.environment = env + @helper.environment = Puppet::Node::Environment.new(:foo) @helper.environment.name.should == :foo end it "should accept an environment as a string" do - env = Puppet::Node::Environment.new "foo" - @helper.environment = env + @helper.environment = 'foo' @helper.environment.name.should == :foo end end @@ -266,14 +285,13 @@ describe Puppet::Node::Environment do before do @parser = Puppet::Parser::Parser.new("test") Puppet::Parser::Parser.stubs(:new).returns @parser - @env = Puppet::Node::Environment.new("env") end it "should set the parser's string to the 'code' setting and parse if code is available" do Puppet.settings[:code] = "my code" @parser.expects(:string=).with "my code" @parser.expects(:parse) - @env.instance_eval { perform_initial_import } + env.instance_eval { perform_initial_import } end it "should set the parser's file to the 'manifest' setting and parse if no code is available and the manifest is available" do @@ -282,7 +300,7 @@ describe Puppet::Node::Environment do Puppet.settings[:manifest] = filename @parser.expects(:file=).with filename @parser.expects(:parse) - @env.instance_eval { perform_initial_import } + env.instance_eval { perform_initial_import } end it "should pass the manifest file to the parser even if it does not exist on disk" do @@ -291,15 +309,15 @@ describe Puppet::Node::Environment do Puppet.settings[:manifest] = filename @parser.expects(:file=).with(filename).once @parser.expects(:parse).once - @env.instance_eval { perform_initial_import } + env.instance_eval { perform_initial_import } end it "should fail helpfully if there is an error importing" do File.stubs(:exist?).returns true - @env.stubs(:known_resource_types).returns Puppet::Resource::TypeCollection.new(@env) + env.stubs(:known_resource_types).returns Puppet::Resource::TypeCollection.new(env) @parser.expects(:file=).once @parser.expects(:parse).raises ArgumentError - lambda { @env.instance_eval { perform_initial_import } }.should raise_error(Puppet::Error) + lambda { env.instance_eval { perform_initial_import } }.should raise_error(Puppet::Error) end it "should not do anything if the ignore_import settings is set" do @@ -307,15 +325,15 @@ describe Puppet::Node::Environment do @parser.expects(:string=).never @parser.expects(:file=).never @parser.expects(:parse).never - @env.instance_eval { perform_initial_import } + env.instance_eval { perform_initial_import } end it "should mark the type collection as needing a reparse when there is an error parsing" do @parser.expects(:parse).raises Puppet::ParseError.new("Syntax error at ...") - @env.stubs(:known_resource_types).returns Puppet::Resource::TypeCollection.new(@env) + env.stubs(:known_resource_types).returns Puppet::Resource::TypeCollection.new(env) - lambda { @env.instance_eval { perform_initial_import } }.should raise_error(Puppet::Error, /Syntax error at .../) - @env.known_resource_types.require_reparse?.should be_true + lambda { env.instance_eval { perform_initial_import } }.should raise_error(Puppet::Error, /Syntax error at .../) + env.known_resource_types.require_reparse?.should be_true end end end diff --git a/spec/unit/parser/ast/asthash_spec.rb b/spec/unit/parser/ast/asthash_spec.rb index d7fbbfae9..ab1281f91 100755 --- a/spec/unit/parser/ast/asthash_spec.rb +++ b/spec/unit/parser/ast/asthash_spec.rb @@ -91,7 +91,6 @@ describe Puppet::Parser::AST::ASTHash do it "should return a valid string with to_s" do hash = Puppet::Parser::AST::ASTHash.new(:value => { "a" => "b", "c" => "d" }) - - hash.to_s.should == '{a => b, c => d}' + ["{a => b, c => d}", "{c => d, a => b}"].should be_include hash.to_s end end diff --git a/spec/unit/parser/ast/leaf_spec.rb b/spec/unit/parser/ast/leaf_spec.rb index ff3fed5e9..881506ea4 100755 --- a/spec/unit/parser/ast/leaf_spec.rb +++ b/spec/unit/parser/ast/leaf_spec.rb @@ -151,6 +151,14 @@ describe Puppet::Parser::AST::HashOrArrayAccess do lambda { access.evaluate(@scope) }.should raise_error end + it "should be able to return :undef for an unknown array index" do + @scope.stubs(:lookupvar).with { |name,options| name == 'a'}.returns(["val1", "val2", "val3"]) + + access = Puppet::Parser::AST::HashOrArrayAccess.new(:variable => "a", :key => 6 ) + + access.evaluate(@scope).should == :undef + end + it "should be able to return an hash value" do @scope.stubs(:lookupvar).with { |name,options| name == 'a'}.returns({ "key1" => "val1", "key2" => "val2", "key3" => "val3" }) @@ -159,6 +167,14 @@ describe Puppet::Parser::AST::HashOrArrayAccess do access.evaluate(@scope).should == "val2" end + it "should be able to return :undef for unknown hash keys" do + @scope.stubs(:lookupvar).with { |name,options| name == 'a'}.returns({ "key1" => "val1", "key2" => "val2", "key3" => "val3" }) + + access = Puppet::Parser::AST::HashOrArrayAccess.new(:variable => "a", :key => "key12" ) + + access.evaluate(@scope).should == :undef + end + it "should be able to return an hash value with a numerical key" do @scope.stubs(:lookupvar).with { |name,options| name == "a"}.returns({ "key1" => "val1", "key2" => "val2", "45" => "45", "key3" => "val3" }) diff --git a/spec/unit/parser/functions/create_resources_spec.rb b/spec/unit/parser/functions/create_resources_spec.rb index da76e75d0..64314f777 100755 --- a/spec/unit/parser/functions/create_resources_spec.rb +++ b/spec/unit/parser/functions/create_resources_spec.rb @@ -20,8 +20,9 @@ describe 'function for dynamically creating resources' do it "should exist" do Puppet::Parser::Functions.function(:create_resources).should == "function_create_resources" end - it 'should require two arguments' do - lambda { @scope.function_create_resources(['foo']) }.should raise_error(ArgumentError, 'create_resources(): wrong number of arguments (1; must be 2)') + it 'should require two or three arguments' do + lambda { @scope.function_create_resources(['foo']) }.should raise_error(ArgumentError, 'create_resources(): wrong number of arguments (1; must be 2 or 3)') + lambda { @scope.function_create_resources(['foo', 'bar', 'blah', 'baz']) }.should raise_error(ArgumentError, 'create_resources(): wrong number of arguments (4; must be 2 or 3)') end describe 'when creating native types' do before :each do @@ -58,6 +59,11 @@ describe 'function for dynamically creating resources' do foo.should be rg.path_between(test,foo).should be end + it 'should account for default values' do + @scope.function_create_resources(['file', {'/etc/foo'=>{'ensure'=>'present'}, '/etc/baz'=>{'group'=>'food'}}, {'group' => 'bar'}]) + @compiler.catalog.resource(:file, "/etc/foo")['group'].should == 'bar' + @compiler.catalog.resource(:file, "/etc/baz")['group'].should == 'food' + end end describe 'when dynamically creating resource types' do before :each do @@ -103,6 +109,11 @@ notify{test:} rg.path_between(test,blah).should be @compiler.catalog.resource(:notify, "blah")['message'].should == 'two' end + it 'should account for default values' do + @scope.function_create_resources(['foo', {'blah'=>{}}, {'one' => 'two'}]) + @scope.compiler.compile + @compiler.catalog.resource(:notify, "blah")['message'].should == 'two' + end end describe 'when creating classes' do before :each do @@ -114,7 +125,7 @@ notify{tester:} @scope.resource=Puppet::Parser::Resource.new('class', 't', :scope => @scope) Puppet::Parser::Functions.function(:create_resources) end - it 'should be able to create classes', :'fails_on_ruby_1.9.2' => true do + it 'should be able to create classes' do @scope.function_create_resources(['class', {'bar'=>{'one'=>'two'}}]) @scope.compiler.compile @compiler.catalog.resource(:notify, "test")['message'].should == 'two' @@ -123,7 +134,7 @@ notify{tester:} it 'should fail to create non-existing classes' do lambda { @scope.function_create_resources(['class', {'blah'=>{'one'=>'two'}}]) }.should raise_error(ArgumentError ,'could not find hostclass blah') end - it 'should be able to add edges', :'fails_on_ruby_1.9.2' => true do + it 'should be able to add edges' do @scope.function_create_resources(['class', {'bar'=>{'one'=>'two', 'require' => 'Notify[tester]'}}]) @scope.compiler.compile rg = @scope.compiler.catalog.to_ral.relationship_graph @@ -133,5 +144,11 @@ notify{tester:} tester.should be rg.path_between(tester,test).should be end + it 'should account for default values' do + @scope.function_create_resources(['class', {'bar'=>{}}, {'one' => 'two'}]) + @scope.compiler.compile + @compiler.catalog.resource(:notify, "test")['message'].should == 'two' + @compiler.catalog.resource(:class, "bar").should_not be_nil#['message'].should == 'two' + end end end diff --git a/spec/unit/property/keyvalue_spec.rb b/spec/unit/property/keyvalue_spec.rb index 821c61799..2bead50e3 100755 --- a/spec/unit/property/keyvalue_spec.rb +++ b/spec/unit/property/keyvalue_spec.rb @@ -35,8 +35,11 @@ describe klass do @property.should_to_s({:foo => "baz", :bar => "boo"}) == "foo=baz;bar=boo" end - it "should return the passed in array values joined with the delimiter from is_to_s" do - @property.is_to_s({"foo" => "baz" , "bar" => "boo"}).should == "foo=baz;bar=boo" + it "should return the passed in hash values joined with the delimiter from is_to_s" do + s = @property.is_to_s({"foo" => "baz" , "bar" => "boo"}) + + # We can't predict the order the hash is processed in... + ["foo=baz;bar=boo", "bar=boo;foo=baz"].should be_include s end describe "when calling inclusive?" do diff --git a/spec/unit/provider/augeas/augeas_spec.rb b/spec/unit/provider/augeas/augeas_spec.rb index 874f70a8d..52ebb34f9 100755 --- a/spec/unit/provider/augeas/augeas_spec.rb +++ b/spec/unit/provider/augeas/augeas_spec.rb @@ -353,7 +353,7 @@ describe provider_class do @augeas_stub = stub("augeas") @provider.aug = @augeas_stub - @augeas_stub.stubs("get").with("/augeas/version").returns("0.7.2") + @augeas_stub.stubs("get").with("/augeas/version").returns("0.10.0") @augeas_stub.stubs(:set).returns(true) @augeas_stub.stubs(:save).returns(true) end @@ -467,7 +467,7 @@ describe provider_class do @augeas = stub("augeas") @provider.aug= @augeas @provider.stubs(:get_augeas_version).returns("0.3.5") - @augeas.stubs(:match).with("/augeas/events/saved") + @augeas.stubs(:match).with("/augeas/events/saved").returns([]) end it "should handle set commands" do diff --git a/spec/unit/provider/group/pw_spec.rb b/spec/unit/provider/group/pw_spec.rb new file mode 100755 index 000000000..3dfc5ec71 --- /dev/null +++ b/spec/unit/provider/group/pw_spec.rb @@ -0,0 +1,81 @@ +#!/usr/bin/env rspec +require 'spec_helper' + +provider_class = Puppet::Type.type(:group).provider(:pw) + +describe provider_class do + let :resource do + Puppet::Type.type(:group).new(:name => "testgroup", :provider => :pw) + end + + let :provider do + resource.provider + end + + describe "when creating groups" do + let :provider do + prov = resource.provider + prov.expects(:exists?).returns nil + prov + end + + it "should run pw with no additional flags when no properties are given" do + provider.addcmd.must == [provider_class.command(:pw), "groupadd", "testgroup"] + provider.expects(:execute).with([provider_class.command(:pw), "groupadd", "testgroup"]) + provider.create + end + + it "should use -o when allowdupe is enabled" do + resource[:allowdupe] = true + provider.expects(:execute).with(includes("-o")) + provider.create + end + + it "should use -g with the correct argument when the gid property is set" do + resource[:gid] = 12345 + provider.expects(:execute).with(all_of(includes("-g"), includes(12345))) + provider.create + end + + it "should use -M with the correct argument when the members property is set" do + resource[:members] = "user1" + provider.expects(:execute).with(all_of(includes("-M"), includes("user1"))) + provider.create + end + + it "should use -M with all the given users when the members property is set to an array" do + resource[:members] = ["user1", "user2"] + provider.expects(:execute).with(all_of(includes("-M"), includes("user1,user2"))) + provider.create + end + end + + describe "when deleting groups" do + it "should run pw with no additional flags" do + provider.expects(:exists?).returns true + provider.deletecmd.must == [provider_class.command(:pw), "groupdel", "testgroup"] + provider.expects(:execute).with([provider_class.command(:pw), "groupdel", "testgroup"]) + provider.delete + end + end + + describe "when modifying groups" do + it "should run pw with the correct arguments" do + provider.modifycmd("gid", 12345).must == [provider_class.command(:pw), "groupmod", "testgroup", "-g", 12345] + provider.expects(:execute).with([provider_class.command(:pw), "groupmod", "testgroup", "-g", 12345]) + provider.gid = 12345 + end + + it "should use -M with the correct argument when the members property is changed" do + resource[:members] = "user1" + provider.expects(:execute).with(all_of(includes("-M"), includes("user2"))) + provider.members = "user2" + end + + it "should use -M with all the given users when the members property is changed with an array" do + resource[:members] = ["user1", "user2"] + provider.expects(:execute).with(all_of(includes("-M"), includes("user3,user4"))) + provider.members = ["user3", "user4"] + end + end +end diff --git a/spec/unit/provider/nameservice/directoryservice_spec.rb b/spec/unit/provider/nameservice/directoryservice_spec.rb index 7a83d7f20..c585b626f 100755 --- a/spec/unit/provider/nameservice/directoryservice_spec.rb +++ b/spec/unit/provider/nameservice/directoryservice_spec.rb @@ -95,3 +95,105 @@ describe 'DirectoryService.get_exec_preamble' do Puppet::Provider::NameService::DirectoryService.get_exec_preamble('-list').should include("-plist") end end + +describe 'DirectoryService password behavior' do + # The below is a binary plist containing a ShadowHashData key which CONTAINS + # another binary plist. The nested binary plist contains a 'SALTED-SHA512' + # key that contains a base64 encoded salted-SHA512 password hash... + let (:binary_plist) { "bplist00\324\001\002\003\004\005\006\a\bXCRAM-MD5RNT]SALTED-SHA512[RECOVERABLEO\020 \231k2\3360\200GI\201\355J\216\202\215y\243\001\206J\300\363\032\031\022\006\2359\024\257\217<\361O\020\020F\353\at\377\277\226\276c\306\254\031\037J(\235O\020D\335\006{\3744g@\377z\204\322\r\332t\021\330\n\003\246K\223\356\034!P\261\305t\035\346\352p\206\003n\247MMA\310\301Z<\366\246\023\0161W3\340\357\000\317T\t\301\311+\204\246L7\276\370\320*\245O\021\002\000k\024\221\270x\353\001\237\346D}\377?\265]\356+\243\v[\350\316a\340h\376<\322\266\327\016\306n\272r\t\212A\253L\216\214\205\016\241 [\360/\335\002#\\A\372\241a\261\346\346\\\251\330\312\365\016\n\341\017\016\225&;\322\\\004*\ru\316\372\a \362?8\031\247\231\030\030\267\315\023\v\343{@\227\301s\372h\212\000a\244&\231\366\nt\277\2036,\027bZ+\223W\212g\333`\264\331N\306\307\362\257(^~ b\262\247&\231\261t\341\231%\244\247\203eOt\365\271\201\273\330\350\363C^A\327F\214!\217hgf\e\320k\260n\315u~\336\371M\t\235k\230S\375\311\303\240\351\037d\273\321y\335=K\016`_\317\230\2612_\023K\036\350\v\232\323Y\310\317_\035\227%\237\v\340\023\016\243\233\025\306:\227\351\370\364x\234\231\266\367\016w\275\333-\351\210}\375x\034\262\272kRuHa\362T/F!\347B\231O`K\304\037'k$$\245h)e\363\365mT\b\317\\2\361\026\351\254\375Jl1~\r\371\267\352\2322I\341\272\376\243^Un\266E7\230[VocUJ\220N\2116D/\025f=\213\314\325\vG}\311\360\377DT\307m\261&\263\340\272\243_\020\271rG^BW\210\030l\344\0324\335\233\300\023\272\225Im\330\n\227*Yv[\006\315\330y'\a\321\373\273A\240\305F{S\246I#/\355\2425\031\031GGF\270y\n\331\004\023G@\331\000\361\343\350\264$\032\355_\210y\000\205\342\375\212q\024\004\026W:\205 \363v?\035\270L-\270=\022\323\2003\v\336\277\t\237\356\374\n\267n\003\367\342\330;\371S\326\016`B6@Njm>\240\021%\336\345\002(P\204Yn\3279l\0228\264\254\304\2528t\372h\217\347sA\314\345\245\337)]\000\b\000\021\000\032\000\035\000+\0007\000Z\000m\000\264\000\000\000\000\000\000\002\001\000\000\000\000\000\000\000\t\000\000\000\000\000\000\000\000\000\000\000\000\000\000\002\270" } + + # The below is a base64 encoded salted-SHA512 password hash. + let (:pw_string) { "\335\006{\3744g@\377z\204\322\r\332t\021\330\n\003\246K\223\356\034!P\261\305t\035\346\352p\206\003n\247MMA\310\301Z<\366\246\023\0161W3\340\357\000\317T\t\301\311+\204\246L7\276\370\320*\245" } + + # The below is a salted-SHA512 password hash in hex. + let (:sha512_hash) { 'dd067bfc346740ff7a84d20dda7411d80a03a64b93ee1c2150b1c5741de6ea7086036ea74d4d41c8c15a3cf6a6130e315733e0ef00cf5409c1c92b84a64c37bef8d02aa5' } + + let :plist_path do + '/var/db/dslocal/nodes/Default/users/jeff.plist' + end + + let :ds_provider do + Puppet::Provider::NameService::DirectoryService + end + + let :shadow_hash_data do + {'ShadowHashData' => [StringIO.new(binary_plist)]} + end + + subject do + Puppet::Provider::NameService::DirectoryService + end + + before :each do + subject.expects(:get_macosx_version_major).returns("10.7") + end + + it 'should execute convert_binary_to_xml once when getting the password on >= 10.7' do + subject.expects(:convert_binary_to_xml).returns({'SALTED-SHA512' => StringIO.new(pw_string)}) + File.expects(:exists?).with(plist_path).once.returns(true) + Plist.expects(:parse_xml).returns(shadow_hash_data) + # On Mac OS X 10.7 we first need to convert to xml when reading the password + subject.expects(:plutil).with('-convert', 'xml1', '-o', '/dev/stdout', plist_path) + subject.get_password('uid', 'jeff') + end + + it 'should fail if a salted-SHA512 password hash is not passed in >= 10.7' do + expect { + subject.set_password('jeff', 'uid', 'badpassword') + }.should raise_error(RuntimeError, /OS X 10.7 requires a Salted SHA512 hash password of 136 characters./) + end + + it 'should convert xml-to-binary and binary-to-xml when setting the pw on >= 10.7' do + subject.expects(:convert_binary_to_xml).returns({'SALTED-SHA512' => StringIO.new(pw_string)}) + subject.expects(:convert_xml_to_binary).returns(binary_plist) + File.expects(:exists?).with(plist_path).once.returns(true) + Plist.expects(:parse_xml).returns(shadow_hash_data) + # On Mac OS X 10.7 we first need to convert to xml + subject.expects(:plutil).with('-convert', 'xml1', '-o', '/dev/stdout', plist_path) + # And again back to a binary plist or DirectoryService will complain + subject.expects(:plutil).with('-convert', 'binary1', plist_path) + Plist::Emit.expects(:save_plist).with(shadow_hash_data, plist_path) + subject.set_password('jeff', 'uid', sha512_hash) + end +end + +describe '(#4855) directoryservice group resource failure' do + let :provider_class do + Puppet::Type.type(:group).provider(:directoryservice) + end + + let :group_members do + ['root','jeff'] + end + + let :user_account do + ['root'] + end + + let :stub_resource do + stub('resource') + end + + subject do + provider_class.new(stub_resource) + end + + before :each do + @resource = stub("resource") + @provider = provider_class.new(@resource) + end + + it 'should delete a group member if the user does not exist' do + stub_resource.stubs(:[]).with(:name).returns('fake_group') + stub_resource.stubs(:name).returns('fake_group') + subject.expects(:execute).with([:dseditgroup, '-o', 'edit', '-n', '.', + '-d', 'jeff', + 'fake_group']).raises(Puppet::ExecutionFailure, + 'it broke') + subject.expects(:execute).with([:dscl, '.', '-delete', + '/Groups/fake_group', 'GroupMembership', + 'jeff']) + subject.remove_unwanted_members(group_members, user_account) + end +end + diff --git a/spec/unit/provider/package/pip_spec.rb b/spec/unit/provider/package/pip_spec.rb index 97b3b5e73..7849ec49c 100755 --- a/spec/unit/provider/package/pip_spec.rb +++ b/spec/unit/provider/package/pip_spec.rb @@ -175,6 +175,13 @@ describe provider_class do expect { @provider.method(:lazy_pip).call("freeze") }.to raise_error(NoMethodError) end + it "should output a useful error message if pip is missing" do + @provider.expects(:pip).with('freeze').raises(NoMethodError) + @provider.expects(:which).with('pip').returns(nil) + expect { @provider.method(:lazy_pip).call("freeze") }. + to raise_error(NoMethodError, 'Could not locate the pip command.') + end + end end diff --git a/spec/unit/provider/package/yum_spec.rb b/spec/unit/provider/package/yum_spec.rb index 601c24009..5148ac079 100755 --- a/spec/unit/provider/package/yum_spec.rb +++ b/spec/unit/provider/package/yum_spec.rb @@ -30,16 +30,19 @@ describe provider do @provider.expects(:yum).with('-d', '0', '-e', '0', '-y', :install, 'mypackage') @provider.install end + it 'should use :install to update' do @provider.expects(:install) @provider.update end + it 'should be able to set version' do @resource.stubs(:should).with(:ensure).returns '1.2' @provider.expects(:yum).with('-d', '0', '-e', '0', '-y', :install, 'mypackage-1.2') @provider.stubs(:query).returns :ensure => '1.2' @provider.install end + it 'should be able to downgrade' do @resource.stubs(:should).with(:ensure).returns '1.0' @provider.expects(:yum).with('-d', '0', '-e', '0', '-y', :downgrade, 'mypackage-1.0') @@ -53,6 +56,7 @@ describe provider do @provider.expects(:yum).with('-y', :erase, 'mypackage') @provider.purge end + it 'should use rpm to uninstall' do @provider.expects(:rpm).with('-e', 'mypackage-1-1.i386') @provider.uninstall @@ -62,5 +66,45 @@ describe provider do it 'should be versionable' do provider.should be_versionable end -end + describe '#latest' do + describe 'when latest_info is nil' do + before :each do + @provider.stubs(:latest_info).returns(nil) + end + + it 'raises if ensure is absent and latest_info is nil' do + @provider.stubs(:properties).returns({:ensure => :absent}) + + expect { @provider.latest }.to raise_error( + Puppet::DevError, + 'Tried to get latest on a missing package' + ) + end + + it 'returns the ensure value if the package is not already installed' do + @provider.stubs(:properties).returns({:ensure => '3.4.5'}) + + @provider.latest.should == '3.4.5' + end + end + + describe 'when latest_info is populated' do + before :each do + @provider.stubs(:latest_info).returns({ + :name => 'mypackage', + :epoch => '1', + :version => '2.3.4', + :release => '5', + :arch => 'i686', + :provider => :yum, + :ensure => '2.3.4-5' + }) + end + + it 'includes the epoch in the version string' do + @provider.latest.should == '1:2.3.4-5' + end + end + end +end diff --git a/spec/unit/provider/service/debian_spec.rb b/spec/unit/provider/service/debian_spec.rb index 4e3d30d61..f858568dc 100755 --- a/spec/unit/provider/service/debian_spec.rb +++ b/spec/unit/provider/service/debian_spec.rb @@ -88,6 +88,21 @@ describe provider_class do @provider.enabled?.should == :true end + context "when invoke-rc.d exits with 105 status" do + it "links count is 4" do + @provider.stubs(:system) + $CHILD_STATUS.stubs(:exitstatus).returns(105) + @provider.stubs(:get_start_link_count).returns(4) + @provider.enabled?.should == :true + end + it "links count is less than 4" do + @provider.stubs(:system) + $CHILD_STATUS.stubs(:exitstatus).returns(105) + @provider.stubs(:get_start_link_count).returns(3) + @provider.enabled?.should == :false + end + end + # pick a range of non-[104.106] numbers, strings and booleans to test with. [-100, -1, 0, 1, 100, "foo", "", :true, :false].each do |exitstatus| it "should return false when invoke-rc.d exits with #{exitstatus} status" do diff --git a/spec/unit/provider/service/launchd_spec.rb b/spec/unit/provider/service/launchd_spec.rb index 04c1f466f..8651ac562 100755 --- a/spec/unit/provider/service/launchd_spec.rb +++ b/spec/unit/provider/service/launchd_spec.rb @@ -37,22 +37,21 @@ describe Puppet::Type.type(:service).provider(:launchd) do describe "when checking whether the service is enabled on OS X 10.5" do it "should return true in if the job plist says disabled is false" do - Facter.stubs(:value).with(:macosx_productversion_major).returns('10.5') - Facter.stubs(:value).with(:kernel).returns('Darwin') - Facter.stubs(:value).with(:macaddress).returns('') - Facter.stubs(:value).with(:arp).returns('') + subject.expects(:has_macosx_plist_overrides?).returns(false) subject.expects(:plist_from_label).with(joblabel).returns(["foo", {"Disabled" => false}]) subject.expects(:resource).returns({:name => joblabel}) subject.enabled?.should == :true end it "should return true in if the job plist has no disabled key" do + subject.expects(:has_macosx_plist_overrides?).returns(false) subject.expects(:resource).returns({:name => joblabel}) - subject.stubs(:plist_from_label).returns(["foo", {}]) + subject.expects(:plist_from_label).returns(["foo", {}]) subject.enabled?.should == :true end it "should return false in if the job plist says disabled is true" do + subject.expects(:has_macosx_plist_overrides?).returns(false) subject.expects(:resource).returns({:name => joblabel}) - subject.stubs(:plist_from_label).returns(["foo", {"Disabled" => true}]) + subject.expects(:plist_from_label).returns(["foo", {"Disabled" => true}]) subject.enabled?.should == :false end end @@ -61,7 +60,7 @@ describe Puppet::Type.type(:service).provider(:launchd) do it "should return true if the job plist says disabled is true and the global overrides says disabled is false" do provider.expects(:get_macosx_version_major).returns("10.6") subject.expects(:plist_from_label).returns([joblabel, {"Disabled" => true}]) - provider.stubs(:read_plist).returns({joblabel => {"Disabled" => false}}) + provider.expects(:read_plist).returns({joblabel => {"Disabled" => false}}) FileTest.expects(:file?).with(launchd_overrides).returns(true) subject.stubs(:resource).returns({:name => joblabel}) subject.enabled?.should == :true @@ -69,7 +68,7 @@ describe Puppet::Type.type(:service).provider(:launchd) do it "should return false if the job plist says disabled is false and the global overrides says disabled is true" do provider.expects(:get_macosx_version_major).returns("10.6") subject.expects(:plist_from_label).returns([joblabel, {"Disabled" => false}]) - provider.stubs(:read_plist).returns({joblabel => {"Disabled" => true}}) + provider.expects(:read_plist).returns({joblabel => {"Disabled" => true}}) FileTest.expects(:file?).with(launchd_overrides).returns(true) subject.stubs(:resource).returns({:name => joblabel}) subject.enabled?.should == :false @@ -77,7 +76,7 @@ describe Puppet::Type.type(:service).provider(:launchd) do it "should return true if the job plist and the global overrides have no disabled keys" do provider.expects(:get_macosx_version_major).returns("10.6") subject.expects(:plist_from_label).returns([joblabel, {}]) - provider.stubs(:read_plist).returns({}) + provider.expects(:read_plist).returns({}) FileTest.expects(:file?).with(launchd_overrides).returns(true) subject.stubs(:resource).returns({:name => joblabel}) subject.enabled?.should == :true @@ -87,61 +86,69 @@ describe Puppet::Type.type(:service).provider(:launchd) do describe "when starting the service" do it "should look for the relevant plist once" do subject.expects(:plist_from_label).returns([joblabel, {}]).once - subject.stubs(:enabled?).returns :true - subject.stubs(:execute).with([:launchctl, :load, joblabel]) + subject.expects(:enabled?).returns :true + subject.expects(:execute).with([:launchctl, :load, joblabel]) subject.stubs(:resource).returns({:name => joblabel}) subject.start end it "should execute 'launchctl load' once without writing to the plist if the job is enabled" do - subject.stubs(:plist_from_label).returns([joblabel, {}]) - subject.stubs(:enabled?).returns :true + subject.expects(:plist_from_label).returns([joblabel, {}]) + subject.expects(:enabled?).returns :true subject.expects(:execute).with([:launchctl, :load, joblabel]).once subject.stubs(:resource).returns({:name => joblabel}) subject.start end it "should execute 'launchctl load' with writing to the plist once if the job is disabled" do - subject.stubs(:plist_from_label).returns([joblabel, {}]) - subject.stubs(:enabled?).returns(:false) + subject.expects(:plist_from_label).returns([joblabel, {}]) + subject.expects(:enabled?).returns(:false) subject.stubs(:resource).returns({:name => joblabel}) subject.expects(:execute).with([:launchctl, :load, "-w", joblabel]).once subject.start end it "should disable the job once if the job is disabled and should be disabled at boot" do - subject.stubs(:plist_from_label).returns([joblabel, {"Disabled" => true}]) - subject.stubs(:enabled?).returns :false - subject.stubs(:execute).with([:launchctl, :load, "-w", joblabel]) + subject.expects(:plist_from_label).returns([joblabel, {"Disabled" => true}]) + subject.expects(:enabled?).returns :false + subject.expects(:execute).with([:launchctl, :load, "-w", joblabel]) subject.stubs(:resource).returns({:name => joblabel, :enable => :false}) subject.expects(:disable).once subject.start end + it "(#2773) should execute 'launchctl load -w' if the job is enabled but stopped" do + subject.expects(:plist_from_label).returns([joblabel, {}]) + subject.expects(:enabled?).returns(:true) + subject.expects(:status).returns(:stopped) + subject.expects(:resource).returns({:name => joblabel}).twice + subject.expects(:execute).with([:launchctl, :load, '-w', joblabel]) + subject.start + end end describe "when stopping the service" do it "should look for the relevant plist once" do subject.expects(:plist_from_label).returns([joblabel, {}]).once - subject.stubs(:enabled?).returns :true - subject.stubs(:execute).with([:launchctl, :unload, '-w', joblabel]) + subject.expects(:enabled?).returns :true + subject.expects(:execute).with([:launchctl, :unload, '-w', joblabel]) subject.stubs(:resource).returns({:name => joblabel}) subject.stop end it "should execute 'launchctl unload' once without writing to the plist if the job is disabled" do - subject.stubs(:plist_from_label).returns([joblabel, {}]) - subject.stubs(:enabled?).returns :false + subject.expects(:plist_from_label).returns([joblabel, {}]) + subject.expects(:enabled?).returns :false subject.expects(:execute).with([:launchctl, :unload, joblabel]).once subject.stubs(:resource).returns({:name => joblabel}) subject.stop end it "should execute 'launchctl unload' with writing to the plist once if the job is enabled" do - subject.stubs(:plist_from_label).returns([joblabel, {}]) - subject.stubs(:enabled?).returns :true + subject.expects(:plist_from_label).returns([joblabel, {}]) + subject.expects(:enabled?).returns :true subject.expects(:execute).with([:launchctl, :unload, '-w', joblabel]).once subject.stubs(:resource).returns({:name => joblabel}) subject.stop end it "should enable the job once if the job is enabled and should be enabled at boot" do - subject.stubs(:plist_from_label).returns([joblabel, {"Disabled" => false}]) - subject.stubs(:enabled?).returns :true - subject.stubs(:execute).with([:launchctl, :unload, "-w", joblabel]) + subject.expects(:plist_from_label).returns([joblabel, {"Disabled" => false}]) + subject.expects(:enabled?).returns :true + subject.expects(:execute).with([:launchctl, :unload, "-w", joblabel]) subject.stubs(:resource).returns({:name => joblabel, :enable => :true}) subject.expects(:enable).once subject.stop @@ -151,15 +158,15 @@ describe Puppet::Type.type(:service).provider(:launchd) do describe "when enabling the service" do it "should look for the relevant plist once" do ### Do we need this test? Differentiating it? subject.expects(:plist_from_label).returns([joblabel, {}]).once - subject.stubs(:enabled?).returns :false - subject.stubs(:execute).with([:launchctl, :unload, joblabel]) + subject.expects(:enabled?).returns :false + subject.expects(:execute).with([:launchctl, :unload, joblabel]) subject.stubs(:resource).returns({:name => joblabel, :enable => :true}) subject.stop end it "should check if the job is enabled once" do - subject.stubs(:plist_from_label).returns([joblabel, {}]).once + subject.expects(:plist_from_label).returns([joblabel, {}]).once subject.expects(:enabled?).once - subject.stubs(:execute).with([:launchctl, :unload, joblabel]) + subject.expects(:execute).with([:launchctl, :unload, joblabel]) subject.stubs(:resource).returns({:name => joblabel, :enable => :true}) subject.stop end @@ -168,8 +175,8 @@ describe Puppet::Type.type(:service).provider(:launchd) do describe "when disabling the service" do it "should look for the relevant plist once" do subject.expects(:plist_from_label).returns([joblabel, {}]).once - subject.stubs(:enabled?).returns :true - subject.stubs(:execute).with([:launchctl, :unload, '-w', joblabel]) + subject.expects(:enabled?).returns :true + subject.expects(:execute).with([:launchctl, :unload, '-w', joblabel]) subject.stubs(:resource).returns({:name => joblabel, :enable => :false}) subject.stop end @@ -177,8 +184,8 @@ describe Puppet::Type.type(:service).provider(:launchd) do describe "when enabling the service on OS X 10.6" do it "should write to the global launchd overrides file once" do - provider.stubs(:get_macosx_version_major).returns("10.6") - provider.stubs(:read_plist).returns({}) + provider.expects(:get_macosx_version_major).returns("10.6") + provider.expects(:read_plist).returns({}) Plist::Emit.expects(:save_plist).once subject.stubs(:resource).returns({:name => joblabel, :enable => :true}) subject.enable @@ -200,15 +207,13 @@ describe Puppet::Type.type(:service).provider(:launchd) do provider.instance_variable_set(:@macosx_version_major, nil) end it "should display a deprecation warning" do - Facter.stubs(:value).with(:macosx_productversion_major).returns(nil) - Facter.stubs(:value).with(:kernel).returns('Darwin') - Facter.stubs(:value).with(:macosx_productversion).returns('10.5.8') + Facter.expects(:value).with(:macosx_productversion_major).returns(nil) + Facter.expects(:value).with(:macosx_productversion).returns('10.5.8') + Facter.expects(:loadfacts) Puppet::Util::Warnings.expects(:maybe_log) - provider.stubs(:read_plist).returns({joblabel => {"Disabled" => false}}) - subject.stubs(:plist_from_label).returns([joblabel, {"Disabled" => false}]) - subject.stubs(:enabled?).returns :false - subject.stubs(:execute).with([:launchctl, :load, '-w', joblabel]).returns('') - File.stubs(:open).returns('') + subject.expects(:plist_from_label).returns([joblabel, {"Disabled" => false}]) + subject.expects(:enabled?).returns :false + File.expects(:open).returns('') subject.stubs(:resource).returns({:name => joblabel, :enable => :true}) subject.enable end diff --git a/spec/unit/provider/service/smf_spec.rb b/spec/unit/provider/service/smf_spec.rb index dc7438cae..752702940 100755 --- a/spec/unit/provider/service/smf_spec.rb +++ b/spec/unit/provider/service/smf_spec.rb @@ -92,7 +92,7 @@ describe provider_class do it "should always execute external command 'svcadm enable /system/myservice'" do @provider.stubs(:status).returns :running - @provider.expects(:texecute).with(:start, ["/usr/sbin/svcadm", :enable, "/system/myservice"], true) + @provider.expects(:texecute).with(:start, ["/usr/sbin/svcadm", :enable, "-s", "/system/myservice"], true) @provider.start end @@ -112,7 +112,7 @@ describe provider_class do it "should import the manifest if service is missing" do @provider.expects(:svccfg).with(:import, "/tmp/myservice.xml") - @provider.expects(:texecute).with(:start, ["/usr/sbin/svcadm", :enable, "/system/myservice"], true) + @provider.expects(:texecute).with(:start, ["/usr/sbin/svcadm", :enable, "-s", "/system/myservice"], true) @provider.expects(:svcs).with('-H', '-o', 'state,nstate', "/system/myservice").returns("online\t-") @provider.start end @@ -125,7 +125,7 @@ describe provider_class do describe "when stopping" do it "should execute external command 'svcadm disable /system/myservice'" do - @provider.expects(:texecute).with(:stop, ["/usr/sbin/svcadm", :disable, "/system/myservice"], true) + @provider.expects(:texecute).with(:stop, ["/usr/sbin/svcadm", :disable, "-s", "/system/myservice"], true) @provider.stop end end diff --git a/spec/unit/provider/user/pw_spec.rb b/spec/unit/provider/user/pw_spec.rb new file mode 100755 index 000000000..495fef35b --- /dev/null +++ b/spec/unit/provider/user/pw_spec.rb @@ -0,0 +1,183 @@ +#!/usr/bin/env rspec +require 'spec_helper' + +provider_class = Puppet::Type.type(:user).provider(:pw) + +describe provider_class do + let :resource do + Puppet::Type.type(:user).new(:name => "testuser", :provider => :pw) + end + + describe "when creating users" do + let :provider do + prov = resource.provider + prov.expects(:exists?).returns nil + prov + end + + it "should run pw with no additional flags when no properties are given" do + provider.addcmd.must == [provider_class.command(:pw), "useradd", "testuser"] + provider.expects(:execute).with([provider_class.command(:pw), "useradd", "testuser"]) + provider.create + end + + it "should use -o when allowdupe is enabled" do + resource[:allowdupe] = true + provider.expects(:execute).with(includes("-o")) + provider.create + end + + it "should use -c with the correct argument when the comment property is set" do + resource[:comment] = "Testuser Name" + provider.expects(:execute).with(all_of(includes("-c"), includes("Testuser Name"))) + provider.create + end + + it "should use -e with the correct argument when the expiry property is set" do + resource[:expiry] = "2010-02-19" + provider.expects(:execute).with(all_of(includes("-e"), includes("19-02-2010"))) + provider.create + end + + it "should use -g with the correct argument when the gid property is set" do + resource[:gid] = 12345 + provider.expects(:execute).with(all_of(includes("-g"), includes(12345))) + provider.create + end + + it "should use -G with the correct argument when the groups property is set" do + resource[:groups] = "group1" + provider.expects(:execute).with(all_of(includes("-G"), includes("group1"))) + provider.create + end + + it "should use -G with all the given groups when the groups property is set to an array" do + resource[:groups] = ["group1", "group2"] + provider.expects(:execute).with(all_of(includes("-G"), includes("group1,group2"))) + provider.create + end + + it "should use -d with the correct argument when the home property is set" do + resource[:home] = "/home/testuser" + provider.expects(:execute).with(all_of(includes("-d"), includes("/home/testuser"))) + provider.create + end + + it "should use -m when the managehome property is enabled" do + resource[:managehome] = true + provider.expects(:execute).with(includes("-m")) + provider.create + end + + it "should call the password set function with the correct argument when the password property is set" do + resource[:password] = "*" + provider.expects(:execute) + provider.expects(:password=).with("*") + provider.create + end + + it "should use -s with the correct argument when the shell property is set" do + resource[:shell] = "/bin/sh" + provider.expects(:execute).with(all_of(includes("-s"), includes("/bin/sh"))) + provider.create + end + + it "should use -u with the correct argument when the uid property is set" do + resource[:uid] = 12345 + provider.expects(:execute).with(all_of(includes("-u"), includes(12345))) + provider.create + end + + # (#7500) -p should not be used to set a password (it means something else) + it "should not use -p when a password is given" do + resource[:password] = "*" + provider.addcmd.should_not include("-p") + provider.expects(:password=) + provider.expects(:execute).with(Not(includes("-p"))) + provider.create + end + end + + describe "when deleting users" do + it "should run pw with no additional flags" do + provider = resource.provider + provider.expects(:exists?).returns true + provider.deletecmd.must == [provider_class.command(:pw), "userdel", "testuser"] + provider.expects(:execute).with([provider_class.command(:pw), "userdel", "testuser"]) + provider.delete + end + end + + describe "when modifying users" do + let :provider do + resource.provider + end + + it "should run pw with the correct arguments" do + provider.modifycmd("uid", 12345).must == [provider_class.command(:pw), "usermod", "testuser", "-u", 12345] + provider.expects(:execute).with([provider_class.command(:pw), "usermod", "testuser", "-u", 12345]) + provider.uid = 12345 + end + + it "should use -c with the correct argument when the comment property is changed" do + resource[:comment] = "Testuser Name" + provider.expects(:execute).with(all_of(includes("-c"), includes("Testuser New Name"))) + provider.comment = "Testuser New Name" + end + + it "should use -e with the correct argument when the expiry property is changed" do + resource[:expiry] = "2010-02-19" + provider.expects(:execute).with(all_of(includes("-e"), includes("19-02-2011"))) + provider.expiry = "2011-02-19" + end + + it "should use -g with the correct argument when the gid property is changed" do + resource[:gid] = 12345 + provider.expects(:execute).with(all_of(includes("-g"), includes(54321))) + provider.gid = 54321 + end + + it "should use -G with the correct argument when the groups property is changed" do + resource[:groups] = "group1" + provider.expects(:execute).with(all_of(includes("-G"), includes("group2"))) + provider.groups = "group2" + end + + it "should use -G with all the given groups when the groups property is changed with an array" do + resource[:groups] = ["group1", "group2"] + provider.expects(:execute).with(all_of(includes("-G"), includes("group3,group4"))) + provider.groups = "group3,group4" + end + + it "should use -d with the correct argument when the home property is changed" do + resource[:home] = "/home/testuser" + provider.expects(:execute).with(all_of(includes("-d"), includes("/newhome/testuser"))) + provider.home = "/newhome/testuser" + end + + it "should use -m and -d with the correct argument when the home property is changed and managehome is enabled" do + resource[:home] = "/home/testuser" + resource[:managehome] = true + provider.expects(:execute).with(all_of(includes("-d"), includes("/newhome/testuser"), includes("-m"))) + provider.home = "/newhome/testuser" + end + + it "should call the password set function with the correct argument when the password property is changed" do + resource[:password] = "*" + provider.expects(:password=).with("!") + provider.password = "!" + end + + it "should use -s with the correct argument when the shell property is changed" do + resource[:shell] = "/bin/sh" + provider.expects(:execute).with(all_of(includes("-s"), includes("/bin/tcsh"))) + provider.shell = "/bin/tcsh" + end + + it "should use -u with the correct argument when the uid property is changed" do + resource[:uid] = 12345 + provider.expects(:execute).with(all_of(includes("-u"), includes(54321))) + provider.uid = 54321 + end + end +end diff --git a/spec/unit/provider/user/windows_adsi_spec.rb b/spec/unit/provider/user/windows_adsi_spec.rb index 6eff947e0..23fba9983 100755 --- a/spec/unit/provider/user/windows_adsi_spec.rb +++ b/spec/unit/provider/user/windows_adsi_spec.rb @@ -74,6 +74,7 @@ describe Puppet::Type.type(:user).provider(:windows_adsi) do user.stubs(:groups).returns(['group2', 'group3']) create = sequence('create') + user.expects(:password=).in_sequence(create) user.expects(:commit).in_sequence(create) user.expects(:set_groups).with('group1,group2', false).in_sequence(create) user.expects(:[]=).with('Description', 'a test user') diff --git a/spec/unit/rails_spec.rb b/spec/unit/rails_spec.rb index fe7fd8e29..60062db1c 100755 --- a/spec/unit/rails_spec.rb +++ b/spec/unit/rails_spec.rb @@ -88,169 +88,100 @@ describe Puppet::Rails, "when initializing a sqlite3 connection", :if => Puppet. end end -describe Puppet::Rails, "when initializing a mysql connection", :if => Puppet.features.rails? do - it "should provide the adapter, log_level, and host, port, username, password, database, and reconnect arguments" do - Puppet.settings.stubs(:value).with(:dbadapter).returns("mysql") - Puppet.settings.stubs(:value).with(:rails_loglevel).returns("testlevel") - Puppet.settings.stubs(:value).with(:dbserver).returns("testserver") - Puppet.settings.stubs(:value).with(:dbport).returns("") - Puppet.settings.stubs(:value).with(:dbuser).returns("testuser") - Puppet.settings.stubs(:value).with(:dbpassword).returns("testpassword") - Puppet.settings.stubs(:value).with(:dbconnections).returns((pool_size = 45).to_s) - Puppet.settings.stubs(:value).with(:dbname).returns("testname") - Puppet.settings.stubs(:value).with(:dbsocket).returns("") - - Puppet::Rails.database_arguments.should == { - :adapter => "mysql", - :log_level => "testlevel", - :host => "testserver", - :username => "testuser", - :password => "testpassword", - :pool => pool_size, - :database => "testname", - :reconnect => true - } - end - - it "should provide the adapter, log_level, and host, port, username, password, database, socket, connections, and reconnect arguments" do - Puppet.settings.stubs(:value).with(:dbadapter).returns("mysql") - Puppet.settings.stubs(:value).with(:rails_loglevel).returns("testlevel") - Puppet.settings.stubs(:value).with(:dbserver).returns("testserver") - Puppet.settings.stubs(:value).with(:dbport).returns("9999") - Puppet.settings.stubs(:value).with(:dbuser).returns("testuser") - Puppet.settings.stubs(:value).with(:dbpassword).returns("testpassword") - Puppet.settings.stubs(:value).with(:dbconnections).returns((pool_size = 12).to_s) - Puppet.settings.stubs(:value).with(:dbname).returns("testname") - Puppet.settings.stubs(:value).with(:dbsocket).returns("testsocket") - - Puppet::Rails.database_arguments.should == { - :adapter => "mysql", - :log_level => "testlevel", - :host => "testserver", - :port => "9999", - :username => "testuser", - :password => "testpassword", - :pool => pool_size, - :database => "testname", - :socket => "testsocket", - :reconnect => true - } - end - - it "should provide the adapter, log_level, and host, port, username, password, database, socket, and connections arguments" do - Puppet.settings.stubs(:value).with(:dbadapter).returns("mysql") - Puppet.settings.stubs(:value).with(:rails_loglevel).returns("testlevel") - Puppet.settings.stubs(:value).with(:dbserver).returns("testserver") - Puppet.settings.stubs(:value).with(:dbport).returns("9999") - Puppet.settings.stubs(:value).with(:dbuser).returns("testuser") - Puppet.settings.stubs(:value).with(:dbpassword).returns("testpassword") - Puppet.settings.stubs(:value).with(:dbconnections).returns((pool_size = 23).to_s) - Puppet.settings.stubs(:value).with(:dbname).returns("testname") - Puppet.settings.stubs(:value).with(:dbsocket).returns("testsocket") - - Puppet::Rails.database_arguments.should == { - :adapter => "mysql", - :log_level => "testlevel", - :host => "testserver", - :port => "9999", - :username => "testuser", - :password => "testpassword", - :pool => pool_size, - :database => "testname", - :socket => "testsocket", - :reconnect => true - } - end - - it "should not provide the pool if dbconnections is 0, '0', or ''" do - Puppet.settings.stubs(:value).with(:dbadapter).returns("mysql") - Puppet.settings.stubs(:value).with(:rails_loglevel).returns("testlevel") - Puppet.settings.stubs(:value).with(:dbserver).returns("testserver") - Puppet.settings.stubs(:value).with(:dbport).returns("9999") - Puppet.settings.stubs(:value).with(:dbuser).returns("testuser") - Puppet.settings.stubs(:value).with(:dbpassword).returns("testpassword") - Puppet.settings.stubs(:value).with(:dbname).returns("testname") - Puppet.settings.stubs(:value).with(:dbsocket).returns("testsocket") - - Puppet.settings.stubs(:value).with(:dbconnections).returns(0) - Puppet::Rails.database_arguments.should_not be_include(:pool) - - Puppet.settings.stubs(:value).with(:dbconnections).returns('0') - Puppet::Rails.database_arguments.should_not be_include(:pool) - - Puppet.settings.stubs(:value).with(:dbconnections).returns('') - Puppet::Rails.database_arguments.should_not be_include(:pool) - end -end - -describe Puppet::Rails, "when initializing a postgresql connection", :if => Puppet.features.rails? do - it "should provide the adapter, log_level, and host, port, username, password, connections, and database arguments" do - Puppet.settings.stubs(:value).with(:dbadapter).returns("postgresql") - Puppet.settings.stubs(:value).with(:rails_loglevel).returns("testlevel") - Puppet.settings.stubs(:value).with(:dbserver).returns("testserver") - Puppet.settings.stubs(:value).with(:dbport).returns("9999") - Puppet.settings.stubs(:value).with(:dbuser).returns("testuser") - Puppet.settings.stubs(:value).with(:dbpassword).returns("testpassword") - Puppet.settings.stubs(:value).with(:dbconnections).returns((pool_size = 200).to_s) - Puppet.settings.stubs(:value).with(:dbname).returns("testname") - Puppet.settings.stubs(:value).with(:dbsocket).returns("") - - Puppet::Rails.database_arguments.should == { - :adapter => "postgresql", - :log_level => "testlevel", - :host => "testserver", - :port => "9999", - :username => "testuser", - :password => "testpassword", - :pool => pool_size, - :database => "testname", - :reconnect => true - } - end +['mysql','mysql2','postgresql'].each do |dbadapter| + describe Puppet::Rails, "when initializing a #{dbadapter} connection", :if => Puppet.features.rails? do + it "should provide the adapter, log_level, and host, port, username, password, database, and reconnect arguments" do + Puppet.settings.stubs(:value).with(:dbadapter).returns(dbadapter) + Puppet.settings.stubs(:value).with(:rails_loglevel).returns("testlevel") + Puppet.settings.stubs(:value).with(:dbserver).returns("testserver") + Puppet.settings.stubs(:value).with(:dbport).returns("") + Puppet.settings.stubs(:value).with(:dbuser).returns("testuser") + Puppet.settings.stubs(:value).with(:dbpassword).returns("testpassword") + Puppet.settings.stubs(:value).with(:dbconnections).returns((pool_size = 45).to_s) + Puppet.settings.stubs(:value).with(:dbname).returns("testname") + Puppet.settings.stubs(:value).with(:dbsocket).returns("") + + Puppet::Rails.database_arguments.should == { + :adapter => dbadapter, + :log_level => "testlevel", + :host => "testserver", + :username => "testuser", + :password => "testpassword", + :pool => pool_size, + :database => "testname", + :reconnect => true + } + end - it "should provide the adapter, log_level, and host, port, username, password, database, connections, and socket arguments" do - Puppet.settings.stubs(:value).with(:dbadapter).returns("postgresql") - Puppet.settings.stubs(:value).with(:rails_loglevel).returns("testlevel") - Puppet.settings.stubs(:value).with(:dbserver).returns("testserver") - Puppet.settings.stubs(:value).with(:dbport).returns("9999") - Puppet.settings.stubs(:value).with(:dbuser).returns("testuser") - Puppet.settings.stubs(:value).with(:dbpassword).returns("testpassword") - Puppet.settings.stubs(:value).with(:dbconnections).returns((pool_size = 122).to_s) - Puppet.settings.stubs(:value).with(:dbname).returns("testname") - Puppet.settings.stubs(:value).with(:dbsocket).returns("testsocket") + it "should provide the adapter, log_level, and host, port, username, password, database, socket, connections, and reconnect arguments" do + Puppet.settings.stubs(:value).with(:dbadapter).returns(dbadapter) + Puppet.settings.stubs(:value).with(:rails_loglevel).returns("testlevel") + Puppet.settings.stubs(:value).with(:dbserver).returns("testserver") + Puppet.settings.stubs(:value).with(:dbport).returns("9999") + Puppet.settings.stubs(:value).with(:dbuser).returns("testuser") + Puppet.settings.stubs(:value).with(:dbpassword).returns("testpassword") + Puppet.settings.stubs(:value).with(:dbconnections).returns((pool_size = 12).to_s) + Puppet.settings.stubs(:value).with(:dbname).returns("testname") + Puppet.settings.stubs(:value).with(:dbsocket).returns("testsocket") + + Puppet::Rails.database_arguments.should == { + :adapter => dbadapter, + :log_level => "testlevel", + :host => "testserver", + :port => "9999", + :username => "testuser", + :password => "testpassword", + :pool => pool_size, + :database => "testname", + :socket => "testsocket", + :reconnect => true + } + end - Puppet::Rails.database_arguments.should == { - :adapter => "postgresql", - :log_level => "testlevel", - :host => "testserver", - :port => "9999", - :username => "testuser", - :password => "testpassword", - :pool => pool_size, - :database => "testname", - :socket => "testsocket", - :reconnect => true - } - end + it "should provide the adapter, log_level, and host, port, username, password, database, socket, and connections arguments" do + Puppet.settings.stubs(:value).with(:dbadapter).returns(dbadapter) + Puppet.settings.stubs(:value).with(:rails_loglevel).returns("testlevel") + Puppet.settings.stubs(:value).with(:dbserver).returns("testserver") + Puppet.settings.stubs(:value).with(:dbport).returns("9999") + Puppet.settings.stubs(:value).with(:dbuser).returns("testuser") + Puppet.settings.stubs(:value).with(:dbpassword).returns("testpassword") + Puppet.settings.stubs(:value).with(:dbconnections).returns((pool_size = 23).to_s) + Puppet.settings.stubs(:value).with(:dbname).returns("testname") + Puppet.settings.stubs(:value).with(:dbsocket).returns("testsocket") + + Puppet::Rails.database_arguments.should == { + :adapter => dbadapter, + :log_level => "testlevel", + :host => "testserver", + :port => "9999", + :username => "testuser", + :password => "testpassword", + :pool => pool_size, + :database => "testname", + :socket => "testsocket", + :reconnect => true + } + end - it "should not provide the pool if dbconnections is 0, '0', or ''" do - Puppet.settings.stubs(:value).with(:dbadapter).returns("mysql") - Puppet.settings.stubs(:value).with(:rails_loglevel).returns("testlevel") - Puppet.settings.stubs(:value).with(:dbserver).returns("testserver") - Puppet.settings.stubs(:value).with(:dbport).returns("9999") - Puppet.settings.stubs(:value).with(:dbuser).returns("testuser") - Puppet.settings.stubs(:value).with(:dbpassword).returns("testpassword") - Puppet.settings.stubs(:value).with(:dbname).returns("testname") - Puppet.settings.stubs(:value).with(:dbsocket).returns("testsocket") + it "should not provide the pool if dbconnections is 0, '0', or ''" do + Puppet.settings.stubs(:value).with(:dbadapter).returns(dbadapter) + Puppet.settings.stubs(:value).with(:rails_loglevel).returns("testlevel") + Puppet.settings.stubs(:value).with(:dbserver).returns("testserver") + Puppet.settings.stubs(:value).with(:dbport).returns("9999") + Puppet.settings.stubs(:value).with(:dbuser).returns("testuser") + Puppet.settings.stubs(:value).with(:dbpassword).returns("testpassword") + Puppet.settings.stubs(:value).with(:dbname).returns("testname") + Puppet.settings.stubs(:value).with(:dbsocket).returns("testsocket") - Puppet.settings.stubs(:value).with(:dbconnections).returns(0) - Puppet::Rails.database_arguments.should_not be_include(:pool) + Puppet.settings.stubs(:value).with(:dbconnections).returns(0) + Puppet::Rails.database_arguments.should_not be_include(:pool) - Puppet.settings.stubs(:value).with(:dbconnections).returns('0') - Puppet::Rails.database_arguments.should_not be_include(:pool) + Puppet.settings.stubs(:value).with(:dbconnections).returns('0') + Puppet::Rails.database_arguments.should_not be_include(:pool) - Puppet.settings.stubs(:value).with(:dbconnections).returns('') - Puppet::Rails.database_arguments.should_not be_include(:pool) + Puppet.settings.stubs(:value).with(:dbconnections).returns('') + Puppet::Rails.database_arguments.should_not be_include(:pool) + end end end @@ -292,7 +223,7 @@ describe Puppet::Rails, "when initializing an Oracle connection", :if => Puppet. end it "should not provide the pool if dbconnections is 0, '0', or ''" do - Puppet.settings.stubs(:value).with(:dbadapter).returns("mysql") + Puppet.settings.stubs(:value).with(:dbadapter).returns("oracle_enhanced") Puppet.settings.stubs(:value).with(:rails_loglevel).returns("testlevel") Puppet.settings.stubs(:value).with(:dbserver).returns("testserver") Puppet.settings.stubs(:value).with(:dbport).returns("9999") diff --git a/spec/unit/reports/store_spec.rb b/spec/unit/reports/store_spec.rb index 73a7e353f..5b752c4b7 100755 --- a/spec/unit/reports/store_spec.rb +++ b/spec/unit/reports/store_spec.rb @@ -3,6 +3,9 @@ require 'spec_helper' require 'puppet/reports' require 'time' +require 'pathname' +require 'tempfile' +require 'fileutils' processor = Puppet::Reports.report(:store) @@ -10,7 +13,7 @@ describe processor do describe "#process" do include PuppetSpec::Files before :each do - Puppet[:reportdir] = tmpdir('reports') << '/reports' + Puppet[:reportdir] = File.join(tmpdir('reports'), 'reports') @report = YAML.load_file(File.join(PuppetSpec::FIXTURE_DIR, 'yaml/report2.6.x.yaml')).extend processor end @@ -26,5 +29,20 @@ describe processor do File.read(File.join(Puppet[:reportdir], @report.host, "201101061200.yaml")).should == @report.to_yaml end + + it "should write to the report directory in the correct sequence" do + # By doing things in this sequence we should protect against race + # conditions + Time.stubs(:now).returns(Time.parse("2011-01-06 12:00:00 UTC")) + writeseq = sequence("write") + file = mock "file" + Tempfile.expects(:new).in_sequence(writeseq).returns(file) + file.expects(:chmod).in_sequence(writeseq).with(0640) + file.expects(:print).with(@report.to_yaml).in_sequence(writeseq) + file.expects(:close).in_sequence(writeseq) + file.stubs(:path).returns(File.join(Dir.tmpdir, "foo123")) + FileUtils.expects(:mv).in_sequence(writeseq).with(File.join(Dir.tmpdir, "foo123"), File.join(Puppet[:reportdir], @report.host, "201101061200.yaml")) + @report.process + end end end diff --git a/spec/unit/ssl/certificate_factory_spec.rb b/spec/unit/ssl/certificate_factory_spec.rb index fc1b3a740..88521b0fb 100755 --- a/spec/unit/ssl/certificate_factory_spec.rb +++ b/spec/unit/ssl/certificate_factory_spec.rb @@ -52,19 +52,19 @@ describe Puppet::SSL::CertificateFactory do it "should have 24 hours grace on the start of the cert" do cert = subject.build(:server, csr, issuer, serial) - cert.not_before.should be_within(1).of(Time.now - 24*60*60) + cert.not_before.should be_within(30).of(Time.now - 24*60*60) end it "should set the default TTL of the certificate" do ttl = Puppet::SSL::CertificateFactory.ttl cert = subject.build(:server, csr, issuer, serial) - cert.not_after.should be_within(1).of(Time.now + ttl) + cert.not_after.should be_within(30).of(Time.now + ttl) end it "should respect a custom TTL for the CA" do Puppet[:ca_ttl] = 12 cert = subject.build(:server, csr, issuer, serial) - cert.not_after.should be_within(1).of(Time.now + 12) + cert.not_after.should be_within(30).of(Time.now + 12) end it "should build extensions for the certificate" do diff --git a/spec/unit/ssl/host_spec.rb b/spec/unit/ssl/host_spec.rb index dd6434359..3f94407be 100755 --- a/spec/unit/ssl/host_spec.rb +++ b/spec/unit/ssl/host_spec.rb @@ -182,57 +182,48 @@ describe Puppet::SSL::Host do it "should cache the localhost instance" do host = stub 'host', :certificate => "eh", :key => 'foo' Puppet::SSL::Host.expects(:new).once.returns host - Puppet::SSL::Host.localhost.should == Puppet::SSL::Host.localhost end it "should be able to verify its certificate matches its key" do - Puppet::SSL::Host.new("foo").should respond_to(:certificate_matches_key?) + Puppet::SSL::Host.new("foo").should respond_to(:validate_certificate_with_key) end it "should consider the certificate invalid if it cannot find a key" do host = Puppet::SSL::Host.new("foo") + certificate = mock('cert', :fingerprint => 'DEADBEEF') + host.expects(:certificate).twice.returns certificate host.expects(:key).returns nil - - host.should_not be_certificate_matches_key + lambda { host.validate_certificate_with_key }.should raise_error(Puppet::Error, "No private key with which to validate certificate with fingerprint: DEADBEEF") end it "should consider the certificate invalid if it cannot find a certificate" do host = Puppet::SSL::Host.new("foo") - host.expects(:key).returns mock("key") + host.expects(:key).never host.expects(:certificate).returns nil - - host.should_not be_certificate_matches_key + lambda { host.validate_certificate_with_key }.should raise_error(Puppet::Error, "No certificate to validate.") end it "should consider the certificate invalid if the SSL certificate's key verification fails" do host = Puppet::SSL::Host.new("foo") - key = mock 'key', :content => "private_key" sslcert = mock 'sslcert' - certificate = mock 'cert', :content => sslcert - + certificate = mock 'cert', {:content => sslcert, :fingerprint => 'DEADBEEF'} host.stubs(:key).returns key host.stubs(:certificate).returns certificate - sslcert.expects(:check_private_key).with("private_key").returns false - - host.should_not be_certificate_matches_key + lambda { host.validate_certificate_with_key }.should raise_error(Puppet::Error, /DEADBEEF/) end it "should consider the certificate valid if the SSL certificate's key verification succeeds" do host = Puppet::SSL::Host.new("foo") - key = mock 'key', :content => "private_key" sslcert = mock 'sslcert' certificate = mock 'cert', :content => sslcert - host.stubs(:key).returns key host.stubs(:certificate).returns certificate - sslcert.expects(:check_private_key).with("private_key").returns true - - host.should be_certificate_matches_key + lambda{ host.validate_certificate_with_key }.should_not raise_error end describe "when specifying the CA location" do @@ -511,15 +502,13 @@ describe Puppet::SSL::Host do before do @realcert = mock 'certificate' @cert = stub 'cert', :content => @realcert - @host.stubs(:key).returns mock("key") - @host.stubs(:certificate_matches_key?).returns true + @host.stubs(:validate_certificate_with_key) end it "should find the CA certificate if it does not have a certificate" do Puppet::SSL::Certificate.indirection.expects(:find).with(Puppet::SSL::CA_NAME).returns mock("cacert") Puppet::SSL::Certificate.indirection.stubs(:find).with("myname").returns @cert - @host.certificate end @@ -541,34 +530,22 @@ describe Puppet::SSL::Host do it "should find the key if it does not have one" do Puppet::SSL::Certificate.indirection.stubs(:find) @host.expects(:key).returns mock("key") - @host.certificate end it "should generate the key if one cannot be found" do Puppet::SSL::Certificate.indirection.stubs(:find) - @host.expects(:key).returns nil @host.expects(:generate_key) - @host.certificate end it "should find the certificate in the Certificate class and return the Puppet certificate instance" do Puppet::SSL::Certificate.indirection.expects(:find).with(Puppet::SSL::CA_NAME).returns mock("cacert") Puppet::SSL::Certificate.indirection.expects(:find).with("myname").returns @cert - @host.certificate.should equal(@cert) end - it "should fail if the found certificate does not match the private key" do - @host.expects(:certificate_matches_key?).returns false - - Puppet::SSL::Certificate.indirection.stubs(:find).returns @cert - - lambda { @host.certificate }.should raise_error(Puppet::Error) - end - it "should return any previously found certificate" do Puppet::SSL::Certificate.indirection.expects(:find).with(Puppet::SSL::CA_NAME).returns mock("cacert") Puppet::SSL::Certificate.indirection.expects(:find).with("myname").returns(@cert).once diff --git a/spec/unit/transaction/report_spec.rb b/spec/unit/transaction/report_spec.rb index fe5c2e218..91a294d01 100755 --- a/spec/unit/transaction/report_spec.rb +++ b/spec/unit/transaction/report_spec.rb @@ -179,6 +179,11 @@ describe Puppet::Transaction::Report do @report.finalize_report metric(:resources, state.to_s).should == 3 end + + it "should provide 0 for states not in status" do + @report.finalize_report + metric(:resources, state.to_s).should == 0 + end end it "should mark the report as 'failed' if there are failing resources" do @@ -274,13 +279,14 @@ describe Puppet::Transaction::Report do resource = Puppet::Type.type(:notify).new(:name => "testing") catalog = Puppet::Resource::Catalog.new catalog.add_resource resource + catalog.version = 1234567 trans = catalog.apply @report = trans.report @report.finalize_report end - %w{changes time resources events}.each do |main| + %w{changes time resources events version}.each do |main| it "should include the key #{main} in the raw summary hash" do @report.raw_summary.should be_key main end @@ -291,6 +297,28 @@ describe Puppet::Transaction::Report do @report.raw_summary["time"]["last_run"].should == 1289390424 end + it "should include all resource statuses" do + resources_report = @report.raw_summary["resources"] + Puppet::Resource::Status::STATES.each do |state| + resources_report.should be_include(state.to_s) + end + end + + %w{total failure success}.each do |r| + it "should include event #{r}" do + events_report = @report.raw_summary["events"] + events_report.should be_include(r) + end + end + + it "should include config version" do + @report.raw_summary["version"]["config"].should == 1234567 + end + + it "should include puppet version" do + @report.raw_summary["version"]["puppet"].should == Puppet.version + end + %w{Changes Total Resources Time Events}.each do |main| it "should include information on #{main} in the textual summary" do @report.summary.should be_include(main) diff --git a/spec/unit/transaction_spec.rb b/spec/unit/transaction_spec.rb index 9e85390eb..f5720e75a 100755 --- a/spec/unit/transaction_spec.rb +++ b/spec/unit/transaction_spec.rb @@ -436,6 +436,10 @@ describe Puppet::Transaction do @transaction.catalog.add_resource(dependent, dependent2) + # We enqueue them here just so we can check their blockers. This is done + # again in traverse. + graph.enqueue_roots + graph.blockers[dependent].should == 1 graph.blockers[dependent2].should == 1 @@ -453,6 +457,8 @@ describe Puppet::Transaction do @transaction.catalog.add_resource(dependent, dependent2) + graph.enqueue_roots + graph.blockers[dependent].should == 1 graph.blockers[dependent2].should == 1 @@ -510,70 +516,50 @@ describe Puppet::Transaction do end end - describe "when generating resources" do - it "should call 'generate' on all created resources" do - first = Puppet::Type.type(:notify).new(:name => "first") - second = Puppet::Type.type(:notify).new(:name => "second") - third = Puppet::Type.type(:notify).new(:name => "third") + describe "when generating resources before traversal" do + let(:catalog) { Puppet::Resource::Catalog.new } + let(:transaction) { Puppet::Transaction.new(catalog) } + let(:generator) { Puppet::Type.type(:notify).create :title => "generator" } + let(:generated) do + %w[a b c].map { |name| Puppet::Type.type(:notify).new(:name => name) } + end - @catalog = Puppet::Resource::Catalog.new - @transaction = Puppet::Transaction.new(@catalog) + before :each do + catalog.add_resource generator + generator.stubs(:generate).returns generated + end - first.expects(:generate).returns [second] - second.expects(:generate).returns [third] - third.expects(:generate) + it "should call 'generate' on all created resources" do + generated.each { |res| res.expects(:generate) } - @transaction.generate_additional_resources(first) + transaction.add_dynamically_generated_resources end it "should finish all resources" do - generator = stub 'generator', :depthfirst? => true, :tags => [], :ref => "Some[resource]" - resource = stub 'resource', :tag => nil - - @catalog = Puppet::Resource::Catalog.new - @transaction = Puppet::Transaction.new(@catalog) - - generator.expects(:generate).returns [resource] - - @catalog.expects(:add_resource).yields(resource) - - resource.expects(:finish) + generated.each { |res| res.expects(:finish) } - @transaction.generate_additional_resources(generator) + transaction.add_dynamically_generated_resources end it "should skip generated resources that conflict with existing resources" do - generator = mock 'generator', :tags => [] - resource = stub 'resource', :tag => nil - - @catalog = Puppet::Resource::Catalog.new - @transaction = Puppet::Transaction.new(@catalog) + duplicate = generated.first + catalog.add_resource(duplicate) - generator.expects(:generate).returns [resource] + duplicate.expects(:finish).never - @catalog.expects(:add_resource).raises(Puppet::Resource::Catalog::DuplicateResourceError.new("foo")) + duplicate.expects(:info).with { |msg| msg =~ /Duplicate generated resource/ } - resource.expects(:finish).never - resource.expects(:info) # log that it's skipped - - @transaction.generate_additional_resources(generator) + transaction.add_dynamically_generated_resources end it "should copy all tags to the newly generated resources" do - child = stub 'child', :ref => "Some[child_resource]" - generator = stub 'resource', :tags => ["one", "two"], :ref => "Some[resource]" - - @catalog = Puppet::Resource::Catalog.new - @transaction = Puppet::Transaction.new(@catalog) - - generator.stubs(:generate).returns [child] - @catalog.stubs(:add_resource) + generator.tag('one', 'two') - child.expects(:tag).with("one", "two") - child.expects(:finish) - generator.expects(:depthfirst?) + transaction.add_dynamically_generated_resources - @transaction.generate_additional_resources(generator) + generated.each do |res| + res.should be_tagged(generator.tags) + end end end diff --git a/spec/unit/type/file/mode_spec.rb b/spec/unit/type/file/mode_spec.rb index 2c6772aba..88881043c 100755 --- a/spec/unit/type/file/mode_spec.rb +++ b/spec/unit/type/file/mode_spec.rb @@ -21,7 +21,7 @@ describe Puppet::Type.type(:file).attrclass(:mode) do it "should not accept strings other than octal numbers" do expect do mode.value = 'readable please!' - end.to raise_error(Puppet::Error, /File modes can only be octal numbers/) + end.to raise_error(Puppet::Error, /The file mode specification is invalid/) end end diff --git a/spec/unit/type/file/source_spec.rb b/spec/unit/type/file/source_spec.rb index f77645770..50e3679ef 100755 --- a/spec/unit/type/file/source_spec.rb +++ b/spec/unit/type/file/source_spec.rb @@ -156,8 +156,10 @@ describe Puppet::Type.type(:file).attrclass(:source) do @resource = Puppet::Type.type(:file).new :path => @foobar @source = source.new(:resource => @resource) - @metadata = stub 'metadata', :owner => 100, :group => 200, :mode => 123, :checksum => "{md5}asdfasdf", :ftype => "file" + @metadata = stub 'metadata', :owner => 100, :group => 200, :mode => 123, :checksum => "{md5}asdfasdf", :ftype => "file", :source => @foobar @source.stubs(:metadata).returns @metadata + + Puppet.features.stubs(:root?).returns true end it "should fail if there is no metadata" do @@ -184,11 +186,10 @@ describe Puppet::Type.type(:file).attrclass(:source) do describe "and the source is a file" do before do @metadata.stubs(:ftype).returns "file" + Puppet.features.stubs(:microsoft_windows?).returns false end it "should copy the metadata's owner, group, checksum, and mode to the resource if they are not set on the resource" do - Puppet.features.expects(:root?).returns true - @source.copy_source_values @resource[:owner].must == 100 @@ -221,6 +222,30 @@ describe Puppet::Type.type(:file).attrclass(:source) do @resource[:owner].should be_nil end end + + describe "on Windows" do + before :each do + Puppet.features.stubs(:microsoft_windows?).returns true + end + + it "should not copy owner and group from remote sources" do + @source.stubs(:local?).returns false + + @source.copy_source_values + + @resource[:owner].must be_nil + @resource[:group].must be_nil + end + + it "should copy owner and group from local sources" do + @source.stubs(:local?).returns true + + @source.copy_source_values + + @resource[:owner].must == 100 + @resource[:group].must == 200 + end + end end describe "and the source is a link" do diff --git a/spec/unit/type/file_spec.rb b/spec/unit/type/file_spec.rb index cf278b4f8..d4df006b3 100755 --- a/spec/unit/type/file_spec.rb +++ b/spec/unit/type/file_spec.rb @@ -500,8 +500,9 @@ describe Puppet::Type.type(:file) do end it "should not copy values to the child which were set by the source" do - file[:source] = File.expand_path(__FILE__) - metadata = stub 'metadata', :owner => "root", :group => "root", :mode => 0755, :ftype => "file", :checksum => "{md5}whatever" + source = File.expand_path(__FILE__) + file[:source] = source + metadata = stub 'metadata', :owner => "root", :group => "root", :mode => 0755, :ftype => "file", :checksum => "{md5}whatever", :source => source file.parameter(:source).stubs(:metadata).returns metadata file.parameter(:source).copy_source_values @@ -841,7 +842,7 @@ describe Puppet::Type.type(:file) do describe "and multiple sources are provided" do let(:sources) do h = {} - %w{/one /two /three /four}.each do |key| + %w{/a /b /c /d}.each do |key| h[key] = URI.unescape(Puppet::Util.path_to_uri(File.expand_path(key)).to_s) end h @@ -850,11 +851,11 @@ describe Puppet::Type.type(:file) do describe "and :sourceselect is set to :first" do it "should create file instances for the results for the first source to return any values" do data = Puppet::FileServing::Metadata.new("/whatever", :relative_path => "foobar") - file[:source] = sources.keys.map { |key| File.expand_path(key) } - file.expects(:perform_recursion).with(sources['/one']).returns nil - file.expects(:perform_recursion).with(sources['/two']).returns [] - file.expects(:perform_recursion).with(sources['/three']).returns [data] - file.expects(:perform_recursion).with(sources['/four']).never + file[:source] = sources.keys.sort.map { |key| File.expand_path(key) } + file.expects(:perform_recursion).with(sources['/a']).returns nil + file.expects(:perform_recursion).with(sources['/b']).returns [] + file.expects(:perform_recursion).with(sources['/c']).returns [data] + file.expects(:perform_recursion).with(sources['/d']).never file.expects(:newchild).with("foobar").returns @resource file.recurse_remote({}) end @@ -867,22 +868,22 @@ describe Puppet::Type.type(:file) do it "should return every found file that is not in a previous source" do klass = Puppet::FileServing::Metadata - file[:source] = %w{/one /two /three /four}.map {|f| File.expand_path(f) } + file[:source] = %w{/a /b /c /d}.map {|f| File.expand_path(f) } file.stubs(:newchild).returns @resource - one = [klass.new("/one", :relative_path => "a")] - file.expects(:perform_recursion).with(sources['/one']).returns one + one = [klass.new("/a", :relative_path => "a")] + file.expects(:perform_recursion).with(sources['/a']).returns one file.expects(:newchild).with("a").returns @resource - two = [klass.new("/two", :relative_path => "a"), klass.new("/two", :relative_path => "b")] - file.expects(:perform_recursion).with(sources['/two']).returns two + two = [klass.new("/b", :relative_path => "a"), klass.new("/b", :relative_path => "b")] + file.expects(:perform_recursion).with(sources['/b']).returns two file.expects(:newchild).with("b").returns @resource - three = [klass.new("/three", :relative_path => "a"), klass.new("/three", :relative_path => "c")] - file.expects(:perform_recursion).with(sources['/three']).returns three + three = [klass.new("/c", :relative_path => "a"), klass.new("/c", :relative_path => "c")] + file.expects(:perform_recursion).with(sources['/c']).returns three file.expects(:newchild).with("c").returns @resource - file.expects(:perform_recursion).with(sources['/four']).returns [] + file.expects(:perform_recursion).with(sources['/d']).returns [] file.recurse_remote({}) end @@ -1253,6 +1254,36 @@ describe Puppet::Type.type(:file) do end describe "when autorequiring" do + describe "target" do + it "should require file resource when specified with the target property" do + file = described_class.new(:path => File.expand_path("/foo"), :ensure => :directory) + link = described_class.new(:path => File.expand_path("/bar"), :ensure => :symlink, :target => File.expand_path("/foo")) + catalog.add_resource file + catalog.add_resource link + reqs = link.autorequire + reqs.size.must == 1 + reqs[0].source.must == file + reqs[0].target.must == link + end + + it "should require file resource when specified with the ensure property" do + file = described_class.new(:path => File.expand_path("/foo"), :ensure => :directory) + link = described_class.new(:path => File.expand_path("/bar"), :ensure => File.expand_path("/foo")) + catalog.add_resource file + catalog.add_resource link + reqs = link.autorequire + reqs.size.must == 1 + reqs[0].source.must == file + reqs[0].target.must == link + end + + it "should not require target if target is not managed" do + link = described_class.new(:path => File.expand_path('/foo'), :ensure => :symlink, :target => '/bar') + catalog.add_resource link + link.autorequire.size.should == 0 + end + end + describe "directories" do it "should autorequire its parent directory" do dir = described_class.new(:path => File.dirname(path)) diff --git a/spec/unit/type/host_spec.rb b/spec/unit/type/host_spec.rb index 602c428af..3cbe56943 100755 --- a/spec/unit/type/host_spec.rb +++ b/spec/unit/type/host_spec.rb @@ -70,10 +70,537 @@ describe host do proc { @class.new(:name => "foo", :ip => '192.168.0.300') }.should raise_error end + it "should reject over-long IPv4 addresses" do + expect { @class.new(:name => "foo", :ip => '10.10.10.10.10') }.to raise_error + end + it "should not accept malformed IP addresses like 2001:0dg8:85a3:08d3:1319:8a2e:0370:7344" do proc { @class.new(:name => "foo", :ip => '2001:0dg8:85a3:08d3:1319:8a2e:0370:7344') }.should raise_error end + # Assorted, annotated IPv6 passes. + ["::1", # loopback, compressed, non-routable + "::", # unspecified, compressed, non-routable + "0:0:0:0:0:0:0:1", # loopback, full + "0:0:0:0:0:0:0:0", # unspecified, full + "2001:DB8:0:0:8:800:200C:417A", # unicast, full + "FF01:0:0:0:0:0:0:101", # multicast, full + "2001:DB8::8:800:200C:417A", # unicast, compressed + "FF01::101", # multicast, compressed + # Some more test cases that should pass. + "2001:0000:1234:0000:0000:C1C0:ABCD:0876", + "3ffe:0b00:0000:0000:0001:0000:0000:000a", + "FF02:0000:0000:0000:0000:0000:0000:0001", + "0000:0000:0000:0000:0000:0000:0000:0001", + "0000:0000:0000:0000:0000:0000:0000:0000", + # Assorted valid, compressed IPv6 addresses. + "2::10", + "ff02::1", + "fe80::", + "2002::", + "2001:db8::", + "2001:0db8:1234::", + "::ffff:0:0", + "::1", + "1:2:3:4:5:6:7:8", + "1:2:3:4:5:6::8", + "1:2:3:4:5::8", + "1:2:3:4::8", + "1:2:3::8", + "1:2::8", + "1::8", + "1::2:3:4:5:6:7", + "1::2:3:4:5:6", + "1::2:3:4:5", + "1::2:3:4", + "1::2:3", + "1::8", + "::2:3:4:5:6:7:8", + "::2:3:4:5:6:7", + "::2:3:4:5:6", + "::2:3:4:5", + "::2:3:4", + "::2:3", + "::8", + "1:2:3:4:5:6::", + "1:2:3:4:5::", + "1:2:3:4::", + "1:2:3::", + "1:2::", + "1::", + "1:2:3:4:5::7:8", + "1:2:3:4::7:8", + "1:2:3::7:8", + "1:2::7:8", + "1::7:8", + # IPv4 addresses as dotted-quads + "1:2:3:4:5:6:1.2.3.4", + "1:2:3:4:5::1.2.3.4", + "1:2:3:4::1.2.3.4", + "1:2:3::1.2.3.4", + "1:2::1.2.3.4", + "1::1.2.3.4", + "1:2:3:4::5:1.2.3.4", + "1:2:3::5:1.2.3.4", + "1:2::5:1.2.3.4", + "1::5:1.2.3.4", + "1::5:11.22.33.44", + "fe80::217:f2ff:254.7.237.98", + "::ffff:192.168.1.26", + "::ffff:192.168.1.1", + "0:0:0:0:0:0:13.1.68.3", # IPv4-compatible IPv6 address, full, deprecated + "0:0:0:0:0:FFFF:129.144.52.38", # IPv4-mapped IPv6 address, full + "::13.1.68.3", # IPv4-compatible IPv6 address, compressed, deprecated + "::FFFF:129.144.52.38", # IPv4-mapped IPv6 address, compressed + "fe80:0:0:0:204:61ff:254.157.241.86", + "fe80::204:61ff:254.157.241.86", + "::ffff:12.34.56.78", + "::ffff:192.0.2.128", # this is OK, since there's a single zero digit in IPv4 + "fe80:0000:0000:0000:0204:61ff:fe9d:f156", + "fe80:0:0:0:204:61ff:fe9d:f156", + "fe80::204:61ff:fe9d:f156", + "::1", + "fe80::", + "fe80::1", + "::ffff:c000:280", + + # Additional test cases from http://rt.cpan.org/Public/Bug/Display.html?id=50693 + "2001:0db8:85a3:0000:0000:8a2e:0370:7334", + "2001:db8:85a3:0:0:8a2e:370:7334", + "2001:db8:85a3::8a2e:370:7334", + "2001:0db8:0000:0000:0000:0000:1428:57ab", + "2001:0db8:0000:0000:0000::1428:57ab", + "2001:0db8:0:0:0:0:1428:57ab", + "2001:0db8:0:0::1428:57ab", + "2001:0db8::1428:57ab", + "2001:db8::1428:57ab", + "0000:0000:0000:0000:0000:0000:0000:0001", + "::1", + "::ffff:0c22:384e", + "2001:0db8:1234:0000:0000:0000:0000:0000", + "2001:0db8:1234:ffff:ffff:ffff:ffff:ffff", + "2001:db8:a::123", + "fe80::", + + "1111:2222:3333:4444:5555:6666:7777:8888", + "1111:2222:3333:4444:5555:6666:7777::", + "1111:2222:3333:4444:5555:6666::", + "1111:2222:3333:4444:5555::", + "1111:2222:3333:4444::", + "1111:2222:3333::", + "1111:2222::", + "1111::", + "1111:2222:3333:4444:5555:6666::8888", + "1111:2222:3333:4444:5555::8888", + "1111:2222:3333:4444::8888", + "1111:2222:3333::8888", + "1111:2222::8888", + "1111::8888", + "::8888", + "1111:2222:3333:4444:5555::7777:8888", + "1111:2222:3333:4444::7777:8888", + "1111:2222:3333::7777:8888", + "1111:2222::7777:8888", + "1111::7777:8888", + "::7777:8888", + "1111:2222:3333:4444::6666:7777:8888", + "1111:2222:3333::6666:7777:8888", + "1111:2222::6666:7777:8888", + "1111::6666:7777:8888", + "::6666:7777:8888", + "1111:2222:3333::5555:6666:7777:8888", + "1111:2222::5555:6666:7777:8888", + "1111::5555:6666:7777:8888", + "::5555:6666:7777:8888", + "1111:2222::4444:5555:6666:7777:8888", + "1111::4444:5555:6666:7777:8888", + "::4444:5555:6666:7777:8888", + "1111::3333:4444:5555:6666:7777:8888", + "::3333:4444:5555:6666:7777:8888", + "::2222:3333:4444:5555:6666:7777:8888", + "1111:2222:3333:4444:5555:6666:123.123.123.123", + "1111:2222:3333:4444:5555::123.123.123.123", + "1111:2222:3333:4444::123.123.123.123", + "1111:2222:3333::123.123.123.123", + "1111:2222::123.123.123.123", + "1111::123.123.123.123", + "::123.123.123.123", + "1111:2222:3333:4444::6666:123.123.123.123", + "1111:2222:3333::6666:123.123.123.123", + "1111:2222::6666:123.123.123.123", + "1111::6666:123.123.123.123", + "::6666:123.123.123.123", + "1111:2222:3333::5555:6666:123.123.123.123", + "1111:2222::5555:6666:123.123.123.123", + "1111::5555:6666:123.123.123.123", + "::5555:6666:123.123.123.123", + "1111:2222::4444:5555:6666:123.123.123.123", + "1111::4444:5555:6666:123.123.123.123", + "::4444:5555:6666:123.123.123.123", + "1111::3333:4444:5555:6666:123.123.123.123", + "::2222:3333:4444:5555:6666:123.123.123.123", + + # Playing with combinations of "0" and "::"; these are all sytactically + # correct, but are bad form because "0" adjacent to "::" should be + # combined into "::" + "::0:0:0:0:0:0:0", + "::0:0:0:0:0:0", + "::0:0:0:0:0", + "::0:0:0:0", + "::0:0:0", + "::0:0", + "::0", + "0:0:0:0:0:0:0::", + "0:0:0:0:0:0::", + "0:0:0:0:0::", + "0:0:0:0::", + "0:0:0::", + "0:0::", + "0::", + + # Additional cases: http://crisp.tweakblogs.net/blog/2031/ipv6-validation-%28and-caveats%29.html + "0:a:b:c:d:e:f::", + "::0:a:b:c:d:e:f", # syntactically correct, but bad form (::0:... could be combined) + "a:b:c:d:e:f:0::", + ].each do |ip| + it "should accept #{ip.inspect} as an IPv6 address" do + expect { @class.new(:name => "foo", :ip => ip) }.not_to raise_error + end + end + + # ...aaaand, some failure cases. + [":", + "02001:0000:1234:0000:0000:C1C0:ABCD:0876", # extra 0 not allowed! + "2001:0000:1234:0000:00001:C1C0:ABCD:0876", # extra 0 not allowed! + "2001:0000:1234:0000:0000:C1C0:ABCD:0876 0", # junk after valid address + "2001:0000:1234: 0000:0000:C1C0:ABCD:0876", # internal space + "3ffe:0b00:0000:0001:0000:0000:000a", # seven segments + "FF02:0000:0000:0000:0000:0000:0000:0000:0001", # nine segments + "3ffe:b00::1::a", # double "::" + "::1111:2222:3333:4444:5555:6666::", # double "::" + "1:2:3::4:5::7:8", # Double "::" + "12345::6:7:8", + # IPv4 embedded, but bad... + "1::5:400.2.3.4", "1::5:260.2.3.4", "1::5:256.2.3.4", "1::5:1.256.3.4", + "1::5:1.2.256.4", "1::5:1.2.3.256", "1::5:300.2.3.4", "1::5:1.300.3.4", + "1::5:1.2.300.4", "1::5:1.2.3.300", "1::5:900.2.3.4", "1::5:1.900.3.4", + "1::5:1.2.900.4", "1::5:1.2.3.900", "1::5:300.300.300.300", "1::5:3000.30.30.30", + "1::400.2.3.4", "1::260.2.3.4", "1::256.2.3.4", "1::1.256.3.4", + "1::1.2.256.4", "1::1.2.3.256", "1::300.2.3.4", "1::1.300.3.4", + "1::1.2.300.4", "1::1.2.3.300", "1::900.2.3.4", "1::1.900.3.4", + "1::1.2.900.4", "1::1.2.3.900", "1::300.300.300.300", "1::3000.30.30.30", + "::400.2.3.4", "::260.2.3.4", "::256.2.3.4", "::1.256.3.4", + "::1.2.256.4", "::1.2.3.256", "::300.2.3.4", "::1.300.3.4", + "::1.2.300.4", "::1.2.3.300", "::900.2.3.4", "::1.900.3.4", + "::1.2.900.4", "::1.2.3.900", "::300.300.300.300", "::3000.30.30.30", + "2001:1:1:1:1:1:255Z255X255Y255", # garbage instead of "." in IPv4 + "::ffff:192x168.1.26", # ditto + "::ffff:2.3.4", + "::ffff:257.1.2.3", + "1.2.3.4:1111:2222:3333:4444::5555", + "1.2.3.4:1111:2222:3333::5555", + "1.2.3.4:1111:2222::5555", + "1.2.3.4:1111::5555", + "1.2.3.4::5555", + "1.2.3.4::", + + # Testing IPv4 addresses represented as dotted-quads Leading zero's in + # IPv4 addresses not allowed: some systems treat the leading "0" in + # ".086" as the start of an octal number Update: The BNF in RFC-3986 + # explicitly defines the dec-octet (for IPv4 addresses) not to have a + # leading zero + "fe80:0000:0000:0000:0204:61ff:254.157.241.086", + "XXXX:XXXX:XXXX:XXXX:XXXX:XXXX:1.2.3.4", + "1111:2222:3333:4444:5555:6666:00.00.00.00", + "1111:2222:3333:4444:5555:6666:000.000.000.000", + "1111:2222:3333:4444:5555:6666:256.256.256.256", + + "1111:2222:3333:4444::5555:", + "1111:2222:3333::5555:", + "1111:2222::5555:", + "1111::5555:", + "::5555:", + ":::", + "1111:", + ":", + + ":1111:2222:3333:4444::5555", + ":1111:2222:3333::5555", + ":1111:2222::5555", + ":1111::5555", + ":::5555", + ":::", + + # Additional test cases from http://rt.cpan.org/Public/Bug/Display.html?id=50693 + "123", + "ldkfj", + "2001::FFD3::57ab", + "2001:db8:85a3::8a2e:37023:7334", + "2001:db8:85a3::8a2e:370k:7334", + "1:2:3:4:5:6:7:8:9", + "1::2::3", + "1:::3:4:5", + "1:2:3::4:5:6:7:8:9", + + # Invalid data + "XXXX:XXXX:XXXX:XXXX:XXXX:XXXX:XXXX:XXXX", + + # Too many components + "1111:2222:3333:4444:5555:6666:7777:8888:9999", + "1111:2222:3333:4444:5555:6666:7777:8888::", + "::2222:3333:4444:5555:6666:7777:8888:9999", + + # Too few components + "1111:2222:3333:4444:5555:6666:7777", + "1111:2222:3333:4444:5555:6666", + "1111:2222:3333:4444:5555", + "1111:2222:3333:4444", + "1111:2222:3333", + "1111:2222", + "1111", + + # Missing : + "11112222:3333:4444:5555:6666:7777:8888", + "1111:22223333:4444:5555:6666:7777:8888", + "1111:2222:33334444:5555:6666:7777:8888", + "1111:2222:3333:44445555:6666:7777:8888", + "1111:2222:3333:4444:55556666:7777:8888", + "1111:2222:3333:4444:5555:66667777:8888", + "1111:2222:3333:4444:5555:6666:77778888", + + # Missing : intended for :: + "1111:2222:3333:4444:5555:6666:7777:8888:", + "1111:2222:3333:4444:5555:6666:7777:", + "1111:2222:3333:4444:5555:6666:", + "1111:2222:3333:4444:5555:", + "1111:2222:3333:4444:", + "1111:2222:3333:", + "1111:2222:", + "1111:", + ":", + ":8888", + ":7777:8888", + ":6666:7777:8888", + ":5555:6666:7777:8888", + ":4444:5555:6666:7777:8888", + ":3333:4444:5555:6666:7777:8888", + ":2222:3333:4444:5555:6666:7777:8888", + ":1111:2222:3333:4444:5555:6666:7777:8888", + + # ::: + ":::2222:3333:4444:5555:6666:7777:8888", + "1111:::3333:4444:5555:6666:7777:8888", + "1111:2222:::4444:5555:6666:7777:8888", + "1111:2222:3333:::5555:6666:7777:8888", + "1111:2222:3333:4444:::6666:7777:8888", + "1111:2222:3333:4444:5555:::7777:8888", + "1111:2222:3333:4444:5555:6666:::8888", + "1111:2222:3333:4444:5555:6666:7777:::", + + # Double ::", + "::2222::4444:5555:6666:7777:8888", + "::2222:3333::5555:6666:7777:8888", + "::2222:3333:4444::6666:7777:8888", + "::2222:3333:4444:5555::7777:8888", + "::2222:3333:4444:5555:7777::8888", + "::2222:3333:4444:5555:7777:8888::", + + "1111::3333::5555:6666:7777:8888", + "1111::3333:4444::6666:7777:8888", + "1111::3333:4444:5555::7777:8888", + "1111::3333:4444:5555:6666::8888", + "1111::3333:4444:5555:6666:7777::", + + "1111:2222::4444::6666:7777:8888", + "1111:2222::4444:5555::7777:8888", + "1111:2222::4444:5555:6666::8888", + "1111:2222::4444:5555:6666:7777::", + + "1111:2222:3333::5555::7777:8888", + "1111:2222:3333::5555:6666::8888", + "1111:2222:3333::5555:6666:7777::", + + "1111:2222:3333:4444::6666::8888", + "1111:2222:3333:4444::6666:7777::", + + "1111:2222:3333:4444:5555::7777::", + + + # Too many components" + "1111:2222:3333:4444:5555:6666:7777:8888:1.2.3.4", + "1111:2222:3333:4444:5555:6666:7777:1.2.3.4", + "1111:2222:3333:4444:5555:6666::1.2.3.4", + "::2222:3333:4444:5555:6666:7777:1.2.3.4", + "1111:2222:3333:4444:5555:6666:1.2.3.4.5", + + # Too few components + "1111:2222:3333:4444:5555:1.2.3.4", + "1111:2222:3333:4444:1.2.3.4", + "1111:2222:3333:1.2.3.4", + "1111:2222:1.2.3.4", + "1111:1.2.3.4", + + # Missing : + "11112222:3333:4444:5555:6666:1.2.3.4", + "1111:22223333:4444:5555:6666:1.2.3.4", + "1111:2222:33334444:5555:6666:1.2.3.4", + "1111:2222:3333:44445555:6666:1.2.3.4", + "1111:2222:3333:4444:55556666:1.2.3.4", + "1111:2222:3333:4444:5555:66661.2.3.4", + + # Missing . + "1111:2222:3333:4444:5555:6666:255255.255.255", + "1111:2222:3333:4444:5555:6666:255.255255.255", + "1111:2222:3333:4444:5555:6666:255.255.255255", + + # Missing : intended for :: + ":1.2.3.4", + ":6666:1.2.3.4", + ":5555:6666:1.2.3.4", + ":4444:5555:6666:1.2.3.4", + ":3333:4444:5555:6666:1.2.3.4", + ":2222:3333:4444:5555:6666:1.2.3.4", + ":1111:2222:3333:4444:5555:6666:1.2.3.4", + + # ::: + ":::2222:3333:4444:5555:6666:1.2.3.4", + "1111:::3333:4444:5555:6666:1.2.3.4", + "1111:2222:::4444:5555:6666:1.2.3.4", + "1111:2222:3333:::5555:6666:1.2.3.4", + "1111:2222:3333:4444:::6666:1.2.3.4", + "1111:2222:3333:4444:5555:::1.2.3.4", + + # Double :: + "::2222::4444:5555:6666:1.2.3.4", + "::2222:3333::5555:6666:1.2.3.4", + "::2222:3333:4444::6666:1.2.3.4", + "::2222:3333:4444:5555::1.2.3.4", + + "1111::3333::5555:6666:1.2.3.4", + "1111::3333:4444::6666:1.2.3.4", + "1111::3333:4444:5555::1.2.3.4", + + "1111:2222::4444::6666:1.2.3.4", + "1111:2222::4444:5555::1.2.3.4", + + "1111:2222:3333::5555::1.2.3.4", + + # Missing parts + "::.", + "::..", + "::...", + "::1...", + "::1.2..", + "::1.2.3.", + "::.2..", + "::.2.3.", + "::.2.3.4", + "::..3.", + "::..3.4", + "::...4", + + # Extra : in front + ":1111:2222:3333:4444:5555:6666:7777::", + ":1111:2222:3333:4444:5555:6666::", + ":1111:2222:3333:4444:5555::", + ":1111:2222:3333:4444::", + ":1111:2222:3333::", + ":1111:2222::", + ":1111::", + ":::", + ":1111:2222:3333:4444:5555:6666::8888", + ":1111:2222:3333:4444:5555::8888", + ":1111:2222:3333:4444::8888", + ":1111:2222:3333::8888", + ":1111:2222::8888", + ":1111::8888", + ":::8888", + ":1111:2222:3333:4444:5555::7777:8888", + ":1111:2222:3333:4444::7777:8888", + ":1111:2222:3333::7777:8888", + ":1111:2222::7777:8888", + ":1111::7777:8888", + ":::7777:8888", + ":1111:2222:3333:4444::6666:7777:8888", + ":1111:2222:3333::6666:7777:8888", + ":1111:2222::6666:7777:8888", + ":1111::6666:7777:8888", + ":::6666:7777:8888", + ":1111:2222:3333::5555:6666:7777:8888", + ":1111:2222::5555:6666:7777:8888", + ":1111::5555:6666:7777:8888", + ":::5555:6666:7777:8888", + ":1111:2222::4444:5555:6666:7777:8888", + ":1111::4444:5555:6666:7777:8888", + ":::4444:5555:6666:7777:8888", + ":1111::3333:4444:5555:6666:7777:8888", + ":::3333:4444:5555:6666:7777:8888", + ":::2222:3333:4444:5555:6666:7777:8888", + ":1111:2222:3333:4444:5555:6666:1.2.3.4", + ":1111:2222:3333:4444:5555::1.2.3.4", + ":1111:2222:3333:4444::1.2.3.4", + ":1111:2222:3333::1.2.3.4", + ":1111:2222::1.2.3.4", + ":1111::1.2.3.4", + ":::1.2.3.4", + ":1111:2222:3333:4444::6666:1.2.3.4", + ":1111:2222:3333::6666:1.2.3.4", + ":1111:2222::6666:1.2.3.4", + ":1111::6666:1.2.3.4", + ":::6666:1.2.3.4", + ":1111:2222:3333::5555:6666:1.2.3.4", + ":1111:2222::5555:6666:1.2.3.4", + ":1111::5555:6666:1.2.3.4", + ":::5555:6666:1.2.3.4", + ":1111:2222::4444:5555:6666:1.2.3.4", + ":1111::4444:5555:6666:1.2.3.4", + ":::4444:5555:6666:1.2.3.4", + ":1111::3333:4444:5555:6666:1.2.3.4", + ":::2222:3333:4444:5555:6666:1.2.3.4", + + # Extra : at end + "1111:2222:3333:4444:5555:6666:7777:::", + "1111:2222:3333:4444:5555:6666:::", + "1111:2222:3333:4444:5555:::", + "1111:2222:3333:4444:::", + "1111:2222:3333:::", + "1111:2222:::", + "1111:::", + ":::", + "1111:2222:3333:4444:5555:6666::8888:", + "1111:2222:3333:4444:5555::8888:", + "1111:2222:3333:4444::8888:", + "1111:2222:3333::8888:", + "1111:2222::8888:", + "1111::8888:", + "::8888:", + "1111:2222:3333:4444:5555::7777:8888:", + "1111:2222:3333:4444::7777:8888:", + "1111:2222:3333::7777:8888:", + "1111:2222::7777:8888:", + "1111::7777:8888:", + "::7777:8888:", + "1111:2222:3333:4444::6666:7777:8888:", + "1111:2222:3333::6666:7777:8888:", + "1111:2222::6666:7777:8888:", + "1111::6666:7777:8888:", + "::6666:7777:8888:", + "1111:2222:3333::5555:6666:7777:8888:", + "1111:2222::5555:6666:7777:8888:", + "1111::5555:6666:7777:8888:", + "::5555:6666:7777:8888:", + "1111:2222::4444:5555:6666:7777:8888:", + "1111::4444:5555:6666:7777:8888:", + "::4444:5555:6666:7777:8888:", + "1111::3333:4444:5555:6666:7777:8888:", + "::3333:4444:5555:6666:7777:8888:", + "::2222:3333:4444:5555:6666:7777:8888:", + ].each do |ip| + it "should reject #{ip.inspect} as an IPv6 address" do + expect { @class.new(:name => "foo", :ip => ip) }.to raise_error + end + end + it "should not accept spaces in resourcename" do proc { @class.new(:name => "foo bar") }.should raise_error end diff --git a/spec/unit/type/schedule_spec.rb b/spec/unit/type/schedule_spec.rb index b302d95cd..d407057a8 100755 --- a/spec/unit/type/schedule_spec.rb +++ b/spec/unit/type/schedule_spec.rb @@ -82,17 +82,17 @@ describe Puppet::Type.type(:schedule) do it "should match when the start time is before the current time and the end time is after the current time" do @schedule[:range] = "10:59:50 - 11:00:10" - @schedule.should be_match + @schedule.must be_match end it "should not match when the start time is after the current time" do @schedule[:range] = "11:00:05 - 11:00:10" - @schedule.should_not be_match + @schedule.must_not be_match end it "should not match when the end time is previous to the current time" do @schedule[:range] = "10:59:50 - 10:59:55" - @schedule.should be_match + @schedule.must_not be_match end it "should throw an error if the upper limit is less than the lower limit" do @@ -103,17 +103,17 @@ describe Puppet::Type.type(:schedule) do it "should not match the current time fails between an array of ranges" do @schedule[:range] = ["4-6", "20-23"] - @schedule.should_not be_match + @schedule.must_not be_match end it "should match the lower array of ranges" do @schedule[:range] = ["9-11", "14-16"] - @schedule.should be_match + @schedule.must be_match end it "should match the upper array of ranges" do @schedule[:range] = ["4-6", "11-12"] - @schedule.should be_match + @schedule.must be_match end end diff --git a/spec/unit/type_spec.rb b/spec/unit/type_spec.rb index 3b5f0cba6..5fa9f4534 100755 --- a/spec/unit/type_spec.rb +++ b/spec/unit/type_spec.rb @@ -588,6 +588,35 @@ describe Puppet::Type, :fails_on_windows => true do resource.should_not be_suitable end end + + describe "::ensurable?" do + before :each do + class TestEnsurableType < Puppet::Type + def exists?; end + def create; end + def destroy; end + end + end + + it "is true if the class has exists?, create, and destroy methods defined" do + TestEnsurableType.should be_ensurable + end + + it "is false if exists? is not defined" do + TestEnsurableType.class_eval { remove_method(:exists?) } + TestEnsurableType.should_not be_ensurable + end + + it "is false if create is not defined" do + TestEnsurableType.class_eval { remove_method(:create) } + TestEnsurableType.should_not be_ensurable + end + + it "is false if destroy is not defined" do + TestEnsurableType.class_eval { remove_method(:destroy) } + TestEnsurableType.should_not be_ensurable + end + end end describe Puppet::Type::RelationshipMetaparam do diff --git a/spec/unit/util/anonymous_filelock_spec.rb b/spec/unit/util/anonymous_filelock_spec.rb new file mode 100644 index 000000000..784ac0fca --- /dev/null +++ b/spec/unit/util/anonymous_filelock_spec.rb @@ -0,0 +1,78 @@ +#!/usr/bin/env rspec +require 'spec_helper' + +require 'puppet/util/anonymous_filelock' + +describe Puppet::Util::AnonymousFilelock do + require 'puppet_spec/files' + include PuppetSpec::Files + + before(:each) do + @lockfile = tmpfile("lock") + @lock = Puppet::Util::AnonymousFilelock.new(@lockfile) + end + + it "should be anonymous" do + @lock.should be_anonymous + end + + describe "#lock" do + it "should return false if already locked" do + @lock.stubs(:locked?).returns(true) + @lock.lock.should be_false + end + + it "should return true if it successfully locked" do + @lock.lock.should be_true + end + + it "should create a lock file" do + @lock.lock + + File.should be_exists(@lockfile) + end + + it "should create a lock file containing a message" do + @lock.lock("message") + + File.read(@lockfile).should == "message" + end + end + + describe "#unlock" do + it "should return true when unlocking" do + @lock.lock + @lock.unlock.should be_true + end + + it "should return false when not locked" do + @lock.unlock.should be_false + end + + it "should clear the lock file" do + File.open(@lockfile, 'w') { |fd| fd.print("locked") } + @lock.unlock + File.should_not be_exists(@lockfile) + end + end + + it "should be locked when locked" do + @lock.lock + @lock.should be_locked + end + + it "should not be locked when not locked" do + @lock.should_not be_locked + end + + it "should not be locked when unlocked" do + @lock.lock + @lock.unlock + @lock.should_not be_locked + end + + it "should return the lock message" do + @lock.lock("lock message") + @lock.message.should == "lock message" + end +end
\ No newline at end of file diff --git a/spec/unit/util/execution_stub_spec.rb b/spec/unit/util/execution_stub_spec.rb index 57305e315..b66f88f83 100755 --- a/spec/unit/util/execution_stub_spec.rb +++ b/spec/unit/util/execution_stub_spec.rb @@ -16,7 +16,8 @@ describe Puppet::Util::ExecutionStub do Puppet::Util::ExecutionStub.current_value.should == nil end - it "should restore normal execution after 'reset' is called" do + # fails on windows, see #11740 + it "should restore normal execution after 'reset' is called", :fails_on_windows => true do # Note: "true" exists at different paths in different OSes if Puppet.features.microsoft_windows? true_command = [Puppet::Util.which('cmd.exe').tr('/', '\\'), '/c', 'exit 0'] diff --git a/spec/unit/util/instrumentation/data_spec.rb b/spec/unit/util/instrumentation/data_spec.rb new file mode 100755 index 000000000..c2465f622 --- /dev/null +++ b/spec/unit/util/instrumentation/data_spec.rb @@ -0,0 +1,44 @@ +#!/usr/bin/env rspec + +require 'spec_helper' +require 'matchers/json' +require 'puppet/util/instrumentation' +require 'puppet/util/instrumentation/data' + +describe Puppet::Util::Instrumentation::Data do + Puppet::Util::Instrumentation::Data + + before(:each) do + @listener = stub 'listener', :name => "name" + Puppet::Util::Instrumentation.stubs(:[]).with("name").returns(@listener) + end + + it "should indirect instrumentation_data" do + Puppet::Util::Instrumentation::Data.indirection.name.should == :instrumentation_data + end + + it "should lookup the corresponding listener" do + Puppet::Util::Instrumentation.expects(:[]).with("name").returns(@listener) + Puppet::Util::Instrumentation::Data.new("name") + end + + it "should error if the listener can not be found" do + Puppet::Util::Instrumentation.expects(:[]).with("name").returns(nil) + expect { Puppet::Util::Instrumentation::Data.new("name") }.to raise_error + end + + it "should return pson data" do + data = Puppet::Util::Instrumentation::Data.new("name") + @listener.stubs(:data).returns({ :this_is_data => "here also" }) + data.should set_json_attribute('name').to("name") + data.should set_json_attribute('this_is_data').to("here also") + end + + it "should not error if the underlying listener doesn't have data" do + lambda { Puppet::Util::Instrumentation::Data.new("name").to_pson }.should_not raise_error + end + + it "should return a hash containing data when unserializing from pson" do + Puppet::Util::Instrumentation::Data.from_pson({:name => "name"}).should == {:name => "name"} + end +end
\ No newline at end of file diff --git a/spec/unit/util/instrumentation/indirection_probe_spec.rb b/spec/unit/util/instrumentation/indirection_probe_spec.rb new file mode 100644 index 000000000..654825c9a --- /dev/null +++ b/spec/unit/util/instrumentation/indirection_probe_spec.rb @@ -0,0 +1,19 @@ +#!/usr/bin/env rspec + +require 'spec_helper' +require 'matchers/json' +require 'puppet/util/instrumentation' +require 'puppet/util/instrumentation/indirection_probe' + +describe Puppet::Util::Instrumentation::IndirectionProbe do + Puppet::Util::Instrumentation::IndirectionProbe + + it "should indirect instrumentation_probe" do + Puppet::Util::Instrumentation::IndirectionProbe.indirection.name.should == :instrumentation_probe + end + + it "should return pson data" do + probe = Puppet::Util::Instrumentation::IndirectionProbe.new("probe") + probe.should set_json_attribute('name').to("probe") + end +end
\ No newline at end of file diff --git a/spec/unit/util/instrumentation/instrumentable_spec.rb b/spec/unit/util/instrumentation/instrumentable_spec.rb new file mode 100755 index 000000000..dd2ad3084 --- /dev/null +++ b/spec/unit/util/instrumentation/instrumentable_spec.rb @@ -0,0 +1,186 @@ +#!/usr/bin/env rspec + +require 'spec_helper' + +require 'puppet/util/instrumentation' +require 'puppet/util/instrumentation/instrumentable' + +describe Puppet::Util::Instrumentation::Instrumentable::Probe do + + before(:each) do + Puppet::Util::Instrumentation.stubs(:start) + Puppet::Util::Instrumentation.stubs(:stop) + + class ProbeTest + def mymethod(arg1, arg2, arg3) + :it_worked + end + end + end + + after(:each) do + if ProbeTest.method_defined?(:instrumented_mymethod) + ProbeTest.class_eval { + remove_method(:mymethod) + alias_method(:mymethod, :instrumented_mymethod) + } + end + Puppet::Util::Instrumentation::Instrumentable.clear_probes + end + + describe "when enabling a probe" do + it "should raise an error if the probe is already enabled" do + probe = Puppet::Util::Instrumentation::Instrumentable::Probe.new(:mymethod, ProbeTest) + probe.enable + lambda { probe.enable }.should raise_error + end + + it "should rename the original method name" do + probe = Puppet::Util::Instrumentation::Instrumentable::Probe.new(:mymethod, ProbeTest) + probe.enable + ProbeTest.new.should respond_to(:instrumented_mymethod) + end + + it "should create a new method of the original name" do + probe = Puppet::Util::Instrumentation::Instrumentable::Probe.new(:mymethod, ProbeTest) + probe.enable + ProbeTest.new.should respond_to(:mymethod) + end + end + + describe "when disabling a probe" do + it "should raise an error if the probe is already enabled" do + probe = Puppet::Util::Instrumentation::Instrumentable::Probe.new(:mymethod, ProbeTest) + lambda { probe.disable }.should raise_error + end + + it "should rename the original method name" do + probe = Puppet::Util::Instrumentation::Instrumentable::Probe.new(:mymethod, ProbeTest) + probe.enable + probe.disable + + Puppet::Util::Instrumentation.expects(:start).never + Puppet::Util::Instrumentation.expects(:stop).never + ProbeTest.new.mymethod(1,2,3).should == :it_worked + end + + it "should remove the created method" do + probe = Puppet::Util::Instrumentation::Instrumentable::Probe.new(:mymethod, ProbeTest) + probe.enable + probe.disable + ProbeTest.new.should_not respond_to(:instrumented_mymethod) + end + end + + describe "when a probe is called" do + it "should call the original method" do + probe = Puppet::Util::Instrumentation::Instrumentable::Probe.new(:mymethod, ProbeTest) + probe.enable + test = ProbeTest.new + test.expects(:instrumented_mymethod).with(1,2,3) + test.mymethod(1,2,3) + end + + it "should start the instrumentation" do + Puppet::Util::Instrumentation.expects(:start) + probe = Puppet::Util::Instrumentation::Instrumentable::Probe.new(:mymethod, ProbeTest) + probe.enable + test = ProbeTest.new + test.mymethod(1,2,3) + end + + it "should stop the instrumentation" do + Puppet::Util::Instrumentation.expects(:stop) + probe = Puppet::Util::Instrumentation::Instrumentable::Probe.new(:mymethod, ProbeTest) + probe.enable + test = ProbeTest.new + test.mymethod(1,2,3) + end + + describe "and the original method raises an exception" do + it "should propagate the exception" do + probe = Puppet::Util::Instrumentation::Instrumentable::Probe.new(:mymethod, ProbeTest) + probe.enable + test = ProbeTest.new + test.expects(:instrumented_mymethod).with(1,2,3).raises + lambda { test.mymethod(1,2,3) }.should raise_error + end + + it "should stop the instrumentation" do + Puppet::Util::Instrumentation.expects(:stop) + probe = Puppet::Util::Instrumentation::Instrumentable::Probe.new(:mymethod, ProbeTest) + probe.enable + test = ProbeTest.new + test.expects(:instrumented_mymethod).with(1,2,3).raises + lambda { test.mymethod(1,2,3) }.should raise_error + end + end + + describe "with a static label" do + it "should send the label to the instrumentation layer" do + probe = Puppet::Util::Instrumentation::Instrumentable::Probe.new(:mymethod, ProbeTest, :label => :mylabel) + probe.enable + test = ProbeTest.new + Puppet::Util::Instrumentation.expects(:start).with { |label,data| label == :mylabel }.returns(42) + Puppet::Util::Instrumentation.expects(:stop).with(:mylabel, 42, {}) + test.mymethod(1,2,3) + end + end + + describe "with a dynamic label" do + it "should send the evaluated label to the instrumentation layer" do + probe = Puppet::Util::Instrumentation::Instrumentable::Probe.new(:mymethod, ProbeTest, :label => Proc.new { |parent,args| "dynamic#{args[0]}" } ) + probe.enable + test = ProbeTest.new + Puppet::Util::Instrumentation.expects(:start).with { |label,data| label == "dynamic1" }.returns(42) + Puppet::Util::Instrumentation.expects(:stop).with("dynamic1",42,{}) + test.mymethod(1,2,3) + end + end + + describe "with static data" do + it "should send the data to the instrumentation layer" do + probe = Puppet::Util::Instrumentation::Instrumentable::Probe.new(:mymethod, ProbeTest, :data => { :static_data => "nothing" }) + probe.enable + test = ProbeTest.new + Puppet::Util::Instrumentation.expects(:start).with { |label,data| data == { :static_data => "nothing" }} + test.mymethod(1,2,3) + end + end + + describe "with dynamic data" do + it "should send the evaluated label to the instrumentation layer" do + probe = Puppet::Util::Instrumentation::Instrumentable::Probe.new(:mymethod, ProbeTest, :data => Proc.new { |parent, args| { :key => args[0] } } ) + probe.enable + test = ProbeTest.new + Puppet::Util::Instrumentation.expects(:start).with { |label,data| data == { :key => 1 } } + Puppet::Util::Instrumentation.expects(:stop) + test.mymethod(1,2,3) + end + end + end +end + +describe Puppet::Util::Instrumentation::Instrumentable do + before(:each) do + class ProbeTest2 + extend Puppet::Util::Instrumentation::Instrumentable + probe :mymethod + def mymethod(arg1,arg2,arg3) + end + end + end + + after do + Puppet::Util::Instrumentation::Instrumentable.clear_probes + end + + it "should allow probe definition" do + Puppet::Util::Instrumentation::Instrumentable.probe_names.should be_include("ProbeTest2.mymethod") + end + + it "should be able to enable all probes" do + Puppet::Util::Instrumentation::Instrumentable.enable_probes + ProbeTest2.new.should respond_to(:instrumented_mymethod) + end +end
\ No newline at end of file diff --git a/spec/unit/util/instrumentation/listener_spec.rb b/spec/unit/util/instrumentation/listener_spec.rb new file mode 100755 index 000000000..bc49d265c --- /dev/null +++ b/spec/unit/util/instrumentation/listener_spec.rb @@ -0,0 +1,100 @@ +#!/usr/bin/env rspec + +require 'spec_helper' +require 'matchers/json' + +require 'puppet/util/instrumentation' +require 'puppet/util/instrumentation/listener' + +describe Puppet::Util::Instrumentation::Listener do + + Listener = Puppet::Util::Instrumentation::Listener + + before(:each) do + @delegate = stub 'listener', :notify => nil, :name => 'listener' + @listener = Listener.new(@delegate) + @listener.enabled = true + end + + it "should indirect instrumentation_listener" do + Listener.indirection.name.should == :instrumentation_listener + end + + it "should raise an error if delegate doesn't support notify" do + lambda { Listener.new(Object.new) }.should raise_error + end + + it "should not be enabled by default" do + Listener.new(@delegate).should_not be_enabled + end + + it "should delegate notification" do + @delegate.expects(:notify).with(:event, :start, {}) + listener = Listener.new(@delegate) + listener.notify(:event, :start, {}) + end + + it "should not listen is not enabled" do + @listener.enabled = false + @listener.should_not be_listen_to(:label) + end + + it "should listen to all label if created without pattern" do + @listener.should be_listen_to(:improbable_label) + end + + it "should listen to specific string pattern" do + listener = Listener.new(@delegate, "specific") + listener.enabled = true + listener.should be_listen_to(:specific) + end + + it "should not listen to non-matching string pattern" do + listener = Listener.new(@delegate, "specific") + listener.enabled = true + listener.should_not be_listen_to(:unspecific) + end + + it "should listen to specific regex pattern" do + listener = Listener.new(@delegate, /spe.*/) + listener.enabled = true + listener.should be_listen_to(:specific_pattern) + end + + it "should not listen to non matching regex pattern" do + listener = Listener.new(@delegate, /^match.*/) + listener.enabled = true + listener.should_not be_listen_to(:not_matching) + end + + it "should delegate its name to the underlying listener" do + @delegate.expects(:name).returns("myname") + @listener.name.should == "myname" + end + + it "should delegate data fetching to the underlying listener" do + @delegate.expects(:data).returns(:data) + @listener.data.should == {:data => :data } + end + + describe "when serializing to pson" do + it "should return a pson object containing pattern, name and status" do + @listener.should set_json_attribute('enabled').to(true) + @listener.should set_json_attribute('name').to("listener") + end + end + + describe "when deserializing from pson" do + it "should lookup the archetype listener from the instrumentation layer" do + Puppet::Util::Instrumentation.expects(:[]).with("listener").returns(@listener) + Puppet::Util::Instrumentation::Listener.from_pson({"name" => "listener"}) + end + + it "should create a new listener shell instance delegating to the archetypal listener" do + Puppet::Util::Instrumentation.expects(:[]).with("listener").returns(@listener) + @listener.stubs(:listener).returns(@delegate) + Puppet::Util::Instrumentation::Listener.expects(:new).with(@delegate, nil, true) + Puppet::Util::Instrumentation::Listener.from_pson({"name" => "listener", "enabled" => true}) + end + end +end
\ No newline at end of file diff --git a/spec/unit/util/instrumentation/listeners/log_spec.rb b/spec/unit/util/instrumentation/listeners/log_spec.rb new file mode 100755 index 000000000..8359625a1 --- /dev/null +++ b/spec/unit/util/instrumentation/listeners/log_spec.rb @@ -0,0 +1,34 @@ +require 'spec_helper' +require 'puppet/util/instrumentation' + +Puppet::Util::Instrumentation.init +log = Puppet::Util::Instrumentation.listener(:log) + +describe log do + before(:each) do + @log = log.new + end + + it "should have a notify method" do + @log.should respond_to(:notify) + end + + it "should have a data method" do + @log.should respond_to(:data) + end + + it "should keep data for stop event" do + @log.notify(:test, :stop, { :started => Time.at(123456789), :finished => Time.at(123456790)}) + @log.data.should == {:test=>["test took 1.0"]} + end + + it "should not keep data for start event" do + @log.notify(:test, :start, { :started => Time.at(123456789)}) + @log.data.should be_empty + end + + it "should not keep more than 20 events per label" do + 25.times { @log.notify(:test, :stop, { :started => Time.at(123456789), :finished => Time.at(123456790)}) } + @log.data[:test].size.should == 20 + end +end
\ No newline at end of file diff --git a/spec/unit/util/instrumentation/listeners/performance_spec.rb b/spec/unit/util/instrumentation/listeners/performance_spec.rb new file mode 100755 index 000000000..4cecbe3f6 --- /dev/null +++ b/spec/unit/util/instrumentation/listeners/performance_spec.rb @@ -0,0 +1,36 @@ +require 'spec_helper' +require 'puppet/util/instrumentation' + +Puppet::Util::Instrumentation.init +performance = Puppet::Util::Instrumentation.listener(:performance) + +describe performance do + before(:each) do + @performance = performance.new + end + + it "should have a notify method" do + @performance.should respond_to(:notify) + end + + it "should have a data method" do + @performance.should respond_to(:data) + end + + it "should keep data for stop event" do + @performance.notify(:test, :stop, { :started => Time.at(123456789), :finished => Time.at(123456790)}) + @performance.data.should == {:test=>{:average=>1.0, :count=>1, :min=>1.0, :max=>1.0, :sum=>1.0}} + end + + it "should accumulate performance statistics" do + @performance.notify(:test, :stop, { :started => Time.at(123456789), :finished => Time.at(123456790)}) + @performance.notify(:test, :stop, { :started => Time.at(123456789), :finished => Time.at(123456791)}) + + @performance.data.should == {:test=>{:average=>1.5, :count=>2, :min=>1.0, :max=>2.0, :sum=>3.0}} + end + + it "should not keep data for start event" do + @performance.notify(:test, :start, { :started => Time.at(123456789)}) + @performance.data.should be_empty + end +end
\ No newline at end of file diff --git a/spec/unit/util/instrumentation/listeners/process_name_spec.rb b/spec/unit/util/instrumentation/listeners/process_name_spec.rb new file mode 100755 index 000000000..521257214 --- /dev/null +++ b/spec/unit/util/instrumentation/listeners/process_name_spec.rb @@ -0,0 +1,201 @@ +#!/usr/bin/env rspec +require 'spec_helper' +require 'puppet/util/instrumentation' + +Puppet::Util::Instrumentation.init +process_name = Puppet::Util::Instrumentation.listener(:process_name) + +describe process_name do + before(:each) do + @process_name = process_name.new + end + + it "should have a notify method" do + @process_name.should respond_to(:notify) + end + + it "should not have a data method" do + @process_name.should_not respond_to(:data) + end + + describe "when managing thread activity" do + before(:each) do + @process_name.stubs(:setproctitle) + @process_name.stubs(:base).returns("base") + end + + it "should be able to append activity" do + thread1 = stub 'thread1' + @process_name.push_activity(:thread1,"activity1") + @process_name.push_activity(:thread1,"activity2") + + @process_name.reason[:thread1].should == ["activity1", "activity2"] + end + + it "should be able to remove activity" do + @process_name.push_activity(:thread1,"activity1") + @process_name.push_activity(:thread1,"activity1") + @process_name.pop_activity(:thread1) + + @process_name.reason[:thread1].should == ["activity1"] + end + + it "should maintain activity thread by thread" do + @process_name.push_activity(:thread1,"activity1") + @process_name.push_activity(:thread2,"activity2") + + @process_name.reason[:thread1].should == ["activity1"] + @process_name.reason[:thread2].should == ["activity2"] + end + + it "should set process title" do + @process_name.expects(:setproctitle) + + @process_name.push_activity("thread1","activity1") + end + end + + describe "when computing the current process name" do + before(:each) do + @process_name.stubs(:setproctitle) + @process_name.stubs(:base).returns("base") + end + + it "should include every running thread activity" do + thread1 = stub 'thread1', :inspect => "\#<Thread:0xdeadbeef run>", :hash => 1 + thread2 = stub 'thread2', :inspect => "\#<Thread:0x12344321 run>", :hash => 0 + + @process_name.push_activity(thread1,"Compiling node1.domain.com") + @process_name.push_activity(thread2,"Compiling node4.domain.com") + @process_name.push_activity(thread1,"Parsing file site.pp") + @process_name.push_activity(thread2,"Parsing file node.pp") + + @process_name.process_name.should =~ /12344321 Compiling node4.domain.com,Parsing file node.pp/ + @process_name.process_name.should =~ /deadbeef Compiling node1.domain.com,Parsing file site.pp/ + end + end + + describe "when finding base process name" do + {:master => "master", :agent => "agent", :user => "puppet"}.each do |program,base| + it "should return #{base} for #{program}" do + Puppet.run_mode.stubs(:name).returns(program) + @process_name.base.should == base + end + end + end + + describe "when finding a thread id" do + it "should return the id from the thread inspect string" do + thread = stub 'thread', :inspect => "\#<Thread:0x1234abdc run>" + @process_name.thread_id(thread).should == "1234abdc" + end + end + + describe "when scrolling the instrumentation string" do + it "should rotate the string of various step" do + @process_name.rotate("this is a rotation", 10).should == "rotation -- this is a " + end + + it "should not rotate the string for the 0 offset" do + @process_name.rotate("this is a rotation", 0).should == "this is a rotation" + end + end + + describe "when setting process name" do + before(:each) do + @process_name.stubs(:process_name).returns("12345 activity") + @process_name.stubs(:base).returns("base") + @oldname = $0 + end + + after(:each) do + $0 = @oldname + end + + it "should do it if the feature is enabled" do + @process_name.setproctitle + + $0.should == "base: 12345 activity" + end + end + + describe "when subscribed" do + before(:each) do + thread = stub 'thread', :inspect => "\#<Thread:0x1234abdc run>" + Thread.stubs(:current).returns(thread) + end + + it "should start the scroller" do + Thread.expects(:new) + @process_name.subscribed + end + end + + describe "when unsubscribed" do + before(:each) do + @thread = stub 'scroller', :inspect => "\#<Thread:0x1234abdc run>" + Thread.stubs(:new).returns(@thread) + Thread.stubs(:kill) + @oldname = $0 + @process_name.subscribed + end + + after(:each) do + $0 = @oldname + end + + it "should stop the scroller" do + Thread.expects(:kill).with(@thread) + @process_name.unsubscribed + end + + it "should reset the process name" do + $0 = "let's see what happens" + @process_name.unsubscribed + $0.should == @oldname + end + end + + describe "when setting a probe" do + before(:each) do + thread = stub 'thread', :inspect => "\#<Thread:0x1234abdc run>" + Thread.stubs(:current).returns(thread) + Thread.stubs(:new) + @process_name.active = true + end + + it "should push current thread activity and execute the block" do + @process_name.notify(:instrumentation, :start, {}) + $0.should == "puppet: 1234abdc instrumentation" + @process_name.notify(:instrumentation, :stop, {}) + end + + it "should finally pop the activity" do + @process_name.notify(:instrumentation, :start, {}) + @process_name.notify(:instrumentation, :stop, {}) + $0.should == "puppet: " + end + end + + describe "when scrolling" do + it "should do nothing for shorter process names" do + @process_name.expects(:setproctitle).never + @process_name.scroll + end + + it "should call setproctitle" do + @process_name.stubs(:process_name).returns("x" * 60) + @process_name.expects(:setproctitle) + @process_name.scroll + end + + it "should increment rotation offset" do + name = "x" * 60 + @process_name.stubs(:process_name).returns(name) + @process_name.expects(:rotate).once.with(name,1).returns("") + @process_name.expects(:rotate).once.with(name,2).returns("") + @process_name.scroll + @process_name.scroll + end + end +end
\ No newline at end of file diff --git a/spec/unit/util/instrumentation_spec.rb b/spec/unit/util/instrumentation_spec.rb new file mode 100755 index 000000000..0d19ee03c --- /dev/null +++ b/spec/unit/util/instrumentation_spec.rb @@ -0,0 +1,181 @@ +#!/usr/bin/env rspec + +require 'spec_helper' + +require 'puppet/util/instrumentation' + +describe Puppet::Util::Instrumentation do + + Instrumentation = Puppet::Util::Instrumentation + + after(:each) do + Instrumentation.clear + end + + it "should instance-load instrumentation listeners" do + Instrumentation.instance_loader(:listener).should be_instance_of(Puppet::Util::Autoload) + end + + it "should have a method for registering instrumentation listeners" do + Instrumentation.should respond_to(:new_listener) + end + + it "should have a method for retrieving instrumentation listener by name" do + Instrumentation.should respond_to(:listener) + end + + describe "when registering listeners" do + it "should evaluate the supplied block as code for a class" do + Instrumentation.expects(:genclass).returns(Class.new { def notify(label, event, data) ; end }) + Instrumentation.new_listener(:testing, :label_pattern => :for_this_label, :event => :all) { } + end + + it "should subscribe a new listener instance" do + Instrumentation.expects(:genclass).returns(Class.new { def notify(label, event, data) ; end }) + Instrumentation.new_listener(:testing, :label_pattern => :for_this_label, :event => :all) { } + Instrumentation.listeners.size.should == 1 + Instrumentation.listeners[0].pattern.should == "for_this_label" + end + + it "should be possible to access listeners by name" do + Instrumentation.expects(:genclass).returns(Class.new { def notify(label, event, data) ; end }) + Instrumentation.new_listener(:testing, :label_pattern => :for_this_label, :event => :all) { } + Instrumentation["testing"].should_not be_nil + end + + it "should be possible to store a new listener by name" do + listener = stub 'listener' + Instrumentation["testing"] = listener + Instrumentation["testing"].should == listener + end + + it "should fail if listener is already subscribed" do + listener = stub 'listener', :notify => nil, :name => "mylistener" + Instrumentation.subscribe(listener, :for_this_label, :all) + expect { Instrumentation.subscribe(listener, :for_this_label, :all) }.to raise_error + end + + it 'should call #unsubscribed' do + listener = stub 'listener', :notify => nil, :name => "mylistener" + + listener.expects(:subscribed) + + Instrumentation.subscribe(listener, :for_this_label, :all) + end + end + + describe "when unsubscribing listener" do + it "should remove it from the listeners" do + listener = stub 'listener', :notify => nil, :name => "mylistener" + Instrumentation.subscribe(listener, :for_this_label, :all) + Instrumentation.unsubscribe(listener) + Instrumentation.listeners.size.should == 0 + end + + it "should warn if the listener wasn't subscribed" do + listener = stub 'listener', :notify => nil, :name => "mylistener" + Puppet.expects(:warning) + Instrumentation.unsubscribe(listener) + end + + it 'should call #unsubscribed' do + listener = stub 'listener', :notify => nil, :name => "mylistener" + Instrumentation.subscribe(listener, :for_this_label, :all) + + listener.expects(:unsubscribed) + + Instrumentation.unsubscribe(listener) + end + end + + describe "when firing events" do + it "should be able to find all listeners matching a label" do + listener = stub 'listener', :notify => nil, :name => "mylistener" + Instrumentation.subscribe(listener, :for_this_label, :all) + Instrumentation.listeners[0].enabled = true + + count = 0 + Instrumentation.each_listener(:for_this_label) { |l| count += 1 } + count.should == 1 + end + + it "should fire events to matching listeners" do + listener = stub 'listener', :notify => nil, :name => "mylistener" + Instrumentation.subscribe(listener, :for_this_label, :all) + Instrumentation.listeners[0].enabled = true + + listener.expects(:notify).with(:for_this_label, :start, {}) + + Instrumentation.publish(:for_this_label, :start, {}) + end + + it "should not fire events to non-matching listeners" do + listener1 = stub 'listener1', :notify => nil, :name => "mylistener1" + listener2 = stub 'listener2', :notify => nil, :name => "mylistener2" + Instrumentation.subscribe(listener1, :for_this_label, :all) + Instrumentation.listeners[0].enabled = true + Instrumentation.subscribe(listener2, :for_this_other_label, :all) + Instrumentation.listeners[1].enabled = true + + listener1.expects(:notify).never + listener2.expects(:notify).with(:for_this_other_label, :start, {}) + + Instrumentation.publish(:for_this_other_label, :start, {}) + end + end + + describe "when instrumenting code" do + before(:each) do + Instrumentation.stubs(:publish) + end + describe "with a block" do + it "should execute it" do + executed = false + Instrumentation.instrument(:event) do + executed = true + end + executed.should be_true + end + + it "should publish an event before execution" do + Instrumentation.expects(:publish).with { |label,event,data| label == :event && event == :start } + Instrumentation.instrument(:event) {} + end + + it "should publish an event after execution" do + Instrumentation.expects(:publish).with { |label,event,data| label == :event && event == :stop } + Instrumentation.instrument(:event) {} + end + + it "should publish the event even when block raised an exception" do + Instrumentation.expects(:publish).with { |label,event,data| label == :event } + lambda { Instrumentation.instrument(:event) { raise "not working" } }.should raise_error + end + + it "should retain start end finish time of the event" do + Instrumentation.expects(:publish).with { |label,event,data| data.include?(:started) and data.include?(:finished) } + Instrumentation.instrument(:event) {} + end + end + + describe "without a block" do + it "should raise an error if stop is called with no matching start" do + lambda{ Instrumentation.stop(:event) }.should raise_error + end + + it "should publish an event on stop" do + Instrumentation.expects(:publish).with { |label,event,data| event == :start } + Instrumentation.expects(:publish).with { |label,event,data| event == :stop and data.include?(:started) and data.include?(:finished) } + data = {} + Instrumentation.start(:event, data) + Instrumentation.stop(:event, 1, data) + end + + it "should return a different id per event" do + data = {} + Instrumentation.start(:event, data).should == 1 + Instrumentation.start(:event, data).should == 2 + end + end + end +end
\ No newline at end of file diff --git a/spec/unit/util/pidlock_spec.rb b/spec/unit/util/pidlock_spec.rb new file mode 100644 index 000000000..37f3ca4f4 --- /dev/null +++ b/spec/unit/util/pidlock_spec.rb @@ -0,0 +1,182 @@ +#!/usr/bin/env rspec +require 'spec_helper' + +require 'puppet/util/pidlock' + +describe Puppet::Util::Pidlock do + require 'puppet_spec/files' + include PuppetSpec::Files + + before(:each) do + @lockfile = tmpfile("lock") + @lock = Puppet::Util::Pidlock.new(@lockfile) + end + + it "should not be anonymous" do + @lock.should_not be_anonymous + end + + describe "#lock" do + it "should not be locked at start" do + @lock.should_not be_locked + end + + it "should not be mine at start" do + @lock.should_not be_mine + end + + it "should become locked" do + @lock.lock + @lock.should be_locked + end + + it "should become mine" do + @lock.lock + @lock.should be_mine + end + + it "should be possible to lock multiple times" do + @lock.lock + lambda { @lock.lock }.should_not raise_error + end + + it "should return true when locking" do + @lock.lock.should be_true + end + + it "should return true if locked by me" do + @lock.lock + @lock.lock.should be_true + end + + it "should return false if locked by someone else" do + Process.stubs(:kill) + File.open(@lockfile, "w") { |fd| fd.print('0') } + + @lock.lock.should be_false + end + + it "should create a lock file" do + @lock.lock + File.should be_exists(@lockfile) + end + + it "should create a lock file containing our pid" do + @lock.lock + File.read(@lockfile).to_i.should == Process.pid.to_i + end + end + + describe "#unlock" do + it "should not be locked anymore" do + @lock.lock + @lock.unlock + @lock.should_not be_locked + end + + it "should return false if not locked" do + @lock.unlock.should be_false + end + + it "should return true if properly unlocked" do + @lock.lock + @lock.unlock.should be_true + end + + it "should get rid of the lock file" do + @lock.lock + @lock.unlock + File.should_not be_exists(@lockfile) + end + end + + describe "#locked?" do + it "should return true if locked" do + @lock.lock + @lock.should be_locked + end + end + + describe "with a stale lock" do + before(:each) do + Process.stubs(:kill).with(0, 6789) + Process.stubs(:kill).with(0, 1234).raises(Errno::ESRCH) + Process.stubs(:pid).returns(6789) + File.open(@lockfile, 'w') { |fd| fd.write("1234") } + end + + it "should not be locked" do + @lock.should_not be_locked + end + + describe "#lock" do + it "should clear stale locks" do + @lock.locked? + File.should_not be_exists(@lockfile) + end + + it "should replace with new locks" do + @lock.lock + File.should be_exists(@lockfile) + @lock.lock_pid.should == 6789 + @lock.should be_mine + @lock.should be_locked + end + end + + describe "#unlock" do + it "should not be allowed" do + @lock.unlock.should be_false + end + + it "should not remove the lock file" do + @lock.unlock + File.should be_exists(@lockfile) + end + end + end + + describe "with another process lock" do + before(:each) do + Process.stubs(:kill).with(0, 6789) + Process.stubs(:kill).with(0, 1234) + Process.stubs(:pid).returns(6789) + File.open(@lockfile, 'w') { |fd| fd.write("1234") } + end + + it "should be locked" do + @lock.should be_locked + end + + it "should not be mine" do + @lock.should_not be_mine + end + + describe "#lock" do + it "should not be possible" do + @lock.lock.should be_false + end + + it "should not overwrite the lock" do + @lock.lock + @lock.should_not be_mine + end + end + + describe "#unlock" do + it "should not be possible" do + @lock.unlock.should be_false + end + + it "should not remove the lock file" do + @lock.unlock + File.should be_exists(@lockfile) + end + + it "should still not be our lock" do + @lock.unlock + @lock.should_not be_mine + end + end + end +end
\ No newline at end of file diff --git a/spec/unit/util/queue/stomp_spec.rb b/spec/unit/util/queue/stomp_spec.rb index 6799becea..7730ab7cb 100755 --- a/spec/unit/util/queue/stomp_spec.rb +++ b/spec/unit/util/queue/stomp_spec.rb @@ -13,7 +13,7 @@ describe 'Puppet::Util::Queue::Stomp', :if => Puppet.features.stomp?, :'fails_on before do # So we make sure we never create a real client instance. # Otherwise we'll try to connect, and that's bad. - Stomp::Client.stubs(:new).returns stub("client") + Stomp::Client.stubs(:new).returns stub("client", :publish => true) end it 'should be registered with Puppet::Util::Queue as :stomp type' do @@ -22,7 +22,7 @@ describe 'Puppet::Util::Queue::Stomp', :if => Puppet.features.stomp?, :'fails_on describe "when initializing" do it "should create a Stomp client instance" do - Stomp::Client.expects(:new).returns stub("stomp_client") + Stomp::Client.expects(:new).returns stub("stomp_client", :publish => true) Puppet::Util::Queue::Stomp.new end @@ -65,7 +65,7 @@ describe 'Puppet::Util::Queue::Stomp', :if => Puppet.features.stomp?, :'fails_on describe "when publishing a message" do before do - @client = stub 'client' + @client = stub 'client', :publish => true Stomp::Client.stubs(:new).returns @client @queue = Puppet::Util::Queue::Stomp.new end @@ -84,11 +84,16 @@ describe 'Puppet::Util::Queue::Stomp', :if => Puppet.features.stomp?, :'fails_on @client.expects(:publish).with { |queue, msg, options| options[:persistent] == true } @queue.publish_message('fooqueue', 'Smite!') end + + it "should use send when the gem does not support publish" do + Stomp::Client.stubs(:new).returns(stub('client', :send => true)) + Puppet::Util::Queue::Stomp.new.publish_message('fooqueue', 'Smite!') + end end describe "when subscribing to a queue" do before do - @client = stub 'client', :acknowledge => true + @client = stub 'client', :acknowledge => true, :publish => true Stomp::Client.stubs(:new).returns @client @queue = Puppet::Util::Queue::Stomp.new end diff --git a/spec/unit/util/rdoc/parser_spec.rb b/spec/unit/util/rdoc/parser_spec.rb index 9c8cc7588..892b932b2 100755 --- a/spec/unit/util/rdoc/parser_spec.rb +++ b/spec/unit/util/rdoc/parser_spec.rb @@ -135,7 +135,7 @@ describe RDoc::Parser, :'fails_on_ruby_1.9.2' => true do describe "when finding modules from filepath" do before :each do - Puppet::Module.stubs(:modulepath).returns("/path/to/modules") + Puppet::Node::Environment.any_instance.stubs(:modulepath).returns("/path/to/modules") end it "should return the module name for modulized puppet manifests" do diff --git a/spec/unit/util/reference_spec.rb b/spec/unit/util/reference_spec.rb index 219a673ef..aa16299c7 100644 --- a/spec/unit/util/reference_spec.rb +++ b/spec/unit/util/reference_spec.rb @@ -8,20 +8,30 @@ describe Puppet::Util::Reference do Puppet::Util::Reference.newreference :testreference, :doc => "A peer of the type and configuration references, but with no useful information" do my_term = "A term" my_definition = <<-EOT -The definition of this term. -We should be able to handle multi-line definitions. + The definition of this term, marked by a colon and a space. + We should be able to handle multi-line definitions. Each subsequent + line should left-align with the first word character after the colon + used as the definition marker. -We should be able to handle multi-paragraph definitions. + We should be able to handle multi-paragraph definitions. + + Leading indentation should be stripped from the definition, which allows + us to indent the source string for cosmetic purposes. EOT my_fragment = markdown_definitionlist(my_term, my_definition) end Puppet::Util::Reference.reference(:testreference).send(:to_markdown, true) my_fragment.should == <<-EOT A term -: The definition of this term. - We should be able to handle multi-line definitions. +: The definition of this term, marked by a colon and a space. + We should be able to handle multi-line definitions. Each subsequent + line should left-align with the first word character after the colon + used as the definition marker. + + We should be able to handle multi-paragraph definitions. - We should be able to handle multi-paragraph definitions. + Leading indentation should be stripped from the definition, which allows + us to indent the source string for cosmetic purposes. EOT end diff --git a/spec/unit/util/retryaction_spec.rb b/spec/unit/util/retryaction_spec.rb new file mode 100644 index 000000000..90f8e8eb2 --- /dev/null +++ b/spec/unit/util/retryaction_spec.rb @@ -0,0 +1,62 @@ +#!/usr/bin/env rspec +require 'spec_helper' + +require 'puppet/util/retryaction' + +describe Puppet::Util::RetryAction do + let (:exceptions) {{ Puppet::Error => 'Puppet Error Exception' }} + + it 'should retry on any exception if no acceptable exceptions given' do + Puppet::Util::RetryAction.expects(:sleep).with( (((2 ** 1) -1) * 0.1) ) + Puppet::Util::RetryAction.expects(:sleep).with( (((2 ** 2) -1) * 0.1) ) + + expect do + Puppet::Util::RetryAction.retry_action( :retries => 2 ) do + raise ArgumentError, 'Fake Failure' + end + end.to raise_exception(Puppet::Util::RetryAction::RetryException::RetriesExceeded) + end + + it 'should retry on acceptable exceptions' do + Puppet::Util::RetryAction.expects(:sleep).with( (((2 ** 1) -1) * 0.1) ) + Puppet::Util::RetryAction.expects(:sleep).with( (((2 ** 2) -1) * 0.1) ) + + expect do + Puppet::Util::RetryAction.retry_action( :retries => 2, :retry_exceptions => exceptions) do + raise Puppet::Error, 'Fake Failure' + end + end.to raise_exception(Puppet::Util::RetryAction::RetryException::RetriesExceeded) + end + + it 'should not retry on unacceptable exceptions' do + Puppet::Util::RetryAction.expects(:sleep).never + + expect do + Puppet::Util::RetryAction.retry_action( :retries => 2, :retry_exceptions => exceptions) do + raise ArgumentError + end + end.to raise_exception(ArgumentError) + end + + it 'should succeed if nothing is raised' do + Puppet::Util::RetryAction.expects(:sleep).never + + Puppet::Util::RetryAction.retry_action( :retries => 2) do + true + end + end + + it 'should succeed if an expected exception is raised retried and succeeds' do + should_retry = nil + Puppet::Util::RetryAction.expects(:sleep).once + + Puppet::Util::RetryAction.retry_action( :retries => 2, :retry_exceptions => exceptions) do + if should_retry + true + else + should_retry = true + raise Puppet::Error, 'Fake error' + end + end + end +end diff --git a/spec/unit/util/suidmanager_spec.rb b/spec/unit/util/suidmanager_spec.rb index 45a351f1b..575762f3c 100755 --- a/spec/unit/util/suidmanager_spec.rb +++ b/spec/unit/util/suidmanager_spec.rb @@ -308,3 +308,22 @@ describe Puppet::Util::SUIDManager do end end end + +describe 'Puppet::Util::SUIDManager#groups=' do + subject do + Puppet::Util::SUIDManager + end + + + it "(#3419) should rescue Errno::EINVAL on OS X" do + Process.expects(:groups=).raises(Errno::EINVAL, 'blew up') + subject.expects(:osx_maj_ver).returns('10.7').twice + subject.groups = ['list', 'of', 'groups'] + end + + it "(#3419) should fail if an Errno::EINVAL is raised NOT on OS X" do + Process.expects(:groups=).raises(Errno::EINVAL, 'blew up') + subject.expects(:osx_maj_ver).returns(false) + expect { subject.groups = ['list', 'of', 'groups'] }.should raise_error(Errno::EINVAL) + end +end diff --git a/spec/unit/util/symbolic_file_mode_spec.rb b/spec/unit/util/symbolic_file_mode_spec.rb new file mode 100755 index 000000000..a6e9509f7 --- /dev/null +++ b/spec/unit/util/symbolic_file_mode_spec.rb @@ -0,0 +1,182 @@ +#!/usr/bin/env rspec +require 'spec_helper' + +require 'puppet/util/symbolic_file_mode' + +describe Puppet::Util::SymbolicFileMode do + include Puppet::Util::SymbolicFileMode + + describe "#valid_symbolic_mode?" do + %w{ + 0 0000 1 1 7 11 77 111 777 11 + 0 00000 01 01 07 011 077 0111 0777 011 + = - + u= g= o= a= u+ g+ o+ a+ u- g- o- a- ugo= ugoa= ugugug= + a=,u=,g= a=,g+ + =rwx +rwx -rwx + 644 go-w =rw,+X +X 755 u=rwx,go=rx u=rwx,go=u-w go= g=u-w + 755 0755 + }.each do |input| + it "should treat #{input.inspect} as valid" do + valid_symbolic_mode?(input).should be_true + end + end + + [0000, 0111, 0640, 0755, 0777].each do |input| + it "should treat the int #{input.to_s(8)} as value" do + valid_symbolic_mode?(input).should be_true + end + end + + %w{ + -1 -8 8 9 18 19 91 81 000000 11111 77777 + 0-1 0-8 08 09 018 019 091 081 0000000 011111 077777 + u g o a ug uo ua ag + }.each do |input| + it "should treat #{input.inspect} as invalid" do + valid_symbolic_mode?(input).should be_false + end + end + end + + describe "#normalize_symbolic_mode" do + it "should turn an int into a string" do + normalize_symbolic_mode(12).should be_an_instance_of String + end + + it "should not add a leading zero to an int" do + normalize_symbolic_mode(12).should_not =~ /^0/ + end + + it "should not add a leading zero to a string with a number" do + normalize_symbolic_mode("12").should_not =~ /^0/ + end + + it "should string a leading zero from a number" do + normalize_symbolic_mode("012").should == '12' + end + + it "should pass through any other string" do + normalize_symbolic_mode("u=rwx").should == 'u=rwx' + end + end + + describe "#symbolic_mode_to_int" do + { + "0654" => 00654, + "u+r" => 00400, + "g+r" => 00040, + "a+r" => 00444, + "a+x" => 00111, + "o+t" => 01000, + "o+t" => 01000, + ["o-t", 07777] => 06777, + ["a-x", 07777] => 07666, + ["a-rwx", 07777] => 07000, + ["ug-rwx", 07777] => 07007, + "a+x,ug-rwx" => 00001, + # My experimentation on debian suggests that +g ignores the sgid flag + ["a+g", 02060] => 02666, + # My experimentation on debian suggests that -g ignores the sgid flag + ["a-g", 02666] => 02000, + "g+x,a+g" => 00111, + # +X without exec set in the original should not set anything + "u+x,g+X" => 00100, + "g+X" => 00000, + # +X only refers to the original, *unmodified* file mode! + ["u+x,a+X", 0600] => 00700, + # Examples from the MacOS chmod(1) manpage + "0644" => 00644, + ["go-w", 07777] => 07755, + ["=rw,+X", 07777] => 07777, + ["=rw,+X", 07766] => 07777, + ["=rw,+X", 07676] => 07777, + ["=rw,+X", 07667] => 07777, + ["=rw,+X", 07666] => 07666, + "0755" => 00755, + "u=rwx,go=rx" => 00755, + "u=rwx,go=u-w" => 00755, + ["go=", 07777] => 07700, + ["g=u-w", 07777] => 07757, + ["g=u-w", 00700] => 00750, + ["g=u-w", 00600] => 00640, + ["g=u-w", 00500] => 00550, + ["g=u-w", 00400] => 00440, + ["g=u-w", 00300] => 00310, + ["g=u-w", 00200] => 00200, + ["g=u-w", 00100] => 00110, + ["g=u-w", 00000] => 00000, + # Cruel, but legal, use of the action set. + ["g=u+r-w", 0300] => 00350, + # Empty assignments. + ["u=", 00000] => 00000, + ["u=", 00600] => 00000, + ["ug=", 00000] => 00000, + ["ug=", 00600] => 00000, + ["ug=", 00660] => 00000, + ["ug=", 00666] => 00006, + ["=", 00000] => 00000, + ["=", 00666] => 00000, + ["+", 00000] => 00000, + ["+", 00124] => 00124, + ["-", 00000] => 00000, + ["-", 00124] => 00124, + }.each do |input, result| + from = input.is_a?(Array) ? "#{input[0]}, 0#{input[1].to_s(8)}" : input + it "should map #{from.inspect} to #{result.inspect}" do + symbolic_mode_to_int(*input).should == result + end + end + + # Now, test some failure modes. + it "should fail if no mode is given" do + expect { symbolic_mode_to_int('') }. + to raise_error Puppet::Error, /empty mode string/ + end + + %w{u g o ug uo go ugo a uu u/x u!x u=r,,g=r}.each do |input| + it "should fail if no (valid) action is given: #{input.inspect}" do + expect { symbolic_mode_to_int(input) }. + to raise_error Puppet::Error, /Missing action/ + end + end + + %w{u+q u-rwF u+rw,g+rw,o+RW}.each do |input| + it "should fail with unknown op #{input.inspect}" do + expect { symbolic_mode_to_int(input) }. + to raise_error Puppet::Error, /Unknown operation/ + end + end + + it "should refuse to subtract the conditional execute op" do + expect { symbolic_mode_to_int("o-rwX") }. + to raise_error Puppet::Error, /only works with/ + end + + it "should refuse to set to the conditional execute op" do + expect { symbolic_mode_to_int("o=rwX") }. + to raise_error Puppet::Error, /only works with/ + end + + %w{8 08 9 09 118 119}.each do |input| + it "should fail for decimal modes: #{input.inspect}" do + expect { symbolic_mode_to_int(input) }. + to raise_error Puppet::Error, /octal/ + end + end + + it "should set the execute bit on a directory, without exec in original" do + symbolic_mode_to_int("u+X", 0444, true).to_s(8).should == "544" + symbolic_mode_to_int("g+X", 0444, true).to_s(8).should == "454" + symbolic_mode_to_int("o+X", 0444, true).to_s(8).should == "445" + symbolic_mode_to_int("+X", 0444, true).to_s(8).should == "555" + end + + it "should set the execute bit on a file with exec in the original" do + symbolic_mode_to_int("+X", 0544).to_s(8).should == "555" + end + + it "should not set the execute bit on a file without exec on the original even if set by earlier DSL" do + symbolic_mode_to_int("u+x,go+X", 0444).to_s(8).should == "544" + end + end +end diff --git a/spec/unit/util/zaml_spec.rb b/spec/unit/util/zaml_spec.rb index d77cf99d2..858ae6044 100755 --- a/spec/unit/util/zaml_spec.rb +++ b/spec/unit/util/zaml_spec.rb @@ -1,4 +1,16 @@ #!/usr/bin/env rspec +# encoding: UTF-8 +# +# The above encoding line is a magic comment to set the default source encoding +# of this file for the Ruby interpreter. It must be on the first or second +# line of the file if an interpreter is in use. In Ruby 1.9 and later, the +# source encoding determines the encoding of String and Regexp objects created +# from this source file. This explicit encoding is important becuase otherwise +# Ruby will pick an encoding based on LANG or LC_CTYPE environment variables. +# These may be different from site to site so it's important for us to +# establish a consistent behavior. For more information on M17n please see: +# http://links.puppetlabs.com/understanding_m17n + require 'spec_helper' require 'puppet/util/monkey_patches' @@ -60,3 +72,28 @@ describe "Pure ruby yaml implementation" do x2[2].should equal(x2) end end + +# Note, many of these tests will pass on Ruby 1.8 but fail on 1.9 if the patch +# fix is not applied to Puppet or there's a regression. These version +# dependant failures are intentional since the string encoding behavior changed +# significantly in 1.9. +describe "UTF-8 encoded String#to_yaml (Bug #11246)" do + # JJM All of these snowmen are different representations of the same + # UTF-8 encoded string. + let(:snowman) { 'Snowman: [☃]' } + let(:snowman_escaped) { "Snowman: [\xE2\x98\x83]" } + + describe "UTF-8 String Literal" do + subject { snowman } + + it "should serialize to YAML" do + subject.to_yaml + end + it "should serialize and deserialize to the same thing" do + YAML.load(subject.to_yaml).should == subject + end + it "should serialize and deserialize to a String compatible with a UTF-8 encoded Regexp" do + YAML.load(subject.to_yaml).should =~ /☃/u + end + end +end diff --git a/tasks/rake/apple.rake b/tasks/rake/apple.rake new file mode 100644 index 000000000..2d30c0622 --- /dev/null +++ b/tasks/rake/apple.rake @@ -0,0 +1,176 @@ +# Title: Rake task to build Apple packages for Puppet. +# Author: Gary Larizza +# Date: 12/5/2011 +# Description: This task will create a DMG-encapsulated package that will +# install Puppet on OS X systems. This happens by building +# a directory tree of files that will then be fed to the +# packagemaker binary (can be installed by installing the +# XCode Tools) which will create the .pkg file. +# +require 'fileutils' +require 'erb' +require 'find' +require 'pathname' + +# Path to Binaries (Constants) +TAR = '/usr/bin/tar' +CP = '/bin/cp' +INSTALL = '/usr/bin/install' +DITTO = '/usr/bin/ditto' +PACKAGEMAKER = '/Developer/usr/bin/packagemaker' +SED = '/usr/bin/sed' + +# Setup task to populate all the variables +task :setup do + @version = `git describe`.chomp + @title = "puppet-#{@version}" + @reverse_domain = 'com.puppetlabs.puppet' + @package_major_version = @version.split('.')[0] + @package_minor_version = @version.split('.')[1] + + @version.split('.')[2].split('-')[0].split('rc')[0] + @pm_restart = 'None' + @build_date = Time.new.strftime("%Y-%m-%dT%H:%M:%SZ") +end + +# method: make_directory_tree +# description: This method sets up the directory structure that packagemaker +# needs to build a package. A prototype.plist file (holding +# package-specific options) is built from an ERB template located +# in the tasks/rake/templates directory. +def make_directory_tree + puppet_tmp = '/tmp/puppet' + @scratch = "#{puppet_tmp}/#{@title}" + @working_tree = { + 'scripts' => "#{@scratch}/scripts", + 'resources' => "#{@scratch}/resources", + 'working' => "#{@scratch}/root", + 'payload' => "#{@scratch}/payload", + } + puts "Cleaning Tree: #{puppet_tmp}" + FileUtils.rm_rf(puppet_tmp) + @working_tree.each do |key,val| + puts "Creating: #{val}" + FileUtils.mkdir_p(val) + end + File.open("#{@scratch}/#{'prototype.plist'}", "w+") do |f| + f.write(ERB.new(File.read('tasks/rake/templates/prototype.plist.erb')).result()) + end +end + +# method: build_dmg +# description: This method builds a package from the directory structure in +# /tmp/puppet and puts it in the +# /tmp/puppet/puppet-#{version}/payload directory. A DMG is +# created, using hdiutil, based on the contents of the +# /tmp/puppet/puppet-#{version}/payload directory. The resultant +# DMG is placed in the pkg/apple directory. +# +def build_dmg + # Local Variables + dmg_format_code = 'UDZO' + zlib_level = '9' + dmg_format_option = "-imagekey zlib-level=#{zlib_level}" + dmg_format = "#{dmg_format_code} #{dmg_format_option}" + dmg_file = "#{@title}.dmg" + package_file = "#{@title}.pkg" + pm_extra_args = '--verbose --no-recommend --no-relocate' + package_target_os = '10.4' + + # Build .pkg file + system("sudo #{PACKAGEMAKER} --root #{@working_tree['working']} \ + --id #{@reverse_domain} \ + --filter DS_Store \ + --target #{package_target_os} \ + --title #{@title} \ + --info #{@scratch}/prototype.plist \ + --scripts #{@working_tree['scripts']} \ + --resources #{@working_tree['resources']} \ + --version #{@version} \ + #{pm_extra_args} --out #{@working_tree['payload']}/#{package_file}") + + # Build .dmg file + system("sudo hdiutil create -volname #{@title} \ + -srcfolder #{@working_tree['payload']} \ + -uid 99 \ + -gid 99 \ + -ov \ + -format #{dmg_format} \ + #{dmg_file}") + + if File.directory?("#{Pathname.pwd}/pkg/apple") + FileUtils.mv("#{Pathname.pwd}/#{dmg_file}", "#{Pathname.pwd}/pkg/apple/#{dmg_file}") + puts "moved: #{dmg_file} has been moved to #{Pathname.pwd}/pkg/apple/#{dmg_file}" + else + FileUtils.mkdir_p("#{Pathname.pwd}/pkg/apple") + FileUtils.mv(dmg_file, "#{Pathname.pwd}/pkg/apple/#{dmg_file}") + puts "moved: #{dmg_file} has been moved to #{Pathname.pwd}/pkg/apple/#{dmg_file}" + end +end + +# method: pack_puppet_source +# description: This method copies the puppet source into a directory +# structure in /tmp/puppet/puppet-#{version}/root mirroring the +# structure on the target system for which the package will be +# installed. Anything installed into /tmp/puppet/root will be +# installed as the package's payload. +# +def pack_puppet_source + work = "#{@working_tree['working']}" + puppet_source = Pathname.pwd + + # Make all necessary directories + directories = ["#{work}/private/etc/puppet/", + "#{work}/usr/bin", + "#{work}/usr/sbin", + "#{work}/usr/share/doc/puppet", + "#{work}/usr/share/man/man5", + "#{work}/usr/share/man/man8", + "#{work}/usr/lib/ruby/site_ruby/1.8/puppet"] + FileUtils.mkdir_p(directories) + + # Install necessary files + system("#{INSTALL} -o root -g wheel -m 644 #{puppet_source}/conf/auth.conf #{work}/private/etc/puppet/auth.conf") + system("#{DITTO} #{puppet_source}/bin/ #{work}/usr/bin") + system("#{DITTO} #{puppet_source}/sbin/ #{work}/usr/sbin") + system("#{INSTALL} -o root -g wheel -m 644 #{puppet_source}/man/man5/puppet.conf.5 #{work}/usr/share/man/man5/") + system("#{DITTO} #{puppet_source}/man/man8/ #{work}/usr/share/man/man8") + system("#{DITTO} #{puppet_source}/lib/ #{work}/usr/lib/ruby/site_ruby/1.8/") + + # Setup a preflight script and replace variables in the files with + # the correct paths. + system("#{INSTALL} -o root -g wheel -m 644 #{puppet_source}/conf/osx/preflight #{@working_tree['scripts']}") + system("#{SED} -i '' \"s\#{SITELIBDIR}\#/usr/lib/ruby/site_ruby/1.8\#g\" #{@working_tree['scripts']}/preflight") + system("#{SED} -i '' \"s\#{BINDIR}\#/usr/bin\#g\" #{@working_tree['scripts']}/preflight") + + # Install documentation (matching for files with capital letters) + Dir.foreach("#{puppet_source}") do |file| + system("#{INSTALL} -o root -g wheel -m 644 #{puppet_source}/#{file} #{work}/usr/share/doc/puppet") if file =~ /^[A-Z][A-Z]/ + end + + # Set Permissions + executable_directories = [ "#{work}/usr/bin", + "#{work}/usr/sbin", + "#{work}/usr/share/man/man8"] + FileUtils.chmod_R(0755, executable_directories) + FileUtils.chown_R('root', 'wheel', directories) + FileUtils.chmod_R(0644, "#{work}/usr/lib/ruby/site_ruby/1.8/") + FileUtils.chown_R('root', 'wheel', "#{work}/usr/lib/ruby/site_ruby/1.8/") + Find.find("#{work}/usr/lib/ruby/site_ruby/1.8/") do |dir| + FileUtils.chmod(0755, dir) if File.directory?(dir) + end +end + +namespace :package do + desc "Task for building an Apple Package" + task :apple => [:setup] do + # Test for Root and Packagemaker binary + raise "Please run rake as root to build Apple Packages" unless Process.uid == 0 + raise "Packagemaker must be installed. Please install XCode Tools" unless \ + File.exists?('/Developer/usr/bin/packagemaker') + + make_directory_tree + pack_puppet_source + build_dmg + FileUtils.chmod_R(0775, "#{Pathname.pwd}/pkg") + end +end diff --git a/tasks/rake/templates/prototype.plist.erb b/tasks/rake/templates/prototype.plist.erb new file mode 100644 index 000000000..e56659a6b --- /dev/null +++ b/tasks/rake/templates/prototype.plist.erb @@ -0,0 +1,38 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> +<plist version="1.0"> +<dict> + <key>CFBundleIdentifier</key> + <string><%= @title %></string> + <key>CFBundleShortVersionString</key> + <string><%= @version %></string> + <key>IFMajorVersion</key> + <integer><%= @package_major_version %></integer> + <key>IFMinorVersion</key> + <integer><%= @package_minor_version %></integer> + <key>IFPkgBuildDate</key> + <date><%= @build_date %></date> + <key>IFPkgFlagAllowBackRev</key> + <false/> + <key>IFPkgFlagAuthorizationAction</key> + <string>RootAuthorization</string> + <key>IFPkgFlagDefaultLocation</key> + <string>/</string> + <key>IFPkgFlagFollowLinks</key> + <true/> + <key>IFPkgFlagInstallFat</key> + <false/> + <key>IFPkgFlagIsRequired</key> + <false/> + <key>IFPkgFlagOverwritePermissions</key> + <false/> + <key>IFPkgFlagRelocatable</key> + <false/> + <key>IFPkgFlagRestartAction</key> + <string><%= @pm_restart %></string> + <key>IFPkgFlagRootVolumeOnly</key> + <true/> + <key>IFPkgFlagUpdateInstalledLanguages</key> + <false/> +</dict> +</plist> diff --git a/test/util/pidlock.rb b/test/util/pidlock.rb deleted file mode 100755 index beaff1089..000000000 --- a/test/util/pidlock.rb +++ /dev/null @@ -1,126 +0,0 @@ -require File.expand_path(File.dirname(__FILE__) + '/../lib/puppettest') - -require 'puppet/util/pidlock' -require 'fileutils' - -# This is *fucked* *up* -Puppet.debug = false - -class TestPuppetUtilPidlock < Test::Unit::TestCase - include PuppetTest - - def setup - super - @workdir = tstdir - end - - def teardown - super - FileUtils.rm_rf(@workdir) - end - - def test_00_basic_create - l = nil - assert_nothing_raised { l = Puppet::Util::Pidlock.new(@workdir + '/nothingmuch') } - - assert_equal Puppet::Util::Pidlock, l.class - - assert_equal @workdir + '/nothingmuch', l.lockfile - end - - def test_10_uncontended_lock - l = Puppet::Util::Pidlock.new(@workdir + '/test_lock') - - assert !l.locked? - assert !l.mine? - assert l.lock - assert l.locked? - assert l.mine? - assert !l.anonymous? - # It's OK to call lock multiple times - assert l.lock - assert l.unlock - assert !l.locked? - assert !l.mine? - end - - def test_20_someone_elses_lock - childpid = nil - l = Puppet::Util::Pidlock.new(@workdir + '/someone_elses_lock') - - # First, we need a PID that's guaranteed to be (a) used, (b) someone - # else's, and (c) around for the life of this test. - childpid = fork { loop do; sleep 10; end } - - File.open(l.lockfile, 'w') { |fd| fd.write(childpid) } - - assert l.locked? - assert !l.mine? - assert !l.lock - assert l.locked? - assert !l.mine? - assert !l.unlock - assert l.locked? - assert !l.mine? - ensure - Process.kill("KILL", childpid) unless childpid.nil? - end - - def test_30_stale_lock - # This is a bit hard to guarantee, but we need a PID that is definitely - # unused, and will stay so for the the life of this test. Our best - # bet is to create a process, get it's PID, let it die, and *then* - # lock on it. - childpid = fork { exit } - - # Now we can't continue until we're sure that the PID is dead - Process.wait(childpid) - - l = Puppet::Util::Pidlock.new(@workdir + '/stale_lock') - - # locked? should clear the lockfile - File.open(l.lockfile, 'w') { |fd| fd.write(childpid) } - assert File.exists?(l.lockfile) - assert !l.locked? - assert !File.exists?(l.lockfile) - - # lock should replace the lockfile with our own - File.open(l.lockfile, 'w') { |fd| fd.write(childpid) } - assert File.exists?(l.lockfile) - assert l.lock - assert l.locked? - assert l.mine? - - # unlock should fail, and should *not* molest the existing lockfile, - # despite it being stale - File.open(l.lockfile, 'w') { |fd| fd.write(childpid) } - assert File.exists?(l.lockfile) - assert !l.unlock - assert File.exists?(l.lockfile) - end - - def test_40_not_locked_at_all - l = Puppet::Util::Pidlock.new(@workdir + '/not_locked') - - assert !l.locked? - # We can't unlock if we don't hold the lock - assert !l.unlock - end - - def test_50_anonymous_lock - l = Puppet::Util::Pidlock.new(@workdir + '/anonymous_lock') - - assert !l.locked? - assert l.lock(:anonymous => true) - assert l.locked? - assert l.anonymous? - assert !l.mine? - assert "", File.read(l.lockfile) - assert !l.unlock - assert l.locked? - assert l.anonymous? - assert l.unlock(:anonymous => true) - assert !File.exists?(l.lockfile) - end -end - |