diff options
500 files changed, 19315 insertions, 5875 deletions
diff --git a/.gitignore b/.gitignore index ecba9f4030..39962d2f5f 100644 --- a/.gitignore +++ b/.gitignore @@ -2,10 +2,12 @@ coverage .DS_Store pkg +tags */tags *~ # you should check in your Gemfile.lock in applications, and not in gems +external_tests/*.lock Gemfile.lock Gemfile.local diff --git a/.travis.yml b/.travis.yml index 699d8237ad..2910725d7b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,4 +1,5 @@ language: ruby +cache: bundler sudo: false # Early warning system to catch if Rubygems breaks something @@ -15,12 +16,8 @@ branches: # do not run expensive spec tests on PRs, only on branches script: " echo '--color\n-fp' > .rspec; -if [ ${TRAVIS_PULL_REQUEST} = 'false' ]; -then - bundle exec rake spec:all; -else - bundle exec rake spec; -fi" +bundle exec rake spec; +" env: global: @@ -28,14 +25,13 @@ env: matrix: include: - - rvm: 2.0.0 - - rvm: 2.1.5 - - rvm: 2.2.0 - - rvm: 2.1.5 + - rvm: 2.1 + - rvm: 2.2 + - rvm: 2.1 gemfile: pedant.gemfile script: bundle exec rake pedant ### START TEST KITCHEN ONLY ### - - rvm: 2.1.5 + - rvm: 2.1 gemfile: kitchen-tests/Gemfile before_install: - echo -n $DO_KEY_CHUNK_{0..30} >> ~/.ssh/id_aws.base64 @@ -45,6 +41,8 @@ matrix: script: # FIXME: we should fix centos-6 against AWS and then enable it here - if [ "$TRAVIS_SECURE_ENV_VARS" = "true" ]; then bundle exec kitchen test ubuntu; fi + after_failure: + - cat .kitchen/logs/kitchen.log after_script: - if [ "$TRAVIS_SECURE_ENV_VARS" = "true" ]; then bundle exec kitchen destroy ubuntu; fi env: @@ -80,6 +78,7 @@ matrix: - secure: QHuMdtFCvttiIOx6iS+lH4bKXZMwsgVQ6FPsUW5zJ7uw6mAEWKEil9xNk4aYV9FywinwUs4fnFlnIW/Gj1gLkUjm4DtxdmRZIlRXIbgsNch6H916TCPg4Q2oPsW2nVdXPjW/2jhkfLUiSnuhL+ylami1NF8Up7vokXknh/jFNZU= - secure: GTfrUVmMQSxho3Ia4Y1ONqKvVMD34GHF2/TJb8UdQV7iH+nVxVXpy3nWaCXa9ri7lRzMefkoVLy0gKK13YoVd7w3d2S3/IfNakC85XfN6VuOzK/FDkA0WoPrgKjcQ64I+3dQ6cgrMWWTieKwRZy+Ve24iRbnN055Hk+VRMu6OGw= - secure: SOMYGVfHLkHsH6koxpw68YQ4ydEo6YXPhHbrYGQbehUbFa6+OZzBcAJRJbKjyhD2AZRvNr2jB8XnjYKvVyDGQRpkWhGYZ7CpHqINpDsqKBsbiMe3/+KmKQqS+UKxNGefquoOvyQ1N8Xy77dkWYokRtGMEuR12RkZLonxiDW8Qyg= + - secure: bSsDg+dJnPFdFiC/tbb61HdLh/Q0z2RVVAReT1wvV1BN4fN4NydvkUGbQmyFNyyunLulEs+X0oFma9L0497nUlTnan8UOg9sIleTSybPX6E9xSKKCItH1GgDw8bM9Igez5OOrrePBD3altVrH+FmGx0dlTQgM/KZMN50BJ79cXw= ### END TEST KITCHEN ONLY ### notifications: on_change: true diff --git a/CHANGELOG.md b/CHANGELOG.md index db07af7989..a3605e3084 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,81 @@ -## Unreleased +## 12.4.0 + +* [**Phil Dibowitz**](https://github.com/jaymzh): + Fix multipackage and architectures +* [**Igor Shpakov**](https://github.com/Igorshp): + Always run exception handlers + Prioritise manual ssh attribute over automatic ones for knife +* [**Noah Kantrowitz**](https://github.com/coderanger): + Cache service\_resource\_providers for the duration of the run. #2953 +* [**Slava Kardakov**](https://github.com/ojab): + Fix installation of yum packages with version constraints #3155 +* [**Dave Eddy**](https://github.com/bahamas10): + fix smartos\_package for new "pkgin" output, fixes #3112 #3165 +* [**Yukihiko SAWANOBORI**](https://github.com/sawanoboly): + Show Chef version on chef shell prompt +* [**Jacob Minshall**](https://github.com/minshallj): + Ensure suid bit is preserved if group or owner changes +* [**Tim Smith**](https://github.com/tas50): + Convert wiki links to point to docs.chef.io +* [**SAWANOBORI Yukihiko**](https://github.com/sawanoboly): + Add Chef::Log::Syslog class for integrating sending logs to syslog +* [**Pavel Yudin**](https://github.com/Kasen): + Ensure LWRP and HWRP @action variable is consistent #3156 +* [**Dan Bjorge**](https://github.com/dbjorge): + Fix bad Windows securable\_resource functional spec assumptions for default file owners/groups #3266 +* [**Yukihiko SAWANOBORI**](https://github.com/sawanoboly): Pass name by + knife cil attribute [pr#3195](https://github.com/chef/chef/pull/3195) +* [**Torben Knerr**](https://github.com/tknerr): + Allow knife sub-command loader to match platform specific gems. [pr#3281](https://github.com/chef/chef/pull/3281) +* [**Steve Lowe**](https://github.com/SteveLowe): + Fix copying ntfs dacl and sacl when they are nil. [pr#3066](https://github.com/chef/chef/pull/3066) + +* [pr#3339](https://github.com/chef/chef/pull/3339): Powershell command wrappers to make argument passing to knife/chef-client etc. easier. +* [pr#3720](https://github.com/chef/chef/pull/3270): Extract chef's configuration to a separate gem. Code stays in the Chef git repo. +* [pr#3321](https://github.com/chef/chef/pull/3321): Add an integration test of chef-client with empty ENV. +* [pr#3278](https://github.com/chef/chef/pull/3278): Switch over Windows builds to universal builds. +* [pr#2877](https://github.com/chef/chef/pull/2877): Convert bootstrap template to use sh. +* [Issue #3316](https://github.com/chef/chef/issues/3316): Fix idempotency issues with the `windows_package` resource +* [pr#3295](https://github.com/chef/chef/pull/3295): Stop mutating `new_resource.checksum` in file providers. Fixes some ChecksumMismatch exceptions like [issue#3168](https://github.com/chef/chef/issues/3168) +* [pr#3320](https://github.com/chef/chef/pull/3320): Sanitize non-UTF8 characters in the node data before doing node.save(). Works around many UTF8 exception issues reported on node.save(). +* Implemented X-Ops-Server-API-Version with a API version of 0, as well as error handling when the Chef server does not support the API version that the client supports. +* [pr#3327](https://github.com/chef/chef/pull/3327): Fix unreliable AIX service group parsing mechanism. +* [pr#3333](https://github.com/chef/chef/pull/3333): Fix SSL errors when connecting to private Supermarkets +* [pr#3340](https://github.com/chef/chef/pull/3340): Allow Event dispatch subscribers to be inspected. +* [Issue #3055](https://github.com/chef/chef/issues/3055): Fix regex parsing for recipe failures on Windows +* [pr#3345](https://github.com/chef/chef/pull/3345): Windows Event log logger +* [pr#3336](https://github.com/chef/chef/pull/3336): Remote file understands UNC paths +* [pr#3269](https://github.com/chef/chef/pull/3269): Deprecate automatic recipe DSL for classes in `Chef::Resource` +* [pr#3360](https://github.com/chef/chef/pull/3360): Add check_resource_semantics! lifecycle method to provider +* [pr#3344](https://github.com/chef/chef/pull/3344): Rewrite Windows user resouce code to use ffi instead of win32-api +* [pr#3318](https://github.com/chef/chef/pull/3318): Modify Windows package provider to allow for url source +* [pr#3381](https://github.com/chef/chef/pull/3381): warn on cookbook self-deps +* [pr#2312](https://github.com/chef/chef/pull/2312): fix `node[:recipes]` duplication, add `node[:cookbooks]` and `node[:expanded_run_list]` +* [pr#3325](https://github.com/chef/chef/pull/3325): enforce passing a node name with validatorless bootstrapping +* [pr#3398](https://github.com/chef/chef/pull/3398): Allow spaces in files for the `remote_file` resource +* [Issue #3010](https://github.com/chef/chef/issues/3010) Fixed `knife user` for use with current and future versions of Chef Server 12, with continued backwards compatible support for use with Open Source Server 11. +* [pr#3438](https://github.com/chef/chef/pull/3438) Server API V1 support. Vast improvements to and testing expansion for Chef::User, Chef::ApiClient, and related knife commands. Deprecated Open Source Server 11 user support to the Chef::OscUser and knife osc_user namespace, but with backwards compatible support via knife user. +* [Issue #2247](https://github.com/chef/chef/issues/2247): `powershell_script` returns 0 for scripts with syntax errors +* [pr#3080](https://github.com/chef/chef/pull/3080): Issue 2247: `powershell_script` exit status should be nonzero for syntax errors +* [pr#3441](https://github.com/chef/chef/pull/3441): Add `powershell_out` mixin to core chef +* [pr#3448](https://github.com/chef/chef/pull/3448): Fix `dsc_resource` to work with wmf5 april preview +* [pr#3392](https://github.com/chef/chef/pull/3392): Comment up `Chef::Client` and privatize/deprecate unused things +* [pr#3419](https://github.com/chef/chef/pull/3419): Fix cli issue with `chef_repo_path` when ENV variable is unset +* [pr#3358](https://github.com/chef/chef/pull/3358): Separate audit and converge failures +* [pr#3431](https://github.com/chef/chef/pull/3431): Fix backups on windows for the file resource +* [pr#3397](https://github.com/chef/chef/pull/3397): Validate owner exists in directory resources +* [pr#3418](https://github.com/chef/chef/pull/3418): Add `shell_out` mixin to Chef::Resource class for use in `not_if`/`only_if` conditionals, etc. +* [pr#3406](https://github.com/chef/chef/pull/3406): Add wide-char 'Environment' to `broadcast_env_change` mixin for setting windows environment variables +* [pr#3442](https://github.com/chef/chef/pull/3442): Add `resource_name` to top-level Resource class to make defining resources easier. +* [pr#3447](https://github.com/chef/chef/pull/3447): Add `allowed_actions` and `default_action` to top-level Resource class. +* [pr#3475](https://github.com/chef/chef/pull/3475): Fix `shell_out` timeouts in all package providers to respect timeout property on the resource. +* [pr#3477](https://github.com/chef/chef/pull/3477): Update `zypper_package` to look like the rest of our package classes. +* [pr#3483](https://github.com/chef/chef/pull/3483): Allow `include_recipe` from LWRP providers. +* [pr#3495](https://github.com/chef/chef/pull/3495): Make resource name automatically determined from class name, and provide DSL for it. +* [pr#3497](https://github.com/chef/chef/pull/3497): Issue 3485: Corruption of node's run\_context when non-default guard\_interpreter is evaluated +* [pr#3299](https://github.com/chef/chef/pull/3299): Remove experimental warning on audit mode + +## 12.3.0 * [pr#3160](https://github.com/chef/chef/pull/3160): Use Chef Zero in socketless mode for local mode, add `--no-listen` flag to disable port @@ -9,16 +86,24 @@ Reset $HOME to user running chef-client when running via sudo * [**Torben Knerr**](https://github.com/tknerr): Allow for the chef gem installation to succeed without elevated privileges #3126 +* [**Mike Dodge**](https://github.com/mikedodge04) + MacOSX services: Load LaunchAgents as console user, adding plist and + session_type options. +* [**Eric Herot**](https://github.com/eherot) + Ensure knife ssh doesn't use a non-existant field for hostname #3131 +* [**Tom Hughes**](https://github.com/tomhughes) + Ensure searches progress in the face of incomplete responses #3135 + * [pr#3162](https://github.com/chef/chef/pull/3162): Add `--minimal-ohai` flag to client/solo/apply; restricts ohai to only the bare minimum of plugins. * Ensure link's path attribute works with delayed #3130 * gem_package, chef_gem should not shell out to using https://rubygems.org #2867 -* [**Mike Dodge**](https://github.com/mikedodge04) - MacOSX services: Load LaunchAgents as console user, adding plist and - session_type options. * Add dynamic resource resolution similar to dynamic provider resolution * Add Chef class fascade to internal structures +* Fix nil pointer for windows event logger #3200 +* Use partial search for knife status +* Ensure chef/knife properly honours proxy config ## 12.2.1 * [Issue 3153](https://github.com/chef/chef/issues/3153): Fix bug where unset HOME would cause chef to crash @@ -34,14 +119,14 @@ ## 12.1.2 * [Issue 3022](https://github.com/chef/chef/issues/3022): Homebrew Cask install fails - FIXME (remove on 12.2.0 release): 3022 was only merged to 12-stable and #3077 or its descendant should fix this + FIXME (remove on 12.2.0 release): 3022 was only merged to 12-stable and #3077 or its descendant should fix this * [Issue 3059](https://github.com/chef/chef/issues/3059): Chef 12.1.1 yum_package silently fails * [Issue 3078](https://github.com/chef/chef/issues/3078): Compat break in audit-mode changes ## 12.1.1 * [**Phil Dibowitz**](https://github.com/jaymzh): [Issue 3008](https://github.com/chef/chef/issues/3008) Allow people to pass in `source` to package -* [Issue 3011](https://github.com/chef/chef/issues/3011) `package` provider base should include +* [Issue 3011](https://github.com/chef/chef/issues/3011) `package` provider base should include `Chef::Mixin::Command` as there are still providers that use it. * [**Ranjib Dey**](https://github.com/ranjib): [Issue 3019](https://github.com/chef/chef/issues/3019) Fix data fetching when explicit attributes are passed diff --git a/DOC_CHANGES.md b/DOC_CHANGES.md index 62de8633cf..46712f114c 100644 --- a/DOC_CHANGES.md +++ b/DOC_CHANGES.md @@ -6,21 +6,79 @@ Example Doc Change: Description of the required change. --> -### Chef Client and Knife `--no-listen` Flag and `listen` Config Option - -Chef Client and Knife have a `--no-listen` CLI option. It is only -relevant when using local mode (`-z`). When this flag is given, Chef -Zero does not bind to a port on localhost. The same behavior can be -activated by setting `listen false` in the relevant config file. - -### Chef Client, Solo, and Apply `--minimal-ohai` Flag - -Chef Client, Solo, and Apply all implement a `--minimal-ohai` flag. When -set, Chef only runs the bare minimum necessary ohai plugins required for -internal functionality. This reduces the run time of ohai and might -improve Chef performance by reducing the amount of data kept in memory. -Most users should NOT use this mode, however, because cookbooks that -rely on data collected by other ohai plugins will definitely be broken -when Chef is run in this mode. It may be possible for advanced users to -work around that by using the ohai resource to collect the "missing" -data during the compile phase. +### Resources now *all* get automatic DSL + +When you declare a resource (no matter where) you now get automatic DSL for it, based on your class name: + +```ruby +module MyModule + class MyResource < Chef::Resource + # Names the resource "my_resource" + end +end +``` + +When this happens, the resource can be used in a recipe: + +```ruby +my_resource 'blah' do +end +``` + +If you have an abstract class that should *not* have DSL, set `resource_name` to `nil`: + +```ruby +module MyModule + # This will not have DSL + class MyBaseResource < Chef::Resource + resource_name nil + end + # This will have DSL `my_resource` + class MyResource < MyBaseResource + end +end +``` + +When you do this, `my_base_resource` will not work in a recipe (but `my_resource` will). + +You can still use `provides` to provide other DSL names: + +```ruby +module MyModule + class MyResource < Chef::Resource + provides :super_resource + end +end +``` + +Which enables this recipe: + +```ruby +super_resource 'wowzers' do +end +``` + +(Note that when you use provides in this manner, resource_name will be `my_resource` and declared_type will be `super_resource`. This won't affect most people, but it is worth noting as a matter of explanation.) + +Users are encouraged to declare resources in their own namespaces instead of putting them in the `Chef::Resource` namespace. + +### Resources may now use `allowed_actions` and `default_action` + +Instead of overriding `Chef::Resource.initialize` and setting `@allowed_actions` and `@action` in the constructor, you may now use the `allowed_actions` and `default_action` DSL to declare them: + +```ruby +class MyResource < Chef::Resource + allowed_actions :create, :delete + default_action :create +end +``` + +### LWRPs are no longer automatically placed in the `Chef::Resource` namespace + +Starting with Chef 12.4.0, accessing an LWRP class by name from the `Chef::Resource` namespace will trigger a deprecation warning message. This means that if your cookbook includes the LWRP `mycookbook/resources/myresource.rb`, you will no longer be able to extend or reference `Chef::Resource::MycookbookMyresource` in Ruby code. LWRP recipe DSL does not change: the LWRP will still be available to recipes as `mycookbook_myresource`. + +You can still get the LWRP class by calling `Chef::ResourceResolver.resolve(:mycookbook_myresource)`. + +The primary aim here is clearing out the `Chef::Resource` namespace. + +References to these classes is deprecated (and will emit a warning) in Chef 12, and will be removed in Chef 13. @@ -3,13 +3,17 @@ gemspec :name => "chef" gem "activesupport", "< 4.0.0", :group => :compat_testing, :platform => "ruby" +gem 'chef-config', path: "chef-config" + group(:docgen) do + gem "tomlrb" gem "yard" end group(:development, :test) do gem "simplecov" gem 'rack', "~> 1.5.1" + gem 'cheffish', "~> 1.2" gem 'ruby-shadow', :platforms => :ruby unless RUBY_PLATFORM.downcase.match(/(aix|cygwin)/) end diff --git a/MAINTAINERS.md b/MAINTAINERS.md new file mode 100644 index 0000000000..e3bf07573a --- /dev/null +++ b/MAINTAINERS.md @@ -0,0 +1,132 @@ +<!-- This is a generated file. Please do not edit directly --> + +# Maintainers + +This file lists how the Chef project is maintained. When making changes to the system, this +file tells you who needs to review your patch - you need a simple majority of maintainers +for the relevant subsystems to provide a :+1: on your pull request. Additionally, you need +to not receive a veto from a Lieutenant or the Project Lead. + +Check out [How Chef is Maintained](https://github.com/opscode/chef-rfc/blob/master/rfc030-maintenance-policy.md#how-the-project-is-maintained) for details on the process, how to become +a maintainer, lieutenant, or the project lead. + +# Project Lead + +* [Adam Jacob](https://github.com/adamhjk) + +## Components + +## Chef Core + +Handles the core parts of the Chef DSL, base resource and provider +infrastructure, and the Chef applications. Includes anything not covered by +another component. + +### Lieutenant + +* [Thom May](https://github.com/thommay) + +### Maintainers + +* [Bryan McLellan](https://github.com/btm) +* [Daniel DeLeo](https://github.com/danielsdeleo) +* [AJ Christensen](https://github.com/fujin) +* [Phil Dibowitz](https://github.com/jaymzh) +* [Jay Mundrawala](https://github.com/jdmundrawala) +* [Jon Cowie](https://github.com/jonlives) +* [Lamont Granquist](https://github.com/lamont-granquist) +* [Steven Murawski](https://github.com/smurawski) +* [Tyler Ball](https://github.com/tyler-ball) +* [Ranjib Dey](https://github.com/ranjib) + +## Dev Tools + +Chef Zero, Knife, Chef Apply and Chef Shell. +### Maintainers + +* [Daniel DeLeo](https://github.com/danielsdeleo) +* [Joshua Timberman](https://github.com/jtimberman) +* [Lamont Granquist](https://github.com/lamont-granquist) +* [Steven Danna](https://github.com/stevendanna) + +## Test Tools + +ChefSpec +### Lieutenant + +* [Seth Vargo](https://github.com/sethvargo) + +### Maintainers + +* [Joshua Timberman](https://github.com/jtimberman) +* [Lamont Granquist](https://github.com/lamont-granquist) +* [Ranjib Dey](https://github.com/ranjib) + +## Platform Specific Components + +The specific components of Chef related to a given platform - including (but not limited to) resources, providers, and the core DSL. + +## Enterprise Linux + +### Lieutenant + +* [Jon Cowie](https://github.com/jonlives) + +### Maintainers + +* [Phil Dibowitz](https://github.com/jaymzh) +* [Lamont Granquist](https://github.com/lamont-granquist) + +## Ubuntu + +### Maintainers + +* [Lamont Granquist](https://github.com/lamont-granquist) +* [Ranjib Dey](https://github.com/ranjib) +* [Thom May](https://github.com/thommay) + +## Windows + +### Lieutenant + +* [Bryan McLellan](https://github.com/btm) + +### Maintainers + +* [Jay Mundrawala](https://github.com/jdmundrawala) +* [Kartik Cating-Subramanian](https://github.com/ksubrama) +* [Steven Murawski](https://github.com/smurawski) + +## Solaris + +### Maintainers + +* [Lamont Granquist](https://github.com/lamont-granquist) + +## AIX + +### Maintainers + +* [Lamont Granquist](https://github.com/lamont-granquist) + +## Mac OS X + +### Lieutenant + +* [Joshua Timberman](https://github.com/jtimberman) + +### Maintainers + +* [Tyler Ball](https://github.com/tyler-ball) + +## FreeBSD + +### Lieutenant + +* [Aaron Kalin](https://github.com/martinisoft) + +### Maintainers + +* [Cory Stephenson](https://github.com/Aevin1387) +* [David Aronsohn](https://github.com/tbunnyman) + diff --git a/MAINTAINERS b/MAINTAINERS.toml index 6ab4a64a80..f949fd4542 100644 --- a/MAINTAINERS +++ b/MAINTAINERS.toml @@ -20,6 +20,8 @@ a maintainer, lieutenant, or the project lead. person = "adamhjk" [Org.Components] + title = "Components" + [Org.Components.Core] title = "Chef Core" text = """ @@ -38,7 +40,7 @@ another component. "jdmundrawala", "jonlives", "lamont-granquist", - "smurawski" + "smurawski", "tyler-ball", "ranjib" ] @@ -105,6 +107,7 @@ The specific components of Chef related to a given platform - including (but not lieutenant = "btm" maintainers = [ "jdmundrawala", + "ksubrama", "smurawski" ] @@ -137,81 +140,92 @@ The specific components of Chef related to a given platform - including (but not lieutenant = "martinisoft" maintainers = [ - "Aevin1387" + "Aevin1387", + "tBunnyMan" ] [people] [people.adamhjk] - Name = Adam Jacob - GitHub = adamhjk + Name = "Adam Jacob" + GitHub = "adamhjk" [people.Aevin1387] - Name = Cory Stephenson - GitHub = Aevin1387 + Name = "Cory Stephenson" + GitHub = "Aevin1387" [people.btm] - Name = Bryan McLellan - GitHub = btm + Name = "Bryan McLellan" + GitHub = "btm" [people.danielsdeleo] - Name = Daniel DeLeo - GitHub = danielsdeleo + Name = "Daniel DeLeo" + GitHub = "danielsdeleo" [people.fujin] - Name = AJ Christensen - GitHub = fujin + Name = "AJ Christensen" + GitHub = "fujin" [people.jaymzh] - Name = Phil Dibowitz - GitHub = jaymzh + Name = "Phil Dibowitz" + GitHub = "jaymzh" [people.jdmundrawala] - Name = Jay Mundrawala - GitHub = jdmundrawala + Name = "Jay Mundrawala" + GitHub = "jdmundrawala" [people.jonlives] - Name = Jon Cowie - GitHub = jonlives + Name = "Jon Cowie" + GitHub = "jonlives" [people.jtimberman] - Name = Joshua Timberman - GitHub = jtimberman + Name = "Joshua Timberman" + GitHub = "jtimberman" [people.lamont-granquist] - Name = Lamont Granquist - GitHub = lamont-granquist + Name = "Lamont Granquist" + GitHub = "lamont-granquist" [people.martinisoft] - Name = Aaron Kalin - GitHub = martinisoft + Name = "Aaron Kalin" + GitHub = "martinisoft" [people.mcquin] - Name = Claire McQuin - GitHub = mcquin + Name = "Claire McQuin" + GitHub = "mcquin" [people.ranjib] - Name = Ranjib Dey - GitHub = ranjib + Name = "Ranjib Dey" + GitHub = "ranjib" [people.sethvargo] - Name = Seth Vargo - GitHub = sethvargo + Name = "Seth Vargo" + GitHub = "sethvargo" [people.smurawski] - Name = Steven Murawski - GitHub = smurawski + Name = "Steven Murawski" + GitHub = "smurawski" [people.stevendanna] - Name = Steven Danna - GitHub = stevendanna + Name = "Steven Danna" + GitHub = "stevendanna" + + [people.tBunnyMan] + Name = "David Aronsohn" + GitHub = "tbunnyman" + IRC = "tBunnyMan" + Twitter = "OnlyHaveCans" [people.thommay] - Name = Thom May - GitHub = thommay - IRC = thom - Twitter = thommay + Name = "Thom May" + GitHub = "thommay" + IRC = "thom" + Twitter = "thommay" [people.tyler-ball] - Name = Tyler Ball - GitHub = tyler-ball + Name = "Tyler Ball" + GitHub = "tyler-ball" + + [people.ksubrama] + Name = "Kartik Cating-Subramanian" + GitHub = "ksubrama" diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 8a7b537670..049640d7ab 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -1,45 +1,87 @@ -# Chef Client Release Notes 12.3.0: +# Chef Client Release Notes 12.4.0: -## Socketless Chef Zero Local Mode -All requests to the Chef Zero server in local mode use Chef Zero's new -socketless request mechanism. By default, Chef Zero will still bind to a -port and accept HTTP requests on localhost; this can be disabled with -the `--no-listen` CLI flag or by adding `listen false` to the relevant -configuration file. +## Knife Key Management Commands for Users and Clients -## Minimal Ohai Flag +`knife user` and `knife client` now have a suite of subcommands that live under +the `key` subcommand. These subcommands allow you to list, show, create, delete +and edit keys for a given user or client. They can be used to implement +key rotation with multiple expiring keys for a single actor or just +for basic key management. See `knife user key` and `knife client key` +for a full list of subcommands and their usage. -Chef Client, Solo, and Apply all now support a `--minimal-ohai` flag. -When set, Chef will only run the bare minimum Ohai plugins necessary to -support node name detection and resource/provider selection. The primary -motivation for this feature is to speed up Chef's integration tests -which run `chef-client` (and solo) many times in various contexts, -however advanced users may find it useful in certain use cases. Any -cookbook that relies on other ohai data will absolutely not work in this -mode unless the user implements workarounds such as running the ohai -resource during the compile phase. +## System Loggers -## Dynamic Resource Resolution and Chef Class Fascade +You can now have all Chef logs sent to a logging system of your choice. -Resolution of Resources is now dynamic and similar to Providers and handles -multiple resources having the same provides line on a given platform. When -the user types a resource like 'package' into the DSL that is resolved via -the provides lines, and if multiple classes provide the same resource (like -Homebrew and MacPorts package resources on Mac) then which one is selected -is governed by the Chef::Platform::ResourcePriorityMap. +### Syslog Logger -In order to change the priorities in both the ResourcePriorityMap and -ProviderPriorityMap a helper API has been constructed off of the Chef class: +Syslog can be used by adding the following line to your chef config +file: -* `Chef.get_provider_priority_array(resource_name)` -* `Chef.get_resource_priority_array(resource_name)` -* `Chef.set_provider_priority_array(resource_name, Array<Class>, *filter)` -* `Chef.set_resoruce_priority_array(resource_name, Array<Class>, *filter)` +```ruby +log_location Chef::Log::Syslog.new("chef-client", ::Syslog::LOG_DAEMON) +``` -In order to change the `package` resource globally on MacOSX to the MacPorts provider: +THis will write to the `daemon` facility with the originator set as +`chef-client`. -`Chef.set_resource_priority_array(:package, [ Chef::Resource::MacportsPackage ], os: 'darwin')` +### Windows Event Logger -That line can be placed into a library file in a cookbook so that it is applied before -any recipes are compiled. +The logger can be used by adding the following line to your chef config file: +```ruby +log_location Chef::Log::WinEvt.new +``` + +This will write to the Application log with the source set as Chef. + +## RemoteFile resource supports UNC paths on Windows + +You can now use UNC paths with `remote_file` on Windows machines. For +example, you can get `Foo.tar.gz` off of `fooshare` on `foohost` using +the following resource: + +```ruby +remote_file 'C:\Foo.tar.gz' do + source "\\\\foohost\\fooshare\\Foo.tar.gz" +end +``` + +## WindowsPackage resource supports URLs + +The `windows_package` resource now allows specifying URLs for the source +attribute. For example, you could install 7zip with the following resource: + +```ruby +windows_package '7zip' do + source "http://www.7-zip.org/a/7z938-x64.msi" +end +``` + +Internally, this is done by using a `remote_file` resource to download the +contents at the specified url. If needed, you can modify the attributes of +the `remote_file` resource using the `remote_file_attributes` attribute. +The `remote_file_attributes` accepts a hash of attributes that will be set +on the underlying remote_file. For example, the checksum of the contents can +be verified using + +```ruby +windows_package '7zip' do + source "http://www.7-zip.org/a/7z938-x64.msi" + remote_file_attributes { + :path => "C:\\7zip.msi", + :checksum => '7c8e873991c82ad9cfcdbdf45254ea6101e9a645e12977dcd518979e50fdedf3' + } +end +``` + +To make the transition easier from the Windows cookbook, `windows_package` also +accepts the `checksum` attribute, and the previous resource could be rewritten +as: + +```ruby +windows_package '7zip' do + source "http://www.7-zip.org/a/7z938-x64.msi" + checksum '7c8e873991c82ad9cfcdbdf45254ea6101e9a645e12977dcd518979e50fdedf3' +end +``` @@ -17,31 +17,120 @@ # limitations under the License. # -require File.dirname(__FILE__) + '/lib/chef/version' +VERSION = IO.read(File.expand_path("../VERSION", __FILE__)).strip require 'rubygems' require 'rubygems/package_task' require 'rdoc/task' -require './tasks/rspec.rb' +require_relative 'tasks/rspec' +require_relative 'tasks/external_tests' +require_relative 'tasks/maintainers' GEM_NAME = "chef" +desc "build Gems of Chef's components" +task :package_components do + Dir.chdir("chef-config") do + sh "rake package" + end +end + +task :package => :package_components + +desc "build and install chef's components" +task :install_components => :package_components do + Dir.chdir("chef-config") do + sh "rake install" + end +end + +task :install => :install_components + +desc "clean up builds of Chef's components" +task :clobber_component_packages do + Dir.chdir("chef-config") do + sh "rake clobber_package" + end +end + +task :clobber_package => :clobber_component_packages + +desc "Update the version number for Chef's components" +task :update_components_versions do + Dir.chdir("chef-config") do + sh "rake version" + end +end + +desc "Regenerate lib/chef/version.rb from VERSION file" +task :version => :update_components_versions do + contents = <<-VERSION_RB +# Copyright:: Copyright (c) 2010-2015 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. + +#!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! +# NOTE: This file is generated by running `rake version` in the top level of +# this repo. Do not edit this manually. Edit the VERSION file and run the rake +# task instead. +#!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +class Chef + CHEF_ROOT = File.dirname(File.expand_path(File.dirname(__FILE__))) + VERSION = '#{VERSION}' +end + +# +# NOTE: the Chef::Version class is defined in version_class.rb +# +# NOTE: DO NOT Use the Chef::Version class on Chef::VERSIONs. The +# Chef::Version class is for _cookbooks_ only, and cannot handle +# pre-release chef-client versions like "10.14.0.rc.2". Please +# use Rubygem's Gem::Version class instead. +# +VERSION_RB + version_rb_path = File.expand_path("../lib/chef/version.rb", __FILE__) + IO.write(version_rb_path, contents) +end + Dir[File.expand_path("../*gemspec", __FILE__)].reverse.each do |gemspec_path| gemspec = eval(IO.read(gemspec_path)) Gem::PackageTask.new(gemspec).define end -task :install => :package do - sh %{gem install pkg/#{GEM_NAME}-#{Chef::VERSION}.gem --no-rdoc --no-ri} +def with_clean_env(&block) + if defined?(Bundler) + Bundler.with_clean_env(&block) + else + block.call + end +end + +desc "Build and install a chef gem" +task :install => [:package] do + with_clean_env do + sh %{gem install pkg/#{GEM_NAME}-#{VERSION}.gem --no-rdoc --no-ri} + end end task :uninstall do - sh %{gem uninstall #{GEM_NAME} -x -v #{Chef::VERSION} } + sh %{gem uninstall #{GEM_NAME} -x -v #{VERSION} } end desc "Build it, tag it and ship it" -task :ship => :gem do - sh("git tag #{Chef::VERSION}") +task :ship => [:clobber_package, :gem] do + sh("git tag #{VERSION}") sh("git push opscode --tags") Dir[File.expand_path("../pkg/*.gem", __FILE__)].reverse.each do |built_gem| sh("gem push #{built_gem}") @@ -79,3 +168,4 @@ begin rescue LoadError puts "yard is not available. (sudo) gem install yard to generate yard documentation." end + diff --git a/VERSION b/VERSION new file mode 100644 index 0000000000..b53f8861dc --- /dev/null +++ b/VERSION @@ -0,0 +1 @@ +12.4.0.rc.2 diff --git a/appveyor.yml b/appveyor.yml index 5ba6d30b81..5609648cb1 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -16,13 +16,19 @@ branches: - master - 12-stable +cache: + - C:\Ruby200\lib\ruby\gems\2.0.0 + - C:\Ruby200\bin + install: - winrm quickconfig -q - SET PATH=C:\Ruby%ruby_version%\bin;%PATH% - echo %PATH% - ruby --version + - gem install bundler --quiet --no-ri --no-rdoc || gem install bundler --quiet --no-ri --no-rdoc || gem install bundler --quiet --no-ri --no-rdoc + - gem install rubygems-pkg/rubygems-update-2.4.6.gem + - update_rubygems - gem --version - - gem install bundler --quiet --no-ri --no-rdoc - bundler --version build_script: diff --git a/chef-config/.gitignore b/chef-config/.gitignore new file mode 100644 index 0000000000..0cb6eeb067 --- /dev/null +++ b/chef-config/.gitignore @@ -0,0 +1,9 @@ +/.bundle/ +/.yardoc +/Gemfile.lock +/_yardoc/ +/coverage/ +/doc/ +/pkg/ +/spec/reports/ +/tmp/ diff --git a/chef-config/.rspec b/chef-config/.rspec new file mode 100644 index 0000000000..eb3ef03653 --- /dev/null +++ b/chef-config/.rspec @@ -0,0 +1,2 @@ +--color +-fd diff --git a/chef-config/.travis.yml b/chef-config/.travis.yml new file mode 100644 index 0000000000..927580f35f --- /dev/null +++ b/chef-config/.travis.yml @@ -0,0 +1,31 @@ +language: ruby + +sudo: false +# Early warning system to catch if Rubygems breaks something +before_install: + gem update --system + +branches: + only: + - master + +matrix: + include: + - rvm: 2.0 + - rvm: 2.1 + +notifications: + on_change: true + on_failure: true + on_success: change + on_pull_requests: false + irc: + channels: + - chat.freenode.net#chef-hacking + hipchat: + rooms: + # Build Statuses + - secure: G8MNo94L8bmWWwkH2/ViB2QaZnZHZscYM/mEjDbOGd15sqrruwckeARyBoUcRI7P1C6AFmS4IKCNVXa6KzX4Pbh51gQWM92zRpRTZpplwtXz53/1l8ajLFLLMLvEMTlBFAANUKEUFAQPY4dMa14V3Qc5oijfIncN61k4nZNTKpY= + - rvm: 2.2 + # Open Source + - secure: hmcex4PpG5dn8WvjndONO4xCUKOC5kPU/bUEGRrfVbe2YKJE7t0XXbNDC96W/xBgzgnJzvf1Er0zJKDrNf4qEDEWFoozdN00WLcqREgaLLS3Seto2FjR/BpBk5q+sCV0rwwEMms2P4Qk+VSnDCnm9EaeM55hOabqNuOrRzoZLBQ= diff --git a/chef-config/Gemfile b/chef-config/Gemfile new file mode 100644 index 0000000000..d39725ff87 --- /dev/null +++ b/chef-config/Gemfile @@ -0,0 +1,4 @@ +source 'https://rubygems.org' + +# Specify your gem's dependencies in chef-config.gemspec +gemspec diff --git a/chef-config/LICENSE b/chef-config/LICENSE new file mode 100644 index 0000000000..11069edd79 --- /dev/null +++ b/chef-config/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + +Copyright [yyyy] [name of copyright owner] + +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. diff --git a/chef-config/README.md b/chef-config/README.md new file mode 100644 index 0000000000..c36527282e --- /dev/null +++ b/chef-config/README.md @@ -0,0 +1,4 @@ +# ChefConfig + +This repo is experimental. Use at your own risk. + diff --git a/chef-config/Rakefile b/chef-config/Rakefile new file mode 100644 index 0000000000..10b6010de3 --- /dev/null +++ b/chef-config/Rakefile @@ -0,0 +1,65 @@ +require 'rspec/core/rake_task' +require 'rubygems/package_task' + +VERSION = IO.read(File.expand_path("../../VERSION", __FILE__)).strip + +Dir[File.expand_path("../*gemspec", __FILE__)].reverse.each do |gemspec_path| + gemspec = eval(IO.read(gemspec_path)) + Gem::PackageTask.new(gemspec).define +end + +def with_clean_env(&block) + if defined?(Bundler) + Bundler.with_clean_env(&block) + else + block.call + end +end + +desc "Build and install a chef-config gem" +task :install => [:package] do + with_clean_env do + sh(%{gem install pkg/chef-config-#{ChefConfig::VERSION}.gem --no-rdoc --no-ri}, verbose: true) + end +end + +task :default => :spec + +desc "Run standard specs" +RSpec::Core::RakeTask.new(:spec) do |t| + t.pattern = FileList['spec/**/*_spec.rb'] +end + +desc "Regenerate lib/chef/version.rb from VERSION file" +task :version do + contents = <<-VERSION_RB +# Copyright:: Copyright (c) 2010-2015 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. + +#!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! +# NOTE: This file is generated by running `rake version` in the top level of +# this repo. Do not edit this manually. Edit the VERSION file and run the rake +# task instead. +#!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +module ChefConfig + VERSION = '#{VERSION}' +end + +VERSION_RB + version_rb_path = File.expand_path("../lib/chef-config/version.rb", __FILE__) + IO.write(version_rb_path, contents) +end + diff --git a/chef-config/chef-config.gemspec b/chef-config/chef-config.gemspec new file mode 100644 index 0000000000..475bd0f2d2 --- /dev/null +++ b/chef-config/chef-config.gemspec @@ -0,0 +1,32 @@ +# coding: utf-8 +lib = File.expand_path('../lib', __FILE__) +$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) +require 'chef-config/version' + +Gem::Specification.new do |spec| + spec.name = "chef-config" + spec.version = ChefConfig::VERSION + spec.authors = ["Adam Jacob"] + spec.email = ["adam@chef.io"] + + spec.summary = %q{Chef's default configuration and config loading} + spec.homepage = "https://github.com/chef/chef" + spec.license = "Apache-2.0" + + spec.require_paths = ["lib"] + + spec.add_dependency "mixlib-shellout", "~> 2.0" + spec.add_dependency "mixlib-config", "~> 2.0" + + spec.add_development_dependency "rake", "~> 10.0" + + %w(rspec-core rspec-expectations rspec-mocks).each do |rspec| + spec.add_development_dependency(rspec, "~> 3.2") + end + + spec.files = %w(Rakefile LICENSE README.md) + + Dir.glob("{lib,spec}/**/*", File::FNM_DOTMATCH).reject {|f| File.directory?(f) } + + spec.bindir = "bin" + spec.executables = [] +end diff --git a/chef-config/lib/chef-config.rb b/chef-config/lib/chef-config.rb new file mode 100644 index 0000000000..1f593c868f --- /dev/null +++ b/chef-config/lib/chef-config.rb @@ -0,0 +1,20 @@ +# +# Copyright:: Copyright (c) 2015 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. +# + +module ChefConfig + +end diff --git a/chef-config/lib/chef-config/config.rb b/chef-config/lib/chef-config/config.rb new file mode 100644 index 0000000000..63de8a451f --- /dev/null +++ b/chef-config/lib/chef-config/config.rb @@ -0,0 +1,744 @@ +# +# Author:: Adam Jacob (<adam@opscode.com>) +# Author:: Christopher Brown (<cb@opscode.com>) +# Author:: AJ Christensen (<aj@opscode.com>) +# Author:: Mark Mzyk (<mmzyk@opscode.com>) +# Author:: Kyle Goodwin (<kgoodwin@primerevenue.com>) +# Copyright:: Copyright (c) 2008 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 'mixlib/config' +require 'pathname' + +require 'chef-config/logger' +require 'chef-config/windows' +require 'chef-config/path_helper' +require 'mixlib/shellout' + +module ChefConfig + + class Config + + extend Mixlib::Config + + # Evaluates the given string as config. + # + # +filename+ is used for context in stacktraces, but doesn't need to be the name of an actual file. + def self.from_string(string, filename) + self.instance_eval(string, filename, 1) + end + + def self.inspect + configuration.inspect + end + + def self.platform_specific_path(path) + path = PathHelper.cleanpath(path) + if ChefConfig.windows? + # turns \etc\chef\client.rb and \var\chef\client.rb into C:/chef/client.rb + if env['SYSTEMDRIVE'] && path[0] == '\\' && path.split('\\')[2] == 'chef' + path = PathHelper.join(env['SYSTEMDRIVE'], path.split('\\', 3)[2]) + end + end + path + end + + def self.add_formatter(name, file_path=nil) + formatters << [name, file_path] + end + + def self.add_event_logger(logger) + event_handlers << logger + end + + # Config file to load (client.rb, knife.rb, etc. defaults set differently in knife, chef-client, etc.) + configurable(:config_file) + + default(:config_dir) do + if config_file + PathHelper.dirname(config_file) + else + PathHelper.join(user_home, ".chef", "") + end + end + + default :formatters, [] + + # Override the config dispatch to set the value of multiple server options simultaneously + # + # === Parameters + # url<String>:: String to be set for all of the chef-server-api URL's + # + configurable(:chef_server_url).writes_value { |url| url.to_s.strip } + + # When you are using ActiveSupport, they monkey-patch 'daemonize' into Kernel. + # So while this is basically identical to what method_missing would do, we pull + # it up here and get a real method written so that things get dispatched + # properly. + configurable(:daemonize).writes_value { |v| v } + + # The root where all local chef object data is stored. cookbooks, data bags, + # environments are all assumed to be in separate directories under this. + # chef-solo uses these directories for input data. knife commands + # that upload or download files (such as knife upload, knife role from file, + # etc.) work. + default :chef_repo_path do + if self.configuration[:cookbook_path] + if self.configuration[:cookbook_path].kind_of?(String) + File.expand_path('..', self.configuration[:cookbook_path]) + else + self.configuration[:cookbook_path].map do |path| + File.expand_path('..', path) + end + end + else + cache_path + end + end + + def self.find_chef_repo_path(cwd) + # In local mode, we auto-discover the repo root by looking for a path with "cookbooks" under it. + # This allows us to run config-free. + path = cwd + until File.directory?(PathHelper.join(path, "cookbooks")) + new_path = File.expand_path('..', path) + if new_path == path + ChefConfig.logger.warn("No cookbooks directory found at or above current directory. Assuming #{Dir.pwd}.") + return Dir.pwd + end + path = new_path + end + ChefConfig.logger.info("Auto-discovered chef repository at #{path}") + path + end + + def self.derive_path_from_chef_repo_path(child_path) + if chef_repo_path.kind_of?(String) + PathHelper.join(chef_repo_path, child_path) + else + chef_repo_path.map { |path| PathHelper.join(path, child_path)} + end + end + + # Location of acls on disk. String or array of strings. + # Defaults to <chef_repo_path>/acls. + # Only applies to Enterprise Chef commands. + default(:acl_path) { derive_path_from_chef_repo_path('acls') } + + # Location of clients on disk. String or array of strings. + # Defaults to <chef_repo_path>/acls. + default(:client_path) { derive_path_from_chef_repo_path('clients') } + + # Location of cookbooks on disk. String or array of strings. + # Defaults to <chef_repo_path>/cookbooks. If chef_repo_path + # is not specified, this is set to [/var/chef/cookbooks, /var/chef/site-cookbooks]). + default(:cookbook_path) do + if self.configuration[:chef_repo_path] + derive_path_from_chef_repo_path('cookbooks') + else + Array(derive_path_from_chef_repo_path('cookbooks')).flatten + + Array(derive_path_from_chef_repo_path('site-cookbooks')).flatten + end + end + + # Location of containers on disk. String or array of strings. + # Defaults to <chef_repo_path>/containers. + # Only applies to Enterprise Chef commands. + default(:container_path) { derive_path_from_chef_repo_path('containers') } + + # Location of data bags on disk. String or array of strings. + # Defaults to <chef_repo_path>/data_bags. + default(:data_bag_path) { derive_path_from_chef_repo_path('data_bags') } + + # Location of environments on disk. String or array of strings. + # Defaults to <chef_repo_path>/environments. + default(:environment_path) { derive_path_from_chef_repo_path('environments') } + + # Location of groups on disk. String or array of strings. + # Defaults to <chef_repo_path>/groups. + # Only applies to Enterprise Chef commands. + default(:group_path) { derive_path_from_chef_repo_path('groups') } + + # Location of nodes on disk. String or array of strings. + # Defaults to <chef_repo_path>/nodes. + default(:node_path) { derive_path_from_chef_repo_path('nodes') } + + # Location of roles on disk. String or array of strings. + # Defaults to <chef_repo_path>/roles. + default(:role_path) { derive_path_from_chef_repo_path('roles') } + + # Location of users on disk. String or array of strings. + # Defaults to <chef_repo_path>/users. + # Does not apply to Enterprise Chef commands. + default(:user_path) { derive_path_from_chef_repo_path('users') } + + # Location of policies on disk. String or array of strings. + # Defaults to <chef_repo_path>/policies. + default(:policy_path) { derive_path_from_chef_repo_path('policies') } + + # Turn on "path sanity" by default. See also: http://wiki.opscode.com/display/chef/User+Environment+PATH+Sanity + default :enforce_path_sanity, true + + # Formatted Chef Client output is a beta feature, disabled by default: + default :formatter, "null" + + # The number of times the client should retry when registering with the server + default :client_registration_retries, 5 + + # An array of paths to search for knife exec scripts if they aren't in the current directory + default :script_path, [] + + # The root of all caches (checksums, cache and backup). If local mode is on, + # this is under the user's home directory. + default(:cache_path) do + if local_mode + PathHelper.join(config_dir, 'local-mode-cache') + else + primary_cache_root = platform_specific_path("/var") + primary_cache_path = platform_specific_path("/var/chef") + # Use /var/chef as the cache path only if that folder exists and we can read and write + # into it, or /var exists and we can read and write into it (we'll create /var/chef later). + # Otherwise, we'll create .chef under the user's home directory and use that as + # the cache path. + unless path_accessible?(primary_cache_path) || path_accessible?(primary_cache_root) + secondary_cache_path = PathHelper.join(user_home, '.chef') + ChefConfig.logger.info("Unable to access cache at #{primary_cache_path}. Switching cache to #{secondary_cache_path}") + secondary_cache_path + else + primary_cache_path + end + end + end + + # Returns true only if the path exists and is readable and writeable for the user. + def self.path_accessible?(path) + File.exists?(path) && File.readable?(path) && File.writable?(path) + end + + # Where cookbook files are stored on the server (by content checksum) + default(:checksum_path) { PathHelper.join(cache_path, "checksums") } + + # Where chef's cache files should be stored + default(:file_cache_path) { PathHelper.join(cache_path, "cache") } + + # Where backups of chef-managed files should go + default(:file_backup_path) { PathHelper.join(cache_path, "backup") } + + # The chef-client (or solo) lockfile. + # + # If your `file_cache_path` resides on a NFS (or non-flock()-supporting + # fs), it's recommended to set this to something like + # '/tmp/chef-client-running.pid' + default(:lockfile) { PathHelper.join(file_cache_path, "chef-client-running.pid") } + + ## Daemonization Settings ## + # What user should Chef run as? + default :user, nil + default :group, nil + default :umask, 0022 + + # Valid log_levels are: + # * :debug + # * :info + # * :warn + # * :fatal + # These work as you'd expect. There is also a special `:auto` setting. + # When set to :auto, Chef will auto adjust the log verbosity based on + # context. When a tty is available (usually because the user is running chef + # in a console), the log level is set to :warn, and output formatters are + # used as the primary mode of output. When a tty is not available, the + # logger is the primary mode of output, and the log level is set to :info + default :log_level, :auto + + # Logging location as either an IO stream or string representing log file path + default :log_location, STDOUT + + # Using `force_formatter` causes chef to default to formatter output when STDOUT is not a tty + default :force_formatter, false + + # Using `force_logger` causes chef to default to logger output when STDOUT is a tty + default :force_logger, false + + default :http_retry_count, 5 + default :http_retry_delay, 5 + default :interval, nil + default :once, nil + default :json_attribs, nil + # toggle info level log items that can create a lot of output + default :verbose_logging, true + default :node_name, nil + default :diff_disabled, false + default :diff_filesize_threshold, 10000000 + default :diff_output_threshold, 1000000 + default :local_mode, false + + default :pid_file, nil + + # Whether Chef Zero local mode should bind to a port. All internal requests + # will go through the socketless code path regardless, so the socket is + # only needed if other processes will connect to the local mode server. + # + # For compatibility this is set to true but it will be changed to false in + # the future. + default :listen, true + + config_context :chef_zero do + config_strict_mode true + default(:enabled) { ChefConfig::Config.local_mode } + default :host, 'localhost' + default :port, 8889.upto(9999) # Will try ports from 8889-9999 until one works + end + default :chef_server_url, "https://localhost:443" + + default(:chef_server_root) do + # if the chef_server_url is a path to an organization, aka + # 'some_url.../organizations/*' then remove the '/organization/*' by default + if self.configuration[:chef_server_url] =~ /\/organizations\/\S*$/ + self.configuration[:chef_server_url].split('/')[0..-3].join('/') + elsif self.configuration[:chef_server_url] # default to whatever chef_server_url is + self.configuration[:chef_server_url] + else + "https://localhost:443" + end + end + + default :rest_timeout, 300 + default :yum_timeout, 900 + default :yum_lock_timeout, 30 + default :solo, false + default :splay, nil + default :why_run, false + default :color, false + default :client_fork, true + default :ez, false + default :enable_reporting, true + default :enable_reporting_url_fatals, false + # Possible values for :audit_mode + # :enabled, :disabled, :audit_only, + # + # TODO: 11 Dec 2014: Currently audit-mode is an experimental feature + # and is disabled by default. When users choose to enable audit-mode, + # a warning is issued in application/client#reconfigure. + # This can be removed when audit-mode is enabled by default. + default :audit_mode, :disabled + + # Chef only needs ohai to run the hostname plugin for the most basic + # functionality. If the rest of the ohai plugins are not needed (like in + # most of our testing scenarios) + default :minimal_ohai, false + + # Policyfile is a feature where a node gets its run list and cookbook + # version set from a single document on the server instead of expanding the + # run list and having the server compute the cookbook version set based on + # environment constraints. + default :use_policyfile, false + + # Policyfiles can be used in a native mode (default) or compatibility mode. + # Native mode requires Chef Server 12.1 (it can be enabled via feature flag + # on some prior versions). In native mode, policies and associated + # cookbooks are accessed via feature-specific APIs. In compat mode, + # policies are stored as data bags and cookbooks are stored at the + # cookbooks/ endpoint. Compatibility mode can be dangerous on existing Chef + # Servers; it's recommended to upgrade your Chef Server rather than use + # compatibility mode. Compatibility mode remains available so you can use + # policyfiles with servers that don't yet support the native endpoints. + default :policy_document_native_api, true + + # Set these to enable SSL authentication / mutual-authentication + # with the server + + # Client side SSL cert/key for mutual auth + default :ssl_client_cert, nil + default :ssl_client_key, nil + + # Whether or not to verify the SSL cert for all HTTPS requests. When set to + # :verify_peer (default), all HTTPS requests will be validated regardless of other + # SSL verification settings. When set to :verify_none no HTTPS requests will + # be validated. + default :ssl_verify_mode, :verify_peer + + # Whether or not to verify the SSL cert for HTTPS requests to the Chef + # server API. If set to `true`, the server's cert will be validated + # regardless of the :ssl_verify_mode setting. This is set to `true` when + # running in local-mode. + # NOTE: This is a workaround until verify_peer is enabled by default. + default(:verify_api_cert) { ChefConfig::Config.local_mode } + + # Path to the default CA bundle files. + default :ssl_ca_path, nil + default(:ssl_ca_file) do + if ChefConfig.windows? and embedded_path = embedded_dir + cacert_path = File.join(embedded_path, "ssl/certs/cacert.pem") + cacert_path if File.exist?(cacert_path) + else + nil + end + end + + # A directory that contains additional SSL certificates to trust. Any + # certificates in this directory will be added to whatever CA bundle ruby + # is using. Use this to add self-signed certs for your Chef Server or local + # HTTP file servers. + default(:trusted_certs_dir) { PathHelper.join(config_dir, "trusted_certs") } + + # Where should chef-solo download recipes from? + default :recipe_url, nil + + # Sets the version of the signed header authentication protocol to use (see + # the 'mixlib-authorization' project for more detail). Currently, versions + # 1.0 and 1.1 are available; however, the chef-server must first be + # upgraded to support version 1.1 before clients can begin using it. + # + # Version 1.1 of the protocol is required when using a `node_name` greater + # than ~90 bytes (~90 ascii characters), so chef-client will automatically + # switch to using version 1.1 when `node_name` is too large for the 1.0 + # protocol. If you intend to use large node names, ensure that your server + # supports version 1.1. Automatic detection of large node names means that + # users will generally not need to manually configure this. + # + # In the future, this configuration option may be replaced with an + # automatic negotiation scheme. + default :authentication_protocol_version, "1.0" + + # This key will be used to sign requests to the Chef server. This location + # must be writable by Chef during initial setup when generating a client + # identity on the server. + # + # The chef-server will look up the public key for the client using the + # `node_name` of the client. + # + # If chef-zero is enabled, this defaults to nil (no authentication). + default(:client_key) { chef_zero.enabled ? nil : platform_specific_path("/etc/chef/client.pem") } + + # When registering the client, should we allow the client key location to + # be a symlink? eg: /etc/chef/client.pem -> /etc/chef/prod-client.pem + # If the path of the key goes through a directory like /tmp this should + # never be set to true or its possibly an easily exploitable security hole. + default :follow_client_key_symlink, false + + # This secret is used to decrypt encrypted data bag items. + default(:encrypted_data_bag_secret) do + if File.exist?(platform_specific_path("/etc/chef/encrypted_data_bag_secret")) + platform_specific_path("/etc/chef/encrypted_data_bag_secret") + else + nil + end + end + + # As of Chef 11.0, version "1" is the default encrypted data bag item + # format. Version "2" is available which adds encrypt-then-mac protection. + # To maintain compatibility, versions other than 1 must be opt-in. + # + # Set this to `2` if you have chef-client 11.6.0+ in your infrastructure. + # Set this to `3` if you have chef-client 11.?.0+, ruby 2 and OpenSSL >= 1.0.1 in your infrastructure. (TODO) + default :data_bag_encrypt_version, 1 + + # When reading data bag items, any supported version is accepted. However, + # if all encrypted data bags have been generated with the version 2 format, + # it is recommended to disable support for earlier formats to improve + # security. For example, the version 2 format is identical to version 1 + # except for the addition of an HMAC, so an attacker with MITM capability + # could downgrade an encrypted data bag to version 1 as part of an attack. + default :data_bag_decrypt_minimum_version, 0 + + # If there is no file in the location given by `client_key`, chef-client + # will temporarily use the "validator" identity to generate one. If the + # `client_key` is not present and the `validation_key` is also not present, + # chef-client will not be able to authenticate to the server. + # + # The `validation_key` is never used if the `client_key` exists. + # + # If chef-zero is enabled, this defaults to nil (no authentication). + 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 send 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 `true`. Set to `false` to disable client-side key + # generation (server generates client keys). + default(:local_key_generation) { true } + + # 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. + # Leaving this set to nil or false is a security hazard! + default :zypper_check_gpg, nil + + # Report Handlers + default :report_handlers, [] + + # Event Handlers + default :event_handlers, [] + + default :disable_event_loggers, false + + # Exception Handlers + default :exception_handlers, [] + + # Start handlers + default :start_handlers, [] + + # Syntax Check Cache. Knife keeps track of files that is has already syntax + # checked by storing files in this directory. `syntax_check_cache_path` is + # the new (and preferred) configuration setting. If not set, knife will + # fall back to using cache_options[:path], which is deprecated but exists in + # many client configs generated by pre-Chef-11 bootstrappers. + default(:syntax_check_cache_path) { cache_options[:path] } + + # Deprecated: + # Move this to the default value of syntax_cache_path when this is removed. + default(:cache_options) { { :path => PathHelper.join(config_dir, "syntaxcache") } } + + # Whether errors should be raised for deprecation warnings. When set to + # `false` (the default setting), a warning is emitted but code using + # deprecated methods/features/etc. should work normally otherwise. When set + # to `true`, usage of deprecated methods/features will raise a + # `DeprecatedFeatureError`. This is used by Chef's tests to ensure that + # deprecated functionality is not used internally by Chef. End users + # should generally leave this at the default setting (especially in + # production), but it may be useful when testing cookbooks or other code if + # the user wishes to aggressively address deprecations. + default(:treat_deprecation_warnings_as_errors) do + # Using an environment variable allows this setting to be inherited in + # tests that spawn new processes. + ENV.key?("CHEF_TREAT_DEPRECATION_WARNINGS_AS_ERRORS") + end + + # knife configuration data + config_context :knife do + default :ssh_port, nil + default :ssh_user, nil + default :ssh_attribute, nil + default :ssh_gateway, nil + default :bootstrap_version, nil + default :bootstrap_proxy, nil + default :bootstrap_template, nil + default :secret, nil + default :secret_file, nil + default :identity_file, nil + default :host_key_verify, nil + default :forward_agent, nil + default :sort_status_reverse, nil + default :hints, {} + end + + def self.set_defaults_for_windows + # Those lists of regular expressions define what chef considers a + # valid user and group name + # From http://technet.microsoft.com/en-us/library/cc776019(WS.10).aspx + principal_valid_regex_part = '[^"\/\\\\\[\]\:;|=,+*?<>]+' + default :user_valid_regex, [ /^(#{principal_valid_regex_part}\\)?#{principal_valid_regex_part}$/ ] + default :group_valid_regex, [ /^(#{principal_valid_regex_part}\\)?#{principal_valid_regex_part}$/ ] + + default :fatal_windows_admin_check, false + end + + def self.set_defaults_for_nix + # Those lists of regular expressions define what chef considers a + # valid user and group name + # + # user/group cannot start with '-', '+' or '~' + # user/group cannot contain ':', ',' or non-space-whitespace or null byte + # everything else is allowed (UTF-8, spaces, etc) and we delegate to your O/S useradd program to barf or not + # copies: http://anonscm.debian.org/viewvc/pkg-shadow/debian/trunk/debian/patches/506_relaxed_usernames?view=markup + default :user_valid_regex, [ /^[^-+~:,\t\r\n\f\0]+[^:,\t\r\n\f\0]*$/ ] + default :group_valid_regex, [ /^[^-+~:,\t\r\n\f\0]+[^:,\t\r\n\f\0]*$/ ] + end + + # Those lists of regular expressions define what chef considers a + # valid user and group name + if ChefConfig.windows? + set_defaults_for_windows + else + set_defaults_for_nix + end + + # This provides a hook which rspec can stub so that we can avoid twiddling + # global state in tests. + def self.env + ENV + end + + def self.windows_home_path + ChefConfig.logger.deprecation("Chef::Config.windows_home_path is now deprecated. Consider using Chef::Util::PathHelper.home instead.") + PathHelper.home + end + + # returns a platform specific path to the user home dir if set, otherwise default to current directory. + default( :user_home ) { PathHelper.home || Dir.pwd } + + # Enable file permission fixup for selinux. Fixup will be done + # only if selinux is enabled in the system. + default :enable_selinux_file_permission_fixup, true + + # Use atomic updates (i.e. move operation) while updating contents + # of the files resources. When set to false copy operation is + # used to update files. + default :file_atomic_update, true + + # There are 3 possible values for this configuration setting. + # true => file staging is done in the destination directory + # false => file staging is done via tempfiles under ENV['TMP'] + # :auto => file staging will try using destination directory if possible and + # will fall back to ENV['TMP'] if destination directory is not usable. + default :file_staging_uses_destdir, :auto + + # Exit if another run is in progress and the chef-client is unable to + # get the lock before time expires. If nil, no timeout is enforced. (Exits + # immediately if 0.) + default :run_lock_timeout, nil + + # Number of worker threads for syncing cookbooks in parallel. Increasing + # this number can result in gateway errors from the server (namely 503 and 504). + # If you are seeing this behavior while using the default setting, reducing + # the number of threads will help. + default :cookbook_sync_threads, 10 + + # At the beginning of the Chef Client run, the cookbook manifests are downloaded which + # contain URLs for every file in every relevant cookbook. Most of the files + # (recipes, resources, providers, libraries, etc) are immediately synchronized + # at the start of the run. The handling of "files" and "templates" directories, + # however, have two modes of operation. They can either all be downloaded immediately + # at the start of the run (no_lazy_load==true) or else they can be lazily loaded as + # cookbook_file or template resources are converged which require them (no_lazy_load==false). + # + # The advantage of lazily loading these files is that unnecessary files are not + # synchronized. This may be useful to users with large files checked into cookbooks which + # are only selectively downloaded to a subset of clients which use the cookbook. However, + # better solutions are to either isolate large files into individual cookbooks and only + # include those cookbooks in the run lists of the servers that need them -- or move to + # using remote_file and a more appropriate backing store like S3 for large file + # distribution. + # + # The disadvantages of lazily loading files are that users some time find it + # confusing that their cookbooks are not fully synchronzied to the cache initially, + # and more importantly the time-sensitive URLs which are in the manifest may time + # out on long Chef runs before the resource that uses the file is converged + # (leading to many confusing 403 errors on template/cookbook_file resources). + # + default :no_lazy_load, true + + # Default for the chef_gem compile_time attribute. Nil is the same as true but will emit + # warnings on every use of chef_gem prompting the user to be explicit. If the user sets this to + # true then the user will get backcompat behavior but with a single nag warning that cookbooks + # may break with this setting in the future. The false setting is the recommended setting and + # will become the default. + default :chef_gem_compile_time, nil + + # A whitelisted array of attributes you want sent over the wire when node + # data is saved. + # The default setting is nil, which collects all data. Setting to [] will not + # collect any data for save. + default :automatic_attribute_whitelist, nil + default :default_attribute_whitelist, nil + default :normal_attribute_whitelist, nil + default :override_attribute_whitelist, nil + + config_context :windows_service do + # Set `watchdog_timeout` to the number of seconds to wait for a chef-client run + # to finish + default :watchdog_timeout, 2 * (60 * 60) # 2 hours + end + + # Chef requires an English-language UTF-8 locale to function properly. We attempt + # to use the 'locale -a' command and search through a list of preferences until we + # find one that we can use. On Ubuntu systems we should find 'C.UTF-8' and be + # able to use that even if there is no English locale on the server, but Mac, Solaris, + # AIX, etc do not have that locale. We then try to find an English locale and fall + # back to 'C' if we do not. The choice of fallback is pick-your-poison. If we try + # to do the work to return a non-US UTF-8 locale then we fail inside of providers when + # things like 'svn info' return Japanese and we can't parse them. OTOH, if we pick 'C' then + # we will blow up on UTF-8 characters. Between the warn we throw and the Encoding + # exception that ruby will throw it is more obvious what is broken if we drop UTF-8 by + # default rather than drop English. + # + # If there is no 'locale -a' then we return 'en_US.UTF-8' since that is the most commonly + # available English UTF-8 locale. However, all modern POSIXen should support 'locale -a'. + def self.guess_internal_locale + # https://github.com/opscode/chef/issues/2181 + # Some systems have the `locale -a` command, but the result has + # invalid characters for the default encoding. + # + # For example, on CentOS 6 with ENV['LANG'] = "en_US.UTF-8", + # `locale -a`.split fails with ArgumentError invalid UTF-8 encoding. + cmd = Mixlib::ShellOut.new("locale -a").run_command + cmd.error! + locales = cmd.stdout.split + case + when locales.include?('C.UTF-8') + 'C.UTF-8' + when locales.include?('en_US.UTF-8'), locales.include?('en_US.utf8') + 'en_US.UTF-8' + when locales.include?('en.UTF-8') + 'en.UTF-8' + else + # Will match en_ZZ.UTF-8, en_ZZ.utf-8, en_ZZ.UTF8, en_ZZ.utf8 + guesses = locales.select { |l| l =~ /^en_.*UTF-?8$/i } + unless guesses.empty? + guessed_locale = guesses.first + # Transform into the form en_ZZ.UTF-8 + guessed_locale.gsub(/UTF-?8$/i, "UTF-8") + else + ChefConfig.logger.warn "Please install an English UTF-8 locale for Chef to use, falling back to C locale and disabling UTF-8 support." + 'C' + end + end + rescue + if ChefConfig.windows? + ChefConfig.logger.debug "Defaulting to locale en_US.UTF-8 on Windows, until it matters that we do something else." + else + ChefConfig.logger.debug "No usable locale -a command found, assuming you have en_US.UTF-8 installed." + end + 'en_US.UTF-8' + end + + default :internal_locale, guess_internal_locale + + # Force UTF-8 Encoding, for when we fire up in the 'C' locale or other strange locales (e.g. + # japanese windows encodings). If we do not do this, then knife upload will fail when a cookbook's + # README.md has UTF-8 characters that do not encode in whatever surrounding encoding we have been + # passed. Effectively, the Chef Ecosystem is globally UTF-8 by default. Anyone who wants to be + # able to upload Shift_JIS or ISO-8859-1 files needs to mark *those* files explicitly with + # magic tags to make ruby correctly identify the encoding being used. Changing this default will + # break Chef community cookbooks and is very highly discouraged. + default :ruby_encoding, Encoding::UTF_8 + + # If installed via an omnibus installer, this gives the path to the + # "embedded" directory which contains all of the software packaged with + # omnibus. This is used to locate the cacert.pem file on windows. + def self.embedded_dir + Pathname.new(_this_file).ascend do |path| + if path.basename.to_s == "embedded" + return path.to_s + end + end + + nil + end + + # Path to this file in the current install. + def self._this_file + File.expand_path(__FILE__) + end + end +end + + + diff --git a/chef-config/lib/chef-config/exceptions.rb b/chef-config/lib/chef-config/exceptions.rb new file mode 100644 index 0000000000..f5d76d856b --- /dev/null +++ b/chef-config/lib/chef-config/exceptions.rb @@ -0,0 +1,26 @@ +# +# Copyright:: Copyright (c) 2015 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-config/windows' +require 'chef-config/logger' + +module ChefConfig + + class InvalidPath < StandardError + end + +end diff --git a/chef-config/lib/chef-config/logger.rb b/chef-config/lib/chef-config/logger.rb new file mode 100644 index 0000000000..57f18809ee --- /dev/null +++ b/chef-config/lib/chef-config/logger.rb @@ -0,0 +1,62 @@ +# +# Copyright:: Copyright (c) 2015 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. +# + + +module ChefConfig + + # Implements enough of Logger's API that we can use it in place of a real + # logger for `ChefConfig.logger` + class NullLogger + + def <<(_msg) + end + + def add(_severity, _message = nil, _progname = nil) + end + + def debug(_progname = nil, &block) + end + + def info(_progname = nil, &block) + end + + def warn(_progname = nil, &block) + end + + def deprecation(_progname = nil, &block) + end + + def error(_progname = nil, &block) + end + + def fatal(_progname = nil, &block) + end + + end + + @logger = NullLogger.new + + def self.logger=(new_logger) + @logger = new_logger + end + + def self.logger + @logger + end +end + + diff --git a/chef-config/lib/chef-config/path_helper.rb b/chef-config/lib/chef-config/path_helper.rb new file mode 100644 index 0000000000..acc6b76377 --- /dev/null +++ b/chef-config/lib/chef-config/path_helper.rb @@ -0,0 +1,233 @@ +# +# 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-config/windows' +require 'chef-config/logger' +require 'chef-config/exceptions' + +module ChefConfig + class PathHelper + # Maximum characters in a standard Windows path (260 including drive letter and NUL) + WIN_MAX_PATH = 259 + + def self.dirname(path) + if ChefConfig.windows? + # Find the first slash, not counting trailing slashes + end_slash = path.size + loop do + slash = path.rindex(/[#{Regexp.escape(File::SEPARATOR)}#{Regexp.escape(path_separator)}]/, end_slash - 1) + if !slash + return end_slash == path.size ? '.' : path_separator + elsif slash == end_slash - 1 + end_slash = slash + else + return path[0..slash-1] + end + end + else + ::File.dirname(path) + end + end + + BACKSLASH = '\\'.freeze + + def self.path_separator + if ChefConfig.windows? + File::ALT_SEPARATOR || BACKSLASH + else + File::SEPARATOR + end + end + + def self.join(*args) + path_separator_regex = Regexp.escape(File::SEPARATOR) + unless path_separator == File::SEPARATOR + path_separator_regex << Regexp.escape(path_separator) + end + + trailing_slashes = /[#{path_separator_regex}]+$/ + leading_slashes = /^[#{path_separator_regex}]+/ + + args.flatten.inject() do |joined_path, component| + joined_path = joined_path.sub(trailing_slashes, '') + component = component.sub(leading_slashes, '') + joined_path += "#{path_separator}#{component}" + end + end + + def self.validate_path(path) + if ChefConfig.windows? + unless printable?(path) + msg = "Path '#{path}' contains non-printable characters. Check that backslashes are escaped with another backslash (e.g. C:\\\\Windows) in double-quoted strings." + ChefConfig.logger.error(msg) + raise ChefConfig::InvalidPath, msg + end + + if windows_max_length_exceeded?(path) + ChefConfig.logger.debug("Path '#{path}' is longer than #{WIN_MAX_PATH}, prefixing with'\\\\?\\'") + path.insert(0, "\\\\?\\") + end + end + + path + end + + def self.windows_max_length_exceeded?(path) + # Check to see if paths without the \\?\ prefix are over the maximum allowed length for the Windows API + # http://msdn.microsoft.com/en-us/library/windows/desktop/aa365247%28v=vs.85%29.aspx + unless path =~ /^\\\\?\\/ + if path.length > WIN_MAX_PATH + return true + end + end + + false + end + + def self.printable?(string) + # returns true if string is free of non-printable characters (escape sequences) + # this returns false for whitespace escape sequences as well, e.g. \n\t + if string =~ /[^[:print:]]/ + false + else + true + end + end + + # Produces a comparable path. + def self.canonical_path(path, add_prefix=true) + # First remove extra separators and resolve any relative paths + abs_path = File.absolute_path(path) + + if ChefConfig.windows? + # Add the \\?\ API prefix on Windows unless add_prefix is false + # Downcase on Windows where paths are still case-insensitive + abs_path.gsub!(::File::SEPARATOR, path_separator) + if add_prefix && abs_path !~ /^\\\\?\\/ + abs_path.insert(0, "\\\\?\\") + end + + abs_path.downcase! + end + + abs_path + end + + def self.cleanpath(path) + path = Pathname.new(path).cleanpath.to_s + # ensure all forward slashes are backslashes + if ChefConfig.windows? + path = path.gsub(File::SEPARATOR, path_separator) + end + path + end + + def self.paths_eql?(path1, path2) + canonical_path(path1) == canonical_path(path2) + end + + # Paths which may contain glob-reserved characters need + # to be escaped before globbing can be done. + # http://stackoverflow.com/questions/14127343 + def self.escape_glob(*parts) + path = cleanpath(join(*parts)) + path.gsub(/[\\\{\}\[\]\*\?]/) { |x| "\\"+x } + end + + def self.relative_path_from(from, to) + Pathname.new(cleanpath(to)).relative_path_from(Pathname.new(cleanpath(from))) + end + + # Retrieves the "home directory" of the current user while trying to ascertain the existence + # of said directory. The path returned uses / for all separators (the ruby standard format). + # If the home directory doesn't exist or an error is otherwise encountered, nil is returned. + # + # If a set of path elements is provided, they are appended as-is to the home path if the + # homepath exists. + # + # If an optional block is provided, the joined path is passed to that block if the home path is + # valid and the result of the block is returned instead. + # + # Home-path discovery is performed once. If a path is discovered, that value is memoized so + # that subsequent calls to home_dir don't bounce around. + # + # See self.all_homes. + def self.home(*args) + @@home_dir ||= self.all_homes { |p| break p } + if @@home_dir + path = File.join(@@home_dir, *args) + block_given? ? (yield path) : path + end + end + + # See self.home. This method performs a similar operation except that it yields all the different + # possible values of 'HOME' that one could have on this platform. Hence, on windows, if + # HOMEDRIVE\HOMEPATH and USERPROFILE are different, the provided block will be called twice. + # This method goes out and checks the existence of each location at the time of the call. + # + # The return is a list of all the returned values from each block invocation or a list of paths + # if no block is provided. + def self.all_homes(*args) + paths = [] + if ChefConfig.windows? + # By default, Ruby uses the the following environment variables to determine Dir.home: + # HOME + # HOMEDRIVE HOMEPATH + # USERPROFILE + # Ruby only checks to see if the variable is specified - not if the directory actually exists. + # On Windows, HOMEDRIVE HOMEPATH can point to a different location (such as an unavailable network mounted drive) + # while USERPROFILE points to the location where the user application settings and profile are stored. HOME + # is not defined as an environment variable (usually). If the home path actually uses UNC, then the prefix is + # HOMESHARE instead of HOMEDRIVE. + # + # We instead walk down the following and only include paths that actually exist. + # HOME + # HOMEDRIVE HOMEPATH + # HOMESHARE HOMEPATH + # USERPROFILE + + paths << ENV['HOME'] + paths << ENV['HOMEDRIVE'] + ENV['HOMEPATH'] if ENV['HOMEDRIVE'] && ENV['HOMEPATH'] + paths << ENV['HOMESHARE'] + ENV['HOMEPATH'] if ENV['HOMESHARE'] && ENV['HOMEPATH'] + paths << ENV['USERPROFILE'] + end + paths << Dir.home if ENV['HOME'] + + # Depending on what environment variables we're using, the slashes can go in any which way. + # Just change them all to / to keep things consistent. + # Note: Maybe this is a bad idea on some unixy systems where \ might be a valid character depending on + # the particular brand of kool-aid you consume. This code assumes that \ and / are both + # path separators on any system being used. + paths = paths.map { |home_path| home_path.gsub(path_separator, ::File::SEPARATOR) if home_path } + + # Filter out duplicate paths and paths that don't exist. + valid_paths = paths.select { |home_path| home_path && Dir.exists?(home_path) } + valid_paths = valid_paths.uniq + + # Join all optional path elements at the end. + # If a block is provided, invoke it - otherwise just return what we've got. + joined_paths = valid_paths.map { |home_path| File.join(home_path, *args) } + if block_given? + joined_paths.each { |p| yield p } + else + joined_paths + end + end + end +end + diff --git a/chef-config/lib/chef-config/version.rb b/chef-config/lib/chef-config/version.rb new file mode 100644 index 0000000000..a6bf636540 --- /dev/null +++ b/chef-config/lib/chef-config/version.rb @@ -0,0 +1,25 @@ +# Copyright:: Copyright (c) 2010-2015 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. + +#!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! +# NOTE: This file is generated by running `rake version` in the top level of +# this repo. Do not edit this manually. Edit the VERSION file and run the rake +# task instead. +#!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +module ChefConfig + VERSION = '12.4.0.rc.2' +end + diff --git a/chef-config/lib/chef-config/windows.rb b/chef-config/lib/chef-config/windows.rb new file mode 100644 index 0000000000..a2e90067df --- /dev/null +++ b/chef-config/lib/chef-config/windows.rb @@ -0,0 +1,29 @@ +# +# Copyright:: Copyright (c) 2015 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. +# + +module ChefConfig + + def self.windows? + if RUBY_PLATFORM =~ /mswin|mingw|windows/ + true + else + false + end + end + +end + diff --git a/chef-config/spec/spec_helper.rb b/chef-config/spec/spec_helper.rb new file mode 100644 index 0000000000..df9461cde9 --- /dev/null +++ b/chef-config/spec/spec_helper.rb @@ -0,0 +1,75 @@ +require 'chef-config/windows' + +# See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration +RSpec.configure do |config| + # rspec-expectations config goes here. You can use an alternate + # assertion/expectation library such as wrong or the stdlib/minitest + # assertions if you prefer. + config.expect_with :rspec do |expectations| + # This option will default to `true` in RSpec 4. It makes the `description` + # and `failure_message` of custom matchers include text for helper methods + # defined using `chain`, e.g.: + # be_bigger_than(2).and_smaller_than(4).description + # # => "be bigger than 2 and smaller than 4" + # ...rather than: + # # => "be bigger than 2" + expectations.include_chain_clauses_in_custom_matcher_descriptions = true + end + + # rspec-mocks config goes here. You can use an alternate test double + # library (such as bogus or mocha) by changing the `mock_with` option here. + config.mock_with :rspec do |mocks| + # Prevents you from mocking or stubbing a method that does not exist on + # a real object. This is generally recommended, and will default to + # `true` in RSpec 4. + mocks.verify_partial_doubles = true + end + + # These two settings work together to allow you to limit a spec run + # to individual examples or groups you care about by tagging them with + # `:focus` metadata. When nothing is tagged with `:focus`, all examples + # get run. + config.filter_run :focus + config.run_all_when_everything_filtered = true + + config.filter_run_excluding :windows_only => true unless ChefConfig.windows? + config.filter_run_excluding :unix_only => true if ChefConfig.windows? + + # Limits the available syntax to the non-monkey patched syntax that is + # recommended. For more details, see: + # - http://myronmars.to/n/dev-blog/2012/06/rspecs-new-expectation-syntax + # - http://teaisaweso.me/blog/2013/05/27/rspecs-new-message-expectation-syntax/ + # - http://myronmars.to/n/dev-blog/2014/05/notable-changes-in-rspec-3#new__config_option_to_disable_rspeccore_monkey_patching + config.disable_monkey_patching! + + # This setting enables warnings. It's recommended, but in some cases may + # be too noisy due to issues in dependencies. + config.warnings = true + + # Many RSpec users commonly either run the entire suite or an individual + # file, and it's useful to allow more verbose output when running an + # individual spec file. + if config.files_to_run.one? + # Use the documentation formatter for detailed output, + # unless a formatter has already been configured + # (e.g. via a command-line flag). + config.default_formatter = 'doc' + end + + # Print the 10 slowest examples and example groups at the + # end of the spec run, to help surface which specs are running + # particularly slow. + # config.profile_examples = 10 + + # Run specs in random order to surface order dependencies. If you find an + # order dependency and want to debug it, you can fix the order by providing + # the seed, which is printed after each run. + # --seed 1234 + config.order = :random + + # Seed global randomization in this process using the `--seed` CLI option. + # Setting this allows you to use `--seed` to deterministically reproduce + # test failures related to randomization by passing the same `--seed` value + # as the one that triggered the failure. + Kernel.srand config.seed +end diff --git a/spec/unit/config_spec.rb b/chef-config/spec/unit/config_spec.rb index 6ea67246b5..395fa2618e 100644 --- a/spec/unit/config_spec.rb +++ b/chef-config/spec/unit/config_spec.rb @@ -18,40 +18,48 @@ # require 'spec_helper' -require 'chef/exceptions' -require 'chef/util/path_helper' +require 'chef-config/config' + +RSpec.describe ChefConfig::Config do + before(:each) do + ChefConfig::Config.reset + + # By default, treat deprecation warnings as errors in tests. + ChefConfig::Config.treat_deprecation_warnings_as_errors(true) + + # Set environment variable so the setting persists in child processes + ENV['CHEF_TREAT_DEPRECATION_WARNINGS_AS_ERRORS'] = "1" + end -describe Chef::Config do describe "config attribute writer: chef_server_url" do before do - Chef::Config.chef_server_url = "https://junglist.gen.nz" + ChefConfig::Config.chef_server_url = "https://junglist.gen.nz" end it "sets the server url" do - expect(Chef::Config.chef_server_url).to eq("https://junglist.gen.nz") + expect(ChefConfig::Config.chef_server_url).to eq("https://junglist.gen.nz") end context "when the url has a leading space" do before do - Chef::Config.chef_server_url = " https://junglist.gen.nz" + ChefConfig::Config.chef_server_url = " https://junglist.gen.nz" end it "strips the space from the url when setting" do - expect(Chef::Config.chef_server_url).to eq("https://junglist.gen.nz") + expect(ChefConfig::Config.chef_server_url).to eq("https://junglist.gen.nz") end end context "when the url is a frozen string" do before do - Chef::Config.chef_server_url = " https://junglist.gen.nz".freeze + ChefConfig::Config.chef_server_url = " https://junglist.gen.nz".freeze end it "strips the space from the url when setting without raising an error" do - expect(Chef::Config.chef_server_url).to eq("https://junglist.gen.nz") + expect(ChefConfig::Config.chef_server_url).to eq("https://junglist.gen.nz") end end - end describe "when configuring formatters" do @@ -79,41 +87,17 @@ describe Chef::Config do # end # it "has an empty list of formatters by default" do - expect(Chef::Config.formatters).to eq([]) + expect(ChefConfig::Config.formatters).to eq([]) end it "configures a formatter with a short name" do - Chef::Config.add_formatter(:doc) - expect(Chef::Config.formatters).to eq([[:doc, nil]]) + ChefConfig::Config.add_formatter(:doc) + expect(ChefConfig::Config.formatters).to eq([[:doc, nil]]) end it "configures a formatter with a file output" do - Chef::Config.add_formatter(:doc, "/var/log/formatter.log") - expect(Chef::Config.formatters).to eq([[:doc, "/var/log/formatter.log"]]) - end - - end - - describe "class method: manage_secret_key" do - before do - allow(Chef::FileCache).to receive(:load).and_return(true) - allow(Chef::FileCache).to receive(:has_key?).with("chef_server_cookie_id").and_return(false) - end - - it "should generate and store a chef server cookie id" do - expect(Chef::FileCache).to receive(:store).with("chef_server_cookie_id", /\w{40}/).and_return(true) - Chef::Config.manage_secret_key - end - - describe "when the filecache has a chef server cookie id key" do - before do - allow(Chef::FileCache).to receive(:has_key?).with("chef_server_cookie_id").and_return(true) - end - - it "should not generate and store a chef server cookie id" do - expect(Chef::FileCache).not_to receive(:store).with("chef_server_cookie_id", /\w{40}/) - Chef::Config.manage_secret_key - end + ChefConfig::Config.add_formatter(:doc, "/var/log/formatter.log") + expect(ChefConfig::Config.formatters).to eq([[:doc, "/var/log/formatter.log"]]) end end @@ -122,27 +106,27 @@ describe Chef::Config do context "On #{is_windows ? 'Windows' : 'Unix'}" do def to_platform(*args) - Chef::Config.platform_specific_path(*args) + ChefConfig::Config.platform_specific_path(*args) end before :each do - allow(Chef::Platform).to receive(:windows?).and_return(is_windows) + allow(ChefConfig).to receive(:windows?).and_return(is_windows) end describe "class method: platform_specific_path" do if is_windows it "should return a windows path on windows systems" do path = "/etc/chef/cookbooks" - allow(Chef::Config).to receive(:env).and_return({ 'SYSTEMDRIVE' => 'C:' }) + allow(ChefConfig::Config).to receive(:env).and_return({ 'SYSTEMDRIVE' => 'C:' }) # match on a regex that looks for the base path with an optional # system drive at the beginning (c:) # system drive is not hardcoded b/c it can change and b/c it is not present on linux systems - expect(Chef::Config.platform_specific_path(path)).to eq("C:\\chef\\cookbooks") + expect(ChefConfig::Config.platform_specific_path(path)).to eq("C:\\chef\\cookbooks") end else it "should return given path on non-windows systems" do path = "/etc/chef/cookbooks" - expect(Chef::Config.platform_specific_path(path)).to eq("/etc/chef/cookbooks") + expect(ChefConfig::Config.platform_specific_path(path)).to eq("/etc/chef/cookbooks") end end end @@ -150,7 +134,7 @@ describe Chef::Config do describe "default values" do let :primary_cache_path do if is_windows - "#{Chef::Config.env['SYSTEMDRIVE']}\\chef" + "#{ChefConfig::Config.env['SYSTEMDRIVE']}\\chef" else "/var/chef" end @@ -158,44 +142,88 @@ describe Chef::Config do let :secondary_cache_path do if is_windows - "#{Chef::Config[:user_home]}\\.chef" + "#{ChefConfig::Config[:user_home]}\\.chef" else - "#{Chef::Config[:user_home]}/.chef" + "#{ChefConfig::Config[:user_home]}/.chef" end end before do if is_windows - allow(Chef::Config).to receive(:env).and_return({ 'SYSTEMDRIVE' => 'C:' }) - Chef::Config[:user_home] = 'C:\Users\charlie' + allow(ChefConfig::Config).to receive(:env).and_return({ 'SYSTEMDRIVE' => 'C:' }) + ChefConfig::Config[:user_home] = 'C:\Users\charlie' else - Chef::Config[:user_home] = '/Users/charlie' + ChefConfig::Config[:user_home] = '/Users/charlie' + end + + allow(ChefConfig::Config).to receive(:path_accessible?).and_return(false) + end + + describe "ChefConfig::Config[:chef_server_root]" do + context "when chef_server_url isn't set manually" do + it "returns the default of 'https://localhost:443'" do + expect(ChefConfig::Config[:chef_server_root]).to eq("https://localhost:443") + end + end + + context "when chef_server_url matches '../organizations/*' without a trailing slash" do + before do + ChefConfig::Config[:chef_server_url] = "https://example.com/organizations/myorg" + end + it "returns the full URL without /organizations/*" do + expect(ChefConfig::Config[:chef_server_root]).to eq("https://example.com") + end + end + + context "when chef_server_url matches '../organizations/*' with a trailing slash" do + before do + ChefConfig::Config[:chef_server_url] = "https://example.com/organizations/myorg/" + end + it "returns the full URL without /organizations/*" do + expect(ChefConfig::Config[:chef_server_root]).to eq("https://example.com") + end end - allow(Chef::Config).to receive(:path_accessible?).and_return(false) + context "when chef_server_url matches '..organizations..' but not '../organizations/*'" do + before do + ChefConfig::Config[:chef_server_url] = "https://organizations.com/organizations" + end + it "returns the full URL without any modifications" do + expect(ChefConfig::Config[:chef_server_root]).to eq(ChefConfig::Config[:chef_server_url]) + end + end + + context "when chef_server_url is a standard URL without the string organization(s)" do + before do + ChefConfig::Config[:chef_server_url] = "https://example.com/some_other_string" + end + it "returns the full URL without any modifications" do + expect(ChefConfig::Config[:chef_server_root]).to eq(ChefConfig::Config[:chef_server_url]) + end + end end - describe "Chef::Config[:cache_path]" do + describe "ChefConfig::Config[:cache_path]" do context "when /var/chef exists and is accessible" do it "defaults to /var/chef" do - allow(Chef::Config).to receive(:path_accessible?).with(to_platform("/var/chef")).and_return(true) - expect(Chef::Config[:cache_path]).to eq(primary_cache_path) + allow(ChefConfig::Config).to receive(:path_accessible?).with(to_platform("/var/chef")).and_return(true) + expect(ChefConfig::Config[:cache_path]).to eq(primary_cache_path) end end context "when /var/chef does not exist and /var is accessible" do it "defaults to /var/chef" do allow(File).to receive(:exists?).with(to_platform("/var/chef")).and_return(false) - allow(Chef::Config).to receive(:path_accessible?).with(to_platform("/var")).and_return(true) - expect(Chef::Config[:cache_path]).to eq(primary_cache_path) + allow(ChefConfig::Config).to receive(:path_accessible?).with(to_platform("/var")).and_return(true) + expect(ChefConfig::Config[:cache_path]).to eq(primary_cache_path) end end context "when /var/chef does not exist and /var is not accessible" do it "defaults to $HOME/.chef" do allow(File).to receive(:exists?).with(to_platform("/var/chef")).and_return(false) - allow(Chef::Config).to receive(:path_accessible?).with(to_platform("/var")).and_return(false) - expect(Chef::Config[:cache_path]).to eq(secondary_cache_path) + allow(ChefConfig::Config).to receive(:path_accessible?).with(to_platform("/var")).and_return(false) + expect(ChefConfig::Config[:cache_path]).to eq(secondary_cache_path) end end @@ -205,68 +233,70 @@ describe Chef::Config do allow(File).to receive(:readable?).with(to_platform("/var/chef")).and_return(true) allow(File).to receive(:writable?).with(to_platform("/var/chef")).and_return(false) - expect(Chef::Config[:cache_path]).to eq(secondary_cache_path) + expect(ChefConfig::Config[:cache_path]).to eq(secondary_cache_path) end end context "when chef is running in local mode" do before do - Chef::Config.local_mode = true + ChefConfig::Config.local_mode = true end context "and config_dir is /a/b/c" do before do - Chef::Config.config_dir to_platform('/a/b/c') + ChefConfig::Config.config_dir to_platform('/a/b/c') end it "cache_path is /a/b/c/local-mode-cache" do - expect(Chef::Config.cache_path).to eq(to_platform('/a/b/c/local-mode-cache')) + expect(ChefConfig::Config.cache_path).to eq(to_platform('/a/b/c/local-mode-cache')) end end context "and config_dir is /a/b/c/" do before do - Chef::Config.config_dir to_platform('/a/b/c/') + ChefConfig::Config.config_dir to_platform('/a/b/c/') end it "cache_path is /a/b/c/local-mode-cache" do - expect(Chef::Config.cache_path).to eq(to_platform('/a/b/c/local-mode-cache')) + expect(ChefConfig::Config.cache_path).to eq(to_platform('/a/b/c/local-mode-cache')) end end end end - it "Chef::Config[:file_backup_path] defaults to /var/chef/backup" do - allow(Chef::Config).to receive(:cache_path).and_return(primary_cache_path) + it "ChefConfig::Config[:file_backup_path] defaults to /var/chef/backup" do + allow(ChefConfig::Config).to receive(:cache_path).and_return(primary_cache_path) backup_path = is_windows ? "#{primary_cache_path}\\backup" : "#{primary_cache_path}/backup" - expect(Chef::Config[:file_backup_path]).to eq(backup_path) + expect(ChefConfig::Config[:file_backup_path]).to eq(backup_path) end - it "Chef::Config[:ssl_verify_mode] defaults to :verify_peer" do - expect(Chef::Config[:ssl_verify_mode]).to eq(:verify_peer) + it "ChefConfig::Config[:ssl_verify_mode] defaults to :verify_peer" do + expect(ChefConfig::Config[:ssl_verify_mode]).to eq(:verify_peer) end - it "Chef::Config[:ssl_ca_path] defaults to nil" do - expect(Chef::Config[:ssl_ca_path]).to be_nil + it "ChefConfig::Config[:ssl_ca_path] defaults to nil" do + expect(ChefConfig::Config[:ssl_ca_path]).to be_nil end - # TODO can this be removed? + # On Windows, we'll detect an omnibus build and set this to the + # cacert.pem included in the package, but it's nil if you're on Windows + # w/o omnibus (e.g., doing development on Windows, custom build, etc.) if !is_windows - it "Chef::Config[:ssl_ca_file] defaults to nil" do - expect(Chef::Config[:ssl_ca_file]).to be_nil + it "ChefConfig::Config[:ssl_ca_file] defaults to nil" do + expect(ChefConfig::Config[:ssl_ca_file]).to be_nil end end - it "Chef::Config[:data_bag_path] defaults to /var/chef/data_bags" do - allow(Chef::Config).to receive(:cache_path).and_return(primary_cache_path) + it "ChefConfig::Config[:data_bag_path] defaults to /var/chef/data_bags" do + allow(ChefConfig::Config).to receive(:cache_path).and_return(primary_cache_path) data_bag_path = is_windows ? "#{primary_cache_path}\\data_bags" : "#{primary_cache_path}/data_bags" - expect(Chef::Config[:data_bag_path]).to eq(data_bag_path) + expect(ChefConfig::Config[:data_bag_path]).to eq(data_bag_path) end - it "Chef::Config[:environment_path] defaults to /var/chef/environments" do - allow(Chef::Config).to receive(:cache_path).and_return(primary_cache_path) + it "ChefConfig::Config[:environment_path] defaults to /var/chef/environments" do + allow(ChefConfig::Config).to receive(:cache_path).and_return(primary_cache_path) environment_path = is_windows ? "#{primary_cache_path}\\environments" : "#{primary_cache_path}/environments" - expect(Chef::Config[:environment_path]).to eq(environment_path) + expect(ChefConfig::Config[:environment_path]).to eq(environment_path) end describe "setting the config dir" do @@ -274,30 +304,30 @@ describe Chef::Config do context "when the config file is /etc/chef/client.rb" do before do - Chef::Config.config_file = to_platform("/etc/chef/client.rb") + ChefConfig::Config.config_file = to_platform("/etc/chef/client.rb") end it "config_dir is /etc/chef" do - expect(Chef::Config.config_dir).to eq(to_platform("/etc/chef")) + expect(ChefConfig::Config.config_dir).to eq(to_platform("/etc/chef")) end context "and chef is running in local mode" do before do - Chef::Config.local_mode = true + ChefConfig::Config.local_mode = true end it "config_dir is /etc/chef" do - expect(Chef::Config.config_dir).to eq(to_platform("/etc/chef")) + expect(ChefConfig::Config.config_dir).to eq(to_platform("/etc/chef")) end end context "when config_dir is set to /other/config/dir/" do before do - Chef::Config.config_dir = to_platform("/other/config/dir/") + ChefConfig::Config.config_dir = to_platform("/other/config/dir/") end it "yields the explicit value" do - expect(Chef::Config.config_dir).to eq(to_platform("/other/config/dir/")) + expect(ChefConfig::Config.config_dir).to eq(to_platform("/other/config/dir/")) end end @@ -305,20 +335,20 @@ describe Chef::Config do context "when the user's home dir is /home/charlie/" do before do - Chef::Config.user_home = to_platform("/home/charlie") + ChefConfig::Config.user_home = to_platform("/home/charlie") end it "config_dir is /home/charlie/.chef/" do - expect(Chef::Config.config_dir).to eq(Chef::Util::PathHelper.join(to_platform("/home/charlie/.chef"), '')) + expect(ChefConfig::Config.config_dir).to eq(ChefConfig::PathHelper.join(to_platform("/home/charlie/.chef"), '')) end context "and chef is running in local mode" do before do - Chef::Config.local_mode = true + ChefConfig::Config.local_mode = true end it "config_dir is /home/charlie/.chef/" do - expect(Chef::Config.config_dir).to eq(Chef::Util::PathHelper.join(to_platform("/home/charlie/.chef"), '')) + expect(ChefConfig::Config.config_dir).to eq(ChefConfig::PathHelper.join(to_platform("/home/charlie/.chef"), '')) end end end @@ -334,43 +364,43 @@ describe Chef::Config do let(:default_ca_file) { "c:/opscode/chef/embedded/ssl/certs/cacert.pem" } it "finds the embedded dir in the default location" do - allow(Chef::Config).to receive(:_this_file).and_return(default_config_location) - expect(Chef::Config.embedded_dir).to eq("c:/opscode/chef/embedded") + allow(ChefConfig::Config).to receive(:_this_file).and_return(default_config_location) + expect(ChefConfig::Config.embedded_dir).to eq("c:/opscode/chef/embedded") end it "finds the embedded dir in a custom install location" do - allow(Chef::Config).to receive(:_this_file).and_return(alternate_install_location) - expect(Chef::Config.embedded_dir).to eq("c:/my/alternate/install/place/chef/embedded") + allow(ChefConfig::Config).to receive(:_this_file).and_return(alternate_install_location) + expect(ChefConfig::Config.embedded_dir).to eq("c:/my/alternate/install/place/chef/embedded") end it "doesn't error when not in an omnibus install" do - allow(Chef::Config).to receive(:_this_file).and_return(non_omnibus_location) - expect(Chef::Config.embedded_dir).to be_nil + allow(ChefConfig::Config).to receive(:_this_file).and_return(non_omnibus_location) + expect(ChefConfig::Config.embedded_dir).to be_nil end it "sets the ssl_ca_cert path if the cert file is available" do - allow(Chef::Config).to receive(:_this_file).and_return(default_config_location) + allow(ChefConfig::Config).to receive(:_this_file).and_return(default_config_location) allow(File).to receive(:exist?).with(default_ca_file).and_return(true) - expect(Chef::Config.ssl_ca_file).to eq(default_ca_file) + expect(ChefConfig::Config.ssl_ca_file).to eq(default_ca_file) end end end end - describe "Chef::Config[:user_home]" do + describe "ChefConfig::Config[:user_home]" do it "should set when HOME is provided" do expected = to_platform("/home/kitten") - allow(Chef::Util::PathHelper).to receive(:home).and_return(expected) - expect(Chef::Config[:user_home]).to eq(expected) + allow(ChefConfig::PathHelper).to receive(:home).and_return(expected) + expect(ChefConfig::Config[:user_home]).to eq(expected) end it "falls back to the current working directory when HOME and USERPROFILE is not set" do - allow(Chef::Util::PathHelper).to receive(:home).and_return(nil) - expect(Chef::Config[:user_home]).to eq(Dir.pwd) + allow(ChefConfig::PathHelper).to receive(:home).and_return(nil) + expect(ChefConfig::Config[:user_home]).to eq(Dir.pwd) end end - describe "Chef::Config[:encrypted_data_bag_secret]" do + describe "ChefConfig::Config[:encrypted_data_bag_secret]" do let(:db_secret_default_path){ to_platform("/etc/chef/encrypted_data_bag_secret") } before do @@ -380,55 +410,57 @@ describe Chef::Config do context "/etc/chef/encrypted_data_bag_secret exists" do let(:secret_exists) { true } it "sets the value to /etc/chef/encrypted_data_bag_secret" do - expect(Chef::Config[:encrypted_data_bag_secret]).to eq db_secret_default_path + expect(ChefConfig::Config[:encrypted_data_bag_secret]).to eq db_secret_default_path end end context "/etc/chef/encrypted_data_bag_secret does not exist" do let(:secret_exists) { false } it "sets the value to nil" do - expect(Chef::Config[:encrypted_data_bag_secret]).to be_nil + expect(ChefConfig::Config[:encrypted_data_bag_secret]).to be_nil end end end - describe "Chef::Config[:event_handlers]" do + describe "ChefConfig::Config[:event_handlers]" do it "sets a event_handlers to an empty array by default" do - expect(Chef::Config[:event_handlers]).to eq([]) + expect(ChefConfig::Config[:event_handlers]).to eq([]) end it "should be able to add custom handlers" do o = Object.new - Chef::Config[:event_handlers] << o - expect(Chef::Config[:event_handlers]).to be_include(o) + ChefConfig::Config[:event_handlers] << o + expect(ChefConfig::Config[:event_handlers]).to be_include(o) end end - describe "Chef::Config[:user_valid_regex]" do + describe "ChefConfig::Config[:user_valid_regex]" do context "on a platform that is not Windows" do it "allows one letter usernames" do - any_match = Chef::Config[:user_valid_regex].any? { |regex| regex.match('a') } + any_match = ChefConfig::Config[:user_valid_regex].any? { |regex| regex.match('a') } expect(any_match).to be_truthy end end end - describe "Chef::Config[:internal_locale]" do + describe "ChefConfig::Config[:internal_locale]" do let(:shell_out) do - double("Chef::Mixin::ShellOut double", :exitstatus => 0, :stdout => locales) + cmd = instance_double("Mixlib::ShellOut", exitstatus: 0, stdout: locales, error!: nil) + allow(cmd).to receive(:run_command).and_return(cmd) + cmd end let(:locales) { locale_array.join("\n") } before do - allow(Chef::Config).to receive(:shell_out_with_systems_locale!).with("locale -a").and_return(shell_out) + allow(Mixlib::ShellOut).to receive(:new).with("locale -a").and_return(shell_out) end shared_examples_for "a suitable locale" do it "returns an English UTF-8 locale" do - expect(Chef::Log).to_not receive(:warn).with(/Please install an English UTF-8 locale for Chef to use/) - expect(Chef::Log).to_not receive(:debug).with(/Defaulting to locale en_US.UTF-8 on Windows/) - expect(Chef::Log).to_not receive(:debug).with(/No usable locale -a command found/) - expect(Chef::Config.guess_internal_locale).to eq expected_locale + expect(ChefConfig.logger).to_not receive(:warn).with(/Please install an English UTF-8 locale for Chef to use/) + expect(ChefConfig.logger).to_not receive(:debug).with(/Defaulting to locale en_US.UTF-8 on Windows/) + expect(ChefConfig.logger).to_not receive(:debug).with(/No usable locale -a command found/) + expect(ChefConfig::Config.guess_internal_locale).to eq expected_locale end end @@ -478,25 +510,29 @@ describe Chef::Config do let(:locale_array) { ["af_ZA", "af_ZA.ISO8859-1", "af_ZA.ISO8859-15", "af_ZA.UTF-8"] } it "should fall back to C locale" do - expect(Chef::Log).to receive(:warn).with("Please install an English UTF-8 locale for Chef to use, falling back to C locale and disabling UTF-8 support.") - expect(Chef::Config.guess_internal_locale).to eq 'C' + expect(ChefConfig.logger).to receive(:warn).with("Please install an English UTF-8 locale for Chef to use, falling back to C locale and disabling UTF-8 support.") + expect(ChefConfig::Config.guess_internal_locale).to eq 'C' end end context "on error" do let(:locale_array) { [] } + let(:shell_out_cmd) { instance_double("Mixlib::ShellOut") } + before do - allow(Chef::Config).to receive(:shell_out_with_systems_locale!).and_raise("THIS IS AN ERROR") + allow(Mixlib::ShellOut).to receive(:new).and_return(shell_out_cmd) + allow(shell_out_cmd).to receive(:run_command) + allow(shell_out_cmd).to receive(:error!).and_raise(Mixlib::ShellOut::ShellCommandFailed, "this is an error") end it "should default to 'en_US.UTF-8'" do if is_windows - expect(Chef::Log).to receive(:debug).with("Defaulting to locale en_US.UTF-8 on Windows, until it matters that we do something else.") + expect(ChefConfig.logger).to receive(:debug).with("Defaulting to locale en_US.UTF-8 on Windows, until it matters that we do something else.") else - expect(Chef::Log).to receive(:debug).with("No usable locale -a command found, assuming you have en_US.UTF-8 installed.") + expect(ChefConfig.logger).to receive(:debug).with("No usable locale -a command found, assuming you have en_US.UTF-8 installed.") end - expect(Chef::Config.guess_internal_locale).to eq "en_US.UTF-8" + expect(ChefConfig::Config.guess_internal_locale).to eq "en_US.UTF-8" end end end @@ -508,7 +544,7 @@ describe Chef::Config do context "when using our default RSpec configuration" do it "defaults to treating deprecation warnings as errors" do - expect(Chef::Config[:treat_deprecation_warnings_as_errors]).to be(true) + expect(ChefConfig::Config[:treat_deprecation_warnings_as_errors]).to be(true) end it "sets CHEF_TREAT_DEPRECATION_WARNINGS_AS_ERRORS environment variable" do @@ -521,8 +557,8 @@ describe Chef::Config do # we're just checking that the presence of the environment variable # causes treat_deprecation_warnings_as_errors to be set to true after a # config reset. - Chef::Config.reset - expect(Chef::Config[:treat_deprecation_warnings_as_errors]).to be(true) + ChefConfig::Config.reset + expect(ChefConfig::Config[:treat_deprecation_warnings_as_errors]).to be(true) end end @@ -531,14 +567,15 @@ describe Chef::Config do before do ENV.delete('CHEF_TREAT_DEPRECATION_WARNINGS_AS_ERRORS') - Chef::Config.reset + ChefConfig::Config.reset end it "defaults to NOT treating deprecation warnings as errors" do - expect(Chef::Config[:treat_deprecation_warnings_as_errors]).to be(false) + expect(ChefConfig::Config[:treat_deprecation_warnings_as_errors]).to be(false) end end end + end diff --git a/chef-config/spec/unit/path_helper_spec.rb b/chef-config/spec/unit/path_helper_spec.rb new file mode 100644 index 0000000000..3e6213597a --- /dev/null +++ b/chef-config/spec/unit/path_helper_spec.rb @@ -0,0 +1,291 @@ +# +# 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-config/path_helper' +require 'spec_helper' + +RSpec.describe ChefConfig::PathHelper do + + let(:path_helper) { described_class } + + shared_examples_for "common_functionality" do + describe "join" do + + it "joins starting with '' resolve to absolute paths" do + expect(path_helper.join('', 'a', 'b')).to eq("#{path_helper.path_separator}a#{path_helper.path_separator}b") + end + + it "joins ending with '' add a / to the end" do + expect(path_helper.join('a', 'b', '')).to eq("a#{path_helper.path_separator}b#{path_helper.path_separator}") + end + + end + + describe "dirname" do + it "dirname('abc') is '.'" do + expect(path_helper.dirname('abc')).to eq('.') + end + it "dirname('/') is '/'" do + expect(path_helper.dirname(path_helper.path_separator)).to eq(path_helper.path_separator) + end + it "dirname('a/b/c') is 'a/b'" do + expect(path_helper.dirname(path_helper.join('a', 'b', 'c'))).to eq(path_helper.join('a', 'b')) + end + it "dirname('a/b/c/') is 'a/b'" do + expect(path_helper.dirname(path_helper.join('a', 'b', 'c', ''))).to eq(path_helper.join('a', 'b')) + end + it "dirname('/a/b/c') is '/a/b'" do + expect(path_helper.dirname(path_helper.join('', 'a', 'b', 'c'))).to eq(path_helper.join('', 'a', 'b')) + end + end + end + + context "on windows" do + + before(:each) do + allow(ChefConfig).to receive(:windows?).and_return(true) + end + + include_examples("common_functionality") + + it "path_separator is \\" do + expect(path_helper.path_separator).to eq('\\') + end + + describe "platform-specific #join behavior" do + + it "joins components on Windows when some end with unix separators" do + expect(path_helper.join('C:\\foo/', "bar", "baz")).to eq('C:\\foo\\bar\\baz') + end + + it "joins components when some end with separators" do + expected = path_helper.cleanpath("/foo/bar/baz") + expected = "C:#{expected}" + expect(path_helper.join('C:\\foo\\', "bar", "baz")).to eq(expected) + end + + it "joins components when some end and start with separators" do + expected = path_helper.cleanpath("/foo/bar/baz") + expected = "C:#{expected}" + expect(path_helper.join('C:\\foo\\', "bar/", "/baz")).to eq(expected) + end + + it "joins components that don't end in separators" do + expected = path_helper.cleanpath("/foo/bar/baz") + expected = "C:#{expected}" + expect(path_helper.join('C:\\foo', "bar", "baz")).to eq(expected) + end + + end + + + it "cleanpath changes slashes into backslashes and leaves backslashes alone" do + expect(path_helper.cleanpath('/a/b\\c/d/')).to eq('\\a\\b\\c\\d') + end + + it "cleanpath does not remove leading double backslash" do + expect(path_helper.cleanpath('\\\\a/b\\c/d/')).to eq('\\\\a\\b\\c\\d') + end + + end + + context "on unix" do + + before(:each) do + allow(ChefConfig).to receive(:windows?).and_return(false) + end + + include_examples("common_functionality") + + it "path_separator is /" do + expect(path_helper.path_separator).to eq('/') + end + + it "cleanpath removes extra slashes alone" do + expect(path_helper.cleanpath('/a///b/c/d/')).to eq('/a/b/c/d') + end + + describe "platform-specific #join behavior" do + + it "joins components when some end with separators" do + expected = path_helper.cleanpath("/foo/bar/baz") + expect(path_helper.join("/foo/", "bar", "baz")).to eq(expected) + end + + it "joins components when some end and start with separators" do + expected = path_helper.cleanpath("/foo/bar/baz") + expect(path_helper.join("/foo/", "bar/", "/baz")).to eq(expected) + end + + it "joins components that don't end in separators" do + expected = path_helper.cleanpath("/foo/bar/baz") + expect(path_helper.join("/foo", "bar", "baz")).to eq(expected) + end + + end + + end + + describe "validate_path" do + context "on windows" do + before(:each) do + # pass by default + allow(ChefConfig).to receive(:windows?).and_return(true) + allow(path_helper).to receive(:printable?).and_return(true) + allow(path_helper).to receive(:windows_max_length_exceeded?).and_return(false) + end + + it "returns the path if the path passes the tests" do + expect(path_helper.validate_path("C:\\ThisIsRigged")).to eql("C:\\ThisIsRigged") + end + + it "does not raise an error if everything looks great" do + expect { path_helper.validate_path("C:\\cool path\\dude.exe") }.not_to raise_error + end + + it "raises an error if the path has invalid characters" do + allow(path_helper).to receive(:printable?).and_return(false) + expect { path_helper.validate_path("Newline!\n") }.to raise_error(ChefConfig::InvalidPath) + end + + it "Adds the \\\\?\\ prefix if the path exceeds MAX_LENGTH and does not have it" do + long_path = "C:\\" + "a" * 250 + "\\" + "b" * 250 + prefixed_long_path = "\\\\?\\" + long_path + allow(path_helper).to receive(:windows_max_length_exceeded?).and_return(true) + expect(path_helper.validate_path(long_path)).to eql(prefixed_long_path) + end + end + end + + describe "windows_max_length_exceeded?" do + it "returns true if the path is too long (259 + NUL) for the API" do + expect(path_helper.windows_max_length_exceeded?("C:\\" + "a" * 250 + "\\" + "b" * 6)).to be_truthy + end + + it "returns false if the path is not too long (259 + NUL) for the standard API" do + expect(path_helper.windows_max_length_exceeded?("C:\\" + "a" * 250 + "\\" + "b" * 5)).to be_falsey + end + + it "returns false if the path is over 259 characters but uses the \\\\?\\ prefix" do + expect(path_helper.windows_max_length_exceeded?("\\\\?\\C:\\" + "a" * 250 + "\\" + "b" * 250)).to be_falsey + end + end + + describe "printable?" do + it "returns true if the string contains no non-printable characters" do + expect(path_helper.printable?("C:\\Program Files (x86)\\Microsoft Office\\Files.lst")).to be_truthy + end + + it "returns true when given 'abc' in unicode" do + expect(path_helper.printable?("\u0061\u0062\u0063")).to be_truthy + end + + it "returns true when given japanese unicode" do + expect(path_helper.printable?("\uff86\uff87\uff88")).to be_truthy + end + + it "returns false if the string contains a non-printable character" do + expect(path_helper.printable?("\my files\work\notes.txt")).to be_falsey + end + + # This isn't necessarily a requirement, but here to be explicit about functionality. + it "returns false if the string contains a newline or tab" do + expect(path_helper.printable?("\tThere's no way,\n\t *no* way,\n\t that you came from my loins.\n")).to be_falsey + end + end + + describe "canonical_path" do + context "on windows", :windows_only do + it "returns an absolute path with backslashes instead of slashes" do + expect(path_helper.canonical_path("\\\\?\\C:/windows/win.ini")).to eq("\\\\?\\c:\\windows\\win.ini") + end + + it "adds the \\\\?\\ prefix if it is missing" do + expect(path_helper.canonical_path("C:/windows/win.ini")).to eq("\\\\?\\c:\\windows\\win.ini") + end + + it "returns a lowercase path" do + expect(path_helper.canonical_path("\\\\?\\C:\\CASE\\INSENSITIVE")).to eq("\\\\?\\c:\\case\\insensitive") + end + end + + context "not on windows", :unix_only do + it "returns a canonical path" do + expect(path_helper.canonical_path("/etc//apache.d/sites-enabled/../sites-available/default")).to eq("/etc/apache.d/sites-available/default") + end + end + end + + describe "paths_eql?" do + it "returns true if the paths are the same" do + allow(path_helper).to receive(:canonical_path).with("bandit").and_return("c:/bandit/bandit") + allow(path_helper).to receive(:canonical_path).with("../bandit/bandit").and_return("c:/bandit/bandit") + expect(path_helper.paths_eql?("bandit", "../bandit/bandit")).to be_truthy + end + + it "returns false if the paths are different" do + allow(path_helper).to receive(:canonical_path).with("bandit").and_return("c:/Bo/Bandit") + allow(path_helper).to receive(:canonical_path).with("../bandit/bandit").and_return("c:/bandit/bandit") + expect(path_helper.paths_eql?("bandit", "../bandit/bandit")).to be_falsey + end + end + + describe "escape_glob" do + it "escapes characters reserved by glob" do + path = "C:\\this\\*path\\[needs]\\escaping?" + escaped_path = "C:\\\\this\\\\\\*path\\\\\\[needs\\]\\\\escaping\\?" + expect(path_helper.escape_glob(path)).to eq(escaped_path) + end + + context "when given more than one argument" do + it "joins, cleanpaths, and escapes characters reserved by glob" do + args = ["this/*path", "[needs]", "escaping?"] + escaped_path = if ChefConfig.windows? + "this\\\\\\*path\\\\\\[needs\\]\\\\escaping\\?" + else + "this/\\*path/\\[needs\\]/escaping\\?" + end + expect(path_helper).to receive(:join).with(*args).and_call_original + expect(path_helper).to receive(:cleanpath).and_call_original + expect(path_helper.escape_glob(*args)).to eq(escaped_path) + end + end + end + + describe "all_homes" do + before do + stub_const('ENV', env) + allow(ChefConfig).to receive(:windows?).and_return(is_windows) + end + + context "on windows" do + let (:is_windows) { true } + end + + context "on unix" do + let (:is_windows) { false } + + context "when HOME is not set" do + let (:env) { {} } + it "returns an empty array" do + expect(path_helper.all_homes).to eq([]) + end + end + end + end +end diff --git a/chef-windows.gemspec b/chef-windows.gemspec new file mode 100644 index 0000000000..167358c7d7 --- /dev/null +++ b/chef-windows.gemspec @@ -0,0 +1,22 @@ +gemspec = eval(IO.read(File.expand_path("../chef.gemspec", __FILE__))) + +gemspec.platform = Gem::Platform.new(["universal", "mingw32"]) + +gemspec.add_dependency "ffi", "~> 1.9" +gemspec.add_dependency "win32-api", "~> 1.5.3" +gemspec.add_dependency "win32-dir", "~> 0.5.0" +gemspec.add_dependency "win32-event", "~> 0.6.1" +gemspec.add_dependency "win32-eventlog", "~> 0.6.2" +gemspec.add_dependency "win32-mmap", "~> 0.4.1" +gemspec.add_dependency "win32-mutex", "~> 0.4.2" +gemspec.add_dependency "win32-process", "~> 0.7.5" +gemspec.add_dependency "win32-service", "0.8.6" +gemspec.add_dependency "windows-api", "~> 0.4.4" +gemspec.add_dependency "windows-pr", "~> 1.2.4" +gemspec.add_dependency "wmi-lite", "~> 1.0" +gemspec.extensions << "ext/win32-eventlog/Rakefile" +gemspec.files += %w(ext/win32-eventlog/Rakefile ext/win32-eventlog/chef-log.man) + +gemspec.executables += %w( chef-service-manager chef-windows-service ) + +gemspec diff --git a/chef-x86-mingw32.gemspec b/chef-x86-mingw32.gemspec deleted file mode 100644 index 18c31d8354..0000000000 --- a/chef-x86-mingw32.gemspec +++ /dev/null @@ -1,23 +0,0 @@ -# x86-mingw32 Gemspec # -gemspec = eval(IO.read(File.expand_path("../chef.gemspec", __FILE__))) - -gemspec.platform = "x86-mingw32" - -gemspec.add_dependency "ffi", "~> 1.9" -gemspec.add_dependency "windows-api", "~> 0.4.2" -gemspec.add_dependency "windows-pr", "~> 1.2.2" -gemspec.add_dependency "win32-api", "~> 1.5.1" -gemspec.add_dependency "win32-dir", "0.4.5" -gemspec.add_dependency "win32-event", "0.6.1" -gemspec.add_dependency "win32-mutex", "0.4.1" -gemspec.add_dependency "win32-process", "~> 0.7.3" -gemspec.add_dependency "win32-service", "0.8.2" -gemspec.add_dependency "win32-mmap", "0.4.0" -gemspec.add_dependency "wmi-lite", "~> 1.0" -gemspec.add_dependency "win32-eventlog", "0.6.1" -gemspec.extensions << "ext/win32-eventlog/Rakefile" -gemspec.files += %w(ext/win32-eventlog/Rakefile ext/win32-eventlog/chef-log.man) - -gemspec.executables += %w( chef-service-manager chef-windows-service ) - -gemspec diff --git a/chef.gemspec b/chef.gemspec index dc0e59ad3e..f4f8a31207 100644 --- a/chef.gemspec +++ b/chef.gemspec @@ -10,19 +10,20 @@ Gem::Specification.new do |s| s.description = s.summary s.license = "Apache-2.0" s.author = "Adam Jacob" - s.email = "adam@getchef.com" - s.homepage = "http://www.getchef.com" + s.email = "adam@chef.io" + s.homepage = "http://www.chef.io" s.required_ruby_version = ">= 2.0.0" - s.add_dependency "mixlib-config", "~> 2.0" + s.add_dependency "chef-config", "= #{Chef::VERSION}" + 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", ">= 2.0.0.rc.0", "< 3.0" s.add_dependency "ohai", "~> 8.0" - s.add_dependency "ffi-yajl", ">= 1.2", "< 3.0" + s.add_dependency "ffi-yajl", "~> 2.2" s.add_dependency "net-ssh", "~> 2.6" s.add_dependency "net-ssh-multi", "~> 1.1" # CHEF-3027: The knife-cloud plugins require newer features from highline, core chef should not. @@ -30,7 +31,7 @@ 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", "~> 4.1" + s.add_dependency "chef-zero", "~> 4.2", ">= 4.2.2" s.add_dependency "pry", "~> 0.9" s.add_dependency 'plist', '~> 3.1.0' @@ -41,10 +42,12 @@ Gem::Specification.new do |s| s.add_dependency "serverspec", "~> 2.7" s.add_dependency "specinfra", "~> 2.10" + s.add_dependency "syslog-logger", "~> 1.6" + s.add_development_dependency "rack" + s.add_development_dependency "cheffish", "~> 1.1" - # Rake 10.2 drops Ruby 1.8 support - s.add_development_dependency "rake", "~> 10.1.0" + s.add_development_dependency "rake", "~> 10.1" s.bindir = "bin" s.executables = %w( chef-client chef-solo knife chef-shell chef-apply ) diff --git a/distro/common/markdown/man1/chef-shell.mkd b/distro/common/markdown/man1/chef-shell.mkd index 5525ef8ea9..216dc73d41 100644 --- a/distro/common/markdown/man1/chef-shell.mkd +++ b/distro/common/markdown/man1/chef-shell.mkd @@ -131,8 +131,8 @@ Recipe mode implements Chef's recipe DSL. Exhaustively documenting this DSL is outside the scope of this document. See the following pages in the Chef documentation for more information: - * <http://wiki.opscode.com/display/chef/Resources> - * <http://wiki.opscode.com/display/chef/Recipes> + * <http://docs.chef.io/resources.html> + * <http://docs.chef.io/recipes.html> Once you have defined resources in the recipe, you can trigger a convergence run via `run_chef` @@ -176,7 +176,7 @@ libraries. ## SEE ALSO chef-client(8) knife(1) - <http://wiki.opscode.com/display/chef/Chef+Shell> + <http://docs.chef.io/ctl_chef_shell.html> ## AUTHOR @@ -192,4 +192,4 @@ libraries. ## CHEF - chef-shell is distributed with Chef. <http://wiki.opscode.com/display/chef/Home> + chef-shell is distributed with Chef. <http://docs.chef.io> diff --git a/distro/common/markdown/man1/knife-bootstrap.mkd b/distro/common/markdown/man1/knife-bootstrap.mkd index cb292de311..a1a2d3460c 100644 --- a/distro/common/markdown/man1/knife-bootstrap.mkd +++ b/distro/common/markdown/man1/knife-bootstrap.mkd @@ -138,4 +138,4 @@ to other users via the process list using tools such as ps(1). ## CHEF - Knife is distributed with Chef. <http://wiki.opscode.com/display/chef/Home> + Knife is distributed with Chef. <http://docs.chef.io> diff --git a/distro/common/markdown/man1/knife-client.mkd b/distro/common/markdown/man1/knife-client.mkd index e7b732ef71..b95a578391 100644 --- a/distro/common/markdown/man1/knife-client.mkd +++ b/distro/common/markdown/man1/knife-client.mkd @@ -99,5 +99,5 @@ setting up a host for management with Chef. Permission is granted to copy, distribute and / or modify this document under the terms of the Apache 2.0 License. ## CHEF - Knife is distributed with Chef. <http://wiki.opscode.com/display/chef/Home> + Knife is distributed with Chef. <http://docs.chef.io> diff --git a/distro/common/markdown/man1/knife-configure.mkd b/distro/common/markdown/man1/knife-configure.mkd index 507d30db4e..f3a4ef02bb 100644 --- a/distro/common/markdown/man1/knife-configure.mkd +++ b/distro/common/markdown/man1/knife-configure.mkd @@ -66,5 +66,5 @@ the specified _directory_. Permission is granted to copy, distribute and / or modify this document under the terms of the Apache 2.0 License. ## CHEF - Knife is distributed with Chef. <http://wiki.opscode.com/display/chef/Home> + Knife is distributed with Chef. <http://docs.chef.io> diff --git a/distro/common/markdown/man1/knife-cookbook-site.mkd b/distro/common/markdown/man1/knife-cookbook-site.mkd index 9496cf1765..68bc8433df 100644 --- a/distro/common/markdown/man1/knife-cookbook-site.mkd +++ b/distro/common/markdown/man1/knife-cookbook-site.mkd @@ -119,5 +119,5 @@ Uploading cookbooks to the Opscode cookbooks site: Permission is granted to copy, distribute and / or modify this document under the terms of the Apache 2.0 License. ## CHEF - Knife is distributed with Chef. <http://wiki.opscode.com/display/chef/Home> + Knife is distributed with Chef. <http://docs.chef.io> diff --git a/distro/common/markdown/man1/knife-cookbook.mkd b/distro/common/markdown/man1/knife-cookbook.mkd index deaf00447a..6a56059e80 100644 --- a/distro/common/markdown/man1/knife-cookbook.mkd +++ b/distro/common/markdown/man1/knife-cookbook.mkd @@ -236,7 +236,7 @@ to specify alternate files to be used on a specific OS platform or host. The default specificity setting is _default_, that is files in `COOKBOOK/files/default` will be used when a more specific copy is not available. Further documentation for this feature is available on the -Chef wiki: <http://wiki.opscode.com/display/chef/File+Distribution#FileDistribution-FileSpecificity> +Chef wiki: <https://docs.chef.io/resource_cookbook_file.html#file-specificity> Cookbooks also contain a metadata file that defines various properties of the cookbook. The most important of these are the _version_ and the @@ -248,8 +248,8 @@ cookbook. ## SEE ALSO __knife-environment(1)__ __knife-cookbook-site(1)__ - <http://wiki.opscode.com/display/chef/Cookbooks> - <http://wiki.opscode.com/display/chef/Metadata> + <http://docs.chef.io/cookbooks.html> + <http://docs.chef.io/cookbook_repo.html> ## AUTHOR Chef was written by Adam Jacob <adam@opscode.com> with many contributions from the community. @@ -260,4 +260,4 @@ cookbook. ## CHEF - Knife is distributed with Chef. <http://wiki.opscode.com/display/chef/Home> + Knife is distributed with Chef. <http://docs.chef.io> diff --git a/distro/common/markdown/man1/knife-data-bag.mkd b/distro/common/markdown/man1/knife-data-bag.mkd index 53abf95272..cab28a2f7f 100644 --- a/distro/common/markdown/man1/knife-data-bag.mkd +++ b/distro/common/markdown/man1/knife-data-bag.mkd @@ -117,5 +117,5 @@ encryption keys. Permission is granted to copy, distribute and / or modify this document under the terms of the Apache 2.0 License. ## CHEF - Knife is distributed with Chef. http://wiki.opscode.com/display/chef/Home + Knife is distributed with Chef. http://docs.chef.io/ diff --git a/distro/common/markdown/man1/knife-environment.mkd b/distro/common/markdown/man1/knife-environment.mkd index 98ca4997bd..06bf423dc0 100644 --- a/distro/common/markdown/man1/knife-environment.mkd +++ b/distro/common/markdown/man1/knife-environment.mkd @@ -137,8 +137,8 @@ The Ruby format of an environment is as follows: ## SEE ALSO __knife-node(1)__ __knife-cookbook(1)__ __knife-role(1)__ - <http://wiki.opscode.com/display/chef/Environments> - <http://wiki.opscode.com/display/chef/Version+Constraints> + <http://docs.chef.io/environments.html> + <http://docs.chef.io/cookbook_versions.html> ## AUTHOR Chef was written by Adam Jacob <adam@opscode.com> with many contributions from the community. @@ -148,4 +148,4 @@ The Ruby format of an environment is as follows: Permission is granted to copy, distribute and / or modify this document under the terms of the Apache 2.0 License. ## CHEF - Knife is distributed with Chef. <http://wiki.opscode.com/display/chef/Home> + Knife is distributed with Chef. <http://docs.chef.io> diff --git a/distro/common/markdown/man1/knife-exec.mkd b/distro/common/markdown/man1/knife-exec.mkd index d4aa87ca46..1b60177d16 100644 --- a/distro/common/markdown/man1/knife-exec.mkd +++ b/distro/common/markdown/man1/knife-exec.mkd @@ -39,4 +39,4 @@ description of the commands available. ## CHEF - Knife is distributed with Chef. <http://wiki.opscode.com/display/chef/Home> + Knife is distributed with Chef. <http://docs.chef.io> diff --git a/distro/common/markdown/man1/knife-index.mkd b/distro/common/markdown/man1/knife-index.mkd index 812f3fe7dd..f1425b8013 100644 --- a/distro/common/markdown/man1/knife-index.mkd +++ b/distro/common/markdown/man1/knife-index.mkd @@ -26,5 +26,5 @@ time for all objects to be indexed and available for search. Permission is granted to copy, distribute and / or modify this document under the terms of the Apache 2.0 License. ## CHEF - Knife is distributed with Chef. <http://wiki.opscode.com/display/chef/Home> + Knife is distributed with Chef. <http://docs.chef.io> diff --git a/distro/common/markdown/man1/knife-node.mkd b/distro/common/markdown/man1/knife-node.mkd index 72b7d0072f..0262d64702 100644 --- a/distro/common/markdown/man1/knife-node.mkd +++ b/distro/common/markdown/man1/knife-node.mkd @@ -126,5 +126,5 @@ When adding a recipe to a run list, there are several valid formats: Permission is granted to copy, distribute and / or modify this document under the terms of the Apache 2.0 License. ## CHEF - Knife is distributed with Chef. <http://wiki.opscode.com/display/chef/Home> + Knife is distributed with Chef. <http://docs.chef.io> diff --git a/distro/common/markdown/man1/knife-role.mkd b/distro/common/markdown/man1/knife-role.mkd index 7e0dac9bc5..e202c52d81 100644 --- a/distro/common/markdown/man1/knife-role.mkd +++ b/distro/common/markdown/man1/knife-role.mkd @@ -70,8 +70,8 @@ run\_list. ## SEE ALSO __knife-node(1)__ __knife-environment(1)__ - <http://wiki.opscode.com/display/chef/Roles> - <http://wiki.opscode.com/display/chef/Attributes> + <http://docs.chef.io/roles.html> + <http://docs.chef.io/attributes.html> ## AUTHOR Chef was written by Adam Jacob <adam@opscode.com> with many contributions from the community. @@ -81,5 +81,5 @@ run\_list. Permission is granted to copy, distribute and / or modify this document under the terms of the Apache 2.0 License. ## CHEF - Knife is distributed with Chef. <http://wiki.opscode.com/display/chef/Home> + Knife is distributed with Chef. <http://docs.chef.io> diff --git a/distro/common/markdown/man1/knife-search.mkd b/distro/common/markdown/man1/knife-search.mkd index d6729be322..b289b2c83b 100644 --- a/distro/common/markdown/man1/knife-search.mkd +++ b/distro/common/markdown/man1/knife-search.mkd @@ -164,7 +164,7 @@ Find all nodes running CentOS in the production environment: ## SEE ALSO __knife-ssh__(1) - <http://wiki.opscode.com/display/chef/Attributes> + <http://docs.chef.io/attributes.html> [Lucene Query Parser Syntax](http://lucene.apache.org/java/2_3_2/queryparsersyntax.html) ## AUTHOR @@ -175,6 +175,6 @@ Find all nodes running CentOS in the production environment: Permission is granted to copy, distribute and / or modify this document under the terms of the Apache 2.0 License. ## CHEF - Knife is distributed with Chef. <http://wiki.opscode.com/display/chef/Home> + Knife is distributed with Chef. <http://docs.chef.io> diff --git a/distro/common/markdown/man1/knife-ssh.mkd b/distro/common/markdown/man1/knife-ssh.mkd index 07fc5940ce..7d37075470 100644 --- a/distro/common/markdown/man1/knife-ssh.mkd +++ b/distro/common/markdown/man1/knife-ssh.mkd @@ -64,6 +64,6 @@ The available multiplexers are: Permission is granted to copy, distribute and / or modify this document under the terms of the Apache 2.0 License. ## CHEF - Knife is distributed with Chef. <http://wiki.opscode.com/display/chef/Home> + Knife is distributed with Chef. <http://docs.chef.io> diff --git a/distro/common/markdown/man1/knife-status.mkd b/distro/common/markdown/man1/knife-status.mkd index 07f0ff305a..0a969e40dd 100644 --- a/distro/common/markdown/man1/knife-status.mkd +++ b/distro/common/markdown/man1/knife-status.mkd @@ -31,6 +31,6 @@ may not be publicly reachable. Permission is granted to copy, distribute and / or modify this document under the terms of the Apache 2.0 License. ## CHEF - Knife is distributed with Chef. <http://wiki.opscode.com/display/chef/Home> + Knife is distributed with Chef. <http://docs.chef.io> diff --git a/distro/common/markdown/man1/knife-tag.mkd b/distro/common/markdown/man1/knife-tag.mkd index 6a1a2c4b56..b5bbb8236f 100644 --- a/distro/common/markdown/man1/knife-tag.mkd +++ b/distro/common/markdown/man1/knife-tag.mkd @@ -35,5 +35,5 @@ Lists the tags applied to _node_ Permission is granted to copy, distribute and / or modify this document under the terms of the Apache 2.0 License. ## CHEF - Knife is distributed with Chef. <http://wiki.opscode.com/display/chef/Home> + Knife is distributed with Chef. <http://docs.chef.io> diff --git a/distro/common/markdown/man1/knife.mkd b/distro/common/markdown/man1/knife.mkd index c3add163f9..3d7c095c10 100644 --- a/distro/common/markdown/man1/knife.mkd +++ b/distro/common/markdown/man1/knife.mkd @@ -186,7 +186,7 @@ recommended though, and git fits with a lot of the workflow paradigms. __knife-node(1)__ __knife-recipe(1)__ __knife-role(1)__ __knife-search(1)__ __knife-ssh(1)__ __knife-tag(1)__ - Complete Chef documentation is available online: <http://wiki.opscode.com/display/chef/Home/> + Complete Chef documentation is available online: <http://docs.chef.io/> JSON is JavaScript Object Notation <http://json.org/> @@ -209,5 +209,5 @@ recommended though, and git fits with a lot of the workflow paradigms. On some systems, the complete text of the Apache 2.0 License may be found in `/usr/share/common-licenses/Apache-2.0`. ## CHEF - Knife is distributed with Chef. <http://wiki.opscode.com/display/chef/Home> + Knife is distributed with Chef. <http://docs.chef.io/> diff --git a/distro/common/markdown/man8/chef-client.mkd b/distro/common/markdown/man8/chef-client.mkd index e37d283133..ffe444ecf2 100644 --- a/distro/common/markdown/man8/chef-client.mkd +++ b/distro/common/markdown/man8/chef-client.mkd @@ -59,8 +59,7 @@ are largely services that exist only to provide the Client with information. ## SEE ALSO -Full documentation for Chef and chef-client is located on the Chef -wiki, http://wiki.opscode.com/display/chef/Home. +Full documentation for Chef and chef-client is located on docs site, http://docs.chef.io/. ## AUTHOR diff --git a/distro/common/markdown/man8/chef-expander.mkd b/distro/common/markdown/man8/chef-expander.mkd index 9190a9aebb..a2bb7d72b0 100644 --- a/distro/common/markdown/man8/chef-expander.mkd +++ b/distro/common/markdown/man8/chef-expander.mkd @@ -67,8 +67,7 @@ See __chef-expanderctl__(8) for details. __chef-expanderctl__(8) __chef-solr__(8) -Full documentation for Chef and chef-server is located on the Chef -wiki, http://wiki.opscode.com/display/chef/Home. +Full documentation for Chef and chef-server is located on docs site, http://docs.chef.io/. ## AUTHOR diff --git a/distro/common/markdown/man8/chef-expanderctl.mkd b/distro/common/markdown/man8/chef-expanderctl.mkd index 03ce6af8ac..db593cb47a 100644 --- a/distro/common/markdown/man8/chef-expanderctl.mkd +++ b/distro/common/markdown/man8/chef-expanderctl.mkd @@ -43,8 +43,7 @@ be restarted by the master process. __chef-expander-cluster__(8) __chef-solr__(8) -Full documentation for Chef and chef-server is located on the Chef -wiki, http://wiki.opscode.com/display/chef/Home. +Full documentation for Chef and chef-server is located on docs site, http://docs.chef.io/. ## AUTHOR diff --git a/distro/common/markdown/man8/chef-server-webui.mkd b/distro/common/markdown/man8/chef-server-webui.mkd index 977e1495e2..b176d12690 100644 --- a/distro/common/markdown/man8/chef-server-webui.mkd +++ b/distro/common/markdown/man8/chef-server-webui.mkd @@ -106,7 +106,7 @@ The default credentials are: ## SEE ALSO Full documentation for Chef and chef-server-webui (Management Console) -is located on the Chef wiki, http://wiki.opscode.com/display/chef/Home. +is located on the Chef docs site, http://docs.chef.io/. ## AUTHOR diff --git a/distro/common/markdown/man8/chef-server.mkd b/distro/common/markdown/man8/chef-server.mkd index 1b0f35eb77..46a5ea4346 100644 --- a/distro/common/markdown/man8/chef-server.mkd +++ b/distro/common/markdown/man8/chef-server.mkd @@ -106,8 +106,7 @@ __chef-client__(8) __chef-server-webui__(8) __knife__(1) -Full documentation for Chef and chef-server is located on the Chef -wiki, http://wiki.opscode.com/display/chef/Home. +Full documentation for Chef and chef-server is located on docs site, http://docs.chef.io/. ## AUTHOR diff --git a/distro/common/markdown/man8/chef-solo.mkd b/distro/common/markdown/man8/chef-solo.mkd index 861a0faa2d..9d5d9a43b7 100644 --- a/distro/common/markdown/man8/chef-solo.mkd +++ b/distro/common/markdown/man8/chef-solo.mkd @@ -92,8 +92,8 @@ and use the run_list from ~/node.json. ## SEE ALSO -Full documentation for Chef and chef-solo is located on the Chef wiki, -http://wiki.opscode.com/display/chef/Home. +Full documentation for Chef and chef-solo is located on the Chef docs site, +http://docs.chef.io/. ## AUTHOR diff --git a/distro/common/markdown/man8/chef-solr.mkd b/distro/common/markdown/man8/chef-solr.mkd index 02e7d6285e..a210a90a07 100644 --- a/distro/common/markdown/man8/chef-solr.mkd +++ b/distro/common/markdown/man8/chef-solr.mkd @@ -75,7 +75,7 @@ when prompted for confirmation. The process should look like this: __chef-expander-cluster__(8) Full documentation for Chef and chef-server is located on the Chef -wiki, http://wiki.opscode.com/display/chef/Home. +Docs site, http://docs.chef.io/. ## AUTHOR diff --git a/distro/powershell/chef/chef.psm1 b/distro/powershell/chef/chef.psm1 new file mode 100644 index 0000000000..6646226795 --- /dev/null +++ b/distro/powershell/chef/chef.psm1 @@ -0,0 +1,327 @@ + +function Load-Win32Bindings { + Add-Type -TypeDefinition @" +using System; +using System.Diagnostics; +using System.Runtime.InteropServices; + +namespace Chef +{ + +[StructLayout(LayoutKind.Sequential)] +public struct PROCESS_INFORMATION +{ + public IntPtr hProcess; + public IntPtr hThread; + public uint dwProcessId; + public uint dwThreadId; +} + +[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] +public struct STARTUPINFO +{ + public uint cb; + public string lpReserved; + public string lpDesktop; + public string lpTitle; + public uint dwX; + public uint dwY; + public uint dwXSize; + public uint dwYSize; + public uint dwXCountChars; + public uint dwYCountChars; + public uint dwFillAttribute; + public STARTF dwFlags; + public ShowWindow wShowWindow; + public short cbReserved2; + public IntPtr lpReserved2; + public IntPtr hStdInput; + public IntPtr hStdOutput; + public IntPtr hStdError; +} + +[StructLayout(LayoutKind.Sequential)] +public struct SECURITY_ATTRIBUTES +{ + public int length; + public IntPtr lpSecurityDescriptor; + public bool bInheritHandle; +} + +[Flags] +public enum CreationFlags : int +{ + NONE = 0, + DEBUG_PROCESS = 0x00000001, + DEBUG_ONLY_THIS_PROCESS = 0x00000002, + CREATE_SUSPENDED = 0x00000004, + DETACHED_PROCESS = 0x00000008, + CREATE_NEW_CONSOLE = 0x00000010, + CREATE_NEW_PROCESS_GROUP = 0x00000200, + CREATE_UNICODE_ENVIRONMENT = 0x00000400, + CREATE_SEPARATE_WOW_VDM = 0x00000800, + CREATE_SHARED_WOW_VDM = 0x00001000, + CREATE_PROTECTED_PROCESS = 0x00040000, + EXTENDED_STARTUPINFO_PRESENT = 0x00080000, + CREATE_BREAKAWAY_FROM_JOB = 0x01000000, + CREATE_PRESERVE_CODE_AUTHZ_LEVEL = 0x02000000, + CREATE_DEFAULT_ERROR_MODE = 0x04000000, + CREATE_NO_WINDOW = 0x08000000, +} + +[Flags] +public enum STARTF : uint +{ + STARTF_USESHOWWINDOW = 0x00000001, + STARTF_USESIZE = 0x00000002, + STARTF_USEPOSITION = 0x00000004, + STARTF_USECOUNTCHARS = 0x00000008, + STARTF_USEFILLATTRIBUTE = 0x00000010, + STARTF_RUNFULLSCREEN = 0x00000020, // ignored for non-x86 platforms + STARTF_FORCEONFEEDBACK = 0x00000040, + STARTF_FORCEOFFFEEDBACK = 0x00000080, + STARTF_USESTDHANDLES = 0x00000100, +} + +public enum ShowWindow : short +{ + SW_HIDE = 0, + SW_SHOWNORMAL = 1, + SW_NORMAL = 1, + SW_SHOWMINIMIZED = 2, + SW_SHOWMAXIMIZED = 3, + SW_MAXIMIZE = 3, + SW_SHOWNOACTIVATE = 4, + SW_SHOW = 5, + SW_MINIMIZE = 6, + SW_SHOWMINNOACTIVE = 7, + SW_SHOWNA = 8, + SW_RESTORE = 9, + SW_SHOWDEFAULT = 10, + SW_FORCEMINIMIZE = 11, + SW_MAX = 11 +} + +public enum StandardHandle : int +{ + Input = -10, + Output = -11, + Error = -12 +} + +public static class Kernel32 +{ + [DllImport("kernel32.dll", SetLastError=true)] + [return: MarshalAs(UnmanagedType.Bool)] + public static extern bool CreateProcess( + string lpApplicationName, + string lpCommandLine, + ref SECURITY_ATTRIBUTES lpProcessAttributes, + ref SECURITY_ATTRIBUTES lpThreadAttributes, + [MarshalAs(UnmanagedType.Bool)] bool bInheritHandles, + CreationFlags dwCreationFlags, + IntPtr lpEnvironment, + string lpCurrentDirectory, + ref STARTUPINFO lpStartupInfo, + out PROCESS_INFORMATION lpProcessInformation); + + [DllImport("kernel32.dll", SetLastError=true)] + public static extern IntPtr GetStdHandle( + StandardHandle nStdHandle); + + [DllImport("kernel32", SetLastError=true)] + public static extern int WaitForSingleObject( + IntPtr hHandle, + int dwMilliseconds); + + [DllImport("kernel32", SetLastError=true)] + [return: MarshalAs(UnmanagedType.Bool)] + public static extern bool CloseHandle( + IntPtr hObject); + + [DllImport("kernel32", SetLastError=true)] + [return: MarshalAs(UnmanagedType.Bool)] + public static extern bool GetExitCodeProcess( + IntPtr hProcess, + out int lpExitCode); +} +} +"@ +} + +function Run-ExecutableAndWait($AppPath, $ArgumentString) { + # Use the Win32 API to create a new process and wait for it to terminate. + $null = Load-Win32Bindings + + $si = New-Object Chef.STARTUPINFO + $pi = New-Object Chef.PROCESS_INFORMATION + + $si.cb = [System.Runtime.InteropServices.Marshal]::SizeOf($si) + $si.wShowWindow = [Chef.ShowWindow]::SW_SHOW + $si.dwFlags = [Chef.STARTF]::STARTF_USESTDHANDLES + $si.hStdError = [Chef.Kernel32]::GetStdHandle([Chef.StandardHandle]::Error) + $si.hStdOutput = [Chef.Kernel32]::GetStdHandle([Chef.StandardHandle]::Output) + $si.hStdInput = [Chef.Kernel32]::GetStdHandle([Chef.StandardHandle]::Input) + + $pSec = New-Object Chef.SECURITY_ATTRIBUTES + $pSec.Length = [System.Runtime.InteropServices.Marshal]::SizeOf($pSec) + $pSec.bInheritHandle = $true + $tSec = New-Object Chef.SECURITY_ATTRIBUTES + $tSec.Length = [System.Runtime.InteropServices.Marshal]::SizeOf($tSec) + $tSec.bInheritHandle = $true + + $success = [Chef.Kernel32]::CreateProcess($AppPath, $ArgumentString, [ref] $pSec, [ref] $tSec, $true, [Chef.CreationFlags]::NONE, [IntPtr]::Zero, $pwd, [ref] $si, [ref] $pi) + if (-Not $success) { + $reason = [System.Runtime.InteropServices.Marshal]::GetLastWin32Error() + throw "Unable to create process [$ArgumentString]. Error code $reason." + } + $waitReason = [Chef.Kernel32]::WaitForSingleObject($pi.hProcess, -1) + if ($waitReason -ne 0) { + if ($waitReason -eq -1) { + $reason = [System.Runtime.InteropServices.Marshal]::GetLastWin32Error() + throw "Could not wait for process to terminate. Error code $reason." + } else { + throw "WaitForSingleObject failed with return code $waitReason - it's impossible!" + } + } + $success = [Chef.Kernel32]::GetExitCodeProcess($pi.hProcess, [ref] $global:LASTEXITCODE) + if (-Not $success) { + $reason = [System.Runtime.InteropServices.Marshal]::GetLastWin32Error() + throw "Process exit code unavailable. Error code $reason." + } + $success = [Chef.Kernel32]::CloseHandle($pi.hProcess) + if (-Not $success) { + $reason = [System.Runtime.InteropServices.Marshal]::GetLastWin32Error() + throw "Unable to release process handle. Error code $reason." + } + $success = [Chef.Kernel32]::CloseHandle($pi.hThread) + if (-Not $success) { + $reason = [System.Runtime.InteropServices.Marshal]::GetLastWin32Error() + throw "Unable to release thread handle. Error code $reason." + } +} + +function Get-ScriptDirectory { + if (!$PSScriptRoot) { + $Invocation = (Get-Variable MyInvocation -Scope 1).Value + $PSScriptRoot = Split-Path $Invocation.MyCommand.Path + } + $PSScriptRoot +} + +function Run-RubyCommand($command, $argList) { + # This method exists to take the given list of arguments and get it past ruby's command-line + # interpreter unscathed and untampered. See https://github.com/ruby/ruby/blob/trunk/win32/win32.c#L1582 + # for a list of transformations that ruby attempts to perform with your command-line arguments + # before passing it onto a script. The most important task is to defeat the globbing + # and wild-card expansion that ruby performs. Note that ruby does not use MSVCRT's argc/argv + # and deliberately reparses the raw command-line instead. + # + # To stop ruby from interpreting command-line arguments as globs, they need to be enclosed in ' + # Ruby doesn't allow any escape characters inside '. This unfortunately prevents us from sending + # any strings which themselves contain '. Ruby does allow multi-fragment arguments though. + # "foo bar"'baz qux'123"foo" is interpreted as 1 argument because there are no un-escaped + # whitespace there. The argument would be interpreted as the string "foo barbaz qux123foo". + # This lets us escape ' characters by exiting the ' quoted string, injecting a "'" fragment and + # then resuming the ' quoted string again. + # + # In the process of defeating ruby, one must also defeat the helpfulness of powershell. + # When arguments come into this method, the standard PS rules for interpreting cmdlet arguments + # apply. When using & (call operator) and providing an array of arguments, powershell (verified + # on PS 4.0 on Windows Server 2012R2) will not evaluate them but (contrary to documentation), + # it will still marginally interpret them. The behaviour of PS 5.0 seems to be different but + # ignore that for now. If any of the provided arguments has a space in it, powershell checks + # the first and last character to ensure that they are " characters (and that's all it checks). + # If they are not, it will blindly surround that argument with " characters. It won't do this + # operation if no space is present, even if other special characters are present. If it notices + # leading and trailing " characters, it won't actually check to see if there are other " + # characters in the string. Since PS 5.0 changes this behavior, we could consider using the --% + # "stop screwing up my arguments" operator, which is available since PS 3.0. When encountered + # --% indicates that the rest of line is to be sent literally... except if the parser encounters + # %FOO% cmd style environment variables. Because reasons. And there is no way to escape the + # % character in *any* waym shape or form. + # https://connect.microsoft.com/PowerShell/feedback/details/376207/executing-commands-which-require-quotes-and-variables-is-practically-impossible + # + # In case you think that you're either reading this incorrectly or that I'm full of shit, here + # are some examples. These use EchoArgs.exe from the PowerShell Community Extensions package. + # I have not included the argument parsing output from EchoArgs.exe to prevent confusing you with + # more details about MSVCRT's parsing algorithm. + # + # $x = "foo '' bar `"baz`"" + # & EchoArgs @($x, $x) + # Command line: + # "C:\Program Files (x86)\PowerShell Community Extensions\Pscx3\Pscx\Apps\EchoArgs.exe" "foo '' bar "baz"" "foo '' bar "baz"" + # + # $x = "abc'123'nospace`"lulz`"!!!" + # & EchoArgs @($x, $x) + # Command line: + # "C:\Program Files (x86)\PowerShell Community Extensions\Pscx3\Pscx\Apps\EchoArgs.exe" abc'123'nospace"lulz"!!! abc'123'nospace"lulz"!!! + # + # $x = "`"`"Look ma! Tonnes of spaces! 'foo' 'bar'`"`"" + # & EchoArgs @($x, $x) + # Command line: + # "C:\Program Files (x86)\PowerShell Community Extensions\Pscx3\Pscx\Apps\EchoArgs.exe" ""Look ma! Tonnes of spaces! 'foo' 'bar'"" ""Look ma! Tonnes of spaces! 'foo' 'bar'"" + # + # Given all this, we can now device a strategy to work around all these immensely helpful, well + # documented and useful tools by looking at each incoming argument, escaping any ' characters + # with a '"'"' sequence, surrounding each argument with ' & joining them with a space separating + # them. + # There is another bug (https://bugs.ruby-lang.org/issues/11142) that causes ruby to mangle any + # "" two-character double quote sequence but since we always emit our strings inside ' except for + # ' characters, this should be ok. Just remember that an argument '' should get translated to + # ''"'"''"'"'' on the command line. If those intervening empty ''s are not present, the presence + # of "" will cause ruby to mangle that argument. + $transformedList = $argList | foreach { "'" + ( $_ -replace "'","'`"'`"'" ) + "'" } + $fortifiedArgString = $transformedList -join ' ' + + # Use the correct embedded ruby path. We'll be deployed at a path that looks like + # [C:\opscode or some other prefix]\chef\modules\chef + $ruby = Join-Path (Get-ScriptDirectory) "..\..\embedded\bin\ruby.exe" + $commandPath = Join-Path (Get-ScriptDirectory) "..\..\bin\$command" + + Run-ExecutableAndWait $ruby """$ruby"" '$commandPath' $fortifiedArgString" +} + + +function chef-apply { + Run-RubyCommand 'chef-apply' $args +} + +function chef-client { + Run-RubyCommand 'chef-client' $args +} + +function chef-service-manager { + Run-RubyCommand 'chef-service-manager' $args +} + +function chef-shell { + Run-RubyCommand 'chef-shell' $args +} + +function chef-solo { + Run-RubyCommand 'chef-solo' $args +} + +function chef-windows-service { + Run-RubyCommand 'chef-windows-service' $args +} + +function knife { + Run-RubyCommand 'knife' $args +} + +Export-ModuleMember -function chef-apply +Export-ModuleMember -function chef-client +Export-ModuleMember -function chef-service-manager +Export-ModuleMember -function chef-shell +Export-ModuleMember -function chef-solo +Export-ModuleMember -function chef-windows-service +Export-ModuleMember -function knife + +# To debug this module, uncomment the line below and then run the following. +# Export-ModuleMember -function Run-RubyCommand +# Remove-Module chef +# Import-Module chef +# "puts ARGV" | Out-File C:\opscode\chef\bin\puts_args +# Run-RubyCommand puts_args 'Here' "are" some '"very interesting"' 'arguments[to]' "`"try out`"" diff --git a/ext/win32-eventlog/chef-log.man b/ext/win32-eventlog/chef-log.man index 4b4a022d7f..10c28e739f 100644 --- a/ext/win32-eventlog/chef-log.man +++ b/ext/win32-eventlog/chef-log.man @@ -24,3 +24,33 @@ Exception type: %3%n Exception message: %4%n Exception backtrace: %5%n . + +MessageId=10100 +SymbolicName=INFO +Language=English +[INFO] %1 +. + +MessageId=10101 +SymbolicName=WARN +Language=English +[WARN] %1 +. + +MessageId=10102 +SymbolicName=DEBUG +Language=English +[DEBUG] %1 +. + +MessageId=10103 +SymbolicName=ERROR +Language=English +[ERROR] %1 +. + +MessageId=10104 +SymbolicName=FATAL +Language=English +[FATAL] %1 +. diff --git a/external_tests/chef-rewind.gemfile b/external_tests/chef-rewind.gemfile new file mode 100644 index 0000000000..39f7d6e0e8 --- /dev/null +++ b/external_tests/chef-rewind.gemfile @@ -0,0 +1,5 @@ +source 'https://rubygems.org' + +gemspec(name: 'chef', path: "../") + +gem 'chef-rewind', github: 'thommay/chef-rewind' diff --git a/external_tests/chef-sugar.gemfile b/external_tests/chef-sugar.gemfile new file mode 100644 index 0000000000..31ef3bb5b8 --- /dev/null +++ b/external_tests/chef-sugar.gemfile @@ -0,0 +1,6 @@ +source 'https://rubygems.org' + +gemspec(name: 'chef', path: "../") + +gem 'chef-sugar', github: 'sethvargo/chef-sugar' +gem 'chefspec' diff --git a/external_tests/chefspec.gemfile b/external_tests/chefspec.gemfile new file mode 100644 index 0000000000..fb7878afbd --- /dev/null +++ b/external_tests/chefspec.gemfile @@ -0,0 +1,7 @@ +source 'https://rubygems.org' + +gemspec(name: 'chef', path: "../") + +gem 'chefspec', github: 'sethvargo/chefspec', group: :development +gem 'aruba' +gem 'yard' diff --git a/external_tests/foodcritic.gemfile b/external_tests/foodcritic.gemfile new file mode 100644 index 0000000000..a2b71a0d8c --- /dev/null +++ b/external_tests/foodcritic.gemfile @@ -0,0 +1,9 @@ +source 'https://rubygems.org' + +gemspec(name: 'chef', path: "../") + +gem 'foodcritic', github: 'acrmp/foodcritic' +gem 'cucumber' +gem 'rubocop' +gem 'simplecov' +gem 'minitest' diff --git a/external_tests/halite.gemfile b/external_tests/halite.gemfile new file mode 100644 index 0000000000..cd8cd05668 --- /dev/null +++ b/external_tests/halite.gemfile @@ -0,0 +1,8 @@ +source 'https://rubygems.org' + +gemspec(name: 'chef', path: "../") + +gem 'poise', github: 'poise/poise' +gem 'halite', github: 'poise/halite' +gem 'poise-boiler', github: 'poise/poise-boiler' +gem 'rspec-command', github: 'coderanger/rspec-command' diff --git a/external_tests/poise.gemfile b/external_tests/poise.gemfile new file mode 100644 index 0000000000..7d274b7a29 --- /dev/null +++ b/external_tests/poise.gemfile @@ -0,0 +1,7 @@ +source 'https://rubygems.org' + +gemspec(name: 'chef', path: "../") + +gem 'poise', github: 'poise/poise' +gem 'halite', github: 'poise/halite' +gem 'poise-boiler', github: 'poise/poise-boiler' diff --git a/kitchen-tests/.kitchen.travis.yml b/kitchen-tests/.kitchen.travis.yml index 15795e033a..2c3de60108 100644 --- a/kitchen-tests/.kitchen.travis.yml +++ b/kitchen-tests/.kitchen.travis.yml @@ -1,35 +1,38 @@ --- -driver_config: +driver: + name: ec2 aws_access_key_id: <%= ENV['AWS_ACCESS_KEY_ID'] %> aws_secret_access_key: <%= ENV['AWS_SECRET_ACCESS_KEY'] %> aws_ssh_key_id: <%= ENV['AWS_KEYPAIR_NAME'] %> - iam_profile_name: <%= ENV['IAM_PROFILE_NAME'] %> + region: "us-west-2" + availability_zone: "us-west-2a" + security_group_ids: ["travis-ci"] + instance_type: "m3.medium" provisioner: - name: chef_zero - github: <%= ENV['TRAVIS_REPO_SLUG'] %> - branch: <%= ENV['TRAVIS_COMMIT'] %> - require_chef_omnibus: 12.0.0-rc.2 + name: chef_github + github_owner: "chef" + github_repo: "chef" + refname: <%= ENV['TRAVIS_COMMIT'] %> + github_access_token: <%= ENV['KITCHEN_GITHUB_TOKEN'] %> data_path: test/fixtures # disable file provider diffs so we don't overflow travis' line limit client_rb: diff_disabled: true +transport: + ssh_key: <%= ENV['EC2_SSH_KEY_PATH'] %> + platforms: - name: ubuntu-12.04 - driver_plugin: ec2 - driver_config: - region: "us-west-2" - availability_zone: "us-west-2a" - ssh_key: <%= ENV['EC2_SSH_KEY_PATH'] %> - security_group_ids: ["travis-ci"] - - name: centos-6.4 - driver_plugin: ec2 - driver_config: - region: "us-west-2" - availability_zone: "us-west-2a" - ssh_key: <%= ENV['EC2_SSH_KEY_PATH'] %> - security_group_ids: ["travis-ci"] + driver: + # http://cloud-images.ubuntu.com/locator/ec2/ + # 12.04 amd64 us-west-2 hvm:ssd + image_id: ami-f3635fc3 + - name: rhel-6 + driver: + # https://github.com/chef/releng-chef-repo/blob/master/script/ci#L93-L96 + image_id: ami-7df0bd4d suites: - name: webapp diff --git a/kitchen-tests/.kitchen.yml b/kitchen-tests/.kitchen.yml index 775bb59378..c853f51b8d 100644 --- a/kitchen-tests/.kitchen.yml +++ b/kitchen-tests/.kitchen.yml @@ -6,10 +6,10 @@ driver: memory: 2048 provisioner: - name: chef_zero - github: "opscode/chef" - branch: <%= %x(git rev-parse HEAD) %> - require_chef_omnibus: true + name: chef_github + github_owner: "chef" + github_repo: "chef" + refname: <%= %x(git rev-parse HEAD) %> data_path: test/fixtures client_rb: diff_disabled: true diff --git a/kitchen-tests/Gemfile b/kitchen-tests/Gemfile index 60df7ef388..988d876417 100644 --- a/kitchen-tests/Gemfile +++ b/kitchen-tests/Gemfile @@ -2,13 +2,8 @@ source "https://rubygems.org" group :end_to_end do gem 'berkshelf' - # Once merged into test-kitchen/test-kitchen:master we can remove - # this and replace it with - # gem 'test-kitchen', :github => 'test-kitchen/test-kitchen' - # until the next test-kitchen gem release. - gem 'test-kitchen', :github => 'mcquin/test-kitchen', - :branch => 'mcquin/install_chef_from_github' - - gem 'kitchen-vagrant' # Used for local testing - gem 'kitchen-ec2' # Used for remote (Travis) testing + gem 'test-kitchen', '~> 1.4.0' + gem 'kitchen-appbundle-updater', '~> 0.0.1' + gem "kitchen-vagrant", '~> 0.17.0' + gem 'kitchen-ec2', github: 'test-kitchen/kitchen-ec2' end diff --git a/lib/chef/api_client.rb b/lib/chef/api_client.rb index ce9ceb312c..ad31fb7d7b 100644 --- a/lib/chef/api_client.rb +++ b/lib/chef/api_client.rb @@ -1,7 +1,7 @@ # -# Author:: Adam Jacob (<adam@opscode.com>) -# Author:: Nuo Yan (<nuo@opscode.com>) -# Copyright:: Copyright (c) 2008 Opscode, Inc. +# Author:: Adam Jacob (<adam@chef.io>) +# Author:: Nuo Yan (<nuo@chef.io>) +# Copyright:: Copyright (c) 2008, 2015 Chef Software, Inc. # License:: Apache License, Version 2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -23,12 +23,18 @@ require 'chef/mixin/from_file' require 'chef/mash' require 'chef/json_compat' require 'chef/search/query' +require 'chef/exceptions' +require 'chef/mixin/api_version_request_handling' +require 'chef/server_api' class Chef class ApiClient include Chef::Mixin::FromFile include Chef::Mixin::ParamsValidate + include Chef::Mixin::ApiVersionRequestHandling + + SUPPORTED_API_VERSIONS = [0,1] # Create a new Chef::ApiClient object. def initialize @@ -37,6 +43,25 @@ class Chef @private_key = nil @admin = false @validator = false + @create_key = nil + end + + def chef_rest_v0 + @chef_rest_v0 ||= Chef::ServerAPI.new(Chef::Config[:chef_server_url], {:api_version => "0"}) + end + + def chef_rest_v1 + @chef_rest_v1 ||= Chef::ServerAPI.new(Chef::Config[:chef_server_url], {:api_version => "1"}) + end + + # will default to the current version (Chef::Authenticator::DEFAULT_SERVER_API_VERSION) + def http_api + @http_api ||= Chef::REST.new(Chef::Config[:chef_server_url]) + end + + # will default to the current version (Chef::Authenticator::DEFAULT_SERVER_API_VERSION) + def self.http_api + Chef::REST.new(Chef::Config[:chef_server_url]) end # Gets or sets the client name. @@ -88,7 +113,8 @@ class Chef ) end - # Gets or sets the private key. + # Private key. The server will return it as a string. + # Set to true under API V0 to have the server regenerate the default key. # # @params [Optional String] The string representation of the private key. # @return [String] The current value. @@ -96,7 +122,19 @@ class Chef set_or_return( :private_key, arg, - :kind_of => [String, FalseClass] + :kind_of => [String, TrueClass, FalseClass] + ) + end + + # Used to ask server to generate key pair under api V1 + # + # @params [Optional True/False] Should be true or false - default is false. + # @return [True/False] The current value + def create_key(arg=nil) + set_or_return( + :create_key, + arg, + :kind_of => [ TrueClass, FalseClass ] ) end @@ -107,13 +145,14 @@ class Chef def to_hash result = { "name" => @name, - "public_key" => @public_key, "validator" => @validator, "admin" => @admin, 'json_class' => self.class.name, "chef_type" => "client" } - result["private_key"] = @private_key if @private_key + result["private_key"] = @private_key unless @private_key.nil? + result["public_key"] = @public_key unless @public_key.nil? + result["create_key"] = @create_key unless @create_key.nil? result end @@ -127,10 +166,11 @@ class Chef def self.from_hash(o) client = Chef::ApiClient.new client.name(o["name"] || o["clientname"]) - client.private_key(o["private_key"]) if o.key?("private_key") - client.public_key(o["public_key"]) client.admin(o["admin"]) client.validator(o["validator"]) + client.private_key(o["private_key"]) if o.key?("private_key") + client.public_key(o["public_key"]) if o.key?("public_key") + client.create_key(o["create_key"]) if o.key?("create_key") client end @@ -142,10 +182,6 @@ class Chef from_hash(Chef::JSONCompat.parse(j)) end - def self.http_api - Chef::REST.new(Chef::Config[:chef_server_url]) - end - def self.reregister(name) api_client = load(name) api_client.reregister @@ -182,11 +218,11 @@ class Chef # Save this client via the REST API, returns a hash including the private key def save begin - http_api.put("clients/#{name}", { :name => self.name, :admin => self.admin, :validator => self.validator}) + update rescue Net::HTTPServerException => e # If that fails, go ahead and try and update it if e.response.code == "404" - http_api.post("clients", {:name => self.name, :admin => self.admin, :validator => self.validator }) + create else raise e end @@ -194,18 +230,95 @@ class Chef end def reregister - reregistered_self = http_api.put("clients/#{name}", { :name => name, :admin => admin, :validator => validator, :private_key => true }) + # Try API V0 and if it fails due to V0 not being supported, raise the proper error message. + # reregister only supported in API V0 or lesser. + reregistered_self = chef_rest_v0.put("clients/#{name}", { :name => name, :admin => admin, :validator => validator, :private_key => true }) if reregistered_self.respond_to?(:[]) private_key(reregistered_self["private_key"]) else private_key(reregistered_self.private_key) end self + rescue Net::HTTPServerException => e + # if there was a 406 related to versioning, give error explaining that + # only API version 0 is supported for reregister command + if e.response.code == "406" && e.response["x-ops-server-api-version"] + version_header = Chef::JSONCompat.from_json(e.response["x-ops-server-api-version"]) + min_version = version_header["min_version"] + max_version = version_header["max_version"] + error_msg = reregister_only_v0_supported_error_msg(max_version, min_version) + raise Chef::Exceptions::OnlyApiVersion0SupportedForAction.new(error_msg) + else + raise e + end + end + + # Updates the client via the REST API + def update + # NOTE: API V1 dropped support for updating client keys via update (aka PUT), + # but this code never supported key updating in the first place. Since + # it was never implemented, we will simply ignore that functionality + # as it is being deprecated. + # Delete this comment after V0 support is dropped. + payload = { :name => name } + payload[:validator] = validator unless validator.nil? + + # DEPRECATION + # This field is ignored in API V1, but left for backwards-compat, + # can remove after API V0 is no longer supported. + payload[:admin] = admin unless admin.nil? + + begin + new_client = chef_rest_v1.put("clients/#{name}", payload) + rescue Net::HTTPServerException => e + # rescue API V0 if 406 and the server supports V0 + supported_versions = server_client_api_version_intersection(e, SUPPORTED_API_VERSIONS) + raise e unless supported_versions && supported_versions.include?(0) + new_client = chef_rest_v0.put("clients/#{name}", payload) + end + + new_client end # Create the client via the REST API def create - http_api.post("clients", self) + payload = { + :name => name, + :validator => validator, + # this field is ignored in API V1, but left for backwards-compat, + # can remove after OSC 11 support is finished? + :admin => admin + } + begin + # try API V1 + raise Chef::Exceptions::InvalidClientAttribute, "You cannot set both public_key and create_key for create." if !create_key.nil? && !public_key.nil? + + payload[:public_key] = public_key unless public_key.nil? + payload[:create_key] = create_key unless create_key.nil? + + new_client = chef_rest_v1.post("clients", payload) + + # get the private_key out of the chef_key hash if it exists + if new_client['chef_key'] + if new_client['chef_key']['private_key'] + new_client['private_key'] = new_client['chef_key']['private_key'] + end + new_client['public_key'] = new_client['chef_key']['public_key'] + new_client.delete('chef_key') + end + + rescue Net::HTTPServerException => e + # rescue API V0 if 406 and the server supports V0 + supported_versions = server_client_api_version_intersection(e, SUPPORTED_API_VERSIONS) + raise e unless supported_versions && supported_versions.include?(0) + + # under API V0, a key pair will always be created unless public_key is + # passed on initial POST + payload[:public_key] = public_key unless public_key.nil? + + new_client = chef_rest_v0.post("clients", payload) + end + Chef::ApiClient.from_hash(self.to_hash.merge(new_client)) end # As a string @@ -213,14 +326,5 @@ class Chef "client[#{@name}]" end - def inspect - "Chef::ApiClient name:'#{name}' admin:'#{admin.inspect}' validator:'#{validator}' " + - "public_key:'#{public_key}' private_key:'#{private_key}'" - end - - def http_api - @http_api ||= Chef::REST.new(Chef::Config[:chef_server_url]) - end - end end diff --git a/lib/chef/application.rb b/lib/chef/application.rb index 297e46ef3c..0563822ede 100644 --- a/lib/chef/application.rb +++ b/lib/chef/application.rb @@ -93,7 +93,6 @@ class Chef if config[:config_file].nil? Chef::Log.warn("No config file found or specified on command line, using command line options.") elsif config_fetcher.config_missing? - pp config_missing: true Chef::Log.warn("*****************************************") Chef::Log.warn("Did not find config file: #{config[:config_file]}, using command line options.") Chef::Log.warn("*****************************************") diff --git a/lib/chef/application/client.rb b/lib/chef/application/client.rb index a5faee9d35..409680b553 100644 --- a/lib/chef/application/client.rb +++ b/lib/chef/application/client.rb @@ -279,6 +279,12 @@ class Chef::Application::Client < Chef::Application Chef::Config[:chef_server_url] = config[:chef_server_url] if config.has_key? :chef_server_url Chef::Config.local_mode = config[:local_mode] if config.has_key?(:local_mode) + + if Chef::Config.has_key?(:chef_repo_path) && Chef::Config.chef_repo_path.nil? + Chef::Config.delete(:chef_repo_path) + Chef::Log.warn "chef_repo_path was set in a config file but was empty. Assuming #{Chef::Config.chef_repo_path}" + end + if Chef::Config.local_mode && !Chef::Config.has_key?(:cookbook_path) && !Chef::Config.has_key?(:chef_repo_path) Chef::Config.chef_repo_path = Chef::Config.find_chef_repo_path(Dir.pwd) end @@ -320,12 +326,6 @@ class Chef::Application::Client < Chef::Application unless expected_modes.include?(mode) Chef::Application.fatal!(unrecognized_audit_mode(mode)) end - - unless mode == :disabled - # This should be removed when audit-mode is enabled by default/no longer - # an experimental feature. - Chef::Log.warn(audit_mode_experimental_message) - end end end @@ -448,7 +448,7 @@ class Chef::Application::Client < Chef::Application "\nEnable chef-client interval runs by setting `:client_fork = true` in your config file or adding `--fork` to your command line options." end - def audit_mode_settings_explaination + def audit_mode_settings_explanation "\n* To enable audit mode after converge, use command line option `--audit-mode enabled` or set `:audit_mode = :enabled` in your config file." + "\n* To disable audit mode, use command line option `--audit-mode disabled` or set `:audit_mode = :disabled` in your config file." + "\n* To only run audit mode, use command line option `--audit-mode audit-only` or set `:audit_mode = :audit_only` in your config file." + @@ -456,18 +456,7 @@ class Chef::Application::Client < Chef::Application end def unrecognized_audit_mode(mode) - "Unrecognized setting #{mode} for audit mode." + audit_mode_settings_explaination - end - - def audit_mode_experimental_message - msg = if Chef::Config[:audit_mode] == :audit_only - "Chef-client has been configured to skip converge and only audit." - else - "Chef-client has been configured to audit after it converges." - end - msg += " Audit mode is an experimental feature currently under development. API changes may occur. Use at your own risk." - msg += audit_mode_settings_explaination - return msg + "Unrecognized setting #{mode} for audit mode." + audit_mode_settings_explanation end def fetch_recipe_tarball(url, path) diff --git a/lib/chef/audit/audit_reporter.rb b/lib/chef/audit/audit_reporter.rb index a4f84ed7eb..d952d8a249 100644 --- a/lib/chef/audit/audit_reporter.rb +++ b/lib/chef/audit/audit_reporter.rb @@ -34,6 +34,7 @@ class Chef @rest_client = rest_client # Ruby 1.9.3 and above "enumerate their values in the order that the corresponding keys were inserted." @ordered_control_groups = Hash.new + @audit_phase_error = nil end def run_context @@ -46,7 +47,7 @@ class Chef @run_status = run_status end - def audit_phase_complete + def audit_phase_complete(audit_output) Chef::Log.debug("Audit Reporter completed successfully without errors.") ordered_control_groups.each do |name, control_group| audit_data.add_control_group(control_group) @@ -57,8 +58,9 @@ class Chef # that runs tests - normal errors are interpreted as EXAMPLE failures and captured. # We still want to send available audit information to the server so we process the # known control groups. - def audit_phase_failed(error) + def audit_phase_failed(error, audit_output) # The stacktrace information has already been logged elsewhere + @audit_phase_error = error Chef::Log.debug("Audit Reporter failed.") ordered_control_groups.each do |name, control_group| audit_data.add_control_group(control_group) @@ -70,7 +72,9 @@ class Chef end def run_failed(error) - post_auditing_data(error) + # Audit phase errors are captured when audit_phase_failed gets called. + # The error passed here isn't relevant to auditing, so we ignore it. + post_auditing_data end def control_group_started(name) @@ -98,7 +102,7 @@ class Chef private - def post_auditing_data(error = nil) + def post_auditing_data unless auditing_enabled? Chef::Log.debug("Audit Reports are disabled. Skipping sending reports.") return @@ -116,8 +120,10 @@ class Chef Chef::Log.debug("Sending audit report (run-id: #{audit_data.run_id})") run_data = audit_data.to_hash - if error - run_data[:error] = "#{error.class.to_s}: #{error.message}\n#{error.backtrace.join("\n")}" + if @audit_phase_error + error_info = "#{@audit_phase_error.class}: #{@audit_phase_error.message}" + error_info << "\n#{@audit_phase_error.backtrace.join("\n")}" if @audit_phase_error.backtrace + run_data[:error] = error_info end Chef::Log.debug "Audit Report:\n#{Chef::JSONCompat.to_json_pretty(run_data)}" @@ -163,7 +169,6 @@ class Chef def iso8601ify(time) time.utc.iso8601.to_s end - end end end diff --git a/lib/chef/audit/logger.rb b/lib/chef/audit/logger.rb new file mode 100644 index 0000000000..e46f54e582 --- /dev/null +++ b/lib/chef/audit/logger.rb @@ -0,0 +1,36 @@ +# +# 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 'stringio' + +class Chef + class Audit + class Logger + def self.puts(message="") + @buffer ||= StringIO.new + @buffer.puts(message) + + Chef::Log.info(message) + end + + def self.read_buffer + return "" if @buffer.nil? + @buffer.string + end + end + end +end diff --git a/lib/chef/audit/runner.rb b/lib/chef/audit/runner.rb index 13c2823dca..234d83ab8f 100644 --- a/lib/chef/audit/runner.rb +++ b/lib/chef/audit/runner.rb @@ -16,6 +16,8 @@ # limitations under the License. # +require 'chef/audit/logger' + class Chef class Audit class Runner @@ -115,8 +117,8 @@ class Chef # the output stream to be changed for a formatter once the formatter has # been added. def set_streams - RSpec.configuration.output_stream = Chef::Config[:log_location] - RSpec.configuration.error_stream = Chef::Config[:log_location] + RSpec.configuration.output_stream = Chef::Audit::Logger + RSpec.configuration.error_stream = Chef::Audit::Logger end # Add formatters which we use to @@ -144,6 +146,7 @@ class Chef def configure_specinfra if Chef::Platform.windows? Specinfra.configuration.backend = :cmd + Specinfra.configuration.os = { :family => 'windows' } else Specinfra.configuration.backend = :exec end diff --git a/lib/chef/chef_class.rb b/lib/chef/chef_class.rb index 60dd5efef4..c490615861 100644 --- a/lib/chef/chef_class.rb +++ b/lib/chef/chef_class.rb @@ -26,6 +26,9 @@ # injected" into this class by other objects and do not reference the class symbols in those files # directly and we do not need to require those files here. +require 'chef/platform/provider_priority_map' +require 'chef/platform/resource_priority_map' + class Chef class << self @@ -33,17 +36,21 @@ class Chef # Public API # + # # Get the node object # # @return [Chef::Node] node object of the chef-client run + # attr_reader :node + # # Get the run context # # @return [Chef::RunContext] run_context of the chef-client run + # attr_reader :run_context - # Adds an event handler with user defined block + # Register an event handler with user specified block def event_handler(&block) dsl = Chef::EventDispatch::DSL.new dsl.instance_eval(&block) @@ -52,37 +59,56 @@ class Chef # Get the array of providers associated with a resource_name for the current node # # @param resource_name [Symbol] name of the resource as a symbol + # # @return [Array<Class>] Priority Array of Provider Classes to use for the resource_name on the node + # def get_provider_priority_array(resource_name) - @provider_priority_map.get_priority_array(node, resource_name).dup + result = provider_priority_map.get_priority_array(node, resource_name) + result = result.dup if result + result end + # # Get the array of resources associated with a resource_name for the current node # # @param resource_name [Symbol] name of the resource as a symbol + # # @return [Array<Class>] Priority Array of Resource Classes to use for the resource_name on the node + # def get_resource_priority_array(resource_name) - @resource_priority_map.get_priority_array(node, resource_name).dup + result = resource_priority_map.get_priority_array(node, resource_name) + result = result.dup if result + result end + # # Set the array of providers associated with a resource_name for the current node # # @param resource_name [Symbol] name of the resource as a symbol - # @param priority_array [Array<Class>] Array of Classes to set as the priority for resource_name on the node + # @param priority_array [Class, Array<Class>] Class or Array of Classes to set as the priority for resource_name on the node # @param filter [Hash] Chef::Nodearray-style filter + # # @return [Array<Class>] Modified Priority Array of Provider Classes to use for the resource_name on the node - def set_provider_priority_array(resource_name, priority_array, *filter) - @provider_priority_map.set_priority_array(resource_name, priority_array, *filter).dup + # + def set_provider_priority_array(resource_name, priority_array, *filter, &block) + result = provider_priority_map.set_priority_array(resource_name, priority_array, *filter, &block) + result = result.dup if result + result end + # # Get the array of resources associated with a resource_name for the current node # # @param resource_name [Symbol] name of the resource as a symbol - # @param priority_array [Array<Class>] Array of Classes to set as the priority for resource_name on the node + # @param priority_array [Class, Array<Class>] Class or Array of Classes to set as the priority for resource_name on the node # @param filter [Hash] Chef::Nodearray-style filter + # # @return [Array<Class>] Modified Priority Array of Resource Classes to use for the resource_name on the node - def set_resource_priority_array(resource_name, priority_array, *filter) - @resource_priority_map.set_priority_array(resource_name, priority_array, *filter).dup + # + def set_resource_priority_array(resource_name, priority_array, *filter, &block) + result = resource_priority_map.set_priority_array(resource_name, priority_array, *filter, &block) + result = result.dup if result + result end # @@ -91,22 +117,27 @@ class Chef # *NOT* for public consumption ] # + # # Sets the resource_priority_map # - # @api private # @param resource_priority_map [Chef::Platform::ResourcePriorityMap] + # + # @api private def set_resource_priority_map(resource_priority_map) @resource_priority_map = resource_priority_map end + # # Sets the provider_priority_map # - # @api private # @param provider_priority_map [Chef::Platform::providerPriorityMap] + # + # @api private def set_provider_priority_map(provider_priority_map) @provider_priority_map = provider_priority_map end + # # Sets the node object # # @api private @@ -115,14 +146,17 @@ class Chef @node = node end + # # Sets the run_context object # - # @api private # @param run_context [Chef::RunContext] + # + # @api private def set_run_context(run_context) @run_context = run_context end + # # Resets the internal state # # @api private @@ -132,5 +166,21 @@ class Chef @provider_priority_map = nil @resource_priority_map = nil end + + # @api private + def provider_priority_map + @provider_priority_map ||= begin + # these slurp in the resource+provider world, so be exceedingly lazy about requiring them + Chef::Platform::ProviderPriorityMap.instance + end + end + # @api private + def resource_priority_map + @resource_priority_map ||= begin + Chef::Platform::ResourcePriorityMap.instance + end + end end + + reset! end diff --git a/lib/chef/client.rb b/lib/chef/client.rb index d04a3dbbd5..86e92585e3 100644 --- a/lib/chef/client.rb +++ b/lib/chef/client.rb @@ -50,6 +50,7 @@ require 'chef/run_lock' require 'chef/policy_builder' require 'chef/request_id' require 'chef/platform/rebooter' +require 'chef/mixin/deprecation' require 'ohai' require 'rbconfig' @@ -60,121 +61,273 @@ 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 + extend Chef::Mixin::Deprecation - # Clears all notifications for client run status events. - # Primarily for testing purposes. - def self.clear_notifications - @run_start_notifications = nil - @run_completed_successfully_notifications = nil - @run_failed_notifications = nil - end - - # The list of notifications to be run when the client run starts. - def self.run_start_notifications - @run_start_notifications ||= [] - end - - # The list of notifications to be run when the client run completes - # successfully. - def self.run_completed_successfully_notifications - @run_completed_successfully_notifications ||= [] - end - - # The list of notifications to be run when the client run fails. - def self.run_failed_notifications - @run_failed_notifications ||= [] - end - - # Add a notification for the 'client run started' event. The notification - # is provided as a block. The current Chef::RunStatus object will be passed - # to the notification_block when the event is triggered. - def self.when_run_starts(¬ification_block) - run_start_notifications << notification_block - end - - # Add a notification for the 'client run success' event. The notification - # is provided as a block. The current Chef::RunStatus object will be passed - # to the notification_block when the event is triggered. - def self.when_run_completes_successfully(¬ification_block) - run_completed_successfully_notifications << notification_block - end + # + # The status of the Chef run. + # + # @return [Chef::RunStatus] + # + attr_reader :run_status - # Add a notification for the 'client run failed' event. The notification - # is provided as a block. The current Chef::RunStatus is passed to the - # notification_block when the event is triggered. - def self.when_run_fails(¬ification_block) - run_failed_notifications << notification_block + # + # The node represented by this client. + # + # @return [Chef::Node] + # + def node + run_status.node end - - # Callback to fire notifications that the Chef run is starting - def run_started - self.class.run_start_notifications.each do |notification| - notification.call(run_status) - end - @events.run_started(run_status) + def node=(value) + run_status.node = value end - # Callback to fire notifications that the run completed successfully - def run_completed_successfully - success_handlers = self.class.run_completed_successfully_notifications - success_handlers.each do |notification| - notification.call(run_status) - end - end + # + # The ohai system used by this client. + # + # @return [Ohai::System] + # + attr_reader :ohai - # Callback to fire notifications that the Chef run failed - def run_failed - failure_handlers = self.class.run_failed_notifications - failure_handlers.each do |notification| - notification.call(run_status) - end - end + # + # The rest object used to communicate with the Chef server. + # + # @return [Chef::REST] + # + attr_reader :rest - attr_accessor :node - attr_accessor :ohai - attr_accessor :rest + # + # The runner used to converge. + # + # @return [Chef::Runner] + # attr_accessor :runner + # + # Extra node attributes that were applied to the node. + # + # @return [Hash] + # attr_reader :json_attribs - attr_reader :run_status + + # + # The event dispatcher for the Chef run, including any configured output + # formatters and event loggers. + # + # @return [EventDispatch::Dispatcher] + # + # @see Chef::Formatters + # @see Chef::Config#formatters + # @see Chef::Config#stdout + # @see Chef::Config#stderr + # @see Chef::Config#force_logger + # @see Chef::Config#force_formatter + # TODO add stdout, stderr, and default formatters to Chef::Config so the + # defaults aren't calculated here. Remove force_logger and force_formatter + # from this code. + # @see Chef::EventLoggers + # @see Chef::Config#disable_event_logger + # @see Chef::Config#event_loggers + # @see Chef::Config#event_handlers + # attr_reader :events + # # Creates a new Chef::Client. + # + # @param json_attribs [Hash] Node attributes to layer into the node when it is + # fetched. + # @param args [Hash] Options: + # @option args [Array<RunList::RunListItem>] :override_runlist A runlist to + # use instead of the node's embedded run list. + # @option args [Array<String>] :specific_recipes A list of recipe file paths + # to load after the run list has been loaded. + # def initialize(json_attribs=nil, args={}) @json_attribs = json_attribs || {} - @node = nil - @run_status = nil - @runner = nil @ohai = Ohai::System.new event_handlers = configure_formatters + configure_event_loggers event_handlers += Array(Chef::Config[:event_handlers]) @events = EventDispatch::Dispatcher.new(*event_handlers) + # TODO it seems like a bad idea to be deletin' other peoples' hashes. @override_runlist = args.delete(:override_runlist) @specific_recipes = args.delete(:specific_recipes) + @run_status = Chef::RunStatus.new(nil, events) if new_runlist = args.delete(:runlist) @json_attribs["run_list"] = new_runlist end + end - # these slurp in the resource+provider world, so be exceedingly lazy about requiring them - require 'chef/platform/provider_priority_map' unless defined? Chef::Platform::ProviderPriorityMap - require 'chef/platform/resource_priority_map' unless defined? Chef::Platform::ResourcePriorityMap + # + # Do a full run for this Chef::Client. + # + # Locks the run while doing its job. + # + # Fires run_start before doing anything and fires run_completed or + # run_failed when finished. Also notifies client listeners of run_started + # at the beginning of Compile, and run_completed_successfully or run_failed + # when all is complete. + # + # Phase 1: Setup + # -------------- + # Gets information about the system and the run we are doing. + # + # 1. Run ohai to collect system information. + # 2. Register / connect to the Chef server (unless in solo mode). + # 3. Retrieve the node (or create a new one). + # 4. Merge in json_attribs, Chef::Config.environment, and override_run_list. + # + # @see #run_ohai + # @see #load_node + # @see #build_node + # @see Chef::Config#lockfile + # @see Chef::RunLock#acquire + # + # Phase 2: Compile + # ---------------- + # Decides *what* we plan to converge by compiling recipes. + # + # 1. Sync required cookbooks to the local cache. + # 2. Load libraries from all cookbooks. + # 3. Load attributes from all cookbooks. + # 4. Load LWRPs from all cookbooks. + # 5. Load resource definitions from all cookbooks. + # 6. Load recipes in the run list. + # 7. Load recipes from the command line. + # + # @see #setup_run_context Syncs and compiles cookbooks. + # @see Chef::CookbookCompiler#compile + # + # Phase 3: Converge + # ----------------- + # Brings the system up to date. + # + # 1. Converge the resources built from recipes in Phase 2. + # 2. Save the node. + # 3. Reboot if we were asked to. + # + # @see #converge_and_save + # @see Chef::Runner + # + # Phase 4: Audit + # -------------- + # Runs 'control_group' audits in recipes. This entire section can be enabled or disabled with config. + # + # 1. 'control_group' DSL collects audits during Phase 2 + # 2. Audits are run using RSpec + # 3. Errors are collected and reported using the formatters + # + # @see #run_audits + # @see Chef::Audit::Runner#run + # + # @raise [Chef::Exceptions::RunFailedWrappingError] If converge or audit failed. + # + # @see Chef::Config#enforce_path_sanity + # @see Chef::Config#solo + # @see Chef::Config#audit_mode + # + # @return Always returns true. + # + def run + run_error = nil - Chef.set_provider_priority_map(Chef::Platform::ProviderPriorityMap.instance) - Chef.set_resource_priority_map(Chef::Platform::ResourcePriorityMap.instance) + runlock = RunLock.new(Chef::Config.lockfile) + # TODO feels like acquire should have its own block arg for this + runlock.acquire + # don't add code that may fail before entering this section to be sure to release lock + begin + runlock.save_pid + + 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 + + register unless Chef::Config[:solo] + + load_node + + build_node + + run_status.run_id = request_id + run_status.start_clock + Chef::Log.info("Starting Chef Run for #{node.name}") + run_started + + do_windows_admin_check + + run_context = setup_run_context + + if Chef::Config[:audit_mode] != :audit_only + converge_error = converge_and_save(run_context) + end + + if Chef::Config[:why_run] == true + # why_run should probably be renamed to why_converge + Chef::Log.debug("Not running controls in 'why_run' mode - this mode is used to see potential converge changes") + elsif Chef::Config[:audit_mode] != :disabled + audit_error = run_audits(run_context) + end + + # Raise converge_error so run_failed reporters/events are processed. + raise converge_error if converge_error + + run_status.stop_clock + Chef::Log.info("Chef Run complete in #{run_status.elapsed_time} seconds") + run_completed_successfully + events.run_completed(node) + + # rebooting has to be the last thing we do, no exceptions. + Chef::Platform::Rebooter.reboot_if_needed!(node) + rescue Exception => run_error + # CHEF-3336: Send the error first in case something goes wrong below and we don't know why + Chef::Log.debug("Re-raising exception: #{run_error.class} - #{run_error.message}\n#{run_error.backtrace.join("\n ")}") + # If we failed really early, we may not have a run_status yet. Too early for these to be of much use. + if run_status + run_status.stop_clock + run_status.exception = run_error + run_failed + end + events.run_failed(run_error) + ensure + Chef::RequestID.instance.reset_request_id + request_id = nil + @run_status = nil + run_context = nil + runlock.release + GC.start + end + + # Raise audit, converge, and other errors here so that we exit + # with the proper exit status code and everything gets raised + # as a RunFailedWrappingError + if run_error || converge_error || audit_error + error = if run_error == converge_error + Chef::Exceptions::RunFailedWrappingError.new(converge_error, audit_error) + else + Chef::Exceptions::RunFailedWrappingError.new(run_error, converge_error, audit_error) + end + error.fill_backtrace + Chef::Application.debug_stacktrace(error) + raise error + end + + true end + # + # Private API + # TODO make this stuff protected or private + # + + # @api private def configure_formatters formatters_for_run.map do |formatter_name, output_path| if output_path.nil? @@ -187,6 +340,7 @@ class Chef end end + # @api private def formatters_for_run if Chef::Config.formatters.empty? [default_formatter] @@ -195,6 +349,7 @@ class Chef end end + # @api private def default_formatter if (STDOUT.tty? && !Chef::Config[:force_logger]) || Chef::Config[:force_formatter] [:doc] @@ -203,6 +358,7 @@ class Chef end end + # @api private def configure_event_loggers if Chef::Config.disable_event_logger [] @@ -219,8 +375,9 @@ class Chef end end - # Resource repoters send event information back to the chef server for processing. - # Can only be called after we have a @rest object + # Resource reporters send event information back to the chef server for + # processing. Can only be called after we have a @rest object + # @api private def register_reporters [ Chef::ResourceReporter.new(rest), @@ -230,43 +387,123 @@ class Chef end end + # + # Callback to fire notifications that the Chef run is starting + # + # @api private + # + def run_started + self.class.run_start_notifications.each do |notification| + notification.call(run_status) + end + events.run_started(run_status) + end + + # + # Callback to fire notifications that the run completed successfully + # + # @api private + # + def run_completed_successfully + success_handlers = self.class.run_completed_successfully_notifications + success_handlers.each do |notification| + notification.call(run_status) + end + end + + # + # Callback to fire notifications that the Chef run failed + # + # @api private + # + def run_failed + failure_handlers = self.class.run_failed_notifications + failure_handlers.each do |notification| + notification.call(run_status) + end + end + + # # Instantiates a Chef::Node object, possibly loading the node's prior state - # when using chef-client. Delegates to policy_builder. Injects the built node - # into the Chef class. + # when using chef-client. Sets Chef.node to the new node. # # @return [Chef::Node] The node object for this Chef run + # + # @see Chef::PolicyBuilder#load_node + # + # @api private + # def load_node policy_builder.load_node - @node = policy_builder.node - Chef.set_node(@node) + run_status.node = policy_builder.node + Chef.set_node(policy_builder.node) node end - # Mutates the `node` object to prepare it for the chef run. Delegates to - # policy_builder + # + # Mutates the `node` object to prepare it for the chef run. # # @return [Chef::Node] The updated node object + # + # @see Chef::PolicyBuilder#build_node + # + # @api private + # def build_node policy_builder.build_node - @run_status = Chef::RunStatus.new(node, events) + run_status.node = node node end + # + # Sync cookbooks to local cache. + # + # TODO this appears to be unused. + # + # @see Chef::PolicyBuilder#sync_cookbooks + # + # @api private + # + def sync_cookbooks + policy_builder.sync_cookbooks + end + + # + # Sets up the run context. + # + # @see Chef::PolicyBuilder#setup_run_context + # + # @return The newly set up run context + # + # @api private def setup_run_context - run_context = policy_builder.setup_run_context(@specific_recipes) + run_context = policy_builder.setup_run_context(specific_recipes) assert_cookbook_path_not_empty(run_context) run_status.run_context = run_context run_context end - def sync_cookbooks - policy_builder.sync_cookbooks - end - + # + # The PolicyBuilder strategy for figuring out run list and cookbooks. + # + # @return [Chef::PolicyBuilder::Policyfile, Chef::PolicyBuilder::ExpandNodeObject] + # + # @api private + # def policy_builder - @policy_builder ||= Chef::PolicyBuilder.strategy.new(node_name, ohai.data, json_attribs, @override_runlist, events) + @policy_builder ||= Chef::PolicyBuilder.strategy.new(node_name, ohai.data, json_attribs, override_runlist, events) end + # + # Save the updated node to Chef. + # + # Does not save if we are in solo mode or using override_runlist. + # + # @see Chef::Node#save + # @see Chef::Config#solo + # + # @api private + # def save_updated_node if Chef::Config[:solo] # nothing to do @@ -274,16 +511,46 @@ class Chef Chef::Log.warn("Skipping final node save because override_runlist was given") else Chef::Log.debug("Saving the current state of node #{node_name}") - @node.save + node.save end end + # + # Run ohai plugins. Runs all ohai plugins unless minimal_ohai is specified. + # + # Sends the ohai_completed event when finished. + # + # @see Chef::EventDispatcher# + # @see Chef::Config#minimal_ohai + # + # @api private + # def run_ohai filter = Chef::Config[:minimal_ohai] ? %w[fqdn machinename hostname platform platform_version os os_version] : nil ohai.all_plugins(filter) - @events.ohai_completed(node) + events.ohai_completed(node) end + # + # Figure out the node name we are working with. + # + # It tries these, in order: + # - Chef::Config.node_name + # - ohai[:fqdn] + # - ohai[:machinename] + # - ohai[:hostname] + # + # If we are running against a server with authentication protocol < 1.0, we + # *require* authentication protocol version 1.1. + # + # @raise [Chef::Exceptions::CannotDetermineNodeName] If the node name is not + # set and cannot be determined via ohai. + # + # @see Chef::Config#node_name + # @see Chef::Config#authentication_protocol_version + # + # @api private + # def node_name name = Chef::Config[:node_name] || ohai[:fqdn] || ohai[:machinename] || ohai[:hostname] Chef::Config[:node_name] = name @@ -292,6 +559,8 @@ class Chef # node names > 90 bytes only work with authentication protocol >= 1.1 # see discussion in config.rb. + # TODO use a computed default in Chef::Config to determine this instead of + # setting it. if name.bytesize > 90 Chef::Config[:authentication_protocol_version] = "1.1" end @@ -300,46 +569,86 @@ class Chef end # - # === Returns - # rest<Chef::REST>:: returns Chef::REST connection object + # Determine our private key and set up the connection to the Chef server. + # + # Skips registration and fires the `skipping_registration` event if + # Chef::Config.client_key is unspecified or already exists. + # + # If Chef::Config.client_key does not exist, we register the client with the + # Chef server and fire the registration_start and registration_completed events. + # + # @return [Chef::REST] The server connection object. + # + # @see Chef::Config#chef_server_url + # @see Chef::Config#client_key + # @see Chef::ApiClient::Registration#run + # @see Chef::EventDispatcher#skipping_registration + # @see Chef::EventDispatcher#registration_start + # @see Chef::EventDispatcher#registration_completed + # @see Chef::EventDispatcher#registration_failed + # + # @api private + # def register(client_name=node_name, config=Chef::Config) if !config[:client_key] - @events.skipping_registration(client_name, config) + events.skipping_registration(client_name, config) Chef::Log.debug("Client key is unspecified - skipping registration") elsif File.exists?(config[:client_key]) - @events.skipping_registration(client_name, config) + events.skipping_registration(client_name, config) Chef::Log.debug("Client key #{config[:client_key]} is present - skipping registration") else - @events.registration_start(node_name, config) + events.registration_start(node_name, config) Chef::Log.info("Client key #{config[:client_key]} is not present - registering") Chef::ApiClient::Registration.new(node_name, config[:client_key]).run - @events.registration_completed + events.registration_completed end # We now have the client key, and should use it from now on. @rest = Chef::REST.new(config[:chef_server_url], client_name, config[:client_key]) register_reporters rescue Exception => e + # TODO this should probably only ever fire if we *started* registration. + # Move it to the block above. # TODO: munge exception so a semantic failure message can be given to the # user - @events.registration_failed(client_name, e, config) + events.registration_failed(client_name, e, config) raise end - # Converges the node. # - # === Returns - # The thrown exception, if there was one. If this returns nil the converge was successful. + # Converges all compiled resources. + # + # Fires the converge_start, converge_complete and converge_failed events. + # + # If the exception `:end_client_run_early` is thrown during convergence, it + # does not mark the run complete *or* failed, and returns `nil` + # + # @param run_context The run context. + # + # @return The thrown exception, if we are in audit mode. `nil` means the + # converge was successful or ended early. + # + # @raise Any converge exception, unless we are in audit mode, in which case + # we *return* the exception. + # + # @see Chef::Runner#converge + # @see Chef::Config#audit_mode + # @see Chef::EventDispatch#converge_start + # @see Chef::EventDispatch#converge_complete + # @see Chef::EventDispatch#converge_failed + # + # @api private + # def converge(run_context) converge_exception = nil catch(:end_client_run_early) do begin - @events.converge_start(run_context) + events.converge_start(run_context) Chef::Log.debug("Converging node #{node_name}") @runner = Chef::Runner.new(run_context) - runner.converge - @events.converge_complete + @runner.converge + events.converge_complete rescue Exception => e - @events.converge_failed(e) + events.converge_failed(e) raise e if Chef::Config[:audit_mode] == :disabled converge_exception = e end @@ -347,8 +656,28 @@ class Chef converge_exception end + # + # Converge the node via and then save it if successful. + # + # @param run_context The run context. + # + # @return The thrown exception, if we are in audit mode. `nil` means the + # converge was successful or ended early. + # + # @raise Any converge or node save exception, unless we are in audit mode, + # in which case we *return* the exception. + # + # @see #converge + # @see #save_updated_mode + # @see Chef::Config#audit_mode + # + # @api private + # # We don't want to change the old API on the `converge` method to have it perform # saving. So we wrap it in this method. + # TODO given this seems to be pretty internal stuff, how badly do we need to + # split this stuff up? + # def converge_and_save(run_context) converge_exception = converge(run_context) unless converge_exception @@ -362,37 +691,67 @@ class Chef converge_exception end + # + # Run the audit phase. + # + # Triggers the audit_phase_start, audit_phase_complete and + # audit_phase_failed events. + # + # @param run_context The run context. + # + # @return Any thrown exceptions. `nil` if successful. + # + # @see Chef::Audit::Runner#run + # @see Chef::EventDispatch#audit_phase_start + # @see Chef::EventDispatch#audit_phase_complete + # @see Chef::EventDispatch#audit_phase_failed + # + # @api private + # def run_audits(run_context) - audit_exception = nil begin - @events.audit_phase_start(run_status) + events.audit_phase_start(run_status) Chef::Log.info("Starting audit phase") auditor = Chef::Audit::Runner.new(run_context) auditor.run if auditor.failed? - raise Chef::Exceptions::AuditsFailed.new(auditor.num_failed, auditor.num_total) + audit_exception = Chef::Exceptions::AuditsFailed.new(auditor.num_failed, auditor.num_total) + @events.audit_phase_failed(audit_exception, Chef::Audit::Logger.read_buffer) + else + @events.audit_phase_complete(Chef::Audit::Logger.read_buffer) end - @events.audit_phase_complete rescue Exception => e Chef::Log.error("Audit phase failed with error message: #{e.message}") - @events.audit_phase_failed(e) + @events.audit_phase_failed(e, Chef::Audit::Logger.read_buffer) audit_exception = e end audit_exception end - # Expands the run list. Delegates to the policy_builder. # - # Normally this does not need to be called from here, it will be called by - # build_node. This is provided so external users (like the chefspec - # project) can inject custom behavior into the run process. + # Expands the run list. + # + # @return [Chef::RunListExpansion] The expanded run list. + # + # @see Chef::PolicyBuilder#expand_run_list # - # === Returns - # RunListExpansion: A RunListExpansion or API compatible object. def expanded_run_list policy_builder.expand_run_list end + # + # Check if the user has Administrator privileges on windows. + # + # Throws an error if the user is not an admin, and + # `Chef::Config.fatal_windows_admin_check` is true. + # + # @raise [Chef::Exceptions::WindowsNotAdmin] If the user is not an admin. + # + # @see Chef::platform#windows? + # @see Chef::Config#fatal_windows_admin_check + # + # @api private + # def do_windows_admin_check if Chef::Platform.windows? Chef::Log.debug("Checking for administrator privileges....") @@ -412,99 +771,121 @@ class Chef end end - # Do a full run for this Chef::Client. Calls: - # - # * run_ohai - Collect information about the system - # * build_node - Get the last known state, merge with local changes - # * register - If not in solo mode, make sure the server knows about this client - # * sync_cookbooks - If not in solo mode, populate the local cache with the node's cookbooks - # * converge - Bring this system up to date - # - # === Returns - # true:: Always returns true. - def run - runlock = RunLock.new(Chef::Config.lockfile) - runlock.acquire - # don't add code that may fail before entering this section to be sure to release lock - begin - runlock.save_pid - - 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 - - register unless Chef::Config[:solo] - - load_node - - build_node + # Notification registration + class<<self + # + # Add a listener for the 'client run started' event. + # + # @param notification_block The callback (takes |run_status| parameter). + # @yieldparam [Chef::RunStatus] run_status The run status. + # + def when_run_starts(¬ification_block) + run_start_notifications << notification_block + end - run_status.run_id = request_id - run_status.start_clock - Chef::Log.info("Starting Chef Run for #{node.name}") - run_started + # + # Add a listener for the 'client run success' event. + # + # @param notification_block The callback (takes |run_status| parameter). + # @yieldparam [Chef::RunStatus] run_status The run status. + # + def when_run_completes_successfully(¬ification_block) + run_completed_successfully_notifications << notification_block + end - do_windows_admin_check + # + # Add a listener for the 'client run failed' event. + # + # @param notification_block The callback (takes |run_status| parameter). + # @yieldparam [Chef::RunStatus] run_status The run status. + # + def when_run_fails(¬ification_block) + run_failed_notifications << notification_block + end - run_context = setup_run_context + # + # Clears all listeners for client run status events. + # + # Primarily for testing purposes. + # + # @api private + # + def clear_notifications + @run_start_notifications = nil + @run_completed_successfully_notifications = nil + @run_failed_notifications = nil + end - if Chef::Config[:audit_mode] != :audit_only - converge_error = converge_and_save(run_context) - end + # + # TODO These seem protected to me. + # + + # + # Listeners to be run when the client run starts. + # + # @return [Array<Proc>] + # + # @api private + # + def run_start_notifications + @run_start_notifications ||= [] + end - if Chef::Config[:why_run] == true - # why_run should probably be renamed to why_converge - Chef::Log.debug("Not running controls in 'why_run' mode - this mode is used to see potential converge changes") - elsif Chef::Config[:audit_mode] != :disabled - audit_error = run_audits(run_context) - end + # + # Listeners to be run when the client run completes successfully. + # + # @return [Array<Proc>] + # + # @api private + # + def run_completed_successfully_notifications + @run_completed_successfully_notifications ||= [] + end - if converge_error || audit_error - e = Chef::Exceptions::RunFailedWrappingError.new(converge_error, audit_error) - e.fill_backtrace - raise e - end + # + # Listeners to be run when the client run fails. + # + # @return [Array<Proc>] + # + # @api private + # + def run_failed_notifications + @run_failed_notifications ||= [] + end + end - run_status.stop_clock - Chef::Log.info("Chef Run complete in #{run_status.elapsed_time} seconds") - run_completed_successfully - @events.run_completed(node) + # + # 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. + # + # @api private + # + STDOUT_FD = STDOUT - # rebooting has to be the last thing we do, no exceptions. - Chef::Platform::Rebooter.reboot_if_needed!(node) + # + # 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. + # + # @api private + # + STDERR_FD = STDERR - true + # + # Deprecated writers + # - rescue Exception => e - # CHEF-3336: Send the error first in case something goes wrong below and we don't know why - Chef::Log.debug("Re-raising exception: #{e.class} - #{e.message}\n#{e.backtrace.join("\n ")}") - # If we failed really early, we may not have a run_status yet. Too early for these to be of much use. - if run_status - run_status.stop_clock - run_status.exception = e - run_failed - end - Chef::Application.debug_stacktrace(e) - @events.run_failed(e) - raise - ensure - Chef::RequestID.instance.reset_request_id - request_id = nil - @run_status = nil - run_context = nil - runlock.release - GC.start - end - true - end + include Chef::Mixin::Deprecation + deprecated_attr_writer :ohai, "There is no alternative. Leave ohai alone!" + deprecated_attr_writer :rest, "There is no alternative. Leave rest alone!" + deprecated_attr :runner, "There is no alternative. Leave runner alone!" private + attr_reader :override_runlist + attr_reader :specific_recipes + def empty_directory?(path) !File.exists?(path) || (Dir.entries(path).size <= 2) end @@ -536,7 +917,6 @@ class Chef Chef::ReservedNames::Win32::Security.has_admin_privileges? end - end end diff --git a/lib/chef/config.rb b/lib/chef/config.rb index 25557b077f..9beb18b53e 100644 --- a/lib/chef/config.rb +++ b/lib/chef/config.rb @@ -20,727 +20,34 @@ # limitations under the License. require 'chef/log' -require 'chef/exceptions' -require 'mixlib/config' -require 'chef/util/selinux' -require 'chef/util/path_helper' -require 'pathname' -require 'chef/mixin/shell_out' +require 'chef-config/logger' -class Chef - class Config - - extend Mixlib::Config - extend Chef::Mixin::ShellOut - - PathHelper = Chef::Util::PathHelper - - # Evaluates the given string as config. - # - # +filename+ is used for context in stacktraces, but doesn't need to be the name of an actual file. - def self.from_string(string, filename) - self.instance_eval(string, filename, 1) - end - - # Manages the chef secret session key - # === Returns - # <newkey>:: A new or retrieved session key - # - def self.manage_secret_key - newkey = nil - if Chef::FileCache.has_key?("chef_server_cookie_id") - newkey = Chef::FileCache.load("chef_server_cookie_id") - else - chars = ("a".."z").to_a + ("A".."Z").to_a + ("0".."9").to_a - newkey = "" - 40.times { |i| newkey << chars[rand(chars.size-1)] } - Chef::FileCache.store("chef_server_cookie_id", newkey) - end - newkey - end - - def self.inspect - configuration.inspect - end - - def self.platform_specific_path(path) - path = PathHelper.cleanpath(path) - if Chef::Platform.windows? - # turns \etc\chef\client.rb and \var\chef\client.rb into C:/chef/client.rb - if env['SYSTEMDRIVE'] && path[0] == '\\' && path.split('\\')[2] == 'chef' - path = PathHelper.join(env['SYSTEMDRIVE'], path.split('\\', 3)[2]) - end - end - path - end - - def self.add_formatter(name, file_path=nil) - formatters << [name, file_path] - end - - def self.add_event_logger(logger) - event_handlers << logger - end - - # Config file to load (client.rb, knife.rb, etc. defaults set differently in knife, chef-client, etc.) - configurable(:config_file) - - default(:config_dir) do - if config_file - PathHelper.dirname(config_file) - else - PathHelper.join(user_home, ".chef", "") - end - end - - default :formatters, [] - - # Override the config dispatch to set the value of multiple server options simultaneously - # - # === Parameters - # url<String>:: String to be set for all of the chef-server-api URL's - # - configurable(:chef_server_url).writes_value { |url| url.to_s.strip } - - # When you are using ActiveSupport, they monkey-patch 'daemonize' into Kernel. - # So while this is basically identical to what method_missing would do, we pull - # it up here and get a real method written so that things get dispatched - # properly. - configurable(:daemonize).writes_value { |v| v } - - # The root where all local chef object data is stored. cookbooks, data bags, - # environments are all assumed to be in separate directories under this. - # chef-solo uses these directories for input data. knife commands - # that upload or download files (such as knife upload, knife role from file, - # etc.) work. - default :chef_repo_path do - if self.configuration[:cookbook_path] - if self.configuration[:cookbook_path].kind_of?(String) - File.expand_path('..', self.configuration[:cookbook_path]) - else - self.configuration[:cookbook_path].map do |path| - File.expand_path('..', path) - end - end - else - cache_path - end - end - - def self.find_chef_repo_path(cwd) - # In local mode, we auto-discover the repo root by looking for a path with "cookbooks" under it. - # This allows us to run config-free. - path = cwd - until File.directory?(PathHelper.join(path, "cookbooks")) - new_path = File.expand_path('..', path) - if new_path == path - Chef::Log.warn("No cookbooks directory found at or above current directory. Assuming #{Dir.pwd}.") - return Dir.pwd - end - path = new_path - end - Chef::Log.info("Auto-discovered chef repository at #{path}") - path - end - - def self.derive_path_from_chef_repo_path(child_path) - if chef_repo_path.kind_of?(String) - PathHelper.join(chef_repo_path, child_path) - else - chef_repo_path.map { |path| PathHelper.join(path, child_path)} - end - end - - # Location of acls on disk. String or array of strings. - # Defaults to <chef_repo_path>/acls. - # Only applies to Enterprise Chef commands. - default(:acl_path) { derive_path_from_chef_repo_path('acls') } - - # Location of clients on disk. String or array of strings. - # Defaults to <chef_repo_path>/acls. - default(:client_path) { derive_path_from_chef_repo_path('clients') } - - # Location of cookbooks on disk. String or array of strings. - # Defaults to <chef_repo_path>/cookbooks. If chef_repo_path - # is not specified, this is set to [/var/chef/cookbooks, /var/chef/site-cookbooks]). - default(:cookbook_path) do - if self.configuration[:chef_repo_path] - derive_path_from_chef_repo_path('cookbooks') - else - Array(derive_path_from_chef_repo_path('cookbooks')).flatten + - Array(derive_path_from_chef_repo_path('site-cookbooks')).flatten - end - end - - # Location of containers on disk. String or array of strings. - # Defaults to <chef_repo_path>/containers. - # Only applies to Enterprise Chef commands. - default(:container_path) { derive_path_from_chef_repo_path('containers') } - - # Location of data bags on disk. String or array of strings. - # Defaults to <chef_repo_path>/data_bags. - default(:data_bag_path) { derive_path_from_chef_repo_path('data_bags') } - - # Location of environments on disk. String or array of strings. - # Defaults to <chef_repo_path>/environments. - default(:environment_path) { derive_path_from_chef_repo_path('environments') } - - # Location of groups on disk. String or array of strings. - # Defaults to <chef_repo_path>/groups. - # Only applies to Enterprise Chef commands. - default(:group_path) { derive_path_from_chef_repo_path('groups') } - - # Location of nodes on disk. String or array of strings. - # Defaults to <chef_repo_path>/nodes. - default(:node_path) { derive_path_from_chef_repo_path('nodes') } - - # Location of roles on disk. String or array of strings. - # Defaults to <chef_repo_path>/roles. - default(:role_path) { derive_path_from_chef_repo_path('roles') } - - # Location of users on disk. String or array of strings. - # Defaults to <chef_repo_path>/users. - # Does not apply to Enterprise Chef commands. - default(:user_path) { derive_path_from_chef_repo_path('users') } - - # Location of policies on disk. String or array of strings. - # Defaults to <chef_repo_path>/policies. - default(:policy_path) { derive_path_from_chef_repo_path('policies') } - - # Turn on "path sanity" by default. See also: http://wiki.opscode.com/display/chef/User+Environment+PATH+Sanity - default :enforce_path_sanity, true - - # Formatted Chef Client output is a beta feature, disabled by default: - default :formatter, "null" - - # The number of times the client should retry when registering with the server - default :client_registration_retries, 5 - - # An array of paths to search for knife exec scripts if they aren't in the current directory - default :script_path, [] - - # The root of all caches (checksums, cache and backup). If local mode is on, - # this is under the user's home directory. - default(:cache_path) do - if local_mode - PathHelper.join(config_dir, 'local-mode-cache') - else - primary_cache_root = platform_specific_path("/var") - primary_cache_path = platform_specific_path("/var/chef") - # Use /var/chef as the cache path only if that folder exists and we can read and write - # into it, or /var exists and we can read and write into it (we'll create /var/chef later). - # Otherwise, we'll create .chef under the user's home directory and use that as - # the cache path. - unless path_accessible?(primary_cache_path) || path_accessible?(primary_cache_root) - secondary_cache_path = PathHelper.join(user_home, '.chef') - Chef::Log.info("Unable to access cache at #{primary_cache_path}. Switching cache to #{secondary_cache_path}") - secondary_cache_path - else - primary_cache_path - end - end - end - - # Returns true only if the path exists and is readable and writeable for the user. - def self.path_accessible?(path) - File.exists?(path) && File.readable?(path) && File.writable?(path) - end - - # Where cookbook files are stored on the server (by content checksum) - default(:checksum_path) { PathHelper.join(cache_path, "checksums") } - - # Where chef's cache files should be stored - default(:file_cache_path) { PathHelper.join(cache_path, "cache") } - - # Where backups of chef-managed files should go - default(:file_backup_path) { PathHelper.join(cache_path, "backup") } - - # The chef-client (or solo) lockfile. - # - # If your `file_cache_path` resides on a NFS (or non-flock()-supporting - # fs), it's recommended to set this to something like - # '/tmp/chef-client-running.pid' - default(:lockfile) { PathHelper.join(file_cache_path, "chef-client-running.pid") } - - ## Daemonization Settings ## - # What user should Chef run as? - default :user, nil - default :group, nil - default :umask, 0022 - - # Valid log_levels are: - # * :debug - # * :info - # * :warn - # * :fatal - # These work as you'd expect. There is also a special `:auto` setting. - # When set to :auto, Chef will auto adjust the log verbosity based on - # context. When a tty is available (usually because the user is running chef - # in a console), the log level is set to :warn, and output formatters are - # used as the primary mode of output. When a tty is not available, the - # logger is the primary mode of output, and the log level is set to :info - default :log_level, :auto - - # Logging location as either an IO stream or string representing log file path - default :log_location, STDOUT - - # Using `force_formatter` causes chef to default to formatter output when STDOUT is not a tty - default :force_formatter, false - - # Using `force_logger` causes chef to default to logger output when STDOUT is a tty - default :force_logger, false - - default :http_retry_count, 5 - default :http_retry_delay, 5 - default :interval, nil - default :once, nil - default :json_attribs, nil - # toggle info level log items that can create a lot of output - default :verbose_logging, true - default :node_name, nil - default :diff_disabled, false - default :diff_filesize_threshold, 10000000 - default :diff_output_threshold, 1000000 - default :local_mode, false - - default :pid_file, nil +# DI our logger into ChefConfig before we load the config. Some defaults are +# auto-detected, and this emits log messages on some systems, all of which will +# occur at require-time. So we need to set the logger first. +ChefConfig.logger = Chef::Log - # Whether Chef Zero local mode should bind to a port. All internal requests - # will go through the socketless code path regardless, so the socket is - # only needed if other processes will connect to the local mode server. - # - # For compatibility this is set to true but it will be changed to false in - # the future. - default :listen, true +require 'chef-config/config' - config_context :chef_zero do - config_strict_mode true - default(:enabled) { Chef::Config.local_mode } - default :host, 'localhost' - default :port, 8889.upto(9999) # Will try ports from 8889-9999 until one works - end - default :chef_server_url, "https://localhost:443" - - default :rest_timeout, 300 - default :yum_timeout, 900 - default :yum_lock_timeout, 30 - default :solo, false - default :splay, nil - default :why_run, false - default :color, false - default :client_fork, true - default :ez, false - default :enable_reporting, true - default :enable_reporting_url_fatals, false - # Possible values for :audit_mode - # :enabled, :disabled, :audit_only, - # - # TODO: 11 Dec 2014: Currently audit-mode is an experimental feature - # and is disabled by default. When users choose to enable audit-mode, - # a warning is issued in application/client#reconfigure. - # This can be removed when audit-mode is enabled by default. - default :audit_mode, :disabled - - # Chef only needs ohai to run the hostname plugin for the most basic - # functionality. If the rest of the ohai plugins are not needed (like in - # most of our testing scenarios) - default :minimal_ohai, false - - # Policyfile is an experimental feature where a node gets its run list and - # cookbook version set from a single document on the server instead of - # expanding the run list and having the server compute the cookbook version - # set based on environment constraints. - # - # Because this feature is experimental, it is not recommended for - # production use. Developent/release of this feature may not adhere to - # semver guidelines. - default :use_policyfile, false - - # Set these to enable SSL authentication / mutual-authentication - # with the server - - # Client side SSL cert/key for mutual auth - default :ssl_client_cert, nil - default :ssl_client_key, nil - - # Whether or not to verify the SSL cert for all HTTPS requests. When set to - # :verify_peer (default), all HTTPS requests will be validated regardless of other - # SSL verification settings. When set to :verify_none no HTTPS requests will - # be validated. - default :ssl_verify_mode, :verify_peer - - # Whether or not to verify the SSL cert for HTTPS requests to the Chef - # server API. If set to `true`, the server's cert will be validated - # regardless of the :ssl_verify_mode setting. This is set to `true` when - # running in local-mode. - # NOTE: This is a workaround until verify_peer is enabled by default. - default(:verify_api_cert) { Chef::Config.local_mode } - - # Path to the default CA bundle files. - default :ssl_ca_path, nil - default(:ssl_ca_file) do - if Chef::Platform.windows? and embedded_path = embedded_dir - cacert_path = File.join(embedded_path, "ssl/certs/cacert.pem") - cacert_path if File.exist?(cacert_path) - else - nil - end - end - - # A directory that contains additional SSL certificates to trust. Any - # certificates in this directory will be added to whatever CA bundle ruby - # is using. Use this to add self-signed certs for your Chef Server or local - # HTTP file servers. - default(:trusted_certs_dir) { PathHelper.join(config_dir, "trusted_certs") } - - # Where should chef-solo download recipes from? - default :recipe_url, nil - - # Sets the version of the signed header authentication protocol to use (see - # the 'mixlib-authorization' project for more detail). Currently, versions - # 1.0 and 1.1 are available; however, the chef-server must first be - # upgraded to support version 1.1 before clients can begin using it. - # - # Version 1.1 of the protocol is required when using a `node_name` greater - # than ~90 bytes (~90 ascii characters), so chef-client will automatically - # switch to using version 1.1 when `node_name` is too large for the 1.0 - # protocol. If you intend to use large node names, ensure that your server - # supports version 1.1. Automatic detection of large node names means that - # users will generally not need to manually configure this. - # - # In the future, this configuration option may be replaced with an - # automatic negotiation scheme. - default :authentication_protocol_version, "1.0" - - # This key will be used to sign requests to the Chef server. This location - # must be writable by Chef during initial setup when generating a client - # identity on the server. - # - # The chef-server will look up the public key for the client using the - # `node_name` of the client. - # - # If chef-zero is enabled, this defaults to nil (no authentication). - default(:client_key) { chef_zero.enabled ? nil : platform_specific_path("/etc/chef/client.pem") } - - # When registering the client, should we allow the client key location to - # be a symlink? eg: /etc/chef/client.pem -> /etc/chef/prod-client.pem - # If the path of the key goes through a directory like /tmp this should - # never be set to true or its possibly an easily exploitable security hole. - default :follow_client_key_symlink, false - - # This secret is used to decrypt encrypted data bag items. - default(:encrypted_data_bag_secret) do - if File.exist?(platform_specific_path("/etc/chef/encrypted_data_bag_secret")) - platform_specific_path("/etc/chef/encrypted_data_bag_secret") - else - nil - end - end - - # As of Chef 11.0, version "1" is the default encrypted data bag item - # format. Version "2" is available which adds encrypt-then-mac protection. - # To maintain compatibility, versions other than 1 must be opt-in. - # - # Set this to `2` if you have chef-client 11.6.0+ in your infrastructure. - # Set this to `3` if you have chef-client 11.?.0+, ruby 2 and OpenSSL >= 1.0.1 in your infrastructure. (TODO) - default :data_bag_encrypt_version, 1 - - # When reading data bag items, any supported version is accepted. However, - # if all encrypted data bags have been generated with the version 2 format, - # it is recommended to disable support for earlier formats to improve - # security. For example, the version 2 format is identical to version 1 - # except for the addition of an HMAC, so an attacker with MITM capability - # could downgrade an encrypted data bag to version 1 as part of an attack. - default :data_bag_decrypt_minimum_version, 0 - - # If there is no file in the location given by `client_key`, chef-client - # will temporarily use the "validator" identity to generate one. If the - # `client_key` is not present and the `validation_key` is also not present, - # chef-client will not be able to authenticate to the server. - # - # The `validation_key` is never used if the `client_key` exists. - # - # If chef-zero is enabled, this defaults to nil (no authentication). - default(:validation_key) { chef_zero.enabled ? nil : platform_specific_path("/etc/chef/validation.pem") } - default :validation_client_name, "chef-validator" +require 'chef/platform/query_helpers' - # When creating a new client via the validation_client account, Chef 11 - # servers allow the client to generate a key pair locally and send 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 `true`. Set to `false` to disable client-side key - # generation (server generates client keys). - default(:local_key_generation) { true } - - # 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. - # Leaving this set to nil or false is a security hazard! - default :zypper_check_gpg, nil - - # Report Handlers - default :report_handlers, [] +class Chef + Config = ChefConfig::Config - # Event Handlers - default :event_handlers, [] + # We re-open ChefConfig::Config to add additional settings. Generally, + # everything should go in chef-config so it's shared with whoever uses that. + # We make execeptions to that rule when: + # * The functionality isn't likely to be useful outside of Chef + # * The functionality makes use of a dependency we don't want to add to chef-config + class Config - default :disable_event_loggers, false default :event_loggers do evt_loggers = [] - if Chef::Platform::windows? and not Chef::Platform::windows_server_2003? + if ChefConfig.windows? and not Chef::Platform.windows_server_2003? evt_loggers << :win_evt end evt_loggers end - # Exception Handlers - default :exception_handlers, [] - - # Start handlers - default :start_handlers, [] - - # Syntax Check Cache. Knife keeps track of files that is has already syntax - # checked by storing files in this directory. `syntax_check_cache_path` is - # the new (and preferred) configuration setting. If not set, knife will - # fall back to using cache_options[:path], which is deprecated but exists in - # many client configs generated by pre-Chef-11 bootstrappers. - default(:syntax_check_cache_path) { cache_options[:path] } - - # Deprecated: - # Move this to the default value of syntax_cache_path when this is removed. - default(:cache_options) { { :path => PathHelper.join(config_dir, "syntaxcache") } } - - # Whether errors should be raised for deprecation warnings. When set to - # `false` (the default setting), a warning is emitted but code using - # deprecated methods/features/etc. should work normally otherwise. When set - # to `true`, usage of deprecated methods/features will raise a - # `DeprecatedFeatureError`. This is used by Chef's tests to ensure that - # deprecated functionality is not used internally by Chef. End users - # should generally leave this at the default setting (especially in - # production), but it may be useful when testing cookbooks or other code if - # the user wishes to aggressively address deprecations. - default(:treat_deprecation_warnings_as_errors) do - # Using an environment variable allows this setting to be inherited in - # tests that spawn new processes. - ENV.key?("CHEF_TREAT_DEPRECATION_WARNINGS_AS_ERRORS") - end - - # knife configuration data - config_context :knife do - default :ssh_port, nil - default :ssh_user, nil - default :ssh_attribute, nil - default :ssh_gateway, nil - default :bootstrap_version, nil - default :bootstrap_proxy, nil - default :bootstrap_template, nil - default :secret, nil - default :secret_file, nil - default :identity_file, nil - default :host_key_verify, nil - default :forward_agent, nil - default :sort_status_reverse, nil - default :hints, {} - end - - def self.set_defaults_for_windows - # Those lists of regular expressions define what chef considers a - # valid user and group name - # From http://technet.microsoft.com/en-us/library/cc776019(WS.10).aspx - principal_valid_regex_part = '[^"\/\\\\\[\]\:;|=,+*?<>]+' - default :user_valid_regex, [ /^(#{principal_valid_regex_part}\\)?#{principal_valid_regex_part}$/ ] - default :group_valid_regex, [ /^(#{principal_valid_regex_part}\\)?#{principal_valid_regex_part}$/ ] - - default :fatal_windows_admin_check, false - end - - def self.set_defaults_for_nix - # Those lists of regular expressions define what chef considers a - # valid user and group name - # - # user/group cannot start with '-', '+' or '~' - # user/group cannot contain ':', ',' or non-space-whitespace or null byte - # everything else is allowed (UTF-8, spaces, etc) and we delegate to your O/S useradd program to barf or not - # copies: http://anonscm.debian.org/viewvc/pkg-shadow/debian/trunk/debian/patches/506_relaxed_usernames?view=markup - default :user_valid_regex, [ /^[^-+~:,\t\r\n\f\0]+[^:,\t\r\n\f\0]*$/ ] - default :group_valid_regex, [ /^[^-+~:,\t\r\n\f\0]+[^:,\t\r\n\f\0]*$/ ] - end - - # Those lists of regular expressions define what chef considers a - # valid user and group name - if Chef::Platform.windows? - set_defaults_for_windows - else - set_defaults_for_nix - end - - # This provides a hook which rspec can stub so that we can avoid twiddling - # global state in tests. - def self.env - ENV - end - - def self.windows_home_path - Chef::Log.deprecation("Chef::Config.windows_home_path is now deprecated. Consider using Chef::Util::PathHelper.home instead.") - PathHelper.home - end - - # returns a platform specific path to the user home dir if set, otherwise default to current directory. - default( :user_home ) { PathHelper.home || Dir.pwd } - - # Enable file permission fixup for selinux. Fixup will be done - # only if selinux is enabled in the system. - default :enable_selinux_file_permission_fixup, true - - # Use atomic updates (i.e. move operation) while updating contents - # of the files resources. When set to false copy operation is - # used to update files. - default :file_atomic_update, true - - # There are 3 possible values for this configuration setting. - # true => file staging is done in the destination directory - # false => file staging is done via tempfiles under ENV['TMP'] - # :auto => file staging will try using destination directory if possible and - # will fall back to ENV['TMP'] if destination directory is not usable. - default :file_staging_uses_destdir, :auto - - # Exit if another run is in progress and the chef-client is unable to - # get the lock before time expires. If nil, no timeout is enforced. (Exits - # immediately if 0.) - default :run_lock_timeout, nil - - # Number of worker threads for syncing cookbooks in parallel. Increasing - # this number can result in gateway errors from the server (namely 503 and 504). - # If you are seeing this behavior while using the default setting, reducing - # the number of threads will help. - default :cookbook_sync_threads, 10 - - # At the beginning of the Chef Client run, the cookbook manifests are downloaded which - # contain URLs for every file in every relevant cookbook. Most of the files - # (recipes, resources, providers, libraries, etc) are immediately synchronized - # at the start of the run. The handling of "files" and "templates" directories, - # however, have two modes of operation. They can either all be downloaded immediately - # at the start of the run (no_lazy_load==true) or else they can be lazily loaded as - # cookbook_file or template resources are converged which require them (no_lazy_load==false). - # - # The advantage of lazily loading these files is that unnecessary files are not - # synchronized. This may be useful to users with large files checked into cookbooks which - # are only selectively downloaded to a subset of clients which use the cookbook. However, - # better solutions are to either isolate large files into individual cookbooks and only - # include those cookbooks in the run lists of the servers that need them -- or move to - # using remote_file and a more appropriate backing store like S3 for large file - # distribution. - # - # The disadvantages of lazily loading files are that users some time find it - # confusing that their cookbooks are not fully synchronzied to the cache initially, - # and more importantly the time-sensitive URLs which are in the manifest may time - # out on long Chef runs before the resource that uses the file is converged - # (leading to many confusing 403 errors on template/cookbook_file resources). - # - default :no_lazy_load, true - - # Default for the chef_gem compile_time attribute. Nil is the same as true but will emit - # warnings on every use of chef_gem prompting the user to be explicit. If the user sets this to - # true then the user will get backcompat behavior but with a single nag warning that cookbooks - # may break with this setting in the future. The false setting is the recommended setting and - # will become the default. - default :chef_gem_compile_time, nil - - # A whitelisted array of attributes you want sent over the wire when node - # data is saved. - # The default setting is nil, which collects all data. Setting to [] will not - # collect any data for save. - default :automatic_attribute_whitelist, nil - default :default_attribute_whitelist, nil - default :normal_attribute_whitelist, nil - default :override_attribute_whitelist, nil - - config_context :windows_service do - # Set `watchdog_timeout` to the number of seconds to wait for a chef-client run - # to finish - default :watchdog_timeout, 2 * (60 * 60) # 2 hours - end - - # Chef requires an English-language UTF-8 locale to function properly. We attempt - # to use the 'locale -a' command and search through a list of preferences until we - # find one that we can use. On Ubuntu systems we should find 'C.UTF-8' and be - # able to use that even if there is no English locale on the server, but Mac, Solaris, - # AIX, etc do not have that locale. We then try to find an English locale and fall - # back to 'C' if we do not. The choice of fallback is pick-your-poison. If we try - # to do the work to return a non-US UTF-8 locale then we fail inside of providers when - # things like 'svn info' return Japanese and we can't parse them. OTOH, if we pick 'C' then - # we will blow up on UTF-8 characters. Between the warn we throw and the Encoding - # exception that ruby will throw it is more obvious what is broken if we drop UTF-8 by - # default rather than drop English. - # - # If there is no 'locale -a' then we return 'en_US.UTF-8' since that is the most commonly - # available English UTF-8 locale. However, all modern POSIXen should support 'locale -a'. - def self.guess_internal_locale - # https://github.com/opscode/chef/issues/2181 - # Some systems have the `locale -a` command, but the result has - # invalid characters for the default encoding. - # - # For example, on CentOS 6 with ENV['LANG'] = "en_US.UTF-8", - # `locale -a`.split fails with ArgumentError invalid UTF-8 encoding. - locales = shell_out_with_systems_locale!("locale -a").stdout.split - case - when locales.include?('C.UTF-8') - 'C.UTF-8' - when locales.include?('en_US.UTF-8'), locales.include?('en_US.utf8') - 'en_US.UTF-8' - when locales.include?('en.UTF-8') - 'en.UTF-8' - else - # Will match en_ZZ.UTF-8, en_ZZ.utf-8, en_ZZ.UTF8, en_ZZ.utf8 - guesses = locales.select { |l| l =~ /^en_.*UTF-?8$/i } - unless guesses.empty? - guessed_locale = guesses.first - # Transform into the form en_ZZ.UTF-8 - guessed_locale.gsub(/UTF-?8$/i, "UTF-8") - else - Chef::Log.warn "Please install an English UTF-8 locale for Chef to use, falling back to C locale and disabling UTF-8 support." - 'C' - end - end - rescue - if Chef::Platform.windows? - Chef::Log.debug "Defaulting to locale en_US.UTF-8 on Windows, until it matters that we do something else." - else - Chef::Log.debug "No usable locale -a command found, assuming you have en_US.UTF-8 installed." - end - 'en_US.UTF-8' - end - - default :internal_locale, guess_internal_locale - - # Force UTF-8 Encoding, for when we fire up in the 'C' locale or other strange locales (e.g. - # japanese windows encodings). If we do not do this, then knife upload will fail when a cookbook's - # README.md has UTF-8 characters that do not encode in whatever surrounding encoding we have been - # passed. Effectively, the Chef Ecosystem is globally UTF-8 by default. Anyone who wants to be - # able to upload Shift_JIS or ISO-8859-1 files needs to mark *those* files explicitly with - # magic tags to make ruby correctly identify the encoding being used. Changing this default will - # break Chef community cookbooks and is very highly discouraged. - default :ruby_encoding, Encoding::UTF_8 - - # If installed via an omnibus installer, this gives the path to the - # "embedded" directory which contains all of the software packaged with - # omnibus. This is used to locate the cacert.pem file on windows. - def self.embedded_dir - Pathname.new(_this_file).ascend do |path| - if path.basename.to_s == "embedded" - return path.to_s - end - end - - nil - end - - # Path to this file in the current install. - def self._this_file - File.expand_path(__FILE__) - end end end diff --git a/lib/chef/cookbook/metadata.rb b/lib/chef/cookbook/metadata.rb index 781d3b40b0..01a98fda39 100644 --- a/lib/chef/cookbook/metadata.rb +++ b/lib/chef/cookbook/metadata.rb @@ -286,9 +286,13 @@ class Chef # === Returns # versions<Array>:: Returns the list of versions for the platform def depends(cookbook, *version_args) - version = new_args_format(:depends, cookbook, version_args) - constraint = validate_version_constraint(:depends, cookbook, version) - @dependencies[cookbook] = constraint.to_s + if cookbook == name + Chef::Log.warn "Ignoring self-dependency in cookbook #{name}, please remove it (in the future this will be fatal)." + else + version = new_args_format(:depends, cookbook, version_args) + constraint = validate_version_constraint(:depends, cookbook, version) + @dependencies[cookbook] = constraint.to_s + end @dependencies[cookbook] end @@ -603,7 +607,7 @@ class Chef msg=<<-OBSOLETED The dependency specification syntax you are using is no longer valid. You may not specify more than one version constraint for a particular cookbook. -Consult http://wiki.opscode.com/display/chef/Metadata for the updated syntax. +Consult https://docs.chef.io/config_rb_metadata.html for the updated syntax. Called by: #{caller_name} '#{dep_name}', #{version_constraints.map {|vc| vc.inspect}.join(", ")} Called from: @@ -622,7 +626,7 @@ OBSOLETED The version constraint syntax you are using is not valid. If you recently upgraded to Chef 0.10.0, be aware that you no may longer use "<<" and ">>" for 'less than' and 'greater than'; use '<' and '>' instead. -Consult http://wiki.opscode.com/display/chef/Metadata for more information. +Consult https://docs.chef.io/config_rb_metadata.html for more information. Called by: #{caller_name} '#{dep_name}', '#{constraint_str}' Called from: diff --git a/lib/chef/cookbook_loader.rb b/lib/chef/cookbook_loader.rb index c05fedb141..79005b1569 100644 --- a/lib/chef/cookbook_loader.rb +++ b/lib/chef/cookbook_loader.rb @@ -106,7 +106,7 @@ class Chef if @cookbooks_by_name.has_key?(cookbook.to_sym) or load_cookbook(cookbook.to_sym) @cookbooks_by_name[cookbook.to_sym] else - raise Exceptions::CookbookNotFoundInRepo, "Cannot find a cookbook named #{cookbook.to_s}; did you forget to add metadata to a cookbook? (http://wiki.opscode.com/display/chef/Metadata)" + raise Exceptions::CookbookNotFoundInRepo, "Cannot find a cookbook named #{cookbook.to_s}; did you forget to add metadata to a cookbook? (https://docs.chef.io/config_rb_metadata.html)" end end diff --git a/lib/chef/cookbook_site_streaming_uploader.rb b/lib/chef/cookbook_site_streaming_uploader.rb index 9e7a55c772..0302a51165 100644 --- a/lib/chef/cookbook_site_streaming_uploader.rb +++ b/lib/chef/cookbook_site_streaming_uploader.rb @@ -106,7 +106,7 @@ class Chef url = URI.parse(to_url) - Chef::Log.logger.debug("Signing: method: #{http_verb}, path: #{url.path}, file: #{content_file}, User-id: #{user_id}, Timestamp: #{timestamp}") + Chef::Log.logger.debug("Signing: method: #{http_verb}, url: #{url}, file: #{content_file}, User-id: #{user_id}, Timestamp: #{timestamp}") # We use the body for signing the request if the file parameter # wasn't a valid file or wasn't included. Extract the body (with @@ -141,13 +141,8 @@ class Chef req.content_type = 'multipart/form-data; boundary=' + boundary unless parts.empty? req.body_stream = body_stream - http = Net::HTTP.new(url.host, url.port) - if url.scheme == "https" - http.use_ssl = true - http.verify_mode = verify_mode - end + http = Chef::HTTP::BasicClient.new(url).http_client res = http.request(req) - #res = http.start {|http_proc| http_proc.request(req) } # alias status to code and to_s to body for test purposes # TODO: stop the following madness! @@ -166,17 +161,6 @@ class Chef res end - private - - def verify_mode - verify_mode = Chef::Config[:ssl_verify_mode] - if verify_mode == :verify_none - OpenSSL::SSL::VERIFY_NONE - elsif verify_mode == :verify_peer - OpenSSL::SSL::VERIFY_PEER - end - end - end class StreamPart diff --git a/lib/chef/dsl/definitions.rb b/lib/chef/dsl/definitions.rb new file mode 100644 index 0000000000..1358f67720 --- /dev/null +++ b/lib/chef/dsl/definitions.rb @@ -0,0 +1,44 @@ +class Chef + module DSL + # + # Module containing a method for each declared definition + # + # Depends on declare_resource(name, created_at, &block) + # + # @api private + # + module Definitions + def self.add_definition(dsl_name) + module_eval <<-EOM, __FILE__, __LINE__+1 + def #{dsl_name}(*args, &block) + evaluate_resource_definition(#{dsl_name.inspect}, *args, &block) + end + EOM + end + + # @api private + def has_resource_definition?(name) + run_context.definitions.has_key?(name) + end + + # Processes the arguments and block as a resource definition. + # + # @api private + def evaluate_resource_definition(definition_name, *args, &block) + + # This dupes the high level object, but we still need to dup the params + new_def = run_context.definitions[definition_name].dup + + new_def.params = new_def.params.dup + new_def.node = run_context.node + # This sets up the parameter overrides + new_def.instance_eval(&block) if block + + new_recipe = Chef::Recipe.new(cookbook_name, recipe_name, run_context) + new_recipe.params = new_def.params + new_recipe.params[:name] = args[0] + new_recipe.instance_eval(&new_def.recipe) + end + end + end +end diff --git a/lib/chef/dsl/recipe.rb b/lib/chef/dsl/recipe.rb index c22f053292..d69f0a8f11 100644 --- a/lib/chef/dsl/recipe.rb +++ b/lib/chef/dsl/recipe.rb @@ -21,6 +21,10 @@ require 'chef/mixin/convert_to_class_name' require 'chef/exceptions' require 'chef/resource_builder' require 'chef/mixin/shell_out' +require 'chef/mixin/powershell_out' +require 'chef/dsl/resources' +require 'chef/dsl/definitions' +require 'chef/resource' class Chef module DSL @@ -31,48 +35,10 @@ class Chef module Recipe include Chef::Mixin::ShellOut - include Chef::Mixin::ConvertToClassName - - def method_missing(method_symbol, *args, &block) - # If we have a definition that matches, we want to use that instead. This should - # let you do some really crazy over-riding of "native" types, if you really want - # to. - if has_resource_definition?(method_symbol) - evaluate_resource_definition(method_symbol, *args, &block) - elsif have_resource_class_for?(method_symbol) - # Otherwise, we're rocking the regular resource call route. - declare_resource(method_symbol, args[0], caller[0], &block) - else - begin - super - rescue NoMethodError - raise NoMethodError, "No resource or method named `#{method_symbol}' for #{describe_self_for_error}" - rescue NameError - raise NameError, "No resource, method, or local variable named `#{method_symbol}' for #{describe_self_for_error}" - end - end - end + include Chef::Mixin::PowershellOut - def has_resource_definition?(name) - run_context.definitions.has_key?(name) - end - - # Processes the arguments and block as a resource definition. - def evaluate_resource_definition(definition_name, *args, &block) - - # This dupes the high level object, but we still need to dup the params - new_def = run_context.definitions[definition_name].dup - - new_def.params = new_def.params.dup - new_def.node = run_context.node - # This sets up the parameter overrides - new_def.instance_eval(&block) if block - - new_recipe = Chef::Recipe.new(cookbook_name, recipe_name, run_context) - new_recipe.params = new_def.params - new_recipe.params[:name] = args[0] - new_recipe.instance_eval(&new_def.recipe) - end + include Chef::DSL::Resources + include Chef::DSL::Definitions # # Instantiates a resource (via #build_resource), then adds it to the @@ -168,14 +134,52 @@ class Chef raise Chef::Exceptions::ResourceNotFound, "exec was called, but you probably meant to use an execute resource. If not, please call Kernel#exec explicitly. The exec block called was \"#{args}\"" end + # DEPRECATED: + # method_missing must live for backcompat purposes until Chef 13. + def method_missing(method_symbol, *args, &block) + # + # If there is already DSL for this, someone must have called + # method_missing manually. Not a fan. Not. A. Fan. + # + if respond_to?(method_symbol) + Chef::Log.deprecation("Calling method_missing(#{method_symbol.inspect}) directly is deprecated in Chef 12 and will be removed in Chef 13.") + Chef::Log.deprecation("Use public_send() or send() instead.") + return send(method_symbol, *args, &block) + end + + # + # If a definition exists, then Chef::DSL::Definitions.add_definition was + # never called. DEPRECATED. + # + if run_context.definitions.has_key?(method_symbol.to_sym) + Chef::Log.deprecation("Definition #{method_symbol} (#{run_context.definitions[method_symbol.to_sym]}) was added to the run_context without calling Chef::DSL::Definitions.add_definition(#{method_symbol.to_sym.inspect}). This will become required in Chef 13.") + Chef::DSL::Definitions.add_definition(method_symbol) + return send(method_symbol, *args, &block) + end + + # + # See if the resource exists anyway. If the user had set + # Chef::Resource::Blah = <resource>, a deprecation warning will be + # emitted and the DSL method 'blah' will be added to the DSL. + # + resource_class = Chef::ResourceResolver.resolve(method_symbol, node: run_context ? run_context.node : nil) + if resource_class + Chef::DSL::Resources.add_resource_dsl(method_symbol) + return send(method_symbol, *args, &block) + end + + begin + super + rescue NoMethodError + raise NoMethodError, "No resource or method named `#{method_symbol}' for #{describe_self_for_error}" + rescue NameError + raise NameError, "No resource, method, or local variable named `#{method_symbol}' for #{describe_self_for_error}" + end + end end end end -# We require this at the BOTTOM of this file to avoid circular requires (it is used -# at runtime but not load time) -require 'chef/resource' - # **DEPRECATED** # This used to be part of chef/mixin/recipe_definition_dsl_core. Load the file to activate the deprecation code. require 'chef/mixin/recipe_definition_dsl_core' diff --git a/lib/chef/dsl/resources.rb b/lib/chef/dsl/resources.rb new file mode 100644 index 0000000000..1ce12ed0a0 --- /dev/null +++ b/lib/chef/dsl/resources.rb @@ -0,0 +1,29 @@ +class Chef + module DSL + # + # Module containing a method for each globally declared Resource + # + # Depends on declare_resource(name, created_at, &block) + # + # @api private + module Resources + def self.add_resource_dsl(dsl_name) + begin + module_eval(<<-EOM, __FILE__, __LINE__+1) + def #{dsl_name}(name=nil, created_at=nil, &block) + declare_resource(#{dsl_name.inspect}, name, created_at || caller[0], &block) + end + EOM + rescue SyntaxError + # Handle the case where dsl_name has spaces, etc. + define_method(dsl_name.to_sym) do |name=nil, created_at=nil, &block| + declare_resource(dsl_name, name, created_at || caller[0], &block) + end + end + end + def self.remove_resource_dsl(dsl_name) + remove_method(dsl_name) if method_defined?(dsl_name) + end + end + end +end diff --git a/lib/chef/event_dispatch/base.rb b/lib/chef/event_dispatch/base.rb index 7274105802..73fe25ec13 100644 --- a/lib/chef/event_dispatch/base.rb +++ b/lib/chef/event_dispatch/base.rb @@ -82,6 +82,11 @@ class Chef def node_load_completed(node, expanded_run_list, config) end + # Called after the Policyfile was loaded. This event only occurs when + # chef is in policyfile mode. + def policyfile_loaded(policy) + end + # Called before the cookbook collection is fetched from the server. def cookbook_resolution_start(expanded_run_list) end @@ -239,13 +244,13 @@ class Chef end # Called when audit phase successfully finishes - def audit_phase_complete + def audit_phase_complete(audit_output) end # Called if there is an uncaught exception during the audit phase. The audit runner should # be catching and handling errors from the examples, so this is only uncaught errors (like # bugs in our handling code) - def audit_phase_failed(exception) + def audit_phase_failed(exception, audit_output) end # Signifies the start of a `control_group` block with a defined name diff --git a/lib/chef/event_dispatch/dispatcher.rb b/lib/chef/event_dispatch/dispatcher.rb index 9f43f14311..370f8c51b4 100644 --- a/lib/chef/event_dispatch/dispatcher.rb +++ b/lib/chef/event_dispatch/dispatcher.rb @@ -9,6 +9,8 @@ class Chef # the registered subscribers. class Dispatcher < Base + attr_reader :subscribers + def initialize(*subscribers) @subscribers = subscribers end diff --git a/lib/chef/event_loggers/windows_eventlog.rb b/lib/chef/event_loggers/windows_eventlog.rb index 6f5ef627fb..7a3a28b61f 100644 --- a/lib/chef/event_loggers/windows_eventlog.rb +++ b/lib/chef/event_loggers/windows_eventlog.rb @@ -18,17 +18,7 @@ require 'chef/event_loggers/base' require 'chef/platform/query_helpers' - -if Chef::Platform::windows? and not Chef::Platform::windows_server_2003? - if defined? Windows::Constants - [:INFINITE, :WAIT_FAILED, :FORMAT_MESSAGE_IGNORE_INSERTS, :ERROR_INSUFFICIENT_BUFFER].each do |c| - # These are redefined in 'win32/eventlog' - Windows::Constants.send(:remove_const, c) if Windows::Constants.const_defined? c - end - end - - require 'win32/eventlog' -end +require 'chef/win32/eventlog' class Chef module EventLoggers @@ -88,15 +78,21 @@ class Chef #Exception message: %4 #Exception backtrace: %5 def run_failed(e) + data = + if @run_status + [@run_status.run_id, + @run_status.elapsed_time.to_s] + else + ["UNKNOWN", "UNKNOWN"] + end + @eventlog.report_event( :event_type => ::Win32::EventLog::ERROR_TYPE, :source => SOURCE, :event_id => RUN_FAILED_EVENT_ID, - :data => [@run_status.run_id, - @run_status.elapsed_time.to_s, - e.class.name, - e.message, - e.backtrace.join("\n")] + :data => data + [e.class.name, + e.message, + e.backtrace.join("\n")] ) end diff --git a/lib/chef/exceptions.rb b/lib/chef/exceptions.rb index 0b7b1634ad..f38bc32571 100644 --- a/lib/chef/exceptions.rb +++ b/lib/chef/exceptions.rb @@ -69,10 +69,17 @@ class Chef class ValidationFailed < ArgumentError; end class InvalidPrivateKey < ArgumentError; end class ConfigurationError < ArgumentError; end + class MissingKeyAttribute < ArgumentError; end + class KeyCommandInputError < ArgumentError; end + class InvalidKeyArgument < ArgumentError; end + class InvalidKeyAttribute < ArgumentError; end + class InvalidUserAttribute < ArgumentError; end + class InvalidClientAttribute < ArgumentError; end class RedirectLimitExceeded < RuntimeError; end class AmbiguousRunlistSpecification < ArgumentError; end class CookbookFrozen < ArgumentError; end class CookbookNotFound < RuntimeError; end + class OnlyApiVersion0SupportedForAction < RuntimeError; end # Cookbook loader used to raise an argument error when cookbook not found. # for back compat, need to raise an error that inherits from ArgumentError class CookbookNotFoundInRepo < ArgumentError; end @@ -432,7 +439,7 @@ class Chef wrapped_errors.each_with_index do |e,i| backtrace << "#{i+1}) #{e.class} - #{e.message}" backtrace += e.backtrace if e.backtrace - backtrace << "" + backtrace << "" unless i == wrapped_errors.length - 1 end set_backtrace(backtrace) end diff --git a/lib/chef/file_access_control/unix.rb b/lib/chef/file_access_control/unix.rb index 472f30b752..c53d832414 100644 --- a/lib/chef/file_access_control/unix.rb +++ b/lib/chef/file_access_control/unix.rb @@ -197,6 +197,8 @@ class Chef # the user has specified a permission, and it does not match the file, so fix the permission Chef::Log.debug("found target_mode != current_mode, updating mode") return true + elsif suid_bit_set? and (should_update_group? or should_update_owner?) + return true else Chef::Log.debug("found target_mode == current_mode, not updating mode") # the user has specified a permission, but it matches the file, so behave idempotently @@ -280,6 +282,9 @@ class Chef return nil end + def suid_bit_set? + return target_mode & 04000 > 0 + end end end end diff --git a/lib/chef/file_content_management/deploy/mv_windows.rb b/lib/chef/file_content_management/deploy/mv_windows.rb index 7504123012..0d16da9717 100644 --- a/lib/chef/file_content_management/deploy/mv_windows.rb +++ b/lib/chef/file_content_management/deploy/mv_windows.rb @@ -63,12 +63,22 @@ class Chef raise Chef::Exceptions::WindowsNotAdmin, "can not get the security information for '#{dst}' due to missing Administrator privileges." end - if dst_sd.dacl_present? - apply_dacl = ACL.create(dst_sd.dacl.select { |ace| !ace.inherited? }) + dacl_present = dst_sd.dacl_present? + if dacl_present + if dst_sd.dacl.nil? + apply_dacl = nil + else + apply_dacl = ACL.create(dst_sd.dacl.select { |ace| !ace.inherited? }) + end end - if dst_sd.sacl_present? - apply_sacl = ACL.create(dst_sd.sacl.select { |ace| !ace.inherited? }) + sacl_present = dst_sd.sacl_present? + if sacl_present + if dst_sd.sacl.nil? + apply_sacl = nil + else + apply_sacl = ACL.create(dst_sd.sacl.select { |ace| !ace.inherited? }) + end end # @@ -84,8 +94,8 @@ class Chef dst_so = Security::SecurableObject.new(dst) dst_so.group = dst_sd.group dst_so.owner = dst_sd.owner - dst_so.set_dacl(apply_dacl, dst_sd.dacl_inherits?) if dst_sd.dacl_present? - dst_so.set_sacl(apply_sacl, dst_sd.sacl_inherits?) if dst_sd.sacl_present? + dst_so.set_dacl(apply_dacl, dst_sd.dacl_inherits?) if dacl_present + dst_so.set_sacl(apply_sacl, dst_sd.sacl_inherits?) if sacl_present end end diff --git a/lib/chef/formatters/doc.rb b/lib/chef/formatters/doc.rb index 7144d00b5d..e76a940c38 100644 --- a/lib/chef/formatters/doc.rb +++ b/lib/chef/formatters/doc.rb @@ -3,9 +3,9 @@ require 'chef/config' class Chef module Formatters - #-- - # TODO: not sold on the name, but the output is similar to what rspec calls - # "specdoc" + + # Formatter similar to RSpec's documentation formatter. Uses indentation to + # show context. class Doc < Formatters::Base attr_reader :start_time, :end_time, :successful_audits, :failed_audits @@ -93,6 +93,10 @@ class Chef def node_load_completed(node, expanded_run_list, config) end + def policyfile_loaded(policy) + puts_line "Using policy '#{policy["name"]}' at revision '#{policy["revision_id"]}'" + end + # Called before the cookbook collection is fetched from the server. def cookbook_resolution_start(expanded_run_list) puts_line "resolving cookbooks for run list: #{expanded_run_list.inspect}" @@ -175,17 +179,21 @@ class Chef puts_line "Starting audit phase" end - def audit_phase_complete + def audit_phase_complete(audit_output) + puts_line audit_output puts_line "Auditing complete" end - def audit_phase_failed(error) + def audit_phase_failed(error, audit_output) + puts_line audit_output puts_line "" puts_line "Audit phase exception:" indent puts_line "#{error.message}" - error.backtrace.each do |l| - puts_line l + if error.backtrace + error.backtrace.each do |l| + puts_line l + end end end diff --git a/lib/chef/formatters/error_inspectors/api_error_formatting.rb b/lib/chef/formatters/error_inspectors/api_error_formatting.rb index 652d478b40..05ee3132a7 100644 --- a/lib/chef/formatters/error_inspectors/api_error_formatting.rb +++ b/lib/chef/formatters/error_inspectors/api_error_formatting.rb @@ -16,6 +16,8 @@ # limitations under the License. # +require 'chef/http/authenticator' + class Chef module Formatters @@ -65,6 +67,24 @@ E error_description.section("Server Response:",format_rest_error) end + def describe_406_error(error_description, response) + if response["x-ops-server-api-version"] + version_header = Chef::JSONCompat.from_json(response["x-ops-server-api-version"]) + client_api_version = version_header["request_version"] + min_server_version = version_header["min_version"] + max_server_version = version_header["max_version"] + + error_description.section("Incompatible server API version:",<<-E) +This version of the API that this Chef request specified is not supported by the Chef server you sent this request to. +The server supports a min API version of #{min_server_version} and a max API version of #{max_server_version}. +Chef just made a request with an API version of #{client_api_version}. +Please either update your Chef client or server to be a compatible set. +E + else + describe_http_error(error_description) + end + end + def describe_500_error(error_description) error_description.section("Unknown Server Error:",<<-E) The server had a fatal error attempting to load the node data. diff --git a/lib/chef/formatters/error_inspectors/compile_error_inspector.rb b/lib/chef/formatters/error_inspectors/compile_error_inspector.rb index 93328adbe3..d64d5e7b01 100644 --- a/lib/chef/formatters/error_inspectors/compile_error_inspector.rb +++ b/lib/chef/formatters/error_inspectors/compile_error_inspector.rb @@ -30,15 +30,16 @@ class Chef def initialize(path, exception) @path, @exception = path, exception + @backtrace_lines_in_cookbooks = nil + @file_lines = nil + @culprit_backtrace_entry = nil + @culprit_line = nil end def add_explanation(error_description) - case exception - when Chef::Exceptions::RecipeNotFound - error_description.section(exception.class.name, exception.message) - else - error_description.section(exception.class.name, exception.message) + error_description.section(exception.class.name, exception.message) + if found_error_in_cookbooks? traceback = filtered_bt.map {|line| " #{line}"}.join("\n") error_description.section("Cookbook Trace:", traceback) error_description.section("Relevant File Content:", context) @@ -93,10 +94,21 @@ class Chef end def filtered_bt - filters = Array(Chef::Config.cookbook_path).map {|p| /^#{Regexp.escape(p)}/ } - r = exception.backtrace.select {|line| filters.any? {|filter| line =~ filter }} - Chef::Log.debug("filtered backtrace of compile error: #{r.join(",")}") - return r.count > 0 ? r : exception.backtrace + backtrace_lines_in_cookbooks.count > 0 ? backtrace_lines_in_cookbooks : exception.backtrace + end + + def found_error_in_cookbooks? + !backtrace_lines_in_cookbooks.empty? + end + + def backtrace_lines_in_cookbooks + @backtrace_lines_in_cookbooks ||= + begin + filters = Array(Chef::Config.cookbook_path).map {|p| /^#{Regexp.escape(p)}/i } + r = exception.backtrace.select {|line| filters.any? {|filter| line =~ filter }} + Chef::Log.debug("filtered backtrace of compile error: #{r.join(",")}") + r + end end end diff --git a/lib/chef/formatters/error_inspectors/cookbook_resolve_error_inspector.rb b/lib/chef/formatters/error_inspectors/cookbook_resolve_error_inspector.rb index aa5eb8485d..e011fa9d9b 100644 --- a/lib/chef/formatters/error_inspectors/cookbook_resolve_error_inspector.rb +++ b/lib/chef/formatters/error_inspectors/cookbook_resolve_error_inspector.rb @@ -72,6 +72,8 @@ E describe_500_error(error_description) when Net::HTTPBadGateway, Net::HTTPServiceUnavailable describe_503_error(error_description) + when Net::HTTPNotAcceptable + describe_406_error(error_description, response) else describe_http_error(error_description) end diff --git a/lib/chef/formatters/error_inspectors/cookbook_sync_error_inspector.rb b/lib/chef/formatters/error_inspectors/cookbook_sync_error_inspector.rb index 0cb849a17f..971dbd664e 100644 --- a/lib/chef/formatters/error_inspectors/cookbook_sync_error_inspector.rb +++ b/lib/chef/formatters/error_inspectors/cookbook_sync_error_inspector.rb @@ -67,6 +67,8 @@ class Chef describe_500_error(error_description) when Net::HTTPBadGateway, Net::HTTPServiceUnavailable, Net::HTTPGatewayTimeOut describe_503_error(error_description) + when Net::HTTPNotAcceptable + describe_406_error(error_description, response) else describe_http_error(error_description) end diff --git a/lib/chef/formatters/error_inspectors/node_load_error_inspector.rb b/lib/chef/formatters/error_inspectors/node_load_error_inspector.rb index e257ee30c0..d81a9f7cc8 100644 --- a/lib/chef/formatters/error_inspectors/node_load_error_inspector.rb +++ b/lib/chef/formatters/error_inspectors/node_load_error_inspector.rb @@ -84,6 +84,8 @@ E describe_500_error(error_description) when Net::HTTPBadGateway, Net::HTTPServiceUnavailable describe_503_error(error_description) + when Net::HTTPNotAcceptable + describe_406_error(error_description, response) else describe_http_error(error_description) end diff --git a/lib/chef/formatters/error_inspectors/registration_error_inspector.rb b/lib/chef/formatters/error_inspectors/registration_error_inspector.rb index f31b348278..dbd23f4a52 100644 --- a/lib/chef/formatters/error_inspectors/registration_error_inspector.rb +++ b/lib/chef/formatters/error_inspectors/registration_error_inspector.rb @@ -9,6 +9,8 @@ class Chef # TODO: Lots of duplication with the node_load_error_inspector, just # slightly tweaked to talk about validation keys instead of other keys. class RegistrationErrorInspector + include APIErrorFormatting + attr_reader :exception attr_reader :node_name attr_reader :config @@ -94,6 +96,8 @@ E error_description.section("Relevant Config Settings:",<<-E) chef_server_url "#{server_url}" E + when Net::HTTPNotAcceptable + describe_406_error(error_description, response) when Net::HTTPInternalServerError error_description.section("Unknown Server Error:",<<-E) The server had a fatal error attempting to load the node data. diff --git a/lib/chef/formatters/error_inspectors/resource_failure_inspector.rb b/lib/chef/formatters/error_inspectors/resource_failure_inspector.rb index 48572d909b..6e4d9322f9 100644 --- a/lib/chef/formatters/error_inspectors/resource_failure_inspector.rb +++ b/lib/chef/formatters/error_inspectors/resource_failure_inspector.rb @@ -63,7 +63,7 @@ class Chef def recipe_snippet return nil if dynamic_resource? @snippet ||= begin - if file = resource.source_line[/^(([\w]:)?[^:]+):([\d]+)/,1] and line = resource.source_line[/^#{file}:([\d]+)/,1].to_i + if file = parse_source and line = parse_line(file) return nil unless ::File.exists?(file) lines = IO.readlines(file) @@ -111,6 +111,16 @@ class Chef line_nr_string + line end + def parse_source + resource.source_line[/^(([\w]:)?[^:]+):([\d]+)/,1] + end + + def parse_line(source) + resource.source_line[/^#{Regexp.escape(source)}:([\d]+)/,1].to_i + end + + + end end end diff --git a/lib/chef/formatters/error_inspectors/run_list_expansion_error_inspector.rb b/lib/chef/formatters/error_inspectors/run_list_expansion_error_inspector.rb index ac19a983af..818228276e 100644 --- a/lib/chef/formatters/error_inspectors/run_list_expansion_error_inspector.rb +++ b/lib/chef/formatters/error_inspectors/run_list_expansion_error_inspector.rb @@ -98,6 +98,8 @@ E error_description.section("Possible Causes:",<<-E) * Your client (#{username}) may have misconfigured authorization permissions. E + when Net::HTTPNotAcceptable + describe_406_error(error_description, response) when Net::HTTPInternalServerError error_description.section("Unknown Server Error:",<<-E) The server had a fatal error attempting to load a role. diff --git a/lib/chef/guard_interpreter/default_guard_interpreter.rb b/lib/chef/guard_interpreter/default_guard_interpreter.rb index df91c2b1ad..fead9886b2 100644 --- a/lib/chef/guard_interpreter/default_guard_interpreter.rb +++ b/lib/chef/guard_interpreter/default_guard_interpreter.rb @@ -16,6 +16,8 @@ # limitations under the License. # +require 'chef/mixin/shell_out' + class Chef class GuardInterpreter class DefaultGuardInterpreter diff --git a/lib/chef/guard_interpreter/resource_guard_interpreter.rb b/lib/chef/guard_interpreter/resource_guard_interpreter.rb index 1e2a534c18..d4b386a15a 100644 --- a/lib/chef/guard_interpreter/resource_guard_interpreter.rb +++ b/lib/chef/guard_interpreter/resource_guard_interpreter.rb @@ -92,8 +92,11 @@ class Chef raise ArgumentError, "Specified guard interpreter class #{resource_class} must be a kind of Chef::Resource::Execute resource" end + # Duplicate the node below because the new RunContext + # overwrites the state of Node instances passed to it. + # See https://github.com/chef/chef/issues/3485. empty_events = Chef::EventDispatch::Dispatcher.new - anonymous_run_context = Chef::RunContext.new(parent_resource.node, {}, empty_events) + anonymous_run_context = Chef::RunContext.new(parent_resource.node.dup, {}, empty_events) interpreter_resource = resource_class.new('Guard resource', anonymous_run_context) interpreter_resource.is_guard_interpreter = true diff --git a/lib/chef/http/authenticator.rb b/lib/chef/http/authenticator.rb index 4255f18cbd..bffa9c4b3a 100644 --- a/lib/chef/http/authenticator.rb +++ b/lib/chef/http/authenticator.rb @@ -24,6 +24,8 @@ class Chef class HTTP class Authenticator + DEFAULT_SERVER_API_VERSION = "1" + attr_reader :signing_key_filename attr_reader :raw_key attr_reader :attr_names @@ -37,10 +39,16 @@ class Chef @signing_key_filename = opts[:signing_key_filename] @key = load_signing_key(opts[:signing_key_filename], opts[:raw_key]) @auth_credentials = AuthCredentials.new(opts[:client_name], @key) + if opts[:api_version] + @api_version = opts[:api_version] + else + @api_version = DEFAULT_SERVER_API_VERSION + end end def handle_request(method, url, headers={}, data=false) headers.merge!(authentication_headers(method, url, data)) if sign_requests? + headers.merge!({'X-Ops-Server-API-Version' => @api_version}) [method, url, headers, data] end diff --git a/lib/chef/http/basic_client.rb b/lib/chef/http/basic_client.rb index 076d152d16..de5e7c03a8 100644 --- a/lib/chef/http/basic_client.rb +++ b/lib/chef/http/basic_client.rb @@ -101,12 +101,16 @@ class Chef env["#{url.scheme.upcase}_PROXY"] || env["#{url.scheme}_proxy"] # Check if the proxy string contains a scheme. If not, add the url's scheme to the - # proxy before parsing. The regex /^.*:\/\// matches, for example, http://. - proxy = if proxy.match(/^.*:\/\//) - URI.parse(proxy) - else - URI.parse("#{url.scheme}://#{proxy}") - end if String === proxy + # proxy before parsing. The regex /^.*:\/\// matches, for example, http://. Reusing proxy + # here since we are really just trying to get the string built correctly. + if String === proxy && !proxy.strip.empty? + if proxy.match(/^.*:\/\//) + proxy = URI.parse(proxy.strip) + else + proxy = URI.parse("#{url.scheme}://#{proxy.strip}") + end + end + no_proxy = Chef::Config[:no_proxy] || env['NO_PROXY'] || env['no_proxy'] excludes = no_proxy.to_s.split(/\s*,\s*/).compact excludes = excludes.map { |exclude| exclude =~ /:\d+$/ ? exclude : "#{exclude}:*" } diff --git a/lib/chef/http/json_input.rb b/lib/chef/http/json_input.rb index 23ccc3a8a7..3296d8821f 100644 --- a/lib/chef/http/json_input.rb +++ b/lib/chef/http/json_input.rb @@ -25,14 +25,19 @@ class Chef # Middleware that takes json input and turns it into raw text class JSONInput + attr_accessor :opts + def initialize(opts={}) + @opts = opts end def handle_request(method, url, headers={}, data=false) if data && should_encode_as_json?(headers) headers.delete_if { |key, _value| key.downcase == 'content-type' } headers["Content-Type"] = 'application/json' - data = Chef::JSONCompat.to_json(data) + json_opts = {} + json_opts[:validate_utf8] = opts[:validate_utf8] if opts.has_key?(:validate_utf8) + data = Chef::JSONCompat.to_json(data, json_opts) # Force encoding to binary to fix SSL related EOFErrors # cf. http://tickets.opscode.com/browse/CHEF-2363 # http://redmine.ruby-lang.org/issues/5233 diff --git a/lib/chef/key.rb b/lib/chef/key.rb new file mode 100644 index 0000000000..be4be7f230 --- /dev/null +++ b/lib/chef/key.rb @@ -0,0 +1,271 @@ +# +# Author:: Tyler Cloke (tyler@chef.io) +# Copyright:: Copyright (c) 2015 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/json_compat' +require 'chef/mixin/params_validate' +require 'chef/exceptions' + +class Chef + # Class for interacting with a chef key object. Can be used to create new keys, + # save to server, load keys from server, list keys, delete keys, etc. + # + # @author Tyler Cloke + # + # @attr [String] actor the name of the client or user that this key is for + # @attr [String] name the name of the key + # @attr [String] public_key the RSA string of this key + # @attr [String] private_key the RSA string of the private key if returned via a POST or PUT + # @attr [String] expiration_date the ISO formatted string YYYY-MM-DDTHH:MM:SSZ, i.e. 2020-12-24T21:00:00Z + # @attr [String] rest Chef::REST object, initialized and cached via chef_rest method + # @attr [string] api_base either "users" or "clients", initialized and cached via api_base method + # + # @attr_reader [String] actor_field_name must be either 'client' or 'user' + class Key + + include Chef::Mixin::ParamsValidate + + attr_reader :actor_field_name + + def initialize(actor, actor_field_name) + # Actor that the key is for, either a client or a user. + @actor = actor + + unless actor_field_name == "user" || actor_field_name == "client" + raise Chef::Exceptions::InvalidKeyArgument, "the second argument to initialize must be either 'user' or 'client'" + end + + @actor_field_name = actor_field_name + + @name = nil + @public_key = nil + @private_key = nil + @expiration_date = nil + @create_key = nil + end + + def chef_rest + @rest ||= if @actor_field_name == "user" + Chef::REST.new(Chef::Config[:chef_server_root]) + else + Chef::REST.new(Chef::Config[:chef_server_url]) + end + end + + def api_base + @api_base ||= if @actor_field_name == "user" + "users" + else + "clients" + end + end + + def actor(arg=nil) + set_or_return(:actor, arg, + :regex => /^[a-z0-9\-_]+$/) + end + + def name(arg=nil) + set_or_return(:name, arg, + :kind_of => String) + end + + def public_key(arg=nil) + raise Chef::Exceptions::InvalidKeyAttribute, "you cannot set the public_key if create_key is true" if !arg.nil? && @create_key + set_or_return(:public_key, arg, + :kind_of => String) + end + + def private_key(arg=nil) + set_or_return(:private_key, arg, + :kind_of => String) + end + + def delete_public_key + @public_key = nil + end + + def delete_create_key + @create_key = nil + end + + def create_key(arg=nil) + raise Chef::Exceptions::InvalidKeyAttribute, "you cannot set create_key to true if the public_key field exists" if arg == true && !@public_key.nil? + set_or_return(:create_key, arg, + :kind_of => [TrueClass, FalseClass]) + end + + def expiration_date(arg=nil) + set_or_return(:expiration_date, arg, + :regex => /^(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z|infinity)$/) + end + + def to_hash + result = { + @actor_field_name => @actor + } + result["name"] = @name if @name + result["public_key"] = @public_key if @public_key + result["private_key"] = @private_key if @private_key + result["expiration_date"] = @expiration_date if @expiration_date + result["create_key"] = @create_key if @create_key + result + end + + def to_json(*a) + Chef::JSONCompat.to_json(to_hash, *a) + end + + def create + # if public_key is undefined and create_key is false, we cannot create + if @public_key.nil? && !@create_key + raise Chef::Exceptions::MissingKeyAttribute, "either public_key must be defined or create_key must be true" + end + + # defaults the key name to the fingerprint of the key + if @name.nil? + # if they didn't pass a public_key, + #then they must supply a name because we can't generate a fingerprint + unless @public_key.nil? + @name = fingerprint + else + raise Chef::Exceptions::MissingKeyAttribute, "a name cannot be auto-generated if no public key passed, either pass a public key or supply a name" + end + end + + payload = {"name" => @name} + payload['public_key'] = @public_key unless @public_key.nil? + payload['create_key'] = @create_key if @create_key + payload['expiration_date'] = @expiration_date unless @expiration_date.nil? + result = chef_rest.post_rest("#{api_base}/#{@actor}/keys", payload) + # append the private key to the current key if the server returned one, + # since the POST endpoint just returns uri and private_key if needed. + new_key = self.to_hash + new_key["private_key"] = result["private_key"] if result["private_key"] + Chef::Key.from_hash(new_key) + end + + def fingerprint + self.class.generate_fingerprint(@public_key) + end + + # set @name and pass put_name if you wish to update the name of an existing key put_name to @name + def update(put_name=nil) + if @name.nil? && put_name.nil? + raise Chef::Exceptions::MissingKeyAttribute, "the name field must be populated or you must pass a name to update when update is called" + end + + # If no name was passed, fall back to using @name in the PUT URL, otherwise + # use the put_name passed. This will update the a key by the name put_name + # to @name. + put_name = @name if put_name.nil? + + new_key = chef_rest.put_rest("#{api_base}/#{@actor}/keys/#{put_name}", to_hash) + # if the server returned a public_key, remove the create_key field, as we now have a key + if new_key["public_key"] + self.delete_create_key + end + Chef::Key.from_hash(self.to_hash.merge(new_key)) + end + + def save + create + rescue Net::HTTPServerException => e + if e.response.code == "409" + update + else + raise e + end + end + + def destroy + if @name.nil? + raise Chef::Exceptions::MissingKeyAttribute, "the name field must be populated when delete is called" + end + + chef_rest.delete_rest("#{api_base}/#{@actor}/keys/#{@name}") + end + + # Class methods + def self.from_hash(key_hash) + if key_hash.has_key?("user") + key = Chef::Key.new(key_hash["user"], "user") + elsif key_hash.has_key?("client") + key = Chef::Key.new(key_hash["client"], "client") + else + raise Chef::Exceptions::MissingKeyAttribute, "The hash passed to from_hash does not contain the key 'user' or 'client'. Please pass a hash that defines one of those keys." + end + key.name key_hash['name'] if key_hash.key?('name') + key.public_key key_hash['public_key'] if key_hash.key?('public_key') + key.private_key key_hash['private_key'] if key_hash.key?('private_key') + key.create_key key_hash['create_key'] if key_hash.key?('create_key') + key.expiration_date key_hash['expiration_date'] if key_hash.key?('expiration_date') + key + end + + def self.from_json(json) + Chef::Key.from_hash(Chef::JSONCompat.from_json(json)) + end + + class << self + alias_method :json_create, :from_json + end + + def self.list_by_user(actor, inflate=false) + keys = Chef::REST.new(Chef::Config[:chef_server_root]).get_rest("users/#{actor}/keys") + self.list(keys, actor, :load_by_user, inflate) + end + + def self.list_by_client(actor, inflate=false) + keys = Chef::REST.new(Chef::Config[:chef_server_url]).get_rest("clients/#{actor}/keys") + self.list(keys, actor, :load_by_client, inflate) + end + + def self.load_by_user(actor, key_name) + response = Chef::REST.new(Chef::Config[:chef_server_root]).get_rest("users/#{actor}/keys/#{key_name}") + Chef::Key.from_hash(response.merge({"user" => actor})) + end + + def self.load_by_client(actor, key_name) + response = Chef::REST.new(Chef::Config[:chef_server_url]).get_rest("clients/#{actor}/keys/#{key_name}") + Chef::Key.from_hash(response.merge({"client" => actor})) + end + + def self.generate_fingerprint(public_key) + openssl_key_object = OpenSSL::PKey::RSA.new(public_key) + data_string = OpenSSL::ASN1::Sequence([ + OpenSSL::ASN1::Integer.new(openssl_key_object.public_key.n), + OpenSSL::ASN1::Integer.new(openssl_key_object.public_key.e) + ]) + OpenSSL::Digest::SHA1.hexdigest(data_string.to_der).scan(/../).join(':') + end + + private + + def self.list(keys, actor, load_method_symbol, inflate) + if inflate + keys.inject({}) do |key_map, result| + name = result["name"] + key_map[name] = Chef::Key.send(load_method_symbol, actor, name) + key_map + end + else + keys + end + end + end +end diff --git a/lib/chef/knife.rb b/lib/chef/knife.rb index 2e0694aebc..4a93697a1b 100644 --- a/lib/chef/knife.rb +++ b/lib/chef/knife.rb @@ -27,6 +27,7 @@ require 'chef/knife/core/subcommand_loader' require 'chef/knife/core/ui' require 'chef/local_mode' require 'chef/rest' +require 'chef/http/authenticator' require 'pp' class Chef @@ -358,7 +359,7 @@ class Chef case Chef::Config[:verbosity] when 0, nil - Chef::Config[:log_level] = :error + Chef::Config[:log_level] = :warn when 1 Chef::Config[:log_level] = :info else @@ -400,6 +401,8 @@ class Chef end def configure_chef + # knife needs to send logger output to STDERR by default + Chef::Config[:log_location] = STDERR config_loader = self.class.load_config(config[:config_file]) config[:config_file] = config_loader.config_location @@ -483,6 +486,15 @@ class Chef when Net::HTTPServiceUnavailable ui.error "Service temporarily unavailable" ui.info "Response: #{format_rest_error(response)}" + when Net::HTTPNotAcceptable + version_header = Chef::JSONCompat.from_json(response["x-ops-server-api-version"]) + client_api_version = version_header["request_version"] + min_server_version = version_header["min_version"] + max_server_version = version_header["max_version"] + ui.error "The version of Chef that Knife is using is not supported by the Chef server you sent this request to" + ui.info "The request that Knife sent was using API version #{client_api_version}" + ui.info "The Chef server you sent the request to supports a min API verson of #{min_server_version} and a max API version of #{max_server_version}" + ui.info "Please either update your Chef client or server to be a compatible set" else ui.error response.message ui.info "Response: #{format_rest_error(response)}" @@ -539,6 +551,16 @@ class Chef self.msg("Deleted #{obj_name}") end + # helper method for testing if a field exists + # and returning the usage and proper error if not + def test_mandatory_field(field, fieldname) + if field.nil? + show_usage + ui.fatal("You must specify a #{fieldname}") + exit 1 + end + end + def rest @rest ||= begin require 'chef/rest' diff --git a/lib/chef/knife/bootstrap.rb b/lib/chef/knife/bootstrap.rb index a4095e8402..5b29591fcc 100644 --- a/lib/chef/knife/bootstrap.rb +++ b/lib/chef/knife/bootstrap.rb @@ -316,6 +316,12 @@ class Chef # new client-side hawtness, just delete your validation key. if chef_vault_handler.doing_chef_vault? || (Chef::Config[:validation_key] && !File.exist?(File.expand_path(Chef::Config[:validation_key]))) + + unless config[:chef_node_name] + ui.error("You must pass a node name with -N when bootstrapping with user credentials") + exit 1 + end + client_builder.run chef_vault_handler.run(node_name: config[:chef_node_name]) diff --git a/lib/chef/knife/bootstrap/templates/chef-full.erb b/lib/chef/knife/bootstrap/templates/chef-full.erb index a87ab8e544..335b1f181c 100644 --- a/lib/chef/knife/bootstrap/templates/chef-full.erb +++ b/lib/chef/knife/bootstrap/templates/chef-full.erb @@ -1,15 +1,16 @@ -bash -c ' +sh -c ' <%= "export https_proxy=\"#{knife_config[:bootstrap_proxy]}\"" if knife_config[:bootstrap_proxy] -%> -distro=`uname -s` - -if test "x$distro" = "xSunOS"; then - if test -d "/usr/sfw/bin"; then - PATH=/usr/sfw/bin:$PATH - export PATH - fi +if test "x$TMPDIR" = "x"; then + tmp="/tmp" +else + tmp=$TMPDIR fi +# secure-ish temp dir creation without having mktemp available (DDoS-able but not exploitable) +tmp_dir="$tmp/install.sh.$$" +(umask 077 && mkdir $tmp_dir) || exit 1 + exists() { if command -v $1 &>/dev/null then @@ -19,41 +20,183 @@ exists() { fi } +http_404_error() { + echo "ERROR 404: Could not retrieve a valid install.sh!" + exit 1 +} + +capture_tmp_stderr() { + # spool up /tmp/stderr from all the commands we called + if test -f "$tmp_dir/stderr"; then + output=`cat $tmp_dir/stderr` + stderr_results="${stderr_results}\nSTDERR from $1:\n\n$output\n" + rm $tmp_dir/stderr + fi +} + +# do_wget URL FILENAME +do_wget() { + echo "trying wget..." + wget <%= "--proxy=on " if knife_config[:bootstrap_proxy] %> <%= knife_config[:bootstrap_wget_options] %> -O "$2" "$1" 2>$tmp_dir/stderr + rc=$? + # check for 404 + grep "ERROR 404" $tmp_dir/stderr 2>&1 >/dev/null + if test $? -eq 0; then + http_404_error + fi + + # check for bad return status or empty output + if test $rc -ne 0 || test ! -s "$2"; then + capture_tmp_stderr "wget" + return 1 + fi + + return 0 +} + +# do_curl URL FILENAME +do_curl() { + echo "trying curl..." + curl -sL <%= "--proxy \"#{knife_config[:bootstrap_proxy]}\" " if knife_config[:bootstrap_proxy] %> <%= knife_config[:bootstrap_curl_options] %> -D $tmp_dir/stderr -o "$2" "$1" 2>$tmp_dir/stderr + rc=$? + # check for 404 + grep "404 Not Found" $tmp_dir/stderr 2>&1 >/dev/null + if test $? -eq 0; then + http_404_error + fi + + # check for bad return status or empty output + if test $rc -ne 0 || test ! -s "$2"; then + capture_tmp_stderr "curl" + return 1 + fi + + return 0 +} + +# do_fetch URL FILENAME +do_fetch() { + echo "trying fetch..." + fetch -o "$2" "$1" 2>$tmp_dir/stderr + # check for bad return status + test $? -ne 0 && return 1 + return 0 +} + +# do_perl URL FILENAME +do_perl() { + echo "trying perl..." + perl -e "use LWP::Simple; getprint(shift @ARGV);" "$1" > "$2" 2>$tmp_dir/stderr + rc=$? + # check for 404 + grep "404 Not Found" $tmp_dir/stderr 2>&1 >/dev/null + if test $? -eq 0; then + http_404_error + fi + + # check for bad return status or empty output + if test $rc -ne 0 || test ! -s "$2"; then + capture_tmp_stderr "perl" + return 1 + fi + + return 0 +} + +# do_python URL FILENAME +do_python() { + echo "trying python..." + python -c "import sys,urllib2 ; sys.stdout.write(urllib2.urlopen(sys.argv[1]).read())" "$1" > "$2" 2>$tmp_dir/stderr + rc=$? + # check for 404 + grep "HTTP Error 404" $tmp_dir/stderr 2>&1 >/dev/null + if test $? -eq 0; then + http_404_error + fi + + # check for bad return status or empty output + if test $rc -ne 0 || test ! -s "$2"; then + capture_tmp_stderr "python" + return 1 + fi + return 0 +} + +# do_download URL FILENAME +do_download() { + PATH=/opt/local/bin:/usr/local/sbin:/usr/local/bin:/usr/sfw/bin:/sbin:/bin:/usr/sbin:/usr/bin + export PATH + + echo "downloading $1" + echo " to file $2" + + # we try all of these until we get success. + # perl, in particular may be present but LWP::Simple may not be installed + + if exists wget; then + do_wget $1 $2 && return 0 + fi + + if exists curl; then + do_curl $1 $2 && return 0 + fi + + if exists fetch; then + do_fetch $1 $2 && return 0 + fi + + if exists perl; then + do_perl $1 $2 && return 0 + fi + + if exists python; then + do_python $1 $2 && return 0 + fi + + echo ">>>>>> wget, curl, fetch, perl, or python not found on this instance." + + if test "x$stderr_results" != "x"; then + echo "\nDEBUG OUTPUT FOLLOWS:\n$stderr_results" + fi + + return 16 +} + <% if knife_config[:bootstrap_install_command] %> <%= knife_config[:bootstrap_install_command] %> <% else %> install_sh="<%= knife_config[:bootstrap_url] ? knife_config[:bootstrap_url] : "https://www.opscode.com/chef/install.sh" %>" if ! exists /usr/bin/chef-client; then - echo "Installing Chef Client..." - if exists wget; then - bash <(wget <%= "--proxy=on " if knife_config[:bootstrap_proxy] %> <%= knife_config[:bootstrap_wget_options] %> ${install_sh} -O -) <%= latest_current_chef_version_string %> - elif exists curl; then - bash <(curl -L <%= "--proxy \"#{knife_config[:bootstrap_proxy]}\" " if knife_config[:bootstrap_proxy] %> <%= knife_config[:bootstrap_curl_options] %> ${install_sh}) <%= latest_current_chef_version_string %> - else - echo "Neither wget nor curl found. Please install one and try again." >&2 - exit 1 - fi + echo "-----> Installing Chef Omnibus (<%= latest_current_chef_version_string %>)" + do_download ${install_sh} $tmp_dir/install.sh + sh $tmp_dir/install.sh -P chef <%= latest_current_chef_version_string %> + else + echo "-----> Existing Chef installation detected" fi <% end %> +if test "x$tmp_dir" != "x"; then + rm -r "$tmp_dir" +fi + mkdir -p /etc/chef <% if client_pem -%> -cat > /etc/chef/client.pem <<'EOP' +cat > /etc/chef/client.pem <<EOP <%= ::File.read(::File.expand_path(client_pem)) %> EOP chmod 0600 /etc/chef/client.pem <% end -%> <% if validation_key -%> -cat > /etc/chef/validation.pem <<'EOP' +cat > /etc/chef/validation.pem <<EOP <%= validation_key %> EOP chmod 0600 /etc/chef/validation.pem <% end -%> <% if encrypted_data_bag_secret -%> -cat > /etc/chef/encrypted_data_bag_secret <<'EOP' +cat > /etc/chef/encrypted_data_bag_secret <<EOP <%= encrypted_data_bag_secret %> EOP chmod 0600 /etc/chef/encrypted_data_bag_secret @@ -69,17 +212,17 @@ mkdir -p /etc/chef/trusted_certs mkdir -p /etc/chef/ohai/hints <% @chef_config[:knife][:hints].each do |name, hash| -%> -cat > /etc/chef/ohai/hints/<%= name %>.json <<'EOP' +cat > /etc/chef/ohai/hints/<%= name %>.json <<EOP <%= Chef::JSONCompat.to_json(hash) %> EOP <% end -%> <% end -%> -cat > /etc/chef/client.rb <<'EOP' +cat > /etc/chef/client.rb <<EOP <%= config_content %> EOP -cat > /etc/chef/first-boot.json <<'EOP' +cat > /etc/chef/first-boot.json <<EOP <%= Chef::JSONCompat.to_json(first_boot) %> EOP diff --git a/lib/chef/knife/client_create.rb b/lib/chef/knife/client_create.rb index 477a400e8a..570c1ee950 100644 --- a/lib/chef/knife/client_create.rb +++ b/lib/chef/knife/client_create.rb @@ -28,58 +28,82 @@ class Chef end option :file, - :short => "-f FILE", - :long => "--file FILE", - :description => "Write the key to a file" + :short => "-f FILE", + :long => "--file FILE", + :description => "Write the private key to a file if the server generated one." option :admin, - :short => "-a", - :long => "--admin", - :description => "Create the client as an admin", - :boolean => true + :short => "-a", + :long => "--admin", + :description => "Open Source Chef 11 only. Create the client as an admin.", + :boolean => true option :validator, - :long => "--validator", - :description => "Create the client as a validator", - :boolean => true + :long => "--validator", + :description => "Create the client as a validator.", + :boolean => true - banner "knife client create CLIENT (options)" + option :public_key, + :short => "-p FILE", + :long => "--public-key", + :description => "Set the initial default key for the client from a file on disk (cannot pass with --prevent-keygen)." + + option :prevent_keygen, + :short => "-k", + :long => "--prevent-keygen", + :description => "API V1 only. Prevent server from generating a default key pair for you. Cannot be passed with --public-key.", + :boolean => true + + banner "knife client create CLIENTNAME (options)" + + def client + @client_field ||= Chef::ApiClient.new + end + + def create_client(client) + # should not be using save :( bad behavior + client.save + end def run - @client_name = @name_args[0] + test_mandatory_field(@name_args[0], "client name") + client.name @name_args[0] - if @client_name.nil? + if config[:public_key] && config[:prevent_keygen] show_usage - ui.fatal("You must specify a client name") + ui.fatal("You cannot pass --public-key and --prevent-keygen") exit 1 end - client_hash = { - "name" => @client_name, - "admin" => !!config[:admin], - "validator" => !!config[:validator] - } + if !config[:prevent_keygen] && !config[:public_key] + client.create_key(true) + end + + if config[:admin] + client.admin(true) + end - output = Chef::ApiClient.from_hash(edit_hash(client_hash)) + if config[:validator] + client.validator(true) + end - # Chef::ApiClient.save will try to create a client and if it - # exists will update it instead silently. - client = output.save + if config[:public_key] + client.public_key File.read(File.expand_path(config[:public_key])) + end - # We only get a private_key on client creation, not on client update. - if client['private_key'] - ui.info("Created #{output}") + output = edit_data(client) + final_client = create_client(output) + ui.info("Created #{output}") + # output private_key if one + if final_client.private_key if config[:file] File.open(config[:file], "w") do |f| - f.print(client['private_key']) + f.print(final_client.private_key) end else - puts client['private_key'] + puts final_client.private_key end - else - ui.error "Client '#{client['name']}' already exists" - exit 1 end end end diff --git a/lib/chef/knife/client_key_create.rb b/lib/chef/knife/client_key_create.rb new file mode 100644 index 0000000000..3b7e97eb24 --- /dev/null +++ b/lib/chef/knife/client_key_create.rb @@ -0,0 +1,67 @@ +# +# Author:: Tyler Cloke (tyler@chef.io) +# Copyright:: Copyright (c) 2015 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/knife/key_create_base' + +class Chef + class Knife + # Implements knife user key create using Chef::Knife::KeyCreate + # as a service class. + # + # @author Tyler Cloke + # + # @attr_reader [String] actor the name of the client that this key is for + class ClientKeyCreate < Knife + include Chef::Knife::KeyCreateBase + + attr_reader :actor + + def initialize(argv=[]) + super(argv) + @service_object = nil + end + + def run + apply_params!(@name_args) + service_object.run + end + + def actor_field_name + 'client' + end + + def service_object + @service_object ||= Chef::Knife::KeyCreate.new(@actor, actor_field_name, ui, config) + end + + def actor_missing_error + 'You must specify a client name' + end + + def apply_params!(params) + @actor = params[0] + if @actor.nil? + show_usage + ui.fatal(actor_missing_error) + exit 1 + end + end + end + end +end diff --git a/lib/chef/knife/client_key_delete.rb b/lib/chef/knife/client_key_delete.rb new file mode 100644 index 0000000000..8ecdfe1ec8 --- /dev/null +++ b/lib/chef/knife/client_key_delete.rb @@ -0,0 +1,76 @@ +# +# Author:: Tyler Cloke (tyler@chef.io) +# Copyright:: Copyright (c) 2015 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' + +class Chef + class Knife + # Implements knife client key delete using Chef::Knife::KeyDelete + # as a service class. + # + # @author Tyler Cloke + # + # @attr_reader [String] actor the name of the client that this key is for + class ClientKeyDelete < Knife + banner "knife client key delete CLIENT KEYNAME (options)" + + attr_reader :actor + + def initialize(argv=[]) + super(argv) + @service_object = nil + end + + def run + apply_params!(@name_args) + service_object.run + end + + def actor_field_name + 'client' + end + + def actor_missing_error + 'You must specify a client name' + end + + def keyname_missing_error + 'You must specify a key name' + end + + def service_object + @service_object ||= Chef::Knife::KeyDelete.new(@name, @actor, actor_field_name, ui) + end + + def apply_params!(params) + @actor = params[0] + if @actor.nil? + show_usage + ui.fatal(actor_missing_error) + exit 1 + end + @name = params[1] + if @name.nil? + show_usage + ui.fatal(keyname_missing_error) + exit 1 + end + end + end + end +end diff --git a/lib/chef/knife/client_key_edit.rb b/lib/chef/knife/client_key_edit.rb new file mode 100644 index 0000000000..1de45f4ca2 --- /dev/null +++ b/lib/chef/knife/client_key_edit.rb @@ -0,0 +1,80 @@ +# +# Author:: Tyler Cloke (tyler@chef.io) +# Copyright:: Copyright (c) 2015 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/knife/key_edit_base' + +class Chef + class Knife + # Implements knife client key edit using Chef::Knife::KeyEdit + # as a service class. + # + # @author Tyler Cloke + # + # @attr_reader [String] actor the name of the client that this key is for + class ClientKeyEdit < Knife + include Chef::Knife::KeyEditBase + + banner 'knife client key edit CLIENT KEYNAME (options)' + + attr_reader :actor + + def initialize(argv=[]) + super(argv) + @service_object = nil + end + + def run + apply_params!(@name_args) + service_object.run + end + + def actor_field_name + 'client' + end + + def service_object + @service_object ||= Chef::Knife::KeyEdit.new(@name, @actor, actor_field_name, ui, config) + end + + def actor_missing_error + 'You must specify a client name' + end + + def keyname_missing_error + 'You must specify a key name' + end + + def apply_params!(params) + @actor = params[0] + if @actor.nil? + show_usage + ui.fatal(actor_missing_error) + exit 1 + end + @name = params[1] + if @name.nil? + show_usage + ui.fatal(keyname_missing_error) + exit 1 + end + end + end + end +end + diff --git a/lib/chef/knife/client_key_list.rb b/lib/chef/knife/client_key_list.rb new file mode 100644 index 0000000000..f6f29ae03f --- /dev/null +++ b/lib/chef/knife/client_key_list.rb @@ -0,0 +1,69 @@ +# +# Author:: Tyler Cloke (tyler@chef.io) +# Copyright:: Copyright (c) 2015 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/knife/key_list_base' + +class Chef + class Knife + # Implements knife user key list using Chef::Knife::KeyList + # as a service class. + # + # @author Tyler Cloke + # + # @attr_reader [String] actor the name of the client that this key is for + class ClientKeyList < Knife + include Chef::Knife::KeyListBase + + banner "knife client key list CLIENT (options)" + + attr_reader :actor + + def initialize(argv=[]) + super(argv) + @service_object = nil + end + + def run + apply_params!(@name_args) + service_object.run + end + + def list_method + :list_by_client + end + + def actor_missing_error + 'You must specify a client name' + end + + def service_object + @service_object ||= Chef::Knife::KeyList.new(@actor, list_method, ui, config) + end + + def apply_params!(params) + @actor = params[0] + if @actor.nil? + show_usage + ui.fatal(actor_missing_error) + exit 1 + end + end + end + end +end diff --git a/lib/chef/knife/client_key_show.rb b/lib/chef/knife/client_key_show.rb new file mode 100644 index 0000000000..c39a279000 --- /dev/null +++ b/lib/chef/knife/client_key_show.rb @@ -0,0 +1,76 @@ +# +# Author:: Tyler Cloke (tyler@chef.io) +# Copyright:: Copyright (c) 2015 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' + +class Chef + class Knife + # Implements knife client key show using Chef::Knife::KeyShow + # as a service class. + # + # @author Tyler Cloke + # + # @attr_reader [String] actor the name of the client that this key is for + class ClientKeyShow < Knife + banner "knife client key show CLIENT KEYNAME (options)" + + attr_reader :actor + + def initialize(argv=[]) + super(argv) + @service_object = nil + end + + def run + apply_params!(@name_args) + service_object.run + end + + def load_method + :load_by_client + end + + def actor_missing_error + 'You must specify a client name' + end + + def keyname_missing_error + 'You must specify a key name' + end + + def service_object + @service_object ||= Chef::Knife::KeyShow.new(@name, @actor, load_method, ui) + end + + def apply_params!(params) + @actor = params[0] + if @actor.nil? + show_usage + ui.fatal(actor_missing_error) + exit 1 + end + @name = params[1] + if @name.nil? + show_usage + ui.fatal(keyname_missing_error) + exit 1 + end + end + end + end +end diff --git a/lib/chef/knife/core/generic_presenter.rb b/lib/chef/knife/core/generic_presenter.rb index f3ea0f0d6c..2df9603faa 100644 --- a/lib/chef/knife/core/generic_presenter.rb +++ b/lib/chef/knife/core/generic_presenter.rb @@ -181,7 +181,7 @@ class Chef # Must check :[] before attr because spec can include # `keys` - want the key named `keys`, not a list of # available keys. - elsif data.respond_to?(:[]) + elsif data.respond_to?(:[]) && data.has_key?(attr) data = data[attr] elsif data.respond_to?(attr.to_sym) data = data.send(attr.to_sym) diff --git a/lib/chef/knife/core/subcommand_loader.rb b/lib/chef/knife/core/subcommand_loader.rb index 1f59515f44..a8705c724f 100644 --- a/lib/chef/knife/core/subcommand_loader.rb +++ b/lib/chef/knife/core/subcommand_loader.rb @@ -23,7 +23,7 @@ class Chef class SubcommandLoader MATCHES_CHEF_GEM = %r{/chef-[\d]+\.[\d]+\.[\d]+}.freeze - MATCHES_THIS_CHEF_GEM = %r{/chef-#{Chef::VERSION}/}.freeze + MATCHES_THIS_CHEF_GEM = %r{/chef-#{Chef::VERSION}(-\w+)?(-\w+)?/}.freeze attr_reader :chef_config_dir attr_reader :env diff --git a/lib/chef/knife/key_create.rb b/lib/chef/knife/key_create.rb new file mode 100644 index 0000000000..5ee36e9793 --- /dev/null +++ b/lib/chef/knife/key_create.rb @@ -0,0 +1,108 @@ +# +# Author:: Tyler Cloke (<tyler@chef.io>) +# Copyright:: Copyright (c) 2015 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/key' +require 'chef/json_compat' +require 'chef/exceptions' + +class Chef + class Knife + # Service class for UserKeyCreate and ClientKeyCreate, + # Implements common functionality of knife [user | org client] key create. + # + # @author Tyler Cloke + # + # @attr_accessor [Hash] cli input, see UserKeyCreate and ClientKeyCreate for what could populate it + class KeyCreate + + attr_accessor :config + + def initialize(actor, actor_field_name, ui, config) + @actor = actor + @actor_field_name = actor_field_name + @ui = ui + @config = config + end + + def public_key_or_key_name_error_msg +<<EOS +You must pass either --public-key or --key-name, or both. +If you only pass --public-key, a key name will be generated from the fingerprint of your key. +If you only pass --key-name, a key pair will be generated by the server. +EOS + end + + def edit_data(key) + @ui.edit_data(key) + end + + def display_info(input) + @ui.info(input) + end + + def display_private_key(private_key) + @ui.msg(private_key) + end + + def output_private_key_to_file(private_key) + File.open(@config[:file], "w") do |f| + f.print(private_key) + end + end + + def create_key_from_hash(output) + Chef::Key.from_hash(output).create + end + + def run + key = Chef::Key.new(@actor, @actor_field_name) + if !@config[:public_key] && !@config[:key_name] + raise Chef::Exceptions::KeyCommandInputError, public_key_or_key_name_error_msg + elsif !@config[:public_key] + key.create_key(true) + end + + if @config[:public_key] + key.public_key(File.read(File.expand_path(@config[:public_key]))) + end + + if @config[:key_name] + key.name(@config[:key_name]) + end + + if @config[:expiration_date] + key.expiration_date(@config[:expiration_date]) + else + key.expiration_date("infinity") + end + + output = edit_data(key) + key = create_key_from_hash(output) + + display_info("Created key: #{key.name}") + if key.private_key + if @config[:file] + output_private_key_to_file(key.private_key) + else + display_private_key(key.private_key) + end + end + end + end + end +end diff --git a/lib/chef/knife/key_create_base.rb b/lib/chef/knife/key_create_base.rb new file mode 100644 index 0000000000..da31f70d1d --- /dev/null +++ b/lib/chef/knife/key_create_base.rb @@ -0,0 +1,50 @@ +# +# Author:: Tyler Cloke (<tyler@chef.io>) +# Copyright:: Copyright (c) 2015 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 Knife + # Extendable module that class_eval's common options into UserKeyCreate and ClientKeyCreate + # + # @author Tyler Cloke + module KeyCreateBase + def self.included(includer) + includer.class_eval do + option :public_key, + :short => "-p FILENAME", + :long => "--public-key FILENAME", + :description => "Public key for newly created key. If not passed, the server will create a key pair for you, but you must pass --key-name NAME in that case." + + option :file, + :short => "-f FILE", + :long => "--file FILE", + :description => "Write the private key to a file, if you requested the server to create one." + + option :key_name, + :short => "-k NAME", + :long => "--key-name NAME", + :description => "The name for your key. If you do not pass a name, you must pass --public-key, and the name will default to the fingerprint of the public key passed." + + option :expiration_date, + :short => "-e DATE", + :long => "--expiration-date DATE", + :description => "Optionally pass the expiration date for the key in ISO 8601 fomatted string: YYYY-MM-DDTHH:MM:SSZ e.g. 2013-12-24T21:00:00Z. Defaults to infinity if not passed. UTC timezone assumed." + end + end + end + end +end diff --git a/lib/chef/knife/key_delete.rb b/lib/chef/knife/key_delete.rb new file mode 100644 index 0000000000..fb996cff17 --- /dev/null +++ b/lib/chef/knife/key_delete.rb @@ -0,0 +1,55 @@ +# +# Author:: Tyler Cloke (<tyler@chef.io>) +# Copyright:: Copyright (c) 2015 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/key' + +class Chef + class Knife + # Service class for UserKeyDelete and ClientKeyDelete, used to delete keys. + # Implements common functionality of knife [user | org client] key delete. + # + # @author Tyler Cloke + # + # @attr_accessor [Hash] cli input, see UserKeyDelete and ClientKeyDelete for what could populate it + class KeyDelete + def initialize(name, actor, actor_field_name, ui) + @name = name + @actor = actor + @actor_field_name = actor_field_name + @ui = ui + end + + def confirm! + @ui.confirm("Do you really want to delete the key named #{@name} for the #{@actor_field_name} named #{@actor}") + end + + def print_destroyed + @ui.info("Destroyed key named #{@name} for the #{@actor_field_name} named #{@actor}") + end + + def run + key = Chef::Key.new(@actor, @actor_field_name) + key.name(@name) + confirm! + key.destroy + print_destroyed + end + + end + end +end diff --git a/lib/chef/knife/key_edit.rb b/lib/chef/knife/key_edit.rb new file mode 100644 index 0000000000..48ae344e6e --- /dev/null +++ b/lib/chef/knife/key_edit.rb @@ -0,0 +1,114 @@ +# +# Author:: Tyler Cloke (<tyler@chef.io>) +# Copyright:: Copyright (c) 2015 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/key' +require 'chef/json_compat' +require 'chef/exceptions' + +class Chef + class Knife + # Service class for UserKeyEdit and ClientKeyEdit, + # Implements common functionality of knife [user | org client] key edit. + # + # @author Tyler Cloke + # + # @attr_accessor [Hash] cli input, see UserKeyEdit and ClientKeyEdit for what could populate it + class KeyEdit + + attr_accessor :config + + def initialize(original_name, actor, actor_field_name, ui, config) + @original_name = original_name + @actor = actor + @actor_field_name = actor_field_name + @ui = ui + @config = config + end + + def public_key_and_create_key_error_msg +<<EOS +You passed both --public-key and --create-key. Only pass one, or the other, or neither. +Do not pass either if you do not want to change the public_key field of your key. +Pass --public-key if you want to update the public_key field of your key from a specific public key. +Pass --create-key if you want the server to generate a new key and use that to update the public_key field of your key. +EOS + end + + def edit_data(key) + @ui.edit_data(key) + end + + def display_info(input) + @ui.info(input) + end + + def display_private_key(private_key) + @ui.msg(private_key) + end + + def output_private_key_to_file(private_key) + File.open(@config[:file], "w") do |f| + f.print(private_key) + end + end + + def update_key_from_hash(output) + Chef::Key.from_hash(output).update(@original_name) + end + + def run + key = Chef::Key.new(@actor, @actor_field_name) + if @config[:public_key] && @config[:create_key] + raise Chef::Exceptions::KeyCommandInputError, public_key_and_create_key_error_msg + end + + if @config[:create_key] + key.create_key(true) + end + + if @config[:public_key] + key.public_key(File.read(File.expand_path(@config[:public_key]))) + end + + if @config[:key_name] + key.name(@config[:key_name]) + else + key.name(@original_name) + end + + if @config[:expiration_date] + key.expiration_date(@config[:expiration_date]) + end + + output = edit_data(key) + key = update_key_from_hash(output) + + to_display = "Updated key: #{key.name}" + to_display << " (formally #{@original_name})" if key.name != @original_name + display_info(to_display) + if key.private_key + if @config[:file] + output_private_key_to_file(key.private_key) + else + display_private_key(key.private_key) + end + end + end + end + end +end diff --git a/lib/chef/knife/key_edit_base.rb b/lib/chef/knife/key_edit_base.rb new file mode 100644 index 0000000000..bb5a951a5b --- /dev/null +++ b/lib/chef/knife/key_edit_base.rb @@ -0,0 +1,55 @@ +# +# Author:: Tyler Cloke (<tyler@chef.io>) +# Copyright:: Copyright (c) 2015 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 Knife + # Extendable module that class_eval's common options into UserKeyEdit and ClientKeyEdit + # + # @author Tyler Cloke + module KeyEditBase + def self.included(includer) + includer.class_eval do + option :public_key, + :short => "-p FILENAME", + :long => "--public-key FILENAME", + :description => "Replace the public_key field from a file on disk. If not passed, the public_key field will not change." + + option :create_key, + :short => "-c", + :long => "--create-key", + :description => "Replace the public_key field with a key generated by the server. The private key will be returned." + + option :file, + :short => "-f FILE", + :long => "--file FILE", + :description => "Write the private key to a file, if you requested the server to create one via --create-key." + + option :key_name, + :short => "-k NAME", + :long => "--key-name NAME", + :description => "The new name for your key. Pass if you wish to update the name field of your key." + + option :expiration_date, + :short => "-e DATE", + :long => "--expiration-date DATE", + :description => "Updates the expiration_date field of your key if passed. Pass in ISO 8601 fomatted string: YYYY-MM-DDTHH:MM:SSZ e.g. 2013-12-24T21:00:00Z or infinity. UTC timezone assumed." + end + end + end + end +end diff --git a/lib/chef/knife/key_list.rb b/lib/chef/knife/key_list.rb new file mode 100644 index 0000000000..e96a27161f --- /dev/null +++ b/lib/chef/knife/key_list.rb @@ -0,0 +1,88 @@ +# +# Author:: Tyler Cloke (<tyler@chef.io>) +# Copyright:: Copyright (c) 2015 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/key' +require 'chef/json_compat' +require 'chef/exceptions' + +class Chef + class Knife + # Service class for UserKeyList and ClientKeyList, used to list keys. + # Implements common functionality of knife [user | org client] key list. + # + # @author Tyler Cloke + # + # @attr_accessor [Hash] cli input, see UserKeyList and ClientKeyList for what could populate it + class KeyList + + attr_accessor :config + + def initialize(actor, list_method, ui, config) + @actor = actor + @list_method = list_method + @ui = ui + @config = config + end + + def expired_and_non_expired_msg +<<EOS +You cannot pass both --only-expired and --only-non-expired. +Please pass one or none. +EOS + end + + def display_info(string) + @ui.output(string) + end + + def colorize(string) + @ui.color(string, :cyan) + end + + def run + if @config[:only_expired] && @config[:only_non_expired] + raise Chef::Exceptions::KeyCommandInputError, expired_and_non_expired_msg + end + + # call proper list function + keys = Chef::Key.send(@list_method, @actor) + if @config[:with_details] + max_length = 0 + keys.each do |key| + key['name'] = key['name'] + ":" + max_length = key['name'].length if key['name'].length > max_length + end + keys.each do |key| + next if !key['expired'] && @config[:only_expired] + next if key['expired'] && @config[:only_non_expired] + display = "#{colorize(key['name'].ljust(max_length))} #{key['uri']}" + display = "#{display} (expired)" if key["expired"] + display_info(display) + end + else + keys.each do |key| + next if !key['expired'] && @config[:only_expired] + next if key['expired'] && @config[:only_non_expired] + display_info(key['name']) + end + end + end + + end + end +end diff --git a/lib/chef/knife/key_list_base.rb b/lib/chef/knife/key_list_base.rb new file mode 100644 index 0000000000..861db0d75a --- /dev/null +++ b/lib/chef/knife/key_list_base.rb @@ -0,0 +1,45 @@ +# +# Author:: Tyler Cloke (<tyler@chef.io>) +# Copyright:: Copyright (c) 2015 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 Knife + # Extendable module that class_eval's common options into UserKeyList and ClientKeyList + # + # @author Tyler Cloke + module KeyListBase + def self.included(includer) + includer.class_eval do + option :with_details, + :short => "-w", + :long => "--with-details", + :description => "Show corresponding URIs and whether the key has expired or not." + + option :only_expired, + :short => "-e", + :long => "--only-expired", + :description => "Only show expired keys." + + option :only_non_expired, + :short => "-n", + :long => "--only-non-expired", + :description => "Only show non-expired keys." + end + end + end + end +end diff --git a/lib/chef/knife/key_show.rb b/lib/chef/knife/key_show.rb new file mode 100644 index 0000000000..522f7a1a77 --- /dev/null +++ b/lib/chef/knife/key_show.rb @@ -0,0 +1,53 @@ +# +# Author:: Tyler Cloke (<tyler@chef.io>) +# Copyright:: Copyright (c) 2015 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/key' +require 'chef/json_compat' +require 'chef/exceptions' + +class Chef + class Knife + # Service class for UserKeyShow and ClientKeyShow, used to show keys. + # Implements common functionality of knife [user | org client] key show. + # + # @author Tyler Cloke + # + # @attr_accessor [Hash] cli input, see UserKeyShow and ClientKeyShow for what could populate it + class KeyShow + + attr_accessor :config + + def initialize(name, actor, load_method, ui) + @name = name + @actor = actor + @load_method = load_method + @ui = ui + end + + def display_output(key) + @ui.output(@ui.format_for_display(key)) + end + + def run + key = Chef::Key.send(@load_method, @actor, @name) + key.public_key(key.public_key.strip) + display_output(key) + end + end + end +end diff --git a/lib/chef/knife/osc_user_create.rb b/lib/chef/knife/osc_user_create.rb new file mode 100644 index 0000000000..c368296040 --- /dev/null +++ b/lib/chef/knife/osc_user_create.rb @@ -0,0 +1,97 @@ +# +# Author:: Steven Danna (<steve@opscode.com>) +# Copyright:: Copyright (c) 2012 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/knife' + +# DEPRECATION NOTE +# This code only remains to support users still operating with +# Open Source Chef Server 11 and should be removed once support +# for OSC 11 ends. New development should occur in user_create.rb. +class Chef + class Knife + class OscUserCreate < Knife + + deps do + require 'chef/osc_user' + require 'chef/json_compat' + end + + option :file, + :short => "-f FILE", + :long => "--file FILE", + :description => "Write the private key to a file" + + option :admin, + :short => "-a", + :long => "--admin", + :description => "Create the user as an admin", + :boolean => true + + option :user_password, + :short => "-p PASSWORD", + :long => "--password PASSWORD", + :description => "Password for newly created user", + :default => "" + + option :user_key, + :long => "--user-key FILENAME", + :description => "Public key for newly created user. By default a key will be created for you." + + banner "knife osc_user create USER (options)" + + def run + @user_name = @name_args[0] + + if @user_name.nil? + show_usage + ui.fatal("You must specify a user name") + exit 1 + end + + if config[:user_password].length == 0 + show_usage + ui.fatal("You must specify a non-blank password") + exit 1 + end + + user = Chef::OscUser.new + user.name(@user_name) + user.admin(config[:admin]) + user.password config[:user_password] + + if config[:user_key] + user.public_key File.read(File.expand_path(config[:user_key])) + end + + output = edit_data(user) + user = Chef::OscUser.from_hash(output).create + + ui.info("Created #{user}") + if user.private_key + if config[:file] + File.open(config[:file], "w") do |f| + f.print(user.private_key) + end + else + ui.msg user.private_key + end + end + end + end + end +end diff --git a/lib/chef/knife/osc_user_delete.rb b/lib/chef/knife/osc_user_delete.rb new file mode 100644 index 0000000000..d6fbd4a6a9 --- /dev/null +++ b/lib/chef/knife/osc_user_delete.rb @@ -0,0 +1,51 @@ +# +# Author:: Steven Danna (<steve@opscode.com>) +# Copyright:: Copyright (c) 2012 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/knife' + +# DEPRECATION NOTE +# This code only remains to support users still operating with +# Open Source Chef Server 11 and should be removed once support +# for OSC 11 ends. New development should occur in the user_delete.rb. + +class Chef + class Knife + class OscUserDelete < Knife + + deps do + require 'chef/osc_user' + require 'chef/json_compat' + end + + banner "knife osc_user delete USER (options)" + + def run + @user_name = @name_args[0] + + if @user_name.nil? + show_usage + ui.fatal("You must specify a user name") + exit 1 + end + + delete_object(Chef::OscUser, @user_name) + end + + end + end +end diff --git a/lib/chef/knife/osc_user_edit.rb b/lib/chef/knife/osc_user_edit.rb new file mode 100644 index 0000000000..4c38674d08 --- /dev/null +++ b/lib/chef/knife/osc_user_edit.rb @@ -0,0 +1,58 @@ +# +# Author:: Steven Danna (<steve@opscode.com>) +# Copyright:: Copyright (c) 2012 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/knife' + +# DEPRECATION NOTE +# This code only remains to support users still operating with +# Open Source Chef Server 11 and should be removed once support +# for OSC 11 ends. New development should occur in user_edit.rb. + +class Chef + class Knife + class OscUserEdit < Knife + + deps do + require 'chef/osc_user' + require 'chef/json_compat' + end + + banner "knife osc_user edit USER (options)" + + def run + @user_name = @name_args[0] + + if @user_name.nil? + show_usage + ui.fatal("You must specify a user name") + exit 1 + end + + original_user = Chef::OscUser.load(@user_name).to_hash + edited_user = edit_data(original_user) + if original_user != edited_user + user = Chef::OscUser.from_hash(edited_user) + user.update + ui.msg("Saved #{user}.") + else + ui.msg("User unchaged, not saving.") + end + end + end + end +end diff --git a/lib/chef/knife/osc_user_list.rb b/lib/chef/knife/osc_user_list.rb new file mode 100644 index 0000000000..92f049cd19 --- /dev/null +++ b/lib/chef/knife/osc_user_list.rb @@ -0,0 +1,47 @@ +# +# Author:: Steven Danna (<steve@opscode.com>) +# Copyright:: Copyright (c) 2012 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/knife' + +# DEPRECATION NOTE +# This code only remains to support users still operating with +# Open Source Chef Server 11 and should be removed once support +# for OSC 11 ends. New development should occur in user_list.rb. + +class Chef + class Knife + class OscUserList < Knife + + deps do + require 'chef/osc_user' + require 'chef/json_compat' + end + + banner "knife osc_user list (options)" + + option :with_uri, + :short => "-w", + :long => "--with-uri", + :description => "Show corresponding URIs" + + def run + output(format_list_for_display(Chef::OscUser.list)) + end + end + end +end diff --git a/lib/chef/knife/osc_user_reregister.rb b/lib/chef/knife/osc_user_reregister.rb new file mode 100644 index 0000000000..a71e0aa677 --- /dev/null +++ b/lib/chef/knife/osc_user_reregister.rb @@ -0,0 +1,64 @@ +# +# Author:: Steven Danna (<steve@opscode.com>) +# Copyright:: Copyright (c) 2012 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/knife' + +# DEPRECATION NOTE +# This code only remains to support users still operating with +# Open Source Chef Server 11 and should be removed once support +# for OSC 11 ends. New development should occur in user_reregister.rb. + +class Chef + class Knife + class OscUserReregister < Knife + + deps do + require 'chef/osc_user' + require 'chef/json_compat' + end + + banner "knife osc_user reregister USER (options)" + + option :file, + :short => "-f FILE", + :long => "--file FILE", + :description => "Write the private key to a file" + + def run + @user_name = @name_args[0] + + if @user_name.nil? + show_usage + ui.fatal("You must specify a user name") + exit 1 + end + + user = Chef::OscUser.load(@user_name).reregister + Chef::Log.debug("Updated user data: #{user.inspect}") + key = user.private_key + if config[:file] + File.open(config[:file], "w") do |f| + f.print(key) + end + else + ui.msg key + end + end + end + end +end diff --git a/lib/chef/knife/osc_user_show.rb b/lib/chef/knife/osc_user_show.rb new file mode 100644 index 0000000000..6a41ddae88 --- /dev/null +++ b/lib/chef/knife/osc_user_show.rb @@ -0,0 +1,54 @@ +# +# Author:: Steven Danna (<steve@opscode.com>) +# Copyright:: Copyright (c) 2009 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/knife' + +# DEPRECATION NOTE +# This code only remains to support users still operating with +# Open Source Chef Server 11 and should be removed once support +# for OSC 11 ends. New development should occur in user_show.rb. + +class Chef + class Knife + class OscUserShow < Knife + + include Knife::Core::MultiAttributeReturnOption + + deps do + require 'chef/osc_user' + require 'chef/json_compat' + end + + banner "knife osc_user show USER (options)" + + def run + @user_name = @name_args[0] + + if @user_name.nil? + show_usage + ui.fatal("You must specify a user name") + exit 1 + end + + user = Chef::OscUser.load(@user_name) + output(format_for_display(user)) + end + + end + end +end diff --git a/lib/chef/knife/ssh.rb b/lib/chef/knife/ssh.rb index 656baf5e8f..68e01cf94f 100644 --- a/lib/chef/knife/ssh.rb +++ b/lib/chef/knife/ssh.rb @@ -160,6 +160,31 @@ class Chef session_from_list(list) end + def get_ssh_attribute(node) + # Order of precedence for ssh target + # 1) command line attribute + # 2) configuration file + # 3) cloud attribute + # 4) fqdn + if config[:attribute] + Chef::Log.debug("Using node attribute '#{config[:attribute]}' as the ssh target") + attribute = config[:attribute] + elsif Chef::Config[:knife][:ssh_attribute] + Chef::Log.debug("Using node attribute #{Chef::Config[:knife][:ssh_attribute]}") + attribute = Chef::Config[:knife][:ssh_attribute] + elsif node[:cloud] && + node[:cloud][:public_hostname] && + !node[:cloud][:public_hostname].empty? + Chef::Log.debug("Using node attribute 'cloud[:public_hostname]' automatically as the ssh target") + attribute = 'cloud.public_hostname' + else + # falling back to default of fqdn + Chef::Log.debug("Using node attribute 'fqdn' as the ssh target") + attribute = "fqdn" + end + attribute + end + def search_nodes list = Array.new query = Chef::Search::Query.new @@ -168,25 +193,9 @@ class Chef # 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[:attribute_from_cli] - if config[:attribute_from_cli] - Chef::Log.debug("Using node attribute '#{config[:attribute_from_cli]}' from the command line as the ssh target") - host = extract_nested_value(item, config[:attribute_from_cli]) - elsif item[:cloud] && - item[:cloud][:public_hostname] && - !item[:cloud][:public_hostname].empty? - Chef::Log.debug("Using node attribute 'cloud[:public_hostname]' automatically as the ssh target") - host = item[:cloud][:public_hostname] - else - # ssh attribute from a configuration file or the default will land here - Chef::Log.debug("Using node attribute '#{config[:attribute]}' as the ssh target") - host = extract_nested_value(item, config[:attribute]) - end # next if we couldn't find the specified attribute in the # returned node object + host = extract_nested_value(item,get_ssh_attribute(item)) next if host.nil? ssh_port = item[:cloud].nil? ? nil : item[:cloud][:public_ssh_port] srv = [host, ssh_port] @@ -416,16 +425,6 @@ class Chef end end - def configure_attribute - # Setting 'knife[:ssh_attribute] = "foo"' in knife.rb => Chef::Config[:knife][:ssh_attribute] == 'foo' - # Running 'knife ssh -a foo' => both Chef::Config[:knife][:ssh_attribute] && config[:attribute] == foo - # Thus we can differentiate between a config file value and a command line override at this point by checking config[:attribute] - # We can tell here if fqdn was passed from the command line, rather than being the default, by checking config[:attribute] - # However, after here, we cannot tell these things, so we must preserve config[:attribute] - config[:attribute_from_cli] = config[:attribute] - config[:attribute] = (config[:attribute_from_cli] || Chef::Config[:knife][:ssh_attribute] || "fqdn").strip - end - def cssh cssh_cmd = nil %w[csshX cssh].each do |cmd| @@ -499,7 +498,6 @@ class Chef @longest = 0 - configure_attribute configure_user configure_password configure_identity_file diff --git a/lib/chef/knife/user_create.rb b/lib/chef/knife/user_create.rb index 4130f06878..e73f6be8b6 100644 --- a/lib/chef/knife/user_create.rb +++ b/lib/chef/knife/user_create.rb @@ -1,6 +1,7 @@ # -# Author:: Steven Danna (<steve@opscode.com>) -# Copyright:: Copyright (c) 2012 Opscode, Inc. +# Author:: Steven Danna (<steve@chef.io>) +# Author:: Tyler Cloke (<tyler@chef.io>) +# Copyright:: Copyright (c) 2012, 2015 Chef Software, Inc. # License:: Apache License, Version 2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,11 +18,14 @@ # require 'chef/knife' +require 'chef/knife/osc_user_create' class Chef class Knife class UserCreate < Knife + attr_accessor :user_field + deps do require 'chef/user' require 'chef/json_compat' @@ -30,63 +34,118 @@ class Chef option :file, :short => "-f FILE", :long => "--file FILE", - :description => "Write the private key to a file" + :description => "Write the private key to a file if the server generated one." + + option :user_key, + :long => "--user-key FILENAME", + :description => "Set the initial default key for the user from a file on disk (cannot pass with --prevent-keygen)." + + option :prevent_keygen, + :short => "-k", + :long => "--prevent-keygen", + :description => "API V1 only. Prevent server from generating a default key pair for you. Cannot be passed with --user-key.", + :boolean => true option :admin, :short => "-a", :long => "--admin", - :description => "Create the user as an admin", + :description => "DEPRECATED: Open Source Chef 11 only. Create the user as an admin.", :boolean => true option :user_password, :short => "-p PASSWORD", :long => "--password PASSWORD", - :description => "Password for newly created user", + :description => "DEPRECATED: Open Source Chef 11 only. Password for newly created user.", :default => "" - option :user_key, - :long => "--user-key FILENAME", - :description => "Public key for newly created user. By default a key will be created for you." + banner "knife user create USERNAME DISPLAY_NAME FIRST_NAME LAST_NAME EMAIL PASSWORD (options)" + + def user + @user_field ||= Chef::User.new + end + + def create_user_from_hash(hash) + Chef::User.from_hash(hash).create + end + + def osc_11_warning +<<-EOF +IF YOU ARE USING CHEF SERVER 12+, PLEASE FOLLOW THE INSTRUCTIONS UNDER knife user create --help. +You only passed a single argument to knife user create. +For backwards compatibility, when only a single argument is passed, +knife user create assumes you want Open Source 11 Server user creation. +knife user create for Open Source 11 Server is being deprecated. +Open Source 11 Server user commands now live under the knife osc_user namespace. +For backwards compatibility, we will forward this request to knife osc_user create. +If you are using an Open Source 11 Server, please use that command to avoid this warning. +EOF + end - banner "knife user create USER (options)" + def run_osc_11_user_create + # run osc_user_create with our input + ARGV.delete("user") + ARGV.unshift("osc_user") + Chef::Knife.run(ARGV, Chef::Application::Knife.options) + end def run - @user_name = @name_args[0] + # DEPRECATION NOTE + # Remove this if statement and corrosponding code post OSC 11 support. + # + # If only 1 arg is passed, assume OSC 11 case. + if @name_args.length == 1 + ui.warn(osc_11_warning) + run_osc_11_user_create + else # EC / CS 12 user create - if @user_name.nil? - show_usage - ui.fatal("You must specify a user name") - exit 1 - end + test_mandatory_field(@name_args[0], "username") + user.username @name_args[0] - if config[:user_password].length == 0 - show_usage - ui.fatal("You must specify a non-blank password") - exit 1 - end + test_mandatory_field(@name_args[1], "display name") + user.display_name @name_args[1] - user = Chef::User.new - user.name(@user_name) - user.admin(config[:admin]) - user.password config[:user_password] + test_mandatory_field(@name_args[2], "first name") + user.first_name @name_args[2] - if config[:user_key] - user.public_key File.read(File.expand_path(config[:user_key])) - end + test_mandatory_field(@name_args[3], "last name") + user.last_name @name_args[3] + + test_mandatory_field(@name_args[4], "email") + user.email @name_args[4] + + test_mandatory_field(@name_args[5], "password") + user.password @name_args[5] + + if config[:user_key] && config[:prevent_keygen] + show_usage + ui.fatal("You cannot pass --user-key and --prevent-keygen") + exit 1 + end + + if !config[:prevent_keygen] && !config[:user_key] + user.create_key(true) + end - output = edit_data(user) - user = Chef::User.from_hash(output).create + if config[:user_key] + user.public_key File.read(File.expand_path(config[:user_key])) + end - ui.info("Created #{user}") - if user.private_key - if config[:file] - File.open(config[:file], "w") do |f| - f.print(user.private_key) + output = edit_data(user) + final_user = create_user_from_hash(output) + + ui.info("Created #{user}") + if final_user.private_key + if config[:file] + File.open(config[:file], "w") do |f| + f.print(final_user.private_key) + end + else + ui.msg final_user.private_key end - else - ui.msg user.private_key end end + + end end end diff --git a/lib/chef/knife/user_delete.rb b/lib/chef/knife/user_delete.rb index b7af11bec8..803be6b90c 100644 --- a/lib/chef/knife/user_delete.rb +++ b/lib/chef/knife/user_delete.rb @@ -29,6 +29,40 @@ class Chef banner "knife user delete USER (options)" + def osc_11_warning +<<-EOF +The Chef Server you are using does not support the username field. +This means it is an Open Source 11 Server. +knife user delete for Open Source 11 Server is being deprecated. +Open Source 11 Server user commands now live under the knife osc_user namespace. +For backwards compatibility, we will forward this request to knife osc_user delete. +If you are using an Open Source 11 Server, please use that command to avoid this warning. +EOF + end + + def run_osc_11_user_delete + # run osc_user_delete with our input + ARGV.delete("user") + ARGV.unshift("osc_user") + Chef::Knife.run(ARGV, Chef::Application::Knife.options) + end + + # DEPRECATION NOTE + # Delete this override method after OSC 11 support is dropped + def delete_object(user_name) + confirm("Do you really want to delete #{user_name}") + + if Kernel.block_given? + object = block.call + else + object = Chef::User.load(user_name) + object.destroy + end + + output(format_for_display(object)) if config[:print_after] + self.msg("Deleted #{user_name}") + end + def run @user_name = @name_args[0] @@ -38,9 +72,25 @@ class Chef exit 1 end - delete_object(Chef::User, @user_name) - end + # DEPRECATION NOTE + # + # Below is modification of Chef::Knife.delete_object to detect OSC 11 server. + # When OSC 11 is deprecated, simply delete all this and go back to: + # + # delete_object(Chef::User, @user_name) + # + # Also delete our override of delete_object above + object = Chef::User.load(@user_name) + # OSC 11 case + if object.username.nil? + ui.warn(osc_11_warning) + run_osc_11_user_delete + else # proceed with EC / CS delete + delete_object(@user_name) + end + + end end end end diff --git a/lib/chef/knife/user_edit.rb b/lib/chef/knife/user_edit.rb index ae319c8872..dd2fc02743 100644 --- a/lib/chef/knife/user_edit.rb +++ b/lib/chef/knife/user_edit.rb @@ -29,6 +29,24 @@ class Chef banner "knife user edit USER (options)" + def osc_11_warning +<<-EOF +The Chef Server you are using does not support the username field. +This means it is an Open Source 11 Server. +knife user edit for Open Source 11 Server is being deprecated. +Open Source 11 Server user commands now live under the knife oc_user namespace. +For backwards compatibility, we will forward this request to knife osc_user edit. +If you are using an Open Source 11 Server, please use that command to avoid this warning. +EOF + end + + def run_osc_11_user_edit + # run osc_user_create with our input + ARGV.delete("user") + ARGV.unshift("osc_user") + Chef::Knife.run(ARGV, Chef::Application::Knife.options) + end + def run @user_name = @name_args[0] @@ -39,14 +57,26 @@ class Chef end original_user = Chef::User.load(@user_name).to_hash - edited_user = edit_data(original_user) - if original_user != edited_user - user = Chef::User.from_hash(edited_user) - user.update - ui.msg("Saved #{user}.") - else - ui.msg("User unchaged, not saving.") + + # DEPRECATION NOTE + # Remove this if statement and corrosponding code post OSC 11 support. + # + # if username is nil, we are in the OSC 11 case, + # forward to deprecated command + if original_user["username"].nil? + ui.warn(osc_11_warning) + run_osc_11_user_edit + else # EC / CS 12 user create + edited_user = edit_data(original_user) + if original_user != edited_user + user = Chef::User.from_hash(edited_user) + user.update + ui.msg("Saved #{user}.") + else + ui.msg("User unchaged, not saving.") + end end + end end end diff --git a/lib/chef/knife/user_key_create.rb b/lib/chef/knife/user_key_create.rb new file mode 100644 index 0000000000..5ed699ff5b --- /dev/null +++ b/lib/chef/knife/user_key_create.rb @@ -0,0 +1,69 @@ +# +# Author:: Tyler Cloke (tyler@chef.io) +# Copyright:: Copyright (c) 2015 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/knife/key_create_base' + +class Chef + class Knife + # Implements knife user key create using Chef::Knife::KeyCreate + # as a service class. + # + # @author Tyler Cloke + # + # @attr_reader [String] actor the name of the user that this key is for + class UserKeyCreate < Knife + include Chef::Knife::KeyCreateBase + + banner 'knife user key create USER (options)' + + attr_reader :actor + + def initialize(argv=[]) + super(argv) + @service_object = nil + end + + def run + apply_params!(@name_args) + service_object.run + end + + def actor_field_name + 'user' + end + + def service_object + @service_object ||= Chef::Knife::KeyCreate.new(@actor, actor_field_name, ui, config) + end + + def actor_missing_error + 'You must specify a user name' + end + + def apply_params!(params) + @actor = params[0] + if @actor.nil? + show_usage + ui.fatal(actor_missing_error) + exit 1 + end + end + end + end +end diff --git a/lib/chef/knife/user_key_delete.rb b/lib/chef/knife/user_key_delete.rb new file mode 100644 index 0000000000..6db1c9f552 --- /dev/null +++ b/lib/chef/knife/user_key_delete.rb @@ -0,0 +1,76 @@ +# +# Author:: Tyler Cloke (tyler@chef.io) +# Copyright:: Copyright (c) 2015 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' + +class Chef + class Knife + # Implements knife user key delete using Chef::Knife::KeyDelete + # as a service class. + # + # @author Tyler Cloke + # + # @attr_reader [String] actor the name of the client that this key is for + class UserKeyDelete < Knife + banner "knife user key delete USER KEYNAME (options)" + + attr_reader :actor + + def initialize(argv=[]) + super(argv) + @service_object = nil + end + + def run + apply_params!(@name_args) + service_object.run + end + + def actor_field_name + 'user' + end + + def actor_missing_error + 'You must specify a user name' + end + + def keyname_missing_error + 'You must specify a key name' + end + + def service_object + @service_object ||= Chef::Knife::KeyDelete.new(@name, @actor, actor_field_name, ui) + end + + def apply_params!(params) + @actor = params[0] + if @actor.nil? + show_usage + ui.fatal(actor_missing_error) + exit 1 + end + @name = params[1] + if @name.nil? + show_usage + ui.fatal(keyname_missing_error) + exit 1 + end + end + end + end +end diff --git a/lib/chef/knife/user_key_edit.rb b/lib/chef/knife/user_key_edit.rb new file mode 100644 index 0000000000..0c35332523 --- /dev/null +++ b/lib/chef/knife/user_key_edit.rb @@ -0,0 +1,80 @@ +# +# Author:: Tyler Cloke (tyler@chef.io) +# Copyright:: Copyright (c) 2015 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/knife/key_edit_base' + +class Chef + class Knife + # Implements knife user key edit using Chef::Knife::KeyEdit + # as a service class. + # + # @author Tyler Cloke + # + # @attr_reader [String] actor the name of the user that this key is for + class UserKeyEdit < Knife + include Chef::Knife::KeyEditBase + + banner 'knife user key edit USER KEYNAME (options)' + + attr_reader :actor + + def initialize(argv=[]) + super(argv) + @service_object = nil + end + + def run + apply_params!(@name_args) + service_object.run + end + + def actor_field_name + 'user' + end + + def service_object + @service_object ||= Chef::Knife::KeyEdit.new(@name, @actor, actor_field_name, ui, config) + end + + def actor_missing_error + 'You must specify a user name' + end + + def keyname_missing_error + 'You must specify a key name' + end + + def apply_params!(params) + @actor = params[0] + if @actor.nil? + show_usage + ui.fatal(actor_missing_error) + exit 1 + end + @name = params[1] + if @name.nil? + show_usage + ui.fatal(keyname_missing_error) + exit 1 + end + end + end + end +end + diff --git a/lib/chef/knife/user_key_list.rb b/lib/chef/knife/user_key_list.rb new file mode 100644 index 0000000000..a73f59c86f --- /dev/null +++ b/lib/chef/knife/user_key_list.rb @@ -0,0 +1,69 @@ +# +# Author:: Tyler Cloke (tyler@chef.io) +# Copyright:: Copyright (c) 2015 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/knife/key_list_base' + +class Chef + class Knife + # Implements knife user key list using Chef::Knife::KeyList + # as a service class. + # + # @author Tyler Cloke + # + # @attr_reader [String] actor the name of the client that this key is for + class UserKeyList < Knife + include Chef::Knife::KeyListBase + + banner "knife user key list USER (options)" + + attr_reader :actor + + def initialize(argv=[]) + super(argv) + @service_object = nil + end + + def run + apply_params!(@name_args) + service_object.run + end + + def list_method + :list_by_user + end + + def actor_missing_error + 'You must specify a user name' + end + + def service_object + @service_object ||= Chef::Knife::KeyList.new(@actor, list_method, ui, config) + end + + def apply_params!(params) + @actor = params[0] + if @actor.nil? + show_usage + ui.fatal(actor_missing_error) + exit 1 + end + end + end + end +end diff --git a/lib/chef/knife/user_key_show.rb b/lib/chef/knife/user_key_show.rb new file mode 100644 index 0000000000..b8453dd28e --- /dev/null +++ b/lib/chef/knife/user_key_show.rb @@ -0,0 +1,76 @@ +# +# Author:: Tyler Cloke (tyler@chef.io) +# Copyright:: Copyright (c) 2015 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' + +class Chef + class Knife + # Implements knife user key show using Chef::Knife::KeyShow + # as a service class. + # + # @author Tyler Cloke + # + # @attr_reader [String] actor the name of the client that this key is for + class UserKeyShow < Knife + banner "knife user key show USER KEYNAME (options)" + + attr_reader :actor + + def initialize(argv=[]) + super(argv) + @service_object = nil + end + + def run + apply_params!(@name_args) + service_object.run + end + + def load_method + :load_by_user + end + + def actor_missing_error + 'You must specify a user name' + end + + def keyname_missing_error + 'You must specify a key name' + end + + def service_object + @service_object ||= Chef::Knife::KeyShow.new(@name, @actor, load_method, ui) + end + + def apply_params!(params) + @actor = params[0] + if @actor.nil? + show_usage + ui.fatal(actor_missing_error) + exit 1 + end + @name = params[1] + if @name.nil? + show_usage + ui.fatal(keyname_missing_error) + exit 1 + end + end + end + end +end diff --git a/lib/chef/knife/user_list.rb b/lib/chef/knife/user_list.rb index 5d2e735a73..7ae43dadc9 100644 --- a/lib/chef/knife/user_list.rb +++ b/lib/chef/knife/user_list.rb @@ -18,6 +18,8 @@ require 'chef/knife' +# NOTE: only knife user command that is backwards compatible with OSC 11, +# so no deprecation warnings are necessary. class Chef class Knife class UserList < Knife @@ -37,6 +39,7 @@ class Chef def run output(format_list_for_display(Chef::User.list)) end + end end end diff --git a/lib/chef/knife/user_reregister.rb b/lib/chef/knife/user_reregister.rb index 946150e6e4..eab2245025 100644 --- a/lib/chef/knife/user_reregister.rb +++ b/lib/chef/knife/user_reregister.rb @@ -29,6 +29,24 @@ class Chef banner "knife user reregister USER (options)" + def osc_11_warning +<<-EOF +The Chef Server you are using does not support the username field. +This means it is an Open Source 11 Server. +knife user reregister for Open Source 11 Server is being deprecated. +Open Source 11 Server user commands now live under the knife osc_user namespace. +For backwards compatibility, we will forward this request to knife osc_user reregister. +If you are using an Open Source 11 Server, please use that command to avoid this warning. +EOF + end + + def run_osc_11_user_reregister + # run osc_user_edit with our input + ARGV.delete("user") + ARGV.unshift("osc_user") + Chef::Knife.run(ARGV, Chef::Application::Knife.options) + end + option :file, :short => "-f FILE", :long => "--file FILE", @@ -43,16 +61,29 @@ class Chef exit 1 end - user = Chef::User.load(@user_name).reregister - Chef::Log.debug("Updated user data: #{user.inspect}") - key = user.private_key - if config[:file] - File.open(config[:file], "w") do |f| - f.print(key) + user = Chef::User.load(@user_name) + + # DEPRECATION NOTE + # Remove this if statement and corrosponding code post OSC 11 support. + # + # if username is nil, we are in the OSC 11 case, + # forward to deprecated command + if user.username.nil? + ui.warn(osc_11_warning) + run_osc_11_user_reregister + else # EC / CS 12 case + user.reregister + Chef::Log.debug("Updated user data: #{user.inspect}") + key = user.private_key + if config[:file] + File.open(config[:file], "w") do |f| + f.print(key) + end + else + ui.msg key end - else - ui.msg key end + end end end diff --git a/lib/chef/knife/user_show.rb b/lib/chef/knife/user_show.rb index 61ea101e4c..f5e81e9972 100644 --- a/lib/chef/knife/user_show.rb +++ b/lib/chef/knife/user_show.rb @@ -31,6 +31,24 @@ class Chef banner "knife user show USER (options)" + def osc_11_warning +<<-EOF +The Chef Server you are using does not support the username field. +This means it is an Open Source 11 Server. +knife user show for Open Source 11 Server is being deprecated. +Open Source 11 Server user commands now live under the knife osc_user namespace. +For backwards compatibility, we will forward this request to knife osc_user show. +If you are using an Open Source 11 Server, please use that command to avoid this warning. +EOF + end + + def run_osc_11_user_show + # run osc_user_edit with our input + ARGV.delete("user") + ARGV.unshift("osc_user") + Chef::Knife.run(ARGV, Chef::Application::Knife.options) + end + def run @user_name = @name_args[0] @@ -41,7 +59,18 @@ class Chef end user = Chef::User.load(@user_name) - output(format_for_display(user)) + + # DEPRECATION NOTE + # Remove this if statement and corrosponding code post OSC 11 support. + # + # if username is nil, we are in the OSC 11 case, + # forward to deprecated command + if user.username.nil? + ui.warn(osc_11_warning) + run_osc_11_user_show + else + output(format_for_display(user)) + end end end diff --git a/lib/chef/log.rb b/lib/chef/log.rb index 682afcea4b..9b27778a40 100644 --- a/lib/chef/log.rb +++ b/lib/chef/log.rb @@ -21,6 +21,8 @@ require 'logger' require 'chef/monologger' require 'chef/exceptions' require 'mixlib/log' +require 'chef/log/syslog' unless (RUBY_PLATFORM =~ /mswin|mingw|windows/) +require 'chef/log/winevt' class Chef class Log diff --git a/lib/chef/log/syslog.rb b/lib/chef/log/syslog.rb new file mode 100644 index 0000000000..0c8190797f --- /dev/null +++ b/lib/chef/log/syslog.rb @@ -0,0 +1,46 @@ +# +# Author:: Lamont Granquist (<lamont@chef.io>) +# Author:: SAWANOBORI Yukihiko (<sawanoboriyu@higanworks.com>) +# Copyright:: Copyright (c) 2015 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 'logger' +require 'syslog-logger' +require 'chef/mixin/unformatter' + +class Chef + class Log + # + # Chef::Log::Syslog class. + # usage in client.rb: + # log_location Chef::Log::Syslog.new("chef-client", ::Syslog::LOG_DAEMON) + # + class Syslog < Logger::Syslog + include Chef::Mixin::Unformatter + + attr_accessor :sync, :formatter + + def initialize(program_name = 'chef-client', facility = ::Syslog::LOG_DAEMON, logopts=nil) + super + return if defined? ::Logger::Syslog::SYSLOG + ::Logger::Syslog.const_set :SYSLOG, SYSLOG + end + + def close + end + end + end +end + diff --git a/lib/chef/log/winevt.rb b/lib/chef/log/winevt.rb new file mode 100644 index 0000000000..c5b7c3485a --- /dev/null +++ b/lib/chef/log/winevt.rb @@ -0,0 +1,99 @@ +# +# Author:: Jay Mundrawala (<jdm@chef.io>) +# +# Copyright:: 2015, Chef Software, Inc. +# +# 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/event_loggers/base' +require 'chef/platform/query_helpers' +require 'chef/mixin/unformatter' + +class Chef + class Log + # + # Chef::Log::WinEvt class. + # usage in client.rb: + # log_location Chef::Log::WinEvt.new + # + class WinEvt + # These must match those that are defined in the manifest file + INFO_EVENT_ID = 10100 + WARN_EVENT_ID = 10101 + DEBUG_EVENT_ID = 10102 + ERROR_EVENT_ID = 10103 + FATAL_EVENT_ID = 10104 + + # Since we must install the event logger, this is not really configurable + SOURCE = 'Chef' + + include Chef::Mixin::Unformatter + + attr_accessor :sync, :formatter, :level + + def initialize(eventlog=nil) + @eventlog = eventlog || ::Win32::EventLog::open('Application') + end + + def close + end + + def info(msg) + @eventlog.report_event( + :event_type => ::Win32::EventLog::INFO_TYPE, + :source => SOURCE, + :event_id => INFO_EVENT_ID, + :data => [msg] + ) + end + + def warn(msg) + @eventlog.report_event( + :event_type => ::Win32::EventLog::WARN_TYPE, + :source => SOURCE, + :event_id => WARN_EVENT_ID, + :data => [msg] + ) + end + + def debug(msg) + @eventlog.report_event( + :event_type => ::Win32::EventLog::INFO_TYPE, + :source => SOURCE, + :event_id => DEBUG_EVENT_ID, + :data => [msg] + ) + end + + def error(msg) + @eventlog.report_event( + :event_type => ::Win32::EventLog::ERROR_TYPE, + :source => SOURCE, + :event_id => ERROR_EVENT_ID, + :data => [msg] + ) + end + + def fatal(msg) + @eventlog.report_event( + :event_type => ::Win32::EventLog::ERROR_TYPE, + :source => SOURCE, + :event_id => FATAL_EVENT_ID, + :data => [msg] + ) + end + + end + end +end diff --git a/lib/chef/mixin/api_version_request_handling.rb b/lib/chef/mixin/api_version_request_handling.rb new file mode 100644 index 0000000000..20ab3bf452 --- /dev/null +++ b/lib/chef/mixin/api_version_request_handling.rb @@ -0,0 +1,66 @@ +# +# Author:: Tyler Cloke (tyler@chef.io) +# Copyright:: Copyright 2015 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 + module Mixin + module ApiVersionRequestHandling + # Input: + # exeception: + # Net::HTTPServerException that may or may not contain the x-ops-server-api-version header + # supported_client_versions: + # An array of Integers that represent the API versions the client supports. + # + # Output: + # nil: + # If the execption was not a 406 or the server does not support versioning + # Array of length zero: + # If there was no intersection between supported client versions and supported server versions + # Arrary of Integers: + # If there was an intersection of supported versions, the array returns will contain that intersection + def server_client_api_version_intersection(exception, supported_client_versions) + # return empty array unless 406 Unacceptable with proper header + return nil if exception.response.code != "406" || exception.response["x-ops-server-api-version"].nil? + + # intersection of versions the server and client support, will be of length zero if no intersection + server_supported_client_versions = Array.new + + header = Chef::JSONCompat.from_json(exception.response["x-ops-server-api-version"]) + min_server_version = Integer(header["min_version"]) + max_server_version = Integer(header["max_version"]) + + supported_client_versions.each do |version| + if version >= min_server_version && version <= max_server_version + server_supported_client_versions.push(version) + end + end + server_supported_client_versions + end + + def reregister_only_v0_supported_error_msg(max_version, min_version) + <<-EOH +The reregister command only supports server API version 0. +The server that received the request supports a min version of #{min_version} and a max version of #{max_version}. +User keys are now managed via the key rotation commmands. +Please refer to the documentation on how to manage your keys via the key rotation commands: +https://docs.chef.io/server_security.html#key-rotation +EOH + end + + end + end +end diff --git a/lib/chef/mixin/convert_to_class_name.rb b/lib/chef/mixin/convert_to_class_name.rb index 19f229fdd3..14676e5ed4 100644 --- a/lib/chef/mixin/convert_to_class_name.rb +++ b/lib/chef/mixin/convert_to_class_name.rb @@ -23,9 +23,7 @@ class Chef extend self def convert_to_class_name(str) - str = str.dup - str.gsub!(/[^A-Za-z0-9_]/,'_') - str.gsub!(/^(_+)?/,'') + str = normalize_snake_case_name(str) rname = nil regexp = %r{^(.+?)(_(.+))?$} @@ -51,6 +49,13 @@ class Chef str end + def normalize_snake_case_name(str) + str = str.dup + str.gsub!(/[^A-Za-z0-9_]/,'_') + str.gsub!(/^(_+)?/,'') + str + end + def snake_case_basename(str) with_namespace = convert_to_snake_case(str) with_namespace.split("::").last.sub(/^_/, '') @@ -58,7 +63,8 @@ class Chef def filename_to_qualified_string(base, filename) file_base = File.basename(filename, ".rb") - base.to_s + (file_base == 'default' ? '' : "_#{file_base}") + str = base.to_s + (file_base == 'default' ? '' : "_#{file_base}") + normalize_snake_case_name(str) end # Copied from rails activesupport. In ruby >= 2.0 const_get will just do this, so this can diff --git a/lib/chef/mixin/deprecation.rb b/lib/chef/mixin/deprecation.rb index 489f27c339..a3eacf75cb 100644 --- a/lib/chef/mixin/deprecation.rb +++ b/lib/chef/mixin/deprecation.rb @@ -95,6 +95,30 @@ class Chef DeprecatedInstanceVariable.new(obj, name, level) end + def deprecated_attr(name, alternative) + deprecated_attr_reader(name, alternative) + deprecated_attr_writer(name, alternative) + end + + def deprecated_attr_reader(name, alternative, level=:warn) + define_method(name) do + Chef::Log.deprecation("#{self.class}.#{name} is deprecated. Support will be removed in a future release.") + Chef::Log.deprecation(alternative) + Chef::Log.deprecation("Called from:") + caller[0..3].each {|c| Chef::Log.deprecation(c)} + instance_variable_get("@#{name}") + end + end + + def deprecated_attr_writer(name, alternative, level=:warn) + define_method("#{name}=") do |value| + Chef::Log.deprecation("Writing to #{self.class}.#{name} with #{name}= is deprecated. Support will be removed in a future release.") + Chef::Log.deprecation(alternative) + Chef::Log.deprecation("Called from:") + caller[0..3].each {|c| Chef::Log.deprecation(c)} + instance_variable_set("@#{name}", value) + end + end end end end diff --git a/lib/chef/mixin/powershell_out.rb b/lib/chef/mixin/powershell_out.rb new file mode 100644 index 0000000000..e4f29c07c4 --- /dev/null +++ b/lib/chef/mixin/powershell_out.rb @@ -0,0 +1,98 @@ +#-- +# Copyright:: Copyright (c) 2015 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/mixin/shell_out' +require 'chef/mixin/windows_architecture_helper' + +class Chef + module Mixin + module PowershellOut + include Chef::Mixin::ShellOut + include Chef::Mixin::WindowsArchitectureHelper + + # Run a command under powershell with the same API as shell_out. The + # options hash is extended to take an "architecture" flag which + # can be set to :i386 or :x86_64 to force the windows architecture. + # + # @param script [String] script to run + # @param options [Hash] options hash + # @return [Mixlib::Shellout] mixlib-shellout object + def powershell_out(*command_args) + script = command_args.first + options = command_args.last.is_a?(Hash) ? command_args.last : nil + + run_command_with_os_architecture(script, options) + end + + # Run a command under powershell with the same API as shell_out! + # (raises exceptions on errors) + # + # @param script [String] script to run + # @param options [Hash] options hash + # @return [Mixlib::Shellout] mixlib-shellout object + def powershell_out!(*command_args) + cmd = powershell_out(*command_args) + cmd.error! + cmd + end + + private + + # Helper function to run shell_out and wrap it with the correct + # flags to possibly disable WOW64 redirection (which we often need + # because chef-client runs as a 32-bit app on 64-bit windows). + # + # @param script [String] script to run + # @param options [Hash] options hash + # @return [Mixlib::Shellout] mixlib-shellout object + def run_command_with_os_architecture(script, options) + options ||= {} + options = options.dup + arch = options.delete(:architecture) + + with_os_architecture(nil, architecture: arch) do + shell_out( + build_powershell_command(script), + options + ) + end + end + + # Helper to build a powershell command around the script to run. + # + # @param script [String] script to run + # @retrurn [String] powershell command to execute + def build_powershell_command(script) + flags = [ + # Hides the copyright banner at startup. + "-NoLogo", + # Does not present an interactive prompt to the user. + "-NonInteractive", + # Does not load the Windows PowerShell profile. + "-NoProfile", + # always set the ExecutionPolicy flag + # see http://technet.microsoft.com/en-us/library/ee176961.aspx + "-ExecutionPolicy Unrestricted", + # Powershell will hang if STDIN is redirected + # http://connect.microsoft.com/PowerShell/feedback/details/572313/powershell-exe-can-hang-if-stdin-is-redirected + "-InputFormat None" + ] + + "powershell.exe #{flags.join(' ')} -Command \"#{script}\"" + end + end + end +end diff --git a/lib/chef/mixin/provides.rb b/lib/chef/mixin/provides.rb index e5bb2c2005..095e273dab 100644 --- a/lib/chef/mixin/provides.rb +++ b/lib/chef/mixin/provides.rb @@ -4,28 +4,23 @@ require 'chef/mixin/descendants_tracker' class Chef module Mixin module Provides + # TODO no longer needed, remove or deprecate? include Chef::Mixin::DescendantsTracker - def node_map - @node_map ||= Chef::NodeMap.new + def provides(short_name, opts={}, &block) + raise NotImplementedError, :provides end - def provides(short_name, opts={}, &block) - if !short_name.kind_of?(Symbol) - # YAGNI: this is probably completely unnecessary and can be removed? - Chef::Log.deprecation "Passing a non-Symbol to Chef::Resource#provides will be removed" - if short_name.kind_of?(String) - short_name.downcase! - short_name.gsub!(/\s/, "_") - end - short_name = short_name.to_sym - end - node_map.set(short_name, true, opts, &block) + # Check whether this resource provides the resource_name DSL for the given + # node. TODO remove this when we stop checking unregistered things. + def provides?(node, resource) + raise NotImplementedError, :provides? end - # provides a node on the resource (early binding) - def provides?(node, resource_name) - node_map.get(node, resource_name) + # Get the list of recipe DSL this resource is responsible for on the given + # node. + def provided_as(node) + node_map.list(node) end end end diff --git a/lib/chef/mixin/unformatter.rb b/lib/chef/mixin/unformatter.rb new file mode 100644 index 0000000000..aa5977edd7 --- /dev/null +++ b/lib/chef/mixin/unformatter.rb @@ -0,0 +1,32 @@ +# +# Author:: Jay Mundrawala (<jdm@chef.io>) +# Copyright:: Copyright (c) 2015 Chef Software +# 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 + module Mixin + module Unformatter + + def write(message) + data = message.match(/(\[.+?\] )?([\w]+):(.*)$/) + self.send(data[2].downcase.chomp.to_sym, data[3].strip) + rescue NoMethodError + self.send(:info, message) + end + + end + end +end diff --git a/lib/chef/mixin/uris.rb b/lib/chef/mixin/uris.rb new file mode 100644 index 0000000000..0136b55f6a --- /dev/null +++ b/lib/chef/mixin/uris.rb @@ -0,0 +1,44 @@ +# +# Author:: Jay Mundrawala (<jdm@chef.io>) +# Copyright:: Copyright (c) 2015 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 'uri' + +class Chef + module Mixin + module Uris + # uri_scheme? returns true if the string starts with + # scheme:// + # For example, it will match http://foo.bar.com + def uri_scheme?(source) + # From open-uri + !!(%r{\A[A-Za-z][A-Za-z0-9+\-\.]*://} =~ source) + end + + + def as_uri(source) + begin + URI.parse(source) + rescue URI::InvalidURIError + Chef::Log.warn("#{source} was an invalid URI. Trying to escape invalid characters") + URI.parse(URI.escape(source)) + end + end + + end + end +end diff --git a/lib/chef/mixin/windows_architecture_helper.rb b/lib/chef/mixin/windows_architecture_helper.rb index a0ac34f627..c5f3e1bd79 100644 --- a/lib/chef/mixin/windows_architecture_helper.rb +++ b/lib/chef/mixin/windows_architecture_helper.rb @@ -42,7 +42,7 @@ class Chef is_i386_process_on_x86_64_windows? end - def with_os_architecture(node) + def with_os_architecture(node, architecture: nil) node ||= begin os_arch = ENV['PROCESSOR_ARCHITEW6432'] || ENV['PROCESSOR_ARCHITECTURE'] @@ -51,9 +51,12 @@ class Chef n[:kernel][:machine] = os_arch == 'AMD64' ? :x86_64 : :i386 end end + + architecture ||= node_windows_architecture(node) + wow64_redirection_state = nil - if wow64_architecture_override_required?(node, node_windows_architecture(node)) + if wow64_architecture_override_required?(node, architecture) wow64_redirection_state = disable_wow64_file_redirection(node) end diff --git a/lib/chef/mixin/windows_env_helper.rb b/lib/chef/mixin/windows_env_helper.rb index 490b235065..a126801a28 100644 --- a/lib/chef/mixin/windows_env_helper.rb +++ b/lib/chef/mixin/windows_env_helper.rb @@ -21,11 +21,11 @@ require 'chef/exceptions' require 'chef/platform/query_helpers' require 'chef/win32/error' if Chef::Platform.windows? require 'chef/win32/api/system' if Chef::Platform.windows? +require 'chef/win32/api/unicode' if Chef::Platform.windows? class Chef module Mixin module WindowsEnvHelper - if Chef::Platform.windows? include Chef::ReservedNames::Win32::API::System end @@ -39,7 +39,16 @@ class Chef def broadcast_env_change flags = SMTO_BLOCK | SMTO_ABORTIFHUNG | SMTO_NOTIMEOUTIFNOTHUNG - SendMessageTimeoutA(HWND_BROADCAST, WM_SETTINGCHANGE, 0, FFI::MemoryPointer.from_string('Environment').address, flags, 5000, nil) + # for why two calls, see: + # http://stackoverflow.com/questions/4968373/why-doesnt-sendmessagetimeout-update-the-environment-variables + if ( SendMessageTimeoutA(HWND_BROADCAST, WM_SETTINGCHANGE, 0, FFI::MemoryPointer.from_string('Environment').address, flags, 5000, nil) == 0 ) + Chef::ReservedNames::Win32::Error.raise! + end + if ( SendMessageTimeoutW(HWND_BROADCAST, WM_SETTINGCHANGE, 0, FFI::MemoryPointer.from_string( + Chef::ReservedNames::Win32::Unicode.utf8_to_wide('Environment') + ).address, flags, 5000, nil) == 0 ) + Chef::ReservedNames::Win32::Error.raise! + end end def expand_path(path) diff --git a/lib/chef/mixin/wstring.rb b/lib/chef/mixin/wstring.rb new file mode 100644 index 0000000000..bb6fdf4884 --- /dev/null +++ b/lib/chef/mixin/wstring.rb @@ -0,0 +1,31 @@ +# +# Author:: Jay Mundrawala(<jdm@chef.io>) +# Copyright:: Copyright 2015 Chef Software +# 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 + module Mixin + module WideString + def wstring(str) + if str.nil? || str.encoding == Encoding::UTF_16LE + str + else + str.to_wstring + end + end + end + end +end diff --git a/lib/chef/node.rb b/lib/chef/node.rb index 9823185ede..d5078371c5 100644 --- a/lib/chef/node.rb +++ b/lib/chef/node.rb @@ -77,13 +77,30 @@ class Chef @run_state = {} end + # after the run_context has been set on the node, go through the cookbook_collection + # and setup the node[:cookbooks] attribute so that it is published in the node object + def set_cookbook_attribute + return unless run_context.cookbook_collection + run_context.cookbook_collection.each do |cookbook_name, cookbook| + automatic_attrs[:cookbooks][cookbook_name][:version] = cookbook.version + end + end + # Used by DSL def node self end def chef_server_rest - @chef_server_rest ||= Chef::REST.new(Chef::Config[:chef_server_url]) + # for saving node data we use validate_utf8: false which will not + # raise an exception on bad utf8 data, but will replace the bad + # characters and render valid JSON. + @chef_server_rest ||= Chef::REST.new( + Chef::Config[:chef_server_url], + Chef::Config[:node_name], + Chef::Config[:client_key], + validate_utf8: false, + ) end # Set the name of this Node, or return the current name. @@ -244,6 +261,7 @@ class Chef # saved back to the node and be searchable def loaded_recipe(cookbook, recipe) fully_qualified_recipe = "#{cookbook}::#{recipe}" + automatic_attrs[:recipes] << fully_qualified_recipe unless Array(self[:recipes]).include?(fully_qualified_recipe) end @@ -354,7 +372,8 @@ class Chef self.tags # make sure they're defined - automatic_attrs[:recipes] = expansion.recipes + automatic_attrs[:recipes] = expansion.recipes.with_fully_qualified_names_and_version_constraints + automatic_attrs[:expanded_run_list] = expansion.recipes.with_fully_qualified_names_and_version_constraints automatic_attrs[:roles] = expansion.roles apply_expansion_attributes(expansion) diff --git a/lib/chef/node_map.rb b/lib/chef/node_map.rb index 2ca6d9ba17..f547018a38 100644 --- a/lib/chef/node_map.rb +++ b/lib/chef/node_map.rb @@ -19,128 +19,183 @@ class Chef class NodeMap - VALID_OPTS = [ - :on_platform, - :on_platforms, - :platform, - :os, - :platform_family, - ] - - DEPRECATED_OPTS = [ - :on_platform, - :on_platforms, - ] - + # # Create a new NodeMap # def initialize @map = {} end + # # Set a key/value pair on the map with a filter. The filter must be true # when applied to the node in order to retrieve the value. # # @param key [Object] Key to store # @param value [Object] Value associated with the key # @param filters [Hash] Node filter options to apply to key retrieval + # # @yield [node] Arbitrary node filter as a block which takes a node argument + # # @return [NodeMap] Returns self for possible chaining # - def set(key, value, filters = {}, &block) - validate_filter!(filters) - deprecate_filter!(filters) + def set(key, value, platform: nil, platform_version: nil, platform_family: nil, os: nil, on_platform: nil, on_platforms: nil, canonical: nil, &block) + Chef::Log.deprecation "The on_platform option to node_map has been deprecated" if on_platform + Chef::Log.deprecation "The on_platforms option to node_map has been deprecated" if on_platforms + platform ||= on_platform || on_platforms + filters = { platform: platform, platform_version: platform_version, platform_family: platform_family, os: os } + new_matcher = { filters: filters, block: block, value: value, canonical: canonical } @map[key] ||= [] - # we match on the first value we find, so we want to unshift so that the - # last setter wins - # FIXME: need a test for this behavior - @map[key].unshift({ filters: filters, block: block, value: value }) + # Decide where to insert the matcher; the new value is preferred over + # anything more specific (see `priority_of`) and is preferred over older + # values of the same specificity. (So all other things being equal, + # newest wins.) + insert_at = nil + @map[key].each_with_index do |matcher, index| + if specificity(new_matcher) >= specificity(matcher) + insert_at = index + break + end + end + if insert_at + @map[key].insert(insert_at, new_matcher) + else + @map[key] << new_matcher + end self end + # # Get a value from the NodeMap via applying the node to the filters that # were set on the key. # - # @param node [Chef::Node] The Chef::Node object for the run + # @param node [Chef::Node] The Chef::Node object for the run, or `nil` to + # ignore all filters. # @param key [Object] Key to look up + # @param canonical [Boolean] `true` or `false` to match canonical or + # non-canonical values only. `nil` to ignore canonicality. Default: `nil` + # # @return [Object] Value # - def get(node, key) - # FIXME: real exception - raise "first argument must be a Chef::Node" unless node.is_a?(Chef::Node) - return nil unless @map.has_key?(key) - @map[key].each do |matcher| - if filters_match?(node, matcher[:filters]) && - block_matches?(node, matcher[:block]) - return matcher[:value] + def get(node, key, canonical: nil) + raise ArgumentError, "first argument must be a Chef::Node" unless node.is_a?(Chef::Node) || node.nil? + list(node, key, canonical: canonical).first + end + + # + # List all matches for the given node and key from the NodeMap, from + # most-recently added to oldest. + # + # @param node [Chef::Node] The Chef::Node object for the run, or `nil` to + # ignore all filters. + # @param key [Object] Key to look up + # @param canonical [Boolean] `true` or `false` to match canonical or + # non-canonical values only. `nil` to ignore canonicality. Default: `nil` + # + # @return [Object] Value + # + def list(node, key, canonical: nil) + raise ArgumentError, "first argument must be a Chef::Node" unless node.is_a?(Chef::Node) || node.nil? + return [] unless @map.has_key?(key) + @map[key].select do |matcher| + node_matches?(node, matcher) && canonical_matches?(canonical, matcher) + end.map { |matcher| matcher[:value] } + end + + # Seriously, don't use this, it's nearly certain to change on you + # @return remaining + # @api private + def delete_canonical(key, value) + remaining = @map[key] + if remaining + remaining.delete_if { |matcher| matcher[:canonical] && Array(matcher[:value]) == Array(value) } + if remaining.empty? + @map.delete(key) + remaining = nil end end - nil + remaining end private - # only allow valid filter options - def validate_filter!(filters) - filters.each_key do |key| - # FIXME: real exception - raise "Bad key #{key} in Chef::NodeMap filter expression" unless VALID_OPTS.include?(key) + # + # Gives a value for "how specific" the matcher is. + # Things which specify more specific filters get a higher number + # (platform_version > platform > platform_family > os); things + # with a block have higher specificity than similar things without + # a block. + # + def specificity(matcher) + if matcher[:filters][:platform_version] + specificity = 8 + elsif matcher[:filters][:platform] + specificity = 6 + elsif matcher[:filters][:platform_family] + specificity = 4 + elsif matcher[:filters][:os] + specificity = 2 + else + specificity = 0 end + specificity += 1 if matcher[:block] + specificity end - # warn on deprecated filter options - def deprecate_filter!(filters) - filters.each_key do |key| - Chef::Log.warn "The #{key} option to node_map has been deprecated" if DEPRECATED_OPTS.include?(key) - end - end + # + # Succeeds if: + # - no negative matches (!value) + # - at least one positive match (value or :all), or no positive filters + # + def matches_black_white_list?(node, filters, attribute) + # It's super common for the filter to be nil. Catch that so we don't + # spend any time here. + return true if !filters[attribute] + filter_values = Array(filters[attribute]) + value = node[attribute] - # @todo: this works fine, but is probably hard to understand - def negative_match(filter, param) - # We support strings prefaced by '!' to mean 'not'. In particular, this is most useful - # for os matching on '!windows'. - negative_matches = filter.select { |f| f[0] == '!' } - return true if !negative_matches.empty? && negative_matches.include?('!' + param) + # Split the blacklist and whitelist + blacklist, whitelist = filter_values.partition { |v| v.is_a?(String) && v.start_with?('!') } - # We support the symbol :all to match everything, for backcompat, but this can and should - # simply be ommitted. - positive_matches = filter.reject { |f| f[0] == '!' || f == :all } - return true if !positive_matches.empty? && !positive_matches.include?(param) + # If any blacklist value matches, we don't match + return false if blacklist.any? { |v| v[1..-1] == value } - # sorry double-negative: this means we pass this filter. - false + # If the whitelist is empty, or anything matches, we match. + whitelist.empty? || whitelist.any? { |v| v == :all || v == value } end - def filters_match?(node, filters) - return true if filters.empty? + def matches_version_list?(node, filters, attribute) + # It's super common for the filter to be nil. Catch that so we don't + # spend any time here. + return true if !filters[attribute] + filter_values = Array(filters[attribute]) + value = node[attribute] - # each filter is applied in turn. if any fail, then it shortcuts and returns false. - # if it passes or does not exist it succeeds and continues on. so multiple filters are - # effectively joined by 'and'. all filters can be single strings, or arrays which are - # effectively joined by 'or'. - - os_filter = [ filters[:os] ].flatten.compact - unless os_filter.empty? - return false if negative_match(os_filter, node[:os]) - end - - platform_family_filter = [ filters[:platform_family] ].flatten.compact - unless platform_family_filter.empty? - return false if negative_match(platform_family_filter, node[:platform_family]) - end - - # :on_platform and :on_platforms here are synonyms which are deprecated - platform_filter = [ filters[:platform] || filters[:on_platform] || filters[:on_platforms] ].flatten.compact - unless platform_filter.empty? - return false if negative_match(platform_filter, node[:platform]) + filter_values.empty? || + Array(filter_values).any? do |v| + Chef::VersionConstraint::Platform.new(v).include?(value) end + end - return true + def filters_match?(node, filters) + matches_black_white_list?(node, filters, :os) && + matches_black_white_list?(node, filters, :platform_family) && + matches_black_white_list?(node, filters, :platform) && + matches_version_list?(node, filters, :platform_version) end def block_matches?(node, block) return true if block.nil? block.call node end + + def node_matches?(node, matcher) + return true if !node + filters_match?(node, matcher[:filters]) && block_matches?(node, matcher[:block]) + end + + def canonical_matches?(canonical, matcher) + return true if canonical.nil? + !!canonical == !!matcher[:canonical] + end end end diff --git a/lib/chef/osc_user.rb b/lib/chef/osc_user.rb new file mode 100644 index 0000000000..52bfd11108 --- /dev/null +++ b/lib/chef/osc_user.rb @@ -0,0 +1,194 @@ +# +# Author:: Steven Danna (steve@opscode.com) +# Copyright:: Copyright 2012 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/config' +require 'chef/mixin/params_validate' +require 'chef/mixin/from_file' +require 'chef/mash' +require 'chef/json_compat' +require 'chef/search/query' + +# TODO +# DEPRECATION NOTE +# This class was previously Chef::User. It is the code to support the User object +# corrosponding to the Open Source Chef Server 11 and only still exists to support +# users still on OSC 11. +# +# Chef::User now supports Chef Server 12. +# +# New development should occur in Chef::User. +# This file and corrosponding osc_user knife files +# should be removed once client support for Open Source Chef Server 11 expires. +class Chef + class OscUser + + include Chef::Mixin::FromFile + include Chef::Mixin::ParamsValidate + + def initialize + @name = '' + @public_key = nil + @private_key = nil + @password = nil + @admin = false + end + + def name(arg=nil) + set_or_return(:name, arg, + :regex => /^[a-z0-9\-_]+$/) + end + + def admin(arg=nil) + set_or_return(:admin, + arg, :kind_of => [TrueClass, FalseClass]) + end + + def public_key(arg=nil) + set_or_return(:public_key, + arg, :kind_of => String) + end + + def private_key(arg=nil) + set_or_return(:private_key, + arg, :kind_of => String) + end + + def password(arg=nil) + set_or_return(:password, + arg, :kind_of => String) + end + + def to_hash + result = { + "name" => @name, + "public_key" => @public_key, + "admin" => @admin + } + result["private_key"] = @private_key if @private_key + result["password"] = @password if @password + result + end + + def to_json(*a) + Chef::JSONCompat.to_json(to_hash, *a) + end + + def destroy + Chef::REST.new(Chef::Config[:chef_server_url]).delete_rest("users/#{@name}") + end + + def create + payload = {:name => self.name, :admin => self.admin, :password => self.password } + payload[:public_key] = public_key if public_key + new_user =Chef::REST.new(Chef::Config[:chef_server_url]).post_rest("users", payload) + Chef::OscUser.from_hash(self.to_hash.merge(new_user)) + end + + def update(new_key=false) + payload = {:name => name, :admin => admin} + payload[:private_key] = new_key if new_key + payload[:password] = password if password + updated_user = Chef::REST.new(Chef::Config[:chef_server_url]).put_rest("users/#{name}", payload) + Chef::OscUser.from_hash(self.to_hash.merge(updated_user)) + end + + def save(new_key=false) + begin + create + rescue Net::HTTPServerException => e + if e.response.code == "409" + update(new_key) + else + raise e + end + end + end + + def reregister + r = Chef::REST.new(Chef::Config[:chef_server_url]) + reregistered_self = r.put_rest("users/#{name}", { :name => name, :admin => admin, :private_key => true }) + private_key(reregistered_self["private_key"]) + self + end + + def to_s + "user[#{@name}]" + end + + def inspect + "Chef::OscUser name:'#{name}' admin:'#{admin.inspect}'" + + "public_key:'#{public_key}' private_key:#{private_key}" + end + + # Class Methods + + def self.from_hash(user_hash) + user = Chef::OscUser.new + user.name user_hash['name'] + user.private_key user_hash['private_key'] if user_hash.key?('private_key') + user.password user_hash['password'] if user_hash.key?('password') + user.public_key user_hash['public_key'] + user.admin user_hash['admin'] + user + end + + def self.from_json(json) + Chef::OscUser.from_hash(Chef::JSONCompat.from_json(json)) + end + + class << self + alias_method :json_create, :from_json + end + + def self.list(inflate=false) + response = Chef::REST.new(Chef::Config[:chef_server_url]).get_rest('users') + users = if response.is_a?(Array) + transform_ohc_list_response(response) # OHC/OPC + else + response # OSC + end + if inflate + users.inject({}) do |user_map, (name, _url)| + user_map[name] = Chef::OscUser.load(name) + user_map + end + else + users + end + end + + def self.load(name) + response = Chef::REST.new(Chef::Config[:chef_server_url]).get_rest("users/#{name}") + Chef::OscUser.from_hash(response) + end + + # Gross. Transforms an API response in the form of: + # [ { "user" => { "username" => USERNAME }}, ...] + # into the form + # { "USERNAME" => "URI" } + def self.transform_ohc_list_response(response) + new_response = Hash.new + response.each do |u| + name = u['user']['username'] + new_response[name] = Chef::Config[:chef_server_url] + "/users/#{name}" + end + new_response + end + + private_class_method :transform_ohc_list_response + end +end diff --git a/lib/chef/platform/provider_mapping.rb b/lib/chef/platform/provider_mapping.rb index 0d7285729f..e3a894c8ac 100644 --- a/lib/chef/platform/provider_mapping.rb +++ b/lib/chef/platform/provider_mapping.rb @@ -20,13 +20,8 @@ require 'chef/log' require 'chef/exceptions' require 'chef/mixin/params_validate' require 'chef/version_constraint/platform' - -# This file depends on nearly every provider in chef, but requiring them -# directly causes circular requires resulting in uninitialized constant errors. -# Therefore, we do the includes inline rather than up top. require 'chef/provider' - class Chef class Platform @@ -34,267 +29,7 @@ class Chef attr_writer :platforms def platforms - @platforms ||= begin - require 'chef/providers' - - { - :freebsd => { - :default => { - :group => Chef::Provider::Group::Pw, - :user => Chef::Provider::User::Pw, - } - }, - :ubuntu => { - :default => { - :package => Chef::Provider::Package::Apt, - :service => Chef::Provider::Service::Debian, - }, - ">= 11.10" => { - :ifconfig => Chef::Provider::Ifconfig::Debian - } - # Chef::Provider::Service::Upstart is a candidate to be used in - # ubuntu versions >= 13.10 but it currently requires all the - # services to have an entry under /etc/init. We need to update it - # to use the service ctl apis in order to migrate to using it on - # ubuntu >= 13.10. - }, - :gcel => { - :default => { - :package => Chef::Provider::Package::Apt, - :service => Chef::Provider::Service::Debian, - } - }, - :linaro => { - :default => { - :package => Chef::Provider::Package::Apt, - :service => Chef::Provider::Service::Debian, - } - }, - :raspbian => { - :default => { - :package => Chef::Provider::Package::Apt, - :service => Chef::Provider::Service::Debian, - } - }, - :linuxmint => { - :default => { - :package => Chef::Provider::Package::Apt, - :service => Chef::Provider::Service::Upstart, - } - }, - :debian => { - :default => { - :package => Chef::Provider::Package::Apt, - :service => Chef::Provider::Service::Debian, - }, - ">= 6.0" => { - :service => Chef::Provider::Service::Insserv - }, - ">= 7.0" => { - :ifconfig => Chef::Provider::Ifconfig::Debian - } - }, - :xenserver => { - :default => { - :service => Chef::Provider::Service::Redhat, - :package => Chef::Provider::Package::Yum, - } - }, - :xcp => { - :default => { - :service => Chef::Provider::Service::Redhat, - :package => Chef::Provider::Package::Yum, - } - }, - :centos => { - :default => { - :service => Chef::Provider::Service::Systemd, - :package => Chef::Provider::Package::Yum, - :ifconfig => Chef::Provider::Ifconfig::Redhat - }, - "< 7" => { - :service => Chef::Provider::Service::Redhat - } - }, - :amazon => { - :default => { - :service => Chef::Provider::Service::Redhat, - :package => Chef::Provider::Package::Yum, - } - }, - :scientific => { - :default => { - :service => Chef::Provider::Service::Systemd, - :package => Chef::Provider::Package::Yum, - }, - "< 7" => { - :service => Chef::Provider::Service::Redhat - } - }, - :fedora => { - :default => { - :service => Chef::Provider::Service::Systemd, - :package => Chef::Provider::Package::Yum, - :ifconfig => Chef::Provider::Ifconfig::Redhat - }, - "< 15" => { - :service => Chef::Provider::Service::Redhat - } - }, - :opensuse => { - :default => { - :service => Chef::Provider::Service::Redhat, - :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 - } - }, - :suse => { - :default => { - :service => Chef::Provider::Service::Systemd, - :package => Chef::Provider::Package::Zypper, - :group => Chef::Provider::Group::Gpasswd - }, - "< 12.0" => { - :group => Chef::Provider::Group::Suse, - :service => Chef::Provider::Service::Redhat - } - }, - :oracle => { - :default => { - :service => Chef::Provider::Service::Systemd, - :package => Chef::Provider::Package::Yum, - }, - "< 7" => { - :service => Chef::Provider::Service::Redhat - } - }, - :redhat => { - :default => { - :service => Chef::Provider::Service::Systemd, - :package => Chef::Provider::Package::Yum, - :ifconfig => Chef::Provider::Ifconfig::Redhat - }, - "< 7" => { - :service => Chef::Provider::Service::Redhat - } - }, - :ibm_powerkvm => { - :default => { - :service => Chef::Provider::Service::Redhat, - :package => Chef::Provider::Package::Yum, - :ifconfig => Chef::Provider::Ifconfig::Redhat - } - }, - :cloudlinux => { - :default => { - :service => Chef::Provider::Service::Redhat, - :package => Chef::Provider::Package::Yum, - :ifconfig => Chef::Provider::Ifconfig::Redhat - } - }, - :parallels => { - :default => { - :service => Chef::Provider::Service::Redhat, - :package => Chef::Provider::Package::Yum, - :ifconfig => Chef::Provider::Ifconfig::Redhat - } - }, - :gentoo => { - :default => { - :package => Chef::Provider::Package::Portage, - :service => Chef::Provider::Service::Gentoo, - } - }, - :arch => { - :default => { - :package => Chef::Provider::Package::Pacman, - :service => Chef::Provider::Service::Systemd, - } - }, - :solaris => {}, - :openindiana => { - :default => { - :mount => Chef::Provider::Mount::Solaris, - :package => Chef::Provider::Package::Ips, - :group => Chef::Provider::Group::Usermod - } - }, - :opensolaris => { - :default => { - :mount => Chef::Provider::Mount::Solaris, - :package => Chef::Provider::Package::Ips, - :group => Chef::Provider::Group::Usermod - } - }, - :nexentacore => { - :default => { - :mount => Chef::Provider::Mount::Solaris, - :package => Chef::Provider::Package::Solaris, - :group => Chef::Provider::Group::Usermod - } - }, - :omnios => { - :default => { - :mount => Chef::Provider::Mount::Solaris, - :package => Chef::Provider::Package::Ips, - :group => Chef::Provider::Group::Usermod, - :user => Chef::Provider::User::Solaris, - } - }, - :solaris2 => { - :default => { - :mount => Chef::Provider::Mount::Solaris, - :package => Chef::Provider::Package::Ips, - :group => Chef::Provider::Group::Usermod, - :user => Chef::Provider::User::Solaris, - }, - "< 5.11" => { - :mount => Chef::Provider::Mount::Solaris, - :package => Chef::Provider::Package::Solaris, - :group => Chef::Provider::Group::Usermod, - :user => Chef::Provider::User::Solaris, - } - }, - :smartos => { - :default => { - :mount => Chef::Provider::Mount::Solaris, - :package => Chef::Provider::Package::SmartOS, - :group => Chef::Provider::Group::Usermod - } - }, - :hpux => { - :default => { - :group => Chef::Provider::Group::Usermod - } - }, - :aix => { - :default => { - :group => Chef::Provider::Group::Aix, - :mount => Chef::Provider::Mount::Aix, - :ifconfig => Chef::Provider::Ifconfig::Aix, - :package => Chef::Provider::Package::Aix, - :user => Chef::Provider::User::Aix, - :service => Chef::Provider::Service::Aix - } - }, - :exherbo => { - :default => { - :package => Chef::Provider::Package::Paludis, - :service => Chef::Provider::Service::Systemd, - } - }, - :default => { - :mount => Chef::Provider::Mount::Mount, - :user => Chef::Provider::User::Useradd, - :group => Chef::Provider::Group::Gpasswd, - :ifconfig => Chef::Provider::Ifconfig, - } - } - end + @platforms ||= { default: {} } end include Chef::Mixin::ParamsValidate @@ -304,7 +39,7 @@ class Chef name_sym = name if name.kind_of?(String) - name.downcase! + name = name.downcase name.gsub!(/\s/, "_") name_sym = name.to_sym end @@ -325,8 +60,6 @@ class Chef Chef::Log.debug("Chef::Version::Comparable does not know how to parse the platform version: #{version}") end end - else - Chef::Log.debug("Platform #{name} not found, using all defaults. (Unsupported platform?)") end provider_map end @@ -460,16 +193,20 @@ class Chef pmap.has_key?(rtkey) ? pmap[rtkey] : nil end + include Chef::Mixin::ConvertToClassName + def resource_matching_provider(platform, version, resource_type) if resource_type.kind_of?(Chef::Resource) + class_name = resource_type.class.to_s.split('::').last + begin - Chef::Provider.const_get(resource_type.class.to_s.split('::').last) + result = Chef::Provider.const_get(class_name) + Chef::Log.warn("Class Chef::Provider::#{class_name} does not declare 'provides #{convert_to_snake_case(class_name).to_sym.inspect}'.") + Chef::Log.warn("This will no longer work in Chef 13: you must use 'provides' to provide DSL.") rescue NameError - nil end - else - nil end + result end end diff --git a/lib/chef/platform/provider_priority_map.rb b/lib/chef/platform/provider_priority_map.rb index 1539f61900..9d703c9178 100644 --- a/lib/chef/platform/provider_priority_map.rb +++ b/lib/chef/platform/provider_priority_map.rb @@ -1,88 +1,25 @@ +require 'singleton' class Chef class Platform class ProviderPriorityMap include Singleton - def initialize - load_default_map - end - def get_priority_array(node, resource_name) priority_map.get(node, resource_name.to_sym) end - def set_priority_array(resource_name, priority_array, *filter) - priority(resource_name.to_sym, priority_array.to_a, *filter) + def set_priority_array(resource_name, priority_array, *filter, &block) + priority_map.set(resource_name.to_sym, Array(priority_array), *filter, &block) end - def priority(*args) - priority_map.set(*args) + # @api private + def list_handlers(node, resource_name) + priority_map.list(node, resource_name.to_sym).flatten(1).uniq end private - def load_default_map - require 'chef/providers' - - # - # Linux - # - - # default block for linux O/Sen must come before platform_family exceptions - priority :service, [ - Chef::Provider::Service::Systemd, - Chef::Provider::Service::Insserv, - Chef::Provider::Service::Redhat, - ], os: "linux" - - priority :service, [ - Chef::Provider::Service::Systemd, - Chef::Provider::Service::Arch, - ], platform_family: "arch" - - priority :service, [ - Chef::Provider::Service::Systemd, - Chef::Provider::Service::Gentoo, - ], platform_family: "gentoo" - - priority :service, [ - # we can determine what systemd supports accurately - Chef::Provider::Service::Systemd, - # on debian-ish system if an upstart script exists that must win over sysv types - Chef::Provider::Service::Upstart, - Chef::Provider::Service::Insserv, - Chef::Provider::Service::Debian, - Chef::Provider::Service::Invokercd, - ], platform_family: "debian" - - priority :service, [ - Chef::Provider::Service::Systemd, - Chef::Provider::Service::Insserv, - Chef::Provider::Service::Redhat, - ], platform_family: [ "rhel", "fedora", "suse" ] - - # - # BSDen - # - - priority :service, Chef::Provider::Service::Freebsd, os: [ "freebsd", "netbsd" ] - priority :service, Chef::Provider::Service::Openbsd, os: [ "openbsd" ] - - # - # Solaris-en - # - - priority :service, Chef::Provider::Service::Solaris, os: "solaris2" - - # - # Mac - # - - priority :service, Chef::Provider::Service::Macosx, os: "darwin" - priority :package, Chef::Provider::Package::Homebrew, os: "darwin" - end - def priority_map require 'chef/node_map' @priority_map ||= Chef::NodeMap.new diff --git a/lib/chef/platform/query_helpers.rb b/lib/chef/platform/query_helpers.rb index f7c85fbe23..b3948eac21 100644 --- a/lib/chef/platform/query_helpers.rb +++ b/lib/chef/platform/query_helpers.rb @@ -21,11 +21,7 @@ class Chef class << self def windows? - if RUBY_PLATFORM =~ /mswin|mingw|windows/ - true - else - false - end + ChefConfig.windows? end def windows_server_2003? @@ -43,6 +39,11 @@ class Chef is_server_2003 end + def supports_powershell_execution_bypass?(node) + node[:languages] && node[:languages][:powershell] && + node[:languages][:powershell][:version].to_i >= 3 + end + def supports_dsc?(node) node[:languages] && node[:languages][:powershell] && node[:languages][:powershell][:version].to_i >= 4 diff --git a/lib/chef/platform/resource_priority_map.rb b/lib/chef/platform/resource_priority_map.rb index fc43b3e7db..fb08debc53 100644 --- a/lib/chef/platform/resource_priority_map.rb +++ b/lib/chef/platform/resource_priority_map.rb @@ -1,33 +1,30 @@ +require 'singleton' + class Chef class Platform class ResourcePriorityMap include Singleton - def initialize - load_default_map + def get_priority_array(node, resource_name, canonical: nil) + priority_map.get(node, resource_name.to_sym, canonical: canonical) end - def get_priority_array(node, resource_name) - priority_map.get(node, resource_name.to_sym) + def set_priority_array(resource_name, priority_array, *filter, &block) + priority_map.set(resource_name.to_sym, Array(priority_array), *filter, &block) end - def set_priority_array(resource_name, priority_array, *filter) - priority resource_name.to_sym, priority_array.to_a, *filter + # @api private + def delete_canonical(resource_name, resource_class) + priority_map.delete_canonical(resource_name, resource_class) end - def priority(*args) - priority_map.set(*args) + # @api private + def list_handlers(*args) + priority_map.list(*args).flatten(1).uniq end private - def load_default_map - require 'chef/resources' - - # MacOSX - priority :package, Chef::Resource::HomebrewPackage, os: "darwin" - end - def priority_map require 'chef/node_map' @priority_map ||= Chef::NodeMap.new diff --git a/lib/chef/platform/service_helpers.rb b/lib/chef/platform/service_helpers.rb index dc0a808c06..d50812e687 100644 --- a/lib/chef/platform/service_helpers.rb +++ b/lib/chef/platform/service_helpers.rb @@ -42,34 +42,34 @@ class Chef # different services is NOT a design concern of this module. # def service_resource_providers - service_resource_providers = [] + @service_resource_providers ||= [].tap do |service_resource_providers| - if ::File.exist?("/usr/sbin/update-rc.d") - service_resource_providers << :debian - end + if ::File.exist?("/usr/sbin/update-rc.d") + service_resource_providers << :debian + end - if ::File.exist?("/usr/sbin/invoke-rc.d") - service_resource_providers << :invokercd - end + if ::File.exist?("/usr/sbin/invoke-rc.d") + service_resource_providers << :invokercd + end - if ::File.exist?("/sbin/insserv") - service_resource_providers << :insserv - end + if ::File.exist?("/sbin/insserv") + service_resource_providers << :insserv + end - # debian >= 6.0 has /etc/init but does not have upstart - if ::File.exist?("/etc/init") && ::File.exist?("/sbin/start") - service_resource_providers << :upstart - end + # debian >= 6.0 has /etc/init but does not have upstart + if ::File.exist?("/etc/init") && ::File.exist?("/sbin/start") + service_resource_providers << :upstart + end - if ::File.exist?("/sbin/chkconfig") - service_resource_providers << :redhat - end + if ::File.exist?("/sbin/chkconfig") + service_resource_providers << :redhat + end - if systemd_sanity_check? - service_resource_providers << :systemd - end + if systemd_sanity_check? + service_resource_providers << :systemd + end - service_resource_providers + end end def config_for_service(service_name) diff --git a/lib/chef/policy_builder/policyfile.rb b/lib/chef/policy_builder/policyfile.rb index ac25b549be..5991e3ce10 100644 --- a/lib/chef/policy_builder/policyfile.rb +++ b/lib/chef/policy_builder/policyfile.rb @@ -119,6 +119,7 @@ class Chef @node = Chef::Node.find_or_create(node_name) validate_policyfile + events.policyfile_loaded(policy) node rescue Exception => e events.node_load_failed(node_name, e, Chef::Config) diff --git a/lib/chef/provider.rb b/lib/chef/provider.rb index 65a56cf726..e50e374804 100644 --- a/lib/chef/provider.rb +++ b/lib/chef/provider.rb @@ -22,14 +22,19 @@ require 'chef/mixin/convert_to_class_name' require 'chef/mixin/enforce_ownership_and_permissions' require 'chef/mixin/why_run' require 'chef/mixin/shell_out' +require 'chef/mixin/powershell_out' require 'chef/mixin/provides' require 'chef/platform/service_helpers' require 'chef/node_map' class Chef class Provider + require 'chef/mixin/why_run' + require 'chef/mixin/shell_out' + require 'chef/mixin/provides' include Chef::Mixin::WhyRun include Chef::Mixin::ShellOut + include Chef::Mixin::PowershellOut extend Chef::Mixin::Provides # supports the given resource and action (late binding) @@ -83,6 +88,9 @@ class Chef new_resource.cookbook_name end + def check_resource_semantics! + end + def load_current_resource raise Chef::Exceptions::Override, "You must override load_current_resource in #{self.to_s}" end @@ -108,6 +116,8 @@ class Chef # TODO: it would be preferable to get the action to be executed in the # constructor... + check_resource_semantics! + # user-defined LWRPs may include unsafe load_current_resource methods that cannot be run in whyrun mode if !whyrun_mode? || whyrun_supported? load_current_resource @@ -165,6 +175,14 @@ class Chef converge_actions.add_action(descriptions, &block) end + def self.provides(short_name, opts={}, &block) + Chef.set_provider_priority_array(short_name, self, opts, &block) + end + + def self.provides?(node, resource) + Chef::ProviderResolver.new(node, resource, :nothing).provided_by?(self) + end + protected def converge_actions @@ -191,5 +209,39 @@ class Chef end end + module DeprecatedLWRPClass + def const_missing(class_name) + if deprecated_constants[class_name.to_sym] + Chef::Log.deprecation("Using an LWRP provider by its name (#{class_name}) directly is no longer supported in Chef 12 and will be removed. Use Chef::ProviderResolver.new(node, resource, action) instead.") + deprecated_constants[class_name.to_sym] + else + raise NameError, "uninitialized constant Chef::Provider::#{class_name}" + end + end + + # @api private + def register_deprecated_lwrp_class(provider_class, class_name) + # Register Chef::Provider::MyProvider with deprecation warnings if you + # try to access it + if Chef::Provider.const_defined?(class_name, false) + Chef::Log.warn "Chef::Provider::#{class_name} already exists! Cannot create deprecation class for #{provider_class}" + else + deprecated_constants[class_name.to_sym] = provider_class + end + end + + private + + def deprecated_constants + @deprecated_constants ||= {} + end + end + extend DeprecatedLWRPClass end end + +# Requiring things at the bottom breaks cycles +require 'chef/chef_class' +require 'chef/mixin/why_run' +require 'chef/resource_collection' +require 'chef/runner' diff --git a/lib/chef/provider/cron/unix.rb b/lib/chef/provider/cron/unix.rb index 0750c0420b..01c61e4253 100644 --- a/lib/chef/provider/cron/unix.rb +++ b/lib/chef/provider/cron/unix.rb @@ -20,6 +20,7 @@ require 'chef/log' require 'chef/provider' +require 'chef/provider/cron' class Chef class Provider diff --git a/lib/chef/provider/directory.rb b/lib/chef/provider/directory.rb index 416393ac60..4d5423d0e8 100644 --- a/lib/chef/provider/directory.rb +++ b/lib/chef/provider/directory.rb @@ -43,6 +43,9 @@ class Chef end def define_resource_requirements + # deep inside FAC we have to assert requirements, so call FACs hook to set that up + access_controls.define_resource_requirements + requirements.assert(:create) do |a| # Make sure the parent dir exists, or else fail. # for why run, print a message explaining the potential error. diff --git a/lib/chef/provider/dsc_resource.rb b/lib/chef/provider/dsc_resource.rb index 2812c154c6..5fa84a21e9 100644 --- a/lib/chef/provider/dsc_resource.rb +++ b/lib/chef/provider/dsc_resource.rb @@ -121,7 +121,14 @@ class Chef # however Invoke-DscResource is not correctly writing to that # stream and instead just dumping to stdout @converge_description = result.stdout - result.return_value[0]["InDesiredState"] + + if result.return_value.is_a?(Array) + # WMF Feb 2015 Preview + result.return_value[0]["InDesiredState"] + else + # WMF April 2015 Preview + result.return_value["InDesiredState"] + end end def set_resource diff --git a/lib/chef/provider/file.rb b/lib/chef/provider/file.rb index c070d29458..5ed7c6ac5b 100644 --- a/lib/chef/provider/file.rb +++ b/lib/chef/provider/file.rb @@ -26,8 +26,10 @@ require 'fileutils' require 'chef/scan_access_control' require 'chef/mixin/checksum' require 'chef/mixin/file_class' +require 'chef/mixin/enforce_ownership_and_permissions' require 'chef/util/backup' require 'chef/util/diff' +require 'chef/util/selinux' require 'chef/deprecation/provider/file' require 'chef/deprecation/warnings' require 'chef/file_content_management/deploy' @@ -386,10 +388,11 @@ class Chef def update_file_contents do_backup unless needs_creating? - deployment_strategy.deploy(tempfile.path, ::File.realpath(@new_resource.path)) - Chef::Log.info("#{@new_resource} updated file contents #{@new_resource.path}") + deployment_strategy.deploy(tempfile.path, ::File.realpath(new_resource.path)) + Chef::Log.info("#{new_resource} updated file contents #{new_resource.path}") if managing_content? - @new_resource.checksum(checksum(@new_resource.path)) # for reporting + # save final checksum for reporting. + new_resource.final_checksum = checksum(new_resource.path) end end diff --git a/lib/chef/provider/group/aix.rb b/lib/chef/provider/group/aix.rb index 6ac9d03357..92bb8cb225 100644 --- a/lib/chef/provider/group/aix.rb +++ b/lib/chef/provider/group/aix.rb @@ -23,6 +23,7 @@ class Chef class Provider class Group class Aix < Chef::Provider::Group::Groupadd + provides :group, platform: 'aix' def required_binaries [ "/usr/bin/mkgroup", diff --git a/lib/chef/provider/group/dscl.rb b/lib/chef/provider/group/dscl.rb index d7e8f2e827..9775ac8270 100644 --- a/lib/chef/provider/group/dscl.rb +++ b/lib/chef/provider/group/dscl.rb @@ -21,7 +21,7 @@ class Chef class Group class Dscl < Chef::Provider::Group - provides :group, os: "darwin" + provides :group, os: 'darwin' def dscl(*args) host = "." diff --git a/lib/chef/provider/group/gpasswd.rb b/lib/chef/provider/group/gpasswd.rb index 521affac11..432c524acd 100644 --- a/lib/chef/provider/group/gpasswd.rb +++ b/lib/chef/provider/group/gpasswd.rb @@ -22,6 +22,7 @@ class Chef class Provider class Group class Gpasswd < Chef::Provider::Group::Groupadd + provides :group def load_current_resource super diff --git a/lib/chef/provider/group/groupmod.rb b/lib/chef/provider/group/groupmod.rb index f9299546c8..82b68b8672 100644 --- a/lib/chef/provider/group/groupmod.rb +++ b/lib/chef/provider/group/groupmod.rb @@ -21,7 +21,7 @@ class Chef class Group class Groupmod < Chef::Provider::Group - provides :group, os: "netbsd" + provides :group, os: 'netbsd' def load_current_resource super diff --git a/lib/chef/provider/group/pw.rb b/lib/chef/provider/group/pw.rb index 7a66ab4d69..f877ed2424 100644 --- a/lib/chef/provider/group/pw.rb +++ b/lib/chef/provider/group/pw.rb @@ -20,6 +20,7 @@ class Chef class Provider class Group class Pw < Chef::Provider::Group + provides :group, platform: 'freebsd' def load_current_resource super diff --git a/lib/chef/provider/group/suse.rb b/lib/chef/provider/group/suse.rb index 7ac2831d02..b47ea33e80 100644 --- a/lib/chef/provider/group/suse.rb +++ b/lib/chef/provider/group/suse.rb @@ -22,6 +22,8 @@ class Chef class Provider class Group class Suse < Chef::Provider::Group::Groupadd + provides :group, platform: 'opensuse', platform_version: '< 12.3' + provides :group, platform: 'suse', platform_version: '< 12.0' def load_current_resource super diff --git a/lib/chef/provider/group/usermod.rb b/lib/chef/provider/group/usermod.rb index e50e13c443..d78d42d6e1 100644 --- a/lib/chef/provider/group/usermod.rb +++ b/lib/chef/provider/group/usermod.rb @@ -23,7 +23,8 @@ class Chef class Group class Usermod < Chef::Provider::Group::Groupadd - provides :group, os: "openbsd" + provides :group, os: %w(openbsd solaris2 hpux) + provides :group, platform: "opensuse" def load_current_resource super diff --git a/lib/chef/provider/group/windows.rb b/lib/chef/provider/group/windows.rb index 54e49b0e06..46d8afc7f6 100644 --- a/lib/chef/provider/group/windows.rb +++ b/lib/chef/provider/group/windows.rb @@ -26,7 +26,7 @@ class Chef class Group class Windows < Chef::Provider::Group - provides :group, os: "windows" + provides :group, os: 'windows' def initialize(new_resource,run_context) super diff --git a/lib/chef/provider/ifconfig.rb b/lib/chef/provider/ifconfig.rb index 06080c90c3..468e1ec639 100644 --- a/lib/chef/provider/ifconfig.rb +++ b/lib/chef/provider/ifconfig.rb @@ -39,6 +39,8 @@ require 'erb' class Chef class Provider class Ifconfig < Chef::Provider + provides :ifconfig + include Chef::Mixin::ShellOut include Chef::Mixin::Command diff --git a/lib/chef/provider/ifconfig/aix.rb b/lib/chef/provider/ifconfig/aix.rb index 8fead44bc6..25c3de3040 100644 --- a/lib/chef/provider/ifconfig/aix.rb +++ b/lib/chef/provider/ifconfig/aix.rb @@ -22,6 +22,7 @@ class Chef class Provider class Ifconfig class Aix < Chef::Provider::Ifconfig + provides :ifconfig, platform: %w(aix) def load_current_resource @current_resource = Chef::Resource::Ifconfig.new(@new_resource.name) diff --git a/lib/chef/provider/ifconfig/debian.rb b/lib/chef/provider/ifconfig/debian.rb index 7589971143..1e6863c8b5 100644 --- a/lib/chef/provider/ifconfig/debian.rb +++ b/lib/chef/provider/ifconfig/debian.rb @@ -23,6 +23,8 @@ class Chef class Provider class Ifconfig class Debian < Chef::Provider::Ifconfig + provides :ifconfig, platform: %w(ubuntu), platform_version: '>= 11.10' + provides :ifconfig, platform: %w(debian), platform_version: '>= 7.0' INTERFACES_FILE = "/etc/network/interfaces" INTERFACES_DOT_D_DIR = "/etc/network/interfaces.d" diff --git a/lib/chef/provider/ifconfig/redhat.rb b/lib/chef/provider/ifconfig/redhat.rb index ef35b0e012..ee053d1e52 100644 --- a/lib/chef/provider/ifconfig/redhat.rb +++ b/lib/chef/provider/ifconfig/redhat.rb @@ -22,6 +22,7 @@ class Chef class Provider class Ifconfig class Redhat < Chef::Provider::Ifconfig + provides :ifconfig, platform_family: %w(fedora rhel) def initialize(new_resource, run_context) super(new_resource, run_context) diff --git a/lib/chef/provider/lwrp_base.rb b/lib/chef/provider/lwrp_base.rb index 492ddda6da..b5efbb284d 100644 --- a/lib/chef/provider/lwrp_base.rb +++ b/lib/chef/provider/lwrp_base.rb @@ -19,6 +19,7 @@ # require 'chef/provider' +require 'chef/dsl/include_recipe' class Chef class Provider @@ -69,9 +70,6 @@ class Chef end - extend Chef::Mixin::ConvertToClassName - extend Chef::Mixin::FromFile - include Chef::DSL::Recipe # These were previously provided by Chef::Mixin::RecipeDefinitionDSLCore. @@ -80,71 +78,95 @@ class Chef include Chef::DSL::PlatformIntrospection include Chef::DSL::DataQuery - def self.build_from_file(cookbook_name, filename, run_context) - provider_class = nil - provider_name = filename_to_qualified_string(cookbook_name, filename) + # Allow include_recipe from within LWRP provider code + include Chef::DSL::IncludeRecipe + + # no-op `load_current_resource`. Allows simple LWRP providers to work + # without defining this method explicitly (silences + # Chef::Exceptions::Override exception) + def load_current_resource + end + + # class methods + class <<self + include Chef::Mixin::ConvertToClassName + include Chef::Mixin::FromFile + + def build_from_file(cookbook_name, filename, run_context) + if LWRPBase.loaded_lwrps[filename] + Chef::Log.info("LWRP provider #{filename} from cookbook #{cookbook_name} has already been loaded! Skipping the reload.") + return loaded_lwrps[filename] + end - class_name = convert_to_class_name(provider_name) + resource_name = filename_to_qualified_string(cookbook_name, filename) - if Chef::Provider.const_defined?(class_name, false) - Chef::Log.info("#{class_name} light-weight provider is already initialized -- Skipping loading #{filename}!") - Chef::Log.debug("Overriding already defined LWRPs is not supported anymore starting with Chef 12.") - provider_class = Chef::Provider.const_get(class_name) - else + # We load the class first to give it a chance to set its own name provider_class = Class.new(self) - Chef::Provider.const_set(class_name, provider_class) + provider_class.provides resource_name.to_sym provider_class.class_from_file(filename) - Chef::Log.debug("Loaded contents of #{filename} into a provider named #{provider_name} defined in Chef::Provider::#{class_name}") - end - provider_class - end + # Respect resource_name set inside the LWRP + provider_class.instance_eval do + define_singleton_method(:to_s) do + "LWRP provider #{resource_name} from cookbook #{cookbook_name}" + end + define_singleton_method(:inspect) { to_s } + end - # Enables inline evaluation of resources in provider actions. - # - # Without this option, any resources declared inside the LWRP are added - # to the resource collection after the current position at the time the - # action is executed. Because they are added to the primary resource - # collection for the chef run, they can notify other resources outside - # the LWRP, and potentially be notified by resources outside the LWRP - # (but this is complicated by the fact that they don't exist until the - # provider executes). In this mode, it is impossible to correctly set the - # updated_by_last_action flag on the parent LWRP resource, since it - # executes and returns before its component resources are run. - # - # With this option enabled, each action creates a temporary run_context - # with its own resource collection, evaluates the action's code in that - # context, and then converges the resources created. If any resources - # were updated, then this provider's new_resource will be marked updated. - # - # In this mode, resources created within the LWRP cannot interact with - # external resources via notifies, though notifications to other - # resources within the LWRP will work. Delayed notifications are executed - # at the conclusion of the provider's action, *not* at the end of the - # main chef run. - # - # This mode of evaluation is experimental, but is believed to be a better - # set of tradeoffs than the append-after mode, so it will likely become - # the default in a future major release of Chef. - # - def self.use_inline_resources - extend InlineResources::ClassMethods - include InlineResources - end + Chef::Log.debug("Loaded contents of #{filename} into provider #{resource_name} (#{provider_class})") + + LWRPBase.loaded_lwrps[filename] = true - # DSL for defining a provider's actions. - def self.action(name, &block) - define_method("action_#{name}") do - instance_eval(&block) + Chef::Provider.register_deprecated_lwrp_class(provider_class, convert_to_class_name(resource_name)) + + provider_class end - end - # no-op `load_current_resource`. Allows simple LWRP providers to work - # without defining this method explicitly (silences - # Chef::Exceptions::Override exception) - def load_current_resource - end + # Enables inline evaluation of resources in provider actions. + # + # Without this option, any resources declared inside the LWRP are added + # to the resource collection after the current position at the time the + # action is executed. Because they are added to the primary resource + # collection for the chef run, they can notify other resources outside + # the LWRP, and potentially be notified by resources outside the LWRP + # (but this is complicated by the fact that they don't exist until the + # provider executes). In this mode, it is impossible to correctly set the + # updated_by_last_action flag on the parent LWRP resource, since it + # executes and returns before its component resources are run. + # + # With this option enabled, each action creates a temporary run_context + # with its own resource collection, evaluates the action's code in that + # context, and then converges the resources created. If any resources + # were updated, then this provider's new_resource will be marked updated. + # + # In this mode, resources created within the LWRP cannot interact with + # external resources via notifies, though notifications to other + # resources within the LWRP will work. Delayed notifications are executed + # at the conclusion of the provider's action, *not* at the end of the + # main chef run. + # + # This mode of evaluation is experimental, but is believed to be a better + # set of tradeoffs than the append-after mode, so it will likely become + # the default in a future major release of Chef. + # + def use_inline_resources + extend InlineResources::ClassMethods + include InlineResources + end + + # DSL for defining a provider's actions. + def action(name, &block) + define_method("action_#{name}") do + instance_eval(&block) + end + end + + protected + def loaded_lwrps + @loaded_lwrps ||= {} + end + end end end end diff --git a/lib/chef/provider/mount.rb b/lib/chef/provider/mount.rb index 1631d87033..2039e9ae51 100644 --- a/lib/chef/provider/mount.rb +++ b/lib/chef/provider/mount.rb @@ -24,7 +24,6 @@ require 'chef/provider' class Chef class Provider class Mount < Chef::Provider - include Chef::Mixin::ShellOut attr_accessor :unmount_retries diff --git a/lib/chef/provider/mount/aix.rb b/lib/chef/provider/mount/aix.rb index 0d7e11a1b8..4ad7b24c15 100644 --- a/lib/chef/provider/mount/aix.rb +++ b/lib/chef/provider/mount/aix.rb @@ -22,6 +22,7 @@ class Chef class Provider class Mount class Aix < Chef::Provider::Mount::Mount + provides :mount, platform: %w(aix) # Override for aix specific handling def initialize(new_resource, run_context) diff --git a/lib/chef/provider/mount/mount.rb b/lib/chef/provider/mount/mount.rb index 0a6e269d2d..ef074166a9 100644 --- a/lib/chef/provider/mount/mount.rb +++ b/lib/chef/provider/mount/mount.rb @@ -24,6 +24,8 @@ class Chef class Mount class Mount < Chef::Provider::Mount + provides :mount + def initialize(new_resource, run_context) super @real_device = nil diff --git a/lib/chef/provider/mount/solaris.rb b/lib/chef/provider/mount/solaris.rb index d8cec24138..deb04d4d7b 100644 --- a/lib/chef/provider/mount/solaris.rb +++ b/lib/chef/provider/mount/solaris.rb @@ -27,6 +27,8 @@ class Chef class Mount # Mount Solaris File systems class Solaris < Chef::Provider::Mount + provides :mount, platform: %w(openindiana opensolaris nexentacore omnios solaris2 smartos) + extend Forwardable VFSTAB = '/etc/vfstab'.freeze diff --git a/lib/chef/provider/ohai.rb b/lib/chef/provider/ohai.rb index a6b5ab5daa..b7f4aa704b 100644 --- a/lib/chef/provider/ohai.rb +++ b/lib/chef/provider/ohai.rb @@ -21,6 +21,7 @@ require 'ohai' class Chef class Provider class Ohai < Chef::Provider + provides :ohai def whyrun_supported? true diff --git a/lib/chef/provider/package.rb b/lib/chef/provider/package.rb index 2e8e29981b..9d534ec414 100644 --- a/lib/chef/provider/package.rb +++ b/lib/chef/provider/package.rb @@ -43,6 +43,12 @@ class Chef true end + def check_resource_semantics! + if new_resource.package_name.is_a?(Array) && new_resource.source != nil + raise Chef::Exceptions::InvalidResourceSpecification, "You may not specify both multipackage and source" + end + end + def load_current_resource end @@ -464,10 +470,7 @@ class Chef # @return [Array] new_version(s) as an array def new_version_array - @new_version_array ||= - [ new_resource.version ].flatten.map do |v| - ( v.nil? || v.empty? ) ? nil : v - end + [ new_resource.version ].flatten.map { |v| v.to_s.empty? ? nil : v } end # @todo: extract apt/dpkg specific preseeding to a helper class @@ -487,6 +490,61 @@ class Chef false end end + + # Set provider priority + require 'chef/chef_class' + require 'chef/provider/package/dpkg' + require 'chef/provider/package/homebrew' + require 'chef/provider/package/macports' + require 'chef/provider/package/apt' + require 'chef/provider/package/yum' + require 'chef/provider/package/zypper' + require 'chef/provider/package/portage' + require 'chef/provider/package/pacman' + require 'chef/provider/package/ips' + require 'chef/provider/package/solaris' + require 'chef/provider/package/smartos' + require 'chef/provider/package/aix' + require 'chef/provider/package/paludis' + + Chef.set_provider_priority_array :package, [ Homebrew, Macports ], os: "darwin" + + Chef.set_provider_priority_array :package, Apt, platform_family: "debian" + Chef.set_provider_priority_array :package, Yum, platform_family: %w(rhel fedora) + Chef.set_provider_priority_array :package, Zypper, platform_family: "suse" + Chef.set_provider_priority_array :package, Portage, platform: "gentoo" + Chef.set_provider_priority_array :package, Pacman, platform: "arch" + Chef.set_provider_priority_array :package, Ips, platform: %w(openindiana opensolaris omnios solaris2) + Chef.set_provider_priority_array :package, Solaris, platform: "nexentacore" + Chef.set_provider_priority_array :package, Solaris, platform: "solaris2", platform_version: '< 5.11' + + Chef.set_provider_priority_array :package, SmartOS, platform: "smartos" + Chef.set_provider_priority_array :package, Aix, platform: "aix" + Chef.set_provider_priority_array :package, Paludis, platform: "exherbo" + + private + + def shell_out_with_timeout(*command_args) + shell_out(*add_timeout_option(command_args)) + end + + def shell_out_with_timeout!(*command_args) + shell_out!(*add_timeout_option(command_args)) + end + + def add_timeout_option(command_args) + args = command_args.dup + if args.last.is_a?(Hash) + options = args.pop.dup + options[:timeout] = new_resource.timeout if new_resource.timeout + options[:timeout] = 900 unless options.has_key?(:timeout) + args << options + else + args << { :timeout => new_resource.timeout ? new_resource.timeout : 900 } + end + args + end + end end end diff --git a/lib/chef/provider/package/aix.rb b/lib/chef/provider/package/aix.rb index 107f914c05..b97db9d061 100644 --- a/lib/chef/provider/package/aix.rb +++ b/lib/chef/provider/package/aix.rb @@ -52,7 +52,7 @@ class Chef @package_source_found = ::File.exists?(@new_resource.source) if @package_source_found Chef::Log.debug("#{@new_resource} checking pkg status") - ret = shell_out("installp -L -d #{@new_resource.source}") + ret = shell_out_with_timeout("installp -L -d #{@new_resource.source}") ret.stdout.each_line do | line | case line when /#{@new_resource.package_name}:/ @@ -60,11 +60,12 @@ class Chef @new_resource.version(fields[2]) end end + raise Chef::Exceptions::Package, "package source #{@new_resource.source} does not provide package #{@new_resource.package_name}" unless @new_resource.version end end Chef::Log.debug("#{@new_resource} checking install state") - ret = shell_out("lslpp -lcq #{@current_resource.package_name}") + ret = shell_out_with_timeout("lslpp -lcq #{@current_resource.package_name}") ret.stdout.each_line do | line | case line when /#{@current_resource.package_name}/ @@ -83,7 +84,7 @@ class Chef def candidate_version return @candidate_version if @candidate_version - ret = shell_out("installp -L -d #{@new_resource.source}") + ret = shell_out_with_timeout("installp -L -d #{@new_resource.source}") ret.stdout.each_line do | line | case line when /\w:#{Regexp.escape(@new_resource.package_name)}:(.*)/ @@ -109,10 +110,10 @@ class Chef def install_package(name, version) Chef::Log.debug("#{@new_resource} package install options: #{@new_resource.options}") if @new_resource.options.nil? - shell_out!( "installp -aYF -d #{@new_resource.source} #{@new_resource.package_name}" ) + shell_out_with_timeout!( "installp -aYF -d #{@new_resource.source} #{@new_resource.package_name}" ) Chef::Log.debug("#{@new_resource} installed version #{@new_resource.version} from: #{@new_resource.source}") else - shell_out!( "installp -aYF #{expand_options(@new_resource.options)} -d #{@new_resource.source} #{@new_resource.package_name}" ) + shell_out_with_timeout!( "installp -aYF #{expand_options(@new_resource.options)} -d #{@new_resource.source} #{@new_resource.package_name}" ) Chef::Log.debug("#{@new_resource} installed version #{@new_resource.version} from: #{@new_resource.source}") end end @@ -121,10 +122,10 @@ class Chef def remove_package(name, version) if @new_resource.options.nil? - shell_out!( "installp -u #{name}" ) + shell_out_with_timeout!( "installp -u #{name}" ) Chef::Log.debug("#{@new_resource} removed version #{@new_resource.version}") else - shell_out!( "installp -u #{expand_options(@new_resource.options)} #{name}" ) + shell_out_with_timeout!( "installp -u #{expand_options(@new_resource.options)} #{name}" ) Chef::Log.debug("#{@new_resource} removed version #{@new_resource.version}") end end diff --git a/lib/chef/provider/package/apt.rb b/lib/chef/provider/package/apt.rb index e426b51992..bd6ed283bf 100644 --- a/lib/chef/provider/package/apt.rb +++ b/lib/chef/provider/package/apt.rb @@ -62,7 +62,7 @@ class Chef installed_version = nil candidate_version = nil - shell_out!("apt-cache#{expand_options(default_release_options)} policy #{pkg}", {:timeout=>900}).stdout.each_line do |line| + shell_out_with_timeout!("apt-cache#{expand_options(default_release_options)} policy #{pkg}").stdout.each_line do |line| case line when /^\s{2}Installed: (.+)$/ installed_version = $1 @@ -78,7 +78,7 @@ class Chef if candidate_version == '(none)' # This may not be an appropriate assumption, but it shouldn't break anything that already worked -- btm is_virtual_package = true - showpkg = shell_out!("apt-cache showpkg #{pkg}", {:timeout => 900}).stdout + showpkg = shell_out_with_timeout!("apt-cache showpkg #{pkg}").stdout providers = Hash.new showpkg.rpartition(/Reverse Provides: ?#{$/}/)[2].each_line do |line| provider, version = line.split @@ -175,7 +175,7 @@ class Chef # interactive prompts. Command is run with default localization rather # than forcing locale to "C", so command output may not be stable. def run_noninteractive(command) - shell_out!(command, :env => { "DEBIAN_FRONTEND" => "noninteractive", "LC_ALL" => nil }, :timeout => @new_resource.timeout) + shell_out_with_timeout!(command, :env => { "DEBIAN_FRONTEND" => "noninteractive", "LC_ALL" => nil }) end end diff --git a/lib/chef/provider/package/dpkg.rb b/lib/chef/provider/package/dpkg.rb index 11691a2479..a262f1ab1a 100644 --- a/lib/chef/provider/package/dpkg.rb +++ b/lib/chef/provider/package/dpkg.rb @@ -62,7 +62,7 @@ class Chef # Get information from the package if supplied Chef::Log.debug("#{@new_resource} checking dpkg status") - shell_out("dpkg-deb -W #{@new_resource.source}").stdout.each_line do |line| + shell_out_with_timeout("dpkg-deb -W #{@new_resource.source}").stdout.each_line do |line| if pkginfo = DPKG_INFO.match(line) @current_resource.package_name(pkginfo[1]) @new_resource.version(pkginfo[2]) @@ -79,7 +79,7 @@ class Chef # Check to see if it is installed package_installed = nil Chef::Log.debug("#{@new_resource} checking install state") - status = shell_out("dpkg -s #{@current_resource.package_name}") + status = shell_out_with_timeout("dpkg -s #{@current_resource.package_name}") status.stdout.each_line do |line| case line when DPKG_INSTALLED @@ -134,13 +134,13 @@ class Chef run_noninteractive("dpkg-reconfigure #{name}") end - # Runs command via shell_out with magic environment to disable + # Runs command via shell_out_with_timeout with magic environment to disable # interactive prompts. Command is run with default localization rather # than forcing locale to "C", so command output may not be stable. # # FIXME: This should be "LC_ALL" => "en_US.UTF-8" in order to stabilize the output and get UTF-8 def run_noninteractive(command) - shell_out!(command, :env => { "DEBIAN_FRONTEND" => "noninteractive", "LC_ALL" => nil }) + shell_out_with_timeout!(command, :env => { "DEBIAN_FRONTEND" => "noninteractive", "LC_ALL" => nil }) end end diff --git a/lib/chef/provider/package/easy_install.rb b/lib/chef/provider/package/easy_install.rb index 90727b738d..2f7880bf08 100644 --- a/lib/chef/provider/package/easy_install.rb +++ b/lib/chef/provider/package/easy_install.rb @@ -32,10 +32,10 @@ class Chef begin # first check to see if we can import it - output = shell_out!("#{python_binary_path} -c \"import #{name}\"", :returns=>[0,1]).stderr + output = shell_out_with_timeout!("#{python_binary_path} -c \"import #{name}\"", :returns=>[0,1]).stderr if output.include? "ImportError" # then check to see if its on the path - output = shell_out!("#{python_binary_path} -c \"import sys; print sys.path\"", :returns=>[0,1]).stdout + output = shell_out_with_timeout!("#{python_binary_path} -c \"import sys; print sys.path\"", :returns=>[0,1]).stdout if output.downcase.include? "#{name.downcase}" check = true end @@ -73,10 +73,10 @@ class Chef package_version = nil if install_check(module_name) begin - output = shell_out!("#{python_binary_path} -c \"import #{module_name}; print #{module_name}.__version__\"").stdout + output = shell_out_with_timeout!("#{python_binary_path} -c \"import #{module_name}; print #{module_name}.__version__\"").stdout package_version = output.strip rescue - output = shell_out!("#{python_binary_path} -c \"import sys; print sys.path\"", :returns=>[0,1]).stdout + output = shell_out_with_timeout!("#{python_binary_path} -c \"import sys; print sys.path\"", :returns=>[0,1]).stdout output_array = output.gsub(/[\[\]]/,'').split(/\s*,\s*/) package_path = "" @@ -107,7 +107,7 @@ class Chef return @candidate_version if @candidate_version # do a dry run to get the latest version - result = shell_out!("#{easy_install_binary_path} -n #{@new_resource.package_name}", :returns=>[0,1]) + result = shell_out_with_timeout!("#{easy_install_binary_path} -n #{@new_resource.package_name}", :returns=>[0,1]) @candidate_version = result.stdout[/(.*)Best match: (.*) (.*)$/, 3] @candidate_version end diff --git a/lib/chef/provider/package/freebsd/base.rb b/lib/chef/provider/package/freebsd/base.rb index 6a3b97a4fd..7c032b3787 100644 --- a/lib/chef/provider/package/freebsd/base.rb +++ b/lib/chef/provider/package/freebsd/base.rb @@ -47,7 +47,7 @@ class Chef # Otherwise look up the path to the ports directory using 'whereis' else - whereis = shell_out!("whereis -s #{port}", :env => nil) + whereis = shell_out_with_timeout!("whereis -s #{port}", :env => nil) unless path = whereis.stdout[/^#{Regexp.escape(port)}:\s+(.+)$/, 1] raise Chef::Exceptions::Package, "Could not find port with the name #{port}" end @@ -57,7 +57,7 @@ class Chef def makefile_variable_value(variable, dir = nil) options = dir ? { :cwd => dir } : {} - make_v = shell_out!("make -V #{variable}", options.merge!(:env => nil, :returns => [0,1])) + make_v = shell_out_with_timeout!("make -V #{variable}", options.merge!(:env => nil, :returns => [0,1])) make_v.exitstatus.zero? ? make_v.stdout.strip.split($\).first : nil # $\ is the line separator, i.e. newline. end end diff --git a/lib/chef/provider/package/freebsd/pkg.rb b/lib/chef/provider/package/freebsd/pkg.rb index ebbfbb19b4..33a8c2c108 100644 --- a/lib/chef/provider/package/freebsd/pkg.rb +++ b/lib/chef/provider/package/freebsd/pkg.rb @@ -34,24 +34,24 @@ class Chef case @new_resource.source when /^http/, /^ftp/ if @new_resource.source =~ /\/$/ - shell_out!("pkg_add -r #{package_name}", :env => { "PACKAGESITE" => @new_resource.source, 'LC_ALL' => nil }).status + shell_out_with_timeout!("pkg_add -r #{package_name}", :env => { "PACKAGESITE" => @new_resource.source, 'LC_ALL' => nil }).status else - shell_out!("pkg_add -r #{package_name}", :env => { "PACKAGEROOT" => @new_resource.source, 'LC_ALL' => nil }).status + shell_out_with_timeout!("pkg_add -r #{package_name}", :env => { "PACKAGEROOT" => @new_resource.source, 'LC_ALL' => nil }).status end Chef::Log.debug("#{@new_resource} installed from: #{@new_resource.source}") when /^\// - shell_out!("pkg_add #{file_candidate_version_path}", :env => { "PKG_PATH" => @new_resource.source , 'LC_ALL'=>nil}).status + shell_out_with_timeout!("pkg_add #{file_candidate_version_path}", :env => { "PKG_PATH" => @new_resource.source , 'LC_ALL'=>nil}).status Chef::Log.debug("#{@new_resource} installed from: #{@new_resource.source}") else - shell_out!("pkg_add -r #{latest_link_name}", :env => nil).status + shell_out_with_timeout!("pkg_add -r #{latest_link_name}", :env => nil).status end end end def remove_package(name, version) - shell_out!("pkg_delete #{package_name}-#{version || @current_resource.version}", :env => nil).status + shell_out_with_timeout!("pkg_delete #{package_name}-#{version || @current_resource.version}", :env => nil).status end # The name of the package (without the version number) as understood by pkg_add and pkg_info. @@ -72,7 +72,7 @@ class Chef end def current_installed_version - pkg_info = shell_out!("pkg_info -E \"#{package_name}*\"", :env => nil, :returns => [0,1]) + pkg_info = shell_out_with_timeout!("pkg_info -E \"#{package_name}*\"", :env => nil, :returns => [0,1]) pkg_info.stdout[/^#{Regexp.escape(package_name)}-(.+)/, 1] end diff --git a/lib/chef/provider/package/freebsd/pkgng.rb b/lib/chef/provider/package/freebsd/pkgng.rb index bfe6dca617..2fdc9dda71 100644 --- a/lib/chef/provider/package/freebsd/pkgng.rb +++ b/lib/chef/provider/package/freebsd/pkgng.rb @@ -28,11 +28,11 @@ class Chef unless @current_resource.version case @new_resource.source when /^(http|ftp|\/)/ - shell_out!("pkg add#{expand_options(@new_resource.options)} #{@new_resource.source}", :env => { 'LC_ALL' => nil }).status + shell_out_with_timeout!("pkg add#{expand_options(@new_resource.options)} #{@new_resource.source}", :env => { 'LC_ALL' => nil }).status Chef::Log.debug("#{@new_resource} installed from: #{@new_resource.source}") else - shell_out!("pkg install -y#{expand_options(@new_resource.options)} #{name}", :env => { 'LC_ALL' => nil }).status + shell_out_with_timeout!("pkg install -y#{expand_options(@new_resource.options)} #{name}", :env => { 'LC_ALL' => nil }).status end end end @@ -40,11 +40,11 @@ class Chef def remove_package(name, version) options = @new_resource.options && @new_resource.options.sub(repo_regex, '') options && !options.empty? || options = nil - shell_out!("pkg delete -y#{expand_options(options)} #{name}#{version ? '-' + version : ''}", :env => nil).status + shell_out_with_timeout!("pkg delete -y#{expand_options(options)} #{name}#{version ? '-' + version : ''}", :env => nil).status end def current_installed_version - pkg_info = shell_out!("pkg info \"#{@new_resource.package_name}\"", :env => nil, :returns => [0,70]) + pkg_info = shell_out_with_timeout!("pkg info \"#{@new_resource.package_name}\"", :env => nil, :returns => [0,70]) pkg_info.stdout[/^Version +: (.+)$/, 1] end @@ -63,7 +63,7 @@ class Chef options = $1 end - pkg_query = shell_out!("pkg rquery#{expand_options(options)} '%v' #{@new_resource.package_name}", :env => nil) + pkg_query = shell_out_with_timeout!("pkg rquery#{expand_options(options)} '%v' #{@new_resource.package_name}", :env => nil) pkg_query.exitstatus.zero? ? pkg_query.stdout.strip.split(/\n/).last : nil end diff --git a/lib/chef/provider/package/freebsd/port.rb b/lib/chef/provider/package/freebsd/port.rb index 8b191179f0..3fbd002214 100644 --- a/lib/chef/provider/package/freebsd/port.rb +++ b/lib/chef/provider/package/freebsd/port.rb @@ -26,18 +26,18 @@ class Chef include PortsHelper def install_package(name, version) - shell_out!("make -DBATCH install clean", :timeout => 1800, :env => nil, :cwd => port_dir).status + shell_out_with_timeout!("make -DBATCH install clean", :timeout => 1800, :env => nil, :cwd => port_dir).status end def remove_package(name, version) - shell_out!("make deinstall", :timeout => 300, :env => nil, :cwd => port_dir).status + shell_out_with_timeout!("make deinstall", :timeout => 300, :env => nil, :cwd => port_dir).status end def current_installed_version pkg_info = if @new_resource.supports_pkgng? - shell_out!("pkg info \"#{@new_resource.package_name}\"", :env => nil, :returns => [0,70]) + shell_out_with_timeout!("pkg info \"#{@new_resource.package_name}\"", :env => nil, :returns => [0,70]) else - shell_out!("pkg_info -E \"#{@new_resource.package_name}*\"", :env => nil, :returns => [0,1]) + shell_out_with_timeout!("pkg_info -E \"#{@new_resource.package_name}*\"", :env => nil, :returns => [0,1]) end pkg_info.stdout[/^#{Regexp.escape(@new_resource.package_name)}-(.+)/, 1] end diff --git a/lib/chef/provider/package/homebrew.rb b/lib/chef/provider/package/homebrew.rb index 603899646f..beede1c916 100644 --- a/lib/chef/provider/package/homebrew.rb +++ b/lib/chef/provider/package/homebrew.rb @@ -27,7 +27,6 @@ class Chef class Homebrew < Chef::Provider::Package provides :homebrew_package - provides :package, os: "darwin" include Chef::Mixin::HomebrewUser @@ -126,7 +125,8 @@ class Chef homebrew_user = Etc.getpwuid(homebrew_uid) Chef::Log.debug "Executing '#{command}' as user '#{homebrew_user.name}'" - output = shell_out!(command, :timeout => 1800, :user => homebrew_uid, :environment => { 'HOME' => homebrew_user.dir, 'RUBYOPT' => nil }) + # FIXME: this 1800 second default timeout should be deprecated + output = shell_out_with_timeout!(command, :timeout => 1800, :user => homebrew_uid, :environment => { 'HOME' => homebrew_user.dir, 'RUBYOPT' => nil }) output.stdout.chomp end diff --git a/lib/chef/provider/package/ips.rb b/lib/chef/provider/package/ips.rb index 87022d770a..4d7f4a3583 100644 --- a/lib/chef/provider/package/ips.rb +++ b/lib/chef/provider/package/ips.rb @@ -42,14 +42,14 @@ class Chef end def get_current_version - shell_out("pkg info #{@new_resource.package_name}").stdout.each_line do |line| + shell_out_with_timeout("pkg info #{@new_resource.package_name}").stdout.each_line do |line| return $1.split[0] if line =~ /^\s+Version: (.*)/ end return nil end def get_candidate_version - shell_out!("pkg info -r #{new_resource.package_name}").stdout.each_line do |line| + shell_out_with_timeout!("pkg info -r #{new_resource.package_name}").stdout.each_line do |line| return $1.split[0] if line =~ /Version: (.*)/ end return nil @@ -73,7 +73,7 @@ class Chef else normal_command end - shell_out(command) + shell_out_with_timeout(command) end def upgrade_package(name, version) @@ -82,7 +82,7 @@ class Chef def remove_package(name, version) package_name = "#{name}@#{version}" - shell_out!( "pkg#{expand_options(@new_resource.options)} uninstall -q #{package_name}" ) + shell_out_with_timeout!( "pkg#{expand_options(@new_resource.options)} uninstall -q #{package_name}" ) end end end diff --git a/lib/chef/provider/package/macports.rb b/lib/chef/provider/package/macports.rb index b252344c99..e945211540 100644 --- a/lib/chef/provider/package/macports.rb +++ b/lib/chef/provider/package/macports.rb @@ -4,7 +4,6 @@ class Chef class Macports < Chef::Provider::Package provides :macports_package - provides :package, os: "darwin" def load_current_resource @current_resource = Chef::Resource::Package.new(@new_resource.name) @@ -49,21 +48,21 @@ class Chef unless @current_resource.version == version command = "port#{expand_options(@new_resource.options)} install #{name}" command << " @#{version}" if version and !version.empty? - shell_out!(command) + shell_out_with_timeout!(command) end end def purge_package(name, version) command = "port#{expand_options(@new_resource.options)} uninstall #{name}" command << " @#{version}" if version and !version.empty? - shell_out!(command) + shell_out_with_timeout!(command) end def remove_package(name, version) command = "port#{expand_options(@new_resource.options)} deactivate #{name}" command << " @#{version}" if version and !version.empty? - shell_out!(command) + shell_out_with_timeout!(command) end def upgrade_package(name, version) @@ -76,14 +75,14 @@ class Chef # that hasn't been installed. install_package(name, version) elsif current_version != version - shell_out!( "port#{expand_options(@new_resource.options)} upgrade #{name} @#{version}" ) + shell_out_with_timeout!( "port#{expand_options(@new_resource.options)} upgrade #{name} @#{version}" ) end end private def get_response_from_command(command) output = nil - status = shell_out(command) + status = shell_out_with_timeout(command) begin output = status.stdout rescue Exception diff --git a/lib/chef/provider/package/openbsd.rb b/lib/chef/provider/package/openbsd.rb index 82048c3bd4..f231101390 100644 --- a/lib/chef/provider/package/openbsd.rb +++ b/lib/chef/provider/package/openbsd.rb @@ -22,7 +22,6 @@ require 'chef/resource/package' require 'chef/provider/package' -require 'chef/mixin/shell_out' require 'chef/mixin/get_source_from_package' require 'chef/exceptions' @@ -72,7 +71,7 @@ class Chef if parts = name.match(/^(.+?)--(.+)/) # use double-dash for stems with flavors, see man page for pkg_add name = parts[1] end - shell_out!("pkg_add -r #{name}#{version_string}", :env => {"PKG_PATH" => pkg_path}).status + shell_out_with_timeout!("pkg_add -r #{name}#{version_string}", :env => {"PKG_PATH" => pkg_path}).status Chef::Log.debug("#{new_resource.package_name} installed") end end @@ -83,7 +82,7 @@ class Chef if parts = name.match(/^(.+?)--(.+)/) name = parts[1] end - shell_out!("pkg_delete #{name}#{version_string}", :env => nil).status + shell_out_with_timeout!("pkg_delete #{name}#{version_string}", :env => nil).status end private @@ -94,7 +93,7 @@ class Chef else name = new_resource.package_name end - pkg_info = shell_out!("pkg_info -e \"#{name}->0\"", :env => nil, :returns => [0,1]) + pkg_info = shell_out_with_timeout!("pkg_info -e \"#{name}->0\"", :env => nil, :returns => [0,1]) result = pkg_info.stdout[/^inst:#{Regexp.escape(name)}-(.+?)\s/, 1] Chef::Log.debug("installed_version of '#{new_resource.package_name}' is '#{result}'") result @@ -103,7 +102,7 @@ class Chef def candidate_version @candidate_version ||= begin results = [] - shell_out!("pkg_info -I \"#{new_resource.package_name}#{version_string}\"", :env => nil, :returns => [0,1]).stdout.each_line do |line| + shell_out_with_timeout!("pkg_info -I \"#{new_resource.package_name}#{version_string}\"", :env => nil, :returns => [0,1]).stdout.each_line do |line| if parts = new_resource.package_name.match(/^(.+?)--(.+)/) results << line[/^#{Regexp.escape(parts[1])}-(.+?)\s/, 1] else diff --git a/lib/chef/provider/package/pacman.rb b/lib/chef/provider/package/pacman.rb index f16fc811f5..bf03e54656 100644 --- a/lib/chef/provider/package/pacman.rb +++ b/lib/chef/provider/package/pacman.rb @@ -34,7 +34,7 @@ class Chef @current_resource.version(nil) Chef::Log.debug("#{@new_resource} checking pacman for #{@new_resource.package_name}") - status = shell_out("pacman -Qi #{@new_resource.package_name}") + status = shell_out_with_timeout("pacman -Qi #{@new_resource.package_name}") status.stdout.each_line do |line| case line when /^Version(\s?)*: (.+)$/ @@ -62,7 +62,7 @@ class Chef package_repos = repos.map {|r| Regexp.escape(r) }.join('|') - status = shell_out("pacman -Sl") + status = shell_out_with_timeout("pacman -Sl") status.stdout.each_line do |line| case line when /^(#{package_repos}) #{Regexp.escape(@new_resource.package_name)} (.+)$/ @@ -85,7 +85,7 @@ class Chef end def install_package(name, version) - shell_out!( "pacman --sync --noconfirm --noprogressbar#{expand_options(@new_resource.options)} #{name}" ) + shell_out_with_timeout!( "pacman --sync --noconfirm --noprogressbar#{expand_options(@new_resource.options)} #{name}" ) end def upgrade_package(name, version) @@ -93,7 +93,7 @@ class Chef end def remove_package(name, version) - shell_out!( "pacman --remove --noconfirm --noprogressbar#{expand_options(@new_resource.options)} #{name}" ) + shell_out_with_timeout!( "pacman --remove --noconfirm --noprogressbar#{expand_options(@new_resource.options)} #{name}" ) end def purge_package(name, version) diff --git a/lib/chef/provider/package/portage.rb b/lib/chef/provider/package/portage.rb index bb047ad2fa..4ba0160bb0 100644 --- a/lib/chef/provider/package/portage.rb +++ b/lib/chef/provider/package/portage.rb @@ -25,6 +25,8 @@ class Chef class Provider class Package class Portage < Chef::Provider::Package + provides :portage_package + PACKAGE_NAME_PATTERN = %r{(?:([^/]+)/)?([^/]+)} def load_current_resource diff --git a/lib/chef/provider/package/rpm.rb b/lib/chef/provider/package/rpm.rb index f10fe23c71..c5d52a8384 100644 --- a/lib/chef/provider/package/rpm.rb +++ b/lib/chef/provider/package/rpm.rb @@ -17,7 +17,6 @@ # require 'chef/provider/package' require 'chef/mixin/command' -require 'chef/mixin/shell_out' require 'chef/resource/package' require 'chef/mixin/get_source_from_package' @@ -60,9 +59,9 @@ class Chef end Chef::Log.debug("#{@new_resource} checking rpm status") - shell_out!("rpm -qp --queryformat '%{NAME} %{VERSION}-%{RELEASE}\n' #{@new_resource.source}").stdout.each_line do |line| + shell_out_with_timeout!("rpm -qp --queryformat '%{NAME} %{VERSION}-%{RELEASE}\n' #{@new_resource.source}").stdout.each_line do |line| case line - when /^([\w\d+_.-]+)\s([\w\d_.-]+)$/ + when /^([\w\d+_.-]+)\s([\w\d~_.-]+)$/ @current_resource.package_name($1) @new_resource.version($2) @candidate_version = $2 @@ -76,10 +75,10 @@ class Chef end Chef::Log.debug("#{@new_resource} checking install state") - @rpm_status = shell_out("rpm -q --queryformat '%{NAME} %{VERSION}-%{RELEASE}\n' #{@current_resource.package_name}") + @rpm_status = shell_out_with_timeout("rpm -q --queryformat '%{NAME} %{VERSION}-%{RELEASE}\n' #{@current_resource.package_name}") @rpm_status.stdout.each_line do |line| case line - when /^([\w\d+_.-]+)\s([\w\d_.-]+)$/ + when /^([\w\d+_.-]+)\s([\w\d~_.-]+)$/ Chef::Log.debug("#{@new_resource} current version is #{$2}") @current_resource.version($2) end @@ -90,12 +89,12 @@ class Chef def install_package(name, version) unless @current_resource.version - shell_out!( "rpm #{@new_resource.options} -i #{@new_resource.source}" ) + shell_out_with_timeout!( "rpm #{@new_resource.options} -i #{@new_resource.source}" ) else if allow_downgrade - shell_out!( "rpm #{@new_resource.options} -U --oldpackage #{@new_resource.source}" ) + shell_out_with_timeout!( "rpm #{@new_resource.options} -U --oldpackage #{@new_resource.source}" ) else - shell_out!( "rpm #{@new_resource.options} -U #{@new_resource.source}" ) + shell_out_with_timeout!( "rpm #{@new_resource.options} -U #{@new_resource.source}" ) end end end @@ -104,9 +103,9 @@ class Chef def remove_package(name, version) if version - shell_out!( "rpm #{@new_resource.options} -e #{name}-#{version}" ) + shell_out_with_timeout!( "rpm #{@new_resource.options} -e #{name}-#{version}" ) else - shell_out!( "rpm #{@new_resource.options} -e #{name}" ) + shell_out_with_timeout!( "rpm #{@new_resource.options} -e #{name}" ) end end diff --git a/lib/chef/provider/package/rubygems.rb b/lib/chef/provider/package/rubygems.rb index c53aa8934a..b5f7dbdd80 100644 --- a/lib/chef/provider/package/rubygems.rb +++ b/lib/chef/provider/package/rubygems.rb @@ -32,14 +32,7 @@ require 'rubygems/version' require 'rubygems/dependency' require 'rubygems/spec_fetcher' require 'rubygems/platform' - -# Compatibility note: Rubygems 2.0 removes rubygems/format in favor of -# rubygems/package. -begin - require 'rubygems/format' -rescue LoadError - require 'rubygems/package' -end +require 'rubygems/package' require 'rubygems/dependency_installer' require 'rubygems/uninstaller' require 'rubygems/specification' @@ -545,9 +538,9 @@ class Chef src = @new_resource.source && " --source=#{@new_resource.source} --source=https://rubygems.org" end if !version.nil? && version.length > 0 - shell_out!("#{gem_binary_path} install #{name} -q --no-rdoc --no-ri -v \"#{version}\"#{src}#{opts}", :env=>nil) + shell_out_with_timeout!("#{gem_binary_path} install #{name} -q --no-rdoc --no-ri -v \"#{version}\"#{src}#{opts}", :env=>nil) else - shell_out!("#{gem_binary_path} install \"#{name}\" -q --no-rdoc --no-ri #{src}#{opts}", :env=>nil) + shell_out_with_timeout!("#{gem_binary_path} install \"#{name}\" -q --no-rdoc --no-ri #{src}#{opts}", :env=>nil) end end @@ -571,9 +564,9 @@ class Chef def uninstall_via_gem_command(name, version) if version - shell_out!("#{gem_binary_path} uninstall #{name} -q -x -I -v \"#{version}\"#{opts}", :env=>nil) + shell_out_with_timeout!("#{gem_binary_path} uninstall #{name} -q -x -I -v \"#{version}\"#{opts}", :env=>nil) else - shell_out!("#{gem_binary_path} uninstall #{name} -q -x -I -a#{opts}", :env=>nil) + shell_out_with_timeout!("#{gem_binary_path} uninstall #{name} -q -x -I -a#{opts}", :env=>nil) end end diff --git a/lib/chef/provider/package/smartos.rb b/lib/chef/provider/package/smartos.rb index 7cef91953a..0d5b801c96 100644 --- a/lib/chef/provider/package/smartos.rb +++ b/lib/chef/provider/package/smartos.rb @@ -43,7 +43,7 @@ class Chef def check_package_state(name) Chef::Log.debug("#{@new_resource} checking package #{name}") version = nil - info = shell_out!("/opt/local/sbin/pkg_info -E \"#{name}*\"", :env => nil, :returns => [0,1]) + info = shell_out_with_timeout!("/opt/local/sbin/pkg_info", "-E", "#{name}*", :env => nil, :returns => [0,1]) if info.stdout version = info.stdout[/^#{@new_resource.package_name}-(.+)/, 1] @@ -60,11 +60,11 @@ class Chef return @candidate_version if @candidate_version name = nil version = nil - pkg = shell_out!("/opt/local/bin/pkgin se #{new_resource.package_name}", :env => nil, :returns => [0,1]) + pkg = shell_out_with_timeout!("/opt/local/bin/pkgin", "se", new_resource.package_name, :env => nil, :returns => [0,1]) pkg.stdout.each_line do |line| case line when /^#{new_resource.package_name}/ - name, version = line.split[0].split(/-([^-]+)$/) + name, version = line.split(/[; ]/)[0].split(/-([^-]+)$/) end end @candidate_version = version @@ -74,7 +74,7 @@ class Chef def install_package(name, version) Chef::Log.debug("#{@new_resource} installing package #{name} version #{version}") package = "#{name}-#{version}" - out = shell_out!("/opt/local/bin/pkgin -y install #{package}", :env => nil) + out = shell_out_with_timeout!("/opt/local/bin/pkgin", "-y", "install", package, :env => nil) end def upgrade_package(name, version) @@ -85,7 +85,7 @@ class Chef def remove_package(name, version) Chef::Log.debug("#{@new_resource} removing package #{name} version #{version}") package = "#{name}" - out = shell_out!("/opt/local/bin/pkgin -y remove #{package}", :env => nil) + out = shell_out_with_timeout!("/opt/local/bin/pkgin", "-y", "remove", package, :env => nil) end end diff --git a/lib/chef/provider/package/solaris.rb b/lib/chef/provider/package/solaris.rb index a2cfd93ef6..9b10403344 100644 --- a/lib/chef/provider/package/solaris.rb +++ b/lib/chef/provider/package/solaris.rb @@ -55,7 +55,7 @@ class Chef @package_source_found = ::File.exists?(@new_resource.source) if @package_source_found Chef::Log.debug("#{@new_resource} checking pkg status") - shell_out("pkginfo -l -d #{@new_resource.source} #{@new_resource.package_name}").stdout.each_line do |line| + shell_out_with_timeout("pkginfo -l -d #{@new_resource.source} #{@new_resource.package_name}").stdout.each_line do |line| case line when /VERSION:\s+(.+)/ @new_resource.version($1) @@ -65,7 +65,7 @@ class Chef end Chef::Log.debug("#{@new_resource} checking install state") - status = shell_out("pkginfo -l #{@current_resource.package_name}") + status = shell_out_with_timeout("pkginfo -l #{@current_resource.package_name}") status.stdout.each_line do |line| case line when /VERSION:\s+(.+)/ @@ -87,7 +87,7 @@ class Chef def candidate_version return @candidate_version if @candidate_version - status = shell_out("pkginfo -l -d #{@new_resource.source} #{new_resource.package_name}") + status = shell_out_with_timeout("pkginfo -l -d #{@new_resource.source} #{new_resource.package_name}") status.stdout.each_line do |line| case line when /VERSION:\s+(.+)/ @@ -110,7 +110,7 @@ class Chef else command = "pkgadd -n -d #{@new_resource.source} all" end - shell_out!(command) + shell_out_with_timeout!(command) Chef::Log.debug("#{@new_resource} installed version #{@new_resource.version} from: #{@new_resource.source}") else if ::File.directory?(@new_resource.source) # CHEF-4469 @@ -118,17 +118,17 @@ class Chef else command = "pkgadd -n#{expand_options(@new_resource.options)} -d #{@new_resource.source} all" end - shell_out!(command) + shell_out_with_timeout!(command) Chef::Log.debug("#{@new_resource} installed version #{@new_resource.version} from: #{@new_resource.source}") end end def remove_package(name, version) if @new_resource.options.nil? - shell_out!( "pkgrm -n #{name}" ) + shell_out_with_timeout!( "pkgrm -n #{name}" ) Chef::Log.debug("#{@new_resource} removed version #{@new_resource.version}") else - shell_out!( "pkgrm -n#{expand_options(@new_resource.options)} #{name}" ) + shell_out_with_timeout!( "pkgrm -n#{expand_options(@new_resource.options)} #{name}" ) Chef::Log.debug("#{@new_resource} removed version #{@new_resource.version}") end end diff --git a/lib/chef/provider/package/windows.rb b/lib/chef/provider/package/windows.rb index 143d82f111..7ff0b71807 100644 --- a/lib/chef/provider/package/windows.rb +++ b/lib/chef/provider/package/windows.rb @@ -16,14 +16,18 @@ # limitations under the License. # +require 'chef/mixin/uris' require 'chef/resource/windows_package' require 'chef/provider/package' require 'chef/util/path_helper' +require 'chef/mixin/checksum' class Chef class Provider class Package class Windows < Chef::Provider::Package + include Chef::Mixin::Uris + include Chef::Mixin::Checksum provides :package, os: "windows" provides :windows_package, os: "windows" @@ -36,19 +40,23 @@ class Chef # load_current_resource is run in Chef::Provider#run_action when not in whyrun_mode? def load_current_resource - @new_resource.source(Chef::Util::PathHelper.validate_path(@new_resource.source)) - @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 + if downloadable_file_missing? + Chef::Log.debug("We do not know the version of #{new_resource.source} because the file is not downloaded") + current_resource.version(:unknown.to_s) + else + current_resource.version(package_provider.installed_version) + new_resource.version(package_provider.package_version) + end + + current_resource end def package_provider @package_provider ||= begin case installer_type when :msi - Chef::Provider::Package::Windows::MSI.new(@new_resource) + Chef::Provider::Package::Windows::MSI.new(resource_for_provider) else raise "Unable to find a Chef::Provider::Package::Windows provider for installer_type '#{installer_type}'" end @@ -71,6 +79,17 @@ class Chef end end + def action_install + if uri_scheme?(new_resource.source) + download_source_file + load_current_resource + else + validate_content! + end + + super + 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) @@ -80,6 +99,71 @@ class Chef def remove_package(name, version) package_provider.remove_package(name, version) end + + # @return [Array] new_version(s) as an array + def new_version_array + # Because the one in the parent caches things + [new_resource.version] + end + + private + + def downloadable_file_missing? + uri_scheme?(new_resource.source) && !::File.exists?(source_location) + end + + def resource_for_provider + @resource_for_provider = Chef::Resource::WindowsPackage.new(new_resource.name).tap do |r| + r.source(Chef::Util::PathHelper.validate_path(source_location)) + r.timeout(new_resource.timeout) + r.returns(new_resource.returns) + r.options(new_resource.options) + end + end + + def download_source_file + source_resource.run_action(:create) + Chef::Log.debug("#{@new_resource} fetched source file to #{source_resource.path}") + end + + def source_resource + @source_resource ||= Chef::Resource::RemoteFile.new(default_download_cache_path, run_context).tap do |r| + r.source(new_resource.source) + r.checksum(new_resource.checksum) + r.backup(false) + + if new_resource.remote_file_attributes + new_resource.remote_file_attributes.each do |(k,v)| + r.send(k.to_sym, v) + end + end + end + end + + def default_download_cache_path + uri = ::URI.parse(new_resource.source) + filename = ::File.basename(::URI.unescape(uri.path)) + file_cache_dir = Chef::FileCache.create_cache_path("package/") + Chef::Util::PathHelper.cleanpath("#{file_cache_dir}/#{filename}") + end + + def source_location + if uri_scheme?(new_resource.source) + source_resource.path + else + Chef::Util::PathHelper.cleanpath(new_resource.source) + end + end + + def validate_content! + if new_resource.checksum + source_checksum = checksum(source_location) + if new_resource.checksum != source_checksum + raise Chef::Exceptions::ChecksumMismatch.new(short_cksum(new_resource.checksum), short_cksum(source_checksum)) + end + end + end + end end end diff --git a/lib/chef/provider/package/windows/msi.rb b/lib/chef/provider/package/windows/msi.rb index 938452945e..31faa78215 100644 --- a/lib/chef/provider/package/windows/msi.rb +++ b/lib/chef/provider/package/windows/msi.rb @@ -56,7 +56,7 @@ class Chef 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}'") diff --git a/lib/chef/provider/package/yum.rb b/lib/chef/provider/package/yum.rb index 49c6f6beb5..85c2ba683c 100644 --- a/lib/chef/provider/package/yum.rb +++ b/lib/chef/provider/package/yum.rb @@ -1,4 +1,4 @@ -# + # Author:: Adam Jacob (<adam@opscode.com>) # Copyright:: Copyright (c) 2008 Opscode, Inc. # License:: Apache License, Version 2.0 @@ -18,7 +18,7 @@ require 'chef/config' require 'chef/provider/package' -require 'chef/mixin/shell_out' +require 'chef/mixin/which' require 'chef/resource/package' require 'singleton' require 'chef/mixin/get_source_from_package' @@ -646,7 +646,7 @@ class Chef # Cache for our installed and available packages, pulled in from yum-dump.py class YumCache - include Chef::Mixin::Command + include Chef::Mixin::Which include Chef::Mixin::ShellOut include Singleton @@ -713,7 +713,7 @@ class Chef status = nil begin - status = shell_out!("/usr/bin/python #{helper}#{opts}", :timeout => Chef::Config[:yum_timeout]) + status = shell_out!("#{python_bin} #{helper}#{opts}", :timeout => Chef::Config[:yum_timeout]) status.stdout.each_line do |line| one_line = true @@ -779,6 +779,32 @@ class Chef @next_refresh = :none end + def python_bin + yum_executable = which("yum") + if yum_executable && shabang?(yum_executable) + extract_interpreter(yum_executable) + else + Chef::Log.warn("Yum executable not found or doesn't start with #!. Using default python.") + "/usr/bin/python" + end + rescue StandardError => e + Chef::Log.warn("An error occured attempting to determine correct python executable. Using default.") + Chef::Log.debug(e) + "/usr/bin/python" + end + + def extract_interpreter(file) + ::File.open(file, 'r', &:readline)[2..-1].chomp + end + + def shabang?(file) + ::File.open(file, 'r') do |f| + f.read(2) == '#!' + end + rescue Errno::ENOENT + false + end + def reload @next_refresh = :all end @@ -958,6 +984,17 @@ class Chef # Extra attributes # + def arch_for_name(n) + if @new_resource.respond_to?("arch") + @new_resource.arch + elsif @arch + idx = package_name_array.index(n) + as_array(@arch)[idx] + else + nil + end + end + def arch if @new_resource.respond_to?("arch") @new_resource.arch @@ -966,6 +1003,12 @@ class Chef end end + def set_arch(arch) + if @new_resource.respond_to?("arch") + @new_resource.arch(arch) + end + end + def flush_cache if @new_resource.respond_to?("flush_cache") @new_resource.flush_cache @@ -977,12 +1020,13 @@ class Chef # Helpers # - def yum_arch + def yum_arch(arch) arch ? ".#{arch}" : nil end def yum_command(command) - status = shell_out(command, {:timeout => Chef::Config[:yum_timeout]}) + Chef::Log.debug("#{@new_resource}: yum command: \"#{command}\"") + status = shell_out_with_timeout(command, {:timeout => Chef::Config[:yum_timeout]}) # This is fun: rpm can encounter errors in the %post/%postun scripts which aren't # considered fatal - meaning the rpm is still successfully installed. These issue @@ -999,7 +1043,7 @@ class Chef if l =~ %r{^error: %(post|postun)\(.*\) scriptlet failed, exit status \d+$} Chef::Log.warn("#{@new_resource} caught non-fatal scriptlet issue: \"#{l}\". Can't trust yum exit status " + "so running install again to verify.") - status = shell_out(command, {:timeout => Chef::Config[:yum_timeout]}) + status = shell_out_with_timeout(command, {:timeout => Chef::Config[:yum_timeout]}) break end end @@ -1059,23 +1103,20 @@ class Chef end end - # Don't overwrite an existing arch - unless arch - parse_arch - end @current_resource = Chef::Resource::Package.new(@new_resource.name) @current_resource.package_name(@new_resource.package_name) installed_version = [] @candidate_version = [] + @arch = [] if @new_resource.source unless ::File.exists?(@new_resource.source) raise Chef::Exceptions::Package, "Package #{@new_resource.name} not found: #{@new_resource.source}" end Chef::Log.debug("#{@new_resource} checking rpm status") - shell_out!("rpm -qp --queryformat '%{NAME} %{VERSION}-%{RELEASE}\n' #{@new_resource.source}", :timeout => Chef::Config[:yum_timeout]).stdout.each_line do |line| + shell_out_with_timeout!("rpm -qp --queryformat '%{NAME} %{VERSION}-%{RELEASE}\n' #{@new_resource.source}", :timeout => Chef::Config[:yum_timeout]).stdout.each_line do |line| case line when /([\w\d_.-]+)\s([\w\d_.-]+)/ @current_resource.package_name($1) @@ -1085,24 +1126,43 @@ class Chef @candidate_version << @new_resource.version installed_version << @yum.installed_version(@current_resource.package_name, arch) else - if @new_resource.version - new_resource = "#{@new_resource.package_name}-#{@new_resource.version}#{yum_arch}" - else - new_resource = "#{@new_resource.package_name}#{yum_arch}" - end - Chef::Log.debug("#{@new_resource} checking yum info for #{new_resource}") + package_name_array.each_with_index do |pkg, idx| + # Don't overwrite an existing arch + if arch + name, parch = pkg, arch + else + name, parch = parse_arch(pkg) + # if we parsed an arch from the name, update the name + # to be just the package name. + if parch + if @new_resource.package_name.is_a?(Array) + @new_resource.package_name[idx] = name + else + @new_resource.package_name(name) + # only set the arch if it's a single package + set_arch(parch) + end + end + end - package_name_array.each do |pkg| - installed_version << @yum.installed_version(pkg, arch) - @candidate_version << @yum.candidate_version(pkg, arch) + if @new_resource.version + new_resource = + "#{@new_resource.package_name}-#{@new_resource.version}#{yum_arch(parch)}" + else + new_resource = "#{@new_resource.package_name}#{yum_arch(parch)}" + end + Chef::Log.debug("#{@new_resource} checking yum info for #{new_resource}") + installed_version << @yum.installed_version(name, parch) + @candidate_version << @yum.candidate_version(name, parch) + @arch << parch end - end if installed_version.size == 1 @current_resource.version(installed_version[0]) @candidate_version = @candidate_version[0] + @arch = @arch[0] else @current_resource.version(installed_version) end @@ -1117,7 +1177,7 @@ class Chef # Work around yum not exiting with an error if a package doesn't exist # for CHEF-2062 all_avail = as_array(name).zip(as_array(version)).any? do |n, v| - @yum.version_available?(n, v, arch) + @yum.version_available?(n, v, arch_for_name(n)) end method = log_method = nil methods = [] @@ -1159,16 +1219,16 @@ class Chef repos = [] pkg_string_bits = [] - index = 0 as_array(name).zip(as_array(version)).each do |n, v| + idx = package_name_array.index(n) + a = arch_for_name(n) s = '' - unless v == current_version_array[index] - s = "#{n}-#{v}#{yum_arch}" - repo = @yum.package_repository(n, v, arch) + unless v == current_version_array[idx] + s = "#{n}-#{v}#{yum_arch(a)}" + repo = @yum.package_repository(n, v, a) repos << "#{s} from #{repo} repository" pkg_string_bits << s end - index += 1 end pkg_string = pkg_string_bits.join(' ') Chef::Log.info("#{@new_resource} #{log_method} #{repos.join(' ')}") @@ -1219,11 +1279,15 @@ class Chef def remove_package(name, version) if version - remove_str = as_array(name).zip(as_array(version)).map do |x| - "#{x.join('-')}#{yum_arch}" + remove_str = as_array(name).zip(as_array(version)).map do |n, v| + a = arch_for_name(n) + "#{[n, v].join('-')}#{yum_arch(a)}" end.join(' ') else - remove_str = as_array(name).map { |n| "#{n}#{yum_arch}" }.join(' ') + remove_str = as_array(name).map do |n| + a = arch_for_name(n) + "#{n}#{yum_arch(a)}" + end.join(' ') end yum_command("yum -d0 -e0 -y#{expand_options(@new_resource.options)} remove #{remove_str}") @@ -1240,22 +1304,26 @@ class Chef private - def parse_arch + def parse_arch(package_name) # Allow for foo.x86_64 style package_name like yum uses in it's output # - if @new_resource.package_name =~ %r{^(.*)\.(.*)$} + if package_name =~ %r{^(.*)\.(.*)$} new_package_name = $1 new_arch = $2 # foo.i386 and foo.beta1 are both valid package names or expressions of an arch. # Ensure we don't have an existing package matching package_name, then ensure we at # least have a match for the new_package+new_arch before we overwrite. If neither # then fall through to standard package handling. - if (@yum.installed_version(@new_resource.package_name).nil? and @yum.candidate_version(@new_resource.package_name).nil?) and - (@yum.installed_version(new_package_name, new_arch) or @yum.candidate_version(new_package_name, new_arch)) - @new_resource.package_name(new_package_name) - @new_resource.arch(new_arch) + old_installed = @yum.installed_version(package_name) + old_candidate = @yum.candidate_version(package_name) + new_installed = @yum.installed_version(new_package_name, new_arch) + new_candidate = @yum.candidate_version(new_package_name, new_arch) + if (old_installed.nil? and old_candidate.nil?) and (new_installed or new_candidate) + Chef::Log.debug("Parsed out arch #{new_arch}, new package name is #{new_package_name}") + return new_package_name, new_arch end end + return package_name, nil end # If we don't have the package we could have been passed a 'whatprovides' feature @@ -1300,7 +1368,7 @@ class Chef new_package_name = packages.first.name new_package_version = packages.first.version.to_s debug_msg = "#{name}: Unable to match package '#{name}' but matched #{packages.size} " - debug_msg << packages.size == 1 ? "package" : "packages" + debug_msg << (packages.size == 1 ? "package" : "packages") debug_msg << ", selected '#{new_package_name}' version '#{new_package_version}'" Chef::Log.debug(debug_msg) diff --git a/lib/chef/provider/package/zypper.rb b/lib/chef/provider/package/zypper.rb index 2cd321660b..c2a3ac4ba8 100644 --- a/lib/chef/provider/package/zypper.rb +++ b/lib/chef/provider/package/zypper.rb @@ -29,46 +29,48 @@ class Chef class Package class Zypper < Chef::Provider::Package + provides :zypper_package, os: "linux" + def load_current_resource - @current_resource = Chef::Resource::Package.new(@new_resource.name) - @current_resource.package_name(@new_resource.package_name) + @current_resource = Chef::Resource::ZypperPackage.new(new_resource.name) + current_resource.package_name(new_resource.package_name) is_installed=false is_out_of_date=false version='' oud_version='' - Chef::Log.debug("#{@new_resource} checking zypper") - status = shell_out("zypper --non-interactive info #{@new_resource.package_name}") + Chef::Log.debug("#{new_resource} checking zypper") + status = shell_out_with_timeout("zypper --non-interactive info #{new_resource.package_name}") status.stdout.each_line do |line| case line when /^Version: (.+)$/ version = $1 - Chef::Log.debug("#{@new_resource} version #{$1}") + Chef::Log.debug("#{new_resource} version #{$1}") when /^Installed: Yes$/ is_installed=true - Chef::Log.debug("#{@new_resource} is installed") + Chef::Log.debug("#{new_resource} is installed") when /^Installed: No$/ is_installed=false - Chef::Log.debug("#{@new_resource} is not installed") + Chef::Log.debug("#{new_resource} is not installed") when /^Status: out-of-date \(version (.+) installed\)$/ is_out_of_date=true oud_version=$1 - Chef::Log.debug("#{@new_resource} out of date version #{$1}") + Chef::Log.debug("#{new_resource} out of date version #{$1}") end end if is_installed==false @candidate_version=version - @current_resource.version(nil) + current_resource.version(nil) end if is_installed==true if is_out_of_date==true - @current_resource.version(oud_version) + current_resource.version(oud_version) @candidate_version=version else - @current_resource.version(version) + current_resource.version(version) @candidate_version=version end end @@ -77,7 +79,7 @@ class Chef raise Chef::Exceptions::Package, "zypper failed - #{status.inspect}!" end - @current_resource + current_resource end def zypper_version() @@ -104,9 +106,9 @@ class Chef def zypper_package(command, pkgname, version) version = "=#{version}" unless version.nil? || version.empty? if zypper_version < 1.0 - shell_out!("zypper#{gpg_checks} #{command} -y #{pkgname}") + shell_out_with_timeout!("zypper#{gpg_checks} #{command} -y #{pkgname}") else - shell_out!("zypper --non-interactive#{gpg_checks} "+ + shell_out_with_timeout!("zypper --non-interactive#{gpg_checks} "+ "#{command} #{pkgname}#{version}") end end diff --git a/lib/chef/provider/powershell_script.rb b/lib/chef/provider/powershell_script.rb index f9dcd6d80c..ed44dee6ae 100644 --- a/lib/chef/provider/powershell_script.rb +++ b/lib/chef/provider/powershell_script.rb @@ -24,71 +24,153 @@ class Chef provides :powershell_script, os: "windows" + def initialize (new_resource, run_context) + super(new_resource, run_context, '.ps1') + add_exit_status_wrapper + end + + def action_run + valid_syntax = validate_script_syntax! + super if valid_syntax + end + + def flags + # Must use -File rather than -Command to launch the script + # file created by the base class that contains the script + # code -- otherwise, powershell.exe does not propagate the + # error status of a failed Windows process that ran at the + # end of the script, it gets changed to '1'. + interpreter_flags = [default_interpreter_flags, '-File'].join(' ') + + if ! (@new_resource.flags.nil?) + interpreter_flags = [@new_resource.flags, interpreter_flags].join(' ') + end + + interpreter_flags + end + protected - EXIT_STATUS_EXCEPTION_HANDLER = "\ntrap [Exception] {write-error -exception ($_.Exception.Message);exit 1}".freeze - EXIT_STATUS_NORMALIZATION_SCRIPT = "\nif ($? -ne $true) { if ( $LASTEXITCODE ) {exit $LASTEXITCODE} else { exit 1 }}".freeze - EXIT_STATUS_RESET_SCRIPT = "\n$global:LASTEXITCODE=$null".freeze - # Process exit codes are strange with PowerShell. Unless you - # explicitly call exit in Powershell, the powershell.exe - # interpreter returns only 0 for success or 1 for failure. Since - # we'd like to get specific exit codes from executable tools run - # with Powershell, we do some work using the automatic variables - # $? and $LASTEXITCODE to return the process exit code of the - # 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 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 - self.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 - Chef::Log.debug("powershell_script provider called with script code:\n\n#{code}\n") + # Process exit codes are strange with PowerShell and require + # special handling to cover common use cases. + def add_exit_status_wrapper + self.code = wrapper_script + Chef::Log.debug("powershell_script provider called with script code:\n\n#{@new_resource.code}\n") Chef::Log.debug("powershell_script provider will execute transformed code:\n\n#{self.code}\n") end - public + def validate_script_syntax! + interpreter_arguments = default_interpreter_flags.join(' ') + Tempfile.open(['chef_powershell_script-user-code', '.ps1']) do | user_script_file | + user_script_file.puts("{#{@new_resource.code}}") + user_script_file.close - def initialize (new_resource, run_context) - super(new_resource, run_context, '.ps1') - normalize_script_exit_status(new_resource.code) + validation_command = "\"#{interpreter}\" #{interpreter_arguments} -Command #{user_script_file.path}" + + # For consistency with other script resources, allow even syntax errors + # to be suppressed if the returns attribute would have suppressed it + # at converge. + valid_returns = [0] + specified_returns = @new_resource.returns.is_a?(Integer) ? + [@new_resource.returns] : + @new_resource.returns + valid_returns.concat([1]) if specified_returns.include?(1) + + result = shell_out!(validation_command, {returns: valid_returns}) + result.exitstatus == 0 + end end - def flags - default_flags = [ + def default_interpreter_flags + # 'Bypass' is preferable since it doesn't require user input confirmation + # for files such as PowerShell modules downloaded from the + # Internet. However, 'Bypass' is not supported prior to + # PowerShell 3.0, so the fallback is 'Unrestricted' + execution_policy = Chef::Platform.supports_powershell_execution_bypass?(run_context.node) ? 'Bypass' : 'Unrestricted' + + [ "-NoLogo", "-NonInteractive", "-NoProfile", - "-ExecutionPolicy Unrestricted", + "-ExecutionPolicy #{execution_policy}", # Powershell will hang if STDIN is redirected # http://connect.microsoft.com/PowerShell/feedback/details/572313/powershell-exe-can-hang-if-stdin-is-redirected - "-InputFormat None", - # Must use -File rather than -Command to launch the script - # file created by the base class that contains the script - # code -- otherwise, powershell.exe does not propagate the - # error status of a failed Windows process that ran at the - # end of the script, it gets changed to '1'. - "-File" + "-InputFormat None" ] + end - interpreter_flags = default_flags.join(' ') + # A wrapper script is used to launch user-supplied script while + # still obtaining useful process exit codes. Unless you + # explicitly call exit in Powershell, the powershell.exe + # interpreter returns only 0 for success or 1 for failure. Since + # we'd like to get specific exit codes from executable tools run + # with Powershell, we do some work using the automatic variables + # $? and $LASTEXITCODE to return the process exit code of the + # 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 wrapper_script +<<-EOH +# Chef Client wrapper for powershell_script resources - if ! (@new_resource.flags.nil?) - interpreter_flags = [@new_resource.flags, interpreter_flags].join(' ') - end +# LASTEXITCODE can be uninitialized -- make it explictly 0 +# to avoid incorrect detection of failure (non-zero) codes +$global:LASTEXITCODE = 0 - interpreter_flags +# Catch any exceptions -- without this, exceptions will result +# In a zero return code instead of the desired non-zero code +# that indicates a failure +trap [Exception] {write-error ($_.Exception.Message);exit 1} + +# Variable state that should not be accessible to the user code +new-variable -name interpolatedexitcode -visibility private -value $#{@new_resource.convert_boolean_return} +new-variable -name chefscriptresult -visibility private + +# Initialize a variable we use to capture $? inside a block +$global:lastcmdlet = $null + +# Execute the user's code in a script block -- +$chefscriptresult = +{ + #{@new_resource.code} + + # This assignment doesn't affect the block's return value + $global:lastcmdlet = $? +}.invokereturnasis() + +# Assume failure status of 1 -- success cases +# will have to override this +$exitstatus = 1 + +# If convert_boolean_return is enabled, the block's return value +# gets precedence in determining our exit status +if ($interpolatedexitcode -and $chefscriptresult -ne $null -and $chefscriptresult.gettype().name -eq 'boolean') +{ + $exitstatus = [int32](!$chefscriptresult) +} +elseif ($lastcmdlet) +{ + # Otherwise, a successful cmdlet execution defines the status + $exitstatus = 0 +} +elseif ( $LASTEXITCODE -ne $null -and $LASTEXITCODE -ne 0 ) +{ + # If the cmdlet status is failed, allow the Win32 status + # in $LASTEXITCODE to define exit status. This handles the case + # where no cmdlets, only Win32 processes have run since $? + # will be set to $false whenever a Win32 process returns a non-zero + # status. + $exitstatus = $LASTEXITCODE +} + +# If this script is launched with -File, the process exit +# status of PowerShell.exe will be $exitstatus. If it was +# launched with -Command, it will be 0 if $exitstatus was 0, +# 1 (i.e. failed) otherwise. +exit $exitstatus +EOH end + end end end diff --git a/lib/chef/provider/reboot.rb b/lib/chef/provider/reboot.rb index 8dde4653ec..22e77dcc13 100644 --- a/lib/chef/provider/reboot.rb +++ b/lib/chef/provider/reboot.rb @@ -22,6 +22,7 @@ require 'chef/provider' class Chef class Provider class Reboot < Chef::Provider + provides :reboot def whyrun_supported? true diff --git a/lib/chef/provider/registry_key.rb b/lib/chef/provider/registry_key.rb index 94f4e2655b..cd62f7c56f 100644 --- a/lib/chef/provider/registry_key.rb +++ b/lib/chef/provider/registry_key.rb @@ -31,6 +31,8 @@ class Chef class Provider class RegistryKey < Chef::Provider + provides :registry_key + include Chef::Mixin::Checksum def whyrun_supported? diff --git a/lib/chef/provider/remote_file.rb b/lib/chef/provider/remote_file.rb index da2573dacb..c4643edc0b 100644 --- a/lib/chef/provider/remote_file.rb +++ b/lib/chef/provider/remote_file.rb @@ -24,6 +24,7 @@ require 'chef/deprecation/warnings' class Chef class Provider class RemoteFile < Chef::Provider::File + provides :remote_file extend Chef::Deprecation::Warnings include Chef::Deprecation::Provider::RemoteFile diff --git a/lib/chef/provider/remote_file/content.rb b/lib/chef/provider/remote_file/content.rb index ef55dd77cd..4f450ce333 100644 --- a/lib/chef/provider/remote_file/content.rb +++ b/lib/chef/provider/remote_file/content.rb @@ -20,6 +20,7 @@ require 'uri' require 'tempfile' require 'chef/file_content_management/content_base' +require 'chef/mixin/uris' class Chef class Provider @@ -28,6 +29,8 @@ class Chef private + include Chef::Mixin::Uris + def file_for_provider Chef::Log.debug("#{@new_resource} checking for changes") @@ -45,7 +48,11 @@ class Chef sources = sources.dup source = sources.shift begin - uri = URI.parse(source) + uri = if Chef::Provider::RemoteFile::Fetcher.network_share?(source) + source + else + as_uri(source) + end raw_file = grab_file_from_uri(uri) rescue SocketError, Errno::ECONNREFUSED, Errno::ENOENT, Errno::EACCES, Timeout::Error, Net::HTTPServerException, Net::HTTPFatalError, Net::FTPError => e Chef::Log.warn("#{@new_resource} cannot be downloaded from #{source}: #{e.to_s}") diff --git a/lib/chef/provider/remote_file/fetcher.rb b/lib/chef/provider/remote_file/fetcher.rb index 249b29186f..53bfe9935c 100644 --- a/lib/chef/provider/remote_file/fetcher.rb +++ b/lib/chef/provider/remote_file/fetcher.rb @@ -23,15 +23,29 @@ class Chef class Fetcher def self.for_resource(uri, new_resource, current_resource) - case uri.scheme - when "http", "https" - Chef::Provider::RemoteFile::HTTP.new(uri, new_resource, current_resource) - when "ftp" - Chef::Provider::RemoteFile::FTP.new(uri, new_resource, current_resource) - when "file" - Chef::Provider::RemoteFile::LocalFile.new(uri, new_resource, current_resource) + if network_share?(uri) + Chef::Provider::RemoteFile::NetworkFile.new(uri, new_resource, current_resource) else - raise ArgumentError, "Invalid uri, Only http(s), ftp, and file are currently supported" + case uri.scheme + when "http", "https" + Chef::Provider::RemoteFile::HTTP.new(uri, new_resource, current_resource) + when "ftp" + Chef::Provider::RemoteFile::FTP.new(uri, new_resource, current_resource) + when "file" + Chef::Provider::RemoteFile::LocalFile.new(uri, new_resource, current_resource) + else + raise ArgumentError, "Invalid uri, Only http(s), ftp, and file are currently supported" + end + end + end + + # Windows network share: \\computername\share\file + def self.network_share?(source) + case source + when String + !!(%r{\A\\\\[A-Za-z0-9+\-\.]+} =~ source) + else + false end end diff --git a/lib/chef/provider/remote_file/local_file.rb b/lib/chef/provider/remote_file/local_file.rb index e78311f2c3..026206b64e 100644 --- a/lib/chef/provider/remote_file/local_file.rb +++ b/lib/chef/provider/remote_file/local_file.rb @@ -32,15 +32,21 @@ class Chef @new_resource = new_resource @uri = uri end - + # CHEF-4472: Remove the leading slash from windows paths that we receive from a file:// URI - def fix_windows_path(path) - path.gsub(/^\/([a-zA-Z]:)/,'\1') + def fix_windows_path(path) + path.gsub(/^\/([a-zA-Z]:)/,'\1') + end + + def source_path + @source_path ||= begin + path = URI.unescape(uri.path) + Chef::Platform.windows? ? fix_windows_path(path) : path + end end # Fetches the file at uri, returning a Tempfile-like File handle def fetch - source_path = Chef::Platform.windows? ? fix_windows_path(uri.path) : uri.path tempfile = Chef::FileContentManagement::Tempfile.new(new_resource).tempfile Chef::Log.debug("#{new_resource} staging #{source_path} to #{tempfile.path}") FileUtils.cp(source_path, tempfile.path) diff --git a/lib/chef/provider/remote_file/network_file.rb b/lib/chef/provider/remote_file/network_file.rb new file mode 100644 index 0000000000..093a388d2a --- /dev/null +++ b/lib/chef/provider/remote_file/network_file.rb @@ -0,0 +1,48 @@ +# +# Author:: Jesse Campbell (<hikeit@gmail.com>) +# Copyright:: Copyright (c) 2013 Jesse Campbell +# 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 'uri' +require 'tempfile' +require 'chef/provider/remote_file' + +class Chef + class Provider + class RemoteFile + class NetworkFile + + attr_reader :new_resource + + def initialize(source, new_resource, current_resource) + @new_resource = new_resource + @source = source + end + + # Fetches the file on a network share, returning a Tempfile-like File handle + # windows only + def fetch + tempfile = Chef::FileContentManagement::Tempfile.new(new_resource).tempfile + Chef::Log.debug("#{new_resource} staging #{@source} to #{tempfile.path}") + FileUtils.cp(@source, tempfile.path) + tempfile.close if tempfile + tempfile + end + + end + end + end +end diff --git a/lib/chef/provider/service.rb b/lib/chef/provider/service.rb index 75da2ddb31..9c523b5e66 100644 --- a/lib/chef/provider/service.rb +++ b/lib/chef/provider/service.rb @@ -168,6 +168,50 @@ class Chef @new_resource.respond_to?(method_name) && !!@new_resource.send(method_name) end + + module ServicePriorityInit + + # + # Platform-specific versions + # + + # + # Linux + # + + require 'chef/chef_class' + require 'chef/provider/service/systemd' + require 'chef/provider/service/insserv' + require 'chef/provider/service/redhat' + require 'chef/provider/service/arch' + require 'chef/provider/service/gentoo' + require 'chef/provider/service/upstart' + require 'chef/provider/service/debian' + require 'chef/provider/service/invokercd' + require 'chef/provider/service/freebsd' + require 'chef/provider/service/openbsd' + require 'chef/provider/service/solaris' + require 'chef/provider/service/macosx' + + def self.os(os, *providers) + Chef.set_provider_priority_array(:service, providers, os: os) + end + def self.platform_family(platform_family, *providers) + Chef.set_provider_priority_array(:service, providers, platform_family: platform_family) + end + + os %w(freebsd netbsd), Freebsd + os %w(openbsd), Openbsd + os %w(solaris2), Solaris + os %w(darwin), Macosx + os %w(linux), Systemd, Insserv, Redhat + + platform_family %w(arch), Systemd, Arch + platform_family %w(gentoo), Systemd, Gentoo + platform_family %w(debian), Systemd, Upstart, Insserv, Debian, Invokercd + platform_family %w(rhel fedora suse), Systemd, Insserv, Redhat + + end end end end diff --git a/lib/chef/provider/service/aix.rb b/lib/chef/provider/service/aix.rb index 0aef62c62e..09ed4bbf01 100644 --- a/lib/chef/provider/service/aix.rb +++ b/lib/chef/provider/service/aix.rb @@ -91,15 +91,18 @@ class Chef protected def determine_current_status! - Chef::Log.debug "#{@new_resource} using lssrc to check the status " + Chef::Log.debug "#{@new_resource} using lssrc to check the status" begin - services = shell_out!("lssrc -a | grep -w #{@new_resource.service_name}").stdout.split("\n") - is_resource_group?(services) - - if services.length == 1 && services[0].split(' ').last == "active" - @current_resource.running true - else + if is_resource_group? + # Groups as a whole have no notion of whether they're running @current_resource.running false + else + service = shell_out!("lssrc -s #{@new_resource.service_name}").stdout + if service.split(' ').last == 'active' + @current_resource.running true + else + @current_resource.running false + end end Chef::Log.debug "#{@new_resource} running: #{@current_resource.running}" # ShellOut sometimes throws different types of Exceptions than ShellCommandFailed. @@ -112,11 +115,9 @@ class Chef end end - def is_resource_group? (services) - if services.length > 1 - Chef::Log.debug("#{@new_resource.service_name} is a group") - @is_resource_group = true - elsif services[0].split(' ')[1] == @new_resource.service_name + def is_resource_group? + so = shell_out!("lssrc -g #{@new_resource.service_name}") + if so.exitstatus == 0 Chef::Log.debug("#{@new_resource.service_name} is a group") @is_resource_group = true end diff --git a/lib/chef/provider/service/freebsd.rb b/lib/chef/provider/service/freebsd.rb index 9204e3ef92..6c78f86fe0 100644 --- a/lib/chef/provider/service/freebsd.rb +++ b/lib/chef/provider/service/freebsd.rb @@ -147,7 +147,7 @@ class Chef # some scripts support multiple instances through symlinks such as openvpn. # We should get the service name from rcvar. Chef::Log.debug("name=\"service\" not found at #{init_command}. falling back to rcvar") - sn = shell_out!("#{init_command} rcvar").stdout[/(\w+_enable)=/, 1] + shell_out!("#{init_command} rcvar").stdout[/(\w+_enable)=/, 1] else # for why-run mode when the rcd_script is not there yet new_resource.service_name diff --git a/lib/chef/provider/service/init.rb b/lib/chef/provider/service/init.rb index 0a219a69e1..355e98a0eb 100644 --- a/lib/chef/provider/service/init.rb +++ b/lib/chef/provider/service/init.rb @@ -18,6 +18,7 @@ require 'chef/provider/service/simple' require 'chef/mixin/command' +require 'chef/platform/service_helpers' class Chef class Provider diff --git a/lib/chef/provider/service/macosx.rb b/lib/chef/provider/service/macosx.rb index 7cfe57a92a..7324822eff 100644 --- a/lib/chef/provider/service/macosx.rb +++ b/lib/chef/provider/service/macosx.rb @@ -28,8 +28,8 @@ class Chef class Service class Macosx < Chef::Provider::Service::Simple - provides :service, os: "darwin" provides :macosx_service, os: "darwin" + provides :service, os: "darwin" def self.gather_plist_dirs locations = %w{/Library/LaunchAgents diff --git a/lib/chef/provider/service/windows.rb b/lib/chef/provider/service/windows.rb index ba53f0a3c3..355ffafc2a 100644 --- a/lib/chef/provider/service/windows.rb +++ b/lib/chef/provider/service/windows.rb @@ -25,7 +25,6 @@ if RUBY_PLATFORM =~ /mswin|mingw32|windows/ end class Chef::Provider::Service::Windows < Chef::Provider::Service - provides :service, os: "windows" provides :windows_service, os: "windows" diff --git a/lib/chef/provider/user.rb b/lib/chef/provider/user.rb index f6ac72448e..ad92a72a0a 100644 --- a/lib/chef/provider/user.rb +++ b/lib/chef/provider/user.rb @@ -23,6 +23,7 @@ require 'etc' class Chef class Provider class User < Chef::Provider + provides :user include Chef::Mixin::Command @@ -208,7 +209,6 @@ class Chef def unlock_user raise NotImplementedError end - end end end diff --git a/lib/chef/provider/user/aix.rb b/lib/chef/provider/user/aix.rb index af08ab4364..a575a41e54 100644 --- a/lib/chef/provider/user/aix.rb +++ b/lib/chef/provider/user/aix.rb @@ -18,9 +18,10 @@ class Chef class Provider class User class Aix < Chef::Provider::User::Useradd + provides :user, platform: %w(aix) UNIVERSAL_OPTIONS = [[:comment, "-c"], [:gid, "-g"], [:shell, "-s"], [:uid, "-u"]] - + def create_user super add_password @@ -88,7 +89,7 @@ class Chef end end end - + end end end diff --git a/lib/chef/provider/user/pw.rb b/lib/chef/provider/user/pw.rb index fe71e93561..810ffb9a8d 100644 --- a/lib/chef/provider/user/pw.rb +++ b/lib/chef/provider/user/pw.rb @@ -22,6 +22,7 @@ class Chef class Provider class User class Pw < Chef::Provider::User + provides :user, platform: %w(freebsd) def load_current_resource super diff --git a/lib/chef/provider/user/solaris.rb b/lib/chef/provider/user/solaris.rb index d480acaced..b242095f0c 100644 --- a/lib/chef/provider/user/solaris.rb +++ b/lib/chef/provider/user/solaris.rb @@ -22,6 +22,8 @@ class Chef class Provider class User class Solaris < Chef::Provider::User::Useradd + provides :user, platform: %w(omnios solaris2) + UNIVERSAL_OPTIONS = [[:comment, "-c"], [:gid, "-g"], [:shell, "-s"], [:uid, "-u"]] attr_writer :password_file diff --git a/lib/chef/provider/user/useradd.rb b/lib/chef/provider/user/useradd.rb index cc770c0be2..a1b5b3459c 100644 --- a/lib/chef/provider/user/useradd.rb +++ b/lib/chef/provider/user/useradd.rb @@ -23,6 +23,7 @@ class Chef class Provider class User class Useradd < Chef::Provider::User + provides :user UNIVERSAL_OPTIONS = [[:comment, "-c"], [:gid, "-g"], [:password, "-p"], [:shell, "-s"], [:uid, "-u"]] diff --git a/lib/chef/provider_resolver.rb b/lib/chef/provider_resolver.rb index 867c3deca8..5bfee343d1 100644 --- a/lib/chef/provider_resolver.rb +++ b/lib/chef/provider_resolver.rb @@ -20,6 +20,30 @@ require 'chef/exceptions' require 'chef/platform/provider_priority_map' class Chef + # + # Provider Resolution + # =================== + # + # Provider resolution is the process of taking a Resource object and an + # action, and determining the Provider class that should be instantiated to + # handle the action. + # + # If the resource has its `provider` set, that is used. + # + # Otherwise, we take the lists of Providers that have registered as + # providing the DSL through `provides :dsl_name, <filters>` or + # `Chef.set_resource_priority_array :dsl_name, <filters>`. We filter each + # list of Providers through: + # + # 1. The filters it was registered with (such as `os: 'linux'` or + # `platform_family: 'debian'`) + # 2. `provides?(node, resource)` + # 3. `supports?(resource, action)` + # + # Anything that passes the filter and returns `true` to provides and supports, + # is considered a match. The first matching Provider in the *most recently + # registered list* is selected and returned. + # class ProviderResolver attr_reader :node @@ -32,31 +56,14 @@ class Chef @action = action end - # return a deterministically sorted list of Chef::Provider subclasses - def providers - @providers ||= Chef::Provider.descendants - end - def resolve maybe_explicit_provider(resource) || maybe_dynamic_provider_resolution(resource, action) || maybe_chef_platform_lookup(resource) end - # this cut looks at if the provider can handle the resource type on the node - def enabled_handlers - @enabled_handlers ||= - providers.select do |klass| - klass.provides?(node, resource.resource_name) - end.sort {|a,b| a.to_s <=> b.to_s } - end - - # this cut looks at if the provider can handle the specific resource and action - def supported_handlers - @supported_handlers ||= - enabled_handlers.select do |klass| - klass.supports?(resource, action) - end + def provided_by?(provider_class) + prioritized_handlers.include?(provider_class) end private @@ -69,40 +76,37 @@ class Chef # try dynamically finding a provider based on querying the providers to see what they support def maybe_dynamic_provider_resolution(resource, action) - # log this so we know what providers will work for the generic resource on the node (early cut) - Chef::Log.debug "providers for generic #{resource.resource_name} resource enabled on node include: #{enabled_handlers}" - - # what providers were excluded by machine state (late cut) - Chef::Log.debug "providers that refused resource #{resource} were: #{enabled_handlers - supported_handlers}" - Chef::Log.debug "providers that support resource #{resource} include: #{supported_handlers}" - - # if none of the providers specifically support the resource, we still need to pick one of the providers that are - # enabled on the node to handle the why-run use case. - handlers = supported_handlers.empty? ? enabled_handlers : supported_handlers - Chef::Log.debug "no providers supported the resource, falling back to enabled handlers" if supported_handlers.empty? - - if handlers.count >= 2 - # this magic stack ranks the providers by where they appear in the provider_priority_map, it is mostly used - # to pick amongst N different ways to start init scripts on different debian/ubuntu systems. - priority_list = [ get_priority_array(node, resource.resource_name) ].flatten.compact - handlers = handlers.sort_by { |x| i = priority_list.index x; i.nil? ? Float::INFINITY : i } - if priority_list.index(handlers.first).nil? - # if we had more than one and we picked one with a precidence of infinity that means that the resource_priority_map - # entry for this resource is missing -- we should probably raise here and force resolution of the ambiguity. - Chef::Log.warn "Ambiguous provider precedence: #{handlers}, please use Chef.set_provider_priority_array to provide determinism" - end - handlers = [ handlers.first ] + Chef::Log.debug "Providers for generic #{resource.resource_name} resource enabled on node include: #{enabled_handlers}" + + # Get all the handlers in the priority bucket + handlers = prioritized_handlers + + # Narrow it down to handlers that return `true` to `provides?` + # TODO deprecate this and don't bother calling--the fact that they said + # `provides` should be enough. But we need to do it right now because + # some classes implement additional handling. + enabled_handlers = prioritized_handlers.select { |handler| handler.provides?(node, resource) } + + # Narrow it down to handlers that return `true` to `supports?` + # TODO deprecate this and allow actions to be passed as a filter to + # `provides` so we don't have to have two separate things. + supported_handlers = enabled_handlers.select { |handler| handler.supports?(resource, action) } + if supported_handlers.empty? + # if none of the providers specifically support the resource, we still need to pick one of the providers that are + # enabled on the node to handle the why-run use case. FIXME we should only do this in why-run mode then. + Chef::Log.debug "No providers responded true to `supports?` for action #{action} on resource #{resource}, falling back to enabled handlers so we can return something anyway." + handler = enabled_handlers.first + else + handler = supported_handlers.first end - Chef::Log.debug "providers that survived replacement include: #{handlers}" - - raise Chef::Exceptions::AmbiguousProviderResolution.new(resource, handlers) if handlers.count >= 2 - - Chef::Log.debug "dynamic provider resolver FAILED to resolve a provider" if handlers.empty? - - return nil if handlers.empty? + if handler + Chef::Log.debug "Provider for action #{action} on resource #{resource} is #{handler}" + else + Chef::Log.debug "Dynamic provider resolver FAILED to resolve a provider for action #{action} on resource #{resource}" + end - handlers[0] + handler end # try the old static lookup of providers by platform @@ -110,13 +114,51 @@ class Chef Chef::Platform.find_provider_for_node(node, resource) end - # dep injection hooks - def get_priority_array(node, resource_name) - provider_priority_map.get_priority_array(node, resource_name) - end - def provider_priority_map Chef::Platform::ProviderPriorityMap.instance end + + def prioritized_handlers + @prioritized_handlers ||= + provider_priority_map.list_handlers(node, resource.resource_name).flatten(1).uniq + end + + module Deprecated + # return a deterministically sorted list of Chef::Provider subclasses + def providers + @providers ||= Chef::Provider.descendants + end + + # this cut looks at if the provider can handle the resource type on the node + def enabled_handlers + @enabled_handlers ||= + providers.select do |klass| + # NB: this is different from resource_resolver which must pass a resource_name + # FIXME: deprecate this and normalize on passing resource_name here + klass.provides?(node, resource) + end.sort {|a,b| a.to_s <=> b.to_s } + end + + # this cut looks at if the provider can handle the specific resource and action + def supported_handlers + @supported_handlers ||= + enabled_handlers.select do |klass| + klass.supports?(resource, action) + end + end + + # If there are no providers for a DSL, we search through the + def prioritized_handlers + @prioritized_handlers ||= super || begin + result = providers.select { |handler| handler.provides?(node, resource) }.sort_by(:name) + if !result.empty? + Chef::Log.deprecation("#{resource.resource_name.to_sym} is marked as providing DSL #{method_symbol}, but provides #{resource.resource_name.to_sym.inspect} was never called!") + Chef::Log.deprecation("In Chef 13, this will break: you must call provides to mark the names you provide, even if you also override provides? yourself.") + end + result + end + end + end + prepend Deprecated end end diff --git a/lib/chef/providers.rb b/lib/chef/providers.rb index a5f5386de3..18500d4669 100644 --- a/lib/chef/providers.rb +++ b/lib/chef/providers.rb @@ -122,6 +122,7 @@ require 'chef/provider/deploy/timestamped' require 'chef/provider/remote_file/ftp' require 'chef/provider/remote_file/http' require 'chef/provider/remote_file/local_file' +require 'chef/provider/remote_file/network_file' require 'chef/provider/remote_file/fetcher' require "chef/provider/lwrp_base" diff --git a/lib/chef/resource.rb b/lib/chef/resource.rb index d934ec8c47..7fe8a52d95 100644 --- a/lib/chef/resource.rb +++ b/lib/chef/resource.rb @@ -22,6 +22,7 @@ require 'chef/dsl/platform_introspection' require 'chef/dsl/data_query' require 'chef/dsl/registry_helper' require 'chef/dsl/reboot_pending' +require 'chef/dsl/resources' require 'chef/mixin/convert_to_class_name' require 'chef/guard_interpreter/resource_guard_interpreter' require 'chef/resource/conditional' @@ -31,9 +32,14 @@ require 'chef/node_map' require 'chef/node' require 'chef/platform' require 'chef/resource/resource_notification' +require 'chef/provider_resolver' +require 'chef/resource_resolver' +require 'set' require 'chef/mixin/deprecation' require 'chef/mixin/provides' +require 'chef/mixin/shell_out' +require 'chef/mixin/powershell_out' class Chef class Resource @@ -48,6 +54,12 @@ class Chef include Chef::DSL::RebootPending extend Chef::Mixin::Provides + # This lets user code do things like `not_if { shell_out!("command") }` + include Chef::Mixin::ShellOut + include Chef::Mixin::PowershellOut + + NULL_ARG = Object.new + # # The node the current Chef run is using. # @@ -79,7 +91,6 @@ class Chef run_context.resource_collection.find(*args) end - # # Resource User Interface (for users) # @@ -98,8 +109,8 @@ class Chef @before = nil @params = Hash.new @provider = nil - @allowed_actions = [ :nothing ] - @action = :nothing + @allowed_actions = self.class.allowed_actions.to_a + @action = self.class.default_action @updated = false @updated_by_last_action = false @supports = {} @@ -160,19 +171,24 @@ class Chef # @param arg [Array[Symbol], Symbol] A list of actions (e.g. `:create`) # @return [Array[Symbol]] the list of actions. # + attr_accessor :action def action(arg=nil) if arg - action_list = arg.kind_of?(Array) ? arg : [ arg ] - action_list = action_list.collect { |a| a.to_sym } - action_list.each do |action| + if arg.is_a?(Array) + arg = arg.map { |a| a.to_sym } + else + arg = arg.to_sym + end + Array(arg).each do |action| validate( { action: action }, - { action: { kind_of: Symbol, equal_to: @allowed_actions } } + { action: { kind_of: Symbol, equal_to: allowed_actions } } ) end - @action = action_list + self.action = arg else - @action + # Pull the action from the class if it's not set + @action || self.class.default_action end end @@ -180,8 +196,7 @@ class Chef # Sets up a notification that will run a particular action on another resource # if and when *this* resource is updated by an action. # - # If the action does nothing--does not update this resource, the - # notification never triggers.) + # If the action does not update this resource, the notification never triggers. # # Only one resource may be specified per notification. # @@ -467,7 +482,7 @@ class Chef # # @return [Hash{Symbol => Object}] A Hash of attribute => value for the # Resource class's `state_attrs`. - def state + def state_for_resource_reporter self.class.state_attrs.inject({}) do |state_attrs, attr_name| state_attrs[attr_name] = send(attr_name) state_attrs @@ -475,6 +490,15 @@ class Chef end # + # Since there are collisions with LWRP parameters named 'state' this + # method is not used by the resource_reporter and is most likely unused. + # It certainly cannot be relied upon and cannot be fixed. + # + # @deprecated + # + alias_method :state, :state_for_resource_reporter + + # # The value of the identity attribute, if declared. Falls back to #name if # no identity attribute is declared. # @@ -588,14 +612,14 @@ class Chef # def to_s - "#{@resource_name}[#{@name}]" + "#{resource_name}[#{name}]" end def to_text return "suppressed sensitive resource output" if sensitive ivars = instance_variables.map { |ivar| ivar.to_sym } - HIDDEN_IVARS text = "# Declared in #{@source_line}\n\n" - text << self.class.dsl_name + "(\"#{name}\") do\n" + text << "#{resource_name}(\"#{name}\") do\n" ivars.each do |ivar| if (value = instance_variable_get(ivar)) && !(value.respond_to?(:empty?) && value.empty?) value_string = value.respond_to?(:to_text) ? value.to_text : value.inspect @@ -749,6 +773,12 @@ class Chef # have. # attr_accessor :allowed_actions + def allowed_actions(value=NULL_ARG) + if value != NULL_ARG + self.allowed_actions = value + end + @allowed_actions + end # # Whether or not this resource was updated during an action. If multiple @@ -807,19 +837,15 @@ class Chef end # - # The DSL name of this resource (e.g. `package` or `yum_package`) + # The display name of this resource type, for printing purposes. # - # @return [String] The DSL name of this resource. - def self.dsl_name - convert_to_snake_case(name, 'Chef::Resource') - end - + # Will be used to print out the resource in messages, e.g. resource_name[name] # - # The name of this resource (e.g. `file`) + # @return [Symbol] The name of this resource type (e.g. `:execute`). # - # @return [String] The name of this resource. - # - attr_reader :resource_name + def resource_name + @resource_name || self.class.resource_name + end # # Sets a list of capabilities of the real resource. For example, `:remount` @@ -852,6 +878,66 @@ class Chef end # + # The DSL name of this resource (e.g. `package` or `yum_package`) + # + # @return [String] The DSL name of this resource. + # + # @deprecated Use resource_name instead. + # + def self.dsl_name + Chef::Log.deprecation "Resource.dsl_name is deprecated and will be removed in Chef 13. Use resource_name instead." + if name + name = self.name.split('::')[-1] + convert_to_snake_case(name) + end + end + + # + # The display name of this resource type, for printing purposes. + # + # This also automatically calls "provides" to provide DSL with the given + # name. + # + # resource_name defaults to your class name. + # + # Call `resource_name nil` to remove the resource name (and any + # corresponding DSL). + # + # @param value [Symbol] The desired name of this resource type (e.g. + # `execute`), or `nil` if this class is abstract and has no resource_name. + # + # @return [Symbol] The name of this resource type (e.g. `:execute`). + # + def self.resource_name(name=NULL_ARG) + # Setter + if name != NULL_ARG + remove_canonical_dsl + + # Set the resource_name and call provides + if name + name = name.to_sym + # If our class is not already providing this name, provide it. + if !Chef::ResourceResolver.list(name).include?(self) + provides name, canonical: true + end + @resource_name = name + else + @resource_name = nil + end + else + # set resource_name automatically if it's not set + if !instance_variable_defined?(:@resource_name) && self.name + resource_name convert_to_snake_case(self.name.split('::')[-1]) + end + end + + @resource_name + end + def self.resource_name=(name) + resource_name(name) + end + + # # The module where Chef should look for providers for this resource. # The provider for `MyResource` will be looked up using # `provider_base::MyResource`. Defaults to `Chef::Provider`. @@ -865,11 +951,70 @@ class Chef # # ...other stuff # end # + # @deprecated Use `provides` on the provider, or `provider` on the resource, instead. + # def self.provider_base(arg=nil) - @provider_base ||= arg - @provider_base ||= Chef::Provider + if arg + Chef::Log.deprecation("Resource.provider_base is deprecated and will be removed in Chef 13. Use provides on the provider, or provider on the resource, instead.") + end + @provider_base ||= arg || Chef::Provider + end + + # + # The list of allowed actions for the resource. + # + # @param actions [Array<Symbol>] The list of actions to add to allowed_actions. + # + # @return [Arrau<Symbol>] The list of actions, as symbols. + # + def self.allowed_actions(*actions) + @allowed_actions ||= + if superclass.respond_to?(:allowed_actions) + superclass.allowed_actions.dup + else + [ :nothing ] + end + @allowed_actions |= actions end + def self.allowed_actions=(value) + @allowed_actions = value + end + + # + # The action that will be run if no other action is specified. + # + # Setting default_action will automatially add the action to + # allowed_actions, if it isn't already there. + # + # Defaults to :nothing. + # + # @param action_name [Symbol,Array<Symbol>] The default action (or series + # of actions) to use. + # + # @return [Symbol,Array<Symbol>] The default actions for the resource. + # + def self.default_action(action_name=NULL_ARG) + unless action_name.equal?(NULL_ARG) + if action_name.is_a?(Array) + @default_action = action_name.map { |arg| arg.to_sym } + else + @default_action = action_name.to_sym + end + self.allowed_actions |= Array(@default_action) + end + + if @default_action + @default_action + elsif superclass.respond_to?(:default_action) + superclass.default_action + else + :nothing + end + end + def self.default_action=(action_name) + default_action(action_name) + end # # Internal Resource Interface (for Chef) @@ -945,10 +1090,31 @@ class Chef # NOTE: that we do not support unregistering classes as descendents like # we used to for LWRP unloading because that was horrible and removed in # Chef-12. + # @deprecated + # @api private alias :resource_classes :descendants + # @deprecated + # @api private alias :find_subclass_by_name :find_descendants_by_name end + # @deprecated + # @api private + # We memoize a sorted version of descendants so that resource lookups don't + # have to sort all the things, all the time. + # This was causing performance issues in test runs, and probably in real + # life as well. + @@sorted_descendants = nil + def self.sorted_descendants + @@sorted_descendants ||= descendants.sort_by { |x| x.to_s } + end + def self.inherited(child) + super + @sorted_descendants = nil + child.resource_name + end + + # If an unknown method is invoked, determine whether the enclosing Provider's # lexical scope can fulfill the request. E.g. This happens when the Resource's # block invokes new_resource. @@ -960,6 +1126,32 @@ class Chef end end + # + # Mark this resource as providing particular DSL. + # + # Resources have an automatic DSL based on their resource_name, equivalent to + # `provides :resource_name` (providing the resource on all OS's). If you + # declare a `provides` with the given resource_name, it *replaces* that + # provides (so that you can provide your resource DSL only on certain OS's). + # + def self.provides(name, **options, &block) + name = name.to_sym + + # `provides :resource_name, os: 'linux'`) needs to remove the old + # canonical DSL before adding the new one. + if @resource_name && name == @resource_name + remove_canonical_dsl + end + + result = Chef.set_resource_priority_array(name, self, options, &block) + Chef::DSL::Resources.add_resource_dsl(name) + result + end + + def self.provides?(node, resource) + Chef::ResourceResolver.resolve(resource, node: node).provided_by?(self) + end + # Helper for #notifies def validate_resource_spec!(resource_spec) run_context.resource_collection.validate_lookup_spec!(resource_spec) @@ -1016,7 +1208,6 @@ class Chef end def provider_for_action(action) - require 'chef/provider_resolver' provider = Chef::ProviderResolver.new(node, self, action).resolve.new(self, run_context) provider.action = action provider @@ -1090,30 +1281,90 @@ class Chef # === Returns # <Chef::Resource>:: returns the proper Chef::Resource class def self.resource_for_node(short_name, node) - require 'chef/resource_resolver' - klass = Chef::ResourceResolver.new(node, short_name).resolve + klass = Chef::ResourceResolver.resolve(short_name, node: node) raise Chef::Exceptions::NoSuchResourceType.new(short_name, node) if klass.nil? klass end - # Returns the class of a Chef::Resource based on the short name + # + # Returns the class with the given resource_name. + # # ==== Parameters # short_name<Symbol>:: short_name of the resource (ie :directory) # # === Returns # <Chef::Resource>:: returns the proper Chef::Resource class + # def self.resource_matching_short_name(short_name) - begin - rname = convert_to_class_name(short_name.to_s) - Chef::Resource.const_get(rname) - rescue NameError - nil + Chef::ResourceResolver.resolve(short_name, canonical: true) + end + + # @api private + def self.register_deprecated_lwrp_class(resource_class, class_name) + if Chef::Resource.const_defined?(class_name, false) + Chef::Log.warn "#{class_name} already exists! Deprecation class overwrites #{resource_class}" + Chef::Resource.send(:remove_const, class_name) end + + # In order to generate deprecation warnings when you use Chef::Resource::MyLwrp, + # we make a special subclass (identical in nearly all respects) of the + # actual LWRP. When you say any of these, a deprecation warning will be + # generated: + # + # - Chef::Resource::MyLwrp.new(...) + # - resource.is_a?(Chef::Resource::MyLwrp) + # - resource.kind_of?(Chef::Resource::MyLwrp) + # - case resource + # when Chef::Resource::MyLwrp + # end + # + resource_subclass = class_eval <<-EOM, __FILE__, __LINE__+1 + class Chef::Resource::#{class_name} < resource_class + resource_name nil # we do not actually provide anything + def initialize(*args, &block) + Chef::Log.deprecation("Using an LWRP by its name (#{class_name}) directly is no longer supported in Chef 13 and will be removed. Use Chef::Resource.resource_for_node(node, name) instead.") + super + end + def self.resource_name(*args) + if args.empty? + @resource_name ||= superclass.resource_name + else + super + end + end + self + end + EOM + # Make case, is_a and kind_of work with the new subclass, for backcompat. + # Any subclass of Chef::Resource::ResourceClass is already a subclass of resource_class + # Any subclass of resource_class is considered a subclass of Chef::Resource::ResourceClass + resource_class.class_eval do + define_method(:is_a?) do |other| + other.is_a?(Module) && other === self + end + define_method(:kind_of?) do |other| + other.is_a?(Module) && other === self + end + end + resource_subclass.class_eval do + define_singleton_method(:===) do |other| + Chef::Log.deprecation("Using an LWRP by its name (#{class_name}) directly is no longer supported in Chef 13 and will be removed. Use Chef::Resource.resource_for_node(node, name) instead.") + # resource_subclass is a superclass of all resource_class descendants. + if self == resource_subclass && other.class <= resource_class + return true + end + super(other) + end + end + deprecated_constants[class_name.to_sym] = resource_subclass end - private + def self.deprecated_constants + @deprecated_constants ||= {} + end - def lookup_provider_constant(name) + # @api private + def lookup_provider_constant(name, action=:nothing) begin self.class.provider_base.const_get(convert_to_class_name(name.to_s)) rescue NameError => e @@ -1124,5 +1375,19 @@ class Chef end end end + + private + + def self.remove_canonical_dsl + if @resource_name + remaining = Chef.resource_priority_map.delete_canonical(@resource_name, self) + if !remaining + Chef::DSL::Resources.remove_resource_dsl(@resource_name) + end + end + end end end + +# Requiring things at the bottom breaks cycles +require 'chef/chef_class' diff --git a/lib/chef/resource/apt_package.rb b/lib/chef/resource/apt_package.rb index f944825ac3..ca119b50c4 100644 --- a/lib/chef/resource/apt_package.rb +++ b/lib/chef/resource/apt_package.rb @@ -23,12 +23,10 @@ class Chef class Resource class AptPackage < Chef::Resource::Package - provides :apt_package provides :package, os: "linux", platform_family: [ "debian" ] def initialize(name, run_context=nil) super - @resource_name = :apt_package @default_release = nil end diff --git a/lib/chef/resource/bash.rb b/lib/chef/resource/bash.rb index 0add0ce501..025687e879 100644 --- a/lib/chef/resource/bash.rb +++ b/lib/chef/resource/bash.rb @@ -25,7 +25,6 @@ class Chef def initialize(name, run_context=nil) super - @resource_name = :bash @interpreter = "bash" end diff --git a/lib/chef/resource/batch.rb b/lib/chef/resource/batch.rb index c091ec56b6..efe3f2205f 100644 --- a/lib/chef/resource/batch.rb +++ b/lib/chef/resource/batch.rb @@ -25,7 +25,7 @@ class Chef provides :batch, os: "windows" def initialize(name, run_context=nil) - super(name, run_context, :batch, "cmd.exe") + super(name, run_context, nil, "cmd.exe") end end diff --git a/lib/chef/resource/bff_package.rb b/lib/chef/resource/bff_package.rb index 917f0d1d50..7c1496a46b 100644 --- a/lib/chef/resource/bff_package.rb +++ b/lib/chef/resource/bff_package.rb @@ -22,14 +22,6 @@ require 'chef/provider/package/aix' class Chef class Resource class BffPackage < Chef::Resource::Package - - def initialize(name, run_context=nil) - super - @resource_name = :bff_package - end - end end end - - diff --git a/lib/chef/resource/breakpoint.rb b/lib/chef/resource/breakpoint.rb index b2210262d2..69dbc48050 100644 --- a/lib/chef/resource/breakpoint.rb +++ b/lib/chef/resource/breakpoint.rb @@ -22,14 +22,12 @@ require 'chef/resource' class Chef class Resource class Breakpoint < Chef::Resource + default_action :break def initialize(action="break", *args) - @name = caller.first - super(@name, *args) - @action = "break" - @allowed_actions << :break - @resource_name = :breakpoint + super(caller.first, *args) end + end end end diff --git a/lib/chef/resource/chef_gem.rb b/lib/chef/resource/chef_gem.rb index 59f575a524..0c2fdfa819 100644 --- a/lib/chef/resource/chef_gem.rb +++ b/lib/chef/resource/chef_gem.rb @@ -23,11 +23,8 @@ class Chef class Resource class ChefGem < Chef::Resource::Package::GemPackage - provides :chef_gem - def initialize(name, run_context=nil) super - @resource_name = :chef_gem @compile_time = Chef::Config[:chef_gem_compile_time] @gem_binary = RbConfig::CONFIG['bindir'] + "/gem" end diff --git a/lib/chef/resource/cookbook_file.rb b/lib/chef/resource/cookbook_file.rb index 7be353b648..42f16e6db6 100644 --- a/lib/chef/resource/cookbook_file.rb +++ b/lib/chef/resource/cookbook_file.rb @@ -27,13 +27,11 @@ class Chef class CookbookFile < Chef::Resource::File include Chef::Mixin::Securable - provides :cookbook_file + default_action :create def initialize(name, run_context=nil) super @provider = Chef::Provider::CookbookFile - @resource_name = :cookbook_file - @action = "create" @source = ::File.basename(name) @cookbook = nil end diff --git a/lib/chef/resource/cron.rb b/lib/chef/resource/cron.rb index cb16506012..93cf41bc37 100644 --- a/lib/chef/resource/cron.rb +++ b/lib/chef/resource/cron.rb @@ -27,13 +27,11 @@ class Chef state_attrs :minute, :hour, :day, :month, :weekday, :user - provides :cron + default_action :create + allowed_actions :create, :delete def initialize(name, run_context=nil) super - @resource_name = :cron - @action = :create - @allowed_actions.push(:create, :delete) @minute = "*" @hour = "*" @day = "*" diff --git a/lib/chef/resource/csh.rb b/lib/chef/resource/csh.rb index 36659c349b..d5e9c910b1 100644 --- a/lib/chef/resource/csh.rb +++ b/lib/chef/resource/csh.rb @@ -25,7 +25,6 @@ class Chef def initialize(name, run_context=nil) super - @resource_name = :csh @interpreter = "csh" end diff --git a/lib/chef/resource/deploy.rb b/lib/chef/resource/deploy.rb index 4252aa230f..3e5255bced 100644 --- a/lib/chef/resource/deploy.rb +++ b/lib/chef/resource/deploy.rb @@ -51,15 +51,15 @@ class Chef # class Deploy < Chef::Resource - provider_base Chef::Provider::Deploy - identity_attr :repository state_attrs :deploy_to, :revision + default_action :deploy + allowed_actions :force_deploy, :deploy, :rollback + def initialize(name, run_context=nil) super - @resource_name = :deploy @deploy_to = name @environment = nil @repository_cache = 'cached-copy' @@ -69,7 +69,6 @@ class Chef @symlink_before_migrate = {"config/database.yml" => "config/database.yml"} @symlinks = {"system" => "public/system", "pids" => "tmp/pids", "log" => "log"} @revision = 'HEAD' - @action = :deploy @migrate = false @rollback_on_error = false @remote = "origin" @@ -77,7 +76,6 @@ class Chef @shallow_clone = false @scm_provider = Chef::Provider::Git @svn_force_export = false - @allowed_actions.push(:force_deploy, :deploy, :rollback) @additional_remotes = Hash[] @keep_releases = 5 @enable_checkout = true @@ -281,6 +279,12 @@ class Chef ) end + # This is to support "provider :revision" without deprecation warnings. + # Do NOT copy this. + def self.provider_base + Chef::Provider::Deploy + end + def svn_force_export(arg=nil) set_or_return( :svn_force_export, diff --git a/lib/chef/resource/deploy_revision.rb b/lib/chef/resource/deploy_revision.rb index e144ce2162..1397359ac8 100644 --- a/lib/chef/resource/deploy_revision.rb +++ b/lib/chef/resource/deploy_revision.rb @@ -22,23 +22,9 @@ class Chef # Convenience class for using the deploy resource with the revision # deployment strategy (provider) class DeployRevision < Chef::Resource::Deploy - - provides :deploy_revision - - def initialize(*args, &block) - super - @resource_name = :deploy_revision - end end class DeployBranch < Chef::Resource::DeployRevision - - provides :deploy_branch - - def initialize(*args, &block) - super - @resource_name = :deploy_branch - end end end diff --git a/lib/chef/resource/directory.rb b/lib/chef/resource/directory.rb index 1ab7f0d16d..9cac2ce243 100644 --- a/lib/chef/resource/directory.rb +++ b/lib/chef/resource/directory.rb @@ -32,15 +32,13 @@ class Chef include Chef::Mixin::Securable - provides :directory + default_action :create + allowed_actions :create, :delete def initialize(name, run_context=nil) super - @resource_name = :directory @path = name - @action = :create @recursive = false - @allowed_actions.push(:create, :delete) end def recursive(arg=nil) diff --git a/lib/chef/resource/dpkg_package.rb b/lib/chef/resource/dpkg_package.rb index 35a47e8a82..38adf24cf6 100644 --- a/lib/chef/resource/dpkg_package.rb +++ b/lib/chef/resource/dpkg_package.rb @@ -25,11 +25,6 @@ class Chef provides :dpkg_package, os: "linux" - def initialize(name, run_context=nil) - super - @resource_name = :dpkg_package - end - end end end diff --git a/lib/chef/resource/dsc_resource.rb b/lib/chef/resource/dsc_resource.rb index 912b683434..5db00f49ca 100644 --- a/lib/chef/resource/dsc_resource.rb +++ b/lib/chef/resource/dsc_resource.rb @@ -25,13 +25,12 @@ class Chef include Chef::DSL::Powershell
+ default_action :run
+
def initialize(name, run_context)
super
@properties = {}
- @resource_name = :dsc_resource
@resource = nil
- @allowed_actions.push(:run)
- @action = :run
end
def resource(value=nil)
diff --git a/lib/chef/resource/dsc_script.rb b/lib/chef/resource/dsc_script.rb index cf96ef6b7f..2fcf183375 100644 --- a/lib/chef/resource/dsc_script.rb +++ b/lib/chef/resource/dsc_script.rb @@ -24,11 +24,10 @@ class Chef provides :dsc_script, platform: "windows" + default_action :run + def initialize(name, run_context=nil) super - @allowed_actions.push(:run) - @action = :run - @resource_name = :dsc_script @imports = {} end diff --git a/lib/chef/resource/easy_install_package.rb b/lib/chef/resource/easy_install_package.rb index 5286e9a289..df4cee1ab3 100644 --- a/lib/chef/resource/easy_install_package.rb +++ b/lib/chef/resource/easy_install_package.rb @@ -22,13 +22,6 @@ class Chef class Resource class EasyInstallPackage < Chef::Resource::Package - provides :easy_install_package - - def initialize(name, run_context=nil) - super - @resource_name = :easy_install_package - end - def easy_install_binary(arg=nil) set_or_return( :easy_install_binary, diff --git a/lib/chef/resource/env.rb b/lib/chef/resource/env.rb index 2072ae5d80..025bfc72b7 100644 --- a/lib/chef/resource/env.rb +++ b/lib/chef/resource/env.rb @@ -27,14 +27,14 @@ class Chef provides :env, os: "windows" + default_action :create + allowed_actions :create, :delete, :modify + def initialize(name, run_context=nil) super - @resource_name = :env @key_name = name @value = nil - @action = :create @delim = nil - @allowed_actions.push(:create, :delete, :modify) end def key_name(arg=nil) diff --git a/lib/chef/resource/erl_call.rb b/lib/chef/resource/erl_call.rb index 24009d51c7..1976c54c45 100644 --- a/lib/chef/resource/erl_call.rb +++ b/lib/chef/resource/erl_call.rb @@ -28,18 +28,16 @@ class Chef identity_attr :code + default_action :run + def initialize(name, run_context=nil) super - @resource_name = :erl_call @code = "q()." # your erlang code goes here @cookie = nil # cookie of the erlang node @distributed = false # if you want to have a distributed erlang node @name_type = "sname" # type of erlang hostname name or sname @node_name = "chef@localhost" # the erlang node hostname - - @action = "run" - @allowed_actions.push(:run) end def code(arg=nil) diff --git a/lib/chef/resource/execute.rb b/lib/chef/resource/execute.rb index 9f8b629fb8..ec669a75d3 100644 --- a/lib/chef/resource/execute.rb +++ b/lib/chef/resource/execute.rb @@ -32,12 +32,12 @@ class Chef # Only execute resources (and subclasses) can be guard interpreters. attr_accessor :is_guard_interpreter + default_action :run + def initialize(name, run_context=nil) super - @resource_name = :execute @command = name @backup = 5 - @action = "run" @creates = nil @cwd = nil @environment = nil @@ -46,7 +46,6 @@ class Chef @returns = 0 @timeout = nil @user = nil - @allowed_actions.push(:run) @umask = nil @default_guard_interpreter = :execute @is_guard_interpreter = false diff --git a/lib/chef/resource/file.rb b/lib/chef/resource/file.rb index 53a6a160af..d278652cc3 100644 --- a/lib/chef/resource/file.rb +++ b/lib/chef/resource/file.rb @@ -38,15 +38,22 @@ class Chef attr_writer :checksum - provides :file + # + # The checksum of the rendered file. This has to be saved on the + # new_resource for the 'after' state for reporting but we cannot + # mutate the new_resource.checksum which would change the + # user intent in the new_resource if the resource is reused. + # + # @returns [String] Checksum of the file we actually rendered + attr_accessor :final_checksum + + default_action :create + allowed_actions :create, :delete, :touch, :create_if_missing def initialize(name, run_context=nil) super - @resource_name = :file @path = name @backup = 5 - @action = "create" - @allowed_actions.push(:create, :delete, :touch, :create_if_missing) @atomic_update = Chef::Config[:file_atomic_update] @force_unlink = false @manage_symlink_source = nil @@ -129,6 +136,15 @@ class Chef @verifications end end + + def state_for_resource_reporter + state_attrs = super() + # fix up checksum state with final_checksum saved by the provider + if checksum.nil? && final_checksum + state_attrs[:checksum] = final_checksum + end + state_attrs + end end end end diff --git a/lib/chef/resource/freebsd_package.rb b/lib/chef/resource/freebsd_package.rb index 9c8db506f8..c7c43450ba 100644 --- a/lib/chef/resource/freebsd_package.rb +++ b/lib/chef/resource/freebsd_package.rb @@ -31,11 +31,6 @@ class Chef provides :package, platform: "freebsd" - def initialize(name, run_context=nil) - super - @resource_name = :freebsd_package - end - def after_created assign_provider end diff --git a/lib/chef/resource/gem_package.rb b/lib/chef/resource/gem_package.rb index 0e838ca040..b981797876 100644 --- a/lib/chef/resource/gem_package.rb +++ b/lib/chef/resource/gem_package.rb @@ -22,11 +22,8 @@ class Chef class Resource class GemPackage < Chef::Resource::Package - provides :gem_package - def initialize(name, run_context=nil) super - @resource_name = :gem_package @clear_sources = false end diff --git a/lib/chef/resource/git.rb b/lib/chef/resource/git.rb index 7156873315..393a0689fe 100644 --- a/lib/chef/resource/git.rb +++ b/lib/chef/resource/git.rb @@ -22,11 +22,8 @@ class Chef class Resource class Git < Chef::Resource::Scm - provides :git - def initialize(name, run_context=nil) super - @resource_name = :git @additional_remotes = Hash[] end diff --git a/lib/chef/resource/group.rb b/lib/chef/resource/group.rb index 9e8f1309b0..2e80f32fea 100644 --- a/lib/chef/resource/group.rb +++ b/lib/chef/resource/group.rb @@ -25,19 +25,17 @@ class Chef state_attrs :members - provides :group + allowed_actions :create, :remove, :modify, :manage + default_action :create def initialize(name, run_context=nil) super - @resource_name = :group @group_name = name @gid = nil @members = [] @excluded_members = [] - @action = :create @append = false @non_unique = false - @allowed_actions.push(:create, :remove, :modify, :manage) end def group_name(arg=nil) diff --git a/lib/chef/resource/homebrew_package.rb b/lib/chef/resource/homebrew_package.rb index 73409b13ac..048ba6b3aa 100644 --- a/lib/chef/resource/homebrew_package.rb +++ b/lib/chef/resource/homebrew_package.rb @@ -25,12 +25,10 @@ class Chef class Resource class HomebrewPackage < Chef::Resource::Package - provides :homebrew_package provides :package, os: "darwin" def initialize(name, run_context=nil) super - @resource_name = :homebrew_package @homebrew_user = nil end diff --git a/lib/chef/resource/http_request.rb b/lib/chef/resource/http_request.rb index ccb0a26629..f9f056325a 100644 --- a/lib/chef/resource/http_request.rb +++ b/lib/chef/resource/http_request.rb @@ -26,14 +26,14 @@ class Chef identity_attr :url + default_action :get + allowed_actions :get, :put, :post, :delete, :head, :options + def initialize(name, run_context=nil) super - @resource_name = :http_request @message = name @url = nil - @action = :get @headers = {} - @allowed_actions.push(:get, :put, :post, :delete, :head, :options) end def url(args=nil) diff --git a/lib/chef/resource/ifconfig.rb b/lib/chef/resource/ifconfig.rb index c289ddadbe..527eb0e515 100644 --- a/lib/chef/resource/ifconfig.rb +++ b/lib/chef/resource/ifconfig.rb @@ -27,12 +27,12 @@ class Chef state_attrs :inet_addr, :mask + default_action :add + allowed_actions :add, :delete, :enable, :disable + def initialize(name, run_context=nil) super - @resource_name = :ifconfig @target = name - @action = :add - @allowed_actions.push(:add, :delete, :enable, :disable) @hwaddr = nil @mask = nil @inet_addr = nil @@ -145,5 +145,3 @@ class Chef end end - - diff --git a/lib/chef/resource/ips_package.rb b/lib/chef/resource/ips_package.rb index c0e699e31a..8d720dd411 100644 --- a/lib/chef/resource/ips_package.rb +++ b/lib/chef/resource/ips_package.rb @@ -25,10 +25,10 @@ class Chef provides :ips_package, os: "solaris2" + allowed_actions :install, :remove, :upgrade + def initialize(name, run_context = nil) super(name, run_context) - @resource_name = :ips_package - @allowed_actions.push(:install, :remove, :upgrade) @accept_license = false end diff --git a/lib/chef/resource/link.rb b/lib/chef/resource/link.rb index 30f8ec86d1..f932383cc1 100644 --- a/lib/chef/resource/link.rb +++ b/lib/chef/resource/link.rb @@ -25,21 +25,19 @@ class Chef class Link < Chef::Resource include Chef::Mixin::Securable - provides :link - identity_attr :target_file state_attrs :to, :owner, :group + default_action :create + allowed_actions :create, :delete + def initialize(name, run_context=nil) verify_links_supported! super - @resource_name = :link @to = nil - @action = :create @link_type = :symbolic @target_file = name - @allowed_actions.push(:create, :delete) end def to(arg=nil) diff --git a/lib/chef/resource/log.rb b/lib/chef/resource/log.rb index 7f970a87a4..9adffb26bb 100644 --- a/lib/chef/resource/log.rb +++ b/lib/chef/resource/log.rb @@ -26,6 +26,8 @@ class Chef identity_attr :message + default_action :write + # Sends a string from a recipe to a log provider # # log "some string to log" do @@ -48,10 +50,7 @@ class Chef # node<Chef::Node>:: Node where resource will be used def initialize(name, run_context=nil) super - @resource_name = :log @level = :info - @action = :write - @allowed_actions.push(:write) @message = name end @@ -75,5 +74,3 @@ class Chef end end end - - diff --git a/lib/chef/resource/lwrp_base.rb b/lib/chef/resource/lwrp_base.rb index ce72e98028..c486233020 100644 --- a/lib/chef/resource/lwrp_base.rb +++ b/lib/chef/resource/lwrp_base.rb @@ -19,6 +19,13 @@ # require 'chef/resource' +require 'chef/resource_resolver' +require 'chef/node' +require 'chef/log' +require 'chef/exceptions' +require 'chef/mixin/convert_to_class_name' +require 'chef/mixin/from_file' +require 'chef/mixin/params_validate' # for DelayedEvaluator class Chef class Resource @@ -28,138 +35,99 @@ class Chef # so attributes, default action, etc. can be defined with pleasing syntax. class LWRPBase < Resource - NULL_ARG = Object.new + # Class methods + class <<self - extend Chef::Mixin::ConvertToClassName - extend Chef::Mixin::FromFile + include Chef::Mixin::ConvertToClassName + include Chef::Mixin::FromFile - # Evaluates the LWRP resource file and instantiates a new Resource class. - def self.build_from_file(cookbook_name, filename, run_context) - resource_class = nil - rname = filename_to_qualified_string(cookbook_name, filename) + attr_accessor :loaded_lwrps - class_name = convert_to_class_name(rname) - if Resource.const_defined?(class_name, false) - Chef::Log.info("#{class_name} light-weight resource is already initialized -- Skipping loading #{filename}!") - Chef::Log.debug("Overriding already defined LWRPs is not supported anymore starting with Chef 12.") - resource_class = Resource.const_get(class_name) - else - resource_class = Class.new(self) + def build_from_file(cookbook_name, filename, run_context) + if LWRPBase.loaded_lwrps[filename] + Chef::Log.info("LWRP resource #{filename} from cookbook #{cookbook_name} has already been loaded! Skipping the reload.") + return loaded_lwrps[filename] + end - Chef::Resource.const_set(class_name, resource_class) - resource_class.resource_name = rname + resource_name = filename_to_qualified_string(cookbook_name, filename) + + # We load the class first to give it a chance to set its own name + resource_class = Class.new(self) + resource_class.resource_name resource_name.to_sym resource_class.run_context = run_context resource_class.class_from_file(filename) - Chef::Log.debug("Loaded contents of #{filename} into a resource named #{rname} defined in Chef::Resource::#{class_name}") - end + # Make a useful string for the class (rather than <Class:312894723894>) + resource_class.instance_eval do + define_singleton_method(:to_s) do + "LWRP resource #{resource_name} from cookbook #{cookbook_name}" + end + define_singleton_method(:inspect) { to_s } + end - resource_class - end + Chef::Log.debug("Loaded contents of #{filename} into resource #{resource_name} (#{resource_class})") - # Set the resource name for this LWRP - def self.resource_name(arg = NULL_ARG) - if arg.equal?(NULL_ARG) - @resource_name - else - @resource_name = arg - end - end - - class << self - alias_method :resource_name=, :resource_name - end + LWRPBase.loaded_lwrps[filename] = true - # Define an attribute on this resource, including optional validation - # parameters. - def self.attribute(attr_name, validation_opts={}) - define_method(attr_name) do |arg=nil| - set_or_return(attr_name.to_sym, arg, validation_opts) + # Create the deprecated Chef::Resource::LwrpFoo class + Chef::Resource.register_deprecated_lwrp_class(resource_class, convert_to_class_name(resource_name)) + resource_class end - end - # Sets the default action - def self.default_action(action_name=NULL_ARG) - unless action_name.equal?(NULL_ARG) - @actions ||= [] - if action_name.is_a?(Array) - action = action_name.map { |arg| arg.to_sym } - @actions = actions | action - @default_action = action - else - action = action_name.to_sym - @actions.push(action) unless @actions.include?(action) - @default_action = action + # Define an attribute on this resource, including optional validation + # parameters. + def attribute(attr_name, validation_opts={}) + define_method(attr_name) do |arg=nil| + set_or_return(attr_name.to_sym, arg, validation_opts) end end - @default_action ||= from_superclass(:default_action) - end - - # Adds +action_names+ to the list of valid actions for this resource. - def self.actions(*action_names) - if action_names.empty? - defined?(@actions) ? @actions : from_superclass(:actions, []).dup - else - # BC-compat way for checking if actions have already been defined - if defined?(@actions) - @actions.push(*action_names) + # Adds +action_names+ to the list of valid actions for this resource. + # Does not include superclass's action list when appending. + def actions(*action_names) + if !action_names.empty? && !@allowed_actions + self.allowed_actions = action_names else - @actions = action_names + allowed_actions(*action_names) end end - end - - # @deprecated - def self.valid_actions(*args) - Chef::Log.warn("`valid_actions' is deprecated, please use actions `instead'!") - actions(*args) - end + alias :actions= :allowed_actions= - # Set the run context on the class. Used to provide access to the node - # during class definition. - def self.run_context=(run_context) - @run_context = run_context - end + # @deprecated + def valid_actions(*args) + Chef::Log.warn("`valid_actions' is deprecated, please use allowed_actions `instead'!") + allowed_actions(*args) + end - def self.run_context - @run_context - end + # Set the run context on the class. Used to provide access to the node + # during class definition. + attr_accessor :run_context - def self.node - run_context.node - end + def node + run_context ? run_context.node : nil + end - def self.lazy(&block) - DelayedEvaluator.new(&block) - end + def lazy(&block) + DelayedEvaluator.new(&block) + end - private + protected - # Get the value from the superclass, if it responds, otherwise return - # +nil+. Since class instance variables are **not** inherited upon - # subclassing, this is a required check to ensure Chef pulls the - # +default_action+ and other DSL-y methods when extending LWRP::Base. - def self.from_superclass(m, default = nil) - return default if superclass == Chef::Resource::LWRPBase - superclass.respond_to?(m) ? superclass.send(m) : default - end + def loaded_lwrps + @loaded_lwrps ||= {} + end - # Default initializer. Sets the default action and allowed actions. - def initialize(name, run_context=nil) - super(name, run_context) + private - # Raise an exception if the resource_name was not defined - if self.class.resource_name.nil? - raise Chef::Exceptions::InvalidResourceSpecification, - "You must specify `resource_name'!" + # Get the value from the superclass, if it responds, otherwise return + # +nil+. Since class instance variables are **not** inherited upon + # subclassing, this is a required check to ensure Chef pulls the + # +default_action+ and other DSL-y methods when extending LWRP::Base. + def from_superclass(m, default = nil) + return default if superclass == Chef::Resource::LWRPBase + superclass.respond_to?(m) ? superclass.send(m) : default end - - @resource_name = self.class.resource_name.to_sym - @action = self.class.default_action - allowed_actions.push(self.class.actions).flatten! end - end end end diff --git a/lib/chef/resource/macosx_service.rb b/lib/chef/resource/macosx_service.rb index 879ea99cf8..f1ed4051cb 100644 --- a/lib/chef/resource/macosx_service.rb +++ b/lib/chef/resource/macosx_service.rb @@ -22,8 +22,8 @@ class Chef class Resource class MacosxService < Chef::Resource::Service - provides :service, os: "darwin" provides :macosx_service, os: "darwin" + provides :service, os: "darwin" identity_attr :service_name @@ -31,7 +31,6 @@ class Chef def initialize(name, run_context=nil) super - @resource_name = :macosx_service @plist = nil @session_type = nil end diff --git a/lib/chef/resource/macports_package.rb b/lib/chef/resource/macports_package.rb index 0d4e5dec65..937839b6e1 100644 --- a/lib/chef/resource/macports_package.rb +++ b/lib/chef/resource/macports_package.rb @@ -19,14 +19,7 @@ class Chef class Resource class MacportsPackage < Chef::Resource::Package - - provides :macports_package provides :package, os: "darwin" - - def initialize(name, run_context=nil) - super - @resource_name = :macports_package - end end end end diff --git a/lib/chef/resource/mdadm.rb b/lib/chef/resource/mdadm.rb index 971b6c51b4..b789fab155 100644 --- a/lib/chef/resource/mdadm.rb +++ b/lib/chef/resource/mdadm.rb @@ -27,11 +27,11 @@ class Chef state_attrs :devices, :level, :chunk - provides :mdadm + default_action :create + allowed_actions :create, :assemble, :stop def initialize(name, run_context=nil) super - @resource_name = :mdadm @chunk = 16 @devices = [] @@ -40,9 +40,6 @@ class Chef @metadata = "0.90" @bitmap = nil @raid_device = name - - @action = :create - @allowed_actions.push(:create, :assemble, :stop) end def chunk(arg=nil) diff --git a/lib/chef/resource/mount.rb b/lib/chef/resource/mount.rb index 142dec87f7..79986d127f 100644 --- a/lib/chef/resource/mount.rb +++ b/lib/chef/resource/mount.rb @@ -27,11 +27,11 @@ class Chef state_attrs :mount_point, :device_type, :fstype, :username, :password, :domain - provides :mount + default_action :mount + allowed_actions :mount, :umount, :remount, :enable, :disable def initialize(name, run_context=nil) super - @resource_name = :mount @mount_point = name @device = nil @device_type = :device @@ -42,9 +42,7 @@ class Chef @pass = 2 @mounted = false @enabled = false - @action = :mount @supports = { :remount => false } - @allowed_actions.push(:mount, :umount, :remount, :enable, :disable) @username = nil @password = nil @domain = nil diff --git a/lib/chef/resource/ohai.rb b/lib/chef/resource/ohai.rb index b567db40f9..9425e55c0c 100644 --- a/lib/chef/resource/ohai.rb +++ b/lib/chef/resource/ohai.rb @@ -25,12 +25,11 @@ class Chef state_attrs :plugin + default_action :reload + def initialize(name, run_context=nil) super - @resource_name = :ohai @name = name - @allowed_actions.push(:reload) - @action = :reload @plugin = nil end diff --git a/lib/chef/resource/openbsd_package.rb b/lib/chef/resource/openbsd_package.rb index 20a2523e3a..f91fdb37e0 100644 --- a/lib/chef/resource/openbsd_package.rb +++ b/lib/chef/resource/openbsd_package.rb @@ -30,11 +30,6 @@ class Chef provides :package, os: "openbsd" - def initialize(name, run_context=nil) - super - @resource_name = :openbsd_package - end - def after_created assign_provider end @@ -48,4 +43,3 @@ class Chef end end end - diff --git a/lib/chef/resource/package.rb b/lib/chef/resource/package.rb index f4f49b543b..1c6da75678 100644 --- a/lib/chef/resource/package.rb +++ b/lib/chef/resource/package.rb @@ -22,24 +22,23 @@ require 'chef/resource' class Chef class Resource class Package < Chef::Resource - identity_attr :package_name state_attrs :version, :options + default_action :install + allowed_actions :install, :upgrade, :remove, :purge, :reconfig + def initialize(name, run_context=nil) super - @action = :install - @allowed_actions.push(:install, :upgrade, :remove, :purge, :reconfig) @candidate_version = nil @options = nil @package_name = name - @resource_name = :package @response_file = nil @response_file_variables = Hash.new @source = nil @version = nil - @timeout = 900 + @timeout = nil end def package_name(arg=nil) @@ -101,3 +100,8 @@ class Chef end end end + +require 'chef/chef_class' +require 'chef/resource/homebrew_package' + +Chef.set_resource_priority_array :package, Chef::Resource::HomebrewPackage, os: "darwin" diff --git a/lib/chef/resource/pacman_package.rb b/lib/chef/resource/pacman_package.rb index 4c45dd004f..54b8efc4c2 100644 --- a/lib/chef/resource/pacman_package.rb +++ b/lib/chef/resource/pacman_package.rb @@ -21,14 +21,7 @@ require 'chef/resource/package' class Chef class Resource class PacmanPackage < Chef::Resource::Package - provides :pacman_package, os: "linux" - - def initialize(name, run_context=nil) - super - @resource_name = :pacman_package - end - end end end diff --git a/lib/chef/resource/paludis_package.rb b/lib/chef/resource/paludis_package.rb index 552c96857a..56c47bc141 100644 --- a/lib/chef/resource/paludis_package.rb +++ b/lib/chef/resource/paludis_package.rb @@ -22,13 +22,12 @@ require 'chef/provider/package/paludis' class Chef class Resource class PaludisPackage < Chef::Resource::Package - provides :paludis_package, os: "linux" + allowed_actions :install, :remove, :upgrade + def initialize(name, run_context=nil) super(name, run_context) - @resource_name = :paludis_package - @allowed_actions.push(:install, :remove, :upgrade) @timeout = 3600 end end diff --git a/lib/chef/resource/perl.rb b/lib/chef/resource/perl.rb index c4bdb6e130..773eba6571 100644 --- a/lib/chef/resource/perl.rb +++ b/lib/chef/resource/perl.rb @@ -22,10 +22,8 @@ require 'chef/provider/script' class Chef class Resource class Perl < Chef::Resource::Script - def initialize(name, run_context=nil) super - @resource_name = :perl @interpreter = "perl" end diff --git a/lib/chef/resource/portage_package.rb b/lib/chef/resource/portage_package.rb index 42c03560b6..1af48702fa 100644 --- a/lib/chef/resource/portage_package.rb +++ b/lib/chef/resource/portage_package.rb @@ -21,10 +21,8 @@ require 'chef/resource/package' class Chef class Resource class PortagePackage < Chef::Resource::Package - def initialize(name, run_context=nil) super - @resource_name = :portage_package @provider = Chef::Provider::Package::Portage end diff --git a/lib/chef/resource/powershell_script.rb b/lib/chef/resource/powershell_script.rb index 43aafe4df2..7d432883e4 100644 --- a/lib/chef/resource/powershell_script.rb +++ b/lib/chef/resource/powershell_script.rb @@ -20,11 +20,10 @@ require 'chef/resource/windows_script' class Chef class Resource class PowershellScript < Chef::Resource::WindowsScript - provides :powershell_script, os: "windows" def initialize(name, run_context=nil) - super(name, run_context, :powershell_script, "powershell.exe") + super(name, run_context, nil, "powershell.exe") @convert_boolean_return = false end diff --git a/lib/chef/resource/python.rb b/lib/chef/resource/python.rb index b1f23d13ce..432ee46b85 100644 --- a/lib/chef/resource/python.rb +++ b/lib/chef/resource/python.rb @@ -21,10 +21,8 @@ require 'chef/provider/script' class Chef class Resource class Python < Chef::Resource::Script - def initialize(name, run_context=nil) super - @resource_name = :python @interpreter = "python" end diff --git a/lib/chef/resource/reboot.rb b/lib/chef/resource/reboot.rb index c111b23d2e..401f2f338f 100644 --- a/lib/chef/resource/reboot.rb +++ b/lib/chef/resource/reboot.rb @@ -24,11 +24,11 @@ require 'chef/resource' class Chef class Resource class Reboot < Chef::Resource + allowed_actions :request_reboot, :reboot_now, :cancel + def initialize(name, run_context=nil) super - @resource_name = :reboot @provider = Chef::Provider::Reboot - @allowed_actions.push(:request_reboot, :reboot_now, :cancel) @reason = "Reboot by Chef" @delay_mins = 0 diff --git a/lib/chef/resource/registry_key.rb b/lib/chef/resource/registry_key.rb index 8126ccf126..4ed0d4a4e0 100644 --- a/lib/chef/resource/registry_key.rb +++ b/lib/chef/resource/registry_key.rb @@ -22,10 +22,12 @@ require 'chef/digester' class Chef class Resource class RegistryKey < Chef::Resource - identity_attr :key state_attrs :values + default_action :create + allowed_actions :create, :create_if_missing, :delete, :delete_key + # Some registry key data types may not be safely reported as json. # Example (CHEF-5323): # @@ -59,13 +61,10 @@ class Chef def initialize(name, run_context=nil) super - @resource_name = :registry_key - @action = :create @architecture = :machine @recursive = false @key = name @values, @unscrubbed_values = [], [] - @allowed_actions.push(:create, :create_if_missing, :delete, :delete_key) end def key(arg=nil) diff --git a/lib/chef/resource/remote_directory.rb b/lib/chef/resource/remote_directory.rb index d4108da47a..b731f7b201 100644 --- a/lib/chef/resource/remote_directory.rb +++ b/lib/chef/resource/remote_directory.rb @@ -26,19 +26,18 @@ class Chef class RemoteDirectory < Chef::Resource::Directory include Chef::Mixin::Securable - provides :remote_directory - identity_attr :path state_attrs :files_owner, :files_group, :files_mode + default_action :create + allowed_actions :create, :create_if_missing, :delete + def initialize(name, run_context=nil) super - @resource_name = :remote_directory @path = name @source = ::File.basename(name) @delete = false - @action = :create @recursive = true @purge = false @files_backup = 5 @@ -46,7 +45,6 @@ class Chef @files_group = nil @files_mode = 0644 unless Chef::Platform.windows? @overwrite = true - @allowed_actions.push(:create, :create_if_missing, :delete) @cookbook = nil end diff --git a/lib/chef/resource/remote_file.rb b/lib/chef/resource/remote_file.rb index e56f69941d..b7a553cbe8 100644 --- a/lib/chef/resource/remote_file.rb +++ b/lib/chef/resource/remote_file.rb @@ -21,18 +21,15 @@ require 'uri' require 'chef/resource/file' require 'chef/provider/remote_file' require 'chef/mixin/securable' +require 'chef/mixin/uris' class Chef class Resource class RemoteFile < Chef::Resource::File include Chef::Mixin::Securable - provides :remote_file - def initialize(name, run_context=nil) super - @resource_name = :remote_file - @action = "create" @source = [] @use_etag = true @use_last_modified = true @@ -127,6 +124,8 @@ class Chef private + include Chef::Mixin::Uris + def validate_source(source) source = Array(source).flatten raise ArgumentError, "#{resource_name} has an empty source" if source.empty? @@ -140,7 +139,7 @@ class Chef end def absolute_uri?(source) - source.kind_of?(String) and URI.parse(source).absolute? + Chef::Provider::RemoteFile::Fetcher.network_share?(source) or (source.kind_of?(String) and as_uri(source).absolute?) rescue URI::InvalidURIError false end diff --git a/lib/chef/resource/route.rb b/lib/chef/resource/route.rb index 942905d138..3ba8f6215b 100644 --- a/lib/chef/resource/route.rb +++ b/lib/chef/resource/route.rb @@ -22,17 +22,16 @@ require 'chef/resource' class Chef class Resource class Route < Chef::Resource - identity_attr :target state_attrs :netmask, :gateway + default_action :add + allowed_actions :add, :delete + def initialize(name, run_context=nil) super - @resource_name = :route @target = name - @action = [:add] - @allowed_actions.push(:add, :delete) @netmask = nil @gateway = nil @metric = nil @@ -136,5 +135,3 @@ class Chef end end end - - diff --git a/lib/chef/resource/rpm_package.rb b/lib/chef/resource/rpm_package.rb index f00121dd69..b8b5144a42 100644 --- a/lib/chef/resource/rpm_package.rb +++ b/lib/chef/resource/rpm_package.rb @@ -22,12 +22,10 @@ require 'chef/provider/package/rpm' class Chef class Resource class RpmPackage < Chef::Resource::Package - provides :rpm_package, os: [ "linux", "aix" ] def initialize(name, run_context=nil) super - @resource_name = :rpm_package @allow_downgrade = false end diff --git a/lib/chef/resource/ruby.rb b/lib/chef/resource/ruby.rb index 2b2aa0249d..3c3909043d 100644 --- a/lib/chef/resource/ruby.rb +++ b/lib/chef/resource/ruby.rb @@ -22,13 +22,10 @@ require 'chef/provider/script' class Chef class Resource class Ruby < Chef::Resource::Script - def initialize(name, run_context=nil) super - @resource_name = :ruby @interpreter = "ruby" end - end end end diff --git a/lib/chef/resource/ruby_block.rb b/lib/chef/resource/ruby_block.rb index a9cbf234cf..ae8e4cb7cd 100644 --- a/lib/chef/resource/ruby_block.rb +++ b/lib/chef/resource/ruby_block.rb @@ -23,14 +23,13 @@ require 'chef/provider/ruby_block' class Chef class Resource class RubyBlock < Chef::Resource + default_action :run + allowed_actions :create, :run identity_attr :block_name def initialize(name, run_context=nil) super - @resource_name = :ruby_block - @action = "run" - @allowed_actions << :create << :run @block_name = name end diff --git a/lib/chef/resource/scm.rb b/lib/chef/resource/scm.rb index 87c217b4cc..85028c266b 100644 --- a/lib/chef/resource/scm.rb +++ b/lib/chef/resource/scm.rb @@ -22,23 +22,22 @@ require 'chef/resource' class Chef class Resource class Scm < Chef::Resource - identity_attr :destination state_attrs :revision + default_action :sync + allowed_actions :checkout, :export, :sync, :diff, :log + def initialize(name, run_context=nil) super @destination = name - @resource_name = :scm @enable_submodules = false @enable_checkout = true @revision = "HEAD" @remote = "origin" @ssh_wrapper = nil @depth = nil - @allowed_actions.push(:checkout, :export, :sync, :diff, :log) - @action = [:sync] @checkout_branch = "deploy" @environment = nil end diff --git a/lib/chef/resource/script.rb b/lib/chef/resource/script.rb index fd0fd5a7fd..30bed367cb 100644 --- a/lib/chef/resource/script.rb +++ b/lib/chef/resource/script.rb @@ -23,13 +23,11 @@ require 'chef/provider/script' class Chef class Resource class Script < Chef::Resource::Execute - # Chef-13: go back to using :name as the identity attr identity_attr :command def initialize(name, run_context=nil) super - @resource_name = :script # Chef-13: the command variable should be initialized to nil @command = name @code = nil diff --git a/lib/chef/resource/service.rb b/lib/chef/resource/service.rb index 36df7c859a..aa59b543be 100644 --- a/lib/chef/resource/service.rb +++ b/lib/chef/resource/service.rb @@ -22,14 +22,15 @@ require 'chef/resource' class Chef class Resource class Service < Chef::Resource - identity_attr :service_name state_attrs :enabled, :running + default_action :nothing + allowed_actions :enable, :disable, :start, :stop, :restart, :reload + def initialize(name, run_context=nil) super - @resource_name = :service @service_name = name @enabled = nil @running = nil @@ -43,9 +44,7 @@ class Chef @init_command = nil @priority = nil @timeout = nil - @action = "nothing" @supports = { :restart => false, :reload => false, :status => false } - @allowed_actions.push(:enable, :disable, :start, :stop, :restart, :reload) end def service_name(arg=nil) diff --git a/lib/chef/resource/smartos_package.rb b/lib/chef/resource/smartos_package.rb index 99b3b953b0..b8bd940c24 100644 --- a/lib/chef/resource/smartos_package.rb +++ b/lib/chef/resource/smartos_package.rb @@ -22,16 +22,7 @@ require 'chef/provider/package/smartos' class Chef class Resource class SmartosPackage < Chef::Resource::Package - - provides :smartos_package provides :package, os: "solaris2", platform_family: "smartos" - - def initialize(name, run_context=nil) - super - @resource_name = :smartos_package - end - end end end - diff --git a/lib/chef/resource/solaris_package.rb b/lib/chef/resource/solaris_package.rb index 94be4693b6..2dc72d5c47 100644 --- a/lib/chef/resource/solaris_package.rb +++ b/lib/chef/resource/solaris_package.rb @@ -23,21 +23,11 @@ require 'chef/provider/package/solaris' class Chef class Resource class SolarisPackage < Chef::Resource::Package - - provides :solaris_package provides :package, os: "solaris2", platform_family: "nexentacore" provides :package, os: "solaris2", platform_family: "solaris2" do |node| # on >= Solaris 11 we default to IPS packages instead node[:platform_version].to_f <= 5.10 end - - def initialize(name, run_context=nil) - super - @resource_name = :solaris_package - end - end end end - - diff --git a/lib/chef/resource/subversion.rb b/lib/chef/resource/subversion.rb index 3afbe0baaf..ae6a37caa2 100644 --- a/lib/chef/resource/subversion.rb +++ b/lib/chef/resource/subversion.rb @@ -22,13 +22,12 @@ require "chef/resource/scm" class Chef class Resource class Subversion < Chef::Resource::Scm + allowed_actions :force_export def initialize(name, run_context=nil) super @svn_arguments = '--no-auth-cache' @svn_info_args = '--no-auth-cache' - @resource_name = :subversion - allowed_actions << :force_export end # Override exception to strip password if any, so it won't appear in logs and different Chef notifications diff --git a/lib/chef/resource/template.rb b/lib/chef/resource/template.rb index 67a9e6a418..5a7f7efd6f 100644 --- a/lib/chef/resource/template.rb +++ b/lib/chef/resource/template.rb @@ -27,15 +27,11 @@ class Chef class Template < Chef::Resource::File include Chef::Mixin::Securable - provides :template - attr_reader :inline_helper_blocks attr_reader :inline_helper_modules def initialize(name, run_context=nil) super - @resource_name = :template - @action = "create" @source = "#{::File.basename(name)}.erb" @cookbook = nil @local = false diff --git a/lib/chef/resource/timestamped_deploy.rb b/lib/chef/resource/timestamped_deploy.rb index b2109db85c..344f8b0a5e 100644 --- a/lib/chef/resource/timestamped_deploy.rb +++ b/lib/chef/resource/timestamped_deploy.rb @@ -21,10 +21,6 @@ class Chef # Convenience class for using the deploy resource with the timestamped # deployment strategy (provider) class TimestampedDeploy < Chef::Resource::Deploy - provides :timestamped_deploy - def initialize(*args, &block) - super(*args, &block) - end end end end diff --git a/lib/chef/resource/user.rb b/lib/chef/resource/user.rb index 7d2ec25596..b85b648c92 100644 --- a/lib/chef/resource/user.rb +++ b/lib/chef/resource/user.rb @@ -21,16 +21,15 @@ require 'chef/resource' class Chef class Resource class User < Chef::Resource - identity_attr :username state_attrs :uid, :gid, :home - provides :user + default_action :create + allowed_actions :create, :remove, :modify, :manage, :lock, :unlock def initialize(name, run_context=nil) super - @resource_name = :user @username = name @comment = nil @uid = nil @@ -42,14 +41,12 @@ class Chef @manage_home = false @force = false @non_unique = false - @action = :create @supports = { :manage_home => false, :non_unique => false } @iterations = 27855 @salt = nil - @allowed_actions.push(:create, :remove, :modify, :manage, :lock, :unlock) end def username(arg=nil) diff --git a/lib/chef/resource/whyrun_safe_ruby_block.rb b/lib/chef/resource/whyrun_safe_ruby_block.rb index 6fa5383f5d..f289f15001 100644 --- a/lib/chef/resource/whyrun_safe_ruby_block.rb +++ b/lib/chef/resource/whyrun_safe_ruby_block.rb @@ -19,12 +19,6 @@ class Chef class Resource class WhyrunSafeRubyBlock < Chef::Resource::RubyBlock - - def initialize(name, run_context=nil) - super - @resource_name = :whyrun_safe_ruby_block - end - end end end diff --git a/lib/chef/resource/windows_package.rb b/lib/chef/resource/windows_package.rb index 16cfcf865e..a76765cc36 100644 --- a/lib/chef/resource/windows_package.rb +++ b/lib/chef/resource/windows_package.rb @@ -16,6 +16,7 @@ # limitations under the License. # +require 'chef/mixin/uris' require 'chef/resource/package' require 'chef/provider/package/windows' require 'chef/win32/error' if RUBY_PLATFORM =~ /mswin|mingw|windows/ @@ -23,14 +24,15 @@ require 'chef/win32/error' if RUBY_PLATFORM =~ /mswin|mingw|windows/ class Chef class Resource class WindowsPackage < Chef::Resource::Package + include Chef::Mixin::Uris - provides :package, os: "windows" provides :windows_package, os: "windows" + provides :package, os: "windows" + + allowed_actions :install, :remove def initialize(name, run_context=nil) super - @allowed_actions.push(:install, :remove) - @resource_name = :windows_package @source ||= source(@package_name) # Unique to this resource @@ -69,10 +71,30 @@ class Chef @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) + if uri_scheme?(arg) + @source = arg + else + @source = Chef::Util::PathHelper.canonical_path(arg, false) + end end end + + def checksum(arg=nil) + set_or_return( + :checksum, + arg, + :kind_of => [ String ] + ) + end + + def remote_file_attributes(arg=nil) + set_or_return( + :remote_file_attributes, + arg, + :kind_of => [ Hash ] + ) + end + end end end diff --git a/lib/chef/resource/windows_script.rb b/lib/chef/resource/windows_script.rb index 6b0827b77c..48e2b535a8 100644 --- a/lib/chef/resource/windows_script.rb +++ b/lib/chef/resource/windows_script.rb @@ -22,6 +22,7 @@ require 'chef/mixin/windows_architecture_helper' class Chef class Resource class WindowsScript < Chef::Resource::Script + # This is an abstract resource meant to be subclasses; thus no 'provides' set_guard_inherited_attributes(:architecture) @@ -30,8 +31,8 @@ class Chef def initialize(name, run_context, resource_name, interpreter_command) super(name, run_context) @interpreter = interpreter_command - @resource_name = resource_name - @default_guard_interpreter = resource_name + @resource_name = resource_name if resource_name + @default_guard_interpreter = self.resource_name end include Chef::Mixin::WindowsArchitectureHelper diff --git a/lib/chef/resource/windows_service.rb b/lib/chef/resource/windows_service.rb index 8090adceb0..a77690652e 100644 --- a/lib/chef/resource/windows_service.rb +++ b/lib/chef/resource/windows_service.rb @@ -25,8 +25,10 @@ class Chef # Until #1773 is resolved, you need to manually specify the windows_service resource # to use action :configure_startup and attribute startup_type - provides :service, os: "windows" provides :windows_service, os: "windows" + provides :service, os: "windows" + + allowed_actions :configure_startup identity_attr :service_name @@ -34,8 +36,6 @@ class Chef def initialize(name, run_context=nil) super - @resource_name = :windows_service - @allowed_actions.push(:configure_startup) @startup_type = :automatic @run_as_user = "" @run_as_password = "" diff --git a/lib/chef/resource/yum_package.rb b/lib/chef/resource/yum_package.rb index 8fbca9b097..4d54f6051f 100644 --- a/lib/chef/resource/yum_package.rb +++ b/lib/chef/resource/yum_package.rb @@ -22,13 +22,10 @@ require 'chef/provider/package/yum' class Chef class Resource class YumPackage < Chef::Resource::Package - - provides :yum_package provides :package, os: "linux", platform_family: [ "rhel", "fedora" ] def initialize(name, run_context=nil) super - @resource_name = :yum_package @flush_cache = { :before => false, :after => false } @allow_downgrade = false end @@ -38,7 +35,7 @@ class Chef set_or_return( :arch, arg, - :kind_of => [ String ] + :kind_of => [ String, Array ] ) end diff --git a/lib/chef/resource/zypper_package.rb b/lib/chef/resource/zypper_package.rb new file mode 100644 index 0000000000..f09a20e2c6 --- /dev/null +++ b/lib/chef/resource/zypper_package.rb @@ -0,0 +1,27 @@ +# +# Author:: Joe Williams (<joe@joetify.com>) +# Copyright:: Copyright (c) 2009 Joe Williams +# 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' + +class Chef + class Resource + class ZypperPackage < Chef::Resource::Package + provides :package, platform_family: "suse" + end + end +end diff --git a/lib/chef/resource_builder.rb b/lib/chef/resource_builder.rb index bb0962d128..9e9f7047a4 100644 --- a/lib/chef/resource_builder.rb +++ b/lib/chef/resource_builder.rb @@ -18,6 +18,10 @@ # NOTE: this was extracted from the Recipe DSL mixin, relevant specs are in spec/unit/recipe_spec.rb +require 'chef/exceptions' +require 'chef/resource' +require 'chef/log' + class Chef class ResourceBuilder attr_reader :type @@ -46,6 +50,9 @@ class Chef raise ArgumentError, "You must supply a name when declaring a #{type} resource" if name.nil? @resource = resource_class.new(name, run_context) + if resource.resource_name.nil? + raise Chef::Exceptions::InvalidResourceSpecification, "#{resource}.resource_name is `nil`! Did you forget to put `provides :blah` or `resource_name :blah` in your resource class?" + end resource.source_line = created_at resource.declared_type = type diff --git a/lib/chef/resource_definition.rb b/lib/chef/resource_definition.rb index 9d6844129c..cffabb6786 100644 --- a/lib/chef/resource_definition.rb +++ b/lib/chef/resource_definition.rb @@ -50,6 +50,7 @@ class Chef else raise ArgumentError, "You must pass a block to a definition." end + Chef::DSL::Definitions.add_definition(name) true end diff --git a/lib/chef/resource_reporter.rb b/lib/chef/resource_reporter.rb index 1816fc857d..7d13a5a5ce 100644 --- a/lib/chef/resource_reporter.rb +++ b/lib/chef/resource_reporter.rb @@ -59,11 +59,11 @@ class Chef # attrs. def for_json as_hash = {} - as_hash["type"] = new_resource.class.dsl_name + as_hash["type"] = new_resource.resource_name.to_sym as_hash["name"] = new_resource.name.to_s as_hash["id"] = new_resource.identity.to_s - as_hash["after"] = state(new_resource) - as_hash["before"] = current_resource ? state(current_resource) : {} + as_hash["after"] = new_resource.state_for_resource_reporter + as_hash["before"] = current_resource ? current_resource.state_for_resource_reporter : {} as_hash["duration"] = (elapsed_time * 1000).to_i.to_s as_hash["delta"] = new_resource.diff if new_resource.respond_to?("diff") as_hash["delta"] = "" if as_hash["delta"].nil? @@ -89,13 +89,6 @@ class Chef def success? !self.exception end - - def state(r) - r.class.state_attrs.inject({}) do |state_attrs, attr_name| - state_attrs[attr_name] = r.send(attr_name) - state_attrs - end - end end # End class ResouceReport attr_reader :updated_resources @@ -220,7 +213,7 @@ class Chef # If we failed before we received the run_started callback, there's not much we can do # in terms of reporting if @run_status - post_reporting_data + post_reporting_data end end diff --git a/lib/chef/resource_resolver.rb b/lib/chef/resource_resolver.rb index ff9d7aeb9f..31b39f7e24 100644 --- a/lib/chef/resource_resolver.rb +++ b/lib/chef/resource_resolver.rb @@ -21,81 +21,137 @@ require 'chef/platform/resource_priority_map' class Chef class ResourceResolver + # + # Resolve a resource by name. + # + # @param resource_name [Symbol] The resource DSL name (e.g. `:file`). + # @param node [Chef::Node] The node against which to resolve. `nil` causes + # platform filters to be ignored. + # + def self.resolve(resource_name, node: nil, canonical: nil) + new(node, resource_name, canonical: canonical).resolve + end + + # + # Resolve a list of all resources that implement the given DSL (in order of + # preference). + # + # @param resource_name [Symbol] The resource DSL name (e.g. `:file`). + # @param node [Chef::Node] The node against which to resolve. `nil` causes + # platform filters to be ignored. + # @param canonical [Boolean] `true` or `false` to match canonical or + # non-canonical values only. `nil` to ignore canonicality. + # + def self.list(resource_name, node: nil, canonical: nil) + new(node, resource_name, canonical: canonical).list + end + + include Chef::Mixin::ConvertToClassName + + # @api private attr_reader :node - attr_reader :resource + # @api private + attr_reader :resource_name + # @api private + def resource + Chef::Log.deprecation("Chef::ResourceResolver.resource deprecated. Use resource_name instead.") + resource_name + end + # @api private attr_reader :action - - def initialize(node, resource) + # @api private + attr_reader :canonical + + # + # Create a resolver. + # + # @param node [Chef::Node] The node against which to resolve. `nil` causes + # platform filters to be ignored. + # @param resource_name [Symbol] The resource DSL name (e.g. `:file`). + # @param canonical [Boolean] `true` or `false` to match canonical or + # non-canonical values only. `nil` to ignore canonicality. Default: `nil` + # + # @api private use Chef::ResourceResolver.resolve or .list instead. + def initialize(node, resource_name, canonical: nil) @node = node - @resource = resource - end - - # return a deterministically sorted list of Chef::Resource subclasses - def resources - @resources ||= Chef::Resource.descendants + @resource_name = resource_name.to_sym + @canonical = canonical end + # @api private use Chef::ResourceResolver.resolve instead. def resolve - maybe_dynamic_resource_resolution(resource) || - maybe_chef_platform_lookup(resource) - end - - # this cut looks at if the resource can handle the resource type on the node - def enabled_handlers - @enabled_handlers ||= - resources.select do |klass| - klass.provides?(node, resource) - end.sort {|a,b| a.to_s <=> b.to_s } - @enabled_handlers - end + # log this so we know what resources will work for the generic resource on the node (early cut) + Chef::Log.debug "Resources for generic #{resource_name} resource enabled on node include: #{prioritized_handlers}" - private + handler = prioritized_handlers.first - # try dynamically finding a resource based on querying the resources to see what they support - def maybe_dynamic_resource_resolution(resource) - # log this so we know what resources will work for the generic resource on the node (early cut) - Chef::Log.debug "resources for generic #{resource} resource enabled on node include: #{enabled_handlers}" - - # if none of the resources specifically support the resource, we still need to pick one of the resources that are - # enabled on the node to handle the why-run use case. - handlers = enabled_handlers - - if handlers.count >= 2 - # this magic stack ranks the resources by where they appear in the resource_priority_map - priority_list = [ get_priority_array(node, resource) ].flatten.compact - handlers = handlers.sort_by { |x| i = priority_list.index x; i.nil? ? Float::INFINITY : i } - if priority_list.index(handlers.first).nil? - # if we had more than one and we picked one with a precidence of infinity that means that the resource_priority_map - # entry for this resource is missing -- we should probably raise here and force resolution of the ambiguity. - Chef::Log.warn "Ambiguous resource precedence: #{handlers}, please use Chef.set_resource_priority_array to provide determinism" - end - handlers = [ handlers.first ] + if handler + Chef::Log.debug "Resource for #{resource_name} is #{handler}" + else + Chef::Log.debug "Dynamic resource resolver FAILED to resolve a resource for #{resource_name}" end - Chef::Log.debug "resources that survived replacement include: #{handlers}" + handler + end - raise Chef::Exceptions::AmbiguousResourceResolution.new(resource, handlers) if handlers.count >= 2 + # @api private + def list + Chef::Log.debug "Resources for generic #{resource_name} resource enabled on node include: #{prioritized_handlers}" + prioritized_handlers + end - Chef::Log.debug "dynamic resource resolver FAILED to resolve a resouce" if handlers.empty? + # + # Whether this DSL is provided by the given resource_class. + # + # @api private + def provided_by?(resource_class) + !prioritized_handlers.include?(resource_class) + end - return nil if handlers.empty? + protected - handlers[0] + def priority_map + Chef::Platform::ResourcePriorityMap.instance end - # try the old static lookup of resources by mangling name to resource klass - def maybe_chef_platform_lookup(resource) - Chef::Resource.resource_matching_short_name(resource) + def prioritized_handlers + @prioritized_handlers ||= + priority_map.list_handlers(node, resource_name, canonical: canonical) end - # dep injection hooks - def get_priority_array(node, resource_name) - resource_priority_map.get_priority_array(node, resource_name) - end + module Deprecated + # return a deterministically sorted list of Chef::Resource subclasses + # @deprecated Now prioritized_handlers does its own work (more efficiently) + def resources + Chef::Resource.sorted_descendants + end - def resource_priority_map - Chef::Platform::ResourcePriorityMap.instance + # A list of all handlers + # @deprecated Now prioritized_handlers does its own work + def enabled_handlers + Chef::Log.deprecation("enabled_handlers is deprecated. If you are implementing a ResourceResolver, use provided_handlers. If you are not, use Chef::ResourceResolver.list(#{resource_name.inspect}, node: <node>)") + resources.select { |klass| klass.provides?(node, resource_name) } + end + + protected + + # A list of all handlers for the given DSL. If there are no handlers in + # the map, we still check all descendants of Chef::Resource for backwards + # compatibility purposes. + def prioritized_handlers + @prioritized_handlers ||= super || + resources.select do |klass| + # Don't bother calling provides? unless it's overridden. We already + # know prioritized_handlers + if klass.method(:provides?).owner != Chef::Resource && klass.provides?(node, resource_name) + Chef::Log.deprecation("Resources #{provided.join(", ")} are marked as providing DSL #{resource_name}, but provides #{resource_name.inspect} was never called!") + Chef::Log.deprecation("In Chef 13, this will break: you must call provides to mark the names you provide, even if you also override provides? yourself.") + true + end + end + end end + prepend Deprecated end end diff --git a/lib/chef/resources.rb b/lib/chef/resources.rb index 40b12a7c5f..af4dd8a642 100644 --- a/lib/chef/resources.rb +++ b/lib/chef/resources.rb @@ -80,6 +80,7 @@ require 'chef/resource/windows_package' require 'chef/resource/yum_package' require 'chef/resource/lwrp_base' require 'chef/resource/bff_package' +require 'chef/resource/zypper_package' begin # Optional resources chef_node, chef_client, machine, machine_image, etc. diff --git a/lib/chef/rest.rb b/lib/chef/rest.rb index 2612714a19..f87cec9b76 100644 --- a/lib/chef/rest.rb +++ b/lib/chef/rest.rb @@ -64,6 +64,7 @@ class Chef options = options.dup options[:client_name] = client_name options[:signing_key_filename] = signing_key_filename + super(url, options) @decompressor = Decompressor.new(options) diff --git a/lib/chef/run_context.rb b/lib/chef/run_context.rb index 4f0215bfd4..44b05f0cc0 100644 --- a/lib/chef/run_context.rb +++ b/lib/chef/run_context.rb @@ -86,6 +86,7 @@ class Chef @reboot_info = {} @node.run_context = self + @node.set_cookbook_attribute @cookbook_compiler = nil end diff --git a/lib/chef/run_list/versioned_recipe_list.rb b/lib/chef/run_list/versioned_recipe_list.rb index 0eefded964..7cce6fa48c 100644 --- a/lib/chef/run_list/versioned_recipe_list.rb +++ b/lib/chef/run_list/versioned_recipe_list.rb @@ -63,6 +63,24 @@ class Chef end end end + + # Get an array of strings of the fully-qualified recipe names (with ::default appended) and + # with the versions in "NAME@VERSION" format. + # + # @return [Array] Array of strings with fully-qualified recipe names + def with_fully_qualified_names_and_version_constraints + self.map do |recipe_name| + ret = if recipe_name.include?('::') + recipe_name + else + "#{recipe_name}::default" + end + if @versions[recipe_name] + ret << "@#{@versions[recipe_name]}" + end + ret + end + end end end end diff --git a/lib/chef/run_status.rb b/lib/chef/run_status.rb index 0f181426b0..ce8ff296f4 100644 --- a/lib/chef/run_status.rb +++ b/lib/chef/run_status.rb @@ -39,15 +39,13 @@ class Chef::RunStatus attr_accessor :run_id + attr_accessor :node + def initialize(node, events) @node = node @events = events end - def node - @node - end - # sets +start_time+ to the current time. def start_clock @start_time = Time.now diff --git a/lib/chef/server_api.rb b/lib/chef/server_api.rb index ec4a864cb3..764296f8c8 100644 --- a/lib/chef/server_api.rb +++ b/lib/chef/server_api.rb @@ -42,3 +42,5 @@ class Chef use Chef::HTTP::RemoteRequestID end end + +require 'chef/config' diff --git a/lib/chef/shell.rb b/lib/chef/shell.rb index ee4fe78808..3a68785ce9 100644 --- a/lib/chef/shell.rb +++ b/lib/chef/shell.rb @@ -110,7 +110,7 @@ module Shell conf.prompt_c = "chef#{leader(m)} > " conf.return_format = " => %s \n" - conf.prompt_i = "chef#{leader(m)} > " + conf.prompt_i = "chef#{leader(m)} (#{Chef::VERSION})> " conf.prompt_n = "chef#{leader(m)} ?> " conf.prompt_s = "chef#{leader(m)}%l> " conf.use_tracer = false diff --git a/lib/chef/user.rb b/lib/chef/user.rb index 42fa6b5fa1..717deb63c3 100644 --- a/lib/chef/user.rb +++ b/lib/chef/user.rb @@ -21,29 +21,85 @@ require 'chef/mixin/from_file' require 'chef/mash' require 'chef/json_compat' require 'chef/search/query' +require 'chef/mixin/api_version_request_handling' +require 'chef/exceptions' +require 'chef/server_api' +# OSC 11 BACKWARDS COMPATIBILITY NOTE (remove after OSC 11 support ends) +# +# In general, Chef::User is no longer expected to support Open Source Chef 11 Server requests. +# The object that handles those requests has been moved to the Chef::OscUser namespace. +# +# Exception: self.list is backwards compatible with OSC 11 class Chef class User include Chef::Mixin::FromFile include Chef::Mixin::ParamsValidate + include Chef::Mixin::ApiVersionRequestHandling + + SUPPORTED_API_VERSIONS = [0,1] def initialize - @name = '' + @username = nil + @display_name = nil + @first_name = nil + @middle_name = nil + @last_name = nil + @email = nil + @password = nil @public_key = nil @private_key = nil + @create_key = nil @password = nil - @admin = false end - def name(arg=nil) - set_or_return(:name, arg, + def chef_root_rest_v0 + @chef_root_rest_v0 ||= Chef::ServerAPI.new(Chef::Config[:chef_server_root], {:api_version => "0"}) + end + + def chef_root_rest_v1 + @chef_root_rest_v1 ||= Chef::ServerAPI.new(Chef::Config[:chef_server_root], {:api_version => "1"}) + end + + def username(arg=nil) + set_or_return(:username, arg, :regex => /^[a-z0-9\-_]+$/) end - def admin(arg=nil) - set_or_return(:admin, - arg, :kind_of => [TrueClass, FalseClass]) + def display_name(arg=nil) + set_or_return(:display_name, + arg, :kind_of => String) + end + + def first_name(arg=nil) + set_or_return(:first_name, + arg, :kind_of => String) + end + + def middle_name(arg=nil) + set_or_return(:middle_name, + arg, :kind_of => String) + end + + def last_name(arg=nil) + set_or_return(:last_name, + arg, :kind_of => String) + end + + def email(arg=nil) + set_or_return(:email, + arg, :kind_of => String) + end + + def password(arg=nil) + set_or_return(:password, + arg, :kind_of => String) + end + + def create_key(arg=nil) + set_or_return(:create_key, arg, + :kind_of => [TrueClass, FalseClass]) end def public_key(arg=nil) @@ -63,12 +119,17 @@ class Chef def to_hash result = { - "name" => @name, - "public_key" => @public_key, - "admin" => @admin + "username" => @username } - result["private_key"] = @private_key if @private_key - result["password"] = @password if @password + result["display_name"] = @display_name unless @display_name.nil? + result["first_name"] = @first_name unless @first_name.nil? + result["middle_name"] = @middle_name unless @middle_name.nil? + result["last_name"] = @last_name unless @last_name.nil? + result["email"] = @email unless @email.nil? + result["password"] = @password unless @password.nil? + result["public_key"] = @public_key unless @public_key.nil? + result["private_key"] = @private_key unless @private_key.nil? + result["create_key"] = @create_key unless @create_key.nil? result end @@ -77,21 +138,86 @@ class Chef end def destroy - Chef::REST.new(Chef::Config[:chef_server_url]).delete_rest("users/#{@name}") + # will default to the current API version (Chef::Authenticator::DEFAULT_SERVER_API_VERSION) + Chef::REST.new(Chef::Config[:chef_server_url]).delete("users/#{@username}") end def create - payload = {:name => self.name, :admin => self.admin, :password => self.password } - payload[:public_key] = public_key if public_key - new_user =Chef::REST.new(Chef::Config[:chef_server_url]).post_rest("users", payload) + # try v1, fail back to v0 if v1 not supported + begin + payload = { + :username => @username, + :display_name => @display_name, + :first_name => @first_name, + :last_name => @last_name, + :email => @email, + :password => @password + } + payload[:public_key] = @public_key unless @public_key.nil? + payload[:create_key] = @create_key unless @create_key.nil? + payload[:middle_name] = @middle_name unless @middle_name.nil? + raise Chef::Exceptions::InvalidUserAttribute, "You cannot set both public_key and create_key for create." if !@create_key.nil? && !@public_key.nil? + new_user = chef_root_rest_v1.post("users", payload) + + # get the private_key out of the chef_key hash if it exists + if new_user['chef_key'] + if new_user['chef_key']['private_key'] + new_user['private_key'] = new_user['chef_key']['private_key'] + end + new_user['public_key'] = new_user['chef_key']['public_key'] + new_user.delete('chef_key') + end + rescue Net::HTTPServerException => e + # rescue API V0 if 406 and the server supports V0 + supported_versions = server_client_api_version_intersection(e, SUPPORTED_API_VERSIONS) + raise e unless supported_versions && supported_versions.include?(0) + payload = { + :username => @username, + :display_name => @display_name, + :first_name => @first_name, + :last_name => @last_name, + :email => @email, + :password => @password + } + payload[:middle_name] = @middle_name unless @middle_name.nil? + payload[:public_key] = @public_key unless @public_key.nil? + # under API V0, the server will create a key pair if public_key isn't passed + new_user = chef_root_rest_v0.post("users", payload) + end + Chef::User.from_hash(self.to_hash.merge(new_user)) end def update(new_key=false) - payload = {:name => name, :admin => admin} - payload[:private_key] = new_key if new_key - payload[:password] = password if password - updated_user = Chef::REST.new(Chef::Config[:chef_server_url]).put_rest("users/#{name}", payload) + begin + payload = {:username => username} + payload[:display_name] = display_name unless display_name.nil? + payload[:first_name] = first_name unless first_name.nil? + payload[:middle_name] = middle_name unless middle_name.nil? + payload[:last_name] = last_name unless last_name.nil? + payload[:email] = email unless email.nil? + payload[:password] = password unless password.nil? + + # API V1 will fail if these key fields are defined, and try V0 below if relevant 400 is returned + payload[:public_key] = public_key unless public_key.nil? + payload[:private_key] = new_key if new_key + + updated_user = chef_root_rest_v1.put("users/#{username}", payload) + rescue Net::HTTPServerException => e + if e.response.code == "400" + # if a 400 is returned but the error message matches the error related to private / public key fields, try V0 + # else, raise the 400 + error = Chef::JSONCompat.from_json(e.response.body)["error"].first + error_match = /Since Server API v1, all keys must be updated via the keys endpoint/.match(error) + if error_match.nil? + raise e + end + else # for other types of errors, test for API versioning errors right away + supported_versions = server_client_api_version_intersection(e, SUPPORTED_API_VERSIONS) + raise e unless supported_versions && supported_versions.include?(0) + end + updated_user = chef_root_rest_v0.put("users/#{username}", payload) + end Chef::User.from_hash(self.to_hash.merge(updated_user)) end @@ -107,31 +233,47 @@ class Chef end end + # Note: remove after API v0 no longer supported by client (and knife command). def reregister - r = Chef::REST.new(Chef::Config[:chef_server_url]) - reregistered_self = r.put_rest("users/#{name}", { :name => name, :admin => admin, :private_key => true }) - private_key(reregistered_self["private_key"]) + begin + payload = self.to_hash.merge({"private_key" => true}) + reregistered_self = chef_root_rest_v0.put("users/#{username}", payload) + private_key(reregistered_self["private_key"]) + # only V0 supported for reregister + rescue Net::HTTPServerException => e + # if there was a 406 related to versioning, give error explaining that + # only API version 0 is supported for reregister command + if e.response.code == "406" && e.response["x-ops-server-api-version"] + version_header = Chef::JSONCompat.from_json(e.response["x-ops-server-api-version"]) + min_version = version_header["min_version"] + max_version = version_header["max_version"] + error_msg = reregister_only_v0_supported_error_msg(max_version, min_version) + raise Chef::Exceptions::OnlyApiVersion0SupportedForAction.new(error_msg) + else + raise e + end + end self end def to_s - "user[#{@name}]" - end - - def inspect - "Chef::User name:'#{name}' admin:'#{admin.inspect}'" + - "public_key:'#{public_key}' private_key:#{private_key}" + "user[#{@username}]" end # Class Methods def self.from_hash(user_hash) user = Chef::User.new - user.name user_hash['name'] - user.private_key user_hash['private_key'] if user_hash.key?('private_key') + user.username user_hash['username'] + user.display_name user_hash['display_name'] if user_hash.key?('display_name') + user.first_name user_hash['first_name'] if user_hash.key?('first_name') + user.middle_name user_hash['middle_name'] if user_hash.key?('middle_name') + user.last_name user_hash['last_name'] if user_hash.key?('last_name') + user.email user_hash['email'] if user_hash.key?('email') user.password user_hash['password'] if user_hash.key?('password') - user.public_key user_hash['public_key'] - user.admin user_hash['admin'] + user.public_key user_hash['public_key'] if user_hash.key?('public_key') + user.private_key user_hash['private_key'] if user_hash.key?('private_key') + user.create_key user_hash['create_key'] if user_hash.key?('create_key') user end @@ -144,12 +286,19 @@ class Chef end def self.list(inflate=false) - response = Chef::REST.new(Chef::Config[:chef_server_url]).get_rest('users') + response = Chef::REST.new(Chef::Config[:chef_server_url]).get('users') users = if response.is_a?(Array) - transform_ohc_list_response(response) # OHC/OPC - else - response # OSC - end + # EC 11 / CS 12 V0, V1 + # GET /organizations/<org>/users + transform_list_response(response) + else + # OSC 11 + # GET /users + # EC 11 / CS 12 V0, V1 + # GET /users + response # OSC + end + if inflate users.inject({}) do |user_map, (name, _url)| user_map[name] = Chef::User.load(name) @@ -160,8 +309,9 @@ class Chef end end - def self.load(name) - response = Chef::REST.new(Chef::Config[:chef_server_url]).get_rest("users/#{name}") + def self.load(username) + # will default to the current API version (Chef::Authenticator::DEFAULT_SERVER_API_VERSION) + response = Chef::REST.new(Chef::Config[:chef_server_url]).get("users/#{username}") Chef::User.from_hash(response) end @@ -169,7 +319,7 @@ class Chef # [ { "user" => { "username" => USERNAME }}, ...] # into the form # { "USERNAME" => "URI" } - def self.transform_ohc_list_response(response) + def self.transform_list_response(response) new_response = Hash.new response.each do |u| name = u['user']['username'] @@ -178,6 +328,7 @@ class Chef new_response end - private_class_method :transform_ohc_list_response + private_class_method :transform_list_response + end end diff --git a/lib/chef/util/backup.rb b/lib/chef/util/backup.rb index 0cc009ca1f..6c95cedad7 100644 --- a/lib/chef/util/backup.rb +++ b/lib/chef/util/backup.rb @@ -78,8 +78,16 @@ class Chef Chef::Log.info("#{@new_resource} removed backup at #{backup_file}") end + def unsorted_backup_files + # If you replace this with Dir[], you will probably break Windows. + fn = Regexp.escape(::File.basename(path)) + Dir.entries(::File.dirname(backup_path)).select do |f| + !!(f =~ /\A#{fn}.chef-[0-9.]*\B/) + end.map {|f| ::File.join(::File.dirname(backup_path), f)} + end + def sorted_backup_files - Dir[Chef::Util::PathHelper.escape_glob(prefix, ".#{path}") + ".chef-*"].sort { |a,b| b <=> a } + unsorted_backup_files.sort { |a,b| b <=> a } end end end diff --git a/lib/chef/util/path_helper.rb b/lib/chef/util/path_helper.rb index 66c2e3f19f..9ebc9319b8 100644 --- a/lib/chef/util/path_helper.rb +++ b/lib/chef/util/path_helper.rb @@ -16,212 +16,10 @@ # limitations under the License. # +require 'chef-config/path_helper' + class Chef class Util - class PathHelper - # Maximum characters in a standard Windows path (260 including drive letter and NUL) - WIN_MAX_PATH = 259 - - def self.dirname(path) - if Chef::Platform.windows? - # Find the first slash, not counting trailing slashes - end_slash = path.size - loop do - slash = path.rindex(/[#{Regexp.escape(File::SEPARATOR)}#{Regexp.escape(path_separator)}]/, end_slash - 1) - if !slash - return end_slash == path.size ? '.' : path_separator - elsif slash == end_slash - 1 - end_slash = slash - else - return path[0..slash-1] - end - end - else - ::File.dirname(path) - end - end - - BACKSLASH = '\\'.freeze - - def self.path_separator - if Chef::Platform.windows? - File::ALT_SEPARATOR || BACKSLASH - else - File::SEPARATOR - end - end - - def self.join(*args) - args.flatten.inject do |joined_path, component| - # Joined path ends with / - joined_path = joined_path.sub(/[#{Regexp.escape(File::SEPARATOR)}#{Regexp.escape(path_separator)}]+$/, '') - component = component.sub(/^[#{Regexp.escape(File::SEPARATOR)}#{Regexp.escape(path_separator)}]+/, '') - joined_path += "#{path_separator}#{component}" - end - end - - def self.validate_path(path) - if Chef::Platform.windows? - unless printable?(path) - msg = "Path '#{path}' contains non-printable characters. Check that backslashes are escaped with another backslash (e.g. C:\\\\Windows) in double-quoted strings." - Chef::Log.error(msg) - raise Chef::Exceptions::ValidationFailed, msg - end - - if windows_max_length_exceeded?(path) - Chef::Log.debug("Path '#{path}' is longer than #{WIN_MAX_PATH}, prefixing with'\\\\?\\'") - path.insert(0, "\\\\?\\") - end - end - - path - end - - def self.windows_max_length_exceeded?(path) - # Check to see if paths without the \\?\ prefix are over the maximum allowed length for the Windows API - # http://msdn.microsoft.com/en-us/library/windows/desktop/aa365247%28v=vs.85%29.aspx - unless path =~ /^\\\\?\\/ - if path.length > WIN_MAX_PATH - return true - end - end - - false - end - - def self.printable?(string) - # returns true if string is free of non-printable characters (escape sequences) - # this returns false for whitespace escape sequences as well, e.g. \n\t - if string =~ /[^[:print:]]/ - false - else - true - end - end - - # Produces a comparable path. - def self.canonical_path(path, add_prefix=true) - # First remove extra separators and resolve any relative paths - abs_path = File.absolute_path(path) - - if Chef::Platform.windows? - # Add the \\?\ API prefix on Windows unless add_prefix is false - # Downcase on Windows where paths are still case-insensitive - abs_path.gsub!(::File::SEPARATOR, path_separator) - if add_prefix && abs_path !~ /^\\\\?\\/ - abs_path.insert(0, "\\\\?\\") - end - - abs_path.downcase! - end - - abs_path - end - - def self.cleanpath(path) - path = Pathname.new(path).cleanpath.to_s - # ensure all forward slashes are backslashes - if Chef::Platform.windows? - path = path.gsub(File::SEPARATOR, path_separator) - end - path - end - - def self.paths_eql?(path1, path2) - canonical_path(path1) == canonical_path(path2) - end - - # Paths which may contain glob-reserved characters need - # to be escaped before globbing can be done. - # http://stackoverflow.com/questions/14127343 - def self.escape_glob(*parts) - path = cleanpath(join(*parts)) - path.gsub(/[\\\{\}\[\]\*\?]/) { |x| "\\"+x } - end - - def self.relative_path_from(from, to) - pathname = Pathname.new(Chef::Util::PathHelper.cleanpath(to)).relative_path_from(Pathname.new(Chef::Util::PathHelper.cleanpath(from))) - end - - # Retrieves the "home directory" of the current user while trying to ascertain the existence - # of said directory. The path returned uses / for all separators (the ruby standard format). - # If the home directory doesn't exist or an error is otherwise encountered, nil is returned. - # - # If a set of path elements is provided, they are appended as-is to the home path if the - # homepath exists. - # - # If an optional block is provided, the joined path is passed to that block if the home path is - # valid and the result of the block is returned instead. - # - # Home-path discovery is performed once. If a path is discovered, that value is memoized so - # that subsequent calls to home_dir don't bounce around. - # - # See self.all_homes. - def self.home(*args) - @@home_dir ||= self.all_homes { |p| break p } - if @@home_dir - path = File.join(@@home_dir, *args) - block_given? ? (yield path) : path - end - end - - # See self.home. This method performs a similar operation except that it yields all the different - # possible values of 'HOME' that one could have on this platform. Hence, on windows, if - # HOMEDRIVE\HOMEPATH and USERPROFILE are different, the provided block will be called twice. - # This method goes out and checks the existence of each location at the time of the call. - # - # The return is a list of all the returned values from each block invocation or a list of paths - # if no block is provided. - def self.all_homes(*args) - paths = [] - if Chef::Platform.windows? - # By default, Ruby uses the the following environment variables to determine Dir.home: - # HOME - # HOMEDRIVE HOMEPATH - # USERPROFILE - # Ruby only checks to see if the variable is specified - not if the directory actually exists. - # On Windows, HOMEDRIVE HOMEPATH can point to a different location (such as an unavailable network mounted drive) - # while USERPROFILE points to the location where the user application settings and profile are stored. HOME - # is not defined as an environment variable (usually). If the home path actually uses UNC, then the prefix is - # HOMESHARE instead of HOMEDRIVE. - # - # We instead walk down the following and only include paths that actually exist. - # HOME - # HOMEDRIVE HOMEPATH - # HOMESHARE HOMEPATH - # USERPROFILE - - paths << ENV['HOME'] - paths << ENV['HOMEDRIVE'] + ENV['HOMEPATH'] if ENV['HOMEDRIVE'] && ENV['HOMEPATH'] - paths << ENV['HOMESHARE'] + ENV['HOMEPATH'] if ENV['HOMESHARE'] && ENV['HOMEPATH'] - paths << ENV['USERPROFILE'] - end - paths << Dir.home if ENV['HOME'] - - # Depending on what environment variables we're using, the slashes can go in any which way. - # Just change them all to / to keep things consistent. - # Note: Maybe this is a bad idea on some unixy systems where \ might be a valid character depending on - # the particular brand of kool-aid you consume. This code assumes that \ and / are both - # path separators on any system being used. - paths = paths.map { |home_path| home_path.gsub(path_separator, ::File::SEPARATOR) if home_path } - - # Filter out duplicate paths and paths that don't exist. - valid_paths = paths.select { |home_path| home_path && Dir.exists?(home_path) } - valid_paths = valid_paths.uniq - - # Join all optional path elements at the end. - # If a block is provided, invoke it - otherwise just return what we've got. - joined_paths = valid_paths.map { |home_path| File.join(home_path, *args) } - if block_given? - joined_paths.each { |p| yield p } - else - joined_paths - end - end - end + PathHelper = ChefConfig::PathHelper end end - -# Break a require loop when require chef/util/path_helper -require 'chef/platform' -require 'chef/exceptions' diff --git a/lib/chef/util/windows/net_user.rb b/lib/chef/util/windows/net_user.rb index 5df1a8aaa4..26fbe53db6 100644 --- a/lib/chef/util/windows/net_user.rb +++ b/lib/chef/util/windows/net_user.rb @@ -18,98 +18,69 @@ require 'chef/util/windows' require 'chef/exceptions' +require 'chef/win32/net' +require 'chef/win32/security' #wrapper around a subset of the NetUser* APIs. #nothing Chef specific, but not complete enough to be its own gem, so util for now. class Chef::Util::Windows::NetUser < Chef::Util::Windows private - - LogonUser = Windows::API.new('LogonUser', 'SSSLLP', 'I', 'advapi32') - - DOMAIN_GROUP_RID_USERS = 0x00000201 - - UF_SCRIPT = 0x000001 - UF_ACCOUNTDISABLE = 0x000002 - UF_PASSWD_CANT_CHANGE = 0x000040 - UF_NORMAL_ACCOUNT = 0x000200 - UF_DONT_EXPIRE_PASSWD = 0x010000 - - #[:symbol_name, default_val] - #default_val duals as field type - #array index duals as structure offset - - #OC-8391 - #Changing [:password, nil], to [:password, ""], - #if :password is set to nil, windows user creation api ignores the password policy applied - #thus initializing it with empty string value. - USER_INFO_3 = [ - [:name, nil], - [:password, ""], - [:password_age, 0], - [:priv, 0], #"The NetUserAdd and NetUserSetInfo functions ignore this member" - [:home_dir, nil], - [:comment, nil], - [:flags, UF_SCRIPT|UF_DONT_EXPIRE_PASSWD|UF_NORMAL_ACCOUNT], - [:script_path, nil], - [:auth_flags, 0], - [:full_name, nil], - [:user_comment, nil], - [:parms, nil], - [:workstations, nil], - [:last_logon, 0], - [:last_logoff, 0], - [:acct_expires, -1], - [:max_storage, -1], - [:units_per_week, 0], - [:logon_hours, nil], - [:bad_pw_count, 0], - [:num_logons, 0], - [:logon_server, nil], - [:country_code, 0], - [:code_page, 0], - [:user_id, 0], - [:primary_group_id, DOMAIN_GROUP_RID_USERS], - [:profile, nil], - [:home_dir_drive, nil], - [:password_expired, 0] - ] - - USER_INFO_3_TEMPLATE = - USER_INFO_3.collect { |field| field[1].class == Fixnum ? 'i' : 'L' }.join - - SIZEOF_USER_INFO_3 = #sizeof(USER_INFO_3) - USER_INFO_3.inject(0){|sum,item| - sum + (item[1].class == Fixnum ? 4 : PTR_SIZE) - } - - def user_info_3(args) - USER_INFO_3.collect { |field| - args.include?(field[0]) ? args[field[0]] : field[1] - } - end - - def user_info_3_pack(user) - user.collect { |v| - v.class == Fixnum ? v : str_to_ptr(multi_to_wide(v)) - }.pack(USER_INFO_3_TEMPLATE) + NetUser = Chef::ReservedNames::Win32::NetUser + Security = Chef::ReservedNames::Win32::Security + + USER_INFO_3_TRANSFORM = { + name: :usri3_name, + password: :usri3_password, + password_age: :usri3_password_age, + priv: :usri3_priv, + home_dir: :usri3_home_dir, + comment: :usri3_comment, + flags: :usri3_flags, + script_path: :usri3_script_path, + auth_flags: :usri3_auth_flags, + full_name: :usri3_full_name, + user_comment: :usri3_usr_comment, + parms: :usri3_parms, + workstations: :usri3_workstations, + last_logon: :usri3_last_logon, + last_logoff: :usri3_last_logoff, + acct_expires: :usri3_acct_expires, + max_storage: :usri3_max_storage, + units_per_week: :usri3_units_per_week, + logon_hours: :usri3_logon_hours, + bad_pw_count: :usri3_bad_pw_count, + num_logons: :usri3_num_logons, + logon_server: :usri3_logon_server, + country_code: :usri3_country_code, + code_page: :usri3_code_page, + user_id: :usri3_user_id, + primary_group_id: :usri3_primary_group_id, + profile: :usri3_profile, + home_dir_drive: :usri3_home_dir_drive, + password_expired: :usri3_password_expired, + } + + def transform_usri3(args) + args.inject({}) do |memo, (k,v)| + memo[USER_INFO_3_TRANSFORM[k]] = v + memo + end end - def user_info_3_unpack(buffer) - user = Hash.new - USER_INFO_3.each_with_index do |field,offset| - user[field[0]] = field[1].class == Fixnum ? - dword_to_i(buffer, offset) : lpwstr_to_s(buffer, offset) + def usri3_to_hash(usri3) + t = USER_INFO_3_TRANSFORM.invert + usri3.inject({}) do |memo, (k,v)| + memo[t[k]] = v + memo end - user end def set_info(args) - user = user_info_3(args) - buffer = user_info_3_pack(user) - rc = NetUserSetInfo.call(nil, @name, 3, buffer, nil) - if rc != NERR_Success - raise ArgumentError, get_last_error(rc) + begin + rc = NetUser::net_user_set_info_l3(nil, @username, transform_usri3(args)) + rescue Chef::Exceptions::Win32APIError => e + raise ArgumentError, e end end @@ -120,49 +91,32 @@ class Chef::Util::Windows::NetUser < Chef::Util::Windows @name = multi_to_wide(username) end - LOGON32_PROVIDER_DEFAULT = 0 - LOGON32_LOGON_NETWORK = 3 + LOGON32_PROVIDER_DEFAULT = Security::LOGON32_PROVIDER_DEFAULT + LOGON32_LOGON_NETWORK = Security::LOGON32_LOGON_NETWORK #XXX for an extra painful alternative, see: http://support.microsoft.com/kb/180548 def validate_credentials(passwd) - token = 0.chr * PTR_SIZE - res = LogonUser.call(@username, nil, passwd, - LOGON32_LOGON_NETWORK, LOGON32_PROVIDER_DEFAULT, token) - if res == 0 + begin + token = Security::logon_user(@username, nil, passwd, + LOGON32_LOGON_NETWORK, LOGON32_PROVIDER_DEFAULT) + return true + rescue Chef::Exceptions::Win32APIError return false end - ::Windows::Handle::CloseHandle.call(token.unpack('L')[0]) - return true end def get_info - ptr = 0.chr * PTR_SIZE - rc = NetUserGetInfo.call(nil, @name, 3, ptr) - - if rc == NERR_UserNotFound - raise Chef::Exceptions::UserIDNotFound, get_last_error(rc) - elsif rc != NERR_Success - raise ArgumentError, get_last_error(rc) + begin + ui3 = NetUser::net_user_get_info_l3(nil, @username) + rescue Chef::Exceptions::Win32APIError => e + raise ArgumentError, e end - - ptr = ptr.unpack('L')[0] - buffer = 0.chr * SIZEOF_USER_INFO_3 - memcpy(buffer, ptr, buffer.size) - NetApiBufferFree(ptr) - user_info_3_unpack(buffer) + usri3_to_hash(ui3) end def add(args) - user = user_info_3(args) - buffer = user_info_3_pack(user) - - rc = NetUserAdd.call(nil, 3, buffer, rc) - if rc != NERR_Success - raise ArgumentError, get_last_error(rc) - end - - #usri3_primary_group_id: - #"When you call the NetUserAdd function, this member must be DOMAIN_GROUP_RID_USERS" - NetLocalGroupAddMembers(nil, multi_to_wide("Users"), 3, buffer[0,PTR_SIZE], 1) + transformed_args = transform_usri3(args) + NetUser::net_user_add_l3(nil, transformed_args) + NetUser::net_local_group_add_member(nil, "Users", args[:name]) end def user_modify(&proc) @@ -182,15 +136,16 @@ class Chef::Util::Windows::NetUser < Chef::Util::Windows end def delete - rc = NetUserDel.call(nil, @name) - if rc != NERR_Success - raise ArgumentError, get_last_error(rc) + begin + NetUser::net_user_del(nil, @username) + rescue Chef::Exceptions::Win32APIError => e + raise ArgumentError, e end end def disable_account user_modify do |user| - user[:flags] |= UF_ACCOUNTDISABLE + user[:flags] |= NetUser::UF_ACCOUNTDISABLE #This does not set the password to nil. It (for some reason) means to ignore updating the field. #See similar behavior for the logon_hours field documented at #http://msdn.microsoft.com/en-us/library/windows/desktop/aa371338%28v=vs.85%29.aspx @@ -200,7 +155,7 @@ class Chef::Util::Windows::NetUser < Chef::Util::Windows def enable_account user_modify do |user| - user[:flags] &= ~UF_ACCOUNTDISABLE + user[:flags] &= ~NetUser::UF_ACCOUNTDISABLE #This does not set the password to nil. It (for some reason) means to ignore updating the field. #See similar behavior for the logon_hours field documented at #http://msdn.microsoft.com/en-us/library/windows/desktop/aa371338%28v=vs.85%29.aspx @@ -209,6 +164,6 @@ class Chef::Util::Windows::NetUser < Chef::Util::Windows end def check_enabled - (get_info()[:flags] & UF_ACCOUNTDISABLE) != 0 + (get_info()[:flags] & NetUser::UF_ACCOUNTDISABLE) != 0 end end diff --git a/lib/chef/version.rb b/lib/chef/version.rb index 3fa3939a62..80fd422c55 100644 --- a/lib/chef/version.rb +++ b/lib/chef/version.rb @@ -1,6 +1,4 @@ - -# Author:: Daniel DeLeo (<dan@opscode.com>) -# Copyright:: Copyright (c) 2010-2011 Opscode, Inc. +# Copyright:: Copyright (c) 2010-2015 Chef Software, Inc. # License:: Apache License, Version 2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -15,9 +13,15 @@ # See the License for the specific language governing permissions and # limitations under the License. +#!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! +# NOTE: This file is generated by running `rake version` in the top level of +# this repo. Do not edit this manually. Edit the VERSION file and run the rake +# task instead. +#!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + class Chef CHEF_ROOT = File.dirname(File.expand_path(File.dirname(__FILE__))) - VERSION = '12.3.0.dev.0' + VERSION = '12.4.0.rc.2' end # diff --git a/lib/chef/win32/api.rb b/lib/chef/win32/api.rb index efa632f454..e9d273808a 100644 --- a/lib/chef/win32/api.rb +++ b/lib/chef/win32/api.rb @@ -67,7 +67,7 @@ class Chef # BaseTsd.h: #ifdef (_WIN64) host.typedef int HALF_PTR; #else host.typedef short HALF_PTR; host.typedef :ulong, :HACCEL # (L) Handle to an accelerator table. WinDef.h: #host.typedef HANDLE HACCEL; # See http://msdn.microsoft.com/en-us/library/ms645526%28VS.85%29.aspx - host.typedef :ulong, :HANDLE # (L) Handle to an object. WinNT.h: #host.typedef PVOID HANDLE; + host.typedef :size_t, :HANDLE # (L) Handle to an object. WinNT.h: #host.typedef PVOID HANDLE; # todo: Platform-dependent! Need to change to :uint64 for Win64 host.typedef :ulong, :HBITMAP # (L) Handle to a bitmap: http://msdn.microsoft.com/en-us/library/dd183377%28VS.85%29.aspx host.typedef :ulong, :HBRUSH # (L) Handle to a brush. http://msdn.microsoft.com/en-us/library/dd183394%28VS.85%29.aspx @@ -117,6 +117,7 @@ class Chef host.typedef :uint32, :LCID # Locale identifier. For more information, see Locales. host.typedef :uint32, :LCTYPE # Locale information type. For a list, see Locale Information Constants. host.typedef :uint32, :LGRPID # Language group identifier. For a list, see EnumLanguageGroupLocales. + host.typedef :pointer, :LMSTR # Pointer to null termiated string of unicode characters host.typedef :long, :LONG # 32-bit signed integer. The range is -2,147,483,648 through +...647 decimal. host.typedef :int32, :LONG32 # 32-bit signed integer. The range is -2,147,483,648 through +...647 decimal. host.typedef :int64, :LONG64 # 64-bit signed integer. The range is –9,223,372,036,854,775,808 through +...807 diff --git a/lib/chef/win32/api/installer.rb b/lib/chef/win32/api/installer.rb index 6864a26e7d..b4851eccf1 100644 --- a/lib/chef/win32/api/installer.rb +++ b/lib/chef/win32/api/installer.rb @@ -158,7 +158,7 @@ UINT MsiCloseHandle( raise Chef::Exceptions::Package, msg end - version + version.chomp(0.chr) end end end diff --git a/lib/chef/win32/api/net.rb b/lib/chef/win32/api/net.rb index eeb2b078a4..72caf46628 100644 --- a/lib/chef/win32/api/net.rb +++ b/lib/chef/win32/api/net.rb @@ -32,8 +32,24 @@ class Chef MAX_PREFERRED_LENGTH = 0xFFFF - NERR_Success = 0 - NERR_UserNotFound = 2221 + DOMAIN_GROUP_RID_USERS = 0x00000201 + + UF_SCRIPT = 0x000001 + UF_ACCOUNTDISABLE = 0x000002 + UF_PASSWD_CANT_CHANGE = 0x000040 + UF_NORMAL_ACCOUNT = 0x000200 + UF_DONT_EXPIRE_PASSWD = 0x010000 + + NERR_Success = 0 + NERR_InvalidComputer = 2351 + NERR_NotPrimary = 2226 + NERR_SpeGroupOp = 2234 + NERR_LastAdmin = 2452 + NERR_BadUsername = 2202 + NERR_BadPassword = 2203 + NERR_PasswordTooShort = 2245 + NERR_UserNotFound = 2221 + ERROR_ACCESS_DENIED = 5 ffi_lib "netapi32" @@ -67,6 +83,57 @@ class Chef :usri3_profile, :LPWSTR, :usri3_home_dir_drive, :LPWSTR, :usri3_password_expired, :DWORD + + def set(key, val) + val = if val.is_a? String + encoded = if val.encoding == Encoding::UTF_16LE + val + else + val.to_wstring + end + FFI::MemoryPointer.from_string(encoded) + else + val + end + self[key] = val + end + + def get(key) + if respond_to? key + send(key) + else + val = self[key] + if val.is_a? FFI::Pointer + if val.null? + nil + else + val.read_wstring + end + else + val + end + end + end + + def usri3_logon_hours + val = self[:usri3_logon_hours] + if !val.nil? && !val.null? + val.read_bytes(21) + else + nil + end + end + + def as_ruby + members.inject({}) do |memo, key| + memo[key] = get(key) + memo + end + end + end + + class LOCALGROUP_MEMBERS_INFO_3 < FFI::Struct + layout :lgrmi3_domainandname, :LPWSTR end # NET_API_STATUS NetUserEnum( @@ -85,6 +152,52 @@ class Chef # _In_ LPVOID Buffer # ); safe_attach_function :NetApiBufferFree, [ :LPVOID ], :DWORD + +#NET_API_STATUS NetUserAdd( + #_In_ LMSTR servername, + #_In_ DWORD level, + #_In_ LPBYTE buf, + #_Out_ LPDWORD parm_err +#); + safe_attach_function :NetUserAdd, [:LMSTR, :DWORD, :LPBYTE, :LPDWORD ], :DWORD + +#NET_API_STATUS NetLocalGroupAddMembers( +# _In_ LPCWSTR servername, +# _In_ LPCWSTR groupname, +# _In_ DWORD level, +# _In_ LPBYTE buf, +# _In_ DWORD totalentries +#); + safe_attach_function :NetLocalGroupAddMembers, [:LPCWSTR, :LPCWSTR, :DWORD, :LPBYTE, :DWORD ], :DWORD + +#NET_API_STATUS NetUserGetInfo( +# _In_ LPCWSTR servername, +# _In_ LPCWSTR username, +# _In_ DWORD level, +# _Out_ LPBYTE *bufptr +#); + safe_attach_function :NetUserGetInfo, [:LPCWSTR, :LPCWSTR, :DWORD, :LPBYTE], :DWORD + +#NET_API_STATUS NetApiBufferFree( +# _In_ LPVOID Buffer +#); + safe_attach_function :NetApiBufferFree, [:LPVOID], :DWORD + +#NET_API_STATUS NetUserSetInfo( +# _In_ LPCWSTR servername, +# _In_ LPCWSTR username, +# _In_ DWORD level, +# _In_ LPBYTE buf, +# _Out_ LPDWORD parm_err +#); + safe_attach_function :NetUserSetInfo, [:LPCWSTR, :LPCWSTR, :DWORD, :LPBYTE, :LPDWORD], :DWORD + +#NET_API_STATUS NetUserDel( +# _In_ LPCWSTR servername, +# _In_ LPCWSTR username +#); + safe_attach_function :NetUserDel, [:LPCWSTR, :LPCWSTR], :DWORD + end end end diff --git a/lib/chef/win32/api/security.rb b/lib/chef/win32/api/security.rb index 229f2ace10..4c352a3554 100644 --- a/lib/chef/win32/api/security.rb +++ b/lib/chef/win32/api/security.rb @@ -193,6 +193,20 @@ class Chef MAXDWORD = 0xffffffff + # LOGON32 constants for LogonUser + LOGON32_LOGON_INTERACTIVE = 2; + LOGON32_LOGON_NETWORK = 3; + LOGON32_LOGON_BATCH = 4; + LOGON32_LOGON_SERVICE = 5; + LOGON32_LOGON_UNLOCK = 7; + LOGON32_LOGON_NETWORK_CLEARTEXT = 8; + LOGON32_LOGON_NEW_CREDENTIALS = 9; + + LOGON32_PROVIDER_DEFAULT = 0; + LOGON32_PROVIDER_WINNT35 = 1; + LOGON32_PROVIDER_WINNT40 = 2; + LOGON32_PROVIDER_WINNT50 = 3; + ############################################### # Win32 API Bindings ############################################### @@ -270,6 +284,14 @@ class Chef :MaxTokenInfoClass ] + class TOKEN_OWNER < FFI::Struct + layout :Owner, :pointer + end + + class TOKEN_PRIMARY_GROUP < FFI::Struct + layout :PrimaryGroup, :pointer + end + # https://msdn.microsoft.com/en-us/library/windows/desktop/aa379572%28v=vs.85%29.aspx SECURITY_IMPERSONATION_LEVEL = enum :SECURITY_IMPERSONATION_LEVEL, [ :SecurityAnonymous, @@ -405,6 +427,8 @@ class Chef safe_attach_function :SetSecurityDescriptorOwner, [ :pointer, :pointer, :BOOL ], :BOOL safe_attach_function :SetSecurityDescriptorSacl, [ :pointer, :BOOL, :pointer, :BOOL ], :BOOL safe_attach_function :GetTokenInformation, [ :HANDLE, :TOKEN_INFORMATION_CLASS, :pointer, :DWORD, :PDWORD ], :BOOL + safe_attach_function :LogonUserW, [:LPTSTR, :LPTSTR, :LPTSTR, :DWORD, :DWORD, :PHANDLE], :BOOL + end end end diff --git a/lib/chef/win32/api/unicode.rb b/lib/chef/win32/api/unicode.rb index 0b2cb09a6b..2e3a599f0a 100644 --- a/lib/chef/win32/api/unicode.rb +++ b/lib/chef/win32/api/unicode.rb @@ -139,7 +139,7 @@ int WideCharToMultiByte( ustring = (ustring + "").force_encoding('UTF-8') if ustring.respond_to?(:force_encoding) && ustring.encoding.name != "UTF-8" # ensure we have the double-null termination Windows Wide likes - ustring = ustring + "\000\000" if ustring[-1].chr != "\000" + ustring = ustring + "\000\000" if ustring.length == 0 or ustring[-1].chr != "\000" # encode it all as UTF-16LE AKA Windows Wide Character AKA Windows Unicode ustring = begin diff --git a/lib/chef/win32/eventlog.rb b/lib/chef/win32/eventlog.rb new file mode 100644 index 0000000000..24af2da0d6 --- /dev/null +++ b/lib/chef/win32/eventlog.rb @@ -0,0 +1,31 @@ +# +# Author:: Jay Mundrawala (<jdm@chef.io>) +# +# Copyright:: 2015, Chef Software, Inc. +# +# 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. +# + +if Chef::Platform::windows? and not Chef::Platform::windows_server_2003? + if !defined? Chef::Win32EventLogLoaded + if defined? Windows::Constants + [:INFINITE, :WAIT_FAILED, :FORMAT_MESSAGE_IGNORE_INSERTS, :ERROR_INSUFFICIENT_BUFFER].each do |c| + # These are redefined in 'win32/eventlog' + Windows::Constants.send(:remove_const, c) if Windows::Constants.const_defined? c + end + end + + require 'win32/eventlog' + Chef::Win32EventLogLoaded = true + end +end diff --git a/lib/chef/win32/net.rb b/lib/chef/win32/net.rb new file mode 100644 index 0000000000..1349091eb9 --- /dev/null +++ b/lib/chef/win32/net.rb @@ -0,0 +1,190 @@ +# +# Author:: Jay Mundrawala(<jdm@chef.io>) +# Copyright:: Copyright 2015 Chef Software +# 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/win32/api/net' +require 'chef/win32/error' +require 'chef/mixin/wstring' + +class Chef + module ReservedNames::Win32 + class NetUser + include Chef::ReservedNames::Win32::API::Error + extend Chef::ReservedNames::Win32::API::Error + + include Chef::ReservedNames::Win32::API::Net + extend Chef::ReservedNames::Win32::API::Net + + include Chef::Mixin::WideString + extend Chef::Mixin::WideString + + def self.default_user_info_3 + ui3 = USER_INFO_3.new.tap do |s| + { usri3_name: nil, + usri3_password: nil, + usri3_password_age: 0, + usri3_priv: 0, + usri3_home_dir: nil, + usri3_comment: nil, + usri3_flags: UF_SCRIPT|UF_DONT_EXPIRE_PASSWD|UF_NORMAL_ACCOUNT, + usri3_script_path: nil, + usri3_auth_flags: 0, + usri3_full_name: nil, + usri3_usr_comment: nil, + usri3_parms: nil, + usri3_workstations: nil, + usri3_last_logon: 0, + usri3_last_logoff: 0, + usri3_acct_expires: -1, + usri3_max_storage: -1, + usri3_units_per_week: 0, + usri3_logon_hours: nil, + usri3_bad_pw_count: 0, + usri3_num_logons: 0, + usri3_logon_server: nil, + usri3_country_code: 0, + usri3_code_page: 0, + usri3_user_id: 0, + usri3_primary_group_id: DOMAIN_GROUP_RID_USERS, + usri3_profile: nil, + usri3_home_dir_drive: nil, + usri3_password_expired: 0 + }.each do |(k,v)| + s.set(k, v) + end + end + end + + def self.net_api_error!(code) + msg = case code + when NERR_InvalidComputer + "The user does not have access to the requested information." + when NERR_NotPrimary + "The operation is allowed only on the primary domain controller of the domain." + when NERR_SpeGroupOp + "This operation is not allowed on this special group." + when NERR_LastAdmin + "This operation is not allowed on the last administrative account." + when NERR_BadUsername + "The user name or group name parameter is invalid." + when NERR_BadPassword + "The password parameter is invalid." + when NERR_UserNotFound + raise Chef::Exceptions::UserIDNotFound, code + when NERR_PasswordTooShort + <<END +The password is shorter than required. (The password could also be too +long, be too recent in its change history, not have enough unique characters, +or not meet another password policy requirement.) +END + when ERROR_ACCESS_DENIED + "The user does not have access to the requested information." + else + "Received unknown error code (#{code})" + end + + formatted_message = "" + formatted_message << "---- Begin Win32 API output ----\n" + formatted_message << "Net Api Error Code: #{code}\n" + formatted_message << "Net Api Error Message: #{msg}\n" + formatted_message << "---- End Win32 API output ----\n" + + raise Chef::Exceptions::Win32APIError, msg + "\n" + formatted_message + end + + def self.net_user_add_l3(server_name, args) + buf = default_user_info_3 + + args.each do |k, v| + buf.set(k, v) + end + + server_name = wstring(server_name) + + rc = NetUserAdd(server_name, 3, buf, nil) + if rc != NERR_Success + net_api_error!(rc) + end + end + + def self.net_user_get_info_l3(server_name, user_name) + server_name = wstring(server_name) + user_name = wstring(user_name) + + ui3_p = FFI::MemoryPointer.new(:pointer) + + rc = NetUserGetInfo(server_name, user_name, 3, ui3_p) + + if rc != NERR_Success + net_api_error!(rc) + end + + ui3 = USER_INFO_3.new(ui3_p.read_pointer).as_ruby + + rc = NetApiBufferFree(ui3_p.read_pointer) + + if rc != NERR_Success + net_api_error!(rc) + end + + ui3 + end + + def self.net_user_set_info_l3(server_name, user_name, info) + buf = default_user_info_3 + + info.each do |k, v| + buf.set(k, v) + end + + server_name = wstring(server_name) + user_name = wstring(user_name) + + rc = NetUserSetInfo(server_name, user_name, 3, buf, nil) + if rc != NERR_Success + net_api_error!(rc) + end + end + + def self.net_user_del(server_name, user_name) + server_name = wstring(server_name) + user_name = wstring(user_name) + + rc = NetUserDel(server_name, user_name) + if rc != NERR_Success + net_api_error!(rc) + end + end + + def self.net_local_group_add_member(server_name, group_name, domain_user) + server_name = wstring(server_name) + group_name = wstring(group_name) + domain_user = wstring(domain_user) + + buf = LOCALGROUP_MEMBERS_INFO_3.new + buf[:lgrmi3_domainandname] = FFI::MemoryPointer.from_string(domain_user) + + rc = NetLocalGroupAddMembers(server_name, group_name, 3, buf, 1) + + if rc != NERR_Success + net_api_error!(rc) + end + end + + end + end +end diff --git a/lib/chef/win32/security.rb b/lib/chef/win32/security.rb index 3902d8caaf..5c83180bc0 100644 --- a/lib/chef/win32/security.rb +++ b/lib/chef/win32/security.rb @@ -22,6 +22,7 @@ require 'chef/win32/memory' require 'chef/win32/process' require 'chef/win32/unicode' require 'chef/win32/security/token' +require 'chef/mixin/wstring' class Chef module ReservedNames::Win32 @@ -31,6 +32,8 @@ class Chef include Chef::ReservedNames::Win32::API::Security extend Chef::ReservedNames::Win32::API::Security extend Chef::ReservedNames::Win32::API::Macros + include Chef::Mixin::WideString + extend Chef::Mixin::WideString def self.access_check(security_descriptor, token, desired_access, generic_mapping) token_handle = token.handle.handle @@ -270,6 +273,36 @@ class Chef [ present.read_char != 0, acl.null? ? nil : ACL.new(acl, security_descriptor), defaulted.read_char != 0 ] end + def self.get_token_information_owner(token) + owner_result_size = FFI::MemoryPointer.new(:ulong) + if GetTokenInformation(token.handle.handle, :TokenOwner, nil, 0, owner_result_size) + raise "Expected ERROR_INSUFFICIENT_BUFFER from GetTokenInformation, and got no error!" + elsif FFI::LastError.error != ERROR_INSUFFICIENT_BUFFER + Chef::ReservedNames::Win32::Error.raise! + end + owner_result_storage = FFI::MemoryPointer.new owner_result_size.read_ulong + unless GetTokenInformation(token.handle.handle, :TokenOwner, owner_result_storage, owner_result_size.read_ulong, owner_result_size) + Chef::ReservedNames::Win32::Error.raise! + end + owner_result = TOKEN_OWNER.new owner_result_storage + SID.new(owner_result[:Owner], owner_result_storage) + end + + def self.get_token_information_primary_group(token) + group_result_size = FFI::MemoryPointer.new(:ulong) + if GetTokenInformation(token.handle.handle, :TokenPrimaryGroup, nil, 0, group_result_size) + raise "Expected ERROR_INSUFFICIENT_BUFFER from GetTokenInformation, and got no error!" + elsif FFI::LastError.error != ERROR_INSUFFICIENT_BUFFER + Chef::ReservedNames::Win32::Error.raise! + end + group_result_storage = FFI::MemoryPointer.new group_result_size.read_ulong + unless GetTokenInformation(token.handle.handle, :TokenPrimaryGroup, group_result_storage, group_result_size.read_ulong, group_result_size) + Chef::ReservedNames::Win32::Error.raise! + end + group_result = TOKEN_PRIMARY_GROUP.new group_result_storage + SID.new(group_result[:PrimaryGroup], group_result_storage) + end + def self.initialize_acl(acl_size) acl = FFI::MemoryPointer.new acl_size unless InitializeAcl(acl, acl_size, ACL_REVISION) @@ -415,6 +448,10 @@ class Chef [ SecurityDescriptor.new(absolute_sd), SID.new(owner), SID.new(group), ACL.new(dacl), ACL.new(sacl) ] end + def self.open_current_process_token(desired_access = TOKEN_READ) + open_process_token(Chef::ReservedNames::Win32::Process.get_current_process, desired_access) + end + def self.open_process_token(process, desired_access) process = process.handle if process.respond_to?(:handle) process = process.handle if process.respond_to?(:handle) @@ -513,7 +550,7 @@ class Chef def self.with_privileges(*privilege_names) # Set privileges - token = open_process_token(Chef::ReservedNames::Win32::Process.get_current_process, TOKEN_READ | TOKEN_ADJUST_PRIVILEGES) + token = open_current_process_token(TOKEN_READ | TOKEN_ADJUST_PRIVILEGES) old_privileges = token.enable_privileges(*privilege_names) # Let the caller do their privileged stuff @@ -533,7 +570,7 @@ class Chef true else - process_token = open_process_token(Chef::ReservedNames::Win32::Process.get_current_process, TOKEN_READ) + process_token = open_current_process_token(TOKEN_READ) elevation_result = FFI::Buffer.new(:ulong) elevation_result_size = FFI::MemoryPointer.new(:uint32) success = GetTokenInformation(process_token.handle.handle, :TokenElevation, elevation_result, 4, elevation_result_size) @@ -543,6 +580,18 @@ class Chef success && (elevation_result.read_ulong != 0) end end + + def self.logon_user(username, domain, password, logon_type, logon_provider) + username = wstring(username) + domain = wstring(domain) + password = wstring(password) + + token = FFI::Buffer.new(:pointer) + unless LogonUserW(username, domain, password, logon_type, logon_provider, token) + Chef::ReservedNames::Win32::Error.raise! + end + Token.new(Handle.new(token.read_pointer)) + end end end end diff --git a/lib/chef/win32/security/sid.rb b/lib/chef/win32/security/sid.rb index 8e9407dc80..f8bd934876 100644 --- a/lib/chef/win32/security/sid.rb +++ b/lib/chef/win32/security/sid.rb @@ -203,6 +203,23 @@ class Chef SID.from_account("#{::ENV['USERDOMAIN']}\\#{::ENV['USERNAME']}") end + # See https://technet.microsoft.com/en-us/library/cc961992.aspx + # In practice, this is SID.Administrators if the current_user is an admin (even if not + # running elevated), and is current_user otherwise. On win2k3, it technically can be + # current_user in all cases if a certain group policy is set. + def self.default_security_object_owner + token = Chef::ReservedNames::Win32::Security.open_current_process_token + Chef::ReservedNames::Win32::Security.get_token_information_owner(token) + end + + # See https://technet.microsoft.com/en-us/library/cc961996.aspx + # In practice, this seems to be SID.current_user for Microsoft Accounts, the current + # user's Domain Users group for domain accounts, and SID.None otherwise. + def self.default_security_object_group + token = Chef::ReservedNames::Win32::Security.open_current_process_token + Chef::ReservedNames::Win32::Security.get_token_information_primary_group(token) + end + def self.admin_account_name @admin_account_name ||= begin admin_account_name = nil diff --git a/pedant.gemfile b/pedant.gemfile index baa3e9aece..8e64fc039c 100644 --- a/pedant.gemfile +++ b/pedant.gemfile @@ -10,6 +10,7 @@ gem "mixlib-shellout", github: "opscode/mixlib-shellout", branch: "master" gem "ohai", github: "opscode/ohai", branch: "master" group(:docgen) do + gem "tomlrb" gem "yard" end diff --git a/rubygems-pkg/rubygems-update-2.4.6.gem b/rubygems-pkg/rubygems-update-2.4.6.gem Binary files differnew file mode 100644 index 0000000000..97ebec693a --- /dev/null +++ b/rubygems-pkg/rubygems-update-2.4.6.gem diff --git a/spec/data/lwrp/providers/buck_passer.rb b/spec/data/lwrp/providers/buck_passer.rb index 9792e2c824..2bbca07bf7 100644 --- a/spec/data/lwrp/providers/buck_passer.rb +++ b/spec/data/lwrp/providers/buck_passer.rb @@ -1,12 +1,28 @@ provides :buck_passer +def without_deprecation_warnings(&block) + old_treat_deprecation_warnings_as_errors = Chef::Config[:treat_deprecation_warnings_as_errors] + Chef::Config[:treat_deprecation_warnings_as_errors] = false + begin + block.call + ensure + Chef::Config[:treat_deprecation_warnings_as_errors] = old_treat_deprecation_warnings_as_errors + end +end + action :pass_buck do lwrp_foo :prepared_thumbs do action :prepare_thumbs - provider :lwrp_thumb_twiddler + # We know there will be a deprecation error here; head it off + without_deprecation_warnings do + provider :lwrp_thumb_twiddler + end end lwrp_foo :twiddled_thumbs do action :twiddle_thumbs - provider :lwrp_thumb_twiddler + # We know there will be a deprecation error here; head it off + without_deprecation_warnings do + provider :lwrp_thumb_twiddler + end end end diff --git a/spec/data/lwrp/providers/buck_passer_2.rb b/spec/data/lwrp/providers/buck_passer_2.rb index d34da3c378..c3bab7266f 100644 --- a/spec/data/lwrp/providers/buck_passer_2.rb +++ b/spec/data/lwrp/providers/buck_passer_2.rb @@ -1,10 +1,26 @@ +def without_deprecation_warnings(&block) + old_treat_deprecation_warnings_as_errors = Chef::Config[:treat_deprecation_warnings_as_errors] + Chef::Config[:treat_deprecation_warnings_as_errors] = false + begin + block.call + ensure + Chef::Config[:treat_deprecation_warnings_as_errors] = old_treat_deprecation_warnings_as_errors + end +end + action :pass_buck do lwrp_bar :prepared_eyes do action :prepare_eyes - provider :lwrp_paint_drying_watcher + # We know there will be a deprecation error here; head it off + without_deprecation_warnings do + provider :lwrp_paint_drying_watcher + end end lwrp_bar :dried_paint_watched do action :watch_paint_dry - provider :lwrp_paint_drying_watcher + # We know there will be a deprecation error here; head it off + without_deprecation_warnings do + provider :lwrp_paint_drying_watcher + end end end diff --git a/spec/data/lwrp/providers/embedded_resource_accesses_providers_scope.rb b/spec/data/lwrp/providers/embedded_resource_accesses_providers_scope.rb index f5841fb01c..77c1111ff5 100644 --- a/spec/data/lwrp/providers/embedded_resource_accesses_providers_scope.rb +++ b/spec/data/lwrp/providers/embedded_resource_accesses_providers_scope.rb @@ -3,11 +3,23 @@ # are passed properly (as demonstrated by the call to generate_new_name). attr_reader :enclosed_resource +def without_deprecation_warnings(&block) + old_treat_deprecation_warnings_as_errors = Chef::Config[:treat_deprecation_warnings_as_errors] + Chef::Config[:treat_deprecation_warnings_as_errors] = false + begin + block.call + ensure + Chef::Config[:treat_deprecation_warnings_as_errors] = old_treat_deprecation_warnings_as_errors + end +end + action :twiddle_thumbs do @enclosed_resource = lwrp_foo :foo do monkey generate_new_name(new_resource.monkey){ 'the monkey' } - action :twiddle_thumbs - provider :lwrp_monkey_name_printer + # We know there will be a deprecation error here; head it off + without_deprecation_warnings do + provider :lwrp_monkey_name_printer + end end end diff --git a/spec/data/lwrp_override/resources/foo.rb b/spec/data/lwrp_override/resources/foo.rb index 14decb9634..2fc13d32fd 100644 --- a/spec/data/lwrp_override/resources/foo.rb +++ b/spec/data/lwrp_override/resources/foo.rb @@ -3,3 +3,8 @@ actions :never_execute attribute :ever, :kind_of => String + +class ::Chef + def method_created_by_override_lwrp_foo + end +end diff --git a/spec/data/big.json b/spec/data/nested.json index 8f703035c3..775bb21981 100644 --- a/spec/data/big.json +++ b/spec/data/nested.json @@ -1,2 +1,2 @@ -{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":"test" -}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}} +{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":{"key":"test" +}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}} diff --git a/spec/functional/audit/runner_spec.rb b/spec/functional/audit/runner_spec.rb index 494942889a..aae8fcf582 100644 --- a/spec/functional/audit/runner_spec.rb +++ b/spec/functional/audit/runner_spec.rb @@ -46,22 +46,12 @@ describe Chef::Audit::Runner do RSpec::Core::Sandbox.sandboxed { ex.run } end - before do - Chef::Config[:log_location] = stdout - end - describe "#run" do let(:audits) { {} } let(:run_context) { instance_double(Chef::RunContext, :events => events, :audits => audits) } let(:control_group_name) { "control_group_name" } - it "Correctly runs an empty controls block" do - in_sub_process do - runner.run - end - end - shared_context "passing audit" do let(:audits) do should_pass = lambda do @@ -84,50 +74,40 @@ describe Chef::Audit::Runner do end end - context "there is a single successful control" do - include_context "passing audit" - it "correctly runs" do - in_sub_process do - runner.run - - expect(stdout.string).to match(/1 example, 0 failures/) + describe "log location is stdout" do + before do + allow(Chef::Log).to receive(:info) do |msg| + stdout.puts(msg) end end - end - context "there is a single failing control" do - include_context "failing audit" - it "correctly runs" do + it "Correctly runs an empty controls block" do in_sub_process do runner.run - - expect(stdout.string).to match(/Failure\/Error: expect\(2 - 1\)\.to eq\(0\)/) - expect(stdout.string).to match(/1 example, 1 failure/) - expect(stdout.string).to match(/# control_group_name should fail/) end end - end - describe "log location is a file" do - let(:tmpfile) { Tempfile.new("audit") } - before do - Chef::Config[:log_location] = tmpfile.path - end + context "there is a single successful control" do + include_context "passing audit" + it "correctly runs" do + in_sub_process do + runner.run - after do - tmpfile.close - tmpfile.unlink + expect(stdout.string).to match(/1 example, 0 failures/) + end + end end - include_context "failing audit" - it "correctly runs" do - in_sub_process do - runner.run + context "there is a single failing control" do + include_context "failing audit" + it "correctly runs" do + in_sub_process do + runner.run - contents = tmpfile.read - expect(contents).to match(/Failure\/Error: expect\(2 - 1\)\.to eq\(0\)/) - expect(contents).to match(/1 example, 1 failure/) - expect(contents).to match(/# control_group_name should fail/) + expect(stdout.string).to match(/Failure\/Error: expect\(2 - 1\)\.to eq\(0\)/) + expect(stdout.string).to match(/1 example, 1 failure/) + expect(stdout.string).to match(/# control_group_name should fail/) + end end end end diff --git a/spec/functional/event_loggers/windows_eventlog_spec.rb b/spec/functional/event_loggers/windows_eventlog_spec.rb index 4e383dd429..0723e7b984 100644 --- a/spec/functional/event_loggers/windows_eventlog_spec.rb +++ b/spec/functional/event_loggers/windows_eventlog_spec.rb @@ -79,4 +79,18 @@ describe Chef::EventLoggers::WindowsEventLogger, :windows_only, :not_supported_o end).to be_truthy end + it 'writes run_failed event with event_id 10003 even when run_status is not set' do + logger.run_failed(mock_exception) + + expect(event_log.read(flags, offset).any? do |e| + e.source == 'Chef' && e.event_id == 10003 && + e.string_inserts[0].include?("UNKNOWN") && + e.string_inserts[1].include?("UNKNOWN") && + e.string_inserts[2].include?(mock_exception.class.name) && + e.string_inserts[3].include?(mock_exception.message) && + e.string_inserts[4].include?(mock_exception.backtrace[0]) && + e.string_inserts[4].include?(mock_exception.backtrace[1]) + end).to be_truthy + end + end diff --git a/spec/functional/knife/ssh_spec.rb b/spec/functional/knife/ssh_spec.rb index 5b8ad6f368..6608d05771 100644 --- a/spec/functional/knife/ssh_spec.rb +++ b/spec/functional/knife/ssh_spec.rb @@ -165,7 +165,7 @@ describe Chef::Knife::Ssh do it "uses the ssh_attribute" do @knife.run - expect(@knife.config[:attribute]).to eq("ec2.public_hostname") + expect(@knife.get_ssh_attribute(Chef::Node.new)).to eq("ec2.public_hostname") end end @@ -177,7 +177,7 @@ describe Chef::Knife::Ssh do it "uses the default" do @knife.run - expect(@knife.config[:attribute]).to eq("fqdn") + expect(@knife.get_ssh_attribute(Chef::Node.new)).to eq("fqdn") end end diff --git a/spec/functional/mixin/powershell_out_spec.rb b/spec/functional/mixin/powershell_out_spec.rb new file mode 100644 index 0000000000..9cc8aeed7e --- /dev/null +++ b/spec/functional/mixin/powershell_out_spec.rb @@ -0,0 +1,43 @@ +# +# 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/mixin/powershell_out' + +describe Chef::Mixin::PowershellOut, windows_only: true do + include Chef::Mixin::PowershellOut + + describe "#powershell_out" do + it "runs a powershell command and collects stdout" do + expect(powershell_out("get-process").run_command.stdout).to match /Handles\s+NPM\(K\)\s+PM\(K\)\s+WS\(K\)\s+VM\(M\)\s+CPU\(s\)\s+Id\s+ProcessName/ + end + + it "does not raise exceptions when the command is invalid" do + powershell_out("this-is-not-a-valid-command").run_command + end + end + + describe "#powershell_out!" do + it "runs a powershell command and collects stdout" do + expect(powershell_out!("get-process").run_command.stdout).to match /Handles\s+NPM\(K\)\s+PM\(K\)\s+WS\(K\)\s+VM\(M\)\s+CPU\(s\)\s+Id\s+ProcessName/ + end + + it "raises exceptions when the command is invalid" do + expect { powershell_out!("this-is-not-a-valid-command").run_command }.to raise_exception(Mixlib::ShellOut::ShellCommandFailed) + end + end +end diff --git a/spec/functional/rebooter_spec.rb b/spec/functional/rebooter_spec.rb index 763021607b..485e98f247 100644 --- a/spec/functional/rebooter_spec.rb +++ b/spec/functional/rebooter_spec.rb @@ -70,7 +70,7 @@ describe Chef::Platform::Rebooter do shared_context 'test a reboot method' do def test_rebooter_method(method_sym, is_windows, expected_reboot_str) - allow(Chef::Platform).to receive(:windows?).and_return(is_windows) + allow(ChefConfig).to receive(:windows?).and_return(is_windows) expect(rebooter).to receive(:shell_out!).once.with(expected_reboot_str) expect(rebooter).to receive(method_sym).once.and_call_original rebooter.send(method_sym, run_context.node) diff --git a/spec/functional/resource/aixinit_service_spec.rb b/spec/functional/resource/aixinit_service_spec.rb index 19b65ca2a0..3d9216158e 100755 --- a/spec/functional/resource/aixinit_service_spec.rb +++ b/spec/functional/resource/aixinit_service_spec.rb @@ -208,4 +208,4 @@ describe Chef::Resource::Service, :requires_root, :aix_only do end end end -end
\ No newline at end of file +end diff --git a/spec/functional/resource/execute_spec.rb b/spec/functional/resource/execute_spec.rb index 8a44d13ba3..692ccfb796 100644 --- a/spec/functional/resource/execute_spec.rb +++ b/spec/functional/resource/execute_spec.rb @@ -62,7 +62,7 @@ describe Chef::Resource::Execute do end describe "when parent resource sets :cwd" do - let(:guard) { %{ruby -e 'exit 1 unless File.exists?("./big.json")'} } + let(:guard) { %{ruby -e 'exit 1 unless File.exists?("./nested.json")'} } it "guard inherits :cwd from resource and runs" do resource.cwd CHEF_SPEC_DATA @@ -137,9 +137,16 @@ describe Chef::Resource::Execute do end end + # Ensure that CommandTimeout is raised, and is caused by resource.timeout really expiring. + # https://github.com/chef/chef/issues/2985 + # + # resource.timeout should be short, this is what we're testing + # resource.command ruby sleep timer should be longer than resource.timeout to give us something to timeout + # Timeout::timeout should be longer than resource.timeout, but less than the resource.command ruby sleep timer, + # so we fail if we finish on resource.command instead of resource.timeout, but raise CommandTimeout anyway (#2175). it "times out when a timeout is set on the resource" do - Timeout::timeout(5) do - resource.command %{ruby -e 'sleep 600'} + Timeout::timeout(30) do + resource.command %{ruby -e 'sleep 300'} resource.timeout 0.1 expect { resource.run_action(:run) }.to raise_error(Mixlib::ShellOut::CommandTimeout) end diff --git a/spec/functional/resource/file_spec.rb b/spec/functional/resource/file_spec.rb index f1a290dea4..9e30e62111 100644 --- a/spec/functional/resource/file_spec.rb +++ b/spec/functional/resource/file_spec.rb @@ -86,6 +86,31 @@ describe Chef::Resource::File do end end + + describe "when using backup" do + before do + Chef::Config[:file_backup_path] = CHEF_SPEC_BACKUP_PATH + resource_without_content.backup(1) + resource_without_content.run_action(:create) + end + + let(:backup_glob) { File.join(CHEF_SPEC_BACKUP_PATH, test_file_dir.sub(/^([A-Za-z]:)/, ""), "#{file_base}*") } + + let(:path) do + # Use native system path + ChefConfig::PathHelper.canonical_path(File.join(test_file_dir, make_tmpname(file_base)), false) + end + + it "only stores the number of requested backups" do + resource_without_content.content('foo') + resource_without_content.run_action(:create) + resource_without_content.content('bar') + resource_without_content.run_action(:create) + expect(Dir.glob(backup_glob).length).to eq(1) + end + + end + # github issue 1842. describe "when running action :create on a relative path" do before do diff --git a/spec/functional/resource/group_spec.rb b/spec/functional/resource/group_spec.rb index 6676aa32e9..529af52d4e 100644 --- a/spec/functional/resource/group_spec.rb +++ b/spec/functional/resource/group_spec.rb @@ -372,6 +372,11 @@ downthestreetalwayshadagoodsmileonhisfacetheoldmanwalkingdownthestreeQQQQQQ" } let(:tested_action) { :manage } describe "when there is no group" do + before(:each) do + group_resource.run_action(:remove) + group_should_not_exist(group_name) + end + it "raises an error on modify" do expect { group_resource.run_action(:modify) }.to raise_error end diff --git a/spec/functional/resource/link_spec.rb b/spec/functional/resource/link_spec.rb index d39a0c2ef6..7e903b30b4 100644 --- a/spec/functional/resource/link_spec.rb +++ b/spec/functional/resource/link_spec.rb @@ -348,8 +348,7 @@ describe Chef::Resource::Link do end it_behaves_like 'delete errors out' end - context 'and the link already exists and is not writeable to this user', :skip => true do - end + it_behaves_like 'a securable resource without existing target' do let(:path) { target_file } def allowed_acl(sid, expected_perms) @@ -360,7 +359,7 @@ describe Chef::Resource::Link do end def parent_inheritable_acls dummy_file_path = File.join(test_file_dir, "dummy_file") - dummy_file = FileUtils.touch(dummy_file_path) + FileUtils.touch(dummy_file_path) dummy_desc = get_security_descriptor(dummy_file_path) FileUtils.rm_rf(dummy_file_path) dummy_desc @@ -416,8 +415,6 @@ describe Chef::Resource::Link do end end end - context "when the link destination is not readable to this user", :skip => true do - end context "when the link destination does not exist" do include_context 'create symbolic link succeeds' include_context 'delete is noop' @@ -518,8 +515,6 @@ describe Chef::Resource::Link do end it_behaves_like 'delete errors out' end - context "and the link already exists and is not writeable to this user", :skip => true do - end context "and specifies security attributes" do before(:each) do resource.owner(windows? ? 'Guest' : 'nobody') @@ -559,10 +554,10 @@ describe Chef::Resource::Link do end context 'and the link does not yet exist' do it 'links to the target file' do + skip('OS X/FreeBSD/AIX symlink? and readlink working on hard links to symlinks') if (os_x? or freebsd? or aix?) resource.run_action(:create) expect(File.exists?(target_file)).to be_truthy # OS X gets angry about this sort of link. Bug in OS X, IMO. - pending('OS X/FreeBSD/AIX symlink? and readlink working on hard links to symlinks') if (os_x? or freebsd? or aix?) expect(symlink?(target_file)).to be_truthy expect(readlink(target_file)).to eq(canonicalize(@other_target)) end @@ -578,7 +573,7 @@ describe Chef::Resource::Link do end context 'and the link does not yet exist' do it 'links to the target file' do - pending('OS X/FreeBSD/AIX fails to create hardlinks to broken symlinks') if (os_x? or freebsd? or aix?) + skip('OS X/FreeBSD/AIX fails to create hardlinks to broken symlinks') if (os_x? or freebsd? or aix?) resource.run_action(:create) # Windows and Unix have different definitions of exists? here, and that's OK. if windows? @@ -593,8 +588,7 @@ describe Chef::Resource::Link do end end end - context "when the link destination is not readable to this user", :skip => true do - end + context "when the link destination does not exist" do context 'and the link does not yet exist' do it 'create errors out' do diff --git a/spec/functional/resource/powershell_spec.rb b/spec/functional/resource/powershell_spec.rb index 56a905efe7..17ae8cbd2a 100644 --- a/spec/functional/resource/powershell_spec.rb +++ b/spec/functional/resource/powershell_spec.rb @@ -56,14 +56,13 @@ describe Chef::Resource::WindowsScript::PowershellScript, :windows_only do resource.run_action(:run) end - it "returns the -27 for a powershell script that exits with -27", :windows_powershell_dsc_only do - # This is broken on Powershell < 4.0 + it "returns the exit status 27 for a powershell script that exits with 27" do file = Tempfile.new(['foo', '.ps1']) begin - file.write "exit -27" + file.write "exit 27" file.close resource.code(". \"#{file.path}\"") - resource.returns(-27) + resource.returns(27) resource.run_action(:run) ensure file.close @@ -71,6 +70,30 @@ describe Chef::Resource::WindowsScript::PowershellScript, :windows_only do end end + let (:negative_exit_status) { -27 } + let (:unsigned_exit_status) { (-negative_exit_status ^ 65535) + 1 } + it "returns the exit status -27 as a signed integer or an unsigned 16-bit 2's complement value of 65509 for a powershell script that exits with -27" do + # Versions of PowerShell prior to 4.0 return a 16-bit unsigned value -- + # PowerShell 4.0 and later versions return a 32-bit signed value. + file = Tempfile.new(['foo', '.ps1']) + begin + file.write "exit #{negative_exit_status.to_s}" + file.close + resource.code(". \"#{file.path}\"") + + # PowerShell earlier than 4.0 takes negative exit codes + # and returns them as the underlying unsigned 16-bit + # 2's complement representation. We cover multiple versions + # of PowerShell in this example by including both the signed + # exit code and its converted counterpart as permitted return values. + # See http://support.microsoft.com/en-us/kb/2646183/zh-cn + resource.returns([negative_exit_status, unsigned_exit_status]) + expect { resource.run_action(:run) }.not_to raise_error + ensure + file.close + file.unlink + end + end it "returns the process exit code" do resource.code(arbitrary_nonzero_process_exit_code_content) @@ -99,7 +122,19 @@ describe Chef::Resource::WindowsScript::PowershellScript, :windows_only do it "returns 1 if the last command was a cmdlet that failed and was preceded by a successfully executed non-cmdlet Windows binary" do resource.code([windows_process_exit_code_success_content, cmdlet_exit_code_not_found_content].join(';')) resource.returns(1) - resource.run_action(:run) + expect { resource.run_action(:run) }.not_to raise_error + end + + it "raises an error if the script is not syntactically correct and returns is not set to 1" do + resource.code('if({)') + resource.returns(0) + expect { resource.run_action(:run) }.to raise_error(Mixlib::ShellOut::ShellCommandFailed) + end + + it "returns 1 if the script provided to the code attribute is not syntactically correct" do + resource.code('if({)') + resource.returns(1) + expect { resource.run_action(:run) }.not_to raise_error end # This somewhat ambiguous case, two failures of different types, diff --git a/spec/functional/resource/user/useradd_spec.rb b/spec/functional/resource/user/useradd_spec.rb index 3e4e4e7604..474f6a4ecf 100644 --- a/spec/functional/resource/user/useradd_spec.rb +++ b/spec/functional/resource/user/useradd_spec.rb @@ -65,8 +65,12 @@ describe Chef::Provider::User::Useradd, metadata do end end - def supports_quote_in_username? - OHAI_SYSTEM["platform_family"] == "debian" + def self.quote_in_username_unsupported? + if OHAI_SYSTEM["platform_family"] == "debian" + false + else + "Only debian family systems support quotes in username" + end end def password_should_be_set @@ -108,7 +112,7 @@ describe Chef::Provider::User::Useradd, metadata do break if status.exitstatus != 8 sleep 1 - max_retries = max_retries -1 + max_retries = max_retries - 1 rescue UserNotFound break end @@ -162,15 +166,10 @@ describe Chef::Provider::User::Useradd, metadata do end end - let(:skip) { false } - describe "action :create" do context "when the user does not exist beforehand" do before do - if reason = skip - pending(reason) - end user_resource.run_action(:create) expect(user_resource).to be_updated_by_last_action end @@ -186,14 +185,7 @@ describe Chef::Provider::User::Useradd, metadata do # tabulation: '\t', etc.). Note that using a slash ('/') may break the # default algorithm for the definition of the user's home directory. - context "and the username contains a single quote" do - let(:skip) do - if supports_quote_in_username? - false - else - "Platform #{OHAI_SYSTEM["platform"]} not expected to support username w/ quote" - end - end + context "and the username contains a single quote", skip: quote_in_username_unsupported? do let(:username) { "t'bilisi" } @@ -342,7 +334,7 @@ describe Chef::Provider::User::Useradd, metadata do before do if reason = skip - pending(reason) + skip(reason) end existing_user.run_action(:create) expect(existing_user).to be_updated_by_last_action @@ -535,7 +527,7 @@ describe Chef::Provider::User::Useradd, metadata do def aix_user_lock_status lock_info = shell_out!("lsuser -a account_locked #{username}") - status = /\S+\s+account_locked=(\S+)/.match(lock_info.stdout)[1] + /\S+\s+account_locked=(\S+)/.match(lock_info.stdout)[1] end def user_account_should_be_locked diff --git a/spec/functional/resource/user/windows_spec.rb b/spec/functional/resource/user/windows_spec.rb new file mode 100644 index 0000000000..5e68478b34 --- /dev/null +++ b/spec/functional/resource/user/windows_spec.rb @@ -0,0 +1,125 @@ +# Author:: Jay Mundrawala (<jdm@chef.io>) +# Copyright:: Copyright (c) 2015 Chef Software +# 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/mixin/shell_out' + +describe Chef::Provider::User::Windows, :windows_only do + include Chef::Mixin::ShellOut + + let(:username) { 'ChefFunctionalTest' } + let(:password) { SecureRandom.uuid } + + let(:node) do + n = Chef::Node.new + n.consume_external_attrs(OHAI_SYSTEM.data.dup, {}) + n + end + + let(:events) { Chef::EventDispatch::Dispatcher.new } + let(:run_context) { Chef::RunContext.new(node, {}, events) } + let(:new_resource) do + Chef::Resource::User.new(username, run_context).tap do |r| + r.provider(Chef::Provider::User::Windows) + r.password(password) + end + end + + def delete_user(u) + shell_out("net user #{u} /delete") + end + + before do + delete_user(username) + end + + describe 'action :create' do + it 'creates a user when a username and password are given' do + new_resource.run_action(:create) + expect(new_resource).to be_updated_by_last_action + expect(shell_out("net user #{username}").exitstatus).to eq(0) + end + + it 'reports no changes if there are no changes needed' do + new_resource.run_action(:create) + new_resource.run_action(:create) + expect(new_resource).not_to be_updated_by_last_action + end + + it 'allows chaning the password' do + new_resource.run_action(:create) + new_resource.password(SecureRandom.uuid) + new_resource.run_action(:create) + expect(new_resource).to be_updated_by_last_action + end + end + + describe 'action :remove' do + before do + new_resource.run_action(:create) + end + + it 'deletes the user' do + new_resource.run_action(:remove) + expect(new_resource).to be_updated_by_last_action + expect(shell_out("net user #{username}").exitstatus).to eq(2) + end + + it 'is idempotent' do + new_resource.run_action(:remove) + new_resource.run_action(:remove) + expect(new_resource).not_to be_updated_by_last_action + end + end + + describe 'action :lock' do + before do + new_resource.run_action(:create) + end + + it 'locks the user account' do + new_resource.run_action(:lock) + expect(new_resource).to be_updated_by_last_action + expect(shell_out("net user #{username}").stdout).to match(/Account active\s*No/) + end + + it 'is idempotent' do + new_resource.run_action(:lock) + new_resource.run_action(:lock) + expect(new_resource).not_to be_updated_by_last_action + end + end + + describe 'action :unlock' do + before do + new_resource.run_action(:create) + new_resource.run_action(:lock) + end + + it 'unlocks the user account' do + new_resource.run_action(:unlock) + expect(new_resource).to be_updated_by_last_action + expect(shell_out("net user #{username}").stdout).to match(/Account active\s*Yes/) + end + + it 'is idempotent' do + new_resource.run_action(:unlock) + new_resource.run_action(:unlock) + expect(new_resource).not_to be_updated_by_last_action + end + end +end diff --git a/spec/functional/shell_spec.rb b/spec/functional/shell_spec.rb index fa9de77b0e..a753948c7f 100644 --- a/spec/functional/shell_spec.rb +++ b/spec/functional/shell_spec.rb @@ -29,6 +29,8 @@ describe Shell do describe "smoke tests", :unix_only => true do include Chef::Mixin::Command::Unix + TIMEOUT=300 + def read_until(io, expected_value) start = Time.new buffer = "" @@ -38,15 +40,30 @@ describe Shell do rescue Errno::EWOULDBLOCK, Errno::EAGAIN, Errno::EIO, EOFError sleep 0.01 end - if Time.new - start > 30 - STDERR.puts "did not read expected value `#{expected_value}' within 15s" - STDERR.puts "Buffer so far: `#{buffer}'" - break + if Time.new - start > TIMEOUT + raise "did not read expected value `#{expected_value}' within #{TIMEOUT}s\n" + + "Buffer so far: `#{buffer}'" end end buffer end + def flush_output(io) + start = Time.new + loop do + begin + io.read_nonblock(1) + rescue Errno::EWOULDBLOCK, Errno::EAGAIN + sleep 0.01 + rescue EOFError, Errno::EIO + break + end + if Time.new - start > TIMEOUT + raise "timed out after #{TIMEOUT}s waiting for output to end" + end + end + end + def wait_or_die(pid) start = Time.new @@ -67,12 +84,12 @@ describe Shell do path_to_chef_shell = File.expand_path("../../../bin/chef-shell", __FILE__) output = '' status = popen4("#{path_to_chef_shell} -c #{config} #{options}", :waitlast => true) do |pid, stdin, stdout, stderr| - read_until(stdout, "chef >") + read_until(stdout, "chef (#{Chef::VERSION})>") yield stdout, stdin if block_given? stdin.write("'done'\n") output = read_until(stdout, '=> "done"') stdin.print("exit\n") - read_until(stdout, "\n") + flush_output(stdout) end [output, status.exitstatus] @@ -84,14 +101,12 @@ describe Shell do config = File.expand_path("shef-config.rb", CHEF_SPEC_DATA) path_to_chef_shell = File.expand_path("../../../bin/chef-shell", __FILE__) reader, writer, pid = PTY.spawn("#{path_to_chef_shell} -c #{config} #{options}") - read_until(reader, "chef >") + read_until(reader, "chef (#{Chef::VERSION})>") yield reader, writer if block_given? writer.puts('"done"') output = read_until(reader, '=> "done"') writer.print("exit\n") - read_until(reader, "exit") - read_until(reader, "\n") - read_until(reader, "\n") + flush_output(reader) writer.close exitstatus = wait_or_die(pid) diff --git a/spec/functional/win32/sid_spec.rb b/spec/functional/win32/sid_spec.rb new file mode 100644 index 0000000000..1f5f66178a --- /dev/null +++ b/spec/functional/win32/sid_spec.rb @@ -0,0 +1,55 @@ +# +# Author:: Dan Bjorge (<dbjorge@gmail.com>) +# Copyright:: Copyright (c) 2015 Dan Bjorge +# 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' +if Chef::Platform.windows? + require 'chef/win32/security' +end + +describe 'Chef::ReservedNames::Win32::SID', :windows_only do + if Chef::Platform.windows? + SID ||= Chef::ReservedNames::Win32::Security::SID + end + + it 'should resolve default_security_object_group as a sane user group', :windows_not_domain_joined_only do + # Domain accounts: domain-specific Domain Users SID + # Microsoft Accounts: SID.current_user + # Else: SID.None + expect(SID.default_security_object_group).to eq(SID.None).or eq(SID.current_user) + end + + context 'running as an elevated administrator user' do + it 'should resolve default_security_object_owner as the Administrators group' do + expect(SID.default_security_object_owner).to eq(SID.Administrators) + end + end + + context 'running as a non-elevated administrator user' do + it 'should resolve default_security_object_owner as the current user' do + skip 'requires user support in mixlib-shellout, see security_spec.rb' + expect(SID.default_security_object_owner).to eq(SID.Administrators) + end + end + + context 'running as a non-elevated, non-administrator user' do + it 'should resolve default_security_object_owner as the current user' do + skip 'requires user support in mixlib-shellout, see security_spec.rb' + expect(SID.default_security_object_owner).to eq(SID.current_user) + end + end +end diff --git a/spec/integration/client/client_spec.rb b/spec/integration/client/client_spec.rb index b5c5e12781..8c72048965 100644 --- a/spec/integration/client/client_spec.rb +++ b/spec/integration/client/client_spec.rb @@ -3,34 +3,35 @@ require 'chef/mixin/shell_out' require 'tiny_server' require 'tmpdir' -def recipes_filename - File.join(CHEF_SPEC_DATA, 'recipes.tgz') -end -def start_tiny_server(server_opts={}) - recipes_size = File::Stat.new(recipes_filename).size - @server = TinyServer::Manager.new(server_opts) - @server.start - @api = TinyServer::API.instance - @api.clear - # - # trivial endpoints - # - # just a normal file - # (expected_content should be uncompressed) - @api.get("/recipes.tgz", 200) { - File.open(recipes_filename, "rb") do |f| - f.read - end - } -end +describe "chef-client" do -def stop_tiny_server - @server.stop - @server = @api = nil -end + def recipes_filename + File.join(CHEF_SPEC_DATA, 'recipes.tgz') + end + + def start_tiny_server(server_opts={}) + @server = TinyServer::Manager.new(server_opts) + @server.start + @api = TinyServer::API.instance + @api.clear + # + # trivial endpoints + # + # just a normal file + # (expected_content should be uncompressed) + @api.get("/recipes.tgz", 200) { + File.open(recipes_filename, "rb") do |f| + f.read + end + } + end + + def stop_tiny_server + @server.stop + @server = @api = nil + end -describe "chef-client" do include IntegrationSupport include Chef::Mixin::ShellOut @@ -47,6 +48,8 @@ describe "chef-client" do # cf. CHEF-4914 let(:chef_client) { "ruby '#{chef_dir}/chef-client' --minimal-ohai" } + let(:critical_env_vars) { %w(PATH RUBYOPT BUNDLE_GEMFILE GEM_PATH).map {|o| "#{o}=#{ENV[o]}"} .join(' ') } + when_the_repository "has a cookbook with a no-op recipe" do before { file 'cookbooks/x/recipes/default.rb', '' } @@ -56,8 +59,23 @@ local_mode true cookbook_path "#{path_to('cookbooks')}" EOM - result = shell_out("#{chef_client} -c \"#{path_to('config/client.rb')}\" -o 'x::default'", :cwd => chef_dir) - result.error! + shell_out!("#{chef_client} -c \"#{path_to('config/client.rb')}\" -o 'x::default'", :cwd => chef_dir) + end + + it "should complete successfully with no other environment variables", :skip => (Chef::Platform.windows?) do + file 'config/client.rb', <<EOM +local_mode true +cookbook_path "#{path_to('cookbooks')}" +EOM + + begin + result = shell_out("env -i #{critical_env_vars} #{chef_client} -c \"#{path_to('config/client.rb')}\" -o 'x::default'", :cwd => chef_dir) + result.error! + rescue + Chef::Log.info "Bare invocation will have the following load-path." + Chef::Log.info shell_out!("env -i #{critical_env_vars} ruby -e 'puts $:'").stdout + raise + end end it "should complete successfully with --no-listen" do @@ -70,6 +88,11 @@ EOM result.error! end + it "should be able to node.save with bad utf8 characters in the node data" do + file "cookbooks/x/attributes/default.rb", 'default["badutf8"] = "Elan Ruusam\xE4e"' + result = shell_out("#{chef_client} -z -r 'x::default' --disable-config", :cwd => path_to('')) + result.error! + end context 'and no config file' do it 'should complete with success when cwd is just above cookbooks and paths are not specified' do @@ -319,7 +342,8 @@ end end end - context "when using recipe-url" do + # Fails on appveyor, but works locally on windows and on windows hosts in Ci. + context "when using recipe-url", :skip_appveyor do before(:all) do start_tiny_server end @@ -340,7 +364,7 @@ EOM it 'should fail when passed --recipe-url and not passed -z' do result = shell_out("#{chef_client} --recipe-url=http://localhost:9000/recipes.tgz", :cwd => tmp_dir) - expect(result.exitstatus).to eq(1) + expect(result.exitstatus).not_to eq(0) end end end diff --git a/spec/integration/knife/deps_spec.rb b/spec/integration/knife/deps_spec.rb index 3120db4940..b7333cefda 100644 --- a/spec/integration/knife/deps_spec.rb +++ b/spec/integration/knife/deps_spec.rb @@ -216,22 +216,16 @@ depends "self"' end it 'knife deps prints each once' do - knife('deps /cookbooks/foo /cookbooks/self').should_succeed <<EOM -/cookbooks/baz -/cookbooks/bar -/cookbooks/foo -/cookbooks/self -EOM + knife('deps /cookbooks/foo /cookbooks/self').should_succeed( + stdout: "/cookbooks/baz\n/cookbooks/bar\n/cookbooks/foo\n/cookbooks/self\n", + stderr: "WARN: Ignoring self-dependency in cookbook self, please remove it (in the future this will be fatal).\n" + ) end it 'knife deps --tree prints each once' do - knife('deps --tree /cookbooks/foo /cookbooks/self').should_succeed <<EOM -/cookbooks/foo - /cookbooks/bar - /cookbooks/baz - /cookbooks/foo -/cookbooks/self - /cookbooks/self -EOM + knife('deps --tree /cookbooks/foo /cookbooks/self').should_succeed( + stdout: "/cookbooks/foo\n /cookbooks/bar\n /cookbooks/baz\n /cookbooks/foo\n/cookbooks/self\n", + stderr: "WARN: Ignoring self-dependency in cookbook self, please remove it (in the future this will be fatal).\n" + ) end end when_the_repository 'has roles with circular dependencies' do diff --git a/spec/integration/knife/upload_spec.rb b/spec/integration/knife/upload_spec.rb index cef4f54e97..826ecec364 100644 --- a/spec/integration/knife/upload_spec.rb +++ b/spec/integration/knife/upload_spec.rb @@ -154,6 +154,24 @@ EOM end end + context 'when cookbook metadata has a self-dependency' do + before do + file 'cookbooks/x/metadata.rb', "name 'x'; version '1.0.0'; depends 'x'" + end + + it "should warn", :chef_lt_13_only do + knife('upload /cookbooks').should_succeed( + stdout: "Updated /cookbooks/x\n", + stderr: "WARN: Ignoring self-dependency in cookbook x, please remove it (in the future this will be fatal).\n" + ) + knife('diff --name-status /').should_succeed '' + end + it "should fail in Chef 13", :chef_gte_13_only do + knife('upload /cookbooks').should_fail '' + # FIXME: include the error message here + end + end + context 'as well as one extra copy of each thing' do before do file 'clients/y.json', { 'public_key' => ChefZero::PUBLIC_KEY } diff --git a/spec/integration/recipes/lwrp_inline_resources_spec.rb b/spec/integration/recipes/lwrp_inline_resources_spec.rb index b4c4e6ca11..e70605d3d3 100644 --- a/spec/integration/recipes/lwrp_inline_resources_spec.rb +++ b/spec/integration/recipes/lwrp_inline_resources_spec.rb @@ -5,7 +5,7 @@ describe "LWRPs with inline resources" do include IntegrationSupport include Chef::Mixin::ShellOut - let(:chef_dir) { File.join(File.dirname(__FILE__), "..", "..", "..", "bin") } + let(:chef_dir) { File.expand_path("../../../../bin", __FILE__) } # Invoke `chef-client` as `ruby PATH/TO/chef-client`. This ensures the # following constraints are satisfied: diff --git a/spec/integration/recipes/lwrp_spec.rb b/spec/integration/recipes/lwrp_spec.rb new file mode 100644 index 0000000000..e93763fddc --- /dev/null +++ b/spec/integration/recipes/lwrp_spec.rb @@ -0,0 +1,57 @@ +require 'support/shared/integration/integration_helper' +require 'chef/mixin/shell_out' + +describe "LWRPs" do + include IntegrationSupport + include Chef::Mixin::ShellOut + + let(:chef_dir) { File.expand_path("../../../../bin", __FILE__) } + + # Invoke `chef-client` as `ruby PATH/TO/chef-client`. This ensures the + # following constraints are satisfied: + # * Windows: windows can only run batch scripts as bare executables. Rubygems + # creates batch wrappers for installed gems, but we don't have batch wrappers + # in the source tree. + # * Other `chef-client` in PATH: A common case is running the tests on a + # machine that has omnibus chef installed. In that case we need to ensure + # we're running `chef-client` from the source tree and not the external one. + # cf. CHEF-4914 + let(:chef_client) { "ruby '#{chef_dir}/chef-client' --minimal-ohai" } + + when_the_repository "has a cookbook named l-w-r-p" do + before do + directory 'cookbooks/l-w-r-p' do + + file 'resources/foo.rb', <<EOM +default_action :create +EOM + file 'providers/foo.rb', <<EOM +action :create do +end +EOM + + file 'recipes/default.rb', <<EOM +l_w_r_p_foo "me" +EOM + + end # directory 'cookbooks/x' + end + + it "should complete with success" do + file 'config/client.rb', <<EOM +local_mode true +cookbook_path "#{path_to('cookbooks')}" +log_level :warn +EOM + + result = shell_out("#{chef_client} -c \"#{path_to('config/client.rb')}\" --no-color -F doc -o 'l-w-r-p::default'", :cwd => chef_dir) + actual = result.stdout.lines.map { |l| l.chomp }.join("\n") + expected = <<EOM + * l_w_r_p_foo[me] action create (up to date) +EOM + expected = expected.lines.map { |l| l.chomp }.join("\n") + expect(actual).to include(expected) + result.error! + end + end +end diff --git a/spec/integration/recipes/provider_choice.rb b/spec/integration/recipes/provider_choice.rb new file mode 100644 index 0000000000..01537b2c05 --- /dev/null +++ b/spec/integration/recipes/provider_choice.rb @@ -0,0 +1,36 @@ +require 'support/shared/integration/integration_helper' + +describe "Recipe DSL methods" do + include IntegrationSupport + + context "With resource class providing 'provider_thingy'" do + before :context do + class Chef::Resource::ProviderThingy < Chef::Resource + resource_name :provider_thingy + default_action :create + def to_s + "provider_thingy resource class" + end + end + end + context "And class Chef::Provider::ProviderThingy with no provides" do + before :context do + class Chef::Provider::ProviderThingy < Chef::Provider + def load_current_resource + end + def action_create + Chef::Log.warn("hello from #{self.class.name}") + end + end + end + + it "provider_thingy 'blah' runs the provider and warns" do + recipe = converge { + provider_thingy 'blah' do; end + } + expect(recipe.logged_warnings).to match /hello from Chef::Provider::ProviderThingy/ + expect(recipe.logged_warnings).to match /you must use 'provides' to provide DSL/i + end + end + end +end diff --git a/spec/integration/recipes/recipe_dsl_spec.rb b/spec/integration/recipes/recipe_dsl_spec.rb new file mode 100644 index 0000000000..3f4bf9fd5f --- /dev/null +++ b/spec/integration/recipes/recipe_dsl_spec.rb @@ -0,0 +1,757 @@ +require 'support/shared/integration/integration_helper' + +describe "Recipe DSL methods" do + include IntegrationSupport + + module Namer + extend self + attr_accessor :current_index + end + + before(:all) { Namer.current_index = 1 } + before { Namer.current_index += 1 } + + context "With resource 'base_thingy' declared as BaseThingy" do + before(:context) { + + class BaseThingy < Chef::Resource + def initialize(*args, &block) + super + @allowed_actions = [ :create ] + @action = :create + end + + resource_name 'base_thingy' + + class<<self + attr_accessor :created_resource + attr_accessor :created_provider + end + + def provider + Provider + end + class Provider < Chef::Provider + def load_current_resource + end + def action_create + BaseThingy.created_resource = new_resource.class + BaseThingy.created_provider = self.class + end + end + end + + # Modules to put stuff in + module RecipeDSLSpecNamespace; end + module RecipeDSLSpecNamespace::Bar; end + + } + + before :each do + BaseThingy.created_resource = nil + BaseThingy.created_provider = nil + end + + context "Deprecated automatic resource DSL" do + before do + Chef::Config[:treat_deprecation_warnings_as_errors] = false + end + + context "With a resource 'backcompat_thingy' declared in Chef::Resource and Chef::Provider" do + before(:context) { + + class Chef::Resource::BackcompatThingy < Chef::Resource + def initialize(*args, &block) + super + @allowed_actions = [ :create ] + @action = :create + end + end + class Chef::Provider::BackcompatThingy < Chef::Provider + def load_current_resource + end + def action_create + BaseThingy.created_resource = new_resource.class + BaseThingy.created_provider = self.class + end + end + + } + + it "backcompat_thingy creates a Chef::Resource::BackcompatThingy" do + recipe = converge { + backcompat_thingy 'blah' do; end + } + expect(BaseThingy.created_resource).to eq Chef::Resource::BackcompatThingy + expect(BaseThingy.created_provider).to eq Chef::Provider::BackcompatThingy + end + + context "and another resource 'backcompat_thingy' in BackcompatThingy with 'provides'" do + before(:context) { + + class RecipeDSLSpecNamespace::BackcompatThingy < BaseThingy + provides :backcompat_thingy + resource_name :backcompat_thingy + end + + } + + it "backcompat_thingy creates a BackcompatThingy" do + recipe = converge { + backcompat_thingy 'blah' do; end + } + expect(recipe.logged_warnings).to eq '' + expect(BaseThingy.created_resource).not_to be_nil + end + end + end + + context "With a resource named RecipeDSLSpecNamespace::Bar::BarThingy" do + before(:context) { + + class RecipeDSLSpecNamespace::Bar::BarThingy < BaseThingy + end + + } + + it "bar_thingy works" do + recipe = converge { + bar_thingy 'blah' do; end + } + expect(recipe.logged_warnings).to eq '' + expect(BaseThingy.created_resource).to eq(RecipeDSLSpecNamespace::Bar::BarThingy) + end + end + + context "With a resource named NoNameThingy with resource_name nil" do + before(:context) { + + class NoNameThingy < BaseThingy + resource_name nil + end + + } + + it "no_name_thingy does not work" do + expect_converge { + thingy 'blah' do; end + }.to raise_error(NoMethodError) + end + end + + context "With a resource named AnotherNoNameThingy with resource_name :another_thingy_name" do + before(:context) { + + class AnotherNoNameThingy < BaseThingy + resource_name :another_thingy_name + end + + } + + it "another_no_name_thingy does not work" do + expect_converge { + another_no_name_thingy 'blah' do; end + }.to raise_error(NoMethodError) + end + + it "another_thingy_name works" do + recipe = converge { + another_thingy_name 'blah' do; end + } + expect(recipe.logged_warnings).to eq '' + expect(BaseThingy.created_resource).to eq(AnotherNoNameThingy) + end + end + + context "With a resource named AnotherNoNameThingy2 with resource_name :another_thingy_name2; resource_name :another_thingy_name3" do + before(:context) { + + class AnotherNoNameThingy2 < BaseThingy + resource_name :another_thingy_name2 + resource_name :another_thingy_name3 + end + + } + + it "another_no_name_thingy does not work" do + expect_converge { + another_no_name_thingy2 'blah' do; end + }.to raise_error(NoMethodError) + end + + it "another_thingy_name2 does not work" do + expect_converge { + another_thingy_name2 'blah' do; end + }.to raise_error(NoMethodError) + end + + it "yet_another_thingy_name3 works" do + recipe = converge { + another_thingy_name3 'blah' do; end + } + expect(recipe.logged_warnings).to eq '' + expect(BaseThingy.created_resource).to eq(AnotherNoNameThingy2) + end + end + + context "provides overriding resource_name" do + context "With a resource named AnotherNoNameThingy3 with provides :another_no_name_thingy3, os: 'blarghle'" do + before(:context) { + + class AnotherNoNameThingy3 < BaseThingy + provides :another_no_name_thingy3, os: 'blarghle' + end + + } + + it "and os = linux, another_no_name_thingy3 does not work" do + expect_converge { + # TODO this is an ugly way to test, make Cheffish expose node attrs + run_context.node.automatic[:os] = 'linux' + another_no_name_thingy3 'blah' do; end + }.to raise_error(Chef::Exceptions::NoSuchResourceType) + end + + it "and os = blarghle, another_no_name_thingy3 works" do + recipe = converge { + # TODO this is an ugly way to test, make Cheffish expose node attrs + run_context.node.automatic[:os] = 'blarghle' + another_no_name_thingy3 'blah' do; end + } + expect(recipe.logged_warnings).to eq '' + expect(BaseThingy.created_resource).to eq (AnotherNoNameThingy3) + end + end + + context "With a resource named AnotherNoNameThingy4 with two provides" do + before(:context) { + + class AnotherNoNameThingy4 < BaseThingy + provides :another_no_name_thingy4, os: 'blarghle' + provides :another_no_name_thingy4, platform_family: 'foo' + end + + } + + it "and os = linux, another_no_name_thingy4 does not work" do + expect_converge { + # TODO this is an ugly way to test, make Cheffish expose node attrs + run_context.node.automatic[:os] = 'linux' + another_no_name_thingy4 'blah' do; end + }.to raise_error(Chef::Exceptions::NoSuchResourceType) + end + + it "and os = blarghle, another_no_name_thingy4 works" do + recipe = converge { + # TODO this is an ugly way to test, make Cheffish expose node attrs + run_context.node.automatic[:os] = 'blarghle' + another_no_name_thingy4 'blah' do; end + } + expect(recipe.logged_warnings).to eq '' + expect(BaseThingy.created_resource).to eq (AnotherNoNameThingy4) + end + + it "and platform_family = foo, another_no_name_thingy4 works" do + recipe = converge { + # TODO this is an ugly way to test, make Cheffish expose node attrs + run_context.node.automatic[:platform_family] = 'foo' + another_no_name_thingy4 'blah' do; end + } + expect(recipe.logged_warnings).to eq '' + expect(BaseThingy.created_resource).to eq (AnotherNoNameThingy4) + end + end + + context "With a resource named AnotherNoNameThingy5, a different resource_name, and a provides with the original resource_name" do + before(:context) { + + class AnotherNoNameThingy5 < BaseThingy + resource_name :another_thingy_name_for_another_no_name_thingy5 + provides :another_no_name_thingy5, os: 'blarghle' + end + + } + + it "and os = linux, another_no_name_thingy5 does not work" do + expect_converge { + # this is an ugly way to test, make Cheffish expose node attrs + run_context.node.automatic[:os] = 'linux' + another_no_name_thingy5 'blah' do; end + }.to raise_error(Chef::Exceptions::NoSuchResourceType) + end + + it "and os = blarghle, another_no_name_thingy5 works" do + recipe = converge { + # this is an ugly way to test, make Cheffish expose node attrs + run_context.node.automatic[:os] = 'blarghle' + another_no_name_thingy5 'blah' do; end + } + expect(recipe.logged_warnings).to eq '' + expect(BaseThingy.created_resource).to eq (AnotherNoNameThingy5) + end + + it "the new resource name can be used in a recipe" do + recipe = converge { + another_thingy_name_for_another_no_name_thingy5 'blah' do; end + } + expect(recipe.logged_warnings).to eq '' + expect(BaseThingy.created_resource).to eq (AnotherNoNameThingy5) + end + end + + context "With a resource named AnotherNoNameThingy6, a provides with the original resource name, and a different resource_name" do + before(:context) { + + class AnotherNoNameThingy6 < BaseThingy + provides :another_no_name_thingy6, os: 'blarghle' + resource_name :another_thingy_name_for_another_no_name_thingy6 + end + + } + + it "and os = linux, another_no_name_thingy6 does not work" do + expect_converge { + # this is an ugly way to test, make Cheffish expose node attrs + run_context.node.automatic[:os] = 'linux' + another_no_name_thingy6 'blah' do; end + }.to raise_error(Chef::Exceptions::NoSuchResourceType) + end + + it "and os = blarghle, another_no_name_thingy6 works" do + recipe = converge { + # this is an ugly way to test, make Cheffish expose node attrs + run_context.node.automatic[:os] = 'blarghle' + another_no_name_thingy6 'blah' do; end + } + expect(recipe.logged_warnings).to eq '' + expect(BaseThingy.created_resource).to eq (AnotherNoNameThingy6) + end + + it "the new resource name can be used in a recipe" do + recipe = converge { + another_thingy_name_for_another_no_name_thingy6 'blah' do; end + } + expect(recipe.logged_warnings).to eq '' + expect(BaseThingy.created_resource).to eq (AnotherNoNameThingy6) + end + end + + context "With a resource named AnotherNoNameThingy7, a new resource_name, and provides with that new resource name" do + before(:context) { + + class AnotherNoNameThingy7 < BaseThingy + resource_name :another_thingy_name_for_another_no_name_thingy7 + provides :another_thingy_name_for_another_no_name_thingy7, os: 'blarghle' + end + + } + + it "and os = linux, another_thingy_name_for_another_no_name_thingy7 does not work" do + expect_converge { + # this is an ugly way to test, make Cheffish expose node attrs + run_context.node.automatic[:os] = 'linux' + another_thingy_name_for_another_no_name_thingy7 'blah' do; end + }.to raise_error(Chef::Exceptions::NoSuchResourceType) + end + + it "and os = blarghle, another_thingy_name_for_another_no_name_thingy7 works" do + recipe = converge { + # this is an ugly way to test, make Cheffish expose node attrs + run_context.node.automatic[:os] = 'blarghle' + another_thingy_name_for_another_no_name_thingy7 'blah' do; end + } + expect(recipe.logged_warnings).to eq '' + expect(BaseThingy.created_resource).to eq (AnotherNoNameThingy7) + end + + it "the old resource name does not work" do + expect_converge { + # this is an ugly way to test, make Cheffish expose node attrs + run_context.node.automatic[:os] = 'linux' + another_no_name_thingy_7 'blah' do; end + }.to raise_error(NoMethodError) + end + end + + # opposite order from the previous test (provides, then resource_name) + context "With a resource named AnotherNoNameThingy8, a provides with a new resource name, and resource_name with that new resource name" do + before(:context) { + + class AnotherNoNameThingy8 < BaseThingy + provides :another_thingy_name_for_another_no_name_thingy8, os: 'blarghle' + resource_name :another_thingy_name_for_another_no_name_thingy8 + end + + } + + it "and os = linux, another_thingy_name_for_another_no_name_thingy8 does not work" do + expect_converge { + # this is an ugly way to test, make Cheffish expose node attrs + run_context.node.automatic[:os] = 'linux' + another_thingy_name_for_another_no_name_thingy8 'blah' do; end + }.to raise_error(Chef::Exceptions::NoSuchResourceType) + end + + it "and os = blarghle, another_thingy_name_for_another_no_name_thingy8 works" do + recipe = converge { + # this is an ugly way to test, make Cheffish expose node attrs + run_context.node.automatic[:os] = 'blarghle' + another_thingy_name_for_another_no_name_thingy8 'blah' do; end + } + expect(recipe.logged_warnings).to eq '' + expect(BaseThingy.created_resource).to eq (AnotherNoNameThingy8) + end + + it "the old resource name does not work" do + expect_converge { + # this is an ugly way to test, make Cheffish expose node attrs + run_context.node.automatic[:os] = 'linux' + another_thingy_name8 'blah' do; end + }.to raise_error(NoMethodError) + end + end + + context "With a resource TwoClassesOneDsl" do + let(:class_name) { "TwoClassesOneDsl#{Namer.current_index}" } + let(:dsl_method) { :"two_classes_one_dsl#{Namer.current_index}" } + + before { + eval <<-EOM, nil, __FILE__, __LINE__+1 + class #{class_name} < BaseThingy + end + EOM + } + context "and resource BlahModule::TwoClassesOneDsl" do + before { + eval <<-EOM, nil, __FILE__, __LINE__+1 + module BlahModule + class #{class_name} < BaseThingy + end + end + EOM + } + it "two_classes_one_dsl resolves to BlahModule::TwoClassesOneDsl (last declared)" do + dsl_method = self.dsl_method + recipe = converge { + instance_eval("#{dsl_method} 'blah' do; end") + } + expect(recipe.logged_warnings).to eq '' + expect(BaseThingy.created_resource).to eq eval("BlahModule::#{class_name}") + end + it "resource_matching_short_name returns BlahModule::TwoClassesOneDsl" do + expect(Chef::Resource.resource_matching_short_name(dsl_method)).to eq eval("BlahModule::#{class_name}") + end + end + context "and resource BlahModule::TwoClassesOneDsl with resource_name nil" do + before { + eval <<-EOM, nil, __FILE__, __LINE__+1 + module BlahModule + class BlahModule::#{class_name} < BaseThingy + resource_name nil + end + end + EOM + } + it "two_classes_one_dsl resolves to ::TwoClassesOneDsl" do + dsl_method = self.dsl_method + recipe = converge { + instance_eval("#{dsl_method} 'blah' do; end") + } + expect(recipe.logged_warnings).to eq '' + expect(BaseThingy.created_resource).to eq eval("::#{class_name}") + end + it "resource_matching_short_name returns ::TwoClassesOneDsl" do + expect(Chef::Resource.resource_matching_short_name(dsl_method)).to eq eval("::#{class_name}") + end + end + context "and resource BlahModule::TwoClassesOneDsl with resource_name :argh" do + before { + eval <<-EOM, nil, __FILE__, __LINE__+1 + module BlahModule + class BlahModule::#{class_name} < BaseThingy + resource_name :argh + end + end + EOM + } + it "two_classes_one_dsl resolves to ::TwoClassesOneDsl" do + dsl_method = self.dsl_method + recipe = converge { + instance_eval("#{dsl_method} 'blah' do; end") + } + expect(recipe.logged_warnings).to eq '' + expect(BaseThingy.created_resource).to eq eval("::#{class_name}") + end + it "resource_matching_short_name returns ::TwoClassesOneDsl" do + expect(Chef::Resource.resource_matching_short_name(dsl_method)).to eq eval("::#{class_name}") + end + end + context "and resource BlahModule::TwoClassesOneDsl with provides :two_classes_one_dsl, os: 'blarghle'" do + before { + eval <<-EOM, nil, __FILE__, __LINE__+1 + module BlahModule + class BlahModule::#{class_name} < BaseThingy + provides #{dsl_method.inspect}, os: 'blarghle' + end + end + EOM + } + + it "and os = blarghle, two_classes_one_dsl resolves to BlahModule::TwoClassesOneDsl" do + dsl_method = self.dsl_method + recipe = converge { + # this is an ugly way to test, make Cheffish expose node attrs + run_context.node.automatic[:os] = 'blarghle' + instance_eval("#{dsl_method} 'blah' do; end") + } + expect(recipe.logged_warnings).to eq '' + expect(BaseThingy.created_resource).to eq eval("BlahModule::#{class_name}") + end + + it "and os = linux, two_classes_one_dsl resolves to ::TwoClassesOneDsl" do + dsl_method = self.dsl_method + recipe = converge { + # this is an ugly way to test, make Cheffish expose node attrs + run_context.node.automatic[:os] = 'linux' + instance_eval("#{dsl_method} 'blah' do; end") + } + expect(recipe.logged_warnings).to eq '' + expect(BaseThingy.created_resource).to eq eval("::#{class_name}") + end + end + end + end + end + + context "provides" do + context "when MySupplier provides :hemlock" do + before(:context) { + + class RecipeDSLSpecNamespace::MySupplier < BaseThingy + resource_name :hemlock + end + + } + + it "my_supplier does not work in a recipe" do + expect_converge { + my_supplier 'blah' do; end + }.to raise_error(NoMethodError) + end + + it "hemlock works in a recipe" do + expect_recipe { + hemlock 'blah' do; end + }.to emit_no_warnings_or_errors + expect(BaseThingy.created_resource).to eq RecipeDSLSpecNamespace::MySupplier + end + end + + context "when Thingy3 has resource_name :thingy3" do + before(:context) { + + class RecipeDSLSpecNamespace::Thingy3 < BaseThingy + resource_name :thingy3 + end + + } + + it "thingy3 works in a recipe" do + expect_recipe { + thingy3 'blah' do; end + }.to emit_no_warnings_or_errors + expect(BaseThingy.created_resource).to eq RecipeDSLSpecNamespace::Thingy3 + end + + context "and Thingy4 has resource_name :thingy3" do + before(:context) { + + class RecipeDSLSpecNamespace::Thingy4 < BaseThingy + resource_name :thingy3 + end + + } + + it "thingy3 works in a recipe and yields Foo::Thingy4 (the explicit one)" do + recipe = converge { + thingy3 'blah' do; end + } + expect(BaseThingy.created_resource).to eq RecipeDSLSpecNamespace::Thingy4 + end + + it "thingy4 does not work in a recipe" do + expect_converge { + thingy4 'blah' do; end + }.to raise_error(NoMethodError) + end + + it "resource_matching_short_name returns Thingy4" do + expect(Chef::Resource.resource_matching_short_name(:thingy3)).to eq RecipeDSLSpecNamespace::Thingy4 + end + end + end + + context "when Thingy5 has resource_name :thingy5" do + before(:context) { + + class RecipeDSLSpecNamespace::Thingy5 < BaseThingy + resource_name :thingy5 + end + + } + + it "thingy5 works in a recipe" do + expect_recipe { + thingy5 'blah' do; end + }.to emit_no_warnings_or_errors + expect(BaseThingy.created_resource).to eq RecipeDSLSpecNamespace::Thingy5 + end + + context "and Thingy6 provides :thingy5" do + before(:context) { + + class RecipeDSLSpecNamespace::Thingy6 < BaseThingy + provides :thingy5 + end + + } + + it "thingy6 works in a recipe and yields Thingy6" do + recipe = converge { + thingy6 'blah' do; end + } + expect(BaseThingy.created_resource).to eq RecipeDSLSpecNamespace::Thingy6 + end + + it "thingy5 works in a recipe and yields Foo::Thingy6 (the later one)" do + recipe = converge { + thingy5 'blah' do; end + } + expect(BaseThingy.created_resource).to eq RecipeDSLSpecNamespace::Thingy6 + end + + it "resource_matching_short_name returns Thingy5" do + expect(Chef::Resource.resource_matching_short_name(:thingy5)).to eq RecipeDSLSpecNamespace::Thingy5 + end + end + end + + context "when Thingy7 provides :thingy8" do + before(:context) { + + class RecipeDSLSpecNamespace::Thingy7 < BaseThingy + provides :thingy8 + end + + } + + context "and Thingy8 has resource_name :thingy8" do + before(:context) { + + class RecipeDSLSpecNamespace::Thingy8 < BaseThingy + resource_name :thingy8 + end + + } + + it "thingy7 works in a recipe and yields Thingy7" do + recipe = converge { + thingy7 'blah' do; end + } + expect(BaseThingy.created_resource).to eq RecipeDSLSpecNamespace::Thingy7 + end + + it "thingy8 works in a recipe and yields Thingy8 (the later one)" do + recipe = converge { + thingy8 'blah' do; end + } + expect(BaseThingy.created_resource).to eq RecipeDSLSpecNamespace::Thingy8 + end + + it "resource_matching_short_name returns Thingy8" do + expect(Chef::Resource.resource_matching_short_name(:thingy8)).to eq RecipeDSLSpecNamespace::Thingy8 + end + end + end + + context "when Thingy5 provides :thingy5, :twizzle and :twizzle2" do + before(:context) { + + class RecipeDSLSpecNamespace::Thingy5 < BaseThingy + resource_name :thingy5 + provides :twizzle + provides :twizzle2 + end + + } + + it "thingy5 works in a recipe and yields Thingy5" do + expect_recipe { + thingy5 'blah' do; end + }.to emit_no_warnings_or_errors + expect(BaseThingy.created_resource).to eq RecipeDSLSpecNamespace::Thingy5 + end + + it "twizzle works in a recipe and yields Thingy5" do + expect_recipe { + twizzle 'blah' do; end + }.to emit_no_warnings_or_errors + expect(BaseThingy.created_resource).to eq RecipeDSLSpecNamespace::Thingy5 + end + + it "twizzle2 works in a recipe and yields Thingy5" do + expect_recipe { + twizzle2 'blah' do; end + }.to emit_no_warnings_or_errors + expect(BaseThingy.created_resource).to eq RecipeDSLSpecNamespace::Thingy5 + end + end + + context "With platform-specific resources 'my_super_thingy_foo' and 'my_super_thingy_bar'" do + before(:context) { + class MySuperThingyFoo < BaseThingy + resource_name :my_super_thingy_foo + provides :my_super_thingy, platform: 'foo' + end + + class MySuperThingyBar < BaseThingy + resource_name :my_super_thingy_bar + provides :my_super_thingy, platform: 'bar' + end + } + + it "A run with platform 'foo' uses MySuperThingyFoo" do + r = Cheffish::ChefRun.new(chef_config) + r.client.run_context.node.automatic['platform'] = 'foo' + r.compile_recipe { + my_super_thingy 'blah' do; end + } + r.converge + expect(r).to emit_no_warnings_or_errors + expect(BaseThingy.created_resource).to eq MySuperThingyFoo + end + + it "A run with platform 'bar' uses MySuperThingyBar" do + r = Cheffish::ChefRun.new(chef_config) + r.client.run_context.node.automatic['platform'] = 'bar' + r.compile_recipe { + my_super_thingy 'blah' do; end + } + r.converge + expect(r).to emit_no_warnings_or_errors + expect(BaseThingy.created_resource).to eq MySuperThingyBar + end + + it "A run with platform 'x' reports that my_super_thingy is not supported" do + r = Cheffish::ChefRun.new(chef_config) + r.client.run_context.node.automatic['platform'] = 'x' + expect { + r.compile_recipe { + my_super_thingy 'blah' do; end + } + }.to raise_error(Chef::Exceptions::NoSuchResourceType) + end + end + end + end +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index fb284c721b..dcf244c3cc 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -54,6 +54,9 @@ Dir['lib/chef/knife/**/*.rb']. map {|f| f.gsub(%r[\.rb$], '') }. each {|f| require f } +require 'chef/resource_resolver' +require 'chef/provider_resolver' + require 'chef/mixins' require 'chef/dsl' require 'chef/application' @@ -112,7 +115,8 @@ RSpec.configure do |config| config.filter_run_excluding :volatile_on_solaris => true if solaris? config.filter_run_excluding :volatile_from_verify => false - # Add jruby filters here + config.filter_run_excluding :skip_appveyor => true if ENV["APPVEYOR"] + config.filter_run_excluding :windows_only => true unless windows? config.filter_run_excluding :not_supported_on_mac_osx_106 => true if mac_osx_106? config.filter_run_excluding :not_supported_on_mac_osx=> true if mac_osx? @@ -126,6 +130,7 @@ RSpec.configure do |config| config.filter_run_excluding :windows_powershell_dsc_only => true unless windows_powershell_dsc? config.filter_run_excluding :windows_powershell_no_dsc_only => true unless ! windows_powershell_dsc? config.filter_run_excluding :windows_domain_joined_only => true unless windows_domain_joined? + config.filter_run_excluding :windows_not_domain_joined_only => true if windows_domain_joined? config.filter_run_excluding :solaris_only => true unless solaris? config.filter_run_excluding :system_windows_service_gem_only => true unless system_windows_service_gem? config.filter_run_excluding :unix_only => true unless unix? @@ -146,7 +151,7 @@ RSpec.configure do |config| config.filter_run_excluding :aes_256_gcm_only => true unless aes_256_gcm? config.filter_run_excluding :broken => true - running_platform_arch = `uname -m`.strip + running_platform_arch = `uname -m`.strip unless windows? config.filter_run_excluding :arch => lambda {|target_arch| running_platform_arch != target_arch diff --git a/spec/support/key_helpers.rb b/spec/support/key_helpers.rb new file mode 100644 index 0000000000..076f709380 --- /dev/null +++ b/spec/support/key_helpers.rb @@ -0,0 +1,104 @@ +# +# Author:: Tyler Cloke (<tyler@chef.io>) +# Copyright:: Copyright (c) 2015 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' + +shared_examples_for "a knife key command" do + let(:stderr) { StringIO.new } + let(:command) do + c = described_class.new([]) + c.ui.config[:disable_editing] = true + allow(c.ui).to receive(:stderr).and_return(stderr) + allow(c.ui).to receive(:stdout).and_return(stderr) + allow(c).to receive(:show_usage) + c + end + + context "before apply_params! is called" do + context "when apply_params! is called with invalid args (missing actor)" do + let(:params) { [] } + it "shows the usage" do + expect(command).to receive(:show_usage) + expect { command.apply_params!(params) }.to exit_with_code(1) + end + + it "outputs the proper error" do + expect { command.apply_params!(params) }.to exit_with_code(1) + expect(stderr.string).to include(command.actor_missing_error) + end + + it "exits 1" do + expect { command.apply_params!(params) }.to exit_with_code(1) + end + end + end # before apply_params! is called + + context "after apply_params! is called with valid args" do + before do + command.apply_params!(params) + end + + it "properly defines the actor" do + expect(command.actor).to eq("charmander") + end + end # after apply_params! is called with valid args + + context "when the command is run" do + before do + allow(command).to receive(:service_object).and_return(service_object) + allow(command).to receive(:name_args).and_return(["charmander"]) + end + + context "when the command is successful" do + before do + expect(service_object).to receive(:run) + end + end + end +end # a knife key command + +shared_examples_for "a knife key command with a keyname as the second arg" do + let(:stderr) { StringIO.new } + let(:command) do + c = described_class.new([]) + c.ui.config[:disable_editing] = true + allow(c.ui).to receive(:stderr).and_return(stderr) + allow(c.ui).to receive(:stdout).and_return(stderr) + allow(c).to receive(:show_usage) + c + end + + context "before apply_params! is called" do + context "when apply_params! is called with invalid args (missing keyname)" do + let(:params) { ["charmander"] } + it "shows the usage" do + expect(command).to receive(:show_usage) + expect { command.apply_params!(params) }.to exit_with_code(1) + end + + it "outputs the proper error" do + expect { command.apply_params!(params) }.to exit_with_code(1) + expect(stderr.string).to include(command.keyname_missing_error) + end + + it "exits 1" do + expect { command.apply_params!(params) }.to exit_with_code(1) + end + end + end # before apply_params! is called +end diff --git a/spec/support/lib/chef/provider/openldap_includer.rb b/spec/support/lib/chef/provider/openldap_includer.rb new file mode 100644 index 0000000000..afb0c7cf01 --- /dev/null +++ b/spec/support/lib/chef/provider/openldap_includer.rb @@ -0,0 +1,29 @@ +# +# Author:: Adam Jacob (<adam@opscode.com>) +# Copyright:: Copyright (c) 2008 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 Provider + class OpenldapIncluder < Chef::Provider::LWRPBase + provides :openldap_includer + + def action_run + include_recipe "openldap::default" + end + end + end +end diff --git a/spec/support/lib/chef/resource/cat.rb b/spec/support/lib/chef/resource/cat.rb index ecca50cb53..efc78aa59c 100644 --- a/spec/support/lib/chef/resource/cat.rb +++ b/spec/support/lib/chef/resource/cat.rb @@ -23,7 +23,6 @@ class Chef attr_accessor :action def initialize(name, run_context=nil) - @resource_name = :cat super @action = "sell" end diff --git a/spec/support/lib/chef/resource/one_two_three_four.rb b/spec/support/lib/chef/resource/one_two_three_four.rb index 296d2cd970..8f273a0cda 100644 --- a/spec/support/lib/chef/resource/one_two_three_four.rb +++ b/spec/support/lib/chef/resource/one_two_three_four.rb @@ -19,12 +19,8 @@ class Chef class Resource class OneTwoThreeFour < Chef::Resource - attr_reader :i_can_count - def initialize(name, run_context) - @resource_name = :one_two_three_four - super - end + attr_reader :i_can_count def i_can_count(tf) @i_can_count = tf diff --git a/spec/support/lib/chef/resource/openldap_includer.rb b/spec/support/lib/chef/resource/openldap_includer.rb new file mode 100644 index 0000000000..6f443b4c7c --- /dev/null +++ b/spec/support/lib/chef/resource/openldap_includer.rb @@ -0,0 +1,27 @@ +# +# Author:: Adam Jacob (<adam@opscode.com>) +# Copyright:: Copyright (c) 2008, 2010 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 Resource + class OpenldapIncluder < Chef::Resource::LWRPBase + allowed_actions :run + default_action :run + end + end +end diff --git a/spec/support/lib/chef/resource/with_state.rb b/spec/support/lib/chef/resource/with_state.rb index 226de0a6d2..773ae7ddb8 100644 --- a/spec/support/lib/chef/resource/with_state.rb +++ b/spec/support/lib/chef/resource/with_state.rb @@ -23,15 +23,6 @@ class Chef class Resource class WithState < Chef::Resource attr_accessor :state - - def initialize(name, run_context=nil) - @resource_name = :with_state - super - end - - def state - @state - end end end end diff --git a/spec/support/lib/chef/resource/zen_follower.rb b/spec/support/lib/chef/resource/zen_follower.rb index ddc289e48d..155e6ae729 100644 --- a/spec/support/lib/chef/resource/zen_follower.rb +++ b/spec/support/lib/chef/resource/zen_follower.rb @@ -24,11 +24,6 @@ class Chef provides :follower, platform: "zen" - def initialize(name, run_context=nil) - @resource_name = :zen_follower - super - end - def master(arg=nil) if !arg.nil? @master = arg diff --git a/spec/support/lib/chef/resource/zen_master.rb b/spec/support/lib/chef/resource/zen_master.rb index d47d174e28..4106549d79 100644 --- a/spec/support/lib/chef/resource/zen_master.rb +++ b/spec/support/lib/chef/resource/zen_master.rb @@ -22,13 +22,10 @@ require 'chef/json_compat' class Chef class Resource class ZenMaster < Chef::Resource + allowed_actions :win, :score + attr_reader :peace - def initialize(name, run_context=nil) - @resource_name = :zen_master - super - allowed_actions << :win << :score - end def peace(tf) @peace = tf diff --git a/spec/support/mock/platform.rb b/spec/support/mock/platform.rb index ab2c19baff..7eae82fe7d 100644 --- a/spec/support/mock/platform.rb +++ b/spec/support/mock/platform.rb @@ -6,7 +6,7 @@ # testing code that mixes in platform specific modules like +Chef::Mixin::Securable+ # or +Chef::FileAccessControl+ def platform_mock(platform = :unix, &block) - allow(Chef::Platform).to receive(:windows?).and_return(platform == :windows ? true : false) + allow(ChefConfig).to receive(:windows?).and_return(platform == :windows ? true : false) ENV['SYSTEMDRIVE'] = (platform == :windows ? 'C:' : nil) if platform == :windows diff --git a/spec/support/shared/context/client.rb b/spec/support/shared/context/client.rb new file mode 100644 index 0000000000..eb537e9889 --- /dev/null +++ b/spec/support/shared/context/client.rb @@ -0,0 +1,277 @@ + +require 'spec_helper' + +# Stubs a basic client object +shared_context "client" do + let(:fqdn) { "hostname.example.org" } + let(:hostname) { "hostname" } + let(:machinename) { "machinename.example.org" } + let(:platform) { "example-platform" } + let(:platform_version) { "example-platform-1.0" } + + let(:ohai_data) do + { + :fqdn => fqdn, + :hostname => hostname, + :machinename => machinename, + :platform => platform, + :platform_version => platform_version + } + end + + let(:ohai_system) do + ohai = instance_double("Ohai::System", :all_plugins => true, :data => ohai_data) + allow(ohai).to receive(:[]) do |k| + ohai_data[k] + end + ohai + 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::Config[:event_loggers] = [] + 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 + #Chef::Config[:node_name] = fqdn + allow(Ohai::System).to receive(:new).and_return(ohai_system) + end +end + +# Stubs a client for a client run. +# Requires a client object be defined in the scope of this included context. +# e.g.: +# describe "some functionality" do +# include_context "client" +# include_context "a client run" +# ... +# end +shared_context "a client run" do + let(:stdout) { StringIO.new } + let(:stderr) { StringIO.new } + + let(:api_client_exists?) { false } + let(:enable_fork) { false } + + let(:http_cookbook_sync) { double("Chef::REST (cookbook sync)") } + let(:http_node_load) { double("Chef::REST (node)") } + let(:http_node_save) { double("Chef::REST (node save)") } + + let(:runner) { instance_double("Chef::Runner") } + let(:audit_runner) { instance_double("Chef::Audit::Runner", :failed? => 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. + allow(File).to receive(:exists?).and_call_original + expect(File).to receive(:exists?). + with(Chef::Config[:client_key]). + exactly(:once). + and_return(api_client_exists?) + + unless api_client_exists? + # Client.register will register with the validation client name. + expect_any_instance_of(Chef::ApiClient::Registration).to receive(:run) + end + 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. + expect(Chef::REST).to receive(:new). + with(Chef::Config[:chef_server_url], fqdn, Chef::Config[:client_key]). + exactly(:once). + and_return(http_node_load) + + # --Client#build_node + # looks up the node, which we will return, then later saves it. + expect(Chef::Node).to 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.) + expect_any_instance_of(Chef::ResourceReporter).to receive(:node_load_completed) + end + + def stub_for_sync_cookbooks + # --Client#setup_run_context + # ---Client#sync_cookbooks -- downloads the list of cookbooks to sync + # + expect_any_instance_of(Chef::CookbookSynchronizer).to receive(:sync_cookbooks) + expect(Chef::REST).to receive(:new).with(Chef::Config[:chef_server_url]).and_return(http_cookbook_sync) + expect(http_cookbook_sync).to receive(:post). + with("environments/_default/cookbook_versions", {:run_list => []}). + and_return({}) + end + + def stub_for_converge + # define me + end + + def stub_for_audit + # define me + end + + def stub_for_node_save + # define me + end + + def stub_for_run + # define me + end + + before do + Chef::Config[:client_fork] = enable_fork + Chef::Config[:cache_path] = windows? ? 'C:\chef' : '/var/chef' + Chef::Config[:why_run] = false + Chef::Config[:audit_mode] = :enabled + + 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_audit + stub_for_node_save + + expect_any_instance_of(Chef::RunLock).to receive(:acquire) + expect_any_instance_of(Chef::RunLock).to receive(:save_pid) + expect_any_instance_of(Chef::RunLock).to receive(:release) + + # Post conditions: check that node has been filled in correctly + expect(client).to receive(:run_started) + + stub_for_run + end +end + +shared_context "converge completed" do + def stub_for_converge + # --Client#converge + expect(Chef::Runner).to receive(:new).and_return(runner) + expect(runner).to receive(:converge).and_return(true) + end + + def stub_for_node_save + allow(node).to receive(:data_for_save).and_return(node.for_json) + + # --Client#save_updated_node + expect(Chef::REST).to receive(:new).with(Chef::Config[:chef_server_url], fqdn, Chef::Config[:client_key], validate_utf8: false).and_return(http_node_save) + expect(http_node_save).to receive(:put_rest).with("nodes/#{fqdn}", node.for_json).and_return(true) + end +end + +shared_context "converge failed" do + let(:converge_error) do + err = Chef::Exceptions::UnsupportedAction.new("Action unsupported") + err.set_backtrace([ "/path/recipe.rb:15", "/path/recipe.rb:12" ]) + err + end + + def stub_for_converge + expect(Chef::Runner).to receive(:new).and_return(runner) + expect(runner).to receive(:converge).and_raise(converge_error) + end + + def stub_for_node_save + expect(client).to_not receive(:save_updated_node) + end +end + +shared_context "audit phase completed" do + def stub_for_audit + # -- Client#run_audits + expect(Chef::Audit::Runner).to receive(:new).and_return(audit_runner) + expect(audit_runner).to receive(:run).and_return(true) + expect(client.events).to receive(:audit_phase_complete) + end +end + +shared_context "audit phase failed with error" do + let(:audit_error) do + err = RuntimeError.new("Unexpected audit error") + err.set_backtrace([ "/path/recipe.rb:57", "/path/recipe.rb:55" ]) + err + end + + def stub_for_audit + expect(Chef::Audit::Runner).to receive(:new).and_return(audit_runner) + expect(Chef::Audit::Logger).to receive(:read_buffer).and_return("Audit mode output!") + expect(audit_runner).to receive(:run).and_raise(audit_error) + expect(client.events).to receive(:audit_phase_failed).with(audit_error, "Audit mode output!") + end +end + +shared_context "audit phase completed with failed controls" do + let(:audit_runner) { instance_double("Chef::Audit::Runner", :failed? => true, + :num_failed => 1, :num_total => 3) } + + let(:audit_error) do + err = Chef::Exceptions::AuditsFailed.new(audit_runner.num_failed, audit_runner.num_total) + err.set_backtrace([ "/path/recipe.rb:108", "/path/recipe.rb:103" ]) + err + end + + def stub_for_audit + expect(Chef::Audit::Runner).to receive(:new).and_return(audit_runner) + expect(Chef::Audit::Logger).to receive(:read_buffer).and_return("Audit mode output!") + expect(audit_runner).to receive(:run) + expect(Chef::Exceptions::AuditsFailed).to receive(:new).with( + audit_runner.num_failed, audit_runner.num_total + ).and_return(audit_error) + expect(client.events).to receive(:audit_phase_failed).with(audit_error, "Audit mode output!") + end +end + +shared_context "run completed" do + def stub_for_run + expect(client).to receive(:run_completed_successfully) + + # --ResourceReporter#run_completed + # updates the server with the resource history + # (has its own tests, so stubbing it here.) + expect_any_instance_of(Chef::ResourceReporter).to receive(:run_completed) + # --AuditReporter#run_completed + # posts the audit data to server. + # (has its own tests, so stubbing it here.) + expect_any_instance_of(Chef::Audit::AuditReporter).to receive(:run_completed) + end +end + +shared_context "run failed" do + def stub_for_run + expect(client).to receive(:run_failed) + + # --ResourceReporter#run_completed + # updates the server with the resource history + # (has its own tests, so stubbing it here.) + expect_any_instance_of(Chef::ResourceReporter).to receive(:run_failed) + # --AuditReporter#run_completed + # posts the audit data to server. + # (has its own tests, so stubbing it here.) + expect_any_instance_of(Chef::Audit::AuditReporter).to receive(:run_failed) + end + + before do + expect(Chef::Application).to receive(:debug_stacktrace).with an_instance_of(Chef::Exceptions::RunFailedWrappingError) + end +end diff --git a/spec/support/shared/examples/client.rb b/spec/support/shared/examples/client.rb new file mode 100644 index 0000000000..330cb40ac6 --- /dev/null +++ b/spec/support/shared/examples/client.rb @@ -0,0 +1,53 @@ + +require 'spec_helper' +require 'spec/support/shared/context/client' + +# requires platform and platform_version be defined +shared_examples "a completed run" do + include_context "run completed" + + it "runs ohai, sets up authentication, loads node state, synchronizes policy, converges, and runs audits" do + # This is what we're testing. + expect(client.run).to be true + + # fork is stubbed, so we can see the outcome of the run + expect(node.automatic_attrs[:platform]).to eq(platform) + expect(node.automatic_attrs[:platform_version]).to eq(platform_version) + end +end + +shared_examples "a completed run with audit failure" do + include_context "run completed" + + before do + expect(Chef::Application).to receive(:debug_stacktrace).with an_instance_of(Chef::Exceptions::RunFailedWrappingError) + end + + it "converges, runs audits, saves the node and raises the error in a wrapping error" do + expect{ client.run }.to raise_error(Chef::Exceptions::RunFailedWrappingError) do |error| + expect(error.wrapped_errors.size).to eq(run_errors.size) + run_errors.each do |run_error| + expect(error.wrapped_errors).to include(run_error) + expect(error.backtrace).to include(*run_error.backtrace) + end + end + + # fork is stubbed, so we can see the outcome of the run + expect(node.automatic_attrs[:platform]).to eq(platform) + expect(node.automatic_attrs[:platform_version]).to eq(platform_version) + end +end + +shared_examples "a failed run" do + include_context "run failed" + + it "skips node save and raises the error in a wrapping error" do + expect{ client.run }.to raise_error(Chef::Exceptions::RunFailedWrappingError) do |error| + expect(error.wrapped_errors.size).to eq(run_errors.size) + run_errors.each do |run_error| + expect(error.wrapped_errors).to include(run_error) + expect(error.backtrace).to include(*run_error.backtrace) + end + end + end +end diff --git a/spec/support/shared/functional/file_resource.rb b/spec/support/shared/functional/file_resource.rb index 4f8e2f5b71..3ce3c9c94e 100644 --- a/spec/support/shared/functional/file_resource.rb +++ b/spec/support/shared/functional/file_resource.rb @@ -592,10 +592,6 @@ shared_examples_for "a configured file resource" do File.open(path, "wb") { |f| f.write(wrong_content) } end - it "updates the source file content" do - skip - end - it "marks the resource as updated" do resource.run_action(:create) expect(resource).to be_updated_by_last_action diff --git a/spec/support/shared/functional/securable_resource.rb b/spec/support/shared/functional/securable_resource.rb index e016bb685d..b3c32356aa 100644 --- a/spec/support/shared/functional/securable_resource.rb +++ b/spec/support/shared/functional/securable_resource.rb @@ -163,9 +163,6 @@ shared_examples_for "a securable resource with existing target" do let(:desired_gid) { 1337 } let(:expected_gid) { 1337 } - skip "should set an owner (Rerun specs under root)", :requires_unprivileged_user => true - skip "should set a group (Rerun specs under root)", :requires_unprivileged_user => true - describe "when setting the owner", :requires_root do before do resource.owner expected_user_name @@ -205,11 +202,6 @@ shared_examples_for "a securable resource with existing target" do resource.run_action(:create) end - it "should set permissions as specified" do - pending("Linux does not support lchmod") - expect{ File.lstat(path).mode & 007777 }.to eq(@mode_string.oct & 007777) - end - it "is marked as updated only if changes are made" do expect(resource.updated_by_last_action?).to eq(expect_updated?) end @@ -222,15 +214,28 @@ shared_examples_for "a securable resource with existing target" do resource.run_action(:create) end - it "should set permissions in numeric form as a ruby-interpreted octal" do - pending('Linux does not support lchmod') - expect{ File.lstat(path).mode & 007777 }.to eq(@mode_integer & 007777) - end - it "is marked as updated only if changes are made" do expect(resource.updated_by_last_action?).to eq(expect_updated?) end end + + describe "when setting the suid bit", :requires_root do + before do + @suid_mode = 04776 + resource.mode @suid_mode + resource.run_action(:create) + end + + it "should set the suid bit" do + expect(File.lstat(path).mode & 007777).to eq(@suid_mode & 007777) + end + + it "should retain the suid bit when updating the user" do + resource.user 1338 + resource.run_action(:create) + expect(File.lstat(path).mode & 007777).to eq(@suid_mode & 007777) + end + end end context "on Windows", :windows_only do @@ -288,17 +293,13 @@ shared_examples_for "a securable resource without existing target" do include_context "diff disabled" - context "on Unix", :unix_only do - skip "if we need any securable resource tests on Unix without existing target resource." - end - context "on Windows", :windows_only do include_context "use Windows permissions" - it "sets owner to Administrators on create if owner is not specified" do + it "leaves owner as system default on create if owner is not specified" do expect(File.exist?(path)).to eq(false) resource.run_action(:create) - expect(descriptor.owner).to eq(SID.Administrators) + expect(descriptor.owner).to eq(SID.default_security_object_owner) end it "sets owner when owner is specified" do @@ -318,22 +319,24 @@ shared_examples_for "a securable resource without existing target" do end it "leaves owner alone if owner is not specified and resource already exists" do - # Set owner to Guest so it's not the same as the current user (which is the default on create) - resource.owner 'Guest' + arbitrary_non_default_owner = SID.Guest + expect(arbitrary_non_default_owner).not_to eq(SID.default_security_object_owner) + + resource.owner 'Guest' # Change to arbitrary_non_default_owner once issue #1508 is fixed resource.run_action(:create) - expect(descriptor.owner).to eq(SID.Guest) + expect(descriptor.owner).to eq(arbitrary_non_default_owner) new_resource = create_resource expect(new_resource.owner).to eq(nil) new_resource.run_action(:create) - expect(descriptor.owner).to eq(SID.Guest) + expect(descriptor.owner).to eq(arbitrary_non_default_owner) end - it "sets group to None on create if group is not specified" do + it "leaves group as system default on create if group is not specified" do expect(resource.group).to eq(nil) expect(File.exist?(path)).to eq(false) resource.run_action(:create) - expect(descriptor.group).to eq(SID.None) + expect(descriptor.group).to eq(SID.default_security_object_group) end it "sets group when group is specified" do @@ -346,23 +349,18 @@ shared_examples_for "a securable resource without existing target" do expect { resource.group 'Lance "The Nose" Glindenberry III' }.to raise_error(Chef::Exceptions::ValidationFailed) end - it "sets group when group is specified with a \\" do - pending("Need to find a group containing a backslash that is on most peoples' machines") - resource.group "#{ENV['COMPUTERNAME']}\\Administrators" - resource.run_action(:create) - expect{ descriptor.group }.to eq(SID.Everyone) - end - it "leaves group alone if group is not specified and resource already exists" do - # Set group to Everyone so it's not the default (None) - resource.group 'Everyone' + arbitrary_non_default_group = SID.Everyone + expect(arbitrary_non_default_group).not_to eq(SID.default_security_object_group) + + resource.group 'Everyone' # Change to arbitrary_non_default_group once issue #1508 is fixed resource.run_action(:create) - expect(descriptor.group).to eq(SID.Everyone) + expect(descriptor.group).to eq(arbitrary_non_default_group) new_resource = create_resource expect(new_resource.group).to eq(nil) new_resource.run_action(:create) - expect(descriptor.group).to eq(SID.Everyone) + expect(descriptor.group).to eq(arbitrary_non_default_group) end describe "with rights and deny_rights attributes" do diff --git a/spec/support/shared/functional/securable_resource_with_reporting.rb b/spec/support/shared/functional/securable_resource_with_reporting.rb index 37fc538801..3176ebba0d 100644 --- a/spec/support/shared/functional/securable_resource_with_reporting.rb +++ b/spec/support/shared/functional/securable_resource_with_reporting.rb @@ -279,14 +279,14 @@ shared_examples_for "a securable resource with reporting" do end it "has empty values for file metadata in 'current_resource'" do - pending "windows reporting not yet fully supported" + skip "windows reporting not yet fully supported" expect(current_resource.owner).to be_nil expect(current_resource.expanded_rights).to be_nil end context "and no security metadata is specified in new_resource" do before do - pending "windows reporting not yet fully supported" + skip "windows reporting not yet fully supported" end it "sets the metadata values on the new_resource as strings after creating" do @@ -322,7 +322,7 @@ shared_examples_for "a securable resource with reporting" do let(:expected_user_name) { 'domain\user' } before do - pending "windows reporting not yet fully supported" + skip "windows reporting not yet fully supported" resource.owner(expected_user_name) resource.run_action(:create) end @@ -336,7 +336,7 @@ shared_examples_for "a securable resource with reporting" do context "when the target file exists" do before do - pending "windows reporting not yet fully supported" + skip "windows reporting not yet fully supported" FileUtils.touch(resource.path) resource.action(:create) end diff --git a/spec/support/shared/functional/windows_script.rb b/spec/support/shared/functional/windows_script.rb index 35b86dc4e8..3499cc98ec 100644 --- a/spec/support/shared/functional/windows_script.rb +++ b/spec/support/shared/functional/windows_script.rb @@ -114,7 +114,7 @@ shared_context Chef::Resource::WindowsScript do describe "when the run action is invoked on Windows" do it "executes the script code" do - resource.code("@whoami > #{script_output_path}") + resource.code("whoami > #{script_output_path}") resource.returns(0) resource.run_action(:run) end diff --git a/spec/support/shared/integration/integration_helper.rb b/spec/support/shared/integration/integration_helper.rb index e6942c62af..927ff2f42b 100644 --- a/spec/support/shared/integration/integration_helper.rb +++ b/spec/support/shared/integration/integration_helper.rb @@ -22,14 +22,19 @@ require 'fileutils' require 'chef/config' require 'chef/json_compat' require 'chef/server_api' -require 'chef_zero/rspec' require 'support/shared/integration/knife_support' require 'support/shared/integration/app_server_support' +require 'cheffish/rspec/chef_run_support' require 'spec_helper' module IntegrationSupport include ChefZero::RSpec + def self.included(includer_class) + includer_class.extend(Cheffish::RSpec::ChefRunSupport) + includer_class.extend(ClassMethods) + end + module ClassMethods include ChefZero::RSpec @@ -49,10 +54,6 @@ module IntegrationSupport end end - def self.included(includer_class) - includer_class.extend(ClassMethods) - end - def api Chef::ServerAPI.new end diff --git a/spec/support/shared/unit/api_versioning.rb b/spec/support/shared/unit/api_versioning.rb new file mode 100644 index 0000000000..a4f353de60 --- /dev/null +++ b/spec/support/shared/unit/api_versioning.rb @@ -0,0 +1,77 @@ +# +# Author:: Tyler Cloke (<tyler@chef.io>) +# Copyright:: Copyright (c) 2015 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" + +shared_examples_for "version handling" do + let(:response_406) { OpenStruct.new(:code => '406') } + let(:exception_406) { Net::HTTPServerException.new("406 Not Acceptable", response_406) } + + before do + allow(rest_v1).to receive(http_verb).and_raise(exception_406) + end + + context "when the server does not support the min or max server API version that Chef::User supports" do + before do + allow(object).to receive(:server_client_api_version_intersection).and_return([]) + end + + it "raises the original exception" do + expect{ object.send(method) }.to raise_error(exception_406) + end + end # when the server does not support the min or max server API version that Chef::User supports +end # version handling + +shared_examples_for "user and client reregister" do + let(:response_406) { OpenStruct.new(:code => '406') } + let(:exception_406) { Net::HTTPServerException.new("406 Not Acceptable", response_406) } + let(:generic_exception) { Exception.new } + let(:min_version) { "2" } + let(:max_version) { "5" } + let(:return_hash_406) { + { + "min_version" => min_version, + "max_version" => max_version, + "request_version" => "30" + } + } + + context "when V0 is not supported by the server" do + context "when the exception is 406 and returns x-ops-server-api-version header" do + before do + allow(rest_v0).to receive(:put).and_raise(exception_406) + allow(response_406).to receive(:[]).with('x-ops-server-api-version').and_return(Chef::JSONCompat.to_json(return_hash_406)) + end + + it "raises an error about only V0 being supported" do + expect(object).to receive(:reregister_only_v0_supported_error_msg).with(max_version, min_version) + expect{ object.reregister }.to raise_error(Chef::Exceptions::OnlyApiVersion0SupportedForAction) + end + + end + context "when the exception is not versioning related" do + before do + allow(rest_v0).to receive(:put).and_raise(generic_exception) + end + + it "raises the original error" do + expect{ object.reregister }.to raise_error(generic_exception) + end + end + end +end diff --git a/spec/support/shared/unit/knife_shared.rb b/spec/support/shared/unit/knife_shared.rb new file mode 100644 index 0000000000..8c9010f3cf --- /dev/null +++ b/spec/support/shared/unit/knife_shared.rb @@ -0,0 +1,40 @@ +# +# Author:: Tyler Cloke (<tyler@chef.io>) +# Copyright:: Copyright (c) 2015 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. +# + + +shared_examples_for "mandatory field missing" do + context "when field is nil" do + before do + knife.name_args = name_args + end + + it "exits 1" do + expect { knife.run }.to raise_error(SystemExit) + end + + it "prints the usage" do + expect(knife).to receive(:show_usage) + expect { knife.run }.to raise_error(SystemExit) + end + + it "prints a relevant error message" do + expect { knife.run }.to raise_error(SystemExit) + expect(stderr.string).to match /You must specify a #{fieldname}/ + end + end +end diff --git a/spec/support/shared/unit/provider/file.rb b/spec/support/shared/unit/provider/file.rb index 86f32c9e89..7de9698451 100644 --- a/spec/support/shared/unit/provider/file.rb +++ b/spec/support/shared/unit/provider/file.rb @@ -255,7 +255,7 @@ shared_examples_for Chef::Provider::File do context "examining file security metadata on Unix with a file that exists" do before do # fake that we're on unix even if we're on windows - allow(Chef::Platform).to receive(:windows?).and_return(false) + allow(ChefConfig).to receive(:windows?).and_return(false) # mock up the filesystem to behave like unix setup_normal_file stat_struct = double("::File.stat", :mode => 0600, :uid => 0, :gid => 0, :mtime => 10000) @@ -331,7 +331,7 @@ shared_examples_for Chef::Provider::File do context "examining file security metadata on Unix with a file that does not exist" do before do # fake that we're on unix even if we're on windows - allow(Chef::Platform).to receive(:windows?).and_return(false) + allow(ChefConfig).to receive(:windows?).and_return(false) setup_missing_file end @@ -380,7 +380,7 @@ shared_examples_for Chef::Provider::File do before do # fake that we're on unix even if we're on windows - allow(Chef::Platform).to receive(:windows?).and_return(false) + allow(ChefConfig).to receive(:windows?).and_return(false) # mock up the filesystem to behave like unix setup_normal_file stat_struct = double("::File.stat", :mode => 0600, :uid => 0, :gid => 0, :mtime => 10000) @@ -529,26 +529,49 @@ shared_examples_for Chef::Provider::File do :for_reporting => diff_for_reporting ) allow(diff).to receive(:diff).with(resource_path, tempfile_path).and_return(true) expect(provider).to receive(:diff).at_least(:once).and_return(diff) - expect(provider).to receive(:managing_content?).at_least(:once).and_return(true) expect(provider).to receive(:checksum).with(tempfile_path).and_return(tempfile_sha256) - expect(provider).to receive(:checksum).with(resource_path).and_return(tempfile_sha256) + allow(provider).to receive(:managing_content?).and_return(true) + allow(provider).to receive(:checksum).with(resource_path).and_return(tempfile_sha256) + expect(resource).not_to receive(:checksum).with(tempfile_sha256) # do not mutate the new resource expect(provider.deployment_strategy).to receive(:deploy).with(tempfile_path, normalized_path) end context "when the file was created" do before { expect(provider).to receive(:needs_creating?).at_least(:once).and_return(true) } - it "does not backup the file and does not produce a diff for reporting" do + it "does not backup the file" do expect(provider).not_to receive(:do_backup) provider.send(:do_contents_changes) + end + + it "does not produce a diff for reporting" do + provider.send(:do_contents_changes) expect(resource.diff).to be_nil end + + it "renders the final checksum correctly for reporting" do + provider.send(:do_contents_changes) + expect(resource.state_for_resource_reporter[:checksum]).to eql(tempfile_sha256) + end end context "when the file was not created" do - before { expect(provider).to receive(:needs_creating?).at_least(:once).and_return(false) } - it "backs up the file and produces a diff for reporting" do + before do + allow(provider).to receive(:do_backup) # stub do_backup + expect(provider).to receive(:needs_creating?).at_least(:once).and_return(false) + end + + it "backs up the file" do expect(provider).to receive(:do_backup) provider.send(:do_contents_changes) + end + + it "produces a diff for reporting" do + provider.send(:do_contents_changes) expect(resource.diff).to eq(diff_for_reporting) end + + it "renders the final checksum correctly for reporting" do + provider.send(:do_contents_changes) + expect(resource.state_for_resource_reporter[:checksum]).to eql(tempfile_sha256) + end end end diff --git a/spec/support/shared/unit/user_and_client_shared.rb b/spec/support/shared/unit/user_and_client_shared.rb new file mode 100644 index 0000000000..bc5ffa07c2 --- /dev/null +++ b/spec/support/shared/unit/user_and_client_shared.rb @@ -0,0 +1,115 @@ +# +# Author:: Tyler Cloke (<tyler@chef.io>) +# Copyright:: Copyright (c) 2015 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. +# + +shared_examples_for "user or client create" do + + context "when server API V1 is valid on the Chef Server receiving the request" do + + it "creates a new object via the API" do + expect(rest_v1).to receive(:post).with(url, payload).and_return({}) + object.create + end + + it "creates a new object via the API with a public_key when it exists" do + object.public_key "some_public_key" + expect(rest_v1).to receive(:post).with(url, payload.merge({:public_key => "some_public_key"})).and_return({}) + object.create + end + + context "raise error when create_key and public_key are both set" do + + before do + object.public_key "key" + object.create_key true + end + + it "rasies the proper error" do + expect { object.create }.to raise_error(error) + end + end + + context "when create_key == true" do + before do + object.create_key true + end + + it "creates a new object via the API with create_key" do + expect(rest_v1).to receive(:post).with(url, payload.merge({:create_key => true})).and_return({}) + object.create + end + end + + context "when chef_key is returned by the server" do + let(:chef_key) { + { + "chef_key" => { + "public_key" => "some_public_key" + } + } + } + + it "puts the public key into the objectr returned by create" do + expect(rest_v1).to receive(:post).with(url, payload).and_return(payload.merge(chef_key)) + new_object = object.create + expect(new_object.public_key).to eq("some_public_key") + end + + context "when private_key is returned in chef_key" do + let(:chef_key) { + { + "chef_key" => { + "public_key" => "some_public_key", + "private_key" => "some_private_key" + } + } + } + + it "puts the private key into the object returned by create" do + expect(rest_v1).to receive(:post).with(url, payload).and_return(payload.merge(chef_key)) + new_object = object.create + expect(new_object.private_key).to eq("some_private_key") + end + end + end # when chef_key is returned by the server + + end # when server API V1 is valid on the Chef Server receiving the request + + context "when server API V1 is not valid on the Chef Server receiving the request" do + + context "when the server supports API V0" do + before do + allow(object).to receive(:server_client_api_version_intersection).and_return([0]) + allow(rest_v1).to receive(:post).and_raise(exception_406) + end + + it "creates a new object via the API" do + expect(rest_v0).to receive(:post).with(url, payload).and_return({}) + object.create + end + + it "creates a new object via the API with a public_key when it exists" do + object.public_key "some_public_key" + expect(rest_v0).to receive(:post).with(url, payload.merge({:public_key => "some_public_key"})).and_return({}) + object.create + end + + end # when the server supports API V0 + end # when server API V1 is not valid on the Chef Server receiving the request + +end # user or client create + diff --git a/spec/unit/api_client_spec.rb b/spec/unit/api_client_spec.rb index 7668e31f5a..ba0eca3284 100644 --- a/spec/unit/api_client_spec.rb +++ b/spec/unit/api_client_spec.rb @@ -53,6 +53,20 @@ describe Chef::ApiClient do expect { @client.admin(Hash.new) }.to raise_error(ArgumentError) end + it "has an create_key flag attribute" do + @client.create_key(true) + expect(@client.create_key).to be_truthy + end + + it "create_key defaults to false" do + expect(@client.create_key).to be_falsey + end + + it "allows only boolean values for the create_key flag" do + expect { @client.create_key(false) }.not_to raise_error + expect { @client.create_key(Hash.new) }.to raise_error(ArgumentError) + end + it "has a 'validator' flag attribute" do @client.validator(true) expect(@client.validator).to be_truthy @@ -115,6 +129,12 @@ describe Chef::ApiClient do expect(@json).to include(%q{"validator":false}) end + it "includes the 'create_key' flag when present" do + @client.create_key(true) + @json = @client.to_json + expect(@json).to include(%q{"create_key":true}) + end + it "includes the private key when present" do @client.private_key("monkeypants") expect(@client.to_json).to include(%q{"private_key":"monkeypants"}) @@ -131,7 +151,7 @@ describe Chef::ApiClient do describe "when deserializing from JSON (string) using ApiClient#from_json" do let(:client_string) do - "{\"name\":\"black\",\"public_key\":\"crowes\",\"private_key\":\"monkeypants\",\"admin\":true,\"validator\":true}" + "{\"name\":\"black\",\"public_key\":\"crowes\",\"private_key\":\"monkeypants\",\"admin\":true,\"validator\":true,\"create_key\":true}" end let(:client) do @@ -158,6 +178,10 @@ describe Chef::ApiClient do expect(client.admin).to be_truthy end + it "preserves the create_key status" do + expect(client.create_key).to be_truthy + end + it "preserves the 'validator' status" do expect(client.validator).to be_truthy end @@ -175,6 +199,7 @@ describe Chef::ApiClient do "private_key" => "monkeypants", "admin" => true, "validator" => true, + "create_key" => true, "json_class" => "Chef::ApiClient" } end @@ -199,6 +224,10 @@ describe Chef::ApiClient do expect(client.admin).to be_truthy end + it "preserves the create_key status" do + expect(client.create_key).to be_truthy + end + it "preserves the 'validator' status" do expect(client.validator).to be_truthy end @@ -214,14 +243,16 @@ describe Chef::ApiClient do before(:each) do client = { - "name" => "black", - "clientname" => "black", - "public_key" => "crowes", - "private_key" => "monkeypants", - "admin" => true, - "validator" => true, - "json_class" => "Chef::ApiClient" + "name" => "black", + "clientname" => "black", + "public_key" => "crowes", + "private_key" => "monkeypants", + "admin" => true, + "create_key" => true, + "validator" => true, + "json_class" => "Chef::ApiClient" } + @http_client = double("Chef::REST mock") allow(Chef::REST).to receive(:new).and_return(@http_client) expect(@http_client).to receive(:get).with("clients/black").and_return(client) @@ -244,6 +275,10 @@ describe Chef::ApiClient do expect(@client.admin).to be_a_kind_of(TrueClass) end + it "preserves the create_key status" do + expect(@client.create_key).to be_a_kind_of(TrueClass) + end + it "preserves the 'validator' status" do expect(@client.validator).to be_a_kind_of(TrueClass) end @@ -297,24 +332,34 @@ describe Chef::ApiClient do end context "and the client exists" do + let(:chef_rest_v0_mock) { double('chef rest root v0 object') } + let(:payload) { + {:name => "lost-my-key", :admin => false, :validator => false, :private_key => true} + } + before do @api_client_without_key = Chef::ApiClient.new @api_client_without_key.name("lost-my-key") - expect(@http_client).to receive(:get).with("clients/lost-my-key").and_return(@api_client_without_key) - end + allow(@api_client_without_key).to receive(:chef_rest_v0).and_return(chef_rest_v0_mock) + #allow(@api_client_with_key).to receive(:http_api).and_return(_api_mock) + allow(chef_rest_v0_mock).to receive(:put).with("clients/lost-my-key", payload).and_return(@api_client_with_key) + allow(chef_rest_v0_mock).to receive(:get).with("clients/lost-my-key").and_return(@api_client_without_key) + allow(@http_client).to receive(:get).with("clients/lost-my-key").and_return(@api_client_without_key) + end context "and the client exists on a Chef 11-like server" do before do @api_client_with_key = Chef::ApiClient.new @api_client_with_key.name("lost-my-key") @api_client_with_key.private_key("the new private key") - expect(@http_client).to receive(:put). - with("clients/lost-my-key", :name => "lost-my-key", :admin => false, :validator => false, :private_key => true). - and_return(@api_client_with_key) + allow(@api_client_with_key).to receive(:chef_rest_v0).and_return(chef_rest_v0_mock) end it "returns an ApiClient with a private key" do + expect(chef_rest_v0_mock).to receive(:put).with("clients/lost-my-key", payload). + and_return(@api_client_with_key) + response = Chef::ApiClient.reregister("lost-my-key") # no sane == method for ApiClient :'( expect(response).to eq(@api_client_without_key) @@ -327,7 +372,7 @@ describe Chef::ApiClient do context "and the client exists on a Chef 10-like server" do before do @api_client_with_key = {"name" => "lost-my-key", "private_key" => "the new private key"} - expect(@http_client).to receive(:put). + expect(chef_rest_v0_mock).to receive(:put). with("clients/lost-my-key", :name => "lost-my-key", :admin => false, :validator => false, :private_key => true). and_return(@api_client_with_key) end @@ -345,4 +390,134 @@ describe Chef::ApiClient do end end + + describe "Versioned API Interactions" do + let(:response_406) { OpenStruct.new(:code => '406') } + let(:exception_406) { Net::HTTPServerException.new("406 Not Acceptable", response_406) } + let(:payload) { + { + :name => "some_name", + :validator => true, + :admin => true + } + } + + before do + @client = Chef::ApiClient.new + allow(@client).to receive(:chef_rest_v0).and_return(double('chef rest root v0 object')) + allow(@client).to receive(:chef_rest_v1).and_return(double('chef rest root v1 object')) + @client.name "some_name" + @client.validator true + @client.admin true + end + + describe "create" do + + # from spec/support/shared/unit/user_and_client_shared.rb + it_should_behave_like "user or client create" do + let(:object) { @client } + let(:error) { Chef::Exceptions::InvalidClientAttribute } + let(:rest_v0) { @client.chef_rest_v0 } + let(:rest_v1) { @client.chef_rest_v1 } + let(:url) { "clients" } + end + + context "when API V1 is not supported by the server" do + # from spec/support/shared/unit/api_versioning.rb + it_should_behave_like "version handling" do + let(:object) { @client } + let(:method) { :create } + let(:http_verb) { :post } + let(:rest_v1) { @client.chef_rest_v1 } + end + end + + end # create + + describe "update" do + context "when a valid client is defined" do + + shared_examples_for "client updating" do + it "updates the client" do + expect(rest). to receive(:put).with("clients/some_name", payload) + @client.update + end + + context "when only the name field exists" do + + before do + # needed since there is no way to set to nil via code + @client.instance_variable_set(:@validator, nil) + @client.instance_variable_set(:@admin, nil) + end + + after do + @client.validator true + @client.admin true + end + + it "updates the client with only the name" do + expect(rest). to receive(:put).with("clients/some_name", {:name => "some_name"}) + @client.update + end + end + + end + + context "when API V1 is supported by the server" do + + it_should_behave_like "client updating" do + let(:rest) { @client.chef_rest_v1 } + end + + end # when API V1 is supported by the server + + context "when API V1 is not supported by the server" do + context "when no version is supported" do + # from spec/support/shared/unit/api_versioning.rb + it_should_behave_like "version handling" do + let(:object) { @client } + let(:method) { :create } + let(:http_verb) { :post } + let(:rest_v1) { @client.chef_rest_v1 } + end + end # when no version is supported + + context "when API V0 is supported" do + + before do + allow(@client.chef_rest_v1).to receive(:put).and_raise(exception_406) + allow(@client).to receive(:server_client_api_version_intersection).and_return([0]) + end + + it_should_behave_like "client updating" do + let(:rest) { @client.chef_rest_v0 } + end + + end + + end # when API V1 is not supported by the server + end # when a valid client is defined + end # update + + # DEPRECATION + # This can be removed after API V0 support is gone + describe "reregister" do + context "when server API V0 is valid on the Chef Server receiving the request" do + it "creates a new object via the API" do + expect(@client.chef_rest_v0).to receive(:put).with("clients/#{@client.name}", payload.merge({:private_key => true})).and_return({}) + @client.reregister + end + end # when server API V0 is valid on the Chef Server receiving the request + + context "when server API V0 is not supported by the Chef Server" do + # from spec/support/shared/unit/api_versioning.rb + it_should_behave_like "user and client reregister" do + let(:object) { @client } + let(:rest_v0) { @client.chef_rest_v0 } + end + end # when server API V0 is not supported by the Chef Server + end # reregister + + end end diff --git a/spec/unit/application/client_spec.rb b/spec/unit/application/client_spec.rb index c753ca0ab8..64a6bcc9d2 100644 --- a/spec/unit/application/client_spec.rb +++ b/spec/unit/application/client_spec.rb @@ -60,7 +60,7 @@ describe Chef::Application::Client, "reconfigure" do context "when interval is given" do before do Chef::Config[:interval] = 600 - allow(Chef::Platform).to receive(:windows?).and_return(false) + allow(ChefConfig).to receive(:windows?).and_return(false) end it "should terminate with message" do @@ -77,7 +77,7 @@ Enable chef-client interval runs by setting `:client_fork = true` in your config context "when interval is given on windows" do before do Chef::Config[:interval] = 600 - allow(Chef::Platform).to receive(:windows?).and_return(true) + allow(ChefConfig).to receive(:windows?).and_return(true) end it "should not terminate" do @@ -165,11 +165,6 @@ Enable chef-client interval runs by setting `:client_fork = true` in your config before do allow(Chef::Log).to receive(:warn) end - - it "emits a warning that audit mode is an experimental feature" do - expect(Chef::Log).to receive(:warn).with(/Audit mode is an experimental feature/) - app.reconfigure - end end shared_examples "unrecognized setting" do diff --git a/spec/unit/audit/audit_reporter_spec.rb b/spec/unit/audit/audit_reporter_spec.rb index 4bf889510a..46c2a96b4c 100644 --- a/spec/unit/audit/audit_reporter_spec.rb +++ b/spec/unit/audit/audit_reporter_spec.rb @@ -88,6 +88,29 @@ describe Chef::Audit::AuditReporter do reporter.run_completed(node) end + context "when audit phase failed" do + + let(:audit_error) { double("AuditError", :class => "Chef::Exceptions::AuditError", + :message => "Audit phase failed with error message: derpderpderp", + :backtrace => ["/path/recipe.rb:57", "/path/library.rb:106"]) } + + before do + reporter.instance_variable_set(:@audit_phase_error, audit_error) + end + + it "reports an error" do + reporter.run_completed(node) + expect(run_data).to have_key(:error) + expect(run_data).to have_key(:error) + expect(run_data[:error]).to eq <<-EOM.strip! +Chef::Exceptions::AuditError: Audit phase failed with error message: derpderpderp +/path/recipe.rb:57 +/path/library.rb:106 +EOM + end + + end + context "when unable to post to server" do let(:error) do @@ -215,9 +238,13 @@ describe Chef::Audit::AuditReporter do let(:audit_data) { Chef::Audit::AuditData.new(node.name, run_id) } let(:run_data) { audit_data.to_hash } - let(:error) { double("AuditError", :class => "Chef::Exception::AuditError", - :message => "Well that certainly didn't work", - :backtrace => ["line 0", "line 1", "line 2"]) } + let(:audit_error) { double("AuditError", :class => "Chef::Exceptions::AuditError", + :message => "Audit phase failed with error message: derpderpderp", + :backtrace => ["/path/recipe.rb:57", "/path/library.rb:106"]) } + + let(:run_error) { double("RunError", :class => "Chef::Exceptions::RunError", + :message => "This error shouldn't be reported.", + :backtrace => ["fix it", "fix it", "fix it"]) } before do allow(reporter).to receive(:auditing_enabled?).and_return(true) @@ -226,15 +253,32 @@ describe Chef::Audit::AuditReporter do allow(audit_data).to receive(:to_hash).and_return(run_data) end - it "adds the error information to the reported data" do - expect(rest).to receive(:create_url) - expect(rest).to receive(:post) - reporter.run_failed(error) - expect(run_data).to have_key(:error) - expect(run_data[:error]).to eq "Chef::Exception::AuditError: Well that certainly didn't work\n" + - "line 0\nline 1\nline 2" + context "when no prior exception is stored" do + it "reports no error" do + expect(rest).to receive(:create_url) + expect(rest).to receive(:post) + reporter.run_failed(run_error) + expect(run_data).to_not have_key(:error) + end end + context "when some prior exception is stored" do + before do + reporter.instance_variable_set(:@audit_phase_error, audit_error) + end + + it "reports the prior error" do + expect(rest).to receive(:create_url) + expect(rest).to receive(:post) + reporter.run_failed(run_error) + expect(run_data).to have_key(:error) + expect(run_data[:error]).to eq <<-EOM.strip! +Chef::Exceptions::AuditError: Audit phase failed with error message: derpderpderp +/path/recipe.rb:57 +/path/library.rb:106 +EOM + end + end end shared_context "audit data" do @@ -270,14 +314,14 @@ describe Chef::Audit::AuditReporter do it "notifies audit phase finished to debug log" do expect(Chef::Log).to receive(:debug).with(/Audit Reporter completed/) - reporter.audit_phase_complete + reporter.audit_phase_complete("Output from audit mode") end it "collects audit data" do ordered_control_groups.each do |_name, group| expect(audit_data).to receive(:add_control_group).with(group) end - reporter.audit_phase_complete + reporter.audit_phase_complete("Output from audit mode") end end @@ -288,14 +332,14 @@ describe Chef::Audit::AuditReporter do it "notifies audit phase failed to debug log" do expect(Chef::Log).to receive(:debug).with(/Audit Reporter failed/) - reporter.audit_phase_failed(error) + reporter.audit_phase_failed(error, "Output from audit mode") end it "collects audit data" do ordered_control_groups.each do |_name, group| expect(audit_data).to receive(:add_control_group).with(group) end - reporter.audit_phase_failed(error) + reporter.audit_phase_failed(error, "Output from audit mode") end end diff --git a/spec/unit/audit/logger_spec.rb b/spec/unit/audit/logger_spec.rb new file mode 100644 index 0000000000..9dd9ce2cd9 --- /dev/null +++ b/spec/unit/audit/logger_spec.rb @@ -0,0 +1,42 @@ +# +# 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::Audit::Logger do + + before(:each) do + Chef::Audit::Logger.instance_variable_set(:@buffer, nil) + end + + it 'calling puts creates @buffer and adds the message' do + Chef::Audit::Logger.puts("Output message") + expect(Chef::Audit::Logger.read_buffer).to eq("Output message\n") + end + + it 'calling puts multiple times adds to the message' do + Chef::Audit::Logger.puts("Output message") + Chef::Audit::Logger.puts("Output message") + Chef::Audit::Logger.puts("Output message") + expect(Chef::Audit::Logger.read_buffer).to eq("Output message\nOutput message\nOutput message\n") + end + + it 'calling it before @buffer is set returns an empty string' do + expect(Chef::Audit::Logger.read_buffer).to eq("") + end + +end diff --git a/spec/unit/audit/runner_spec.rb b/spec/unit/audit/runner_spec.rb index 0bd4c18388..1de024260f 100644 --- a/spec/unit/audit/runner_spec.rb +++ b/spec/unit/audit/runner_spec.rb @@ -68,8 +68,8 @@ describe Chef::Audit::Runner do in_sub_process do runner.send(:setup) - expect(RSpec.configuration.output_stream).to eq(log_location) - expect(RSpec.configuration.error_stream).to eq(log_location) + expect(RSpec.configuration.output_stream).to eq(Chef::Audit::Logger) + expect(RSpec.configuration.error_stream).to eq(Chef::Audit::Logger) expect(RSpec.configuration.formatters.size).to eq(2) expect(RSpec.configuration.formatters).to include(instance_of(Chef::Audit::AuditEventProxy)) diff --git a/spec/unit/chef_fs/file_pattern_spec.rb b/spec/unit/chef_fs/file_pattern_spec.rb index a9f06e8424..ed5f314605 100644 --- a/spec/unit/chef_fs/file_pattern_spec.rb +++ b/spec/unit/chef_fs/file_pattern_spec.rb @@ -157,7 +157,7 @@ describe Chef::ChefFS::FilePattern do end end - context 'with simple pattern "a\*\b"', :pending => (Chef::Platform.windows?) do + context 'with simple pattern "a\*\b"', :skip => (Chef::Platform.windows?) do let(:pattern) { Chef::ChefFS::FilePattern.new('a\*\b') } it 'match?' do expect(pattern.match?('a*b')).to be_truthy @@ -264,7 +264,7 @@ describe Chef::ChefFS::FilePattern do end end - context 'with star pattern "/abc/d[a-z][0-9]f/ghi"', :pending => (Chef::Platform.windows?) do + context 'with star pattern "/abc/d[a-z][0-9]f/ghi"', :skip => (Chef::Platform.windows?) do let(:pattern) { Chef::ChefFS::FilePattern.new('/abc/d[a-z][0-9]f/ghi') } it 'match?' do expect(pattern.match?('/abc/de1f/ghi')).to be_truthy @@ -352,11 +352,7 @@ describe Chef::ChefFS::FilePattern do expect(pattern.could_match_children?('/abc/def/ghi')).to be_truthy expect(pattern.could_match_children?('abc')).to be_falsey end - it 'could_match_children? /abc** returns false for /xyz' do - pending 'Make could_match_children? more rigorous' - # At the moment, we return false for this, but in the end it would be nice to return true: - expect(pattern.could_match_children?('/xyz')).to be_falsey - end + it 'exact_child_name_under' do expect(pattern.exact_child_name_under('/')).to eq(nil) expect(pattern.exact_child_name_under('/abc')).to eq(nil) @@ -440,14 +436,6 @@ describe Chef::ChefFS::FilePattern do expect(p('/.').exact_path).to eq('/') expect(p('/.').match?('/')).to be_truthy end - it 'handles dot by itself', :pending => "decide what to do with dot by itself" do - expect(p('.').normalized_pattern).to eq('.') - expect(p('.').exact_path).to eq('.') - expect(p('.').match?('.')).to be_truthy - expect(p('./').normalized_pattern).to eq('.') - expect(p('./').exact_path).to eq('.') - expect(p('./').match?('.')).to be_truthy - end it 'handles dotdot' do expect(p('abc/../def').normalized_pattern).to eq('def') expect(p('abc/../def').exact_path).to eq('def') diff --git a/spec/unit/client_spec.rb b/spec/unit/client_spec.rb index fa8317744c..1e4bbb5c56 100644 --- a/spec/unit/client_spec.rb +++ b/spec/unit/client_spec.rb @@ -19,6 +19,8 @@ # require 'spec_helper' +require 'spec/support/shared/context/client' +require 'spec/support/shared/examples/client' require 'chef/run_context' require 'chef/rest' @@ -28,55 +30,7 @@ class FooError < RuntimeError end 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) - allow(ohai_system).to receive(:[]) 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::Config[:event_loggers] = [] - 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 - #Chef::Config[:node_name] = fqdn - allow(Ohai::System).to receive(:new).and_return(ohai_system) - end + include_context "client" context "when minimal ohai is configured" do before do @@ -88,7 +42,6 @@ describe Chef::Client do expect(ohai_system).to receive(:all_plugins).with(expected_filter) client.run_ohai end - end describe "authentication protocol selection" do @@ -117,7 +70,6 @@ describe Chef::Client do describe "configuring output formatters" do context "when no formatter has been configured" do - context "and STDOUT is a TTY" do before do allow(STDOUT).to receive(:tty?).and_return(true) @@ -203,135 +155,12 @@ describe Chef::Client do end describe "a full client run" do - shared_context "a 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") } - let(:audit_runner) { instance_double("Chef::Audit::Runner", :failed? => false) } - - 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. - allow(File).to receive(:exists?).and_call_original - expect(File).to receive(:exists?). - with(Chef::Config[:client_key]). - exactly(:once). - and_return(api_client_exists?) - - unless api_client_exists? - # Client.register will register with the validation client name. - expect_any_instance_of(Chef::ApiClient::Registration).to receive(:run) - end - 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. - expect(Chef::REST).to receive(:new). - with(Chef::Config[:chef_server_url], fqdn, Chef::Config[:client_key]). - exactly(:once). - and_return(http_node_load) - - # --Client#build_node - # looks up the node, which we will return, then later saves it. - expect(Chef::Node).to 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.) - expect_any_instance_of(Chef::ResourceReporter).to receive(:node_load_completed) - end - - def stub_for_sync_cookbooks - # --Client#setup_run_context - # ---Client#sync_cookbooks -- downloads the list of cookbooks to sync - # - expect_any_instance_of(Chef::CookbookSynchronizer).to receive(:sync_cookbooks) - expect(Chef::REST).to receive(:new).with(Chef::Config[:chef_server_url]).and_return(http_cookbook_sync) - expect(http_cookbook_sync).to receive(:post). - with("environments/_default/cookbook_versions", {:run_list => []}). - and_return({}) - end - - def stub_for_converge - # --Client#converge - expect(Chef::Runner).to receive(:new).and_return(runner) - expect(runner).to receive(:converge).and_return(true) - end - - def stub_for_audit - # -- Client#run_audits - expect(Chef::Audit::Runner).to receive(:new).and_return(audit_runner) - expect(audit_runner).to receive(:run).and_return(true) - end - - def stub_for_node_save - allow(node).to receive(:data_for_save).and_return(node.for_json) - - # --Client#save_updated_node - expect(Chef::REST).to receive(:new).with(Chef::Config[:chef_server_url]).and_return(http_node_save) - expect(http_node_save).to receive(:put_rest).with("nodes/#{fqdn}", node.for_json).and_return(true) - end - - def stub_for_run - expect_any_instance_of(Chef::RunLock).to receive(:acquire) - expect_any_instance_of(Chef::RunLock).to receive(:save_pid) - expect_any_instance_of(Chef::RunLock).to receive(:release) - - # Post conditions: check that node has been filled in correctly - expect(client).to receive(:run_started) - expect(client).to receive(:run_completed_successfully) - - # --ResourceReporter#run_completed - # updates the server with the resource history - # (has its own tests, so stubbing it here.) - expect_any_instance_of(Chef::ResourceReporter).to receive(:run_completed) - # --AuditReporter#run_completed - # posts the audit data to server. - # (has its own tests, so stubbing it here.) - expect_any_instance_of(Chef::Audit::AuditReporter).to receive(:run_completed) - end - - before do - Chef::Config[:client_fork] = enable_fork - Chef::Config[:cache_path] = windows? ? 'C:\chef' : '/var/chef' - Chef::Config[:why_run] = false - Chef::Config[:audit_mode] = :enabled - - 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_audit - stub_for_node_save - stub_for_run - end - end - shared_examples_for "a successful client run" do include_context "a client run" + include_context "converge completed" + include_context "audit phase completed" - it "runs ohai, sets up authentication, loads node state, synchronizes policy, converges, and runs audits" do - # This is what we're testing. - client.run - - # fork is stubbed, so we can see the outcome of the run - expect(node.automatic_attrs[:platform]).to eq("example-platform") - expect(node.automatic_attrs[:platform_version]).to eq("example-platform-1.0") - end + include_examples "a completed run" end describe "when running chef-client without fork" do @@ -339,24 +168,19 @@ describe Chef::Client do end describe "when the client key already exists" do - let(:api_client_exists?) { true } - include_examples "a successful client run" + include_examples "a successful client run" do + let(:api_client_exists?) { true } + end end - describe "when an override run list is given" do - let(:client_opts) { {:override_runlist => "recipe[override_recipe]"} } - - it "should permit spaces in overriding run list" do + context "when an override run list is given" do + it "permits spaces in overriding run list" do Chef::Client.new(nil, :override_runlist => 'role[a], role[b]') end - describe "when running the client" do + describe "calling run" do include_examples "a successful client run" do - - before do - # Client will try to compile and run override_recipe - expect_any_instance_of(Chef::RunContext::CookbookCompiler).to receive(:compile) - end + let(:client_opts) { {:override_runlist => "recipe[override_recipe]"} } def stub_for_sync_cookbooks # --Client#setup_run_context @@ -373,13 +197,22 @@ describe Chef::Client do # Expect NO node save expect(node).not_to receive(:save) end + + before do + # Client will try to compile and run override_recipe + expect_any_instance_of(Chef::RunContext::CookbookCompiler).to receive(:compile) + end end end end describe "when a permanent run list is passed as an option" do - include_examples "a successful client run" do + it "sets the new run list on the node" do + client.run + expect(node.run_list).to eq(Chef::RunList.new(new_runlist)) + end + include_examples "a successful client run" do let(:new_runlist) { "recipe[new_run_list_recipe]" } let(:client_opts) { {:runlist => new_runlist} } @@ -399,214 +232,61 @@ describe Chef::Client do # do not create a fixture for this. expect_any_instance_of(Chef::RunContext::CookbookCompiler).to receive(:compile) end - - it "sets the new run list on the node" do - client.run - expect(node.run_list).to eq(Chef::RunList.new(new_runlist)) - end end end - describe "when converge fails" do - include_context "a client run" do - let(:e) { Exception.new } - def stub_for_converge - expect(Chef::Runner).to receive(:new).and_return(runner) - expect(runner).to receive(:converge).and_raise(e) - expect(Chef::Application).to receive(:debug_stacktrace).with an_instance_of(Chef::Exceptions::RunFailedWrappingError) - end - - def stub_for_node_save - expect(client).to_not receive(:save_updated_node) - end - - def stub_for_run - expect_any_instance_of(Chef::RunLock).to receive(:acquire) - expect_any_instance_of(Chef::RunLock).to receive(:save_pid) - expect_any_instance_of(Chef::RunLock).to receive(:release) - - # Post conditions: check that node has been filled in correctly - expect(client).to receive(:run_started) - expect(client).to receive(:run_failed) + describe "when converge completes successfully" do + include_context "a client run" + include_context "converge completed" - expect_any_instance_of(Chef::ResourceReporter).to receive(:run_failed) - expect_any_instance_of(Chef::Audit::AuditReporter).to receive(:run_failed) + describe "when audit phase errors" do + include_context "audit phase failed with error" + include_examples "a completed run with audit failure" do + let(:run_errors) { [audit_error] } end end - it "runs the audits and raises the error" do - expect{ client.run }.to raise_error(Chef::Exceptions::RunFailedWrappingError) do |error| - expect(error.wrapped_errors.size).to eq(1) - expect(error.wrapped_errors[0]).to eq(e) - end + describe "when audit phase completed" do + include_context "audit phase completed" + include_examples "a completed run" end - end - - describe "when the audit phase fails" do - context "with an exception" do - context "when audit mode is enabled" do - include_context "a client run" do - let(:e) { Exception.new } - def stub_for_audit - expect(Chef::Audit::Runner).to receive(:new).and_return(audit_runner) - expect(audit_runner).to receive(:run).and_raise(e) - expect(Chef::Application).to receive(:debug_stacktrace).with an_instance_of(Chef::Exceptions::RunFailedWrappingError) - end - - def stub_for_run - expect_any_instance_of(Chef::RunLock).to receive(:acquire) - expect_any_instance_of(Chef::RunLock).to receive(:save_pid) - expect_any_instance_of(Chef::RunLock).to receive(:release) - - # Post conditions: check that node has been filled in correctly - expect(client).to receive(:run_started) - expect(client).to receive(:run_failed) - - expect_any_instance_of(Chef::ResourceReporter).to receive(:run_failed) - expect_any_instance_of(Chef::Audit::AuditReporter).to receive(:run_failed) - end - end - it "should save the node after converge and raise exception" do - expect{ client.run }.to raise_error(Chef::Exceptions::RunFailedWrappingError) do |error| - expect(error.wrapped_errors.size).to eq(1) - expect(error.wrapped_errors[0]).to eq(e) - end - end - end - - context "when audit mode is disabled" do - include_context "a client run" do - before do - Chef::Config[:audit_mode] = :disabled - end - - let(:e) { FooError.new } - - def stub_for_audit - expect(Chef::Audit::Runner).to_not receive(:new) - end - - def stub_for_converge - expect(Chef::Runner).to receive(:new).and_return(runner) - expect(runner).to receive(:converge).and_raise(e) - expect(Chef::Application).to receive(:debug_stacktrace).with an_instance_of(FooError) - end - - def stub_for_node_save - expect(client).to_not receive(:save_updated_node) - end - - def stub_for_run - expect_any_instance_of(Chef::RunLock).to receive(:acquire) - expect_any_instance_of(Chef::RunLock).to receive(:save_pid) - expect_any_instance_of(Chef::RunLock).to receive(:release) - - - # Post conditions: check that node has been filled in correctly - expect(client).to receive(:run_started) - expect(client).to receive(:run_failed) - - expect_any_instance_of(Chef::ResourceReporter).to receive(:run_failed) - - end - - it "re-raises an unwrapped exception" do - expect { client.run }.to raise_error(FooError) - end - end - end - - - end - - context "with failed audits" do - include_context "a client run" do - let(:audit_runner) do - instance_double("Chef::Audit::Runner", :run => true, :failed? => true, :num_failed => 1, :num_total => 1) - end - - def stub_for_audit - expect(Chef::Audit::Runner).to receive(:new).and_return(audit_runner) - expect(Chef::Application).to receive(:debug_stacktrace).with an_instance_of(Chef::Exceptions::RunFailedWrappingError) - end - - def stub_for_run - expect_any_instance_of(Chef::RunLock).to receive(:acquire) - expect_any_instance_of(Chef::RunLock).to receive(:save_pid) - expect_any_instance_of(Chef::RunLock).to receive(:release) - - # Post conditions: check that node has been filled in correctly - expect(client).to receive(:run_started) - expect(client).to receive(:run_failed) - - expect_any_instance_of(Chef::ResourceReporter).to receive(:run_failed) - expect_any_instance_of(Chef::Audit::AuditReporter).to receive(:run_failed) - end - end - - it "should save the node after converge and raise exception" do - expect{ client.run }.to raise_error(Chef::Exceptions::RunFailedWrappingError) do |error| - expect(error.wrapped_errors.size).to eq(1) - expect(error.wrapped_errors[0]).to be_instance_of(Chef::Exceptions::AuditsFailed) - end + describe "when audit phase completed with failed controls" do + include_context "audit phase completed with failed controls" + include_examples "a completed run with audit failure" do + let(:run_errors) { [audit_error] } end end end - describe "when why_run mode is enabled" do - include_context "a client run" do - - before do - Chef::Config[:why_run] = true - end - - def stub_for_audit - expect(Chef::Audit::Runner).to_not receive(:new) - end - - def stub_for_node_save - # This is how we should be mocking external calls - not letting it fall all the way through to the - # REST call - expect(node).to receive(:save) - end - - it "runs successfully without enabling the audit runner" do - client.run + describe "when converge errors" do + include_context "a client run" + include_context "converge failed" - # fork is stubbed, so we can see the outcome of the run - expect(node.automatic_attrs[:platform]).to eq("example-platform") - expect(node.automatic_attrs[:platform_version]).to eq("example-platform-1.0") + describe "when audit phase errors" do + include_context "audit phase failed with error" + include_examples "a failed run" do + let(:run_errors) { [converge_error, audit_error] } end end - end - - describe "when audits are disabled" do - include_context "a client run" do - before do - Chef::Config[:audit_mode] = :disabled - end - - def stub_for_audit - expect(Chef::Audit::Runner).to_not receive(:new) + describe "when audit phase completed" do + include_context "audit phase completed" + include_examples "a failed run" do + let(:run_errors) { [converge_error] } end + end - it "runs successfully without enabling the audit runner" do - client.run - - # fork is stubbed, so we can see the outcome of the run - expect(node.automatic_attrs[:platform]).to eq("example-platform") - expect(node.automatic_attrs[:platform_version]).to eq("example-platform-1.0") + describe "when audit phase completed with failed controls" do + include_context "audit phase completed with failed controls" + include_examples "a failed run" do + let(:run_errors) { [converge_error, audit_error] } 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) allow(Chef::RunLock).to receive(:new).and_return(@run_lock) @@ -680,6 +360,7 @@ describe Chef::Client do # check pre-conditions. expect(node[:roles]).to be_nil expect(node[:recipes]).to be_nil + expect(node[:expanded_run_list]).to be_nil allow(client.policy_builder).to receive(:node).and_return(node) @@ -692,7 +373,10 @@ describe Chef::Client do expect(node[:roles]).to include("role_containing_cookbook1") expect(node[:recipes]).not_to be_nil expect(node[:recipes].length).to eq(1) - expect(node[:recipes]).to include("cookbook1") + expect(node[:recipes]).to include("cookbook1::default") + expect(node[:expanded_run_list]).not_to be_nil + expect(node[:expanded_run_list].length).to eq(1) + expect(node[:expanded_run_list]).to include("cookbook1::default") end it "should set the environment from the specified configuration value" do @@ -715,7 +399,7 @@ describe Chef::Client do describe "windows_admin_check" do context "platform is not windows" do before do - allow(Chef::Platform).to receive(:windows?).and_return(false) + allow(ChefConfig).to receive(:windows?).and_return(false) end it "shouldn't be called" do @@ -726,7 +410,7 @@ describe Chef::Client do context "platform is windows" do before do - allow(Chef::Platform).to receive(:windows?).and_return(true) + allow(ChefConfig).to receive(:windows?).and_return(true) end it "should be called" do @@ -775,6 +459,7 @@ describe Chef::Client do Chef::Config[:solo] = true Chef::Config[:cookbook_path] = ["/path/to/invalid/cookbook_path"] end + context "when any directory of cookbook_path contains no cookbook" do it "raises CookbookNotFound error" do expect do @@ -819,4 +504,20 @@ describe Chef::Client do end end + + describe "always attempt to run handlers" do + subject { client } + before do + # fail on the first thing in begin block + allow_any_instance_of(Chef::RunLock).to receive(:save_pid).and_raise(NoMethodError) + end + + it "should run exception handlers on early fail" do + expect(subject).to receive(:run_failed) + expect { subject.run }.to raise_error(Chef::Exceptions::RunFailedWrappingError) do |error| + expect(error.wrapped_errors.size).to eq 1 + expect(error.wrapped_errors).to include(NoMethodError) + end + end + end end diff --git a/spec/unit/cookbook/cookbook_version_loader_spec.rb b/spec/unit/cookbook/cookbook_version_loader_spec.rb index 2c4ad11787..23ffc21f7f 100644 --- a/spec/unit/cookbook/cookbook_version_loader_spec.rb +++ b/spec/unit/cookbook/cookbook_version_loader_spec.rb @@ -20,7 +20,7 @@ require 'spec_helper' describe Chef::Cookbook::CookbookVersionLoader do before do - allow(Chef::Platform).to receive(:windows?) { false } + allow(ChefConfig).to receive(:windows?) { false } end describe "loading a cookbook" do diff --git a/spec/unit/cookbook/metadata_spec.rb b/spec/unit/cookbook/metadata_spec.rb index 760ae5dd2a..d2954726e8 100644 --- a/spec/unit/cookbook/metadata_spec.rb +++ b/spec/unit/cookbook/metadata_spec.rb @@ -304,6 +304,21 @@ describe Chef::Cookbook::Metadata do end end end + + it "strips out self-dependencies", :chef_lt_13_only do + metadata.name('foo') + expect(Chef::Log).to receive(:warn).with( + "Ignoring self-dependency in cookbook foo, please remove it (in the future this will be fatal)." + ) + metadata.depends('foo') + expect(metadata.dependencies).to eql({}) + end + + it "errors on self-dependencies", :chef_gte_13_only do + metadata.name('foo') + expect { metadata.depends('foo') }.to raise_error + # FIXME: add the error type + end end describe "attribute groupings" do diff --git a/spec/unit/cookbook/syntax_check_spec.rb b/spec/unit/cookbook/syntax_check_spec.rb index 471fc01831..ee4e0bed02 100644 --- a/spec/unit/cookbook/syntax_check_spec.rb +++ b/spec/unit/cookbook/syntax_check_spec.rb @@ -21,7 +21,7 @@ require "chef/cookbook/syntax_check" describe Chef::Cookbook::SyntaxCheck do before do - allow(Chef::Platform).to receive(:windows?) { false } + allow(ChefConfig).to receive(:windows?) { false } end let(:cookbook_path) { File.join(CHEF_SPEC_DATA, 'cookbooks', 'openldap') } diff --git a/spec/unit/cookbook_loader_spec.rb b/spec/unit/cookbook_loader_spec.rb index 45a985bafd..b1384bffe7 100644 --- a/spec/unit/cookbook_loader_spec.rb +++ b/spec/unit/cookbook_loader_spec.rb @@ -20,7 +20,7 @@ require 'spec_helper' describe Chef::CookbookLoader do before do - allow(Chef::Platform).to receive(:windows?) {false} + allow(ChefConfig).to receive(:windows?) {false} end let(:repo_paths) do [ diff --git a/spec/unit/cookbook_site_streaming_uploader_spec.rb b/spec/unit/cookbook_site_streaming_uploader_spec.rb index ef0f649163..0041a142dc 100644 --- a/spec/unit/cookbook_site_streaming_uploader_spec.rb +++ b/spec/unit/cookbook_site_streaming_uploader_spec.rb @@ -121,27 +121,6 @@ describe Chef::CookbookSiteStreamingUploader do }) end - describe "http verify mode" do - before do - @uri = "https://cookbooks.dummy.com/api/v1/cookbooks" - uri_info = URI.parse(@uri) - @http = Net::HTTP.new(uri_info.host, uri_info.port) - expect(Net::HTTP).to receive(:new).with(uri_info.host, uri_info.port).and_return(@http) - end - - it "should be VERIFY_NONE when ssl_verify_mode is :verify_none" do - Chef::Config[:ssl_verify_mode] = :verify_none - Chef::CookbookSiteStreamingUploader.make_request(:post, @uri, 'bill', @secret_filename) - expect(@http.verify_mode).to eq(OpenSSL::SSL::VERIFY_NONE) - end - - it "should be VERIFY_PEER when ssl_verify_mode is :verify_peer" do - Chef::Config[:ssl_verify_mode] = :verify_peer - Chef::CookbookSiteStreamingUploader.make_request(:post, @uri, 'bill', @secret_filename) - expect(@http.verify_mode).to eq(OpenSSL::SSL::VERIFY_PEER) - end - end - end # make_request describe "StreamPart" do diff --git a/spec/unit/cookbook_spec.rb b/spec/unit/cookbook_spec.rb index 7b3cda2af1..f36b031309 100644 --- a/spec/unit/cookbook_spec.rb +++ b/spec/unit/cookbook_spec.rb @@ -59,15 +59,6 @@ describe Chef::CookbookVersion do expect(@cookbook.fully_qualified_recipe_names.include?("openldap::three")).to eq(true) end - it "should find a preferred file" do - skip - end - - it "should not return an unchanged preferred file" do - pending - expect(@cookbook.preferred_filename(@node, :files, 'a-filename', 'the-checksum')).to be_nil - end - it "should raise an ArgumentException if you try to load a bad recipe name" do expect { @cookbook.load_recipe("doesnt_exist", @node) }.to raise_error(ArgumentError) end diff --git a/spec/unit/cookbook_version_spec.rb b/spec/unit/cookbook_version_spec.rb index 440dd9da6c..4990aef004 100644 --- a/spec/unit/cookbook_version_spec.rb +++ b/spec/unit/cookbook_version_spec.rb @@ -306,26 +306,6 @@ describe Chef::CookbookVersion do subject(:cbv) { Chef::CookbookVersion.new("version validation", '/tmp/blah') } - describe "HTTP Resource behaviors", pending: "will be deprected when CookbookManifest API is stablized" do - - it "errors on #save_url" do - expect { cbv.save_url }.to raise_error(Chef::Exceptions::DeprecatedFeatureError) - end - - it "errors on #force_save_url" do - expect { cbv.force_save_url }.to raise_error(Chef::Exceptions::DeprecatedFeatureError) - end - - it "errors on #to_hash" do - expect { cbv.to_hash }.to raise_error(Chef::Exceptions::DeprecatedFeatureError) - end - - it "errors on #to_json" do - expect { cbv.to_json }.to raise_error(Chef::Exceptions::DeprecatedFeatureError) - end - - end - it "errors on #status and #status=" do expect { cbv.status = :wat }.to raise_error(Chef::Exceptions::DeprecatedFeatureError) expect { cbv.status }.to raise_error(Chef::Exceptions::DeprecatedFeatureError) diff --git a/spec/unit/data_bag_spec.rb b/spec/unit/data_bag_spec.rb index f6db1e222a..bd9a99a1de 100644 --- a/spec/unit/data_bag_spec.rb +++ b/spec/unit/data_bag_spec.rb @@ -22,7 +22,7 @@ require 'chef/data_bag' describe Chef::DataBag do before(:each) do @data_bag = Chef::DataBag.new - allow(Chef::Platform)::to receive(:windows?) { false } + allow(ChefConfig).to receive(:windows?) { false } end describe "initialize" do diff --git a/spec/unit/deprecation_spec.rb b/spec/unit/deprecation_spec.rb index f824cb7c76..2e1f3c39f3 100644 --- a/spec/unit/deprecation_spec.rb +++ b/spec/unit/deprecation_spec.rb @@ -95,4 +95,59 @@ describe Chef::Deprecation do expect { test_instance.deprecated_method(10) }.to raise_error(Chef::Exceptions::DeprecatedFeatureError) end + context "When a class has deprecated_attr, _reader and _writer" do + before(:context) do + class DeprecatedAttrTest + extend Chef::Mixin::Deprecation + def initialize + @a = @r = @w = 1 + end + deprecated_attr :a, "a" + deprecated_attr_reader :r, "r" + deprecated_attr_writer :w, "w" + end + end + + it "The deprecated_attr emits warnings" do + test = DeprecatedAttrTest.new + expect { test.a = 10 }.to raise_error(Chef::Exceptions::DeprecatedFeatureError) + expect { test.a }.to raise_error(Chef::Exceptions::DeprecatedFeatureError) + end + + it "The deprecated_attr_writer emits warnings, and does not create a reader" do + test = DeprecatedAttrTest.new + expect { test.w = 10 }.to raise_error(Chef::Exceptions::DeprecatedFeatureError) + expect { test.w }.to raise_error(NoMethodError) + end + + it "The deprecated_attr_reader emits warnings, and does not create a writer" do + test = DeprecatedAttrTest.new + expect { test.r = 10 }.to raise_error(NoMethodError) + expect { test.r }.to raise_error(Chef::Exceptions::DeprecatedFeatureError) + end + + context "With deprecation warnings not throwing exceptions" do + before do + Chef::Config[:treat_deprecation_warnings_as_errors] = false + end + + it "The deprecated_attr can be written to and read from" do + test = DeprecatedAttrTest.new + test.a = 10 + expect(test.a).to eq 10 + end + + it "The deprecated_attr_reader can be read from" do + test = DeprecatedAttrTest.new + expect(test.r).to eq 1 + end + + it "The deprecated_attr_writer can be written to" do + test = DeprecatedAttrTest.new + test.w = 10 + expect(test.instance_eval { @w }).to eq 10 + end + end + end + end diff --git a/spec/unit/dsl/resources_spec.rb b/spec/unit/dsl/resources_spec.rb new file mode 100644 index 0000000000..581c835290 --- /dev/null +++ b/spec/unit/dsl/resources_spec.rb @@ -0,0 +1,85 @@ +# +# Author:: Noah Kantrowitz (<noah@coderanger.net>) +# Copyright:: Copyright (c) 2015 Noah Kantrowitz +# 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/dsl/resources' + +describe Chef::DSL::Resources do + let(:declared_resources) { [] } + let(:test_class) do + r = declared_resources + Class.new do + include Chef::DSL::Resources + define_method(:declare_resource) do |dsl_name, name, _created_at, &_block| + r << [dsl_name, name] + end + end + end + subject { declared_resources } + after do + # Always clean up after ourselves. + described_class.remove_resource_dsl(:test_resource) + end + + context 'with a resource added' do + before do + Chef::DSL::Resources.add_resource_dsl(:test_resource) + test_class.new.instance_eval do + test_resource 'test_name' do + end + end + end + it { is_expected.to eq [[:test_resource, 'test_name']]} + end + + context 'with no resource added' do + subject do + test_class.new.instance_eval do + test_resource 'test_name' do + end + end + end + + it { expect { subject }.to raise_error NoMethodError } + end + + context 'with a resource added and removed' do + before do + Chef::DSL::Resources.add_resource_dsl(:test_resource) + Chef::DSL::Resources.remove_resource_dsl(:test_resource) + end + subject do + test_class.new.instance_eval do + test_resource 'test_name' do + end + end + end + + it { expect { subject }.to raise_error NoMethodError } + end + + context 'with a nameless resource' do + before do + Chef::DSL::Resources.add_resource_dsl(:test_resource) + test_class.new.instance_eval do + test_resource { } + end + end + it { is_expected.to eq [[:test_resource, nil]]} + end +end diff --git a/spec/unit/event_dispatch/dispatcher_spec.rb b/spec/unit/event_dispatch/dispatcher_spec.rb new file mode 100644 index 0000000000..7e43b1933f --- /dev/null +++ b/spec/unit/event_dispatch/dispatcher_spec.rb @@ -0,0 +1,61 @@ +# +# Author:: Daniel DeLeo (<dan@chef.io>) +# +# Copyright:: Copyright (c) 2015 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/event_dispatch/dispatcher' + +describe Chef::EventDispatch::Dispatcher do + + subject(:dispatcher) { Chef::EventDispatch::Dispatcher.new } + + let(:event_sink) { instance_double("Chef::EventDispatch::Base") } + + it "has no subscribers by default" do + expect(dispatcher.subscribers).to be_empty + end + + context "when an event sink is registered" do + + before do + dispatcher.register(event_sink) + end + + it "it has the event sink as a subscriber" do + expect(dispatcher.subscribers.size).to eq(1) + expect(dispatcher.subscribers.first).to eq(event_sink) + end + + it "forwards events to the subscribed event sink" do + # the events all have different arity and such so we just hit a few different events: + + expect(event_sink).to receive(:run_start).with("12.4.0") + dispatcher.run_start("12.4.0") + + expect(event_sink).to receive(:synchronized_cookbook).with("apache2") + dispatcher.synchronized_cookbook("apache2") + + exception = StandardError.new("foo") + expect(event_sink).to receive(:recipe_file_load_failed).with("/path/to/file.rb", exception) + dispatcher.recipe_file_load_failed("/path/to/file.rb", exception) + end + + end + +end + diff --git a/spec/unit/exceptions_spec.rb b/spec/unit/exceptions_spec.rb index d35ecc8ec8..fd90aeab71 100644 --- a/spec/unit/exceptions_spec.rb +++ b/spec/unit/exceptions_spec.rb @@ -113,7 +113,7 @@ describe Chef::Exceptions do context "initialized with 1 error and nil" do let(:e) { Chef::Exceptions::RunFailedWrappingError.new(RuntimeError.new("foo"), nil) } let(:num_errors) { 1 } - let(:backtrace) { ["1) RuntimeError - foo", ""] } + let(:backtrace) { ["1) RuntimeError - foo"] } include_examples "RunFailedWrappingError expectations" end @@ -121,7 +121,7 @@ describe Chef::Exceptions do context "initialized with 2 errors" do let(:e) { Chef::Exceptions::RunFailedWrappingError.new(RuntimeError.new("foo"), RuntimeError.new("bar")) } let(:num_errors) { 2 } - let(:backtrace) { ["1) RuntimeError - foo", "", "2) RuntimeError - bar", ""] } + let(:backtrace) { ["1) RuntimeError - foo", "", "2) RuntimeError - bar"] } include_examples "RunFailedWrappingError expectations" end diff --git a/spec/unit/file_content_management/deploy/mv_windows_spec.rb b/spec/unit/file_content_management/deploy/mv_windows_spec.rb index c52001cd26..2d1981befc 100644 --- a/spec/unit/file_content_management/deploy/mv_windows_spec.rb +++ b/spec/unit/file_content_management/deploy/mv_windows_spec.rb @@ -115,6 +115,66 @@ describe Chef::FileContentManagement::Deploy::MvWindows do end + context "and the target file has null dacl and sacl" do + + before do + allow(target_file_security_descriptor).to receive(:dacl_present?).and_return(true) + allow(target_file_security_descriptor).to receive(:dacl).and_return(nil) + allow(target_file_security_descriptor).to receive(:dacl_inherits?).and_return(false) + + allow(target_file_security_descriptor).to receive(:sacl_present?).and_return(true) + allow(target_file_security_descriptor).to receive(:sacl).and_return(nil) + allow(target_file_security_descriptor).to receive(:sacl_inherits?).and_return(false) + + expect(updated_target_security_object).to receive(:set_dacl).with(nil, false) + expect(updated_target_security_object).to receive(:set_sacl).with(nil, false) + end + + + it "fixes up permissions and moves the file into place" do + content_deployer.deploy(staging_file_path, target_file_path) + end + + end + + context "and the target has an empty dacl and sacl" do + let(:original_target_file_dacl) { [] } + let(:original_target_file_sacl) { [] } + + let(:empty_dacl) { double("Windows ACL with no dacl ACEs") } + let(:empty_sacl) { double("Windows ACL with no sacl ACEs") } + + before do + allow(target_file_security_descriptor).to receive(:dacl_present?).and_return(true) + allow(target_file_security_descriptor).to receive(:dacl_inherits?).and_return(false) + + allow(target_file_security_descriptor).to receive(:dacl).and_return(original_target_file_dacl) + expect(Chef::ReservedNames::Win32::Security::ACL). + to receive(:create). + with([]). + and_return(empty_dacl) + + + allow(target_file_security_descriptor).to receive(:sacl_present?).and_return(true) + allow(target_file_security_descriptor).to receive(:sacl_inherits?).and_return(false) + + allow(target_file_security_descriptor).to receive(:sacl).and_return(original_target_file_sacl) + expect(Chef::ReservedNames::Win32::Security::ACL). + to receive(:create). + with([]). + and_return(empty_sacl) + + + expect(updated_target_security_object).to receive(:set_dacl).with(empty_dacl, false) + expect(updated_target_security_object).to receive(:set_sacl).with(empty_sacl, false) + end + + + it "fixes up permissions and moves the file into place" do + content_deployer.deploy(staging_file_path, target_file_path) + end + end + context "and the target has a dacl and sacl" do let(:inherited_dacl_ace) { double("Windows dacl ace (inherited)", :inherited? => true) } let(:not_inherited_dacl_ace) { double("Windows dacl ace (not inherited)", :inherited? => false) } diff --git a/spec/unit/formatters/doc_spec.rb b/spec/unit/formatters/doc_spec.rb new file mode 100644 index 0000000000..d018207f49 --- /dev/null +++ b/spec/unit/formatters/doc_spec.rb @@ -0,0 +1,46 @@ +# +# Author:: Daniel DeLeo (<dan@chef.io>) +# +# Copyright:: Copyright (c) 2015 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::Formatters::Base do + + let(:out) { StringIO.new } + let(:err) { StringIO.new } + + subject(:formatter) { Chef::Formatters::Doc.new(out, err) } + + it "prints a policyfile's name and revision ID" do + minimal_policyfile = { + "revision_id"=> "613f803bdd035d574df7fa6da525b38df45a74ca82b38b79655efed8a189e073", + "name"=> "jenkins", + "run_list"=> [ + "recipe[apt::default]", + "recipe[java::default]", + "recipe[jenkins::master]", + "recipe[policyfile_demo::default]" + ], + "cookbook_locks"=> { } + } + + formatter.policyfile_loaded(minimal_policyfile) + expect(out.string).to include("Using policy 'jenkins' at revision '613f803bdd035d574df7fa6da525b38df45a74ca82b38b79655efed8a189e073'") + end + +end diff --git a/spec/unit/formatters/error_inspectors/api_error_formatting_spec.rb b/spec/unit/formatters/error_inspectors/api_error_formatting_spec.rb new file mode 100644 index 0000000000..b8c2de2b8b --- /dev/null +++ b/spec/unit/formatters/error_inspectors/api_error_formatting_spec.rb @@ -0,0 +1,77 @@ +# +# Author:: Tyler Cloke (<tyler@chef.io>) +# Copyright:: Copyright (c) 2015 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/formatters/error_inspectors/api_error_formatting' + +describe Chef::Formatters::APIErrorFormatting do + let(:class_instance) { (Class.new { include Chef::Formatters::APIErrorFormatting }).new } + let(:error_description) { instance_double(Chef::Formatters::ErrorDescription) } + let(:response) { double("response") } + before do + allow(response).to receive(:body) + end + + + context "when describe_406_error is called" do + context "when response['x-ops-server-api-version'] exists" do + let(:min_version) { "2" } + let(:max_version) { "5" } + let(:request_version) { "30" } + let(:return_hash) { + { + "min_version" => min_version, + "max_version" => max_version, + "request_version" => request_version + } + } + + before do + # mock out the header + allow(response).to receive(:[]).with('x-ops-server-api-version').and_return(Chef::JSONCompat.to_json(return_hash)) + end + + it "prints an error about client and server API version incompatibility with a min API version" do + expect(error_description).to receive(:section).with("Incompatible server API version:",/a min API version of #{min_version}/) + class_instance.describe_406_error(error_description, response) + end + + it "prints an error about client and server API version incompatibility with a max API version" do + expect(error_description).to receive(:section).with("Incompatible server API version:",/a max API version of #{max_version}/) + class_instance.describe_406_error(error_description, response) + end + + it "prints an error describing the request API version" do + expect(error_description).to receive(:section).with("Incompatible server API version:",/a request with an API version of #{request_version}/) + class_instance.describe_406_error(error_description, response) + end + end + + context "when response.body['error'] != 'invalid-x-ops-server-api-version'" do + + before do + allow(response).to receive(:[]).with('x-ops-server-api-version').and_return(nil) + end + + it "forwards the error_description to describe_http_error" do + expect(class_instance).to receive(:describe_http_error).with(error_description) + class_instance.describe_406_error(error_description, response) + end + end + end +end diff --git a/spec/unit/formatters/error_inspectors/compile_error_inspector_spec.rb b/spec/unit/formatters/error_inspectors/compile_error_inspector_spec.rb index ac19e91922..5f95beb259 100644 --- a/spec/unit/formatters/error_inspectors/compile_error_inspector_spec.rb +++ b/spec/unit/formatters/error_inspectors/compile_error_inspector_spec.rb @@ -37,69 +37,122 @@ end E describe Chef::Formatters::ErrorInspectors::CompileErrorInspector do - before do - @node_name = "test-node.example.com" - @description = Chef::Formatters::ErrorDescription.new("Error Evaluating File:") - @exception = NoMethodError.new("undefined method `this_is_not_a_valid_method' for Chef::Resource::File") - @outputter = Chef::Formatters::IndentableOutputStream.new(StringIO.new, STDERR) - #@outputter = Chef::Formatters::IndentableOutputStream.new(STDOUT, STDERR) - end + let(:node_name) { "test-node.example.com" } - describe "when scrubbing backtraces" do - it "shows backtrace lines from cookbook files" do - # Error inspector originally used file_cache_path which is incorrect on - # chef-solo. Using cookbook_path should do the right thing for client and - # solo. - allow(Chef::Config).to receive(:cookbook_path).and_return([ "/home/someuser/dev-laptop/cookbooks" ]) - @trace = [ - "/home/someuser/dev-laptop/cookbooks/syntax-err/recipes/default.rb:14:in `from_file'", - "/home/someuser/dev-laptop/cookbooks/syntax-err/recipes/default.rb:11:in `from_file'", - "/home/someuser/.multiruby/gems/chef/lib/chef/client.rb:123:in `run'" - ] - @exception.set_backtrace(@trace) - @path = "/var/chef/cache/cookbooks/syntax-err/recipes/default.rb" - @inspector = described_class.new(@path, @exception) + let(:description) { Chef::Formatters::ErrorDescription.new("Error Evaluating File:") } - @expected_filtered_trace = [ - "/home/someuser/dev-laptop/cookbooks/syntax-err/recipes/default.rb:14:in `from_file'", - "/home/someuser/dev-laptop/cookbooks/syntax-err/recipes/default.rb:11:in `from_file'", - ] - expect(@inspector.filtered_bt).to eq(@expected_filtered_trace) - end + let(:exception) do + e = NoMethodError.new("undefined method `this_is_not_a_valid_method' for Chef::Resource::File") + e.set_backtrace(trace) + e end - describe "when explaining an error in the compile phase" do - before do - allow(Chef::Config).to receive(:cookbook_path).and_return([ "/var/chef/cache/cookbooks" ]) - recipe_lines = BAD_RECIPE.split("\n").map {|l| l << "\n" } - expect(IO).to receive(:readlines).with("/var/chef/cache/cookbooks/syntax-err/recipes/default.rb").and_return(recipe_lines) - @trace = [ - "/var/chef/cache/cookbooks/syntax-err/recipes/default.rb:14:in `from_file'", - "/var/chef/cache/cookbooks/syntax-err/recipes/default.rb:11:in `from_file'", - "/usr/local/lib/ruby/gems/chef/lib/chef/client.rb:123:in `run'" # should not display - ] - @exception.set_backtrace(@trace) - @path = "/var/chef/cache/cookbooks/syntax-err/recipes/default.rb" - @inspector = described_class.new(@path, @exception) - @inspector.add_explanation(@description) - end + # Change to $stdout to print error messages for manual inspection + let(:stdout) { StringIO.new } + + let(:outputter) { Chef::Formatters::IndentableOutputStream.new(StringIO.new, STDERR) } - it "finds the line number of the error from the stacktrace" do - expect(@inspector.culprit_line).to eq(14) + subject(:inspector) { described_class.new(path_to_failed_file, exception) } + + describe "finding the code responsible for the error" do + + context "when the stacktrace includes cookbook files" do + + let(:trace) do + [ + "/home/someuser/dev-laptop/cookbooks/syntax-err/recipes/default.rb:14:in `from_file'", + "/home/someuser/dev-laptop/cookbooks/syntax-err/recipes/default.rb:11:in `from_file'", + "/home/someuser/.multiruby/gems/chef/lib/chef/client.rb:123:in `run'" + ] + end + + let(:expected_filtered_trace) do + [ + "/home/someuser/dev-laptop/cookbooks/syntax-err/recipes/default.rb:14:in `from_file'", + "/home/someuser/dev-laptop/cookbooks/syntax-err/recipes/default.rb:11:in `from_file'", + ] + end + + let(:path_to_failed_file) { "/home/someuser/dev-laptop/cookbooks/syntax-err/recipes/default.rb" } + + before do + # Error inspector originally used file_cache_path which is incorrect on + # chef-solo. Using cookbook_path should do the right thing for client and + # solo. + allow(Chef::Config).to receive(:cookbook_path).and_return([ "/home/someuser/dev-laptop/cookbooks" ]) + end + + describe "when scrubbing backtraces" do + it "shows backtrace lines from cookbook files" do + expect(inspector.filtered_bt).to eq(expected_filtered_trace) + end + end + + describe "when explaining an error in the compile phase" do + before do + recipe_lines = BAD_RECIPE.split("\n").map {|l| l << "\n" } + expect(IO).to receive(:readlines).with(path_to_failed_file).and_return(recipe_lines) + inspector.add_explanation(description) + end + + it "reports the error was not located within cookbooks" do + expect(inspector.found_error_in_cookbooks?).to be(true) + end + + it "finds the line number of the error from the stacktrace" do + expect(inspector.culprit_line).to eq(14) + end + + it "prints a pretty message" do + description.display(outputter) + end + end end - it "prints a pretty message" do - @description.display(@outputter) + context "when the error does not contain any lines from cookbooks" do + + let(:trace) do + [ + "/opt/chef/embedded/lib/ruby/gems/1.9.1/gems/chef-10.14.0/lib/chef/run_context.rb:144:in `rescue in block in load_libraries'", + "/opt/chef/embedded/lib/ruby/gems/1.9.1/gems/chef-10.14.0/lib/chef/run_context.rb:138:in `block in load_libraries'", + "/opt/chef/embedded/lib/ruby/gems/1.9.1/gems/chef-10.14.0/lib/chef/run_context.rb:230:in `call'", + "/opt/chef/embedded/lib/ruby/gems/1.9.1/gems/chef-10.14.0/lib/chef/run_context.rb:230:in `block (2 levels) in foreach_cookbook_load_segment'", + "/opt/chef/embedded/lib/ruby/gems/1.9.1/gems/chef-10.14.0/lib/chef/run_context.rb:229:in `each'", + "/opt/chef/embedded/lib/ruby/gems/1.9.1/gems/chef-10.14.0/lib/chef/run_context.rb:229:in `block in foreach_cookbook_load_segment'", + "/opt/chef/embedded/lib/ruby/gems/1.9.1/gems/chef-10.14.0/lib/chef/run_context.rb:227:in `each'", + "/opt/chef/embedded/lib/ruby/gems/1.9.1/gems/chef-10.14.0/lib/chef/run_context.rb:227:in `foreach_cookbook_load_segment'", + "/opt/chef/embedded/lib/ruby/gems/1.9.1/gems/chef-10.14.0/lib/chef/run_context.rb:137:in `load_libraries'" + ] + end + + let(:exception) do + e = Chef::Exceptions::RecipeNotFound.new("recipe nope:nope not found") + e.set_backtrace(trace) + e + end + + let(:path_to_failed_file) { nil } + + it "gives a full, non-filtered trace" do + expect(inspector.filtered_bt).to eq(trace) + end + + it "does not error when displaying the error" do + expect { description.display(outputter) }.to_not raise_error + end + + it "reports the error was not located within cookbooks" do + expect(inspector.found_error_in_cookbooks?).to be(false) + end + end end describe "when explaining an error on windows" do - before do - allow(Chef::Config).to receive(:cookbook_path).and_return([ "C:/opscode/chef/var/cache/cookbooks" ]) - recipe_lines = BAD_RECIPE.split("\n").map {|l| l << "\n" } - expect(IO).to receive(:readlines).at_least(1).times.with(/:\/opscode\/chef\/var\/cache\/cookbooks\/foo\/recipes\/default.rb/).and_return(recipe_lines) - @trace = [ + + let(:trace_with_upcase_drive) do + [ "C:/opscode/chef/var/cache/cookbooks/foo/recipes/default.rb:14 in `from_file'", "C:/opscode/chef/embedded/lib/ruby/gems/1.9.1/gems/chef-10.14.0/lib/chef/run_context.rb:144:in `rescue in block in load_libraries'", "C:/opscode/chef/embedded/lib/ruby/gems/1.9.1/gems/chef-10.14.0/lib/chef/run_context.rb:138:in `block in load_libraries'", @@ -122,81 +175,65 @@ describe Chef::Formatters::ErrorInspectors::CompileErrorInspector do "C:/opscode/chef/bin/chef-client:19:in `load'", "C:/opscode/chef/bin/chef-client:19:in `<main>'" ] - @exception.set_backtrace(@trace) - @path = "/var/chef/cache/cookbooks/syntax-err/recipes/default.rb" - @inspector = described_class.new(@path, @exception) - @inspector.add_explanation(@description) end + let(:trace) { trace_with_upcase_drive } - describe "and examining the stack trace for a recipe" do - it "find the culprit recipe name when the drive letter is upper case" do - expect(@inspector.culprit_file).to eq("C:/opscode/chef/var/cache/cookbooks/foo/recipes/default.rb") + let(:path_to_failed_file) { "/var/cache/cookbooks/foo/recipes/default.rb" } + + before do + allow(Chef::Config).to receive(:cookbook_path).and_return([ "C:/opscode/chef/var/cache/cookbooks" ]) + recipe_lines = BAD_RECIPE.split("\n").map {|l| l << "\n" } + expect(IO).to receive(:readlines).at_least(1).times.with(full_path_to_failed_file).and_return(recipe_lines) + inspector.add_explanation(description) + end + + context "when the drive letter in the path is uppercase" do + + let(:full_path_to_failed_file) { "C:/opscode/chef#{path_to_failed_file}" } + + it "reports the error was not located within cookbooks" do + expect(inspector.found_error_in_cookbooks?).to be(true) end - it "find the culprit recipe name when the drive letter is lower case" do - @trace.each { |line| line.gsub!(/^C:/, "c:") } - @exception.set_backtrace(@trace) - @inspector = described_class.new(@path, @exception) - @inspector.add_explanation(@description) - expect(@inspector.culprit_file).to eq("c:/opscode/chef/var/cache/cookbooks/foo/recipes/default.rb") + it "finds the culprit recipe name" do + expect(inspector.culprit_file).to eq("C:/opscode/chef/var/cache/cookbooks/foo/recipes/default.rb") end - end - it "finds the line number of the error from the stack trace" do - expect(@inspector.culprit_line).to eq(14) - end + it "finds the line number of the error from the stack trace" do + expect(inspector.culprit_line).to eq(14) + end - it "prints a pretty message" do - @description.display(@outputter) + it "prints a pretty message" do + description.display(outputter) + end end - end - describe "when explaining an error on windows, and the backtrace lowercases the drive letter" do - before do - allow(Chef::Config).to receive(:cookbook_path).and_return([ "C:/opscode/chef/var/cache/cookbooks" ]) - recipe_lines = BAD_RECIPE.split("\n").map {|l| l << "\n" } - expect(IO).to receive(:readlines).with("c:/opscode/chef/var/cache/cookbooks/foo/recipes/default.rb").and_return(recipe_lines) - @trace = [ - "c:/opscode/chef/var/cache/cookbooks/foo/recipes/default.rb:14 in `from_file'", - "c:/opscode/chef/embedded/lib/ruby/gems/1.9.1/gems/chef-10.14.0/lib/chef/run_context.rb:144:in `rescue in block in load_libraries'", - "c:/opscode/chef/embedded/lib/ruby/gems/1.9.1/gems/chef-10.14.0/lib/chef/run_context.rb:138:in `block in load_libraries'", - "c:/opscode/chef/embedded/lib/ruby/gems/1.9.1/gems/chef-10.14.0/lib/chef/run_context.rb:230:in `call'", - "c:/opscode/chef/embedded/lib/ruby/gems/1.9.1/gems/chef-10.14.0/lib/chef/run_context.rb:230:in `block (2 levels) in foreach_cookbook_load_segment'", - "c:/opscode/chef/embedded/lib/ruby/gems/1.9.1/gems/chef-10.14.0/lib/chef/run_context.rb:229:in `each'", - "c:/opscode/chef/embedded/lib/ruby/gems/1.9.1/gems/chef-10.14.0/lib/chef/run_context.rb:229:in `block in foreach_cookbook_load_segment'", - "c:/opscode/chef/embedded/lib/ruby/gems/1.9.1/gems/chef-10.14.0/lib/chef/run_context.rb:227:in `each'", - "c:/opscode/chef/embedded/lib/ruby/gems/1.9.1/gems/chef-10.14.0/lib/chef/run_context.rb:227:in `foreach_cookbook_load_segment'", - "c:/opscode/chef/embedded/lib/ruby/gems/1.9.1/gems/chef-10.14.0/lib/chef/run_context.rb:137:in `load_libraries'", - "c:/opscode/chef/embedded/lib/ruby/gems/1.9.1/gems/chef-10.14.0/lib/chef/run_context.rb:62:in `load'", - "c:/opscode/chef/embedded/lib/ruby/gems/1.9.1/gems/chef-10.14.0/lib/chef/client.rb:198:in `setup_run_context'", - "c:/opscode/chef/embedded/lib/ruby/gems/1.9.1/gems/chef-10.14.0/lib/chef/client.rb:418:in `do_run'", - "c:/opscode/chef/embedded/lib/ruby/gems/1.9.1/gems/chef-10.14.0/lib/chef/client.rb:176:in `run'", - "c:/opscode/chef/embedded/lib/ruby/gems/1.9.1/gems/chef-10.14.0/lib/chef/application/client.rb:283:in `block in run_application'", - "c:/opscode/chef/embedded/lib/ruby/gems/1.9.1/gems/chef-10.14.0/lib/chef/application/client.rb:270:in `loop'", - "c:/opscode/chef/embedded/lib/ruby/gems/1.9.1/gems/chef-10.14.0/lib/chef/application/client.rb:270:in `run_application'", - "c:/opscode/chef/embedded/lib/ruby/gems/1.9.1/gems/chef-10.14.0/lib/chef/application.rb:70:in `run'", - "c:/opscode/chef/embedded/lib/ruby/gems/1.9.1/gems/chef-10.14.0/bin/chef-client:26:in `<top (required)>'", - "c:/opscode/chef/bin/chef-client:19:in `load'", - "c:/opscode/chef/bin/chef-client:19:in `<main>'" - ] - @exception.set_backtrace(@trace) - @path = "/var/chef/cache/cookbooks/syntax-err/recipes/default.rb" - @inspector = described_class.new(@path, @exception) - @inspector.add_explanation(@description) - end + context "when the drive letter in the path is lowercase" do - it "finds the culprit recipe name from the stacktrace" do - expect(@inspector.culprit_file).to eq("c:/opscode/chef/var/cache/cookbooks/foo/recipes/default.rb") - end + let(:trace) do + trace_with_upcase_drive.map { |line| line.gsub(/^C:/, "c:") } + end - it "finds the line number of the error from the stack trace" do - expect(@inspector.culprit_line).to eq(14) - end + let(:full_path_to_failed_file) { "c:/opscode/chef#{path_to_failed_file}" } - it "prints a pretty message" do - @description.display(@outputter) + it "reports the error was not located within cookbooks" do + expect(inspector.found_error_in_cookbooks?).to be(true) + end + + it "finds the culprit recipe name from the stacktrace" do + expect(inspector.culprit_file).to eq("c:/opscode/chef/var/cache/cookbooks/foo/recipes/default.rb") + end + + it "finds the line number of the error from the stack trace" do + expect(inspector.culprit_line).to eq(14) + end + + it "prints a pretty message" do + description.display(outputter) + end end + end end diff --git a/spec/unit/formatters/error_inspectors/resource_failure_inspector_spec.rb b/spec/unit/formatters/error_inspectors/resource_failure_inspector_spec.rb index a42d234601..5594d6e18a 100644 --- a/spec/unit/formatters/error_inspectors/resource_failure_inspector_spec.rb +++ b/spec/unit/formatters/error_inspectors/resource_failure_inspector_spec.rb @@ -126,6 +126,13 @@ describe Chef::Formatters::ErrorInspectors::ResourceFailureInspector do expect(@inspector.recipe_snippet).to match(/^# In C:\/Users\/btm/) end + it "parses a Windows path" do + source_line = "C:\\Windows\\Temp\\packer\\cookbooks\\fake_file.rb:2: undefined local variable or method `non_existent' for main:Object (NameError)" + @resource.source_line = source_line + @inspector = Chef::Formatters::ErrorInspectors::ResourceFailureInspector.new(@resource, :create, @exception) + expect(@inspector.recipe_snippet).to match(/^# In C:\\Windows\\Temp\\packer\\/) + end + it "parses a unix path" do source_line = "/home/btm/src/chef/chef/spec/unit/fake_file.rb:2: undefined local variable or method `non_existent' for main:Object (NameError)" @resource.source_line = source_line diff --git a/spec/unit/guard_interpreter/resource_guard_interpreter_spec.rb b/spec/unit/guard_interpreter/resource_guard_interpreter_spec.rb index 4cf3ba827a..acf1b15fd8 100644 --- a/spec/unit/guard_interpreter/resource_guard_interpreter_spec.rb +++ b/spec/unit/guard_interpreter/resource_guard_interpreter_spec.rb @@ -24,6 +24,7 @@ describe Chef::GuardInterpreter::ResourceGuardInterpreter do node.default["kernel"] = Hash.new node.default["kernel"][:machine] = :x86_64.to_s + node.automatic[:os] = 'windows' node end @@ -83,6 +84,14 @@ describe Chef::GuardInterpreter::ResourceGuardInterpreter do expect(guard_interpreter.evaluate).to eq(true) end + it "does not corrupt the run_context of the node" do + node_run_context_before_guard_execution = parent_resource.run_context + expect(node_run_context_before_guard_execution.object_id).to eq(parent_resource.node.run_context.object_id) + guard_interpreter.evaluate + node_run_context_after_guard_execution = parent_resource.run_context + expect(node_run_context_after_guard_execution.object_id).to eq(parent_resource.node.run_context.object_id) + end + describe "script command opts switch" do let(:command_opts) { {} } let(:guard_interpreter) { Chef::GuardInterpreter::ResourceGuardInterpreter.new(parent_resource, "exit 0", command_opts) } @@ -144,4 +153,3 @@ describe Chef::GuardInterpreter::ResourceGuardInterpreter do end end end - diff --git a/spec/unit/http/authenticator_spec.rb b/spec/unit/http/authenticator_spec.rb new file mode 100644 index 0000000000..48bbdcf76c --- /dev/null +++ b/spec/unit/http/authenticator_spec.rb @@ -0,0 +1,78 @@ +# +# Author:: Tyler Cloke (<tyler@chef.io>) +# Copyright:: Copyright (c) 2015 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/http/authenticator' + +describe Chef::HTTP::Authenticator do + let(:class_instance) { Chef::HTTP::Authenticator.new } + let(:method) { double("method") } + let(:url) { double("url") } + let(:headers) { Hash.new } + let(:data) { double("data") } + + before do + allow(class_instance).to receive(:authentication_headers).and_return({}) + end + + context "when handle_request is called" do + shared_examples_for "merging the server API version into the headers" do + it "merges the default version of X-Ops-Server-API-Version into the headers" do + # headers returned + expect(class_instance.handle_request(method, url, headers, data)[2]). + to include({'X-Ops-Server-API-Version' => Chef::HTTP::Authenticator::DEFAULT_SERVER_API_VERSION}) + end + + context "when api_version is set to something other than the default" do + let(:class_instance) { Chef::HTTP::Authenticator.new({:api_version => '-10'}) } + + it "merges the requested version of X-Ops-Server-API-Version into the headers" do + expect(class_instance.handle_request(method, url, headers, data)[2]). + to include({'X-Ops-Server-API-Version' => '-10'}) + end + end + end + + context "when !sign_requests?" do + before do + allow(class_instance).to receive(:sign_requests?).and_return(false) + end + + it_behaves_like "merging the server API version into the headers" + + it "authentication_headers is not called" do + expect(class_instance).to_not receive(:authentication_headers) + class_instance.handle_request(method, url, headers, data) + end + + end + + context "when sign_requests?" do + before do + allow(class_instance).to receive(:sign_requests?).and_return(true) + end + + it_behaves_like "merging the server API version into the headers" + + it "calls authentication_headers with the proper input" do + expect(class_instance).to receive(:authentication_headers).with(method, url, data).and_return({}) + class_instance.handle_request(method, url, headers, data) + end + end + end +end diff --git a/spec/unit/http/basic_client_spec.rb b/spec/unit/http/basic_client_spec.rb index 32b32a5f4c..b7552f54aa 100644 --- a/spec/unit/http/basic_client_spec.rb +++ b/spec/unit/http/basic_client_spec.rb @@ -109,5 +109,21 @@ describe "HTTP Connection" do end end + + context "when an empty proxy is set by the environment" do + let(:env) do + { + "https_proxy" => "" + } + end + + before do + allow(subject).to receive(:env).and_return(env) + end + + it "to not fail with URI parse exception" do + expect { subject.proxy_uri }.to_not raise_error + end + end end end diff --git a/spec/unit/json_compat_spec.rb b/spec/unit/json_compat_spec.rb index 4631429bd6..7482ba8a28 100644 --- a/spec/unit/json_compat_spec.rb +++ b/spec/unit/json_compat_spec.rb @@ -72,19 +72,19 @@ describe Chef::JSONCompat do end end - # On FreeBSD 10.1 i386 rspec fails with a SystemStackError loading the expect line with more that 254 entries + # On FreeBSD 10.1 i386 rspec fails with a SystemStackError loading the expect line with more that 252 entries # https://github.com/chef/chef/issues/3101 - describe "with a file with 254 or less nested entries" do - let(:json) { IO.read(File.join(CHEF_SPEC_DATA, 'big.json')) } + describe "with the file with 252 or less nested entries" do + let(:json) { IO.read(File.join(CHEF_SPEC_DATA, 'nested.json')) } let(:hash) { Chef::JSONCompat.from_json(json) } - describe "when a 254 json file is loaded" do + describe "when the 252 json file is loaded" do it "should create a Hash from the file" do expect(hash).to be_kind_of(Hash) end - it "should has 'test' as a 254 nested value" do - expect(hash['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']).to eq('test') + it "should has 'test' as a 252 nested value" do + expect(hash['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']).to eq('test') end end end diff --git a/spec/unit/key_spec.rb b/spec/unit/key_spec.rb new file mode 100644 index 0000000000..94ebbf6ae8 --- /dev/null +++ b/spec/unit/key_spec.rb @@ -0,0 +1,634 @@ +# +# Author:: Tyler Cloke (tyler@chef.io) +# Copyright:: Copyright (c) 2015 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/key' + +describe Chef::Key do + # whether user or client irrelevent to these tests + let(:key) { Chef::Key.new("original_actor", "user") } + let(:public_key_string) do + <<EOS +-----BEGIN PUBLIC KEY----- +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvPo+oNPB7uuNkws0fC02 +KxSwdyqPLu0fhI1pOweNKAZeEIiEz2PkybathHWy8snSXGNxsITkf3eyvIIKa8OZ +WrlqpI3yv/5DOP8HTMCxnFuMJQtDwMcevlqebX4bCxcByuBpNYDcAHjjfLGSfMjn +E5lZpgYWwnpic4kSjYcL9ORK9nYvlWV9P/kCYmRhIjB4AhtpWRiOfY/TKi3P2LxT +IjSmiN/ihHtlhV/VSnBJ5PzT/lRknlrJ4kACoz7Pq9jv+aAx5ft/xE9yDa2DYs0q +Tfuc9dUYsFjptWYrV6pfEQ+bgo1OGBXORBFcFL+2D7u9JYquKrMgosznHoEkQNLo +0wIDAQAB +-----END PUBLIC KEY----- +EOS + end + + shared_examples_for "fields with username type validation" do + context "when invalid input is passed" do + # It is not feasible to check all invalid characters. Here are a few + # that we probably care about. + it "should raise an ArgumentError" do + # capital letters + expect { key.send(field, "Bar") }.to raise_error(ArgumentError) + # slashes + expect { key.send(field, "foo/bar") }.to raise_error(ArgumentError) + # ? + expect { key.send(field, "foo?") }.to raise_error(ArgumentError) + # & + expect { key.send(field, "foo&") }.to raise_error(ArgumentError) + # spaces + expect { key.send(field, "foo ") }.to raise_error(ArgumentError) + end + end + end + + shared_examples_for "string fields that are settable" do + context "when it is set with valid input" do + it "should set the field" do + key.send(field, valid_input) + expect(key.send(field)).to eq(valid_input) + end + end + + context "when you feed it anything but a string" do + it "should raise an ArgumentError" do + expect { key.send(field, Hash.new) }.to raise_error(ArgumentError) + end + end + end + + + describe "when a new Chef::Key object is initialized with invalid input" do + it "should raise an InvalidKeyArgument" do + expect { Chef::Key.new("original_actor", "not_a_user_or_client") }.to raise_error(Chef::Exceptions::InvalidKeyArgument) + end + end + + describe "when a new Chef::Key object is initialized with valid input" do + it "should be a Chef::Key" do + expect(key).to be_a_kind_of(Chef::Key) + end + + it "should properly set the actor" do + expect(key.actor).to eq("original_actor") + end + end + + describe "when actor field is set" do + it_should_behave_like "string fields that are settable" do + let(:field) { :actor } + let(:valid_input) { "new_field_value" } + end + + it_should_behave_like "fields with username type validation" do + let(:field) { :actor } + end + end + + describe "when the name field is set" do + it_should_behave_like "string fields that are settable" do + let(:field) { :name } + let(:valid_input) { "new_field_value" } + end + end + + describe "when the private_key field is set" do + it_should_behave_like "string fields that are settable" do + let(:field) { :private_key } + let(:valid_input) { "new_field_value" } + end + end + + describe "when the public_key field is set" do + it_should_behave_like "string fields that are settable" do + let(:field) { :public_key } + let(:valid_input) { "new_field_value" } + end + + context "when create_key is true" do + before do + key.create_key true + end + + it "should raise an InvalidKeyAttribute" do + expect { key.public_key public_key_string }.to raise_error(Chef::Exceptions::InvalidKeyAttribute) + end + end + end + + describe "when the create_key field is set" do + context "when it is set to true" do + it "should set the field" do + key.create_key(true) + expect(key.create_key).to eq(true) + end + end + + context "when it is set to false" do + it "should set the field" do + key.create_key(false) + expect(key.create_key).to eq(false) + end + end + + context "when anything but a TrueClass or FalseClass is passed" do + it "should raise an ArgumentError" do + expect { key.create_key "not_a_boolean" }.to raise_error(ArgumentError) + end + end + + context "when public_key is defined" do + before do + key.public_key public_key_string + end + + it "should raise an InvalidKeyAttribute" do + expect { key.create_key true }.to raise_error(Chef::Exceptions::InvalidKeyAttribute) + end + end + end + + describe "when the expiration_date field is set" do + context "when a valid date is passed" do + it_should_behave_like "string fields that are settable" do + let(:field) { :public_key } + let(:valid_input) { "2020-12-24T21:00:00Z" } + end + end + + context "when infinity is passed" do + it_should_behave_like "string fields that are settable" do + let(:field) { :public_key } + let(:valid_input) { "infinity" } + end + end + + context "when an invalid date is passed" do + it "should raise an ArgumentError" do + expect { key.expiration_date "invalid_date" }.to raise_error(ArgumentError) + # wrong years + expect { key.expiration_date "20-12-24T21:00:00Z" }.to raise_error(ArgumentError) + end + + context "when it is a valid UTC date missing a Z" do + it "should raise an ArgumentError" do + expect { key.expiration_date "2020-12-24T21:00:00" }.to raise_error(ArgumentError) + end + end + end + end # when the expiration_date field is set + + describe "when serializing to JSON" do + shared_examples_for "common json operations" do + it "should serializes as a JSON object" do + expect(json).to match(/^\{.+\}$/) + end + + it "should include the actor value under the key relative to the actor_field_name passed" do + expect(json).to include(%Q("#{new_key.actor_field_name}":"original_actor")) + end + + it "should include the name field when present" do + new_key.name("monkeypants") + expect(new_key.to_json).to include(%q{"name":"monkeypants"}) + end + + it "should not include the name if not present" do + expect(json).to_not include("name") + end + + it "should include the public_key field when present" do + new_key.public_key "this_public_key" + expect(new_key.to_json).to include(%q("public_key":"this_public_key")) + end + + it "should not include the public_key if not present" do + expect(json).to_not include("public_key") + end + + it "should include the private_key field when present" do + new_key.private_key "this_public_key" + expect(new_key.to_json).to include(%q("private_key":"this_public_key")) + end + + it "should not include the private_key if not present" do + expect(json).to_not include("private_key") + end + + it "should include the expiration_date field when present" do + new_key.expiration_date "2020-12-24T21:00:00Z" + expect(new_key.to_json).to include(%Q("expiration_date":"2020-12-24T21:00:00Z")) + end + + it "should not include the expiration_date if not present" do + expect(json).to_not include("expiration_date") + end + + it "should include the create_key field when present" do + new_key.create_key true + expect(new_key.to_json).to include(%q("create_key":true)) + end + + it "should not include the create_key if not present" do + expect(json).to_not include("create_key") + end + end + + context "when key is for a user" do + it_should_behave_like "common json operations" do + let(:new_key) { Chef::Key.new("original_actor", "user") } + let(:json) do + new_key.to_json + end + end + end + + context "when key is for a client" do + it_should_behave_like "common json operations" do + let(:new_key) { Chef::Key.new("original_actor", "client") } + let(:json) do + new_key.to_json + end + end + end + + end # when serializing to JSON + + describe "when deserializing from JSON" do + shared_examples_for "a deserializable object" do + it "deserializes to a Chef::Key object" do + expect(key).to be_a_kind_of(Chef::Key) + end + + it "preserves the actor" do + expect(key.actor).to eq("turtle") + end + + it "preserves the name" do + expect(key.name).to eq("key_name") + end + + it "includes the public key if present" do + expect(key.public_key).to eq(public_key_string) + end + + it "includes the expiration_date if present" do + expect(key.expiration_date).to eq("infinity") + end + + it "includes the private_key if present" do + expect(key.private_key).to eq("some_private_key") + end + + it "includes the create_key if present" do + expect(key_with_create_key_field.create_key).to eq(true) + end + end + + context "when deserializing a key for a user" do + it_should_behave_like "a deserializable object" do + let(:key) do + o = { "user" => "turtle", + "name" => "key_name", + "public_key" => public_key_string, + "private_key" => "some_private_key", + "expiration_date" => "infinity"} + Chef::Key.from_json(o.to_json) + end + let(:key_with_create_key_field) do + o = { "user" => "turtle", + "create_key" => true } + Chef::Key.from_json(o.to_json) + end + end + end + + context "when deserializing a key for a client" do + it_should_behave_like "a deserializable object" do + let(:key) do + o = { "client" => "turtle", + "name" => "key_name", + "public_key" => public_key_string, + "private_key" => "some_private_key", + "expiration_date" => "infinity"} + Chef::Key.from_json(o.to_json) + end + let(:key_with_create_key_field) do + o = { "client" => "turtle", + "create_key" => true } + Chef::Key.from_json(o.to_json) + end + end + end + end # when deserializing from JSON + + + describe "API Interactions" do + let(:rest) do + Chef::Config[:chef_server_root] = "http://www.example.com" + Chef::Config[:chef_server_url] = "http://www.example.com/organizations/test_org" + r = double('rest') + allow(Chef::REST).to receive(:new).and_return(r) + r + end + + let(:user_key) do + o = Chef::Key.new("foobar", "user") + o + end + + let(:client_key) do + o = Chef::Key.new("foobar", "client") + o + end + + describe "list" do + context "when listing keys for a user" do + let(:response) { [{"uri" => "http://www.example.com/users/keys/foobar", "name"=>"foobar", "expired"=>false}] } + let(:inflated_response) { {"foobar" => user_key} } + + it "lists all keys" do + expect(rest).to receive(:get_rest).with("users/#{user_key.actor}/keys").and_return(response) + expect(Chef::Key.list_by_user("foobar")).to eq(response) + end + + it "inflate all keys" do + allow(Chef::Key).to receive(:load_by_user).with(user_key.actor, "foobar").and_return(user_key) + expect(rest).to receive(:get_rest).with("users/#{user_key.actor}/keys").and_return(response) + expect(Chef::Key.list_by_user("foobar", true)).to eq(inflated_response) + end + + end + + context "when listing keys for a client" do + let(:response) { [{"uri" => "http://www.example.com/users/keys/foobar", "name"=>"foobar", "expired"=>false}] } + let(:inflated_response) { {"foobar" => client_key} } + + it "lists all keys" do + expect(rest).to receive(:get_rest).with("clients/#{client_key.actor}/keys").and_return(response) + expect(Chef::Key.list_by_client("foobar")).to eq(response) + end + + it "inflate all keys" do + allow(Chef::Key).to receive(:load_by_client).with(client_key.actor, "foobar").and_return(client_key) + expect(rest).to receive(:get_rest).with("clients/#{user_key.actor}/keys").and_return(response) + expect(Chef::Key.list_by_client("foobar", true)).to eq(inflated_response) + end + + end + end + + + describe "create" do + shared_examples_for "create key" do + context "when a field is missing" do + it "should raise a MissingKeyAttribute" do + expect { key.create }.to raise_error(Chef::Exceptions::MissingKeyAttribute) + end + end + + context "when the name field is missing" do + before do + key.public_key public_key_string + key.expiration_date "2020-12-24T21:00:00Z" + end + + it "creates a new key via the API with the fingerprint as the name" do + expect(rest).to receive(:post_rest).with(url, + {"name" => "12:3e:33:73:0b:f4:ec:72:dc:f0:4c:51:62:27:08:76:96:24:f4:4a", + "public_key" => key.public_key, + "expiration_date" => key.expiration_date}).and_return({}) + key.create + end + end + + context "when every field is populated" do + before do + key.name "key_name" + key.public_key public_key_string + key.expiration_date "2020-12-24T21:00:00Z" + key.create_key false + end + + context "when create_key is false" do + it "creates a new key via the API" do + expect(rest).to receive(:post_rest).with(url, + {"name" => key.name, + "public_key" => key.public_key, + "expiration_date" => key.expiration_date}).and_return({}) + key.create + end + end + + context "when create_key is true and public_key is nil" do + + before do + key.delete_public_key + key.create_key true + $expected_output = { + actor_type => "foobar", + "name" => key.name, + "create_key" => true, + "expiration_date" => key.expiration_date + } + $expected_input = { + "name" => key.name, + "create_key" => true, + "expiration_date" => key.expiration_date + } + end + + it "should create a new key via the API" do + expect(rest).to receive(:post_rest).with(url, $expected_input).and_return({}) + key.create + end + + context "when the server returns the private_key via key.create" do + before do + allow(rest).to receive(:post_rest).with(url, $expected_input).and_return({"private_key" => "this_private_key"}) + end + + it "key.create returns the original key plus the private_key" do + expect(key.create.to_hash).to eq($expected_output.merge({"private_key" => "this_private_key"})) + end + end + end + + context "when create_key is false and public_key is nil" do + before do + key.delete_public_key + key.create_key false + end + it "should raise an InvalidKeyArgument" do + expect { key.create }.to raise_error(Chef::Exceptions::MissingKeyAttribute) + end + end + end + end + + context "when creating a user key" do + it_should_behave_like "create key" do + let(:url) { "users/#{key.actor}/keys" } + let(:key) { user_key } + let(:actor_type) { "user" } + end + end + + context "when creating a client key" do + it_should_behave_like "create key" do + let(:url) { "clients/#{client_key.actor}/keys" } + let(:key) { client_key } + let(:actor_type) { "client" } + end + end + end # create + + describe "update" do + shared_examples_for "update key" do + context "when name is missing and no argument was passed to update" do + it "should raise an MissingKeyAttribute" do + expect { key.update }.to raise_error(Chef::Exceptions::MissingKeyAttribute) + end + end + + context "when some fields are populated" do + before do + key.name "key_name" + key.expiration_date "2020-12-24T21:00:00Z" + end + + it "should update the key via the API" do + expect(rest).to receive(:put_rest).with(url, key.to_hash).and_return({}) + key.update + end + end + + context "when @name is not nil and a arg is passed to update" do + before do + key.name "new_name" + end + + it "passes @name in the body and the arg in the PUT URL" do + expect(rest).to receive(:put_rest).with(update_name_url, key.to_hash).and_return({}) + key.update("old_name") + end + end + + context "when the server returns a public_key and create_key is true" do + before do + key.name "key_name" + key.create_key true + allow(rest).to receive(:put_rest).with(url, key.to_hash).and_return({ + "key" => "key_name", + "public_key" => public_key_string + }) + + end + + it "returns a key with public_key populated" do + new_key = key.update + expect(new_key.public_key).to eq(public_key_string) + end + + it "returns a key without create_key set" do + new_key = key.update + expect(new_key.create_key).to be_nil + end + end + end + + context "when updating a user key" do + it_should_behave_like "update key" do + let(:url) { "users/#{key.actor}/keys/#{key.name}" } + let(:update_name_url) { "users/#{key.actor}/keys/old_name" } + let(:key) { user_key } + end + end + + context "when updating a client key" do + it_should_behave_like "update key" do + let(:url) { "clients/#{client_key.actor}/keys/#{key.name}" } + let(:update_name_url) { "clients/#{client_key.actor}/keys/old_name" } + let(:key) { client_key } + end + end + + end #update + + describe "load" do + shared_examples_for "load" do + it "should load a named key from the API" do + expect(rest).to receive(:get_rest).with(url).and_return({"user" => "foobar", "name" => "test_key_name", "public_key" => public_key_string, "expiration_date" => "infinity"}) + key = Chef::Key.send(load_method, "foobar", "test_key_name") + expect(key.actor).to eq("foobar") + expect(key.name).to eq("test_key_name") + expect(key.public_key).to eq(public_key_string) + expect(key.expiration_date).to eq("infinity") + end + end + + describe "load_by_user" do + it_should_behave_like "load" do + let(:load_method) { :load_by_user } + let(:url) { "users/foobar/keys/test_key_name" } + end + end + + describe "load_by_client" do + it_should_behave_like "load" do + let(:load_method) { :load_by_client } + let(:url) { "clients/foobar/keys/test_key_name" } + end + end + + end #load + + describe "destroy" do + shared_examples_for "destroy key" do + context "when name is missing" do + it "should raise an MissingKeyAttribute" do + expect { Chef::Key.new("username", "user").destroy }.to raise_error(Chef::Exceptions::MissingKeyAttribute) + end + end + + before do + key.name "key_name" + end + context "when name is not missing" do + it "should delete the key via the API" do + expect(rest).to receive(:delete_rest).with(url).and_return({}) + key.destroy + end + end + end + + context "when destroying a user key" do + it_should_behave_like "destroy key" do + let(:url) { "users/#{key.actor}/keys/#{key.name}" } + let(:key) { user_key } + end + end + + context "when destroying a client key" do + it_should_behave_like "destroy key" do + let(:url) { "clients/#{client_key.actor}/keys/#{key.name}" } + let(:key) { client_key } + end + end + end + end # API Interactions +end diff --git a/spec/unit/knife/bootstrap_spec.rb b/spec/unit/knife/bootstrap_spec.rb index f1ca510ed3..0195e6d406 100644 --- a/spec/unit/knife/bootstrap_spec.rb +++ b/spec/unit/knife/bootstrap_spec.rb @@ -23,7 +23,7 @@ require 'net/ssh' describe Chef::Knife::Bootstrap do before do - allow(Chef::Platform).to receive(:windows?) { false } + allow(ChefConfig).to receive(:windows?) { false } end let(:knife) do Chef::Log.logger = Logger.new(StringIO.new) @@ -531,6 +531,7 @@ describe Chef::Knife::Bootstrap do describe "when running the bootstrap" do let(:knife_ssh) do knife.name_args = ["foo.example.com"] + knife.config[:chef_node_name] = "foo.example.com" knife.config[:ssh_user] = "rooty" knife.config[:identity_file] = "~/.ssh/me.rsa" allow(knife).to receive(:render_template).and_return("") @@ -590,6 +591,12 @@ describe Chef::Knife::Bootstrap do expect(knife.chef_vault_handler).not_to receive(:run).with(node_name: knife.config[:chef_node_name]) knife.run end + + it "raises an exception if the config[:chef_node_name] is not present" do + knife.config[:chef_node_name] = nil + + expect { knife.run }.to raise_error(SystemExit) + end end context "when the validation key is not present" do @@ -604,6 +611,12 @@ describe Chef::Knife::Bootstrap do expect(knife.chef_vault_handler).to receive(:run).with(node_name: knife.config[:chef_node_name]) knife.run end + + it "raises an exception if the config[:chef_node_name] is not present" do + knife.config[:chef_node_name] = nil + + expect { knife.run }.to raise_error(SystemExit) + end end context "when the validation_key is nil" do diff --git a/spec/unit/knife/client_create_spec.rb b/spec/unit/knife/client_create_spec.rb index 10d386b5ff..8fecfc885f 100644 --- a/spec/unit/knife/client_create_spec.rb +++ b/spec/unit/knife/client_create_spec.rb @@ -22,6 +22,8 @@ Chef::Knife::ClientCreate.load_deps describe Chef::Knife::ClientCreate do let(:stderr) { StringIO.new } + let(:stdout) { StringIO.new } + let(:default_client_hash) do { @@ -32,84 +34,153 @@ describe Chef::Knife::ClientCreate do end let(:client) do - c = double("Chef::ApiClient") - allow(c).to receive(:save).and_return({"private_key" => ""}) - allow(c).to receive(:to_s).and_return("client[adam]") - c + Chef::ApiClient.new end let(:knife) do k = Chef::Knife::ClientCreate.new - k.name_args = [ "adam" ] - k.ui.config[:disable_editing] = true + k.name_args = [] + allow(k).to receive(:client).and_return(client) + allow(k).to receive(:edit_data).with(client).and_return(client) allow(k.ui).to receive(:stderr).and_return(stderr) - allow(k.ui).to receive(:stdout).and_return(stderr) + allow(k.ui).to receive(:stdout).and_return(stdout) k end + before do + allow(client).to receive(:to_s).and_return("client[adam]") + allow(knife).to receive(:create_client).and_return(client) + end + before(:each) do Chef::Config[:node_name] = "webmonkey.example.com" end describe "run" do - it "should create and save the ApiClient" do - expect(Chef::ApiClient).to receive(:from_hash).and_return(client) - expect(client).to receive(:save) - knife.run + context "when nothing is passed" do + # from spec/support/shared/unit/knife_shared.rb + it_should_behave_like "mandatory field missing" do + let(:name_args) { [] } + let(:fieldname) { 'client name' } + end end - it "should print a message upon creation" do - expect(Chef::ApiClient).to receive(:from_hash).and_return(client) - expect(client).to receive(:save) - knife.run - expect(stderr.string).to match /Created client.*adam/i - end + context "when clientname is passed" do + before do + knife.name_args = ['adam'] + end - it "should set the Client name" do - expect(Chef::ApiClient).to receive(:from_hash).with(hash_including("name" => "adam")).and_return(client) - knife.run - end + context "when public_key and prevent_keygen are passed" do + before do + knife.config[:public_key] = "some_key" + knife.config[:prevent_keygen] = true + end + + it "prints the usage" do + expect(knife).to receive(:show_usage) + expect { knife.run }.to raise_error(SystemExit) + end + + it "prints a relevant error message" do + expect { knife.run }.to raise_error(SystemExit) + expect(stderr.string).to match /You cannot pass --public-key and --prevent-keygen/ + end + end - it "by default it is not an admin" do - expect(Chef::ApiClient).to receive(:from_hash).with(hash_including("admin" => false)).and_return(client) - knife.run - end + it "should create the ApiClient" do + expect(knife).to receive(:create_client) + knife.run + end - it "by default it is not a validator" do - expect(Chef::ApiClient).to receive(:from_hash).with(hash_including("validator" => false)).and_return(client) - knife.run - end + it "should print a message upon creation" do + expect(knife).to receive(:create_client) + knife.run + expect(stderr.string).to match /Created client.*adam/i + end - it "should allow you to edit the data" do - expect(knife).to receive(:edit_hash).with(default_client_hash).and_return(default_client_hash) - allow(Chef::ApiClient).to receive(:from_hash).and_return(client) - knife.run - end + it "should set the Client name" do + knife.run + expect(client.name).to eq("adam") + end - describe "with -f or --file" do - it "should write the private key to a file" do - knife.config[:file] = "/tmp/monkeypants" - allow_any_instance_of(Chef::ApiClient).to receive(:save).and_return({ 'private_key' => "woot" }) - filehandle = double("Filehandle") - expect(filehandle).to receive(:print).with('woot') - expect(File).to receive(:open).with("/tmp/monkeypants", "w").and_yield(filehandle) + it "by default it is not an admin" do knife.run + expect(client.admin).to be_falsey end - end - describe "with -a or --admin" do - it "should create an admin client" do - knife.config[:admin] = true - expect(Chef::ApiClient).to receive(:from_hash).with(hash_including("admin" => true)).and_return(client) + it "by default it is not a validator" do knife.run + expect(client.admin).to be_falsey end - end - describe "with --validator" do - it "should create an validator client" do - knife.config[:validator] = true - expect(Chef::ApiClient).to receive(:from_hash).with(hash_including("validator" => true)).and_return(client) + it "by default it should set create_key to true" do knife.run + expect(client.create_key).to be_truthy + end + + it "should allow you to edit the data" do + expect(knife).to receive(:edit_data).with(client).and_return(client) + knife.run + end + + describe "with -f or --file" do + before do + client.private_key "woot" + end + + it "should write the private key to a file" do + knife.config[:file] = "/tmp/monkeypants" + filehandle = double("Filehandle") + expect(filehandle).to receive(:print).with('woot') + expect(File).to receive(:open).with("/tmp/monkeypants", "w").and_yield(filehandle) + knife.run + end + end + + describe "with -a or --admin" do + before do + knife.config[:admin] = true + end + + it "should create an admin client" do + knife.run + expect(client.admin).to be_truthy + end + end + + describe "with -p or --public-key" do + before do + knife.config[:public_key] = 'some_key' + allow(File).to receive(:read).and_return('some_key') + allow(File).to receive(:expand_path) + end + + it "sets the public key" do + knife.run + expect(client.public_key).to eq('some_key') + end + end + + describe "with -k or --prevent-keygen" do + before do + knife.config[:prevent_keygen] = true + end + + it "does not set create_key" do + knife.run + expect(client.create_key).to be_falsey + end + end + + describe "with --validator" do + before do + knife.config[:validator] = true + end + + it "should create an validator client" do + knife.run + expect(client.validator).to be_truthy + end end end end diff --git a/spec/unit/knife/core/subcommand_loader_spec.rb b/spec/unit/knife/core/subcommand_loader_spec.rb index 7f9308b28a..219a1f2906 100644 --- a/spec/unit/knife/core/subcommand_loader_spec.rb +++ b/spec/unit/knife/core/subcommand_loader_spec.rb @@ -22,14 +22,14 @@ describe Chef::Knife::SubcommandLoader do let(:loader) { Chef::Knife::SubcommandLoader.new(File.join(CHEF_SPEC_DATA, 'knife-site-subcommands')) } let(:home) { File.join(CHEF_SPEC_DATA, 'knife-home') } let(:plugin_dir) { File.join(home, '.chef', 'plugins', 'knife') } - + before do - allow(Chef::Platform).to receive(:windows?) { false } - Chef::Util::PathHelper.class_variable_set(:@@home_dir, home) + allow(ChefConfig).to receive(:windows?) { false } + Chef::Util::PathHelper.class_variable_set(:@@home_dir, home) end after do - Chef::Util::PathHelper.class_variable_set(:@@home_dir, nil) + Chef::Util::PathHelper.class_variable_set(:@@home_dir, nil) end it "builds a list of the core subcommand file require paths" do @@ -106,6 +106,18 @@ describe Chef::Knife::SubcommandLoader do # Chef 12.0.0.rc.0 gem also: "/opt/chefdk/embedded/lib/ruby/gems/2.1.0/gems/chef-#{Chef::VERSION}.rc.0/lib/chef/knife/thing.rb", + # Test that we ignore the platform suffix when checking for different + # gem versions. + "/opt/chefdk/embedded/lib/ruby/gems/2.1.0/gems/chef-#{Chef::VERSION}-x86-mingw32/lib/chef/knife/valid.rb", + "/opt/chefdk/embedded/lib/ruby/gems/2.1.0/gems/chef-#{Chef::VERSION}-i386-mingw64/lib/chef/knife/valid-too.rb", + "/opt/chefdk/embedded/lib/ruby/gems/2.1.0/gems/chef-#{Chef::VERSION}-mswin32/lib/chef/knife/also-valid.rb", + "/opt/chefdk/embedded/lib/ruby/gems/2.1.0/gems/chef-#{Chef::VERSION}-universal-mingw32/lib/chef/knife/universal-is-valid.rb", + # ...but don't ignore the .rc / .dev parts in the case when we have + # platform suffixes + "/opt/chefdk/embedded/lib/ruby/gems/2.1.0/gems/chef-#{Chef::VERSION}.rc.0-x86-mingw32/lib/chef/knife/invalid.rb", + "/opt/chefdk/embedded/lib/ruby/gems/2.1.0/gems/chef-#{Chef::VERSION}.dev-mswin32/lib/chef/knife/invalid-too.rb", + "/opt/chefdk/embedded/lib/ruby/gems/2.1.0/gems/chef-#{Chef::VERSION}.dev.0-x86-mingw64/lib/chef/knife/still-invalid.rb", + # This command is "extra" compared to what's in the embedded/apps/chef install: "/opt/chefdk/embedded/lib/ruby/gems/2.1.0/gems/chef-1.0.0/lib/chef/knife/data_bag_secret_options.rb", "/opt/chefdk/embedded/lib/ruby/gems/2.1.0/gems/chef-vault-2.2.4/lib/chef/knife/decrypt.rb", @@ -133,6 +145,10 @@ describe Chef::Knife::SubcommandLoader do "/opt/chefdk/embedded/apps/chef/lib/chef/knife/bootstrap.rb", "/opt/chefdk/embedded/apps/chef/lib/chef/knife/client_bulk_delete.rb", "/opt/chefdk/embedded/apps/chef/lib/chef/knife/client_create.rb", + "/opt/chefdk/embedded/lib/ruby/gems/2.1.0/gems/chef-#{Chef::VERSION}-x86-mingw32/lib/chef/knife/valid.rb", + "/opt/chefdk/embedded/lib/ruby/gems/2.1.0/gems/chef-#{Chef::VERSION}-i386-mingw64/lib/chef/knife/valid-too.rb", + "/opt/chefdk/embedded/lib/ruby/gems/2.1.0/gems/chef-#{Chef::VERSION}-mswin32/lib/chef/knife/also-valid.rb", + "/opt/chefdk/embedded/lib/ruby/gems/2.1.0/gems/chef-#{Chef::VERSION}-universal-mingw32/lib/chef/knife/universal-is-valid.rb", "/opt/chefdk/embedded/lib/ruby/gems/2.1.0/gems/chef-vault-2.2.4/lib/chef/knife/decrypt.rb", "/opt/chefdk/embedded/lib/ruby/gems/2.1.0/gems/knife-spork-1.4.1/lib/chef/knife/spork-bump.rb", "/opt/chefdk/embedded/lib/ruby/gems/2.1.0/gems/chef-foo-#{Chef::VERSION}/lib/chef/knife/chef-foo.rb", diff --git a/spec/unit/knife/core/ui_spec.rb b/spec/unit/knife/core/ui_spec.rb index ac42ad6dd6..ab420518a3 100644 --- a/spec/unit/knife/core/ui_spec.rb +++ b/spec/unit/knife/core/ui_spec.rb @@ -368,6 +368,20 @@ EOM @ui.config[:attribute] = "keys.keys" expect(@ui.format_for_display(input)).to eq({ "sample-data-bag-item" => { "keys.keys" => "values" } }) end + + it "should return the name attribute" do + allow_any_instance_of(Chef::Node).to receive(:name).and_return("chef.localdomain") + input = Chef::Node.new + @ui.config[:attribute] = "name" + expect(@ui.format_for_display(input)).to eq( {"chef.localdomain"=>{"name"=>"chef.localdomain"} }) + end + + it "returns nil when given an attribute path that isn't a name or attribute" do + input = { "keys" => {"keys" => "values"}, "hi" => "ho", "id" => "sample-data-bag-item" } + non_existing_path = "nope.nada.nothingtoseehere" + @ui.config[:attribute] = non_existing_path + expect(@ui.format_for_display(input)).to eq({ "sample-data-bag-item" => { non_existing_path => nil } }) + end end describe "with --run-list passed" do @@ -420,7 +434,7 @@ EOM before(:each) do stdout = double('StringIO', :tty? => true) allow(@ui).to receive(:stdout).and_return(stdout) - allow(Chef::Platform).to receive(:windows?) { true } + allow(ChefConfig).to receive(:windows?) { true } Chef::Config.reset end diff --git a/spec/unit/knife/data_bag_from_file_spec.rb b/spec/unit/knife/data_bag_from_file_spec.rb index 3882bff349..8b6502145c 100644 --- a/spec/unit/knife/data_bag_from_file_spec.rb +++ b/spec/unit/knife/data_bag_from_file_spec.rb @@ -26,7 +26,7 @@ Chef::Knife::DataBagFromFile.load_deps describe Chef::Knife::DataBagFromFile do before :each do - allow(Chef::Platform).to receive(:windows?) { false } + allow(ChefConfig).to receive(:windows?) { false } Chef::Config[:node_name] = "webmonkey.example.com" FileUtils.mkdir_p([db_folder, db_folder2]) db_file.write(Chef::JSONCompat.to_json(plain_data)) diff --git a/spec/unit/knife/environment_from_file_spec.rb b/spec/unit/knife/environment_from_file_spec.rb index d150e5ee64..11ad23c919 100644 --- a/spec/unit/knife/environment_from_file_spec.rb +++ b/spec/unit/knife/environment_from_file_spec.rb @@ -23,7 +23,7 @@ Chef::Knife::EnvironmentFromFile.load_deps describe Chef::Knife::EnvironmentFromFile do before(:each) do - allow(Chef::Platform).to receive(:windows?) { false } + allow(ChefConfig).to receive(:windows?) { false } @knife = Chef::Knife::EnvironmentFromFile.new @stdout = StringIO.new allow(@knife.ui).to receive(:stdout).and_return(@stdout) diff --git a/spec/unit/knife/key_create_spec.rb b/spec/unit/knife/key_create_spec.rb new file mode 100644 index 0000000000..5998e10274 --- /dev/null +++ b/spec/unit/knife/key_create_spec.rb @@ -0,0 +1,224 @@ +# +# Author:: Tyler Cloke (<tyler@chef.io>) +# Copyright:: Copyright (c) 2015 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/user_key_create' +require 'chef/knife/client_key_create' +require 'chef/knife/key_create' +require 'chef/key' + +describe "key create commands that inherit knife" do + shared_examples_for "a key create command" do + let(:stderr) { StringIO.new } + let(:params) { [] } + let(:service_object) { instance_double(Chef::Knife::KeyCreate) } + let(:command) do + c = described_class.new([]) + c.ui.config[:disable_editing] = true + allow(c.ui).to receive(:stderr).and_return(stderr) + allow(c.ui).to receive(:stdout).and_return(stderr) + allow(c).to receive(:show_usage) + c + end + + context "after apply_params! is called with valid args" do + let(:params) { ["charmander"] } + before do + command.apply_params!(params) + end + + context "when the service object is called" do + it "creates a new instance of Chef::Knife::KeyCreate with the correct args" do + expect(Chef::Knife::KeyCreate).to receive(:new). + with("charmander", command.actor_field_name, command.ui, command.config). + and_return(service_object) + command.service_object + end + end # when the service object is called + end # after apply_params! is called with valid args + end # a key create command + + describe Chef::Knife::UserKeyCreate do + it_should_behave_like "a key create command" + # defined in key_helper.rb + it_should_behave_like "a knife key command" do + let(:service_object) { instance_double(Chef::Knife::KeyCreate) } + let(:params) { ["charmander"] } + end + end + + describe Chef::Knife::ClientKeyCreate do + it_should_behave_like "a key create command" + # defined in key_helper.rb + it_should_behave_like "a knife key command" do + let(:service_object) { instance_double(Chef::Knife::KeyCreate) } + let(:params) { ["charmander"] } + end + end +end + +describe Chef::Knife::KeyCreate do + let(:public_key) { + "-----BEGIN PUBLIC KEY----- +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvPo+oNPB7uuNkws0fC02 +KxSwdyqPLu0fhI1pOweNKAZeEIiEz2PkybathHWy8snSXGNxsITkf3eyvIIKa8OZ +WrlqpI3yv/5DOP8HTMCxnFuMJQtDwMcevlqebX4bCxcByuBpNYDcAHjjfLGSfMjn +E5lZpgYWwnpic4kSjYcL9ORK9nYvlWV9P/kCYmRhIjB4AhtpWRiOfY/TKi3P2LxT +IjSmiN/ihHtlhV/VSnBJ5PzT/lRknlrJ4kACoz7Pq9jv+aAx5ft/xE9yDa2DYs0q +Tfuc9dUYsFjptWYrV6pfEQ+bgo1OGBXORBFcFL+2D7u9JYquKrMgosznHoEkQNLo +0wIDAQAB +-----END PUBLIC KEY-----" + } + let(:config) { Hash.new } + let(:actor) { "charmander" } + let(:ui) { instance_double("Chef::Knife::UI") } + + shared_examples_for "key create run command" do + let(:key_create_object) { + described_class.new(actor, actor_field_name, ui, config) + } + + context "when public_key and key_name weren't passed" do + it "raises a Chef::Exceptions::KeyCommandInputError with the proper error message" do + expect{ key_create_object.run }.to raise_error(Chef::Exceptions::KeyCommandInputError, key_create_object.public_key_or_key_name_error_msg) + end + end + + context "when the command is run" do + let(:expected_hash) { + { + actor_field_name => "charmander" + } + } + + before do + allow(File).to receive(:read).and_return(public_key) + allow(File).to receive(:expand_path) + + allow(key_create_object).to receive(:output_private_key_to_file) + allow(key_create_object).to receive(:display_private_key) + allow(key_create_object).to receive(:edit_data).and_return(expected_hash) + allow(key_create_object).to receive(:create_key_from_hash).and_return(Chef::Key.from_hash(expected_hash)) + allow(key_create_object).to receive(:display_info) + end + + context "when a valid hash is passed" do + let(:key_name) { "charmander-key" } + let(:valid_expiration_date) { "2020-12-24T21:00:00Z" } + let(:expected_hash) { + { + actor_field_name => "charmander", + "public_key" => public_key, + "expiration_date" => valid_expiration_date, + "key_name" => key_name + } + } + before do + key_create_object.config[:public_key] = "public_key_path" + key_create_object.config[:expiration_Date] = valid_expiration_date, + key_create_object.config[:key_name] = key_name + end + + it "creates the proper hash" do + expect(key_create_object).to receive(:create_key_from_hash).with(expected_hash) + key_create_object.run + end + end + + context "when public_key is passed" do + let(:expected_hash) { + { + actor_field_name => "charmander", + "public_key" => public_key + } + } + before do + key_create_object.config[:public_key] = "public_key_path" + end + + it "calls File.expand_path with the public_key input" do + expect(File).to receive(:expand_path).with("public_key_path") + key_create_object.run + end + end # when public_key is passed + + context "when public_key isn't passed and key_name is" do + let(:expected_hash) { + { + actor_field_name => "charmander", + "name" => "charmander-key", + "create_key" => true + } + } + before do + key_create_object.config[:key_name] = "charmander-key" + end + + it "should set create_key to true" do + expect(key_create_object).to receive(:create_key_from_hash).with(expected_hash) + key_create_object.run + end + end + + context "when the server returns a private key" do + let(:expected_hash) { + { + actor_field_name => "charmander", + "public_key" => public_key, + "private_key" => "super_private" + } + } + + before do + key_create_object.config[:public_key] = "public_key_path" + end + + context "when file is not passed" do + it "calls display_private_key with the private_key" do + expect(key_create_object).to receive(:display_private_key).with("super_private") + key_create_object.run + end + end + + context "when file is passed" do + before do + key_create_object.config[:file] = "/fake/file" + end + + it "calls output_private_key_to_file with the private_key" do + expect(key_create_object).to receive(:output_private_key_to_file).with("super_private") + key_create_object.run + end + end + end # when the server returns a private key + end # when the command is run + end #key create run command" + + context "when actor_field_name is 'user'" do + it_should_behave_like "key create run command" do + let(:actor_field_name) { "user" } + end + end + + context "when actor_field_name is 'client'" do + it_should_behave_like "key create run command" do + let(:actor_field_name) { "client" } + end + end +end + diff --git a/spec/unit/knife/key_delete_spec.rb b/spec/unit/knife/key_delete_spec.rb new file mode 100644 index 0000000000..1d4b9f825f --- /dev/null +++ b/spec/unit/knife/key_delete_spec.rb @@ -0,0 +1,135 @@ +# +# Author:: Tyler Cloke (<tyler@chef.io>) +# Copyright:: Copyright (c) 2015 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/user_key_delete' +require 'chef/knife/client_key_delete' +require 'chef/knife/key_delete' +require 'chef/key' + +describe "key delete commands that inherit knife" do + shared_examples_for "a key delete command" do + let(:stderr) { StringIO.new } + let(:params) { [] } + let(:service_object) { instance_double(Chef::Knife::KeyDelete) } + let(:command) do + c = described_class.new([]) + c.ui.config[:disable_editing] = true + allow(c.ui).to receive(:stderr).and_return(stderr) + allow(c.ui).to receive(:stdout).and_return(stderr) + allow(c).to receive(:show_usage) + c + end + + context "after apply_params! is called with valid args" do + let(:params) { ["charmander", "charmander-key"] } + before do + command.apply_params!(params) + end + + context "when the service object is called" do + it "creates a new instance of Chef::Knife::KeyDelete with the correct args" do + expect(Chef::Knife::KeyDelete).to receive(:new). + with("charmander-key", "charmander", command.actor_field_name, command.ui). + and_return(service_object) + command.service_object + end + end # when the service object is called + end # after apply_params! is called with valid args + end # a key delete command + + describe Chef::Knife::UserKeyDelete do + it_should_behave_like "a key delete command" + # defined in key_helpers.rb + it_should_behave_like "a knife key command with a keyname as the second arg" + it_should_behave_like "a knife key command" do + let(:service_object) { instance_double(Chef::Knife::KeyDelete) } + let(:params) { ["charmander", "charmander-key"] } + end + end + + describe Chef::Knife::ClientKeyDelete do + it_should_behave_like "a key delete command" + # defined in key_helpers.rb + it_should_behave_like "a knife key command with a keyname as the second arg" + it_should_behave_like "a knife key command" do + let(:service_object) { instance_double(Chef::Knife::KeyDelete) } + let(:params) { ["charmander", "charmander-key"] } + end + end +end + +describe Chef::Knife::KeyDelete do + let(:actor) { "charmander" } + let(:keyname) { "charmander-key" } + let(:ui) { instance_double("Chef::Knife::UI") } + + shared_examples_for "key delete run command" do + let(:key_delete_object) { + described_class.new(keyname, actor, actor_field_name, ui) + } + + before do + allow_any_instance_of(Chef::Key).to receive(:destroy) + allow(key_delete_object).to receive(:print_destroyed) + allow(key_delete_object).to receive(:confirm!) + end + + context "when the command is run" do + it "calls Chef::Key.new with the proper input" do + expect(Chef::Key).to receive(:new).with(actor, actor_field_name).and_call_original + key_delete_object.run + end + + it "calls name on the Chef::Key instance with the proper input" do + expect_any_instance_of(Chef::Key).to receive(:name).with(keyname) + key_delete_object.run + end + + it "calls destroy on the Chef::Key instance" do + expect_any_instance_of(Chef::Key).to receive(:destroy).once + key_delete_object.run + end + + it "calls confirm!" do + expect(key_delete_object).to receive(:confirm!) + key_delete_object.run + end + + it "calls print_destroyed" do + expect(key_delete_object).to receive(:print_destroyed) + key_delete_object.run + end + end # when the command is run + + + end # key delete run command + + context "when actor_field_name is 'user'" do + it_should_behave_like "key delete run command" do + let(:actor_field_name) { "user" } + end + end + + context "when actor_field_name is 'client'" do + it_should_behave_like "key delete run command" do + let(:actor_field_name) { "client" } + end + end +end + diff --git a/spec/unit/knife/key_edit_spec.rb b/spec/unit/knife/key_edit_spec.rb new file mode 100644 index 0000000000..538b91de2d --- /dev/null +++ b/spec/unit/knife/key_edit_spec.rb @@ -0,0 +1,267 @@ +# +# Author:: Tyler Cloke (<tyler@chef.io>) +# Copyright:: Copyright (c) 2015 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/user_key_edit' +require 'chef/knife/client_key_edit' +require 'chef/knife/key_edit' +require 'chef/key' + +describe "key edit commands that inherit knife" do + shared_examples_for "a key edit command" do + let(:stderr) { StringIO.new } + let(:params) { [] } + let(:service_object) { instance_double(Chef::Knife::KeyEdit) } + let(:command) do + c = described_class.new([]) + c.ui.config[:disable_editing] = true + allow(c.ui).to receive(:stderr).and_return(stderr) + allow(c.ui).to receive(:stdout).and_return(stderr) + allow(c).to receive(:show_usage) + c + end + + context "after apply_params! is called with valid args" do + let(:params) { ["charmander", "charmander-key"] } + before do + command.apply_params!(params) + end + + context "when the service object is called" do + it "creates a new instance of Chef::Knife::KeyEdit with the correct args" do + expect(Chef::Knife::KeyEdit).to receive(:new). + with("charmander-key", "charmander", command.actor_field_name, command.ui, command.config). + and_return(service_object) + command.service_object + end + end # when the service object is called + end # after apply_params! is called with valid args + end # a key edit command + + describe Chef::Knife::UserKeyEdit do + it_should_behave_like "a key edit command" + # defined in key_helpers.rb + it_should_behave_like "a knife key command with a keyname as the second arg" + it_should_behave_like "a knife key command" do + let(:service_object) { instance_double(Chef::Knife::KeyEdit) } + let(:params) { ["charmander", "charmander-key"] } + end + end + + describe Chef::Knife::ClientKeyEdit do + it_should_behave_like "a key edit command" + # defined in key_helpers.rb + it_should_behave_like "a knife key command with a keyname as the second arg" + it_should_behave_like "a knife key command" do + let(:service_object) { instance_double(Chef::Knife::KeyEdit) } + let(:params) { ["charmander", "charmander-key"] } + end + end +end + +describe Chef::Knife::KeyEdit do + let(:public_key) { + "-----BEGIN PUBLIC KEY----- +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvPo+oNPB7uuNkws0fC02 +KxSwdyqPLu0fhI1pOweNKAZeEIiEz2PkybathHWy8snSXGNxsITkf3eyvIIKa8OZ +WrlqpI3yv/5DOP8HTMCxnFuMJQtDwMcevlqebX4bCxcByuBpNYDcAHjjfLGSfMjn +E5lZpgYWwnpic4kSjYcL9ORK9nYvlWV9P/kCYmRhIjB4AhtpWRiOfY/TKi3P2LxT +IjSmiN/ihHtlhV/VSnBJ5PzT/lRknlrJ4kACoz7Pq9jv+aAx5ft/xE9yDa2DYs0q +Tfuc9dUYsFjptWYrV6pfEQ+bgo1OGBXORBFcFL+2D7u9JYquKrMgosznHoEkQNLo +0wIDAQAB +-----END PUBLIC KEY-----" + } + let(:config) { Hash.new } + let(:actor) { "charmander" } + let(:keyname) { "charmander-key" } + let(:ui) { instance_double("Chef::Knife::UI") } + + shared_examples_for "key edit run command" do + let(:key_edit_object) { + described_class.new(keyname, actor, actor_field_name, ui, config) + } + + context "when the command is run" do + let(:expected_hash) { + { + actor_field_name => "charmander" + } + } + let(:new_keyname) { "charizard-key" } + + before do + allow(File).to receive(:read).and_return(public_key) + allow(File).to receive(:expand_path) + + allow(key_edit_object).to receive(:output_private_key_to_file) + allow(key_edit_object).to receive(:display_private_key) + allow(key_edit_object).to receive(:edit_data).and_return(expected_hash) + allow(key_edit_object).to receive(:display_info) + end + + + context "when public_key and create_key are passed" do + before do + key_edit_object.config[:public_key] = "public_key_path" + key_edit_object.config[:create_key] = true + end + + it "raises a Chef::Exceptions::KeyCommandInputError with the proper error message" do + expect{ key_edit_object.run }.to raise_error(Chef::Exceptions::KeyCommandInputError, key_edit_object.public_key_and_create_key_error_msg) + end + end + + context "when key_name is passed" do + let(:expected_hash) { + { + actor_field_name => "charmander", + "name" => new_keyname + } + } + before do + key_edit_object.config[:key_name] = new_keyname + allow_any_instance_of(Chef::Key).to receive(:update) + end + + it "update_key_from_hash gets passed a hash with new key name" do + expect(key_edit_object).to receive(:update_key_from_hash).with(expected_hash).and_return(Chef::Key.from_hash(expected_hash)) + key_edit_object.run + end + + it "Chef::Key.update is passed a string containing the original keyname" do + expect_any_instance_of(Chef::Key).to receive(:update).with(/#{keyname}/).and_return(Chef::Key.from_hash(expected_hash)) + key_edit_object.run + end + + it "Chef::Key.update is not passed a string containing the new keyname" do + expect_any_instance_of(Chef::Key).not_to receive(:update).with(/#{new_keyname}/) + allow_any_instance_of(Chef::Key).to receive(:update).and_return(Chef::Key.from_hash(expected_hash)) + key_edit_object.run + end + end + + context "when public_key, key_name, and expiration_date are passed" do + let(:expected_hash) { + { + actor_field_name => "charmander", + "public_key" => public_key, + "name" => new_keyname, + "expiration_date" => "infinity" + } + } + before do + key_edit_object.config[:public_key] = "this-public-key" + key_edit_object.config[:key_name] = new_keyname + key_edit_object.config[:expiration_date] = "infinity" + allow(key_edit_object).to receive(:update_key_from_hash).and_return(Chef::Key.from_hash(expected_hash)) + end + + it "passes the right hash to update_key_from_hash" do + expect(key_edit_object).to receive(:update_key_from_hash).with(expected_hash) + key_edit_object.run + end + end + + context "when create_key is passed" do + let(:expected_hash) { + { + actor_field_name => "charmander", + "create_key" => true + } + } + + before do + key_edit_object.config[:create_key] = true + allow(key_edit_object).to receive(:update_key_from_hash).and_return(Chef::Key.from_hash(expected_hash)) + end + + it "passes the right hash to update_key_from_hash" do + expect(key_edit_object).to receive(:update_key_from_hash).with(expected_hash) + key_edit_object.run + end + end + + context "when public_key is passed" do + let(:expected_hash) { + { + actor_field_name => "charmander", + "public_key" => public_key + } + } + before do + allow(key_edit_object).to receive(:update_key_from_hash).and_return(Chef::Key.from_hash(expected_hash)) + key_edit_object.config[:public_key] = "public_key_path" + end + + it "calls File.expand_path with the public_key input" do + expect(File).to receive(:expand_path).with("public_key_path") + key_edit_object.run + end + end # when public_key is passed + + context "when the server returns a private key" do + let(:expected_hash) { + { + actor_field_name => "charmander", + "public_key" => public_key, + "private_key" => "super_private" + } + } + + before do + allow(key_edit_object).to receive(:update_key_from_hash).and_return(Chef::Key.from_hash(expected_hash)) + key_edit_object.config[:public_key] = "public_key_path" + end + + context "when file is not passed" do + it "calls display_private_key with the private_key" do + expect(key_edit_object).to receive(:display_private_key).with("super_private") + key_edit_object.run + end + end + + context "when file is passed" do + before do + key_edit_object.config[:file] = "/fake/file" + end + + it "calls output_private_key_to_file with the private_key" do + expect(key_edit_object).to receive(:output_private_key_to_file).with("super_private") + key_edit_object.run + end + end + end # when the server returns a private key + + end # when the command is run + + + + end # key edit run command + + context "when actor_field_name is 'user'" do + it_should_behave_like "key edit run command" do + let(:actor_field_name) { "user" } + end + end + + context "when actor_field_name is 'client'" do + it_should_behave_like "key edit run command" do + let(:actor_field_name) { "client" } + end + end +end diff --git a/spec/unit/knife/key_helper.rb b/spec/unit/knife/key_helper.rb new file mode 100644 index 0000000000..36ababc09a --- /dev/null +++ b/spec/unit/knife/key_helper.rb @@ -0,0 +1,74 @@ +# +# Author:: Tyler Cloke (<tyler@chef.io>) +# Copyright:: Copyright (c) 2015 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' + +shared_examples_for "a knife key command" do + let(:stderr) { StringIO.new } + let(:params) { [] } + let(:command) do + c = described_class.new([]) + c.ui.config[:disable_editing] = true + allow(c.ui).to receive(:stderr).and_return(stderr) + allow(c.ui).to receive(:stdout).and_return(stderr) + allow(c).to receive(:show_usage) + c + end + + context "before apply_params! is called" do + context "when apply_params! is called with invalid args" do + it "shows the usage" do + expect(command).to receive(:show_usage) + expect { command.apply_params!(params) }.to exit_with_code(1) + end + + it "outputs the proper error" do + expect { command.apply_params!(params) }.to exit_with_code(1) + expect(stderr.string).to include(command.actor_missing_error) + end + + it "exits 1" do + expect { command.apply_params!(params) }.to exit_with_code(1) + end + end + end # before apply_params! is called + + context "after apply_params! is called with valid args" do + let(:params) { ["charmander"] } + before do + command.apply_params!(params) + end + + it "properly defines the actor" do + expect(command.actor).to eq("charmander") + end + end # after apply_params! is called with valid args + + context "when the command is run" do + before do + allow(command).to receive(:service_object).and_return(service_object) + allow(command).to receive(:name_args).and_return(["charmander"]) + end + + context "when the command is successful" do + before do + expect(service_object).to receive(:run) + end + end + end +end # a knife key command diff --git a/spec/unit/knife/key_list_spec.rb b/spec/unit/knife/key_list_spec.rb new file mode 100644 index 0000000000..aabe02ac02 --- /dev/null +++ b/spec/unit/knife/key_list_spec.rb @@ -0,0 +1,216 @@ +# +# Author:: Tyler Cloke (<tyler@chef.io>) +# Copyright:: Copyright (c) 2015 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/user_key_list' +require 'chef/knife/client_key_list' +require 'chef/knife/key_list' +require 'chef/key' + +describe "key list commands that inherit knife" do + shared_examples_for "a key list command" do + let(:stderr) { StringIO.new } + let(:params) { [] } + let(:service_object) { instance_double(Chef::Knife::KeyList) } + let(:command) do + c = described_class.new([]) + c.ui.config[:disable_editing] = true + allow(c.ui).to receive(:stderr).and_return(stderr) + allow(c.ui).to receive(:stdout).and_return(stderr) + allow(c).to receive(:show_usage) + c + end + + context "after apply_params! is called with valid args" do + let(:params) { ["charmander"] } + before do + command.apply_params!(params) + end + + context "when the service object is called" do + it "creates a new instance of Chef::Knife::KeyList with the correct args" do + expect(Chef::Knife::KeyList).to receive(:new). + with("charmander", command.list_method, command.ui, command.config). + and_return(service_object) + command.service_object + end + end # when the service object is called + end # after apply_params! is called with valid args + end # a key list command + + describe Chef::Knife::UserKeyList do + it_should_behave_like "a key list command" + # defined in key_helpers.rb + it_should_behave_like "a knife key command" do + let(:service_object) { instance_double(Chef::Knife::KeyList) } + let(:params) { ["charmander"] } + end + end + + describe Chef::Knife::ClientKeyList do + it_should_behave_like "a key list command" + # defined in key_helpers.rb + it_should_behave_like "a knife key command" do + let(:service_object) { instance_double(Chef::Knife::KeyList) } + let(:params) { ["charmander"] } + end + end +end + +describe Chef::Knife::KeyList do + let(:config) { Hash.new } + let(:actor) { "charmander" } + let(:ui) { instance_double("Chef::Knife::UI") } + + shared_examples_for "key list run command" do + let(:key_list_object) { + described_class.new(actor, list_method, ui, config) + } + + before do + allow(Chef::Key).to receive(list_method).and_return(http_response) + allow(key_list_object).to receive(:display_info) + # simply pass the string though that colorize takes in + allow(key_list_object).to receive(:colorize).with(kind_of(String)) do |input| + input + end + end + + context "when only_expired and only_non_expired were both passed" do + before do + key_list_object.config[:only_expired] = true + key_list_object.config[:only_non_expired] = true + end + + it "raises a Chef::Exceptions::KeyCommandInputError with the proper error message" do + expect{ key_list_object.run }.to raise_error(Chef::Exceptions::KeyCommandInputError, key_list_object.expired_and_non_expired_msg) + end + end + + context "when the command is run" do + before do + key_list_object.config[:only_expired] = false + key_list_object.config[:only_non_expired] = false + key_list_object.config[:with_details] = false + end + + it "calls Chef::Key with the proper list command and input" do + expect(Chef::Key).to receive(list_method).with(actor) + key_list_object.run + end + + it "displays all the keys" do + expect(key_list_object).to receive(:display_info).with(/non-expired/).twice + expect(key_list_object).to receive(:display_info).with(/out-of-date/).once + key_list_object.run + end + + context "when only_expired is called" do + before do + key_list_object.config[:only_expired] = true + end + + it "excludes displaying non-expired keys" do + expect(key_list_object).to receive(:display_info).with(/non-expired/).exactly(0).times + key_list_object.run + end + + it "displays the expired keys" do + expect(key_list_object).to receive(:display_info).with(/out-of-date/).once + key_list_object.run + end + end # when only_expired is called + + context "when only_non_expired is called" do + before do + key_list_object.config[:only_non_expired] = true + end + + it "excludes displaying expired keys" do + expect(key_list_object).to receive(:display_info).with(/out-of-date/).exactly(0).times + key_list_object.run + end + + it "displays the non-expired keys" do + expect(key_list_object).to receive(:display_info).with(/non-expired/).twice + key_list_object.run + end + end # when only_expired is called + + context "when with_details is false" do + before do + key_list_object.config[:with_details] = false + end + + it "does not display the uri" do + expect(key_list_object).to receive(:display_info).with(/https/).exactly(0).times + key_list_object.run + end + + it "does not display the expired status" do + expect(key_list_object).to receive(:display_info).with(/\(expired\)/).exactly(0).times + key_list_object.run + end + end # when with_details is false + + context "when with_details is true" do + before do + key_list_object.config[:with_details] = true + end + + it "displays the uri" do + expect(key_list_object).to receive(:display_info).with(/https/).exactly(3).times + key_list_object.run + end + + it "displays the expired status" do + expect(key_list_object).to receive(:display_info).with(/\(expired\)/).once + key_list_object.run + end + end # when with_details is true + + end # when the command is run + + end # key list run command + + context "when list_method is :list_by_user" do + it_should_behave_like "key list run command" do + let(:list_method) { :list_by_user } + let(:http_response) { + [ + {"uri"=>"https://api.opscode.piab/users/charmander/keys/non-expired1", "name"=>"non-expired1", "expired"=>false}, + {"uri"=>"https://api.opscode.piab/users/charmander/keys/non-expired2", "name"=>"non-expired2", "expired"=>false}, + {"uri"=>"https://api.opscode.piab/users/mary/keys/out-of-date", "name"=>"out-of-date", "expired"=>true} + ] + } + end + end + + context "when list_method is :list_by_client" do + it_should_behave_like "key list run command" do + let(:list_method) { :list_by_client } + let(:http_response) { + [ + {"uri"=>"https://api.opscode.piab/organizations/pokemon/clients/charmander/keys/non-expired1", "name"=>"non-expired1", "expired"=>false}, + {"uri"=>"https://api.opscode.piab/organizations/pokemon/clients/charmander/keys/non-expired2", "name"=>"non-expired2", "expired"=>false}, + {"uri"=>"https://api.opscode.piab/organizations/pokemon/clients/mary/keys/out-of-date", "name"=>"out-of-date", "expired"=>true} + ] + } + end + end +end diff --git a/spec/unit/knife/key_show_spec.rb b/spec/unit/knife/key_show_spec.rb new file mode 100644 index 0000000000..5a0d839e4f --- /dev/null +++ b/spec/unit/knife/key_show_spec.rb @@ -0,0 +1,126 @@ +# +# Author:: Tyler Cloke (<tyler@chef.io>) +# Copyright:: Copyright (c) 2015 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/user_key_show' +require 'chef/knife/client_key_show' +require 'chef/knife/key_show' +require 'chef/key' + +describe "key show commands that inherit knife" do + shared_examples_for "a key show command" do + let(:stderr) { StringIO.new } + let(:params) { [] } + let(:service_object) { instance_double(Chef::Knife::KeyShow) } + let(:command) do + c = described_class.new([]) + c.ui.config[:disable_editing] = true + allow(c.ui).to receive(:stderr).and_return(stderr) + allow(c.ui).to receive(:stdout).and_return(stderr) + allow(c).to receive(:show_usage) + c + end + + context "after apply_params! is called with valid args" do + let(:params) { ["charmander", "charmander-key"] } + before do + command.apply_params!(params) + end + + context "when the service object is called" do + it "creates a new instance of Chef::Knife::KeyShow with the correct args" do + expect(Chef::Knife::KeyShow).to receive(:new). + with("charmander-key", "charmander", command.load_method, command.ui). + and_return(service_object) + command.service_object + end + end # when the service object is called + end # after apply_params! is called with valid args + end # a key show command + + describe Chef::Knife::UserKeyShow do + it_should_behave_like "a key show command" + # defined in key_helpers.rb + it_should_behave_like "a knife key command with a keyname as the second arg" + it_should_behave_like "a knife key command" do + let(:service_object) { instance_double(Chef::Knife::KeyShow) } + let(:params) { ["charmander", "charmander-key"] } + end + end + + describe Chef::Knife::ClientKeyShow do + it_should_behave_like "a key show command" + # defined in key_helpers.rb + it_should_behave_like "a knife key command with a keyname as the second arg" + it_should_behave_like "a knife key command" do + let(:service_object) { instance_double(Chef::Knife::KeyShow) } + let(:params) { ["charmander", "charmander-key"] } + end + end +end + +describe Chef::Knife::KeyShow do + let(:actor) { "charmander" } + let(:keyname) { "charmander" } + let(:ui) { instance_double("Chef::Knife::UI") } + let(:expected_hash) { + { + actor_field_name => "charmander", + "name" => "charmander-key", + "public_key" => "some-public-key", + "expiration_date" => "infinity" + } + } + + shared_examples_for "key show run command" do + let(:key_show_object) { + described_class.new(keyname, actor, load_method, ui) + } + + before do + allow(key_show_object).to receive(:display_output) + allow(Chef::Key).to receive(load_method).and_return(Chef::Key.from_hash(expected_hash)) + end + + context "when the command is run" do + it "loads the key using the proper method and args" do + expect(Chef::Key).to receive(load_method).with(actor, keyname) + key_show_object.run + end + + it "displays the key" do + expect(key_show_object).to receive(:display_output) + key_show_object.run + end + end + end + + context "when load_method is :load_by_user" do + it_should_behave_like "key show run command" do + let(:load_method) { :load_by_user } + let(:actor_field_name) { 'user' } + end + end + + context "when load_method is :load_by_client" do + it_should_behave_like "key show run command" do + let(:load_method) { :load_by_client } + let(:actor_field_name) { 'user' } + end + end +end diff --git a/spec/unit/knife/osc_user_create_spec.rb b/spec/unit/knife/osc_user_create_spec.rb new file mode 100644 index 0000000000..1b17d0d22f --- /dev/null +++ b/spec/unit/knife/osc_user_create_spec.rb @@ -0,0 +1,93 @@ +# +# Author:: Steven Danna (<steve@opscode.com>) +# Copyright:: Copyright (c) 2012 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' + +Chef::Knife::OscUserCreate.load_deps + +# DEPRECATION NOTE +# This code only remains to support users still operating with +# Open Source Chef Server 11 and should be removed once support +# for OSC 11 ends. New development should occur in user_create_spec.rb. + +describe Chef::Knife::OscUserCreate do + before(:each) do + @knife = Chef::Knife::OscUserCreate.new + + @stdout = StringIO.new + @stderr = StringIO.new + allow(@knife.ui).to receive(:stdout).and_return(@stdout) + allow(@knife.ui).to receive(:stderr).and_return(@stderr) + + @knife.name_args = [ 'a_user' ] + @knife.config[:user_password] = "foobar" + @user = Chef::OscUser.new + @user.name "a_user" + @user_with_private_key = Chef::OscUser.new + @user_with_private_key.name "a_user" + @user_with_private_key.private_key 'private_key' + allow(@user).to receive(:create).and_return(@user_with_private_key) + allow(Chef::OscUser).to receive(:new).and_return(@user) + allow(Chef::OscUser).to receive(:from_hash).and_return(@user) + allow(@knife).to receive(:edit_data).and_return(@user.to_hash) + end + + it "creates a new user" do + expect(Chef::OscUser).to receive(:new).and_return(@user) + expect(@user).to receive(:create) + @knife.run + expect(@stderr.string).to match /created user.+a_user/i + end + + it "sets the password" do + @knife.config[:user_password] = "a_password" + expect(@user).to receive(:password).with("a_password") + @knife.run + end + + it "exits with an error if password is blank" do + @knife.config[:user_password] = '' + expect { @knife.run }.to raise_error SystemExit + expect(@stderr.string).to match /You must specify a non-blank password/ + end + + it "sets the user name" do + expect(@user).to receive(:name).with("a_user") + @knife.run + end + + it "sets the public key if given" do + @knife.config[:user_key] = "/a/filename" + allow(File).to receive(:read).with(File.expand_path("/a/filename")).and_return("a_key") + expect(@user).to receive(:public_key).with("a_key") + @knife.run + end + + it "allows you to edit the data" do + expect(@knife).to receive(:edit_data).with(@user) + @knife.run + end + + it "writes the private key to a file when --file is specified" do + @knife.config[:file] = "/tmp/a_file" + filehandle = double("filehandle") + expect(filehandle).to receive(:print).with('private_key') + expect(File).to receive(:open).with("/tmp/a_file", "w").and_yield(filehandle) + @knife.run + end +end diff --git a/spec/unit/knife/osc_user_delete_spec.rb b/spec/unit/knife/osc_user_delete_spec.rb new file mode 100644 index 0000000000..0e16393ffe --- /dev/null +++ b/spec/unit/knife/osc_user_delete_spec.rb @@ -0,0 +1,44 @@ +# +# Author:: Steven Danna (<steve@opscode.com>) +# Copyright:: Copyright (c) 2012 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' + +# DEPRECATION NOTE +# This code only remains to support users still operating with +# Open Source Chef Server 11 and should be removed once support +# for OSC 11 ends. New development should occur in user_delete_spec.rb. + +describe Chef::Knife::OscUserDelete do + before(:each) do + Chef::Knife::OscUserDelete.load_deps + @knife = Chef::Knife::OscUserDelete.new + @knife.name_args = [ 'my_user' ] + end + + it 'deletes the user' do + expect(@knife).to receive(:delete_object).with(Chef::OscUser, 'my_user') + @knife.run + end + + it 'prints usage and exits when a user name is not provided' do + @knife.name_args = [] + expect(@knife).to receive(:show_usage) + expect(@knife.ui).to receive(:fatal) + expect { @knife.run }.to raise_error(SystemExit) + end +end diff --git a/spec/unit/knife/osc_user_edit_spec.rb b/spec/unit/knife/osc_user_edit_spec.rb new file mode 100644 index 0000000000..71a9192389 --- /dev/null +++ b/spec/unit/knife/osc_user_edit_spec.rb @@ -0,0 +1,52 @@ +# +# Author:: Steven Danna (<steve@opscode.com>) +# Copyright:: Copyright (c) 2012 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' + +# DEPRECATION NOTE +# This code only remains to support users still operating with +# Open Source Chef Server 11 and should be removed once support +# for OSC 11 ends. New development should occur in user_edit_spec.rb. + +describe Chef::Knife::OscUserEdit do + before(:each) do + @stderr = StringIO.new + @stdout = StringIO.new + + Chef::Knife::OscUserEdit.load_deps + @knife = Chef::Knife::OscUserEdit.new + allow(@knife.ui).to receive(:stderr).and_return(@stderr) + allow(@knife.ui).to receive(:stdout).and_return(@stdout) + @knife.name_args = [ 'my_user' ] + @knife.config[:disable_editing] = true + end + + it 'loads and edits the user' do + data = { :name => "my_user" } + allow(Chef::OscUser).to receive(:load).with("my_user").and_return(data) + expect(@knife).to receive(:edit_data).with(data).and_return(data) + @knife.run + end + + it 'prints usage and exits when a user name is not provided' do + @knife.name_args = [] + expect(@knife).to receive(:show_usage) + expect(@knife.ui).to receive(:fatal) + expect { @knife.run }.to raise_error(SystemExit) + end +end diff --git a/spec/unit/knife/osc_user_list_spec.rb b/spec/unit/knife/osc_user_list_spec.rb new file mode 100644 index 0000000000..59a15be058 --- /dev/null +++ b/spec/unit/knife/osc_user_list_spec.rb @@ -0,0 +1,37 @@ +# +# Author:: Steven Danna +# Copyright:: Copyright (c) 2012 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' + +# DEPRECATION NOTE +# This code only remains to support users still operating with +# Open Source Chef Server 11 and should be removed once support +# for OSC 11 ends. New development should occur in user_list_spec.rb. + +describe Chef::Knife::OscUserList do + before(:each) do + Chef::Knife::OscUserList.load_deps + @knife = Chef::Knife::OscUserList.new + end + + it 'lists the users' do + expect(Chef::OscUser).to receive(:list) + expect(@knife).to receive(:format_list_for_display) + @knife.run + end +end diff --git a/spec/unit/knife/osc_user_reregister_spec.rb b/spec/unit/knife/osc_user_reregister_spec.rb new file mode 100644 index 0000000000..406bbf1f3e --- /dev/null +++ b/spec/unit/knife/osc_user_reregister_spec.rb @@ -0,0 +1,58 @@ +# +# Author:: Steven Danna (<steve@opscode.com>) +# Copyright:: Copyright (c) 2012 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' + +# DEPRECATION NOTE +# This code only remains to support users still operating with +# Open Source Chef Server 11 and should be removed once support +# for OSC 11 ends. New development should occur in user_reregister_spec.rb. + +describe Chef::Knife::OscUserReregister do + before(:each) do + Chef::Knife::OscUserReregister.load_deps + @knife = Chef::Knife::OscUserReregister.new + @knife.name_args = [ 'a_user' ] + @user_mock = double('user_mock', :private_key => "private_key") + allow(Chef::OscUser).to receive(:load).and_return(@user_mock) + @stdout = StringIO.new + allow(@knife.ui).to receive(:stdout).and_return(@stdout) + end + + it 'prints usage and exits when a user name is not provided' do + @knife.name_args = [] + expect(@knife).to receive(:show_usage) + expect(@knife.ui).to receive(:fatal) + expect { @knife.run }.to raise_error(SystemExit) + end + + it 'reregisters the user and prints the key' do + expect(@user_mock).to receive(:reregister).and_return(@user_mock) + @knife.run + expect(@stdout.string).to match( /private_key/ ) + end + + it 'writes the private key to a file when --file is specified' do + expect(@user_mock).to receive(:reregister).and_return(@user_mock) + @knife.config[:file] = '/tmp/a_file' + filehandle = StringIO.new + expect(File).to receive(:open).with('/tmp/a_file', 'w').and_yield(filehandle) + @knife.run + expect(filehandle.string).to eq("private_key") + end +end diff --git a/spec/unit/knife/osc_user_show_spec.rb b/spec/unit/knife/osc_user_show_spec.rb new file mode 100644 index 0000000000..67b9b45809 --- /dev/null +++ b/spec/unit/knife/osc_user_show_spec.rb @@ -0,0 +1,46 @@ +# +# Author:: Steven Danna (<steve@opscode.com>) +# Copyright:: Copyright (c) 2012 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' + +# DEPRECATION NOTE +# This code only remains to support users still operating with +# Open Source Chef Server 11 and should be removed once support +# for OSC 11 ends. New development should occur user_show_spec.rb. + +describe Chef::Knife::OscUserShow do + before(:each) do + Chef::Knife::OscUserShow.load_deps + @knife = Chef::Knife::OscUserShow.new + @knife.name_args = [ 'my_user' ] + @user_mock = double('user_mock') + end + + it 'loads and displays the user' do + expect(Chef::OscUser).to receive(:load).with('my_user').and_return(@user_mock) + expect(@knife).to receive(:format_for_display).with(@user_mock) + @knife.run + end + + it 'prints usage and exits when a user name is not provided' do + @knife.name_args = [] + expect(@knife).to receive(:show_usage) + expect(@knife.ui).to receive(:fatal) + expect { @knife.run }.to raise_error(SystemExit) + end +end diff --git a/spec/unit/knife/ssh_spec.rb b/spec/unit/knife/ssh_spec.rb index a838a21edc..723280bead 100644 --- a/spec/unit/knife/ssh_spec.rb +++ b/spec/unit/knife/ssh_spec.rb @@ -28,10 +28,10 @@ describe Chef::Knife::Ssh do before do @knife = Chef::Knife::Ssh.new @knife.merge_configs - @knife.config[:attribute] = "fqdn" @node_foo = Chef::Node.new @node_foo.automatic_attrs[:fqdn] = "foo.example.org" @node_foo.automatic_attrs[:ipaddress] = "10.0.0.1" + @node_bar = Chef::Node.new @node_bar.automatic_attrs[:fqdn] = "bar.example.org" @node_bar.automatic_attrs[:ipaddress] = "10.0.0.2" @@ -52,15 +52,15 @@ describe Chef::Knife::Ssh do def self.should_return_specified_attributes it "returns an array of the attributes specified on the command line OR config file, if only one is set" do @knife.config[:attribute] = "ipaddress" - @knife.config[:attribute_from_cli] = "ipaddress" + Chef::Config[:knife][:ssh_attribute] = "ipaddress" # this value will be in the config file configure_query([@node_foo, @node_bar]) expect(@knife).to receive(:session_from_list).with([['10.0.0.1', nil], ['10.0.0.2', nil]]) @knife.configure_session end it "returns an array of the attributes specified on the command line even when a config value is set" do - @knife.config[:attribute] = "config_file" # this value will be the config file - @knife.config[:attribute_from_cli] = "ipaddress" # this is the value of the command line via #configure_attribute + Chef::Config[:knife][:ssh_attribute] = "config_file" # this value will be in the config file + @knife.config[:attribute] = "ipaddress" # this is the value of the command line via #configure_attribute configure_query([@node_foo, @node_bar]) expect(@knife).to receive(:session_from_list).with([['10.0.0.1', nil], ['10.0.0.2', nil]]) @knife.configure_session @@ -83,7 +83,6 @@ describe Chef::Knife::Ssh do @node_foo.automatic_attrs[:cloud][:public_hostname] = "ec2-10-0-0-1.compute-1.amazonaws.com" @node_bar.automatic_attrs[:cloud][:public_hostname] = "ec2-10-0-0-2.compute-1.amazonaws.com" end - it "returns an array of cloud public hostnames" do configure_query([@node_foo, @node_bar]) expect(@knife).to receive(:session_from_list).with([ @@ -150,42 +149,40 @@ describe Chef::Knife::Ssh do end end - describe "#configure_attribute" do + describe "#get_ssh_attribute" do + # Order of precedence for ssh target + # 1) command line attribute + # 2) configuration file + # 3) cloud attribute + # 4) fqdn before do Chef::Config[:knife][:ssh_attribute] = nil @knife.config[:attribute] = nil + @node_foo.automatic_attrs[:cloud][:public_hostname] = "ec2-10-0-0-1.compute-1.amazonaws.com" + @node_bar.automatic_attrs[:cloud][:public_hostname] = '' end it "should return fqdn by default" do - @knife.configure_attribute - expect(@knife.config[:attribute]).to eq("fqdn") + expect(@knife.get_ssh_attribute(Chef::Node.new)).to eq("fqdn") end - it "should return the value set in the configuration file" do - Chef::Config[:knife][:ssh_attribute] = "config_file" - @knife.configure_attribute - expect(@knife.config[:attribute]).to eq("config_file") + it "should return cloud.public_hostname attribute if available" do + expect(@knife.get_ssh_attribute(@node_foo)).to eq("cloud.public_hostname") end - it "should return the value set on the command line" do + it "should favor to attribute_from_cli over config file and cloud" do @knife.config[:attribute] = "command_line" - @knife.configure_attribute - expect(@knife.config[:attribute]).to eq("command_line") + Chef::Config[:knife][:ssh_attribute] = "config_file" + expect( @knife.get_ssh_attribute(@node_foo)).to eq("command_line") end - it "should set attribute_from_cli to the value of attribute from the command line" do - @knife.config[:attribute] = "command_line" - @knife.configure_attribute - expect(@knife.config[:attribute]).to eq("command_line") - expect(@knife.config[:attribute_from_cli]).to eq("command_line") + it "should favor config file over cloud and default" do + Chef::Config[:knife][:ssh_attribute] = "config_file" + expect( @knife.get_ssh_attribute(@node_foo)).to eq("config_file") end - it "should prefer the command line over the config file for the value of attribute_from_cli" do - Chef::Config[:knife][:ssh_attribute] = "config_file" - @knife.config[:attribute] = "command_line" - @knife.configure_attribute - expect(@knife.config[:attribute]).to eq("command_line") - expect(@knife.config[:attribute_from_cli]).to eq("command_line") + it "should return fqdn if cloud.hostname is empty" do + expect( @knife.get_ssh_attribute(@node_bar)).to eq("fqdn") end end diff --git a/spec/unit/knife/user_create_spec.rb b/spec/unit/knife/user_create_spec.rb index ad8821cd0e..49d62cc2d7 100644 --- a/spec/unit/knife/user_create_spec.rb +++ b/spec/unit/knife/user_create_spec.rb @@ -1,6 +1,7 @@ # -# Author:: Steven Danna (<steve@opscode.com>) -# Copyright:: Copyright (c) 2012 Opscode, Inc. +# Author:: Steven Danna (<steve@chef.io>) +# Author:: Tyler Cloke (<tyler@chef.io>) +# Copyright:: Copyright (c) 2012, 2015 Chef Software, Inc. # License:: Apache License, Version 2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -21,68 +22,193 @@ require 'spec_helper' Chef::Knife::UserCreate.load_deps describe Chef::Knife::UserCreate do + let(:knife) { Chef::Knife::UserCreate.new } + + let(:stderr) { + StringIO.new + } + + let(:stdout) { + StringIO.new + } + before(:each) do - @knife = Chef::Knife::UserCreate.new - - @stdout = StringIO.new - @stderr = StringIO.new - allow(@knife.ui).to receive(:stdout).and_return(@stdout) - allow(@knife.ui).to receive(:stderr).and_return(@stderr) - - @knife.name_args = [ 'a_user' ] - @knife.config[:user_password] = "foobar" - @user = Chef::User.new - @user.name "a_user" - @user_with_private_key = Chef::User.new - @user_with_private_key.name "a_user" - @user_with_private_key.private_key 'private_key' - allow(@user).to receive(:create).and_return(@user_with_private_key) - allow(Chef::User).to receive(:new).and_return(@user) - allow(Chef::User).to receive(:from_hash).and_return(@user) - allow(@knife).to receive(:edit_data).and_return(@user.to_hash) + allow(knife.ui).to receive(:stdout).and_return(stdout) + allow(knife.ui).to receive(:stderr).and_return(stderr) + allow(knife.ui).to receive(:warn) end - it "creates a new user" do - expect(Chef::User).to receive(:new).and_return(@user) - expect(@user).to receive(:create) - @knife.run - expect(@stderr.string).to match /created user.+a_user/i - end + # delete this once OSC11 support is gone + context "when only one name_arg is passed" do + before do + knife.name_args = ['some_user'] + allow(knife).to receive(:run_osc_11_user_create).and_raise(SystemExit) + end + + it "displays the osc warning" do + expect(knife.ui).to receive(:warn).with(knife.osc_11_warning) + expect{ knife.run }.to raise_error(SystemExit) + end + + it "calls knife osc_user create" do + expect(knife).to receive(:run_osc_11_user_create) + expect{ knife.run }.to raise_error(SystemExit) + end - it "sets the password" do - @knife.config[:user_password] = "a_password" - expect(@user).to receive(:password).with("a_password") - @knife.run end - it "exits with an error if password is blank" do - @knife.config[:user_password] = '' - expect { @knife.run }.to raise_error SystemExit - expect(@stderr.string).to match /You must specify a non-blank password/ + context "when USERNAME isn't specified" do + # from spec/support/shared/unit/knife_shared.rb + it_should_behave_like "mandatory field missing" do + let(:name_args) { [] } + let(:fieldname) { 'username' } + end end - it "sets the user name" do - expect(@user).to receive(:name).with("a_user") - @knife.run + # uncomment once OSC11 support is gone, + # pending doesn't work for shared_examples_for by default + # + # context "when DISPLAY_NAME isn't specified" do + # # from spec/support/shared/unit/knife_shared.rb + # it_should_behave_like "mandatory field missing" do + # let(:name_args) { ['some_user'] } + # let(:fieldname) { 'display name' } + # end + # end + + context "when FIRST_NAME isn't specified" do + # from spec/support/shared/unit/knife_shared.rb + it_should_behave_like "mandatory field missing" do + let(:name_args) { ['some_user', 'some_display_name'] } + let(:fieldname) { 'first name' } + end end - it "sets the public key if given" do - @knife.config[:user_key] = "/a/filename" - allow(File).to receive(:read).with(File.expand_path("/a/filename")).and_return("a_key") - expect(@user).to receive(:public_key).with("a_key") - @knife.run + context "when LAST_NAME isn't specified" do + # from spec/support/shared/unit/knife_shared.rb + it_should_behave_like "mandatory field missing" do + let(:name_args) { ['some_user', 'some_display_name', 'some_first_name'] } + let(:fieldname) { 'last name' } + end end - it "allows you to edit the data" do - expect(@knife).to receive(:edit_data).with(@user) - @knife.run + context "when EMAIL isn't specified" do + # from spec/support/shared/unit/knife_shared.rb + it_should_behave_like "mandatory field missing" do + let(:name_args) { ['some_user', 'some_display_name', 'some_first_name', 'some_last_name'] } + let(:fieldname) { 'email' } + end end - it "writes the private key to a file when --file is specified" do - @knife.config[:file] = "/tmp/a_file" - filehandle = double("filehandle") - expect(filehandle).to receive(:print).with('private_key') - expect(File).to receive(:open).with("/tmp/a_file", "w").and_yield(filehandle) - @knife.run + context "when PASSWORD isn't specified" do + # from spec/support/shared/unit/knife_shared.rb + it_should_behave_like "mandatory field missing" do + let(:name_args) { ['some_user', 'some_display_name', 'some_first_name', 'some_last_name', 'some_email'] } + let(:fieldname) { 'password' } + end end + + context "when all mandatory fields are validly specified" do + before do + knife.name_args = ['some_user', 'some_display_name', 'some_first_name', 'some_last_name', 'some_email', 'some_password'] + allow(knife).to receive(:edit_data).and_return(knife.user.to_hash) + allow(knife).to receive(:create_user_from_hash).and_return(knife.user) + end + + before(:each) do + # reset the user field every run + knife.user_field = nil + end + + it "sets all the mandatory fields" do + knife.run + expect(knife.user.username).to eq('some_user') + expect(knife.user.display_name).to eq('some_display_name') + expect(knife.user.first_name).to eq('some_first_name') + expect(knife.user.last_name).to eq('some_last_name') + expect(knife.user.email).to eq('some_email') + expect(knife.user.password).to eq('some_password') + end + + context "when user_key and prevent_keygen are passed" do + before do + knife.config[:user_key] = "some_key" + knife.config[:prevent_keygen] = true + end + it "prints the usage" do + expect(knife).to receive(:show_usage) + expect { knife.run }.to raise_error(SystemExit) + end + + it "prints a relevant error message" do + expect { knife.run }.to raise_error(SystemExit) + expect(stderr.string).to match /You cannot pass --user-key and --prevent-keygen/ + end + end + + context "when --prevent-keygen is passed" do + before do + knife.config[:prevent_keygen] = true + end + + it "does not set user.create_key" do + knife.run + expect(knife.user.create_key).to be_falsey + end + end + + context "when --prevent-keygen is not passed" do + it "sets user.create_key to true" do + knife.run + expect(knife.user.create_key).to be_truthy + end + end + + context "when --user-key is passed" do + before do + knife.config[:user_key] = 'some_key' + allow(File).to receive(:read).and_return('some_key') + allow(File).to receive(:expand_path) + end + + it "sets user.public_key" do + knife.run + expect(knife.user.public_key).to eq('some_key') + end + end + + context "when --user-key is not passed" do + it "does not set user.public_key" do + knife.run + expect(knife.user.public_key).to be_nil + end + end + + context "when a private_key is returned" do + before do + allow(knife).to receive(:create_user_from_hash).and_return(Chef::User.from_hash(knife.user.to_hash.merge({"private_key" => "some_private_key"}))) + end + + context "when --file is passed" do + before do + knife.config[:file] = '/some/path' + end + + it "creates a new file of the path passed" do + filehandle = double('filehandle') + expect(filehandle).to receive(:print).with('some_private_key') + expect(File).to receive(:open).with('/some/path', 'w').and_yield(filehandle) + knife.run + end + end + + context "when --file is not passed" do + it "prints the private key to stdout" do + expect(knife.ui).to receive(:msg).with('some_private_key') + knife.run + end + end + end + + end # when all mandatory fields are validly specified end diff --git a/spec/unit/knife/user_delete_spec.rb b/spec/unit/knife/user_delete_spec.rb index 94cfbf3db1..e49c781358 100644 --- a/spec/unit/knife/user_delete_spec.rb +++ b/spec/unit/knife/user_delete_spec.rb @@ -19,21 +19,47 @@ require 'spec_helper' describe Chef::Knife::UserDelete do + let(:knife) { Chef::Knife::UserDelete.new } + let(:user) { double('user_object') } + let(:stdout) { StringIO.new } + before(:each) do Chef::Knife::UserDelete.load_deps - @knife = Chef::Knife::UserDelete.new - @knife.name_args = [ 'my_user' ] + knife.name_args = [ 'my_user' ] + allow(Chef::User).to receive(:load).and_return(user) + allow(user).to receive(:username).and_return('my_user') + allow(knife.ui).to receive(:stderr).and_return(stdout) + allow(knife.ui).to receive(:stdout).and_return(stdout) + end + + # delete this once OSC11 support is gone + context "when the username field is not supported by the server" do + before do + allow(knife).to receive(:run_osc_11_user_delete).and_raise(SystemExit) + allow(user).to receive(:username).and_return(nil) + end + + it "displays the osc warning" do + expect(knife.ui).to receive(:warn).with(knife.osc_11_warning) + expect{ knife.run }.to raise_error(SystemExit) + end + + it "forwards the command to knife osc_user edit" do + expect(knife).to receive(:run_osc_11_user_delete) + expect{ knife.run }.to raise_error(SystemExit) + end end it 'deletes the user' do - expect(@knife).to receive(:delete_object).with(Chef::User, 'my_user') - @knife.run + #expect(knife).to receive(:delete_object).with(Chef::User, 'my_user') + expect(knife).to receive(:delete_object).with('my_user') + knife.run end it 'prints usage and exits when a user name is not provided' do - @knife.name_args = [] - expect(@knife).to receive(:show_usage) - expect(@knife.ui).to receive(:fatal) - expect { @knife.run }.to raise_error(SystemExit) + knife.name_args = [] + expect(knife).to receive(:show_usage) + expect(knife.ui).to receive(:fatal) + expect { knife.run }.to raise_error(SystemExit) end end diff --git a/spec/unit/knife/user_edit_spec.rb b/spec/unit/knife/user_edit_spec.rb index 0eb75cfa9b..15a7726b20 100644 --- a/spec/unit/knife/user_edit_spec.rb +++ b/spec/unit/knife/user_edit_spec.rb @@ -19,29 +19,48 @@ require 'spec_helper' describe Chef::Knife::UserEdit do + let(:knife) { Chef::Knife::UserEdit.new } + before(:each) do @stderr = StringIO.new @stdout = StringIO.new Chef::Knife::UserEdit.load_deps - @knife = Chef::Knife::UserEdit.new - allow(@knife.ui).to receive(:stderr).and_return(@stderr) - allow(@knife.ui).to receive(:stdout).and_return(@stdout) - @knife.name_args = [ 'my_user' ] - @knife.config[:disable_editing] = true + allow(knife.ui).to receive(:stderr).and_return(@stderr) + allow(knife.ui).to receive(:stdout).and_return(@stdout) + knife.name_args = [ 'my_user' ] + knife.config[:disable_editing] = true + end + + # delete this once OSC11 support is gone + context "when the username field is not supported by the server" do + before do + allow(knife).to receive(:run_osc_11_user_edit).and_raise(SystemExit) + allow(Chef::User).to receive(:load).and_return({"username" => nil}) + end + + it "displays the osc warning" do + expect(knife.ui).to receive(:warn).with(knife.osc_11_warning) + expect{ knife.run }.to raise_error(SystemExit) + end + + it "forwards the command to knife osc_user edit" do + expect(knife).to receive(:run_osc_11_user_edit) + expect{ knife.run }.to raise_error(SystemExit) + end end it 'loads and edits the user' do - data = { :name => "my_user" } + data = { "username" => "my_user" } allow(Chef::User).to receive(:load).with("my_user").and_return(data) - expect(@knife).to receive(:edit_data).with(data).and_return(data) - @knife.run + expect(knife).to receive(:edit_data).with(data).and_return(data) + knife.run end it 'prints usage and exits when a user name is not provided' do - @knife.name_args = [] - expect(@knife).to receive(:show_usage) - expect(@knife.ui).to receive(:fatal) - expect { @knife.run }.to raise_error(SystemExit) + knife.name_args = [] + expect(knife).to receive(:show_usage) + expect(knife.ui).to receive(:fatal) + expect { knife.run }.to raise_error(SystemExit) end end diff --git a/spec/unit/knife/user_list_spec.rb b/spec/unit/knife/user_list_spec.rb index db097a5c16..9990cc802d 100644 --- a/spec/unit/knife/user_list_spec.rb +++ b/spec/unit/knife/user_list_spec.rb @@ -19,14 +19,18 @@ require 'spec_helper' describe Chef::Knife::UserList do + let(:knife) { Chef::Knife::UserList.new } + let(:stdout) { StringIO.new } + before(:each) do Chef::Knife::UserList.load_deps - @knife = Chef::Knife::UserList.new + allow(knife.ui).to receive(:stderr).and_return(stdout) + allow(knife.ui).to receive(:stdout).and_return(stdout) end it 'lists the users' do expect(Chef::User).to receive(:list) - expect(@knife).to receive(:format_list_for_display) - @knife.run + expect(knife).to receive(:format_list_for_display) + knife.run end end diff --git a/spec/unit/knife/user_reregister_spec.rb b/spec/unit/knife/user_reregister_spec.rb index 1268716f40..412a6ec374 100644 --- a/spec/unit/knife/user_reregister_spec.rb +++ b/spec/unit/knife/user_reregister_spec.rb @@ -19,35 +19,56 @@ require 'spec_helper' describe Chef::Knife::UserReregister do - before(:each) do + let(:knife) { Chef::Knife::UserReregister.new } + let(:user_mock) { double('user_mock', :private_key => "private_key") } + let(:stdout) { StringIO.new } + + before do Chef::Knife::UserReregister.load_deps - @knife = Chef::Knife::UserReregister.new - @knife.name_args = [ 'a_user' ] - @user_mock = double('user_mock', :private_key => "private_key") - allow(Chef::User).to receive(:load).and_return(@user_mock) - @stdout = StringIO.new - allow(@knife.ui).to receive(:stdout).and_return(@stdout) + knife.name_args = [ 'a_user' ] + allow(Chef::User).to receive(:load).and_return(user_mock) + allow(knife.ui).to receive(:stdout).and_return(stdout) + allow(knife.ui).to receive(:stderr).and_return(stdout) + allow(user_mock).to receive(:username).and_return('a_user') + end + + # delete this once OSC11 support is gone + context "when the username field is not supported by the server" do + before do + allow(knife).to receive(:run_osc_11_user_reregister).and_raise(SystemExit) + allow(user_mock).to receive(:username).and_return(nil) + end + + it "displays the osc warning" do + expect(knife.ui).to receive(:warn).with(knife.osc_11_warning) + expect{ knife.run }.to raise_error(SystemExit) + end + + it "forwards the command to knife osc_user edit" do + expect(knife).to receive(:run_osc_11_user_reregister) + expect{ knife.run }.to raise_error(SystemExit) + end end it 'prints usage and exits when a user name is not provided' do - @knife.name_args = [] - expect(@knife).to receive(:show_usage) - expect(@knife.ui).to receive(:fatal) - expect { @knife.run }.to raise_error(SystemExit) + knife.name_args = [] + expect(knife).to receive(:show_usage) + expect(knife.ui).to receive(:fatal) + expect { knife.run }.to raise_error(SystemExit) end it 'reregisters the user and prints the key' do - expect(@user_mock).to receive(:reregister).and_return(@user_mock) - @knife.run - expect(@stdout.string).to match( /private_key/ ) + expect(user_mock).to receive(:reregister).and_return(user_mock) + knife.run + expect(stdout.string).to match( /private_key/ ) end it 'writes the private key to a file when --file is specified' do - expect(@user_mock).to receive(:reregister).and_return(@user_mock) - @knife.config[:file] = '/tmp/a_file' + expect(user_mock).to receive(:reregister).and_return(user_mock) + knife.config[:file] = '/tmp/a_file' filehandle = StringIO.new expect(File).to receive(:open).with('/tmp/a_file', 'w').and_yield(filehandle) - @knife.run + knife.run expect(filehandle.string).to eq("private_key") end end diff --git a/spec/unit/knife/user_show_spec.rb b/spec/unit/knife/user_show_spec.rb index f97cbc3f13..43392a3a5c 100644 --- a/spec/unit/knife/user_show_spec.rb +++ b/spec/unit/knife/user_show_spec.rb @@ -19,23 +19,47 @@ require 'spec_helper' describe Chef::Knife::UserShow do - before(:each) do + let(:knife) { Chef::Knife::UserShow.new } + let(:user_mock) { double('user_mock') } + let(:stdout) { StringIO.new } + + before do Chef::Knife::UserShow.load_deps - @knife = Chef::Knife::UserShow.new - @knife.name_args = [ 'my_user' ] - @user_mock = double('user_mock') + knife.name_args = [ 'my_user' ] + allow(user_mock).to receive(:username).and_return('my_user') + allow(knife.ui).to receive(:stderr).and_return(stdout) + allow(knife.ui).to receive(:stdout).and_return(stdout) + end + + # delete this once OSC11 support is gone + context "when the username field is not supported by the server" do + before do + allow(knife).to receive(:run_osc_11_user_show).and_raise(SystemExit) + allow(Chef::User).to receive(:load).with('my_user').and_return(user_mock) + allow(user_mock).to receive(:username).and_return(nil) + end + + it "displays the osc warning" do + expect(knife.ui).to receive(:warn).with(knife.osc_11_warning) + expect{ knife.run }.to raise_error(SystemExit) + end + + it "forwards the command to knife osc_user edit" do + expect(knife).to receive(:run_osc_11_user_show) + expect{ knife.run }.to raise_error(SystemExit) + end end it 'loads and displays the user' do - expect(Chef::User).to receive(:load).with('my_user').and_return(@user_mock) - expect(@knife).to receive(:format_for_display).with(@user_mock) - @knife.run + expect(Chef::User).to receive(:load).with('my_user').and_return(user_mock) + expect(knife).to receive(:format_for_display).with(user_mock) + knife.run end it 'prints usage and exits when a user name is not provided' do - @knife.name_args = [] - expect(@knife).to receive(:show_usage) - expect(@knife.ui).to receive(:fatal) - expect { @knife.run }.to raise_error(SystemExit) + knife.name_args = [] + expect(knife).to receive(:show_usage) + expect(knife.ui).to receive(:fatal) + expect { knife.run }.to raise_error(SystemExit) end end diff --git a/spec/unit/knife_spec.rb b/spec/unit/knife_spec.rb index b748232081..022256f370 100644 --- a/spec/unit/knife_spec.rb +++ b/spec/unit/knife_spec.rb @@ -30,11 +30,20 @@ describe Chef::Knife do let(:knife) { Chef::Knife.new } + let(:config_location) { File.expand_path("~/.chef/config.rb") } + + let(:config_loader) do + instance_double("WorkstationConfigLoader", load: nil, no_config_found?: false, config_location: config_location) + end + before(:each) do Chef::Log.logger = Logger.new(StringIO.new) Chef::Config[:node_name] = "webmonkey.example.com" + allow(Chef::WorkstationConfigLoader).to receive(:new).and_return(config_loader) + allow(config_loader).to receive(:explicit_config_file=) + # Prevent gratuitous code reloading: allow(Chef::Knife).to receive(:load_commands) allow(knife.ui).to receive(:puts) @@ -130,7 +139,8 @@ describe Chef::Knife do "Accept-Encoding"=>"gzip;q=1.0,deflate;q=0.6,identity;q=0.3", 'X-Chef-Version' => Chef::VERSION, "Host"=>"api.opscode.piab", - "X-REMOTE-REQUEST-ID"=>request_id}} + "X-REMOTE-REQUEST-ID"=>request_id, + 'X-Ops-Server-API-Version' => Chef::HTTP::Authenticator::DEFAULT_SERVER_API_VERSION}} let(:request_id) {"1234"} @@ -251,6 +261,18 @@ describe Chef::Knife do :default => "default-value") end + it "sets the default log_location to STDERR for Chef::Log warnings" do + knife_command = KnifeSpecs::TestYourself.new([]) + knife_command.configure_chef + expect(Chef::Config[:log_location]).to eq(STDERR) + end + + it "sets the default log_level to warn so we can issue Chef::Log.warn" do + knife_command = KnifeSpecs::TestYourself.new([]) + knife_command.configure_chef + expect(Chef::Config[:log_level]).to eql(:warn) + end + it "prefers the default value if no config or command line value is present" do knife_command = KnifeSpecs::TestYourself.new([]) #empty argv knife_command.configure_chef @@ -374,6 +396,22 @@ describe Chef::Knife do expect(stderr.string).to match(%r[Response: nothing to see here]) end + it "formats 406s (non-supported API version error) nicely" do + response = Net::HTTPNotAcceptable.new("1.1", "406", "Not Acceptable") + response.instance_variable_set(:@read, true) # I hate you, net/http. + + # set the header + response["x-ops-server-api-version"] = Chef::JSONCompat.to_json(:min_version => "0", :max_version => "1", :request_version => "10000000") + + allow(response).to receive(:body).and_return(Chef::JSONCompat.to_json(:error => "sad trombone")) + allow(knife).to receive(:run).and_raise(Net::HTTPServerException.new("406 Not Acceptable", response)) + + knife.run_with_pretty_exceptions + expect(stderr.string).to include('The request that Knife sent was using API version 10000000') + expect(stderr.string).to include('The Chef server you sent the request to supports a min API verson of 0 and a max API version of 1') + expect(stderr.string).to include('Please either update your Chef client or server to be a compatible set') + end + it "formats 500s nicely" do response = Net::HTTPInternalServerError.new("1.1", "500", "Internal Server Error") response.instance_variable_set(:@read, true) # I hate you, net/http. diff --git a/spec/unit/log/syslog_spec.rb b/spec/unit/log/syslog_spec.rb new file mode 100644 index 0000000000..3db90e50c6 --- /dev/null +++ b/spec/unit/log/syslog_spec.rb @@ -0,0 +1,53 @@ +# +# Author:: SAWANOBORI Yukihiko (<sawanoboriyu@higanworks.com>) +# Copyright:: Copyright (c) 2015 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' + +describe "Chef::Log::Syslog", :unix_only => true do + let(:syslog) { Chef::Log::Syslog.new } + let(:app) { Chef::Application.new } + + before do + Chef::Log.init(MonoLogger.new(syslog)) + @old_log_level = Chef::Log.level + Chef::Log.level = :info + @old_loggers = Chef::Log.loggers + Chef::Log.use_log_devices([syslog]) + end + + after do + Chef::Log.level = @old_log_level + Chef::Log.use_log_devices(@old_loggers) + end + + it "should send message with severity info to syslog." do + expect(syslog).to receive(:info).with("*** Chef 12.4.0.dev.0 ***") + Chef::Log.info("*** Chef 12.4.0.dev.0 ***") + end + + it "should send message with severity warning to syslog." do + expect(syslog).to receive(:warn).with("No config file found or specified on command line, using command line options.") + Chef::Log.warn("No config file found or specified on command line, using command line options.") + end + + it "should fallback into send message with severity info to syslog when wrong format." do + expect(syslog).to receive(:info).with("chef message") + syslog.write("chef message") + end +end diff --git a/spec/unit/log/winevt_spec.rb b/spec/unit/log/winevt_spec.rb new file mode 100644 index 0000000000..867ef55900 --- /dev/null +++ b/spec/unit/log/winevt_spec.rb @@ -0,0 +1,55 @@ +# +# Author:: Jay Mundrawala (jdm@chef.io) +# Author:: SAWANOBORI Yukihiko (<sawanoboriyu@higanworks.com>) +# Copyright:: Copyright (c) 2015 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::Log::WinEvt do + let(:evtlog) { instance_double("Win32::EventLog")} + let(:winevt) { Chef::Log::WinEvt.new(evtlog) } + let(:app) { Chef::Application.new } + + before do + + Chef::Log.init(MonoLogger.new(winevt)) + @old_log_level = Chef::Log.level + Chef::Log.level = :info + @old_loggers = Chef::Log.loggers + Chef::Log.use_log_devices([winevt]) + end + + after do + Chef::Log.level = @old_log_level + Chef::Log.use_log_devices(@old_loggers) + end + + it "should send message with severity info to Windows Event Log." do + expect(winevt).to receive(:info).with("*** Chef 12.4.0.dev.0 ***") + Chef::Log.info("*** Chef 12.4.0.dev.0 ***") + end + + it "should send message with severity warning to Windows Event Log." do + expect(winevt).to receive(:warn).with("No config file found or specified on command line, using command line options.") + Chef::Log.warn("No config file found or specified on command line, using command line options.") + end + + it "should fallback into send message with severity info to Windows Event Log when wrong format." do + expect(winevt).to receive(:info).with("chef message") + winevt.write("chef message") + end +end diff --git a/spec/unit/lwrp_spec.rb b/spec/unit/lwrp_spec.rb index ec39174da6..34c6f6f1c5 100644 --- a/spec/unit/lwrp_spec.rb +++ b/spec/unit/lwrp_spec.rb @@ -17,20 +17,40 @@ # require 'spec_helper' +require 'tmpdir' +require 'fileutils' +require 'chef/mixin/convert_to_class_name' module LwrpConstScopingConflict end describe "LWRP" do + include Chef::Mixin::ConvertToClassName + before do @original_VERBOSE = $VERBOSE $VERBOSE = nil + Chef::Resource::LWRPBase.class_eval { @loaded_lwrps = {} } end after do $VERBOSE = @original_VERBOSE end + def get_lwrp(name) + Chef::ResourceResolver.resolve(name) + end + + def get_lwrp_provider(name) + old_treat_deprecation_warnings_as_errors = Chef::Config[:treat_deprecation_warnings_as_errors] + Chef::Config[:treat_deprecation_warnings_as_errors] = false + begin + Chef::Provider.const_get(convert_to_class_name(name.to_s)) + ensure + Chef::Config[:treat_deprecation_warnings_as_errors] = old_treat_deprecation_warnings_as_errors + end + end + describe "when overriding an existing class" do before :each do allow($stderr).to receive(:write) @@ -43,7 +63,6 @@ describe "LWRP" do expect(Chef::Log).not_to receive(:debug).with(/anymore/) Chef::Resource::LWRPBase.build_from_file("lwrp", file, nil) Object.send(:remove_const, 'LwrpFoo') - Chef::Resource.send(:remove_const, 'LwrpFoo') end it "should not skip loading a provider when there's a top level symbol of the same name" do @@ -53,7 +72,6 @@ describe "LWRP" do expect(Chef::Log).not_to receive(:debug).with(/anymore/) Chef::Provider::LWRPBase.build_from_file("lwrp", file, nil) Object.send(:remove_const, 'LwrpBuckPasser') - Chef::Provider.send(:remove_const, 'LwrpBuckPasser') end # @todo: we need a before block to manually remove_const all of the LWRPs that we @@ -67,7 +85,6 @@ describe "LWRP" do Dir[File.expand_path( "lwrp/resources/*", CHEF_SPEC_DATA)].each do |file| expect(Chef::Log).to receive(:info).with(/Skipping/) - expect(Chef::Log).to receive(:debug).with(/anymore/) Chef::Resource::LWRPBase.build_from_file("lwrp", file, nil) end end @@ -79,7 +96,6 @@ describe "LWRP" do Dir[File.expand_path( "lwrp/providers/*", CHEF_SPEC_DATA)].each do |file| expect(Chef::Log).to receive(:info).with(/Skipping/) - expect(Chef::Log).to receive(:debug).with(/anymore/) Chef::Provider::LWRPBase.build_from_file("lwrp", file, nil) end end @@ -90,7 +106,7 @@ describe "LWRP" do Dir[File.expand_path( "lwrp/resources/*", CHEF_SPEC_DATA)].each do |file| Chef::Resource::LWRPBase.build_from_file("lwrp", file, nil) end - first_lwr_foo_class = Chef::Resource::LwrpFoo + first_lwr_foo_class = get_lwrp(:lwrp_foo) expect(Chef::Resource.resource_classes).to include(first_lwr_foo_class) Dir[File.expand_path( "lwrp/resources/*", CHEF_SPEC_DATA)].each do |file| Chef::Resource::LWRPBase.build_from_file("lwrp", file, nil) @@ -106,40 +122,95 @@ describe "LWRP" do end + context "When an LWRP resource in cookbook l-w-r-p is loaded" do + before do + @tmpdir = Dir.mktmpdir("lwrp_test") + resource_path = File.join(@tmpdir, "foo.rb") + IO.write(resource_path, "default_action :create") + provider_path = File.join(@tmpdir, "foo.rb") + IO.write(provider_path, <<-EOM) + action :create do + raise "hi" + end + EOM + end + + it "Can find the resource at l_w_r_p_foo" do + end + end + + context "When an LWRP resource lwrp_foo is loaded" do + before do + @tmpdir = Dir.mktmpdir("lwrp_test") + @lwrp_path = File.join(@tmpdir, "foo.rb") + content = IO.read(File.expand_path("../../data/lwrp/resources/foo.rb", __FILE__)) + IO.write(@lwrp_path, content) + Chef::Resource::LWRPBase.build_from_file("lwrp", @lwrp_path, nil) + @original_resource = Chef::ResourceResolver.resolve(:lwrp_foo) + end + + after do + FileUtils.remove_entry @tmpdir + end + + context "And the LWRP is asked to load again, this time with different code" do + before do + content = IO.read(File.expand_path("../../data/lwrp_override/resources/foo.rb", __FILE__)) + IO.write(@lwrp_path, content) + Chef::Resource::LWRPBase.build_from_file("lwrp", @lwrp_path, nil) + end + + it "Should load the old content, and not the new" do + resource = Chef::ResourceResolver.resolve(:lwrp_foo) + expect(resource).to eq @original_resource + expect(resource.default_action).to eq(:pass_buck) + expect(Chef.method_defined?(:method_created_by_override_lwrp_foo)).to be_falsey + end + end + end + describe "Lightweight Chef::Resource" do before do Dir[File.expand_path(File.join(File.dirname(__FILE__), "..", "data", "lwrp", "resources", "*"))].each do |file| Chef::Resource::LWRPBase.build_from_file("lwrp", file, nil) end + end - Dir[File.expand_path(File.join(File.dirname(__FILE__), "..", "data", "lwrp_override", "resources", "*"))].each do |file| - Chef::Resource::LWRPBase.build_from_file("lwrp", file, nil) - end + it "should load the resource into a properly-named class and emit a warning when it is initialized" do + expect { Chef::Resource::LwrpFoo.new('hi') }.to raise_error(Chef::Exceptions::DeprecatedFeatureError) end - it "should load the resource into a properly-named class" do - expect(Chef::Resource.const_get("LwrpFoo")).to be_kind_of(Class) + it "should be resolvable with Chef::ResourceResolver.resolve(:lwrp_foo)" do + expect(Chef::ResourceResolver.resolve(:lwrp_foo, node: Chef::Node.new)).to eq(get_lwrp(:lwrp_foo)) end it "should set resource_name" do - expect(Chef::Resource::LwrpFoo.new("blah").resource_name).to eql(:lwrp_foo) + expect(get_lwrp(:lwrp_foo).new("blah").resource_name).to eql(:lwrp_foo) + end + + it "should output the resource_name in .to_s" do + expect(get_lwrp(:lwrp_foo).new("blah").to_s).to eq "lwrp_foo[blah]" + end + + it "should have a class that outputs a reasonable string" do + expect(get_lwrp(:lwrp_foo).to_s).to eq "LWRP resource lwrp_foo from cookbook lwrp" end it "should add the specified actions to the allowed_actions array" do - expect(Chef::Resource::LwrpFoo.new("blah").allowed_actions).to include(:pass_buck, :twiddle_thumbs) + expect(get_lwrp(:lwrp_foo).new("blah").allowed_actions).to include(:pass_buck, :twiddle_thumbs) end it "should set the specified action as the default action" do - expect(Chef::Resource::LwrpFoo.new("blah").action).to eq(:pass_buck) + expect(get_lwrp(:lwrp_foo).new("blah").action).to eq(:pass_buck) end it "should create a method for each attribute" do - expect(Chef::Resource::LwrpFoo.new("blah").methods.map{ |m| m.to_sym}).to include(:monkey) + expect(get_lwrp(:lwrp_foo).new("blah").methods.map{ |m| m.to_sym}).to include(:monkey) end it "should build attribute methods that respect validation rules" do - expect { Chef::Resource::LwrpFoo.new("blah").monkey(42) }.to raise_error(ArgumentError) + expect { get_lwrp(:lwrp_foo).new("blah").monkey(42) }.to raise_error(ArgumentError) end it "should have access to the run context and node during class definition" do @@ -151,12 +222,133 @@ describe "LWRP" do Chef::Resource::LWRPBase.build_from_file("lwrp", file, run_context) end - cls = Chef::Resource.const_get("LwrpNodeattr") + cls = get_lwrp(:lwrp_nodeattr) expect(cls.node).to be_kind_of(Chef::Node) expect(cls.run_context).to be_kind_of(Chef::RunContext) expect(cls.node[:penguin_name]).to eql("jackass") end + context "resource class created" do + before do + @old_treat_deprecation_warnings_as_errors = Chef::Config[:treat_deprecation_warnings_as_errors] + Chef::Config[:treat_deprecation_warnings_as_errors] = false + end + after do + Chef::Config[:treat_deprecation_warnings_as_errors] = @old_treat_deprecation_warnings_as_errors + end + + it "should load the resource into a properly-named class" do + expect(Chef::Resource::LwrpFoo).to be_kind_of(Class) + expect(Chef::Resource::LwrpFoo <= Chef::Resource::LWRPBase).to be_truthy + end + + it "get_lwrp(:lwrp_foo).new is a Chef::Resource::LwrpFoo" do + lwrp = get_lwrp(:lwrp_foo).new('hi') + expect(lwrp.kind_of?(Chef::Resource::LwrpFoo)).to be_truthy + expect(lwrp.is_a?(Chef::Resource::LwrpFoo)).to be_truthy + expect(get_lwrp(:lwrp_foo) === lwrp).to be_truthy + expect(Chef::Resource::LwrpFoo === lwrp).to be_truthy + end + + it "Chef::Resource::LwrpFoo.new is a get_lwrp(:lwrp_foo)" do + lwrp = Chef::Resource::LwrpFoo.new('hi') + expect(lwrp.kind_of?(get_lwrp(:lwrp_foo))).to be_truthy + expect(lwrp.is_a?(get_lwrp(:lwrp_foo))).to be_truthy + expect(get_lwrp(:lwrp_foo) === lwrp).to be_truthy + expect(Chef::Resource::LwrpFoo === lwrp).to be_truthy + end + + it "works even if LwrpFoo exists in the top level" do + module ::LwrpFoo + end + expect(Chef::Resource::LwrpFoo).not_to eq(::LwrpFoo) + end + + context "with a subclass of get_lwrp(:lwrp_foo)" do + let(:subclass) do + Class.new(get_lwrp(:lwrp_foo)) + end + + it "subclass.new is a subclass" do + lwrp = subclass.new('hi') + expect(lwrp.kind_of?(subclass)).to be_truthy + expect(lwrp.is_a?(subclass)).to be_truthy + expect(subclass === lwrp).to be_truthy + expect(lwrp.class === subclass) + end + it "subclass.new is a Chef::Resource::LwrpFoo" do + lwrp = subclass.new('hi') + expect(lwrp.kind_of?(Chef::Resource::LwrpFoo)).to be_truthy + expect(lwrp.is_a?(Chef::Resource::LwrpFoo)).to be_truthy + expect(Chef::Resource::LwrpFoo === lwrp).to be_truthy + expect(lwrp.class === Chef::Resource::LwrpFoo) + end + it "subclass.new is a get_lwrp(:lwrp_foo)" do + lwrp = subclass.new('hi') + expect(lwrp.kind_of?(get_lwrp(:lwrp_foo))).to be_truthy + expect(lwrp.is_a?(get_lwrp(:lwrp_foo))).to be_truthy + expect(get_lwrp(:lwrp_foo) === lwrp).to be_truthy + expect(lwrp.class === get_lwrp(:lwrp_foo)) + end + it "Chef::Resource::LwrpFoo.new is *not* a subclass" do + lwrp = Chef::Resource::LwrpFoo.new('hi') + expect(lwrp.kind_of?(subclass)).to be_falsey + expect(lwrp.is_a?(subclass)).to be_falsey + expect(subclass === lwrp.class).to be_falsey + expect(subclass === Chef::Resource::LwrpFoo).to be_falsey + end + it "get_lwrp(:lwrp_foo).new is *not* a subclass" do + lwrp = get_lwrp(:lwrp_foo).new('hi') + expect(lwrp.kind_of?(subclass)).to be_falsey + expect(lwrp.is_a?(subclass)).to be_falsey + expect(subclass === lwrp.class).to be_falsey + expect(subclass === get_lwrp(:lwrp_foo)).to be_falsey + end + end + + context "with a subclass of Chef::Resource::LwrpFoo" do + let(:subclass) do + Class.new(Chef::Resource::LwrpFoo) + end + + it "subclass.new is a subclass" do + lwrp = subclass.new('hi') + expect(lwrp.kind_of?(subclass)).to be_truthy + expect(lwrp.is_a?(subclass)).to be_truthy + expect(subclass === lwrp).to be_truthy + expect(lwrp.class === subclass) + end + it "subclass.new is a Chef::Resource::LwrpFoo" do + lwrp = subclass.new('hi') + expect(lwrp.kind_of?(Chef::Resource::LwrpFoo)).to be_truthy + expect(lwrp.is_a?(Chef::Resource::LwrpFoo)).to be_truthy + expect(Chef::Resource::LwrpFoo === lwrp).to be_truthy + expect(lwrp.class === Chef::Resource::LwrpFoo) + end + it "subclass.new is a get_lwrp(:lwrp_foo)" do + lwrp = subclass.new('hi') + expect(lwrp.kind_of?(get_lwrp(:lwrp_foo))).to be_truthy + expect(lwrp.is_a?(get_lwrp(:lwrp_foo))).to be_truthy + expect(get_lwrp(:lwrp_foo) === lwrp).to be_truthy + expect(lwrp.class === get_lwrp(:lwrp_foo)) + end + it "Chef::Resource::LwrpFoo.new is *not* a subclass" do + lwrp = Chef::Resource::LwrpFoo.new('hi') + expect(lwrp.kind_of?(subclass)).to be_falsey + expect(lwrp.is_a?(subclass)).to be_falsey + expect(subclass === lwrp.class).to be_falsey + expect(subclass === Chef::Resource::LwrpFoo).to be_falsey + end + it "get_lwrp(:lwrp_foo).new is *not* a subclass" do + lwrp = get_lwrp(:lwrp_foo).new('hi') + expect(lwrp.kind_of?(subclass)).to be_falsey + expect(lwrp.is_a?(subclass)).to be_falsey + expect(subclass === lwrp.class).to be_falsey + expect(subclass === get_lwrp(:lwrp_foo)).to be_falsey + end + end + end + context "resource_name" do let(:klass) { Class.new(Chef::Resource::LWRPBase) } @@ -175,14 +367,6 @@ describe "LWRP" do expect(klass.resource_name).to eq(:foo) end - context "when creating a new instance" do - it "raises an exception if resource_name is nil" do - expect { - klass.new('blah') - }.to raise_error(Chef::Exceptions::InvalidResourceSpecification) - end - end - context "lazy default values" do let(:klass) do Class.new(Chef::Resource::LWRPBase) do @@ -281,102 +465,146 @@ describe "LWRP" do end describe "Lightweight Chef::Provider" do - before do - @node = Chef::Node.new - @node.automatic[:platform] = :ubuntu - @node.automatic[:platform_version] = '8.10' - @events = Chef::EventDispatch::Dispatcher.new - @run_context = Chef::RunContext.new(@node, Chef::CookbookCollection.new({}), @events) - @runner = Chef::Runner.new(@run_context) - end - before(:each) do - Dir[File.expand_path(File.join(File.dirname(__FILE__), "..", "data", "lwrp", "resources", "*"))].each do |file| - Chef::Resource::LWRPBase.build_from_file("lwrp", file, @run_context) + let(:node) do + Chef::Node.new.tap do |n| + n.automatic[:platform] = :ubuntu + n.automatic[:platform_version] = '8.10' end + end - Dir[File.expand_path(File.join(File.dirname(__FILE__), "..", "data", "lwrp_override", "resources", "*"))].each do |file| - Chef::Resource::LWRPBase.build_from_file("lwrp", file, @run_context) - end + let(:events) { Chef::EventDispatch::Dispatcher.new } - Dir[File.expand_path(File.join(File.dirname(__FILE__), "..", "data", "lwrp", "providers", "*"))].each do |file| - Chef::Provider::LWRPBase.build_from_file("lwrp", file, @run_context) - end + let(:run_context) { Chef::RunContext.new(node, Chef::CookbookCollection.new({}), events) } + + let(:runner) { Chef::Runner.new(run_context) } - Dir[File.expand_path(File.join(File.dirname(__FILE__), "..", "data", "lwrp_override", "providers", "*"))].each do |file| - Chef::Provider::LWRPBase.build_from_file("lwrp", file, @run_context) + let(:lwrp_cookbok_name) { "lwrp" } + + before do + Chef::Provider::LWRPBase.class_eval { @loaded_lwrps = {} } + end + + before(:each) do + Dir[File.expand_path(File.expand_path("../../data/lwrp/resources/*", __FILE__))].each do |file| + Chef::Resource::LWRPBase.build_from_file(lwrp_cookbok_name, file, run_context) end + Dir[File.expand_path(File.expand_path("../../data/lwrp/providers/*", __FILE__))].each do |file| + Chef::Provider::LWRPBase.build_from_file(lwrp_cookbok_name, file, run_context) + end end it "should properly handle a new_resource reference" do - resource = Chef::Resource::LwrpFoo.new("morpheus") + resource = get_lwrp(:lwrp_foo).new("morpheus", run_context) resource.monkey("bob") - resource.provider(:lwrp_monkey_name_printer) - resource.run_context = @run_context + resource.provider(get_lwrp_provider(:lwrp_monkey_name_printer)) provider = Chef::Platform.provider_for_resource(resource, :twiddle_thumbs) provider.action_twiddle_thumbs end - it "should load the provider into a properly-named class" do - expect(Chef::Provider.const_get("LwrpBuckPasser")).to be_kind_of(Class) - end + context "provider class created" do + before do + @old_treat_deprecation_warnings_as_errors = Chef::Config[:treat_deprecation_warnings_as_errors] + Chef::Config[:treat_deprecation_warnings_as_errors] = false + end - it "should create a method for each attribute" do - new_resource = double("new resource").as_null_object - expect(Chef::Provider::LwrpBuckPasser.new(nil, new_resource).methods.map{|m|m.to_sym}).to include(:action_pass_buck) - expect(Chef::Provider::LwrpThumbTwiddler.new(nil, new_resource).methods.map{|m|m.to_sym}).to include(:action_twiddle_thumbs) + after do + Chef::Config[:treat_deprecation_warnings_as_errors] = @old_treat_deprecation_warnings_as_errors + end + + it "should load the provider into a properly-named class" do + expect(Chef::Provider.const_get("LwrpBuckPasser")).to be_kind_of(Class) + expect(Chef::Provider::LwrpBuckPasser <= Chef::Provider::LWRPBase).to be_truthy + end + + it "should create a method for each action" do + expect(get_lwrp_provider(:lwrp_buck_passer).instance_methods).to include(:action_pass_buck) + expect(get_lwrp_provider(:lwrp_thumb_twiddler).instance_methods).to include(:action_twiddle_thumbs) + end + + it "sets itself as a provider for a resource of the same name" do + found_providers = Chef::Platform::ProviderPriorityMap.instance.list_handlers(node, :lwrp_buck_passer) + # we bypass the per-file loading to get the file to load each time, + # which creates the LWRP class repeatedly. New things get prepended to + # the list of providers. + expect(found_providers.first).to eq(get_lwrp_provider(:lwrp_buck_passer)) + end + + context "with a cookbook with an underscore in the name" do + + let(:lwrp_cookbok_name) { "l_w_r_p" } + + it "sets itself as a provider for a resource of the same name" do + found_providers = Chef::Platform::ProviderPriorityMap.instance.list_handlers(node, :l_w_r_p_buck_passer) + expect(found_providers.size).to eq(1) + expect(found_providers.last).to eq(get_lwrp_provider(:l_w_r_p_buck_passer)) + end + end + + context "with a cookbook with a hypen in the name" do + + let(:lwrp_cookbok_name) { "l-w-r-p" } + + it "sets itself as a provider for a resource of the same name" do + incorrect_providers = Chef::Platform::ProviderPriorityMap.instance.list_handlers(node, :'l-w-r-p_buck_passer') + expect(incorrect_providers).to eq([]) + + found_providers = Chef::Platform::ProviderPriorityMap.instance.list_handlers(node, :l_w_r_p_buck_passer) + expect(found_providers.first).to eq(get_lwrp_provider(:l_w_r_p_buck_passer)) + end + end end it "should insert resources embedded in the provider into the middle of the resource collection" do - injector = Chef::Resource::LwrpFoo.new("morpheus", @run_context) + injector = get_lwrp(:lwrp_foo).new("morpheus", run_context) injector.action(:pass_buck) - injector.provider(:lwrp_buck_passer) - dummy = Chef::Resource::ZenMaster.new("keanu reeves", @run_context) + injector.provider(get_lwrp_provider(:lwrp_buck_passer)) + dummy = Chef::Resource::ZenMaster.new("keanu reeves", run_context) dummy.provider(Chef::Provider::Easy) - @run_context.resource_collection.insert(injector) - @run_context.resource_collection.insert(dummy) + run_context.resource_collection.insert(injector) + run_context.resource_collection.insert(dummy) - Chef::Runner.new(@run_context).converge + Chef::Runner.new(run_context).converge - expect(@run_context.resource_collection[0]).to eql(injector) - expect(@run_context.resource_collection[1].name).to eql('prepared_thumbs') - expect(@run_context.resource_collection[2].name).to eql('twiddled_thumbs') - expect(@run_context.resource_collection[3]).to eql(dummy) + expect(run_context.resource_collection[0]).to eql(injector) + expect(run_context.resource_collection[1].name).to eql('prepared_thumbs') + expect(run_context.resource_collection[2].name).to eql('twiddled_thumbs') + expect(run_context.resource_collection[3]).to eql(dummy) end it "should insert embedded resources from multiple providers, including from the last position, properly into the resource collection" do - injector = Chef::Resource::LwrpFoo.new("morpheus", @run_context) + injector = get_lwrp(:lwrp_foo).new("morpheus", run_context) injector.action(:pass_buck) - injector.provider(:lwrp_buck_passer) + injector.provider(get_lwrp_provider(:lwrp_buck_passer)) - injector2 = Chef::Resource::LwrpBar.new("tank", @run_context) + injector2 = get_lwrp(:lwrp_bar).new("tank", run_context) injector2.action(:pass_buck) - injector2.provider(:lwrp_buck_passer_2) + injector2.provider(get_lwrp_provider(:lwrp_buck_passer_2)) - dummy = Chef::Resource::ZenMaster.new("keanu reeves", @run_context) + dummy = Chef::Resource::ZenMaster.new("keanu reeves", run_context) dummy.provider(Chef::Provider::Easy) - @run_context.resource_collection.insert(injector) - @run_context.resource_collection.insert(dummy) - @run_context.resource_collection.insert(injector2) + run_context.resource_collection.insert(injector) + run_context.resource_collection.insert(dummy) + run_context.resource_collection.insert(injector2) - Chef::Runner.new(@run_context).converge + Chef::Runner.new(run_context).converge - expect(@run_context.resource_collection[0]).to eql(injector) - expect(@run_context.resource_collection[1].name).to eql('prepared_thumbs') - expect(@run_context.resource_collection[2].name).to eql('twiddled_thumbs') - expect(@run_context.resource_collection[3]).to eql(dummy) - expect(@run_context.resource_collection[4]).to eql(injector2) - expect(@run_context.resource_collection[5].name).to eql('prepared_eyes') - expect(@run_context.resource_collection[6].name).to eql('dried_paint_watched') + expect(run_context.resource_collection[0]).to eql(injector) + expect(run_context.resource_collection[1].name).to eql('prepared_thumbs') + expect(run_context.resource_collection[2].name).to eql('twiddled_thumbs') + expect(run_context.resource_collection[3]).to eql(dummy) + expect(run_context.resource_collection[4]).to eql(injector2) + expect(run_context.resource_collection[5].name).to eql('prepared_eyes') + expect(run_context.resource_collection[6].name).to eql('dried_paint_watched') end it "should properly handle a new_resource reference" do - resource = Chef::Resource::LwrpFoo.new("morpheus", @run_context) + resource = get_lwrp(:lwrp_foo).new("morpheus", run_context) resource.monkey("bob") - resource.provider(:lwrp_monkey_name_printer) + resource.provider(get_lwrp_provider(:lwrp_monkey_name_printer)) provider = Chef::Platform.provider_for_resource(resource, :twiddle_thumbs) provider.action_twiddle_thumbs @@ -385,9 +613,9 @@ describe "LWRP" do end it "should properly handle an embedded Resource accessing the enclosing Provider's scope" do - resource = Chef::Resource::LwrpFoo.new("morpheus", @run_context) + resource = get_lwrp(:lwrp_foo).new("morpheus", run_context) resource.monkey("bob") - resource.provider(:lwrp_embedded_resource_accesses_providers_scope) + resource.provider(get_lwrp_provider(:lwrp_embedded_resource_accesses_providers_scope)) provider = Chef::Platform.provider_for_resource(resource, :twiddle_thumbs) #provider = @runner.build_provider(resource) @@ -404,15 +632,15 @@ describe "LWRP" do # Side effect of lwrp_inline_compiler provider for testing notifications. $interior_ruby_block_2 = nil # resource type doesn't matter, so make an existing resource type work with provider. - @resource = Chef::Resource::LwrpFoo.new("morpheus", @run_context) + @resource = get_lwrp(:lwrp_foo).new("morpheus", run_context) @resource.allowed_actions << :test @resource.action(:test) - @resource.provider(:lwrp_inline_compiler) + @resource.provider(get_lwrp_provider(:lwrp_inline_compiler)) end it "does not add interior resources to the exterior resource collection" do @resource.run_action(:test) - expect(@run_context.resource_collection).to be_empty + expect(run_context.resource_collection).to be_empty end context "when interior resources are updated" do diff --git a/spec/unit/mixin/api_version_request_handling_spec.rb b/spec/unit/mixin/api_version_request_handling_spec.rb new file mode 100644 index 0000000000..cc5340e424 --- /dev/null +++ b/spec/unit/mixin/api_version_request_handling_spec.rb @@ -0,0 +1,127 @@ +# +# Author:: Tyler Cloke (tyler@chef.io) +# Copyright:: Copyright 2015 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::Mixin::ApiVersionRequestHandling do + let(:dummy_class) { Class.new { include Chef::Mixin::ApiVersionRequestHandling } } + let(:object) { dummy_class.new } + + describe ".server_client_api_version_intersection" do + let(:default_supported_client_versions) { [0,1,2] } + + + context "when the response code is not 406" do + let(:response) { OpenStruct.new(:code => '405') } + let(:exception) { Net::HTTPServerException.new("405 Something Else", response) } + + it "returns nil" do + expect(object.server_client_api_version_intersection(exception, default_supported_client_versions)). + to be_nil + end + + end # when the response code is not 406 + + context "when the response code is 406" do + let(:response) { OpenStruct.new(:code => '406') } + let(:exception) { Net::HTTPServerException.new("406 Not Acceptable", response) } + + context "when x-ops-server-api-version header does not exist" do + it "returns nil" do + expect(object.server_client_api_version_intersection(exception, default_supported_client_versions)). + to be_nil + end + end # when x-ops-server-api-version header does not exist + + context "when x-ops-server-api-version header exists" do + let(:min_server_version) { 2 } + let(:max_server_version) { 4 } + let(:return_hash) { + { + "min_version" => min_server_version, + "max_version" => max_server_version + } + } + + before(:each) do + allow(response).to receive(:[]).with('x-ops-server-api-version').and_return(Chef::JSONCompat.to_json(return_hash)) + end + + context "when there is no intersection between client and server versions" do + shared_examples_for "no intersection between client and server versions" do + it "return an array" do + expect(object.server_client_api_version_intersection(exception, supported_client_versions)). + to be_a_kind_of(Array) + end + + it "returns an empty array" do + expect(object.server_client_api_version_intersection(exception, supported_client_versions).length). + to eq(0) + end + + end + + context "when all the versions are higher than the max" do + it_should_behave_like "no intersection between client and server versions" do + let(:supported_client_versions) { [5,6,7] } + end + end + + context "when all the versions are lower than the min" do + it_should_behave_like "no intersection between client and server versions" do + let(:supported_client_versions) { [0,1] } + end + end + + end # when there is no intersection between client and server versions + + context "when there is an intersection between client and server versions" do + context "when multiple versions intersect" do + let(:supported_client_versions) { [1,2,3,4,5] } + + it "includes all of the intersection" do + expect(object.server_client_api_version_intersection(exception, supported_client_versions)). + to eq([2,3,4]) + end + end # when multiple versions intersect + + context "when only the min client version intersects" do + let(:supported_client_versions) { [0,1,2] } + + it "includes the intersection" do + expect(object.server_client_api_version_intersection(exception, supported_client_versions)). + to eq([2]) + end + end # when only the min client version intersects + + context "when only the max client version intersects" do + let(:supported_client_versions) { [4,5,6] } + + it "includes the intersection" do + expect(object.server_client_api_version_intersection(exception, supported_client_versions)). + to eq([4]) + end + end # when only the max client version intersects + + end # when there is an intersection between client and server versions + + end # when x-ops-server-api-version header exists + end # when the response code is 406 + + end # .server_client_api_version_intersection +end # Chef::Mixin::ApiVersionRequestHandling diff --git a/spec/unit/mixin/command_spec.rb b/spec/unit/mixin/command_spec.rb index e198e3addd..050b261256 100644 --- a/spec/unit/mixin/command_spec.rb +++ b/spec/unit/mixin/command_spec.rb @@ -22,7 +22,7 @@ describe Chef::Mixin::Command, :volatile do if windows? - pending("TODO MOVE: this is a platform specific integration test.") + skip("TODO MOVE: this is a platform specific integration test.") else @@ -61,7 +61,6 @@ describe Chef::Mixin::Command, :volatile do it "returns immediately after the first child process exits" do expect {Timeout.timeout(10) do - pid, stdin,stdout,stderr = nil,nil,nil,nil evil_forker="exit if fork; 10.times { sleep 1}" popen4("ruby -e '#{evil_forker}'") do |pid,stdin,stdout,stderr| end diff --git a/spec/unit/mixin/path_sanity_spec.rb b/spec/unit/mixin/path_sanity_spec.rb index ec8e182e3d..3a924b9538 100644 --- a/spec/unit/mixin/path_sanity_spec.rb +++ b/spec/unit/mixin/path_sanity_spec.rb @@ -35,7 +35,7 @@ describe Chef::Mixin::PathSanity do @gem_bindir = '/some/gem/bin' allow(Gem).to receive(:bindir).and_return(@gem_bindir) allow(RbConfig::CONFIG).to receive(:[]).with('bindir').and_return(@ruby_bindir) - allow(Chef::Platform).to receive(:windows?).and_return(false) + allow(ChefConfig).to receive(:windows?).and_return(false) end it "adds all useful PATHs even if environment is an empty hash" do @@ -77,7 +77,7 @@ describe Chef::Mixin::PathSanity do gem_bindir = 'C:\gems\bin' allow(Gem).to receive(:bindir).and_return(gem_bindir) allow(RbConfig::CONFIG).to receive(:[]).with('bindir').and_return(ruby_bindir) - allow(Chef::Platform).to receive(:windows?).and_return(true) + allow(ChefConfig).to receive(:windows?).and_return(true) env = {"PATH" => 'C:\Windows\system32;C:\mr\softie'} @sanity.enforce_path_sanity(env) expect(env["PATH"]).to eq("C:\\Windows\\system32;C:\\mr\\softie;#{ruby_bindir};#{gem_bindir}") diff --git a/spec/unit/mixin/powershell_out_spec.rb b/spec/unit/mixin/powershell_out_spec.rb new file mode 100644 index 0000000000..0fede582fa --- /dev/null +++ b/spec/unit/mixin/powershell_out_spec.rb @@ -0,0 +1,70 @@ +# +# Copyright:: Copyright (c) 2015 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/mixin/powershell_out' + +describe Chef::Mixin::PowershellOut do + let(:shell_out_class) { Class.new { include Chef::Mixin::PowershellOut } } + subject(:object) { shell_out_class.new } + let(:architecture) { "something" } + let(:flags) { + "-NoLogo -NonInteractive -NoProfile -ExecutionPolicy Unrestricted -InputFormat None" + } + + describe "#powershell_out" do + it "runs a command and returns the shell_out object" do + ret = double("Mixlib::ShellOut") + expect(object).to receive(:shell_out).with( + "powershell.exe #{flags} -Command \"Get-Process\"", + {} + ).and_return(ret) + expect(object.powershell_out("Get-Process")).to eql(ret) + end + + it "passes options" do + ret = double("Mixlib::ShellOut") + expect(object).to receive(:shell_out).with( + "powershell.exe #{flags} -Command \"Get-Process\"", + timeout: 600 + ).and_return(ret) + expect(object.powershell_out("Get-Process", timeout: 600)).to eql(ret) + end + end + + describe "#powershell_out!" do + it "runs a command and returns the shell_out object" do + mixlib_shellout = double("Mixlib::ShellOut") + expect(object).to receive(:shell_out).with( + "powershell.exe #{flags} -Command \"Get-Process\"", + {} + ).and_return(mixlib_shellout) + expect(mixlib_shellout).to receive(:error!) + expect(object.powershell_out!("Get-Process")).to eql(mixlib_shellout) + end + + it "passes options" do + mixlib_shellout = double("Mixlib::ShellOut") + expect(object).to receive(:shell_out).with( + "powershell.exe #{flags} -Command \"Get-Process\"", + timeout: 600 + ).and_return(mixlib_shellout) + expect(mixlib_shellout).to receive(:error!) + expect(object.powershell_out!("Get-Process", timeout: 600)).to eql(mixlib_shellout) + end + end +end diff --git a/spec/unit/mixin/template_spec.rb b/spec/unit/mixin/template_spec.rb index f02bd34b8f..6a867b5f9a 100644 --- a/spec/unit/mixin/template_spec.rb +++ b/spec/unit/mixin/template_spec.rb @@ -39,7 +39,7 @@ describe Chef::Mixin::Template, "render_template" do describe "when running on windows" do before do - allow(Chef::Platform).to receive(:windows?).and_return(true) + allow(ChefConfig).to receive(:windows?).and_return(true) end it "should render the templates with windows line endings" do @@ -54,7 +54,7 @@ describe Chef::Mixin::Template, "render_template" do describe "when running on unix" do before do - allow(Chef::Platform).to receive(:windows?).and_return(false) + allow(ChefConfig).to receive(:windows?).and_return(false) end it "should render the templates with unix line endings" do diff --git a/spec/unit/mixin/unformatter_spec.rb b/spec/unit/mixin/unformatter_spec.rb new file mode 100644 index 0000000000..2eae0ac9bb --- /dev/null +++ b/spec/unit/mixin/unformatter_spec.rb @@ -0,0 +1,61 @@ +# +# Author:: Jay Mundrawala (<jdm@chef.io>) +# Copyright:: Copyright (c) 2015 Chef Software +# 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/mixin/unformatter' + +class Chef::UnformatterTest + include Chef::Mixin::Unformatter + + def foo + end + +end + +describe Chef::Mixin::Unformatter do + let (:unformatter) { Chef::UnformatterTest.new } + let (:message) { "Test Message" } + + describe "#write" do + context "with a timestamp" do + it "sends foo to itself when the message is of severity foo" do + expect(unformatter).to receive(:foo).with(message) + unformatter.write("[time] foo: #{message}") + end + + it "sends foo to itself when the message is of severity FOO" do + expect(unformatter).to receive(:foo).with(message) + unformatter.write("[time] FOO: #{message}") + end + end + + context "without a timestamp" do + it "sends foo to itself when the message is of severity foo" do + expect(unformatter).to receive(:foo).with(message) + unformatter.write("foo: #{message}") + end + + it "sends foo to itself when the message is of severity FOO" do + expect(unformatter).to receive(:foo).with(message) + unformatter.write("FOO: #{message}") + end + end + + end + +end diff --git a/spec/unit/mixin/uris_spec.rb b/spec/unit/mixin/uris_spec.rb new file mode 100644 index 0000000000..d4985c4f67 --- /dev/null +++ b/spec/unit/mixin/uris_spec.rb @@ -0,0 +1,57 @@ +# +# Author:: Jay Mundrawala (<jdm@chef.io>) +# Copyright:: Copyright (c) 2015 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/mixin/uris' + +class Chef::UrisTest + include Chef::Mixin::Uris +end + +describe Chef::Mixin::Uris do + let (:uris) { Chef::UrisTest.new } + + describe "#uri_scheme?" do + it "matches 'scheme://foo.com'" do + expect(uris.uri_scheme?('scheme://foo.com')).to eq(true) + end + + it "does not match 'c:/foo.com'" do + expect(uris.uri_scheme?('c:/foo.com')).to eq(false) + end + + it "does not match '/usr/bin/foo.com'" do + expect(uris.uri_scheme?('/usr/bin/foo.com')).to eq(false) + end + + it "does not match 'c:/foo.com://bar.com'" do + expect(uris.uri_scheme?('c:/foo.com://bar.com')).to eq(false) + end + end + + describe "#as_uri" do + it "parses a file scheme uri with spaces" do + expect{ uris.as_uri("file:///c:/foo bar.txt") }.not_to raise_exception + end + + it "returns a URI object" do + expect( uris.as_uri("file:///c:/foo bar.txt") ).to be_a(URI) + end + end + +end diff --git a/spec/unit/node_map_spec.rb b/spec/unit/node_map_spec.rb index fe7372961b..9b5ff5e8c6 100644 --- a/spec/unit/node_map_spec.rb +++ b/spec/unit/node_map_spec.rb @@ -134,6 +134,10 @@ describe Chef::NodeMap do end describe "resource back-compat testing" do + before :each do + Chef::Config[:treat_deprecation_warnings_as_errors] = false + end + it "should handle :on_platforms => :all" do node_map.set(:chef_gem, :foo, :on_platforms => :all) allow(node).to receive(:[]).with(:platform).and_return("windows") @@ -152,4 +156,3 @@ describe Chef::NodeMap do end end - diff --git a/spec/unit/osc_user_spec.rb b/spec/unit/osc_user_spec.rb new file mode 100644 index 0000000000..678486a16d --- /dev/null +++ b/spec/unit/osc_user_spec.rb @@ -0,0 +1,276 @@ +# +# Author:: Steven Danna (steve@opscode.com) +# Copyright:: Copyright (c) 2012 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. +# + +# DEPRECATION NOTE +# This code only remains to support users still operating with +# Open Source Chef Server 11 and should be removed once support +# for OSC 11 ends. New development should occur in user_spec.rb. + +require 'spec_helper' + +require 'chef/osc_user' +require 'tempfile' + +describe Chef::OscUser do + before(:each) do + @user = Chef::OscUser.new + end + + describe "initialize" do + it "should be a Chef::OscUser" do + expect(@user).to be_a_kind_of(Chef::OscUser) + end + end + + describe "name" do + it "should let you set the name to a string" do + expect(@user.name("ops_master")).to eq("ops_master") + end + + it "should return the current name" do + @user.name "ops_master" + expect(@user.name).to eq("ops_master") + end + + # It is not feasible to check all invalid characters. Here are a few + # that we probably care about. + it "should not accept invalid characters" do + # capital letters + expect { @user.name "Bar" }.to raise_error(ArgumentError) + # slashes + expect { @user.name "foo/bar" }.to raise_error(ArgumentError) + # ? + expect { @user.name "foo?" }.to raise_error(ArgumentError) + # & + expect { @user.name "foo&" }.to raise_error(ArgumentError) + end + + + it "should not accept spaces" do + expect { @user.name "ops master" }.to raise_error(ArgumentError) + end + + it "should throw an ArgumentError if you feed it anything but a string" do + expect { @user.name Hash.new }.to raise_error(ArgumentError) + end + end + + describe "admin" do + it "should let you set the admin bit" do + expect(@user.admin(true)).to eq(true) + end + + it "should return the current admin value" do + @user.admin true + expect(@user.admin).to eq(true) + end + + it "should default to false" do + expect(@user.admin).to eq(false) + end + + it "should throw an ArgumentError if you feed it anything but true or false" do + expect { @user.name Hash.new }.to raise_error(ArgumentError) + end + end + + describe "public_key" do + it "should let you set the public key" do + expect(@user.public_key("super public")).to eq("super public") + end + + it "should return the current public key" do + @user.public_key("super public") + expect(@user.public_key).to eq("super public") + end + + it "should throw an ArgumentError if you feed it something lame" do + expect { @user.public_key Hash.new }.to raise_error(ArgumentError) + end + end + + describe "private_key" do + it "should let you set the private key" do + expect(@user.private_key("super private")).to eq("super private") + end + + it "should return the private key" do + @user.private_key("super private") + expect(@user.private_key).to eq("super private") + end + + it "should throw an ArgumentError if you feed it something lame" do + expect { @user.private_key Hash.new }.to raise_error(ArgumentError) + end + end + + describe "when serializing to JSON" do + before(:each) do + @user.name("black") + @user.public_key("crowes") + @json = @user.to_json + end + + it "serializes as a JSON object" do + expect(@json).to match(/^\{.+\}$/) + end + + it "includes the name value" do + expect(@json).to include(%q{"name":"black"}) + end + + it "includes the public key value" do + expect(@json).to include(%{"public_key":"crowes"}) + end + + it "includes the 'admin' flag" do + expect(@json).to include(%q{"admin":false}) + end + + it "includes the private key when present" do + @user.private_key("monkeypants") + expect(@user.to_json).to include(%q{"private_key":"monkeypants"}) + end + + it "does not include the private key if not present" do + expect(@json).not_to include("private_key") + end + + it "includes the password if present" do + @user.password "password" + expect(@user.to_json).to include(%q{"password":"password"}) + end + + it "does not include the password if not present" do + expect(@json).not_to include("password") + end + + include_examples "to_json equalivent to Chef::JSONCompat.to_json" do + let(:jsonable) { @user } + end + end + + describe "when deserializing from JSON" do + before(:each) do + user = { "name" => "mr_spinks", + "public_key" => "turtles", + "private_key" => "pandas", + "password" => "password", + "admin" => true } + @user = Chef::OscUser.from_json(Chef::JSONCompat.to_json(user)) + end + + it "should deserialize to a Chef::OscUser object" do + expect(@user).to be_a_kind_of(Chef::OscUser) + end + + it "preserves the name" do + expect(@user.name).to eq("mr_spinks") + end + + it "preserves the public key" do + expect(@user.public_key).to eq("turtles") + end + + it "preserves the admin status" do + expect(@user.admin).to be_truthy + end + + it "includes the private key if present" do + expect(@user.private_key).to eq("pandas") + end + + it "includes the password if present" do + expect(@user.password).to eq("password") + end + + end + + describe "API Interactions" do + before (:each) do + @user = Chef::OscUser.new + @user.name "foobar" + @http_client = double("Chef::REST mock") + allow(Chef::REST).to receive(:new).and_return(@http_client) + end + + describe "list" do + before(:each) do + Chef::Config[:chef_server_url] = "http://www.example.com" + @osc_response = { "admin" => "http://www.example.com/users/admin"} + @ohc_response = [ { "user" => { "username" => "admin" }} ] + allow(Chef::OscUser).to receive(:load).with("admin").and_return(@user) + @osc_inflated_response = { "admin" => @user } + end + + it "lists all clients on an OSC server" do + allow(@http_client).to receive(:get_rest).with("users").and_return(@osc_response) + expect(Chef::OscUser.list).to eq(@osc_response) + end + + it "inflate all clients on an OSC server" do + allow(@http_client).to receive(:get_rest).with("users").and_return(@osc_response) + expect(Chef::OscUser.list(true)).to eq(@osc_inflated_response) + end + + it "lists all clients on an OHC/OPC server" do + allow(@http_client).to receive(:get_rest).with("users").and_return(@ohc_response) + # We expect that Chef::OscUser.list will give a consistent response + # so OHC API responses should be transformed to OSC-style output. + expect(Chef::OscUser.list).to eq(@osc_response) + end + + it "inflate all clients on an OHC/OPC server" do + allow(@http_client).to receive(:get_rest).with("users").and_return(@ohc_response) + expect(Chef::OscUser.list(true)).to eq(@osc_inflated_response) + end + end + + describe "create" do + it "creates a new user via the API" do + @user.password "password" + expect(@http_client).to receive(:post_rest).with("users", {:name => "foobar", :admin => false, :password => "password"}).and_return({}) + @user.create + end + end + + describe "read" do + it "loads a named user from the API" do + expect(@http_client).to receive(:get_rest).with("users/foobar").and_return({"name" => "foobar", "admin" => true, "public_key" => "pubkey"}) + user = Chef::OscUser.load("foobar") + expect(user.name).to eq("foobar") + expect(user.admin).to eq(true) + expect(user.public_key).to eq("pubkey") + end + end + + describe "update" do + it "updates an existing user on via the API" do + expect(@http_client).to receive(:put_rest).with("users/foobar", {:name => "foobar", :admin => false}).and_return({}) + @user.update + end + end + + describe "destroy" do + it "deletes the specified user via the API" do + expect(@http_client).to receive(:delete_rest).with("users/foobar") + @user.destroy + end + end + end +end diff --git a/spec/unit/platform/query_helpers_spec.rb b/spec/unit/platform/query_helpers_spec.rb index 1dbd07a021..33d4c2c3b7 100644 --- a/spec/unit/platform/query_helpers_spec.rb +++ b/spec/unit/platform/query_helpers_spec.rb @@ -20,7 +20,7 @@ require 'spec_helper' describe "Chef::Platform#windows_server_2003?" do it "returns false early when not on windows" do - allow(Chef::Platform).to receive(:windows?).and_return(false) + allow(ChefConfig).to receive(:windows?).and_return(false) expect(Chef::Platform).not_to receive(:require) expect(Chef::Platform.windows_server_2003?).to be_falsey end diff --git a/spec/unit/platform_spec.rb b/spec/unit/platform_spec.rb index e0115bc42a..36325d5411 100644 --- a/spec/unit/platform_spec.rb +++ b/spec/unit/platform_spec.rb @@ -18,29 +18,6 @@ require 'spec_helper' -describe "Chef::Platform supports" do - [ - :freebsd, - :ubuntu, - :debian, - :centos, - :fedora, - :suse, - :opensuse, - :redhat, - :oracle, - :gentoo, - :arch, - :solaris, - :gcel, - :ibm_powerkvm - ].each do |platform| - it "#{platform}" do - expect(Chef::Platform.platforms).to have_key(platform) - end - end -end - describe Chef::Platform do context "while testing with fake data" do @@ -261,41 +238,4 @@ describe Chef::Platform do 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") - expect(pmap[:package]).to eql(Chef::Provider::Package::Solaris) - end - - it "should use the IPS package provider on Solaris 11" do - pmap = Chef::Platform.find("Solaris2", "5.11") - expect(pmap[:package]).to eql(Chef::Provider::Package::Ips) - end - - it "should use the Redhat service provider on SLES11" do - 1.upto(3) do |sp| - pmap = Chef::Platform.find("SUSE", "11.#{sp}") - expect(pmap[:service]).to eql(Chef::Provider::Service::Redhat) - end - end - - it "should use the Systemd service provider on SLES12" do - pmap = Chef::Platform.find("SUSE", "12.0") - expect(pmap[:service]).to eql(Chef::Provider::Service::Systemd) - end - - it "should use the SUSE group provider on SLES11" do - 1.upto(3) do |sp| - pmap = Chef::Platform.find("SUSE", "11.#{sp}") - expect(pmap[:group]).to eql(Chef::Provider::Group::Suse) - end - end - - it "should use the Gpasswd group provider on SLES12" do - pmap = Chef::Platform.find("SUSE", "12.0") - expect(pmap[:group]).to eql(Chef::Provider::Group::Gpasswd) - end - end - end diff --git a/spec/unit/policy_builder/policyfile_spec.rb b/spec/unit/policy_builder/policyfile_spec.rb index e4f7388a1c..5fa00d8f2b 100644 --- a/spec/unit/policy_builder/policyfile_spec.rb +++ b/spec/unit/policy_builder/policyfile_spec.rb @@ -166,13 +166,17 @@ describe Chef::PolicyBuilder::Policyfile do end before do - # TODO: agree on this name and logic. + Chef::Config[:policy_document_native_api] = false Chef::Config[:deployment_group] = "example-policy-stage" allow(policy_builder).to receive(:http_api).and_return(http_api) end describe "when using compatibility mode (policy_document_native_api == false)" do + before do + Chef::Config[:deployment_group] = "example-policy-stage" + end + context "when the deployment group cannot be loaded" do let(:error404) { Net::HTTPServerException.new("404 message", :body) } @@ -389,8 +393,8 @@ describe Chef::PolicyBuilder::Policyfile do let(:example1_cookbook_data) { double("CookbookVersion Hash for example1 cookbook") } let(:example2_cookbook_data) { double("CookbookVersion Hash for example2 cookbook") } - let(:example1_cookbook_object) { double("Chef::CookbookVersion for example1 cookbook") } - let(:example2_cookbook_object) { double("Chef::CookbookVersion for example2 cookbook") } + let(:example1_cookbook_object) { double("Chef::CookbookVersion for example1 cookbook", version: "0.1.2") } + let(:example2_cookbook_object) { double("Chef::CookbookVersion for example2 cookbook", version: "1.2.3") } let(:expected_cookbook_hash) do { "example1" => example1_cookbook_object, "example2" => example2_cookbook_object } diff --git a/spec/unit/provider/deploy/revision_spec.rb b/spec/unit/provider/deploy/revision_spec.rb index 4ca64e3445..caa60878e1 100644 --- a/spec/unit/provider/deploy/revision_spec.rb +++ b/spec/unit/provider/deploy/revision_spec.rb @@ -21,7 +21,7 @@ require 'spec_helper' describe Chef::Provider::Deploy::Revision do before do - allow(Chef::Platform).to receive(:windows?) { false } + allow(ChefConfig).to receive(:windows?) { false } @temp_dir = Dir.mktmpdir Chef::Config[:file_cache_path] = @temp_dir @resource = Chef::Resource::Deploy.new("/my/deploy/dir") diff --git a/spec/unit/provider/deploy_spec.rb b/spec/unit/provider/deploy_spec.rb index c95a9b3d57..63658ac601 100644 --- a/spec/unit/provider/deploy_spec.rb +++ b/spec/unit/provider/deploy_spec.rb @@ -21,7 +21,7 @@ require 'spec_helper' describe Chef::Provider::Deploy do before do - allow(Chef::Platform).to receive(:windows?) { false } + allow(ChefConfig).to receive(:windows?) { false } @release_time = Time.utc( 2004, 8, 15, 16, 23, 42) allow(Time).to receive(:now).and_return(@release_time) @expected_release_dir = "/my/deploy/dir/releases/20040815162342" @@ -622,7 +622,7 @@ describe Chef::Provider::Deploy do gems = @provider.send(:gem_packages) - expect(gems.map { |g| g.action }).to eq([[:install]]) + expect(gems.map { |g| g.action }).to eq([:install]) expect(gems.map { |g| g.name }).to eq(%w{eventmachine}) expect(gems.map { |g| g.version }).to eq(%w{0.12.9}) end diff --git a/spec/unit/provider/directory_spec.rb b/spec/unit/provider/directory_spec.rb index 13c57bfe56..38d6db8320 100644 --- a/spec/unit/provider/directory_spec.rb +++ b/spec/unit/provider/directory_spec.rb @@ -16,173 +16,237 @@ # limitations under the License. # -require 'ostruct' - require 'spec_helper' require 'tmpdir' describe Chef::Provider::Directory do - before(:each) do - @new_resource = Chef::Resource::Directory.new(Dir.tmpdir) - if !windows? - @new_resource.owner(500) - @new_resource.group(500) - @new_resource.mode(0644) + let(:tmp_dir) { Dir.mktmpdir } + let(:new_resource) { Chef::Resource::Directory.new(tmp_dir) } + let(:node) { Chef::Node.new } + let(:events) { Chef::EventDispatch::Dispatcher.new } + let(:run_context) { Chef::RunContext.new(node, {}, events) } + let(:directory) { Chef::Provider::Directory.new(new_resource, run_context) } + + describe "#load_current_resource" do + describe "scanning file security metadata" + describe "on unix", unix_only: true do + describe "when the directory exists" do + let(:dir_stat) { File::Stat.new(tmp_dir) } + let(:expected_uid) { dir_stat.uid } + let(:expected_gid) { dir_stat.gid } + let(:expected_mode) { "0%o" % ( dir_stat.mode & 007777 ) } + let(:expected_pwnam) { Etc.getpwuid(expected_uid).name } + let(:expected_grnam) { Etc.getgrgid(expected_gid).name } + + it "describes the access mode as a String of octal integers" do + directory.load_current_resource + expect(directory.current_resource.mode).to eq(expected_mode) + end + + it "when the new_resource.owner is numeric, describes the owner as a numeric uid" do + new_resource.owner(500) + directory.load_current_resource + expect(directory.current_resource.owner).to eql(expected_uid) + end + + it "when the new_resource.group is numeric, describes the group as a numeric gid" do + new_resource.group(500) + directory.load_current_resource + expect(directory.current_resource.group).to eql(expected_gid) + end + + it "when the new_resource.owner is a string, describes the owner as a string" do + new_resource.owner("foo") + directory.load_current_resource + expect(directory.current_resource.owner).to eql(expected_pwnam) + end + + it "when the new_resource.group is a string, describes the group as a string" do + new_resource.group("bar") + directory.load_current_resource + expect(directory.current_resource.group).to eql(expected_grnam) + end + end end - @node = Chef::Node.new - @events = Chef::EventDispatch::Dispatcher.new - @run_context = Chef::RunContext.new(@node, {}, @events) - @directory = Chef::Provider::Directory.new(@new_resource, @run_context) - end + describe "on windows", windows_only: true do + describe "when the directory exists" do + it "the mode is always nil" do + directory.load_current_resource + expect(directory.current_resource.mode).to be nil + end + + it "the owner is always nil" do + directory.load_current_resource + expect(directory.current_resource.owner).to be nil + end + + it "the group is always nil" do + directory.load_current_resource + expect(directory.current_resource.group).to be nil + end + + it "rights are always nil (incorrectly)" do + directory.load_current_resource + expect(directory.current_resource.rights).to be nil + end + + it "inherits is always nil (incorrectly)" do + directory.load_current_resource + expect(directory.current_resource.inherits).to be nil + end + end + end + describe "when the directory does not exist" do + before do + FileUtils.rmdir tmp_dir + end - describe "scanning file security metadata on windows" do - before do + it "sets the mode, group and owner to nil" do + directory.load_current_resource + expect(directory.current_resource.mode).to eq(nil) + expect(directory.current_resource.group).to eq(nil) + expect(directory.current_resource.owner).to eq(nil) + end end - it "describes the directory's access rights" do - skip - end end - describe "scanning file security metadata on unix" do - before do - allow(Chef::Platform).to receive(:windows?).and_return(false) - end - let(:mock_stat) do - cstats = double("stats") - allow(cstats).to receive(:uid).and_return(500) - allow(cstats).to receive(:gid).and_return(500) - allow(cstats).to receive(:mode).and_return(0755) - cstats - end + describe "#define_resource_requirements" do + describe "on unix", unix_only: true do + it "raises an exception if the user does not exist" do + new_resource.owner("arglebargle_iv") + expect(Etc).to receive(:getpwnam).with("arglebargle_iv").and_raise(ArgumentError) + directory.action = :create + directory.load_current_resource + expect(directory.access_controls).to receive(:define_resource_requirements).and_call_original + directory.define_resource_requirements + expect { directory.process_resource_requirements }.to raise_error(ArgumentError) + end - it "describes the access mode as a String of octal integers" do - allow(File).to receive(:exists?).and_return(true) - expect(File).to receive(:stat).and_return(mock_stat) - @directory.load_current_resource - expect(@directory.current_resource.mode).to eq("0755") + it "raises an exception if the group does not exist" do + new_resource.group("arglebargle_iv") + expect(Etc).to receive(:getgrnam).with("arglebargle_iv").and_raise(ArgumentError) + directory.action = :create + directory.load_current_resource + expect(directory.access_controls).to receive(:define_resource_requirements).and_call_original + directory.define_resource_requirements + expect { directory.process_resource_requirements }.to raise_error(ArgumentError) + end end + end - context "when user and group are specified with UID/GID" do - it "describes the current owner and group as UID and GID" do - allow(File).to receive(:exists?).and_return(true) - expect(File).to receive(:stat).and_return(mock_stat) - @directory.load_current_resource - expect(@directory.current_resource.path).to eql(@new_resource.path) - expect(@directory.current_resource.owner).to eql(500) - expect(@directory.current_resource.group).to eql(500) + describe "#run_action(:create)" do + describe "when the directory exists" do + it "does not create the directory" do + expect(Dir).not_to receive(:mkdir).with(new_resource.path) + directory.run_action(:create) + end + + it "should not set the resource as updated" do + directory.run_action(:create) + expect(new_resource).not_to be_updated end end - context "when user/group are specified with user/group names" do + describe "when the directory does not exist" do + before do + FileUtils.rmdir tmp_dir + end + + it "creates the directory" do + directory.run_action(:create) + expect(File.exist?(tmp_dir)).to be true + end + + it "sets the new resource as updated" do + directory.run_action(:create) + expect(new_resource).to be_updated + end end - end - # Unix only for now. While file security attribute reporting for windows is - # disabled, unix and windows differ in the number of exists? calls that are - # made by the provider. - it "should create a new directory on create, setting updated to true", :unix_only do - @new_resource.path "/tmp/foo" + describe "when the parent directory does not exist" do + before do + new_resource.path "#{tmp_dir}/foobar" + FileUtils.rmdir tmp_dir + end - expect(File).to receive(:exists?).at_least(:once).and_return(false) - expect(File).to receive(:directory?).with("/tmp").and_return(true) - expect(Dir).to receive(:mkdir).with(@new_resource.path).once.and_return(true) + it "raises an exception when recursive is false" do + new_resource.recursive false + expect { directory.run_action(:create) }.to raise_error(Chef::Exceptions::EnclosingDirectoryDoesNotExist) + end - expect(@directory).to receive(:do_acl_changes) - allow(@directory).to receive(:do_selinux) - @directory.run_action(:create) - expect(@directory.new_resource).to be_updated - end + it "creates the directories when recursive is true" do + new_resource.recursive true + directory.run_action(:create) + expect(new_resource).to be_updated + expect(File.exist?("#{tmp_dir}/foobar")).to be true + end - it "should raise an exception if the parent directory does not exist and recursive is false" do - @new_resource.path "/tmp/some/dir" - @new_resource.recursive false - expect { @directory.run_action(:create) }.to raise_error(Chef::Exceptions::EnclosingDirectoryDoesNotExist) - end + it "raises an exception when the parent directory is a file and recursive is true" do + FileUtils.touch tmp_dir + new_resource.recursive true + expect { directory.run_action(:create) }.to raise_error + end - # Unix only for now. While file security attribute reporting for windows is - # disabled, unix and windows differ in the number of exists? calls that are - # made by the provider. - it "should create a new directory when parent directory does not exist if recursive is true and permissions are correct", :unix_only do - @new_resource.path "/path/to/dir" - @new_resource.recursive true - expect(File).to receive(:exists?).with(@new_resource.path).ordered.and_return(false) - - expect(File).to receive(:exists?).with('/path/to').ordered.and_return(false) - expect(File).to receive(:exists?).with('/path').ordered.and_return(true) - expect(Chef::FileAccessControl).to receive(:writable?).with('/path').ordered.and_return(true) - expect(File).to receive(:exists?).with(@new_resource.path).ordered.and_return(false) - - expect(FileUtils).to receive(:mkdir_p).with(@new_resource.path).and_return(true) - expect(@directory).to receive(:do_acl_changes) - allow(@directory).to receive(:do_selinux) - @directory.run_action(:create) - expect(@new_resource).to be_updated + it "raises the right exception when the parent directory is a file and recursive is true" do + pending "this seems to return the wrong error" # FIXME + FileUtils.touch tmp_dir + new_resource.recursive true + expect { directory.run_action(:create) }.to raise_error(Chef::Exceptions::EnclosingDirectoryDoesNotExist) + end + end end + describe "#run_action(:create)" do + describe "when the directory exists" do + it "deletes the directory" do + directory.run_action(:delete) + expect(File.exist?(tmp_dir)).to be false + end - it "should raise an error when creating a directory when parent directory is a file" do - expect(File).to receive(:directory?).and_return(false) - expect(Dir).not_to receive(:mkdir).with(@new_resource.path) - expect { @directory.run_action(:create) }.to raise_error(Chef::Exceptions::EnclosingDirectoryDoesNotExist) - expect(@directory.new_resource).not_to be_updated - end + it "sets the new resource as updated" do + directory.run_action(:delete) + expect(new_resource).to be_updated + end + end - # Unix only for now. While file security attribute reporting for windows is - # disabled, unix and windows differ in the number of exists? calls that are - # made by the provider. - it "should not create the directory if it already exists", :unix_only do - stub_file_cstats - @new_resource.path "/tmp/foo" - expect(File).to receive(:directory?).at_least(:once).and_return(true) - expect(Chef::FileAccessControl).to receive(:writable?).with("/tmp").and_return(true) - expect(File).to receive(:exists?).at_least(:once).and_return(true) - expect(Dir).not_to receive(:mkdir).with(@new_resource.path) - expect(@directory).to receive(:do_acl_changes) - @directory.run_action(:create) - end + describe "when the directory does not exist" do + before do + FileUtils.rmdir tmp_dir + end - it "should delete the directory if it exists, and is writable with action_delete" do - expect(File).to receive(:directory?).and_return(true) - expect(Chef::FileAccessControl).to receive(:writable?).once.and_return(true) - expect(Dir).to receive(:delete).with(@new_resource.path).once.and_return(true) - @directory.run_action(:delete) - end + it "does not delete the directory" do + expect(Dir).not_to receive(:delete).with(new_resource.path) + directory.run_action(:delete) + end - it "should raise an exception if it cannot delete the directory due to bad permissions" do - allow(File).to receive(:exists?).and_return(true) - allow(Chef::FileAccessControl).to receive(:writable?).and_return(false) - expect { @directory.run_action(:delete) }.to raise_error(RuntimeError) - end + it "sets the new resource as updated" do + directory.run_action(:delete) + expect(new_resource).not_to be_updated + end + end - it "should take no action when deleting a target directory that does not exist" do - @new_resource.path "/an/invalid/path" - allow(File).to receive(:exists?).and_return(false) - expect(Dir).not_to receive(:delete).with(@new_resource.path) - @directory.run_action(:delete) - expect(@directory.new_resource).not_to be_updated - end + describe "when the directory is not writable" do + before do + allow(Chef::FileAccessControl).to receive(:writable?).and_return(false) + end - it "should raise an exception when deleting a directory when target directory is a file" do - stub_file_cstats - @new_resource.path "/an/invalid/path" - allow(File).to receive(:exists?).and_return(true) - expect(File).to receive(:directory?).and_return(false) - expect(Dir).not_to receive(:delete).with(@new_resource.path) - expect { @directory.run_action(:delete) }.to raise_error(RuntimeError) - expect(@directory.new_resource).not_to be_updated - end + it "cannot delete it and raises an exception" do + expect { directory.run_action(:delete) }.to raise_error(RuntimeError) + end + end + + describe "when the target directory is a file" do + before do + FileUtils.rmdir tmp_dir + FileUtils.touch tmp_dir + end - def stub_file_cstats - cstats = double("stats") - allow(cstats).to receive(:uid).and_return(500) - allow(cstats).to receive(:gid).and_return(500) - allow(cstats).to receive(:mode).and_return(0755) - # File.stat is called in: - # - Chef::Provider::File.load_current_resource_attrs - # - Chef::ScanAccessControl via Chef::Provider::File.setup_acl - allow(File).to receive(:stat).and_return(cstats) + it "cannot delete it and raises an exception" do + expect { directory.run_action(:delete) }.to raise_error(RuntimeError) + end + end end end diff --git a/spec/unit/provider/execute_spec.rb b/spec/unit/provider/execute_spec.rb index 51305b6225..1274203ce3 100644 --- a/spec/unit/provider/execute_spec.rb +++ b/spec/unit/provider/execute_spec.rb @@ -39,7 +39,7 @@ describe Chef::Provider::Execute do let(:new_resource) { Chef::Resource::Execute.new("foo_resource", run_context) } before do - allow(Chef::Platform).to receive(:windows?) { false } + allow(ChefConfig).to receive(:windows?) { false } @original_log_level = Chef::Log.level Chef::Log.level = :info allow(STDOUT).to receive(:tty?).and_return(true) diff --git a/spec/unit/provider/ifconfig/debian_spec.rb b/spec/unit/provider/ifconfig/debian_spec.rb index 351e734040..0c02ae9cd4 100644 --- a/spec/unit/provider/ifconfig/debian_spec.rb +++ b/spec/unit/provider/ifconfig/debian_spec.rb @@ -144,11 +144,6 @@ EOF 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_falsey - end end context "when the /etc/network/interfaces file does not have the source line" do @@ -280,11 +275,6 @@ another line 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_falsey - end end context "when the /etc/network/interfaces file does not have the source line" do diff --git a/spec/unit/provider/package/aix_spec.rb b/spec/unit/provider/package/aix_spec.rb index 5bc861b849..13992cb8d1 100644 --- a/spec/unit/provider/package/aix_spec.rb +++ b/spec/unit/provider/package/aix_spec.rb @@ -36,23 +36,27 @@ describe Chef::Provider::Package::Aix do @bffinfo ="/usr/lib/objrepos:samba.base:3.3.12.0::COMMITTED:I:Samba for AIX: /etc/objrepos:samba.base:3.3.12.0::COMMITTED:I:Samba for AIX:" - @status = double("Status", :stdout => "", :exitstatus => 0) + @empty_status = double("Status", :stdout => "", :exitstatus => 0) end it "should create a current resource with the name of new_resource" do - allow(@provider).to receive(:shell_out).and_return(@status) + status = double("Status", :stdout => @bffinfo, :exitstatus => 0) + expect(@provider).to receive(:shell_out).with("installp -L -d /tmp/samba.base", timeout: 900).and_return(status) + expect(@provider).to receive(:shell_out).with("lslpp -lcq samba.base", timeout: 900).and_return(@empty_status) @provider.load_current_resource expect(@provider.current_resource.name).to eq("samba.base") end it "should set the current resource bff package name to the new resource bff package name" do - allow(@provider).to receive(:shell_out).and_return(@status) + status = double("Status", :stdout => @bffinfo, :exitstatus => 0) + expect(@provider).to receive(:shell_out).with("installp -L -d /tmp/samba.base", timeout: 900).and_return(status) + expect(@provider).to receive(:shell_out).with("lslpp -lcq samba.base", timeout: 900).and_return(@empty_status) @provider.load_current_resource expect(@provider.current_resource.package_name).to eq("samba.base") end it "should raise an exception if a source is supplied but not found" do - allow(@provider).to receive(:shell_out).and_return(@status) + allow(@provider).to receive(:shell_out).and_return(@empty_status) allow(::File).to receive(:exists?).and_return(false) @provider.load_current_resource @provider.define_resource_requirements @@ -61,8 +65,8 @@ describe Chef::Provider::Package::Aix do it "should get the source package version from lslpp if provided" do status = double("Status", :stdout => @bffinfo, :exitstatus => 0) - expect(@provider).to receive(:shell_out).with("installp -L -d /tmp/samba.base").and_return(status) - expect(@provider).to receive(:shell_out).with("lslpp -lcq samba.base").and_return(@status) + expect(@provider).to receive(:shell_out).with("installp -L -d /tmp/samba.base", timeout: 900).and_return(status) + expect(@provider).to receive(:shell_out).with("lslpp -lcq samba.base", timeout: 900).and_return(@empty_status) @provider.load_current_resource expect(@provider.current_resource.package_name).to eq("samba.base") @@ -73,8 +77,8 @@ describe Chef::Provider::Package::Aix do status = double("Status", :stdout => @bffinfo, :exitstatus => 0) @stdout = StringIO.new(@bffinfo) @stdin, @stderr = StringIO.new, StringIO.new - expect(@provider).to receive(:shell_out).with("installp -L -d /tmp/samba.base").and_return(@status) - expect(@provider).to receive(:shell_out).with("lslpp -lcq samba.base").and_return(status) + expect(@provider).to receive(:shell_out).with("installp -L -d /tmp/samba.base", timeout: 900).and_return(status) + expect(@provider).to receive(:shell_out).with("lslpp -lcq samba.base", timeout: 900).and_return(status) @provider.load_current_resource expect(@provider.current_resource.version).to eq("3.3.12.0") end @@ -94,12 +98,20 @@ describe Chef::Provider::Package::Aix do end it "should return a current resource with a nil version if the package is not found" do - status = double(:stdout => "", :exitstatus => 0) - expect(@provider).to receive(:shell_out).with("installp -L -d /tmp/samba.base").and_return(status) - expect(@provider).to receive(:shell_out).with("lslpp -lcq samba.base").and_return(status) + status = double("Status", :stdout => @bffinfo, :exitstatus => 0) + expect(@provider).to receive(:shell_out).with("installp -L -d /tmp/samba.base", timeout: 900).and_return(status) + expect(@provider).to receive(:shell_out).with("lslpp -lcq samba.base", timeout: 900).and_return(@empty_status) @provider.load_current_resource expect(@provider.current_resource.version).to be_nil end + + it "should raise an exception if the source doesn't provide the requested package" do + wrongbffinfo = "/usr/lib/objrepos:openssl.base:0.9.8.2400::COMMITTED:I:Open Secure Socket Layer: +/etc/objrepos:openssl.base:0.9.8.2400::COMMITTED:I:Open Secure Socket Layer:" + status = double("Status", :stdout => wrongbffinfo, :exitstatus => 0) + expect(@provider).to receive(:shell_out).with("installp -L -d /tmp/samba.base", timeout: 900).and_return(status) + expect { @provider.load_current_resource }.to raise_error(Chef::Exceptions::Package) + end end describe "candidate_version" do @@ -125,7 +137,7 @@ describe Chef::Provider::Package::Aix do describe "install and upgrade" do it "should run installp -aYF -d with the package source to install" do - expect(@provider).to receive(:shell_out!).with("installp -aYF -d /tmp/samba.base samba.base") + expect(@provider).to receive(:shell_out!).with("installp -aYF -d /tmp/samba.base samba.base", timeout: 900) @provider.install_package("samba.base", "3.3.12.0") end @@ -133,26 +145,26 @@ describe Chef::Provider::Package::Aix do @new_resource = Chef::Resource::Package.new("/tmp/samba.base") @provider = Chef::Provider::Package::Aix.new(@new_resource, @run_context) expect(@new_resource.source).to eq("/tmp/samba.base") - expect(@provider).to receive(:shell_out!).with("installp -aYF -d /tmp/samba.base /tmp/samba.base") + expect(@provider).to receive(:shell_out!).with("installp -aYF -d /tmp/samba.base /tmp/samba.base", timeout: 900) @provider.install_package("/tmp/samba.base", "3.3.12.0") end it "should run installp with -eLogfile option." do allow(@new_resource).to receive(:options).and_return("-e/tmp/installp.log") - expect(@provider).to receive(:shell_out!).with("installp -aYF -e/tmp/installp.log -d /tmp/samba.base samba.base") + expect(@provider).to receive(:shell_out!).with("installp -aYF -e/tmp/installp.log -d /tmp/samba.base samba.base", timeout: 900) @provider.install_package("samba.base", "3.3.12.0") end end describe "remove" do it "should run installp -u samba.base to remove the package" do - expect(@provider).to receive(:shell_out!).with("installp -u samba.base") + expect(@provider).to receive(:shell_out!).with("installp -u samba.base", timeout: 900) @provider.remove_package("samba.base", "3.3.12.0") end it "should run installp -u -e/tmp/installp.log with options -e/tmp/installp.log" do allow(@new_resource).to receive(:options).and_return("-e/tmp/installp.log") - expect(@provider).to receive(:shell_out!).with("installp -u -e/tmp/installp.log samba.base") + expect(@provider).to receive(:shell_out!).with("installp -u -e/tmp/installp.log samba.base", timeout: 900) @provider.remove_package("samba.base", "3.3.12.0") end diff --git a/spec/unit/provider/package/dpkg_spec.rb b/spec/unit/provider/package/dpkg_spec.rb index 3fd86218d2..4974cff934 100644 --- a/spec/unit/provider/package/dpkg_spec.rb +++ b/spec/unit/provider/package/dpkg_spec.rb @@ -51,7 +51,7 @@ describe Chef::Provider::Package::Dpkg do describe 'gets the source package version from dpkg-deb' do def check_version(version) @status = double(:stdout => "wget\t#{version}", :exitstatus => 0) - allow(@provider).to receive(:shell_out).with("dpkg-deb -W #{@new_resource.source}").and_return(@status) + allow(@provider).to receive(:shell_out).with("dpkg-deb -W #{@new_resource.source}", timeout: 900).and_return(@status) @provider.load_current_resource expect(@provider.current_resource.package_name).to eq("wget") expect(@new_resource.version).to eq(version) @@ -106,7 +106,7 @@ Depends: libc6 (>= 2.8~20080505), libssl0.9.8 (>= 0.9.8f-5) Conflicts: wget-ssl DPKG_S status = double(:stdout => stdout, :exitstatus => 1) - allow(@provider).to receive(:shell_out).with("dpkg -s wget").and_return(status) + allow(@provider).to receive(:shell_out).with("dpkg -s wget", timeout: 900).and_return(status) @provider.load_current_resource expect(@provider.current_resource.version).to eq("1.11.4-1ubuntu1") diff --git a/spec/unit/provider/package/freebsd/pkg_spec.rb b/spec/unit/provider/package/freebsd/pkg_spec.rb index f67161930f..d1f5a649bc 100644 --- a/spec/unit/provider/package/freebsd/pkg_spec.rb +++ b/spec/unit/provider/package/freebsd/pkg_spec.rb @@ -77,7 +77,7 @@ describe Chef::Provider::Package::Freebsd::Pkg, "load_current_resource" do it "should return the version number when it is installed" do pkg_info = OpenStruct.new(:stdout => "zsh-4.3.6_7") - expect(@provider).to receive(:shell_out!).with('pkg_info -E "zsh*"', :env => nil, :returns => [0,1]).and_return(pkg_info) + expect(@provider).to receive(:shell_out!).with('pkg_info -E "zsh*"', env: nil, returns: [0,1], timeout: 900).and_return(pkg_info) #@provider.should_receive(:popen4).with('pkg_info -E "zsh*"').and_yield(@pid, @stdin, ["zsh-4.3.6_7"], @stderr).and_return(@status) allow(@provider).to receive(:package_name).and_return("zsh") expect(@provider.current_installed_version).to eq("4.3.6_7") @@ -85,14 +85,14 @@ describe Chef::Provider::Package::Freebsd::Pkg, "load_current_resource" do it "does not set the current version number when the package is not installed" do pkg_info = OpenStruct.new(:stdout => "") - expect(@provider).to receive(:shell_out!).with('pkg_info -E "zsh*"', :env => nil, :returns => [0,1]).and_return(pkg_info) + expect(@provider).to receive(:shell_out!).with('pkg_info -E "zsh*"', env: nil, returns: [0,1], timeout: 900).and_return(pkg_info) allow(@provider).to receive(:package_name).and_return("zsh") expect(@provider.current_installed_version).to be_nil end it "should return the port path for a valid port name" do whereis = OpenStruct.new(:stdout => "zsh: /usr/ports/shells/zsh") - expect(@provider).to receive(:shell_out!).with("whereis -s zsh", :env => nil).and_return(whereis) + expect(@provider).to receive(:shell_out!).with("whereis -s zsh", env: nil, timeout: 900).and_return(whereis) #@provider.should_receive(:popen4).with("whereis -s zsh").and_yield(@pid, @stdin, ["zsh: /usr/ports/shells/zsh"], @stderr).and_return(@status) allow(@provider).to receive(:port_name).and_return("zsh") expect(@provider.port_path).to eq("/usr/ports/shells/zsh") @@ -102,7 +102,7 @@ describe Chef::Provider::Package::Freebsd::Pkg, "load_current_resource" do it "should return the ports candidate version when given a valid port path" do allow(@provider).to receive(:port_path).and_return("/usr/ports/shells/zsh") make_v = OpenStruct.new(:stdout => "4.3.6\n", :exitstatus => 0) - expect(@provider).to receive(:shell_out!).with("make -V PORTVERSION", {:cwd=>"/usr/ports/shells/zsh", :returns=>[0, 1], :env=>nil}).and_return(make_v) + expect(@provider).to receive(:shell_out!).with("make -V PORTVERSION", {cwd: "/usr/ports/shells/zsh", returns: [0, 1], env: nil, timeout: 900}).and_return(make_v) expect(@provider.ports_candidate_version).to eq("4.3.6") end @@ -110,7 +110,7 @@ describe Chef::Provider::Package::Freebsd::Pkg, "load_current_resource" do allow(::File).to receive(:exist?).with('/usr/ports/Makefile').and_return(true) allow(@provider).to receive(:port_path).and_return("/usr/ports/shells/zsh") make_v = OpenStruct.new(:stdout => "zsh-4.3.6_7\n", :exitstatus => 0) - expect(@provider).to receive(:shell_out!).with("make -V PKGNAME", {:cwd=>"/usr/ports/shells/zsh", :env=>nil, :returns=>[0, 1]}).and_return(make_v) + expect(@provider).to receive(:shell_out!).with("make -V PKGNAME", {cwd: "/usr/ports/shells/zsh", env: nil, returns: [0, 1], timeout: 900}).and_return(make_v) #@provider.should_receive(:ports_makefile_variable_value).with("PKGNAME").and_return("zsh-4.3.6_7") expect(@provider.package_name).to eq("zsh") end @@ -127,7 +127,7 @@ describe Chef::Provider::Package::Freebsd::Pkg, "load_current_resource" do end it "should run pkg_add -r with the package name" do - expect(@provider).to receive(:shell_out!).with("pkg_add -r zsh", :env => nil).and_return(@cmd_result) + expect(@provider).to receive(:shell_out!).with("pkg_add -r zsh", env: nil, timeout: 900).and_return(@cmd_result) @provider.install_package("zsh", "4.3.6_7") end end @@ -142,7 +142,7 @@ describe Chef::Provider::Package::Freebsd::Pkg, "load_current_resource" do it "should figure out the port path from the package_name using whereis" do whereis = OpenStruct.new(:stdout => "zsh: /usr/ports/shells/zsh") - expect(@provider).to receive(:shell_out!).with("whereis -s zsh", :env=>nil).and_return(whereis) + expect(@provider).to receive(:shell_out!).with("whereis -s zsh", env: nil, timeout: 900).and_return(whereis) expect(@provider.port_path).to eq("/usr/ports/shells/zsh") end @@ -178,7 +178,7 @@ describe Chef::Provider::Package::Freebsd::Pkg, "load_current_resource" do end it "should run pkg_add -r with the package name" do - expect(@provider).to receive(:shell_out!).with("pkg_add -r ruby18-iconv", :env => nil).and_return(@install_result) + expect(@provider).to receive(:shell_out!).with("pkg_add -r ruby18-iconv", env: nil, timeout: 900).and_return(@install_result) @provider.install_package("ruby-iconv", "1.0") end end @@ -193,7 +193,7 @@ describe Chef::Provider::Package::Freebsd::Pkg, "load_current_resource" do end it "should run pkg_delete with the package name and version" do - expect(@provider).to receive(:shell_out!).with("pkg_delete zsh-4.3.6_7", :env => nil).and_return(@pkg_delete) + expect(@provider).to receive(:shell_out!).with("pkg_delete zsh-4.3.6_7", env: nil, timeout: 900).and_return(@pkg_delete) @provider.remove_package("zsh", "4.3.6_7") end end @@ -213,14 +213,14 @@ describe Chef::Provider::Package::Freebsd::Pkg, "load_current_resource" do it "should return the port path for a valid port name" do whereis = OpenStruct.new(:stdout => "bonnie++: /usr/ports/benchmarks/bonnie++") - expect(@provider).to receive(:shell_out!).with("whereis -s bonnie++", :env => nil).and_return(whereis) + expect(@provider).to receive(:shell_out!).with("whereis -s bonnie++", env: nil, timeout: 900).and_return(whereis) allow(@provider).to receive(:port_name).and_return("bonnie++") expect(@provider.port_path).to eq("/usr/ports/benchmarks/bonnie++") end it "should return the version number when it is installed" do pkg_info = OpenStruct.new(:stdout => "bonnie++-1.96") - expect(@provider).to receive(:shell_out!).with('pkg_info -E "bonnie++*"', :env => nil, :returns => [0,1]).and_return(pkg_info) + expect(@provider).to receive(:shell_out!).with('pkg_info -E "bonnie++*"', env: nil, returns: [0,1], timeout: 900).and_return(pkg_info) allow(@provider).to receive(:package_name).and_return("bonnie++") expect(@provider.current_installed_version).to eq("1.96") end @@ -253,7 +253,7 @@ describe Chef::Provider::Package::Freebsd::Pkg, "load_current_resource" do allow(@provider).to receive(:latest_link_name).and_return("perl") cmd = OpenStruct.new(:status => true) - expect(@provider).to receive(:shell_out!).with("pkg_add -r perl", :env => nil).and_return(cmd) + expect(@provider).to receive(:shell_out!).with("pkg_add -r perl", env: nil, timeout: 900).and_return(cmd) @provider.install_package("perl5.8", "5.8.8_1") end @@ -267,7 +267,7 @@ describe Chef::Provider::Package::Freebsd::Pkg, "load_current_resource" do allow(@provider).to receive(:latest_link_name).and_return("mysql50-server") cmd = OpenStruct.new(:status => true) - expect(@provider).to receive(:shell_out!).with("pkg_add -r mysql50-server", :env=>nil).and_return(cmd) + expect(@provider).to receive(:shell_out!).with("pkg_add -r mysql50-server", env: nil, timeout: 900).and_return(cmd) @provider.install_package("mysql50-server", "5.0.45_1") end end diff --git a/spec/unit/provider/package/freebsd/pkgng_spec.rb b/spec/unit/provider/package/freebsd/pkgng_spec.rb index 0c1e89c7ab..59215f855b 100644 --- a/spec/unit/provider/package/freebsd/pkgng_spec.rb +++ b/spec/unit/provider/package/freebsd/pkgng_spec.rb @@ -71,7 +71,7 @@ describe Chef::Provider::Package::Freebsd::Port do end it "should query pkg database" do - expect(@provider).to receive(:shell_out!).with('pkg info "zsh"', :env => nil, :returns => [0,70]).and_return(@pkg_info) + expect(@provider).to receive(:shell_out!).with('pkg info "zsh"', env: nil, returns: [0,70], timeout: 900).and_return(@pkg_info) expect(@provider.current_installed_version).to eq("3.1.7") end end @@ -80,14 +80,14 @@ describe Chef::Provider::Package::Freebsd::Port do describe "determining candidate version" do it "should query repository" do pkg_query = OpenStruct.new(:stdout => "5.0.5\n", :exitstatus => 0) - expect(@provider).to receive(:shell_out!).with("pkg rquery '%v' zsh", :env => nil).and_return(pkg_query) + expect(@provider).to receive(:shell_out!).with("pkg rquery '%v' zsh", env: nil, timeout: 900).and_return(pkg_query) expect(@provider.candidate_version).to eq("5.0.5") end it "should query specified repository when given option" do @provider.new_resource.options('-r LocalMirror') # This requires LocalMirror repo configuration. pkg_query = OpenStruct.new(:stdout => "5.0.3\n", :exitstatus => 0) - expect(@provider).to receive(:shell_out!).with("pkg rquery -r LocalMirror '%v' zsh", :env => nil).and_return(pkg_query) + expect(@provider).to receive(:shell_out!).with("pkg rquery -r LocalMirror '%v' zsh", env: nil, timeout: 900).and_return(pkg_query) expect(@provider.candidate_version).to eq("5.0.3") end @@ -106,7 +106,7 @@ describe Chef::Provider::Package::Freebsd::Port do it "should handle package source from file" do @provider.new_resource.source("/nas/pkg/repo/zsh-5.0.1.txz") expect(@provider).to receive(:shell_out!). - with("pkg add /nas/pkg/repo/zsh-5.0.1.txz", :env => { 'LC_ALL' => nil }). + with("pkg add /nas/pkg/repo/zsh-5.0.1.txz", env: { 'LC_ALL' => nil }, timeout: 900). and_return(@install_result) @provider.install_package("zsh", "5.0.1") end @@ -114,21 +114,21 @@ describe Chef::Provider::Package::Freebsd::Port do it "should handle package source over ftp or http" do @provider.new_resource.source("http://repo.example.com/zsh-5.0.1.txz") expect(@provider).to receive(:shell_out!). - with("pkg add http://repo.example.com/zsh-5.0.1.txz", :env => { 'LC_ALL' => nil }). + with("pkg add http://repo.example.com/zsh-5.0.1.txz", env: { 'LC_ALL' => nil }, timeout: 900). and_return(@install_result) @provider.install_package("zsh", "5.0.1") end it "should handle a package name" do expect(@provider).to receive(:shell_out!). - with("pkg install -y zsh", :env => { 'LC_ALL' => nil }).and_return(@install_result) + with("pkg install -y zsh", env: { 'LC_ALL' => nil }, timeout: 900).and_return(@install_result) @provider.install_package("zsh", "5.0.1") end it "should handle a package name with a specified repo" do @provider.new_resource.options('-r LocalMirror') # This requires LocalMirror repo configuration. expect(@provider).to receive(:shell_out!). - with("pkg install -y -r LocalMirror zsh", :env => { 'LC_ALL' => nil }).and_return(@install_result) + with("pkg install -y -r LocalMirror zsh", env: { 'LC_ALL' => nil }, timeout: 900).and_return(@install_result) @provider.install_package("zsh", "5.0.1") end end @@ -141,14 +141,14 @@ describe Chef::Provider::Package::Freebsd::Port do it "should call pkg delete" do expect(@provider).to receive(:shell_out!). - with("pkg delete -y zsh-5.0.1", :env => nil).and_return(@install_result) + with("pkg delete -y zsh-5.0.1", env: nil, timeout: 900).and_return(@install_result) @provider.remove_package("zsh", "5.0.1") end it "should not include repo option in pkg delete" do @provider.new_resource.options('-r LocalMirror') # This requires LocalMirror repo configuration. expect(@provider).to receive(:shell_out!). - with("pkg delete -y zsh-5.0.1", :env => nil).and_return(@install_result) + with("pkg delete -y zsh-5.0.1", env: nil, timeout: 900).and_return(@install_result) @provider.remove_package("zsh", "5.0.1") end end diff --git a/spec/unit/provider/package/freebsd/port_spec.rb b/spec/unit/provider/package/freebsd/port_spec.rb index 2e32e88f97..4b23575740 100644 --- a/spec/unit/provider/package/freebsd/port_spec.rb +++ b/spec/unit/provider/package/freebsd/port_spec.rb @@ -72,7 +72,7 @@ describe Chef::Provider::Package::Freebsd::Port do it "should check 'pkg_info' if system uses pkg_* tools" do allow(@new_resource).to receive(:supports_pkgng?) expect(@new_resource).to receive(:supports_pkgng?).and_return(false) - expect(@provider).to receive(:shell_out!).with('pkg_info -E "zsh*"', :env => nil, :returns => [0,1]).and_return(@pkg_info) + expect(@provider).to receive(:shell_out!).with('pkg_info -E "zsh*"', env: nil, returns: [0,1], timeout: 900).and_return(@pkg_info) expect(@provider.current_installed_version).to eq("3.1.7") end @@ -80,8 +80,8 @@ describe Chef::Provider::Package::Freebsd::Port do pkg_enabled = OpenStruct.new(:stdout => "yes\n") [1000016, 1000000, 901503, 902506, 802511].each do |__freebsd_version| @node.automatic_attrs[:os_version] = __freebsd_version - expect(@new_resource).to receive(:shell_out!).with('make -V WITH_PKGNG', :env => nil).and_return(pkg_enabled) - expect(@provider).to receive(:shell_out!).with('pkg info "zsh"', :env => nil, :returns => [0,70]).and_return(@pkg_info) + expect(@new_resource).to receive(:shell_out!).with('make -V WITH_PKGNG', env: nil).and_return(pkg_enabled) + expect(@provider).to receive(:shell_out!).with('pkg info "zsh"', env: nil, returns: [0,70], timeout: 900).and_return(@pkg_info) expect(@provider.current_installed_version).to eq("3.1.7") end end @@ -89,7 +89,7 @@ describe Chef::Provider::Package::Freebsd::Port do it "should check 'pkg info' if the freebsd version is greater than or equal to 1000017" do __freebsd_version = 1000017 @node.automatic_attrs[:os_version] = __freebsd_version - expect(@provider).to receive(:shell_out!).with('pkg info "zsh"', :env => nil, :returns => [0,70]).and_return(@pkg_info) + expect(@provider).to receive(:shell_out!).with('pkg info "zsh"', env: nil, returns: [0,70], timeout: 900).and_return(@pkg_info) expect(@provider.current_installed_version).to eq("3.1.7") end end @@ -102,7 +102,7 @@ describe Chef::Provider::Package::Freebsd::Port do it "should return candidate version if port exists" do allow(::File).to receive(:exist?).with('/usr/ports/Makefile').and_return(true) allow(@provider).to receive(:port_dir).and_return('/usr/ports/shells/zsh') - expect(@provider).to receive(:shell_out!).with("make -V PORTVERSION", :cwd => "/usr/ports/shells/zsh", :env => nil, :returns => [0,1]). + expect(@provider).to receive(:shell_out!).with("make -V PORTVERSION", cwd: "/usr/ports/shells/zsh", env: nil, returns: [0,1], timeout: 900). and_return(@port_version) expect(@provider.candidate_version).to eq("5.0.5") end @@ -127,13 +127,13 @@ describe Chef::Provider::Package::Freebsd::Port do it "should query system for path given just a name" do whereis = OpenStruct.new(:stdout => "zsh: /usr/ports/shells/zsh\n") - expect(@provider).to receive(:shell_out!).with("whereis -s zsh", :env => nil).and_return(whereis) + expect(@provider).to receive(:shell_out!).with("whereis -s zsh", env: nil, timeout: 900).and_return(whereis) expect(@provider.port_dir).to eq("/usr/ports/shells/zsh") end it "should raise exception if not found" do whereis = OpenStruct.new(:stdout => "zsh:\n") - expect(@provider).to receive(:shell_out!).with("whereis -s zsh", :env => nil).and_return(whereis) + expect(@provider).to receive(:shell_out!).with("whereis -s zsh", env: nil, timeout: 900).and_return(whereis) expect { @provider.port_dir }.to raise_error(Chef::Exceptions::Package, "Could not find port with the name zsh") end end diff --git a/spec/unit/provider/package/ips_spec.rb b/spec/unit/provider/package/ips_spec.rb index 342ac4c040..ad69dffb10 100644 --- a/spec/unit/provider/package/ips_spec.rb +++ b/spec/unit/provider/package/ips_spec.rb @@ -65,28 +65,28 @@ PKG_STATUS context "when loading current resource" do it "should create a current resource with the name of the new_resource" do - expect(@provider).to receive(:shell_out).with("pkg info #{@new_resource.package_name}").and_return(local_output) - expect(@provider).to receive(:shell_out!).with("pkg info -r #{@new_resource.package_name}").and_return(remote_output) + expect(@provider).to receive(:shell_out).with("pkg info #{@new_resource.package_name}", timeout: 900).and_return(local_output) + expect(@provider).to receive(:shell_out!).with("pkg info -r #{@new_resource.package_name}", timeout: 900).and_return(remote_output) expect(Chef::Resource::Package).to receive(:new).and_return(@current_resource) @provider.load_current_resource end it "should set the current resources package name to the new resources package name" do - expect(@provider).to receive(:shell_out).with("pkg info #{@new_resource.package_name}").and_return(local_output) - expect(@provider).to receive(:shell_out!).with("pkg info -r #{@new_resource.package_name}").and_return(remote_output) + expect(@provider).to receive(:shell_out).with("pkg info #{@new_resource.package_name}", timeout: 900).and_return(local_output) + expect(@provider).to receive(:shell_out!).with("pkg info -r #{@new_resource.package_name}", timeout: 900).and_return(remote_output) @provider.load_current_resource expect(@current_resource.package_name).to eq(@new_resource.package_name) end it "should run pkg info with the package name" do - expect(@provider).to receive(:shell_out).with("pkg info #{@new_resource.package_name}").and_return(local_output) - expect(@provider).to receive(:shell_out!).with("pkg info -r #{@new_resource.package_name}").and_return(remote_output) + expect(@provider).to receive(:shell_out).with("pkg info #{@new_resource.package_name}", timeout: 900).and_return(local_output) + expect(@provider).to receive(:shell_out!).with("pkg info -r #{@new_resource.package_name}", timeout: 900).and_return(remote_output) @provider.load_current_resource end it "should set the installed version to nil on the current resource if package state is not installed" do - expect(@provider).to receive(:shell_out).with("pkg info #{@new_resource.package_name}").and_return(local_output) - expect(@provider).to receive(:shell_out!).with("pkg info -r #{@new_resource.package_name}").and_return(remote_output) + expect(@provider).to receive(:shell_out).with("pkg info #{@new_resource.package_name}", timeout: 900).and_return(local_output) + expect(@provider).to receive(:shell_out!).with("pkg info -r #{@new_resource.package_name}", timeout: 900).and_return(remote_output) @provider.load_current_resource expect(@current_resource.version).to be_nil end @@ -108,27 +108,27 @@ Packaging Date: October 19, 2011 09:14:50 AM Size: 8.07 MB FMRI: pkg://solaris/crypto/gnupg@2.0.17,5.11-0.175.0.0.0.2.537:20111019T091450Z INSTALLED - expect(@provider).to receive(:shell_out).with("pkg info #{@new_resource.package_name}").and_return(local) - expect(@provider).to receive(:shell_out!).with("pkg info -r #{@new_resource.package_name}").and_return(remote_output) + expect(@provider).to receive(:shell_out).with("pkg info #{@new_resource.package_name}", timeout: 900).and_return(local) + expect(@provider).to receive(:shell_out!).with("pkg info -r #{@new_resource.package_name}", timeout: 900).and_return(remote_output) @provider.load_current_resource expect(@current_resource.version).to eq("2.0.17") end it "should return the current resource" do - expect(@provider).to receive(:shell_out).with("pkg info #{@new_resource.package_name}").and_return(local_output) - expect(@provider).to receive(:shell_out!).with("pkg info -r #{@new_resource.package_name}").and_return(remote_output) + expect(@provider).to receive(:shell_out).with("pkg info #{@new_resource.package_name}", timeout: 900).and_return(local_output) + expect(@provider).to receive(:shell_out!).with("pkg info -r #{@new_resource.package_name}", timeout: 900).and_return(remote_output) expect(@provider.load_current_resource).to eql(@current_resource) end end context "when installing a package" do it "should run pkg install with the package name and version" do - expect(@provider).to receive(:shell_out).with("pkg install -q crypto/gnupg@2.0.17") + expect(@provider).to receive(:shell_out).with("pkg install -q crypto/gnupg@2.0.17", timeout: 900) @provider.install_package("crypto/gnupg", "2.0.17") end it "should run pkg install with the package name and version and options if specified" do - expect(@provider).to receive(:shell_out).with("pkg --no-refresh install -q crypto/gnupg@2.0.17") + expect(@provider).to receive(:shell_out).with("pkg --no-refresh install -q crypto/gnupg@2.0.17", timeout: 900) allow(@new_resource).to receive(:options).and_return("--no-refresh") @provider.install_package("crypto/gnupg", "2.0.17") end @@ -147,8 +147,8 @@ Packaging Date: April 1, 2012 05:55:52 PM Size: 2.57 MB FMRI: pkg://omnios/security/sudo@1.8.4.1,5.11-0.151002:20120401T175552Z PKG_STATUS - expect(@provider).to receive(:shell_out).with("pkg info #{@new_resource.package_name}").and_return(local_output) - expect(@provider).to receive(:shell_out!).with("pkg info -r #{@new_resource.package_name}").and_return(remote) + expect(@provider).to receive(:shell_out).with("pkg info #{@new_resource.package_name}", timeout: 900).and_return(local_output) + expect(@provider).to receive(:shell_out!).with("pkg info -r #{@new_resource.package_name}", timeout: 900).and_return(remote) @provider.load_current_resource expect(@current_resource.version).to be_nil expect(@provider.candidate_version).to eql("1.8.4.1") @@ -188,8 +188,8 @@ Packaging Date: October 19, 2011 09:14:50 AM FMRI: pkg://solaris/crypto/gnupg@2.0.18,5.11-0.175.0.0.0.2.537:20111019T091450Z REMOTE - expect(@provider).to receive(:shell_out).with("pkg info #{@new_resource.package_name}").and_return(local) - expect(@provider).to receive(:shell_out!).with("pkg info -r #{@new_resource.package_name}").and_return(remote) + expect(@provider).to receive(:shell_out).with("pkg info #{@new_resource.package_name}", timeout: 900).and_return(local) + expect(@provider).to receive(:shell_out!).with("pkg info -r #{@new_resource.package_name}", timeout: 900).and_return(remote) expect(@provider).to receive(:install_package).exactly(0).times @provider.run_action(:install) end @@ -200,7 +200,7 @@ REMOTE end it "should run pkg install with the --accept flag" do - expect(@provider).to receive(:shell_out).with("pkg install -q --accept crypto/gnupg@2.0.17") + expect(@provider).to receive(:shell_out).with("pkg install -q --accept crypto/gnupg@2.0.17", timeout: 900) @provider.install_package("crypto/gnupg", "2.0.17") end end @@ -208,19 +208,19 @@ REMOTE context "when upgrading a package" do it "should run pkg install with the package name and version" do - expect(@provider).to receive(:shell_out).with("pkg install -q crypto/gnupg@2.0.17") + expect(@provider).to receive(:shell_out).with("pkg install -q crypto/gnupg@2.0.17", timeout: 900) @provider.upgrade_package("crypto/gnupg", "2.0.17") end end context "when uninstalling a package" do it "should run pkg uninstall with the package name and version" do - expect(@provider).to receive(:shell_out!).with("pkg uninstall -q crypto/gnupg@2.0.17") + expect(@provider).to receive(:shell_out!).with("pkg uninstall -q crypto/gnupg@2.0.17", timeout: 900) @provider.remove_package("crypto/gnupg", "2.0.17") end it "should run pkg uninstall with the package name and version and options if specified" do - expect(@provider).to receive(:shell_out!).with("pkg --no-refresh uninstall -q crypto/gnupg@2.0.17") + expect(@provider).to receive(:shell_out!).with("pkg --no-refresh uninstall -q crypto/gnupg@2.0.17", timeout: 900) allow(@new_resource).to receive(:options).and_return("--no-refresh") @provider.remove_package("crypto/gnupg", "2.0.17") end diff --git a/spec/unit/provider/package/macports_spec.rb b/spec/unit/provider/package/macports_spec.rb index 9822fb3928..eef84113b4 100644 --- a/spec/unit/provider/package/macports_spec.rb +++ b/spec/unit/provider/package/macports_spec.rb @@ -105,7 +105,7 @@ EOF it "should run the port install command with the correct version" do expect(@current_resource).to receive(:version).and_return("4.1.6") @provider.current_resource = @current_resource - expect(@provider).to receive(:shell_out!).with("port install zsh @4.2.7") + expect(@provider).to receive(:shell_out!).with("port install zsh @4.2.7", timeout: 900) @provider.install_package("zsh", "4.2.7") end @@ -122,7 +122,7 @@ EOF expect(@current_resource).to receive(:version).and_return("4.1.6") @provider.current_resource = @current_resource allow(@new_resource).to receive(:options).and_return("-f") - expect(@provider).to receive(:shell_out!).with("port -f install zsh @4.2.7") + expect(@provider).to receive(:shell_out!).with("port -f install zsh @4.2.7", timeout: 900) @provider.install_package("zsh", "4.2.7") end @@ -130,36 +130,36 @@ EOF describe "purge_package" do it "should run the port uninstall command with the correct version" do - expect(@provider).to receive(:shell_out!).with("port uninstall zsh @4.2.7") + expect(@provider).to receive(:shell_out!).with("port uninstall zsh @4.2.7", timeout: 900) @provider.purge_package("zsh", "4.2.7") end it "should purge the currently active version if no explicit version is passed in" do - expect(@provider).to receive(:shell_out!).with("port uninstall zsh") + expect(@provider).to receive(:shell_out!).with("port uninstall zsh", timeout: 900) @provider.purge_package("zsh", nil) end it "should add options to the port command when specified" do allow(@new_resource).to receive(:options).and_return("-f") - expect(@provider).to receive(:shell_out!).with("port -f uninstall zsh @4.2.7") + expect(@provider).to receive(:shell_out!).with("port -f uninstall zsh @4.2.7", timeout: 900) @provider.purge_package("zsh", "4.2.7") end end describe "remove_package" do it "should run the port deactivate command with the correct version" do - expect(@provider).to receive(:shell_out!).with("port deactivate zsh @4.2.7") + expect(@provider).to receive(:shell_out!).with("port deactivate zsh @4.2.7", timeout: 900) @provider.remove_package("zsh", "4.2.7") end it "should remove the currently active version if no explicit version is passed in" do - expect(@provider).to receive(:shell_out!).with("port deactivate zsh") + expect(@provider).to receive(:shell_out!).with("port deactivate zsh", timeout: 900) @provider.remove_package("zsh", nil) end it "should add options to the port command when specified" do allow(@new_resource).to receive(:options).and_return("-f") - expect(@provider).to receive(:shell_out!).with("port -f deactivate zsh @4.2.7") + expect(@provider).to receive(:shell_out!).with("port -f deactivate zsh @4.2.7", timeout: 900) @provider.remove_package("zsh", "4.2.7") end end @@ -169,7 +169,7 @@ EOF expect(@current_resource).to receive(:version).at_least(:once).and_return("4.1.6") @provider.current_resource = @current_resource - expect(@provider).to receive(:shell_out!).with("port upgrade zsh @4.2.7") + expect(@provider).to receive(:shell_out!).with("port upgrade zsh @4.2.7", timeout: 900) @provider.upgrade_package("zsh", "4.2.7") end @@ -195,7 +195,7 @@ EOF expect(@current_resource).to receive(:version).at_least(:once).and_return("4.1.6") @provider.current_resource = @current_resource - expect(@provider).to receive(:shell_out!).with("port -f upgrade zsh @4.2.7") + expect(@provider).to receive(:shell_out!).with("port -f upgrade zsh @4.2.7", timeout: 900) @provider.upgrade_package("zsh", "4.2.7") end diff --git a/spec/unit/provider/package/openbsd_spec.rb b/spec/unit/provider/package/openbsd_spec.rb index b0cdb9969d..8407f83785 100644 --- a/spec/unit/provider/package/openbsd_spec.rb +++ b/spec/unit/provider/package/openbsd_spec.rb @@ -50,25 +50,13 @@ describe Chef::Provider::Package::Openbsd do context 'when there is a single candidate' do - context 'when installing from source' do - it 'should run the installation command' do - pending('Installing from source is not supported yet') - # This is a consequence of load_current_resource being called before define_resource_requirements - # It can be deleted once an implementation is provided - allow(provider).to receive(:shell_out!).with("pkg_info -I \"#{name}\"", anything()).and_return( - instance_double('shellout', :stdout => "#{name}-#{version}\n")) - new_resource.source('/some/path/on/disk.tgz') - provider.run_action(:install) - end - end - context 'when source is not provided' do it 'should run the installation command' do expect(provider).to receive(:shell_out!).with("pkg_info -I \"#{name}\"", anything()).and_return( instance_double('shellout', :stdout => "#{name}-#{version}\n")) expect(provider).to receive(:shell_out!).with( "pkg_add -r #{name}-#{version}", - {:env => {"PKG_PATH" => "http://ftp.OpenBSD.org/pub/OpenBSD/5.5/packages/amd64/"}} + {:env => {"PKG_PATH" => "http://ftp.OpenBSD.org/pub/OpenBSD/5.5/packages/amd64/"}, timeout: 900} ) {OpenStruct.new :status => true} provider.run_action(:install) end @@ -100,21 +88,12 @@ describe Chef::Provider::Package::Openbsd do instance_double('shellout', :stdout => "#{name}-#{version}-#{flavor}\n")) expect(provider).to receive(:shell_out!).with( "pkg_add -r #{name}-#{version}-#{flavor}", - {:env => {"PKG_PATH" => "http://ftp.OpenBSD.org/pub/OpenBSD/5.5/packages/amd64/"}} + {env: {"PKG_PATH" => "http://ftp.OpenBSD.org/pub/OpenBSD/5.5/packages/amd64/"}, timeout: 900} ) {OpenStruct.new :status => true} provider.run_action(:install) end end - context 'if a version is specified' do - it 'runs the installation command' do - pending('Specifying both a version and flavor is not supported') - new_resource.version(version) - allow(provider).to receive(:shell_out!).with(/pkg_info -e/, anything()).and_return(instance_double('shellout', :stdout => '')) - allow(provider).to receive(:candidate_version).and_return("#{package_name}-#{version}-#{flavor}") - provider.run_action(:install) - end - end end context 'if a version is specified' do @@ -125,7 +104,7 @@ describe Chef::Provider::Package::Openbsd do new_resource.version("#{version}-#{flavor_b}") expect(provider).to receive(:shell_out!).with( "pkg_add -r #{name}-#{version}-#{flavor_b}", - {:env => {"PKG_PATH" => "http://ftp.OpenBSD.org/pub/OpenBSD/5.5/packages/amd64/"}} + {env: {"PKG_PATH" => "http://ftp.OpenBSD.org/pub/OpenBSD/5.5/packages/amd64/"}, timeout: 900} ) {OpenStruct.new :status => true} provider.run_action(:install) end @@ -144,11 +123,10 @@ describe Chef::Provider::Package::Openbsd do end it "should run the command to delete the installed package" do expect(@provider).to receive(:shell_out!).with( - "pkg_delete #{@name}", :env=>nil + "pkg_delete #{@name}", env: nil, timeout: 900 ) {OpenStruct.new :status => true} @provider.remove_package(@name, nil) end end end - diff --git a/spec/unit/provider/package/pacman_spec.rb b/spec/unit/provider/package/pacman_spec.rb index 3b8848c41b..fcb9f8a86c 100644 --- a/spec/unit/provider/package/pacman_spec.rb +++ b/spec/unit/provider/package/pacman_spec.rb @@ -51,7 +51,7 @@ ERR end it "should run pacman query with the package name" do - expect(@provider).to receive(:shell_out).with("pacman -Qi #{@new_resource.package_name}").and_return(@status) + expect(@provider).to receive(:shell_out).with("pacman -Qi #{@new_resource.package_name}", {timeout: 900}).and_return(@status) @provider.load_current_resource end @@ -152,12 +152,12 @@ PACMAN_CONF describe Chef::Provider::Package::Pacman, "install_package" do it "should run pacman install with the package name and version" do - expect(@provider).to receive(:shell_out!).with("pacman --sync --noconfirm --noprogressbar nano") + expect(@provider).to receive(:shell_out!).with("pacman --sync --noconfirm --noprogressbar nano", {timeout: 900}) @provider.install_package("nano", "1.0") end it "should run pacman install with the package name and version and options if specified" do - expect(@provider).to receive(:shell_out!).with("pacman --sync --noconfirm --noprogressbar --debug nano") + expect(@provider).to receive(:shell_out!).with("pacman --sync --noconfirm --noprogressbar --debug nano", {timeout: 900}) allow(@new_resource).to receive(:options).and_return("--debug") @provider.install_package("nano", "1.0") @@ -173,12 +173,12 @@ PACMAN_CONF describe Chef::Provider::Package::Pacman, "remove_package" do it "should run pacman remove with the package name" do - expect(@provider).to receive(:shell_out!).with("pacman --remove --noconfirm --noprogressbar nano") + expect(@provider).to receive(:shell_out!).with("pacman --remove --noconfirm --noprogressbar nano", {timeout: 900}) @provider.remove_package("nano", "1.0") end it "should run pacman remove with the package name and options if specified" do - expect(@provider).to receive(:shell_out!).with("pacman --remove --noconfirm --noprogressbar --debug nano") + expect(@provider).to receive(:shell_out!).with("pacman --remove --noconfirm --noprogressbar --debug nano", {timeout: 900}) allow(@new_resource).to receive(:options).and_return("--debug") @provider.remove_package("nano", "1.0") diff --git a/spec/unit/provider/package/rpm_spec.rb b/spec/unit/provider/package/rpm_spec.rb index 411afd3755..e0e45d0b4f 100644 --- a/spec/unit/provider/package/rpm_spec.rb +++ b/spec/unit/provider/package/rpm_spec.rb @@ -23,183 +23,394 @@ describe Chef::Provider::Package::Rpm do let(:node) { Chef::Node.new } let(:events) { Chef::EventDispatch::Dispatcher.new } let(:run_context) { Chef::RunContext.new(node, {}, events) } + + let(:package_source) { "/tmp/ImageMagick-c++-6.5.4.7-7.el6_5.x86_64.rpm" } + + let(:package_name) { "ImageMagick-c++" } + let(:new_resource) do - Chef::Resource::Package.new("ImageMagick-c++").tap do |resource| - resource.source "/tmp/ImageMagick-c++-6.5.4.7-7.el6_5.x86_64.rpm" + Chef::Resource::Package.new(package_name).tap do |resource| + resource.source(package_source) end end - let(:exitstatus) { 0 } - let(:stdout) { String.new('') } - let(:status) { double('Process::Status', exitstatus: exitstatus, stdout: stdout) } + + # `rpm -qp [stuff] $source` + let(:rpm_qp_status) { instance_double('Mixlib::ShellOut', exitstatus: rpm_qp_exitstatus, stdout: rpm_qp_stdout) } + + # `rpm -q [stuff] $package_name` + let(:rpm_q_status) { instance_double('Mixlib::ShellOut', exitstatus: rpm_q_exitstatus, stdout: rpm_q_stdout) } before(:each) do - allow(::File).to receive(:exists?).and_return(true) - allow(provider).to receive(:shell_out!).and_return(status) + allow(::File).to receive(:exists?).with("PLEASE STUB File.exists? EXACTLY").and_return(true) + + # Ensure all shell out usage is stubbed with exact arguments + allow(provider).to receive(:shell_out!).with("PLEASE STUB YOUR SHELLOUT CALLS").and_return(nil) + allow(provider).to receive(:shell_out).with("PLEASE STUB YOUR SHELLOUT CALLS").and_return(nil) end - describe "when determining the current state of the package" do - it "should create a current resource with the name of new_resource" do - provider.load_current_resource - expect(provider.current_resource.name).to eq("ImageMagick-c++") - end + describe "when the package source is not valid" do - it "should set the current reource package name to the new resource package name" do - provider.load_current_resource - expect(provider.current_resource.package_name).to eq('ImageMagick-c++') - end + context "when source is not defiend" do + let(:new_resource) { Chef::Resource::Package.new("ImageMagick-c++") } - it "should raise an exception if a source is supplied but not found" do - allow(::File).to receive(:exists?).and_return(false) - expect { provider.run_action(:any) }.to raise_error(Chef::Exceptions::Package) + it "should raise an exception when attempting any action" do + expect { provider.run_action(:any) }.to raise_error(Chef::Exceptions::Package) + end end - context "installation exists" do - let(:stdout) { "ImageMagick-c++ 6.5.4.7-7.el6_5" } + context "when the source is a file that doesn't exist" do - it "should get the source package version from rpm if provided" do - expect(provider).to receive(:shell_out!).with("rpm -qp --queryformat '%{NAME} %{VERSION}-%{RELEASE}\n' /tmp/ImageMagick-c++-6.5.4.7-7.el6_5.x86_64.rpm").and_return(status) - expect(provider).to receive(:shell_out).with("rpm -q --queryformat '%{NAME} %{VERSION}-%{RELEASE}\n' ImageMagick-c++").and_return(status) - provider.load_current_resource - expect(provider.current_resource.package_name).to eq("ImageMagick-c++") - expect(provider.new_resource.version).to eq("6.5.4.7-7.el6_5") + it "should raise an exception when attempting any action" do + allow(::File).to receive(:exists?).with(package_source).and_return(false) + expect { provider.run_action(:any) }.to raise_error(Chef::Exceptions::Package) end + end - it "should return the current version installed if found by rpm" do - expect(provider).to receive(:shell_out!).with("rpm -qp --queryformat '%{NAME} %{VERSION}-%{RELEASE}\n' /tmp/ImageMagick-c++-6.5.4.7-7.el6_5.x86_64.rpm").and_return(status) - expect(provider).to receive(:shell_out).with("rpm -q --queryformat '%{NAME} %{VERSION}-%{RELEASE}\n' ImageMagick-c++").and_return(status) - provider.load_current_resource - expect(provider.current_resource.version).to eq("6.5.4.7-7.el6_5") + context "when the source is an unsupported URI scheme" do + + let(:package_source) { "foobar://example.com/ImageMagick-c++-6.5.4.7-7.el6_5.x86_64.rpm" } + + it "should raise an exception if an uri formed source is non-supported scheme" do + allow(::File).to receive(:exists?).with(package_source).and_return(false) + + # verify let bindings are as we expect + expect(new_resource.source).to eq("foobar://example.com/ImageMagick-c++-6.5.4.7-7.el6_5.x86_64.rpm") + expect(provider.load_current_resource).to be_nil + expect { provider.run_action(:any) }.to raise_error(Chef::Exceptions::Package) end end - context "source is uri formed" do - before(:each) do - allow(::File).to receive(:exists?).and_return(false) + end + + describe "when the package source is valid" do + + before do + expect(provider).to receive(:shell_out!). + with("rpm -qp --queryformat '%{NAME} %{VERSION}-%{RELEASE}\n' #{package_source}", timeout: 900). + and_return(rpm_qp_status) + + expect(provider).to receive(:shell_out). + with("rpm -q --queryformat '%{NAME} %{VERSION}-%{RELEASE}\n' #{package_name}", timeout: 900). + and_return(rpm_q_status) + end + + context "when rpm fails when querying package installed state" do + + before do + allow(::File).to receive(:exists?).with(package_source).and_return(true) end - %w(http HTTP https HTTPS ftp FTP).each do |scheme| - it "should accept uri formed source (#{scheme})" do - new_resource.source "#{scheme}://example.com/ImageMagick-c++-6.5.4.7-7.el6_5.x86_64.rpm" - expect(provider.load_current_resource).not_to be_nil + let(:rpm_qp_stdout) { "ImageMagick-c++ 6.5.4.7-7.el6_5" } + let(:rpm_q_stdout) { "" } + + let(:rpm_qp_exitstatus) { 0 } + let(:rpm_q_exitstatus) { -1 } + + it "raises an exception when attempting any action" do + expected_message = "Unable to determine current version due to RPM failure." + + expect { provider.run_action(:install) }.to raise_error do |error| + expect(error).to be_a_kind_of(Chef::Exceptions::Package) + expect(error.to_s).to include(expected_message) end end + end + + + context "when the package is installed" do + + let(:rpm_qp_stdout) { "ImageMagick-c++ 6.5.4.7-7.el6_5" } + let(:rpm_q_stdout) { "ImageMagick-c++ 6.5.4.7-7.el6_5" } + + let(:rpm_qp_exitstatus) { 0 } + let(:rpm_q_exitstatus) { 0 } - %w(file FILE).each do |scheme| - it "should accept uri formed source (#{scheme})" do - new_resource.source "#{scheme}:///ImageMagick-c++-6.5.4.7-7.el6_5.x86_64.rpm" - expect(provider.load_current_resource).not_to be_nil + let(:action) { :install } + + context "when the source is a file system path" do + + before do + allow(::File).to receive(:exists?).with(package_source).and_return(true) + + provider.action = action + + provider.load_current_resource + provider.define_resource_requirements + provider.process_resource_requirements end - end - it "should raise an exception if an uri formed source is non-supported scheme" do - new_resource.source "foobar://example.com/ImageMagick-c++-6.5.4.7-7.el6_5.x86_64.rpm" - expect(provider.load_current_resource).to be_nil - expect { provider.run_action(:any) }.to raise_error(Chef::Exceptions::Package) - end - end + it "should get the source package version from rpm if provided" do + expect(provider.current_resource.package_name).to eq("ImageMagick-c++") + expect(provider.new_resource.version).to eq("6.5.4.7-7.el6_5") + end - context "source is not defiend" do - let(:new_resource) { Chef::Resource::Package.new("ImageMagick-c++") } + it "should return the current version installed if found by rpm" do + expect(provider.current_resource.version).to eq("6.5.4.7-7.el6_5") + end + + describe "action install" do + + context "when at the desired version already" do + it "does nothing when the correct version is installed" do + expect(provider).to_not receive(:shell_out!).with("rpm -i /tmp/imagemagick-c++-6.5.4.7-7.el6_5.x86_64.rpm", timeout: 900) + + provider.action_install + end + end + + context "when a newer version is desired" do + + let(:rpm_q_stdout) { "imagemagick-c++ 0.5.4.7-7.el6_5" } + + it "runs rpm -u with the package source to upgrade" do + expect(provider).to receive(:shell_out!).with("rpm -U /tmp/ImageMagick-c++-6.5.4.7-7.el6_5.x86_64.rpm", timeout: 900) + provider.action_install + end + end + + context "when an older version is desired" do + let(:new_resource) do + Chef::Resource::RpmPackage.new(package_name).tap do |r| + r.source(package_source) + r.allow_downgrade(true) + end + end + + let(:rpm_q_stdout) { "imagemagick-c++ 21.4-19.el6_5" } + + it "should run rpm -u --oldpackage with the package source to downgrade" do + expect(provider).to receive(:shell_out!).with("rpm -U --oldpackage /tmp/ImageMagick-c++-6.5.4.7-7.el6_5.x86_64.rpm", timeout: 900) + provider.action_install + end + + end + + end + + describe "action upgrade" do + + let(:action) { :upgrade } + + context "when at the desired version already" do + it "does nothing when the correct version is installed" do + expect(provider).to_not receive(:shell_out!).with("rpm -i /tmp/imagemagick-c++-6.5.4.7-7.el6_5.x86_64.rpm", timeout: 900) + + provider.action_upgrade + end + end + + context "when a newer version is desired" do + + let(:rpm_q_stdout) { "imagemagick-c++ 0.5.4.7-7.el6_5" } + + it "runs rpm -u with the package source to upgrade" do + expect(provider).to receive(:shell_out!).with("rpm -U /tmp/ImageMagick-c++-6.5.4.7-7.el6_5.x86_64.rpm", timeout: 900) + provider.action_upgrade + end + end + + context "when an older version is desired" do + let(:new_resource) do + Chef::Resource::RpmPackage.new(package_name).tap do |r| + r.source(package_source) + r.allow_downgrade(true) + end + end + + let(:rpm_q_stdout) { "imagemagick-c++ 21.4-19.el6_5" } + + it "should run rpm -u --oldpackage with the package source to downgrade" do + expect(provider).to receive(:shell_out!).with("rpm -U --oldpackage /tmp/ImageMagick-c++-6.5.4.7-7.el6_5.x86_64.rpm", timeout: 900) + provider.action_upgrade + end + + end + end + + describe "action :remove" do + + let(:action) { :remove } + + it "should remove the package" do + expect(provider).to receive(:shell_out!).with("rpm -e ImageMagick-c++-6.5.4.7-7.el6_5", timeout: 900) + provider.action_remove + end + end + + + context "when the package name contains a tilde (chef#3503)" do + + let(:package_name) { "supermarket" } + + let(:package_source) { "/tmp/supermarket-1.10.1~alpha.0-1.el5.x86_64.rpm" } + + let(:rpm_qp_stdout) { "supermarket 1.10.1~alpha.0-1.el5" } + let(:rpm_q_stdout) { "supermarket 1.10.1~alpha.0-1.el5" } + + let(:rpm_qp_exitstatus) { 0 } + let(:rpm_q_exitstatus) { 0 } + + it "should correctly determine the candidate version and installed version" do + expect(provider.current_resource.package_name).to eq("supermarket") + expect(provider.new_resource.version).to eq("1.10.1~alpha.0-1.el5") + end + end - it "should raise an exception if the source is not set but we are installing" do - expect { provider.run_action(:any) }.to raise_error(Chef::Exceptions::Package) end - end - context "installation does not exist" do - let(:stdout) { String.new("package openssh-askpass is not installed") } - let(:exitstatus) { -1 } - let(:new_resource) do - Chef::Resource::Package.new("openssh-askpass").tap do |resource| - resource.source "openssh-askpass" + context "when the source is given as an URI" do + before(:each) do + allow(::File).to receive(:exists?).with(package_source).and_return(false) + + provider.action = action + + provider.load_current_resource + provider.define_resource_requirements + provider.process_resource_requirements + end + + %w(http HTTP https HTTPS ftp FTP file FILE).each do |scheme| + + context "when the source URI uses protocol scheme '#{scheme}'" do + + let(:package_source) { "#{scheme}://example.com/ImageMagick-c++-6.5.4.7-7.el6_5.x86_64.rpm" } + + it "should get the source package version from rpm if provided" do + expect(provider.current_resource.package_name).to eq("ImageMagick-c++") + expect(provider.new_resource.version).to eq("6.5.4.7-7.el6_5") + end + + it "should return the current version installed if found by rpm" do + expect(provider.current_resource.version).to eq("6.5.4.7-7.el6_5") + end + + end end + end - it "should raise an exception if rpm fails to run" do - allow(provider).to receive(:shell_out).and_return(status) - expect { provider.run_action(:any) }.to raise_error(Chef::Exceptions::Package) + end + + context "when the package is not installed" do + + let(:package_name) { "openssh-askpass" } + + let(:package_source) { "/tmp/openssh-askpass-1.2.3-4.el6_5.x86_64.rpm" } + + let(:rpm_qp_stdout) { "openssh-askpass 1.2.3-4.el6_5" } + let(:rpm_q_stdout) { "package openssh-askpass is not installed" } + + let(:rpm_qp_exitstatus) { 0 } + let(:rpm_q_exitstatus) { 0 } + + let(:action) { :install } + + before do + allow(File).to receive(:exists?).with(package_source).and_return(true) + + provider.action = action + + provider.load_current_resource + provider.define_resource_requirements + provider.process_resource_requirements end it "should not detect the package name as version when not installed" do - expect(provider).to receive(:shell_out!).with("rpm -qp --queryformat '%{NAME} %{VERSION}-%{RELEASE}\n' openssh-askpass").and_return(status) - expect(provider).to receive(:shell_out).with("rpm -q --queryformat '%{NAME} %{VERSION}-%{RELEASE}\n' openssh-askpass").and_return(status) - provider.load_current_resource expect(provider.current_resource.version).to be_nil end - end - end - describe "after the current resource is loaded" do - let(:current_resource) { Chef::Resource::Package.new("ImageMagick-c++") } - let(:provider) do - Chef::Provider::Package::Rpm.new(new_resource, run_context).tap do |provider| - provider.current_resource = current_resource - end - end + context "when the package name contains a tilde (chef#3503)" do - describe "when installing or upgrading" do - it "should run rpm -i with the package source to install" do - expect(provider).to receive(:shell_out!).with("rpm -i /tmp/ImageMagick-c++-6.5.4.7-7.el6_5.x86_64.rpm") - provider.install_package("ImageMagick-c++", "6.5.4.7-7.el6_5") - end + let(:package_name) { "supermarket" } - it "should run rpm -U with the package source to upgrade" do - current_resource.version("21.4-19.el5") - expect(provider).to receive(:shell_out!).with("rpm -U /tmp/ImageMagick-c++-6.5.4.7-7.el6_5.x86_64.rpm") - provider.upgrade_package("ImageMagick-c++", "6.5.4.7-7.el6_5") - end + let(:package_source) { "/tmp/supermarket-1.10.1~alpha.0-1.el5.x86_64.rpm" } - it "should install package if missing and set to upgrade" do - current_resource.version("ImageMagick-c++") - expect(provider).to receive(:shell_out!).with("rpm -U /tmp/ImageMagick-c++-6.5.4.7-7.el6_5.x86_64.rpm") - provider.upgrade_package("ImageMagick-c++", "6.5.4.7-7.el6_5") - end + let(:rpm_qp_stdout) { "supermarket 1.10.1~alpha.0-1.el5" } + let(:rpm_q_stdout) { "package supermarket is not installed" } - context "allowing downgrade" do - let(:new_resource) { Chef::Resource::RpmPackage.new("/tmp/ImageMagick-c++-6.5.4.7-7.el6_5.x86_64.rpm") } - let(:current_resource) { Chef::Resource::RpmPackage.new("ImageMagick-c++") } + let(:rpm_qp_exitstatus) { 0 } + let(:rpm_q_exitstatus) { 0 } - it "should run rpm -U --oldpackage with the package source to downgrade" do - new_resource.allow_downgrade(true) - current_resource.version("21.4-19.el5") - expect(provider).to receive(:shell_out!).with("rpm -U --oldpackage /tmp/ImageMagick-c++-6.5.4.7-7.el6_5.x86_64.rpm") - provider.upgrade_package("ImageMagick-c++", "6.5.4.7-7.el6_5") + it "should correctly determine the candidate version" do + expect(provider.new_resource.version).to eq("1.10.1~alpha.0-1.el5") end end - context "installing when the name is a path" do - let(:new_resource) { Chef::Resource::Package.new("/tmp/ImageMagick-c++-6.5.4.7-7.el6_5.x86_64.rpm") } - let(:current_resource) { Chef::Resource::Package.new("ImageMagick-c++") } + describe "managing the package" do + + describe "action install" do + + it "installs the package" do + expect(provider).to receive(:shell_out!).with("rpm -i #{package_source}", timeout: 900) - it "should install from a path when the package is a path and the source is nil" do - expect(new_resource.source).to eq("/tmp/ImageMagick-c++-6.5.4.7-7.el6_5.x86_64.rpm") - provider.current_resource = current_resource - expect(provider).to receive(:shell_out!).with("rpm -i /tmp/ImageMagick-c++-6.5.4.7-7.el6_5.x86_64.rpm") - provider.install_package("/tmp/ImageMagick-c++-6.5.4.7-7.el6_5.x86_64.rpm", "6.5.4.7-7.el6_5") + provider.action_install + end + + context "when custom resource options are given" do + it "installs with custom options specified in the resource" do + new_resource.options("--dbpath /var/lib/rpm") + expect(provider).to receive(:shell_out!).with("rpm --dbpath /var/lib/rpm -i #{package_source}", timeout: 900) + provider.action_install + end + end end - it "should uprgrade from a path when the package is a path and the source is nil" do - expect(new_resource.source).to eq("/tmp/ImageMagick-c++-6.5.4.7-7.el6_5.x86_64.rpm") - current_resource.version("21.4-19.el5") - provider.current_resource = current_resource - expect(provider).to receive(:shell_out!).with("rpm -U /tmp/ImageMagick-c++-6.5.4.7-7.el6_5.x86_64.rpm") - provider.upgrade_package("/tmp/ImageMagick-c++-6.5.4.7-7.el6_5.x86_64.rpm", "6.5.4.7-7.el6_5") + describe "action upgrade" do + + let(:action) { :upgrade } + + it "installs the package" do + expect(provider).to receive(:shell_out!).with("rpm -i #{package_source}", timeout: 900) + + provider.action_upgrade + end + end + + describe "when removing the package" do + + let(:action) { :remove } + + it "should do nothing" do + expect(provider).to_not receive(:shell_out!).with("rpm -e ImageMagick-c++-6.5.4.7-7.el6_5", timeout: 900) + provider.action_remove + end end - end - it "installs with custom options specified in the resource" do - provider.candidate_version = '11' - new_resource.options("--dbpath /var/lib/rpm") - expect(provider).to receive(:shell_out!).with("rpm --dbpath /var/lib/rpm -i /tmp/ImageMagick-c++-6.5.4.7-7.el6_5.x86_64.rpm") - provider.install_package(new_resource.name, provider.candidate_version) end + + end + end - describe "when removing the package" do - it "should run rpm -e to remove the package" do - expect(provider).to receive(:shell_out!).with("rpm -e ImageMagick-c++-6.5.4.7-7.el6_5") - provider.remove_package("ImageMagick-c++", "6.5.4.7-7.el6_5") - end + context "when the resource name is the path to the package" do + + let(:new_resource) do + # When we pass a source in as the name, then #initialize in the + # provider will call File.exists?. Because of the ordering in our + # let() bindings and such, we have to set the stub here and not in a + # before block. + allow(::File).to receive(:exists?).with(package_source).and_return(true) + Chef::Resource::Package.new("/tmp/ImageMagick-c++-6.5.4.7-7.el6_5.x86_64.rpm") + end + + let(:current_resource) { Chef::Resource::Package.new("ImageMagick-c++") } + + it "should install from a path when the package is a path and the source is nil" do + expect(new_resource.source).to eq("/tmp/ImageMagick-c++-6.5.4.7-7.el6_5.x86_64.rpm") + provider.current_resource = current_resource + expect(provider).to receive(:shell_out!).with("rpm -i /tmp/ImageMagick-c++-6.5.4.7-7.el6_5.x86_64.rpm", timeout: 900) + provider.install_package("/tmp/ImageMagick-c++-6.5.4.7-7.el6_5.x86_64.rpm", "6.5.4.7-7.el6_5") + end + + it "should uprgrade from a path when the package is a path and the source is nil" do + expect(new_resource.source).to eq("/tmp/ImageMagick-c++-6.5.4.7-7.el6_5.x86_64.rpm") + current_resource.version("21.4-19.el5") + provider.current_resource = current_resource + expect(provider).to receive(:shell_out!).with("rpm -U /tmp/ImageMagick-c++-6.5.4.7-7.el6_5.x86_64.rpm", timeout: 900) + provider.upgrade_package("/tmp/ImageMagick-c++-6.5.4.7-7.el6_5.x86_64.rpm", "6.5.4.7-7.el6_5") end end + + end + diff --git a/spec/unit/provider/package/rubygems_spec.rb b/spec/unit/provider/package/rubygems_spec.rb index 380572499c..67ffb7bb9e 100644 --- a/spec/unit/provider/package/rubygems_spec.rb +++ b/spec/unit/provider/package/rubygems_spec.rb @@ -107,38 +107,6 @@ describe Chef::Provider::Package::Rubygems::CurrentGemEnvironment do expect(@gem_env.candidate_version_from_remote(Gem::Dependency.new('rspec', '>= 0'))).to eq(Gem::Version.new('1.3.0')) end - context "when rubygems was upgraded from 1.8->2.0" do - # https://github.com/rubygems/rubygems/issues/404 - # tl;dr rubygems 1.8 and 2.0 can both be in the load path, which means that - # require "rubygems/format" will load even though rubygems 2.0 doesn't have - # that file. - - before do - if defined?(Gem::Format) - # tests are running under rubygems 1.8, or 2.0 upgraded from 1.8 - @remove_gem_format = false - else - Gem.const_set(:Format, Object.new) - @remove_gem_format = true - end - allow(Gem::Package).to receive(:respond_to?).and_call_original - allow(Gem::Package).to receive(:respond_to?).with(:open).and_return(false) - end - - after do - if @remove_gem_format - Gem.send(:remove_const, :Format) - end - end - - it "finds a matching gem candidate version on rubygems 2.0+ with some rubygems 1.8 code loaded" do - package = double("Gem::Package", :spec => "a gemspec from package") - expect(Gem::Package).to receive(:new).with("/path/to/package.gem").and_return(package) - expect(@gem_env.spec_from_file("/path/to/package.gem")).to eq("a gemspec from package") - end - - end - it "gives the candidate version as nil if none is found" do dep = Gem::Dependency.new('rspec', '>= 0') latest = [] @@ -222,8 +190,6 @@ describe Chef::Provider::Package::Rubygems::AlternateGemEnvironment do end it "uses the cached result for gem paths when available" do - gem_env_output = ['/path/to/gems', '/another/path/to/gems'].join(File::PATH_SEPARATOR) - shell_out_result = OpenStruct.new(:stdout => gem_env_output) expect(@gem_env).not_to receive(:shell_out!) expected = ['/path/to/gems', '/another/path/to/gems'] Chef::Provider::Package::Rubygems::AlternateGemEnvironment.gempath_cache['/usr/weird/bin/gem']= expected @@ -261,7 +227,7 @@ describe Chef::Provider::Package::Rubygems::AlternateGemEnvironment do else `which gem`.strip end - pending("cant find your gem executable") if path_to_gem.empty? + skip("cant find your gem executable") if path_to_gem.empty? gem_env = Chef::Provider::Package::Rubygems::AlternateGemEnvironment.new(path_to_gem) expected = ['rspec-core', Gem::Version.new(RSpec::Core::Version::STRING)] actual = gem_env.installed_versions(Gem::Dependency.new('rspec-core', nil)).map { |s| [s.name, s.version] } @@ -542,7 +508,7 @@ describe Chef::Provider::Package::Rubygems do it "installs the gem by shelling out when options are provided as a String" do @new_resource.options('-i /alt/install/location') expected ="gem install rspec-core -q --no-rdoc --no-ri -v \"#{@spec_version}\" -i /alt/install/location" - expect(@provider).to receive(:shell_out!).with(expected, :env => nil) + expect(@provider).to receive(:shell_out!).with(expected, env: nil, timeout: 900) @provider.run_action(:install) expect(@new_resource).to be_updated_by_last_action end @@ -551,7 +517,7 @@ describe Chef::Provider::Package::Rubygems do @new_resource.gem_binary('/foo/bar') @new_resource.source('http://mirror.ops.rhcloud.com/mirror/ruby') expected ="/foo/bar install rspec-core -q --no-rdoc --no-ri -v \"#{@spec_version}\" --source=#{@new_resource.source} --source=https://rubygems.org" - expect(@provider).to receive(:shell_out!).with(expected, :env => nil) + expect(@provider).to receive(:shell_out!).with(expected, env: nil, timeout: 900) @provider.run_action(:install) expect(@new_resource).to be_updated_by_last_action end @@ -561,7 +527,7 @@ describe Chef::Provider::Package::Rubygems do @new_resource.source('http://mirror.ops.rhcloud.com/mirror/ruby') @new_resource.clear_sources(true) expected ="/foo/bar install rspec-core -q --no-rdoc --no-ri -v \"#{@spec_version}\" --clear-sources --source=#{@new_resource.source}" - expect(@provider).to receive(:shell_out!).with(expected, :env => nil) + expect(@provider).to receive(:shell_out!).with(expected, env: nil, timeout: 900) @provider.run_action(:install) expect(@new_resource).to be_updated_by_last_action end @@ -572,7 +538,7 @@ describe Chef::Provider::Package::Rubygems do it "installs the gem by shelling out when options are provided but no version is given" do @new_resource.options('-i /alt/install/location') expected ="gem install rspec-core -q --no-rdoc --no-ri -v \"#{@provider.candidate_version}\" -i /alt/install/location" - expect(@provider).to receive(:shell_out!).with(expected, :env => nil) + expect(@provider).to receive(:shell_out!).with(expected, env: nil, timeout: 900) @provider.run_action(:install) expect(@new_resource).to be_updated_by_last_action end @@ -618,7 +584,7 @@ describe Chef::Provider::Package::Rubygems do describe "in an alternate gem environment" do it "installs the gem by shelling out to gem install" do @new_resource.gem_binary('/usr/weird/bin/gem') - expect(@provider).to receive(:shell_out!).with("/usr/weird/bin/gem install rspec-core -q --no-rdoc --no-ri -v \"#{@spec_version}\"", :env=>nil) + expect(@provider).to receive(:shell_out!).with("/usr/weird/bin/gem install rspec-core -q --no-rdoc --no-ri -v \"#{@spec_version}\"", env: nil, timeout: 900) @provider.run_action(:install) expect(@new_resource).to be_updated_by_last_action end @@ -627,7 +593,7 @@ describe Chef::Provider::Package::Rubygems do @new_resource.gem_binary('/usr/weird/bin/gem') @new_resource.source(CHEF_SPEC_DATA + '/gems/chef-integration-test-0.1.0.gem') @new_resource.version('>= 0') - expect(@provider).to receive(:shell_out!).with("/usr/weird/bin/gem install #{CHEF_SPEC_DATA}/gems/chef-integration-test-0.1.0.gem -q --no-rdoc --no-ri -v \">= 0\"", :env=>nil) + expect(@provider).to receive(:shell_out!).with("/usr/weird/bin/gem install #{CHEF_SPEC_DATA}/gems/chef-integration-test-0.1.0.gem -q --no-rdoc --no-ri -v \">= 0\"", env: nil, timeout: 900) @provider.run_action(:install) expect(@new_resource).to be_updated_by_last_action end @@ -639,7 +605,7 @@ describe Chef::Provider::Package::Rubygems do @new_resource.gem_binary('/usr/weird/bin/gem') @new_resource.version('>= 0') expect(@new_resource.source).to eq(CHEF_SPEC_DATA + '/gems/chef-integration-test-0.1.0.gem') - expect(@provider).to receive(:shell_out!).with("/usr/weird/bin/gem install #{CHEF_SPEC_DATA}/gems/chef-integration-test-0.1.0.gem -q --no-rdoc --no-ri -v \">= 0\"", :env=>nil) + expect(@provider).to receive(:shell_out!).with("/usr/weird/bin/gem install #{CHEF_SPEC_DATA}/gems/chef-integration-test-0.1.0.gem -q --no-rdoc --no-ri -v \">= 0\"", env: nil, timeout: 900) @provider.run_action(:install) expect(@new_resource).to be_updated_by_last_action end @@ -678,7 +644,7 @@ describe Chef::Provider::Package::Rubygems do it "uninstalls via the gem command when options are given as a String" do @new_resource.options('-i /alt/install/location') - expect(@provider).to receive(:shell_out!).with("gem uninstall rspec -q -x -I -a -i /alt/install/location", :env=>nil) + expect(@provider).to receive(:shell_out!).with("gem uninstall rspec -q -x -I -a -i /alt/install/location", env: nil, timeout: 900) @provider.action_remove end @@ -692,7 +658,7 @@ describe Chef::Provider::Package::Rubygems do describe "in an alternate gem environment" do it "uninstalls via the gem command" do @new_resource.gem_binary('/usr/weird/bin/gem') - expect(@provider).to receive(:shell_out!).with("/usr/weird/bin/gem uninstall rspec -q -x -I -a", :env=>nil) + expect(@provider).to receive(:shell_out!).with("/usr/weird/bin/gem uninstall rspec -q -x -I -a", env: nil, timeout: 900) @provider.action_remove end end diff --git a/spec/unit/provider/package/smartos_spec.rb b/spec/unit/provider/package/smartos_spec.rb index db39589b85..8f2d2bb8ea 100644 --- a/spec/unit/provider/package/smartos_spec.rb +++ b/spec/unit/provider/package/smartos_spec.rb @@ -29,45 +29,45 @@ describe Chef::Provider::Package::SmartOS, "load_current_resource" do @current_resource = Chef::Resource::Package.new("varnish") - @status = double("Status", :exitstatus => 0) - @provider = Chef::Provider::Package::SmartOS.new(@new_resource, @run_context) - allow(Chef::Resource::Package).to receive(:new).and_return(@current_resource) - @stdin = StringIO.new - @stdout = "varnish-2.1.5nb2\n" - @stderr = StringIO.new - @pid = 10 - @shell_out = OpenStruct.new(:stdout => @stdout, :stdin => @stdin, :stderr => @stderr, :status => @status, :exitstatus => 0) + @status = double("Status", :exitstatus => 0) + @provider = Chef::Provider::Package::SmartOS.new(@new_resource, @run_context) + allow(Chef::Resource::Package).to receive(:new).and_return(@current_resource) + @stdin = StringIO.new + @stdout = "varnish-2.1.5nb2\n" + @stderr = StringIO.new + @pid = 10 + @shell_out = OpenStruct.new(:stdout => @stdout, :stdin => @stdin, :stderr => @stderr, :status => @status, :exitstatus => 0) end - describe "when loading current resource" do + describe "when loading current resource" do it "should create a current resource with the name of the new_resource" do - expect(@provider).to receive(:shell_out!).and_return(@shell_out) - expect(Chef::Resource::Package).to receive(:new).and_return(@current_resource) - @provider.load_current_resource + expect(@provider).to receive(:shell_out!).and_return(@shell_out) + expect(Chef::Resource::Package).to receive(:new).and_return(@current_resource) + @provider.load_current_resource end - it "should set the current resource package name" do - expect(@provider).to receive(:shell_out!).and_return(@shell_out) - expect(@current_resource).to receive(:package_name).with(@new_resource.package_name) - @provider.load_current_resource - end + it "should set the current resource package name" do + expect(@provider).to receive(:shell_out!).and_return(@shell_out) + expect(@current_resource).to receive(:package_name).with(@new_resource.package_name) + @provider.load_current_resource + end - it "should set the installed version if it is installed" do - expect(@provider).to receive(:shell_out!).and_return(@shell_out) - @provider.load_current_resource - expect(@current_resource.version).to eq("2.1.5nb2") - end + it "should set the installed version if it is installed" do + expect(@provider).to receive(:shell_out!).and_return(@shell_out) + @provider.load_current_resource + expect(@current_resource.version).to eq("2.1.5nb2") + end - it "should set the installed version to nil if it's not installed" do - out = OpenStruct.new(:stdout => nil) - expect(@provider).to receive(:shell_out!).and_return(out) - @provider.load_current_resource - expect(@current_resource.version).to eq(nil) - end + it "should set the installed version to nil if it's not installed" do + out = OpenStruct.new(:stdout => nil) + expect(@provider).to receive(:shell_out!).and_return(out) + @provider.load_current_resource + expect(@current_resource.version).to eq(nil) + end - end + end describe "candidate_version" do it "should return the candidate_version variable if already setup" do @@ -76,27 +76,37 @@ describe Chef::Provider::Package::SmartOS, "load_current_resource" do @provider.candidate_version end - it "should lookup the candidate_version if the variable is not already set" do + it "should lookup the candidate_version if the variable is not already set (pkgin separated by spaces)" do search = double() expect(search).to receive(:each_line). - and_yield("something-varnish-1.1.1 something varnish like\n"). - and_yield("varnish-2.3.4 actual varnish\n") + and_yield("something-varnish-1.1.1 something varnish like\n"). + and_yield("varnish-2.3.4 actual varnish\n") @shell_out = double('shell_out!', :stdout => search) - expect(@provider).to receive(:shell_out!).with('/opt/local/bin/pkgin se varnish', :env => nil, :returns => [0,1]).and_return(@shell_out) + expect(@provider).to receive(:shell_out!).with('/opt/local/bin/pkgin', 'se', 'varnish', :env => nil, :returns => [0,1], :timeout=>900).and_return(@shell_out) + expect(@provider.candidate_version).to eq("2.3.4") + end + + it "should lookup the candidate_version if the variable is not already set (pkgin separated by semicolons)" do + search = double() + expect(search).to receive(:each_line). + and_yield("something-varnish-1.1.1;;something varnish like\n"). + and_yield("varnish-2.3.4;;actual varnish\n") + @shell_out = double('shell_out!', :stdout => search) + expect(@provider).to receive(:shell_out!).with('/opt/local/bin/pkgin', 'se', 'varnish', :env => nil, :returns => [0,1], :timeout=>900).and_return(@shell_out) expect(@provider.candidate_version).to eq("2.3.4") end end - describe "when manipulating a resource" do + describe "when manipulating a resource" do - it "run pkgin and install the package" do - out = OpenStruct.new(:stdout => nil) - expect(@provider).to receive(:shell_out!).with("/opt/local/sbin/pkg_info -E \"varnish*\"", {:env => nil, :returns=>[0,1]}).and_return(@shell_out) - expect(@provider).to receive(:shell_out!).with("/opt/local/bin/pkgin -y install varnish-2.1.5nb2", {:env=>nil}).and_return(out) + it "run pkgin and install the package" do + out = OpenStruct.new(:stdout => nil) + expect(@provider).to receive(:shell_out!).with("/opt/local/sbin/pkg_info", "-E", "varnish*", {:env => nil, :returns=>[0,1], :timeout=>900}).and_return(@shell_out) + expect(@provider).to receive(:shell_out!).with("/opt/local/bin/pkgin", "-y", "install", "varnish-2.1.5nb2", {:env=>nil, :timeout=>900}).and_return(out) @provider.load_current_resource @provider.install_package("varnish", "2.1.5nb2") - end + end - end + end end diff --git a/spec/unit/provider/package/solaris_spec.rb b/spec/unit/provider/package/solaris_spec.rb index c348d665e8..ae6c96da00 100644 --- a/spec/unit/provider/package/solaris_spec.rb +++ b/spec/unit/provider/package/solaris_spec.rb @@ -71,8 +71,8 @@ PKGINFO it "should get the source package version from pkginfo if provided" do status = double(:stdout => @pkginfo, :exitstatus => 0) - expect(@provider).to receive(:shell_out).with("pkginfo -l -d /tmp/bash.pkg SUNWbash").and_return(status) - expect(@provider).to receive(:shell_out).with("pkginfo -l SUNWbash").and_return(@status) + expect(@provider).to receive(:shell_out).with("pkginfo -l -d /tmp/bash.pkg SUNWbash", { timeout: 900 }).and_return(status) + expect(@provider).to receive(:shell_out).with("pkginfo -l SUNWbash", { timeout: 900 }).and_return(@status) @provider.load_current_resource expect(@provider.current_resource.package_name).to eq("SUNWbash") @@ -81,8 +81,8 @@ PKGINFO it "should return the current version installed if found by pkginfo" do status = double(:stdout => @pkginfo, :exitstatus => 0) - expect(@provider).to receive(:shell_out).with("pkginfo -l -d /tmp/bash.pkg SUNWbash").and_return(@status) - expect(@provider).to receive(:shell_out).with("pkginfo -l SUNWbash").and_return(status) + expect(@provider).to receive(:shell_out).with("pkginfo -l -d /tmp/bash.pkg SUNWbash", { timeout: 900 }).and_return(@status) + expect(@provider).to receive(:shell_out).with("pkginfo -l SUNWbash", { timeout: 900 }).and_return(status) @provider.load_current_resource expect(@provider.current_resource.version).to eq("11.10.0,REV=2005.01.08.05.16") end @@ -101,8 +101,8 @@ PKGINFO end it "should return a current resource with a nil version if the package is not found" do - expect(@provider).to receive(:shell_out).with("pkginfo -l -d /tmp/bash.pkg SUNWbash").and_return(@status) - expect(@provider).to receive(:shell_out).with("pkginfo -l SUNWbash").and_return(@status) + expect(@provider).to receive(:shell_out).with("pkginfo -l -d /tmp/bash.pkg SUNWbash", { timeout: 900 }).and_return(@status) + expect(@provider).to receive(:shell_out).with("pkginfo -l SUNWbash", { timeout: 900 }).and_return(@status) @provider.load_current_resource expect(@provider.current_resource.version).to be_nil end @@ -132,7 +132,7 @@ PKGINFO describe "install and upgrade" do it "should run pkgadd -n -d with the package source to install" do - expect(@provider).to receive(:shell_out!).with("pkgadd -n -d /tmp/bash.pkg all") + expect(@provider).to receive(:shell_out!).with("pkgadd -n -d /tmp/bash.pkg all", { timeout: 900 }) @provider.install_package("SUNWbash", "11.10.0,REV=2005.01.08.05.16") end @@ -140,26 +140,26 @@ PKGINFO @new_resource = Chef::Resource::Package.new("/tmp/bash.pkg") @provider = Chef::Provider::Package::Solaris.new(@new_resource, @run_context) expect(@new_resource.source).to eq("/tmp/bash.pkg") - expect(@provider).to receive(:shell_out!).with("pkgadd -n -d /tmp/bash.pkg all") + expect(@provider).to receive(:shell_out!).with("pkgadd -n -d /tmp/bash.pkg all", { timeout: 900 }) @provider.install_package("/tmp/bash.pkg", "11.10.0,REV=2005.01.08.05.16") end it "should run pkgadd -n -a /tmp/myadmin -d with the package options -a /tmp/myadmin" do allow(@new_resource).to receive(:options).and_return("-a /tmp/myadmin") - expect(@provider).to receive(:shell_out!).with("pkgadd -n -a /tmp/myadmin -d /tmp/bash.pkg all") + expect(@provider).to receive(:shell_out!).with("pkgadd -n -a /tmp/myadmin -d /tmp/bash.pkg all", { timeout: 900 }) @provider.install_package("SUNWbash", "11.10.0,REV=2005.01.08.05.16") end end describe "remove" do it "should run pkgrm -n to remove the package" do - expect(@provider).to receive(:shell_out!).with("pkgrm -n SUNWbash") + expect(@provider).to receive(:shell_out!).with("pkgrm -n SUNWbash", { timeout: 900 }) @provider.remove_package("SUNWbash", "11.10.0,REV=2005.01.08.05.16") end it "should run pkgrm -n -a /tmp/myadmin with options -a /tmp/myadmin" do allow(@new_resource).to receive(:options).and_return("-a /tmp/myadmin") - expect(@provider).to receive(:shell_out!).with("pkgrm -n -a /tmp/myadmin SUNWbash") + expect(@provider).to receive(:shell_out!).with("pkgrm -n -a /tmp/myadmin SUNWbash", { timeout: 900 }) @provider.remove_package("SUNWbash", "11.10.0,REV=2005.01.08.05.16") end diff --git a/spec/unit/provider/package/windows_spec.rb b/spec/unit/provider/package/windows_spec.rb index d402113d72..e5acc87694 100644 --- a/spec/unit/provider/package/windows_spec.rb +++ b/spec/unit/provider/package/windows_spec.rb @@ -19,50 +19,129 @@ require 'spec_helper' describe Chef::Provider::Package::Windows, :windows_only do + before(:each) do + allow(Chef::Util::PathHelper).to receive(:windows?).and_return(true) + allow(Chef::FileCache).to receive(:create_cache_path).with("package/").and_return(cache_path) + end + 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(:resource_source) { 'calculator.msi' } + let(:new_resource) { Chef::Resource::WindowsPackage.new(resource_source) } let(:provider) { Chef::Provider::Package::Windows.new(new_resource, run_context) } + let(:cache_path) { 'c:\\cache\\' } describe "load_current_resource" do - before(:each) do - allow(Chef::Util::PathHelper).to receive(:validate_path) - allow(provider).to receive(:package_provider).and_return(double('package_provider', + shared_examples "a local file" do + before(:each) do + allow(Chef::Util::PathHelper).to receive(:validate_path) + allow(provider).to receive(:package_provider).and_return(double('package_provider', :installed_version => "1.0", :package_version => "2.0")) - end + 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 "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(resource_source) + 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 current version if the package is installed" do - provider.load_current_resource - expect(provider.current_resource.version).to eql("1.0") + it "sets the version to be installed" do + provider.load_current_resource + expect(provider.new_resource.version).to eql("2.0") + end end - it "sets the version to be installed" do - provider.load_current_resource - expect(provider.new_resource.version).to eql("2.0") + context "when the source is a uri" do + let(:resource_source) { 'https://foo.bar/calculator.msi' } + + context "when the source has not been downloaded" do + before(:each) do + allow(provider).to receive(:downloadable_file_missing?).and_return(true) + end + it "sets the current version to unknown" do + provider.load_current_resource + expect(provider.current_resource.version).to eql("unknown") + end + end + + context "when the source has been downloaded" do + before(:each) do + allow(provider).to receive(:downloadable_file_missing?).and_return(false) + end + it_behaves_like "a local file" + end + + context "when remote_file_attributes are provided" do + let (:remote_file_attributes) { {:path => 'C:\\foobar.msi'} } + before(:each) do + new_resource.remote_file_attributes(remote_file_attributes) + end + + it 'should override the attributes of the remote file resource used' do + expect(::File).to receive(:exists?).with(remote_file_attributes[:path]) + provider.load_current_resource + end + + end end - it "checks that the source path is valid" do - expect(Chef::Util::PathHelper).to receive(:validate_path) - provider.load_current_resource + context "when source is a local file" do + it_behaves_like "a local file" end end describe "package_provider" do - it "sets the package provider to MSI if the the installer type is :msi" do - allow(provider).to receive(:installer_type).and_return(:msi) - expect(provider.package_provider).to be_a(Chef::Provider::Package::Windows::MSI) + shared_examples "a local file" do + it "checks that the source path is valid" do + expect(Chef::Util::PathHelper).to receive(:validate_path) + provider.package_provider + end + + it "sets the package provider to MSI if the the installer type is :msi" do + allow(provider).to receive(: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 + allow(provider).to receive(:installer_type).and_return(:apt_for_windows) + expect { provider.package_provider }.to raise_error + end + end + + context "when the source is a uri" do + let(:resource_source) { 'https://foo.bar/calculator.msi' } + + context "when the source has not been downloaded" do + before(:each) do + allow(provider).to receive(:should_download?).and_return(true) + end + + it "should create a package provider with source pointing at the local file" do + expect(Chef::Provider::Package::Windows::MSI).to receive(:new) do |r| + expect(r.source).to eq("#{cache_path}#{::File.basename(resource_source)}") + end + provider.package_provider + end + + it_behaves_like "a local file" + end + + context "when the source has been downloaded" do + before(:each) do + allow(provider).to receive(:should_download?).and_return(false) + end + it_behaves_like "a local file" + end end - it "raises an error if the installer_type is unknown" do - allow(provider).to receive(:installer_type).and_return(:apt_for_windows) - expect { provider.package_provider }.to raise_error + context "when source is a local file" do + it_behaves_like "a local file" end end diff --git a/spec/unit/provider/package/yum_spec.rb b/spec/unit/provider/package/yum_spec.rb index 865dce23fa..e878b92621 100644 --- a/spec/unit/provider/package/yum_spec.rb +++ b/spec/unit/provider/package/yum_spec.rb @@ -17,6 +17,7 @@ # require 'spec_helper' +require 'securerandom' describe Chef::Provider::Package::Yum do before(:each) do @@ -122,6 +123,26 @@ describe Chef::Provider::Package::Yum do expect(@provider.arch).to eq("noarch") end + describe "when version constraint in package_name" do + it "should set package_version if no existing package_name is found and new_package_name is available" do + @new_resource = Chef::Resource::Package.new('cups = 1.2.4-11.18.el5_2.3') + @provider = Chef::Provider::Package::Yum.new(@new_resource, @run_context) + allow(@yum_cache).to receive(:package_available?) { |pkg| pkg == 'cups' ? true : false } + allow(@yum_cache).to receive(:packages_from_require) do |pkg| + [Chef::Provider::Package::Yum::RPMDbPackage.new("cups", "1.2.4-11.18.el5_2.3", "noarch", [], false, true, "base"), + Chef::Provider::Package::Yum::RPMDbPackage.new("cups", "1.2.4-11.18.el5_2.2", "noarch", [], false, true, "base"),] + end + expect(Chef::Log).to receive(:debug).exactly(1).times.with(%r{checking yum info}) + expect(Chef::Log).to receive(:debug).exactly(1).times.with(%r{installed version}) + expect(Chef::Log).to receive(:debug).exactly(1).times.with(%r{matched 2 packages,}) + @provider.load_current_resource + expect(@provider.new_resource.package_name).to eq("cups") + expect(@provider.new_resource.version).to eq("1.2.4-11.18.el5_2.3") + expect(@provider.send(:new_version_array)).to eq(["1.2.4-11.18.el5_2.3"]) + expect(@provider.send(:package_name_array)).to eq(["cups"]) + end + end + it "should not set the arch when an existing package_name is found" do @new_resource = Chef::Resource::YumPackage.new('testing.beta3') @yum_cache = double( @@ -1659,6 +1680,14 @@ describe Chef::Provider::Package::Yum::YumCache do end end + let(:yum_exe) { + StringIO.new("#!/usr/bin/python\n\naldsjfa\ldsajflkdsjf\lajsdfj") + } + + let(:bin_exe) { + StringIO.new(SecureRandom.random_bytes) + } + before(:each) do @stdin = double("STDIN", :nil_object => true) @stdout = double("STDOUT", :nil_object => true) @@ -1704,12 +1733,19 @@ file: file://///etc/yum.repos.d/CentOS-Base.repo, line: 12 'qeqwewe\n' EOF @status = double("Status", :exitstatus => 0, :stdin => @stdin, :stdout => @stdout_good, :stderr => @stderr) - # new singleton each time Chef::Provider::Package::Yum::YumCache.reset_instance @yc = Chef::Provider::Package::Yum::YumCache.instance # load valid data allow(@yc).to receive(:shell_out!).and_return(@status) + allow_any_instance_of(described_class).to receive(:which).with("yum").and_return("/usr/bin/yum") + allow(::File).to receive(:open).with("/usr/bin/yum", "r") do |&block| + res = block.call(yum_exe) + # a bit of a hack. rewind this since it seem that no matter what + # I do, we get the same StringIO objects on multiple calls to + # ::File.open + yum_exe.rewind; res + end end describe "initialize" do @@ -1726,6 +1762,24 @@ EOF end end + describe "python_bin" do + it "should return the default python if an error occurs" do + allow(::File).to receive(:open).with("/usr/bin/yum", "r").and_raise(StandardError) + expect(@yc.python_bin).to eq("/usr/bin/python") + end + + it "should return the default python if the yum-executable doesn't start with #!" do + allow(::File).to receive(:open).with("/usr/bin/yum", "r") { |&b| r = b.call(bin_exe); bin_exe.rewind; r} + expect(@yc.python_bin).to eq("/usr/bin/python") + end + + it "should return the interpreter for yum" do + other = StringIO.new("#!/usr/bin/super_python\n\nlasjdfdsaljf\nlasdjfs") + allow(::File).to receive(:open).with("/usr/bin/yum", "r") { |&b| r = b.call(other); other.rewind; r} + expect(@yc.python_bin).to eq("/usr/bin/super_python") + end + end + describe "refresh" do it "should implicitly call yum-dump.py only once by default after being instantiated" do expect(@yc).to receive(:shell_out!).once @@ -2041,6 +2095,36 @@ describe "Chef::Provider::Package::Yum - Multi" do it "should return the current resouce" do expect(@provider.load_current_resource).to eql(@provider.current_resource) end + + describe "when version constraint in package_name" do + it "should set package_version if no existing package_name is found and new_package_name is available" do + @new_resource = Chef::Resource::Package.new(['cups = 1.2.4-11.18.el5_2.3', 'emacs = 24.4']) + @provider = Chef::Provider::Package::Yum.new(@new_resource, @run_context) + allow(@yum_cache).to receive(:package_available?) { |pkg| %w(cups emacs).include?(pkg) ? true : false } + allow(@yum_cache).to receive(:candidate_version) do |pkg| + if pkg == 'cups' + "1.2.4-11.18.el5_2.3" + elsif pkg == 'emacs' + "24.4" + end + end + allow(@yum_cache).to receive(:packages_from_require) do |pkg| + if pkg.name == 'cups' + [Chef::Provider::Package::Yum::RPMDbPackage.new("cups", "1.2.4-11.18.el5_2.3", "noarch", [], false, true, "base")] + elsif pkg.name == 'emacs' + [Chef::Provider::Package::Yum::RPMDbPackage.new("emacs", "24.4", "noarch", [], false, true, "base")] + end + end + expect(Chef::Log).to receive(:debug).exactly(2).times.with(%r{matched 1 package,}) + expect(Chef::Log).to receive(:debug).exactly(1).times.with(%r{candidate version: \["1.2.4-11.18.el5_2.3", "24.4"\]}) + expect(Chef::Log).to receive(:debug).at_least(2).times.with(%r{checking yum info}) + @provider.load_current_resource + expect(@provider.new_resource.package_name).to eq(["cups", "emacs"]) + expect(@provider.new_resource.version).to eq(["1.2.4-11.18.el5_2.3", "24.4"]) + expect(@provider.send(:package_name_array)).to eq(["cups", "emacs"]) + expect(@provider.send(:new_version_array)).to eq(["1.2.4-11.18.el5_2.3", "24.4"]) + end + end end describe "when installing a package" do @@ -2076,5 +2160,31 @@ describe "Chef::Provider::Package::Yum - Multi" do allow(@new_resource).to receive(:options).and_return("--disablerepo epmd") @provider.install_package(["cups", "vim"], ["1.2.4-11.19.el5", '1.0']) end + + it "should run yum install with the package name and version when name has arch" do + @new_resource = Chef::Resource::Package.new(['cups.x86_64', 'vim']) + @provider = Chef::Provider::Package::Yum.new(@new_resource, @run_context) + allow(Chef::Provider::Package::Yum::RPMUtils).to receive(:rpmvercmp).and_return(-1) + + # Inside of load_current_resource() we'll call parse_arch for cups, + # and we need to craft the right response. The default mock setup above + # will just return valid versions all the time which won't work for this + # test. + allow(@yum_cache).to receive(:installed_version).with('cups', 'x86_64').and_return('XXXX') + allow(@yum_cache).to receive(:candidate_version).with('cups', 'x86_64').and_return('1.2.4-11.18.el5') + allow(@yum_cache).to receive(:installed_version).with('cups.x86_64').and_return(nil) + allow(@yum_cache).to receive(:candidate_version).with('cups.x86_64').and_return(nil) + + # Normal mock's for the idempotency check + allow(@yum_cache).to receive(:installed_version).with('cups', nil).and_return('1.2.4-11.18.el5') + allow(@yum_cache).to receive(:installed_version).with('vim', nil).and_return('0.9') + + @provider.load_current_resource + expect(@provider).to receive(:yum_command).with( + "yum -d0 -e0 -y install cups-1.2.4-11.19.el5.x86_64 vim-1.0" + ) + @provider.install_package(["cups", "vim"], ["1.2.4-11.19.el5", '1.0']) + end + end end diff --git a/spec/unit/provider/package/zypper_spec.rb b/spec/unit/provider/package/zypper_spec.rb index 706ad722dd..18ff739bc6 100644 --- a/spec/unit/provider/package/zypper_spec.rb +++ b/spec/unit/provider/package/zypper_spec.rb @@ -19,126 +19,150 @@ require 'spec_helper' describe Chef::Provider::Package::Zypper do + let!(:new_resource) { Chef::Resource::ZypperPackage.new("cups") } + + let!(:current_resource) { Chef::Resource::ZypperPackage.new("cups") } + + let(:provider) do + node = Chef::Node.new + events = Chef::EventDispatch::Dispatcher.new + run_context = Chef::RunContext.new(node, {}, events) + Chef::Provider::Package::Zypper.new(new_resource, run_context) + end + + let(:status) { double(:stdout => "\n", :exitstatus => 0) } + before(:each) do - @node = Chef::Node.new - @events = Chef::EventDispatch::Dispatcher.new - @run_context = Chef::RunContext.new(@node, {}, @events) - @new_resource = Chef::Resource::Package.new("cups") - - @current_resource = Chef::Resource::Package.new("cups") - - @provider = Chef::Provider::Package::Zypper.new(@new_resource, @run_context) - allow(Chef::Resource::Package).to receive(:new).and_return(@current_resource) - @status = double(:stdout => "\n", :exitstatus => 0) - allow(@provider).to receive(:shell_out).and_return(@status) - allow(@provider).to receive(:`).and_return("2.0") + allow(Chef::Resource::Package).to receive(:new).and_return(current_resource) + allow(provider).to receive(:shell_out).and_return(status) + allow(provider).to receive(:`).and_return("2.0") + end + + def shell_out_expectation(command, options=nil) + options ||= { timeout: 900 } + expect(provider).to receive(:shell_out).with(command, options) + end + + def shell_out_expectation!(command, options=nil) + options ||= { timeout: 900 } + expect(provider).to receive(:shell_out!).with(command, options) end describe "when loading the current package state" do it "should create a current resource with the name of the new_resource" do - expect(Chef::Resource::Package).to receive(:new).and_return(@current_resource) - @provider.load_current_resource + expect(Chef::Resource::Package).to receive(:new).with(new_resource.name).and_return(current_resource) + provider.load_current_resource end it "should set the current resources package name to the new resources package name" do - expect(@current_resource).to receive(:package_name).with(@new_resource.package_name) - @provider.load_current_resource + expect(current_resource).to receive(:package_name).with(new_resource.package_name) + provider.load_current_resource end it "should run zypper info with the package name" do - expect(@provider).to receive(:shell_out).with("zypper --non-interactive info #{@new_resource.package_name}").and_return(@status) - @provider.load_current_resource + shell_out_expectation( + "zypper --non-interactive info #{new_resource.package_name}" + ).and_return(status) + provider.load_current_resource end it "should set the installed version to nil on the current resource if zypper info installed version is (none)" do - allow(@provider).to receive(:shell_out).and_return(@status) - expect(@current_resource).to receive(:version).with(nil).and_return(true) - @provider.load_current_resource + allow(provider).to receive(:shell_out).and_return(status) + expect(current_resource).to receive(:version).with(nil).and_return(true) + provider.load_current_resource end it "should set the installed version if zypper info has one" do status = double(:stdout => "Version: 1.0\nInstalled: Yes\n", :exitstatus => 0) - allow(@provider).to receive(:shell_out).and_return(status) - expect(@current_resource).to receive(:version).with("1.0").and_return(true) - @provider.load_current_resource + allow(provider).to receive(:shell_out).and_return(status) + expect(current_resource).to receive(:version).with("1.0").and_return(true) + provider.load_current_resource end it "should set the candidate version if zypper info has one" do status = double(:stdout => "Version: 1.0\nInstalled: No\nStatus: out-of-date (version 0.9 installed)", :exitstatus => 0) - allow(@provider).to receive(:shell_out).and_return(status) - @provider.load_current_resource - expect(@provider.candidate_version).to eql("1.0") + allow(provider).to receive(:shell_out).and_return(status) + provider.load_current_resource + expect(provider.candidate_version).to eql("1.0") end it "should raise an exception if zypper info fails" do - expect(@status).to receive(:exitstatus).and_return(1) - expect { @provider.load_current_resource }.to raise_error(Chef::Exceptions::Package) + expect(status).to receive(:exitstatus).and_return(1) + expect { provider.load_current_resource }.to raise_error(Chef::Exceptions::Package) end it "should not raise an exception if zypper info succeeds" do - expect(@status).to receive(:exitstatus).and_return(0) - expect { @provider.load_current_resource }.not_to raise_error + expect(status).to receive(:exitstatus).and_return(0) + expect { provider.load_current_resource }.not_to raise_error end it "should return the current resouce" do - expect(@provider.load_current_resource).to eql(@current_resource) + expect(provider.load_current_resource).to eql(current_resource) end end describe "install_package" do it "should run zypper install with the package name and version" do allow(Chef::Config).to receive(:[]).with(:zypper_check_gpg).and_return(true) - expect(@provider).to receive(:shell_out!).with( - "zypper --non-interactive install --auto-agree-with-licenses emacs=1.0") - @provider.install_package("emacs", "1.0") + shell_out_expectation!( + "zypper --non-interactive install --auto-agree-with-licenses emacs=1.0" + ) + provider.install_package("emacs", "1.0") end it "should run zypper install without gpg checks" do allow(Chef::Config).to receive(:[]).with(:zypper_check_gpg).and_return(false) - expect(@provider).to receive(:shell_out!).with( + shell_out_expectation!( "zypper --non-interactive --no-gpg-checks install "+ - "--auto-agree-with-licenses emacs=1.0") - @provider.install_package("emacs", "1.0") + "--auto-agree-with-licenses emacs=1.0" + ) + provider.install_package("emacs", "1.0") end it "should warn about gpg checks on zypper install" do expect(Chef::Log).to receive(:warn).with( - /All packages will be installed without gpg signature checks/) - expect(@provider).to receive(:shell_out!).with( + /All packages will be installed without gpg signature checks/ + ) + shell_out_expectation!( "zypper --non-interactive --no-gpg-checks install "+ - "--auto-agree-with-licenses emacs=1.0") - @provider.install_package("emacs", "1.0") + "--auto-agree-with-licenses emacs=1.0" + ) + provider.install_package("emacs", "1.0") end end describe "upgrade_package" do it "should run zypper update with the package name and version" do allow(Chef::Config).to receive(:[]).with(:zypper_check_gpg).and_return(true) - expect(@provider).to receive(:shell_out!).with( - "zypper --non-interactive install --auto-agree-with-licenses emacs=1.0") - @provider.upgrade_package("emacs", "1.0") + shell_out_expectation!( + "zypper --non-interactive install --auto-agree-with-licenses emacs=1.0" + ) + provider.upgrade_package("emacs", "1.0") end it "should run zypper update without gpg checks" do allow(Chef::Config).to receive(:[]).with(:zypper_check_gpg).and_return(false) - expect(@provider).to receive(:shell_out!).with( + shell_out_expectation!( "zypper --non-interactive --no-gpg-checks install "+ - "--auto-agree-with-licenses emacs=1.0") - @provider.upgrade_package("emacs", "1.0") + "--auto-agree-with-licenses emacs=1.0" + ) + provider.upgrade_package("emacs", "1.0") end it "should warn about gpg checks on zypper upgrade" do expect(Chef::Log).to receive(:warn).with( - /All packages will be installed without gpg signature checks/) - expect(@provider).to receive(:shell_out!).with( + /All packages will be installed without gpg signature checks/ + ) + shell_out_expectation!( "zypper --non-interactive --no-gpg-checks install "+ - "--auto-agree-with-licenses emacs=1.0") - @provider.upgrade_package("emacs", "1.0") + "--auto-agree-with-licenses emacs=1.0" + ) + provider.upgrade_package("emacs", "1.0") end it "should run zypper upgrade without gpg checks" do - expect(@provider).to receive(:shell_out!).with( + shell_out_expectation!( "zypper --non-interactive --no-gpg-checks install "+ - "--auto-agree-with-licenses emacs=1.0") - - @provider.upgrade_package("emacs", "1.0") + "--auto-agree-with-licenses emacs=1.0" + ) + provider.upgrade_package("emacs", "1.0") end end @@ -147,83 +171,94 @@ describe Chef::Provider::Package::Zypper do context "when package version is not explicitly specified" do it "should run zypper remove with the package name" do allow(Chef::Config).to receive(:[]).with(:zypper_check_gpg).and_return(true) - expect(@provider).to receive(:shell_out!).with( - "zypper --non-interactive remove emacs") - @provider.remove_package("emacs", nil) + shell_out_expectation!( + "zypper --non-interactive remove emacs" + ) + provider.remove_package("emacs", nil) end end context "when package version is explicitly specified" do it "should run zypper remove with the package name" do allow(Chef::Config).to receive(:[]).with(:zypper_check_gpg).and_return(true) - expect(@provider).to receive(:shell_out!).with( - "zypper --non-interactive remove emacs=1.0") - @provider.remove_package("emacs", "1.0") + shell_out_expectation!( + "zypper --non-interactive remove emacs=1.0" + ) + provider.remove_package("emacs", "1.0") end it "should run zypper remove without gpg checks" do allow(Chef::Config).to receive(:[]).with(:zypper_check_gpg).and_return(false) - expect(@provider).to receive(:shell_out!).with( - "zypper --non-interactive --no-gpg-checks remove emacs=1.0") - @provider.remove_package("emacs", "1.0") + shell_out_expectation!( + "zypper --non-interactive --no-gpg-checks remove emacs=1.0" + ) + provider.remove_package("emacs", "1.0") end it "should warn about gpg checks on zypper remove" do expect(Chef::Log).to receive(:warn).with( - /All packages will be installed without gpg signature checks/) - expect(@provider).to receive(:shell_out!).with( - "zypper --non-interactive --no-gpg-checks remove emacs=1.0") - - @provider.remove_package("emacs", "1.0") + /All packages will be installed without gpg signature checks/ + ) + shell_out_expectation!( + "zypper --non-interactive --no-gpg-checks remove emacs=1.0" + ) + provider.remove_package("emacs", "1.0") end end end describe "purge_package" do it "should run remove_package with the name and version" do - expect(@provider).to receive(:shell_out!).with( - "zypper --non-interactive --no-gpg-checks remove --clean-deps emacs=1.0") - @provider.purge_package("emacs", "1.0") + shell_out_expectation!( + "zypper --non-interactive --no-gpg-checks remove --clean-deps emacs=1.0" + ) + provider.purge_package("emacs", "1.0") end it "should run zypper purge without gpg checks" do allow(Chef::Config).to receive(:[]).with(:zypper_check_gpg).and_return(false) - expect(@provider).to receive(:shell_out!).with( - "zypper --non-interactive --no-gpg-checks remove --clean-deps emacs=1.0") - @provider.purge_package("emacs", "1.0") + shell_out_expectation!( + "zypper --non-interactive --no-gpg-checks remove --clean-deps emacs=1.0" + ) + provider.purge_package("emacs", "1.0") end it "should warn about gpg checks on zypper purge" do expect(Chef::Log).to receive(:warn).with( - /All packages will be installed without gpg signature checks/) - expect(@provider).to receive(:shell_out!).with( - "zypper --non-interactive --no-gpg-checks remove --clean-deps emacs=1.0") - @provider.purge_package("emacs", "1.0") + /All packages will be installed without gpg signature checks/ + ) + shell_out_expectation!( + "zypper --non-interactive --no-gpg-checks remove --clean-deps emacs=1.0" + ) + provider.purge_package("emacs", "1.0") end end describe "on an older zypper" do before(:each) do - allow(@provider).to receive(:`).and_return("0.11.6") + allow(provider).to receive(:`).and_return("0.11.6") end describe "install_package" do it "should run zypper install with the package name and version" do - expect(@provider).to receive(:shell_out!).with( - "zypper --no-gpg-checks install --auto-agree-with-licenses -y emacs") - @provider.install_package("emacs", "1.0") + shell_out_expectation!( + "zypper --no-gpg-checks install --auto-agree-with-licenses -y emacs" + ) + provider.install_package("emacs", "1.0") end end describe "upgrade_package" do it "should run zypper update with the package name and version" do - expect(@provider).to receive(:shell_out!).with( - "zypper --no-gpg-checks install --auto-agree-with-licenses -y emacs") - @provider.upgrade_package("emacs", "1.0") + shell_out_expectation!( + "zypper --no-gpg-checks install --auto-agree-with-licenses -y emacs" + ) + provider.upgrade_package("emacs", "1.0") end end describe "remove_package" do it "should run zypper remove with the package name" do - expect(@provider).to receive(:shell_out!).with( - "zypper --no-gpg-checks remove -y emacs") - @provider.remove_package("emacs", "1.0") + shell_out_expectation!( + "zypper --no-gpg-checks remove -y emacs" + ) + provider.remove_package("emacs", "1.0") end end end diff --git a/spec/unit/provider/package_spec.rb b/spec/unit/provider/package_spec.rb index 1633d18f9d..432d968906 100644 --- a/spec/unit/provider/package_spec.rb +++ b/spec/unit/provider/package_spec.rb @@ -37,6 +37,12 @@ describe Chef::Provider::Package do allow(@provider).to receive(:install_package).and_return(true) end + it "raises a Chef::Exceptions::InvalidResourceSpecification if both multipackage and source are provided" do + @new_resource.package_name(['a', 'b']) + @new_resource.source('foo') + expect { @provider.run_action(:install) }.to raise_error(Chef::Exceptions::InvalidResourceSpecification) + end + it "should raise a Chef::Exceptions::Package if no version is specified, and no candidate is available" do @provider.candidate_version = nil expect { @provider.run_action(:install) }.to raise_error(Chef::Exceptions::Package) @@ -698,4 +704,38 @@ describe "Chef::Provider::Package - Multi" do expect(@new_resource).not_to be_updated_by_last_action end end + + describe "shell_out helpers" do + [ :shell_out_with_timeout, :shell_out_with_timeout! ].each do |method| + stubbed_method = method == :shell_out_with_timeout! ? :shell_out! : :shell_out + [ %w{command arg1 arg2}, "command arg1 arg2" ].each do |command| + it "#{method} defaults to 900 seconds" do + expect(@provider).to receive(stubbed_method).with(*command, timeout: 900) + @provider.send(method, *command) + end + it "#{method} overrides the default timeout with its options" do + expect(@provider).to receive(stubbed_method).with(*command, timeout: 1) + @provider.send(method, *command, timeout: 1) + end + it "#{method} overrides both timeouts with the new_resource.timeout" do + @new_resource.timeout(99) + expect(@provider).to receive(stubbed_method).with(*command, timeout: 99) + @provider.send(method, *command, timeout: 1) + end + it "#{method} defaults to 900 seconds and preserves options" do + expect(@provider).to receive(stubbed_method).with(*command, env: nil, timeout: 900) + @provider.send(method, *command, env: nil) + end + it "#{method} overrides the default timeout with its options and preserves options" do + expect(@provider).to receive(stubbed_method).with(*command, timeout: 1, env: nil) + @provider.send(method, *command, timeout: 1, env: nil) + end + it "#{method} overrides both timeouts with the new_resource.timeout and preseves options" do + @new_resource.timeout(99) + expect(@provider).to receive(stubbed_method).with(*command, timeout: 99, env: nil) + @provider.send(method, *command, timeout: 1, env: nil) + end + end + end + end end diff --git a/spec/unit/provider/powershell_spec.rb b/spec/unit/provider/powershell_spec.rb index 60dbcf80b0..855c18af9b 100644 --- a/spec/unit/provider/powershell_spec.rb +++ b/spec/unit/provider/powershell_spec.rb @@ -19,20 +19,62 @@ require 'spec_helper' describe Chef::Provider::PowershellScript, "action_run" do - before(:each) do - @node = Chef::Node.new + let(:powershell_version) { nil } + let(:node) { + node = Chef::Node.new + node.default["kernel"] = Hash.new + node.default["kernel"][:machine] = :x86_64.to_s + if ! powershell_version.nil? + node.default[:languages] = { :powershell => { :version => powershell_version } } + end + node + } - @node.default["kernel"] = Hash.new - @node.default["kernel"][:machine] = :x86_64.to_s + let(:provider) { + empty_events = Chef::EventDispatch::Dispatcher.new + run_context = Chef::RunContext.new(node, {}, empty_events) + new_resource = Chef::Resource::PowershellScript.new('run some powershell code', run_context) + Chef::Provider::PowershellScript.new(new_resource, run_context) + } - @run_context = Chef::RunContext.new(@node, {}, @events) - @new_resource = Chef::Resource::PowershellScript.new('run some powershell code', @run_context) + context 'when setting interpreter flags' do + it "should set the -File flag as the last flag" do + expect(provider.flags.split(' ').pop).to eq("-File") + end - @provider = Chef::Provider::PowershellScript.new(@new_resource, @run_context) - end + let(:execution_policy_flag) do + execution_policy_index = 0 + provider_flags = provider.flags.split(' ') + execution_policy_specified = false - it "should set the -File flag as the last flag" do - expect(@provider.flags.split(' ').pop).to eq("-File") - end + provider_flags.find do | value | + execution_policy_index += 1 + execution_policy_specified = value.downcase == '-ExecutionPolicy'.downcase + end + + execution_policy = execution_policy_specified ? provider_flags[execution_policy_index] : nil + end + context 'when running with an unspecified PowerShell version' do + let(:powershell_version) { nil } + it "should set the -ExecutionPolicy flag to 'Unrestricted' by default" do + expect(execution_policy_flag.downcase).to eq('unrestricted'.downcase) + end + end + + { '2.0' => 'Unrestricted', + '2.5' => 'Unrestricted', + '3.0' => 'Bypass', + '3.6' => 'Bypass', + '4.0' => 'Bypass', + '5.0' => 'Bypass' }.each do | version_policy | + let(:powershell_version) { version_policy[0].to_f } + context "when running PowerShell version #{version_policy[0]}" do + let(:powershell_version) { version_policy[0].to_f } + it "should set the -ExecutionPolicy flag to '#{version_policy[1]}'" do + expect(execution_policy_flag.downcase).to eq(version_policy[1].downcase) + end + end + end + end end diff --git a/spec/unit/provider/remote_directory_spec.rb b/spec/unit/provider/remote_directory_spec.rb index 4434714ebc..99e2fe285c 100644 --- a/spec/unit/provider/remote_directory_spec.rb +++ b/spec/unit/provider/remote_directory_spec.rb @@ -194,8 +194,8 @@ describe Chef::Provider::RemoteDirectory do expect(::File.exist?(symlinked_dir_path)).to be_falsey expect(::File.exist?(tmp_dir)).to be_truthy - rescue Chef::Exceptions::Win32APIError => e - pending "This must be run as an Administrator to create symlinks" + rescue Chef::Exceptions::Win32APIError + skip "This must be run as an Administrator to create symlinks" end end end diff --git a/spec/unit/provider/remote_file/fetcher_spec.rb b/spec/unit/provider/remote_file/fetcher_spec.rb index c049848fbf..8bd3b7c625 100644 --- a/spec/unit/provider/remote_file/fetcher_spec.rb +++ b/spec/unit/provider/remote_file/fetcher_spec.rb @@ -24,6 +24,26 @@ describe Chef::Provider::RemoteFile::Fetcher do let(:new_resource) { double("new resource") } let(:fetcher_instance) { double("fetcher") } + describe "when passed a network share" do + before do + expect(Chef::Provider::RemoteFile::NetworkFile).to receive(:new).and_return(fetcher_instance) + end + + context "when host is a name" do + let(:source) { "\\\\foohost\\fooshare\\Foo.tar.gz" } + it "returns a network file fetcher" do + expect(described_class.for_resource(source, new_resource, current_resource)).to eq(fetcher_instance) + end + end + + context "when host is an ip" do + let(:source) { "\\\\127.0.0.1\\fooshare\\Foo.tar.gz" } + it "returns a network file fetcher" do + expect(described_class.for_resource(source, new_resource, current_resource)).to eq(fetcher_instance) + end + end + end + describe "when passed an http url" do let(:uri) { double("uri", :scheme => "http" ) } before do @@ -72,4 +92,3 @@ describe Chef::Provider::RemoteFile::Fetcher do end end - diff --git a/spec/unit/provider/remote_file/local_file_spec.rb b/spec/unit/provider/remote_file/local_file_spec.rb index b33d82f624..575996a540 100644 --- a/spec/unit/provider/remote_file/local_file_spec.rb +++ b/spec/unit/provider/remote_file/local_file_spec.rb @@ -25,26 +25,45 @@ describe Chef::Provider::RemoteFile::LocalFile do let(:new_resource) { Chef::Resource::RemoteFile.new("local file backend test (new_resource)") } let(:current_resource) { Chef::Resource::RemoteFile.new("local file backend test (current_resource)") } subject(:fetcher) { Chef::Provider::RemoteFile::LocalFile.new(uri, new_resource, current_resource) } - - context "when parsing source path" do + + context "when parsing source path on windows" do + + before do + allow(Chef::Platform).to receive(:windows?).and_return(true) + end + describe "when given local unix path" do let(:uri) { URI.parse("file:///nyan_cat.png") } it "returns a correct unix path" do - expect(fetcher.fix_windows_path(uri.path)).to eq("/nyan_cat.png") + expect(fetcher.source_path).to eq("/nyan_cat.png") end end describe "when given local windows path" do let(:uri) { URI.parse("file:///z:/windows/path/file.txt") } it "returns a valid windows local path" do - expect(fetcher.fix_windows_path(uri.path)).to eq("z:/windows/path/file.txt") + expect(fetcher.source_path).to eq("z:/windows/path/file.txt") + end + end + + describe "when given local windows path with spaces" do + let(:uri) { URI.parse(URI.escape("file:///z:/windows/path/foo & bar.txt")) } + it "returns a valid windows local path" do + expect(fetcher.source_path).to eq("z:/windows/path/foo & bar.txt") end end describe "when given unc windows path" do let(:uri) { URI.parse("file:////server/share/windows/path/file.txt") } it "returns a valid windows unc path" do - expect(fetcher.fix_windows_path(uri.path)).to eq("//server/share/windows/path/file.txt") + expect(fetcher.source_path).to eq("//server/share/windows/path/file.txt") + end + end + + describe "when given unc windows path with spaces" do + let(:uri) { URI.parse(URI.escape("file:////server/share/windows/path/foo & bar.txt")) } + it "returns a valid windows unc path" do + expect(fetcher.source_path).to eq("//server/share/windows/path/foo & bar.txt") end end end @@ -73,7 +92,7 @@ describe Chef::Provider::RemoteFile::LocalFile do it "stages the local file to a temporary file" do expect(Chef::FileContentManagement::Tempfile).to receive(:new).with(new_resource).and_return(chef_tempfile) expect(::FileUtils).to receive(:cp).with(uri.path, tempfile.path) - expect(tempfile).to receive(:close) + expect(tempfile).to receive(:close) result = fetcher.fetch expect(result).to eq(tempfile) diff --git a/spec/unit/provider/remote_file/network_file_spec.rb b/spec/unit/provider/remote_file/network_file_spec.rb new file mode 100644 index 0000000000..3666a47468 --- /dev/null +++ b/spec/unit/provider/remote_file/network_file_spec.rb @@ -0,0 +1,45 @@ +# +# Author:: Jay Mundrawala (<jdm@chef.io>) +# Copyright:: Copyright (c) 2015 Chef Software +# 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::RemoteFile::NetworkFile do + + let(:source) { "\\\\foohost\\fooshare\\Foo.tar.gz" } + + let(:new_resource) { Chef::Resource::RemoteFile.new("network file (new_resource)") } + let(:current_resource) { Chef::Resource::RemoteFile.new("network file (current_resource)") } + subject(:fetcher) { Chef::Provider::RemoteFile::NetworkFile.new(source, new_resource, current_resource) } + + describe "when fetching the object" do + + let(:tempfile) { double("Tempfile", :path => "/tmp/foo/bar/Foo.tar.gz", :close => nil) } + let(:chef_tempfile) { double("Chef::FileContentManagement::Tempfile", :tempfile => tempfile) } + + it "stages the local file to a temporary file" do + expect(Chef::FileContentManagement::Tempfile).to receive(:new).with(new_resource).and_return(chef_tempfile) + expect(::FileUtils).to receive(:cp).with(source, tempfile.path) + expect(tempfile).to receive(:close) + + result = fetcher.fetch + expect(result).to eq(tempfile) + end + + end + +end diff --git a/spec/unit/provider/service/aix_service_spec.rb b/spec/unit/provider/service/aix_service_spec.rb index 796661145b..a0c8bb3407 100644 --- a/spec/unit/provider/service/aix_service_spec.rb +++ b/spec/unit/provider/service/aix_service_spec.rb @@ -51,22 +51,35 @@ describe Chef::Provider::Service::Aix do end it "current resource is running" do - expect(@provider).to receive(:shell_out!).with("lssrc -a | grep -w chef").and_return(@status) - expect(@provider).to receive(:is_resource_group?).with(["chef chef 12345 active"]) + expect(@provider).to receive(:shell_out!).with("lssrc -s chef").and_return(@status) + expect(@provider).to receive(:is_resource_group?).and_return false @provider.load_current_resource expect(@current_resource.running).to be_truthy end end - context "when the service is inoprative" do + context "when the service is inoperative" do before do @status = double("Status", :exitstatus => 0, :stdout => "chef chef inoperative\n") end it "current resource is not running" do - expect(@provider).to receive(:shell_out!).with("lssrc -a | grep -w chef").and_return(@status) - expect(@provider).to receive(:is_resource_group?).with(["chef chef inoperative"]) + expect(@provider).to receive(:shell_out!).with("lssrc -s chef").and_return(@status) + expect(@provider).to receive(:is_resource_group?).and_return false + + @provider.load_current_resource + expect(@current_resource.running).to be_falsey + end + end + + context "when there is no such service" do + before do + @status = double("Status", :exitstatus => 1, :stdout => "0513-085 The chef Subsystem is not on file.\n") + end + it "current resource is not running" do + expect(@provider).to receive(:shell_out!).with("lssrc -s chef").and_return(@status) + expect(@provider).to receive(:is_resource_group?).and_return false @provider.load_current_resource expect(@current_resource.running).to be_falsey @@ -75,13 +88,13 @@ describe Chef::Provider::Service::Aix do end describe "is resource group" do - context "when there are mutiple subsystems associated with group" do + context "when there are multiple subsystems associated with group" do before do @status = double("Status", :exitstatus => 0, :stdout => "chef1 chef 12345 active\nchef2 chef 12334 active\nchef3 chef inoperative") end it "service is a group" do - expect(@provider).to receive(:shell_out!).with("lssrc -a | grep -w chef").and_return(@status) + expect(@provider).to receive(:shell_out!).with("lssrc -g chef").and_return(@status) @provider.load_current_resource expect(@provider.instance_eval("@is_resource_group")).to be_truthy end @@ -93,19 +106,21 @@ describe Chef::Provider::Service::Aix do end it "service is a group" do - expect(@provider).to receive(:shell_out!).with("lssrc -a | grep -w chef").and_return(@status) + expect(@provider).to receive(:shell_out!).with("lssrc -g chef").and_return(@status) @provider.load_current_resource expect(@provider.instance_eval("@is_resource_group")).to be_truthy end end - context "when there service is a subsytem" do + context "when the service is a subsystem" do before do - @status = double("Status", :exitstatus => 0, :stdout => "chef chef123 inoperative\n") + @group_status = double("Status", :exitstatus => 1, :stdout => "0513-086 The chef Group is not on file.\n") + @service_status = double("Status", :exitstatus => 0, :stdout => "chef chef inoperative\n") end it "service is a subsystem" do - expect(@provider).to receive(:shell_out!).with("lssrc -a | grep -w chef").and_return(@status) + expect(@provider).to receive(:shell_out!).with("lssrc -g chef").and_return(@group_status) + expect(@provider).to receive(:shell_out!).with("lssrc -s chef").and_return(@service_status) @provider.load_current_resource expect(@provider.instance_eval("@is_resource_group")).to be_falsey end diff --git a/spec/unit/provider/service/freebsd_service_spec.rb b/spec/unit/provider/service/freebsd_service_spec.rb index 5a55425d87..cfc28c94d5 100644 --- a/spec/unit/provider/service/freebsd_service_spec.rb +++ b/spec/unit/provider/service/freebsd_service_spec.rb @@ -189,18 +189,6 @@ PS_SAMPLE expect(provider.status_load_success).to be_nil end - context "when ps command is nil" do - before do - node.automatic_attrs[:command] = {:ps => nil} - end - - it "should set running to nil" do - pending "superclass raises no conversion of nil to string which seems broken" - provider.determine_current_status! - expect(current_resource.running).to be_nil - end - end - context "when ps is empty string" do before do node.automatic_attrs[:command] = {:ps => ""} diff --git a/spec/unit/provider/user/dscl_spec.rb b/spec/unit/provider/user/dscl_spec.rb index 5ea037d944..32d0812d8c 100644 --- a/spec/unit/provider/user/dscl_spec.rb +++ b/spec/unit/provider/user/dscl_spec.rb @@ -24,7 +24,7 @@ require 'mixlib/shellout' describe Chef::Provider::User::Dscl do before do - allow(Chef::Platform).to receive(:windows?) { false } + allow(ChefConfig).to receive(:windows?) { false } end let(:node) { node = Chef::Node.new diff --git a/spec/unit/provider/user_spec.rb b/spec/unit/provider/user_spec.rb index 381168647b..2345ce18fb 100644 --- a/spec/unit/provider/user_spec.rb +++ b/spec/unit/provider/user_spec.rb @@ -143,8 +143,8 @@ describe Chef::Provider::User do begin require 'rubygems' require 'shadow' - rescue LoadError => e - pending "ruby-shadow gem not installed for dynamic load test" + rescue LoadError + skip "ruby-shadow gem not installed for dynamic load test" true else false @@ -161,7 +161,7 @@ describe Chef::Provider::User do unless shadow_lib_unavail? context "and we have the ruby-shadow gem" do - pending "and we are not root (rerun this again as root)", :requires_unprivileged_user => true + skip "and we are not root (rerun this again as root)", :requires_unprivileged_user => true context "and we are root", :requires_root => true do it "should pass assertions when ruby-shadow can be loaded" do diff --git a/spec/unit/provider_resolver_spec.rb b/spec/unit/provider_resolver_spec.rb index 718eebfdf4..e18d69bc19 100644 --- a/spec/unit/provider_resolver_spec.rb +++ b/spec/unit/provider_resolver_spec.rb @@ -19,9 +19,13 @@ require 'spec_helper' require 'chef/mixin/convert_to_class_name' require 'chef/provider_resolver' +require 'chef/platform/service_helpers' include Chef::Mixin::ConvertToClassName +# Open up Provider so we can write things down easier in here +#module Chef::Provider + describe Chef::ProviderResolver do let(:node) do @@ -46,6 +50,55 @@ describe Chef::ProviderResolver do let(:resource) { double(Chef::Resource, provider: provider, resource_name: resource_name) } + before do + allow(resource).to receive(:is_a?).with(Chef::Resource).and_return(true) + end + + def self.on_platform(platform, *tags, + platform_version: '11.0.1', + platform_family: nil, + os: nil, + &block) + Array(platform).each do |platform| + Array(platform_version).each do |platform_version| + on_one_platform(platform, platform_version, platform_family || platform, os || platform_family || platform, *tags, &block) + end + end + end + + def self.on_one_platform(platform, platform_version, platform_family, os, *tags, &block) + describe "on #{platform} #{platform_version}, platform_family: #{platform_family}, os: #{os}", *tags do + let(:os) { os } + let(:platform) { platform } + let(:platform_family) { platform_family } + let(:platform_version) { platform_version } + + define_singleton_method(:os) { os } + define_singleton_method(:platform) { platform } + define_singleton_method(:platform_family) { platform_family } + define_singleton_method(:platform_version) { platform_version } + + instance_eval(&block) + end + end + + def self.expect_providers(**providers) + providers.each do |name, provider| + describe name.to_s do + let(:resource_name) { name } + if provider + it "resolves to a #{provider}" do + expect(resolved_provider).to eql(provider) + end + else + it "Fails to resolve (since #{name.inspect} is unsupported on #{platform} #{platform_version})" do + expect { resolved_provider }.to raise_error /Cannot find a provider/ + end + end + end + end + end + describe "resolving service resource" do def stub_service_providers(*services) services ||= [] @@ -60,7 +113,6 @@ describe Chef::ProviderResolver do end before do - expect(provider_resolver).not_to receive(:maybe_chef_platform_lookup) allow(resource).to receive(:service_name).and_return("ntp") end @@ -297,389 +349,477 @@ describe Chef::ProviderResolver do end end - describe "on Ubuntu 14.10" do - let(:os) { "linux" } - let(:platform) { "ubuntu" } - let(:platform_family) { "debian" } - let(:platform_version) { "14.04" } - + on_platform "ubuntu", platform_version: "14.10", platform_family: "debian", os: "linux" do it_behaves_like "an ubuntu platform with upstart, update-rc.d and systemd" end - describe "on Ubuntu 14.04" do - let(:os) { "linux" } - let(:platform) { "ubuntu" } - let(:platform_family) { "debian" } - let(:platform_version) { "14.04" } - + on_platform "ubuntu", platform_version: "14.04", platform_family: "debian", os: "linux" do it_behaves_like "an ubuntu platform with upstart and update-rc.d" end - describe "on Ubuntu 10.04" do - let(:os) { "linux" } - let(:platform) { "ubuntu" } - let(:platform_family) { "debian" } - let(:platform_version) { "10.04" } - + on_platform "ubuntu", platform_version: "10.04", platform_family: "debian", os: "linux" do it_behaves_like "an ubuntu platform with upstart and update-rc.d" end # old debian uses the Debian provider (does not have insserv or upstart, or update-rc.d???) - describe "on Debian 4.0" do - let(:os) { "linux" } - let(:platform) { "debian" } - let(:platform_family) { "debian" } - let(:platform_version) { "4.0" } - + on_platform "debian", platform_version: "4.0", os: "linux" do #it_behaves_like "a debian platform using the debian provider" end # Debian replaced the debian provider with insserv in the FIXME:VERSION distro - describe "on Debian 7.0" do - let(:os) { "linux" } - let(:platform) { "debian" } - let(:platform_family) { "debian" } - let(:platform_version) { "7.0" } - + on_platform "debian", platform_version: "7.0", os: "linux" do it_behaves_like "a debian platform using the insserv provider" end - %w{solaris2 openindiana opensolaris nexentacore omnios smartos}.each do |platform| - describe "on #{platform}" do - let(:os) { "solaris2" } - let(:platform) { platform } - let(:platform_family) { platform } - let(:platform_version) { "5.11" } - - it "returns a Solaris provider" do - stub_service_providers - stub_service_configs - expect(resolved_provider).to eql(Chef::Provider::Service::Solaris) - end + on_platform %w{solaris2 openindiana opensolaris nexentacore omnios smartos}, os: "solaris2", platform_version: "5.11" do + it "returns a Solaris provider" do + stub_service_providers + stub_service_configs + expect(resolved_provider).to eql(Chef::Provider::Service::Solaris) + end - it "always returns a Solaris provider" do - # no matter what we stub on the next two lines we should get a Solaris provider - stub_service_providers(:debian, :invokercd, :insserv, :upstart, :redhat, :systemd) - stub_service_configs(:initd, :upstart, :xinetd, :user_local_etc_rcd, :systemd) - expect(resolved_provider).to eql(Chef::Provider::Service::Solaris) - end + it "always returns a Solaris provider" do + # no matter what we stub on the next two lines we should get a Solaris provider + stub_service_providers(:debian, :invokercd, :insserv, :upstart, :redhat, :systemd) + stub_service_configs(:initd, :upstart, :xinetd, :user_local_etc_rcd, :systemd) + expect(resolved_provider).to eql(Chef::Provider::Service::Solaris) end end - %w{mswin mingw32 windows}.each do |platform| - describe "on #{platform}" do - let(:os) { "windows" } - let(:platform) { platform } - let(:platform_family) { "windows" } - let(:platform_version) { "5.11" } - - it "returns a Windows provider" do - stub_service_providers - stub_service_configs - expect(resolved_provider).to eql(Chef::Provider::Service::Windows) - end + on_platform %w{mswin mingw32 windows}, platform_family: "windows", platform_version: "5.11" do + it "returns a Windows provider" do + stub_service_providers + stub_service_configs + expect(resolved_provider).to eql(Chef::Provider::Service::Windows) + end - it "always returns a Windows provider" do - # no matter what we stub on the next two lines we should get a Windows provider - stub_service_providers(:debian, :invokercd, :insserv, :upstart, :redhat, :systemd) - stub_service_configs(:initd, :upstart, :xinetd, :user_local_etc_rcd, :systemd) - expect(resolved_provider).to eql(Chef::Provider::Service::Windows) - end + it "always returns a Windows provider" do + # no matter what we stub on the next two lines we should get a Windows provider + stub_service_providers(:debian, :invokercd, :insserv, :upstart, :redhat, :systemd) + stub_service_configs(:initd, :upstart, :xinetd, :user_local_etc_rcd, :systemd) + expect(resolved_provider).to eql(Chef::Provider::Service::Windows) end end - %w{mac_os_x mac_os_x_server}.each do |platform| - describe "on #{platform}" do - let(:os) { "darwin" } - let(:platform) { platform } - let(:platform_family) { "mac_os_x" } - let(:platform_version) { "10.9.2" } - - it "returns a Macosx provider" do - stub_service_providers - stub_service_configs - expect(resolved_provider).to eql(Chef::Provider::Service::Macosx) - end + on_platform %w{mac_os_x mac_os_x_server}, os: "darwin", platform_family: "mac_os_x", platform_version: "10.9.2" do + it "returns a Macosx provider" do + stub_service_providers + stub_service_configs + expect(resolved_provider).to eql(Chef::Provider::Service::Macosx) + end - it "always returns a Macosx provider" do - # no matter what we stub on the next two lines we should get a Macosx provider - stub_service_providers(:debian, :invokercd, :insserv, :upstart, :redhat, :systemd) - stub_service_configs(:initd, :upstart, :xinetd, :user_local_etc_rcd, :systemd) - expect(resolved_provider).to eql(Chef::Provider::Service::Macosx) - end + it "always returns a Macosx provider" do + # no matter what we stub on the next two lines we should get a Macosx provider + stub_service_providers(:debian, :invokercd, :insserv, :upstart, :redhat, :systemd) + stub_service_configs(:initd, :upstart, :xinetd, :user_local_etc_rcd, :systemd) + expect(resolved_provider).to eql(Chef::Provider::Service::Macosx) end end - %w{freebsd netbsd}.each do |platform| - describe "on #{platform}" do - let(:os) { platform } - let(:platform) { platform } - let(:platform_family) { platform } - let(:platform_version) { "10.0-RELEASE" } - - it "returns a Freebsd provider if it finds the /usr/local/etc/rc.d initscript" do - stub_service_providers - stub_service_configs(:usr_local_etc_rcd) - expect(resolved_provider).to eql(Chef::Provider::Service::Freebsd) - end + on_platform %w(freebsd netbsd), platform_version: '3.1.4' do + it "returns a Freebsd provider if it finds the /usr/local/etc/rc.d initscript" do + stub_service_providers + stub_service_configs(:usr_local_etc_rcd) + expect(resolved_provider).to eql(Chef::Provider::Service::Freebsd) + end - it "returns a Freebsd provider if it finds the /etc/rc.d initscript" do - stub_service_providers - stub_service_configs(:etc_rcd) - expect(resolved_provider).to eql(Chef::Provider::Service::Freebsd) - end + it "returns a Freebsd provider if it finds the /etc/rc.d initscript" do + stub_service_providers + stub_service_configs(:etc_rcd) + expect(resolved_provider).to eql(Chef::Provider::Service::Freebsd) + end - it "always returns a Freebsd provider if it finds the /usr/local/etc/rc.d initscript" do - # should only care about :usr_local_etc_rcd stub in the service configs - stub_service_providers(:debian, :invokercd, :insserv, :upstart, :redhat, :systemd) - stub_service_configs(:usr_local_etc_rcd, :initd, :upstart, :xinetd, :systemd) - expect(resolved_provider).to eql(Chef::Provider::Service::Freebsd) - end + it "always returns a Freebsd provider if it finds the /usr/local/etc/rc.d initscript" do + # should only care about :usr_local_etc_rcd stub in the service configs + stub_service_providers(:debian, :invokercd, :insserv, :upstart, :redhat, :systemd) + stub_service_configs(:usr_local_etc_rcd, :initd, :upstart, :xinetd, :systemd) + expect(resolved_provider).to eql(Chef::Provider::Service::Freebsd) + end - it "always returns a Freebsd provider if it finds the /usr/local/etc/rc.d initscript" do - # should only care about :etc_rcd stub in the service configs - stub_service_providers(:debian, :invokercd, :insserv, :upstart, :redhat, :systemd) - stub_service_configs(:etc_rcd, :initd, :upstart, :xinetd, :systemd) - expect(resolved_provider).to eql(Chef::Provider::Service::Freebsd) - end + it "always returns a Freebsd provider if it finds the /usr/local/etc/rc.d initscript" do + # should only care about :etc_rcd stub in the service configs + stub_service_providers(:debian, :invokercd, :insserv, :upstart, :redhat, :systemd) + stub_service_configs(:etc_rcd, :initd, :upstart, :xinetd, :systemd) + expect(resolved_provider).to eql(Chef::Provider::Service::Freebsd) + end - it "foo" do - stub_service_providers - stub_service_configs - expect(resolved_provider).to eql(Chef::Provider::Service::Freebsd) - end + it "foo" do + stub_service_providers + stub_service_configs + expect(resolved_provider).to eql(Chef::Provider::Service::Freebsd) end end end - describe "for the package provider" do - let(:resource_name) { :package } - - before do - expect(provider_resolver).not_to receive(:maybe_chef_platform_lookup) - end - - %w{mac_os_x mac_os_x_server}.each do |platform| - describe "on #{platform}" do - let(:os) { "darwin" } - let(:platform) { platform } - let(:platform_family) { "mac_os_x" } - let(:platform_version) { "10.9.2" } - - - it "returns a Chef::Provider::Package::Homebrew provider" do - expect(resolved_provider).to eql(Chef::Provider::Package::Homebrew) - end - end - end - end + PROVIDERS = + { + bash: Chef::Provider::Script, + breakpoint: Chef::Provider::Breakpoint, + chef_gem: Chef::Provider::Package::Rubygems, + cookbook_file: Chef::Provider::CookbookFile, + csh: Chef::Provider::Script, + deploy: Chef::Provider::Deploy::Timestamped, + deploy_revision: Chef::Provider::Deploy::Revision, + directory: Chef::Provider::Directory, + easy_install_package: Chef::Provider::Package::EasyInstall, + erl_call: Chef::Provider::ErlCall, + execute: Chef::Provider::Execute, + file: Chef::Provider::File, + gem_package: Chef::Provider::Package::Rubygems, + git: Chef::Provider::Git, + group: Chef::Provider::Group::Gpasswd, + homebrew_package: Chef::Provider::Package::Homebrew, + http_request: Chef::Provider::HttpRequest, + ifconfig: Chef::Provider::Ifconfig, + link: Chef::Provider::Link, + log: Chef::Provider::Log::ChefLog, + macports_package: Chef::Provider::Package::Macports, + mdadm: Chef::Provider::Mdadm, + mount: Chef::Provider::Mount::Mount, + perl: Chef::Provider::Script, + portage_package: Chef::Provider::Package::Portage, + python: Chef::Provider::Script, + remote_directory: Chef::Provider::RemoteDirectory, + route: Chef::Provider::Route, + rpm_package: Chef::Provider::Package::Rpm, + ruby: Chef::Provider::Script, + ruby_block: Chef::Provider::RubyBlock, + script: Chef::Provider::Script, + subversion: Chef::Provider::Subversion, + template: Chef::Provider::Template, + timestamped_deploy: Chef::Provider::Deploy::Timestamped, + user: Chef::Provider::User::Useradd, + whyrun_safe_ruby_block: Chef::Provider::WhyrunSafeRubyBlock, + + # We want to check that these are unsupported: + apt_package: nil, + bff_package: nil, + dsc_script: nil, + dpkg_package: nil, + ips_package: nil, + pacman_package: nil, + paludis_package: nil, + rpm_package: nil, + smartos_package: nil, + solaris_package: nil, + yum_package: nil, + windows_package: nil, + windows_service: nil, + + "linux" => { + apt_package: Chef::Provider::Package::Apt, + dpkg_package: Chef::Provider::Package::Dpkg, + pacman_package: Chef::Provider::Package::Pacman, + paludis_package: Chef::Provider::Package::Paludis, + rpm_package: Chef::Provider::Package::Rpm, + yum_package: Chef::Provider::Package::Yum, + + "debian" => { + ifconfig: Chef::Provider::Ifconfig::Debian, + package: Chef::Provider::Package::Apt, +# service: Chef::Provider::Service::Debian, + + "debian" => { + "7.0" => { + }, + "6.0" => { + ifconfig: Chef::Provider::Ifconfig, +# service: Chef::Provider::Service::Insserv, + }, + "5.0" => { + ifconfig: Chef::Provider::Ifconfig, + }, + }, + "gcel" => { + "3.1.4" => { + ifconfig: Chef::Provider::Ifconfig, + }, + }, + "linaro" => { + "3.1.4" => { + ifconfig: Chef::Provider::Ifconfig, + }, + }, + "linuxmint" => { + "3.1.4" => { + ifconfig: Chef::Provider::Ifconfig, +# service: Chef::Provider::Service::Upstart, + }, + }, + "raspbian" => { + "3.1.4" => { + ifconfig: Chef::Provider::Ifconfig, + }, + }, + "ubuntu" => { + "11.10" => { + }, + "10.04" => { + ifconfig: Chef::Provider::Ifconfig, + }, + }, + }, + + "arch" => { + package: Chef::Provider::Package::Pacman, + + "arch" => { + "3.1.4" => { + } + }, + }, + + "freebsd" => { + group: Chef::Provider::Group::Pw, + user: Chef::Provider::User::Pw, + + "freebsd" => { + "3.1.4" => { + }, + }, + }, + "suse" => { + group: Chef::Provider::Group::Gpasswd, + "suse" => { + "12.0" => { + }, + %w(11.1 11.2 11.3) => { + group: Chef::Provider::Group::Suse, + }, + }, + "opensuse" => { +# service: Chef::Provider::Service::Redhat, + package: Chef::Provider::Package::Zypper, + group: Chef::Provider::Group::Usermod, + "12.3" => { + }, + "12.2" => { + group: Chef::Provider::Group::Suse, + }, + }, + }, + + "gentoo" => { + package: Chef::Provider::Package::Portage, + portage_package: Chef::Provider::Package::Portage, +# service: Chef::Provider::Service::Gentoo, + + "gentoo" => { + "3.1.4" => { + }, + }, + }, + + "rhel" => { +# service: Chef::Provider::Service::Systemd, + package: Chef::Provider::Package::Yum, + ifconfig: Chef::Provider::Ifconfig::Redhat, + + %w(amazon xcp xenserver ibm_powerkvm cloudlinux parallels) => { + "3.1.4" => { +# service: Chef::Provider::Service::Redhat, + }, + }, + %w(redhat centos scientific oracle) => { + "7.0" => { + }, + "6.0" => { +# service: Chef::Provider::Service::Redhat, + }, + }, + "fedora" => { + "15.0" => { + }, + "14.0" => { +# service: Chef::Provider::Service::Redhat, + }, + }, + }, - provider_mapping = { - "mac_os_x" => { - :package => Chef::Provider::Package::Homebrew, - :user => Chef::Provider::User::Dscl, - :group => Chef::Provider::Group::Dscl, - }, - "mac_os_x_server" => { - :package => Chef::Provider::Package::Homebrew, - :user => Chef::Provider::User::Dscl, - :group => Chef::Provider::Group::Dscl, - }, - "mswin" => { - :env => Chef::Provider::Env::Windows, - :user => Chef::Provider::User::Windows, - :group => Chef::Provider::Group::Windows, - :mount => Chef::Provider::Mount::Windows, - :batch => Chef::Provider::Batch, - :powershell_script => Chef::Provider::PowershellScript, }, - "mingw32" => { - :env => Chef::Provider::Env::Windows, - :user => Chef::Provider::User::Windows, - :group => Chef::Provider::Group::Windows, - :mount => Chef::Provider::Mount::Windows, - :batch => Chef::Provider::Batch, - :powershell_script => Chef::Provider::PowershellScript, + + "darwin" => { + %w(mac_os_x mac_os_x_server) => { + group: Chef::Provider::Group::Dscl, + package: Chef::Provider::Package::Homebrew, + user: Chef::Provider::User::Dscl, + + "mac_os_x" => { + "10.9.2" => { + }, + }, + }, }, + "windows" => { - :env => Chef::Provider::Env::Windows, - :user => Chef::Provider::User::Windows, - :group => Chef::Provider::Group::Windows, - :mount => Chef::Provider::Mount::Windows, - :batch => Chef::Provider::Batch, - :powershell_script => Chef::Provider::PowershellScript, + batch: Chef::Provider::Batch, + dsc_script: Chef::Provider::DscScript, + env: Chef::Provider::Env::Windows, + group: Chef::Provider::Group::Windows, + mount: Chef::Provider::Mount::Windows, + package: Chef::Provider::Package::Windows, + powershell_script: Chef::Provider::PowershellScript, + service: Chef::Provider::Service::Windows, + user: Chef::Provider::User::Windows, + windows_package: Chef::Provider::Package::Windows, + windows_service: Chef::Provider::Service::Windows, + + "windows" => { + %w(mswin mingw32 windows) => { + "10.9.2" => { + }, + }, + }, }, + "aix" => { - :cron => Chef::Provider::Cron::Aix, - }, - "netbsd"=> { - :group => Chef::Provider::Group::Groupmod, - }, - "openbsd" => { - :group => Chef::Provider::Group::Usermod, - :package => Chef::Provider::Package::Openbsd, + bff_package: Chef::Provider::Package::Aix, + cron: Chef::Provider::Cron::Aix, + group: Chef::Provider::Group::Aix, + ifconfig: Chef::Provider::Ifconfig::Aix, + mount: Chef::Provider::Mount::Aix, + package: Chef::Provider::Package::Aix, + rpm_package: Chef::Provider::Package::Rpm, + user: Chef::Provider::User::Aix, +# service: Chef::Provider::Service::Aix, + + "aix" => { + "aix" => { + "5.6" => { + }, + }, + }, }, - } - - def self.do_platform(platform_hash) - platform_hash.each do |resource, provider| - describe "for #{resource}" do - let(:resource_name) { resource } - - it "resolves to a #{provider}" do - expect(resolved_provider).to eql(provider) - end - end - end - end - - describe "individual platform mappings" do - let(:resource_name) { :user } - - before do - expect(provider_resolver).not_to receive(:maybe_chef_platform_lookup) - end - - %w{mac_os_x mac_os_x_server}.each do |platform| - describe "on #{platform}" do - let(:os) { "darwin" } - let(:platform) { platform } - let(:platform_family) { "mac_os_x" } - let(:platform_version) { "10.9.2" } - do_platform(provider_mapping[platform]) - end - end - - %w{mswin mingw32 windows}.each do |platform| - describe "on #{platform}" do - let(:os) { "windows" } - let(:platform) { platform } - let(:platform_family) { "windows" } - let(:platform_version) { "10.9.2" } + "hpux" => { + "hpux" => { + "hpux" => { + "3.1.4" => { + group: Chef::Provider::Group::Usermod + } + } + } + }, - do_platform(provider_mapping[platform]) - end - end + "netbsd" => { + "netbsd" => { + "netbsd" => { + "3.1.4" => { + group: Chef::Provider::Group::Groupmod, + }, + }, + }, + }, - describe "on AIX" do - let(:os) { "aix" } - let(:platform) { "aix" } - let(:platform_family) { "aix" } - let(:platform_version) { "6.2" } + "openbsd" => { + group: Chef::Provider::Group::Usermod, + package: Chef::Provider::Package::Openbsd, + + "openbsd" => { + "openbsd" => { + "3.1.4" => { + }, + }, + }, + }, - do_platform(provider_mapping['aix']) - end + "solaris2" => { + group: Chef::Provider::Group::Usermod, + ips_package: Chef::Provider::Package::Ips, + package: Chef::Provider::Package::Ips, + mount: Chef::Provider::Mount::Solaris, + solaris_package: Chef::Provider::Package::Solaris, + + "smartos" => { + smartos_package: Chef::Provider::Package::SmartOS, + package: Chef::Provider::Package::SmartOS, + + "smartos" => { + "3.1.4" => { + }, + }, + }, + + "solaris2" => { + "nexentacore" => { + "3.1.4" => { + package: Chef::Provider::Package::Solaris, + }, + }, + "omnios" => { + "3.1.4" => { + user: Chef::Provider::User::Solaris, + } + }, + "openindiana" => { + "3.1.4" => { + }, + }, + "opensolaris" => { + "3.1.4" => { + }, + }, + "solaris2" => { + user: Chef::Provider::User::Solaris, + "5.11" => { + }, + "5.9" => { + package: Chef::Provider::Package::Solaris, + }, + }, + }, - %w{netbsd openbsd}.each do |platform| - describe "on #{platform}" do - let(:os) { platform } - let(:platform) { platform } - let(:platform_family) { platform } - let(:platform_version) { "10.0-RELEASE" } + }, - do_platform(provider_mapping[platform]) - end - end - end + "solaris" => { + "solaris" => { + "solaris" => { + "3.1.4" => { + }, + }, + }, + }, - describe "resolving static providers" do - def resource_class(resource) - Chef::Resource.const_get(convert_to_class_name(resource.to_s)) - end - static_mapping = { - apt_package: Chef::Provider::Package::Apt, - bash: Chef::Provider::Script, - bff_package: Chef::Provider::Package::Aix, - breakpoint: Chef::Provider::Breakpoint, - chef_gem: Chef::Provider::Package::Rubygems, - cookbook_file: Chef::Provider::CookbookFile, - csh: Chef::Provider::Script, - deploy: Chef::Provider::Deploy::Timestamped, - deploy_revision: Chef::Provider::Deploy::Revision, - directory: Chef::Provider::Directory, - dpkg_package: Chef::Provider::Package::Dpkg, - dsc_script: Chef::Provider::DscScript, - easy_install_package: Chef::Provider::Package::EasyInstall, - erl_call: Chef::Provider::ErlCall, - execute: Chef::Provider::Execute, - file: Chef::Provider::File, - gem_package: Chef::Provider::Package::Rubygems, - git: Chef::Provider::Git, - homebrew_package: Chef::Provider::Package::Homebrew, - http_request: Chef::Provider::HttpRequest, - ips_package: Chef::Provider::Package::Ips, - link: Chef::Provider::Link, - log: Chef::Provider::Log::ChefLog, - macports_package: Chef::Provider::Package::Macports, - mdadm: Chef::Provider::Mdadm, - pacman_package: Chef::Provider::Package::Pacman, - paludis_package: Chef::Provider::Package::Paludis, - perl: Chef::Provider::Script, - portage_package: Chef::Provider::Package::Portage, - python: Chef::Provider::Script, - remote_directory: Chef::Provider::RemoteDirectory, - route: Chef::Provider::Route, - rpm_package: Chef::Provider::Package::Rpm, - ruby: Chef::Provider::Script, - ruby_block: Chef::Provider::RubyBlock, - script: Chef::Provider::Script, - smartos_package: Chef::Provider::Package::SmartOS, - solaris_package: Chef::Provider::Package::Solaris, - subversion: Chef::Provider::Subversion, - template: Chef::Provider::Template, - timestamped_deploy: Chef::Provider::Deploy::Timestamped, - whyrun_safe_ruby_block: Chef::Provider::WhyrunSafeRubyBlock, - windows_package: Chef::Provider::Package::Windows, - windows_service: Chef::Provider::Service::Windows, - yum_package: Chef::Provider::Package::Yum, + "exherbo" => { + "exherbo" => { + "exherbo" => { + "3.1.4" => { + package: Chef::Provider::Package::Paludis + } + } } + } + } - describe "on Ubuntu 14.04" do - let(:os) { "linux" } - let(:platform) { "ubuntu" } - let(:platform_family) { "debian" } - let(:platform_version) { "14.04" } - - supported_providers = [ - :apt_package, :bash, :breakpoint, :chef_gem, :cookbook_file, :csh, :deploy, - :deploy_revision, :directory, :dpkg_package, :easy_install_package, :erl_call, - :execute, :file, :gem_package, :git, :homebrew_package, :http_request, :link, - :log, :macports_package, :pacman_package, :paludis_package, :perl, :python, - :remote_directory, :route, :rpm_package, :ruby, :ruby_block, :script, :subversion, - :template, :timestamped_deploy, :whyrun_safe_ruby_block, :yum_package, - ] - - supported_providers.each do |static_resource| - static_provider = static_mapping[static_resource] - context "when the resource is a #{static_resource}" do - let(:resource) { double(Chef::Resource, provider: nil, resource_name: static_resource) } - let(:action) { :start } # in reality this doesn't matter much - it "should resolve to a #{static_provider} provider" do - expect(provider_resolver).not_to receive(:maybe_chef_platform_lookup) - expect(resolved_provider).to eql(static_provider) + def self.create_provider_tests(providers, test, expected, filter) + expected = expected.merge(providers.select { |key, value| key.is_a?(Symbol) }) + providers.each do |key, value| + if !key.is_a?(Symbol) + next_test = test.merge({ filter => key }) + next_filter = + case filter + when :os + :platform_family + when :platform_family + :platform + when :platform + :platform_version + when :platform_version + nil + else + raise "Hash too deep; only os, platform_family, platform and platform_version supported" end - end + create_provider_tests(value, next_test, expected, next_filter) end - - unsupported_providers = [ - :bff_package, :dsc_script, :ips_package, :smartos_package, - :solaris_package, :windows_package, :windows_service, - ] - - unsupported_providers.each do |static_resource| - static_provider = static_mapping[static_resource] - context "when the resource is a #{static_resource}" do - let(:resource) { double(Chef::Resource, provider: nil, resource_name: static_resource) } - let(:action) { :start } # in reality this doesn't matter much - it "should fall back into the old provider mapper code and hooks" do - retval = Object.new - expect(provider_resolver).to receive(:maybe_chef_platform_lookup).and_return(retval) - expect(resolved_provider).to equal(retval) - end - end + end + # If there is no filter, we're as deep as we need to go + if !filter + on_platform test.delete(:platform), test do + expect_providers(expected) end end end + + create_provider_tests(PROVIDERS, {}, {}, :os) end diff --git a/spec/unit/provider_spec.rb b/spec/unit/provider_spec.rb index 5a21b094d0..d7a34bc21b 100644 --- a/spec/unit/provider_spec.rb +++ b/spec/unit/provider_spec.rb @@ -49,6 +49,13 @@ class ConvergeActionDemonstrator < Chef::Provider end end +class CheckResourceSemanticsDemonstrator < ConvergeActionDemonstrator + def check_resource_semantics! + raise Chef::Exceptions::InvalidResourceSpecification.new("check_resource_semantics!") + end +end + + describe Chef::Provider do before(:each) do @cookbook_collection = Chef::CookbookCollection.new([]) @@ -89,6 +96,10 @@ describe Chef::Provider do expect(@provider.send(:whyrun_supported?)).to eql(false) end + it "should do nothing for check_resource_semantics! by default" do + expect { @provider.check_resource_semantics! }.not_to raise_error + end + it "should return true for action_nothing" do expect(@provider.action_nothing).to eql(true) end @@ -176,6 +187,15 @@ describe Chef::Provider do expect(@resource).not_to be_updated_by_last_action end end + + describe "and the resource is invalid" do + let(:provider) { CheckResourceSemanticsDemonstrator.new(@resource, @run_context) } + + it "fails with InvalidResourceSpecification when run" do + expect { provider.run_action(:foo) }.to raise_error(Chef::Exceptions::InvalidResourceSpecification) + end + + end end end diff --git a/spec/unit/recipe_spec.rb b/spec/unit/recipe_spec.rb index 7442f4477e..ee98e63c1f 100644 --- a/spec/unit/recipe_spec.rb +++ b/spec/unit/recipe_spec.rb @@ -83,7 +83,7 @@ describe Chef::Recipe do it "should require a name argument" do expect { recipe.cat - }.to raise_error(ArgumentError, "You must supply a name when declaring a cat resource") + }.to raise_error(ArgumentError) end it "should allow regular errors (not NameErrors) to pass unchanged" do @@ -121,7 +121,7 @@ describe Chef::Recipe do it "locate resource for particular platform" do ShaunTheSheep = Class.new(Chef::Resource) - ShaunTheSheep.provides :laughter, :on_platforms => ["television"] + ShaunTheSheep.provides :laughter, :platform => ["television"] node.automatic[:platform] = "television" node.automatic[:platform_version] = "123" res = recipe.laughter "timmy" @@ -141,9 +141,7 @@ describe Chef::Recipe do before do node.automatic[:platform] = "nbc_sports" Sounders = Class.new(Chef::Resource) - Sounders.provides :football, platform: "nbc_sports" TottenhamHotspur = Class.new(Chef::Resource) - TottenhamHotspur.provides :football, platform: "nbc_sports" end after do @@ -151,16 +149,12 @@ describe Chef::Recipe do Object.send(:remove_const, :TottenhamHotspur) end - it "warns if resolution of the two resources is ambiguous" do - expect(Chef::Log).to receive(:warn).at_least(:once).with(/Ambiguous resource precedence/) - res1 = recipe.football "club world cup" - expect(res1.name).to eql("club world cup") - # the class of res1 is not defined. - end - - it "selects one if it is given priority" do + it "selects one if it is the last declared" do expect(Chef::Log).not_to receive(:warn) - Chef::Platform::ResourcePriorityMap.instance.send(:priority, :football, TottenhamHotspur, platform: "nbc_sports") + + Sounders.provides :football, platform: "nbc_sports" + TottenhamHotspur.provides :football, platform: "nbc_sports" + res1 = recipe.football "club world cup" expect(res1.name).to eql("club world cup") expect(res1).to be_a_kind_of(TottenhamHotspur) @@ -168,7 +162,10 @@ describe Chef::Recipe do it "selects the other one if it is given priority" do expect(Chef::Log).not_to receive(:warn) - Chef::Platform::ResourcePriorityMap.instance.send(:priority, :football, Sounders, platform: "nbc_sports") + + TottenhamHotspur.provides :football, platform: "nbc_sports" + Sounders.provides :football, platform: "nbc_sports" + res1 = recipe.football "club world cup" expect(res1.name).to eql("club world cup") expect(res1).to be_a_kind_of(Sounders) @@ -408,7 +405,7 @@ describe Chef::Recipe do end it "does not copy the action from the first resource" do - expect(original_resource.action).to eq([:score]) + expect(original_resource.action).to eq(:score) expect(duplicated_resource.action).to eq(:nothing) end @@ -504,7 +501,7 @@ describe Chef::Recipe do recipe.from_file(File.join(CHEF_SPEC_DATA, "recipes", "test.rb")) res = recipe.resources(:file => "/etc/nsswitch.conf") expect(res.name).to eql("/etc/nsswitch.conf") - expect(res.action).to eql([:create]) + expect(res.action).to eql(:create) expect(res.owner).to eql("root") expect(res.group).to eql("root") expect(res.mode).to eql(0644) @@ -577,6 +574,36 @@ describe Chef::Recipe do expect(cookbook_collection[:openldap]).not_to receive(:load_recipe).with("default", run_context) openldap_recipe.include_recipe "::default" end + + it "will not load a recipe twice when called first from an LWRP provider" do + openldap_recipe = Chef::Recipe.new("openldap", "test", run_context) + expect(node).to receive(:loaded_recipe).with(:openldap, "default").exactly(:once) + allow(run_context).to receive(:unreachable_cookbook?).with(:openldap).and_return(false) + expect(cookbook_collection[:openldap]).to receive(:load_recipe).with("default", run_context) + openldap_recipe.include_recipe "::default" + expect(cookbook_collection[:openldap]).not_to receive(:load_recipe).with("default", run_context) + openldap_recipe.openldap_includer("do it").run_action(:run) + end + + it "will not load a recipe twice when called last from an LWRP provider" do + openldap_recipe = Chef::Recipe.new("openldap", "test", run_context) + expect(node).to receive(:loaded_recipe).with(:openldap, "default").exactly(:once) + allow(run_context).to receive(:unreachable_cookbook?).with(:openldap).and_return(false) + expect(cookbook_collection[:openldap]).to receive(:load_recipe).with("default", run_context) + openldap_recipe.openldap_includer("do it").run_action(:run) + expect(cookbook_collection[:openldap]).not_to receive(:load_recipe).with("default", run_context) + openldap_recipe.include_recipe "::default" + end + + it "will not load a recipe twice when called both times from an LWRP provider" do + openldap_recipe = Chef::Recipe.new("openldap", "test", run_context) + expect(node).to receive(:loaded_recipe).with(:openldap, "default").exactly(:once) + allow(run_context).to receive(:unreachable_cookbook?).with(:openldap).and_return(false) + expect(cookbook_collection[:openldap]).to receive(:load_recipe).with("default", run_context) + openldap_recipe.openldap_includer("do it").run_action(:run) + expect(cookbook_collection[:openldap]).not_to receive(:load_recipe).with("default", run_context) + openldap_recipe.openldap_includer("do it").run_action(:run) + end end describe "tags" do diff --git a/spec/unit/resource/batch_spec.rb b/spec/unit/resource/batch_spec.rb index 4a056b8735..b8c2897f42 100644 --- a/spec/unit/resource/batch_spec.rb +++ b/spec/unit/resource/batch_spec.rb @@ -25,6 +25,7 @@ describe Chef::Resource::Batch do node.default["kernel"] = Hash.new node.default["kernel"][:machine] = :x86_64.to_s + node.automatic[:os] = 'windows' run_context = Chef::RunContext.new(node, nil, nil) diff --git a/spec/unit/resource/breakpoint_spec.rb b/spec/unit/resource/breakpoint_spec.rb index ed1f3ebcd5..9c867ebcc7 100644 --- a/spec/unit/resource/breakpoint_spec.rb +++ b/spec/unit/resource/breakpoint_spec.rb @@ -37,7 +37,7 @@ describe Chef::Resource::Breakpoint do end it "defaults to the break action" do - expect(@breakpoint.action).to eq("break") + expect(@breakpoint.action).to eq(:break) end it "names itself after the line number of the file where it's created" do diff --git a/spec/unit/resource/erl_call_spec.rb b/spec/unit/resource/erl_call_spec.rb index 8ec182665f..008d27372a 100644 --- a/spec/unit/resource/erl_call_spec.rb +++ b/spec/unit/resource/erl_call_spec.rb @@ -35,7 +35,7 @@ describe Chef::Resource::ErlCall do end it "should have a default action of run" do - expect(@resource.action).to eql("run") + expect(@resource.action).to eql(:run) end it "should accept run as an action" do diff --git a/spec/unit/resource/file_spec.rb b/spec/unit/resource/file_spec.rb index db52e35004..dd20f5f03a 100644 --- a/spec/unit/resource/file_spec.rb +++ b/spec/unit/resource/file_spec.rb @@ -29,7 +29,7 @@ describe Chef::Resource::File do end it "should have a default action of 'create'" do - expect(@resource.action).to eql("create") + expect(@resource.action).to eql(:create) end it "should have a default content of nil" do diff --git a/spec/unit/resource/ifconfig_spec.rb b/spec/unit/resource/ifconfig_spec.rb index ea5282acd5..e3e1f6daa2 100644 --- a/spec/unit/resource/ifconfig_spec.rb +++ b/spec/unit/resource/ifconfig_spec.rb @@ -47,21 +47,23 @@ describe Chef::Resource::Ifconfig do end end - shared_examples "being a platform using the default ifconfig provider" do |platform, version| + shared_examples "being a platform based on an old Debian" do |platform, version| before do + @node.automatic_attrs[:os] = 'linux' + @node.automatic_attrs[:platform_family] = 'debian' @node.automatic_attrs[:platform] = platform @node.automatic_attrs[:platform_version] = version end it "should use an ordinary Provider::Ifconfig as a provider for #{platform} #{version}" do - expect(@resource.provider_for_action(:add)).to be_a_kind_of(Chef::Provider::Ifconfig) - expect(@resource.provider_for_action(:add)).not_to be_a_kind_of(Chef::Provider::Ifconfig::Debian) - expect(@resource.provider_for_action(:add)).not_to be_a_kind_of(Chef::Provider::Ifconfig::Redhat) + expect(@resource.provider_for_action(:add).class).to eq(Chef::Provider::Ifconfig) end end shared_examples "being a platform based on RedHat" do |platform, version| before do + @node.automatic_attrs[:os] = 'linux' + @node.automatic_attrs[:platform_family] = 'rhel' @node.automatic_attrs[:platform] = platform @node.automatic_attrs[:platform_version] = version end @@ -73,6 +75,8 @@ describe Chef::Resource::Ifconfig do shared_examples "being a platform based on a recent Debian" do |platform, version| before do + @node.automatic_attrs[:os] = 'linux' + @node.automatic_attrs[:platform_family] = 'debian' @node.automatic_attrs[:platform] = platform @node.automatic_attrs[:platform_version] = version end @@ -87,7 +91,7 @@ describe Chef::Resource::Ifconfig do end describe "when it is an old Debian platform" do - it_should_behave_like "being a platform using the default ifconfig provider", "debian", "6.0" + it_should_behave_like "being a platform based on an old Debian", "debian", "6.0" end describe "when it is a new Debian platform" do @@ -95,7 +99,7 @@ describe Chef::Resource::Ifconfig do end describe "when it is an old Ubuntu platform" do - it_should_behave_like "being a platform using the default ifconfig provider", "ubuntu", "11.04" + it_should_behave_like "being a platform based on an old Debian", "ubuntu", "11.04" end describe "when it is a new Ubuntu platform" do diff --git a/spec/unit/resource/powershell_spec.rb b/spec/unit/resource/powershell_spec.rb index c263172ae6..2505c4a3d7 100644 --- a/spec/unit/resource/powershell_spec.rb +++ b/spec/unit/resource/powershell_spec.rb @@ -25,6 +25,7 @@ describe Chef::Resource::PowershellScript do node.default["kernel"] = Hash.new node.default["kernel"][:machine] = :x86_64.to_s + node.automatic[:os] = 'windows' run_context = Chef::RunContext.new(node, nil, nil) diff --git a/spec/unit/resource/remote_file_spec.rb b/spec/unit/resource/remote_file_spec.rb index 3731d1aee2..0a379ff574 100644 --- a/spec/unit/resource/remote_file_spec.rb +++ b/spec/unit/resource/remote_file_spec.rb @@ -39,6 +39,11 @@ describe Chef::Resource::RemoteFile do expect(Chef::Platform.find_provider(:noplatform, 'noversion', @resource)).to eq(Chef::Provider::RemoteFile) end + it "says its provider is RemoteFile when the source is a network share" do + @resource.source("\\\\fakey\\fakerton\\fake.txt") + expect(@resource.provider).to eq(Chef::Provider::RemoteFile) + expect(Chef::Platform.find_provider(:noplatform, 'noversion', @resource)).to eq(Chef::Provider::RemoteFile) + end describe "source" do it "does not have a default value for 'source'" do @@ -50,6 +55,16 @@ describe Chef::Resource::RemoteFile do expect(@resource.source).to eql([ "http://opscode.com/" ]) end + it "should accept a windows network share source" do + @resource.source "\\\\fakey\\fakerton\\fake.txt" + expect(@resource.source).to eql([ "\\\\fakey\\fakerton\\fake.txt" ]) + end + + it 'should accept file URIs with spaces' do + @resource.source("file:///C:/foo bar") + expect(@resource.source).to eql(["file:///C:/foo bar"]) + end + it "should accept a delayed evalutator (string) for the remote file source" do @resource.source Chef::DelayedEvaluator.new {"http://opscode.com/"} expect(@resource.source).to eql([ "http://opscode.com/" ]) diff --git a/spec/unit/resource/route_spec.rb b/spec/unit/resource/route_spec.rb index ec1d369932..ffb9304511 100644 --- a/spec/unit/resource/route_spec.rb +++ b/spec/unit/resource/route_spec.rb @@ -35,7 +35,7 @@ describe Chef::Resource::Route do end it "should have a default action of 'add'" do - expect(@resource.action).to eql([:add]) + expect(@resource.action).to eql(:add) end it "should accept add or delete for action" do diff --git a/spec/unit/resource/ruby_block_spec.rb b/spec/unit/resource/ruby_block_spec.rb index 9f19fecd4f..5d83f7e367 100644 --- a/spec/unit/resource/ruby_block_spec.rb +++ b/spec/unit/resource/ruby_block_spec.rb @@ -31,7 +31,7 @@ describe Chef::Resource::RubyBlock do end it "should have a default action of 'create'" do - expect(@resource.action).to eql("run") + expect(@resource.action).to eql(:run) end it "should have a resource name of :ruby_block" do @@ -46,7 +46,7 @@ describe Chef::Resource::RubyBlock do it "allows the action to be 'create'" do @resource.action :create - expect(@resource.action).to eq([:create]) + expect(@resource.action).to eq(:create) end describe "when it has been initialized with block code" do diff --git a/spec/unit/resource/template_spec.rb b/spec/unit/resource/template_spec.rb index df5ca94b8a..2fd951b72d 100644 --- a/spec/unit/resource/template_spec.rb +++ b/spec/unit/resource/template_spec.rb @@ -98,7 +98,7 @@ describe Chef::Resource::Template do context "on windows", :windows_only do # according to Chef::Resource::File, windows state attributes are rights + deny_rights - pending "it describes its state" + skip "it describes its state" end it "returns the file path as its identity" do diff --git a/spec/unit/resource/timestamped_deploy_spec.rb b/spec/unit/resource/timestamped_deploy_spec.rb index eca6c570d4..4ebfdaf059 100644 --- a/spec/unit/resource/timestamped_deploy_spec.rb +++ b/spec/unit/resource/timestamped_deploy_spec.rb @@ -23,11 +23,10 @@ describe Chef::Resource::TimestampedDeploy, "initialize" do static_provider_resolution( resource: Chef::Resource::TimestampedDeploy, provider: Chef::Provider::Deploy::Timestamped, - name: :deploy, + name: :timestamped_deploy, action: :deploy, os: 'linux', platform_family: 'rhel', ) end - diff --git a/spec/unit/resource/windows_package_spec.rb b/spec/unit/resource/windows_package_spec.rb index 1e02f2449b..6aa5d357ea 100644 --- a/spec/unit/resource/windows_package_spec.rb +++ b/spec/unit/resource/windows_package_spec.rb @@ -63,9 +63,9 @@ describe Chef::Resource::WindowsPackage, "initialize" do end it "coverts a source to an absolute path" do - allow(::File).to receive(:absolute_path).and_return("c:\\Files\\frost.msi") + allow(::File).to receive(:absolute_path).and_return("c:\\files\\frost.msi") resource.source("frost.msi") - expect(resource.source).to eql "c:\\Files\\frost.msi" + expect(resource.source).to eql "c:\\files\\frost.msi" end it "converts slashes to backslashes in the source path" do @@ -78,4 +78,18 @@ describe Chef::Resource::WindowsPackage, "initialize" do # it's a little late to stub out File.absolute_path expect(resource.source).to include("solitaire.msi") end + + it "supports the checksum attribute" do + resource.checksum('somechecksum') + expect(resource.checksum).to eq('somechecksum') + end + + context 'when a URL is used' do + let(:resource_source) { 'https://foo.bar/solitare.msi' } + let(:resource) { Chef::Resource::WindowsPackage.new(resource_source) } + + it "should return the source unmodified" do + expect(resource.source).to eq(resource_source) + end + end end diff --git a/spec/unit/resource/windows_service_spec.rb b/spec/unit/resource/windows_service_spec.rb index 45a295c24e..8866cad1bf 100644 --- a/spec/unit/resource/windows_service_spec.rb +++ b/spec/unit/resource/windows_service_spec.rb @@ -44,6 +44,6 @@ describe Chef::Resource::WindowsService, "initialize" do it "allows the action to be 'configure_startup'" do resource.action :configure_startup - expect(resource.action).to eq([:configure_startup]) + expect(resource.action).to eq(:configure_startup) end end diff --git a/spec/unit/resource_spec.rb b/spec/unit/resource_spec.rb index 6b2d6c89d3..8ba45d9350 100644 --- a/spec/unit/resource_spec.rb +++ b/spec/unit/resource_spec.rb @@ -21,10 +21,6 @@ require 'spec_helper' -class ResourceTestHarness < Chef::Resource - provider_base Chef::Provider::Package -end - describe Chef::Resource do before(:each) do @cookbook_repo_path = File.join(CHEF_SPEC_DATA, 'cookbooks') @@ -35,6 +31,18 @@ describe Chef::Resource do @resource = Chef::Resource.new("funk", @run_context) end + it "should mixin shell_out" do + expect(@resource.respond_to?(:shell_out)).to be true + end + + it "should mixin shell_out!" do + expect(@resource.respond_to?(:shell_out!)).to be true + end + + it "should mixin shell_out_with_systems_locale" do + expect(@resource.respond_to?(:shell_out_with_systems_locale)).to be true + end + describe "when inherited" do it "adds an entry to a list of subclasses" do @@ -324,6 +332,71 @@ describe Chef::Resource do end end + describe "self.resource_name" do + context "When resource_name is not set" do + it "and there are no provides lines, resource_name is nil" do + c = Class.new(Chef::Resource) do + end + + r = c.new('hi') + r.declared_type = :d + expect(c.resource_name).to be_nil + expect(r.resource_name).to be_nil + expect(r.declared_type).to eq :d + end + it "and there are no provides lines, @resource_name is used" do + c = Class.new(Chef::Resource) do + def initialize(*args, &block) + @resource_name = :blah + super + end + end + + r = c.new('hi') + r.declared_type = :d + expect(c.resource_name).to be_nil + expect(r.resource_name).to eq :blah + expect(r.declared_type).to eq :d + end + end + + it "resource_name without provides is honored" do + c = Class.new(Chef::Resource) do + resource_name 'blah' + end + + r = c.new('hi') + r.declared_type = :d + expect(c.resource_name).to eq :blah + expect(r.resource_name).to eq :blah + expect(r.declared_type).to eq :d + end + it "setting class.resource_name with 'resource_name = blah' overrides declared_type" do + c = Class.new(Chef::Resource) do + provides :self_resource_name_test_2 + end + c.resource_name = :blah + + r = c.new('hi') + r.declared_type = :d + expect(c.resource_name).to eq :blah + expect(r.resource_name).to eq :blah + expect(r.declared_type).to eq :d + end + it "setting class.resource_name with 'resource_name blah' overrides declared_type" do + c = Class.new(Chef::Resource) do + resource_name :blah + provides :self_resource_name_test_3 + end + + r = c.new('hi') + r.declared_type = :d + expect(c.resource_name).to eq :blah + expect(r.resource_name).to eq :blah + expect(r.declared_type).to eq :d + end + end + describe "is" do it "should return the arguments passed with 'is'" do zm = Chef::Resource::ZenMaster.new("coffee") @@ -447,8 +520,21 @@ describe Chef::Resource do expect(Chef::Resource.provider_base).to eq(Chef::Provider) end - it "allows the base provider to be overriden by a " do - expect(ResourceTestHarness.provider_base).to eq(Chef::Provider::Package) + it "allows the base provider to be overridden" do + Chef::Config.treat_deprecation_warnings_as_errors(false) + class OverrideProviderBaseTest < Chef::Resource + provider_base Chef::Provider::Package + end + + expect(OverrideProviderBaseTest.provider_base).to eq(Chef::Provider::Package) + end + + it "warns when setting provider_base" do + expect { + class OverrideProviderBaseTest2 < Chef::Resource + provider_base Chef::Provider::Package + end + }.to raise_error(Chef::Exceptions::DeprecatedFeatureError) end end @@ -709,57 +795,73 @@ describe Chef::Resource do end it 'adds mappings for a single platform' do - expect(Chef::Resource::Klz.node_map).to receive(:set).with( - :dinobot, true, { platform: ['autobots'] } + expect(Chef).to receive(:set_resource_priority_array).with( + :dinobot, Chef::Resource::Klz, { platform: ['autobots'] } ) klz.provides :dinobot, platform: ['autobots'] end it 'adds mappings for multiple platforms' do - expect(Chef::Resource::Klz.node_map).to receive(:set).with( - :energy, true, { platform: ['autobots', 'decepticons']} + expect(Chef).to receive(:set_resource_priority_array).with( + :energy, Chef::Resource::Klz, { platform: ['autobots', 'decepticons']} ) klz.provides :energy, platform: ['autobots', 'decepticons'] end it 'adds mappings for all platforms' do - expect(Chef::Resource::Klz.node_map).to receive(:set).with( - :tape_deck, true, {} + expect(Chef).to receive(:set_resource_priority_array).with( + :tape_deck, Chef::Resource::Klz, {} ) klz.provides :tape_deck end end - describe "lookups from the platform map" do - let(:klz1) { Class.new(Chef::Resource) } - let(:klz2) { Class.new(Chef::Resource) } + describe "resource_for_node" do + describe "lookups from the platform map" do + let(:klz1) { Class.new(Chef::Resource) } + + before(:each) do + Chef::Resource::Klz1 = klz1 + @node = Chef::Node.new + @node.name("bumblebee") + @node.automatic[:platform] = "autobots" + @node.automatic[:platform_version] = "6.1" + Object.const_set('Soundwave', klz1) + klz1.provides :soundwave + end - before(:each) do - Chef::Resource::Klz1 = klz1 - Chef::Resource::Klz2 = klz2 - @node = Chef::Node.new - @node.name("bumblebee") - @node.automatic[:platform] = "autobots" - @node.automatic[:platform_version] = "6.1" - Object.const_set('Soundwave', klz1) - klz2.provides :dinobot, :on_platforms => ['autobots'] - Object.const_set('Grimlock', klz2) - end + after(:each) do + Object.send(:remove_const, :Soundwave) + Chef::Resource.send(:remove_const, :Klz1) + end - after(:each) do - Object.send(:remove_const, :Soundwave) - Object.send(:remove_const, :Grimlock) - Chef::Resource.send(:remove_const, :Klz1) - Chef::Resource.send(:remove_const, :Klz2) + it "returns a resource by short_name if nothing else matches" do + expect(Chef::Resource.resource_for_node(:soundwave, @node)).to eql(klz1) + end end - describe "resource_for_node" do - it "returns a resource by short_name and node" do - expect(Chef::Resource.resource_for_node(:dinobot, @node)).to eql(Grimlock) + describe "lookups from the platform map" do + let(:klz2) { Class.new(Chef::Resource) } + + before(:each) do + Chef::Resource::Klz2 = klz2 + @node = Chef::Node.new + @node.name("bumblebee") + @node.automatic[:platform] = "autobots" + @node.automatic[:platform_version] = "6.1" + klz2.provides :dinobot, :platform => ['autobots'] + Object.const_set('Grimlock', klz2) + klz2.provides :grimlock end - it "returns a resource by short_name if nothing else matches" do - expect(Chef::Resource.resource_for_node(:soundwave, @node)).to eql(Soundwave) + + after(:each) do + Object.send(:remove_const, :Grimlock) + Chef::Resource.send(:remove_const, :Klz2) + end + + it "returns a resource by short_name and node" do + expect(Chef::Resource.resource_for_node(:dinobot, @node)).to eql(klz2) end end diff --git a/spec/unit/rest_spec.rb b/spec/unit/rest_spec.rb index 85c9e3df8f..3b04981610 100644 --- a/spec/unit/rest_spec.rb +++ b/spec/unit/rest_spec.rb @@ -69,8 +69,8 @@ describe Chef::REST do 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}} + 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, 'X-Ops-Server-API-Version' => Chef::HTTP::Authenticator::DEFAULT_SERVER_API_VERSION}} + 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, 'X-Ops-Server-API-Version' => Chef::HTTP::Authenticator::DEFAULT_SERVER_API_VERSION}} before(:each) do Chef::Log.init(log_stringio) @@ -277,19 +277,6 @@ describe Chef::REST do rest end - let(:base_headers) do - { - 'Accept' => 'application/json', - 'X-Chef-Version' => Chef::VERSION, - 'Accept-Encoding' => Chef::REST::RESTRequest::ENCODING_GZIP_DEFLATE, - 'X-REMOTE-REQUEST-ID' => request_id - } - end - - let (:req_with_body_headers) do - base_headers.merge("Content-Type" => "application/json", "Content-Length" => '13') - end - before(:each) do Chef::Config[:ssl_client_cert] = nil Chef::Config[:ssl_client_key] = nil @@ -304,7 +291,8 @@ 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 + 'X-REMOTE-REQUEST-ID' => request_id, + 'X-Ops-Server-API-Version' => Chef::HTTP::Authenticator::DEFAULT_SERVER_API_VERSION } end @@ -548,7 +536,7 @@ describe Chef::REST do end end end - end + end # as JSON API requests context "when streaming downloads to a tempfile" do let!(:tempfile) { Tempfile.open("chef-rspec-rest_spec-line-@{__LINE__}--") } @@ -586,7 +574,8 @@ 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 + 'X-REMOTE-REQUEST-ID'=> request_id, + 'X-Ops-Server-API-Version' => Chef::HTTP::Authenticator::DEFAULT_SERVER_API_VERSION } expect(Net::HTTP::Get).to receive(:new).with("/?foo=bar", expected_headers).and_return(request_mock) rest.streaming_request(url, {}) @@ -597,7 +586,8 @@ 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 + 'X-REMOTE-REQUEST-ID'=> request_id, + 'X-Ops-Server-API-Version' => Chef::HTTP::Authenticator::DEFAULT_SERVER_API_VERSION } expect(Net::HTTP::Get).to receive(:new).with("/?foo=bar", expected_headers).and_return(request_mock) rest.streaming_request(url, {}) @@ -695,7 +685,7 @@ describe Chef::REST do expect(block_called).to be_truthy end end - end + end # when making REST requests context "when following redirects" do let(:rest) do diff --git a/spec/unit/role_spec.rb b/spec/unit/role_spec.rb index 5421b5a7b3..f120ca6da6 100644 --- a/spec/unit/role_spec.rb +++ b/spec/unit/role_spec.rb @@ -21,7 +21,7 @@ require 'chef/role' describe Chef::Role do before(:each) do - allow(Chef::Platform).to receive(:windows?) { false } + allow(ChefConfig).to receive(:windows?) { false } @role = Chef::Role.new @role.name("ops_master") end diff --git a/spec/unit/run_context_spec.rb b/spec/unit/run_context_spec.rb index d656111a7d..e20ba63b72 100644 --- a/spec/unit/run_context_spec.rb +++ b/spec/unit/run_context_spec.rb @@ -53,6 +53,37 @@ describe Chef::RunContext do expect(run_context.node).to eq(node) end + it "loads up node[:cookbooks]" do + expect(run_context.node[:cookbooks]).to eql( + { + "circular-dep1" => { + "version" => "0.0.0", + }, + "circular-dep2" => { + "version" => "0.0.0", + }, + "dependency1" => { + "version" => "0.0.0", + }, + "dependency2" => { + "version" => "0.0.0", + }, + "no-default-attr" => { + "version" => "0.0.0", + }, + "test" => { + "version" => "0.0.0", + }, + "test-with-circular-deps" => { + "version" => "0.0.0", + }, + "test-with-deps" => { + "version" => "0.0.0", + }, + } + ) + end + describe "loading cookbooks for a run list" do before do @@ -159,4 +190,45 @@ describe Chef::RunContext do expect(run_context.reboot_requested?).to be_falsey end end + + describe "notifications" do + let(:notification) { Chef::Resource::Notification.new(nil, nil, notifying_resource) } + + shared_context "notifying resource is a Chef::Resource" do + let(:notifying_resource) { Chef::Resource.new("gerbil") } + + it "should be keyed off the resource name" do + run_context.send(setter, notification) + expect(run_context.send(getter, notifying_resource)).to eq([notification]) + end + end + + shared_context "notifying resource is a subclass of Chef::Resource" do + let(:declared_type) { :alpaca } + let(:notifying_resource) { + r = Class.new(Chef::Resource).new("guinea pig") + r.declared_type = declared_type + r + } + + it "should be keyed off the resource declared key" do + run_context.send(setter, notification) + expect(run_context.send(getter, notifying_resource)).to eq([notification]) + end + end + + describe "of the immediate kind" do + let(:setter) { :notifies_immediately } + let(:getter) { :immediate_notifications } + include_context "notifying resource is a Chef::Resource" + include_context "notifying resource is a subclass of Chef::Resource" + end + + describe "of the delayed kind" do + let(:setter) { :notifies_delayed } + let(:getter) { :delayed_notifications } + include_context "notifying resource is a Chef::Resource" + include_context "notifying resource is a subclass of Chef::Resource" + end + end end diff --git a/spec/unit/runner_spec.rb b/spec/unit/runner_spec.rb index b30f818da1..82e57e068c 100644 --- a/spec/unit/runner_spec.rb +++ b/spec/unit/runner_spec.rb @@ -273,8 +273,8 @@ describe Chef::Runner do expected_message =<<-E Multiple failures occurred: -* FailureProvider::ChefClientFail occurred in delayed notification: [explode] (dynamically defined) had an error: FailureProvider::ChefClientFail: chef had an error of some sort -* FailureProvider::ChefClientFail occurred in delayed notification: [explode again] (dynamically defined) had an error: FailureProvider::ChefClientFail: chef had an error of some sort +* FailureProvider::ChefClientFail occurred in delayed notification: failure_resource[explode] (dynamically defined) had an error: FailureProvider::ChefClientFail: chef had an error of some sort +* FailureProvider::ChefClientFail occurred in delayed notification: failure_resource[explode again] (dynamically defined) had an error: FailureProvider::ChefClientFail: chef had an error of some sort E expect(exception.message).to eq(expected_message) diff --git a/spec/unit/shell_spec.rb b/spec/unit/shell_spec.rb index acbb1891e7..379043a017 100644 --- a/spec/unit/shell_spec.rb +++ b/spec/unit/shell_spec.rb @@ -43,7 +43,7 @@ describe Shell do before do Shell.irb_conf = {} allow(Shell::ShellSession.instance).to receive(:reset!) - allow(Chef::Platform).to receive(:windows?).and_return(false) + allow(ChefConfig).to receive(:windows?).and_return(false) allow(Chef::Util::PathHelper).to receive(:home).and_return('/home/foo') end @@ -71,7 +71,7 @@ describe Shell do Shell.irb_conf[:IRB_RC].call(conf) expect(conf.prompt_c).to eq("chef > ") expect(conf.return_format).to eq(" => %s \n") - expect(conf.prompt_i).to eq("chef > ") + expect(conf.prompt_i).to eq("chef (#{Chef::VERSION})> ") expect(conf.prompt_n).to eq("chef ?> ") expect(conf.prompt_s).to eq("chef%l> ") expect(conf.use_tracer).to eq(false) @@ -85,7 +85,7 @@ describe Shell do conf.main = Chef::Recipe.new(nil,nil,Chef::RunContext.new(Chef::Node.new, {}, events)) Shell.irb_conf[:IRB_RC].call(conf) expect(conf.prompt_c).to eq("chef:recipe > ") - expect(conf.prompt_i).to eq("chef:recipe > ") + expect(conf.prompt_i).to eq("chef:recipe (#{Chef::VERSION})> ") expect(conf.prompt_n).to eq("chef:recipe ?> ") expect(conf.prompt_s).to eq("chef:recipe%l> ") end @@ -97,7 +97,7 @@ describe Shell do conf.main = Chef::Node.new Shell.irb_conf[:IRB_RC].call(conf) expect(conf.prompt_c).to eq("chef:attributes > ") - expect(conf.prompt_i).to eq("chef:attributes > ") + expect(conf.prompt_i).to eq("chef:attributes (#{Chef::VERSION})> ") expect(conf.prompt_n).to eq("chef:attributes ?> ") expect(conf.prompt_s).to eq("chef:attributes%l> ") end diff --git a/spec/unit/user_spec.rb b/spec/unit/user_spec.rb index d451531b16..57822df7e3 100644 --- a/spec/unit/user_spec.rb +++ b/spec/unit/user_spec.rb @@ -26,98 +26,141 @@ describe Chef::User do @user = Chef::User.new end + shared_examples_for "string fields with no contraints" do + it "should let you set the public key" do + expect(@user.send(method, "some_string")).to eq("some_string") + end + + it "should return the current public key" do + @user.send(method, "some_string") + expect(@user.send(method)).to eq("some_string") + end + + it "should throw an ArgumentError if you feed it something lame" do + expect { @user.send(method, Hash.new) }.to raise_error(ArgumentError) + end + end + + shared_examples_for "boolean fields with no constraints" do + it "should let you set the field" do + expect(@user.send(method, true)).to eq(true) + end + + it "should return the current field value" do + @user.send(method, true) + expect(@user.send(method)).to eq(true) + end + + it "should return the false value when false" do + @user.send(method, false) + expect(@user.send(method)).to eq(false) + end + + it "should throw an ArgumentError if you feed it anything but true or false" do + expect { @user.send(method, Hash.new) }.to raise_error(ArgumentError) + end + end + describe "initialize" do it "should be a Chef::User" do expect(@user).to be_a_kind_of(Chef::User) end end - describe "name" do - it "should let you set the name to a string" do - expect(@user.name("ops_master")).to eq("ops_master") + describe "username" do + it "should let you set the username to a string" do + expect(@user.username("ops_master")).to eq("ops_master") end - it "should return the current name" do - @user.name "ops_master" - expect(@user.name).to eq("ops_master") + it "should return the current username" do + @user.username "ops_master" + expect(@user.username).to eq("ops_master") end # It is not feasible to check all invalid characters. Here are a few # that we probably care about. it "should not accept invalid characters" do # capital letters - expect { @user.name "Bar" }.to raise_error(ArgumentError) + expect { @user.username "Bar" }.to raise_error(ArgumentError) # slashes - expect { @user.name "foo/bar" }.to raise_error(ArgumentError) + expect { @user.username "foo/bar" }.to raise_error(ArgumentError) # ? - expect { @user.name "foo?" }.to raise_error(ArgumentError) + expect { @user.username "foo?" }.to raise_error(ArgumentError) # & - expect { @user.name "foo&" }.to raise_error(ArgumentError) + expect { @user.username "foo&" }.to raise_error(ArgumentError) end it "should not accept spaces" do - expect { @user.name "ops master" }.to raise_error(ArgumentError) + expect { @user.username "ops master" }.to raise_error(ArgumentError) end it "should throw an ArgumentError if you feed it anything but a string" do - expect { @user.name Hash.new }.to raise_error(ArgumentError) + expect { @user.username Hash.new }.to raise_error(ArgumentError) end end - describe "admin" do - it "should let you set the admin bit" do - expect(@user.admin(true)).to eq(true) - end - - it "should return the current admin value" do - @user.admin true - expect(@user.admin).to eq(true) + describe "boolean fields" do + describe "create_key" do + it_should_behave_like "boolean fields with no constraints" do + let(:method) { :create_key } + end end + end - it "should default to false" do - expect(@user.admin).to eq(false) + describe "string fields" do + describe "public_key" do + it_should_behave_like "string fields with no contraints" do + let(:method) { :public_key } + end end - it "should throw an ArgumentError if you feed it anything but true or false" do - expect { @user.name Hash.new }.to raise_error(ArgumentError) + describe "private_key" do + it_should_behave_like "string fields with no contraints" do + let(:method) { :private_key } + end end - end - describe "public_key" do - it "should let you set the public key" do - expect(@user.public_key("super public")).to eq("super public") + describe "display_name" do + it_should_behave_like "string fields with no contraints" do + let(:method) { :display_name } + end end - it "should return the current public key" do - @user.public_key("super public") - expect(@user.public_key).to eq("super public") + describe "first_name" do + it_should_behave_like "string fields with no contraints" do + let(:method) { :first_name } + end end - it "should throw an ArgumentError if you feed it something lame" do - expect { @user.public_key Hash.new }.to raise_error(ArgumentError) + describe "middle_name" do + it_should_behave_like "string fields with no contraints" do + let(:method) { :middle_name } + end end - end - describe "private_key" do - it "should let you set the private key" do - expect(@user.private_key("super private")).to eq("super private") + describe "last_name" do + it_should_behave_like "string fields with no contraints" do + let(:method) { :last_name } + end end - it "should return the private key" do - @user.private_key("super private") - expect(@user.private_key).to eq("super private") + describe "email" do + it_should_behave_like "string fields with no contraints" do + let(:method) { :email } + end end - it "should throw an ArgumentError if you feed it something lame" do - expect { @user.private_key Hash.new }.to raise_error(ArgumentError) + describe "password" do + it_should_behave_like "string fields with no contraints" do + let(:method) { :password } + end end end describe "when serializing to JSON" do before(:each) do - @user.name("black") - @user.public_key("crowes") + @user.username("black") @json = @user.to_json end @@ -125,16 +168,62 @@ describe Chef::User do expect(@json).to match(/^\{.+\}$/) end - it "includes the name value" do - expect(@json).to include(%q{"name":"black"}) + it "includes the username value" do + expect(@json).to include(%q{"username":"black"}) + end + + it "includes the display name when present" do + @user.display_name("get_displayed") + expect(@user.to_json).to include(%{"display_name":"get_displayed"}) + end + + it "does not include the display name if not present" do + expect(@json).not_to include("display_name") end - it "includes the public key value" do - expect(@json).to include(%{"public_key":"crowes"}) + it "includes the first name when present" do + @user.first_name("char") + expect(@user.to_json).to include(%{"first_name":"char"}) end - it "includes the 'admin' flag" do - expect(@json).to include(%q{"admin":false}) + it "does not include the first name if not present" do + expect(@json).not_to include("first_name") + end + + it "includes the middle name when present" do + @user.middle_name("man") + expect(@user.to_json).to include(%{"middle_name":"man"}) + end + + it "does not include the middle name if not present" do + expect(@json).not_to include("middle_name") + end + + it "includes the last name when present" do + @user.last_name("der") + expect(@user.to_json).to include(%{"last_name":"der"}) + end + + it "does not include the last name if not present" do + expect(@json).not_to include("last_name") + end + + it "includes the email when present" do + @user.email("charmander@pokemon.poke") + expect(@user.to_json).to include(%{"email":"charmander@pokemon.poke"}) + end + + it "does not include the email if not present" do + expect(@json).not_to include("email") + end + + it "includes the public key when present" do + @user.public_key("crowes") + expect(@user.to_json).to include(%{"public_key":"crowes"}) + end + + it "does not include the public key if not present" do + expect(@json).not_to include("public_key") end it "includes the private key when present" do @@ -162,11 +251,18 @@ describe Chef::User do describe "when deserializing from JSON" do before(:each) do - user = { "name" => "mr_spinks", + user = { + "username" => "mr_spinks", + "display_name" => "displayed", + "first_name" => "char", + "middle_name" => "man", + "last_name" => "der", + "email" => "charmander@pokemon.poke", + "password" => "password", "public_key" => "turtles", "private_key" => "pandas", - "password" => "password", - "admin" => true } + "create_key" => false + } @user = Chef::User.from_json(Chef::JSONCompat.to_json(user)) end @@ -174,32 +270,275 @@ describe Chef::User do expect(@user).to be_a_kind_of(Chef::User) end - it "preserves the name" do - expect(@user.name).to eq("mr_spinks") + it "preserves the username" do + expect(@user.username).to eq("mr_spinks") end - it "preserves the public key" do - expect(@user.public_key).to eq("turtles") + it "preserves the display name if present" do + expect(@user.display_name).to eq("displayed") end - it "preserves the admin status" do - expect(@user.admin).to be_truthy + it "preserves the first name if present" do + expect(@user.first_name).to eq("char") end - it "includes the private key if present" do - expect(@user.private_key).to eq("pandas") + it "preserves the middle name if present" do + expect(@user.middle_name).to eq("man") + end + + it "preserves the last name if present" do + expect(@user.last_name).to eq("der") + end + + it "preserves the email if present" do + expect(@user.email).to eq("charmander@pokemon.poke") end it "includes the password if present" do expect(@user.password).to eq("password") end + it "preserves the public key if present" do + expect(@user.public_key).to eq("turtles") + end + + it "includes the private key if present" do + expect(@user.private_key).to eq("pandas") + end + + it "includes the create key status if not nil" do + expect(@user.create_key).to be_falsey + end end + describe "Versioned API Interactions" do + let(:response_406) { OpenStruct.new(:code => '406') } + let(:exception_406) { Net::HTTPServerException.new("406 Not Acceptable", response_406) } + + before (:each) do + @user = Chef::User.new + allow(@user).to receive(:chef_root_rest_v0).and_return(double('chef rest root v0 object')) + allow(@user).to receive(:chef_root_rest_v1).and_return(double('chef rest root v1 object')) + end + + describe "update" do + before do + # populate all fields that are valid between V0 and V1 + @user.username "some_username" + @user.display_name "some_display_name" + @user.first_name "some_first_name" + @user.middle_name "some_middle_name" + @user.last_name "some_last_name" + @user.email "some_email" + @user.password "some_password" + end + + let(:payload) { + { + :username => "some_username", + :display_name => "some_display_name", + :first_name => "some_first_name", + :middle_name => "some_middle_name", + :last_name => "some_last_name", + :email => "some_email", + :password => "some_password" + } + } + + context "when server API V1 is valid on the Chef Server receiving the request" do + context "when the user submits valid data" do + it "properly updates the user" do + expect(@user.chef_root_rest_v1).to receive(:put).with("users/some_username", payload).and_return({}) + @user.update + end + end + end + + context "when server API V1 is not valid on the Chef Server receiving the request" do + let(:payload) { + { + :username => "some_username", + :display_name => "some_display_name", + :first_name => "some_first_name", + :middle_name => "some_middle_name", + :last_name => "some_last_name", + :email => "some_email", + :password => "some_password", + :public_key => "some_public_key" + } + } + + before do + @user.public_key "some_public_key" + allow(@user.chef_root_rest_v1).to receive(:put) + end + + context "when the server returns a 400" do + let(:response_400) { OpenStruct.new(:code => '400') } + let(:exception_400) { Net::HTTPServerException.new("400 Bad Request", response_400) } + + context "when the 400 was due to public / private key fields no longer being supported" do + let(:response_body_400) { '{"error":["Since Server API v1, all keys must be updated via the keys endpoint. "]}' } + + before do + allow(response_400).to receive(:body).and_return(response_body_400) + allow(@user.chef_root_rest_v1).to receive(:put).and_raise(exception_400) + end + + it "proceeds with the V0 PUT since it can handle public / private key fields" do + expect(@user.chef_root_rest_v0).to receive(:put).with("users/some_username", payload).and_return({}) + @user.update + end + + it "does not call server_client_api_version_intersection, since we know to proceed with V0 in this case" do + expect(@user).to_not receive(:server_client_api_version_intersection) + allow(@user.chef_root_rest_v0).to receive(:put).and_return({}) + @user.update + end + end # when the 400 was due to public / private key fields + + context "when the 400 was NOT due to public / private key fields no longer being supported" do + let(:response_body_400) { '{"error":["Some other error. "]}' } + + before do + allow(response_400).to receive(:body).and_return(response_body_400) + allow(@user.chef_root_rest_v1).to receive(:put).and_raise(exception_400) + end + + it "will not proceed with the V0 PUT since the original bad request was not key related" do + expect(@user.chef_root_rest_v0).to_not receive(:put).with("users/some_username", payload) + expect { @user.update }.to raise_error(exception_400) + end + + it "raises the original error" do + expect { @user.update }.to raise_error(exception_400) + end + + end + end # when the server returns a 400 + + context "when the server returns a 406" do + # from spec/support/shared/unit/api_versioning.rb + it_should_behave_like "version handling" do + let(:object) { @user } + let(:method) { :update } + let(:http_verb) { :put } + let(:rest_v1) { @user.chef_root_rest_v1 } + end + + context "when the server supports API V0" do + before do + allow(@user).to receive(:server_client_api_version_intersection).and_return([0]) + allow(@user.chef_root_rest_v1).to receive(:put).and_raise(exception_406) + end + + it "properly updates the user" do + expect(@user.chef_root_rest_v0).to receive(:put).with("users/some_username", payload).and_return({}) + @user.update + end + end # when the server supports API V0 + end # when the server returns a 406 + + end # when server API V1 is not valid on the Chef Server receiving the request + end # update + + describe "create" do + let(:payload) { + { + :username => "some_username", + :display_name => "some_display_name", + :first_name => "some_first_name", + :last_name => "some_last_name", + :email => "some_email", + :password => "some_password" + } + } + before do + @user.username "some_username" + @user.display_name "some_display_name" + @user.first_name "some_first_name" + @user.last_name "some_last_name" + @user.email "some_email" + @user.password "some_password" + end + + # from spec/support/shared/unit/user_and_client_shared.rb + it_should_behave_like "user or client create" do + let(:object) { @user } + let(:error) { Chef::Exceptions::InvalidUserAttribute } + let(:rest_v0) { @user.chef_root_rest_v0 } + let(:rest_v1) { @user.chef_root_rest_v1 } + let(:url) { "users" } + end + + context "when handling API V1" do + it "creates a new user via the API with a middle_name when it exists" do + @user.middle_name "some_middle_name" + expect(@user.chef_root_rest_v1).to receive(:post).with("users", payload.merge({:middle_name => "some_middle_name"})).and_return({}) + @user.create + end + end # when server API V1 is valid on the Chef Server receiving the request + + context "when API V1 is not supported by the server" do + # from spec/support/shared/unit/api_versioning.rb + it_should_behave_like "version handling" do + let(:object) { @user } + let(:method) { :create } + let(:http_verb) { :post } + let(:rest_v1) { @user.chef_root_rest_v1 } + end + end + + context "when handling API V0" do + before do + allow(@user).to receive(:server_client_api_version_intersection).and_return([0]) + allow(@user.chef_root_rest_v1).to receive(:post).and_raise(exception_406) + end + + it "creates a new user via the API with a middle_name when it exists" do + @user.middle_name "some_middle_name" + expect(@user.chef_root_rest_v0).to receive(:post).with("users", payload.merge({:middle_name => "some_middle_name"})).and_return({}) + @user.create + end + end # when server API V1 is not valid on the Chef Server receiving the request + + end # create + + # DEPRECATION + # This can be removed after API V0 support is gone + describe "reregister" do + let(:payload) { + { + "username" => "some_username", + } + } + + before do + @user.username "some_username" + end + + context "when server API V0 is valid on the Chef Server receiving the request" do + it "creates a new object via the API" do + expect(@user.chef_root_rest_v0).to receive(:put).with("users/#{@user.username}", payload.merge({"private_key" => true})).and_return({}) + @user.reregister + end + end # when server API V0 is valid on the Chef Server receiving the request + + context "when server API V0 is not supported by the Chef Server" do + # from spec/support/shared/unit/api_versioning.rb + it_should_behave_like "user and client reregister" do + let(:object) { @user } + let(:rest_v0) { @user.chef_root_rest_v0 } + end + end # when server API V0 is not supported by the Chef Server + end # reregister + + end # Versioned API Interactions + describe "API Interactions" do before (:each) do @user = Chef::User.new - @user.name "foobar" + @user.username "foobar" @http_client = double("Chef::REST mock") allow(Chef::REST).to receive(:new).and_return(@http_client) end @@ -213,57 +552,31 @@ describe Chef::User do @osc_inflated_response = { "admin" => @user } end - it "lists all clients on an OSC server" do - allow(@http_client).to receive(:get_rest).with("users").and_return(@osc_response) - expect(Chef::User.list).to eq(@osc_response) - end - - it "inflate all clients on an OSC server" do - allow(@http_client).to receive(:get_rest).with("users").and_return(@osc_response) - expect(Chef::User.list(true)).to eq(@osc_inflated_response) - end - it "lists all clients on an OHC/OPC server" do - allow(@http_client).to receive(:get_rest).with("users").and_return(@ohc_response) + allow(@http_client).to receive(:get).with("users").and_return(@ohc_response) # We expect that Chef::User.list will give a consistent response # so OHC API responses should be transformed to OSC-style output. expect(Chef::User.list).to eq(@osc_response) end it "inflate all clients on an OHC/OPC server" do - allow(@http_client).to receive(:get_rest).with("users").and_return(@ohc_response) + allow(@http_client).to receive(:get).with("users").and_return(@ohc_response) expect(Chef::User.list(true)).to eq(@osc_inflated_response) end end - describe "create" do - it "creates a new user via the API" do - @user.password "password" - expect(@http_client).to receive(:post_rest).with("users", {:name => "foobar", :admin => false, :password => "password"}).and_return({}) - @user.create - end - end - describe "read" do it "loads a named user from the API" do - expect(@http_client).to receive(:get_rest).with("users/foobar").and_return({"name" => "foobar", "admin" => true, "public_key" => "pubkey"}) + expect(@http_client).to receive(:get).with("users/foobar").and_return({"username" => "foobar", "admin" => true, "public_key" => "pubkey"}) user = Chef::User.load("foobar") - expect(user.name).to eq("foobar") - expect(user.admin).to eq(true) + expect(user.username).to eq("foobar") expect(user.public_key).to eq("pubkey") end end - describe "update" do - it "updates an existing user on via the API" do - expect(@http_client).to receive(:put_rest).with("users/foobar", {:name => "foobar", :admin => false}).and_return({}) - @user.update - end - end - describe "destroy" do it "deletes the specified user via the API" do - expect(@http_client).to receive(:delete_rest).with("users/foobar") + expect(@http_client).to receive(:delete).with("users/foobar") @user.destroy end end diff --git a/spec/unit/util/path_helper_spec.rb b/spec/unit/util/path_helper_spec.rb deleted file mode 100644 index 23db9587a6..0000000000 --- a/spec/unit/util/path_helper_spec.rb +++ /dev/null @@ -1,255 +0,0 @@ -# -# 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/util/path_helper' -require 'spec_helper' - -describe Chef::Util::PathHelper do - PathHelper = Chef::Util::PathHelper - - [ false, true ].each do |is_windows| - context "on #{is_windows ? "windows" : "unix"}" do - before(:each) do - allow(Chef::Platform).to receive(:windows?).and_return(is_windows) - end - - describe "join" do - it "joins components when some end with separators" do - expected = PathHelper.cleanpath("/foo/bar/baz") - expected = "C:#{expected}" if is_windows - expect(PathHelper.join(is_windows ? 'C:\\foo\\' : "/foo/", "bar", "baz")).to eq(expected) - end - - it "joins components when some end and start with separators" do - expected = PathHelper.cleanpath("/foo/bar/baz") - expected = "C:#{expected}" if is_windows - expect(PathHelper.join(is_windows ? 'C:\\foo\\' : "/foo/", "bar/", "/baz")).to eq(expected) - end - - it "joins components that don't end in separators" do - expected = PathHelper.cleanpath("/foo/bar/baz") - expected = "C:#{expected}" if is_windows - expect(PathHelper.join(is_windows ? 'C:\\foo' : "/foo", "bar", "baz")).to eq(expected) - end - - it "joins starting with '' resolve to absolute paths" do - expect(PathHelper.join('', 'a', 'b')).to eq("#{PathHelper.path_separator}a#{PathHelper.path_separator}b") - end - - it "joins ending with '' add a / to the end" do - expect(PathHelper.join('a', 'b', '')).to eq("a#{PathHelper.path_separator}b#{PathHelper.path_separator}") - end - - if is_windows - it "joins components on Windows when some end with unix separators" do - expect(PathHelper.join('C:\\foo/', "bar", "baz")).to eq('C:\\foo\\bar\\baz') - end - end - end - - if is_windows - it "path_separator is \\" do - expect(PathHelper.path_separator).to eq('\\') - end - else - it "path_separator is /" do - expect(PathHelper.path_separator).to eq('/') - end - end - - if is_windows - it "cleanpath changes slashes into backslashes and leaves backslashes alone" do - expect(PathHelper.cleanpath('/a/b\\c/d/')).to eq('\\a\\b\\c\\d') - end - it "cleanpath does not remove leading double backslash" do - expect(PathHelper.cleanpath('\\\\a/b\\c/d/')).to eq('\\\\a\\b\\c\\d') - end - else - it "cleanpath removes extra slashes alone" do - expect(PathHelper.cleanpath('/a///b/c/d/')).to eq('/a/b/c/d') - end - end - - describe "dirname" do - it "dirname('abc') is '.'" do - expect(PathHelper.dirname('abc')).to eq('.') - end - it "dirname('/') is '/'" do - expect(PathHelper.dirname(PathHelper.path_separator)).to eq(PathHelper.path_separator) - end - it "dirname('a/b/c') is 'a/b'" do - expect(PathHelper.dirname(PathHelper.join('a', 'b', 'c'))).to eq(PathHelper.join('a', 'b')) - end - it "dirname('a/b/c/') is 'a/b'" do - expect(PathHelper.dirname(PathHelper.join('a', 'b', 'c', ''))).to eq(PathHelper.join('a', 'b')) - end - it "dirname('/a/b/c') is '/a/b'" do - expect(PathHelper.dirname(PathHelper.join('', 'a', 'b', 'c'))).to eq(PathHelper.join('', 'a', 'b')) - end - end - end - end - - describe "validate_path" do - context "on windows" do - before(:each) do - # pass by default - allow(Chef::Platform).to receive(:windows?).and_return(true) - allow(PathHelper).to receive(:printable?).and_return(true) - allow(PathHelper).to receive(:windows_max_length_exceeded?).and_return(false) - end - - it "returns the path if the path passes the tests" do - expect(PathHelper.validate_path("C:\\ThisIsRigged")).to eql("C:\\ThisIsRigged") - end - - it "does not raise an error if everything looks great" do - expect { PathHelper.validate_path("C:\\cool path\\dude.exe") }.not_to raise_error - end - - it "raises an error if the path has invalid characters" do - allow(PathHelper).to receive(:printable?).and_return(false) - expect { PathHelper.validate_path("Newline!\n") }.to raise_error(Chef::Exceptions::ValidationFailed) - end - - it "Adds the \\\\?\\ prefix if the path exceeds MAX_LENGTH and does not have it" do - long_path = "C:\\" + "a" * 250 + "\\" + "b" * 250 - prefixed_long_path = "\\\\?\\" + long_path - allow(PathHelper).to receive(:windows_max_length_exceeded?).and_return(true) - expect(PathHelper.validate_path(long_path)).to eql(prefixed_long_path) - end - end - end - - describe "windows_max_length_exceeded?" do - it "returns true if the path is too long (259 + NUL) for the API" do - expect(PathHelper.windows_max_length_exceeded?("C:\\" + "a" * 250 + "\\" + "b" * 6)).to be_truthy - end - - it "returns false if the path is not too long (259 + NUL) for the standard API" do - expect(PathHelper.windows_max_length_exceeded?("C:\\" + "a" * 250 + "\\" + "b" * 5)).to be_falsey - end - - it "returns false if the path is over 259 characters but uses the \\\\?\\ prefix" do - expect(PathHelper.windows_max_length_exceeded?("\\\\?\\C:\\" + "a" * 250 + "\\" + "b" * 250)).to be_falsey - end - end - - describe "printable?" do - it "returns true if the string contains no non-printable characters" do - expect(PathHelper.printable?("C:\\Program Files (x86)\\Microsoft Office\\Files.lst")).to be_truthy - end - - it "returns true when given 'abc' in unicode" do - expect(PathHelper.printable?("\u0061\u0062\u0063")).to be_truthy - end - - it "returns true when given japanese unicode" do - expect(PathHelper.printable?("\uff86\uff87\uff88")).to be_truthy - end - - it "returns false if the string contains a non-printable character" do - expect(PathHelper.printable?("\my files\work\notes.txt")).to be_falsey - end - - # This isn't necessarily a requirement, but here to be explicit about functionality. - it "returns false if the string contains a newline or tab" do - expect(PathHelper.printable?("\tThere's no way,\n\t *no* way,\n\t that you came from my loins.\n")).to be_falsey - end - end - - describe "canonical_path" do - context "on windows", :windows_only do - it "returns an absolute path with backslashes instead of slashes" do - expect(PathHelper.canonical_path("\\\\?\\C:/windows/win.ini")).to eq("\\\\?\\c:\\windows\\win.ini") - end - - it "adds the \\\\?\\ prefix if it is missing" do - expect(PathHelper.canonical_path("C:/windows/win.ini")).to eq("\\\\?\\c:\\windows\\win.ini") - end - - it "returns a lowercase path" do - expect(PathHelper.canonical_path("\\\\?\\C:\\CASE\\INSENSITIVE")).to eq("\\\\?\\c:\\case\\insensitive") - end - end - - context "not on windows", :unix_only do - it "returns a canonical path" do - expect(PathHelper.canonical_path("/etc//apache.d/sites-enabled/../sites-available/default")).to eq("/etc/apache.d/sites-available/default") - end - end - end - - describe "paths_eql?" do - it "returns true if the paths are the same" do - allow(PathHelper).to receive(:canonical_path).with("bandit").and_return("c:/bandit/bandit") - allow(PathHelper).to receive(:canonical_path).with("../bandit/bandit").and_return("c:/bandit/bandit") - expect(PathHelper.paths_eql?("bandit", "../bandit/bandit")).to be_truthy - end - - it "returns false if the paths are different" do - allow(PathHelper).to receive(:canonical_path).with("bandit").and_return("c:/Bo/Bandit") - allow(PathHelper).to receive(:canonical_path).with("../bandit/bandit").and_return("c:/bandit/bandit") - expect(PathHelper.paths_eql?("bandit", "../bandit/bandit")).to be_falsey - end - end - - describe "escape_glob" do - it "escapes characters reserved by glob" do - path = "C:\\this\\*path\\[needs]\\escaping?" - escaped_path = "C:\\\\this\\\\\\*path\\\\\\[needs\\]\\\\escaping\\?" - expect(PathHelper.escape_glob(path)).to eq(escaped_path) - end - - context "when given more than one argument" do - it "joins, cleanpaths, and escapes characters reserved by glob" do - args = ["this/*path", "[needs]", "escaping?"] - escaped_path = if windows? - "this\\\\\\*path\\\\\\[needs\\]\\\\escaping\\?" - else - "this/\\*path/\\[needs\\]/escaping\\?" - end - expect(PathHelper).to receive(:join).with(*args).and_call_original - expect(PathHelper).to receive(:cleanpath).and_call_original - expect(PathHelper.escape_glob(*args)).to eq(escaped_path) - end - end - end - - describe "all_homes" do - before do - stub_const('ENV', env) - allow(Chef::Platform).to receive(:windows?).and_return(is_windows) - end - - context "on windows" do - let (:is_windows) { true } - end - - context "on unix" do - let (:is_windows) { false } - - context "when HOME is not set" do - let (:env) { {} } - it "returns an empty array" do - expect(PathHelper.all_homes).to eq([]) - end - end - end - end -end diff --git a/tasks/external_tests.rb b/tasks/external_tests.rb new file mode 100644 index 0000000000..2ff991ddf7 --- /dev/null +++ b/tasks/external_tests.rb @@ -0,0 +1,29 @@ +task :chef_sugar_spec do + gem_path = Bundler.environment.specs['chef-sugar'].first.full_gem_path + system("cd #{gem_path} && rake") +end + +task :foodcritic_spec do + gem_path = Bundler.environment.specs['foodcritic'].first.full_gem_path + system("cd #{gem_path} && rake test") +end + +task :chefspec_spec do + gem_path = Bundler.environment.specs['chefspec'].first.full_gem_path + system("cd #{gem_path} && rake") +end + +task :chef_rewind_spec do + gem_path = Bundler.environment.specs['chef-rewind'].first.full_gem_path + system("cd #{gem_path} && rake spec") +end + +task :poise_spec do + gem_path = Bundler.environment.specs['poise'].first.full_gem_path + system("cd #{gem_path} && rake spec") +end + +task :halite_spec do + gem_path = Bundler.environment.specs['halite'].first.full_gem_path + system("cd #{gem_path} && rake spec") +end diff --git a/tasks/maintainers.rb b/tasks/maintainers.rb new file mode 100644 index 0000000000..5a2c8d9c2d --- /dev/null +++ b/tasks/maintainers.rb @@ -0,0 +1,69 @@ +# +# Copyright:: Copyright (c) 2015 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 'rake' + +SOURCE = File.join(File.dirname(__FILE__), "..", "MAINTAINERS.toml") +TARGET = File.join(File.dirname(__FILE__), "..", "MAINTAINERS.md") + +begin + require 'tomlrb' + task :default => :generate + + namespace :maintainers do + desc "Generate MarkDown version of MAINTAINERS file" + task :generate do + maintainers = Tomlrb.load_file SOURCE + out = "<!-- This is a generated file. Please do not edit directly -->\n\n" + out << "# " + maintainers["Preamble"]["title"] + "\n\n" + out << maintainers["Preamble"]["text"] + "\n" + out << "# " + maintainers["Org"]["Lead"]["title"] + "\n\n" + out << person(maintainers["people"], maintainers["Org"]["Lead"]["person"]) + "\n\n" + out << components(maintainers["people"], maintainers["Org"]["Components"]) + File.open(TARGET, "w") { |fn| + fn.write out + } + end + end + + def components(list, cmp) + out = "## " + cmp.delete("title") + "\n\n" + out << cmp.delete("text") + "\n" if cmp.has_key?("text") + if cmp.has_key?("lieutenant") + out << "### Lieutenant\n\n" + out << person(list, cmp.delete("lieutenant")) + "\n\n" + end + out << maintainers(list, cmp.delete("maintainers")) + "\n" if cmp.has_key?("maintainers") + cmp.delete("paths") + cmp.each {|k,v| out << components(list, v) } + out + end + + def maintainers(list, people) + o = "### Maintainers\n\n" + people.each do |p| + o << person(list, p) + "\n" + end + o + end + + def person(list, person) + "* [#{list[person]["Name"]}](https://github.com/#{list[person]["GitHub"]})" + end +rescue LoadError + STDERR.puts "\n*** TomlRb not available.\n\n" +end diff --git a/tasks/rspec.rb b/tasks/rspec.rb index a6fc5a9180..6e802d3df8 100644 --- a/tasks/rspec.rb +++ b/tasks/rspec.rb @@ -25,13 +25,26 @@ CHEF_ROOT = File.join(File.dirname(__FILE__), "..") begin require 'rspec/core/rake_task' + + desc "Run specs for Chef's Components" + task :component_specs do + Dir.chdir("chef-config") do + Bundler.with_clean_env do + sh("bundle install --local") + sh("bundle exec rake spec") + end + end + end + task :default => :spec + task :spec => :component_specs + desc "Run standard specs (minus long running specs)" RSpec::Core::RakeTask.new(:spec) do |t| # right now this just limits to functional + unit, but could also remove # individual tests marked long-running - t.pattern = FileList['spec/{functional,unit}/**/*_spec.rb'] + t.pattern = FileList['spec/**/*_spec.rb'] end namespace :spec do |