diff options
165 files changed, 7152 insertions, 1094 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000000..657e1f6adf --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,72 @@ +# Chef Client Changelog + +## Unreleased + +* Including a recipe from a cookbook not in the dependency graph logs + a MissingCookbookDependency warning. Fixes CHEF-4367. +* Improves syntax check speed for Ruby 1.9+, especially when using bundler. +* Send X-Remote-Request-Id header in order to be able to correlate actions during a single run. +* Fix for CHEF-5048. +* Fix for CHEF-5052. +* Fix for CHEF-5018. +* Add --validator option to `knife client create` to be able to create validator clients via knife. +* Add --delete-validators option to `knife client delete` in order to prevent accidental deletion of validator clients. +* Add --delete-validators option to `knife client bulk delete` in order to prevent accidental deletion of validator clients. +* Add -r / --runlist option to chef-client which permanently sets or changes the run_list of a node. +* CHEF-5030: clean up debian ifconfig provider code +* CHEF-5001: spec tests for multiple rollbacks +* Added ohai7 'machinename' attribute as source of `node_name` information +* CHEF-4773: add ruby-shadow support to Mac and FreeBSD distros +* Service Provider for MacOSX now supports `enable` and `disable` +* CHEF-5086: Add reboot_pending? helper to DSL +* Upgrade ohai to 7.0.0.rc.0 +* Make the initial bootstrap message more user friendly (CHEF-5102) +* Correctly handle exceptions in formatters when exception.message is nil (CHEF-4743) +* Fix convergence message in deploy provider (CHEF-4929) +* Make group resource idempotent when gid is specified as a string. (CHEF-4927) +* Non-dupable elements are now handled when duping attribute arrays. (CHEF-4799) +* ruby-shadow is not installed on cygwin platform anymore. (CHEF-4946) +* Upgrade chef-zero to 2.0, remove native-compiled puma as chef dependency. (CHEF-4901/CHEF-5005) +* Don't honor splay when sent USR1 signal. +* Don't set log_level in client.rb by default (CHEF-3698) +* Add IBM PowerKVM to Platform map. (CHEF-5135) +* Cookbook metadata now allows boolean and numeric attributes. (CHEF-4075) +* Knife ssh uses cloud port attribute when available. (CHEF-4962) +* Client info and debug logs now contain cookbook versions in addition to cookbook names. (CHEF-4643) +* ShellOut mixin now exposes a method to capture the live stream during command execution. (CHEF-5017) +* Service provider is now aware of maintenance state on Solaris. (CHEF-4990) +* Refactor Chef::Util::FileEdit to indicate the purpose of the former file_edited, now unwritten_changes?. (CHEF-3714) +* Fixed FileEdit#insert_line_if_no_match to match multiple times. (CHEF-4173) +* Hide passwords in error messages from the Subversion resource. (CHEF-4680) +* The dpkg package provider now supports epoch versions. (CHEF-1752) +* Multiple missing dependencies are now listed on knife cookbook upload. (CHEF-4851) +* Add a public file_edited? method to Chef::Util::FileEdit. (CHEF-3714) +* Package provider defaults to IPS provider on Solaris 5.11+ (CHEF-5037) +* Chef::REST works with frozen options. (CHEF-5064) +* Service provider now uses Systemd on ArchLinux. (CHEF-4905) +* Support knife node run_list add --before. (CHEF-3812) +* Don't destructively merge subhashes in hash_only_merge!. (CHEF-4918) +* Display correct host name in knife ssh error message (CHEF-5029) +* Knife::UI#confirm now has a default_choice option. (CHEF-5057) +* Add knife 'ssl check' and 'ssl fetch' commands for debugging SSL errors. (CHEF-4711) +* Usermod group provider is only used on OpenSuse. (OHAI-339) +* Add knife 'ssl check' and 'ssl fetch' commands for debugging SSL errors (CHEF-4711) +* Cron resource accepts a weekday attribute as a symbol. (CHEF-4848) +* Cron resource accepts special strings, e.g. @reboot (CHEF-2816) +* Call WIN32OLE.ole_initialize before using WMI (CHEF-4888) +* Fix TypeError when calling dup on un-dupable objects in DeepMerge +* Add optional client-side generation of client keys during registration (CHEF-4373) +* Restore warning for the overlay feature in `knife cookbook upload`, + which was accidentally removed in 11.0.0. +* Don't save the run_list during `node.save` when running with override run list. (CHEF-4443) +* Enable Content-Length validation for Chef::HTTP::Simple and fix issues around it. (CHEF-5041, CHEF-5100) +* Windows MSI Package Provider (CHEF-5087) +* Fix mount resource when device is a relative symlink (CHEF-4957) +* Increase bootstrap log_level when knife -V -V is set (CHEF-3610) +* Knife cookbook test should honor chefignore (CHEF-4203) +* Fix ImmutableMash and ImmutableArray to_hash and to_a methods (CHEF-5132) +* guard_interpreter attribute: use powershell\_script, other script resources in guards (CHEF-4553) + +## Last Release: 11.10.0 (02/06/2014) + +http://docs.opscode.com/release/11-10/release_notes.html diff --git a/CHEF_MVPS.md b/CHEF_MVPS.md new file mode 100644 index 0000000000..dfd0985bba --- /dev/null +++ b/CHEF_MVPS.md @@ -0,0 +1,84 @@ +### Chef is proud of our community! + +Every release of Chef we pick someone from the community to name as the Most Valuable Player for that release. It could be someone who provided a big feature, reported a security vulnerability, or someone doing great things in the community that we want to highlight. + +#### Hall of Fame + +After receiving three MVP awards, we add someone to the hall of fame. We want to express our gratitude to their continuing participation and give newer community members the opportunity to be reconignized. + +* Matthew Kent +* Doug MacEachern +* Tollef Fog Heen +* Thom May +* Bryan Berry +* Bryan McLellan + +#### The MVP recipients + +| Release | Date | MVP | +|---------|------|-----| +| [Client 11.10.4](http://www.getchef.com/blog/2014/02/20/chef-client-patch-release-11-10-4/) | 2014-02-20 | Jon Cowie | +| [Client 11.10.2](http://www.getchef.com/blog/2014/02/18/chef-client-release-11-10-2-10-30-4/) | 2014-02-18 | Eric Tucker | +| [Client 11.10.0](http://www.getchef.com/blog/2014/02/06/chef-client-11-10-0-release/) | 2014-02-06 | Nikhil Benesch | +| [Client 11.8.2](http://www.getchef.com/blog/2013/12/06/release-chef-client-10-30-2-11-8-2-mixlib-shellout-1-3-0/) | 2013-12-06 | James Ogden | +| [Client 11.8.0](http://www.opscode.com/blog/2013/10/31/release-chef-client-11-8-0-ohai-6-20-0/) | 2013-10-31 | Eric Saxby | +| [Client 11.6.2](http://www.getchef.com/blog/2013/10/08/release-chef-client-11-6-2-10-28-2/) | 2013-10-08 | Jeff Blaine | +| [Client 11.6.0](http://www.opscode.com/blog/2013/07/23/chef-client-11-6-0-ohai-6-18-0-and-more/) | 2013-07-23 | Jesse Campbell | +| [Client 11.4.0](http://www.opscode.com/blog/2013/02/13/chef-client-11-4-0-10-22-0-released/) | 2013-02-13 | Vaidas Jablonskis | +| [Client 11.2.0](http://www.opscode.com/blog/2013/02/07/chef-client-11-2-0-10-20-0-released/) | 2013-02-06 | Mike Javorski | +| [Chef 11.0.0](http://www.opscode.com/blog/2013/02/04/chef-11-released/) | 2013-02-04 | Andrea Campi, Bryan Berry | +| [Chef 10.30.4](http://www.getchef.com/blog/2014/02/18/chef-client-release-11-10-2-10-30-4/) | 2014-02-18 | Christopher Laco | +| [Chef 10.30.2](http://www.getchef.com/blog/2013/12/06/release-chef-client-10-30-2-11-8-2-mixlib-shellout-1-3-0/) | 2013-12-06 | Phil Dibowitz | +| [Chef 10.28.2](http://www.getchef.com/blog/2013/10/08/release-chef-client-11-6-2-10-28-2/) | 2013-10-08 | Jeff Blaine | +| [Chef 10.28.0](http://www.opscode.com/blog/2013/09/03/chef-10-28-0-released/) | 2013-09-03 | Jeff Blaine | +| [Chef 10.26.0](http://www.opscode.com/blog/2013/05/08/chef-10-26-0-released/) | 2013-05-08 | Ranjib Dey | +| [Chef 10.24.0](http://www.opscode.com/blog/2013/02/15/chef-server-11-0-6-and-10-24-0-released/) | 2013-02-15 | Anthony Goddard | +| [Chef 10.22.0](http://www.opscode.com/blog/2013/02/13/chef-client-11-4-0-10-22-0-released/) | 2013-02-13 | Brian Bianco | +| [Chef 10.20.0](http://www.opscode.com/blog/2013/02/07/chef-client-11-2-0-10-20-0-released/) | 2013-02-06 | Chris Roberts | +| [Chef 10.18.2](http://www.opscode.com/blog/2013/01/18/chef-10-18-2-bugfix-release/) | 2013-01-18 | Fletcher Nichol | +| [Chef 10.18.0](http://www.opscode.com/blog/2013/01/16/chef-10-18-0-released/) | 2013-01-16 | Xabier de Zuazo | +| [Chef 10.16.6](http://www.opscode.com/blog/2013/01/11/chef-10-16-6-security-release/) | 2013-01-11 | Dan Kubb | +| [Chef 10.16.4](http://www.opscode.com/blog/2012/12/26/chef-10-16-4-released/) | 2012-12-26 | Avishai Ish-Shalom | +| [Chef 10.16.2](http://www.opscode.com/blog/2012/10/26/chef-10-16-2-released/) | 2012-10-26 | Jamie Winsor | +| [Chef 10.16.0](http://www.opscode.com/blog/2012/10/22/chef-10-16-0-released/) | 2012-10-22 | John Dewey | +| [Chef 10.14.4](http://www.opscode.com/blog/2012/09/28/chef-10-14-4-released/) | 2012-09-27 | Kendrick Martin | +| [Chef 10.14.2](http://www.opscode.com/blog/2012/09/11/chef-10-14-2-released/) | 2012-09-10 | Phil Dibowitz, Tim Smith | +| [Chef 10.14.0](http://www.opscode.com/blog/2012/09/07/chef-10-14-0-released/) | 2012-09-07 | Xabier de Zuazo | +| [Chef 10.12.0](http://www.opscode.com/blog/2012/06/19/chef-10-12-0-released/) | 2012-06-18 | Chris Roberts | +| [Chef 0.10.10](http://www.opscode.com/blog/2012/05/11/chef-0-10-10-released/) | 2012-05-11 | Juanje Ojeda, Igor Afonov | +| [Chef 0.10.8](http://www.opscode.com/blog/2011/12/15/chef-0-10-8-released/) | 2011-12-15 | Bryan Berry | +| [Chef 0.10.6](http://www.opscode.com/blog/2011/12/14/chef-0-10-6-released/) | 2011-12-13 | Andrea Campi | +| [Chef 0.10.4](http://www.opscode.com/blog/2011/08/11/chef-0-10-4-released/) | 2011-08-11 | Matthew Kent | +| [Chef 0.10.2](http://www.opscode.com/blog/2011/06/29/chef-0-10-2-and-0-9-18-released/) | 2011-06-29 | Daniel Oliver | +| [Chef 0.10.0](http://www.opscode.com/blog/2011/05/02/chef-0-10-0-released/) | 2011-05-02 | Grace Mollison, Darrin Eden | +| [Chef 0.9.18](http://www.opscode.com/blog/2011/06/29/chef-0-10-2-and-0-9-18-released/) | 2011-06-29 | Jesai Langenbach | +| [Chef 0.9.16](http://www.opscode.com/blog/2011/04/15/chef-0-9-16-released/) | 2011-04-15 | Michael Leinartas | +| [Chef 0.9.14](http://www.opscode.com/blog/2011/03/04/chef-0-9-14-released/) | 2011-03-04 | Gilles Devaux | +| [Chef 0.9.12](http://www.opscode.com/blog/2010/10/22/chef-0-9-12-released/) | 2010-10-22 | Laurent Désarmes | +| [Chef 0.9.10](http://www.opscode.com/blog/2010/10/19/chef-0-9-10-ohai-0-5-8-and-mixliblog-1-2-0-released/) | 2010-10-19 | Toomas Pelberg, Tommy Bishop | +| [Chef 0.9.8](http://www.opscode.com/blog/2010/08/05/chef-0-9-8-and-mixlib-authentication-1-1-4-released/) | 2010-08-05 | Joe Williams | +| [Chef 0.9.6](http://www.opscode.com/blog/2010/07/03/chef-0-9-6-released/) | 2010-07-03 | Caleb Tennis | +| [Chef 0.9.4](http://www.opscode.com/blog/2010/06/30/chef-0-9-4-released/) | 2010-06-30 | Ian Meyer | +| [Chef 0.9.0](http://www.opscode.com/blog/2010/06/21/chef-0-9-0-and-ohai-0-5-6-released/) | 2010-06-21 | Doug MacEachern | +| [Chef 0.8.16](http://www.opscode.com/blog/2010/05/11/chef-0-8-16-and-ohai-0-5-4-release/) | 2010-05-11 | Akzhan Abdulin | +| [Chef 0.8.14](http://www.opscode.com/blog/2010/05/07/chef-0-8-14-release/) | 2010-05-07 | Renaud Chaput | +| [Chef 0.8.10](http://www.opscode.com/blog/2010/04/02/chef-0-8-10-release/) | 2010-04-02 | Thom May, Tollef Fog Heen | +| [Chef 0.8.8](http://www.opscode.com/blog/2010/03/18/chef-0-8-8-release/) | 2010-03-18 | Eric Hankins | +| [Chef 0.8.6](http://www.opscode.com/blog/2010/03/05/chef-0-8-6-release/) | 2010-03-05 | Ian Meyer | +| [Chef 0.8.4](http://www.opscode.com/blog/2010/03/02/chef-0-8-4-release/) | 2010-03-02 | Tollef Fog Heen | +| [Chef 0.8.2](http://www.opscode.com/blog/2010/03/01/chef-0-8-2-release/) | 2010-03-01 | Scott M. Likens | +| [Chef 0.7.16](http://www.opscode.com/blog/2009/12/22/chef-0-7-16-release/) | 2009-12-22 | Bryan McLellan | +| [Chef 0.7.14](http://www.opscode.com/blog/2009/10/26/chef-0-7-14-ohai-0-3-6-releases/) | 2009-10-16 | Thom May | +| [Chef 0.7.12](http://www.opscode.com/blog/2009/10/06/chef-0-7-12rc0-ohai-0-3-4rc0-releases/) | 2009-10-06 | Diego Algorta | +| [Chef 0.7.10](http://www.opscode.com/blog/2009/09/04/chef-0-7-10-release/) | 2009-09-04 | Dan DeLeo | +| [Chef 0.7.8](http://www.opscode.com/blog/2009/08/13/chef-0-7-8-release/) | 2009-08-13 | Jeppe Nejsum Madsen | +| [Chef 0.7.6](http://www.opscode.com/blog/2009/08/08/chef-0-7-6-release/) | 2009-08-08 | Grant Zanetti | +| [Chef 0.7.4](http://www.opscode.com/blog/2009/06/26/back-to-back-chef-0-7-2-and-chef-0-7-4-released/) | 2009-06-26 | Hongli Lai | +| [Chef 0.7.2](http://www.opscode.com/blog/2009/06/26/back-to-back-chef-0-7-2-and-chef-0-7-4-released/) | 2009-06-26 | Joshua Sierles | +| [Chef 0.7.0](http://www.opscode.com/blog/2009/06/10/chef-0-7-0-release/) | 2009-06-10 | Matthew Kent | +| [Chef 0.6.2](http://www.opscode.com/blog/2009/04/29/chef-0-6-2-release/) | 2009-04-29 | David Balatero | +| [Chef 0.6.0](http://www.opscode.com/blog/2009/04/29/chef-0-6-0-release/) | 2009-04-29 | Matthew Kent | +| [Chef 0.5.6](http://www.opscode.com/blog/2009/03/06/chef-0-5-6/) | 2009-03-06 | Sean Cribbs | +| [Chef 0.5.4](http://www.opscode.com/blog/2009/02/13/chef-0-5-4/) | 2009-02-13 | Arjuna Christensen | +| [Chef 0.5.2](http://www.opscode.com/blog/2009/02/01/chef-0-5-2-and-ohai-0-1-4/) | 2009-02-01 | Bryan McLellan | + diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 523fadc45f..9d9839fd5c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -24,7 +24,7 @@ Chef uses the Apache 2.0 license to strike a balance between open contribution a The license tells you what rights you have that are provided by the copyright holder. It is important that the contributor fully understands what rights they are licensing and agrees to them. Sometimes the copyright holder isn't the contributor, most often when the contributor is doing work for a company. -To make a good faith effort to ensure these criteria are met, Opscode requires a Contributor License Agreement (CLA) or a Corporate Contributor License +To make a good faith effort to ensure these criteria are met, Chef requires a Contributor License Agreement (CLA) or a Corporate Contributor License Agreement (CCLA) for all contributions. This is without exception due to some matters not being related to copyright and to avoid having to continually check with our lawyers about small patches. @@ -74,7 +74,7 @@ helpful to be clear about your use case and change so they can understand it eve ### Github and Pull Requests -All of Opscode's open source projects are available on [Github](http://www.github.com/opscode). +All of Chef's open source projects are available on [Github](http://www.github.com/opscode). We don't require you to use Github, and we will even take patch diffs attached to tickets on the tracker. However Github has a lot of convenient features, such as being able to see a diff of changes between a @@ -115,7 +115,7 @@ and accounting for it. ## Code Review -Opscode regularly reviews code contributions and provides suggestions for improvement in the code itself or the implementation. +Chef regularly reviews code contributions and provides suggestions for improvement in the code itself or the implementation. We find contributions by searching the ticket tracker for _resolved_ tickets with a status of _fixed_. If we have feedback we will reopen the ticket and you should resolve it again when you've made the changes or have a response to our feedback. When we believe @@ -134,14 +134,14 @@ The versioning for the Chef project is X.Y.Z. * Y is a minor release, which adds both new features and bug fixes * Z is a patch release, which adds just bug fixes -Major releases and have historically been once a year. Minor releases for Chef average every two months and patch releases come as needed. +Major releases have historically been once a year. Minor releases for Chef average every three months and patch releases come as needed. There are usually beta releases and release candidates (RC) of major and minor releases announced on the [chef-dev mailing list](http://lists.opscode.com/sympa/info/chef-dev). Once an RC is released, we wait at least three days to allow for testing for regressions before the final release. If a blocking regression is found then another RC is made containing the fix and the timer is reset. -Once the official release is made, the release notes are available on the [Opscode blog](http://www.opscode.com/blog). +Once the official release is made, the release notes are available on the [Chef blog](http://www.getchef.com/blog). ## Working with the community @@ -151,5 +151,5 @@ These resources will help you learn more about Chef and connect to other members * #chef and #chef-hacking IRC channels on irc.freenode.net * [Community Cookbook site](http://community.opscode.com) * [Chef wiki](http://wiki.opscode.com/display/chef) -* Opscode Chef [product page](http://www.opscode.com/chef) +* Chef [product page](http://www.getchef.com/chef) diff --git a/CONTRIBUTIONS.md b/CONTRIBUTIONS.md new file mode 100644 index 0000000000..9daf7afb40 --- /dev/null +++ b/CONTRIBUTIONS.md @@ -0,0 +1,40 @@ +<!--- +This file is reset every time a new release is done. The contents of this file are for the currently unreleased version. + +Example Contribution: +* **kalistec**: Improved file resource greatly. +--> +# Chef Client Contributions: + +* **jonlives**: Changed the order of recipe and cookbook name setting. Fixes CHEF-5052. +* **jaymzh**: Added support for `enable` and `disable` to MacOSX service provider. +* **bossmc**: Made formatters more resilient to nil exception messages. +* **valodzka**: Fixed the convergence message in deploy provider. +* **linkfanel**: Made attribute arrays able to handle non-dupable elements while being duped. +* **linkfanel**: Removed ruby-shadow installation on cygwin platform. +* **lbragstad**: Add IBM PowerKVM to platform map. +* **slantview**: Allow boolean and numerics in cookbook metadata. +* **jeffmendoza**: Made knife to use cloud attribute for port when available. +* **ryotarai**: Added a method to capture IO for live stream. +* **sawanoboly**: Fixed service provider to be aware of maintenance state on Solaris. +* **cbandy**: Refactored Chef::Util::FileEdit. +* **cbandy**: Fixed insert_line_if_no_match to run multiple times. +* **pavelbrylov**: Modified subversion resource to hide password from error messages. +* **eherot**: Add support for epoch versions to the dpkg package provider. +* **jdmurphy**: Display all missing dependencies when uploading cookbooks. +* **nkrinner**: Add a public file_edited? method to Chef::Util::FileEdit. +* **ccope**: Made package provider to use IPS provider in Solaris 5.11+ +* **josephholsten**: Changed Chef::REST to be able to handle frozen options. +* **andreasrs**: Changed service provider to use Systemd on ArchLinux. +* **eherot**: Add support for epoch versions to the dpkg package provider. +* **jdmurphy**: Display all missing dependencies when uploading cookbooks. +* **nkrinner**: Add a public file_edited? method to Chef::Util::FileEdit. +* **jjasghar**: Output correct host name in knife ssh error message. +* **esigler**: Added default_choice option to Knife::UI#confirm. +* **DracoAter**: Add support to the Cron resource for special strings, e.g. @reboot. +* **ryotarai**: Add support to the Cron resource for weekday passed as a symbol. +* **thommay **: Made sure that `node.save` doesn't save the run_list when chef is running with override-run-list. +* **Maxime Caumartin**: Fix mount resource when device is a relative symlink. +* **jessehu**: Increase bootstrap log_level when knife -V -V is set +* **mveitas**: knife cookbook test honors chefignore +* **zuazo**: Fix ImmutableMash and ImmutableArray to_hash and to_a methods diff --git a/DOC_CHANGES.md b/DOC_CHANGES.md new file mode 100644 index 0000000000..f29d6ab132 --- /dev/null +++ b/DOC_CHANGES.md @@ -0,0 +1,287 @@ +<!--- +This file is reset every time a new release is done. This file describes changes that have not yet been released. + +Example Doc Change: +### Headline for the required change +Description of the required change. +--> + +# Chef Client Doc Changes: + +### --validator option for `knife client create` +Boolean value. If set to true, knife creates a validator client o.w. it creates a user client. Default is false. + +### --delete-validators for `knife client delete` +Option that is required to be specified if user is attempting to delete a validator client. No effect while deleting a user client. + +### --delete-validators for `knife client bulk delete` +Option that is required to be specified if user is attempting to delete a validator client. If not specified users cannot delete a client if it's validator client. If specified knife asks users for confirmation of deleting clients and validator clients seperately. Some examples for scripting: + +To delete all non-validator clients: +`knife client bulk delete regexp --yes` + +To delete all clients including validators: +`knife client bulk delete regexp --delete-validators --yes` + +### -r / --runlist option for chef-client +Option similar to `-o` which sets or changes the run_list of a node permanently. + +### knife bootstrap -V -V + +Running ```knife bootstrap -V -V``` will run the initial chef-client with a log level of debug. + +### knife cookbook test + +```knife cookbook test``` respects chefignore files when selecting which files to test. + +### OHAI 7 Upgrade +Unless there are major issues, 11.12.0 will include OHAI 7. We already have ohai 7 docs in place. We probably need to add some notes to ohai 6 notes that one should now use the newer version when possible. + +### New knife command: `knife ssl check [URI]` + +The `knife ssl check` command is used to check or troubleshoot SSL +configuration. When run without arguments, it tests whether chef/knife +can verify the Chef server's SSL certificate. Otherwise it connects to +the server specified by the given URL. + +Examples: + +* Check knife's configuration against the chef-server: `knife ssl check` +* Check chef-client's configuration: `knife ssl check -c /etc/chef/client.rb` +* Check whether an external server's SSL certificate can be verified: + `knife ssl check https://www.getchef.com` + +### New knife command: `knife ssl fetch [URI]` + +The `knife ssl fetch` command is used to copy certificates from an HTTPS +server to the `trusted_certs_dir` of knife or `chef-client`. If the +certificates match the hostname of the remote server, this command is +all that is required for knife or chef-client to verify the remote +server in the future. WARNING: `knife` has no way to determine whether +the certificates were tampered with in transit. If that happens, +knife/chef-client will trust potentially forged/malicious certificates +until they are deleted from the `trusted_certs_dir`. Users are *VERY STRONGLY* +encouraged to verify the authenticity of the certificates downloaded +with `knife fetch` by some trustworthy means. + +Examples: + +* Fetch the chef server's certificates for use with knife: + `knife ssl fetch` +* Fetch the chef server's certificates for use with chef-client: + `knife ssl fetch -c /etc/chef/client.rb` +* Fetch the certificates from an arbitrary server: + `knife ssl fetch https://www.getchef.com` + +### OpenSUSE and SUSE differentiation + +With the recent change in OHAI to differentiate between SUSE (or SLES - SUSE Enterprise Linux Server) and OpenSUSE we need to update our docs to reflect following (quoting btm): + +* Platform SUSE should be changed to OpenSUSE everywhere that it previously meant OpenSUSE but said SUSE. +* Keeping SLES as platform SUSE is still a bit confusing, but that's the least horrible path we chose. +* It's all still very confusing. :) + +This page is an example but we probably want to search for `suse` in our doc repo and see if there is anywhere else. + +http://docs.opscode.com/dsl_recipe_method_platform_family.html + +### Cron Resource + +The weekday attribute now accepts the weekday as a symbol, e.g. :monday or :thursday. + +The new time attribute takes special time values specified by cron as a symbol, such as :reboot or :monthly. + +### SSL Verification Warnings + +Chef 11.12 emits verbose warnings when configured to not verify SSL +certificates. Though not verifying certificates is currently the default +setting, this is unsecure and a future release of Chef will change the +default setting so that SSL certificates are verified. + +Users are encouraged to resolve these warnings by adding the following +to their configuration files (client.rb or solo.rb): + +`ssl_verify_mode :verify_peer` + +This setting will check that the certificate presented by HTTPS servers +is signed by a trusted authority. By default, the on-premises Enterprise +Chef and Open Source Chef server use a self-signed certificate that +chef-client will not be able to verify, which will result in SSL errors +when connecting to the server. To check SSL connectivity with the +server, users can use the `knife ssl check` command. If the server is +configured to use an untrusted self-signed certificate, users can +configure chef-client to trust the remote server by copying the server's +certificate to the `trusted_certs_dir`. The `knife ssl fetch` command +can be used to automate this process; however, `knife` is not able to +determine whether certificates downloaded with `knife ssl fetch` have +been tampered with during the download, so users should verify the +authenticity of any certificates downloaded this way. + +If a user absolutely cannot enable certificate verification and wishes +to suppress SSL warnings, they can use HTTP instead of HTTPS as a +workaround. This is highly discouraged. If some behavior of Chef +prevents a user from enabling SSL certificate verification, they are +encouraged to file a bug report. + +### New Configuration Option: `local_key_generation` + +Chef 11.x servers support client-side generation of keys when creating +new clients. Generating the keys on the client provides two benefits: 1) +the private key never travels over the network, which improves security; +2) the CPU load imposed by key creation is moved to the node and +distributed, which allows the server to handle more concurrent client +registrations. + +For compatibility reasons, this feature is opt-in, but will likely be +the default or even only behavior in Chef 12. + +To enable it, add this to client.rb before running chef-client on a node +for the first time: + +``` +local_key_generation true +``` + +The default value of this setting is `false` + +*NOTE:* Chef servers that implement the 10.x API do not support this +feature. Enabling this on a client that connects to a 10.X API server +will cause client registration to silently fail. Don't do it. + +### Windows Installer (MSI) Package Provider + +The windows_package provider installs and removes Windows Installer (MSI) packages. +This provider utilizies the ProductCode extracted from the MSI package to determine +if the package is currently installed. + +You may use the ```package``` resource to use this provider, and you must use the +```package``` resource if you are also using the windows cookbook, which contains +the windows_package LWRP. + +#### Example + +``` +package "7zip" do + action :install + source 'C:\7z920.msi' +end +``` + +#### Actions +* :install +* :remove + +#### Attributes +* source - The location of the package to install. Default value: the ```name``` of the resource. +* options - Additional options that are passed to msiexec. +* installer_type - The type of package being installed. Can be auto-detected. Currently only :msi is supported. +* timeout - The time in seconds allowed for the package to successfully be installed. Defaults to 600 seconds. +* returns - Return codes that signal a successful installation. Defaults to 0. + +### New resource attribute: `guard_interpreter` +All resources have a new attribute, `guard_interpreter`, which specifies a +Chef script resource that should be used to evaluate a string command +passed to a guard. Any attributes of the evaluating resource may be specified in +the options that normally follow the guard's command string argument. For example: + + # Tell Chef to use bash to interpret the guard string. + # Then we can use a guard command that is valid for bash + # but not for csh for instance + bash 'backupsettings' do + guard_interpreter :bash + code 'cp ~/appsettings.json ~/backup/appsettings.json' + not_if '[[ -e ./appsettings.json ]]', :cwd => '~/backup' + end + +The argument for `guard_interpreter` may be set to any of the following values: +* The symbol name for a Chef Resource derived from the Chef `script` resource + such as `:bash`, `:powershell_script`, etc. +* The symbol `:default` which means that a resource is not used to evaluate + the guard command argument, it is simply executed by the default shell as in + previous releases of Chef. + +By default, `guard_interpreter` is set to `:default` in this release. + +#### Attribute inheritance with `guard_interpreter` + +When `guard_interpreter` is not set to `:default`, the resource that evaluates the command will +also inherit certain attribute values from the resource that contains the +guard. + +Inherited attributes for all `script` resources are: + +* `:cwd` +* `:environment` +* `:group` +* `:path` +* `:user` +* `:umask` + +For the `powershell_script` resource, the following attribute is inherited: +* `:architecture` + +Inherited attributes may be overridden by specifying the same attribute as an +argument to the guard itself. + +#### Guard inheritance example + +In the following example, the `:environment` hash only needs to be set once +since the `bash` resource that execute the guard will inherit the same value: + + script "javatooling" do + environment {"JAVA_HOME" => '/usr/lib/java/jdk1.7/home'} + code 'java-based-daemon-ctl.sh -start' + not_if 'java-based-daemon-ctl.sh -test-started' # No need to specify environment again + end + +### New `powershell_script` resource attribute: `convert_boolean_return` + +The `powershell_script` resource has a new attribute, `convert_boolean_return` +that causes the script interpreter to return 0 if the last line of the command +evaluted by PowerShell results in a boolean PowerShell data type that is true, or 1 if +it results in a boolean PowerShell data type that is false. For example, the +following two fragments will run successfully without error: + + powershell_script 'false' do + code '$false' + end + + powershell_script 'true' do + code '$true' + end + +But when `convert_boolean_return` is set to `true`, the "true" case above will +still succeed, but the false case will raise an exception: + + # Raises an exception + powershell_script 'false' do + convert_boolean_return true + code '$false' + end + +When used at recipe scope, the default value of `convert_boolean_return` is +`false` in this release. However, if `guard_interpreter` is set to +`:powershell_script`, the guard expression will be evaluted with a +`powershell_script` resource that has the `convert_boolean_return` attribute +set to `true`. + +#### Guard command example + +The behavior of `convert_boolean_return` is similar to the "$?" +expression's value after use of the `test` command in Unix-flavored shells and +its translation to an exit code for the shell. Since this attribute is set to +`true` when `powershell_script` is used via the `guard_interpreter` to +evaluate the guard expression, the behavior of `powershell_script` is very +similar to guards executed with Unix shell interpreters as seen below: + + bash 'make_safe_backup' do + code 'cp ~/data/nodes.json ~/data/nodes.bak' + not_if 'test -e ~/data/nodes.bak' + end + + # convert_boolean_return is true by default in guards + powershell_script 'make_safe_backup' do + guard_interpreter :powershell_script + code 'cp ~/data/nodes.json ~/data/nodes.bak' + not_if 'test-path ~/data/nodes.bak' + end @@ -11,7 +11,7 @@ group(:development, :test) do gem "simplecov" gem 'rack', "~> 1.5.1" - gem 'ruby-shadow', :platforms => :ruby unless RUBY_PLATFORM.downcase.match(/(darwin|freebsd|aix)/) + gem 'ruby-shadow', :platforms => :ruby unless RUBY_PLATFORM.downcase.match(/(aix|cygwin)/) end # If you want to load debugging tools into the bundle exec sandbox, @@ -61,7 +61,7 @@ Then get the source and install it: Before working on the code, if you plan to contribute your changes, you need to read the -[Opscode Contributing document](http://docs.opscode.com/community_contributions.html). +[Chef Contributions document](http://docs.opscode.com/community_contributions.html). You will also need to set up the repository with the appropriate branches. We document the process on the diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md new file mode 100644 index 0000000000..a4796c3e5a --- /dev/null +++ b/RELEASE_NOTES.md @@ -0,0 +1,195 @@ +<!--- +This file is reset every time a new release is done. The contents of this file are for the currently unreleased version. + +Example Note: + +## Example Heading +Details about the thing that changed that needs to get included in the Release Notes in markdown. +--> +# Chef Client Release Notes: + +#### `knife ssl check` and `knife ssl fetch` Commands + +As part of our process to transition to verifying SSL certificates by +default, we've added knife commands to help you test (and fix, if +needed) your SSL configuration. + +`knife ssl check` makes an SSL connection to your Chef server or any +other HTTPS server and tells you if the server presents a valid +certificate. If the certificate is not valid, knife will give further +information about the cause and some instructions on how to remedy the +issue. For example, if your Chef server uses an untrusted self-signed +certificate: + +``` +ERROR: The SSL certificate of chefserver.test could not be +verified +Certificate issuer data: +/C=US/ST=WA/L=Seattle/O=YouCorp/OU=Operations/CN=chefserver.test/emailAddress=you@example.com + +Configuration Info: + +OpenSSL Configuration: +* Version: OpenSSL 1.0.1e 11 Feb 2013 +* Certificate file: /usr/local/etc/openssl/cert.pem +* Certificate directory: /usr/local/etc/openssl/certs +Chef SSL Configuration: +* ssl_ca_path: nil +* ssl_ca_file: nil +* trusted_certs_dir: "/Users/ddeleo/.chef/trusted_certs" + +TO FIX THIS ERROR: + +If the server you are connecting to uses a self-signed certificate, you +must +configure chef to trust that server's certificate. + +By default, the certificate is stored in the following location on the +host +where your chef-server runs: + + /var/opt/chef-server/nginx/ca/SERVER_HOSTNAME.crt + +Copy that file to you trusted_certs_dir (currently: /home/user/.chef/trusted_certs) +using SSH/SCP or some other secure method, then re-run this command to confirm +that the server's certificate is now trusted. +``` + +`knife ssl fetch` allows you to automatically fetch a server's +certificates to your trusted certs directory. This provides an easy way +to configure chef to trust your self-signed certificates. Note that +knife cannot verify that the certificates haven't been tampered with, so +you should verify their content after downloading. + + +#### Unsecure SSL Verification Mode Now Triggers a Warning + +When `ssl_verify_mode` is set to `:verify_none`, Chef will print a +warning. Use `knife ssl check` to test SSL connectivity and then add +`ssl_verify_mode :verify_peer` to your configuration file to fix the +warning. Though `:verify_none` is currently the default, this will be +changed in a future release, so users are encouraged to be proactive in +testing and updating their SSL configuration. + +#### Chef Solo Missing Dependency Warning ([CHEF-4367](https://tickets.opscode.com/browse/CHEF-4367)) + +Chef 11.0 introduced ordered evaluation of non-recipe files in +cookbooks, based on the dependencies specified in your cookbooks' +metadata. This was a huge improvement on the previous behavior for all +chef users, but it also introduced a problem for chef-solo users: +because of the way chef-solo works, it was possible to use +`include_recipe` to load a recipe from a cookbook without specifying the +dependency in the metadata. This would load the recipe without having +evaluated the associated attributes, libraries, LWRPs, etc. in that +recipe's cookbook, and the recipe would fail to load with errors that +did not suggest the actual cause of the failure. + +We've added a check to `include_recipe` so that attempting to include a +recipe which is not a dependency of any cookbook specified in the run +list will now log a warning with a message describing the problem and +solution. In the future, this warning will become an error. + +#### Windows MSI Package Provider + +The first windows package provider has been added to core Chef. It supports Windows Installer (MSI) files only, +and maintains idempotency by using the ProductCode from inside the MSI to determine if the products installation state. + +``` +package "install 7zip" do + action :install + source 'c:\downloads\7zip.msi' +end +``` + +You can continue to use the windows_package LWRP from the windows cookbook alongside this provider. + +#### reboot_pending? + +We have added a ```reboot_pending?``` method to the recipe DSL. This method returns true or false if the operating system +has a rebooting pending due to updates and a reboot being necessary to complete the installation. It does not report if a reboot has been requested, e.g. if someone has scheduled a restart using shutdown. It currently supports Windows and Ubuntu Linux. + +``` +Chef::Log.warn "There is a pending reboot, which will affect this Chef run" if reboot_pending? + +execute "Install Application" do + command 'C:\application\setup.exe' + not_if { reboot_pending? } +end +``` + +#### FileEdit + +Chef::Util::FileEdit has been refactored into a Chef::Util::Editor class. The existing class continues to manage the files being edited while the new class handles the actual modification of the data. +Along with this refactor, #insert_line_if_no_match can now manipulate a file multiple times. FileEdit also now has a #file_edited? method that can be used to tell if changes were made to the file on disk. + +#### DeepMerge sub-hash precedence bugfix ([CHEF-4918](https://tickets.opscode.com/browse/CHEF-4918)) + +We discovered a bug where Chef incorrectly merged override attribute sub-hashes that were at least three levels deep as normal attributes. +This has been corrected, and is not expected to cause any behavior change +If you're an advanced user of attribute precedence, you may find some attributes were saved to your node object that you hadn't expected. + +#### Cron Resource + +The weekday attribute now accepts the weekday as a symbol, e.g. :monday or :thursday. +There is a new attribute named ```time``` that takes special cron time values as a symbol, such as :reboot or :monthly. + +#### `guard_interpreter` attribute + +All Chef resources now support the `guard_interpreter` attribute, which +enables you to use a Chef `script` such as `bash`, `powershell_script`, +`perl`, etc., to evaluate the string command passed to a +guard (i.e. `not_if` or `only_if` attribute). This addresses the related ticket +[CHEF-4553](https://tickets.opscode.com/browse/CHEF-4453) which is concerned +with the usability of the `powershell_script` resource, but also benefits +users of resources like `python`, `bash`, etc: + + # See CHEF-4553 -- let powershell_script execute the guard + powershell_script 'make_logshare' do + guard_interpreter :powershell_script + code 'new-smbshare logshare $env:systemdrive\\logs' + not_if 'get-smbshare logshare' + end + +#### `convert_boolean_return` attribute for `powershell_script` + +When set to `true`, the `convert_boolean_return` attribute will allow any script executed by +`powershell_script` that exits with a PowerShell boolean data type to convert +PowerShell boolean `$true` to exit status 0 and `$false` to exit status 1. + +The new attribute defaults to `false` except when the `powershell_script` resource is executing script passed to a guard attribute +via the `guard_interpreter` attribute in which case it is `true` by default. + +#### knife bootstrap log_level + +Running ```knife bootstrap -V -V``` will run the initial chef-client with a log level of debug. + +#### knife cookbook test + +Knife cookbook test now respects [chefignore files](http://docs.opscode.com/essentials_repository.html#chefignore-files), allowing you to exclude unrelated ruby code such as unit tests. + +#### Miscellaneous + +* The subversion resource will now mask plaintext passwords in error output. +* The debian pkg provider now supports epochs in the version string. +* When a cookbook upload is missing multiple dependencies, all of them are now listed. +* knife node run_list add now supports a --before option. + +#### OHAI 7 + +After spending 3 months in the RC stage, OHAI 7 is now included in Chef Client 11.10.0. Note that Chef Client 10.32.0 still includes OHAI 6. + +For more information about the changes in OHAI 7 please see our previous blog post [here](http://www.getchef.com/blog/2014/01/20/ohai-7-0-release-candidate/). + +# Chef Client Breaking Changes: + +#### OpenSuse and Suse Differentiation + +The Ohai version currently included in Chef reports both SUSE and OpenSUSE platforms as "suse" and the way to differentiate between these two platforms has been to use the version numbers. But since SUSE version numbers have caught up with OpenSUSE, it's not possible to differentiate between these platforms anymore. + +This issue is being resolved in Ohai 7 that is included in the current release of Chef Client by reporting these two platforms separately. This resolves the overall problem however it's a breaking change in the sense that OpenSUSE platforms will be reported as "opensuse" as the platform. + +Normally Chef would require a major version bump for this change but since the original scenario is currently broken we've decided to include this change without a major version bump in Chef. + +If you need to differentiate between OpenSUSE and SUSE in your cookbooks, please make sure the differentiation logic is updated to use the new :platform attribute values rather than the :platform_version in your cookbooks before upgrading to this version. + +None. diff --git a/chef.gemspec b/chef.gemspec index fcc585c363..12084e30e4 100644 --- a/chef.gemspec +++ b/chef.gemspec @@ -16,8 +16,8 @@ Gem::Specification.new do |s| s.add_dependency "mixlib-cli", "~> 1.4" s.add_dependency "mixlib-log", "~> 1.3" s.add_dependency "mixlib-authentication", "~> 1.3" - s.add_dependency "mixlib-shellout", "~> 1.3" - s.add_dependency "ohai", "~> 6.0" + s.add_dependency "mixlib-shellout", "= 1.4.0.rc.0" + s.add_dependency "ohai", "= 7.0.0.rc.1" s.add_dependency "rest-client", ">= 1.0.4", "< 1.7.0" # rest-client has an unbounded dependency on mime-types. @@ -36,12 +36,15 @@ Gem::Specification.new do |s| s.add_dependency "erubis", "~> 2.7" s.add_dependency "diff-lcs", "~> 1.2", ">= 1.2.4" - s.add_dependency "chef-zero", "~> 1.7", ">= 1.7.2" - s.add_dependency "puma", "~> 1.6" + # There's a bug with Chef Zero and IPV6 prior to version 2.0.2 + s.add_dependency "chef-zero", "~> 2.0", ">= 2.0.2" s.add_dependency "pry", "~> 0.9" - %w(rake rack rspec_junit_formatter).each { |gem| s.add_development_dependency gem } + # Rake 10.2 drops Ruby 1.8 support + s.add_development_dependency("rake", "~> 10.1.0") + + %w( rack rspec_junit_formatter).each { |gem| s.add_development_dependency gem } %w(rspec-core rspec-expectations rspec-mocks).each { |gem| s.add_development_dependency gem, "~> 2.14.0" } s.bindir = "bin" diff --git a/lib/chef/api_client.rb b/lib/chef/api_client.rb index 66cbd3f30e..7b7fd99ff7 100644 --- a/lib/chef/api_client.rb +++ b/lib/chef/api_client.rb @@ -162,9 +162,7 @@ class Chef if response.kind_of?(Chef::ApiClient) response else - client = Chef::ApiClient.new - client.name(response['clientname']) - client + json_create(response) end end diff --git a/lib/chef/api_client/registration.rb b/lib/chef/api_client/registration.rb index f44c326d5d..213d0b7f49 100644 --- a/lib/chef/api_client/registration.rb +++ b/lib/chef/api_client/registration.rb @@ -30,14 +30,13 @@ class Chef # a new client/node identity by borrowing the validator client identity # when creating a new client. class Registration - attr_reader :private_key attr_reader :destination attr_reader :name def initialize(name, destination) @name = name @destination = destination - @private_key = nil + @server_generated_private_key = nil end # Runs the client registration process, including creating the client on @@ -90,29 +89,67 @@ class Chef end def create - response = http_api.post("clients", :name => name, :admin => false) - @private_key = response["private_key"] + response = http_api.post("clients", post_data) + @server_generated_private_key = response["private_key"] response end def update - response = http_api.put("clients/#{name}", :name => name, - :admin => false, - :private_key => true) + response = http_api.put("clients/#{name}", put_data) if response.respond_to?(:private_key) # Chef 11 - @private_key = response.private_key + @server_generated_private_key = response.private_key else # Chef 10 - @private_key = response["private_key"] + @server_generated_private_key = response["private_key"] end response end + def put_data + base_put_data = { :name => name, :admin => false } + if self_generate_keys? + base_put_data[:public_key] = generated_public_key + else + base_put_data[:private_key] = true + end + base_put_data + end + + def post_data + post_data = { :name => name, :admin => false } + post_data[:public_key] = generated_public_key if self_generate_keys? + post_data + end + + def http_api @http_api_as_validator ||= Chef::REST.new(Chef::Config[:chef_server_url], Chef::Config[:validation_client_name], Chef::Config[:validation_key]) end + # Whether or not to generate keys locally and post the public key to the + # server. Delegates to `Chef::Config.local_key_generation`. Servers + # before 11.0 do not support this feature. + def self_generate_keys? + Chef::Config.local_key_generation + end + + def private_key + if self_generate_keys? + generated_private_key.to_pem + else + @server_generated_private_key + end + end + + def generated_private_key + @generated_key ||= OpenSSL::PKey::RSA.generate(2048) + end + + def generated_public_key + generated_private_key.public_key.to_pem + end + def file_flags base_flags = File::CREAT|File::TRUNC|File::RDWR # Windows doesn't have symlinks, so it doesn't have NOFOLLOW diff --git a/lib/chef/application.rb b/lib/chef/application.rb index 04e88de2ce..601bbd91f1 100644 --- a/lib/chef/application.rb +++ b/lib/chef/application.rb @@ -208,7 +208,8 @@ class Chef::Application @chef_client = Chef::Client.new( @chef_client_json, :override_runlist => config[:override_runlist], - :specific_recipes => specific_recipes + :specific_recipes => specific_recipes, + :runlist => config[:runlist] ) @chef_client_json = nil diff --git a/lib/chef/application/client.rb b/lib/chef/application/client.rb index de644b5f31..c579fe4ba1 100644 --- a/lib/chef/application/client.rb +++ b/lib/chef/application/client.rb @@ -25,7 +25,6 @@ require 'chef/log' require 'chef/config_fetcher' require 'chef/handler/error_report' - class Chef::Application::Client < Chef::Application # Mimic self_pipe sleep from Unicorn to capture signals safely @@ -170,7 +169,7 @@ class Chef::Application::Client < Chef::Application option :override_runlist, :short => "-o RunlistItem,RunlistItem...", :long => "--override-runlist RunlistItem,RunlistItem...", - :description => "Replace current run list with specified items", + :description => "Replace current run list with specified items for a single run", :proc => lambda{|items| items = items.split(',') items.compact.map{|item| @@ -178,6 +177,16 @@ class Chef::Application::Client < Chef::Application } } + option :runlist, + :short => "-r RunlistItem,RunlistItem...", + :long => "--runlist RunlistItem,RunlistItem...", + :description => "Permanently replace current run list with specified items", + :proc => lambda{|items| + items = items.split(',') + items.compact.map{|item| + Chef::RunList::RunListItem.new(item) + } + } option :why_run, :short => '-W', :long => '--why-run', @@ -218,12 +227,10 @@ class Chef::Application::Client < Chef::Application :boolean => true end - attr_reader :chef_client_json + IMMEDIATE_RUN_SIGNAL = "1".freeze + GRACEFUL_EXIT_SIGNAL = "2".freeze - def initialize - super - @exit_gracefully = false - end + attr_reader :chef_client_json # Reconfigure the chef client # Re-open the JSON attributes and load them into the node @@ -285,13 +292,12 @@ class Chef::Application::Client < Chef::Application trap("USR1") do Chef::Log.info("SIGUSR1 received, waking up") - SELF_PIPE[1].putc('.') # wakeup master process from select + SELF_PIPE[1].putc(IMMEDIATE_RUN_SIGNAL) # wakeup master process from select end trap("TERM") do Chef::Log.info("SIGTERM received, exiting gracefully") - @exit_gracefully = true - SELF_PIPE[1].putc('.') + SELF_PIPE[1].putc(GRACEFUL_EXIT_SIGNAL) end end @@ -303,23 +309,24 @@ class Chef::Application::Client < Chef::Application Chef::Daemon.daemonize("chef-client") end + signal = nil + loop do begin - Chef::Application.exit!("Exiting", 0) if @exit_gracefully - if Chef::Config[:splay] + Chef::Application.exit!("Exiting", 0) if signal == GRACEFUL_EXIT_SIGNAL + + if Chef::Config[:splay] and signal != IMMEDIATE_RUN_SIGNAL splay = rand Chef::Config[:splay] Chef::Log.debug("Splay sleep #{splay} seconds") sleep splay end + + signal = nil run_chef_client(Chef::Config[:specific_recipes]) + if Chef::Config[:interval] Chef::Log.debug("Sleeping for #{Chef::Config[:interval]} seconds") - unless SELF_PIPE.empty? - client_sleep Chef::Config[:interval] - else - # Windows - sleep Chef::Config[:interval] - end + signal = interval_sleep else Chef::Application.exit! "Exiting", 0 end @@ -329,12 +336,7 @@ class Chef::Application::Client < Chef::Application if Chef::Config[:interval] Chef::Log.error("#{e.class}: #{e}") Chef::Log.error("Sleeping for #{Chef::Config[:interval]} seconds before trying again") - unless SELF_PIPE.empty? - client_sleep Chef::Config[:interval] - else - # Windows - sleep Chef::Config[:interval] - end + signal = interval_sleep retry else Chef::Application.fatal!("#{e.class}: #{e.message}", 1) @@ -345,8 +347,17 @@ class Chef::Application::Client < Chef::Application private + def interval_sleep + unless SELF_PIPE.empty? + client_sleep Chef::Config[:interval] + else + # Windows + sleep Chef::Config[:interval] + end + end + def client_sleep(sec) IO.select([ SELF_PIPE[0] ], nil, nil, sec) or return - SELF_PIPE[0].getc + SELF_PIPE[0].getc.chr end end diff --git a/lib/chef/client.rb b/lib/chef/client.rb index 722c9915e9..2e5963e996 100644 --- a/lib/chef/client.rb +++ b/lib/chef/client.rb @@ -44,6 +44,7 @@ require 'chef/version' require 'chef/resource_reporter' require 'chef/run_lock' require 'chef/policy_builder' +require 'chef/request_id' require 'ohai' require 'rbconfig' @@ -54,6 +55,16 @@ class Chef class Client include Chef::Mixin::PathSanity + # IO stream that will be used as 'STDOUT' for formatters. Formatters are + # configured during `initialize`, so this provides a convenience for + # setting alternative IO stream during tests. + STDOUT_FD = STDOUT + + # IO stream that will be used as 'STDERR' for formatters. Formatters are + # configured during `initialize`, so this provides a convenience for + # setting alternative IO stream during tests. + STDERR_FD = STDERR + # Clears all notifications for client run status events. # Primarily for testing purposes. def self.clear_notifications @@ -128,15 +139,13 @@ class Chef attr_accessor :rest attr_accessor :runner - #-- - # TODO: timh/cw: 5-19-2010: json_attribs should be moved to RunContext? attr_reader :json_attribs attr_reader :run_status attr_reader :events # Creates a new Chef::Client. def initialize(json_attribs=nil, args={}) - @json_attribs = json_attribs + @json_attribs = json_attribs || {} @node = nil @run_status = nil @runner = nil @@ -148,12 +157,16 @@ class Chef @events = EventDispatch::Dispatcher.new(*event_handlers) @override_runlist = args.delete(:override_runlist) @specific_recipes = args.delete(:specific_recipes) + + if new_runlist = args.delete(:runlist) + @json_attribs["run_list"] = new_runlist + end end def configure_formatters formatters_for_run.map do |formatter_name, output_path| if output_path.nil? - Chef::Formatters.new(formatter_name, STDOUT, STDERR) + Chef::Formatters.new(formatter_name, STDOUT_FD, STDERR_FD) else io = File.open(output_path, "a+") io.sync = true @@ -280,13 +293,10 @@ class Chef end def node_name - name = Chef::Config[:node_name] || ohai[:fqdn] || ohai[:hostname] + name = Chef::Config[:node_name] || ohai[:fqdn] || ohai[:machinename] || ohai[:hostname] Chef::Config[:node_name] = name - unless name - msg = "Unable to determine node name: configure node_name or configure the system's hostname and fqdn" - raise Chef::Exceptions::CannotDetermineNodeName, msg - end + raise Chef::Exceptions::CannotDetermineNodeName unless name # node names > 90 bytes only work with authentication protocol >= 1.1 # see discussion in config.rb. @@ -391,10 +401,15 @@ class Chef # don't add code that may fail before entering this section to be sure to release lock begin runlock.save_pid + + check_ssl_config + + request_id = Chef::RequestID.instance.request_id run_context = nil @events.run_start(Chef::VERSION) Chef::Log.info("*** Chef #{Chef::VERSION} ***") Chef::Log.info "Chef-client pid: #{Process.pid}" + Chef::Log.debug("Chef-client request_id: #{request_id}") enforce_path_sanity run_ohai @events.ohai_completed(node) @@ -404,6 +419,7 @@ class Chef build_node + run_status.run_id = request_id run_status.start_clock Chef::Log.info("Starting Chef Run for #{node.name}") run_started @@ -434,6 +450,8 @@ class Chef @events.run_failed(e) raise ensure + Chef::RequestID.instance.reset_request_id + request_id = nil @run_status = nil run_context = nil runlock.release @@ -474,6 +492,37 @@ class Chef Chef::ReservedNames::Win32::Security.has_admin_privileges? end + def check_ssl_config + if Chef::Config[:ssl_verify_mode] == :verify_none and !Chef::Config[:verify_api_cert] + Chef::Log.warn(<<-WARN) + +* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +SSL validation of HTTPS requests is disabled. HTTPS connections are still +encrypted, but chef is not able to detect forged replies or man in the middle +attacks. + +To fix this issue add an entry like this to your configuration file: + +``` + # Verify all HTTPS connections (recommended) + ssl_verify_mode :verify_peer + + # OR, Verify only connections to chef-server + verify_api_cert true +``` + +To check your SSL configuration, or troubleshoot errors, you can use the +`knife ssl check` command like so: + +``` + knife ssl check -c #{Chef::Config.config_file} +``` + +* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +WARN + end + end + end end diff --git a/lib/chef/config.rb b/lib/chef/config.rb index da3f3790f6..3099d876c1 100644 --- a/lib/chef/config.rb +++ b/lib/chef/config.rb @@ -432,6 +432,17 @@ class Chef default(:validation_key) { chef_zero.enabled ? nil : platform_specific_path("/etc/chef/validation.pem") } default :validation_client_name, "chef-validator" + # When creating a new client via the validation_client account, Chef 11 + # servers allow the client to generate a key pair locally and sent the + # public key to the server. This is more secure and helps offload work from + # the server, enhancing scalability. If enabled and the remote server + # implements only the Chef 10 API, client registration will not work + # properly. + # + # The default value is `false` (Server generates client keys). Set to + # `true` to enable client-side key generation. + default(:local_key_generation) { false } + # Zypper package provider gpg checks. Set to true to enable package # gpg signature checking. This will be default in the # future. Setting to false disables the warnings. diff --git a/lib/chef/cookbook/chefignore.rb b/lib/chef/cookbook/chefignore.rb index 17c000350d..aa9345e64e 100644 --- a/lib/chef/cookbook/chefignore.rb +++ b/lib/chef/cookbook/chefignore.rb @@ -25,7 +25,11 @@ class Chef attr_reader :ignores def initialize(ignore_file_or_repo) + # Check the 'ignore_file_or_repo' path first and then look in the parent directory + # to handle both the chef repo cookbook layout and a standalone cookbook @ignore_file = find_ignore_file(ignore_file_or_repo) + @ignore_file = find_ignore_file(File.dirname(ignore_file_or_repo)) unless readable_file_or_symlink?(@ignore_file) + @ignores = parse_ignore_file end @@ -43,8 +47,7 @@ class Chef def parse_ignore_file ignore_globs = [] - if File.exist?(@ignore_file) && File.readable?(@ignore_file) && - (File.file?(@ignore_file) || File.symlink?(@ignore_file)) + if readable_file_or_symlink?(@ignore_file) File.foreach(@ignore_file) do |line| ignore_globs << line.strip unless line =~ COMMENTS_AND_WHITESPACE end @@ -61,6 +64,11 @@ class Chef File.join(path, 'chefignore') end end + + def readable_file_or_symlink?(path) + File.exist?(@ignore_file) && File.readable?(@ignore_file) && + (File.file?(@ignore_file) || File.symlink?(@ignore_file)) + end end end end diff --git a/lib/chef/cookbook/metadata.rb b/lib/chef/cookbook/metadata.rb index b9b32c8224..32597490d3 100644 --- a/lib/chef/cookbook/metadata.rb +++ b/lib/chef/cookbook/metadata.rb @@ -391,14 +391,14 @@ class Chef :description => { :kind_of => String }, :choice => { :kind_of => [ Array ], :default => [] }, :calculated => { :equal_to => [ true, false ], :default => false }, - :type => { :equal_to => [ "string", "array", "hash", "symbol" ], :default => "string" }, + :type => { :equal_to => [ "string", "array", "hash", "symbol", "boolean", "numeric" ], :default => "string" }, :required => { :equal_to => [ "required", "recommended", "optional", true, false ], :default => "optional" }, :recipes => { :kind_of => [ Array ], :default => [] }, - :default => { :kind_of => [ String, Array, Hash ] } + :default => { :kind_of => [ String, Array, Hash, Symbol, Numeric, TrueClass, FalseClass ] } } ) options[:required] = remap_required_attribute(options[:required]) unless options[:required].nil? - validate_string_array(options[:choice]) + validate_choice_array(options) validate_calculated_default_rule(options) validate_choice_default_rule(options) @@ -546,6 +546,34 @@ INVALID end end + # Validate the choice of the options hash + # + # Raise an exception if the members of the array do not match the defaults + # === Parameters + # opts<Hash>:: The options hash + def validate_choice_array(opts) + if opts[:choice].kind_of?(Array) + case opts[:type] + when "string" + validator = [ String ] + when "array" + validator = [ Array ] + when "hash" + validator = [ Hash ] + when "symbol" + validator = [ Symbol ] + when "boolean" + validator = [ TrueClass, FalseClass ] + when "numeric" + validator = [ Numeric ] + end + + opts[:choice].each do |choice| + validate( {:choice => choice}, {:choice => {:kind_of => validator}} ) + end + end + end + # For backwards compatibility, remap Boolean values to String # true is mapped to "required" # false is mapped to "optional" diff --git a/lib/chef/cookbook/synchronizer.rb b/lib/chef/cookbook/synchronizer.rb index 4522323fac..fc5d16617c 100644 --- a/lib/chef/cookbook/synchronizer.rb +++ b/lib/chef/cookbook/synchronizer.rb @@ -92,7 +92,7 @@ class Chef # === Returns # true:: Always returns true def sync_cookbooks - Chef::Log.info("Loading cookbooks [#{cookbook_names.sort.join(', ')}]") + Chef::Log.info("Loading cookbooks [#{cookbooks.map {|ckbk| ckbk.name + '@' + ckbk.version}.join(', ')}]") Chef::Log.debug("Cookbooks detail: #{cookbooks.inspect}") clear_obsoleted_cookbooks @@ -136,7 +136,7 @@ class Chef # valid_cache_entries<Hash>:: Out-param; Added to this hash are the files that # were referred to by this cookbook def sync_cookbook(cookbook) - Chef::Log.debug("Synchronizing cookbook #{cookbook.name}") + Chef::Log.debug("Synchronizing cookbook #{cookbook.name} #{cookbook.version}") # files and templates are lazily loaded, and will be done later. diff --git a/lib/chef/cookbook/syntax_check.rb b/lib/chef/cookbook/syntax_check.rb index 59888e2ba3..effc7dd01d 100644 --- a/lib/chef/cookbook/syntax_check.rb +++ b/lib/chef/cookbook/syntax_check.rb @@ -17,6 +17,8 @@ # require 'pathname' +require 'stringio' +require 'erubis' require 'chef/mixin/shell_out' require 'chef/mixin/checksum' @@ -75,6 +77,8 @@ class Chef # validated. attr_reader :validated_files + attr_reader :chefignore + # Creates a new SyntaxCheck given the +cookbook_name+ and a +cookbook_path+. # If no +cookbook_path+ is given, +Chef::Config.cookbook_path+ is used. def self.for_cookbook(cookbook_name, cookbook_path=nil) @@ -90,11 +94,9 @@ class Chef # cookbook_path::: the (on disk) path to the cookbook def initialize(cookbook_path) @cookbook_path = cookbook_path - @validated_files = PersistentSet.new - end + @chefignore ||= Chefignore.new(cookbook_path) - def chefignore - @chefignore ||= Chefignore.new(File.dirname(cookbook_path)) + @validated_files = PersistentSet.new end def remove_ignored_files(file_list) @@ -161,28 +163,127 @@ class Chef def validate_template(erb_file) Chef::Log.debug("Testing template #{erb_file} for syntax errors...") - result = shell_out("erubis -x #{erb_file} | ruby -c") + if validate_inline? + validate_erb_file_inline(erb_file) + else + validate_erb_via_subcommand(erb_file) + end + end + + def validate_ruby_file(ruby_file) + Chef::Log.debug("Testing #{ruby_file} for syntax errors...") + if validate_inline? + validate_ruby_file_inline(ruby_file) + else + validate_ruby_by_subcommand(ruby_file) + end + end + + # Whether or not we're running on a version of ruby that can support + # inline validation. Inline validation relies on the `RubyVM` features + # introduced with ruby 1.9, so 1.8 cannot be supported. + def validate_inline? + defined?(RubyVM::InstructionSequence) + end + + # Validate the ruby code in an erb template. Uses RubyVM to do syntax + # checking, so callers should check #validate_inline? before calling. + def validate_erb_file_inline(erb_file) + old_stderr = $stderr + + engine = Erubis::Eruby.new + engine.convert!(IO.read(erb_file)) + + ruby_code = engine.src + + # Even when we're compiling the code w/ RubyVM, syntax errors just + # print to $stderr. We want to capture this and handle the printing + # ourselves, so we must temporarily swap $stderr to capture the output. + tmp_stderr = $stderr = StringIO.new + + abs_path = File.expand_path(erb_file) + RubyVM::InstructionSequence.new(ruby_code, erb_file, abs_path, 0) + + true + rescue SyntaxError + $stderr = old_stderr + invalid_erb_file(erb_file, tmp_stderr.string) + false + ensure + # be paranoid about setting stderr back to the old value. + $stderr = old_stderr if defined?(old_stderr) && old_stderr + end + + # Validate the ruby code in an erb template. Pipes the output of `erubis + # -x` to `ruby -c`, so it works with any ruby version, but is much slower + # than the inline version. + # -- + # TODO: This can be removed when ruby 1.8 support is dropped. + def validate_erb_via_subcommand(erb_file) + result = shell_out("erubis -x #{erb_file} | #{ruby} -c") result.error! true rescue Mixlib::ShellOut::ShellCommandFailed + invalid_erb_file(erb_file, result.stderr) + false + end + + # Debug a syntax error in a template. + def invalid_erb_file(erb_file, error_message) file_relative_path = erb_file[/^#{Regexp.escape(cookbook_path+File::Separator)}(.*)/, 1] Chef::Log.fatal("Erb template #{file_relative_path} has a syntax error:") - result.stderr.each_line { |l| Chef::Log.fatal(l.chomp) } + error_message.each_line { |l| Chef::Log.fatal(l.chomp) } + nil + end + + # Validate the syntax of a ruby file. Uses (Ruby 1.9+ only) RubyVM to + # compile the code without evaluating it or spawning a new process. + # Callers should check #validate_inline? before calling. + def validate_ruby_file_inline(ruby_file) + # Even when we're compiling the code w/ RubyVM, syntax errors just + # print to $stderr. We want to capture this and handle the printing + # ourselves, so we must temporarily swap $stderr to capture the output. + old_stderr = $stderr + tmp_stderr = $stderr = StringIO.new + abs_path = File.expand_path(ruby_file) + file_content = IO.read(abs_path) + RubyVM::InstructionSequence.new(file_content, ruby_file, abs_path, 0) + true + rescue SyntaxError + $stderr = old_stderr + invalid_ruby_file(ruby_file, tmp_stderr.string) false + ensure + # be paranoid about setting stderr back to the old value. + $stderr = old_stderr if defined?(old_stderr) && old_stderr end - def validate_ruby_file(ruby_file) - Chef::Log.debug("Testing #{ruby_file} for syntax errors...") - result = shell_out("ruby -c #{ruby_file}") + # Validate the syntax of a ruby file by shelling out to `ruby -c`. Should + # work for all ruby versions, but is slower and uses more resources than + # the inline strategy. + def validate_ruby_by_subcommand(ruby_file) + result = shell_out("#{ruby} -c #{ruby_file}") result.error! true rescue Mixlib::ShellOut::ShellCommandFailed + invalid_ruby_file(ruby_file, result.stderr) + false + end + + # Debugs ruby syntax errors by printing the path to the file and any + # diagnostic info given in +error_message+ + def invalid_ruby_file(ruby_file, error_message) file_relative_path = ruby_file[/^#{Regexp.escape(cookbook_path+File::Separator)}(.*)/, 1] Chef::Log.fatal("Cookbook file #{file_relative_path} has a ruby syntax error:") - result.stderr.each_line { |l| Chef::Log.fatal(l.chomp) } + error_message.each_line { |l| Chef::Log.fatal(l.chomp) } false end + # Returns the full path to the running ruby. + def ruby + Gem.ruby + end + end end end diff --git a/lib/chef/dsl/reboot_pending.rb b/lib/chef/dsl/reboot_pending.rb new file mode 100644 index 0000000000..9f80d38c61 --- /dev/null +++ b/lib/chef/dsl/reboot_pending.rb @@ -0,0 +1,61 @@ +# Author:: Bryan McLellan <btm@loftninjas.org> +# Author:: Seth Chisamore <schisamo@opscode.com> +# Copyright:: Copyright (c) 2011,2014, Chef Software, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require 'chef/dsl/platform_introspection' +require 'chef/dsl/registry_helper' + +class Chef + module DSL + module RebootPending + + include Chef::DSL::RegistryHelper + include Chef::DSL::PlatformIntrospection + + # Returns true if the system needs a reboot or is expected to reboot + # Raises UnsupportedPlatform if this functionality isn't provided yet + def reboot_pending? + + if platform?("windows") + # PendingFileRenameOperations contains pairs (REG_MULTI_SZ) of filenames that cannot be updated + # due to a file being in use (usually a temporary file and a system file) + # \??\c:\temp\test.sys!\??\c:\winnt\system32\test.sys + # http://technet.microsoft.com/en-us/library/cc960241.aspx + registry_value_exists?('HKLM\SYSTEM\CurrentControlSet\Control\Session Manager', { :name => 'PendingFileRenameOperations' }) || + + # RebootRequired key contains Update IDs with a value of 1 if they require a reboot. + # The existence of RebootRequired alone is sufficient on my Windows 8.1 workstation in Windows Update + registry_key_exists?('HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Auto Update\RebootRequired') || + + # Vista + Server 2008 and newer may have reboots pending from CBS + registry_key_exists?('HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Component Based Servicing\RebootRequired') || + + # The mere existance of the UpdateExeVolatile key should indicate a pending restart for certain updates + # http://support.microsoft.com/kb/832475 + (registry_key_exists?('HKLM\SOFTWARE\Microsoft\Updates\UpdateExeVolatile') && + !registry_get_values('HKLM\SOFTWARE\Microsoft\Updates\UpdateExeVolatile').select { |v| v[:name] == "Flags" }[0].nil? && + [1,2,3].include?(registry_get_values('HKLM\SOFTWARE\Microsoft\Updates\UpdateExeVolatile').select { |v| v[:name] == "Flags" }[0][:data])) + elsif platform?("ubuntu") + # This should work for Debian as well if update-notifier-common happens to be installed. We need an API for that. + File.exists?('/var/run/reboot-required') + else + raise Chef::Exceptions::UnsupportedPlatform.new(node[:platform]) + end + end + end + end +end diff --git a/lib/chef/encrypted_data_bag_item.rb b/lib/chef/encrypted_data_bag_item.rb index b38a6f3512..b0d9337212 100644 --- a/lib/chef/encrypted_data_bag_item.rb +++ b/lib/chef/encrypted_data_bag_item.rb @@ -26,7 +26,7 @@ require 'open-uri' # all values, except for the value associated with the id key, have # been encrypted. # -# EncrypedDataBagItem can be used in recipes to decrypt data bag item +# EncryptedDataBagItem can be used in recipes to decrypt data bag item # members. # # Data bag item values are assumed to have been encrypted using the @@ -49,6 +49,22 @@ require 'open-uri' class Chef::EncryptedDataBagItem ALGORITHM = 'aes-256-cbc' + # + # === Synopsis + # + # EncryptedDataBagItem.new(hash, secret) + # + # === Args + # + # +enc_hash+:: + # The encrypted hash to be decrypted + # +secret+:: + # The raw secret key + # + # === Description + # + # Create a new encrypted data bag item for reading (decryption) + # def initialize(enc_hash, secret) @enc_hash = enc_hash @secret = secret @@ -82,6 +98,26 @@ class Chef::EncryptedDataBagItem end end + # + # === Synopsis + # + # EncryptedDataBagItem.load(data_bag, name, secret = nil) + # + # === Args + # + # +data_bag+:: + # The name of the data bag to fetch + # +name+:: + # The name of the data bag item to fetch + # +secret+:: + # The raw secret key. If the +secret+ is nil, the value of the file at + # +Chef::Config[:encrypted_data_bag_secret]+ is loaded. See +load_secret+ + # for more information. + # + # === Description + # + # Loads and decrypts the data bag item with the given name. + # def self.load(data_bag, name, secret = nil) raw_hash = Chef::DataBagItem.load(data_bag, name) secret = secret || self.load_secret diff --git a/lib/chef/exceptions.rb b/lib/chef/exceptions.rb index afd42885f9..bd99cb3ebd 100644 --- a/lib/chef/exceptions.rb +++ b/lib/chef/exceptions.rb @@ -50,7 +50,13 @@ class Chef class Override < RuntimeError; end class UnsupportedAction < RuntimeError; end class MissingLibrary < RuntimeError; end - class CannotDetermineNodeName < RuntimeError; end + + class CannotDetermineNodeName < RuntimeError + def initialize + super "Unable to determine node name: configure node_name or configure the system's hostname and fqdn" + end + end + class User < RuntimeError; end class Group < RuntimeError; end class Link < RuntimeError; end @@ -70,6 +76,7 @@ class Chef class CookbookNotFoundInRepo < ArgumentError; end class RecipeNotFound < ArgumentError; end class AttributeNotFound < RuntimeError; end + class MissingCookbookDependency < StandardError; end # CHEF-5120 class InvalidCommandOption < RuntimeError; end class CommandTimeout < RuntimeError; end class RequestedUIDUnavailable < RuntimeError; end @@ -309,5 +316,10 @@ class Chef end end + class UnsupportedPlatform < RuntimeError + def initialize(platform) + super "This functionality is not supported on platform #{platform}." + end + end end end diff --git a/lib/chef/formatters/error_descriptor.rb b/lib/chef/formatters/error_descriptor.rb index 3f0756df73..c2e656f167 100644 --- a/lib/chef/formatters/error_descriptor.rb +++ b/lib/chef/formatters/error_descriptor.rb @@ -31,7 +31,7 @@ class Chef end def section(heading, text) - @sections << {heading => text} + @sections << {heading => (text or "")} end def display(out) diff --git a/lib/chef/guard_interpreter/default_guard_interpreter.rb b/lib/chef/guard_interpreter/default_guard_interpreter.rb new file mode 100644 index 0000000000..df91c2b1ad --- /dev/null +++ b/lib/chef/guard_interpreter/default_guard_interpreter.rb @@ -0,0 +1,42 @@ +# +# Author:: Adam Edwards (<adamed@getchef.com>) +# Copyright:: Copyright (c) 2014 Chef Software, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +class Chef + class GuardInterpreter + class DefaultGuardInterpreter + include Chef::Mixin::ShellOut + + protected + + def initialize(command, opts) + @command = command + @command_opts = opts + end + + public + + def evaluate + shell_out(@command, @command_opts).status.success? + rescue Chef::Exceptions::CommandTimeout + Chef::Log.warn "Command '#{@command}' timed out" + false + end + end + end +end + diff --git a/lib/chef/guard_interpreter/resource_guard_interpreter.rb b/lib/chef/guard_interpreter/resource_guard_interpreter.rb new file mode 100644 index 0000000000..229a8502c7 --- /dev/null +++ b/lib/chef/guard_interpreter/resource_guard_interpreter.rb @@ -0,0 +1,122 @@ +# +# Author:: Adam Edwards (<adamed@getchef.com>) +# Copyright:: Copyright (c) 2014 Opscode, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require 'chef/guard_interpreter/default_guard_interpreter' + +class Chef + class GuardInterpreter + class ResourceGuardInterpreter < DefaultGuardInterpreter + + def initialize(parent_resource, command, opts, &block) + super(command, opts) + @parent_resource = parent_resource + @resource = get_interpreter_resource(parent_resource) + end + + def evaluate + # Add attributes inherited from the parent class + # to the resource + merge_inherited_attributes + + # Script resources have a code attribute, which is + # what is used to execute the command, so include + # that with attributes specified by caller in opts + block_attributes = @command_opts.merge({:code => @command}) + + # Handles cases like powershell_script where default + # attributes are different when used in a guard vs. not. For + # powershell_script in particular, this will go away when + # the one attribue that causes this changes its default to be + # the same after some period to prepare for deprecation + if @resource.class.respond_to?(:get_default_attributes) + block_attributes = @resource.class.send(:get_default_attributes, @command_opts).merge(block_attributes) + end + + resource_block = block_from_attributes(block_attributes) + evaluate_action(nil, &resource_block) + end + + protected + + def evaluate_action(action=nil, &block) + @resource.instance_eval(&block) + + run_action = action || @resource.action + + begin + @resource.run_action(run_action) + resource_updated = @resource.updated + rescue Mixlib::ShellOut::ShellCommandFailed + resource_updated = nil + end + + resource_updated + end + + def get_interpreter_resource(parent_resource) + if parent_resource.nil? || parent_resource.node.nil? + raise ArgumentError, "Node for guard resource parent must not be nil" + end + + resource_class = Chef::Resource.resource_for_node(parent_resource.guard_interpreter, parent_resource.node) + + if resource_class.nil? + raise ArgumentError, "Specified guard_interpreter resource #{parent_resource.guard_interpreter.to_s} unknown for this platform" + end + + if ! resource_class.ancestors.include?(Chef::Resource::Script) + raise ArgumentError, "Specified guard interpreter class #{resource_class} must be a kind of Chef::Resource::Script resource" + end + + empty_events = Chef::EventDispatch::Dispatcher.new + anonymous_run_context = Chef::RunContext.new(parent_resource.node, {}, empty_events) + interpreter_resource = resource_class.new('Guard resource', anonymous_run_context) + + interpreter_resource + end + + def block_from_attributes(attributes) + Proc.new do + attributes.keys.each do |attribute_name| + send(attribute_name, attributes[attribute_name]) if respond_to?(attribute_name) + end + end + end + + def merge_inherited_attributes + inherited_attributes = [] + + if @parent_resource.class.respond_to?(:guard_inherited_attributes) + inherited_attributes = @parent_resource.class.send(:guard_inherited_attributes) + end + + if inherited_attributes && !inherited_attributes.empty? + inherited_attributes.each do |attribute| + if @parent_resource.respond_to?(attribute) && @resource.respond_to?(attribute) + parent_value = @parent_resource.send(attribute) + child_value = @resource.send(attribute) + if parent_value || child_value + @resource.send(attribute, parent_value) + end + end + end + end + end + end + end +end diff --git a/lib/chef/http.rb b/lib/chef/http.rb index 78c47735d2..42b5decd6b 100644 --- a/lib/chef/http.rb +++ b/lib/chef/http.rb @@ -393,4 +393,3 @@ class Chef end end - diff --git a/lib/chef/http/decompressor.rb b/lib/chef/http/decompressor.rb index 78af47798c..e1d776da60 100644 --- a/lib/chef/http/decompressor.rb +++ b/lib/chef/http/decompressor.rb @@ -94,16 +94,21 @@ class Chef # object you can use to unzip/inflate a streaming response. def stream_response_handler(response) if gzip_disabled? + Chef::Log.debug "disable_gzip is set. \ + Not using #{response[CONTENT_ENCODING]} \ + and initializing noop stream deflator." NoopInflater.new else case response[CONTENT_ENCODING] when GZIP - Chef::Log.debug "decompressing gzip stream" + Chef::Log.debug "Initializing gzip stream deflator" GzipInflater.new when DEFLATE - Chef::Log.debug "decompressing inflate stream" + Chef::Log.debug "Initializing deflate stream deflator" DeflateInflater.new else + Chef::Log.debug "content_encoding = '#{response[CONTENT_ENCODING]}' \ + initializing noop stream deflator." NoopInflater.new end end @@ -137,5 +142,3 @@ class Chef end end end - - diff --git a/lib/chef/http/remote_request_id.rb b/lib/chef/http/remote_request_id.rb new file mode 100644 index 0000000000..6bec5dba4f --- /dev/null +++ b/lib/chef/http/remote_request_id.rb @@ -0,0 +1,46 @@ +# Author:: Prajakta Purohit (<prajakta@opscode.com>) +# Copyright:: Copyright (c) 2009, 2010, 2013, 2014 Opscode, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require 'chef/request_id' + +class Chef + class HTTP + class RemoteRequestID + + def initialize(opts={}) + end + + def handle_request(method, url, headers={}, data=false) + headers.merge!({'X-REMOTE-REQUEST-ID' => Chef::RequestID.instance.request_id}) + [method, url, headers, data] + end + + def handle_response(http_response, rest_request, return_value) + [http_response, rest_request, return_value] + end + + def stream_response_handler(response) + nil + end + + def handle_stream_complete(http_response, rest_request, return_value) + [http_response, rest_request, return_value] + end + + end + end +end diff --git a/lib/chef/http/simple.rb b/lib/chef/http/simple.rb index 0ecb28846c..d675a17ee8 100644 --- a/lib/chef/http/simple.rb +++ b/lib/chef/http/simple.rb @@ -11,6 +11,11 @@ class Chef use Decompressor use CookieManager + # ValidateContentLength should come after Decompressor + # because the order of middlewares is reversed when handling + # responses. + use ValidateContentLength + end end end diff --git a/lib/chef/http/validate_content_length.rb b/lib/chef/http/validate_content_length.rb index 49f1738d42..076194e31a 100644 --- a/lib/chef/http/validate_content_length.rb +++ b/lib/chef/http/validate_content_length.rb @@ -49,22 +49,20 @@ class Chef end def handle_response(http_response, rest_request, return_value) - unless http_response['content-length'] - Chef::Log.debug("HTTP server did not include a Content-Length header in response, cannot identify truncated downloads.") - return [http_response, rest_request, return_value] - end - validate(response_content_length(http_response), http_response.body.bytesize) + validate(http_response, http_response.body.bytesize) if http_response && http_response.body return [http_response, rest_request, return_value] end def handle_stream_complete(http_response, rest_request, return_value) - if http_response['content-length'].nil? - Chef::Log.debug("HTTP server did not include a Content-Length header in response, cannot idenfity streamed download.") - elsif @content_length_counter.nil? + if @content_length_counter.nil? Chef::Log.debug("No content-length information collected for the streamed download, cannot identify streamed download.") else - validate(response_content_length(http_response), @content_length_counter.content_length) + validate(http_response, @content_length_counter.content_length) end + + # Make sure the counter is reset since this object might get used + # again. See CHEF-5100 + @content_length_counter = nil return [http_response, rest_request, return_value] end @@ -73,7 +71,9 @@ class Chef end private + def response_content_length(response) + return nil if response['content-length'].nil? if response['content-length'].is_a?(Array) response['content-length'].first.to_i else @@ -81,12 +81,28 @@ class Chef end end - def validate(content_length, response_length) - Chef::Log.debug "Content-Length header = #{content_length}" - Chef::Log.debug "Response body length = #{response_length}" + def validate(http_response, response_length) + content_length = response_content_length(http_response) + transfer_encoding = http_response['transfer-encoding'] + content_encoding = http_response['content-encoding'] + + if content_length.nil? + Chef::Log.debug "HTTP server did not include a Content-Length header in response, cannot identify truncated downloads." + return true + end + + # if Transfer-Encoding is set the RFC states that we must ignore the Content-Length field + # CHEF-5041: some proxies uncompress gzip content, leave the incorrect content-length, but set the transfer-encoding field + unless transfer_encoding.nil? + Chef::Log.debug "Transfer-Encoding header is set, skipping Content-Length check." + return true + end + if response_length != content_length raise Chef::Exceptions::ContentLengthMismatch.new(response_length, content_length) end + + Chef::Log.debug "Content-Length validated correctly." true end end diff --git a/lib/chef/knife.rb b/lib/chef/knife.rb index eb2c321cab..5cbc968980 100644 --- a/lib/chef/knife.rb +++ b/lib/chef/knife.rb @@ -421,6 +421,7 @@ class Chef # Don't try to load a knife.rb if it wasn't specified. if config[:config_file] + Chef::Config.config_file = config[:config_file] fetcher = Chef::ConfigFetcher.new(config[:config_file], Chef::Config.config_file_jail) if fetcher.config_missing? ui.error("Specified config file #{config[:config_file]} does not exist#{Chef::Config.config_file_jail ? " or is not under config file jail #{Chef::Config.config_file_jail}" : ""}!") diff --git a/lib/chef/knife/bootstrap.rb b/lib/chef/knife/bootstrap.rb index 14dccb3892..a7c10fc608 100644 --- a/lib/chef/knife/bootstrap.rb +++ b/lib/chef/knife/bootstrap.rb @@ -201,7 +201,7 @@ class Chef $stdout.sync = true - ui.info("Bootstrapping Chef on #{ui.color(@node_name, :bold)}") + ui.info("Connecting to #{ui.color(@node_name, :bold)}") begin knife_ssh.run diff --git a/lib/chef/knife/bootstrap/README.md b/lib/chef/knife/bootstrap/README.md new file mode 100644 index 0000000000..13a0fe7ada --- /dev/null +++ b/lib/chef/knife/bootstrap/README.md @@ -0,0 +1,12 @@ +This directory contains bootstrap templates which can be used with the -d flag +to 'knife bootstrap' to install Chef in different ways. To simplify installation, +and reduce the matrix of common installation patterns to support, we have +standardized on the [Omnibus](https://github.com/opscode/omnibus-ruby) built installation +packages. + +The 'chef-full' template downloads a script which is used to determine the correct +Omnibus package for this system from the [Omnitruck](https://github.com/opscode/opscode-omnitruck) API. All other templates in this directory are deprecated and will be removed +in the future. + +You can still utilize custom bootstrap templates on your system if your installation +needs are unique. Additional information can be found on the [docs site](http://docs.opscode.com/knife_bootstrap.html#custom-templates).
\ No newline at end of file diff --git a/lib/chef/knife/bootstrap/chef-full.erb b/lib/chef/knife/bootstrap/chef-full.erb index 24ffca2c69..1d75117b72 100644 --- a/lib/chef/knife/bootstrap/chef-full.erb +++ b/lib/chef/knife/bootstrap/chef-full.erb @@ -23,6 +23,7 @@ install_sh="https://www.opscode.com/chef/install.sh" version_string="-v <%= chef_version %>" if ! exists /usr/bin/chef-client; then + echo "Installing Chef Client..." if exists wget; then bash <(wget <%= "--proxy=on " if knife_config[:bootstrap_proxy] %> ${install_sh} -O -) ${version_string} elif exists curl; then @@ -66,4 +67,6 @@ cat > /etc/chef/first-boot.json <<'EOP' <%= first_boot.to_json %> EOP +echo "Starting first Chef Client run..." + <%= start_chef %>' diff --git a/lib/chef/knife/client_bulk_delete.rb b/lib/chef/knife/client_bulk_delete.rb index 8bf2c2f116..f2be772759 100644 --- a/lib/chef/knife/client_bulk_delete.rb +++ b/lib/chef/knife/client_bulk_delete.rb @@ -27,6 +27,11 @@ class Chef require 'chef/json_compat' end + option :delete_validators, + :short => "-D", + :long => "--delete-validators", + :description => "Force deletion of clients if they're validators" + banner "knife client bulk delete REGEX (options)" def run @@ -38,28 +43,62 @@ class Chef matcher = /#{name_args[0]}/ clients_to_delete = {} + validators_to_delete = {} all_clients.each do |name, client| next unless name =~ matcher - clients_to_delete[client.name] = client + if client.validator + validators_to_delete[client.name] = client + else + clients_to_delete[client.name] = client + end end - if clients_to_delete.empty? + if clients_to_delete.empty? && validators_to_delete.empty? ui.info "No clients match the expression /#{name_args[0]}/" exit 0 end - ui.msg("The following clients will be deleted:") - ui.msg("") - ui.msg(ui.list(clients_to_delete.keys.sort, :columns_down)) - ui.msg("") - ui.confirm("Are you sure you want to delete these clients") + check_and_delete_validators(validators_to_delete) + check_and_delete_clients(clients_to_delete) + end - clients_to_delete.sort.each do |name, client| + def check_and_delete_validators(validators) + unless validators.empty? + unless config[:delete_validators] + ui.msg("Following clients are validators and will not be deleted.") + print_clients(validators) + ui.msg("You must specify --delete-validators to delete the validator clients") + else + ui.msg("The following validators will be deleted:") + print_clients(validators) + if ui.confirm_without_exit("Are you sure you want to delete these validators") + destroy_clients(validators) + end + end + end + end + + def check_and_delete_clients(clients) + unless clients.empty? + ui.msg("The following clients will be deleted:") + print_clients(clients) + ui.confirm("Are you sure you want to delete these clients") + destroy_clients(clients) + end + end + + def destroy_clients(clients) + clients.sort.each do |name, client| client.destroy ui.msg("Deleted client #{name}") end end + + def print_clients(clients) + ui.msg("") + ui.msg(ui.list(clients.keys.sort, :columns_down)) + ui.msg("") + end end end end - diff --git a/lib/chef/knife/client_create.rb b/lib/chef/knife/client_create.rb index 285254aef0..b2bac36081 100644 --- a/lib/chef/knife/client_create.rb +++ b/lib/chef/knife/client_create.rb @@ -38,6 +38,11 @@ class Chef :description => "Create the client as an admin", :boolean => true + option :validator, + :long => "--validator", + :description => "Create the client as a validator", + :boolean => true + banner "knife client create CLIENT (options)" def run @@ -52,6 +57,7 @@ class Chef client = Chef::ApiClient.new client.name(@client_name) client.admin(config[:admin]) + client.validator(config[:validator]) output = edit_data(client) diff --git a/lib/chef/knife/client_delete.rb b/lib/chef/knife/client_delete.rb index 6a6fae7ea0..1902145c8d 100644 --- a/lib/chef/knife/client_delete.rb +++ b/lib/chef/knife/client_delete.rb @@ -27,6 +27,11 @@ class Chef require 'chef/json_compat' end + option :delete_validators, + :short => "-D", + :long => "--delete-validators", + :description => "Force deletion of client if it's a validator" + banner "knife client delete CLIENT (options)" def run @@ -38,7 +43,16 @@ class Chef exit 1 end - delete_object(Chef::ApiClient, @client_name) + delete_object(Chef::ApiClient, @client_name, 'client') { + object = Chef::ApiClient.load(@client_name) + if object.validator + unless config[:delete_validators] + ui.fatal("You must specify --force to delete the validator client #{@client_name}") + exit 2 + end + end + object.destroy + } end end diff --git a/lib/chef/knife/cookbook_bulk_delete.rb b/lib/chef/knife/cookbook_bulk_delete.rb index f8ad74d856..65fa888486 100644 --- a/lib/chef/knife/cookbook_bulk_delete.rb +++ b/lib/chef/knife/cookbook_bulk_delete.rb @@ -49,7 +49,7 @@ class Chef ui.msg "" unless config[:yes] - ui.confirm("Do you really want to delete these cookbooks? (Y/N) ", false) + ui.confirm("Do you really want to delete these cookbooks") if config[:purge] ui.msg("Files that are common to multiple cookbooks are shared, so purging the files may break other cookbooks.") diff --git a/lib/chef/knife/cookbook_upload.rb b/lib/chef/knife/cookbook_upload.rb index a882cd7109..9d6e0d438d 100644 --- a/lib/chef/knife/cookbook_upload.rb +++ b/lib/chef/knife/cookbook_upload.rb @@ -93,6 +93,7 @@ class Chef end assert_environment_valid! + warn_about_cookbook_shadowing version_constraints_to_update = {} upload_failures = 0 upload_ok = 0 @@ -139,6 +140,7 @@ class Chef end end + upload_failures += @name_args.length - @cookbooks_to_upload.length if upload_failures == 0 @@ -199,6 +201,10 @@ class Chef end def warn_about_cookbook_shadowing + # because cookbooks are lazy-loaded, we have to force the loader + # to load the cookbooks the user intends to upload here: + cookbooks_to_upload + unless cookbook_repo.merged_cookbooks.empty? ui.warn "* " * 40 ui.warn(<<-WARNING) @@ -257,14 +263,18 @@ WARNING end def check_for_dependencies!(cookbook) - # for each dependency, check if the version is on the server, or + # for all dependencies, check if the version is on the server, or # the version is in the cookbooks being uploaded. If not, exit and warn the user. - cookbook.metadata.dependencies.each do |cookbook_name, version| - unless check_server_side_cookbooks(cookbook_name, version) || check_uploading_cookbooks(cookbook_name, version) - ui.error "Cookbook #{cookbook.name} depends on cookbook '#{cookbook_name}' version '#{version}'," - ui.error "which is not currently being uploaded and cannot be found on the server." - exit 1 - end + missing_dependencies = cookbook.metadata.dependencies.reject do |cookbook_name, version| + check_server_side_cookbooks(cookbook_name, version) || check_uploading_cookbooks(cookbook_name, version) + end + + unless missing_dependencies.empty? + missing_cookbook_names = missing_dependencies.map { |cookbook_name, version| "'#{cookbook_name}' version '#{version}'"} + ui.error "Cookbook #{cookbook.name} depends on cookbooks which are not currently" + ui.error "being uploaded and cannot be found on the server." + ui.error "The missing cookbook(s) are: #{missing_cookbook_names.join(', ')}" + exit 1 end end diff --git a/lib/chef/knife/core/bootstrap_context.rb b/lib/chef/knife/core/bootstrap_context.rb index e1ad606c80..dc10bbb3d3 100644 --- a/lib/chef/knife/core/bootstrap_context.rb +++ b/lib/chef/knife/core/bootstrap_context.rb @@ -62,7 +62,6 @@ class Chef def config_content client_rb = <<-CONFIG -log_level :auto log_location STDOUT chef_server_url "#{@chef_config[:chef_server_url]}" validation_client_name "#{@chef_config[:validation_client_name]}" @@ -93,6 +92,7 @@ CONFIG # If the user doesn't have a client path configure, let bash use the PATH for what it was designed for client_path = @chef_config[:chef_client_path] || 'chef-client' s = "#{client_path} -j /etc/chef/first-boot.json" + s << ' -l debug' if @config[:verbosity] and @config[:verbosity] >= 2 s << " -E #{bootstrap_environment}" if chef_version.to_f != 0.9 # only use the -E option on Chef 0.10+ s end diff --git a/lib/chef/knife/core/ui.rb b/lib/chef/knife/core/ui.rb index dfa8c11644..ff2545cfed 100644 --- a/lib/chef/knife/core/ui.rb +++ b/lib/chef/knife/core/ui.rb @@ -205,24 +205,61 @@ class Chef output(format_for_display(object)) if config[:print_after] end - def confirm(question, append_instructions=true) + def confirmation_instructions(default_choice) + case default_choice + when true + '? (Y/n)' + when false + '? (y/N)' + else + '? (Y/N)' + end + end + + # See confirm method for argument information + def confirm_without_exit(question, append_instructions=true, default_choice=nil) return true if config[:yes] stdout.print question - stdout.print "? (Y/N) " if append_instructions + stdout.print confirmation_instructions(default_choice) if append_instructions + answer = stdin.readline answer.chomp! + case answer when "Y", "y" true when "N", "n" self.msg("You said no, so I'm done here.") - exit 3 + false + when "" + unless default_choice.nil? + default_choice + else + self.msg("I have no idea what to do with '#{answer}'") + self.msg("Just say Y or N, please.") + confirm_without_exit(question, append_instructions, default_choice) + end else - self.msg("I have no idea what to do with #{answer}") + self.msg("I have no idea what to do with '#{answer}'") self.msg("Just say Y or N, please.") - confirm(question) + confirm_without_exit(question, append_instructions, default_choice) + end + end + + # + # Not the ideal signature for a function but we need to stick with this + # for now until we get a chance to break our API in Chef 12. + # + # question => Question to print before asking for confirmation + # append_instructions => Should print '? (Y/N)' as instructions + # default_choice => Set to true for 'Y', and false for 'N' as default answer + # + def confirm(question, append_instructions=true, default_choice=nil) + unless confirm_without_exit(question, append_instructions, default_choice) + exit 3 end + true end end diff --git a/lib/chef/knife/node_run_list_add.rb b/lib/chef/knife/node_run_list_add.rb index dcd41ae997..519c280400 100644 --- a/lib/chef/knife/node_run_list_add.rb +++ b/lib/chef/knife/node_run_list_add.rb @@ -34,6 +34,11 @@ class Chef :long => "--after ITEM", :description => "Place the ENTRY in the run list after ITEM" + option :before, + :short => "-b ITEM", + :long => "--before ITEM", + :description => "Place the ENTRY in the run list before ITEM" + def run node = Chef::Node.load(@name_args[0]) if @name_args.size > 2 @@ -46,7 +51,18 @@ class Chef entries = @name_args[1].split(',').map { |e| e.strip } end - add_to_run_list(node, entries, config[:after]) + if config[:after] && config[:before] + ui.fatal("You cannot specify both --before and --after!") + exit 1 + end + + if config[:after] + add_to_run_list_after(node, entries, config[:after]) + elsif config[:before] + add_to_run_list_before(node, entries, config[:before]) + else + add_to_run_list_after(node, entries) + end node.save @@ -55,7 +71,9 @@ class Chef output(format_for_display(node)) end - def add_to_run_list(node, entries, after=nil) + private + + def add_to_run_list_after(node, entries, after=nil) if after nlist = [] node.run_list.each do |entry| @@ -70,6 +88,17 @@ class Chef end end + def add_to_run_list_before(node, entries, before) + nlist = [] + node.run_list.each do |entry| + if entry == before + entries.each { |e| nlist << e } + end + nlist << entry + end + node.run_list.reset!(nlist) + end + end end end diff --git a/lib/chef/knife/raw.rb b/lib/chef/knife/raw.rb index 2756de1a5a..954d46beee 100644 --- a/lib/chef/knife/raw.rb +++ b/lib/chef/knife/raw.rb @@ -42,6 +42,7 @@ class Chef use Chef::HTTP::CookieManager use Chef::HTTP::Decompressor use Chef::HTTP::Authenticator + use Chef::HTTP::RemoteRequestID end def run diff --git a/lib/chef/knife/ssh.rb b/lib/chef/knife/ssh.rb index 83c1735b4a..d32b3309ed 100644 --- a/lib/chef/knife/ssh.rb +++ b/lib/chef/knife/ssh.rb @@ -114,7 +114,7 @@ class Chef end case config[:on_error] when :skip - ui.warn "Failed to connect to #{node_name} -- #{$!.class.name}: #{$!.message}" + ui.warn "Failed to connect to #{server.host} -- #{$!.class.name}: #{$!.message}" $!.backtrace.each { |l| Chef::Log.debug(l) } when :raise #Net::SSH::Multi magic to force exception to be re-raised. @@ -142,31 +142,9 @@ class Chef end def configure_session - list = case config[:manual] - when true - @name_args[0].split(" ") - when false - r = Array.new - q = Chef::Search::Query.new - @action_nodes = q.search(:node, @name_args[0])[0] - @action_nodes.each do |item| - # we should skip the loop to next iteration if the item returned by the search is nil - next if item.nil? - # if a command line attribute was not passed, and we have a cloud public_hostname, use that. - # see #configure_attribute for the source of config[:attribute] and config[:override_attribute] - if !config[:override_attribute] && item[:cloud] and item[:cloud][:public_hostname] - i = item[:cloud][:public_hostname] - elsif config[:override_attribute] - i = extract_nested_value(item, config[:override_attribute]) - else - i = extract_nested_value(item, config[:attribute]) - end - # next if we couldn't find the specified attribute in the returned node object - next if i.nil? - r.push(i) - end - r - end + list = config[:manual] ? + @name_args[0].split(" ") : + search_nodes if list.length == 0 if @action_nodes.length == 0 ui.fatal("No nodes returned from search!") @@ -180,21 +158,54 @@ class Chef session_from_list(list) end + def search_nodes + list = Array.new + query = Chef::Search::Query.new + @action_nodes = query.search(:node, @name_args[0])[0] + @action_nodes.each do |item| + # we should skip the loop to next iteration if the item + # returned by the search is nil + next if item.nil? + # if a command line attribute was not passed, and we have a + # cloud public_hostname, use that. see #configure_attribute + # for the source of config[:attribute] and + # config[:override_attribute] + if config[:override_attribute] + host = extract_nested_value(item, config[:override_attribute]) + elsif item[:cloud] && item[:cloud][:public_hostname] + host = item[:cloud][:public_hostname] + else + host = extract_nested_value(item, config[:attribute]) + end + # next if we couldn't find the specified attribute in the + # returned node object + next if host.nil? + ssh_port = item[:cloud].nil? ? nil : item[:cloud][:public_ssh_port] + srv = [host, ssh_port] + list.push(srv) + end + list + end + def session_from_list(list) list.each do |item| - Chef::Log.debug("Adding #{item}") + host, ssh_port = item + Chef::Log.debug("Adding #{host}") session_opts = {} - ssh_config = Net::SSH.configuration_for(item) + ssh_config = Net::SSH.configuration_for(host) # Chef::Config[:knife][:ssh_user] is parsed in #configure_user and written to config[:ssh_user] user = config[:ssh_user] || ssh_config[:user] - hostspec = user ? "#{user}@#{item}" : item + hostspec = user ? "#{user}@#{host}" : host session_opts[:keys] = File.expand_path(config[:identity_file]) if config[:identity_file] session_opts[:keys_only] = true if config[:identity_file] session_opts[:password] = config[:ssh_password] if config[:ssh_password] session_opts[:forward_agent] = config[:forward_agent] - session_opts[:port] = config[:ssh_port] || Chef::Config[:knife][:ssh_port] || ssh_config[:port] + session_opts[:port] = config[:ssh_port] || + ssh_port || # Use cloud port if available + Chef::Config[:knife][:ssh_port] || + ssh_config[:port] session_opts[:logger] = Chef::Log.logger if Chef::Log.level == :debug if !config[:host_key_verify] @@ -204,7 +215,7 @@ class Chef session.use(hostspec, session_opts) - @longest = item.length if item.length > @longest + @longest = host.length if host.length > @longest end session @@ -510,6 +521,8 @@ class Chef end end + private :search_nodes + end end end diff --git a/lib/chef/knife/ssl_check.rb b/lib/chef/knife/ssl_check.rb new file mode 100644 index 0000000000..e98469d5aa --- /dev/null +++ b/lib/chef/knife/ssl_check.rb @@ -0,0 +1,213 @@ +# +# Author:: Daniel DeLeo (<dan@getchef.com>) +# Copyright:: Copyright (c) 2014 Chef Software, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require 'chef/knife' +require 'chef/config' + +class Chef + class Knife + class SslCheck < Chef::Knife + + deps do + require 'pp' + require 'socket' + require 'uri' + require 'chef/http/ssl_policies' + require 'openssl' + end + + banner "knife ssl check [URL] (options)" + + def initialize(*args) + @host = nil + @verify_peer_socket = nil + @ssl_policy = HTTP::DefaultSSLPolicy + super + end + + def uri + @uri ||= begin + Chef::Log.debug("Checking SSL cert on #{given_uri}") + URI.parse(given_uri) + end + end + + def given_uri + (name_args[0] or Chef::Config.chef_server_url) + end + + def host + uri.host + end + + def port + uri.port + end + + def validate_uri + unless host && port + invalid_uri! + end + rescue URI::Error + invalid_uri! + end + + def invalid_uri! + ui.error("Given URI: `#{given_uri}' is invalid") + show_usage + exit 1 + end + + + def verify_peer_socket + @verify_peer_socket ||= begin + tcp_connection = TCPSocket.new(host, port) + OpenSSL::SSL::SSLSocket.new(tcp_connection, verify_peer_ssl_context) + end + end + + def verify_peer_ssl_context + @verify_peer_ssl_context ||= begin + verify_peer_context = OpenSSL::SSL::SSLContext.new + @ssl_policy.apply_to(verify_peer_context) + verify_peer_context.verify_mode = OpenSSL::SSL::VERIFY_PEER + verify_peer_context + end + end + + def noverify_socket + @noverify_socket ||= begin + tcp_connection = TCPSocket.new(host, port) + OpenSSL::SSL::SSLSocket.new(tcp_connection, noverify_peer_ssl_context) + end + end + + def noverify_peer_ssl_context + @noverify_peer_ssl_context ||= begin + noverify_peer_context = OpenSSL::SSL::SSLContext.new + @ssl_policy.apply_to(noverify_peer_context) + noverify_peer_context.verify_mode = OpenSSL::SSL::VERIFY_NONE + noverify_peer_context + end + end + + def verify_cert + ui.msg("Connecting to host #{host}:#{port}") + verify_peer_socket.connect + true + rescue OpenSSL::SSL::SSLError => e + ui.error "The SSL certificate of #{host} could not be verified" + Chef::Log.debug e.message + debug_invalid_cert + false + end + + def verify_cert_host + verify_peer_socket.post_connection_check(host) + true + rescue OpenSSL::SSL::SSLError => e + ui.error "The SSL cert is signed by a trusted authority but is not valid for the given hostname" + Chef::Log.debug(e) + debug_invalid_host + false + end + + def debug_invalid_cert + noverify_socket.connect + issuer_info = noverify_socket.peer_cert.issuer + ui.msg("Certificate issuer data: #{issuer_info}") + + ui.msg("\n#{ui.color("Configuration Info:", :bold)}\n\n") + debug_ssl_settings + debug_chef_ssl_config + + ui.err(<<-ADVICE) + +#{ui.color("TO FIX THIS ERROR:", :bold)} + +If the server you are connecting to uses a self-signed certificate, you must +configure chef to trust that server's certificate. + +By default, the certificate is stored in the following location on the host +where your chef-server runs: + + /var/opt/chef-server/nginx/ca/SERVER_HOSTNAME.crt + +Copy that file to you trusted_certs_dir (currently: #{configuration.trusted_certs_dir}) +using SSH/SCP or some other secure method, then re-run this command to confirm +that the server's certificate is now trusted. + +ADVICE + end + + def debug_invalid_host + noverify_socket.connect + subject = noverify_socket.peer_cert.subject + cn_field_tuple = subject.to_a.find {|field| field[0] == "CN" } + cn = cn_field_tuple[1] + + ui.error("You are attempting to connect to: '#{host}'") + ui.error("The server's certificate belongs to '#{cn}'") + ui.err(<<-ADVICE) + +#{ui.color("TO FIX THIS ERROR:", :bold)} + +The solution for this issue depends on your networking configuration. If you +are able to connect to this server using the hostname #{cn} +instead of #{host}, then you can resolve this issue by updating chef_server_url +in your configuration file. + +If you are not able to connect to the server using the hostname #{cn} +you will have to update the certificate on the server to use the correct hostname. +ADVICE + end + + def debug_ssl_settings + ui.err "OpenSSL Configuration:" + ui.err "* Version: #{OpenSSL::OPENSSL_VERSION}" + ui.err "* Certificate file: #{OpenSSL::X509::DEFAULT_CERT_FILE}" + ui.err "* Certificate directory: #{OpenSSL::X509::DEFAULT_CERT_DIR}" + end + + def debug_chef_ssl_config + ui.err "Chef SSL Configuration:" + ui.err "* ssl_ca_path: #{configuration.ssl_ca_path.inspect}" + ui.err "* ssl_ca_file: #{configuration.ssl_ca_file.inspect}" + ui.err "* trusted_certs_dir: #{configuration.trusted_certs_dir.inspect}" + end + + def configuration + Chef::Config + end + + def run + validate_uri + if verify_cert && verify_cert_host + ui.msg "Successfully verified certificates from `#{host}'" + else + exit 1 + end + end + + end + end +end + + + + diff --git a/lib/chef/knife/ssl_fetch.rb b/lib/chef/knife/ssl_fetch.rb new file mode 100644 index 0000000000..5626a5610d --- /dev/null +++ b/lib/chef/knife/ssl_fetch.rb @@ -0,0 +1,145 @@ +# +# Author:: Daniel DeLeo (<dan@getchef.com>) +# Copyright:: Copyright (c) 2014 Chef Software, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require 'chef/knife/ssl_fetch' +require 'chef/config' + +class Chef + class Knife + class SslFetch < Chef::Knife + + deps do + require 'pp' + require 'socket' + require 'uri' + require 'openssl' + end + + banner "knife ssl fetch [URL] (options)" + + def initialize(*args) + super + @uri = nil + end + + def uri + @uri ||= begin + Chef::Log.debug("Checking SSL cert on #{given_uri}") + URI.parse(given_uri) + end + end + + def given_uri + (name_args[0] or Chef::Config.chef_server_url) + end + + def host + uri.host + end + + def port + uri.port + end + + def validate_uri + unless host && port + invalid_uri! + end + rescue URI::Error + invalid_uri! + end + + def invalid_uri! + ui.error("Given URI: `#{given_uri}' is invalid") + show_usage + exit 1 + end + + def remote_cert_chain + tcp_connection = TCPSocket.new(host, port) + shady_ssl_connection = OpenSSL::SSL::SSLSocket.new(tcp_connection, noverify_peer_ssl_context) + shady_ssl_connection.connect + shady_ssl_connection.peer_cert_chain + end + + def noverify_peer_ssl_context + @noverify_peer_ssl_context ||= begin + noverify_peer_context = OpenSSL::SSL::SSLContext.new + noverify_peer_context.verify_mode = OpenSSL::SSL::VERIFY_NONE + noverify_peer_context + end + end + + + def cn_of(certificate) + subject = certificate.subject + cn_field_tuple = subject.to_a.find {|field| field[0] == "CN" } + cn_field_tuple[1] + end + + # Convert the CN of a certificate into something that will work well as a + # filename. To do so, all `*` characters are converted to the string + # "wildcard" and then all characters other than alphanumeric and hypen + # characters are converted to underscores. + # NOTE: There is some confustion about what the CN will contain when + # using internationalized domain names. RFC 6125 mandates that the ascii + # representation be used, but it is not clear whether this is followed in + # practice. + # https://tools.ietf.org/html/rfc6125#section-6.4.2 + def normalize_cn(cn) + cn.gsub("*", "wildcard").gsub(/[^[:alnum:]\-]/, '_') + end + + def configuration + Chef::Config + end + + def trusted_certs_dir + configuration.trusted_certs_dir + end + + def write_cert(cert) + FileUtils.mkdir_p(trusted_certs_dir) + cn = cn_of(cert) + filename = File.join(trusted_certs_dir, "#{normalize_cn(cn)}.crt") + ui.msg("Adding certificate for #{cn} in #{filename}") + File.open(filename, File::CREAT|File::TRUNC|File::RDWR, 0644) do |f| + f.print(cert.to_s) + end + end + + def run + validate_uri + ui.warn(<<-TRUST_TRUST) +Certificates from #{host} will be fetched and placed in your trusted_cert +directory (#{trusted_certs_dir}). + +Knife has no means to verify these are the correct certificates. You should +verify the authenticity of these certificates after downloading. + +TRUST_TRUST + remote_cert_chain.each do |cert| + write_cert(cert) + end + end + + + end + end +end + diff --git a/lib/chef/mixin/deep_merge.rb b/lib/chef/mixin/deep_merge.rb index ad3e5803fd..a8a4737758 100644 --- a/lib/chef/mixin/deep_merge.rb +++ b/lib/chef/mixin/deep_merge.rb @@ -111,7 +111,13 @@ class Chef end # deep_merge! def hash_only_merge(merge_onto, merge_with) - hash_only_merge!(merge_onto.dup, merge_with.dup) + hash_only_merge!(safe_dup(merge_onto), safe_dup(merge_with)) + end + + def safe_dup(thing) + thing.dup + rescue TypeError + thing end # Deep merge without Array merge. @@ -122,7 +128,11 @@ class Chef # If there are two Hashes, recursively merge. if merge_onto.kind_of?(Hash) && merge_with.kind_of?(Hash) merge_with.each do |key, merge_with_value| - merge_onto[key] = hash_only_merge!(merge_onto[key], merge_with_value) + merge_onto[key] = if merge_onto.has_key?(key) + hash_only_merge(merge_onto[key], merge_with_value) + else + merge_with_value + end end merge_onto @@ -158,11 +168,9 @@ class Chef end def deep_merge(source, dest) - deep_merge!(source.dup, dest.dup) + deep_merge!(safe_dup(source), safe_dup(dest)) end end end end - - diff --git a/lib/chef/mixin/shell_out.rb b/lib/chef/mixin/shell_out.rb index f0c2ba2000..56b02d780f 100644 --- a/lib/chef/mixin/shell_out.rb +++ b/lib/chef/mixin/shell_out.rb @@ -33,9 +33,7 @@ class Chef def shell_out(*command_args) cmd = Mixlib::ShellOut.new(*run_command_compatible_options(command_args)) - if STDOUT.tty? && !Chef::Config[:daemon] && Chef::Log.debug? - cmd.live_stream = STDOUT - end + cmd.live_stream = io_for_live_stream cmd.run_command cmd end @@ -73,6 +71,14 @@ class Chef def deprecate_option(old_option, new_option) Chef::Log.logger.warn "DEPRECATION: Chef::Mixin::ShellOut option :#{old_option} is deprecated. Use :#{new_option}" end + + def io_for_live_stream + if STDOUT.tty? && !Chef::Config[:daemon] && Chef::Log.debug? + STDOUT + else + nil + end + end end end end diff --git a/lib/chef/node.rb b/lib/chef/node.rb index 69e5e05b01..4992ec2430 100644 --- a/lib/chef/node.rb +++ b/lib/chef/node.rb @@ -42,7 +42,7 @@ class Chef def_delegators :attributes, :keys, :each_key, :each_value, :key?, :has_key? - attr_accessor :recipe_list, :run_state, :run_list + attr_accessor :recipe_list, :run_state, :override_runlist # RunContext will set itself as run_context via this setter when # initialized. This is needed so DSL::IncludeAttribute (in particular, @@ -63,7 +63,8 @@ class Chef @name = nil @chef_environment = '_default' - @run_list = Chef::RunList.new + @primary_runlist = Chef::RunList.new + @override_runlist = Chef::RunList.new @attributes = Chef::Node::Attribute.new({}, {}, {}, {}) @@ -259,10 +260,28 @@ class Chef run_list.include?("role[#{role_name}]") end + def primary_runlist + @primary_runlist + end + + def override_runlist(*args) + args.length > 0 ? @override_runlist.reset!(args) : @override_runlist + end + + def select_run_list + @override_runlist.empty? ? @primary_runlist : @override_runlist + end + # Returns an Array of roles and recipes, in the order they will be applied. # If you call it with arguments, they will become the new list of roles and recipes. def run_list(*args) - args.length > 0 ? @run_list.reset!(args) : @run_list + rl = select_run_list + args.length > 0 ? rl.reset!(args) : rl + end + + def run_list=(list) + rl = select_run_list + rl = list end # Returns true if this Node expects a given role, false if not. @@ -312,7 +331,7 @@ class Chef if attrs.key?("recipes") || attrs.key?("run_list") raise Chef::Exceptions::AmbiguousRunlistSpecification, "please set the node's run list using the 'run_list' attribute only." end - Chef::Log.info("Setting the run_list to #{new_run_list.inspect} from JSON") + Chef::Log.info("Setting the run_list to #{new_run_list.inspect} from CLI options") run_list(new_run_list) end attrs @@ -410,7 +429,7 @@ class Chef "default" => attributes.combined_default, "override" => attributes.combined_override, #Render correctly for run_list items so malformed json does not result - "run_list" => run_list.run_list.map { |item| item.to_s } + "run_list" => @primary_runlist.run_list.map { |item| item.to_s } } result end diff --git a/lib/chef/node/attribute_collections.rb b/lib/chef/node/attribute_collections.rb index d5d496fd60..f09b02b106 100644 --- a/lib/chef/node/attribute_collections.rb +++ b/lib/chef/node/attribute_collections.rb @@ -76,8 +76,15 @@ class Chef super(data) end + # For elements like Fixnums, true, nil... + def safe_dup(e) + e.dup + rescue TypeError + e + end + def dup - Array.new(map {|e| e.dup}) + Array.new(map {|e| safe_dup(e)}) end end diff --git a/lib/chef/node/immutable_collections.rb b/lib/chef/node/immutable_collections.rb index f5b3a5121d..3558ba3a86 100644 --- a/lib/chef/node/immutable_collections.rb +++ b/lib/chef/node/immutable_collections.rb @@ -85,8 +85,31 @@ class Chef METHOD_DEFN end + # For elements like Fixnums, true, nil... + def safe_dup(e) + e.dup + rescue TypeError + e + end + def dup - Array.new(map {|e| e.dup }) + Array.new(map {|e| safe_dup(e)}) + end + + def to_a + a = Array.new + each do |v| + a << + case v + when ImmutableArray + v.to_a + when ImmutableMash + v.to_hash + else + v + end + end + a end end @@ -180,6 +203,22 @@ class Chef Mash.new(self) end + def to_hash + h = Hash.new + each_pair do |k, v| + h[k] = + case v + when ImmutableMash + v.to_hash + when ImmutableArray + v.to_a + else + v + end + end + h + end + end end diff --git a/lib/chef/platform/provider_mapping.rb b/lib/chef/platform/provider_mapping.rb index 92a7278d2f..a773da550e 100644 --- a/lib/chef/platform/provider_mapping.rb +++ b/lib/chef/platform/provider_mapping.rb @@ -180,6 +180,7 @@ class Chef :package => Chef::Provider::Package::Zypper, :group => Chef::Provider::Group::Suse }, + # Only OpenSuSE 12.3+ should use the Usermod group provider: ">= 12.3" => { :group => Chef::Provider::Group::Usermod } @@ -190,19 +191,6 @@ class Chef :cron => Chef::Provider::Cron, :package => Chef::Provider::Package::Zypper, :group => Chef::Provider::Group::Suse - }, - ############################################### - # TODO: Remove this after ohai update is released. - # Only OpenSuSE 12.3+ should use the Usermod group provider: - # Ohai before OHAI-339 is applied reports both OpenSuSE and SuSE - # Enterprise as "suse", Ohai after OHAI-339 will report OpenSuSE as - # "opensuse". - # - # In order to support OpenSuSE both before and after the Ohai - # change, I'm leaving this here. It needs to get removed before - # SuSE enterprise 12.3 ships. - ">= 12.3" => { - :group => Chef::Provider::Group::Usermod } }, :oracle => { @@ -222,6 +210,15 @@ class Chef :ifconfig => Chef::Provider::Ifconfig::Redhat } }, + :ibm_powerkvm => { + :default => { + :service => Chef::Provider::Service::Redhat, + :cron => Chef::Provider::Cron, + :package => Chef::Provider::Package::Yum, + :mdadm => Chef::Provider::Mdadm, + :ifconfig => Chef::Provider::Ifconfig::Redhat + } + }, :gentoo => { :default => { :package => Chef::Provider::Package::Portage, @@ -233,7 +230,7 @@ class Chef :arch => { :default => { :package => Chef::Provider::Package::Pacman, - :service => Chef::Provider::Service::Arch, + :service => Chef::Provider::Service::Systemd, :cron => Chef::Provider::Cron, :mdadm => Chef::Provider::Mdadm } @@ -244,7 +241,9 @@ class Chef :service => Chef::Provider::Service::Windows, :user => Chef::Provider::User::Windows, :group => Chef::Provider::Group::Windows, - :mount => Chef::Provider::Mount::Windows + :mount => Chef::Provider::Mount::Windows, + :batch => Chef::Provider::Batch, + :powershell_script => Chef::Provider::PowershellScript } }, :mingw32 => { @@ -253,7 +252,9 @@ class Chef :service => Chef::Provider::Service::Windows, :user => Chef::Provider::User::Windows, :group => Chef::Provider::Group::Windows, - :mount => Chef::Provider::Mount::Windows + :mount => Chef::Provider::Mount::Windows, + :batch => Chef::Provider::Batch, + :powershell_script => Chef::Provider::PowershellScript } }, :windows => { @@ -262,7 +263,9 @@ class Chef :service => Chef::Provider::Service::Windows, :user => Chef::Provider::User::Windows, :group => Chef::Provider::Group::Windows, - :mount => Chef::Provider::Mount::Windows + :mount => Chef::Provider::Mount::Windows, + :batch => Chef::Provider::Batch, + :powershell_script => Chef::Provider::PowershellScript } }, :solaris => {}, @@ -307,7 +310,7 @@ class Chef :group => Chef::Provider::Group::Usermod, :user => Chef::Provider::User::Solaris, }, - ">= 5.9" => { + "< 5.11" => { :service => Chef::Provider::Service::Solaris, :package => Chef::Provider::Package::Solaris, :cron => Chef::Provider::Cron::Solaris, diff --git a/lib/chef/platform/query_helpers.rb b/lib/chef/platform/query_helpers.rb index 028a220a5d..f9f7af0343 100644 --- a/lib/chef/platform/query_helpers.rb +++ b/lib/chef/platform/query_helpers.rb @@ -30,11 +30,19 @@ class Chef def windows_server_2003? return false unless windows? - require 'ruby-wmi' + # CHEF-4888: Work around ruby #2618, expected to be fixed in Ruby 2.1.0 + # https://github.com/ruby/ruby/commit/588504b20f5cc880ad51827b93e571e32446e5db + # https://github.com/ruby/ruby/commit/27ed294c7134c0de582007af3c915a635a6506cd + WIN32OLE.ole_initialize + host = WMI::Win32_OperatingSystem.find(:first) - (host.version && host.version.start_with?("5.2")) + is_server_2003 = (host.version && host.version.start_with?("5.2")) + + WIN32OLE.ole_uninitialize + + is_server_2003 end end diff --git a/lib/chef/policy_builder/expand_node_object.rb b/lib/chef/policy_builder/expand_node_object.rb index 38b8b7551b..269e722797 100644 --- a/lib/chef/policy_builder/expand_node_object.rb +++ b/lib/chef/policy_builder/expand_node_object.rb @@ -40,7 +40,6 @@ class Chef attr_reader :ohai_data attr_reader :json_attribs attr_reader :override_runlist - attr_reader :original_runlist attr_reader :run_context attr_reader :run_list_expansion @@ -52,7 +51,6 @@ class Chef @events = events @node = nil - @original_runlist = nil @run_list_expansion = nil end @@ -190,7 +188,7 @@ class Chef # override_runlist was provided. Chef::Client uses this to decide whether # to do the final node save at the end of the run or not. def temporary_policy? - !!@original_runlist + !node.override_runlist.empty? end ######################################## @@ -200,10 +198,9 @@ class Chef def setup_run_list_override runlist_override_sanity_check! unless(override_runlist.empty?) - @original_runlist = node.run_list.run_list_items.dup - node.run_list(*override_runlist) + node.override_runlist(*override_runlist) Chef::Log.warn "Run List override has been provided." - Chef::Log.warn "Original Run List: [#{original_runlist.join(', ')}]" + Chef::Log.warn "Original Run List: [#{node.primary_runlist}]" Chef::Log.warn "Overridden Run List: [#{node.run_list}]" end end diff --git a/lib/chef/provider/cron.rb b/lib/chef/provider/cron.rb index 87452b4872..1be15f9f5f 100644 --- a/lib/chef/provider/cron.rb +++ b/lib/chef/provider/cron.rb @@ -25,11 +25,14 @@ class Chef class Cron < Chef::Provider include Chef::Mixin::Command + SPECIAL_TIME_VALUES = [:reboot, :yearly, :annually, :monthly, :weekly, :daily, :midnight, :hourly] + CRON_ATTRIBUTES = [:minute, :hour, :day, :month, :weekday, :time, :command, :mailto, :path, :shell, :home, :environment] + WEEKDAY_SYMBOLS = [:sunday, :monday, :tuesday, :wednesday, :thursday, :friday, :saturday] + CRON_PATTERN = /\A([-0-9*,\/]+)\s([-0-9*,\/]+)\s([-0-9*,\/]+)\s([-0-9*,\/]+|[a-zA-Z]{3})\s([-0-9*,\/]+|[a-zA-Z]{3})\s(.*)/ + SPECIAL_PATTERN = /\A(@(#{SPECIAL_TIME_VALUES.join('|')}))\s(.*)/ ENV_PATTERN = /\A(\S+)=(\S*)/ - CRON_ATTRIBUTES = [:minute, :hour, :day, :month, :weekday, :command, :mailto, :path, :shell, :home, :environment] - def initialize(new_resource, run_context) super(new_resource, run_context) @cron_exists = false @@ -58,6 +61,12 @@ class Chef when ENV_PATTERN set_environment_var($1, $2) if cron_found next + when SPECIAL_PATTERN + if cron_found + @current_resource.time($2.to_sym) + @current_resource.command($3) + cron_found=false + end when CRON_PATTERN if cron_found @current_resource.minute($1) @@ -220,9 +229,22 @@ class Chef @new_resource.environment.each do |name, value| newcron << "#{name}=#{value}\n" end - newcron << "#{@new_resource.minute} #{@new_resource.hour} #{@new_resource.day} #{@new_resource.month} #{@new_resource.weekday} #{@new_resource.command}\n" + if @new_resource.time + newcron << "@#{@new_resource.time} #{@new_resource.command}\n" + else + newcron << "#{@new_resource.minute} #{@new_resource.hour} #{@new_resource.day} #{@new_resource.month} #{@new_resource.weekday} #{@new_resource.command}\n" + end newcron end + + def weekday_in_crontab + weekday_in_crontab = WEEKDAY_SYMBOLS.index(@new_resource.weekday) + if weekday_in_crontab.nil? + @new_resource.weekday + else + weekday_in_crontab.to_s + end + end end end end diff --git a/lib/chef/provider/deploy.rb b/lib/chef/provider/deploy.rb index d1017dba62..516aee6159 100644 --- a/lib/chef/provider/deploy.rb +++ b/lib/chef/provider/deploy.rb @@ -266,7 +266,7 @@ class Chef def copy_cached_repo target_dir_path = @new_resource.deploy_to + "/releases" - converge_by("deploy from repo to #{@target_dir_path} ") do + converge_by("deploy from repo to #{target_dir_path} ") do FileUtils.rm_rf(release_path) if ::File.exist?(release_path) FileUtils.mkdir_p(target_dir_path) FileUtils.cp_r(::File.join(@new_resource.destination, "."), release_path, :preserve => true) diff --git a/lib/chef/provider/group.rb b/lib/chef/provider/group.rb index f01677b3ac..35a16c870c 100644 --- a/lib/chef/provider/group.rb +++ b/lib/chef/provider/group.rb @@ -84,7 +84,7 @@ class Chef # <false>:: If a change is not required def compare_group @change_desc = [ ] - if @new_resource.gid != @current_resource.gid + if @new_resource.gid.to_s != @current_resource.gid.to_s @change_desc << "change gid #{@current_resource.gid} to #{@new_resource.gid}" end diff --git a/lib/chef/provider/ifconfig/debian.rb b/lib/chef/provider/ifconfig/debian.rb index 821f4fe924..7589971143 100644 --- a/lib/chef/provider/ifconfig/debian.rb +++ b/lib/chef/provider/ifconfig/debian.rb @@ -24,6 +24,9 @@ class Chef class Ifconfig class Debian < Chef::Provider::Ifconfig + INTERFACES_FILE = "/etc/network/interfaces" + INTERFACES_DOT_D_DIR = "/etc/network/interfaces.d" + def initialize(new_resource, run_context) super(new_resource, run_context) @config_template = %{ @@ -46,22 +49,30 @@ iface <%= @new_resource.device %> inet static <% end %> <% end %> } - @config_path = "/etc/network/interfaces.d/ifcfg-#{@new_resource.device}" + @config_path = "#{INTERFACES_DOT_D_DIR}/ifcfg-#{@new_resource.device}" end def generate_config - check_interfaces_config + enforce_interfaces_dot_d_sanity super end protected - def check_interfaces_config - converge_by ('modify configuration file : /etc/network/interfaces') do - Dir.mkdir('/etc/network/interfaces.d') unless ::File.directory?('/etc/network/interfaces.d') - conf = Chef::Util::FileEdit.new('/etc/network/interfaces') - conf.insert_line_if_no_match('^\s*source\s+/etc/network/interfaces[.]d/[*]\s*$', 'source /etc/network/interfaces.d/*') - conf.write_file + def enforce_interfaces_dot_d_sanity + # create /etc/network/interfaces.d via dir resource (to get reporting, etc) + dir = Chef::Resource::Directory.new(INTERFACES_DOT_D_DIR, run_context) + dir.run_action(:create) + new_resource.updated_by_last_action(true) if dir.updated_by_last_action? + # roll our own file_edit resource, this will not get reported until we have a file_edit resource + interfaces_dot_d_for_regexp = INTERFACES_DOT_D_DIR.gsub(/\./, '\.') # escape dots for the regexp + regexp = %r{^\s*source\s+#{interfaces_dot_d_for_regexp}/\*\s*$} + unless ::File.exists?(INTERFACES_FILE) && regexp.match(IO.read(INTERFACES_FILE)) + converge_by("modifying #{INTERFACES_FILE} to source #{INTERFACES_DOT_D_DIR}") do + conf = Chef::Util::FileEdit.new(INTERFACES_FILE) + conf.insert_line_if_no_match(regexp, "source #{INTERFACES_DOT_D_DIR}/*") + conf.write_file + end end end diff --git a/lib/chef/provider/mount/mount.rb b/lib/chef/provider/mount/mount.rb index 25dfd42725..22d61a9236 100644 --- a/lib/chef/provider/mount/mount.rb +++ b/lib/chef/provider/mount/mount.rb @@ -244,7 +244,7 @@ class Chef # So given a symlink like this: # /dev/mapper/vgroot-tmp.vol -> /dev/dm-9 # First it will try to match "/dev/mapper/vgroot-tmp.vol". If there is no match it will try matching for "/dev/dm-9". - "(?:#{Regexp.escape(device_real)}|#{Regexp.escape(::File.readlink(device_real))})" + "(?:#{Regexp.escape(device_real)}|#{Regexp.escape(::File.expand_path(::File.readlink(device_real),::File.dirname(device_real)))})" else Regexp.escape(device_real) end diff --git a/lib/chef/provider/ohai.rb b/lib/chef/provider/ohai.rb index c686f67450..a6b5ab5daa 100644 --- a/lib/chef/provider/ohai.rb +++ b/lib/chef/provider/ohai.rb @@ -33,11 +33,12 @@ class Chef def action_reload converge_by("re-run ohai and merge results into node attributes") do ohai = ::Ohai::System.new - if @new_resource.plugin - ohai.require_plugin @new_resource.plugin - else - ohai.all_plugins - end + + # If @new_resource.plugin is nil, ohai will reload all the plugins + # Otherwise it will only reload the specified plugin + # Note that any changes to plugins, or new plugins placed on + # the path are picked up by ohai. + ohai.all_plugins @new_resource.plugin node.automatic_attrs.merge! ohai.data Chef::Log.info("#{@new_resource} reloaded") end diff --git a/lib/chef/provider/package/dpkg.rb b/lib/chef/provider/package/dpkg.rb index 8ec1ad5878..fb366fb6eb 100644 --- a/lib/chef/provider/package/dpkg.rb +++ b/lib/chef/provider/package/dpkg.rb @@ -25,7 +25,8 @@ class Chef class Provider class Package class Dpkg < Chef::Provider::Package::Apt - DPKG_INFO = /([a-z\d\-\+\.]+)\t([\w\d.~-]+)/ + # http://www.debian.org/doc/debian-policy/ch-controlfields.html#s-f-Version + DPKG_INFO = /([a-z\d\-\+\.]+)\t([\w\d.~:-]+)/ DPKG_INSTALLED = /^Status: install ok installed/ DPKG_VERSION = /^Version: (.+)$/ diff --git a/lib/chef/provider/package/windows.rb b/lib/chef/provider/package/windows.rb new file mode 100644 index 0000000000..be1de0b969 --- /dev/null +++ b/lib/chef/provider/package/windows.rb @@ -0,0 +1,80 @@ +# +# Author:: Bryan McLellan <btm@loftninjas.org> +# Copyright:: Copyright (c) 2014 Chef Software, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require 'chef/resource/windows_package' +require 'chef/provider/package' + +class Chef + class Provider + class Package + class Windows < Chef::Provider::Package + + # Depending on the installer, we may need to examine installer_type or + # source attributes, or search for text strings in the installer file + # binary to determine the installer type for the user. Since the file + # must be on disk to do so, we have to make this choice in the provider. + require 'chef/provider/package/windows/msi.rb' + + # load_current_resource is run in Chef::Provider#run_action when not in whyrun_mode? + def load_current_resource + @current_resource = Chef::Resource::WindowsPackage.new(@new_resource.name) + @current_resource.version(package_provider.installed_version) + @new_resource.version(package_provider.package_version) + @current_resource + end + + def package_provider + @package_provider ||= begin + case installer_type + when :msi + Chef::Provider::Package::Windows::MSI.new(@new_resource) + else + raise "Unable to find a Chef::Provider::Package::Windows provider for installer_type '#{installer_type}'" + end + end + end + + def installer_type + @installer_type ||= begin + if @new_resource.installer_type + @new_resource.installer_type + else + file_extension = ::File.basename(@new_resource.source).split(".").last.downcase + + if file_extension == "msi" + :msi + else + raise ArgumentError, "Installer type for Windows Package '#{@new_resource.name}' not specified and cannot be determined from file extension '#{file_extension}'" + end + end + end + end + + # Chef::Provider::Package action_install + action_remove call install_package + remove_package + # Pass those calls to the correct sub-provider + def install_package(name, version) + package_provider.install_package(name, version) + end + + def remove_package(name, version) + package_provider.remove_package(name, version) + end + end + end + end +end diff --git a/lib/chef/provider/package/windows/msi.rb b/lib/chef/provider/package/windows/msi.rb new file mode 100644 index 0000000000..a342600678 --- /dev/null +++ b/lib/chef/provider/package/windows/msi.rb @@ -0,0 +1,69 @@ +# +# Author:: Bryan McLellan <btm@loftninjas.org> +# Copyright:: Copyright (c) 2014 Chef Software, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# TODO: Allow @new_resource.source to be a Product Code as a GUID for uninstall / network install + +require 'chef/mixin/shell_out' +require 'chef/win32/api/installer' if RUBY_PLATFORM =~ /mswin|mingw32|windows/ + +class Chef + class Provider + class Package + class Windows + class MSI + include Chef::ReservedNames::Win32::API::Installer if RUBY_PLATFORM =~ /mswin|mingw32|windows/ + include Chef::Mixin::ShellOut + + def initialize(resource) + @new_resource = resource + end + + # From Chef::Provider::Package + def expand_options(options) + options ? " #{options}" : "" + end + + # Returns a version if the package is installed or nil if it is not. + def installed_version + Chef::Log.debug("#{@new_resource} getting product code for package at #{@new_resource.source}") + product_code = get_product_property(@new_resource.source, "ProductCode") + Chef::Log.debug("#{@new_resource} checking package status and verion for #{product_code}") + get_installed_version(product_code) + end + + def package_version + Chef::Log.debug("#{@new_resource} getting product version for package at #{@new_resource.source}") + get_product_property(@new_resource.source, "ProductVersion") + end + + def install_package(name, version) + # We could use MsiConfigureProduct here, but we'll start off with msiexec + Chef::Log.debug("#{@new_resource} installing MSI package '#{@new_resource.source}'") + shell_out!("msiexec /qn /i \"#{@new_resource.source}\" #{expand_options(@new_resource.options)}", {:timeout => @new_resource.timeout, :returns => @new_resource.returns}) + end + + def remove_package(name, version) + # We could use MsiConfigureProduct here, but we'll start off with msiexec + Chef::Log.debug("#{@new_resource} removing MSI package '#{@new_resource.source}'") + shell_out!("msiexec /qn /x \"#{@new_resource.source}\" #{expand_options(@new_resource.options)}", {:timeout => @new_resource.timeout, :returns => @new_resource.returns}) + end + end + end + end + end +end diff --git a/lib/chef/provider/powershell_script.rb b/lib/chef/provider/powershell_script.rb index c459cdf678..967b2d822b 100644 --- a/lib/chef/provider/powershell_script.rb +++ b/lib/chef/provider/powershell_script.rb @@ -23,9 +23,9 @@ class Chef class PowershellScript < Chef::Provider::WindowsScript protected - - EXIT_STATUS_NORMALIZATION_SCRIPT = "\nif ($? -eq $true) {exit 0} elseif ( $LASTEXITCODE -ne 0) {exit $LASTEXITCODE} else { exit 1 }" - EXIT_STATUS_RESET_SCRIPT = "$LASTEXITCODE=0\n" + EXIT_STATUS_EXCEPTION_HANDLER = "\ntrap [Exception] {write-error -exception ($_.Exception.Message);exit 1}".freeze + EXIT_STATUS_NORMALIZATION_SCRIPT = "\nif ($? -ne $true) { if ( $LASTEXITCODE -ne 0) {exit $LASTEXITCODE} else { exit 1 }}".freeze + EXIT_STATUS_RESET_SCRIPT = "\n$LASTEXITCODE=0".freeze # Process exit codes are strange with PowerShell. Unless you # explicitly call exit in Powershell, the powershell.exe @@ -36,15 +36,28 @@ class Chef # last process run in the script if it is the last command # executed, otherwise 0 or 1 based on whether $? is set to true # (success, where we return 0) or false (where we return 1). - def NormalizeScriptExitStatus( code ) - @code = (! code.nil?) ? ( EXIT_STATUS_RESET_SCRIPT + code + EXIT_STATUS_NORMALIZATION_SCRIPT ) : nil + def normalize_script_exit_status( code ) + target_code = ( EXIT_STATUS_EXCEPTION_HANDLER + + EXIT_STATUS_RESET_SCRIPT + + "\n" + + code.to_s + + EXIT_STATUS_NORMALIZATION_SCRIPT ) + convert_boolean_return = @new_resource.convert_boolean_return + @code = <<EOH +new-variable -name interpolatedexitcode -visibility private -value $#{convert_boolean_return} +new-variable -name chefscriptresult -visibility private +$chefscriptresult = { +#{target_code} +}.invokereturnasis() +if ($interpolatedexitcode -and $chefscriptresult.gettype().name -eq 'boolean') { exit [int32](!$chefscriptresult) } else { exit 0 } +EOH end public def initialize (new_resource, run_context) super(new_resource, run_context, '.ps1') - NormalizeScriptExitStatus(new_resource.code) + normalize_script_exit_status(new_resource.code) end def flags diff --git a/lib/chef/provider/service/macosx.rb b/lib/chef/provider/service/macosx.rb index 4f2de2ccbf..ca78c2eaee 100644 --- a/lib/chef/provider/service/macosx.rb +++ b/lib/chef/provider/service/macosx.rb @@ -17,6 +17,7 @@ # require 'chef/provider/service' +require 'rexml/document' class Chef class Provider @@ -41,6 +42,7 @@ class Chef @current_resource.service_name(@new_resource.service_name) @plist_size = 0 @plist = find_service_plist + @service_label = find_service_label set_service_status @current_resource @@ -48,14 +50,6 @@ class Chef def define_resource_requirements #super - requirements.assert(:enable) do |a| - a.failure_message Chef::Exceptions::UnsupportedAction, "#{self.to_s} does not support :enable" - end - - requirements.assert(:disable) do |a| - a.failure_message Chef::Exceptions::UnsupportedAction, "#{self.to_s} does not support :disable" - end - requirements.assert(:reload) do |a| a.failure_message Chef::Exceptions::UnsupportedAction, "#{self.to_s} does not support :reload" end @@ -66,6 +60,12 @@ class Chef end requirements.assert(:all_actions) do |a| + a.assertion { !@service_label.to_s.empty? } + a.failure_message Chef::Exceptions::Service, + "Could not find service's label in plist file '#{@plist}'!" + end + + requirements.assert(:all_actions) do |a| a.assertion { @plist_size > 0 } # No failrue here in original code - so we also will not # fail. Instead warn that the service is potentially missing @@ -74,7 +74,6 @@ class Chef @current_resource.running(false) end end - end def start_service @@ -111,19 +110,56 @@ class Chef end end + # On OS/X, enabling a service has the side-effect of starting it, + # and disabling a service has the side-effect of stopping it. + # + # This makes some sense on OS/X since launchctl is an "init"-style + # supervisor that will restart daemons that are crashing, etc. + def enable_service + if @current_resource.enabled + Chef::Log.debug("#{@new_resource} already enabled, not enabling") + else + shell_out!( + "launchctl load -w '#{@plist}'", + :user => @owner_uid, :group => @owner_gid + ) + end + end + + def disable_service + unless @current_resource.enabled + Chef::Log.debug("#{@new_resource} not enabled, not disabling") + else + shell_out!( + "launchctl unload -w '#{@plist}'", + :user => @owner_uid, :group => @owner_gid + ) + end + end def set_service_status - return if @plist == nil + return if @plist == nil or @service_label.to_s.empty? - @current_resource.enabled(!@plist.nil?) + cmd = shell_out( + "launchctl list #{@service_label}", + :user => @owner_uid, :group => @owner_gid + ) + + if cmd.exitstatus == 0 + @current_resource.enabled(true) + else + @current_resource.enabled(false) + end if @current_resource.enabled @owner_uid = ::File.stat(@plist).uid @owner_gid = ::File.stat(@plist).gid - shell_out!("launchctl list", :user => @owner_uid, :group => @owner_gid).stdout.each_line do |line| + shell_out!( + "launchctl list", :user => @owner_uid, :group => @owner_gid + ).stdout.each_line do |line| case line - when /(\d+|-)\s+(?:\d+|-)\s+(.*\.?)#{@current_resource.service_name}/ + when /(\d+|-)\s+(?:\d+|-)\s+(.*\.?)#{@service_label}/ pid = $1 @current_resource.running(!pid.to_i.zero?) end @@ -135,9 +171,27 @@ class Chef private + def find_service_label + # Most services have the same internal label as the name of the + # plist file. However, there is no rule saying that *has* to be + # the case, and some core services (notably, ssh) do not follow + # this rule. + + # plist files can come in XML or Binary formats. this command + # will make sure we get XML every time. + plist_xml = shell_out!("plutil -convert xml1 -o - #{@plist}").stdout + + plist_doc = REXML::Document.new(plist_xml) + plist_doc.elements[ + "/plist/dict/key[text()='Label']/following::string[1]/text()"] + end + def find_service_plist plists = PLIST_DIRS.inject([]) do |results, dir| - entries = Dir.glob("#{::File.expand_path(dir)}/*#{@current_resource.service_name}*.plist") + edir = ::File.expand_path(dir) + entries = Dir.glob( + "#{edir}/*#{@current_resource.service_name}*.plist" + ) entries.any? ? results << entries : results end plists.flatten! diff --git a/lib/chef/provider/service/solaris.rb b/lib/chef/provider/service/solaris.rb index 4bdb6fbfd1..7f06ac561b 100644 --- a/lib/chef/provider/service/solaris.rb +++ b/lib/chef/provider/service/solaris.rb @@ -25,11 +25,13 @@ class Chef class Service class Solaris < Chef::Provider::Service include Chef::Mixin::ShellOut + attr_reader :maintenance def initialize(new_resource, run_context=nil) super @init_command = "/usr/sbin/svcadm" @status_command = "/bin/svcs -l" + @maintenace = false end @@ -44,6 +46,7 @@ class Chef end def enable_service + shell_out!("#{default_init_command} clear #{@new_resource.service_name}") if @maintenance shell_out!("#{default_init_command} enable -s #{@new_resource.service_name}") end @@ -65,13 +68,14 @@ class Chef end def service_status - status = popen4("#{@status_command} #{@current_resource.service_name}") do |pid, stdin, stdout, stderr| - stdout.each do |line| - case line - when /state\s+online/ - @current_resource.enabled(true) - @current_resource.running(true) - end + status = shell_out!("#{@status_command} #{@current_resource.service_name}") + status.stdout.each_line do |line| + case line + when /state\s+online/ + @current_resource.enabled(true) + @current_resource.running(true) + when /state\s+maintenance/ + @maintenance = true end end unless @current_resource.enabled diff --git a/lib/chef/recipe.rb b/lib/chef/recipe.rb index 0c688cb5f8..5b95d80590 100644 --- a/lib/chef/recipe.rb +++ b/lib/chef/recipe.rb @@ -23,6 +23,7 @@ require 'chef/dsl/data_query' require 'chef/dsl/platform_introspection' require 'chef/dsl/include_recipe' require 'chef/dsl/registry_helper' +require 'chef/dsl/reboot_pending' require 'chef/mixin/from_file' @@ -38,6 +39,7 @@ class Chef include Chef::DSL::IncludeRecipe include Chef::DSL::Recipe include Chef::DSL::RegistryHelper + include Chef::DSL::RebootPending include Chef::Mixin::FromFile include Chef::Mixin::Deprecation diff --git a/lib/chef/request_id.rb b/lib/chef/request_id.rb new file mode 100644 index 0000000000..7fc177c633 --- /dev/null +++ b/lib/chef/request_id.rb @@ -0,0 +1,37 @@ +# Author:: Prajakta Purohit (<prajakta@opscode.com>) +# Copyright:: Copyright (c) 2009, 2010, 2013, 2014 Opscode, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require 'chef/monkey_patches/securerandom' +require 'singleton' + +class Chef + class RequestID + include Singleton + + def reset_request_id + @request_id = nil + end + + def request_id + @request_id ||= generate_request_id + end + + def generate_request_id + SecureRandom.uuid + end + end +end diff --git a/lib/chef/resource.rb b/lib/chef/resource.rb index 997c614171..7d96b26b4b 100644 --- a/lib/chef/resource.rb +++ b/lib/chef/resource.rb @@ -21,7 +21,9 @@ require 'chef/mixin/params_validate' require 'chef/dsl/platform_introspection' require 'chef/dsl/data_query' require 'chef/dsl/registry_helper' +require 'chef/dsl/reboot_pending' require 'chef/mixin/convert_to_class_name' +require 'chef//guard_interpreter/resource_guard_interpreter' require 'chef/resource/conditional' require 'chef/resource/conditional_action_not_nothing' require 'chef/resource_collection' @@ -125,6 +127,7 @@ F include Chef::Mixin::ParamsValidate include Chef::DSL::PlatformIntrospection include Chef::DSL::RegistryHelper + include Chef::DSL::RebootPending include Chef::Mixin::ConvertToClassName include Chef::Mixin::Deprecation @@ -247,6 +250,7 @@ F @not_if = [] @only_if = [] @source_line = nil + @guard_interpreter = :default @elapsed_time = 0 @node = run_context ? deprecated_ivar(run_context.node, :node, :warn) : nil @@ -399,6 +403,14 @@ F ignore_failure(arg) end + def guard_interpreter(arg=nil) + set_or_return( + :guard_interpreter, + arg, + :kind_of => Symbol + ) + end + # Sets up a notification from this resource to the resource specified by +resource_spec+. def notifies(action, resource_spec, timing=:delayed) # when using old-style resources(:template => "/foo.txt") style, you @@ -550,7 +562,7 @@ F # * evaluates to false if the block is false, or if the command returns a non-zero exit code. def only_if(command=nil, opts={}, &block) if command || block_given? - @only_if << Conditional.only_if(command, opts, &block) + @only_if << Conditional.only_if(self, command, opts, &block) end @only_if end @@ -571,7 +583,7 @@ F # * evaluates to false if the block is true, or if the command returns a 0 exit status. def not_if(command=nil, opts={}, &block) if command || block_given? - @not_if << Conditional.not_if(command, opts, &block) + @not_if << Conditional.not_if(self, command, opts, &block) end @not_if end @@ -625,7 +637,7 @@ F provider_for_action(action).run_action rescue Exception => e if ignore_failure - Chef::Log.error("#{self} (#{defined_at}) had an error: #{e.message}; ignore_failure is set, continuing") + Chef::Log.error("#{custom_exception_message(e)}; ignore_failure is set, continuing") events.resource_failed(self, action, e) elsif retries > 0 events.resource_failed_retriable(self, action, retries, e) @@ -660,8 +672,12 @@ F end end + def custom_exception_message(e) + "#{self} (#{defined_at}) had an error: #{e.class.name}: #{e.message}" + end + def customize_exception(e) - new_exception = e.exception("#{self} (#{defined_at}) had an error: #{e.class.name}: #{e.message}") + new_exception = e.exception(custom_exception_message(e)) new_exception.set_backtrace(e.backtrace) new_exception end @@ -813,6 +829,5 @@ F end end end - end end diff --git a/lib/chef/resource/conditional.rb b/lib/chef/resource/conditional.rb index 60f65e14e2..e6623be5dd 100644 --- a/lib/chef/resource/conditional.rb +++ b/lib/chef/resource/conditional.rb @@ -17,6 +17,7 @@ # require 'chef/mixin/shell_out' +require 'chef/guard_interpreter/resource_guard_interpreter' class Chef class Resource @@ -29,12 +30,12 @@ class Chef private :new end - def self.not_if(command=nil, command_opts={}, &block) - new(:not_if, command, command_opts, &block) + def self.not_if(parent_resource, command=nil, command_opts={}, &block) + new(:not_if, parent_resource, command, command_opts, &block) end - def self.only_if(command=nil, command_opts={}, &block) - new(:only_if, command, command_opts, &block) + def self.only_if(parent_resource, command=nil, command_opts={}, &block) + new(:only_if, parent_resource, command, command_opts, &block) end attr_reader :positivity @@ -42,14 +43,16 @@ class Chef attr_reader :command_opts attr_reader :block - def initialize(positivity, command=nil, command_opts={}, &block) + def initialize(positivity, parent_resource, command=nil, command_opts={}, &block) @positivity = positivity case command when String + @guard_interpreter = new_guard_interpreter(parent_resource, command, command_opts, &block) @command, @command_opts = command, command_opts @block = nil when nil raise ArgumentError, "only_if/not_if requires either a command or a block" unless block_given? + @guard_interpreter = nil @command, @command_opts = nil, nil @block = block else @@ -69,11 +72,11 @@ class Chef end def evaluate - @command ? evaluate_command : evaluate_block + @guard_interpreter ? evaluate_command : evaluate_block end def evaluate_command - shell_out(@command, @command_opts).status.success? + @guard_interpreter.evaluate rescue Chef::Exceptions::CommandTimeout Chef::Log.warn "Command '#{@command}' timed out" false @@ -100,6 +103,16 @@ class Chef end end + private + + def new_guard_interpreter(parent_resource, command, opts) + if parent_resource.guard_interpreter == :default + guard_interpreter = Chef::GuardInterpreter::DefaultGuardInterpreter.new(command, opts) + else + guard_interpreter = Chef::GuardInterpreter::ResourceGuardInterpreter.new(parent_resource, command, opts) + end + end + end end end diff --git a/lib/chef/resource/cron.rb b/lib/chef/resource/cron.rb index dfbb91f80c..9c04658bf3 100644 --- a/lib/chef/resource/cron.rb +++ b/lib/chef/resource/cron.rb @@ -43,6 +43,7 @@ class Chef @path = nil @shell = nil @home = nil + @time = nil @environment = {} end @@ -121,13 +122,28 @@ class Chef converted_arg = arg end begin - if integerize(arg) > 7 then raise RangeError end + error_message = "You provided '#{arg}' as a weekday, acceptable values are " + error_message << Provider::Cron::WEEKDAY_SYMBOLS.map {|sym| ":#{sym.to_s}"}.join(', ') + error_message << " and a string in crontab format" + if (arg.is_a?(Symbol) && !Provider::Cron::WEEKDAY_SYMBOLS.include?(arg)) || + (!arg.is_a?(Symbol) && integerize(arg) > 7) || + (!arg.is_a?(Symbol) && integerize(arg) < 0) + raise RangeError, error_message + end rescue ArgumentError end set_or_return( :weekday, converted_arg, - :kind_of => String + :kind_of => [String, Symbol] + ) + end + + def time(arg=nil) + set_or_return( + :time, + arg, + :equal_to => Chef::Provider::Cron::SPECIAL_TIME_VALUES ) end diff --git a/lib/chef/resource/execute.rb b/lib/chef/resource/execute.rb index 6c07bf9352..7c4fa48c0a 100644 --- a/lib/chef/resource/execute.rb +++ b/lib/chef/resource/execute.rb @@ -125,8 +125,6 @@ class Chef ) end - - end end end diff --git a/lib/chef/resource/powershell_script.rb b/lib/chef/resource/powershell_script.rb index cbd81b1259..1b47e7411a 100644 --- a/lib/chef/resource/powershell_script.rb +++ b/lib/chef/resource/powershell_script.rb @@ -15,17 +15,39 @@ # See the License for the specific language governing permissions and # limitations under the License. # - require 'chef/resource/windows_script' class Chef class Resource class PowershellScript < Chef::Resource::WindowsScript + set_guard_inherited_attributes(:architecture) + def initialize(name, run_context=nil) super(name, run_context, :powershell_script, "powershell.exe") + @convert_boolean_return = false + end + + def convert_boolean_return(arg=nil) + set_or_return( + :convert_boolean_return, + arg, + :kind_of => [ FalseClass, TrueClass ] + ) end + protected + + # Allow callers evaluating guards to request default + # attribute values. This is needed to allow + # convert_boolean_return to be true in guard context by default, + # and false by default otherwise. When this mode becomes the + # default for this resource, this method can be removed since + # guard context and recipe resource context will have the + # same behavior. + def self.get_default_attributes(opts) + {:convert_boolean_return => true} + end end end end diff --git a/lib/chef/resource/script.rb b/lib/chef/resource/script.rb index 8cc9c6f0c5..6f66fb9094 100644 --- a/lib/chef/resource/script.rb +++ b/lib/chef/resource/script.rb @@ -58,6 +58,31 @@ class Chef ) end + def self.set_guard_inherited_attributes(*inherited_attributes) + @class_inherited_attributes = inherited_attributes + end + + def self.guard_inherited_attributes(*inherited_attributes) + # Similar to patterns elsewhere, return attributes from this + # class and superclasses as a form of inheritance + ancestor_attributes = [] + + if superclass.respond_to?(:guard_inherited_attributes) + ancestor_attributes = superclass.guard_inherited_attributes + end + + ancestor_attributes.concat(@class_inherited_attributes ? @class_inherited_attributes : []).uniq + end + + set_guard_inherited_attributes( + :cwd, + :environment, + :group, + :path, + :user, + :umask + ) + end end end diff --git a/lib/chef/resource/subversion.rb b/lib/chef/resource/subversion.rb index 04fec9b1d8..44158cb080 100644 --- a/lib/chef/resource/subversion.rb +++ b/lib/chef/resource/subversion.rb @@ -32,6 +32,10 @@ class Chef allowed_actions << :force_export end + # Override exception to strip password if any, so it won't appear in logs and different Chef notifications + def custom_exception_message(e) + "#{self} (#{defined_at}) had an error: #{e.class.name}: #{svn_password ? e.message.gsub(svn_password, "[hidden_password]") : e.message}" + end end end end diff --git a/lib/chef/resource/windows_package.rb b/lib/chef/resource/windows_package.rb new file mode 100644 index 0000000000..8bd41e0cb7 --- /dev/null +++ b/lib/chef/resource/windows_package.rb @@ -0,0 +1,79 @@ +# +# Author:: Bryan McLellan <btm@loftninjas.org> +# Copyright:: Copyright (c) 2014 Chef Software, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require 'chef/resource/package' +require 'chef/provider/package/windows' +require 'chef/win32/error' if RUBY_PLATFORM =~ /mswin|mingw|windows/ + +class Chef + class Resource + class WindowsPackage < Chef::Resource::Package + + provides :package, :on_platforms => ["windows"] + + def initialize(name, run_context=nil) + super + @allowed_actions = [ :install, :remove ] + @provider = Chef::Provider::Package::Windows + @resource_name = :windows_package + @source ||= source(@package_name) + + # Unique to this resource + @installer_type = nil + @timeout = 600 + # In the past we accepted return code 127 for an unknown reason and 42 because of a bug + @returns = [ 0 ] + end + + def installer_type(arg=nil) + set_or_return( + :installer_type, + arg, + :kind_of => [ String ] + ) + end + + def timeout(arg=nil) + set_or_return( + :timeout, + arg, + :kind_of => [ String, Integer ] + ) + end + + def returns(arg=nil) + set_or_return( + :returns, + arg, + :kind_of => [ String, Integer, Array ] + ) + end + + def source(arg=nil) + if arg == nil && self.instance_variable_defined?(:@source) == true + @source + else + raise ArgumentError, "Bad type for WindowsPackage resource, use a String" unless arg.is_a?(String) + Chef::Log.debug("#{package_name}: sanitizing source path '#{arg}'") + @source = ::File.absolute_path(arg).gsub(::File::SEPARATOR, ::File::ALT_SEPARATOR) + end + end + end + end +end + diff --git a/lib/chef/resource/windows_script.rb b/lib/chef/resource/windows_script.rb index 2b563f5bec..108891e9ba 100644 --- a/lib/chef/resource/windows_script.rb +++ b/lib/chef/resource/windows_script.rb @@ -52,11 +52,6 @@ class Chef "cannot execute script with requested architecture '#{desired_architecture.to_s}' on a system with architecture '#{node_windows_architecture(node)}'" end end - - def node - run_context && run_context.node - end - end end end diff --git a/lib/chef/resource_reporter.rb b/lib/chef/resource_reporter.rb index 04f4ee26de..d191710cb4 100644 --- a/lib/chef/resource_reporter.rb +++ b/lib/chef/resource_reporter.rb @@ -107,7 +107,6 @@ class Chef @pending_update = nil @status = "success" @exception = nil - @run_id = SecureRandom.uuid @rest_client = rest_client @error_descriptions = {} end @@ -118,7 +117,7 @@ class Chef if reporting_enabled? begin resource_history_url = "reports/nodes/#{node_name}/runs" - server_response = @rest_client.post_rest(resource_history_url, {:action => :start, :run_id => @run_id, + server_response = @rest_client.post_rest(resource_history_url, {:action => :start, :run_id => run_id, :start_time => start_time.to_s}, headers) rescue Timeout::Error, Errno::EINVAL, Errno::ECONNRESET, EOFError, Net::HTTPBadResponse, Net::HTTPHeaderSyntaxError, Net::ProtocolError => e handle_error_starting_run(e, resource_history_url) @@ -158,6 +157,10 @@ class Chef @reporting_enabled = false end + def run_id + @run_status.run_id + end + def resource_current_state_loaded(new_resource, action, current_resource) unless nested_resource?(new_resource) @pending_update = ResourceReport.new_with_current_state(new_resource, action, current_resource) @@ -214,8 +217,8 @@ class Chef def post_reporting_data if reporting_enabled? run_data = prepare_run_data - resource_history_url = "reports/nodes/#{node_name}/runs/#{@run_id}" - Chef::Log.info("Sending resource update report (run-id: #{@run_id})") + resource_history_url = "reports/nodes/#{node_name}/runs/#{run_id}" + Chef::Log.info("Sending resource update report (run-id: #{run_id})") Chef::Log.debug run_data.inspect compressed_data = encode_gzip(run_data.to_json) begin diff --git a/lib/chef/resources.rb b/lib/chef/resources.rb index 76adb6f1e1..711becef8c 100644 --- a/lib/chef/resources.rb +++ b/lib/chef/resources.rb @@ -69,6 +69,7 @@ require 'chef/resource/template' require 'chef/resource/timestamped_deploy' require 'chef/resource/user' require 'chef/resource/whyrun_safe_ruby_block' +require 'chef/resource/windows_package' require 'chef/resource/yum_package' require 'chef/resource/lwrp_base' require 'chef/resource/bff_package' diff --git a/lib/chef/rest.rb b/lib/chef/rest.rb index a1139d7fa2..f0de443058 100644 --- a/lib/chef/rest.rb +++ b/lib/chef/rest.rb @@ -36,6 +36,7 @@ require 'chef/http/validate_content_length' require 'chef/config' require 'chef/exceptions' require 'chef/platform/query_helpers' +require 'chef/http/remote_request_id' class Chef # == Chef::REST @@ -56,19 +57,27 @@ class Chef # http://localhost:4000, a call to +get_rest+ with 'nodes' will make an # HTTP GET request to http://localhost:4000/nodes def initialize(url, client_name=Chef::Config[:node_name], signing_key_filename=Chef::Config[:client_key], options={}) + options = options.dup options[:client_name] = client_name options[:signing_key_filename] = signing_key_filename super(url, options) @decompressor = Decompressor.new(options) @authenticator = Authenticator.new(options) + @request_id = RemoteRequestID.new(options) - @middlewares << ValidateContentLength.new(options) @middlewares << JSONInput.new(options) @middlewares << JSONToModelOutput.new(options) @middlewares << CookieManager.new(options) @middlewares << @decompressor @middlewares << @authenticator + @middlewares << @request_id + + # ValidateContentLength should come after Decompressor + # because the order of middlewares is reversed when handling + # responses. + @middlewares << ValidateContentLength.new(options) + end def signing_key_filename @@ -132,7 +141,7 @@ class Chef def raw_http_request(method, path, headers, data) url = create_url(path) method, url, headers, data = @authenticator.handle_request(method, url, headers, data) - + method, url, headers, data = @request_id.handle_request(method, url, headers, data) response, rest_request, return_value = send_http_request(method, url, headers, data) response.error! unless success_response?(response) return_value diff --git a/lib/chef/run_context.rb b/lib/chef/run_context.rb index 05a954ad15..a102ef4692 100644 --- a/lib/chef/run_context.rb +++ b/lib/chef/run_context.rb @@ -77,13 +77,15 @@ class Chef @events = events @node.run_context = self + + @cookbook_compiler = nil end # Triggers the compile phase of the chef run. Implemented by # Chef::RunContext::CookbookCompiler def load(run_list_expansion) - compiler = CookbookCompiler.new(self, run_list_expansion, events) - compiler.compile + @cookbook_compiler = CookbookCompiler.new(self, run_list_expansion, events) + @cookbook_compiler.compile end # Adds an immediate notification to the @@ -141,6 +143,18 @@ class Chef Chef::Log.debug("Loading Recipe #{recipe_name} via include_recipe") cookbook_name, recipe_short_name = Chef::Recipe.parse_recipe_name(recipe_name) + + if unreachable_cookbook?(cookbook_name) # CHEF-4367 + Chef::Log.warn(<<-ERROR_MESSAGE) +MissingCookbookDependency: +Recipe `#{recipe_name}` is not in the run_list, and cookbook '#{cookbook_name}' +is not a dependency of any cookbook in the run_list. To load this recipe, +first add a dependency on cookbook '#{cookbook_name}' in the cookbook you're +including it from in that cookbook's metadata. +ERROR_MESSAGE + end + + if loaded_fully_qualified_recipe?(cookbook_name, recipe_short_name) Chef::Log.debug("I am not loading #{recipe_name}, because I have already seen it.") false @@ -228,6 +242,12 @@ class Chef cookbook.has_cookbook_file_for_node?(node, cb_file_name) end + # Delegates to CookbookCompiler#unreachable_cookbook? + # Used to raise an error when attempting to load a recipe belonging to a + # cookbook that is not in the dependency graph. See also: CHEF-4367 + def unreachable_cookbook?(cookbook_name) + @cookbook_compiler.unreachable_cookbook?(cookbook_name) + end private diff --git a/lib/chef/run_context/cookbook_compiler.rb b/lib/chef/run_context/cookbook_compiler.rb index 0a05061152..abe5afa7ae 100644 --- a/lib/chef/run_context/cookbook_compiler.rb +++ b/lib/chef/run_context/cookbook_compiler.rb @@ -16,6 +16,7 @@ # limitations under the License. # +require 'set' require 'chef/log' require 'chef/recipe' require 'chef/resource/lwrp_base' @@ -149,6 +150,17 @@ class Chef @events.recipe_load_complete end + # Whether or not a cookbook is reachable from the set of cookbook given + # by the run_list plus those cookbooks' dependencies. + def unreachable_cookbook?(cookbook_name) + !reachable_cookbooks.include?(cookbook_name) + end + + # All cookbooks in the dependency graph, returned as a Set. + def reachable_cookbooks + @reachable_cookbooks ||= Set.new(cookbook_order) + end + private def load_attributes_from_cookbook(cookbook_name) diff --git a/lib/chef/run_status.rb b/lib/chef/run_status.rb index 9354f7872a..0f181426b0 100644 --- a/lib/chef/run_status.rb +++ b/lib/chef/run_status.rb @@ -37,6 +37,8 @@ class Chef::RunStatus attr_writer :exception + attr_accessor :run_id + def initialize(node, events) @node = node @events = events @@ -112,7 +114,8 @@ class Chef::RunStatus :all_resources => all_resources, :updated_resources => updated_resources, :exception => formatted_exception, - :backtrace => backtrace} + :backtrace => backtrace, + :run_id => run_id} end # Returns a string of the format "ExceptionClass: message" or +nil+ if no diff --git a/lib/chef/server_api.rb b/lib/chef/server_api.rb index e9e7593dd6..8cdcd7a09d 100644 --- a/lib/chef/server_api.rb +++ b/lib/chef/server_api.rb @@ -22,6 +22,7 @@ require 'chef/http/cookie_manager' require 'chef/http/decompressor' require 'chef/http/json_input' require 'chef/http/json_output' +require 'chef/http/remote_request_id' class Chef class ServerAPI < Chef::HTTP @@ -37,5 +38,6 @@ class Chef use Chef::HTTP::CookieManager use Chef::HTTP::Decompressor use Chef::HTTP::Authenticator + use Chef::HTTP::RemoteRequestID end -end
\ No newline at end of file +end diff --git a/lib/chef/util/editor.rb b/lib/chef/util/editor.rb new file mode 100644 index 0000000000..973cf48e30 --- /dev/null +++ b/lib/chef/util/editor.rb @@ -0,0 +1,92 @@ +# +# Author:: Chris Bandy (<bandy.chris@gmail.com>) +# Copyright:: Copyright (c) 2014 Opscode, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +class Chef + class Util + class Editor + attr_reader :lines + + def initialize(lines) + @lines = lines.to_a.clone + end + + def append_line_after(search, line_to_append) + lines = [] + + @lines.each do |line| + lines << line + lines << line_to_append if line.match(search) + end + + (lines.length - @lines.length).tap { @lines = lines } + end + + def append_line_if_missing(search, line_to_append) + count = 0 + + unless @lines.find { |line| line.match(search) } + count = 1 + @lines << line_to_append + end + + count + end + + def remove_lines(search) + count = 0 + + @lines.delete_if do |line| + count += 1 if line.match(search) + end + + count + end + + def replace(search, replace) + count = 0 + + @lines.map! do |line| + if line.match(search) + count += 1 + line.gsub!(search, replace) + else + line + end + end + + count + end + + def replace_lines(search, replace) + count = 0 + + @lines.map! do |line| + if line.match(search) + count += 1 + replace + else + line + end + end + + count + end + end + end +end + diff --git a/lib/chef/util/file_edit.rb b/lib/chef/util/file_edit.rb index bb19435a12..92cefb4bb4 100644 --- a/lib/chef/util/file_edit.rb +++ b/lib/chef/util/file_edit.rb @@ -15,8 +15,8 @@ # See the License for the specific language governing permissions and # limitations under the License. +require 'chef/util/editor' require 'fileutils' -require 'tempfile' class Chef class Util @@ -24,108 +24,76 @@ class Chef private - attr_accessor :original_pathname, :contents, :file_edited + attr_reader :editor, :original_pathname public def initialize(filepath) + raise ArgumentError, "File '#{filepath}' does not exist" unless File.exist?(filepath) + @editor = Editor.new(File.open(filepath, &:readlines)) @original_pathname = filepath @file_edited = false + end - raise ArgumentError, "File doesn't exist" unless File.exist? @original_pathname - @contents = File.open(@original_pathname) { |f| f.readlines } + # return if file has been edited + def file_edited? + @file_edited end #search the file line by line and match each line with the given regex #if matched, replace the whole line with newline. def search_file_replace_line(regex, newline) - search_match(regex, newline, 'r', 1) + @changes = (editor.replace_lines(regex, newline) > 0) || @changes end #search the file line by line and match each line with the given regex #if matched, replace the match (all occurances) with the replace parameter def search_file_replace(regex, replace) - search_match(regex, replace, 'r', 2) + @changes = (editor.replace(regex, replace) > 0) || @changes end #search the file line by line and match each line with the given regex #if matched, delete the line def search_file_delete_line(regex) - search_match(regex, " ", 'd', 1) + @changes = (editor.remove_lines(regex) > 0) || @changes end #search the file line by line and match each line with the given regex #if matched, delete the match (all occurances) from the line def search_file_delete(regex) - search_match(regex, " ", 'd', 2) + search_file_replace(regex, '') end #search the file line by line and match each line with the given regex #if matched, insert newline after each matching line def insert_line_after_match(regex, newline) - search_match(regex, newline, 'i', 1) + @changes = (editor.append_line_after(regex, newline) > 0) || @changes end #search the file line by line and match each line with the given regex #if not matched, insert newline at the end of the file def insert_line_if_no_match(regex, newline) - search_match(regex, newline, 'i', 2) + @changes = (editor.append_line_if_missing(regex, newline) > 0) || @changes + end + + def unwritten_changes? + !!@changes end #Make a copy of old_file and write new file out (only if file changed) def write_file - - # file_edited is false when there was no match in the whole file and thus no contents have changed. - if file_edited + if @changes backup_pathname = original_pathname + ".old" FileUtils.cp(original_pathname, backup_pathname, :preserve => true) File.open(original_pathname, "w") do |newfile| - contents.each do |line| + editor.lines.each do |line| newfile.puts(line) end newfile.flush end + @file_edited = true end - self.file_edited = false - end - - private - - #helper method to do the match, replace, delete, and insert operations - #command is the switch of delete, replace, and insert ('d', 'r', 'i') - #method is to control operation on whole line or only the match (1 for line, 2 for match) - def search_match(regex, replace, command, method) - - #convert regex to a Regexp object (if not already is one) and store it in exp. - exp = Regexp.new(regex) - - #loop through contents and do the appropriate operation depending on 'command' and 'method' - new_contents = [] - - contents.each do |line| - if line.match(exp) - self.file_edited = true - case - when command == 'r' - new_contents << ((method == 1) ? replace : line.gsub!(exp, replace)) - when command == 'd' - if method == 2 - new_contents << line.gsub!(exp, "") - end - when command == 'i' - new_contents << line - new_contents << replace unless method == 2 - end - else - new_contents << line - end - end - if command == 'i' && method == 2 && ! file_edited - new_contents << replace - self.file_edited = true - end - - self.contents = new_contents + @changes = false end end end diff --git a/lib/chef/version.rb b/lib/chef/version.rb index 3c3972d0b4..f55160b56c 100644 --- a/lib/chef/version.rb +++ b/lib/chef/version.rb @@ -17,7 +17,7 @@ class Chef CHEF_ROOT = File.dirname(File.expand_path(File.dirname(__FILE__))) - VERSION = '11.10.4' + VERSION = '11.12.0.rc.0' end # NOTE: the Chef::Version class is defined in version_class.rb diff --git a/lib/chef/win32/api/installer.rb b/lib/chef/win32/api/installer.rb new file mode 100644 index 0000000000..745802d260 --- /dev/null +++ b/lib/chef/win32/api/installer.rb @@ -0,0 +1,166 @@ +# +# Author:: Bryan McLellan <btm@loftninjas.org> +# Copyright:: Copyright (c) 2014 Chef Software, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require 'chef/exceptions' +require 'chef/win32/api' +require 'chef/win32/error' +require 'pathname' + +class Chef + module ReservedNames::Win32 + module API + module Installer + extend Chef::ReservedNames::Win32 + extend Chef::ReservedNames::Win32::API + + ############################################### + # Win32 API Constants + ############################################### + + + ############################################### + # Win32 API Bindings + ############################################### + + ffi_lib 'msi' + +=begin +UINT MsiOpenPackage( + _In_ LPCTSTR szPackagePath, + _Out_ MSIHANDLE *hProduct +); +=end + safe_attach_function :msi_open_package, :MsiOpenPackageExA, [ :string, :int, :pointer ], :int + +=begin +UINT MsiGetProductProperty( + _In_ MSIHANDLE hProduct, + _In_ LPCTSTR szProperty, + _Out_ LPTSTR lpValueBuf, + _Inout_ DWORD *pcchValueBuf +); +=end + safe_attach_function :msi_get_product_property, :MsiGetProductPropertyA, [ :pointer, :pointer, :pointer, :pointer ], :int + +=begin +UINT MsiGetProductInfo( + _In_ LPCTSTR szProduct, + _In_ LPCTSTR szProperty, + _Out_ LPTSTR lpValueBuf, + _Inout_ DWORD *pcchValueBuf +); +=end + safe_attach_function :msi_get_product_info, :MsiGetProductInfoA, [ :pointer, :pointer, :pointer, :pointer ], :int + +=begin +UINT MsiCloseHandle( + _In_ MSIHANDLE hAny +); +=end + safe_attach_function :msi_close_handle, :MsiCloseHandle, [ :pointer ], :int + + ############################################### + # Helpers + ############################################### + + # Opens a Microsoft Installer (MSI) file from an absolute path and returns the specified property + def get_product_property(package_path, property_name) + pkg_ptr = open_package(package_path) + + buffer = 0.chr + buffer_length = FFI::Buffer.new(:long).write_long(0) + + # Fetch the length of the property + status = msi_get_product_property(pkg_ptr.read_pointer, property_name, buffer, buffer_length) + + # We expect error ERROR_MORE_DATA (234) here because we passed a buffer length of 0 + if status != 234 + msg = "msi_get_product_property: returned unknown error #{status} when retrieving #{property_name}: " + msg << Chef::ReservedNames::Win32::Error.format_message(status) + raise Chef::Exceptions::Package, msg + end + + buffer_length = FFI::Buffer.new(:long).write_long(buffer_length.read_long + 1) + buffer = 0.chr * buffer_length.read_long + + # Fetch the property + status = msi_get_product_property(pkg_ptr.read_pointer, property_name, buffer, buffer_length) + + if status != 0 + msg = "msi_get_product_property: returned unknown error #{status} when retrieving #{property_name}: " + msg << Chef::ReservedNames::Win32::Error.format_message(status) + raise Chef::Exceptions::Package, msg + end + + msi_close_handle(pkg_ptr.read_pointer) + return buffer + end + + # Opens a Microsoft Installer (MSI) file from an absolute path and returns a pointer to a handle + # Remember to close the handle with msi_close_handle() + def open_package(package_path) + # MsiOpenPackage expects a perfect absolute Windows path to the MSI + raise ArgumentError, "Provided path '#{package_path}' must be an absolute path" unless Pathname.new(package_path).absolute? + + pkg_ptr = FFI::MemoryPointer.new(:pointer, 4) + status = msi_open_package(package_path, 1, pkg_ptr) + case status + when 0 + # success + else + raise Chef::Exceptions::Package, "msi_open_package: unexpected status #{status}: #{Chef::ReservedNames::Win32::Error.format_message(status)}" + end + return pkg_ptr + end + + # All installed product_codes should have a VersionString + # Returns a version if installed, nil if not installed + def get_installed_version(product_code) + version = 0.chr + version_length = FFI::Buffer.new(:long).write_long(0) + + status = msi_get_product_info(product_code, "VersionString", version, version_length) + + return nil if status == 1605 # ERROR_UNKNOWN_PRODUCT (0x645) + + # We expect error ERROR_MORE_DATA (234) here because we passed a buffer length of 0 + if status != 234 + msg = "msi_get_product_info: product code '#{product_code}' returned unknown error #{status} when retrieving VersionString: " + msg << Chef::ReservedNames::Win32::Error.format_message(status) + raise Chef::Exceptions::Package, msg + end + + # We could fetch the product version now that we know the variable length, but we don't need it here. + + version_length = FFI::Buffer.new(:long).write_long(version_length.read_long + 1) + version = 0.chr * version_length.read_long + + status = msi_get_product_info(product_code, "VersionString", version, version_length) + + if status != 0 + msg = "msi_get_product_info: product code '#{product_code}' returned unknown error #{status} when retrieving VersionString: " + msg << Chef::ReservedNames::Win32::Error.format_message(status) + raise Chef::Exceptions::Package, msg + end + + version + end + end + end + end +end diff --git a/lib/chef/win32/version.rb b/lib/chef/win32/version.rb index e008ff15e8..7f5fcceead 100644 --- a/lib/chef/win32/version.rb +++ b/lib/chef/win32/version.rb @@ -116,9 +116,17 @@ class Chef # http://msdn.microsoft.com/en-us/library/windows/desktop/ms724439(v=vs.85).aspx require 'ruby-wmi' + # CHEF-4888: Work around ruby #2618, expected to be fixed in Ruby 2.1.0 + # https://github.com/ruby/ruby/commit/588504b20f5cc880ad51827b93e571e32446e5db + # https://github.com/ruby/ruby/commit/27ed294c7134c0de582007af3c915a635a6506cd + + WIN32OLE.ole_initialize + os_info = WMI::Win32_OperatingSystem.find(:first) os_version = os_info.send('Version') + WIN32OLE.ole_uninitialize + # The operating system version is a string in the following form # that can be split into components based on the '.' delimiter: # MajorVersionNumber.MinorVersionNumber.BuildNumber diff --git a/spec/data/standalone_cookbook/Gemfile b/spec/data/standalone_cookbook/Gemfile new file mode 100644 index 0000000000..9c961848d8 --- /dev/null +++ b/spec/data/standalone_cookbook/Gemfile @@ -0,0 +1 @@ +source "https://rubygems.org/"
\ No newline at end of file diff --git a/spec/data/standalone_cookbook/chefignore b/spec/data/standalone_cookbook/chefignore new file mode 100644 index 0000000000..cd18e699c1 --- /dev/null +++ b/spec/data/standalone_cookbook/chefignore @@ -0,0 +1,9 @@ +# +# The ignore file allows you to skip files in cookbooks with the same name that appear +# later in the search path. +# + +recipes/ignoreme.rb + # comments can be indented +ignored +vendor/bundle/* diff --git a/spec/data/standalone_cookbook/recipes/default.rb b/spec/data/standalone_cookbook/recipes/default.rb new file mode 100644 index 0000000000..c2fa53be32 --- /dev/null +++ b/spec/data/standalone_cookbook/recipes/default.rb @@ -0,0 +1,3 @@ +# +# Nothing ot see here +#
\ No newline at end of file diff --git a/spec/data/standalone_cookbook/vendor/bundle/ruby/2.0.0/gems/multi_json-1.9.0/lib/multi_json.rb b/spec/data/standalone_cookbook/vendor/bundle/ruby/2.0.0/gems/multi_json-1.9.0/lib/multi_json.rb new file mode 100644 index 0000000000..3b992add1a --- /dev/null +++ b/spec/data/standalone_cookbook/vendor/bundle/ruby/2.0.0/gems/multi_json-1.9.0/lib/multi_json.rb @@ -0,0 +1 @@ +# This is a dummy ruby file
\ No newline at end of file diff --git a/spec/functional/dsl/reboot_pending_spec.rb b/spec/functional/dsl/reboot_pending_spec.rb new file mode 100644 index 0000000000..10d667f7bd --- /dev/null +++ b/spec/functional/dsl/reboot_pending_spec.rb @@ -0,0 +1,118 @@ +# +# Author:: Bryan McLellan <btm@loftninjas.org> +# Copyright:: Copyright (c) 2014 Chef Software, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require "chef/dsl/reboot_pending" +require "chef/win32/registry" +require "spec_helper" + +describe Chef::DSL::RebootPending, :windows_only do + def run_ohai + ohai = Ohai::System.new + # Would be nice to limit this to platform/kernel/arch etc for Ohai 7 + ohai.all_plugins + node.consume_external_attrs(ohai.data,{}) + + ohai + end + + def registry_safe? + !registry.value_exists?('HKLM\SYSTEM\CurrentControlSet\Control\Session Manager', { :name => 'PendingFileRenameOperations' }) || + !registry.key_exists?('HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Auto Update\RebootRequired') || + !registry.key_exists?('HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Component Based Servicing\RebootRequired') || + !registry.key_exists?('HKLM\SOFTWARE\Microsoft\Updates\UpdateExeVolatile') + end + + let(:node) { Chef::Node.new } + let(:events) { Chef::EventDispatch::Dispatcher.new } + let!(:ohai) { run_ohai } # Ensure we have necessary node data + let(:run_context) { Chef::RunContext.new(node, {}, events) } + let(:recipe) { Chef::Recipe.new("a windows cookbook", "the windows recipe", run_context) } + let(:registry) { Chef::Win32::Registry.new(run_context) } + + describe "reboot_pending?" do + + context "when there is nothing to indicate a reboot is pending" do + it { expect(recipe.reboot_pending?).to be_false } + end + + describe 'HKLM\SYSTEM\CurrentControlSet\Control\Session Manager\PendingFileRenameOperations' do + it "returns true if the registry value exists" do + pending "Found existing registry keys" unless registry_safe? + registry.set_value('HKLM\SYSTEM\CurrentControlSet\Control\Session Manager', + { :name => 'PendingFileRenameOperations', :type => :multi_string, :data => ['\??\C:\foo.txt|\??\C:\bar.txt'] }) + + expect(recipe.reboot_pending?).to be_true + end + + after do + if registry_safe? + registry.delete_value('HKLM\SYSTEM\CurrentControlSet\Control\Session Manager', { :name => 'PendingFileRenameOperations' }) + end + end + end + + describe 'HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Auto Update\RebootRequired' do + it "returns true if the registry key exists" do + pending "Found existing registry keys" unless registry_safe? + registry.create_key('HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Auto Update\RebootRequired', false) + + expect(recipe.reboot_pending?).to be_true + end + + after do + if registry_safe? + registry.delete_key('HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Auto Update\RebootRequired', false) + end + end + end + + describe 'HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Component Based Servicing\RebootRequired' do + it "returns true if the registry key exists" do + pending "Permissions are limited to 'TrustedInstaller' by default" + pending "Found existing registry keys" unless registry_safe? + registry.create_key('HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Component Based Servicing\RebootRequired', false) + + expect(recipe.reboot_pending?).to be_true + end + + after do + if registry_safe? + registry.delete_key('HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Component Based Servicing\RebootRequired', false) + end + end + end + + describe 'HKLM\SOFTWARE\Microsoft\Updates\UpdateExeVolatile\Flags' do + it "returns true if the registry key exists" do + pending "Found existing registry keys" unless registry_safe? + registry.create_key('HKLM\SOFTWARE\Microsoft\Updates\UpdateExeVolatile', true) + registry.set_value('HKLM\SOFTWARE\Microsoft\Updates\UpdateExeVolatile', + { :name => 'Flags', :type => :dword, :data => 3 }) + + expect(recipe.reboot_pending?).to be_true + end + + after do + if registry_safe? + registry.delete_value('HKLM\SOFTWARE\Microsoft\Updates\UpdateExeVolatile', { :name => 'Flags' }) + registry.delete_key('HKLM\SOFTWARE\Microsoft\Updates\UpdateExeVolatile', false) + end + end + end + end +end diff --git a/spec/functional/resource/base.rb b/spec/functional/resource/base.rb index 13438c1759..056db39877 100644 --- a/spec/functional/resource/base.rb +++ b/spec/functional/resource/base.rb @@ -22,9 +22,7 @@ def ohai # provider is platform-dependent, we need platform ohai data: @OHAI_SYSTEM ||= begin ohai = Ohai::System.new - ohai.require_plugin("os") - ohai.require_plugin("platform") - ohai.require_plugin("passwd") + ohai.all_plugins("platform") ohai end end diff --git a/spec/functional/resource/deploy_revision_spec.rb b/spec/functional/resource/deploy_revision_spec.rb index 14e6e69d6d..9ff1391e35 100644 --- a/spec/functional/resource/deploy_revision_spec.rb +++ b/spec/functional/resource/deploy_revision_spec.rb @@ -45,7 +45,7 @@ describe Chef::Resource::DeployRevision, :unix_only => true do before(:all) do @ohai = Ohai::System.new - @ohai.require_plugin("os") + @ohai.all_plugins("os") end let(:node) do @@ -78,6 +78,9 @@ describe Chef::Resource::DeployRevision, :unix_only => true do # This is the third version let(:previous_rev) { "6d19a6dbecc8e37f5b2277345885c0c783eb8fb1" } + # This is the second version + let(:second_rev) { "0827e1b0e5043608ac0a824da5c558e252154ad0" } + # This is the sixth version, it is on the "with-deploy-scripts" branch let(:rev_with_in_repo_callbacks) { "2404d015882659754bdb93ad6e4b4d3d02691a82" } @@ -100,6 +103,7 @@ describe Chef::Resource::DeployRevision, :unix_only => true do let(:basic_deploy_resource) do Chef::Resource::DeployRevision.new(deploy_directory, run_context).tap do |r| + r.name "deploy-revision-unit-test" r.repo git_bundle_repo r.symlink_before_migrate({}) r.symlinks({}) @@ -127,6 +131,34 @@ describe Chef::Resource::DeployRevision, :unix_only => true do end end + let(:deploy_to_previous_rev_again) do + basic_deploy_resource.dup.tap do |r| + r.revision(previous_rev) + r.restart_command shell_restart_command(:deploy_to_previous_rev_again) + end + end + + let(:deploy_to_second_rev) do + basic_deploy_resource.dup.tap do |r| + r.revision(second_rev) + r.restart_command shell_restart_command(:deploy_to_second_rev) + end + end + + let(:deploy_to_second_rev_again) do + basic_deploy_resource.dup.tap do |r| + r.revision(second_rev) + r.restart_command shell_restart_command(:deploy_to_second_rev_again) + end + end + + let(:deploy_to_second_rev_again_again) do + basic_deploy_resource.dup.tap do |r| + r.revision(second_rev) + r.restart_command shell_restart_command(:deploy_to_second_rev_again_again) + end + end + # Computes the full path for +path+ relative to the deploy directory def rel_path(path) File.expand_path(path, deploy_directory) @@ -306,6 +338,165 @@ describe Chef::Resource::DeployRevision, :unix_only => true do end end + describe "back to a previously deployed revision where resource rev == latest revision (explicit rollback)" do + before do + deploy_to_previous_rev.run_action(:deploy) + @previous_rev_all_releases = deploy_to_previous_rev.provider_for_action(:deploy).all_releases + deploy_to_latest_rev.run_action(:deploy) + @latest_rev_all_releases = deploy_to_latest_rev.provider_for_action(:deploy).all_releases + deploy_to_latest_rev_again.run_action(:rollback) + @previous_rev_again_all_releases = deploy_to_latest_rev_again.provider_for_action(:deploy).all_releases + end + + the_app_is_deployed_at_revision(:previous_rev) + + it "restarts the application after rolling back" do + actual_operations_order.should == %w[deploy_to_previous_rev deploy_to_latest_rev deploy_to_latest_rev_again] + end + + it "is marked updated" do + deploy_to_latest_rev_again.should be_updated_by_last_action + end + + it "deploys the right code" do + IO.read(rel_path("current/app/app.rb")).should include("this is the third version of the app") + end + + it "all_releases after first deploy should have one entry" do + @previous_rev_all_releases.length.should == 1 + end + + it "all_releases after second deploy should have two entries" do + @latest_rev_all_releases.length.should == 2 + end + + it "all_releases after rollback should have one entry" do + @previous_rev_again_all_releases.length.should == 1 + end + + it "all_releases after rollback should be the same as after the first deploy" do + @previous_rev_again_all_releases.should == @previous_rev_all_releases + end + + end + + describe "back to a previously deployed revision where resource rev == previous revision (explicit rollback)" do + before do + deploy_to_previous_rev.run_action(:deploy) + @previous_rev_all_releases = deploy_to_previous_rev.provider_for_action(:deploy).all_releases + deploy_to_latest_rev.run_action(:deploy) + @latest_rev_all_releases = deploy_to_latest_rev.provider_for_action(:deploy).all_releases + deploy_to_previous_rev_again.run_action(:rollback) + # FIXME: only difference with previous test is using latest_rev_again insetad of previous_rev_again + @previous_rev_again_all_releases = deploy_to_latest_rev_again.provider_for_action(:deploy).all_releases + end + + the_app_is_deployed_at_revision(:previous_rev) + + it "restarts the application after rolling back" do + actual_operations_order.should == %w[deploy_to_previous_rev deploy_to_latest_rev deploy_to_previous_rev_again] + end + + it "is marked updated" do + deploy_to_previous_rev_again.should be_updated_by_last_action + end + + it "deploys the right code" do + IO.read(rel_path("current/app/app.rb")).should include("this is the third version of the app") + end + + it "all_releases after first deploy should have one entry" do + @previous_rev_all_releases.length.should == 1 + end + + it "all_releases after second deploy should have two entries" do + @latest_rev_all_releases.length.should == 2 + end + + it "all_releases after rollback should have one entry" do + @previous_rev_again_all_releases.length.should == 1 + end + + it "all_releases after rollback should be the same as after the first deploy" do + @previous_rev_again_all_releases.should == @previous_rev_all_releases + end + end + + describe "back to a previously deployed revision where resource rev == latest revision (explicit rollback)" do + before do + deploy_to_second_rev.run_action(:deploy) + @first_deploy_all_releases = deploy_to_second_rev.provider_for_action(:deploy).all_releases + deploy_to_previous_rev.run_action(:deploy) + @second_deploy_all_releases = deploy_to_previous_rev.provider_for_action(:deploy).all_releases + deploy_to_previous_rev_again.run_action(:rollback) + @third_deploy_all_releases = deploy_to_previous_rev_again.provider_for_action(:deploy).all_releases + deploy_to_latest_rev.run_action(:deploy) + @fourth_deploy_all_releases = deploy_to_latest_rev.provider_for_action(:deploy).all_releases + deploy_to_latest_rev_again.run_action(:rollback) + @fifth_deploy_all_releases = deploy_to_latest_rev_again.provider_for_action(:deploy).all_releases + end + + the_app_is_deployed_at_revision(:second_rev) + + it "restarts the application after rolling back" do + actual_operations_order.should == %w[deploy_to_second_rev deploy_to_previous_rev deploy_to_previous_rev_again deploy_to_latest_rev deploy_to_latest_rev_again] + end + + it "is marked updated" do + deploy_to_latest_rev_again.should be_updated_by_last_action + end + + it "deploys the right code" do + IO.read(rel_path("current/app/app.rb")).should include("this is the second version of the app") + end + + it "all_releases after rollback should have one entry" do + @fifth_deploy_all_releases.length.should == 1 + end + + it "all_releases after rollback should be the same as after the first deploy" do + @fifth_deploy_all_releases.should == @first_deploy_all_releases + end + end + + describe "back to a previously deployed revision where resource rev == latest revision (explicit rollback)" do + before do + deploy_to_second_rev.run_action(:deploy) + @first_deploy_all_releases = deploy_to_second_rev.provider_for_action(:deploy).all_releases + deploy_to_previous_rev.run_action(:deploy) + @second_deploy_all_releases = deploy_to_previous_rev.provider_for_action(:deploy).all_releases + deploy_to_second_rev_again.run_action(:rollback) + @third_deploy_all_releases = deploy_to_second_rev_again.provider_for_action(:deploy).all_releases + deploy_to_latest_rev.run_action(:deploy) + @fourth_deploy_all_releases = deploy_to_latest_rev.provider_for_action(:deploy).all_releases + deploy_to_second_rev_again_again.run_action(:rollback) + @fifth_deploy_all_releases = deploy_to_second_rev_again_again.provider_for_action(:deploy).all_releases + end + + the_app_is_deployed_at_revision(:second_rev) + + it "restarts the application after rolling back" do + actual_operations_order.should == %w[deploy_to_second_rev deploy_to_previous_rev deploy_to_second_rev_again deploy_to_latest_rev deploy_to_second_rev_again_again] + end + + it "is marked updated" do + deploy_to_second_rev_again_again.should be_updated_by_last_action + end + + it "deploys the right code" do + IO.read(rel_path("current/app/app.rb")).should include("this is the second version of the app") + end + + it "all_releases after rollback should have one entry" do + @fifth_deploy_all_releases.length.should == 1 + end + + it "all_releases after rollback should be the same as after the first deploy" do + @fifth_deploy_all_releases.should == @first_deploy_all_releases + end + + end + # CHEF-3435 describe "to a deploy_to path that does not yet exist" do diff --git a/spec/functional/resource/git_spec.rb b/spec/functional/resource/git_spec.rb index 7ade6eea21..f0bd94b0c0 100644 --- a/spec/functional/resource/git_spec.rb +++ b/spec/functional/resource/git_spec.rb @@ -92,7 +92,7 @@ E before(:all) do @ohai = Ohai::System.new - @ohai.require_plugin("os") + @ohai.all_plugins("os") end context "working with pathes with special characters" do diff --git a/spec/functional/resource/ohai_spec.rb b/spec/functional/resource/ohai_spec.rb new file mode 100644 index 0000000000..b1e4891293 --- /dev/null +++ b/spec/functional/resource/ohai_spec.rb @@ -0,0 +1,65 @@ +# +# Author:: Serdar Sutay (<serdar@opscode.com>) +# Copyright:: Copyright (c) 2014 Opscode, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require 'spec_helper' + +describe Chef::Resource::Ohai do + let(:ohai) { + o = Ohai::System.new + o.all_plugins + o + } + + let(:node) { Chef::Node.new } + + let(:run_context) { + node.default[:platform] = ohai[:platform] + node.default[:platform_version] = ohai[:platform_version] + events = Chef::EventDispatch::Dispatcher.new + Chef::RunContext.new(node, {}, events) + } + + shared_examples_for "reloaded :uptime" do + it "should reload :uptime" do + initial_uptime = ohai[:uptime] + + # Sleep for a second so the uptime gets updated. + sleep 1 + + ohai_resource.run_action(:reload) + node[:uptime].should_not == initial_uptime + end + end + + describe "when reloading all plugins" do + let(:ohai_resource) { Chef::Resource::Ohai.new("reload all", run_context)} + + it_behaves_like "reloaded :uptime" + end + + describe "when reloading only uptime" do + let(:ohai_resource) { + r = Chef::Resource::Ohai.new("reload all", run_context) + r.plugin("uptime") + r + } + + + it_behaves_like "reloaded :uptime" + end +end diff --git a/spec/functional/resource/powershell_spec.rb b/spec/functional/resource/powershell_spec.rb index 6bd3b3c1e5..5001e870a9 100644 --- a/spec/functional/resource/powershell_spec.rb +++ b/spec/functional/resource/powershell_spec.rb @@ -84,7 +84,7 @@ describe Chef::Resource::WindowsScript::PowershellScript, :windows_only do # last line executed -- in this case, we return the status of the # second to last line. This happens because Powershell gives no # way for us to determine whether the last operation was a cmdlet - # or Windows process. Because the latter gives more specified + # or Windows process. Because the latter gives more specific # errors than 0 or 1, we return that instead, which is acceptable # since callers can test for nonzero rather than testing for 1. it "returns 1 if the last command was a cmdlet that failed and was preceded by an unsuccessfully executed non-cmdlet Windows binary" do @@ -111,6 +111,32 @@ describe Chef::Resource::WindowsScript::PowershellScript, :windows_only do resource.run_action(:run) end + it "returns 0 for $false as the last line of the script when convert_boolean_return is false" do + resource.code "$false" + resource.returns(0) + resource.run_action(:run) + end + + it "returns 0 for $true as the last line of the script when convert_boolean_return is false" do + resource.code "$true" + resource.returns(0) + resource.run_action(:run) + end + + it "returns 1 for $false as the last line of the script when convert_boolean_return is true" do + resource.convert_boolean_return true + resource.code "$false" + resource.returns(1) + resource.run_action(:run) + end + + it "returns 0 for $true as the last line of the script when convert_boolean_return is true" do + resource.convert_boolean_return true + resource.code "$true" + resource.returns(0) + resource.run_action(:run) + end + it "executes a script with a 64-bit process on a 64-bit OS, otherwise a 32-bit process" do resource.code(processor_architecture_script_content + " | out-file -encoding ASCII #{script_output_path}") resource.returns(0) @@ -177,6 +203,241 @@ describe Chef::Resource::WindowsScript::PowershellScript, :windows_only do end end + describe "when executing guards" do + + before(:each) do + resource.not_if.clear + resource.only_if.clear + resource.guard_interpreter :powershell_script + end + + it "evaluates a succeeding not_if block using cmd.exe as false by default" do + resource.guard_interpreter :default + resource.not_if "exit /b 0" + resource.should_skip?(:run).should be_true + end + + it "evaluates a failing not_if block using cmd.exe as true by default" do + resource.guard_interpreter :default + resource.not_if "exit /b 2" + resource.should_skip?(:run).should be_false + end + + it "evaluates an succeeding only_if block using cmd.exe as true by default" do + resource.guard_interpreter :default + resource.only_if "exit /b 0" + resource.should_skip?(:run).should be_false + end + + it "evaluates a failing only_if block using cmd.exe as false by default" do + resource.guard_interpreter :default + resource.only_if "exit /b 2" + resource.should_skip?(:run).should be_true + end + + it "evaluates a powershell $false for a not_if block as true" do + resource.not_if "$false" + resource.should_skip?(:run).should be_false + end + + it "evaluates a powershell $true for a not_if block as false" do + resource.not_if "$true" + resource.should_skip?(:run).should be_true + end + + it "evaluates a powershell $false for an only_if block as false" do + resource.only_if "$false" + resource.should_skip?(:run).should be_true + end + + it "evaluates a powershell $true for a only_if block as true" do + resource.only_if "$true" + resource.should_skip?(:run).should be_false + end + + it "evaluates a not_if block using powershell.exe" do + resource.not_if "exit([int32](![System.Environment]::CommandLine.Contains('powershell.exe')))" + resource.should_skip?(:run).should be_true + end + + it "evaluates an only_if block using powershell.exe" do + resource.only_if "exit([int32](![System.Environment]::CommandLine.Contains('powershell.exe')))" + resource.should_skip?(:run).should be_false + end + + it "evaluates a not_if block as false" do + resource.not_if { false } + resource.should_skip?(:run).should be_false + end + + it "evaluates a not_if block as true" do + resource.not_if { true } + resource.should_skip?(:run).should be_true + end + + it "evaluates an only_if block as false" do + resource.only_if { false } + resource.should_skip?(:run).should be_true + end + + it "evaluates an only_if block as true" do + resource.only_if { true } + resource.should_skip?(:run).should be_false + end + + it "evaluates a non-zero powershell exit status for not_if as true" do + resource.not_if "exit 37" + resource.should_skip?(:run).should be_false + end + + it "evaluates a zero powershell exit status for not_if as false" do + resource.not_if "exit 0" + resource.should_skip?(:run).should be_true + end + + it "evaluates a failed executable exit status for not_if as false" do + resource.not_if windows_process_exit_code_not_found_content + resource.should_skip?(:run).should be_false + end + + it "evaluates a successful executable exit status for not_if as true" do + resource.not_if windows_process_exit_code_success_content + resource.should_skip?(:run).should be_true + end + + it "evaluates a failed executable exit status for only_if as false" do + resource.only_if windows_process_exit_code_not_found_content + resource.should_skip?(:run).should be_true + end + + it "evaluates a successful executable exit status for only_if as true" do + resource.only_if windows_process_exit_code_success_content + resource.should_skip?(:run).should be_false + end + + it "evaluates a failed cmdlet exit status for not_if as true" do + resource.not_if "throw 'up'" + resource.should_skip?(:run).should be_false + end + + it "evaluates a successful cmdlet exit status for not_if as true" do + resource.not_if "cd ." + resource.should_skip?(:run).should be_true + end + + it "evaluates a failed cmdlet exit status for only_if as false" do + resource.only_if "throw 'up'" + resource.should_skip?(:run).should be_true + end + + it "evaluates a successful cmdlet exit status for only_if as true" do + resource.only_if "cd ." + resource.should_skip?(:run).should be_false + end + + it "evaluates a not_if block using the cwd guard parameter" do + custom_cwd = "#{ENV['SystemRoot']}\\system32\\drivers\\etc" + resource.not_if "exit ! [int32]($pwd.path -eq '#{custom_cwd}')", :cwd => custom_cwd + resource.should_skip?(:run).should be_true + end + + it "evaluates an only_if block using the cwd guard parameter" do + custom_cwd = "#{ENV['SystemRoot']}\\system32\\drivers\\etc" + resource.only_if "exit ! [int32]($pwd.path -eq '#{custom_cwd}')", :cwd => custom_cwd + resource.should_skip?(:run).should be_false + end + + it "inherits cwd from the parent resource for only_if" do + custom_cwd = "#{ENV['SystemRoot']}\\system32\\drivers\\etc" + resource.cwd custom_cwd + resource.only_if "exit ! [int32]($pwd.path -eq '#{custom_cwd}')" + resource.should_skip?(:run).should be_false + end + + it "inherits cwd from the parent resource for not_if" do + custom_cwd = "#{ENV['SystemRoot']}\\system32\\drivers\\etc" + resource.cwd custom_cwd + resource.not_if "exit ! [int32]($pwd.path -eq '#{custom_cwd}')" + resource.should_skip?(:run).should be_true + end + + it "evaluates a 64-bit resource with a 64-bit guard and interprets boolean false as zero status code", :windows64_only do + resource.architecture :x86_64 + resource.only_if "exit [int32]($env:PROCESSOR_ARCHITECTURE -ne 'AMD64')" + resource.should_skip?(:run).should be_false + end + + it "evaluates a 64-bit resource with a 64-bit guard and interprets boolean true as nonzero status code", :windows64_only do + resource.architecture :x86_64 + resource.only_if "exit [int32]($env:PROCESSOR_ARCHITECTURE -eq 'AMD64')" + resource.should_skip?(:run).should be_true + end + + it "evaluates a 32-bit resource with a 32-bit guard and interprets boolean false as zero status code" do + resource.architecture :i386 + resource.only_if "exit [int32]($env:PROCESSOR_ARCHITECTURE -ne 'X86')" + resource.should_skip?(:run).should be_false + end + + it "evaluates a 32-bit resource with a 32-bit guard and interprets boolean true as nonzero status code" do + resource.architecture :i386 + resource.only_if "exit [int32]($env:PROCESSOR_ARCHITECTURE -eq 'X86')" + resource.should_skip?(:run).should be_true + end + + it "evaluates a simple boolean false as nonzero status code when convert_boolean_return is true for only_if" do + resource.convert_boolean_return true + resource.only_if "$false" + resource.should_skip?(:run).should be_true + end + + it "evaluates a simple boolean false as nonzero status code when convert_boolean_return is true for not_if" do + resource.convert_boolean_return true + resource.not_if "$false" + resource.should_skip?(:run).should be_false + end + + it "evaluates a simple boolean true as 0 status code when convert_boolean_return is true for only_if" do + resource.convert_boolean_return true + resource.only_if "$true" + resource.should_skip?(:run).should be_false + end + + it "evaluates a simple boolean true as 0 status code when convert_boolean_return is true for not_if" do + resource.convert_boolean_return true + resource.not_if "$true" + resource.should_skip?(:run).should be_true + end + + it "evaluates a 32-bit resource with a 32-bit guard and interprets boolean false as zero status code using convert_boolean_return for only_if" do + resource.convert_boolean_return true + resource.architecture :i386 + resource.only_if "$env:PROCESSOR_ARCHITECTURE -eq 'X86'" + resource.should_skip?(:run).should be_false + end + + it "evaluates a 32-bit resource with a 32-bit guard and interprets boolean false as zero status code using convert_boolean_return for not_if" do + resource.convert_boolean_return true + resource.architecture :i386 + resource.not_if "$env:PROCESSOR_ARCHITECTURE -ne 'X86'" + resource.should_skip?(:run).should be_false + end + + it "evaluates a 32-bit resource with a 32-bit guard and interprets boolean true as nonzero status code using convert_boolean_return for only_if" do + resource.convert_boolean_return true + resource.architecture :i386 + resource.only_if "$env:PROCESSOR_ARCHITECTURE -ne 'X86'" + resource.should_skip?(:run).should be_true + end + + it "evaluates a 32-bit resource with a 32-bit guard and interprets boolean true as nonzero status code using convert_boolean_return for not_if" do + resource.convert_boolean_return true + resource.architecture :i386 + resource.not_if "$env:PROCESSOR_ARCHITECTURE -eq 'X86'" + resource.should_skip?(:run).should be_true + end + end + def get_script_output script_output = File.read(script_output_path) end diff --git a/spec/functional/resource/registry_spec.rb b/spec/functional/resource/registry_spec.rb index 455c31c283..2d24eee6a3 100644 --- a/spec/functional/resource/registry_spec.rb +++ b/spec/functional/resource/registry_spec.rb @@ -118,10 +118,10 @@ describe Chef::Resource::RegistryKey, :windows_only do @resource_reporter = Chef::ResourceReporter.new(@rest_client) @events.register(@resource_reporter) - @run_id = @resource_reporter.run_id @run_status = Chef::RunStatus.new(@node, @events) - @resource_reporter.run_started(@run_status) + @run_id = @resource_reporter.run_id + @new_resource.cookbook_name = "monkey" @cookbook_version = double("Cookbook::Version", :version => "1.2.3") @@ -265,7 +265,7 @@ describe Chef::Resource::RegistryKey, :windows_only do @new_resource.key(reg_child + '\Slitheen\Raxicoricofallapatorius') @new_resource.values([{:name=>"BriskWalk",:type=>:string,:data=>"is good for health"}]) @new_resource.recursive(false) - lambda{@new_resource.run_action(:create)}.should_not raise_error + @new_resource.run_action(:create) # should not raise_error @registry.key_exists?(reg_child + '\Slitheen').should == false @registry.key_exists?(reg_child + '\Slitheen\Raxicoricofallapatorius').should == false end @@ -376,7 +376,7 @@ describe Chef::Resource::RegistryKey, :windows_only do @new_resource.key(reg_child + '\Zygons\Zygor') @new_resource.values([{:name=>"BriskWalk",:type=>:string,:data=>"is good for health"}]) @new_resource.recursive(false) - lambda{@new_resource.run_action(:create_if_missing)}.should_not raise_error + @new_resource.run_action(:create_if_missing) # should not raise_error @registry.key_exists?(reg_child + '\Zygons').should == false @registry.key_exists?(reg_child + '\Zygons\Zygor').should == false end @@ -547,7 +547,6 @@ describe Chef::Resource::RegistryKey, :windows_only do @new_resource.values([{:name=>"BriskWalk",:type=>:string,:data=>"is good for health"}]) @new_resource.recursive(false) @new_resource.run_action(:delete_key) - @new_resource.should_not raise_error end it "does nothing if the action is delete_key" do @new_resource.key(reg_parent + '\OpscodeWhyRun') diff --git a/spec/functional/win32/versions_spec.rb b/spec/functional/win32/versions_spec.rb index 0b8a65114c..b983b711da 100644 --- a/spec/functional/win32/versions_spec.rb +++ b/spec/functional/win32/versions_spec.rb @@ -22,7 +22,7 @@ if Chef::Platform.windows? require 'ruby-wmi' end -describe "Chef::ReservedNames::Win32::Version", :windows_only do +describe "Chef::ReservedNames::Win32::Version", :windows_only, :not_supported_on_win2k3 do before do host = WMI::Win32_OperatingSystem.find(:first) @@ -57,7 +57,7 @@ describe "Chef::ReservedNames::Win32::Version", :windows_only do end end end - + context "Win32 version object" do it "should have have one method for each marketing version" do versions = 0 @@ -88,7 +88,7 @@ describe "Chef::ReservedNames::Win32::Version", :windows_only do for_each_windows_version { |method_name| @version.send(method_name.to_sym) } end end - + context "Windows Operating System version" do it "should match the version from WMI" do @current_os_version.should include(@version.marketing_name) diff --git a/spec/integration/client/client_spec.rb b/spec/integration/client/client_spec.rb index bca9ed4854..6357f1e2c0 100644 --- a/spec/integration/client/client_spec.rb +++ b/spec/integration/client/client_spec.rb @@ -215,5 +215,19 @@ EOM result = shell_out("#{chef_client} -c \"#{path_to('config/client.rb')}\" -o 'x::default' -z", :cwd => chef_dir) result.error! end + + it "should complete with success when setting the run list with -r" do + file 'config/client.rb', <<EOM +chef_server_url 'http://omg.com/blah' +cookbook_path "#{path_to('cookbooks')}" +EOM + + result = shell_out("#{chef_client} -c \"#{path_to('config/client.rb')}\" -r 'x::default' -z", :cwd => chef_dir) + result.stdout.should_not include("Overridden Run List") + result.stdout.should include("Run List is [recipe[x::default]]") + #puts result.stdout + result.error! + end + end end diff --git a/spec/integration/knife/chefignore_spec.rb b/spec/integration/knife/chefignore_spec.rb index f2a8d9ad75..4c3d2fa3aa 100644 --- a/spec/integration/knife/chefignore_spec.rb +++ b/spec/integration/knife/chefignore_spec.rb @@ -38,14 +38,13 @@ describe 'chefignore tests' do file 'data_bags/bag1/chefignore', chefignore file 'cookbooks/cookbook1/chefignore', chefignore - it 'nothing is ignored' do + it 'matching files and directories get ignored' do # NOTE: many of the "chefignore" files should probably not show up # themselves, but we have other tests that talk about that knife('list --local -Rfp /').should_succeed <<EOM /cookbooks/ /cookbooks/cookbook1/ /cookbooks/cookbook1/chefignore -/cookbooks/cookbook1/x.json /data_bags/ /data_bags/bag1/ /data_bags/bag1/x.json diff --git a/spec/integration/knife/raw_spec.rb b/spec/integration/knife/raw_spec.rb index 69630061ac..fafd0a47ee 100644 --- a/spec/integration/knife/raw_spec.rb +++ b/spec/integration/knife/raw_spec.rb @@ -22,6 +22,7 @@ require 'chef/knife/show' describe 'knife raw' do extend IntegrationSupport include KnifeSupport + include AppServerSupport when_the_chef_server "has one of each thing" do client 'x', '{}' @@ -55,7 +56,7 @@ EOM end it 'knife raw /blarghle returns 404' do - knife('raw /blarghle').should_fail(/ERROR: Server responded with error 404 "Not Found"/) + knife('raw /blarghle').should_fail(/ERROR: Server responded with error 404 "Not Found\s*"/) end it 'knife raw -m DELETE /roles/x succeeds', :pending => (RUBY_VERSION < "1.9") do @@ -165,19 +166,16 @@ EOM context 'When a server returns raw json' do before :each do - @real_chef_server_url = Chef::Config.chef_server_url Chef::Config.chef_server_url = "http://127.0.0.1:9018" app = lambda do |env| [200, {'Content-Type' => 'application/json' }, ['{ "x": "y", "a": "b" }'] ] end - @raw_server = Puma::Server.new(app, Puma::Events.new(STDERR, STDOUT)) - @raw_server.add_tcp_listener("127.0.0.1", 9018) - @raw_server.run + @raw_server, @raw_server_thread = start_app_server(app, 9018) end after :each do - Chef::Config.chef_server_url = @real_chef_server_url - @raw_server.stop(true) + @raw_server.shutdown if @raw_server + @raw_server_thread.kill if @raw_server_thread end it 'knife raw /blah returns the prettified json', :pending => (RUBY_VERSION < "1.9") do @@ -198,19 +196,16 @@ EOM context 'When a server returns text' do before :each do - @real_chef_server_url = Chef::Config.chef_server_url Chef::Config.chef_server_url = "http://127.0.0.1:9018" app = lambda do |env| [200, {'Content-Type' => 'text' }, ['{ "x": "y", "a": "b" }'] ] end - @raw_server = Puma::Server.new(app, Puma::Events.new(STDERR, STDOUT)) - @raw_server.add_tcp_listener("127.0.0.1", 9018) - @raw_server.run + @raw_server, @raw_server_thread = start_app_server(app, 9018) end after :each do - Chef::Config.chef_server_url = @real_chef_server_url - @raw_server.stop(true) + @raw_server.shutdown if @raw_server + @raw_server_thread.kill if @raw_server_thread end it 'knife raw /blah returns the raw text' do diff --git a/spec/integration/knife/redirection_spec.rb b/spec/integration/knife/redirection_spec.rb index 5af9fd36e1..2ed49a7b24 100644 --- a/spec/integration/knife/redirection_spec.rb +++ b/spec/integration/knife/redirection_spec.rb @@ -15,38 +15,30 @@ # See the License for the specific language governing permissions and # limitations under the License. -require 'puma' require 'support/shared/integration/integration_helper' require 'chef/knife/list' describe 'redirection' do extend IntegrationSupport include KnifeSupport + include AppServerSupport when_the_chef_server 'has a role' do role 'x', {} context 'and another server redirects to it with 302' do before :each do - @real_chef_server_url = Chef::Config.chef_server_url + real_chef_server_url = Chef::Config.chef_server_url Chef::Config.chef_server_url = "http://127.0.0.1:9018" app = lambda do |env| - [302, {'Content-Type' => 'text','Location' => "#{@real_chef_server_url}#{env['PATH_INFO']}" }, ['302 found'] ] - end - @redirector_server = Puma::Server.new(app, Puma::Events.new(STDERR, STDOUT)) - @redirector_server.add_tcp_listener("127.0.0.1", 9018) - @redirector_server.run - Timeout::timeout(5) do - until @redirector_server.running - sleep(0.01) - end - raise @server_error if @server_error + [302, {'Content-Type' => 'text','Location' => "#{real_chef_server_url}#{env['PATH_INFO']}" }, ['302 found'] ] end + @redirector_server, @redirector_server_thread = start_app_server(app, 9018) end after :each do - Chef::Config.chef_server_url = @real_chef_server_url - @redirector_server.stop(true) + @redirector_server.shutdown if @redirector_server + @redirector_thread.kill if @redirector_thread end it 'knife list /roles returns the role' do diff --git a/spec/integration/solo/solo_spec.rb b/spec/integration/solo/solo_spec.rb index cd4678f94d..23ec8d0bad 100644 --- a/spec/integration/solo/solo_spec.rb +++ b/spec/integration/solo/solo_spec.rb @@ -42,6 +42,25 @@ E end + when_the_repository "has a cookbook with an undeclared dependency" do + file 'cookbooks/x/metadata.rb', 'version "1.0.0"' + file 'cookbooks/x/recipes/default.rb', 'include_recipe "ancient::aliens"' + + file 'cookbooks/ancient/metadata.rb', 'version "1.0.0"' + file 'cookbooks/ancient/recipes/aliens.rb', 'print "it was aliens"' + + it "should exit with an error" do + file 'config/solo.rb', <<EOM +cookbook_path "#{path_to('cookbooks')}" +file_cache_path "#{path_to('config/cache')}" +EOM + result = shell_out("ruby bin/chef-solo -c \"#{path_to('config/solo.rb')}\" -o 'x::default' -l debug", :cwd => chef_dir) + result.exitstatus.should == 0 # For CHEF-5120 this becomes 1 + result.stdout.should include("WARN: MissingCookbookDependency") + end + end + + when_the_repository "has a cookbook with a recipe with sleep" do directory 'logs' file 'logs/runs.log', '' diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 5098a1fb85..88e38bdc4b 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -87,8 +87,7 @@ Dir["spec/support/**/*.rb"]. each { |f| require f } OHAI_SYSTEM = Ohai::System.new -OHAI_SYSTEM.require_plugin("os") -OHAI_SYSTEM.require_plugin("platform") +OHAI_SYSTEM.all_plugins("platform") TEST_PLATFORM = OHAI_SYSTEM["platform"].dup.freeze TEST_PLATFORM_VERSION = OHAI_SYSTEM["platform_version"].dup.freeze diff --git a/spec/support/shared/functional/windows_script.rb b/spec/support/shared/functional/windows_script.rb index afeb4c029c..fc06fb55d0 100644 --- a/spec/support/shared/functional/windows_script.rb +++ b/spec/support/shared/functional/windows_script.rb @@ -23,8 +23,7 @@ shared_context Chef::Resource::WindowsScript do before(:all) do ohai_reader = Ohai::System.new - ohai_reader.require_plugin("os") - ohai_reader.require_plugin("windows::platform") + ohai_reader.all_plugins("platform") new_node = Chef::Node.new new_node.consume_external_attrs(ohai_reader.data,{}) @@ -39,7 +38,7 @@ shared_context Chef::Resource::WindowsScript do end before(:each) do -k File.delete(script_output_path) if File.exists?(script_output_path) + File.delete(script_output_path) if File.exists?(script_output_path) end after(:each) do diff --git a/spec/support/shared/integration/app_server_support.rb b/spec/support/shared/integration/app_server_support.rb new file mode 100644 index 0000000000..a0d5e7fa5c --- /dev/null +++ b/spec/support/shared/integration/app_server_support.rb @@ -0,0 +1,42 @@ +# +# Author:: John Keiser (<jkeiser@opscode.com>) +# Author:: Ho-Sheng Hsiao (<hosh@opscode.com>) +# Copyright:: Copyright (c) 2012, 2013 Opscode, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require 'rack' +require 'stringio' + +module AppServerSupport + def start_app_server(app, port) + server = nil + thread = Thread.new do + Rack::Handler::WEBrick.run(app, + :Port => 9018, + :AccessLog => [], + :Logger => WEBrick::Log::new(StringIO.new, 7) + ) do |found_server| + server = found_server + end + end + Timeout::timeout(5) do + until server && server.status == :Running + sleep(0.01) + end + end + [server, thread] + end +end diff --git a/spec/support/shared/integration/integration_helper.rb b/spec/support/shared/integration/integration_helper.rb index 0c4bf990af..abed4c2715 100644 --- a/spec/support/shared/integration/integration_helper.rb +++ b/spec/support/shared/integration/integration_helper.rb @@ -23,6 +23,7 @@ require 'chef/config' require 'chef_zero/rspec' require 'json' require 'support/shared/integration/knife_support' +require 'support/shared/integration/app_server_support' require 'spec_helper' module IntegrationSupport diff --git a/spec/support/shared/unit/script_resource.rb b/spec/support/shared/unit/script_resource.rb index 5f37506df6..1137958420 100644 --- a/spec/support/shared/unit/script_resource.rb +++ b/spec/support/shared/unit/script_resource.rb @@ -48,5 +48,43 @@ shared_examples_for "a script resource" do @resource.flags.should eql("-f") end + describe "when executing guards" do + let(:resource) { @resource } + + before(:each) do + node = Chef::Node.new + + node.automatic[:platform] = "debian" + node.automatic[:platform_version] = "6.0" + + events = Chef::EventDispatch::Dispatcher.new + run_context = Chef::RunContext.new(node, {}, events) + resource.run_context = run_context + resource.code 'echo hi' + end + + it "inherits exactly the :cwd, :environment, :group, :path, :user, and :umask attributes from a parent resource class" do + inherited_difference = Chef::Resource::Script.guard_inherited_attributes - + [:cwd, :environment, :group, :path, :user, :umask ] + + inherited_difference.should == [] + end + + it "when guard_interpreter is set to the default value, the guard command string should be evaluated by command execution and not through a resource" do + Chef::Resource::Conditional.any_instance.should_not_receive(:evaluate_block) + Chef::Resource::Conditional.any_instance.should_receive(:evaluate_command).and_return(true) + Chef::GuardInterpreter::ResourceGuardInterpreter.any_instance.should_not_receive(:evaluate_action) + resource.only_if 'echo hi' + resource.should_skip?(:run).should == nil + end + + it "when a valid guard_interpreter resource is specified, a block should be used to evaluate the guard" do + Chef::GuardInterpreter::DefaultGuardInterpreter.any_instance.should_not_receive(:evaluate) + Chef::GuardInterpreter::ResourceGuardInterpreter.any_instance.should_receive(:evaluate_action).and_return(true) + resource.guard_interpreter :script + resource.only_if 'echo hi' + resource.should_skip?(:run).should == nil + end + end end diff --git a/spec/unit/api_client/registration_spec.rb b/spec/unit/api_client/registration_spec.rb index 845c217f72..15a9c30482 100644 --- a/spec/unit/api_client/registration_spec.rb +++ b/spec/unit/api_client/registration_spec.rb @@ -22,16 +22,45 @@ require 'tempfile' require 'chef/api_client/registration' describe Chef::ApiClient::Registration do + let(:key_location) do make_tmpname("client-registration-key") end - let(:registration) { Chef::ApiClient::Registration.new("silent-bob", key_location) } + let(:client_name) { "silent-bob" } + + subject(:registration) { Chef::ApiClient::Registration.new(client_name, key_location) } - let :private_key_data do + let(:private_key_data) do File.open(Chef::Config[:validation_key], "r") {|f| f.read.chomp } end + let(:http_mock) { double("Chef::REST mock") } + + let(:expected_post_data) do + { :name => client_name, :admin => false } + end + + let(:expected_put_data) do + { :name => client_name, :admin => false, :private_key => true } + end + + let(:server_v10_response) do + {"uri" => "https://chef.local/clients/#{client_name}", + "private_key" => "--begin rsa key etc--"} + end + + # Server v11 includes `json_class` on all replies + let(:server_v11_response) do + response = Chef::ApiClient.new + response.name(client_name) + response.private_key("--begin rsa key etc--") + response + end + + let(:response_409) { Net::HTTPConflict.new("1.1", "409", "Conflict") } + let(:exception_409) { Net::HTTPServerException.new("409 conflict", response_409) } + before do Chef::Config[:validation_client_name] = "test-validator" Chef::Config[:validation_key] = File.expand_path('ssl/private_key.pem', CHEF_SPEC_DATA) @@ -39,8 +68,6 @@ describe Chef::ApiClient::Registration do after do File.unlink(key_location) if File.exist?(key_location) - Chef::Config[:validation_client_name] = nil - Chef::Config[:validation_key] = nil end it "has an HTTP client configured with validator credentials" do @@ -50,57 +77,107 @@ describe Chef::ApiClient::Registration do end describe "when creating/updating the client on the server" do - let(:http_mock) { double("Chef::REST mock") } - before do registration.stub(:http_api).and_return(http_mock) end it "creates a new ApiClient on the server using the validator identity" do - response = {"uri" => "https://chef.local/clients/silent-bob", - "private_key" => "--begin rsa key etc--"} http_mock.should_receive(:post). - with("clients", :name => 'silent-bob', :admin => false). - and_return(response) - registration.create_or_update.should == response + with("clients", expected_post_data). + and_return(server_v10_response) + registration.create_or_update.should == server_v10_response registration.private_key.should == "--begin rsa key etc--" end context "and the client already exists on a Chef 10 server" do it "requests a new key from the server and saves it" do - response = {"name" => "silent-bob", "private_key" => "--begin rsa key etc--" } - - response_409 = Net::HTTPConflict.new("1.1", "409", "Conflict") - exception_409 = Net::HTTPServerException.new("409 conflict", response_409) - - http_mock.should_receive(:post).and_raise(exception_409) + http_mock.should_receive(:post).with("clients", expected_post_data). + and_raise(exception_409) http_mock.should_receive(:put). - with("clients/silent-bob", :name => 'silent-bob', :admin => false, :private_key => true). - and_return(response) - registration.create_or_update.should == response + with("clients/#{client_name}", expected_put_data). + and_return(server_v10_response) + registration.create_or_update.should == server_v10_response registration.private_key.should == "--begin rsa key etc--" end end context "and the client already exists on a Chef 11 server" do it "requests a new key from the server and saves it" do - response = Chef::ApiClient.new - response.name("silent-bob") - response.private_key("--begin rsa key etc--") - - response_409 = Net::HTTPConflict.new("1.1", "409", "Conflict") - exception_409 = Net::HTTPServerException.new("409 conflict", response_409) - http_mock.should_receive(:post).and_raise(exception_409) http_mock.should_receive(:put). - with("clients/silent-bob", :name => 'silent-bob', :admin => false, :private_key => true). - and_return(response) - registration.create_or_update.should == response + with("clients/#{client_name}", expected_put_data). + and_return(server_v11_response) + registration.create_or_update.should == server_v11_response registration.private_key.should == "--begin rsa key etc--" end end end + context "when local key generation is enabled", :nofocus do + let(:generated_private_key_pem) { IO.read(File.expand_path('ssl/private_key.pem', CHEF_SPEC_DATA)) } + let(:generated_private_key) { OpenSSL::PKey::RSA.new(generated_private_key_pem) } + let(:generated_public_key) { generated_private_key.public_key } + + let(:expected_post_data) do + { :name => client_name, :admin => false, :public_key => generated_public_key.to_pem } + end + + let(:expected_put_data) do + { :name => client_name, :admin => false, :public_key => generated_public_key.to_pem } + end + + let(:create_with_pkey_response) do + { + "uri" => "", + "public_key" => generated_public_key.to_pem + } + end + + let(:update_with_pkey_response) do + {"name"=>client_name, + "admin"=>false, + "public_key"=> generated_public_key, + "validator"=>false, + "private_key"=>false, + "clientname"=>client_name} + end + + + before do + registration.stub(:http_api).and_return(http_mock) + Chef::Config.local_key_generation = true + OpenSSL::PKey::RSA.should_receive(:generate).with(2048).and_return(generated_private_key) + end + + it "posts a locally generated public key to the server to create a client" do + http_mock.should_receive(:post). + with("clients", expected_post_data). + and_return(create_with_pkey_response) + registration.create_or_update.should == create_with_pkey_response + registration.private_key.should == generated_private_key_pem + end + + it "puts a locally generated public key to the server to update a client" do + http_mock.should_receive(:post). + with("clients", expected_post_data). + and_raise(exception_409) + http_mock.should_receive(:put). + with("clients/#{client_name}", expected_put_data). + and_return(update_with_pkey_response) + registration.create_or_update.should == update_with_pkey_response + registration.private_key.should == generated_private_key_pem + end + + it "writes the generated private key to disk" do + http_mock.should_receive(:post). + with("clients", expected_post_data). + and_return(create_with_pkey_response) + registration.run + IO.read(key_location).should == generated_private_key_pem + end + + end + describe "when writing the private key to disk" do before do registration.stub(:private_key).and_return('--begin rsa key etc--') @@ -125,16 +202,12 @@ describe Chef::ApiClient::Registration do describe "when registering a client" do - let(:http_mock) { double("Chef::REST mock") } - before do registration.stub(:http_api).and_return(http_mock) end it "creates the client on the server and writes the key" do - response = {"uri" => "http://chef.local/clients/silent-bob", - "private_key" => "--begin rsa key etc--" } - http_mock.should_receive(:post).ordered.and_return(response) + http_mock.should_receive(:post).ordered.and_return(server_v10_response) registration.run IO.read(key_location).should == "--begin rsa key etc--" end @@ -149,9 +222,7 @@ describe Chef::ApiClient::Registration do http_mock.should_receive(:post).ordered.and_raise(exception_500) # 4 http_mock.should_receive(:post).ordered.and_raise(exception_500) # 5 - response = {"uri" => "http://chef.local/clients/silent-bob", - "private_key" => "--begin rsa key etc--" } - http_mock.should_receive(:post).ordered.and_return(response) + http_mock.should_receive(:post).ordered.and_return(server_v10_response) registration.run IO.read(key_location).should == "--begin rsa key etc--" end diff --git a/spec/unit/api_client_spec.rb b/spec/unit/api_client_spec.rb index 4ccd64bafe..8657fa59a8 100644 --- a/spec/unit/api_client_spec.rb +++ b/spec/unit/api_client_spec.rb @@ -164,6 +164,52 @@ describe Chef::ApiClient do end + describe "when loading from JSON" do + before do + end + + before(:each) do + client = { + "name" => "black", + "clientname" => "black", + "public_key" => "crowes", + "private_key" => "monkeypants", + "admin" => true, + "validator" => true, + "json_class" => "Chef::ApiClient" + } + @http_client = double("Chef::REST mock") + Chef::REST.stub(:new).and_return(@http_client) + @http_client.should_receive(:get).with("clients/black").and_return(client) + @client = Chef::ApiClient.load(client['name']) + end + + it "should deserialize to a Chef::ApiClient object" do + @client.should be_a_kind_of(Chef::ApiClient) + end + + it "preserves the name" do + @client.name.should == "black" + end + + it "preserves the public key" do + @client.public_key.should == "crowes" + end + + it "preserves the admin status" do + @client.admin.should be_a_kind_of(Chef::TrueClass) + end + + it "preserves the 'validator' status" do + @client.validator.should be_a_kind_of(Chef::TrueClass) + end + + it "includes the private key if present" do + @client.private_key.should == "monkeypants" + end + + end + describe "with correctly configured API credentials" do before do Chef::Config[:node_name] = "silent-bob" diff --git a/spec/unit/application/client_spec.rb b/spec/unit/application/client_spec.rb index f84932073f..8b4ea6a077 100644 --- a/spec/unit/application/client_spec.rb +++ b/spec/unit/application/client_spec.rb @@ -127,7 +127,7 @@ describe Chef::Application::Client, "configure_chef" do end describe Chef::Application::Client, "run_application", :unix_only do - before do + before(:each) do @pipe = IO.pipe @app = Chef::Application::Client.new @app.stub(:run_chef_client) do @@ -147,4 +147,51 @@ describe Chef::Application::Client, "run_application", :unix_only do IO.select([@pipe[0]], nil, nil, 0).should_not be_nil @pipe[0].gets.should == "finished\n" end + + describe "when splay is set" do + before do + Chef::Config[:splay] = 10 + Chef::Config[:interval] = 10 + + run_count = 0 + + # uncomment to debug failures... + # Chef::Log.init($stderr) + # Chef::Log.level = :debug + + @app.stub(:run_chef_client) do + + run_count += 1 + if run_count > 3 + exit 0 + end + + # If everything is fine, sending USR1 to self should prevent + # app to go into splay sleep forever. + Process.kill("USR1", Process.pid) + end + + number_of_sleep_calls = 0 + + # This is a very complicated way of writing + # @app.should_receive(:sleep).once. + # We have to do it this way because the main loop of + # Chef::Application::Client swallows most exceptions, and we need to be + # able to expose our expectation failures to the parent process in the test. + @app.stub(:sleep) do |arg| + number_of_sleep_calls += 1 + if number_of_sleep_calls > 1 + exit 127 + end + end + end + + it "shouldn't sleep when sent USR1" do + pid = fork do + @app.run_application + end + _pid, result = Process.waitpid2(pid) + result.exitstatus.should == 0 + end + end end diff --git a/spec/unit/client_spec.rb b/spec/unit/client_spec.rb index 58a643d403..9688cce2f4 100644 --- a/spec/unit/client_spec.rb +++ b/spec/unit/client_spec.rb @@ -24,29 +24,54 @@ require 'chef/run_context' require 'chef/rest' require 'rbconfig' -shared_examples_for Chef::Client do +describe Chef::Client do + + let(:hostname) { "hostname" } + let(:machinename) { "machinename.example.org" } + let(:fqdn) { "hostname.example.org" } + + let(:ohai_data) do + { :fqdn => fqdn, + :hostname => hostname, + :machinename => machinename, + :platform => 'example-platform', + :platform_version => 'example-platform-1.0', + :data => {} + } + end + + let(:ohai_system) do + ohai_system = double( "Ohai::System", + :all_plugins => true, + :data => ohai_data) + ohai_system.stub(:[]) do |key| + ohai_data[key] + end + ohai_system + end + + let(:node) do + Chef::Node.new.tap do |n| + n.name(fqdn) + n.chef_environment("_default") + end + end + + let(:json_attribs) { nil } + let(:client_opts) { {} } + + let(:client) do + Chef::Client.new(json_attribs, client_opts).tap do |c| + c.node = node + end + end + before do Chef::Log.logger = Logger.new(StringIO.new) # Node/Ohai data - @hostname = "hostname" - @fqdn = "hostname.example.org" - Chef::Config[:node_name] = @fqdn - ohai_data = { :fqdn => @fqdn, - :hostname => @hostname, - :platform => 'example-platform', - :platform_version => 'example-platform-1.0', - :data => {} } - ohai_data.stub(:all_plugins).and_return(true) - ohai_data.stub(:data).and_return(ohai_data) - Ohai::System.stub(:new).and_return(ohai_data) - - @node = Chef::Node.new - @node.name(@fqdn) - @node.chef_environment("_default") - - @client = Chef::Client.new - @client.node = @node + #Chef::Config[:node_name] = fqdn + Ohai::System.stub(:new).and_return(ohai_system) end describe "authentication protocol selection" do @@ -58,7 +83,7 @@ shared_examples_for Chef::Client do it "does not force the authentication protocol to 1.1" do Chef::Config[:node_name] = ("f" * 90) # ugly that this happens as a side effect of a getter :( - @client.node_name + client.node_name Chef::Config[:authentication_protocol_version].should == "1.0" end end @@ -67,7 +92,7 @@ shared_examples_for Chef::Client do it "sets the authentication protocol to version 1.1" do Chef::Config[:node_name] = ("f" * 91) # ugly that this happens as a side effect of a getter :( - @client.node_name + client.node_name Chef::Config[:authentication_protocol_version].should == "1.1" end end @@ -75,9 +100,6 @@ shared_examples_for Chef::Client do describe "configuring output formatters" do context "when no formatter has been configured" do - before do - @client = Chef::Client.new - end context "and STDOUT is a TTY" do before do @@ -85,7 +107,7 @@ shared_examples_for Chef::Client do end it "configures the :doc formatter" do - @client.formatters_for_run.should == [[:doc]] + client.formatters_for_run.should == [[:doc]] end context "and force_logger is set" do @@ -95,7 +117,7 @@ shared_examples_for Chef::Client do it "configures the :null formatter" do Chef::Config[:force_logger].should be_true - @client.formatters_for_run.should == [[:null]] + client.formatters_for_run.should == [[:null]] end end @@ -108,7 +130,7 @@ shared_examples_for Chef::Client do end it "configures the :null formatter" do - @client.formatters_for_run.should == [[:null]] + client.formatters_for_run.should == [[:null]] end context "and force_formatter is set" do @@ -116,7 +138,7 @@ shared_examples_for Chef::Client do Chef::Config[:force_formatter] = true end it "it configures the :doc formatter" do - @client.formatters_for_run.should == [[:doc]] + client.formatters_for_run.should == [[:doc]] end end end @@ -126,16 +148,15 @@ shared_examples_for Chef::Client do context "when a formatter is configured" do context "with no output path" do before do - @client = Chef::Client.new Chef::Config.add_formatter(:min) end it "does not configure a default formatter" do - @client.formatters_for_run.should == [[:min, nil]] + client.formatters_for_run.should == [[:min, nil]] end it "configures the formatter for STDOUT/STDERR" do - configured_formatters = @client.configure_formatters + configured_formatters = client.configure_formatters min_formatter = configured_formatters[0] min_formatter.output.out.should == STDOUT min_formatter.output.err.should == STDERR @@ -144,7 +165,6 @@ shared_examples_for Chef::Client do context "with an output path" do before do - @client = Chef::Client.new @tmpout = Tempfile.open("rspec-for-client-formatter-selection-#{Process.pid}") Chef::Config.add_formatter(:min, @tmpout.path) end @@ -155,7 +175,7 @@ shared_examples_for Chef::Client do end it "configures the formatter for the file path" do - configured_formatters = @client.configure_formatters + configured_formatters = client.configure_formatters min_formatter = configured_formatters[0] min_formatter.output.out.path.should == @tmpout.path min_formatter.output.err.path.should == @tmpout.path @@ -165,93 +185,216 @@ shared_examples_for Chef::Client do end end - describe "run" do - - it "should identify the node and run ohai, then register the client" do - mock_chef_rest_for_node = double("Chef::REST (node)") - mock_chef_rest_for_cookbook_sync = double("Chef::REST (cookbook sync)") - mock_chef_rest_for_node_save = double("Chef::REST (node save)") - mock_chef_runner = double("Chef::Runner") - - # --Client.register - # Make sure Client#register thinks the client key doesn't - # exist, so it tries to register and create one. - File.should_receive(:exists?).with(Chef::Config[:client_key]).exactly(1).times.and_return(false) - - # Client.register will register with the validation client name. - Chef::ApiClient::Registration.any_instance.should_receive(:run) - # Client.register will then turn around create another - # Chef::REST object, this time with the client key it got from the - # previous step. - Chef::REST.should_receive(:new).with(Chef::Config[:chef_server_url], @fqdn, Chef::Config[:client_key]).exactly(1).and_return(mock_chef_rest_for_node) - - # --Client#build_node - # looks up the node, which we will return, then later saves it. - Chef::Node.should_receive(:find_or_create).with(@fqdn).and_return(@node) - - # --ResourceReporter#node_load_completed - # gets a run id from the server for storing resource history - # (has its own tests, so stubbing it here.) - Chef::ResourceReporter.any_instance.should_receive(:node_load_completed) - - # --ResourceReporter#run_completed - # updates the server with the resource history - # (has its own tests, so stubbing it here.) - Chef::ResourceReporter.any_instance.should_receive(:run_completed) - # --Client#setup_run_context - # ---Client#sync_cookbooks -- downloads the list of cookbooks to sync - # - Chef::CookbookSynchronizer.any_instance.should_receive(:sync_cookbooks) - Chef::REST.should_receive(:new).with(Chef::Config[:chef_server_url]).and_return(mock_chef_rest_for_cookbook_sync) - mock_chef_rest_for_cookbook_sync.should_receive(:post).with("environments/_default/cookbook_versions", {:run_list => []}).and_return({}) - - # --Client#converge - Chef::Runner.should_receive(:new).and_return(mock_chef_runner) - mock_chef_runner.should_receive(:converge).and_return(true) - - # --Client#save_updated_node - Chef::REST.should_receive(:new).with(Chef::Config[:chef_server_url]).and_return(mock_chef_rest_for_node_save) - mock_chef_rest_for_node_save.should_receive(:put_rest).with("nodes/#{@fqdn}", @node).and_return(true) - - Chef::RunLock.any_instance.should_receive(:acquire) - Chef::RunLock.any_instance.should_receive(:save_pid) - Chef::RunLock.any_instance.should_receive(:release) - - # Post conditions: check that node has been filled in correctly - @client.should_receive(:run_started) - @client.should_receive(:run_completed_successfully) + describe "a full client run" do + shared_examples_for "a successful client run" do + let(:http_node_load) { double("Chef::REST (node)") } + let(:http_cookbook_sync) { double("Chef::REST (cookbook sync)") } + let(:http_node_save) { double("Chef::REST (node save)") } + let(:runner) { double("Chef::Runner") } - if(Chef::Config[:client_fork] && !windows?) - require 'stringio' - if(Chef::Config[:pipe_node]) - pipe_sim = StringIO.new - pipe_sim.should_receive(:close).exactly(4).and_return(nil) - res = '' - pipe_sim.should_receive(:puts) do |string| - res.replace(string) - end - pipe_sim.should_receive(:gets).and_return(res) - IO.should_receive(:pipe).and_return([pipe_sim, pipe_sim]) - IO.should_receive(:select).and_return(true) + let(:api_client_exists?) { false } + + let(:stdout) { StringIO.new } + let(:stderr) { StringIO.new } + + let(:enable_fork) { false } + + def stub_for_register + # --Client.register + # Make sure Client#register thinks the client key doesn't + # exist, so it tries to register and create one. + File.should_receive(:exists?).with(Chef::Config[:client_key]).exactly(1).times.and_return(api_client_exists?) + + unless api_client_exists? + # Client.register will register with the validation client name. + Chef::ApiClient::Registration.any_instance.should_receive(:run) end - proc_ret = Class.new.new - proc_ret.should_receive(:success?).and_return(true) - Process.should_receive(:waitpid2).and_return([1, proc_ret]) - @client.should_receive(:exit).and_return(nil) - @client.should_receive(:fork) do |&block| - block.call + end + + def stub_for_node_load + # Client.register will then turn around create another + # Chef::REST object, this time with the client key it got from the + # previous step. + Chef::REST.should_receive(:new). + with(Chef::Config[:chef_server_url], fqdn, Chef::Config[:client_key]). + exactly(1). + and_return(http_node_load) + + # --Client#build_node + # looks up the node, which we will return, then later saves it. + Chef::Node.should_receive(:find_or_create).with(fqdn).and_return(node) + + # --ResourceReporter#node_load_completed + # gets a run id from the server for storing resource history + # (has its own tests, so stubbing it here.) + Chef::ResourceReporter.any_instance.should_receive(:node_load_completed) + end + + def stub_for_sync_cookbooks + # --Client#setup_run_context + # ---Client#sync_cookbooks -- downloads the list of cookbooks to sync + # + Chef::CookbookSynchronizer.any_instance.should_receive(:sync_cookbooks) + Chef::REST.should_receive(:new).with(Chef::Config[:chef_server_url]).and_return(http_cookbook_sync) + http_cookbook_sync.should_receive(:post). + with("environments/_default/cookbook_versions", {:run_list => []}). + and_return({}) + end + + def stub_for_converge + # --Client#converge + Chef::Runner.should_receive(:new).and_return(runner) + runner.should_receive(:converge).and_return(true) + + # --ResourceReporter#run_completed + # updates the server with the resource history + # (has its own tests, so stubbing it here.) + Chef::ResourceReporter.any_instance.should_receive(:run_completed) + end + + def stub_for_node_save + # --Client#save_updated_node + Chef::REST.should_receive(:new).with(Chef::Config[:chef_server_url]).and_return(http_node_save) + http_node_save.should_receive(:put_rest).with("nodes/#{fqdn}", node).and_return(true) + end + + def stub_for_run + Chef::RunLock.any_instance.should_receive(:acquire) + Chef::RunLock.any_instance.should_receive(:save_pid) + Chef::RunLock.any_instance.should_receive(:release) + + # Post conditions: check that node has been filled in correctly + client.should_receive(:run_started) + client.should_receive(:run_completed_successfully) + end + + before do + Chef::Config[:client_fork] = enable_fork + + stub_const("Chef::Client::STDOUT_FD", stdout) + stub_const("Chef::Client::STDERR_FD", stderr) + + stub_for_register + stub_for_node_load + stub_for_sync_cookbooks + stub_for_converge + stub_for_node_save + stub_for_run + end + + it "runs ohai, sets up authentication, loads node state, synchronizes policy, and converges" do + # This is what we're testing. + client.run + + # fork is stubbed, so we can see the outcome of the run + node.automatic_attrs[:platform].should == "example-platform" + node.automatic_attrs[:platform_version].should == "example-platform-1.0" + end + end + + + describe "when running chef-client without fork" do + + include_examples "a successful client run" + end + + describe "when running chef-client with forking enabled", :unix_only do + include_examples "a successful client run" do + let(:process_status) do + double("Process::Status") + end + + let(:enable_fork) { true } + + before do + Process.should_receive(:waitpid2).and_return([1, process_status]) + + process_status.should_receive(:success?).and_return(true) + client.should_receive(:exit).and_return(nil) + client.should_receive(:fork).and_yield end end - # This is what we're testing. - @client.run + end + + describe "when the client key already exists" do + + let(:api_client_exists?) { true } + + include_examples "a successful client run" + end + + describe "when an override run list is given" do + let(:client_opts) { {:override_runlist => "recipe[override_recipe]"} } - if(!Chef::Config[:client_fork] || Chef::Config[:pipe_node]) - @node.automatic_attrs[:platform].should == "example-platform" - @node.automatic_attrs[:platform_version].should == "example-platform-1.0" + it "should permit spaces in overriding run list" do + Chef::Client.new(nil, :override_runlist => 'role[a], role[b]') + end + + describe "when running the client" do + include_examples "a successful client run" do + + before do + # Client will try to compile and run override_recipe + Chef::RunContext::CookbookCompiler.any_instance.should_receive(:compile) + end + + def stub_for_sync_cookbooks + # --Client#setup_run_context + # ---Client#sync_cookbooks -- downloads the list of cookbooks to sync + # + Chef::CookbookSynchronizer.any_instance.should_receive(:sync_cookbooks) + Chef::REST.should_receive(:new).with(Chef::Config[:chef_server_url]).and_return(http_cookbook_sync) + http_cookbook_sync.should_receive(:post). + with("environments/_default/cookbook_versions", {:run_list => ["override_recipe"]}). + and_return({}) + end + + def stub_for_node_save + # Expect NO node save + node.should_not_receive(:save) + end + end end end + describe "when a permanent run list is passed as an option" do + + include_examples "a successful client run" do + + let(:new_runlist) { "recipe[new_run_list_recipe]" } + let(:client_opts) { {:runlist => new_runlist} } + + def stub_for_sync_cookbooks + # --Client#setup_run_context + # ---Client#sync_cookbooks -- downloads the list of cookbooks to sync + # + Chef::CookbookSynchronizer.any_instance.should_receive(:sync_cookbooks) + Chef::REST.should_receive(:new).with(Chef::Config[:chef_server_url]).and_return(http_cookbook_sync) + http_cookbook_sync.should_receive(:post). + with("environments/_default/cookbook_versions", {:run_list => ["new_run_list_recipe"]}). + and_return({}) + end + + before do + # Client will try to compile and run the new_run_list_recipe, but we + # do not create a fixture for this. + Chef::RunContext::CookbookCompiler.any_instance.should_receive(:compile) + end + + it "sets the new run list on the node" do + client.run + node.run_list.should == Chef::RunList.new(new_runlist) + end + + end + end + + end + + + describe "when handling run failures" do + it "should remove the run_lock on failure of #load_node" do @run_lock = double("Chef::RunLock", :acquire => true) Chef::RunLock.stub(:new).and_return(@run_lock) @@ -260,64 +403,64 @@ shared_examples_for Chef::Client do Chef::EventDispatch::Dispatcher.stub(:new).and_return(@events) # @events is created on Chef::Client.new, so we need to recreate it after mocking - @client = Chef::Client.new - @client.stub(:load_node).and_raise(Exception) + client = Chef::Client.new + client.stub(:load_node).and_raise(Exception) @run_lock.should_receive(:release) if(Chef::Config[:client_fork] && !windows?) - @client.should_receive(:fork) do |&block| + client.should_receive(:fork) do |&block| block.call end end - lambda { @client.run }.should raise_error(Exception) + lambda { client.run }.should raise_error(Exception) end + end - describe "when notifying other objects of the status of the chef run" do - before do - Chef::Client.clear_notifications - Chef::Node.stub(:find_or_create).and_return(@node) - @node.stub(:save) - @client.load_node - @client.build_node - end - - it "notifies observers that the run has started" do - notified = false - Chef::Client.when_run_starts do |run_status| - run_status.node.should == @node - notified = true - end + describe "when notifying other objects of the status of the chef run" do + before do + Chef::Client.clear_notifications + Chef::Node.stub(:find_or_create).and_return(node) + node.stub(:save) + client.load_node + client.build_node + end - @client.run_started - notified.should be_true + it "notifies observers that the run has started" do + notified = false + Chef::Client.when_run_starts do |run_status| + run_status.node.should == node + notified = true end - it "notifies observers that the run has completed successfully" do - notified = false - Chef::Client.when_run_completes_successfully do |run_status| - run_status.node.should == @node - notified = true - end + client.run_started + notified.should be_true + end - @client.run_completed_successfully - notified.should be_true + it "notifies observers that the run has completed successfully" do + notified = false + Chef::Client.when_run_completes_successfully do |run_status| + run_status.node.should == node + notified = true end - it "notifies observers that the run failed" do - notified = false - Chef::Client.when_run_fails do |run_status| - run_status.node.should == @node - notified = true - end + client.run_completed_successfully + notified.should be_true + end - @client.run_failed - notified.should be_true + it "notifies observers that the run failed" do + notified = false + Chef::Client.when_run_fails do |run_status| + run_status.node.should == node + notified = true end + + client.run_failed + notified.should be_true end end describe "build_node" do it "should expand the roles and recipes for the node" do - @node.run_list << "role[role_containing_cookbook1]" + node.run_list << "role[role_containing_cookbook1]" role_containing_cookbook1 = Chef::Role.new role_containing_cookbook1.name("role_containing_cookbook1") role_containing_cookbook1.run_list << "cookbook1" @@ -329,37 +472,33 @@ shared_examples_for Chef::Client do Chef::REST.should_receive(:new).and_return(mock_chef_rest) # check pre-conditions. - @node[:roles].should be_nil - @node[:recipes].should be_nil + node[:roles].should be_nil + node[:recipes].should be_nil - @client.policy_builder.stub(:node).and_return(@node) + client.policy_builder.stub(:node).and_return(node) # chefspec and possibly others use the return value of this method - @client.build_node.should == @node + client.build_node.should == node # check post-conditions. - @node[:roles].should_not be_nil - @node[:roles].length.should == 1 - @node[:roles].should include("role_containing_cookbook1") - @node[:recipes].should_not be_nil - @node[:recipes].length.should == 1 - @node[:recipes].should include("cookbook1") + node[:roles].should_not be_nil + node[:roles].length.should == 1 + node[:roles].should include("role_containing_cookbook1") + node[:recipes].should_not be_nil + node[:recipes].length.should == 1 + node[:recipes].should include("cookbook1") end end describe "windows_admin_check" do - before do - @client = Chef::Client.new - end - context "platform is not windows" do before do Chef::Platform.stub(:windows?).and_return(false) end it "shouldn't be called" do - @client.should_not_receive(:has_admin_privileges?) - @client.do_windows_admin_check + client.should_not_receive(:has_admin_privileges?) + client.do_windows_admin_check end end @@ -369,91 +508,46 @@ shared_examples_for Chef::Client do end it "should be called" do - @client.should_receive(:has_admin_privileges?) - @client.do_windows_admin_check + client.should_receive(:has_admin_privileges?) + client.do_windows_admin_check end context "admin privileges exist" do before do - @client.should_receive(:has_admin_privileges?).and_return(true) + client.should_receive(:has_admin_privileges?).and_return(true) end it "should not log a warning message" do Chef::Log.should_not_receive(:warn) - @client.do_windows_admin_check + client.do_windows_admin_check end context "fatal admin check is configured" do it "should not raise an exception" do - @client.do_windows_admin_check.should_not raise_error + client.do_windows_admin_check #should not raise end end end context "admin privileges doesn't exist" do before do - @client.should_receive(:has_admin_privileges?).and_return(false) + client.should_receive(:has_admin_privileges?).and_return(false) end it "should log a warning message" do Chef::Log.should_receive(:warn) - @client.do_windows_admin_check + client.do_windows_admin_check end context "fatal admin check is configured" do it "should raise an exception" do - @client.do_windows_admin_check.should_not raise_error + client.do_windows_admin_check # should not raise end end end end end - describe "when a run list override is provided" do - before do - @node = Chef::Node.new - @node.name(@fqdn) - @node.chef_environment("_default") - @node.automatic_attrs[:platform] = "example-platform" - @node.automatic_attrs[:platform_version] = "example-platform-1.0" - end - - it "should permit spaces in overriding run list" do - @client = Chef::Client.new(nil, :override_runlist => 'role[a], role[b]') - end - - it "should override the run list and skip the final node save" do - @client = Chef::Client.new(nil, :override_runlist => 'role[test_role]') - @client.node = @node - - @node.run_list << "role[role_containing_cookbook1]" - - override_role = Chef::Role.new - override_role.name 'test_role' - override_role.run_list << 'cookbook1' - - original_runlist = @node.run_list.dup - - mock_chef_rest = double("Chef::REST") - mock_chef_rest.should_receive(:get_rest).with("roles/test_role").and_return(override_role) - Chef::REST.should_receive(:new).and_return(mock_chef_rest) - - @node.should_not_receive(:save) - - @client.policy_builder.stub(:node).and_return(@node) - @client.policy_builder.build_node - - @node[:roles].should_not be_nil - @node[:roles].should eql(['test_role']) - @node[:recipes].should eql(['cookbook1']) - - @client.save_updated_node - - @node.run_list.should == original_runlist - - end - end - describe "assert_cookbook_path_not_empty" do before do Chef::Config[:solo] = true @@ -462,24 +556,46 @@ shared_examples_for Chef::Client do context "when any directory of cookbook_path contains no cookbook" do it "raises CookbookNotFound error" do expect do - @client.send(:assert_cookbook_path_not_empty, nil) + client.send(:assert_cookbook_path_not_empty, nil) end.to raise_error(Chef::Exceptions::CookbookNotFound, 'None of the cookbook paths set in Chef::Config[:cookbook_path], ["/path/to/invalid/cookbook_path"], contain any cookbooks') end end end -end + describe "setting node name" do + context "when machinename, hostname and fqdn are all set" do + it "favors the fqdn" do + expect(client.node_name).to eql(fqdn) + end + end -describe Chef::Client do - Chef::Config[:client_fork] = false - it_behaves_like Chef::Client -end + context "when fqdn is missing" do + # ohai 7 should always have machinename == return of hostname + let(:fqdn) { nil } + it "favors the machinename" do + expect(client.node_name).to eql(machinename) + end + end -describe "Chef::Client Forked" do - before do - Chef::Config[:client_fork] = true - end + context "when fqdn and machinename are missing" do + # ohai 6 will not have machinename, return the short hostname + let(:fqdn) { nil } + let(:machinename) { nil } + it "falls back to hostname" do + expect(client.node_name).to eql(hostname) + end + end + + context "when they're all missing" do + let(:machinename) { nil } + let(:hostname) { nil } + let(:fqdn) { nil } - it_behaves_like Chef::Client + it "throws an exception" do + expect { client.node_name }.to raise_error(Chef::Exceptions::CannotDetermineNodeName) + end + end + end end + diff --git a/spec/unit/cookbook/chefignore_spec.rb b/spec/unit/cookbook/chefignore_spec.rb index aacb60c012..e529a6d05a 100644 --- a/spec/unit/cookbook/chefignore_spec.rb +++ b/spec/unit/cookbook/chefignore_spec.rb @@ -36,4 +36,14 @@ describe Chef::Cookbook::Chefignore do @chefignore.ignored?('recipes/ignoreme.rb').should be_true @chefignore.ignored?('recipes/dontignoreme.rb').should be_false end + + context "when using the single cookbook pattern" do + before do + @chefignore = Chef::Cookbook::Chefignore.new(File.join(CHEF_SPEC_DATA, 'standalone_cookbook')) + end + + it "loads the globs in the chefignore file" do + @chefignore.ignores.should =~ %w[recipes/ignoreme.rb ignored vendor/bundle/*] + end + end end diff --git a/spec/unit/cookbook/metadata_spec.rb b/spec/unit/cookbook/metadata_spec.rb index cba2aff5da..88c4a1a5f5 100644 --- a/spec/unit/cookbook/metadata_spec.rb +++ b/spec/unit/cookbook/metadata_spec.rb @@ -402,7 +402,7 @@ describe Chef::Cookbook::Metadata do @meta.attributes["db/mysql/databases"][:recipes].should == [] end - it "should allow the default value to be a string, array, or hash" do + it "should allow the default value to be a string, array, hash, boolean or numeric" do lambda { @meta.attribute("db/mysql/databases", :default => []) }.should_not raise_error @@ -413,10 +413,54 @@ describe Chef::Cookbook::Metadata do @meta.attribute("db/mysql/databases", :default => "alice in chains") }.should_not raise_error lambda { + @meta.attribute("db/mysql/databases", :default => 1337) + }.should_not raise_error + lambda { + @meta.attribute("db/mysql/databases", :default => true) + }.should_not raise_error + lambda { @meta.attribute("db/mysql/databases", :required => :not_gonna_do_it) }.should raise_error(ArgumentError) end + it "should limit the types allowed in the choice array" do + options = { + :type => "string", + :choice => [ "test1", "test2" ], + :default => "test1" + } + lambda { + @meta.attribute("test_cookbook/test", options) + }.should_not raise_error + + options = { + :type => "boolean", + :choice => [ true, false ], + :default => true + } + lambda { + @meta.attribute("test_cookbook/test", options) + }.should_not raise_error + + options = { + :type => "numeric", + :choice => [ 1337, 420 ], + :default => 1337 + } + lambda { + @meta.attribute("test_cookbook/test", options) + }.should_not raise_error + + options = { + :type => "numeric", + :choice => [ true, "false" ], + :default => false + } + lambda { + @meta.attribute("test_cookbook/test", options) + }.should raise_error + end + it "should error if default used with calculated" do lambda { attrs = { diff --git a/spec/unit/cookbook/syntax_check_spec.rb b/spec/unit/cookbook/syntax_check_spec.rb index 85d6950a45..a674f6ab40 100644 --- a/spec/unit/cookbook/syntax_check_spec.rb +++ b/spec/unit/cookbook/syntax_check_spec.rb @@ -24,11 +24,21 @@ describe Chef::Cookbook::SyntaxCheck do let(:cookbook_path) { File.join(CHEF_SPEC_DATA, 'cookbooks', 'openldap') } let(:syntax_check) { Chef::Cookbook::SyntaxCheck.new(cookbook_path) } + let(:open_ldap_cookbook_files) { + %w{ attributes/default.rb + attributes/smokey.rb + definitions/client.rb + definitions/server.rb + metadata.rb + recipes/default.rb + recipes/gigantor.rb + recipes/one.rb }.map{ |f| File.join(cookbook_path, f) } +} + before do Chef::Log.logger = Logger.new(StringIO.new) Chef::Log.level = :warn # suppress "Syntax OK" messages - @attr_files = %w{default.rb smokey.rb}.map { |f| File.join(cookbook_path, 'attributes', f) } @defn_files = %w{client.rb server.rb}.map { |f| File.join(cookbook_path, 'definitions', f)} @recipes = %w{default.rb gigantor.rb one.rb}.map { |f| File.join(cookbook_path, 'recipes', f) } @@ -48,6 +58,23 @@ describe Chef::Cookbook::SyntaxCheck do Chef::Config[:cookbook_path] = File.dirname(cookbook_path) syntax_check = Chef::Cookbook::SyntaxCheck.for_cookbook(:openldap) syntax_check.cookbook_path.should == cookbook_path + syntax_check.ruby_files.sort.should == open_ldap_cookbook_files.sort + end + + it "creates a syntax checker given the cookbook name and cookbook_path" do + syntax_check = Chef::Cookbook::SyntaxCheck.for_cookbook(:openldap, File.join(CHEF_SPEC_DATA, 'cookbooks')) + syntax_check.cookbook_path.should == cookbook_path + syntax_check.ruby_files.sort.should == open_ldap_cookbook_files.sort + end + + context "when using a standalone cookbook" do + let(:cookbook_path) { File.join(CHEF_SPEC_DATA, 'standalone_cookbook') } + + it "creates a syntax checker given the cookbook name and cookbook_path for a standalone cookbook" do + syntax_check = Chef::Cookbook::SyntaxCheck.for_cookbook(:standalone_cookbook, CHEF_SPEC_DATA) + syntax_check.cookbook_path.should == cookbook_path + syntax_check.ruby_files.should == [File.join(cookbook_path, 'recipes/default.rb')] + end end describe "when first created" do diff --git a/spec/unit/cookbook_spec.rb b/spec/unit/cookbook_spec.rb index ca4f4adc08..9bcea97d98 100644 --- a/spec/unit/cookbook_spec.rb +++ b/spec/unit/cookbook_spec.rb @@ -68,16 +68,6 @@ describe Chef::CookbookVersion do @cookbook.preferred_filename(@node, :files, 'a-filename', 'the-checksum').should be_nil end - it "should allow you to include a fully-qualified recipe using the DSL" do - # DSL method include_recipe allows multiple arguments, so extract the first - @node.should_receive(:loaded_recipe).with(:openldap, "gigantor") - recipe = @run_context.include_recipe("openldap::gigantor").first - - recipe.recipe_name.should == "gigantor" - recipe.cookbook_name.should == :openldap - @run_context.resource_collection[0].name.should == "blanket" - end - it "should raise an ArgumentException if you try to load a bad recipe name" do lambda { @cookbook.load_recipe("doesnt_exist", @node) }.should raise_error(ArgumentError) end diff --git a/spec/unit/dsl/reboot_pending_spec.rb b/spec/unit/dsl/reboot_pending_spec.rb new file mode 100644 index 0000000000..8576ae168a --- /dev/null +++ b/spec/unit/dsl/reboot_pending_spec.rb @@ -0,0 +1,100 @@ +# +# Author:: Bryan McLellan <btm@loftninjas.org> +# Copyright:: Copyright (c) 2014 Chef Software, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require "chef/dsl/reboot_pending" +require "spec_helper" + +describe Chef::DSL::RebootPending do + describe "reboot_pending?" do + describe "in isoloation" do + let(:recipe) { Object.new.extend(Chef::DSL::RebootPending) } + + before do + recipe.stub(:platform?).and_return(false) + end + + context "platform is windows" do + before do + recipe.stub(:platform?).with('windows').and_return(true) + recipe.stub(:registry_key_exists?).and_return(false) + recipe.stub(:registry_value_exists?).and_return(false) + end + + it 'should return true if "HKLM\SYSTEM\CurrentControlSet\Control\Session Manager\PendingFileRenameOperations" exists' do + recipe.stub(:registry_value_exists?).with('HKLM\SYSTEM\CurrentControlSet\Control\Session Manager', { :name => 'PendingFileRenameOperations' }).and_return(true) + expect(recipe.reboot_pending?).to be_true + end + + it 'should return true if "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Auto Update\RebootRequired" exists' do + recipe.stub(:registry_key_exists?).with('HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Auto Update\RebootRequired').and_return(true) + expect(recipe.reboot_pending?).to be_true + end + + it 'should return true if key "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Component Based Servicing\RebootRequired" exists' do + recipe.stub(:registry_key_exists?).with('HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Component Based Servicing\RebootRequired').and_return(true) + expect(recipe.reboot_pending?).to be_true + end + + it 'should return true if value "HKLM\SOFTWARE\Microsoft\Updates\UpdateExeVolatile" contains specific data' do + recipe.stub(:registry_key_exists?).with('HKLM\SOFTWARE\Microsoft\Updates\UpdateExeVolatile').and_return(true) + recipe.stub(:registry_get_values).with('HKLM\SOFTWARE\Microsoft\Updates\UpdateExeVolatile').and_return( + [{:name => "Flags", :type => :dword, :data => 3}]) + expect(recipe.reboot_pending?).to be_true + end + end + + context "platform is ubuntu" do + before do + recipe.stub(:platform?).with('ubuntu').and_return(true) + end + + it 'should return true if /var/run/reboot-required exists' do + File.stub(:exists?).with('/var/run/reboot-required').and_return(true) + expect(recipe.reboot_pending?).to be_true + end + + it 'should return false if /var/run/reboot-required does not exist' do + File.stub(:exists?).with('/var/run/reboot-required').and_return(false) + expect(recipe.reboot_pending?).to be_false + end + end + + context "platform is not supported" do + it 'should raise an exception' do + recipe.stub_chain(:node, :[]).with(:platform).and_return('msdos') + expect { recipe.reboot_pending? }.to raise_error(Chef::Exceptions::UnsupportedPlatform) + end + end + end # describe in isolation + + describe "in a recipe" do + it "responds to reboot_pending?" do + # Chef::Recipe.new(cookbook_name, recipe_name, run_context(node, cookbook_collection, events)) + recipe = Chef::Recipe.new(nil,nil,Chef::RunContext.new(Chef::Node.new, {}, nil)) + expect(recipe).to respond_to(:reboot_pending?) + end + end # describe in a recipe + + describe "in a resource" do + it "responds to reboot_pending?" do + resource = Chef::Resource::new("Crackerjack::Timing", nil) + expect(resource).to respond_to(:reboot_pending?) + end + end # describe in a resource + end +end diff --git a/spec/unit/guard_interpreter/resource_guard_interpreter_spec.rb b/spec/unit/guard_interpreter/resource_guard_interpreter_spec.rb new file mode 100644 index 0000000000..a016cbfeb8 --- /dev/null +++ b/spec/unit/guard_interpreter/resource_guard_interpreter_spec.rb @@ -0,0 +1,56 @@ +# +# Author:: Adam Edwards (<adamed@getchef.com>) +# Copyright:: Copyright (c) 2014 Chef Software, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require 'spec_helper' + +describe Chef::GuardInterpreter::ResourceGuardInterpreter do + before(:each) do + node = Chef::Node.new + + node.default["kernel"] = Hash.new + node.default["kernel"][:machine] = :x86_64.to_s + + run_context = Chef::RunContext.new(node, nil, nil) + + @resource = Chef::Resource.new("powershell_unit_test", run_context) + @resource.stub(:run_action) + @resource.stub(:updated).and_return(true) + end + + describe "when evaluating a guard resource" do + let(:resource) { @resource } + + it "should allow guard interpreter to be set to Chef::Resource::Script" do + resource.guard_interpreter(:script) + allow_any_instance_of(Chef::GuardInterpreter::ResourceGuardInterpreter).to receive(:evaluate_action).and_return(false) + resource.only_if("echo hi") + end + + it "should allow guard interpreter to be set to Chef::Resource::PowershellScript derived indirectly from Chef::Resource::Script" do + resource.guard_interpreter(:powershell_script) + allow_any_instance_of(Chef::GuardInterpreter::ResourceGuardInterpreter).to receive(:evaluate_action).and_return(false) + resource.only_if("echo hi") + end + + it "should raise an exception if guard_interpreter is set to a resource not derived from Chef::Resource::Script" do + resource.guard_interpreter(:file) + expect { resource.only_if("echo hi") }.to raise_error ArgumentError + end + end +end + diff --git a/spec/unit/http/simple_spec.rb b/spec/unit/http/simple_spec.rb new file mode 100644 index 0000000000..b33ef1d553 --- /dev/null +++ b/spec/unit/http/simple_spec.rb @@ -0,0 +1,32 @@ +# +# Author:: Serdar Sutay (<serdar@opscode.com>) +# Copyright:: Copyright (c) 2014 Chef Software, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require 'spec_helper' + +describe Chef::HTTP::Simple do + it "should have content length validation middleware after compressor middleware" do + client = Chef::HTTP::Simple.new("dummy.com") + middlewares = client.instance_variable_get(:@middlewares) + content_length = middlewares.find_index { |e| e.is_a? Chef::HTTP::ValidateContentLength } + decompressor = middlewares.find_index { |e| e.is_a? Chef::HTTP::Decompressor } + + content_length.should_not be_nil + decompressor.should_not be_nil + (decompressor < content_length).should be_true + end +end diff --git a/spec/unit/http/validate_content_length_spec.rb b/spec/unit/http/validate_content_length_spec.rb new file mode 100644 index 0000000000..091f2b0757 --- /dev/null +++ b/spec/unit/http/validate_content_length_spec.rb @@ -0,0 +1,187 @@ +# +# Author:: Serdar Sutay (<serdar@opscode.com>) +# Copyright:: Copyright (c) 2014 Chef Software, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require 'spec_helper' +require 'stringio' + +describe Chef::HTTP::ValidateContentLength do + class TestClient < Chef::HTTP + use Chef::HTTP::ValidateContentLength + end + + let(:method) { "GET" } + let(:url) { "http://dummy.com" } + let(:headers) { {} } + let(:data) { false } + + let(:request) { } + let(:return_value) { "200" } + + # Test Variables + let(:request_type) { :streaming } + let(:content_length_value) { 23 } + let(:streaming_length) { 23 } + let(:response_body) { "Thanks for checking in." } + let(:response_headers) { + { + "content-length" => content_length_value + } + } + + let(:response) { + m = double('HttpResponse', :body => response_body) + m.stub(:[]) do |key| + response_headers[key] + end + + m + } + + let(:middleware) { + client = TestClient.new(url) + client.middlewares[0] + } + + def run_content_length_validation + stream_handler = middleware.stream_response_handler(response) + middleware.handle_request(method, url, headers, data) + + case request_type + when :streaming + # First stream the data + data_length = streaming_length + while data_length > 0 + chunk_size = data_length > 10 ? 10 : data_length + stream_handler.handle_chunk(double("Chunk", :bytesize => chunk_size)) + data_length -= chunk_size + end + + # Finally call stream complete + middleware.handle_stream_complete(response, request, return_value) + when :direct + middleware.handle_response(response, request, return_value) + else + raise "Unknown request_type: #{request_type}" + end + end + + let(:debug_stream) { StringIO.new } + let(:debug_output) { debug_stream.string } + + before(:each) { + Chef::Log.level = :debug + Chef::Log.stub(:debug) do |message| + debug_stream.puts message + end + } + + describe "without response body" do + let(:request_type) { :direct } + let(:response_body) { "Thanks for checking in." } + + it "shouldn't raise error" do + lambda { run_content_length_validation }.should_not raise_error + end + end + + describe "without Content-Length header" do + let(:response_headers) { { } } + + [ "direct", "streaming" ].each do |req_type| + describe "when running #{req_type} request" do + let(:request_type) { req_type.to_sym } + + it "should skip validation and log for debug" do + run_content_length_validation + debug_output.should include("HTTP server did not include a Content-Length header in response") + end + end + end + end + + describe "with correct Content-Length header" do + [ "direct", "streaming" ].each do |req_type| + describe "when running #{req_type} request" do + let(:request_type) { req_type.to_sym } + + it "should validate correctly" do + run_content_length_validation + debug_output.should include("Content-Length validated correctly.") + end + end + end + end + + describe "with wrong Content-Length header" do + let(:content_length_value) { 25 } + [ "direct", "streaming" ].each do |req_type| + describe "when running #{req_type} request" do + let(:request_type) { req_type.to_sym } + + it "should raise ContentLengthMismatch error" do + lambda { run_content_length_validation }.should raise_error(Chef::Exceptions::ContentLengthMismatch) + end + end + end + end + + describe "when download is interrupted" do + let(:streaming_length) { 12 } + + it "should raise ContentLengthMismatch error" do + lambda { run_content_length_validation }.should raise_error(Chef::Exceptions::ContentLengthMismatch) + end + end + + describe "when Transfer-Encoding & Content-Length is set" do + let(:response_headers) { + { + "content-length" => content_length_value, + "transfer-encoding" => "chunked" + } + } + + [ "direct", "streaming" ].each do |req_type| + describe "when running #{req_type} request" do + let(:request_type) { req_type.to_sym } + + it "should skip validation and log for debug" do + run_content_length_validation + debug_output.should include("Transfer-Encoding header is set, skipping Content-Length check.") + end + end + end + end + + describe "when client is being reused" do + before do + run_content_length_validation + debug_output.should include("Content-Length validated correctly.") + end + + it "should reset internal counter" do + middleware.instance_variable_get(:@content_length_counter).should be_nil + end + + it "should validate correctly second time" do + run_content_length_validation + debug_output.should include("Content-Length validated correctly.") + end + end + +end diff --git a/spec/unit/knife/bootstrap_spec.rb b/spec/unit/knife/bootstrap_spec.rb index b055cadcee..501e678a2b 100644 --- a/spec/unit/knife/bootstrap_spec.rb +++ b/spec/unit/knife/bootstrap_spec.rb @@ -124,12 +124,21 @@ describe Chef::Knife::Bootstrap do end describe "specifying no_proxy with various entries" do - subject(:knife) { described_class.new } - let(:options){ ["--bootstrap-no-proxy", setting] } + subject(:knife) do + k = described_class.new + k.instance_variable_set("@template_file", template_file) + k.parse_options(options) + k.merge_configs + k + end + + # Include a data bag secret in the options to prevent Bootstrap from + # attempting to access /etc/chef/encrypted_data_bag_secret, which + # can fail when the file exists but can't be accessed by the user + # running the tests. + let(:options){ ["--bootstrap-no-proxy", setting, "-s", "foo"] } let(:template_file) { File.expand_path(File.join(CHEF_SPEC_DATA, "bootstrap", "no_proxy.erb")) } let(:rendered_template) do - knife.instance_variable_set("@template_file", template_file) - knife.parse_options(options) template_string = knife.read_template knife.render_template(template_string) end diff --git a/spec/unit/knife/client_bulk_delete_spec.rb b/spec/unit/knife/client_bulk_delete_spec.rb index bedd4911c5..7df7d02e9b 100644 --- a/spec/unit/knife/client_bulk_delete_spec.rb +++ b/spec/unit/knife/client_bulk_delete_spec.rb @@ -19,60 +19,145 @@ require 'spec_helper' describe Chef::Knife::ClientBulkDelete do - before(:each) do - Chef::Log.logger = Logger.new(StringIO.new) - - Chef::Config[:node_name] = "webmonkey.example.com" - @knife = Chef::Knife::ClientBulkDelete.new - @knife.name_args = ["."] - @stdout = StringIO.new - @knife.ui.stub(:stdout).and_return(@stdout) - @knife.ui.stub(:confirm).and_return(true) - @clients = Hash.new - %w{tim dan stephen}.each do |client_name| + let(:stdout_io) { StringIO.new } + let(:stdout) {stdout_io.string} + + let(:knife) { + k = Chef::Knife::ClientBulkDelete.new + k.name_args = name_args + k.config = option_args + k.ui.stub(:stdout).and_return(stdout_io) + k.ui.stub(:confirm).and_return(knife_confirm) + k.ui.stub(:confirm_without_exit).and_return(knife_confirm) + k + } + + let(:name_args) { [ "." ] } + let(:option_args) { {} } + + let(:knife_confirm) { true } + + let(:nonvalidator_client_names) { %w{tim dan stephen} } + let(:nonvalidator_clients) { + clients = Hash.new + + nonvalidator_client_names.each do |client_name| client = Chef::ApiClient.new() client.name(client_name) client.stub(:destroy).and_return(true) - @clients[client_name] = client + clients[client_name] = client + end + + clients + } + + let(:validator_client_names) { %w{myorg-validator} } + let(:validator_clients) { + clients = Hash.new + + validator_client_names.each do |validator_client_name| + validator_client = Chef::ApiClient.new() + validator_client.name(validator_client_name) + validator_client.stub(:validator).and_return(true) + validator_client.stub(:destroy).and_return(true) + clients[validator_client_name] = validator_client end - Chef::ApiClient.stub(:list).and_return(@clients) + + clients + } + + let(:client_names) { nonvalidator_client_names + validator_client_names} + let(:clients) { + nonvalidator_clients.merge(validator_clients) + } + + before(:each) do + Chef::ApiClient.stub(:list).and_return(clients) end describe "run" do + describe "without a regex" do + let(:name_args) { [ ] } - it "should get the list of the clients" do - Chef::ApiClient.should_receive(:list).and_return(@clients) - @knife.run + it "should exit if the regex is not provided" do + lambda { knife.run }.should raise_error(SystemExit) + end end - it "should print the clients you are about to delete" do - @knife.run - @stdout.string.should match(/#{@knife.ui.list(@clients.keys.sort, :columns_down)}/) - end + describe "with any clients" do + it "should get the list of the clients" do + Chef::ApiClient.should_receive(:list) + knife.run + end - it "should confirm you really want to delete them" do - @knife.ui.should_receive(:confirm) - @knife.run - end + it "should print the name of the clients" do + knife.run + client_names.each do |client_name| + stdout.should include(client_name) + end + end - it "should delete each client" do - @clients.each_value do |c| - c.should_receive(:destroy) + it "should confirm you really want to delete them" do + knife.ui.should_receive(:confirm) + knife.run end - @knife.run - end - it "should only delete clients that match the regex" do - @knife.name_args = ["tim"] - @clients["tim"].should_receive(:destroy) - @clients["stephen"].should_not_receive(:destroy) - @clients["dan"].should_not_receive(:destroy) - @knife.run + describe "without --delete-validators" do + it "should mention that validator clients wont be deleted" do + knife.run + stdout.should include("Following clients are validators and will not be deleted.") + info = stdout.index "Following clients are validators and will not be deleted." + val = stdout.index "myorg-validator" + (val > info).should be_true + end + + it "should only delete nonvalidator clients" do + nonvalidator_clients.each_value do |c| + c.should_receive(:destroy) + end + + validator_clients.each_value do |c| + c.should_not_receive(:destroy) + end + + knife.run + end + end + + describe "with --delete-validators" do + let(:option_args) { {:delete_validators => true} } + + it "should mention that validator clients will be deleted" do + knife.run + stdout.should include("The following validators will be deleted") + end + + it "should confirm twice" do + knife.ui.should_receive(:confirm).once + knife.ui.should_receive(:confirm_without_exit).once + knife.run + end + + it "should delete all clients" do + clients.each_value do |c| + c.should_receive(:destroy) + end + + knife.run + end + end end - it "should exit if the regex is not provided" do - @knife.name_args = [] - lambda { @knife.run }.should raise_error(SystemExit) + describe "with some clients" do + let(:name_args) { [ "^ti" ] } + + it "should only delete clients that match the regex" do + clients["tim"].should_receive(:destroy) + clients["stephen"].should_not_receive(:destroy) + clients["dan"].should_not_receive(:destroy) + clients["myorg-validator"].should_not_receive(:destroy) + knife.run + end end end end diff --git a/spec/unit/knife/client_create_spec.rb b/spec/unit/knife/client_create_spec.rb index 69c55ba015..897cee8974 100644 --- a/spec/unit/knife/client_create_spec.rb +++ b/spec/unit/knife/client_create_spec.rb @@ -25,7 +25,9 @@ describe Chef::Knife::ClientCreate do Chef::Config[:node_name] = "webmonkey.example.com" @knife = Chef::Knife::ClientCreate.new @knife.config = { - :file => nil + :file => nil, + :admin => false, + :validator => false } @knife.name_args = [ "adam" ] @client = Chef::ApiClient.new @@ -49,6 +51,16 @@ describe Chef::Knife::ClientCreate do @knife.run end + it "by default it is not an admin" do + @client.should_receive(:admin).with(false) + @knife.run + end + + it "by default it is not a validator" do + @client.should_receive(:validator).with(false) + @knife.run + end + it "should allow you to edit the data" do @knife.should_receive(:edit_data).with(@client) @knife.run @@ -70,5 +82,21 @@ describe Chef::Knife::ClientCreate do end end + describe "with -a or --admin" do + it "should create an admin client" do + @knife.config[:admin] = true + @client.should_receive(:admin).with(true) + @knife.run + end + end + + describe "with --validator" do + it "should create an validator client" do + @knife.config[:validator] = true + @client.should_receive(:validator).with(true) + @knife.run + end + end + end end diff --git a/spec/unit/knife/client_delete_spec.rb b/spec/unit/knife/client_delete_spec.rb index 9ebccbae15..01b49b3d7c 100644 --- a/spec/unit/knife/client_delete_spec.rb +++ b/spec/unit/knife/client_delete_spec.rb @@ -21,12 +21,16 @@ require 'spec_helper' describe Chef::Knife::ClientDelete do before(:each) do @knife = Chef::Knife::ClientDelete.new + # defaults + @knife.config = { + :delete_validators => false + } @knife.name_args = [ 'adam' ] end describe 'run' do it 'should delete the client' do - @knife.should_receive(:delete_object).with(Chef::ApiClient, 'adam') + @knife.should_receive(:delete_object).with(Chef::ApiClient, 'adam', 'client') @knife.run end @@ -37,4 +41,43 @@ describe Chef::Knife::ClientDelete do lambda { @knife.run }.should raise_error(SystemExit) end end + + describe 'with a validator' do + before(:each) do + Chef::Knife::UI.stub(:confirm).and_return(true) + @knife.stub(:confirm).and_return(true) + @client = Chef::ApiClient.new + Chef::ApiClient.should_receive(:load).and_return(@client) + end + + it 'should delete non-validator client if --force is not set' do + @knife.config[:delete_validators] = false + @client.should_receive(:destroy).and_return(@client) + @knife.should_receive(:msg) + + @knife.run + end + + it 'should delete non-validator client if --force is set' do + @knife.config[:delete_validators] = true + @client.should_receive(:destroy).and_return(@client) + @knife.should_receive(:msg) + + @knife.run + end + + it 'should not delete validator client if --force is not set' do + @client.validator(true) + @knife.ui.should_receive(:fatal) + lambda { @knife.run}.should raise_error(SystemExit) + end + + it 'should delete validator client if --force is set' do + @knife.config[:delete_validators] = true + @client.should_receive(:destroy).and_return(@client) + @knife.should_receive(:msg) + + @knife.run + end + end end diff --git a/spec/unit/knife/cookbook_upload_spec.rb b/spec/unit/knife/cookbook_upload_spec.rb index 65331b3952..5c7a4c1125 100644 --- a/spec/unit/knife/cookbook_upload_spec.rb +++ b/spec/unit/knife/cookbook_upload_spec.rb @@ -23,178 +23,271 @@ require 'chef/cookbook_uploader' require 'timeout' describe Chef::Knife::CookbookUpload do - before(:each) do - @knife = Chef::Knife::CookbookUpload.new - @knife.name_args = ['test_cookbook'] + let(:cookbook) { Chef::CookbookVersion.new('test_cookbook') } + + let(:cookbooks_by_name) do + {cookbook.name => cookbook} + end - @cookbook = Chef::CookbookVersion.new('test_cookbook') + let(:cookbook_loader) do + cookbook_loader = cookbooks_by_name.dup + cookbook_loader.stub(:merged_cookbooks).and_return([]) + cookbook_loader.stub(:load_cookbooks).and_return(cookbook_loader) + cookbook_loader + end + + let(:cookbook_uploader) { double(:upload_cookbooks => nil) } - @cookbook_loader = {} - @cookbook_loader.stub(:[]).and_return(@cookbook) - @cookbook_loader.stub(:merged_cookbooks).and_return([]) - @cookbook_loader.stub(:load_cookbooks).and_return(@cookbook_loader) - Chef::CookbookLoader.stub(:new).and_return(@cookbook_loader) + let(:output) { StringIO.new } + + let(:name_args) { ['test_cookbook'] } - @output = StringIO.new - @knife.ui.stub(:stdout).and_return(@output) - @knife.ui.stub(:stderr).and_return(@output) + let(:knife) do + k = Chef::Knife::CookbookUpload.new + k.name_args = name_args + k.ui.stub(:stdout).and_return(output) + k.ui.stub(:stderr).and_return(output) + k + end + + before(:each) do + Chef::CookbookLoader.stub(:new).and_return(cookbook_loader) end describe 'with --concurrency' do it 'should upload cookbooks with predefined concurrency' do - @cookbook_uploader = double(:upload_cookbooks => nil) Chef::CookbookVersion.stub(:list_all_versions).and_return({}) - @knife.config[:concurrency] = 3 - @test_cookbook = Chef::CookbookVersion.new('test_cookbook') - @cookbook_loader.stub(:each).and_yield("test_cookbook", @test_cookbook) - @cookbook_loader.stub(:cookbook_names).and_return(["test_cookbook"]) + knife.config[:concurrency] = 3 + test_cookbook = Chef::CookbookVersion.new('test_cookbook') + cookbook_loader.stub(:each).and_yield("test_cookbook", test_cookbook) + cookbook_loader.stub(:cookbook_names).and_return(["test_cookbook"]) Chef::CookbookUploader.should_receive(:new).with( kind_of(Array), kind_of(Array), {:force=>nil, :concurrency => 3}).and_return(double("Chef::CookbookUploader", :upload_cookbooks=> true)) - @knife.run + knife.run end end describe 'run' do before(:each) do - @cookbook_uploader = double(:upload_cookbooks => nil) - Chef::CookbookUploader.stub(:new => @cookbook_uploader) + Chef::CookbookUploader.stub(:new => cookbook_uploader) Chef::CookbookVersion.stub(:list_all_versions).and_return({}) end it 'should print usage and exit when a cookbook name is not provided' do - @knife.name_args = [] - @knife.should_receive(:show_usage) - @knife.ui.should_receive(:fatal) - lambda { @knife.run }.should raise_error(SystemExit) + knife.name_args = [] + knife.should_receive(:show_usage) + knife.ui.should_receive(:fatal) + lambda { knife.run }.should raise_error(SystemExit) end describe 'when specifying a cookbook name' do it 'should upload the cookbook' do - @knife.should_receive(:upload).once - @knife.run + knife.should_receive(:upload).once + knife.run end it 'should report on success' do - @knife.should_receive(:upload).once - @knife.ui.should_receive(:info).with(/Uploaded 1 cookbook/) - @knife.run + knife.should_receive(:upload).once + knife.ui.should_receive(:info).with(/Uploaded 1 cookbook/) + knife.run end end describe 'when specifying the same cookbook name twice' do it 'should upload the cookbook only once' do - @knife.name_args = ['test_cookbook', 'test_cookbook'] - @knife.should_receive(:upload).once - @knife.run + knife.name_args = ['test_cookbook', 'test_cookbook'] + knife.should_receive(:upload).once + knife.run + end + end + + context "when uploading a cookbook that uses deprecated overlays" do + + before do + cookbook_loader.stub(:merged_cookbooks).and_return(['test_cookbook']) + cookbook_loader.stub(:merged_cookbook_paths). + and_return({'test_cookbook' => %w{/path/one/test_cookbook /path/two/test_cookbook}}) + end + + it "emits a warning" do + knife.run + expected_message=<<-E +WARNING: The cookbooks: test_cookbook exist in multiple places in your cookbook_path. +A composite version of these cookbooks has been compiled for uploading. + +IMPORTANT: In a future version of Chef, this behavior will be removed and you will no longer +be able to have the same version of a cookbook in multiple places in your cookbook_path. +WARNING: The affected cookbooks are located: +test_cookbook: + /path/one/test_cookbook + /path/two/test_cookbook +E + output.string.should include(expected_message) end end describe 'when specifying a cookbook name among many' do - before(:each) do - @knife.name_args = ['test_cookbook1'] - @cookbooks = { + let(:name_args) { ['test_cookbook1'] } + + let(:cookbooks_by_name) do + { 'test_cookbook1' => Chef::CookbookVersion.new('test_cookbook1'), 'test_cookbook2' => Chef::CookbookVersion.new('test_cookbook2'), 'test_cookbook3' => Chef::CookbookVersion.new('test_cookbook3') } - @cookbook_loader = {} - @cookbook_loader.stub(:merged_cookbooks).and_return([]) - @cookbook_loader.stub(:[]) { |ckbk| @cookbooks[ckbk] } - Chef::CookbookLoader.stub(:new).and_return(@cookbook_loader) end it "should read only one cookbook" do - @cookbook_loader.should_receive(:[]).once.with('test_cookbook1') - @knife.run + cookbook_loader.should_receive(:[]).once.with('test_cookbook1').and_call_original + knife.run end it "should not read all cookbooks" do - @cookbook_loader.should_not_receive(:load_cookbooks) - @knife.run + cookbook_loader.should_not_receive(:load_cookbooks) + knife.run end it "should upload only one cookbook" do - @knife.should_receive(:upload).exactly(1).times - @knife.run + knife.should_receive(:upload).exactly(1).times + knife.run end end # This is testing too much. We should break it up. describe 'when specifying a cookbook name with dependencies' do + let(:name_args) { ["test_cookbook2"] } + + let(:cookbooks_by_name) do + { "test_cookbook1" => test_cookbook1, + "test_cookbook2" => test_cookbook2, + "test_cookbook3" => test_cookbook3 } + end + + let(:test_cookbook1) { Chef::CookbookVersion.new('test_cookbook1') } + + let(:test_cookbook2) do + c = Chef::CookbookVersion.new('test_cookbook2') + c.metadata.depends("test_cookbook3") + c + end + + let(:test_cookbook3) do + c = Chef::CookbookVersion.new('test_cookbook3') + c.metadata.depends("test_cookbook1") + c.metadata.depends("test_cookbook2") + c + end + it "should upload all dependencies once" do - @knife.name_args = ["test_cookbook2"] - @knife.config[:depends] = true - @test_cookbook1 = Chef::CookbookVersion.new('test_cookbook1') - @test_cookbook2 = Chef::CookbookVersion.new('test_cookbook2') - @test_cookbook3 = Chef::CookbookVersion.new('test_cookbook3') - @test_cookbook2.metadata.depends("test_cookbook3") - @test_cookbook3.metadata.depends("test_cookbook1") - @test_cookbook3.metadata.depends("test_cookbook2") - @cookbook_loader.stub(:[]) do |ckbk| - { "test_cookbook1" => @test_cookbook1, - "test_cookbook2" => @test_cookbook2, - "test_cookbook3" => @test_cookbook3 }[ckbk] - end - @knife.stub(:cookbook_names).and_return(["test_cookbook1", "test_cookbook2", "test_cookbook3"]) - @knife.should_receive(:upload).exactly(3).times - Timeout::timeout(5) do - @knife.run + knife.config[:depends] = true + knife.stub(:cookbook_names).and_return(["test_cookbook1", "test_cookbook2", "test_cookbook3"]) + knife.should_receive(:upload).exactly(3).times + lambda do + Timeout::timeout(5) do + knife.run + end end.should_not raise_error end end + describe 'when specifying a cookbook name with missing dependencies' do + let(:cookbook_dependency) { Chef::CookbookVersion.new('dependency') } + + before(:each) do + cookbook.metadata.depends("dependency") + cookbook_loader.stub(:[]) do |ckbk| + { "test_cookbook" => cookbook, + "dependency" => cookbook_dependency}[ckbk] + end + knife.stub(:cookbook_names).and_return(["cookbook_dependency", "test_cookbook"]) + @stdout, @stderr, @stdin = StringIO.new, StringIO.new, StringIO.new + knife.ui = Chef::Knife::UI.new(@stdout, @stderr, @stdin, {}) + end + + it 'should exit and not upload the cookbook' do + cookbook_loader.should_receive(:[]).once.with('test_cookbook') + cookbook_loader.should_not_receive(:load_cookbooks) + cookbook_uploader.should_not_receive(:upload_cookbooks) + expect {knife.run}.to raise_error(SystemExit) + end + + it 'should output a message for a single missing dependency' do + expect {knife.run}.to raise_error(SystemExit) + @stderr.string.should include('Cookbook test_cookbook depends on cookbooks which are not currently') + @stderr.string.should include('being uploaded and cannot be found on the server.') + @stderr.string.should include("The missing cookbook(s) are: 'dependency' version '>= 0.0.0'") + end + + it 'should output a message for a multiple missing dependencies which are concatenated' do + cookbook_dependency2 = Chef::CookbookVersion.new('dependency2') + cookbook.metadata.depends("dependency2") + cookbook_loader.stub(:[]) do |ckbk| + { "test_cookbook" => cookbook, + "dependency" => cookbook_dependency, + "dependency2" => cookbook_dependency2}[ckbk] + end + knife.stub(:cookbook_names).and_return(["dependency", "dependency2", "test_cookbook"]) + expect {knife.run}.to raise_error(SystemExit) + @stderr.string.should include('Cookbook test_cookbook depends on cookbooks which are not currently') + @stderr.string.should include('being uploaded and cannot be found on the server.') + @stderr.string.should include("The missing cookbook(s) are:") + @stderr.string.should include("'dependency' version '>= 0.0.0'") + @stderr.string.should include("'dependency2' version '>= 0.0.0'") + end + end + it "should freeze the version of the cookbooks if --freeze is specified" do - @knife.config[:freeze] = true - @cookbook.should_receive(:freeze_version).once - @knife.run + knife.config[:freeze] = true + cookbook.should_receive(:freeze_version).once + knife.run end describe 'with -a or --all' do before(:each) do - @knife.config[:all] = true + knife.config[:all] = true @test_cookbook1 = Chef::CookbookVersion.new('test_cookbook1') @test_cookbook2 = Chef::CookbookVersion.new('test_cookbook2') - @cookbook_loader.stub(:each).and_yield("test_cookbook1", @test_cookbook1).and_yield("test_cookbook2", @test_cookbook2) - @cookbook_loader.stub(:cookbook_names).and_return(["test_cookbook1", "test_cookbook2"]) + cookbook_loader.stub(:each).and_yield("test_cookbook1", @test_cookbook1).and_yield("test_cookbook2", @test_cookbook2) + cookbook_loader.stub(:cookbook_names).and_return(["test_cookbook1", "test_cookbook2"]) end it 'should upload all cookbooks' do - @knife.should_receive(:upload).once - @knife.run + knife.should_receive(:upload).once + knife.run end it 'should report on success' do - @knife.should_receive(:upload).once - @knife.ui.should_receive(:info).with(/Uploaded all cookbooks/) - @knife.run + knife.should_receive(:upload).once + knife.ui.should_receive(:info).with(/Uploaded all cookbooks/) + knife.run end it 'should update the version constraints for an environment' do - @knife.stub(:assert_environment_valid!).and_return(true) - @knife.config[:environment] = "production" - @knife.should_receive(:update_version_constraints).once - @knife.run + knife.stub(:assert_environment_valid!).and_return(true) + knife.config[:environment] = "production" + knife.should_receive(:update_version_constraints).once + knife.run end end describe 'when a frozen cookbook exists on the server' do it 'should fail to replace it' do exception = Chef::Exceptions::CookbookFrozen.new - @cookbook_uploader.should_receive(:upload_cookbooks). + cookbook_uploader.should_receive(:upload_cookbooks). and_raise(exception) - @knife.ui.stub(:error) - @knife.ui.should_receive(:error).with(exception) - lambda { @knife.run }.should raise_error(SystemExit) + knife.ui.stub(:error) + knife.ui.should_receive(:error).with(exception) + lambda { knife.run }.should raise_error(SystemExit) end it 'should not update the version constraints for an environment' do - @knife.stub(:assert_environment_valid!).and_return(true) - @knife.config[:environment] = "production" - @knife.stub(:upload).and_raise(Chef::Exceptions::CookbookFrozen) - @knife.ui.should_receive(:error).with(/Failed to upload 1 cookbook/) - @knife.ui.should_receive(:warn).with(/Not updating version constraints/) - @knife.should_not_receive(:update_version_constraints) - lambda { @knife.run }.should raise_error(SystemExit) + knife.stub(:assert_environment_valid!).and_return(true) + knife.config[:environment] = "production" + knife.stub(:upload).and_raise(Chef::Exceptions::CookbookFrozen) + knife.ui.should_receive(:error).with(/Failed to upload 1 cookbook/) + knife.ui.should_receive(:warn).with(/Not updating version constraints/) + knife.should_not_receive(:update_version_constraints) + lambda { knife.run }.should raise_error(SystemExit) end end end # run diff --git a/spec/unit/knife/core/bootstrap_context_spec.rb b/spec/unit/knife/core/bootstrap_context_spec.rb index 47261e2068..17e265edad 100644 --- a/spec/unit/knife/core/bootstrap_context_spec.rb +++ b/spec/unit/knife/core/bootstrap_context_spec.rb @@ -41,13 +41,19 @@ describe Chef::Knife::Core::BootstrapContext do bootstrap_context.start_chef.should eq "chef-client -j /etc/chef/first-boot.json -E _default" end + describe "when in verbosity mode" do + let(:config) { {:verbosity => 2} } + it "adds '-l debug' when verbosity is >= 2" do + bootstrap_context.start_chef.should eq "chef-client -j /etc/chef/first-boot.json -l debug -E _default" + end + end + it "reads the validation key" do bootstrap_context.validation_key.should eq IO.read(File.join(CHEF_SPEC_DATA, 'ssl', 'private_key.pem')) end it "generates the config file data" do expected=<<-EXPECTED -log_level :auto log_location STDOUT chef_server_url "http://chef.example.com:4444" validation_client_name "chef-validator-testing" @@ -56,6 +62,10 @@ EXPECTED bootstrap_context.config_content.should eq expected end + it "does not set a default log_level" do + expect(bootstrap_context.config_content).not_to match(/log_level/) + end + describe "alternate chef-client path" do let(:chef_config){ {:chef_client_path => '/usr/local/bin/chef-client'} } it "runs chef-client from another path when specified" do diff --git a/spec/unit/knife/core/ui_spec.rb b/spec/unit/knife/core/ui_spec.rb index c626747918..9044bc2f2f 100644 --- a/spec/unit/knife/core/ui_spec.rb +++ b/spec/unit/knife/core/ui_spec.rb @@ -406,61 +406,132 @@ EOM end describe "confirm" do - before(:each) do - @question = "monkeys rule" - @stdout = StringIO.new - @ui.stub(:stdout).and_return(@stdout) - @ui.stdin.stub(:readline).and_return("y") + let(:stdout) {StringIO.new} + let(:output) {stdout.string} + + let(:question) { "monkeys rule" } + let(:answer) { 'y' } + + let(:default_choice) { nil } + let(:append_instructions) { true } + + def run_confirm + @ui.stub(:stdout).and_return(stdout) + @ui.stdin.stub(:readline).and_return(answer) + @ui.confirm(question, append_instructions, default_choice) end - it "should return true if you answer Y" do - @ui.stdin.stub(:readline).and_return("Y") - @ui.confirm(@question).should == true + def run_confirm_without_exit + @ui.stub(:stdout).and_return(stdout) + @ui.stdin.stub(:readline).and_return(answer) + @ui.confirm_without_exit(question, append_instructions, default_choice) end - it "should return true if you answer y" do - @ui.stdin.stub(:readline).and_return("y") - @ui.confirm(@question).should == true + shared_examples_for "confirm with positive answer" do + it "confirm should return true" do + run_confirm.should be_true + end + + it "confirm_without_exit should return true" do + run_confirm_without_exit.should be_true + end end - it "should exit 3 if you answer N" do - @ui.stdin.stub(:readline).and_return("N") - lambda { - @ui.confirm(@question) - }.should raise_error(SystemExit) { |e| e.status.should == 3 } + shared_examples_for "confirm with negative answer" do + it "confirm should exit 3" do + lambda { + run_confirm + }.should raise_error(SystemExit) { |e| e.status.should == 3 } + end + + it "confirm_without_exit should return false" do + run_confirm_without_exit.should be_false + end end - it "should exit 3 if you answer n" do - @ui.stdin.stub(:readline).and_return("n") - lambda { - @ui.confirm(@question) - }.should raise_error(SystemExit) { |e| e.status.should == 3 } + describe "with default choice set to true" do + let(:default_choice) { true } + + it "should show 'Y/n' in the instructions" do + run_confirm + output.should include("Y/n") + end + + describe "with empty answer" do + let(:answer) { "" } + + it_behaves_like "confirm with positive answer" + end + + describe "with answer N " do + let(:answer) { "N" } + + it_behaves_like "confirm with negative answer" + end end - describe "with --y or --yes passed" do - it "should return true" do - @ui.config[:yes] = true - @ui.confirm(@question).should == true + describe "with default choice set to false" do + let(:default_choice) { false } + + it "should show 'y/N' in the instructions" do + run_confirm + output.should include("y/N") + end + + describe "with empty answer" do + let(:answer) { "" } + + it_behaves_like "confirm with negative answer" + end + + describe "with answer N " do + let(:answer) { "Y" } + + it_behaves_like "confirm with positive answer" end end - describe "when asking for free-form user input" do - it "asks a question and returns the answer provided by the user" do - out = StringIO.new - @ui.stub(:stdout).and_return(out) - @ui.stub(:stdin).and_return(StringIO.new("http://mychefserver.example.com\n")) - @ui.ask_question("your chef server URL?").should == "http://mychefserver.example.com" - out.string.should == "your chef server URL?" + ["Y", "y"].each do |answer| + describe "with answer #{answer}" do + let(:answer) { answer } + + it_behaves_like "confirm with positive answer" end + end - it "suggests a default setting and returns the default when the user's response only contains whitespace" do - out = StringIO.new - @ui.stub(:stdout).and_return(out) - @ui.stub(:stdin).and_return(StringIO.new(" \n")) - @ui.ask_question("your chef server URL? ", :default => 'http://localhost:4000').should == "http://localhost:4000" - out.string.should == "your chef server URL? [http://localhost:4000] " + ["N", "n"].each do |answer| + describe "with answer #{answer}" do + let(:answer) { answer } + + it_behaves_like "confirm with negative answer" end end + describe "with --y or --yes passed" do + it "should return true" do + @ui.config[:yes] = true + run_confirm.should be_true + output.should eq("") + end + end + end + + describe "when asking for free-form user input" do + it "asks a question and returns the answer provided by the user" do + out = StringIO.new + @ui.stub(:stdout).and_return(out) + @ui.stub(:stdin).and_return(StringIO.new("http://mychefserver.example.com\n")) + @ui.ask_question("your chef server URL?").should == "http://mychefserver.example.com" + out.string.should == "your chef server URL?" + end + + it "suggests a default setting and returns the default when the user's response only contains whitespace" do + out = StringIO.new + @ui.stub(:stdout).and_return(out) + @ui.stub(:stdin).and_return(StringIO.new(" \n")) + @ui.ask_question("your chef server URL? ", :default => 'http://localhost:4000').should == "http://localhost:4000" + out.string.should == "your chef server URL? [http://localhost:4000] " + end end + end diff --git a/spec/unit/knife/node_run_list_add_spec.rb b/spec/unit/knife/node_run_list_add_spec.rb index 61f2e6af4c..bd33a359a2 100644 --- a/spec/unit/knife/node_run_list_add_spec.rb +++ b/spec/unit/knife/node_run_list_add_spec.rb @@ -65,6 +65,29 @@ describe Chef::Knife::NodeRunListAdd do end end + describe "with -b or --before specified" do + it "should add to the run list before the specified entry" do + @node.run_list << "role[acorns]" + @node.run_list << "role[barn]" + @knife.config[:before] = "role[acorns]" + @knife.run + @node.run_list[0].should == "role[monkey]" + @node.run_list[1].should == "role[acorns]" + @node.run_list[2].should == "role[barn]" + end + end + + describe "with both --after and --before specified" do + it "exits with an error" do + @node.run_list << "role[acorns]" + @node.run_list << "role[barn]" + @knife.config[:before] = "role[acorns]" + @knife.config[:after] = "role[acorns]" + @knife.ui.should_receive(:fatal) + lambda { @knife.run }.should raise_error(SystemExit) + end + end + describe "with more than one role or recipe" do it "should add to the run list all the entries" do @knife.name_args = [ "adam", "role[monkey],role[duck]" ] @@ -98,7 +121,7 @@ describe Chef::Knife::NodeRunListAdd do end end - describe "with more than one role or recipe as different arguments and list separated by comas" do + describe "with more than one role or recipe as different arguments and list separated by commas" do it "should add to the run list all the entries" do @knife.name_args = [ "adam", "role[monkey]", "role[duck],recipe[bird::fly]" ] @node.run_list << "role[acorns]" diff --git a/spec/unit/knife/ssh_spec.rb b/spec/unit/knife/ssh_spec.rb index eff7c9ba5b..9247db3c90 100644 --- a/spec/unit/knife/ssh_spec.rb +++ b/spec/unit/knife/ssh_spec.rb @@ -54,7 +54,7 @@ describe Chef::Knife::Ssh do @knife.config[:attribute] = "ipaddress" @knife.config[:override_attribute] = "ipaddress" configure_query([@node_foo, @node_bar]) - @knife.should_receive(:session_from_list).with(['10.0.0.1', '10.0.0.2']) + @knife.should_receive(:session_from_list).with([['10.0.0.1', nil], ['10.0.0.2', nil]]) @knife.configure_session end @@ -62,14 +62,17 @@ describe Chef::Knife::Ssh do @knife.config[:attribute] = "config_file" # this value will be the config file @knife.config[:override_attribute] = "ipaddress" # this is the value of the command line via #configure_attribute configure_query([@node_foo, @node_bar]) - @knife.should_receive(:session_from_list).with(['10.0.0.1', '10.0.0.2']) + @knife.should_receive(:session_from_list).with([['10.0.0.1', nil], ['10.0.0.2', nil]]) @knife.configure_session end end it "searchs for and returns an array of fqdns" do configure_query([@node_foo, @node_bar]) - @knife.should_receive(:session_from_list).with(['foo.example.org', 'bar.example.org']) + @knife.should_receive(:session_from_list).with([ + ['foo.example.org', nil], + ['bar.example.org', nil] + ]) @knife.configure_session end @@ -83,7 +86,10 @@ describe Chef::Knife::Ssh do it "returns an array of cloud public hostnames" do configure_query([@node_foo, @node_bar]) - @knife.should_receive(:session_from_list).with(['ec2-10-0-0-1.compute-1.amazonaws.com', 'ec2-10-0-0-2.compute-1.amazonaws.com']) + @knife.should_receive(:session_from_list).with([ + ['ec2-10-0-0-1.compute-1.amazonaws.com', nil], + ['ec2-10-0-0-2.compute-1.amazonaws.com', nil] + ]) @knife.configure_session end @@ -179,12 +185,17 @@ describe Chef::Knife::Ssh do end it "uses the port from an ssh config file" do - @knife.session_from_list(['the.b.org']) + @knife.session_from_list([['the.b.org', nil]]) @knife.session.servers[0].port.should == 23 end + it "uses the port from a cloud attr" do + @knife.session_from_list([['the.b.org', 123]]) + @knife.session.servers[0].port.should == 123 + end + it "uses the user from an ssh config file" do - @knife.session_from_list(['the.b.org']) + @knife.session_from_list([['the.b.org', 123]]) @knife.session.servers[0].user.should == "locutus" end end diff --git a/spec/unit/knife/ssl_check_spec.rb b/spec/unit/knife/ssl_check_spec.rb new file mode 100644 index 0000000000..32405a5977 --- /dev/null +++ b/spec/unit/knife/ssl_check_spec.rb @@ -0,0 +1,187 @@ +# +# Author:: Daniel DeLeo (<dan@getchef.com>) +# Copyright:: Copyright (c) 2014 Chef Software, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require "spec_helper" +require 'stringio' + +describe Chef::Knife::SslCheck do + + let(:name_args) { [] } + let(:stdout_io) { StringIO.new } + let(:stderr_io) { StringIO.new } + + def stderr + stderr_io.string + end + + def stdout + stdout_io.string + end + + subject(:ssl_check) do + s = Chef::Knife::SslCheck.new + s.ui.stub(:stdout).and_return(stdout_io) + s.ui.stub(:stderr).and_return(stderr_io) + s.name_args = name_args + s + end + + before do + Chef::Config.chef_server_url = "https://example.com:8443/chef-server" + end + + context "when no arguments are given" do + it "uses the chef_server_url as the host to check" do + expect(ssl_check.host).to eq("example.com") + expect(ssl_check.port).to eq(8443) + end + end + + context "when a specific URI is given" do + let(:name_args) { %w{https://example.test:10443/foo} } + + it "checks the SSL configuration against the given host" do + expect(ssl_check.host).to eq("example.test") + expect(ssl_check.port).to eq(10443) + end + end + + context "when an invalid URI is given" do + + let(:name_args) { %w{foo.test} } + + it "prints an error and exits" do + expect { ssl_check.run }.to raise_error(SystemExit) + expected_stdout=<<-E +USAGE: knife ssl check [URL] (options) +E + expected_stderr=<<-E +ERROR: Given URI: `foo.test' is invalid +E + expect(stdout_io.string).to eq(expected_stdout) + expect(stderr_io.string).to eq(expected_stderr) + end + + context "and its malformed enough to make URI.parse barf" do + + let(:name_args) { %w{ftp://lkj\\blah:example.com/blah} } + + it "prints an error and exits" do + expect { ssl_check.run }.to raise_error(SystemExit) + expected_stdout=<<-E +USAGE: knife ssl check [URL] (options) +E + expected_stderr=<<-E +ERROR: Given URI: `#{name_args[0]}' is invalid +E + expect(stdout_io.string).to eq(expected_stdout) + expect(stderr_io.string).to eq(expected_stderr) + end + end + end + + describe "verifying the remote certificate" do + let(:name_args) { %w{https://foo.example.com:8443} } + + let(:tcp_socket) { double(TCPSocket) } + let(:ssl_socket) { double(OpenSSL::SSL::SSLSocket) } + + before do + TCPSocket.should_receive(:new).with("foo.example.com", 8443).and_return(tcp_socket) + OpenSSL::SSL::SSLSocket.should_receive(:new).with(tcp_socket, ssl_check.verify_peer_ssl_context).and_return(ssl_socket) + end + + def run + ssl_check.run + rescue Exception + #puts "OUT: #{stdout_io.string}" + #puts "ERR: #{stderr_io.string}" + raise + end + + context "when the remote host's certificate is valid" do + + before do + ssl_socket.should_receive(:connect) # no error + ssl_socket.should_receive(:post_connection_check).with("foo.example.com") # no error + end + + it "prints a success message" do + ssl_check.run + expect(stdout_io.string).to include("Successfully verified certificates from `foo.example.com'") + end + end + + describe "and the certificate is not valid" do + + let(:tcp_socket_for_debug) { double(TCPSocket) } + let(:ssl_socket_for_debug) { double(OpenSSL::SSL::SSLSocket) } + + let(:self_signed_crt_path) { File.join(CHEF_SPEC_DATA, "trusted_certs", "example.crt") } + let(:self_signed_crt) { OpenSSL::X509::Certificate.new(File.read(self_signed_crt_path)) } + + before do + trap(:INT, "DEFAULT") + + TCPSocket.should_receive(:new). + with("foo.example.com", 8443). + and_return(tcp_socket_for_debug) + OpenSSL::SSL::SSLSocket.should_receive(:new). + with(tcp_socket_for_debug, ssl_check.noverify_peer_ssl_context). + and_return(ssl_socket_for_debug) + end + + context "when the certificate's CN does not match the hostname" do + before do + ssl_socket.should_receive(:connect) # no error + ssl_socket.should_receive(:post_connection_check). + with("foo.example.com"). + and_raise(OpenSSL::SSL::SSLError) + ssl_socket_for_debug.should_receive(:connect) + ssl_socket_for_debug.should_receive(:peer_cert).and_return(self_signed_crt) + end + + it "shows the CN used by the certificate and prints an error" do + expect { run }.to raise_error(SystemExit) + expect(stderr).to include("The SSL cert is signed by a trusted authority but is not valid for the given hostname") + expect(stderr).to include("You are attempting to connect to: 'foo.example.com'") + expect(stderr).to include("The server's certificate belongs to 'example.local'") + end + + end + + context "when the cert is not signed by any trusted authority" do + before do + ssl_socket.should_receive(:connect). + and_raise(OpenSSL::SSL::SSLError) + ssl_socket_for_debug.should_receive(:connect) + ssl_socket_for_debug.should_receive(:peer_cert).and_return(self_signed_crt) + end + + it "shows the CN used by the certificate and prints an error" do + expect { run }.to raise_error(SystemExit) + expect(stderr).to include("The SSL certificate of foo.example.com could not be verified") + end + + end + end + + end + +end + diff --git a/spec/unit/knife/ssl_fetch_spec.rb b/spec/unit/knife/ssl_fetch_spec.rb new file mode 100644 index 0000000000..0d3c8913f7 --- /dev/null +++ b/spec/unit/knife/ssl_fetch_spec.rb @@ -0,0 +1,151 @@ +# +# Author:: Daniel DeLeo (<dan@getchef.com>) +# Copyright:: Copyright (c) 2014 Chef Software, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require 'spec_helper' +require 'chef/knife/ssl_fetch' + +describe Chef::Knife::SslFetch do + + let(:name_args) { [] } + let(:stdout_io) { StringIO.new } + let(:stderr_io) { StringIO.new } + + def stderr + stderr_io.string + end + + def stdout + stdout_io.string + end + + subject(:ssl_fetch) do + s = Chef::Knife::SslFetch.new + s.name_args = name_args + s.ui.stub(:stdout).and_return(stdout_io) + s.ui.stub(:stderr).and_return(stderr_io) + s + end + + context "when no arguments are given" do + + before do + Chef::Config.chef_server_url = "https://example.com:8443/chef-server" + end + + it "uses the chef_server_url as the host to fetch" do + expect(ssl_fetch.host).to eq("example.com") + expect(ssl_fetch.port).to eq(8443) + end + end + + context "when a specific URI is given" do + let(:name_args) { %w{https://example.test:10443/foo} } + + it "fetchs the SSL configuration against the given host" do + expect(ssl_fetch.host).to eq("example.test") + expect(ssl_fetch.port).to eq(10443) + end + end + + context "when an invalid URI is given" do + + let(:name_args) { %w{foo.test} } + + it "prints an error and exits" do + expect { ssl_fetch.run }.to raise_error(SystemExit) + expected_stdout=<<-E +USAGE: knife ssl fetch [URL] (options) +E + expected_stderr=<<-E +ERROR: Given URI: `foo.test' is invalid +E + expect(stdout_io.string).to eq(expected_stdout) + expect(stderr_io.string).to eq(expected_stderr) + end + + context "and its malformed enough to make URI.parse barf" do + + let(:name_args) { %w{ftp://lkj\\blah:example.com/blah} } + + it "prints an error and exits" do + expect { ssl_fetch.run }.to raise_error(SystemExit) + expected_stdout=<<-E +USAGE: knife ssl fetch [URL] (options) +E + expected_stderr=<<-E +ERROR: Given URI: `#{name_args[0]}' is invalid +E + expect(stdout_io.string).to eq(expected_stdout) + expect(stderr_io.string).to eq(expected_stderr) + end + end + end + + describe "normalizing CNs for use as paths" do + + it "normalizes '*' to 'wildcard'" do + expect(ssl_fetch.normalize_cn("*.example.com")).to eq("wildcard_example_com") + end + + it "normalizes non-alnum and hyphen characters to underscores" do + expect(ssl_fetch.normalize_cn("Billy-Bob's Super Awesome CA!")).to eq("Billy-Bob_s_Super_Awesome_CA_") + end + + end + + describe "fetching the remote cert chain" do + + let(:name_args) { %w{https://foo.example.com:8443} } + + let(:tcp_socket) { double(TCPSocket) } + let(:ssl_socket) { double(OpenSSL::SSL::SSLSocket) } + + let(:self_signed_crt_path) { File.join(CHEF_SPEC_DATA, "trusted_certs", "example.crt") } + let(:self_signed_crt) { OpenSSL::X509::Certificate.new(File.read(self_signed_crt_path)) } + + let(:trusted_certs_dir) { Dir.mktmpdir } + + def run + ssl_fetch.run + rescue Exception + puts "OUT: #{stdout_io.string}" + puts "ERR: #{stderr_io.string}" + raise + end + + before do + Chef::Config.trusted_certs_dir = trusted_certs_dir + + TCPSocket.should_receive(:new).with("foo.example.com", 8443).and_return(tcp_socket) + OpenSSL::SSL::SSLSocket.should_receive(:new).with(tcp_socket, ssl_fetch.noverify_peer_ssl_context).and_return(ssl_socket) + ssl_socket.should_receive(:connect) + ssl_socket.should_receive(:peer_cert_chain).and_return([self_signed_crt]) + end + + after do + FileUtils.rm_rf(trusted_certs_dir) + end + + it "fetches the cert chain and writes the certs to the trusted_certs_dir" do + run + stored_cert_path = File.join(trusted_certs_dir, "example_local.crt") + expect(File).to exist(stored_cert_path) + expect(File.read(stored_cert_path)).to eq(File.read(self_signed_crt_path)) + end + end +end diff --git a/spec/unit/knife_spec.rb b/spec/unit/knife_spec.rb index daace18106..8bf85bf604 100644 --- a/spec/unit/knife_spec.rb +++ b/spec/unit/knife_spec.rb @@ -22,6 +22,7 @@ module KnifeSpecs end require 'spec_helper' +require 'uri' describe Chef::Knife do before(:each) do @@ -141,6 +142,60 @@ describe Chef::Knife do end + describe "the headers include X-Remote-Request-Id" do + + let(:headers) {{"Accept"=>"application/json", + "Accept-Encoding"=>"gzip;q=1.0,deflate;q=0.6,identity;q=0.3", + 'X-Chef-Version' => Chef::VERSION, + "Host"=>"api.opscode.piab:443", + "X-REMOTE-REQUEST-ID"=>request_id}} + + let(:request_id) {"1234"} + + let(:request_mock) { {} } + + let(:rest) do + Net::HTTP.stub(:new).and_return(http_client) + Chef::RequestID.instance.stub(:request_id).and_return(request_id) + Chef::Config.stub(:chef_server_url).and_return("https://api.opscode.piab") + command = Chef::Knife.run(%w{test yourself}) + rest = command.noauth_rest + rest + end + + let!(:http_client) do + http_client = Net::HTTP.new(url.host, url.port) + http_client.stub(:request).and_yield(http_response).and_return(http_response) + http_client + end + + let(:url) { URI.parse("https://api.opscode.piab") } + + let(:http_response) do + http_response = Net::HTTPSuccess.new("1.1", "200", "successful rest req") + http_response.stub(:read_body) + http_response.stub(:body).and_return(body) + http_response["Content-Length"] = body.bytesize.to_s + http_response + end + + let(:body) { "ninja" } + + before(:each) do + Chef::Config[:chef_server_url] = "https://api.opscode.piab" + if KnifeSpecs.const_defined?(:TestYourself) + KnifeSpecs.send :remove_const, :TestYourself + end + Kernel.load(File.join(CHEF_SPEC_DATA, 'knife_subcommand', 'test_yourself.rb')) + Chef::Knife.subcommands.each { |name, klass| Chef::Knife.subcommands.delete(name) unless klass.kind_of?(Class) } + end + + it "confirms that the headers include X-Remote-Request-Id" do + Net::HTTP::Get.should_receive(:new).with("/monkey", headers).and_return(request_mock) + rest.get_rest("monkey") + end + end + describe "when running a command" do before(:each) do if KnifeSpecs.const_defined?(:TestYourself) diff --git a/spec/unit/mixin/deep_merge_spec.rb b/spec/unit/mixin/deep_merge_spec.rb index 0a7bbffa41..76f5c68a29 100644 --- a/spec/unit/mixin/deep_merge_spec.rb +++ b/spec/unit/mixin/deep_merge_spec.rb @@ -284,6 +284,10 @@ describe Chef::Mixin::DeepMerge do ret.should == {"property" => ["1","2","3","4","5","6"]} end + it "should not error merging un-dupable objects" do + @dm.deep_merge(nil, 4) + end + end describe "role_merge" do @@ -347,5 +351,18 @@ describe Chef::Mixin::DeepMerge do merged_result["top_level_a"]["1_deep_b"].should == %w[B B B] end + it "does not mutate deeply-nested original hashes by default" do + merge_ee_hash = {"top_level_a" => {"1_deep_a" => { "2_deep_a" => { "3_deep_a" => "foo" }}}} + merge_with_hash = {"top_level_a" => {"1_deep_a" => { "2_deep_a" => { "3_deep_b" => "bar" }}}} + @dm.hash_only_merge(merge_ee_hash, merge_with_hash) + merge_ee_hash.should == {"top_level_a" => {"1_deep_a" => { "2_deep_a" => { "3_deep_a" => "foo" }}}} + merge_with_hash.should == {"top_level_a" => {"1_deep_a" => { "2_deep_a" => { "3_deep_b" => "bar" }}}} + end + + it "does not error merging un-dupable items" do + merge_ee_hash = {"top_level_a" => 1, "top_level_b" => false} + merge_with_hash = {"top_level_a" => 2, "top_level_b" => true } + @dm.hash_only_merge(merge_ee_hash, merge_with_hash) + end end end diff --git a/spec/unit/node/attribute_spec.rb b/spec/unit/node/attribute_spec.rb index ef3fc60cc6..bab2e33aa9 100644 --- a/spec/unit/node/attribute_spec.rb +++ b/spec/unit/node/attribute_spec.rb @@ -488,6 +488,13 @@ describe Chef::Node::Attribute do end end + describe "dup" do + it "array can be duped even if some elements can't" do + @attributes.default[:foo] = %w[foo bar baz] + Array(1..3) + [nil, true, false, [ "el", 0, nil ] ] + @attributes.default[:foo].dup + end + end + describe "has_key?" do it "should return true if an attribute exists" do @attributes.has_key?("music").should == true diff --git a/spec/unit/node/immutable_collections_spec.rb b/spec/unit/node/immutable_collections_spec.rb index 0c2b878cd2..d7abfa26e6 100644 --- a/spec/unit/node/immutable_collections_spec.rb +++ b/spec/unit/node/immutable_collections_spec.rb @@ -54,6 +54,32 @@ describe Chef::Node::ImmutableMash do @immutable_mash[:top_level_4][:level2].should be_a(Chef::Node::ImmutableMash) end + describe "to_hash" do + before do + @copy = @immutable_mash.to_hash + end + + it "converts an immutable mash to a new mutable hash" do + @copy.should be_instance_of(Hash) + end + + it "converts an immutable nested mash to a new mutable hash" do + @copy['top_level_4']['level2'].should be_instance_of(Hash) + end + + it "converts an immutable nested array to a new mutable array" do + @copy['top_level_2'].should be_instance_of(Array) + end + + it "should create a mash with the same content" do + @copy.should == @immutable_mash + end + + it 'should allow mutation' do + lambda { @copy['m'] = 'm' }.should_not raise_error(Chef::Exceptions::ImmutableAttributeModification) + end + + end [ :[]=, @@ -86,7 +112,9 @@ end describe Chef::Node::ImmutableArray do before do - @immutable_array = Chef::Node::ImmutableArray.new(%w[foo bar baz]) + @immutable_array = Chef::Node::ImmutableArray.new(%w[foo bar baz] + Array(1..3) + [nil, true, false, [ "el", 0, nil ] ]) + immutable_mash = Chef::Node::ImmutableMash.new({:m => 'm'}) + @immutable_nested_array = Chef::Node::ImmutableArray.new(["level1",@immutable_array, immutable_mash]) end ## @@ -130,10 +158,41 @@ describe Chef::Node::ImmutableArray do end end + it "can be duped even if some elements can't" do + @immutable_array.dup + end + it "returns a mutable version of itself when duped" do mutable = @immutable_array.dup mutable[0] = :value mutable[0].should == :value end + + describe "to_a" do + before do + @copy = @immutable_nested_array.to_a + end + + it "converts an immutable array to a new mutable array" do + @copy.should be_instance_of(Array) + end + + it "converts an immutable nested array to a new mutable array" do + @copy[1].should be_instance_of(Array) + end + + it "converts an immutable nested mash to a new mutable hash" do + @copy[2].should be_instance_of(Hash) + end + + it "should create an array with the same content" do + @copy.should == @immutable_nested_array + end + + it 'should allow mutation' do + lambda { @copy << 'm' }.should_not raise_error(Chef::Exceptions::ImmutableAttributeModification) + end + end + end diff --git a/spec/unit/node_spec.rb b/spec/unit/node_spec.rb index f2a78f87cd..832e10f645 100644 --- a/spec/unit/node_spec.rb +++ b/spec/unit/node_spec.rb @@ -724,6 +724,15 @@ describe Chef::Node do json.should =~ /\"run_list\":\[\"role\[Cthulu\]\",\"role\[Hastur\]\"\]/ end + it "should serialize the correct run list", :json => true do + node.run_list << "role[marxist]" + node.run_list << "role[leninist]" + node.override_runlist << "role[stalinist]" + node.run_list.should be_include("role[stalinist]") + json = Chef::JSONCompat.to_json(node) + json.should =~ /\"run_list\":\[\"role\[marxist\]\",\"role\[leninist\]\"\]/ + end + it "merges the override components into a combined override object" do node.attributes.role_override["role override"] = "role override" node.attributes.env_override["env override"] = "env override" diff --git a/spec/unit/platform/query_helpers_spec.rb b/spec/unit/platform/query_helpers_spec.rb new file mode 100644 index 0000000000..2414bdf552 --- /dev/null +++ b/spec/unit/platform/query_helpers_spec.rb @@ -0,0 +1,32 @@ +# +# Author:: Bryan McLellan <btm@loftninjas.org> +# Copyright:: Copyright (c) 2014 Chef Software, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require 'spec_helper' + +describe "Chef::Platform#windows_server_2003?" do + it "returns false early when not on windows" do + Chef::Platform.stub(:windows?).and_return(false) + expect(Chef::Platform).not_to receive(:require) + expect(Chef::Platform.windows_server_2003?).to be_false + end + + # CHEF-4888: Need to call WIN32OLE.ole_initialize in new threads + it "does not raise an exception" do + expect { Thread.fork { Chef::Platform.windows_server_2003? }.join }.not_to raise_error + end +end diff --git a/spec/unit/platform_spec.rb b/spec/unit/platform_spec.rb index e0386a1a61..3d7aef98a0 100644 --- a/spec/unit/platform_spec.rb +++ b/spec/unit/platform_spec.rb @@ -37,7 +37,8 @@ describe "Chef::Platform supports" do :mswin, :mingw32, :windows, - :gcel + :gcel, + :ibm_powerkvm ].each do |platform| it "#{platform}" do Chef::Platform.platforms.should have_key(platform) @@ -47,209 +48,226 @@ end describe Chef::Platform do - before :all do - @original_platform_map = Chef::Platform.platforms - end + context "while testing with fake data" do - after :all do || - Chef::Platform.platforms = @original_platform_map - end + before :all do + @original_platform_map = Chef::Platform.platforms + end - before(:each) do - Chef::Platform.platforms = { - :darwin => { - ">= 10.11" => { - :file => "new_darwinian" + after :all do || + Chef::Platform.platforms = @original_platform_map + end + + before(:each) do + Chef::Platform.platforms = { + :darwin => { + ">= 10.11" => { + :file => "new_darwinian" + }, + "9.2.2" => { + :file => "darwinian", + :else => "thing" + }, + :default => { + :file => "old school", + :snicker => "snack" + } }, - "9.2.2" => { - :file => "darwinian", - :else => "thing" + :mars_volta => { }, :default => { - :file => "old school", - :snicker => "snack" + :file => Chef::Provider::File, + :pax => "brittania", + :cat => "nice" } - }, - :mars_volta => { - }, - :default => { - :file => Chef::Provider::File, - :pax => "brittania", - :cat => "nice" } - } - @events = Chef::EventDispatch::Dispatcher.new - end + @events = Chef::EventDispatch::Dispatcher.new + end - it "should allow you to look up a platform by name and version, returning the provider map for it" do - pmap = Chef::Platform.find("Darwin", "9.2.2") - pmap.should be_a_kind_of(Hash) - pmap[:file].should eql("darwinian") - end + it "should allow you to look up a platform by name and version, returning the provider map for it" do + pmap = Chef::Platform.find("Darwin", "9.2.2") + pmap.should be_a_kind_of(Hash) + pmap[:file].should eql("darwinian") + end - it "should allow you to look up a platform by name and version using \"greater than\" style operators" do - pmap = Chef::Platform.find("Darwin", "11.1.0") - pmap.should be_a_kind_of(Hash) - pmap[:file].should eql("new_darwinian") - end + it "should allow you to look up a platform by name and version using \"greater than\" style operators" do + pmap = Chef::Platform.find("Darwin", "11.1.0") + pmap.should be_a_kind_of(Hash) + pmap[:file].should eql("new_darwinian") + end - it "should use the default providers for an os if the specific version does not exist" do - pmap = Chef::Platform.find("Darwin", "1") - pmap.should be_a_kind_of(Hash) - pmap[:file].should eql("old school") - end + it "should use the default providers for an os if the specific version does not exist" do + pmap = Chef::Platform.find("Darwin", "1") + pmap.should be_a_kind_of(Hash) + pmap[:file].should eql("old school") + end - it "should use the default providers if the os doesn't give me a default, but does exist" do - pmap = Chef::Platform.find("mars_volta", "1") - pmap.should be_a_kind_of(Hash) - pmap[:file].should eql(Chef::Provider::File) - end + it "should use the default providers if the os doesn't give me a default, but does exist" do + pmap = Chef::Platform.find("mars_volta", "1") + pmap.should be_a_kind_of(Hash) + pmap[:file].should eql(Chef::Provider::File) + end - it "should use the default provider if the os does not exist" do - pmap = Chef::Platform.find("AIX", "1") - pmap.should be_a_kind_of(Hash) - pmap[:file].should eql(Chef::Provider::File) - end + it "should use the default provider if the os does not exist" do + pmap = Chef::Platform.find("AIX", "1") + pmap.should be_a_kind_of(Hash) + pmap[:file].should eql(Chef::Provider::File) + end - it "should merge the defaults for an os with the specific version" do - pmap = Chef::Platform.find("Darwin", "9.2.2") - pmap[:file].should eql("darwinian") - pmap[:snicker].should eql("snack") - end + it "should merge the defaults for an os with the specific version" do + pmap = Chef::Platform.find("Darwin", "9.2.2") + pmap[:file].should eql("darwinian") + pmap[:snicker].should eql("snack") + end - it "should merge the defaults for an os with the universal defaults" do - pmap = Chef::Platform.find("Darwin", "9.2.2") - pmap[:file].should eql("darwinian") - pmap[:pax].should eql("brittania") - end + it "should merge the defaults for an os with the universal defaults" do + pmap = Chef::Platform.find("Darwin", "9.2.2") + pmap[:file].should eql("darwinian") + pmap[:pax].should eql("brittania") + end - it "should allow you to look up a provider for a platform directly by symbol" do - Chef::Platform.find_provider("Darwin", "9.2.2", :file).should eql("darwinian") - end + it "should allow you to look up a provider for a platform directly by symbol" do + Chef::Platform.find_provider("Darwin", "9.2.2", :file).should eql("darwinian") + end - it "should raise an exception if a provider cannot be found for a resource type" do - lambda { Chef::Platform.find_provider("Darwin", "9.2.2", :coffee) }.should raise_error(ArgumentError) - end + it "should raise an exception if a provider cannot be found for a resource type" do + lambda { Chef::Platform.find_provider("Darwin", "9.2.2", :coffee) }.should raise_error(ArgumentError) + end - it "should look up a provider for a resource with a Chef::Resource object" do - kitty = Chef::Resource::Cat.new("loulou") - Chef::Platform.find_provider("Darwin", "9.2.2", kitty).should eql("nice") - end + it "should look up a provider for a resource with a Chef::Resource object" do + kitty = Chef::Resource::Cat.new("loulou") + Chef::Platform.find_provider("Darwin", "9.2.2", kitty).should eql("nice") + end - it "should look up a provider with a node and a Chef::Resource object" do - kitty = Chef::Resource::Cat.new("loulou") - node = Chef::Node.new - node.name("Intel") - node.automatic_attrs[:platform] = "mac_os_x" - node.automatic_attrs[:platform_version] = "9.2.2" - Chef::Platform.find_provider_for_node(node, kitty).should eql("nice") - end + it "should look up a provider with a node and a Chef::Resource object" do + kitty = Chef::Resource::Cat.new("loulou") + node = Chef::Node.new + node.name("Intel") + node.automatic_attrs[:platform] = "mac_os_x" + node.automatic_attrs[:platform_version] = "9.2.2" + Chef::Platform.find_provider_for_node(node, kitty).should eql("nice") + end - it "should not throw an exception when the platform version has an unknown format" do - Chef::Platform.find_provider(:darwin, "bad-version", :file).should eql("old school") - end + it "should not throw an exception when the platform version has an unknown format" do + Chef::Platform.find_provider(:darwin, "bad-version", :file).should eql("old school") + end - it "should prefer an explicit provider" do - kitty = Chef::Resource::Cat.new("loulou") - kitty.stub(:provider).and_return(Chef::Provider::File) - node = Chef::Node.new - node.name("Intel") - node.automatic_attrs[:platform] = "mac_os_x" - node.automatic_attrs[:platform_version] = "9.2.2" - Chef::Platform.find_provider_for_node(node, kitty).should eql(Chef::Provider::File) - end + it "should prefer an explicit provider" do + kitty = Chef::Resource::Cat.new("loulou") + kitty.stub(:provider).and_return(Chef::Provider::File) + node = Chef::Node.new + node.name("Intel") + node.automatic_attrs[:platform] = "mac_os_x" + node.automatic_attrs[:platform_version] = "9.2.2" + Chef::Platform.find_provider_for_node(node, kitty).should eql(Chef::Provider::File) + end - it "should look up a provider based on the resource name if nothing else matches" do - kitty = Chef::Resource::Cat.new("loulou") - class Chef::Provider::Cat < Chef::Provider; end - Chef::Platform.platforms[:default].delete(:cat) - node = Chef::Node.new - node.name("Intel") - node.automatic_attrs[:platform] = "mac_os_x" - node.automatic_attrs[:platform_version] = "8.5" - Chef::Platform.find_provider_for_node(node, kitty).should eql(Chef::Provider::Cat) - end + it "should look up a provider based on the resource name if nothing else matches" do + kitty = Chef::Resource::Cat.new("loulou") + class Chef::Provider::Cat < Chef::Provider; end + Chef::Platform.platforms[:default].delete(:cat) + node = Chef::Node.new + node.name("Intel") + node.automatic_attrs[:platform] = "mac_os_x" + node.automatic_attrs[:platform_version] = "8.5" + Chef::Platform.find_provider_for_node(node, kitty).should eql(Chef::Provider::Cat) + end - def setup_file_resource - node = Chef::Node.new - node.automatic_attrs[:platform] = "mac_os_x" - node.automatic_attrs[:platform_version] = "9.2.2" - run_context = Chef::RunContext.new(node, {}, @events) - [ Chef::Resource::File.new("whateva", run_context), run_context ] - end + def setup_file_resource + node = Chef::Node.new + node.automatic_attrs[:platform] = "mac_os_x" + node.automatic_attrs[:platform_version] = "9.2.2" + run_context = Chef::RunContext.new(node, {}, @events) + [ Chef::Resource::File.new("whateva", run_context), run_context ] + end - it "returns a provider object given a Chef::Resource object which has a valid run context and an action" do - file, run_context = setup_file_resource - provider = Chef::Platform.provider_for_resource(file, :foo) - provider.should be_an_instance_of(Chef::Provider::File) - provider.new_resource.should equal(file) - provider.run_context.should equal(run_context) - end + it "returns a provider object given a Chef::Resource object which has a valid run context and an action" do + file, run_context = setup_file_resource + provider = Chef::Platform.provider_for_resource(file, :foo) + provider.should be_an_instance_of(Chef::Provider::File) + provider.new_resource.should equal(file) + provider.run_context.should equal(run_context) + end - it "returns a provider object given a Chef::Resource object which has a valid run context without an action" do - file, run_context = setup_file_resource - provider = Chef::Platform.provider_for_resource(file) - provider.should be_an_instance_of(Chef::Provider::File) - provider.new_resource.should equal(file) - provider.run_context.should equal(run_context) - end + it "returns a provider object given a Chef::Resource object which has a valid run context without an action" do + file, run_context = setup_file_resource + provider = Chef::Platform.provider_for_resource(file) + provider.should be_an_instance_of(Chef::Provider::File) + provider.new_resource.should equal(file) + provider.run_context.should equal(run_context) + end - it "raises an error when trying to find the provider for a resource with no run context" do - file = Chef::Resource::File.new("whateva") - lambda {Chef::Platform.provider_for_resource(file)}.should raise_error(ArgumentError) - end + it "raises an error when trying to find the provider for a resource with no run context" do + file = Chef::Resource::File.new("whateva") + lambda {Chef::Platform.provider_for_resource(file)}.should raise_error(ArgumentError) + end - it "does not support finding a provider by resource and node -- a run context is required" do - lambda {Chef::Platform.provider_for_node('node', 'resource')}.should raise_error(NotImplementedError) - end + it "does not support finding a provider by resource and node -- a run context is required" do + lambda {Chef::Platform.provider_for_node('node', 'resource')}.should raise_error(NotImplementedError) + end + + it "should update the provider map with map" do + Chef::Platform.set( + :platform => :darwin, + :version => "9.2.2", + :resource => :file, + :provider => "masterful" + ) + Chef::Platform.platforms[:darwin]["9.2.2"][:file].should eql("masterful") + Chef::Platform.set( + :platform => :darwin, + :resource => :file, + :provider => "masterful" + ) + Chef::Platform.platforms[:darwin][:default][:file].should eql("masterful") + Chef::Platform.set( + :resource => :file, + :provider => "masterful" + ) + Chef::Platform.platforms[:default][:file].should eql("masterful") + + Chef::Platform.set( + :platform => :hero, + :version => "9.2.2", + :resource => :file, + :provider => "masterful" + ) + Chef::Platform.platforms[:hero]["9.2.2"][:file].should eql("masterful") + + Chef::Platform.set( + :resource => :file, + :provider => "masterful" + ) + Chef::Platform.platforms[:default][:file].should eql("masterful") + + Chef::Platform.platforms = {} + + Chef::Platform.set( + :resource => :file, + :provider => "masterful" + ) + Chef::Platform.platforms[:default][:file].should eql("masterful") + + Chef::Platform.platforms = { :neurosis => {} } + Chef::Platform.set(:platform => :neurosis, :resource => :package, :provider => "masterful") + Chef::Platform.platforms[:neurosis][:default][:package].should eql("masterful") - it "should update the provider map with map" do - Chef::Platform.set( - :platform => :darwin, - :version => "9.2.2", - :resource => :file, - :provider => "masterful" - ) - Chef::Platform.platforms[:darwin]["9.2.2"][:file].should eql("masterful") - Chef::Platform.set( - :platform => :darwin, - :resource => :file, - :provider => "masterful" - ) - Chef::Platform.platforms[:darwin][:default][:file].should eql("masterful") - Chef::Platform.set( - :resource => :file, - :provider => "masterful" - ) - Chef::Platform.platforms[:default][:file].should eql("masterful") - - Chef::Platform.set( - :platform => :hero, - :version => "9.2.2", - :resource => :file, - :provider => "masterful" - ) - Chef::Platform.platforms[:hero]["9.2.2"][:file].should eql("masterful") - - Chef::Platform.set( - :resource => :file, - :provider => "masterful" - ) - Chef::Platform.platforms[:default][:file].should eql("masterful") - - Chef::Platform.platforms = {} - - Chef::Platform.set( - :resource => :file, - :provider => "masterful" - ) - Chef::Platform.platforms[:default][:file].should eql("masterful") - - Chef::Platform.platforms = { :neurosis => {} } - Chef::Platform.set(:platform => :neurosis, :resource => :package, :provider => "masterful") - Chef::Platform.platforms[:neurosis][:default][:package].should eql("masterful") + end end + context "while testing the configured platform data" do + + it "should use the solaris package provider on Solaris <11" do + pmap = Chef::Platform.find("Solaris2", "5.9") + pmap[:package].should eql(Chef::Provider::Package::Solaris) + end + + it "should use the IPS package provider on Solaris 11" do + pmap = Chef::Platform.find("Solaris2", "5.11") + pmap[:package].should eql(Chef::Provider::Package::Ips) + end + + end end diff --git a/spec/unit/policy_builder/expand_node_object_spec.rb b/spec/unit/policy_builder/expand_node_object_spec.rb index 5c6f39d28c..a1e0b881d5 100644 --- a/spec/unit/policy_builder/expand_node_object_spec.rb +++ b/spec/unit/policy_builder/expand_node_object_spec.rb @@ -244,7 +244,7 @@ describe Chef::PolicyBuilder::ExpandNodeObject do it "sets the override run_list on the node" do expect(node.run_list).to eq([override_runlist]) - expect(policy_builder.original_runlist).to eq(primary_runlist) + expect(node.primary_runlist).to eq(primary_runlist) end it "reports that a temporary policy is being used" do diff --git a/spec/unit/provider/cron_spec.rb b/spec/unit/provider/cron_spec.rb index 3a7a96c549..b78266fb25 100644 --- a/spec/unit/provider/cron_spec.rb +++ b/spec/unit/provider/cron_spec.rb @@ -19,15 +19,137 @@ require 'spec_helper' describe Chef::Provider::Cron do + describe "when with special time string" do + before do + @node = Chef::Node.new + @events = Chef::EventDispatch::Dispatcher.new + @run_context = Chef::RunContext.new(@node, {}, @events) + + @new_resource = Chef::Resource::Cron.new("cronhole some stuff", @run_context) + @new_resource.user "root" + @new_resource.minute "30" + @new_resource.command "/bin/true" + @new_resource.time :reboot + @provider = Chef::Provider::Cron.new(@new_resource, @run_context) + end + + context "with a matching entry in the user's crontab" do + before :each do + @provider.stub!(:read_crontab).and_return(<<-CRONTAB) +0 2 * * * /some/other/command + +# Chef Name: cronhole some stuff +@reboot /bin/true param1 param2 +# Chef Name: something else +2 * 1 * * /bin/false + +# Another comment +CRONTAB + end + + it "should set cron_exists" do + @provider.load_current_resource + @provider.cron_exists.should == true + @provider.cron_empty.should == false + end + + it "should pull the details out of the cron line" do + cron = @provider.load_current_resource + cron.time.should == :reboot + cron.command.should == '/bin/true param1 param2' + end + + it "should pull env vars out" do + @provider.stub!(:read_crontab).and_return(<<-CRONTAB) +0 2 * * * /some/other/command + +# Chef Name: cronhole some stuff +MAILTO=foo@example.com +SHELL=/bin/foosh +PATH=/bin:/foo +HOME=/home/foo +@reboot /bin/true param1 param2 +# Chef Name: something else +2 * 1 * * /bin/false + +# Another comment +CRONTAB + cron = @provider.load_current_resource + cron.mailto.should == 'foo@example.com' + cron.shell.should == '/bin/foosh' + cron.path.should == '/bin:/foo' + cron.home.should == '/home/foo' + cron.time.should == :reboot + cron.command.should == '/bin/true param1 param2' + end + + it "should parse and load generic and standard environment variables from cron entry" do + @provider.stub!(:read_crontab).and_return(<<-CRONTAB) +# Chef Name: cronhole some stuff +MAILTO=warn@example.com +TEST=lol +FLAG=1 +@reboot /bin/true +CRONTAB + cron = @provider.load_current_resource + + cron.mailto.should == "warn@example.com" + cron.environment.should == {"TEST" => "lol", "FLAG" => "1"} + end + + it "should not break with variables that match the cron resource internals" do + @provider.stub!(:read_crontab).and_return(<<-CRONTAB) +# Chef Name: cronhole some stuff +MINUTE=40 +REBOOT=midnight +TEST=lol +ENVIRONMENT=production +@reboot /bin/true +CRONTAB + cron = @provider.load_current_resource + + cron.time.should == :reboot + cron.environment.should == {"MINUTE" => "40", "REBOOT" => "midnight", "TEST" => "lol", "ENVIRONMENT" => "production"} + end + + it "should report the match" do + Chef::Log.should_receive(:debug).with("Found cron '#{@new_resource.name}'") + @provider.load_current_resource + end + + describe "action_create" do + before :each do + @provider.stub!(:write_crontab) + @provider.stub!(:read_crontab).and_return(nil) + end + + context "when there is no existing crontab" do + before :each do + @provider.cron_exists = false + @provider.cron_empty = true + end + + it "should create a crontab with the entry" do + @provider.should_receive(:write_crontab).with(<<-ENDCRON) +# Chef Name: cronhole some stuff +@reboot /bin/true + ENDCRON + @provider.run_action(:create) + end + end + end + end + end + before do @node = Chef::Node.new @events = Chef::EventDispatch::Dispatcher.new @run_context = Chef::RunContext.new(@node, {}, @events) + @new_resource = Chef::Resource::Cron.new("cronhole some stuff", @run_context) @new_resource.user "root" @new_resource.minute "30" @new_resource.command "/bin/true" - @provider = Chef::Provider::Cron.new(@new_resource, @run_context) end @@ -110,6 +232,7 @@ CRONTAB cron.day.should == '*' cron.month.should == '1' cron.weekday.should == '*' + cron.time.should == nil cron.command.should == '/bin/true param1 param2' end @@ -138,6 +261,7 @@ CRONTAB cron.day.should == '*' cron.month.should == '1' cron.weekday.should == '*' + cron.time.should == nil cron.command.should == '/bin/true param1 param2' end @@ -227,6 +351,7 @@ CRONTAB cron.day.should == '*' cron.month.should == '*' cron.weekday.should == '*' + cron.time.should == nil cron.command.should == nil end @@ -244,6 +369,7 @@ CRONTAB cron.day.should == '*' cron.month.should == '*' cron.weekday.should == '*' + cron.time.should == nil cron.command.should == nil end @@ -265,6 +391,7 @@ CRONTAB cron.day.should == '*' cron.month.should == '*' cron.weekday.should == '*' + cron.time.should == nil cron.command.should == nil end end @@ -286,6 +413,11 @@ CRONTAB end end + it "should return true if special time string doesn't match" do + @new_resource.send(:time, :reboot) + @provider.cron_different?.should eql(true) + end + it "should return true if environment doesn't match" do @new_resource.environment "FOO" => "something_else" @provider.cron_different?.should eql(true) @@ -833,4 +965,46 @@ MAILTO=foo@example.com end end + + describe "weekday_in_crontab" do + context "when weekday is symbol" do + it "should return weekday in crontab format" do + @new_resource.weekday :wednesday + @provider.send(:weekday_in_crontab).should eq("3") + end + + it "should raise an error with an unknown weekday" do + expect { @new_resource.weekday :caturday }.to raise_error(RangeError) + end + end + + context "when weekday is a number in a string" do + it "should return the string" do + @new_resource.weekday "3" + @provider.send(:weekday_in_crontab).should eq("3") + end + + it "should raise an error with an out of range number" do + expect { @new_resource.weekday "-1" }.to raise_error(RangeError) + end + end + + context "when weekday is string with the name of the week" do + it "should return the string" do + @new_resource.weekday "mon" + @provider.send(:weekday_in_crontab).should eq("mon") + end + end + + context "when weekday is an integer" do + it "should return the integer" do + @new_resource.weekday 1 + @provider.send(:weekday_in_crontab).should eq("1") + end + + it "should raise an error with an out of range integer" do + expect { @new_resource.weekday 45 }.to raise_error(RangeError) + end + end + end end diff --git a/spec/unit/provider/group_spec.rb b/spec/unit/provider/group_spec.rb index 9ff9f85c7c..b138f6b210 100644 --- a/spec/unit/provider/group_spec.rb +++ b/spec/unit/provider/group_spec.rb @@ -96,6 +96,11 @@ describe Chef::Provider::User do @provider.compare_group.should be_false end + it "should coerce an integer to a string for comparison" do + @current_resource.stub!(:gid).and_return("500") + @provider.compare_group.should be_false + end + it "should return false if append is true and the group member(s) already exists" do @current_resource.members << "extra_user" @new_resource.stub(:append).and_return(true) diff --git a/spec/unit/provider/ifconfig/debian_spec.rb b/spec/unit/provider/ifconfig/debian_spec.rb index c2e2d1bfd1..c6a37fdd5b 100644 --- a/spec/unit/provider/ifconfig/debian_spec.rb +++ b/spec/unit/provider/ifconfig/debian_spec.rb @@ -53,38 +53,264 @@ describe Chef::Provider::Ifconfig::Debian do let(:config_filename_ifcfg) { "/etc/network/interfaces.d/ifcfg-#{new_resource.device}" } - describe "generate_config for action_add" do + describe "generate_config" do - let(:config_file_ifaces) { StringIO.new } + context "when writing a file" do + let(:config_file_ifcfg) { StringIO.new } - let(:config_file_ifcfg) { StringIO.new } + let(:tempfile) { Tempfile.new("rspec-chef-ifconfig-debian") } - before do - expect(FileUtils).to receive(:cp) - expect(File).to receive(:open).with(config_filename_ifaces).and_return(StringIO.new) - expect(File).to receive(:open).with(config_filename_ifaces, "w").and_yield(config_file_ifaces) - expect(File).to receive(:new).with(config_filename_ifcfg, "w").and_return(config_file_ifcfg) - expect(File).to receive(:exist?).with(config_filename_ifaces).and_return(true) - end + let(:tempdir_path) { Dir.mktmpdir("rspec-chef-ifconfig-debian-dir") } + + let(:config_filename_ifcfg) { "#{tempdir_path}/ifcfg-#{new_resource.device}" } + + before do + stub_const("Chef::Provider::Ifconfig::Debian::INTERFACES_FILE", tempfile.path) + stub_const("Chef::Provider::Ifconfig::Debian::INTERFACES_DOT_D_DIR", tempdir_path) + expect(File).to receive(:new).with(config_filename_ifcfg, "w").and_return(config_file_ifcfg) + end + + it "should write a network-script" do + provider.run_action(:add) + expect(config_file_ifcfg.string).to match(/^iface eth0 inet static\s*$/) + expect(config_file_ifcfg.string).to match(/^\s+address 10\.0\.0\.1\s*$/) + expect(config_file_ifcfg.string).to match(/^\s+netmask 255\.255\.254\.0\s*$/) + end + + context "when the interface_dot_d directory does not exist" do + before do + FileUtils.rmdir tempdir_path + expect(File.exists?(tempdir_path)).to be_false + end + + it "should create the /etc/network/interfaces.d directory" do + provider.run_action(:add) + expect(File.exists?(tempdir_path)).to be_true + expect(File.directory?(tempdir_path)).to be_true + end - it "should create network-scripts directory" do - expect(File).to receive(:directory?).with(File.dirname(config_filename_ifcfg)).and_return(false) - expect(Dir).to receive(:mkdir).with(File.dirname(config_filename_ifcfg)) - provider.run_action(:add) + it "should mark the resource as updated" do + provider.run_action(:add) + expect(new_resource.updated_by_last_action?).to be_true + end + end + + context "when the interface_dot_d directory exists" do + before do + expect(File.exists?(tempdir_path)).to be_true + end + + it "should still mark the resource as updated (we still write a file to it)" do + provider.run_action(:add) + expect(new_resource.updated_by_last_action?).to be_true + end + end end - it "should write configure network-scripts directory" do - expect(File).to receive(:directory?).with(File.dirname(config_filename_ifcfg)).and_return(true) - provider.run_action(:add) - expect(config_file_ifaces.string).to match(/^\s*source\s+\/etc\/network\/interfaces[.]d\/[*]\s*$/) + context "when the file is up-to-date" do + let(:tempfile) { Tempfile.new("rspec-chef-ifconfig-debian") } + + let(:tempdir_path) { Dir.mktmpdir("rspec-chef-ifconfig-debian-dir") } + + let(:config_filename_ifcfg) { "#{tempdir_path}/ifcfg-#{new_resource.device}" } + + before do + stub_const("Chef::Provider::Ifconfig::Debian::INTERFACES_FILE", tempfile.path) + stub_const("Chef::Provider::Ifconfig::Debian::INTERFACES_DOT_D_DIR", tempdir_path) + config_file_ifcfg = StringIO.new(<<-EOF +iface eth0 inet static + address 10.0.0.1 + netmask 255.255.254.0 +EOF + ) + expect(File).to receive(:new).with(config_filename_ifcfg, "w").and_return(config_file_ifcfg) + expect(File.exists?(tempdir_path)).to be_true # since the file exists, the enclosing dir must also exist + end + + context "when the /etc/network/interfaces file has the source line" do + let(:expected_string) do + <<-EOF +a line +source #{tempdir_path}/* +another line +EOF + end + + before do + tempfile.write(expected_string) + tempfile.close + end + + it "should preserve all the contents" do + provider.run_action(:add) + expect(IO.read(tempfile.path)).to eq(expected_string) + end + + it "should not mark the resource as updated" do + provider.run_action(:add) + pending "superclass ifconfig provider is not idempotent" + expect(new_resource.updated_by_last_action?).to be_false + end + end + + context "when the /etc/network/interfaces file does not have the source line" do + let(:expected_string) do + <<-EOF +a line +another line +source #{tempdir_path}/* +EOF + end + + before do + tempfile.write("a line\nanother line\n") + tempfile.close + end + + it "should preserve the original contents and add the source line" do + provider.run_action(:add) + expect(IO.read(tempfile.path)).to eq(expected_string) + end + + it "should mark the resource as updated" do + provider.run_action(:add) + expect(new_resource.updated_by_last_action?).to be_true + end + end end - it "should write a network-script" do - expect(File).to receive(:directory?).with(File.dirname(config_filename_ifcfg)).and_return(true) - provider.run_action(:add) - expect(config_file_ifcfg.string).to match(/^iface eth0 inet static\s*$/) - expect(config_file_ifcfg.string).to match(/^\s+address 10\.0\.0\.1\s*$/) - expect(config_file_ifcfg.string).to match(/^\s+netmask 255\.255\.254\.0\s*$/) + describe "when running under why run" do + + before do + Chef::Config[:why_run] = true + end + + after do + Chef::Config[:why_run] = false + end + + context "when writing a file" do + let(:config_file_ifcfg) { StringIO.new } + + let(:tempfile) { Tempfile.new("rspec-chef-ifconfig-debian") } + + let(:tempdir_path) { Dir.mktmpdir("rspec-chef-ifconfig-debian-dir") } + + let(:config_filename_ifcfg) { "#{tempdir_path}/ifcfg-#{new_resource.device}" } + + before do + stub_const("Chef::Provider::Ifconfig::Debian::INTERFACES_FILE", tempfile.path) + stub_const("Chef::Provider::Ifconfig::Debian::INTERFACES_DOT_D_DIR", tempdir_path) + expect(File).not_to receive(:new).with(config_filename_ifcfg, "w") + end + + it "should write a network-script" do + provider.run_action(:add) + expect(config_file_ifcfg.string).not_to match(/^iface eth0 inet static\s*$/) + expect(config_file_ifcfg.string).not_to match(/^\s+address 10\.0\.0\.1\s*$/) + expect(config_file_ifcfg.string).not_to match(/^\s+netmask 255\.255\.254\.0\s*$/) + end + + context "when the interface_dot_d directory does not exist" do + before do + FileUtils.rmdir tempdir_path + expect(File.exists?(tempdir_path)).to be_false + end + + it "should not create the /etc/network/interfaces.d directory" do + provider.run_action(:add) + expect(File.exists?(tempdir_path)).not_to be_true + end + + it "should mark the resource as updated" do + provider.run_action(:add) + expect(new_resource.updated_by_last_action?).to be_true + end + end + + context "when the interface_dot_d directory exists" do + before do + expect(File.exists?(tempdir_path)).to be_true + end + + it "should still mark the resource as updated (we still write a file to it)" do + provider.run_action(:add) + expect(new_resource.updated_by_last_action?).to be_true + end + end + end + + context "when the file is up-to-date" do + let(:tempfile) { Tempfile.new("rspec-chef-ifconfig-debian") } + + let(:tempdir_path) { Dir.mktmpdir("rspec-chef-ifconfig-debian-dir") } + + let(:config_filename_ifcfg) { "#{tempdir_path}/ifcfg-#{new_resource.device}" } + + before do + stub_const("Chef::Provider::Ifconfig::Debian::INTERFACES_FILE", tempfile.path) + stub_const("Chef::Provider::Ifconfig::Debian::INTERFACES_DOT_D_DIR", tempdir_path) + config_file_ifcfg = StringIO.new(<<-EOF +iface eth0 inet static + address 10.0.0.1 + netmask 255.255.254.0 + EOF + ) + expect(File).not_to receive(:new).with(config_filename_ifcfg, "w") + expect(File.exists?(tempdir_path)).to be_true # since the file exists, the enclosing dir must also exist + end + + context "when the /etc/network/interfaces file has the source line" do + let(:expected_string) do + <<-EOF +a line +source #{tempdir_path}/* +another line + EOF + end + + before do + tempfile.write(expected_string) + tempfile.close + end + + it "should preserve all the contents" do + provider.run_action(:add) + expect(IO.read(tempfile.path)).to eq(expected_string) + end + + it "should not mark the resource as updated" do + provider.run_action(:add) + pending "superclass ifconfig provider is not idempotent" + expect(new_resource.updated_by_last_action?).to be_false + end + end + + context "when the /etc/network/interfaces file does not have the source line" do + let(:expected_string) do + <<-EOF +a line +another line +source #{tempdir_path}/* + EOF + end + + before do + tempfile.write("a line\nanother line\n") + tempfile.close + end + + it "should preserve the original contents and not add the source line" do + provider.run_action(:add) + expect(IO.read(tempfile.path)).to eq("a line\nanother line\n") + end + + it "should mark the resource as updated" do + provider.run_action(:add) + expect(new_resource.updated_by_last_action?).to be_true + end + end + end end end @@ -98,4 +324,5 @@ describe Chef::Provider::Ifconfig::Debian do provider.run_action(:delete) end end + end diff --git a/spec/unit/provider/mount/mount_spec.rb b/spec/unit/provider/mount/mount_spec.rb index 99e78590f1..e27cf71e01 100644 --- a/spec/unit/provider/mount/mount_spec.rb +++ b/spec/unit/provider/mount/mount_spec.rb @@ -141,6 +141,17 @@ describe Chef::Provider::Mount::Mount do @provider.current_resource.mounted.should be_true end + it "should set mounted true if the symlink target of the device is relative and is found in the mounts list - CHEF-4957" do + target = "xsdz1" + + ::File.stub(:symlink?).with("#{@new_resource.device}").and_return(true) + ::File.stub(:readlink).with("#{@new_resource.device}").and_return(target) + + @provider.stub(:shell_out!).and_return(OpenStruct.new(:stdout => "/dev/xsdz1 on /tmp/foo type ext3 (rw)\n")) + @provider.load_current_resource() + @provider.current_resource.mounted.should be_true + end + it "should set mounted true if the mount point is found last in the mounts list" do mount = "/dev/sdy1 on #{@new_resource.mount_point} type ext3 (rw)\n" mount << "#{@new_resource.device} on #{@new_resource.mount_point} type ext3 (rw)\n" @@ -199,6 +210,20 @@ describe Chef::Provider::Mount::Mount do @provider.current_resource.enabled.should be_true end + it "should set enabled to true if the symlink target is relative and is in fstab - CHEF-4957" do + target = "xsdz1" + + ::File.stub(:symlink?).with("#{@new_resource.device}").and_return(true) + ::File.stub(:readlink).with("#{@new_resource.device}").and_return(target) + + fstab = "/dev/sdz1 /tmp/foo ext3 defaults 1 2\n" + + ::File.stub(:foreach).with("/etc/fstab").and_yield fstab + + @provider.load_current_resource + @provider.current_resource.enabled.should be_true + end + it "should set enabled to false if the mount point is not in fstab" do fstab = "/dev/sdy1 #{@new_resource.mount_point} ext3 defaults 1 2\n" ::File.stub(:foreach).with("/etc/fstab").and_yield fstab diff --git a/spec/unit/provider/ohai_spec.rb b/spec/unit/provider/ohai_spec.rb index 8b8a6b5939..2085f44309 100644 --- a/spec/unit/provider/ohai_spec.rb +++ b/spec/unit/provider/ohai_spec.rb @@ -41,9 +41,8 @@ describe Chef::Provider::Ohai do :newdata => "somevalue" } } - mock_ohai.stub(:all_plugins).and_return(true) - mock_ohai.stub(:require_plugin).and_return(true) - mock_ohai.stub(:data).and_return(mock_ohai[:data], + mock_ohai.stub!(:all_plugins).and_return(true) + mock_ohai.stub!(:data).and_return(mock_ohai[:data], mock_ohai[:data2]) Ohai::System.stub(:new).and_return(mock_ohai) Chef::Platform.stub(:find_platform_and_version).and_return({ "platform" => @platform, diff --git a/spec/unit/provider/package/dpkg_spec.rb b/spec/unit/provider/package/dpkg_spec.rb index 6ba7695a1e..22edeb7b9b 100644 --- a/spec/unit/provider/package/dpkg_spec.rb +++ b/spec/unit/provider/package/dpkg_spec.rb @@ -72,6 +72,10 @@ describe Chef::Provider::Package::Dpkg do it 'if distro-specific version provided' do check_version('1.11.4-1ubuntu1~lucid') end + + it 'returns the version if an epoch is used' do + check_version('1:1.8.3-2') + end end it "gets the source package name from dpkg-deb correctly when the package name has `-', `+' or `.' characters" do diff --git a/spec/unit/provider/package/windows/msi_spec.rb b/spec/unit/provider/package/windows/msi_spec.rb new file mode 100644 index 0000000000..c8a63ad066 --- /dev/null +++ b/spec/unit/provider/package/windows/msi_spec.rb @@ -0,0 +1,60 @@ +# +# Author:: Bryan McLellan <btm@loftninjas.org> +# Copyright:: Copyright (c) 2014 Chef Software, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require 'spec_helper' + +describe Chef::Provider::Package::Windows::MSI, :windows_only do + let(:node) { double('Chef::Node') } + let(:events) { double('Chef::Events').as_null_object } # mock all the methods + let(:run_context) { double('Chef::RunContext', :node => node, :events => events) } + let(:new_resource) { Chef::Resource::WindowsPackage.new("calculator.msi") } + let(:provider) { Chef::Provider::Package::Windows::MSI.new(new_resource) } + + describe "expand_options" do + it "returns an empty string if passed no options" do + expect(provider.expand_options(nil)).to eql "" + end + + it "returns a string with a leading space if passed options" do + expect(provider.expand_options("--train nope --town no_way")).to eql(" --train nope --town no_way") + end + end + + describe "installed_version" do + it "returns the installed version" do + provider.stub(:get_product_property).and_return("{23170F69-40C1-2702-0920-000001000000}") + provider.stub(:get_installed_version).with("{23170F69-40C1-2702-0920-000001000000}").and_return("3.14159.1337.42") + expect(provider.installed_version).to eql("3.14159.1337.42") + end + end + + describe "package_version" do + it "returns the version of a package" do + provider.stub(:get_product_property).with(/calculator.msi$/, "ProductVersion").and_return(42) + expect(provider.package_version).to eql(42) + end + end + + describe "install_package" do + # calls shell_out! + end + + describe "remove_package" do + # calls shell_out! + end +end diff --git a/spec/unit/provider/package/windows_spec.rb b/spec/unit/provider/package/windows_spec.rb new file mode 100644 index 0000000000..e94404eea5 --- /dev/null +++ b/spec/unit/provider/package/windows_spec.rb @@ -0,0 +1,80 @@ +# +# Author:: Bryan McLellan <btm@loftninjas.org> +# Copyright:: Copyright (c) 2014 Chef Software, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require 'spec_helper' + +describe Chef::Provider::Package::Windows, :windows_only do + let(:node) { double('Chef::Node') } + let(:events) { double('Chef::Events').as_null_object } # mock all the methods + let(:run_context) { double('Chef::RunContext', :node => node, :events => events) } + let(:new_resource) { Chef::Resource::WindowsPackage.new("calculator.msi") } + let(:provider) { Chef::Provider::Package::Windows.new(new_resource, run_context) } + + describe "load_current_resource" do + before(:each) do + provider.stub(:package_provider).and_return(double('package_provider', + :installed_version => "1.0", :package_version => "2.0")) + end + + it "creates a current resource with the name of the new resource" do + provider.load_current_resource + expect(provider.current_resource).to be_a(Chef::Resource::WindowsPackage) + expect(provider.current_resource.name).to eql("calculator.msi") + end + + it "sets the current version if the package is installed" do + provider.load_current_resource + expect(provider.current_resource.version).to eql("1.0") + end + + it "sets the version to be installed" do + provider.load_current_resource + expect(provider.new_resource.version).to eql("2.0") + end + end + + describe "package_provider" do + it "sets the package provider to MSI if the the installer type is :msi" do + provider.stub(:installer_type).and_return(:msi) + expect(provider.package_provider).to be_a(Chef::Provider::Package::Windows::MSI) + end + + it "raises an error if the installer_type is unknown" do + provider.stub(:installer_type).and_return(:apt_for_windows) + expect { provider.package_provider }.to raise_error + end + end + + describe "installer_type" do + it "it returns @installer_type if it is set" do + provider.new_resource.installer_type("downeaster") + expect(provider.installer_type).to eql("downeaster") + end + + it "sets installer_type to msi if the source ends in .msi" do + provider.new_resource.source("microsoft_installer.msi") + expect(provider.installer_type).to eql(:msi) + end + + it "raises an error if it cannot determine the installer type" do + provider.new_resource.installer_type(nil) + provider.new_resource.source("tomfoolery.now") + expect { provider.installer_type }.to raise_error(ArgumentError) + end + end +end diff --git a/spec/unit/provider/service/macosx_spec.rb b/spec/unit/provider/service/macosx_spec.rb index 65639f2084..1e9656aeac 100644 --- a/spec/unit/provider/service/macosx_spec.rb +++ b/spec/unit/provider/service/macosx_spec.rb @@ -46,14 +46,32 @@ describe Chef::Provider::Service::Macosx do let(:events) {Chef::EventDispatch::Dispatcher.new} let(:run_context) { Chef::RunContext.new(node, {}, events) } let(:provider) { described_class.new(new_resource, run_context) } - let(:stdout) { StringIO.new } + let(:launchctl_stdout) { StringIO.new } + let(:plutil_stdout) { String.new <<-XML } +<?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>Label</key> + <string>io.redis.redis-server</string> +</dict> +</plist> +XML ["redis-server", "io.redis.redis-server"].each do |service_name| before do Dir.stub(:glob).and_return(["/Users/igor/Library/LaunchAgents/io.redis.redis-server.plist"], []) provider.stub(:shell_out!). with("launchctl list", {:group => 1001, :user => 101}). - and_return(double("ouput", :stdout => stdout)) + and_return(double("Status", :stdout => launchctl_stdout)) + provider.stub(:shell_out). + with(/launchctl list /, + {:group => nil, :user => nil}). + and_return(double("Status", + :stdout => launchctl_stdout, :exitstatus => 0)) + provider.stub(:shell_out!). + with(/plutil -convert xml1 -o/). + and_return(double("Status", :stdout => plutil_stdout)) File.stub(:stat).and_return(double("stat", :gid => 1001, :uid => 101)) end @@ -64,7 +82,7 @@ describe Chef::Provider::Service::Macosx do describe "#load_current_resource" do context "when launchctl returns pid in service list" do - let(:stdout) { StringIO.new <<-SVC_LIST } + let(:launchctl_stdout) { StringIO.new <<-SVC_LIST } 12761 - 0x100114220.old.machinit.thing 7777 - io.redis.redis-server - - com.lol.stopped-thing @@ -84,21 +102,21 @@ describe Chef::Provider::Service::Macosx do end describe "running unsupported actions" do + let(:launchctl_stdout) { StringIO.new <<-SVC_LIST } +12761 - 0x100114220.old.machinit.thing +7777 - io.redis.redis-server +- - com.lol.stopped-thing +SVC_LIST + before do Dir.stub(:glob).and_return(["/Users/igor/Library/LaunchAgents/io.redis.redis-server.plist"], []) end - it "should throw an exception when enable action is attempted" do - lambda {provider.run_action(:enable)}.should raise_error(Chef::Exceptions::UnsupportedAction) - end it "should throw an exception when reload action is attempted" do lambda {provider.run_action(:reload)}.should raise_error(Chef::Exceptions::UnsupportedAction) end - it "should throw an exception when disable action is attempted" do - lambda {provider.run_action(:disable)}.should raise_error(Chef::Exceptions::UnsupportedAction) - end end context "when launchctl returns empty service pid" do - let(:stdout) { StringIO.new <<-SVC_LIST } + let(:launchctl_stdout) { StringIO.new <<-SVC_LIST } 12761 - 0x100114220.old.machinit.thing - - io.redis.redis-server - - com.lol.stopped-thing @@ -118,7 +136,7 @@ describe Chef::Provider::Service::Macosx do end context "when launchctl doesn't return service entry at all" do - let(:stdout) { StringIO.new <<-SVC_LIST } + let(:launchctl_stdout) { StringIO.new <<-SVC_LIST } 12761 - 0x100114220.old.machinit.thing - - com.lol.stopped-thing SVC_LIST diff --git a/spec/unit/provider/service/solaris_smf_service_spec.rb b/spec/unit/provider/service/solaris_smf_service_spec.rb index 887c1f6b5f..af1351a4ff 100644 --- a/spec/unit/provider/service/solaris_smf_service_spec.rb +++ b/spec/unit/provider/service/solaris_smf_service_spec.rb @@ -54,34 +54,47 @@ describe Chef::Provider::Service::Solaris do describe "when discovering the current service state" do it "should create a current resource with the name of the new resource" do - @provider.stub(:popen4).with("/bin/svcs -l chef").and_return(@status) + @provider.stub!(:shell_out!).with("/bin/svcs -l chef").and_return(@status) Chef::Resource::Service.should_receive(:new).and_return(@current_resource) @provider.load_current_resource end it "should return the current resource" do - @provider.stub(:popen4).with("/bin/svcs -l chef").and_return(@status) + @provider.stub!(:shell_out!).with("/bin/svcs -l chef").and_return(@status) @provider.load_current_resource.should eql(@current_resource) end - it "should popen4 '/bin/svcs -l service_name'" do - @provider.should_receive(:popen4).with("/bin/svcs -l chef").and_return(@status) + it "should call '/bin/svcs -l service_name'" do + @provider.should_receive(:shell_out!).with("/bin/svcs -l chef").and_return(@status) @provider.load_current_resource end it "should mark service as not running" do - @provider.stub(:popen4).and_yield(@pid, @stdin, @stdout, @stderr).and_return(@status) + @provider.stub!(:shell_out!).and_return(@status) @current_resource.should_receive(:running).with(false) @provider.load_current_resource end it "should mark service as running" do - @stdout.stub(:each).and_yield("state online") - @provider.stub(:popen4).and_yield(@pid, @stdin, @stdout, @stderr).and_return(@status) + @status = mock("Status", :exitstatus => 0, :stdout => 'state online') + @provider.stub!(:shell_out!).and_return(@status) @current_resource.should_receive(:running).with(true) @provider.load_current_resource end + + it "should not mark service as maintenance" do + @provider.stub!(:shell_out!).and_return(@status) + @provider.load_current_resource + @provider.maintenance.should be_false + end + + it "should mark service as maintenance" do + @status = mock("Status", :exitstatus => 0, :stdout => 'state maintenance') + @provider.stub!(:shell_out!).and_return(@status) + @provider.load_current_resource + @provider.maintenance.should be_true + end end describe "when enabling the service" do @@ -91,19 +104,31 @@ describe Chef::Provider::Service::Solaris do end it "should call svcadm enable -s chef" do - @new_resource.stub(:enable_command).and_return("#{@new_resource.enable_command}") + @new_resource.stub!(:enable_command).and_return("#{@new_resource.enable_command}") + @provider.should_not_receive(:shell_out!).with("/usr/sbin/svcadm clear #{@current_resource.service_name}") @provider.should_receive(:shell_out!).with("/usr/sbin/svcadm enable -s #{@current_resource.service_name}").and_return(@status) - @provider.enable_service.should be_true + @provider.enable_service.should be_true @current_resource.enabled.should be_true end it "should call svcadm enable -s chef for start_service" do - @new_resource.stub(:start_command).and_return("#{@new_resource.start_command}") + @new_resource.stub!(:start_command).and_return("#{@new_resource.start_command}") + @provider.should_not_receive(:shell_out!).with("/usr/sbin/svcadm clear #{@current_resource.service_name}") @provider.should_receive(:shell_out!).with("/usr/sbin/svcadm enable -s #{@current_resource.service_name}").and_return(@status) @provider.start_service.should be_true @current_resource.enabled.should be_true end + it "should call svcadm clear chef for start_service when state maintenance" do + @status = mock("Status", :exitstatus => 0, :stdout => 'state maintenance') + @provider.stub!(:shell_out!).and_return(@status) + @provider.load_current_resource + @new_resource.stub!(:enable_command).and_return("#{@new_resource.enable_command}") + @provider.should_receive(:shell_out!).with("/usr/sbin/svcadm clear #{@current_resource.service_name}").and_return(@status) + @provider.should_receive(:shell_out!).with("/usr/sbin/svcadm enable -s #{@current_resource.service_name}").and_return(@status) + @provider.enable_service.should be_true + @current_resource.enabled.should be_true + end end diff --git a/spec/unit/recipe_spec.rb b/spec/unit/recipe_spec.rb index b0cd04b245..2bdf470143 100644 --- a/spec/unit/recipe_spec.rb +++ b/spec/unit/recipe_spec.rb @@ -339,6 +339,7 @@ describe Chef::Recipe do describe "include_recipe" do it "should evaluate another recipe with include_recipe" do node.should_receive(:loaded_recipe).with(:openldap, "gigantor") + run_context.stub(:unreachable_cookbook?).with(:openldap).and_return(false) run_context.include_recipe "openldap::gigantor" res = run_context.resource_collection.resources(:cat => "blanket") res.name.should eql("blanket") @@ -347,6 +348,7 @@ describe Chef::Recipe do it "should load the default recipe for a cookbook if include_recipe is called without a ::" do node.should_receive(:loaded_recipe).with(:openldap, "default") + run_context.stub(:unreachable_cookbook?).with(:openldap).and_return(false) run_context.include_recipe "openldap" res = run_context.resource_collection.resources(:cat => "blanket") res.name.should eql("blanket") @@ -355,12 +357,14 @@ describe Chef::Recipe do it "should store that it has seen a recipe in the run_context" do node.should_receive(:loaded_recipe).with(:openldap, "default") + run_context.stub(:unreachable_cookbook?).with(:openldap).and_return(false) run_context.include_recipe "openldap" run_context.loaded_recipe?("openldap").should be_true end it "should not include the same recipe twice" do node.should_receive(:loaded_recipe).with(:openldap, "default").exactly(:once) + run_context.stub(:unreachable_cookbook?).with(:openldap).and_return(false) cookbook_collection[:openldap].should_receive(:load_recipe).with("default", run_context) recipe.include_recipe "openldap" cookbook_collection[:openldap].should_not_receive(:load_recipe).with("default", run_context) diff --git a/spec/unit/resource/conditional_spec.rb b/spec/unit/resource/conditional_spec.rb index 1be7bcea71..4df185bcd6 100644 --- a/spec/unit/resource/conditional_spec.rb +++ b/spec/unit/resource/conditional_spec.rb @@ -24,12 +24,13 @@ describe Chef::Resource::Conditional do Mixlib::ShellOut.any_instance.stub(:run_command).and_return(nil) @status = OpenStruct.new(:success? => true) Mixlib::ShellOut.any_instance.stub(:status).and_return(@status) + @parent_resource = Chef::Resource.new(nil, Chef::Node.new) end describe "when created as an `only_if`" do describe "after running a successful command" do before do - @conditional = Chef::Resource::Conditional.only_if("true") + @conditional = Chef::Resource::Conditional.only_if(@parent_resource, "true") end it "indicates that resource convergence should continue" do @@ -40,7 +41,7 @@ describe Chef::Resource::Conditional do describe "after running a negative/false command" do before do @status.send("success?=", false) - @conditional = Chef::Resource::Conditional.only_if("false") + @conditional = Chef::Resource::Conditional.only_if(@parent_resource, "false") end it "indicates that resource convergence should not continue" do @@ -50,8 +51,8 @@ describe Chef::Resource::Conditional do describe 'after running a command which timed out' do before do - @conditional = Chef::Resource::Conditional.only_if("false") - @conditional.stub(:shell_out).and_raise(Chef::Exceptions::CommandTimeout) + @conditional = Chef::Resource::Conditional.only_if(@parent_resource, "false") + Chef::GuardInterpreter::DefaultGuardInterpreter.any_instance.stub(:shell_out).and_raise(Chef::Exceptions::CommandTimeout) end it 'indicates that resource convergence should not continue' do @@ -66,7 +67,7 @@ describe Chef::Resource::Conditional do describe "after running a block that returns a truthy value" do before do - @conditional = Chef::Resource::Conditional.only_if { Object.new } + @conditional = Chef::Resource::Conditional.only_if(@parent_resource) { Object.new } end it "indicates that resource convergence should continue" do @@ -76,7 +77,7 @@ describe Chef::Resource::Conditional do describe "after running a block that returns a falsey value" do before do - @conditional = Chef::Resource::Conditional.only_if { nil } + @conditional = Chef::Resource::Conditional.only_if(@parent_resource) { nil } end it "indicates that resource convergence should not continue" do @@ -88,7 +89,7 @@ describe Chef::Resource::Conditional do describe "when created as a `not_if`" do describe "after running a successful/true command" do before do - @conditional = Chef::Resource::Conditional.not_if("true") + @conditional = Chef::Resource::Conditional.not_if(@parent_resource, "true") end it "indicates that resource convergence should not continue" do @@ -99,7 +100,7 @@ describe Chef::Resource::Conditional do describe "after running a failed/false command" do before do @status.send("success?=", false) - @conditional = Chef::Resource::Conditional.not_if("false") + @conditional = Chef::Resource::Conditional.not_if(@parent_resource, "false") end it "indicates that resource convergence should continue" do @@ -109,8 +110,8 @@ describe Chef::Resource::Conditional do describe 'after running a command which timed out' do before do - @conditional = Chef::Resource::Conditional.not_if("false") - @conditional.stub(:shell_out).and_raise(Chef::Exceptions::CommandTimeout) + @conditional = Chef::Resource::Conditional.not_if(@parent_resource, "false") + Chef::GuardInterpreter::DefaultGuardInterpreter.any_instance.stub(:shell_out).and_raise(Chef::Exceptions::CommandTimeout) end it 'indicates that resource convergence should continue' do @@ -125,7 +126,7 @@ describe Chef::Resource::Conditional do describe "after running a block that returns a truthy value" do before do - @conditional = Chef::Resource::Conditional.not_if { Object.new } + @conditional = Chef::Resource::Conditional.not_if(@parent_resource) { Object.new } end it "indicates that resource convergence should not continue" do @@ -135,7 +136,7 @@ describe Chef::Resource::Conditional do describe "after running a block that returns a falsey value" do before do - @conditional = Chef::Resource::Conditional.not_if { nil } + @conditional = Chef::Resource::Conditional.not_if(@parent_resource) { nil } end it "indicates that resource convergence should continue" do diff --git a/spec/unit/resource/cron_spec.rb b/spec/unit/resource/cron_spec.rb index 355a7f09ba..cf821e3d32 100644 --- a/spec/unit/resource/cron_spec.rb +++ b/spec/unit/resource/cron_spec.rb @@ -143,8 +143,13 @@ describe Chef::Resource::Cron do lambda { @resource.month "13" }.should raise_error(RangeError) end - it "should reject any weekday over 7" do - lambda { @resource.weekday "8" }.should raise_error(RangeError) + describe "weekday" do + it "should reject any weekday over 7" do + lambda { @resource.weekday "8" }.should raise_error(RangeError) + end + it "should reject any symbols which don't represent day of week" do + lambda { @resource.weekday :foo }.should raise_error(RangeError) + end end it "should convert integer schedule values to a string" do diff --git a/spec/unit/resource/powershell_spec.rb b/spec/unit/resource/powershell_spec.rb index a35e37c696..da20c4f0bf 100644 --- a/spec/unit/resource/powershell_spec.rb +++ b/spec/unit/resource/powershell_spec.rb @@ -36,7 +36,91 @@ describe Chef::Resource::PowershellScript do @resource.should be_a_kind_of(Chef::Resource::PowershellScript) end - context "windowsscript" do + it "should set convert_boolean_return to false by default" do + @resource.convert_boolean_return.should == false + end + + it "should return the value for convert_boolean_return that was set" do + @resource.convert_boolean_return true + @resource.convert_boolean_return.should == true + @resource.convert_boolean_return false + @resource.convert_boolean_return.should == false + end + + context "when using guards" do + let(:resource) { @resource } + before(:each) do + resource.stub(:run_action) + resource.stub(:updated).and_return(true) + end + + it "inherits exactly the :cwd, :environment, :group, :path, :user, :umask, and :architecture attributes from a parent resource class" do + inherited_difference = Chef::Resource::PowershellScript.guard_inherited_attributes - + [:cwd, :environment, :group, :path, :user, :umask, :architecture ] + + inherited_difference.should == [] + end + + it "should allow guard interpreter to be set to Chef::Resource::Script" do + resource.guard_interpreter(:script) + allow_any_instance_of(Chef::GuardInterpreter::ResourceGuardInterpreter).to receive(:evaluate_action).and_return(false) + resource.only_if("echo hi") + end + + it "should allow guard interpreter to be set to Chef::Resource::Bash derived from Chef::Resource::Script" do + resource.guard_interpreter(:bash) + allow_any_instance_of(Chef::GuardInterpreter::ResourceGuardInterpreter).to receive(:evaluate_action).and_return(false) + resource.only_if("echo hi") + end + + it "should allow guard interpreter to be set to Chef::Resource::PowershellScript derived indirectly from Chef::Resource::Script" do + resource.guard_interpreter(:powershell_script) + allow_any_instance_of(Chef::GuardInterpreter::ResourceGuardInterpreter).to receive(:evaluate_action).and_return(false) + resource.only_if("echo hi") + end + + it "should enable convert_boolean_return by default for guards in the context of powershell_script when no guard params are specified" do + allow_any_instance_of(Chef::GuardInterpreter::ResourceGuardInterpreter).to receive(:evaluate_action).and_return(true) + allow_any_instance_of(Chef::GuardInterpreter::ResourceGuardInterpreter).to receive(:block_from_attributes).with( + {:convert_boolean_return => true, :code => "$true"}).and_return(Proc.new {}) + resource.only_if("$true") + end + + it "should enable convert_boolean_return by default for guards in non-Chef::Resource::Script derived resources when no guard params are specified" do + node = Chef::Node.new + run_context = Chef::RunContext.new(node, nil, nil) + file_resource = Chef::Resource::File.new('idontexist', run_context) + file_resource.guard_interpreter :powershell_script + + allow_any_instance_of(Chef::GuardInterpreter::ResourceGuardInterpreter).to receive(:block_from_attributes).with( + {:convert_boolean_return => true, :code => "$true"}).and_return(Proc.new {}) + resource.only_if("$true") + end + + it "should enable convert_boolean_return by default for guards in the context of powershell_script when guard params are specified" do + guard_parameters = {:cwd => '/etc/chef', :architecture => :x86_64} + allow_any_instance_of(Chef::GuardInterpreter::ResourceGuardInterpreter).to receive(:block_from_attributes).with( + {:convert_boolean_return => true, :code => "$true"}.merge(guard_parameters)).and_return(Proc.new {}) + resource.only_if("$true", guard_parameters) + end + + it "should pass convert_boolean_return as true if it was specified as true in a guard parameter" do + guard_parameters = {:cwd => '/etc/chef', :convert_boolean_return => true, :architecture => :x86_64} + allow_any_instance_of(Chef::GuardInterpreter::ResourceGuardInterpreter).to receive(:block_from_attributes).with( + {:convert_boolean_return => true, :code => "$true"}.merge(guard_parameters)).and_return(Proc.new {}) + resource.only_if("$true", guard_parameters) + end + + it "should pass convert_boolean_return as false if it was specified as true in a guard parameter" do + other_guard_parameters = {:cwd => '/etc/chef', :architecture => :x86_64} + parameters_with_boolean_disabled = other_guard_parameters.merge({:convert_boolean_return => false, :code => "$true"}) + allow_any_instance_of(Chef::GuardInterpreter::ResourceGuardInterpreter).to receive(:block_from_attributes).with( + parameters_with_boolean_disabled).and_return(Proc.new {}) + resource.only_if("$true", parameters_with_boolean_disabled) + end + end + + context "as a script running in Windows-based scripting language" do let(:resource_instance) { @resource } let(:resource_instance_name ) { @resource.command } let(:resource_name) { :powershell_script } @@ -44,5 +128,4 @@ describe Chef::Resource::PowershellScript do it_should_behave_like "a Windows script resource" end - end diff --git a/spec/unit/resource/subversion_spec.rb b/spec/unit/resource/subversion_spec.rb index 67593c5a7c..ae06ce665a 100644 --- a/spec/unit/resource/subversion_spec.rb +++ b/spec/unit/resource/subversion_spec.rb @@ -55,4 +55,9 @@ describe Chef::Resource::Subversion do @svn.svn_arguments.should be_nil end + it "hides password from custom exception message" do + @svn.svn_password "l33th4x0rpa$$w0rd" + e = @svn.customize_exception(Chef::Exceptions::Exec.new "Exception with password #{@svn.svn_password}") + e.message.include?(@svn.svn_password).should be_false + end end diff --git a/spec/unit/resource/windows_package_spec.rb b/spec/unit/resource/windows_package_spec.rb new file mode 100644 index 0000000000..c9ef8d910c --- /dev/null +++ b/spec/unit/resource/windows_package_spec.rb @@ -0,0 +1,74 @@ +# +# Author:: Bryan McLellan <btm@loftninjas.org> +# Copyright:: Copyright (c) 2014 Chef Software, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require 'spec_helper' + +describe Chef::Resource::WindowsPackage, "initialize", :windows_only do + + let(:resource) { Chef::Resource::WindowsPackage.new("solitaire.msi") } + + it "returns a Chef::Resource::WindowsPackage" do + expect(resource).to be_a_kind_of(Chef::Resource::WindowsPackage) + end + + it "sets the resource_name to :windows_package" do + expect(resource.resource_name).to eql(:windows_package) + end + + it "sets the provider to Chef::Provider::Package::Windows" do + expect(resource.provider).to eql(Chef::Provider::Package::Windows) + end + + it "supports setting installer_type" do + resource.installer_type("msi") + expect(resource.installer_type).to eql("msi") + end + + # String, Integer + [ "600", 600 ].each do |val| + it "supports setting a timeout as a #{val.class}" do + resource.timeout(val) + expect(resource.timeout).to eql(val) + end + end + + # String, Integer, Array + [ "42", 42, [47, 48, 49] ].each do |val| + it "supports setting an alternate return value as a #{val.class}" do + resource.returns(val) + expect(resource.returns).to eql(val) + end + end + + it "coverts a source to an absolute path" do + ::File.stub(:absolute_path).and_return("c:\\Files\\frost.msi") + resource.source("frost.msi") + expect(resource.source).to eql "c:\\Files\\frost.msi" + end + + it "converts slashes to backslashes in the source path" do + ::File.stub(:absolute_path).and_return("c:\\frost.msi") + resource.source("c:/frost.msi") + expect(resource.source).to eql "c:\\frost.msi" + end + + it "defaults source to the resource name" do + # it's a little late to stub out File.absolute_path + expect(resource.source).to include("solitaire.msi") + end +end diff --git a/spec/unit/resource_reporter_spec.rb b/spec/unit/resource_reporter_spec.rb index 52fd44e692..d412234596 100644 --- a/spec/unit/resource_reporter_spec.rb +++ b/spec/unit/resource_reporter_spec.rb @@ -38,7 +38,6 @@ describe Chef::ResourceReporter do @rest_client = double("Chef::REST (mock)") @rest_client.stub(:post_rest).and_return(true) @resource_reporter = Chef::ResourceReporter.new(@rest_client) - @run_id = @resource_reporter.run_id @new_resource = Chef::Resource::File.new("/tmp/a-file.txt") @new_resource.cookbook_name = "monkey" @cookbook_version = double("Cookbook::Version", :version => "1.2.3") @@ -49,6 +48,7 @@ describe Chef::ResourceReporter do @events = Chef::EventDispatch::Dispatcher.new @run_context = Chef::RunContext.new(@node, {}, @events) @run_status = Chef::RunStatus.new(@node, @events) + @run_id = @run_status.run_id Time.stub(:now).and_return(@start_time, @end_time) end diff --git a/spec/unit/resource_spec.rb b/spec/unit/resource_spec.rb index e9a60c9861..60f3bdb8ea 100644 --- a/spec/unit/resource_spec.rb +++ b/spec/unit/resource_spec.rb @@ -344,7 +344,7 @@ describe Chef::Resource do expected_keys = [ :allowed_actions, :params, :provider, :updated, :updated_by_last_action, :before, :supports, :noop, :ignore_failure, :name, :source_line, - :action, :retries, :retry_delay, :elapsed_time] + :action, :retries, :retry_delay, :elapsed_time, :guard_interpreter] (hash.keys - expected_keys).should == [] (expected_keys - hash.keys).should == [] hash[:name].should eql("funk") @@ -526,6 +526,28 @@ describe Chef::Resource do snitch_var2.should be_false end + describe "guard_interpreter attribute" do + let(:resource) { @resource } + + it "should be set to :default by default" do + resource.guard_interpreter.should == :default + end + + it "if set to :default should return :default when read" do + resource.guard_interpreter(:default) + resource.guard_interpreter.should == :default + end + + it "should raise Chef::Exceptions::ValidationFailed on an attempt to set the guard_interpreter attribute to something other than a Symbol" do + expect { resource.guard_interpreter('command_dot_com') }.to raise_error(Chef::Exceptions::ValidationFailed) + end + + it "should not raise an exception when setting the guard interpreter attribute to a Symbol" do + Chef::GuardInterpreter::ResourceGuardInterpreter.stub(:new).and_return(nil) + expect { resource.guard_interpreter(:command_dot_com) }.not_to raise_error + end + end + end describe "should_skip?" do diff --git a/spec/unit/rest_spec.rb b/spec/unit/rest_spec.rb index a53b4c9507..a8eb1ac7db 100644 --- a/spec/unit/rest_spec.rb +++ b/spec/unit/rest_spec.rb @@ -59,17 +59,38 @@ describe Chef::REST do let(:log_stringio) { StringIO.new } + let(:request_id) {"1234"} + let(:rest) do Chef::REST::CookieJar.stub(:instance).and_return({}) + Chef::RequestID.instance.stub(:request_id).and_return(request_id) rest = Chef::REST.new(base_url, nil, nil) Chef::REST::CookieJar.instance.clear rest end + let(:standard_read_headers) {{"Accept"=>"application/json", "Accept"=>"application/json", "Accept-Encoding"=>"gzip;q=1.0,deflate;q=0.6,identity;q=0.3", "X-REMOTE-REQUEST-ID"=>request_id}} + let(:standard_write_headers) {{"Accept"=>"application/json", "Content-Type"=>"application/json", "Accept"=>"application/json", "Accept-Encoding"=>"gzip;q=1.0,deflate;q=0.6,identity;q=0.3", "X-REMOTE-REQUEST-ID"=>request_id}} + before(:each) do Chef::Log.init(log_stringio) end + it "should have content length validation middleware after compressor middleware" do + middlewares = rest.instance_variable_get(:@middlewares) + content_length = middlewares.find_index { |e| e.is_a? Chef::HTTP::ValidateContentLength } + decompressor = middlewares.find_index { |e| e.is_a? Chef::HTTP::Decompressor } + + content_length.should_not be_nil + decompressor.should_not be_nil + (decompressor < content_length).should be_true + end + + it "should allow the options hash to be frozen" do + options = {}.freeze + # should not raise any exception + Chef::REST.new(base_url, nil, nil, options) + end describe "calling an HTTP verb on a path or absolute URL" do it "adds a relative URL to the base url it was initialized with" do @@ -82,7 +103,7 @@ describe Chef::REST do it "makes a :GET request with the composed url object" do rest.should_receive(:send_http_request). - with(:GET, monkey_uri, STANDARD_READ_HEADERS, false). + with(:GET, monkey_uri, standard_read_headers, false). and_return([1,2,3]) rest.should_receive(:apply_response_middleware).with(1,2,3).and_return([1,2,3]) rest.should_receive('success_response?'.to_sym).with(1).and_return(true) @@ -94,12 +115,9 @@ describe Chef::REST do rest.get_rest("monkey", true) end - STANDARD_READ_HEADERS = {"Accept"=>"application/json", "Accept"=>"application/json", "Accept-Encoding"=>"gzip;q=1.0,deflate;q=0.6,identity;q=0.3"} - STANDARD_WRITE_HEADERS = {"Accept"=>"application/json", "Content-Type"=>"application/json", "Accept"=>"application/json", "Accept-Encoding"=>"gzip;q=1.0,deflate;q=0.6,identity;q=0.3"} - it "makes a :DELETE request with the composed url object" do rest.should_receive(:send_http_request). - with(:DELETE, monkey_uri, STANDARD_READ_HEADERS, false). + with(:DELETE, monkey_uri, standard_read_headers, false). and_return([1,2,3]) rest.should_receive(:apply_response_middleware).with(1,2,3).and_return([1,2,3]) rest.should_receive('success_response?'.to_sym).with(1).and_return(true) @@ -108,7 +126,7 @@ describe Chef::REST do it "makes a :POST request with the composed url object and data" do rest.should_receive(:send_http_request). - with(:POST, monkey_uri, STANDARD_WRITE_HEADERS, "\"data\""). + with(:POST, monkey_uri, standard_write_headers, "\"data\""). and_return([1,2,3]) rest.should_receive(:apply_response_middleware).with(1,2,3).and_return([1,2,3]) rest.should_receive('success_response?'.to_sym).with(1).and_return(true) @@ -117,7 +135,7 @@ describe Chef::REST do it "makes a :PUT request with the composed url object and data" do rest.should_receive(:send_http_request). - with(:PUT, monkey_uri, STANDARD_WRITE_HEADERS, "\"data\""). + with(:PUT, monkey_uri, standard_write_headers, "\"data\""). and_return([1,2,3]) rest.should_receive(:apply_response_middleware).with(1,2,3).and_return([1,2,3]) rest.should_receive('success_response?'.to_sym).with(1).and_return(true) @@ -142,27 +160,27 @@ describe Chef::REST do it 'calls the authn middleware' do data = "\"secure data\"" - auth_headers = STANDARD_WRITE_HEADERS.merge({"auth_done"=>"yep"}) + auth_headers = standard_write_headers.merge({"auth_done"=>"yep"}) rest.authenticator.should_receive(:handle_request). - with(:POST, monkey_uri, STANDARD_WRITE_HEADERS, data). + with(:POST, monkey_uri, standard_write_headers, data). and_return([:POST, monkey_uri, auth_headers, data]) rest.should_receive(:send_http_request). with(:POST, monkey_uri, auth_headers, data). and_return([1,2,3]) rest.should_receive('success_response?'.to_sym).with(1).and_return(true) - rest.raw_http_request(:POST, monkey_uri, STANDARD_WRITE_HEADERS, data) + rest.raw_http_request(:POST, monkey_uri, standard_write_headers, data) end it 'sets correct authn headers' do data = "\"secure data\"" - method, uri, auth_headers, d = rest.authenticator.handle_request(:POST, monkey_uri, STANDARD_WRITE_HEADERS, data) + method, uri, auth_headers, d = rest.authenticator.handle_request(:POST, monkey_uri, standard_write_headers, data) rest.should_receive(:send_http_request). with(:POST, monkey_uri, auth_headers, data). and_return([1,2,3]) rest.should_receive('success_response?'.to_sym).with(1).and_return(true) - rest.raw_http_request(:POST, monkey_uri, STANDARD_WRITE_HEADERS, data) + rest.raw_http_request(:POST, monkey_uri, standard_write_headers, data) end end @@ -244,6 +262,7 @@ describe Chef::REST do let(:rest) do Net::HTTP.stub(:new).and_return(http_client) Chef::REST::CookieJar.stub(:instance).and_return({}) + Chef::RequestID.instance.stub(:request_id).and_return(request_id) rest = Chef::REST.new(base_url, nil, nil) Chef::REST::CookieJar.instance.clear rest @@ -254,6 +273,7 @@ describe Chef::REST do 'Accept' => 'application/json', 'X-Chef-Version' => Chef::VERSION, 'Accept-Encoding' => Chef::REST::RESTRequest::ENCODING_GZIP_DEFLATE, + 'X-REMOTE-REQUEST-ID' => request_id } end @@ -275,6 +295,7 @@ describe Chef::REST do 'X-Chef-Version' => Chef::VERSION, 'Accept-Encoding' => Chef::REST::RESTRequest::ENCODING_GZIP_DEFLATE, 'Host' => host_header, + 'X-REMOTE-REQUEST-ID' => request_id } end @@ -287,6 +308,11 @@ describe Chef::REST do rest.request(:GET, url, {}) end + it "should always include the X-Remote-Request-Id header" do + Net::HTTP::Get.should_receive(:new).with("/?foo=bar", base_headers).and_return(request_mock) + rest.request(:GET, url, {}) + end + it "sets the user agent to chef-client" do # XXX: must reset to default b/c knife changes the UA Chef::REST::RESTRequest.user_agent = Chef::REST::RESTRequest::DEFAULT_UA @@ -342,6 +368,7 @@ describe Chef::REST do let(:rest) do Net::HTTP.stub(:new).and_return(http_client) Chef::REST::CookieJar.instance["#{url.host}:#{url.port}"] = "cookie monster" + Chef::RequestID.instance.stub(:request_id).and_return(request_id) rest = Chef::REST.new(base_url, nil, nil) rest end @@ -542,7 +569,20 @@ describe Chef::REST do expected_headers = {'Accept' => "*/*", 'X-Chef-Version' => Chef::VERSION, 'Accept-Encoding' => Chef::REST::RESTRequest::ENCODING_GZIP_DEFLATE, - 'Host' => host_header} + 'Host' => host_header, + 'X-REMOTE-REQUEST-ID'=> request_id + } + Net::HTTP::Get.should_receive(:new).with("/?foo=bar", expected_headers).and_return(request_mock) + rest.streaming_request(url, {}) + end + + it "build a new HTTP GET request with the X-Remote-Request-Id header" do + expected_headers = {'Accept' => "*/*", + 'X-Chef-Version' => Chef::VERSION, + 'Accept-Encoding' => Chef::REST::RESTRequest::ENCODING_GZIP_DEFLATE, + 'Host' => host_header, + 'X-REMOTE-REQUEST-ID'=> request_id + } Net::HTTP::Get.should_receive(:new).with("/?foo=bar", expected_headers).and_return(request_mock) rest.streaming_request(url, {}) end diff --git a/spec/unit/run_context/cookbook_compiler_spec.rb b/spec/unit/run_context/cookbook_compiler_spec.rb index 52f4772206..5c50c3dd4b 100644 --- a/spec/unit/run_context/cookbook_compiler_spec.rb +++ b/spec/unit/run_context/cookbook_compiler_spec.rb @@ -170,5 +170,17 @@ describe Chef::RunContext::CookbookCompiler do :"circular-dep1", :"test-with-circular-deps"] end + + it "determines if a cookbook is in the list of cookbooks reachable by dependency" do + node.run_list("test-with-deps::default", "test-with-deps::server") + compiler.cookbook_order.should == [:dependency1, :dependency2, :"test-with-deps"] + compiler.unreachable_cookbook?(:dependency1).should be_false + compiler.unreachable_cookbook?(:dependency2).should be_false + compiler.unreachable_cookbook?(:'test-with-deps').should be_false + compiler.unreachable_cookbook?(:'circular-dep1').should be_true + compiler.unreachable_cookbook?(:'circular-dep2').should be_true + end + + end end diff --git a/spec/unit/run_context_spec.rb b/spec/unit/run_context_spec.rb index 39b8a8a50d..813102527b 100644 --- a/spec/unit/run_context_spec.rb +++ b/spec/unit/run_context_spec.rb @@ -79,6 +79,13 @@ describe Chef::RunContext do @node.include_attribute("test::george") end + it "raises an error when attempting to include_recipe from a cookbook not reachable by run list or dependencies" do + @node.should_receive(:loaded_recipe).with(:ancient, "aliens") + lambda do + @run_context.include_recipe("ancient::aliens") + # In CHEF-5120, this becomes a Chef::Exceptions::MissingCookbookDependency error: + end.should raise_error(Chef::Exceptions::CookbookNotFound) + end end diff --git a/spec/unit/util/editor_spec.rb b/spec/unit/util/editor_spec.rb new file mode 100644 index 0000000000..06370f7de0 --- /dev/null +++ b/spec/unit/util/editor_spec.rb @@ -0,0 +1,152 @@ +require 'spec_helper' +require 'chef/util/editor' + +describe Chef::Util::Editor do + describe '#initialize' do + it 'takes an Enumerable of lines' do + editor = described_class.new(File.open(__FILE__)) + expect(editor.lines).to be == IO.readlines(__FILE__) + end + + it 'makes a copy of an Array' do + array = Array.new + editor = described_class.new(array) + expect(editor.lines).to_not be(array) + end + end + + subject(:editor) { described_class.new(input_lines) } + let(:input_lines) { ['one', 'two', 'two', 'three'] } + + describe '#append_line_after' do + context 'when there is no match' do + subject(:execute) { editor.append_line_after('missing', 'new') } + + it('returns the number of added lines') { should be == 0 } + it 'does not add any lines' do + expect { execute }.to_not change { editor.lines } + end + end + + context 'when there is a match' do + subject(:execute) { editor.append_line_after('two', 'new') } + + it('returns the number of added lines') { should be == 2 } + it 'adds a line after each match' do + execute + expect(editor.lines).to be == ['one', 'two', 'new', 'two', 'new', 'three'] + end + end + + it 'matches a Regexp' do + expect(editor.append_line_after(/^ee/, 'new')).to be == 0 + expect(editor.append_line_after(/ee$/, 'new')).to be == 1 + end + end + + describe '#append_line_if_missing' do + context 'when there is no match' do + subject(:execute) { editor.append_line_if_missing('missing', 'new') } + + it('returns the number of added lines') { should be == 1 } + it 'adds a line to the end' do + execute + expect(editor.lines).to be == ['one', 'two', 'two', 'three', 'new'] + end + end + + context 'when there is a match' do + subject(:execute) { editor.append_line_if_missing('one', 'new') } + + it('returns the number of added lines') { should be == 0 } + it 'does not add any lines' do + expect { execute }.to_not change { editor.lines } + end + end + + it 'matches a Regexp' do + expect(editor.append_line_if_missing(/ee$/, 'new')).to be == 0 + expect(editor.append_line_if_missing(/^ee/, 'new')).to be == 1 + end + end + + describe '#remove_lines' do + context 'when there is no match' do + subject(:execute) { editor.remove_lines('missing') } + + it('returns the number of removed lines') { should be == 0 } + it 'does not remove any lines' do + expect { execute }.to_not change { editor.lines } + end + end + + context 'when there is a match' do + subject(:execute) { editor.remove_lines('two') } + + it('returns the number of removed lines') { should be == 2 } + it 'removes the matching lines' do + execute + expect(editor.lines).to be == ['one', 'three'] + end + end + + it 'matches a Regexp' do + expect(editor.remove_lines(/^ee/)).to be == 0 + expect(editor.remove_lines(/ee$/)).to be == 1 + end + end + + describe '#replace' do + context 'when there is no match' do + subject(:execute) { editor.replace('missing', 'new') } + + it('returns the number of changed lines') { should be == 0 } + it 'does not change any lines' do + expect { execute }.to_not change { editor.lines } + end + end + + context 'when there is a match' do + subject(:execute) { editor.replace('two', 'new') } + + it('returns the number of changed lines') { should be == 2 } + it 'replaces the matching portions' do + execute + expect(editor.lines).to be == ['one', 'new', 'new', 'three'] + end + end + + it 'matches a Regexp' do + expect(editor.replace(/^ee/, 'new')).to be == 0 + expect(editor.replace(/ee$/, 'new')).to be == 1 + expect(editor.lines).to be == ['one', 'two', 'two', 'thrnew'] + end + end + + describe '#replace_lines' do + context 'when there is no match' do + subject(:execute) { editor.replace_lines('missing', 'new') } + + it('returns the number of changed lines') { should be == 0 } + it 'does not change any lines' do + expect { execute }.to_not change { editor.lines } + end + end + + context 'when there is a match' do + subject(:execute) { editor.replace_lines('two', 'new') } + + it('returns the number of replaced lines') { should be == 2 } + it 'replaces the matching line' do + execute + expect(editor.lines).to be == ['one', 'new', 'new', 'three'] + end + end + + it 'matches a Regexp' do + expect(editor.replace_lines(/^ee/, 'new')).to be == 0 + expect(editor.replace_lines(/ee$/, 'new')).to be == 1 + expect(editor.lines).to be == ['one', 'two', 'two', 'new'] + end + end +end diff --git a/spec/unit/util/file_edit_spec.rb b/spec/unit/util/file_edit_spec.rb index d1d87a6bda..139b29d9ce 100644 --- a/spec/unit/util/file_edit_spec.rb +++ b/spec/unit/util/file_edit_spec.rb @@ -17,6 +17,7 @@ # require 'spec_helper' +require 'tempfile' describe Chef::Util::FileEdit do @@ -80,6 +81,17 @@ new line inserted EOF end + let(:append_twice) do + <<-EOF +127.0.0.1 localhost +255.255.255.255 broadcasthost +::1 localhost +fe80::1%lo0 localhost +once +twice + EOF + end + let(:target_file) do f = Tempfile.open('file_edit_spec') f.write(starting_content) @@ -104,7 +116,7 @@ new line inserted # CHEF-5018: people have monkey patched this and it has accidentally been broken it "should read the contents into memory as an array" do - expect(fedit.send(:contents)).to be_instance_of(Array) + expect(fedit.send(:editor).lines).to be_instance_of(Array) end end @@ -123,18 +135,21 @@ new line inserted describe "search_file_replace" do it "should accept regex passed in as a string (not Regexp object) and replace the match if there is one" do fedit.search_file_replace("localhost", "replacement") + fedit.unwritten_changes?.should be_true fedit.write_file expect(edited_file_contents).to eq(localhost_replaced) end it "should accept regex passed in as a Regexp object and replace the match if there is one" do fedit.search_file_replace(/localhost/, "replacement") + fedit.unwritten_changes?.should be_true fedit.write_file expect(edited_file_contents).to eq(localhost_replaced) end it "should do nothing if there isn't a match" do fedit.search_file_replace(/pattern/, "replacement") + fedit.unwritten_changes?.should be_false fedit.write_file expect(edited_file_contents).to eq(starting_content) end @@ -143,6 +158,7 @@ new line inserted describe "search_file_replace_line" do it "should search for match and replace the whole line" do fedit.search_file_replace_line(/localhost/, "replacement line") + fedit.unwritten_changes?.should be_true fedit.write_file expect(edited_file_contents).to eq(localhost_line_replaced) end @@ -151,6 +167,7 @@ new line inserted describe "search_file_delete" do it "should search for match and delete the match" do fedit.search_file_delete(/localhost/) + fedit.unwritten_changes?.should be_true fedit.write_file expect(edited_file_contents).to eq(localhost_deleted) end @@ -159,6 +176,7 @@ new line inserted describe "search_file_delete_line" do it "should search for match and delete the matching line" do fedit.search_file_delete_line(/localhost/) + fedit.unwritten_changes?.should be_true fedit.write_file expect(edited_file_contents).to eq(localhost_line_deleted) end @@ -167,6 +185,7 @@ new line inserted describe "insert_line_after_match" do it "should search for match and insert the given line after the matching line" do fedit.insert_line_after_match(/localhost/, "new line inserted") + fedit.unwritten_changes?.should be_true fedit.write_file expect(edited_file_contents).to eq(append_after_all_localhost) end @@ -175,14 +194,31 @@ new line inserted describe "insert_line_if_no_match" do it "should search for match and insert the given line if no line match" do fedit.insert_line_if_no_match(/pattern/, "new line inserted") + fedit.unwritten_changes?.should be_true fedit.write_file expect(edited_file_contents).to eq(append_after_content) end it "should do nothing if there is a match" do fedit.insert_line_if_no_match(/localhost/, "replacement") + fedit.unwritten_changes?.should be_false fedit.write_file expect(edited_file_contents).to eq(starting_content) end + + it "should work more than once" do + fedit.insert_line_if_no_match(/missing/, "once") + fedit.insert_line_if_no_match(/missing/, "twice") + fedit.write_file + expect(edited_file_contents).to eq(append_twice) + end + end + + describe "file_edited" do + it "should return true if a file got edited" do + fedit.insert_line_if_no_match(/pattern/, "new line inserted") + fedit.write_file + expect(fedit.file_edited?).to be_true + end end end |